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;
|
||||
|
||||
--hover-color: var(--color1);
|
||||
/* --hover-color: var(darkblue); */
|
||||
--selected-color: var(--color1);
|
||||
--selected-color: var(--color1);
|
||||
}
|
||||
|
@ -36,8 +37,18 @@ svg.dragging {
|
|||
|
||||
svg .links line,svg .links path{
|
||||
stroke: #f3722c;
|
||||
stroke-width: 3;
|
||||
stroke-width: 6;
|
||||
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{
|
||||
|
@ -47,8 +58,28 @@ svg .links line,svg .links path{
|
|||
fill: whitesmoke;
|
||||
}
|
||||
|
||||
.node text{
|
||||
text-anchor: middle;
|
||||
.node{
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -56,14 +87,23 @@ svg .links line,svg .links path{
|
|||
fill: white;
|
||||
}
|
||||
|
||||
.node:hover{
|
||||
cursor: pointer;
|
||||
/* Whenever a connected link is hovered */
|
||||
.node.linkHover circle{
|
||||
stroke: var(--hover-color);
|
||||
stroke-width: 5px;
|
||||
}
|
||||
.node.linkHover text.nodeTitle.overlapping{
|
||||
transition: opacity 0s;
|
||||
}
|
||||
|
||||
.node:hover circle{
|
||||
stroke: var(--hover-color);
|
||||
stroke-width: 5px;
|
||||
}
|
||||
.node:hover text{
|
||||
transition: none;
|
||||
fill: var(--hover-color);
|
||||
}
|
||||
.node.selected circle{
|
||||
stroke: var(--selected-color);
|
||||
stroke-width: 5px;
|
||||
|
@ -94,6 +134,13 @@ svg .links line,svg .links path{
|
|||
fill: plum
|
||||
}
|
||||
|
||||
|
||||
|
||||
.labels .label text{
|
||||
fill:yellow;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* .node.Person circle {
|
||||
fill: var(--color2)
|
||||
}
|
||||
|
@ -160,3 +207,9 @@ a:hover{
|
|||
background: white;
|
||||
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,
|
||||
'center': [11, 47],
|
||||
},
|
||||
"filters": ["Institution", "Deployments", "Technology", "Dataset"],
|
||||
"filters": ["Institution", "Deployments",/* "Technology", "Dataset"*/],
|
||||
|
||||
"link_properties": [
|
||||
"Clients",
|
||||
|
@ -159,6 +159,32 @@ class NodeMap {
|
|||
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() {
|
||||
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>');
|
||||
|
@ -166,8 +192,8 @@ class NodeMap {
|
|||
|
||||
this.projection = d3.geoHill()
|
||||
.rotate([-12, 0, 0])
|
||||
.translate([this.vbWidth / 2, this.vbHeight * 1.5])
|
||||
.scale(this.vbHeight * 1.5);
|
||||
.translate([this.vbWidth, this.vbHeight * 3])
|
||||
.scale(this.vbHeight * 3);
|
||||
|
||||
this.nodeSize = this.vbHeight / 200;
|
||||
|
||||
|
@ -175,12 +201,12 @@ class NodeMap {
|
|||
const graticule = d3.geoGraticule10();
|
||||
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")
|
||||
|
||||
this.g_countries = container.append("g").attr("id", "countries");
|
||||
this.g_borders = container.append("g").attr("id", "borders");
|
||||
this.g_graticule = container.append("g").attr('id', 'graticule')
|
||||
this.g_countries = this.container.append("g").attr("id", "countries");
|
||||
this.g_borders = this.container.append("g").attr("id", "borders");
|
||||
this.g_graticule = this.container.append("g").attr('id', 'graticule')
|
||||
.append('path')
|
||||
.attr("class", "graticule")
|
||||
.attr("fill", "none")
|
||||
|
@ -209,26 +235,33 @@ class NodeMap {
|
|||
.append("path")
|
||||
.attr("class", "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");
|
||||
}).on("end", () => {
|
||||
this.svg.node().classList.remove("dragging");
|
||||
}).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")
|
||||
.attr('class', 'nodes')
|
||||
.selectAll(".node");
|
||||
this.link = container.append("g")
|
||||
setTimeout(() => {
|
||||
this.calculateLabels();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
this.svg
|
||||
.call(zoom)
|
||||
.call(zoom.transform, d3.zoomIdentity.scale(.5, .5));
|
||||
|
||||
this.link = this.container.append("g")
|
||||
.attr('class', 'links')
|
||||
.selectAll(".link");
|
||||
this.node = this.container.append("g")
|
||||
.attr('class', 'nodes')
|
||||
.selectAll(".node");
|
||||
|
||||
|
||||
this.graph.nodes.forEach((d) => {
|
||||
|
@ -282,6 +315,8 @@ class NodeMap {
|
|||
;
|
||||
|
||||
this.update();
|
||||
|
||||
setTimeout(() => this.calculateLabels(), 1000);
|
||||
}
|
||||
|
||||
getSizeForNode(node) {
|
||||
|
@ -296,10 +331,10 @@ class NodeMap {
|
|||
let group = enter.append("g").attr("class", getClasses);
|
||||
// group.call(drag(simulation));
|
||||
group.on("click", (evt, n) => selectNode(evt, n, node));
|
||||
group.append('circle').attr("r", this.nodeSize);
|
||||
var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "5");
|
||||
group.append('circle').attr("r", 5 /*this.nodeSize*/);
|
||||
var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "3").attr('x', 5);
|
||||
nodeTitle
|
||||
.each(function (node, nodes) {
|
||||
.each(function (node, i, nodes) {
|
||||
var textLength = void 0;
|
||||
const self = d3.select(this);
|
||||
const titleText = getTitle(node);
|
||||
|
@ -308,30 +343,108 @@ class NodeMap {
|
|||
titleTexts = splitText(titleText);
|
||||
}
|
||||
if (titleTexts !== false) {
|
||||
const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "-10").attr("x", "0");
|
||||
const tspan = self.append("tspan").text(titleTexts[1]).attr("y", "10").attr("x", "0");
|
||||
const textLength1 = tspan.node().getComputedTextLength();
|
||||
const textLength2 = tspan.node().getComputedTextLength();
|
||||
textLength = Math.max(textLength1, textLength2);
|
||||
const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "3").attr("x", "5");
|
||||
const tspan = self.append("tspan").text(titleTexts[1]).attr("dy", "1em").attr("x", "5");
|
||||
// const textLength1 = tspan.node().getComputedTextLength();
|
||||
// const textLength2 = tspan.node().getComputedTextLength();
|
||||
// textLength = Math.max(textLength1, textLength2);
|
||||
} else {
|
||||
self.text(titleText);
|
||||
textLength = self.node().getComputedTextLength();
|
||||
}
|
||||
// scale according to text length:
|
||||
if (textLength > this.nodeSize * 2) {
|
||||
self.attr('transform', 'scale(' + this.nodeSize * 2 / textLength / 1.05 + ')');
|
||||
// textLength = self.node().getComputedTextLength();
|
||||
}
|
||||
});
|
||||
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
|
||||
.data(this.graph.links)
|
||||
.join(
|
||||
enter => {
|
||||
console.log(enter);
|
||||
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) {
|
||||
return l.name;
|
||||
});
|
||||
|
@ -447,7 +560,7 @@ class NodeMap {
|
|||
|
||||
this.simulation.alpha = 0;
|
||||
this.simulation.restart();
|
||||
|
||||
this.calculateLabels()
|
||||
}
|
||||
|
||||
setWorld(world) {
|
||||
|
@ -526,7 +639,7 @@ JsonToGraph = function (data) {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
console.debug(`Fixed location for ${fixes} nodes`);
|
||||
|
||||
console.log(links.length);
|
||||
|
|
|
@ -15,14 +15,13 @@
|
|||
</div>
|
||||
|
||||
<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/topojson.v3.min.js"></script>
|
||||
<script src="//unpkg.com/d3-geo-zoom"></script>
|
||||
<script src="cola.min.js"></script>
|
||||
<script src="mapChart2.js"></script>
|
||||
<script src="graph2.js"></script>
|
||||
|
||||
<script src="//unpkg.com/d3fc@14.0.1"></script>
|
||||
<script src="graph.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in a new issue