forked from security_vision/semantic_graph
Show tooltip and select moves into view
This commit is contained in:
parent
4dec76a325
commit
07503883d3
3 changed files with 362 additions and 64 deletions
125
www/graph.css
125
www/graph.css
|
@ -17,9 +17,9 @@
|
|||
--color9: #577590;
|
||||
--color10: #277da1;
|
||||
--hover-color: var(--color1);
|
||||
--hover-related-color: #d1bce9;
|
||||
/* --hover-color: var(darkblue); */
|
||||
--selected-color: var(--color1);
|
||||
--selected-color: var(--color1);
|
||||
}
|
||||
|
||||
body {
|
||||
|
@ -41,16 +41,20 @@ svg.dragging {
|
|||
}
|
||||
|
||||
#arrowHead {
|
||||
fill: #9df32c;
|
||||
fill: #b0e99a;
|
||||
}
|
||||
|
||||
#arrowHeadSelected {
|
||||
fill: var(--hover-color);;
|
||||
}
|
||||
#arrowHeadSelectedRelated {
|
||||
fill: var(--hover-related-color);;
|
||||
}
|
||||
|
||||
svg .links line, svg .links path {
|
||||
stroke: #f3722c;
|
||||
stroke: #9df32c;
|
||||
/* stroke: #f3722c; */
|
||||
/* stroke: #9df32c; */
|
||||
stroke: #b0e99a;
|
||||
stroke-width: 6;
|
||||
fill: none;
|
||||
transition: stroke-width 1s;
|
||||
|
@ -58,6 +62,18 @@ svg .links line, svg .links path {
|
|||
}
|
||||
|
||||
svg .links line.hover, svg .links path.hover {
|
||||
stroke: var(--hover-color);
|
||||
/* stroke-width: 12; */
|
||||
marker-end: url(#arrowHeadSelected);
|
||||
}
|
||||
|
||||
svg .links .linkedHover path{
|
||||
stroke: var(--hover-related-color);
|
||||
stroke-width: 12;
|
||||
marker-end: url(#arrowHeadSelectedRelated);
|
||||
}
|
||||
|
||||
svg .links .linkedSelected path{
|
||||
stroke: var(--hover-color);
|
||||
stroke-width: 12;
|
||||
marker-end: url(#arrowHeadSelected);
|
||||
|
@ -67,10 +83,9 @@ svg.zoomed .links line, svg.zoomed .links path {
|
|||
stroke-width: 2;
|
||||
}
|
||||
|
||||
svg.zoomed .links line, svg.zoomed .links path.hover {
|
||||
stroke-width: 2;
|
||||
/* svg.zoomed .links line, svg.zoomed .links path.hover {
|
||||
stroke-width: 4;
|
||||
}
|
||||
} */
|
||||
|
||||
svg .title {
|
||||
font-size: 200;
|
||||
|
@ -88,7 +103,7 @@ svg #countries .country.eu_country {
|
|||
fill: black;
|
||||
}
|
||||
|
||||
svg #header #titlePath, svg #header #subtitlePath {
|
||||
svg #header #titlePath, svg #header #title2Path, svg #header #subtitlePath {
|
||||
stroke: none;
|
||||
fill: none;
|
||||
}
|
||||
|
@ -104,8 +119,8 @@ svg #header text {
|
|||
}
|
||||
|
||||
svg #header text:nth-of-type(2) {
|
||||
dominant-baseline: hanging;
|
||||
transform: translate(10px, 25px);
|
||||
/* dominant-baseline: hanging; */
|
||||
/* transform: translate(10px, 25px); */
|
||||
}
|
||||
|
||||
svg #header text#subtitle {
|
||||
|
@ -131,13 +146,15 @@ svg #header text#subtitle {
|
|||
/* font-size: 16pt; */
|
||||
/*Set this in JS*/
|
||||
transition: font-size .4s, opacity 1s;
|
||||
fill: white;
|
||||
fill: white; /*also when hovering node*/
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
/*prevent mouse glitches*/
|
||||
}
|
||||
|
||||
.node:not(:hover):not(.linkHover) text.nodeTitle.overlapping {
|
||||
/* .node:not(:hover):not(.linkHover) text.nodeTitle.overlapping { */
|
||||
.node text.nodeTitle.overlapping {
|
||||
/* used to be shown on hover, but disabled now that we have a tooltip */
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
@ -155,26 +172,30 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
|||
|
||||
/* Whenever a connected link is hovered */
|
||||
|
||||
.node.linkHover circle, .node.linkHover path, label:hover .node path {
|
||||
fill: var(--hover-color) !important;
|
||||
stroke: var(--hover-color);
|
||||
.node.linkHover circle, .node.linkHover path, .node.linkedHover path, label:hover .node path {
|
||||
fill: var(--hover-related-color) !important;
|
||||
stroke: var(--hover-related-color);
|
||||
stroke-width: 5px;
|
||||
}
|
||||
.node.linkedSelected path {
|
||||
fill: var(--hover-related-color) !important;
|
||||
/* same as linkHover/linkedHover but without border */
|
||||
}
|
||||
|
||||
.node.linkHover text.nodeTitle.overlapping {
|
||||
transition: opacity 0s;
|
||||
}
|
||||
|
||||
.node:hover circle, .node:hover path {
|
||||
.node:hover circle, .node:hover path, .node.selected path {
|
||||
fill: var(--hover-color) !important;
|
||||
stroke: var(--hover-color);
|
||||
stroke-width: 5px;
|
||||
}
|
||||
|
||||
/*
|
||||
.node:hover text {
|
||||
transition: none;
|
||||
fill: var(--hover-color);
|
||||
}
|
||||
} */
|
||||
|
||||
.node.selected circle, .node.selected path {
|
||||
fill: var(--selected-color) !important;
|
||||
|
@ -248,6 +269,7 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
|||
display: none;
|
||||
}
|
||||
|
||||
|
||||
#nodeInfo h2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@ -258,6 +280,45 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
|||
height: calc(100vh - 40px - 20px - 30px);
|
||||
}
|
||||
|
||||
#tooltip{
|
||||
position:absolute;
|
||||
z-index: 100;
|
||||
opacity: 1;
|
||||
transition: opacity .3s;
|
||||
background:white;
|
||||
padding: 20px 10px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
#tooltip:not(.visible){
|
||||
position:absolute;
|
||||
z-index: 100;
|
||||
opacity:0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#tooltip h3{
|
||||
margin: 5px 0;
|
||||
text-align: center;;
|
||||
}
|
||||
#tooltip .category{
|
||||
display: block;
|
||||
color: black;
|
||||
text-align: center;;
|
||||
}
|
||||
#tooltip .category::before{
|
||||
content:'· '
|
||||
}
|
||||
#tooltip .category::after{
|
||||
content:' ·'
|
||||
}
|
||||
#tooltip .clickForMore{
|
||||
display: block;
|
||||
color: gray;
|
||||
text-align: center;;
|
||||
}
|
||||
|
||||
#closeInfo {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
|
@ -283,6 +344,7 @@ header {
|
|||
right: 0;
|
||||
background: white;
|
||||
padding: 10px;
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
@ -352,4 +414,31 @@ p.subtitle {
|
|||
|
||||
#alluvial .flow_label text {
|
||||
font-size: 30;
|
||||
}
|
||||
|
||||
|
||||
|
||||
body.light{
|
||||
background:white;
|
||||
}
|
||||
body.light #map .borders{
|
||||
stroke: white;
|
||||
}
|
||||
body.light svg #countries .country{
|
||||
fill:white;
|
||||
stroke:lightgray;
|
||||
stroke-width: 5;;
|
||||
}
|
||||
body.light svg #countries .country.eu_country{
|
||||
fill:rgb(235, 226, 236);
|
||||
}
|
||||
body.light .node text.nodeTitle {
|
||||
fill:black;
|
||||
}
|
||||
|
||||
body.light #arrowHead{
|
||||
fill:#577590;
|
||||
}
|
||||
body.light svg .links line, body.light svg .links path {
|
||||
stroke:#577590;
|
||||
}
|
294
www/graph.js
294
www/graph.js
|
@ -4,7 +4,7 @@ const CONFIG = {
|
|||
'subtitle': "Connections in the European Union & beyond",
|
||||
// 'nodeSize': 8,
|
||||
'nodeRadius': 5,
|
||||
'nodeRepositionPadding': 8,
|
||||
'nodeRepositionPadding': 10,
|
||||
'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
|
||||
|
@ -51,7 +51,16 @@ const CONFIG = {
|
|||
"Company": ["Managed by", "Provided by", "Developped by (institutions)"],
|
||||
"Tech": ["Technologies Used", "Software Deployed"],
|
||||
"Funding": ["Funded by"],
|
||||
}
|
||||
},
|
||||
"zoom": {
|
||||
"scale_min": .2,
|
||||
"scale_max": 20,
|
||||
},
|
||||
|
||||
"cases": [
|
||||
"Data-lab Burglary-Free Neighbourhood",
|
||||
"Dragonfly Project",
|
||||
]
|
||||
};
|
||||
|
||||
// let width = window.innerWidth;
|
||||
|
@ -59,7 +68,7 @@ const CONFIG = {
|
|||
|
||||
|
||||
function getSymbolForCategories(classes) {
|
||||
if(!Array.isArray(classes)) {
|
||||
if (!Array.isArray(classes)) {
|
||||
classes = [classes];
|
||||
}
|
||||
if (classes.includes('Institution')) {
|
||||
|
@ -194,12 +203,19 @@ function getClasses(obj) {
|
|||
return 'node ' + classes.join(' ');
|
||||
}
|
||||
|
||||
function getLinkId(link) {
|
||||
return "link_" + link.nr;
|
||||
// return "link-" + link.source.id + '-' + link.target.id + '-' + slugify(link.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
class NodeMap {
|
||||
constructor(parent) {
|
||||
this.root = d3.select(parent);
|
||||
this.resizeEvent = window.addEventListener('resize', this.resize.bind(this));
|
||||
this.tooltipEl = document.getElementById('tooltip');
|
||||
this.selectedNode = null;
|
||||
}
|
||||
|
||||
resize() {
|
||||
|
@ -247,7 +263,7 @@ class NodeMap {
|
|||
|
||||
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" ><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"></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" ><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"></path></marker><marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelectedRelated"><path d="M0,-3L8,0L0,3"></path></marker>
|
||||
<!--Sketching:-->
|
||||
<defs>
|
||||
<filter id="tint">
|
||||
|
@ -315,34 +331,40 @@ class NodeMap {
|
|||
.attr("d", this.proj(this.borders))
|
||||
|
||||
let zoomTimeout = null;
|
||||
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 }) => {
|
||||
this.container.attr("transform", transform);
|
||||
const oldZoom = this.svg.classed('zoomed');
|
||||
const newZoom = transform.k > 2.0;
|
||||
if (zoomTimeout) {
|
||||
clearTimeout(zoomTimeout)
|
||||
}
|
||||
zoomTimeout = setTimeout(() => {
|
||||
this.g_nodes.attr('style', `font-size:${22000 / this.height / transform.k}pt`)
|
||||
setTimeout(() => {
|
||||
this.calculateLabels();
|
||||
}, 500);
|
||||
}, 500);
|
||||
if (oldZoom != newZoom) {
|
||||
this.svg.classed('zoomed', newZoom);
|
||||
this.zoom = d3.zoom()
|
||||
.scaleExtent([CONFIG.zoom.scale_min, CONFIG.zoom.scale_max])
|
||||
.on("start", () => {
|
||||
this.svg.node().classList.add("dragging");
|
||||
}).on("end", () => {
|
||||
this.svg.node().classList.remove("dragging");
|
||||
}).on("zoom", (evt) => {
|
||||
this.container.attr("transform", evt.transform);
|
||||
const oldZoom = this.svg.classed('zoomed');
|
||||
const newZoom = evt.transform.k > 2.0;
|
||||
if (zoomTimeout) {
|
||||
clearTimeout(zoomTimeout)
|
||||
}
|
||||
zoomTimeout = setTimeout(() => {
|
||||
this.g_nodes.attr('style', `font-size:${22000 / this.height / evt.transform.k}pt`)
|
||||
setTimeout(() => {
|
||||
this.calculateLabels();
|
||||
}, 500);
|
||||
}, 250);
|
||||
if (oldZoom != newZoom) {
|
||||
this.svg.classed('zoomed', newZoom);
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
this.title = this.container.append('g').attr('id', 'header');
|
||||
|
||||
const titleFeature = {
|
||||
"type": "LineString",
|
||||
"coordinates": []
|
||||
};
|
||||
const title2Feature = {
|
||||
"type": "LineString",
|
||||
"coordinates": []
|
||||
};
|
||||
const subtitleFeature = {
|
||||
"type": "LineString",
|
||||
"coordinates": []
|
||||
|
@ -351,12 +373,17 @@ class NodeMap {
|
|||
// projection apparently tries to find the shortest path between two points
|
||||
// which is NOT following a lat/lon line on the globe
|
||||
titleFeature.coordinates.push([index, 52]);
|
||||
title2Feature.coordinates.push([index, 50.5]);
|
||||
subtitleFeature.coordinates.push([index, 49]);
|
||||
}
|
||||
this.title.append("path")
|
||||
.attr("id", "titlePath")
|
||||
.attr("d", this.proj(titleFeature))
|
||||
;
|
||||
this.title.append("path")
|
||||
.attr("id", "title2Path")
|
||||
.attr("d", this.proj(title2Feature))
|
||||
;
|
||||
this.title.append("path")
|
||||
.attr("id", "subtitlePath")
|
||||
.attr("d", this.proj(subtitleFeature))
|
||||
|
@ -364,7 +391,7 @@ class NodeMap {
|
|||
this.title.append("text")
|
||||
.html('<textPath xlink:href="#titlePath">Biometric</textPath>')
|
||||
this.title.append("text")
|
||||
.html('<textPath xlink:href="#titlePath">Mass Surveillance</textPath>')
|
||||
.html('<textPath xlink:href="#title2Path">Mass Surveillance</textPath>')
|
||||
this.title.append("text")
|
||||
.attr("id", "subtitle")
|
||||
.html('<textPath xlink:href="#subtitlePath">' + CONFIG.subtitle + '</textPath>')
|
||||
|
@ -448,8 +475,8 @@ class NodeMap {
|
|||
;
|
||||
|
||||
this.svg
|
||||
.call(zoom)
|
||||
.call(zoom.transform, d3.zoomIdentity.scale(.5, .5));
|
||||
.call(this.zoom)
|
||||
.call(this.zoom.transform, d3.zoomIdentity.scale(.5, .5));
|
||||
|
||||
|
||||
this.update();
|
||||
|
@ -457,6 +484,14 @@ class NodeMap {
|
|||
setTimeout(() => this.calculateLabels(), 1000);
|
||||
}
|
||||
|
||||
resetZoom() {
|
||||
this.deselectNode();
|
||||
this.svg
|
||||
.transition()
|
||||
.duration(2000) // milliseconds
|
||||
.call(this.zoom.transform, d3.zoomIdentity.scale(.5, .5));
|
||||
}
|
||||
|
||||
getSizeForNode(node) {
|
||||
return this.nodeSize;
|
||||
}
|
||||
|
@ -532,17 +567,111 @@ class NodeMap {
|
|||
console.log(`moved ${moved} nodes`);
|
||||
}
|
||||
|
||||
showTooltip(el, node, links) {
|
||||
// TODO: make links optional (otherwise collect links here)
|
||||
|
||||
this.tooltipEl.innerHTML = `
|
||||
<span class='category'>${getCategories(node)[0]}</span>
|
||||
<h3>${node.fulltext}</h3>
|
||||
`;
|
||||
if (links.length) {
|
||||
const rels = links.length === 1 ? 'relationship' : 'relationships';
|
||||
this.tooltipEl.innerHTML += `
|
||||
<span class='clickForMore'>Click to examine ${links.length} ${rels}</span>
|
||||
`;
|
||||
}
|
||||
const rect = el.getBoundingClientRect()
|
||||
const rectTT = this.tooltipEl.getBoundingClientRect();
|
||||
this.tooltipEl.style.top = (rect.top - rectTT.height) + 'px';
|
||||
this.tooltipEl.style.left = (rect.left + rect.width / 2 - rectTT.width / 2) + 'px';
|
||||
// console.log(el, node, rect.top);
|
||||
|
||||
this.tooltipEl.classList.add('visible');
|
||||
}
|
||||
|
||||
hideTooltip() {
|
||||
this.tooltipEl.classList.remove('visible');
|
||||
}
|
||||
|
||||
selectNode(node) {
|
||||
this.deselectNode(); // remove potential old selection
|
||||
|
||||
this.selectedNode = node;
|
||||
let links = [];
|
||||
let connectedNodes = [];
|
||||
for (let link of this.graph.links) {
|
||||
if (link.source == node || link.target == node) {
|
||||
links.push(link);
|
||||
const otherNode = node == link.target ? link.source : link.target;
|
||||
connectedNodes.push(otherNode);
|
||||
}
|
||||
}
|
||||
|
||||
let allNodes = [...connectedNodes, node];
|
||||
|
||||
this.zoomFit(allNodes);
|
||||
|
||||
document.getElementById(node.id).classList.add('selected');
|
||||
connectedNodes.forEach(n => document.getElementById(n.id).classList.add('linkedSelected'));
|
||||
links.forEach(l => document.getElementById(getLinkId(l)).classList.add('linkedSelected'));
|
||||
|
||||
// TODO: show details;
|
||||
|
||||
// alert('not yet implemented');
|
||||
}
|
||||
|
||||
deselectNode() {
|
||||
this.selectedNode = null;
|
||||
let nodeEls = document.getElementsByClassName('selected');
|
||||
while (nodeEls.length) {
|
||||
nodeEls[0].classList.remove('selected');
|
||||
}
|
||||
let els = document.getElementsByClassName('linkedSelected');
|
||||
while (els.length) {
|
||||
els[0].classList.remove('linkedSelected');
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
console.log(this.graph)
|
||||
// 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) => {
|
||||
let group = enter.append("g").attr("class", getClasses);
|
||||
let group = enter.append("g")
|
||||
.attr("class", getClasses)
|
||||
.attr("id", (n) => getIdForTitle(n.fulltext));
|
||||
// group.call(drag(simulation));
|
||||
group.on("click", (evt, n) => selectNode(evt, n, node));
|
||||
group.on("click", (evt, n) => this.selectNode(n));
|
||||
group.on("mouseover", (evt, n) => {
|
||||
// d3.select(this).classed('hover', true);
|
||||
const links = document.getElementsByClassName('link');
|
||||
const linkedLinks = [];
|
||||
for (let link of links) {
|
||||
const l = d3.select(link).datum();
|
||||
if (n == l.target || n == l.source) {
|
||||
link.classList.add('linkedHover');
|
||||
// make sure it's the last element, so it's drawn on top
|
||||
// link.parentNode.appendChild(link); .. causes gliches
|
||||
// find related related node:
|
||||
const otherNode = n == l.target ? l.source : l.target;
|
||||
const otherNodeEl = document.getElementById(otherNode.id);
|
||||
otherNodeEl.classList.add('linkedHover');
|
||||
linkedLinks.push(l);
|
||||
}
|
||||
}
|
||||
this.showTooltip(evt.target, n, linkedLinks);
|
||||
|
||||
});
|
||||
group.on("mouseout", (evt, n) => {
|
||||
this.hideTooltip();
|
||||
const links = document.getElementsByClassName('linkedHover');
|
||||
while (links.length) {
|
||||
links[0].classList.remove('linkedHover');
|
||||
}
|
||||
});
|
||||
// group.append('circle').attr("r", 5 /*this.nodeSize*/);
|
||||
group.append('path')
|
||||
.attr('d', (n) => {
|
||||
|
@ -637,10 +766,11 @@ class NodeMap {
|
|||
.join(
|
||||
enter => {
|
||||
let group = enter.append("g")
|
||||
.attr("class", (l) => "link " + slugify(l.name));
|
||||
.attr("class", (l) => "link " + slugify(l.name))
|
||||
.attr("id", getLinkId);
|
||||
group.append("path")
|
||||
.attr("marker-end", "url(#arrowHead)")
|
||||
.attr('id', (d, i) => 'linkid_' + i)
|
||||
.attr('id', (d, i) => 'linkpath_' + i)
|
||||
.on("mouseover", function (ev, link) {
|
||||
d3.select(this).classed('hover', true);
|
||||
const nodes = document.getElementsByClassName('node');
|
||||
|
@ -654,14 +784,17 @@ class NodeMap {
|
|||
}).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');
|
||||
while (nodes.length) {
|
||||
nodes[0].classList.remove('linkHover');
|
||||
}
|
||||
// l.classed('hover',false);
|
||||
// l.target.classed('hover',false);
|
||||
// l.source.classed('hover',false);
|
||||
// console.log(l,'l');
|
||||
});
|
||||
}).on("click", (ev, link) => {
|
||||
this.selectNode(link.source);
|
||||
})
|
||||
;
|
||||
group.filter((l) => l.name != "City").append("text").attr("class", "labelText").text(function (l) {
|
||||
return l.name;
|
||||
});
|
||||
|
@ -795,6 +928,60 @@ class NodeMap {
|
|||
redraw() {
|
||||
this.update()
|
||||
}
|
||||
|
||||
// viewBox + preserveAspectRatio can lead to a visible area that is larger than
|
||||
// the viewBox. Try to get this
|
||||
getVisibleBox() {
|
||||
const svgEl = this.svg.node()
|
||||
const vbRatio = svgEl.viewBox.baseVal.width / svgEl.viewBox.baseVal.height;
|
||||
const wRatio = this.width / this.height;
|
||||
if (wRatio > vbRatio) {
|
||||
// wider
|
||||
return {
|
||||
width: (wRatio / vbRatio) * svgEl.viewBox.baseVal.width,
|
||||
height: svgEl.viewBox.baseVal.height
|
||||
}
|
||||
} else {
|
||||
// taller
|
||||
return {
|
||||
width: svgEl.viewBox.baseVal.width,
|
||||
height: (vbRatio / wRatio) * svgEl.viewBox.baseVal.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zoomFit(nodes, paddingPercent = 0.8, transitionDuration = 2000) {
|
||||
// var bounds = root.node().getBBox();
|
||||
const x0 = Math.min(...nodes.map(n => n.x - CONFIG.nodeRadius));
|
||||
const x1 = Math.max(...nodes.map(n => n.x + CONFIG.nodeRadius));
|
||||
const y0 = Math.min(...nodes.map(n => n.y - CONFIG.nodeRadius));
|
||||
const y1 = Math.max(...nodes.map(n => n.y + CONFIG.nodeRadius));
|
||||
|
||||
const width = x1 - x0;
|
||||
const height = y1 - y0;
|
||||
|
||||
const visibleBox = this.getVisibleBox();
|
||||
const fullWidth = visibleBox.width, //2000, //this.width, TODO: use viewbox now, but consider the overflowing of the vbox
|
||||
fullHeight = visibleBox.height; //2000; //this.height;
|
||||
const viewBox = this.svg.node().viewBox.baseVal;
|
||||
const midX = x0 + width / 2,
|
||||
midY = y0 + height / 2;
|
||||
if (width == 0 || height == 0) return; // nothing to fit
|
||||
let scale = paddingPercent / Math.max(width / fullWidth, height / fullHeight);
|
||||
scale = Math.min(CONFIG.zoom.scale_max, scale);
|
||||
const translate = [fullWidth / 2 - midX, fullHeight / 2 - midY];
|
||||
const transform = d3.zoomIdentity.translate(
|
||||
-midX * scale + viewBox.width / 2,
|
||||
-midY * scale + viewBox.height / 2)
|
||||
.scale(scale);
|
||||
// console.log("zoomFit", x0, x1, y0, y1, translate, scale, transform);
|
||||
this.svg
|
||||
.transition()
|
||||
.duration(transitionDuration || 0) // milliseconds
|
||||
.call(this.zoom.transform, transform);
|
||||
// .call(this.zoom.translateTo, midX, midY)
|
||||
// .call(this.zoom.scaleTo, scale);
|
||||
}
|
||||
}
|
||||
|
||||
class AlluvialMap {
|
||||
|
@ -838,7 +1025,7 @@ class AlluvialMap {
|
|||
.nodeWidth(40) // height
|
||||
.nodePadding(10)
|
||||
.extent([[1, 5], [2000 - 1, 2000 - 5]]);
|
||||
console.log(this.graph);
|
||||
// console.log(this.graph);
|
||||
let s = this.sankey({
|
||||
nodes: this.graph.nodes.map(d => Object.assign({}, d)),
|
||||
links: this.graph.links.map(d => Object.assign({}, d))
|
||||
|
@ -938,16 +1125,27 @@ class AlluvialMap {
|
|||
|
||||
}
|
||||
|
||||
titleIdMap = {};
|
||||
function getIdForTitle(title) {
|
||||
if (!titleIdMap.hasOwnProperty(title)) {
|
||||
titleIdMap[title] = slugify(title) + `-${Object.keys(titleIdMap).length}`
|
||||
}
|
||||
return titleIdMap[title];
|
||||
}
|
||||
|
||||
JsonToGraph = function (data) {
|
||||
let nodes = [];
|
||||
let links = [];
|
||||
|
||||
let smwBugFixLocationMaps = {};
|
||||
console.log(data)
|
||||
let i = 0;
|
||||
let linkI = 0;
|
||||
for (const node_id in data.results) {
|
||||
if (Object.hasOwnProperty.call(data.results, node_id)) {
|
||||
i++;
|
||||
let node = data.results[node_id];
|
||||
node.id = node.fulltext; //node_id;
|
||||
node.id = getIdForTitle(node.fulltext); //node_id;
|
||||
nodes.push(node);
|
||||
// console.log(node_id, node);
|
||||
|
||||
|
@ -967,9 +1165,9 @@ JsonToGraph = function (data) {
|
|||
}
|
||||
for (const target_node of node.printouts[prop]) {
|
||||
links.push({
|
||||
"source": node_id,
|
||||
"target": target_node.fulltext,
|
||||
"name": prop
|
||||
"source": node.id,
|
||||
"target": getIdForTitle(target_node.fulltext),
|
||||
"name": prop,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -998,14 +1196,18 @@ JsonToGraph = function (data) {
|
|||
|
||||
console.debug(`Fixed location for ${fixes} nodes`);
|
||||
|
||||
console.log(links.length);
|
||||
|
||||
nodeMap = Object.fromEntries(nodes.map(d => [d['id'], d]));
|
||||
links = links.filter(l => nodeMap[l.source] && nodeMap[l.target]).map(l => {
|
||||
l.source = nodeMap[l.source];
|
||||
l.target = nodeMap[l.target];
|
||||
l.nr = linkI++;
|
||||
return l;
|
||||
});
|
||||
}).filter((link, index, self) =>
|
||||
// remove incidental duplicates
|
||||
index === self.findIndex((l) => (
|
||||
l.source.id === link.source.id && l.target.id === link.target.id && l.name === link.name
|
||||
))
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
@ -1318,9 +1520,9 @@ class Store {
|
|||
let inputEl = document.createElement('input')
|
||||
let textEl = document.createElement('span');
|
||||
let svg = d3.select(labelEl).append('svg')
|
||||
.attr("viewBox", [-12,-12,24,24]);
|
||||
.attr("viewBox", [-12, -12, 24, 24]);
|
||||
svg.append('g')
|
||||
.attr("class", "node "+ f)
|
||||
.attr("class", "node " + f)
|
||||
.append('path')
|
||||
.attr('d', getSymbolForCategories(f)());
|
||||
inputEl.type = "checkbox";
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
<body>
|
||||
|
||||
<div id='tooltip'></div>
|
||||
<div id='map'></div>
|
||||
<!-- <div id='alluvial'></div> -->
|
||||
|
||||
|
@ -31,6 +32,12 @@
|
|||
<script src="https://unpkg.com/d3-sankey@0"></script>
|
||||
<!-- <script src="//unpkg.com/d3fc@14.0.1"></script> -->
|
||||
<script src="graph.js"></script>
|
||||
|
||||
<script>
|
||||
if(window.location.hash == '#light') {
|
||||
document.body.classList.add('light');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in a new issue