From 1277131f0d1645e733f2cabaddeb4f20a3249fbf Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Mon, 19 Apr 2021 19:49:33 +0200 Subject: [PATCH] Preliminary zoom effects and label cleaning --- www/graph.css | 63 +++++++++++++++-- www/graph.js | 185 +++++++++++++++++++++++++++++++++++++++---------- www/index.html | 9 ++- 3 files changed, 211 insertions(+), 46 deletions(-) diff --git a/www/graph.css b/www/graph.css index 2b06c50..44b1849 100644 --- a/www/graph.css +++ b/www/graph.css @@ -12,6 +12,7 @@ --color10: #277da1; --hover-color: var(--color1); + /* --hover-color: var(darkblue); */ --selected-color: var(--color1); --selected-color: var(--color1); } @@ -36,8 +37,18 @@ svg.dragging { svg .links line,svg .links path{ stroke: #f3722c; - stroke-width: 3; + stroke-width: 6; 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{ @@ -47,8 +58,28 @@ svg .links line,svg .links path{ fill: whitesmoke; } -.node text{ - text-anchor: middle; +.node{ + + 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; } @@ -56,14 +87,23 @@ svg .links line,svg .links path{ fill: white; } -.node:hover{ - cursor: pointer; +/* Whenever a connected link is hovered */ +.node.linkHover circle{ + stroke: var(--hover-color); + stroke-width: 5px; +} +.node.linkHover text.nodeTitle.overlapping{ + transition: opacity 0s; } .node:hover circle{ stroke: var(--hover-color); stroke-width: 5px; } +.node:hover text{ + transition: none; + fill: var(--hover-color); +} .node.selected circle{ stroke: var(--selected-color); stroke-width: 5px; @@ -94,6 +134,13 @@ svg .links line,svg .links path{ fill: plum } + + +.labels .label text{ + fill:yellow; + opacity: 1 !important; +} + /* .node.Person circle { fill: var(--color2) } @@ -160,3 +207,9 @@ a:hover{ background: white; padding: 10px; } + +#map .borders{ + stroke-width: 6px; + stroke: white; + fill:none; +} \ No newline at end of file diff --git a/www/graph.js b/www/graph.js index 709e31b..c56ab1c 100644 --- a/www/graph.js +++ b/www/graph.js @@ -13,7 +13,7 @@ const CONFIG = { 'lonMax': 35, 'center': [11, 47], }, - "filters": ["Institution", "Deployments", "Technology", "Dataset"], + "filters": ["Institution", "Deployments",/* "Technology", "Dataset"*/], "link_properties": [ "Clients", @@ -159,6 +159,32 @@ class NodeMap { 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() { this.svg = this.root.append('svg') this.svg.append('defs').html(''); @@ -166,8 +192,8 @@ class NodeMap { this.projection = d3.geoHill() .rotate([-12, 0, 0]) - .translate([this.vbWidth / 2, this.vbHeight * 1.5]) - .scale(this.vbHeight * 1.5); + .translate([this.vbWidth, this.vbHeight * 3]) + .scale(this.vbHeight * 3); this.nodeSize = this.vbHeight / 200; @@ -175,12 +201,12 @@ class NodeMap { const graticule = d3.geoGraticule10(); 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") - this.g_countries = container.append("g").attr("id", "countries"); - this.g_borders = container.append("g").attr("id", "borders"); - this.g_graticule = container.append("g").attr('id', 'graticule') + this.g_countries = this.container.append("g").attr("id", "countries"); + this.g_borders = this.container.append("g").attr("id", "borders"); + this.g_graticule = this.container.append("g").attr('id', 'graticule') .append('path') .attr("class", "graticule") .attr("fill", "none") @@ -209,26 +235,33 @@ class NodeMap { .append("path") .attr("class", "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"); }).on("end", () => { this.svg.node().classList.remove("dragging"); }).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") - .attr('class', 'nodes') - .selectAll(".node"); - this.link = container.append("g") + setTimeout(() => { + this.calculateLabels(); + }, 500); + } + }); + this.svg + .call(zoom) + .call(zoom.transform, d3.zoomIdentity.scale(.5, .5)); + + this.link = this.container.append("g") .attr('class', 'links') .selectAll(".link"); + this.node = this.container.append("g") + .attr('class', 'nodes') + .selectAll(".node"); this.graph.nodes.forEach((d) => { @@ -282,6 +315,8 @@ class NodeMap { ; this.update(); + + setTimeout(() => this.calculateLabels(), 1000); } getSizeForNode(node) { @@ -296,10 +331,10 @@ class NodeMap { let group = enter.append("g").attr("class", getClasses); // group.call(drag(simulation)); group.on("click", (evt, n) => selectNode(evt, n, node)); - group.append('circle').attr("r", this.nodeSize); - var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "5"); + group.append('circle').attr("r", 5 /*this.nodeSize*/); + var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "3").attr('x', 5); nodeTitle - .each(function (node, nodes) { + .each(function (node, i, nodes) { var textLength = void 0; const self = d3.select(this); const titleText = getTitle(node); @@ -308,30 +343,108 @@ class NodeMap { titleTexts = splitText(titleText); } if (titleTexts !== false) { - const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "-10").attr("x", "0"); - const tspan = self.append("tspan").text(titleTexts[1]).attr("y", "10").attr("x", "0"); - const textLength1 = tspan.node().getComputedTextLength(); - const textLength2 = tspan.node().getComputedTextLength(); - textLength = Math.max(textLength1, textLength2); + const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "3").attr("x", "5"); + const tspan = self.append("tspan").text(titleTexts[1]).attr("dy", "1em").attr("x", "5"); + // const textLength1 = tspan.node().getComputedTextLength(); + // const textLength2 = tspan.node().getComputedTextLength(); + // textLength = Math.max(textLength1, textLength2); } else { self.text(titleText); - textLength = self.node().getComputedTextLength(); - } - // scale according to text length: - if (textLength > this.nodeSize * 2) { - self.attr('transform', 'scale(' + this.nodeSize * 2 / textLength / 1.05 + ')'); + // textLength = self.node().getComputedTextLength(); } }); 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 .data(this.graph.links) .join( enter => { - console.log(enter); 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) { return l.name; }); @@ -447,7 +560,7 @@ class NodeMap { this.simulation.alpha = 0; this.simulation.restart(); - + this.calculateLabels() } setWorld(world) { @@ -526,7 +639,7 @@ JsonToGraph = function (data) { } } }) - + console.debug(`Fixed location for ${fixes} nodes`); console.log(links.length); diff --git a/www/index.html b/www/index.html index a2e7a7e..99ba437 100644 --- a/www/index.html +++ b/www/index.html @@ -15,14 +15,13 @@ - - + - - - + + + \ No newline at end of file