forked from security_vision/semantic_graph
Preliminary zoom effects and label cleaning
This commit is contained in:
parent
2de48856ed
commit
1277131f0d
3 changed files with 211 additions and 46 deletions
|
@ -12,6 +12,7 @@
|
||||||
--color10: #277da1;
|
--color10: #277da1;
|
||||||
|
|
||||||
--hover-color: var(--color1);
|
--hover-color: var(--color1);
|
||||||
|
/* --hover-color: var(darkblue); */
|
||||||
--selected-color: var(--color1);
|
--selected-color: var(--color1);
|
||||||
--selected-color: var(--color1);
|
--selected-color: var(--color1);
|
||||||
}
|
}
|
||||||
|
@ -36,8 +37,18 @@ svg.dragging {
|
||||||
|
|
||||||
svg .links line,svg .links path{
|
svg .links line,svg .links path{
|
||||||
stroke: #f3722c;
|
stroke: #f3722c;
|
||||||
stroke-width: 3;
|
stroke-width: 6;
|
||||||
fill:none;
|
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{
|
.links text{
|
||||||
|
@ -47,8 +58,28 @@ svg .links line,svg .links path{
|
||||||
fill: whitesmoke;
|
fill: whitesmoke;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node text{
|
.node{
|
||||||
text-anchor: middle;
|
|
||||||
|
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;
|
font-size:3pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,14 +87,23 @@ svg .links line,svg .links path{
|
||||||
fill: white;
|
fill: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node:hover{
|
/* Whenever a connected link is hovered */
|
||||||
cursor: pointer;
|
.node.linkHover circle{
|
||||||
|
stroke: var(--hover-color);
|
||||||
|
stroke-width: 5px;
|
||||||
|
}
|
||||||
|
.node.linkHover text.nodeTitle.overlapping{
|
||||||
|
transition: opacity 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node:hover circle{
|
.node:hover circle{
|
||||||
stroke: var(--hover-color);
|
stroke: var(--hover-color);
|
||||||
stroke-width: 5px;
|
stroke-width: 5px;
|
||||||
}
|
}
|
||||||
|
.node:hover text{
|
||||||
|
transition: none;
|
||||||
|
fill: var(--hover-color);
|
||||||
|
}
|
||||||
.node.selected circle{
|
.node.selected circle{
|
||||||
stroke: var(--selected-color);
|
stroke: var(--selected-color);
|
||||||
stroke-width: 5px;
|
stroke-width: 5px;
|
||||||
|
@ -94,6 +134,13 @@ svg .links line,svg .links path{
|
||||||
fill: plum
|
fill: plum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.labels .label text{
|
||||||
|
fill:yellow;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* .node.Person circle {
|
/* .node.Person circle {
|
||||||
fill: var(--color2)
|
fill: var(--color2)
|
||||||
}
|
}
|
||||||
|
@ -160,3 +207,9 @@ a:hover{
|
||||||
background: white;
|
background: white;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#map .borders{
|
||||||
|
stroke-width: 6px;
|
||||||
|
stroke: white;
|
||||||
|
fill:none;
|
||||||
|
}
|
185
www/graph.js
185
www/graph.js
|
@ -13,7 +13,7 @@ const CONFIG = {
|
||||||
'lonMax': 35,
|
'lonMax': 35,
|
||||||
'center': [11, 47],
|
'center': [11, 47],
|
||||||
},
|
},
|
||||||
"filters": ["Institution", "Deployments", "Technology", "Dataset"],
|
"filters": ["Institution", "Deployments",/* "Technology", "Dataset"*/],
|
||||||
|
|
||||||
"link_properties": [
|
"link_properties": [
|
||||||
"Clients",
|
"Clients",
|
||||||
|
@ -159,6 +159,32 @@ class NodeMap {
|
||||||
this.render();
|
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() {
|
render() {
|
||||||
this.svg = this.root.append('svg')
|
this.svg = this.root.append('svg')
|
||||||
this.svg.append('defs').html('<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHead" fill="#f3722c"><path d="M0,-3L8,0L0,3"></path></marker><marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelected"><path d="M0,-3L8,0L0,3" fill="white"></path></marker>');
|
this.svg.append('defs').html('<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHead" fill="#f3722c"><path d="M0,-3L8,0L0,3"></path></marker><marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelected"><path d="M0,-3L8,0L0,3" fill="white"></path></marker>');
|
||||||
|
@ -166,8 +192,8 @@ class NodeMap {
|
||||||
|
|
||||||
this.projection = d3.geoHill()
|
this.projection = d3.geoHill()
|
||||||
.rotate([-12, 0, 0])
|
.rotate([-12, 0, 0])
|
||||||
.translate([this.vbWidth / 2, this.vbHeight * 1.5])
|
.translate([this.vbWidth, this.vbHeight * 3])
|
||||||
.scale(this.vbHeight * 1.5);
|
.scale(this.vbHeight * 3);
|
||||||
|
|
||||||
this.nodeSize = this.vbHeight / 200;
|
this.nodeSize = this.vbHeight / 200;
|
||||||
|
|
||||||
|
@ -175,12 +201,12 @@ class NodeMap {
|
||||||
const graticule = d3.geoGraticule10();
|
const graticule = d3.geoGraticule10();
|
||||||
const euCenter = this.projection(CONFIG.eu.center);
|
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")
|
// 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_countries = this.container.append("g").attr("id", "countries");
|
||||||
this.g_borders = container.append("g").attr("id", "borders");
|
this.g_borders = this.container.append("g").attr("id", "borders");
|
||||||
this.g_graticule = container.append("g").attr('id', 'graticule')
|
this.g_graticule = this.container.append("g").attr('id', 'graticule')
|
||||||
.append('path')
|
.append('path')
|
||||||
.attr("class", "graticule")
|
.attr("class", "graticule")
|
||||||
.attr("fill", "none")
|
.attr("fill", "none")
|
||||||
|
@ -209,26 +235,33 @@ class NodeMap {
|
||||||
.append("path")
|
.append("path")
|
||||||
.attr("class", "borders")
|
.attr("class", "borders")
|
||||||
.attr("d", this.proj(this.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");
|
this.svg.node().classList.add("dragging");
|
||||||
}).on("end", () => {
|
}).on("end", () => {
|
||||||
this.svg.node().classList.remove("dragging");
|
this.svg.node().classList.remove("dragging");
|
||||||
}).on("zoom", ({ transform }) => {
|
}).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")
|
setTimeout(() => {
|
||||||
.attr('class', 'nodes')
|
this.calculateLabels();
|
||||||
.selectAll(".node");
|
}, 500);
|
||||||
this.link = container.append("g")
|
}
|
||||||
|
});
|
||||||
|
this.svg
|
||||||
|
.call(zoom)
|
||||||
|
.call(zoom.transform, d3.zoomIdentity.scale(.5, .5));
|
||||||
|
|
||||||
|
this.link = this.container.append("g")
|
||||||
.attr('class', 'links')
|
.attr('class', 'links')
|
||||||
.selectAll(".link");
|
.selectAll(".link");
|
||||||
|
this.node = this.container.append("g")
|
||||||
|
.attr('class', 'nodes')
|
||||||
|
.selectAll(".node");
|
||||||
|
|
||||||
|
|
||||||
this.graph.nodes.forEach((d) => {
|
this.graph.nodes.forEach((d) => {
|
||||||
|
@ -282,6 +315,8 @@ class NodeMap {
|
||||||
;
|
;
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
|
|
||||||
|
setTimeout(() => this.calculateLabels(), 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSizeForNode(node) {
|
getSizeForNode(node) {
|
||||||
|
@ -296,10 +331,10 @@ class NodeMap {
|
||||||
let group = enter.append("g").attr("class", getClasses);
|
let group = enter.append("g").attr("class", getClasses);
|
||||||
// group.call(drag(simulation));
|
// group.call(drag(simulation));
|
||||||
group.on("click", (evt, n) => selectNode(evt, n, node));
|
group.on("click", (evt, n) => selectNode(evt, n, node));
|
||||||
group.append('circle').attr("r", this.nodeSize);
|
group.append('circle').attr("r", 5 /*this.nodeSize*/);
|
||||||
var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "5");
|
var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "3").attr('x', 5);
|
||||||
nodeTitle
|
nodeTitle
|
||||||
.each(function (node, nodes) {
|
.each(function (node, i, nodes) {
|
||||||
var textLength = void 0;
|
var textLength = void 0;
|
||||||
const self = d3.select(this);
|
const self = d3.select(this);
|
||||||
const titleText = getTitle(node);
|
const titleText = getTitle(node);
|
||||||
|
@ -308,30 +343,108 @@ class NodeMap {
|
||||||
titleTexts = splitText(titleText);
|
titleTexts = splitText(titleText);
|
||||||
}
|
}
|
||||||
if (titleTexts !== false) {
|
if (titleTexts !== false) {
|
||||||
const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "-10").attr("x", "0");
|
const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "3").attr("x", "5");
|
||||||
const tspan = self.append("tspan").text(titleTexts[1]).attr("y", "10").attr("x", "0");
|
const tspan = self.append("tspan").text(titleTexts[1]).attr("dy", "1em").attr("x", "5");
|
||||||
const textLength1 = tspan.node().getComputedTextLength();
|
// const textLength1 = tspan.node().getComputedTextLength();
|
||||||
const textLength2 = tspan.node().getComputedTextLength();
|
// const textLength2 = tspan.node().getComputedTextLength();
|
||||||
textLength = Math.max(textLength1, textLength2);
|
// textLength = Math.max(textLength1, textLength2);
|
||||||
} else {
|
} else {
|
||||||
self.text(titleText);
|
self.text(titleText);
|
||||||
textLength = self.node().getComputedTextLength();
|
// textLength = self.node().getComputedTextLength();
|
||||||
}
|
|
||||||
// scale according to text length:
|
|
||||||
if (textLength > this.nodeSize * 2) {
|
|
||||||
self.attr('transform', 'scale(' + this.nodeSize * 2 / textLength / 1.05 + ')');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return group;
|
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
|
this.link = this.link
|
||||||
.data(this.graph.links)
|
.data(this.graph.links)
|
||||||
.join(
|
.join(
|
||||||
enter => {
|
enter => {
|
||||||
console.log(enter);
|
|
||||||
let group = enter.append("g").attr("class", "link");
|
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) {
|
group.filter((l) => l.name != "City").append("text").attr("class", "labelText").text(function (l) {
|
||||||
return l.name;
|
return l.name;
|
||||||
});
|
});
|
||||||
|
@ -447,7 +560,7 @@ class NodeMap {
|
||||||
|
|
||||||
this.simulation.alpha = 0;
|
this.simulation.alpha = 0;
|
||||||
this.simulation.restart();
|
this.simulation.restart();
|
||||||
|
this.calculateLabels()
|
||||||
}
|
}
|
||||||
|
|
||||||
setWorld(world) {
|
setWorld(world) {
|
||||||
|
@ -526,7 +639,7 @@ JsonToGraph = function (data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.debug(`Fixed location for ${fixes} nodes`);
|
console.debug(`Fixed location for ${fixes} nodes`);
|
||||||
|
|
||||||
console.log(links.length);
|
console.log(links.length);
|
||||||
|
|
|
@ -15,14 +15,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://d3js.org/d3.v6.js"></script>
|
<script src="https://d3js.org/d3.v6.js"></script>
|
||||||
<script src="crossfilter.js"></script>
|
|
||||||
<script src="dc.js"></script>
|
|
||||||
<script src="https://d3js.org/d3-geo-projection.v3.min.js"></script>
|
<script src="https://d3js.org/d3-geo-projection.v3.min.js"></script>
|
||||||
<script src="https://d3js.org/topojson.v3.min.js"></script>
|
<script src="https://d3js.org/topojson.v3.min.js"></script>
|
||||||
<script src="//unpkg.com/d3-geo-zoom"></script>
|
<script src="//unpkg.com/d3-geo-zoom"></script>
|
||||||
<script src="cola.min.js"></script>
|
|
||||||
<script src="mapChart2.js"></script>
|
<script src="//unpkg.com/d3fc@14.0.1"></script>
|
||||||
<script src="graph2.js"></script>
|
<script src="graph.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Loading…
Reference in a new issue