forked from security_vision/semantic_graph
Show sources and fix arrow angle offset
This commit is contained in:
parent
815bb200be
commit
3c0184fe97
3 changed files with 111 additions and 85 deletions
|
@ -391,6 +391,10 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, .5);
|
box-shadow: 2px 2px 5px rgba(0, 0, 0, .5);
|
||||||
}
|
}
|
||||||
|
#tooltip.deploymentTooltip{
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
#tooltip.link{
|
#tooltip.link{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -415,6 +419,9 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
||||||
color: black;
|
color: black;
|
||||||
text-align: center;;
|
text-align: center;;
|
||||||
}
|
}
|
||||||
|
#tooltip.deploymentTooltip .category{
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
#tooltip .category::before{
|
#tooltip .category::before{
|
||||||
content:'· '
|
content:'· '
|
||||||
}
|
}
|
||||||
|
@ -426,6 +433,12 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
||||||
color: gray;
|
color: gray;
|
||||||
text-align: center;;
|
text-align: center;;
|
||||||
}
|
}
|
||||||
|
#tooltip .node_sources::before{
|
||||||
|
content: 'source ';
|
||||||
|
}
|
||||||
|
#tooltip .node_sources{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
#closeInfo {
|
#closeInfo {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -564,19 +577,55 @@ p.subtitle {
|
||||||
|
|
||||||
#closeSelection{
|
#closeSelection{
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0;
|
position: fixed;
|
||||||
position: absolute;
|
bottom: -100px;
|
||||||
top: 0;
|
left: 0;
|
||||||
right: 0;
|
|
||||||
background-color: white;
|
background-color: white;
|
||||||
font-size: 200%;
|
font-size: 50px;
|
||||||
width: 35px;
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
text-align:center;
|
text-align:center;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
line-height: 80px;
|
||||||
|
transition: bottom .2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectedNode #closeSelection{
|
.selectedNode #closeSelection{
|
||||||
pointer-events: all;;
|
pointer-events: all;
|
||||||
opacity: 1;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sources{
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 80px;
|
||||||
|
height: 47px;
|
||||||
|
padding: 20px 20px 13px 20px;
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
transition: bottom .2s;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sources:not(.visible){
|
||||||
|
bottom:-100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sources h3{
|
||||||
|
margin: 5px 0;
|
||||||
|
text-align: left;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
#sources h3::after{
|
||||||
|
content:':';
|
||||||
|
}
|
||||||
|
|
||||||
|
#sources .node_sources{
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sources a{
|
||||||
|
color: lightblue;
|
||||||
}
|
}
|
129
www/graph.js
129
www/graph.js
|
@ -366,6 +366,7 @@ class NodeMap {
|
||||||
this.root = d3.select(parent);
|
this.root = d3.select(parent);
|
||||||
this.resizeEvent = window.addEventListener('resize', this.resize.bind(this));
|
this.resizeEvent = window.addEventListener('resize', this.resize.bind(this));
|
||||||
this.tooltipEl = document.getElementById('tooltip');
|
this.tooltipEl = document.getElementById('tooltip');
|
||||||
|
this.sourcesEl = document.getElementById('sources');
|
||||||
this.selectedNode = null;
|
this.selectedNode = null;
|
||||||
|
|
||||||
document.getElementById('closeSelection').addEventListener('click', (ev) => this.deselectNode());
|
document.getElementById('closeSelection').addEventListener('click', (ev) => this.deselectNode());
|
||||||
|
@ -760,10 +761,17 @@ class NodeMap {
|
||||||
}
|
}
|
||||||
el = parentEl.querySelector('path');
|
el = parentEl.querySelector('path');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const categories = getCategories(node);
|
||||||
|
if (categories.includes('Deployments')){
|
||||||
|
this.tooltipEl.classList.add('deploymentTooltip');
|
||||||
|
} else {
|
||||||
|
this.tooltipEl.classList.remove('deploymentTooltip');
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: make links optional (otherwise collect links here)
|
// TODO: make links optional (otherwise collect links here)
|
||||||
this.tooltipEl.innerHTML = `
|
this.tooltipEl.innerHTML = `
|
||||||
<span class='category'>${getCategories(node)[0]}</span>
|
<span class='category'>${categories[0]}</span>
|
||||||
<h3>${node.fulltext}</h3>
|
<h3>${node.fulltext}</h3>
|
||||||
`;
|
`;
|
||||||
if (links.length) {
|
if (links.length) {
|
||||||
|
@ -772,6 +780,7 @@ class NodeMap {
|
||||||
<span class='clickForMore'>Click to examine ${links.length} ${rels}</span>
|
<span class='clickForMore'>Click to examine ${links.length} ${rels}</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect()
|
const rect = el.getBoundingClientRect()
|
||||||
const rectTT = this.tooltipEl.getBoundingClientRect();
|
const rectTT = this.tooltipEl.getBoundingClientRect();
|
||||||
this.tooltipEl.style.top = (rect.top - rectTT.height) + 'px';
|
this.tooltipEl.style.top = (rect.top - rectTT.height) + 'px';
|
||||||
|
@ -783,6 +792,7 @@ class NodeMap {
|
||||||
|
|
||||||
showRelationTooltip(link, evt) {
|
showRelationTooltip(link, evt) {
|
||||||
const {label, swap} = getLinkLabelConfig(link.name);
|
const {label, swap} = getLinkLabelConfig(link.name);
|
||||||
|
this.tooltipEl.classList.remove('deploymentTooltip');
|
||||||
if(swap){
|
if(swap){
|
||||||
this.tooltipEl.innerHTML = `
|
this.tooltipEl.innerHTML = `
|
||||||
<span class='relation'>
|
<span class='relation'>
|
||||||
|
@ -845,12 +855,15 @@ class NodeMap {
|
||||||
this.container.classed('selectedNode', true);
|
this.container.classed('selectedNode', true);
|
||||||
|
|
||||||
document.body.classList.add('selectedNode');
|
document.body.classList.add('selectedNode');
|
||||||
|
|
||||||
|
this.showSources(node);
|
||||||
// TODO: show details;
|
// TODO: show details;
|
||||||
|
|
||||||
// alert('not yet implemented');
|
// alert('not yet implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
deselectNode() {
|
deselectNode() {
|
||||||
|
this.hideSources();
|
||||||
this.selectedNode = null;
|
this.selectedNode = null;
|
||||||
let nodeEls = document.getElementsByClassName('selected');
|
let nodeEls = document.getElementsByClassName('selected');
|
||||||
while (nodeEls.length) {
|
while (nodeEls.length) {
|
||||||
|
@ -864,6 +877,37 @@ class NodeMap {
|
||||||
document.body.classList.remove('selectedNode');
|
document.body.classList.remove('selectedNode');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showSources(node){
|
||||||
|
|
||||||
|
const categories = getCategories(node);
|
||||||
|
if (!categories.includes('Deployments')){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!node.printouts['Source'].length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setTimeout(() => { // give potential visible sources time to hide
|
||||||
|
let sources = [];
|
||||||
|
for(let source of node.printouts['Source']){
|
||||||
|
const url = document.createElement('a');
|
||||||
|
url.href = source;
|
||||||
|
const hostname = url.hostname.startsWith('www.') ? url.hostname.substring(4) : url.hostname;
|
||||||
|
sources .push(`<a href="${source}" target="_blank">${hostname}</a>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const title = node.printouts['Source'].length > 1 ? "Sources" : "Source"
|
||||||
|
this.sourcesEl.innerHTML = `<h3>${title}</h3> ` + sources.join(', ');
|
||||||
|
this.sourcesEl.classList.add('visible');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSources(){
|
||||||
|
this.sourcesEl.classList.remove('visible');
|
||||||
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
// console.log(this.graph)
|
// console.log(this.graph)
|
||||||
|
|
||||||
|
@ -941,64 +985,6 @@ class NodeMap {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 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(
|
||||||
|
@ -1020,8 +1006,6 @@ class NodeMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.showRelationTooltip(link, ev);
|
this.showRelationTooltip(link, ev);
|
||||||
|
|
||||||
// console.log(l);
|
|
||||||
}).on("mouseout", (ev, link) => {
|
}).on("mouseout", (ev, link) => {
|
||||||
this.hideTooltip();
|
this.hideTooltip();
|
||||||
d3.select(ev.target).classed('hover', false);
|
d3.select(ev.target).classed('hover', false);
|
||||||
|
@ -1029,10 +1013,6 @@ class NodeMap {
|
||||||
while (nodes.length) {
|
while (nodes.length) {
|
||||||
nodes[0].classList.remove('linkHover');
|
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) => {
|
}).on("click", (ev, link) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.selectNode(link.source);
|
this.selectNode(link.source);
|
||||||
|
@ -1105,28 +1085,25 @@ class NodeMap {
|
||||||
var tgtSize = _mapGraph.getSizeForNode(l.target);
|
var tgtSize = _mapGraph.getSizeForNode(l.target);
|
||||||
|
|
||||||
// Compute the line endpoint such that the arrow
|
// Compute the line endpoint such that the arrow
|
||||||
// is touching the edge of the node rectangle perfectly.
|
// it not in the center, but rather slightly out of it
|
||||||
l.sourceX = sourceX + Math.sin(angle) * srcSize;
|
// use a small ofset for the angle to compensate roughly for the curve
|
||||||
l.targetX = targetX - Math.sin(angle) * tgtSize;
|
l.sourceX = sourceX + Math.sin(angle+.5) * srcSize;
|
||||||
l.sourceY = l.source.y + Math.cos(angle) * srcSize;
|
l.targetX = targetX - Math.sin(angle-.5) * tgtSize;
|
||||||
l.targetY = l.target.y - Math.cos(angle) * tgtSize;
|
l.sourceY = l.source.y + Math.cos(angle+.5) * srcSize;
|
||||||
|
l.targetY = l.target.y - Math.cos(angle-.5) * tgtSize;
|
||||||
|
|
||||||
// const coor_source = _mapGraph.projection.invert([l.source.x, l.source.y]);
|
// const coor_source = _mapGraph.projection.invert([l.source.x, l.source.y]);
|
||||||
// const coor_target = _mapGraph.projection.invert([l.target.x, l.target.y]);
|
// const coor_target = _mapGraph.projection.invert([l.target.x, l.target.y]);
|
||||||
// const middleCoor = [coor_source[0] * .5 + coor_target[0] * .5, coor_source[1] * .5 + coor_target[1] * .5];
|
// const middleCoor = [coor_source[0] * .5 + coor_target[0] * .5, coor_source[1] * .5 + coor_target[1] * .5];
|
||||||
// const middlePoint = _mapGraph.projection(middleCoor);
|
// const middlePoint = _mapGraph.projection(middleCoor);
|
||||||
|
|
||||||
|
// find radius of arc based on distance between points
|
||||||
const dr = Math.sqrt(dx * dx + dy * dy);
|
const dr = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
// "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y
|
// "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y
|
||||||
let rel = d3.select(this);
|
let rel = d3.select(this);
|
||||||
rel.select("path") //${middlePoint[0]},${middlePoint[1]}
|
rel.select("path")
|
||||||
// .attr('d', `M ${l.sourceX},${l.sourceY} L ${l.targetX},${l.targetY}`)
|
|
||||||
.attr('d', `M ${l.sourceX},${l.sourceY} A ${dr},${dr} 0 0,1 ${l.targetX},${l.targetY}`)
|
.attr('d', `M ${l.sourceX},${l.sourceY} A ${dr},${dr} 0 0,1 ${l.targetX},${l.targetY}`)
|
||||||
// .attr("x1", l.sourceX)
|
|
||||||
// .attr("y1", l.sourceY)
|
|
||||||
// .attr("x2", l.targetX)
|
|
||||||
// .attr("y2", l.targetY)
|
|
||||||
|
|
||||||
rel.select('text')
|
rel.select('text')
|
||||||
.attr("transform", function (d) {
|
.attr("transform", function (d) {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div id='tooltip'></div>
|
<div id='tooltip'></div>
|
||||||
|
<div id='sources'></div>
|
||||||
<div id='closeSelection'>×</div>
|
<div id='closeSelection'>×</div>
|
||||||
<div id='map'></div>
|
<div id='map'></div>
|
||||||
|
|
||||||
|
@ -16,7 +17,6 @@
|
||||||
<h1>Remote Biometric Identification</h1>
|
<h1>Remote Biometric Identification</h1>
|
||||||
<p class='subtitle'>A survey of the European Union</p>
|
<p class='subtitle'>A survey of the European Union</p>
|
||||||
|
|
||||||
|
|
||||||
<aside id="filters">
|
<aside id="filters">
|
||||||
<h3 onclick="this.parentNode.classList.toggle('hide');">Filter</h3>
|
<h3 onclick="this.parentNode.classList.toggle('hide');">Filter</h3>
|
||||||
<div id="filter-items"></div>
|
<div id="filter-items"></div>
|
||||||
|
|
Loading…
Reference in a new issue