forked from security_vision/semantic_graph
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{
|
||||
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;
|
||||
|
|
162
www/graph.js
162
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);
|
||||
|
|
Loading…
Reference in a new issue