Remove overlaps on update()
This commit is contained in:
parent
0b6e4661cb
commit
9a79cea9d7
2 changed files with 150 additions and 20 deletions
|
@ -44,12 +44,17 @@ svg .links line,svg .links path{
|
||||||
}
|
}
|
||||||
|
|
||||||
svg .links line.hover, svg .links path.hover{
|
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{
|
svg.zoomed .links line, svg.zoomed .links path{
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
}
|
}
|
||||||
|
svg.zoomed .links line, svg.zoomed .links path.hover{
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-width: 4;
|
||||||
|
}
|
||||||
|
|
||||||
.links text{
|
.links text{
|
||||||
display:none;
|
display:none;
|
||||||
|
@ -70,6 +75,7 @@ svg.zoomed .links line, svg.zoomed .links path{
|
||||||
transition: font-size .4s, opacity 1s;
|
transition: font-size .4s, opacity 1s;
|
||||||
fill: white;
|
fill: white;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
pointer-events: none; /*prevent mouse glitches*/
|
||||||
}
|
}
|
||||||
.node:not(:hover):not(.linkHover) text.nodeTitle.overlapping{
|
.node:not(:hover):not(.linkHover) text.nodeTitle.overlapping{
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
162
www/graph.js
162
www/graph.js
|
@ -1,6 +1,8 @@
|
||||||
|
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
'nodeSize': 8,
|
// 'nodeSize': 8,
|
||||||
|
'nodeRadius': 5,
|
||||||
|
'nodeRepositionPadding': 3,
|
||||||
'baseUrl': 'https://www.securityvision.io/wiki/index.php/',
|
'baseUrl': 'https://www.securityvision.io/wiki/index.php/',
|
||||||
'dataUrl': 'result.json',
|
'dataUrl': 'result.json',
|
||||||
'preSimulate': false, // run simulation before starting, so we don't start with lines jumping around
|
'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) {
|
if (d.printouts[prop].length) {
|
||||||
// console.log("fix node", d);
|
// console.log("fix node", d);
|
||||||
var p = this.projection([d.printouts[prop][0].lon, d.printouts[prop][0].lat]);
|
var p = this.projection([d.printouts[prop][0].lon, d.printouts[prop][0].lat]);
|
||||||
|
|
||||||
|
// initial positions:
|
||||||
d.x = p[0];
|
d.x = p[0];
|
||||||
d.y = p[1];
|
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.fx = p[0];
|
||||||
d.fy = p[1];
|
d.fy = p[1];
|
||||||
// d.targetLat = d.printouts[prop][0].lat;
|
// 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]));
|
// this.nodeMap = Object.fromEntries(this.graph.nodes.map(d => [d['id'], d]));
|
||||||
|
@ -322,8 +332,8 @@ class NodeMap {
|
||||||
.force("collision", d3.forceCollide(this.nodeSize))
|
.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 simpler labels https://github.com/d3fc/d3fc/tree/master/packages/d3fc-label-layout
|
||||||
// TODO look at rects https://github.com/emeeks/d3-bboxCollide
|
// 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("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("posY", d3.forceY(n => n.targetY || 0).strength(n => n.targetY ? 1 : 0))
|
||||||
;
|
;
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
|
@ -335,8 +345,82 @@ class NodeMap {
|
||||||
return this.nodeSize;
|
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() {
|
update() {
|
||||||
console.log(this.graph)
|
console.log(this.graph)
|
||||||
|
|
||||||
|
this.resolveOverlaps();
|
||||||
|
|
||||||
// see also: https://www.createwithdata.com/enter-exit-with-d3-join/
|
// see also: https://www.createwithdata.com/enter-exit-with-d3-join/
|
||||||
this.node = this.node.data(this.graph.nodes, d => d.id)
|
this.node = this.node.data(this.graph.nodes, d => d.id)
|
||||||
.join((enter) => {
|
.join((enter) => {
|
||||||
|
@ -643,7 +727,7 @@ class AlluvialMap {
|
||||||
this.links = s.links;
|
this.links = s.links;
|
||||||
|
|
||||||
const scale = d3.scaleOrdinal(d3.schemeCategory10);
|
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")
|
this.svg.append("g")
|
||||||
|
@ -669,12 +753,12 @@ class AlluvialMap {
|
||||||
.join("g")
|
.join("g")
|
||||||
.style("mix-blend-mode", "multiply");
|
.style("mix-blend-mode", "multiply");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const edgeColor = 'path'; // either: path, none, input, output
|
const edgeColor = 'path'; // either: path, none, input, output
|
||||||
if (edgeColor === "path") {
|
if (edgeColor === "path") {
|
||||||
const gradient = link.append("linearGradient")
|
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
|
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})`;
|
d.uid = `url(#${id})`;
|
||||||
return id;
|
return id;
|
||||||
|
@ -682,12 +766,12 @@ class AlluvialMap {
|
||||||
.attr("gradientUnits", "userSpaceOnUse")
|
.attr("gradientUnits", "userSpaceOnUse")
|
||||||
.attr("y1", d => d.source.x1)
|
.attr("y1", d => d.source.x1)
|
||||||
.attr("y2", d => d.target.x0)
|
.attr("y2", d => d.target.x0)
|
||||||
.attr("x1",0)
|
.attr("x1", 0)
|
||||||
.attr("x2", 0);
|
.attr("x2", 0);
|
||||||
// .attr("y1", "0%")
|
// .attr("y1", "0%")
|
||||||
// .attr("y2", "100%")
|
// .attr("y2", "100%")
|
||||||
// .attr("x1", "0%")
|
// .attr("x1", "0%")
|
||||||
// .attr("x2", "0%");
|
// .attr("x2", "0%");
|
||||||
|
|
||||||
gradient.append("stop")
|
gradient.append("stop")
|
||||||
.attr("offset", "0%")
|
.attr("offset", "0%")
|
||||||
|
@ -733,8 +817,6 @@ class AlluvialMap {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
JsonToGraph = function (data) {
|
JsonToGraph = function (data) {
|
||||||
let nodes = [];
|
let nodes = [];
|
||||||
let links = [];
|
let links = [];
|
||||||
|
@ -999,6 +1081,7 @@ class Store {
|
||||||
constructor(graph, parent) {
|
constructor(graph, parent) {
|
||||||
this.nodes = graph.nodes;
|
this.nodes = graph.nodes;
|
||||||
this.links = graph.links;
|
this.links = graph.links;
|
||||||
|
// graph is a filtered version of this.nodes & this.links
|
||||||
this.graph = {
|
this.graph = {
|
||||||
'nodes': [],
|
'nodes': [],
|
||||||
'links': []
|
'links': []
|
||||||
|
@ -1012,10 +1095,52 @@ class Store {
|
||||||
'categories': [],
|
'categories': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.filter();
|
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) {
|
registerMap(map) {
|
||||||
this._maps.push(map);
|
this._maps.push(map);
|
||||||
return this;
|
return this;
|
||||||
|
@ -1103,7 +1228,7 @@ class Store {
|
||||||
|
|
||||||
|
|
||||||
var mapGraph = new NodeMap('#map')
|
var mapGraph = new NodeMap('#map')
|
||||||
var alluvialGraph = new AlluvialMap('#alluvial')
|
// var alluvialGraph = new AlluvialMap('#alluvial')
|
||||||
|
|
||||||
// REQUEST ATLAS & GRAPH
|
// REQUEST ATLAS & GRAPH
|
||||||
const req_data = new Request(CONFIG.dataUrl, { method: 'GET' });
|
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.setWorld(world);
|
||||||
mapGraph.setStore(store);
|
mapGraph.setStore(store);
|
||||||
// console.log();
|
// alluvialGraph.setData(data.results);
|
||||||
alluvialGraph.setData(data.results);
|
|
||||||
|
|
||||||
store.render()
|
store.render()
|
||||||
mapGraph.render()
|
mapGraph.render()
|
||||||
alluvialGraph.render()
|
// alluvialGraph.render()
|
||||||
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
Loading…
Reference in a new issue