From 9a79cea9d798772ad749c8298f87b1e8df232ccf Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Tue, 27 Apr 2021 16:10:23 +0200 Subject: [PATCH] Remove overlaps on update() --- www/graph.css | 8 ++- www/graph.js | 162 ++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 150 insertions(+), 20 deletions(-) diff --git a/www/graph.css b/www/graph.css index 2210ad3..aaf449b 100644 --- a/www/graph.css +++ b/www/graph.css @@ -44,12 +44,17 @@ svg .links line,svg .links path{ } svg .links line.hover, svg .links path.hover{ - stroke:red; + stroke:var(--hover-color); + stroke-width: 12; } svg.zoomed .links line, svg.zoomed .links path{ stroke-width: 2; } +svg.zoomed .links line, svg.zoomed .links path.hover{ + stroke-width: 2; + stroke-width: 4; +} .links text{ display:none; @@ -70,6 +75,7 @@ svg.zoomed .links line, svg.zoomed .links path{ transition: font-size .4s, opacity 1s; fill: white; opacity: 1; + pointer-events: none; /*prevent mouse glitches*/ } .node:not(:hover):not(.linkHover) text.nodeTitle.overlapping{ opacity: 0; diff --git a/www/graph.js b/www/graph.js index 5a46c55..b128b7d 100644 --- a/www/graph.js +++ b/www/graph.js @@ -1,6 +1,8 @@ const CONFIG = { - 'nodeSize': 8, + // 'nodeSize': 8, + 'nodeRadius': 5, + 'nodeRepositionPadding': 3, 'baseUrl': 'https://www.securityvision.io/wiki/index.php/', 'dataUrl': 'result.json', 'preSimulate': false, // run simulation before starting, so we don't start with lines jumping around @@ -283,10 +285,16 @@ class NodeMap { if (d.printouts[prop].length) { // console.log("fix node", d); var p = this.projection([d.printouts[prop][0].lon, d.printouts[prop][0].lat]); + + // initial positions: d.x = p[0]; d.y = p[1]; - // d.targetX = p[0]; - // d.targetY = p[1]; + + //These are used when we need to move overlapping points: + d.originalX = p[0]; + d.originalY = p[1]; + + // target pos for the force layout d.fx = p[0]; d.fy = p[1]; // d.targetLat = d.printouts[prop][0].lat; @@ -296,6 +304,8 @@ class NodeMap { } }) + this.store.configureTree(); + // this.nodeMap = Object.fromEntries(this.graph.nodes.map(d => [d['id'], d])); @@ -322,8 +332,8 @@ class NodeMap { .force("collision", d3.forceCollide(this.nodeSize)) // TODO look at simpler labels https://github.com/d3fc/d3fc/tree/master/packages/d3fc-label-layout // TODO look at rects https://github.com/emeeks/d3-bboxCollide - .force("posX", d3.forceX(n => n.targetX || 0).strength(n => n.targetX ? 1 : 0)) // TODO: should not be or 0 - .force("posY", d3.forceY(n => n.targetY || 0).strength(n => n.targetY ? 1 : 0)) + // .force("posX", d3.forceX(n => n.targetX || 0).strength(n => n.targetX ? 1 : 0)) // TODO: should not be or 0 + // .force("posY", d3.forceY(n => n.targetY || 0).strength(n => n.targetY ? 1 : 0)) ; this.update(); @@ -335,8 +345,82 @@ class NodeMap { return this.nodeSize; } + getNodeRadius(node) { + return CONFIG.nodeRadius; + } + + resolveOverlaps() { + // reset: + this.graph.nodes.forEach((n) => { + if (n.hasOwnProperty('originalX')) { + n.x = n.originalX; + n.y = n.originalY; + n.fx = n.originalX; + n.fy = n.originalY; + } + }) + + this.store.configureTree(); + + let moved = 0; + + // resolve overlapping points by repositioning + this.graph.nodes.forEach((n) => { + // only for fixed points: + if (!n.hasOwnProperty('originalX')) { + return; + } + + const startX = n.originalX; + const startY = n.originalY; + let alpha = 0; // angle + let step = 1; + const d_alpha = Math.PI / 4; + const r = this.getNodeRadius(n) + CONFIG.nodeRepositionPadding/2; + let foundNodes; + let i = 0; + // find a new pos until it's not overlapping anymore... + while ((foundNodes = this.store.findVisibleInCircle(n.x, n.y, r)).length > 1) { + this.store.quadtree.remove(n); // remove uses the current x,y so we need to do this before reconfiguring these + n.x = startX + Math.cos(alpha) * r * 2 * step; + n.y = startY + Math.sin(alpha) * r * 2 * step; + this.store.quadtree.add(n); + alpha += d_alpha; + // on to the next round: + if(alpha > Math.PI * 2) { + step++; + alpha -= Math.PI *2; + alpha += d_alpha-2; // little offset + } + i++; + + // if(n.fulltext == 'Control Room (Venice)') { + // console.log(startX, startY, n.x, n.y, r, foundNodes); + // // this.store.configureTree(); + // // this.store.findVisibleInCircle(n.x, n.y, r).forEach((found) => console.log(found.x, found.y, found)); + // } + } + + n.fx = n.x; + n.fy = n.y; + + if (i > 0) { + // we moved something, update tree + + console.debug('resolved for', n.fulltext, i); + moved ++; + } + + }); + + console.log(`moved ${moved} nodes`); + } + update() { console.log(this.graph) + + this.resolveOverlaps(); + // see also: https://www.createwithdata.com/enter-exit-with-d3-join/ this.node = this.node.data(this.graph.nodes, d => d.id) .join((enter) => { @@ -643,7 +727,7 @@ class AlluvialMap { this.links = s.links; const scale = d3.scaleOrdinal(d3.schemeCategory10); - const color = (c) => c == "Unknown" ? "#333": scale(c); + const color = (c) => c == "Unknown" ? "#333" : scale(c); this.svg.append("g") @@ -669,12 +753,12 @@ class AlluvialMap { .join("g") .style("mix-blend-mode", "multiply"); - + const edgeColor = 'path'; // either: path, none, input, output if (edgeColor === "path") { const gradient = link.append("linearGradient") - .attr("id", (d,i) => { + .attr("id", (d, i) => { const id = `link-${i}`; // thanks https://talk.observablehq.com/t/how-do-i-work-with-the-d3-sankey-example/1696/3 d.uid = `url(#${id})`; return id; @@ -682,12 +766,12 @@ class AlluvialMap { .attr("gradientUnits", "userSpaceOnUse") .attr("y1", d => d.source.x1) .attr("y2", d => d.target.x0) - .attr("x1",0) + .attr("x1", 0) .attr("x2", 0); - // .attr("y1", "0%") - // .attr("y2", "100%") - // .attr("x1", "0%") - // .attr("x2", "0%"); + // .attr("y1", "0%") + // .attr("y2", "100%") + // .attr("x1", "0%") + // .attr("x2", "0%"); gradient.append("stop") .attr("offset", "0%") @@ -733,8 +817,6 @@ class AlluvialMap { } - - JsonToGraph = function (data) { let nodes = []; let links = []; @@ -999,6 +1081,7 @@ class Store { constructor(graph, parent) { this.nodes = graph.nodes; this.links = graph.links; + // graph is a filtered version of this.nodes & this.links this.graph = { 'nodes': [], 'links': [] @@ -1012,10 +1095,52 @@ class Store { 'categories': [], } + + this.filter(); } + configureTree() { + // set up the tree, we do this only after all points are configured. + this.quadtree = d3.quadtree( + this.nodes, + (n) => n.x, + (n) => n.y + ); + } + + // from: https://observablehq.com/@d3/quadtree-findincircle + findInCircle(x, y, radius, filter) { + if (typeof this.quadtree === 'undefined') { + this.configureTree(); + } + + const result = [], + radius2 = radius * radius, + accept = filter + ? d => filter(d) && result.push(d) + : d => result.push(d); + + this.quadtree.visit((node, x1, y1, x2, y2) => { + if (node.length) { + return x1 >= x + radius || y1 >= y + radius || x2 < x - radius || y2 < y - radius; + } + + const dx = +this.quadtree._x.call(null, node.data) - x, + dy = +this.quadtree._y.call(null, node.data) - y; + if (dx * dx + dy * dy < radius2) { + do { accept(node.data); } while (node = node.next); + } + }); + + return result; + } + + findVisibleInCircle(x, y, radius) { + return this.findInCircle(x, y, radius, (n) => !n.filtered); + } + registerMap(map) { this._maps.push(map); return this; @@ -1103,7 +1228,7 @@ class Store { var mapGraph = new NodeMap('#map') -var alluvialGraph = new AlluvialMap('#alluvial') +// var alluvialGraph = new AlluvialMap('#alluvial') // REQUEST ATLAS & GRAPH const req_data = new Request(CONFIG.dataUrl, { method: 'GET' }); @@ -1118,12 +1243,11 @@ Promise.all([fetch(req_data), fetch(req_world)]) mapGraph.setWorld(world); mapGraph.setStore(store); - // console.log(); - alluvialGraph.setData(data.results); + // alluvialGraph.setData(data.results); store.render() mapGraph.render() - alluvialGraph.render() + // alluvialGraph.render() }).catch(error => { console.error(error);