Remove overlaps on update()

This commit is contained in:
Ruben van de Ven 2021-04-27 16:10:23 +02:00
parent 0b6e4661cb
commit 9a79cea9d7
2 changed files with 150 additions and 20 deletions

View file

@ -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;

View file

@ -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);