Preliminary zoom effects and label cleaning

This commit is contained in:
Ruben van de Ven 2021-04-19 19:49:33 +02:00
parent 2de48856ed
commit 1277131f0d
3 changed files with 211 additions and 46 deletions

View file

@ -12,6 +12,7 @@
--color10: #277da1; --color10: #277da1;
--hover-color: var(--color1); --hover-color: var(--color1);
/* --hover-color: var(darkblue); */
--selected-color: var(--color1); --selected-color: var(--color1);
--selected-color: var(--color1); --selected-color: var(--color1);
} }
@ -36,8 +37,18 @@ svg.dragging {
svg .links line,svg .links path{ svg .links line,svg .links path{
stroke: #f3722c; stroke: #f3722c;
stroke-width: 3; stroke-width: 6;
fill:none; fill:none;
transition: stroke-width 1s;
cursor: pointer;
}
svg .links line.hover, svg .links path.hover{
stroke:red;
}
svg.zoomed .links line, svg.zoomed .links path{
stroke-width: 2;
} }
.links text{ .links text{
@ -47,8 +58,28 @@ svg .links line,svg .links path{
fill: whitesmoke; fill: whitesmoke;
} }
.node text{ .node{
text-anchor: middle;
cursor: pointer;
}
.node text.nodeTitle{
text-anchor: start;
dominant-baseline: hanging; /*achieves a 'text-anchor: top'*/
font-size:16pt;
transition: font-size .4s, opacity 1s;
fill: white;
opacity: 1;
}
.node:not(:hover):not(.linkHover) text.nodeTitle.overlapping{
opacity: 0;
}
svg.zoomed .node text.nodeTitle{
font-size:6pt;
}
svg.zoomed.zoomed2 .node text.nodeTitle{
font-size:3pt; font-size:3pt;
} }
@ -56,14 +87,23 @@ svg .links line,svg .links path{
fill: white; fill: white;
} }
.node:hover{ /* Whenever a connected link is hovered */
cursor: pointer; .node.linkHover circle{
stroke: var(--hover-color);
stroke-width: 5px;
}
.node.linkHover text.nodeTitle.overlapping{
transition: opacity 0s;
} }
.node:hover circle{ .node:hover circle{
stroke: var(--hover-color); stroke: var(--hover-color);
stroke-width: 5px; stroke-width: 5px;
} }
.node:hover text{
transition: none;
fill: var(--hover-color);
}
.node.selected circle{ .node.selected circle{
stroke: var(--selected-color); stroke: var(--selected-color);
stroke-width: 5px; stroke-width: 5px;
@ -94,6 +134,13 @@ svg .links line,svg .links path{
fill: plum fill: plum
} }
.labels .label text{
fill:yellow;
opacity: 1 !important;
}
/* .node.Person circle { /* .node.Person circle {
fill: var(--color2) fill: var(--color2)
} }
@ -160,3 +207,9 @@ a:hover{
background: white; background: white;
padding: 10px; padding: 10px;
} }
#map .borders{
stroke-width: 6px;
stroke: white;
fill:none;
}

View file

@ -13,7 +13,7 @@ const CONFIG = {
'lonMax': 35, 'lonMax': 35,
'center': [11, 47], 'center': [11, 47],
}, },
"filters": ["Institution", "Deployments", "Technology", "Dataset"], "filters": ["Institution", "Deployments",/* "Technology", "Dataset"*/],
"link_properties": [ "link_properties": [
"Clients", "Clients",
@ -159,6 +159,32 @@ class NodeMap {
this.render(); this.render();
} }
calculateLabels() {
const els = document.querySelectorAll('.node text')
for (let i = 0; i < els.length; i++) {
const el = els[i];
let overlapping = false;
for (let index = 0; index < i; index++) {
const el2 = els[index];
const box1 = el.getBoundingClientRect()
const box2 = el2.getBoundingClientRect()
const overlap = !(box1.right < box2.left ||
box1.left > box2.right ||
box1.bottom < box2.top ||
box1.top > box2.bottom)
if (overlap) {
// TODO: try to flip labels horizontally to see if that helps
el.classList.add('overlapping');
overlapping = true;
break;
}
}
if(!overlapping)
el.classList.remove('overlapping');
}
}
render() { render() {
this.svg = this.root.append('svg') this.svg = this.root.append('svg')
this.svg.append('defs').html('<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHead" fill="#f3722c"><path d="M0,-3L8,0L0,3"></path></marker><marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelected"><path d="M0,-3L8,0L0,3" fill="white"></path></marker>'); this.svg.append('defs').html('<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHead" fill="#f3722c"><path d="M0,-3L8,0L0,3"></path></marker><marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelected"><path d="M0,-3L8,0L0,3" fill="white"></path></marker>');
@ -166,8 +192,8 @@ class NodeMap {
this.projection = d3.geoHill() this.projection = d3.geoHill()
.rotate([-12, 0, 0]) .rotate([-12, 0, 0])
.translate([this.vbWidth / 2, this.vbHeight * 1.5]) .translate([this.vbWidth, this.vbHeight * 3])
.scale(this.vbHeight * 1.5); .scale(this.vbHeight * 3);
this.nodeSize = this.vbHeight / 200; this.nodeSize = this.vbHeight / 200;
@ -175,12 +201,12 @@ class NodeMap {
const graticule = d3.geoGraticule10(); const graticule = d3.geoGraticule10();
const euCenter = this.projection(CONFIG.eu.center); const euCenter = this.projection(CONFIG.eu.center);
const container = this.svg.append("g").attr("id", "container"); this.container = this.svg.append("g").attr("id", "container");
// container.append("circle").attr("cx", euCenter[0]).attr("cy", euCenter[1]).attr("r", 500).attr("fill","red") // container.append("circle").attr("cx", euCenter[0]).attr("cy", euCenter[1]).attr("r", 500).attr("fill","red")
this.g_countries = container.append("g").attr("id", "countries"); this.g_countries = this.container.append("g").attr("id", "countries");
this.g_borders = container.append("g").attr("id", "borders"); this.g_borders = this.container.append("g").attr("id", "borders");
this.g_graticule = container.append("g").attr('id', 'graticule') this.g_graticule = this.container.append("g").attr('id', 'graticule')
.append('path') .append('path')
.attr("class", "graticule") .attr("class", "graticule")
.attr("fill", "none") .attr("fill", "none")
@ -209,26 +235,33 @@ class NodeMap {
.append("path") .append("path")
.attr("class", "borders") .attr("class", "borders")
.attr("d", this.proj(this.borders)) .attr("d", this.proj(this.borders))
.attr("fill", "none")
.attr("stroke-width", "2px")
.attr("stroke", (n) => {
return "white";
});
this.svg.call(d3.zoom().scaleExtent([0.3, 8]).on("start", () => { const zoom = d3.zoom().scaleExtent([0.2, 10]).on("start", () => {
this.svg.node().classList.add("dragging"); this.svg.node().classList.add("dragging");
}).on("end", () => { }).on("end", () => {
this.svg.node().classList.remove("dragging"); this.svg.node().classList.remove("dragging");
}).on("zoom", ({ transform }) => { }).on("zoom", ({ transform }) => {
container.attr("transform", transform); this.container.attr("transform", transform);
})); const oldZoom = this.svg.classed('zoomed');
const newZoom = transform.k > 2.0;
if (oldZoom != newZoom) {
this.svg.classed('zoomed', newZoom);
this.node = container.append("g") setTimeout(() => {
.attr('class', 'nodes') this.calculateLabels();
.selectAll(".node"); }, 500);
this.link = container.append("g") }
});
this.svg
.call(zoom)
.call(zoom.transform, d3.zoomIdentity.scale(.5, .5));
this.link = this.container.append("g")
.attr('class', 'links') .attr('class', 'links')
.selectAll(".link"); .selectAll(".link");
this.node = this.container.append("g")
.attr('class', 'nodes')
.selectAll(".node");
this.graph.nodes.forEach((d) => { this.graph.nodes.forEach((d) => {
@ -282,6 +315,8 @@ class NodeMap {
; ;
this.update(); this.update();
setTimeout(() => this.calculateLabels(), 1000);
} }
getSizeForNode(node) { getSizeForNode(node) {
@ -296,10 +331,10 @@ class NodeMap {
let group = enter.append("g").attr("class", getClasses); let group = enter.append("g").attr("class", getClasses);
// group.call(drag(simulation)); // group.call(drag(simulation));
group.on("click", (evt, n) => selectNode(evt, n, node)); group.on("click", (evt, n) => selectNode(evt, n, node));
group.append('circle').attr("r", this.nodeSize); group.append('circle').attr("r", 5 /*this.nodeSize*/);
var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "5"); var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "3").attr('x', 5);
nodeTitle nodeTitle
.each(function (node, nodes) { .each(function (node, i, nodes) {
var textLength = void 0; var textLength = void 0;
const self = d3.select(this); const self = d3.select(this);
const titleText = getTitle(node); const titleText = getTitle(node);
@ -308,30 +343,108 @@ class NodeMap {
titleTexts = splitText(titleText); titleTexts = splitText(titleText);
} }
if (titleTexts !== false) { if (titleTexts !== false) {
const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "-10").attr("x", "0"); const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "3").attr("x", "5");
const tspan = self.append("tspan").text(titleTexts[1]).attr("y", "10").attr("x", "0"); const tspan = self.append("tspan").text(titleTexts[1]).attr("dy", "1em").attr("x", "5");
const textLength1 = tspan.node().getComputedTextLength(); // const textLength1 = tspan.node().getComputedTextLength();
const textLength2 = tspan.node().getComputedTextLength(); // const textLength2 = tspan.node().getComputedTextLength();
textLength = Math.max(textLength1, textLength2); // textLength = Math.max(textLength1, textLength2);
} else { } else {
self.text(titleText); self.text(titleText);
textLength = self.node().getComputedTextLength(); // textLength = self.node().getComputedTextLength();
}
// scale according to text length:
if (textLength > this.nodeSize * 2) {
self.attr('transform', 'scale(' + this.nodeSize * 2 / textLength / 1.05 + ')');
} }
}); });
return group; return group;
}); });
// const labelPadding = 1;
// // // the component used to render each label
// var textLabel = fc.layoutTextLabel()
// .padding(labelPadding)
// //.value(function(d) { return map_data.properties.iso; });
// //.value(function(d) { return d.properties.iso; });
// .value( (d) => getTitle(d));
// // a strategy that combines simulated annealing with removal
// // of overlapping labels
// // */fc.layoutGreedy
// const strategy = fc.layoutRemoveOverlaps(fc.layoutGreedy());
// // create the layout that positions the labels
// this.layoutLabels = fc.layoutLabel(strategy)
// .size((d, i, g) => {
// // measure the label and add the required padding
// const textSize = g[i].getElementsByTagName('text')[0].getBBox();
// console.log(textSize);
// // return [30, 20];
// return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
// })
// .position(d => [d.x, d.y])
// .component(textLabel);
// // render!
// // this.node.datum(this.graph.nodes,).call(labels)
// this.labels = this.container.append('g').attr('class','labels');
// this.labels.datum(this.graph.nodes)
// // // this.node
// .call(this.layoutLabels);
// // use simulate annealing to find minimum overlapping text label positions
// //https://github.com/d3fc/d3fc-label-layout/blob/master/README.md
// var strategy = fc.layoutGreedy();
// //var strategy = fc.layoutAnnealing();
// // create the layout that positions the labels
// var labels = fc.layoutLabel(strategy)
// .size(function (_, i, g) {
// // measure the label and add the required padding
// var textSize = d3.select(g[i])
// .select('text')
// .node()
// .getBBox();
// return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
// })
// .position((d) => this.projection([d.lon, d.lat]); })
// .component(textLabel);
// // render!
// this.container.datum(countries)
// .call(labels);
this.link = this.link this.link = this.link
.data(this.graph.links) .data(this.graph.links)
.join( .join(
enter => { enter => {
console.log(enter);
let group = enter.append("g").attr("class", "link"); let group = enter.append("g").attr("class", "link");
group.append("path").attr("marker-end", "url(#arrowHead)").attr('id', (d, i) => 'linkid_' + i); group.append("path")
.attr("marker-end", "url(#arrowHead)")
.attr('id', (d, i) => 'linkid_' + i)
.on("mouseover", function (ev,link) {
d3.select(this).classed('hover',true);
const nodes = document.getElementsByClassName('node');
for(let n of nodes){
const d = d3.select(n).datum();
if(d == link.target || d == link.source){
n.classList.add('linkHover');
}
}
// console.log(l);
}).on("mouseout", function (ev, link) {
d3.select(this).classed('hover',false);
const nodes = document.getElementsByClassName('linkHover');
for(let n of nodes){
n.classList.remove('linkHover');
}
// l.classed('hover',false);
// l.target.classed('hover',false);
// l.source.classed('hover',false);
// console.log(l,'l');
});
group.filter((l) => l.name != "City").append("text").attr("class", "labelText").text(function (l) { group.filter((l) => l.name != "City").append("text").attr("class", "labelText").text(function (l) {
return l.name; return l.name;
}); });
@ -447,7 +560,7 @@ class NodeMap {
this.simulation.alpha = 0; this.simulation.alpha = 0;
this.simulation.restart(); this.simulation.restart();
this.calculateLabels()
} }
setWorld(world) { setWorld(world) {

View file

@ -15,14 +15,13 @@
</div> </div>
<script src="https://d3js.org/d3.v6.js"></script> <script src="https://d3js.org/d3.v6.js"></script>
<script src="crossfilter.js"></script>
<script src="dc.js"></script>
<script src="https://d3js.org/d3-geo-projection.v3.min.js"></script> <script src="https://d3js.org/d3-geo-projection.v3.min.js"></script>
<script src="https://d3js.org/topojson.v3.min.js"></script> <script src="https://d3js.org/topojson.v3.min.js"></script>
<script src="//unpkg.com/d3-geo-zoom"></script> <script src="//unpkg.com/d3-geo-zoom"></script>
<script src="cola.min.js"></script>
<script src="mapChart2.js"></script> <script src="//unpkg.com/d3fc@14.0.1"></script>
<script src="graph2.js"></script> <script src="graph.js"></script>
</body> </body>
</html> </html>