Relation tooltip, filter, page title

This commit is contained in:
Ruben van de Ven 2021-08-12 09:00:05 +02:00
parent a43c320af6
commit 1650efae49
3 changed files with 185 additions and 56 deletions

View File

@ -22,11 +22,13 @@
--selected-color: var(--color1); --selected-color: var(--color1);
--link-color: rgba(255,255,255,0.5); --link-color: rgba(255,255,255,0.5);
--link-hover-color: var(--hover-color); --link-hover-color: var(--hover-color);
--link-hover-related-color: var(--hover-related-color); --link-hover-related-color: var(--hover-related-color);
--link-focus-color: var(--hover-color); --link-focus-color: var(--hover-color);
--body-back: #96a7b7; /*#9cb3c9; /*#8195a7; /*#9cb3c9; #b9cada*/ --body-back: #96a7b7; /*#9cb3c9; /*#8195a7; /*#9cb3c9; #b9cada*/
--title-color: #1c1c1c; --title-color: #1c1c1c;
--zoom: 1; /* to be overriden by js */
} }
body { body {
@ -63,33 +65,33 @@ svg .links line, svg .links path {
/* stroke: #f3722c; */ /* stroke: #f3722c; */
/* stroke: #9df32c; */ /* stroke: #9df32c; */
stroke: var(--link-color); stroke: var(--link-color);
stroke-width: 6; stroke-width: calc(10px / var(--zoom));
fill: none; fill: none;
transition: stroke-width 1s; /* transition: stroke-width 1s; */
cursor: pointer; cursor: pointer;
} }
svg .links line.hover, svg .links path.hover { svg .links line.hover, svg .links path.hover {
stroke: var(--link-hover-color); stroke: var(--link-hover-color) !important;
/* stroke-width: 12; */ /* stroke-width: 12; */
marker-end: url(#arrowHeadSelected); marker-end: url(#arrowHeadSelected) !important;
} }
svg .links .linkedHover path{ svg .links .linkedHover path{
stroke: var(--link-hover-related-color); stroke: var(--link-hover-related-color);
stroke-width: 12; stroke-width: calc(12px / var(--zoom));
marker-end: url(#arrowHeadSelectedRelated); marker-end: url(#arrowHeadSelectedRelated);
} }
svg .links .linkedSelected path{ svg .links .linkedSelected path{
stroke: var(--link-focus-color); stroke: var(--link-hover-related-color);
stroke-width: 12; stroke-width: calc(12px / var(--zoom));
marker-end: url(#arrowHeadSelected); marker-end: url(#arrowHeadSelectedRelated);
} }
svg.zoomed .links line, svg.zoomed .links path { /* svg.zoomed .links line, svg.zoomed .links path {
stroke-width: 2; stroke-width: 2;
} } */
/* svg.zoomed .links line, svg.zoomed .links path.hover { /* svg.zoomed .links line, svg.zoomed .links path.hover {
stroke-width: 4; stroke-width: 4;
@ -154,16 +156,18 @@ svg #header text#subtitle {
cursor: pointer; cursor: pointer;
} }
.node text.nodeTitle { .node text.nodeTitle {
text-anchor: start; text-anchor: start;
dominant-baseline: hanging; dominant-baseline: hanging;
font-size: calc(20pt / var(--zoom)) !important;
/*achieves a 'text-anchor: top'*/ /*achieves a 'text-anchor: top'*/
/* font-size: 16pt; */ /* font-size: 16pt; */
/*Set this in JS*/ /*Set this in JS*/
transition: font-size .4s, opacity 1s; transition: opacity 1s;
fill: #5d5d5f; /*also when hovering node*/ fill: #5d5d5f; /*also when hovering node*/
opacity: 1; opacity: 1;
pointer-events: none; /* pointer-events: none; */
/*prevent mouse glitches*/ /*prevent mouse glitches*/
} }
@ -171,6 +175,7 @@ svg #header text#subtitle {
.node text.nodeTitle.overlapping { .node text.nodeTitle.overlapping {
/* used to be shown on hover, but disabled now that we have a tooltip */ /* used to be shown on hover, but disabled now that we have a tooltip */
opacity: 0; opacity: 0;
pointer-events: none;
} }
svg.zoomed .node text.nodeTitle { svg.zoomed .node text.nodeTitle {
@ -190,10 +195,10 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
.node.linkHover circle, .node.linkHover path, .node.linkedHover path, label:hover .node path { .node.linkHover circle, .node.linkHover path, .node.linkedHover path, label:hover .node path {
fill: var(--hover-related-color) !important; fill: var(--hover-related-color) !important;
stroke: var(--hover-related-color); stroke: var(--hover-related-color);
stroke-width: 5px; stroke-width: 2px;
} }
.node.linkedSelected path { .selectedNode .node:not(.linkedSelected) path {
fill: var(--hover-related-color) !important; fill: lightgray !important;
/* same as linkHover/linkedHover but without border */ /* same as linkHover/linkedHover but without border */
} }
@ -204,7 +209,7 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
.node:hover circle, .node:hover path, .node.selected path { .node:hover circle, .node:hover path, .node.selected path {
fill: var(--hover-color) !important; fill: var(--hover-color) !important;
stroke: var(--hover-color); stroke: var(--hover-color);
stroke-width: 5px; stroke-width: 2px;
} }
/* /*
.node:hover text { .node:hover text {
@ -212,7 +217,11 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
fill: var(--hover-color); fill: var(--hover-color);
} */ } */
.node.selected circle, .node.selected path {
/* .selectedNode .node circle, .selectedNode .node path {
fill: lightgray !important;
} */
.selectedNode .node.selected circle, .selectedNode .node.selected path {
fill: var(--selected-color) !important; fill: var(--selected-color) !important;
stroke: var(--selected-color); stroke: var(--selected-color);
stroke-width: 5px; stroke-width: 5px;
@ -245,7 +254,7 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
.node.Institution circle, .node.Institution path { .node.Institution circle, .node.Institution path {
/* fill: lightcoral; */ /* fill: lightcoral; */
fill: red; fill: #11F999;
} }
.node.Dataset circle, .node.Dataset path { .node.Dataset circle, .node.Dataset path {
@ -258,37 +267,37 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
} }
.node.Institution.Institution-company path{ .node.Institution.Institution-company path{
fill: blue; fill: #45F68A;
} }
.node.Institution.Institution-government path{ .node.Institution.Institution-government path{
fill: orange; fill: #60F37B;
} }
.node.Institution.Institution-local-government path{ .node.Institution.Institution-local-government path{
fill: purple; fill: #75F06D;
} }
.node.Institution.Institution-law-enforcement path{ .node.Institution.Institution-law-enforcement path{
fill: seagreen; fill: #87EC60;
} }
.node.Institution.Institution-ngo path{ .node.Institution.Institution-ngo path{
fill: yellow; fill: #97E853;
} }
.node.Institution.Institution-university path{ .node.Institution.Institution-university path{
fill: pink; fill: #A6E447;
} }
.node.Institution.Institution-research path{ .node.Institution.Institution-research path{
fill: fuchsia; fill: #B4E03C;
} }
.node.Institution.Institution-project path{ .node.Institution.Institution-project path{
fill: rgb(0, 255, 255); fill: #C1DB31;
} }
.node.Institution.Institution-watchdog path{ .node.Institution.Institution-watchdog path{
fill: rgb(145, 255, 0); fill: #CED628;
} }
.node.Institution.Institution-expert-group path{ .node.Institution.Institution-expert-group path{
fill: rgb(149, 87, 161); fill: #DAD121;
} }
.node.Institution.Institution-foundation path{ .node.Institution.Institution-foundation path{
fill: brown; fill: #E5CB1E;
} }
.node.Institution.Institution-international-organization path{ .node.Institution.Institution-international-organization path{
fill: gray; fill: gray;
@ -348,6 +357,13 @@ 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.link{
}
#tooltip .entity{
font-weight: bold;
}
#tooltip:not(.visible){ #tooltip:not(.visible){
position:absolute; position:absolute;

View File

@ -22,6 +22,13 @@ const CONFIG = {
"label": "Institution", "label": "Institution",
"type": "categories", "type": "categories",
}, },
// TODO: nested filters
// TODO:restructure, allow for groups of types:
// [12:08, 07-05-2021] Francesco Ragazzi: Government / Regional / local can be the same
// [12:08, 07-05-2021] Francesco Ragazzi: Also IO
// [12:08, 07-05-2021] Francesco Ragazzi: NGO, Foundation can be grouped
// [12:08, 07-05-2021] Francesco Ragazzi: University, Research, Expert group can be grouped
// [12:08, 07-05-2021] Francesco Ragazzi: Watchdog and can also be grouped with government
"Law Enforcement": { "Law Enforcement": {
// "label": "Institution", // "label": "Institution",
"type": "institution_types", "type": "institution_types",
@ -95,8 +102,53 @@ const CONFIG = {
"Software Deployed", "Software Deployed",
"Software Developer", "Software Developer",
"Dataset Developer", "Dataset Developer",
"Related Institutions",
"Involved Entities",
], ],
"link_labels": {
"Clients": {
"label": "is client of",
"swap": true,
},
"Managed by": {
"label": "manages",
"swap": true,
},
"Used by": {
"label": "uses",
"swap": true,
},
"Funded by": {
"label": "is funded by",
"swap": false,
},
"Provided by": {
"label": "provides",
"swap": true,
},
"Software Deployed": {
"label": "is used in",
"swap": true,
},
"Software Developer": {
"label": "is developed by",
"swap": false,
},
"Dataset Developer": {
"label": "develops dataset for",
"swap": true,
},
"Related Institutions": {
"label": "is related to",
"swap": true,
},
"Involved Entities": {
"label": "is involved in",
"swap": true,
},
},
"geo_properties": [ "geo_properties": [
"Geolocation", "Geolocation",
"City Coordinates", "City Coordinates",
@ -113,7 +165,7 @@ const CONFIG = {
"Country": ["Country"], "Country": ["Country"],
"City": ["City"], "City": ["City"],
// ["Deployment type"], // TODO: select this // ["Deployment type"], // TODO: select this
"Institution" : ["Institution Type"], // TODO: select this (local gov, etc.) "Institution": ["Institution Type"], // TODO: select this (local gov, etc.)
"Dataset": ["Datasets used"], "Dataset": ["Datasets used"],
"Company": ["Managed by", "Provided by", "Developped by (institutions)"], "Company": ["Managed by", "Provided by", "Developped by (institutions)"],
"Tech": ["Technologies Used", "Software Deployed"], "Tech": ["Technologies Used", "Software Deployed"],
@ -130,6 +182,17 @@ const CONFIG = {
] ]
}; };
function getLinkLabelConfig(linkName){
if(CONFIG.link_labels.hasOwnProperty(linkName)){
return CONFIG.link_labels[linkName];
}
return {
'label': linkName,
'swap': false,
}
}
// let width = window.innerWidth; // let width = window.innerWidth;
// let height = window.innerHeight; // let height = window.innerHeight;
@ -264,7 +327,7 @@ function getTitle(obj) {
function getCategories(obj) { function getCategories(obj) {
// console.log(obj); // console.log(obj);
let cats = obj.printouts['Category'].map(n => n.fulltext.split(':')[1]); let cats = obj.printouts['Category'].map(n => n.fulltext.split(':')[1]);
if(obj.printouts.hasOwnProperty("Institution Type") && obj.printouts['Institution Type'].length) { if (obj.printouts.hasOwnProperty("Institution Type") && obj.printouts['Institution Type'].length) {
obj.printouts['Institution Type'].forEach(type => { obj.printouts['Institution Type'].forEach(type => {
cats.push(getInstitutionClass(type.fulltext)); cats.push(getInstitutionClass(type.fulltext));
}); });
@ -272,7 +335,7 @@ function getCategories(obj) {
return cats; return cats;
} }
function getInstitutionClass(name) { function getInstitutionClass(name) {
return "Institution-"+ slugify(name); return "Institution-" + slugify(name);
} }
function getClasses(obj) { function getClasses(obj) {
const classes = getCategories(obj); const classes = getCategories(obj);
@ -339,7 +402,7 @@ class NodeMap {
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" ><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> this.svg.append('defs').html(`<marker markerHeight="3" markerWidth="3" 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="3" markerWidth="3" 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="3" markerWidth="3" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelectedRelated"><path d="M0,-3L8,0L0,3"></path></marker>
<!--Sketching:--> <!--Sketching:-->
<defs> <defs>
<filter id="tint"> <filter id="tint">
@ -358,7 +421,7 @@ class NodeMap {
</filter> </filter>
</defs>`); </defs>`);
this.svg.on('click', (e) => { console.log(e); this.deselectNode()}) this.svg.on('click', (e) => { console.log(e); this.deselectNode() })
// const noise = 0.001; // const noise = 0.001;
// this.svg.append('defs').append('filter').attr('id', 'splotch').html( `${ // this.svg.append('defs').append('filter').attr('id', 'splotch').html( `${
@ -421,7 +484,7 @@ class NodeMap {
return "country"; return "country";
}) })
.attr("d", this.proj) .attr("d", this.proj)
// .attr("filter", 'url(#splotch)') // .attr("filter", 'url(#splotch)')
// .attr("fill", ); // .attr("fill", );
this.g_borders this.g_borders
@ -443,6 +506,7 @@ class NodeMap {
if (zoomTimeout) { if (zoomTimeout) {
clearTimeout(zoomTimeout) clearTimeout(zoomTimeout)
} }
document.querySelector(':root').style.setProperty('--zoom', evt.transform.k);
zoomTimeout = setTimeout(() => { zoomTimeout = setTimeout(() => {
this.g_nodes.attr('style', `font-size:${22000 / this.height / evt.transform.k}pt`) this.g_nodes.attr('style', `font-size:${22000 / this.height / evt.transform.k}pt`)
setTimeout(() => { setTimeout(() => {
@ -517,7 +581,6 @@ class NodeMap {
this.graph.nodes.forEach((d) => { this.graph.nodes.forEach((d) => {
for (const prop of CONFIG.geo_properties) { for (const prop of CONFIG.geo_properties) {
// console.log(this,d.printouts, prop) // console.log(this,d.printouts, prop)
if (d.printouts[prop].length) { if (d.printouts[prop].length) {
// console.log("fix node", d); // console.log("fix node", d);
@ -667,8 +730,15 @@ class NodeMap {
} }
showTooltip(el, node, links) { showTooltip(el, node, links) {
if(el.tagName != 'path'){
let parentEl = el.parentNode;
if(parentEl.tagName != 'g'){
parentEl = parentEl.parentNode;
}
el = parentEl.querySelector('path');
}
// 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'>${getCategories(node)[0]}</span>
<h3>${node.fulltext}</h3> <h3>${node.fulltext}</h3>
@ -685,11 +755,46 @@ class NodeMap {
this.tooltipEl.style.left = (rect.left + rect.width / 2 - rectTT.width / 2) + 'px'; this.tooltipEl.style.left = (rect.left + rect.width / 2 - rectTT.width / 2) + 'px';
// console.log(el, node, rect.top); // console.log(el, node, rect.top);
this.tooltipEl.classList.add('visible'); this.tooltipEl.classList.add('visible', 'node');
}
showRelationTooltip(link, evt) {
const {label, swap} = getLinkLabelConfig(link.name);
if(swap){
this.tooltipEl.innerHTML = `
<span class='relation'>
<span class='entity'>${link.target.fulltext}</span>
${label}
<span class='entity'>${link.source.fulltext}</span>
</span>
`;
} else {
this.tooltipEl.innerHTML = `
<span class='relation'>
<span class='entity'>${link.source.fulltext}</span>
${label}
<span class='entity'>${link.target.fulltext}</span>
</span>
`;
}
const rectTT = this.tooltipEl.getBoundingClientRect();
this.trackerEv = (evt) => {
this.tooltipEl.style.top = (evt.clientY - rectTT.height - 10) + 'px';
this.tooltipEl.style.left = (evt.clientX - rectTT.width / 2) + 'px';
};
window.addEventListener('mousemove', this.trackerEv);
this.tooltipEl.classList.add('visible', 'link');
} }
hideTooltip() { hideTooltip() {
this.tooltipEl.classList.remove('visible'); this.tooltipEl.classList.remove('visible', 'node', 'link');
if(this.trackerEv){
window.removeEventListener('mousemove', this.trackerEv);
this.trackerEv = null;
}
} }
selectNode(node) { selectNode(node) {
@ -713,8 +818,8 @@ class NodeMap {
document.getElementById(node.id).classList.add('selected'); document.getElementById(node.id).classList.add('selected');
connectedNodes.forEach(n => document.getElementById(n.id).classList.add('linkedSelected')); connectedNodes.forEach(n => document.getElementById(n.id).classList.add('linkedSelected'));
links.forEach(l => document.getElementById(getLinkId(l)).classList.add('linkedSelected')); links.forEach(l => document.getElementById(getLinkId(l)).classList.add('linkedSelected'));
this.container.classed('selectedNode', true);
// TODO: show details; // TODO: show details;
// alert('not yet implemented'); // alert('not yet implemented');
@ -730,6 +835,7 @@ class NodeMap {
while (els.length) { while (els.length) {
els[0].classList.remove('linkedSelected'); els[0].classList.remove('linkedSelected');
} }
this.container.classed('selectedNode', false);
} }
update() { update() {
@ -779,7 +885,10 @@ class NodeMap {
.attr('d', (n) => { .attr('d', (n) => {
return getSymbolForNode(n)(n); return getSymbolForNode(n)(n);
}) })
var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "3").attr('x', 5); var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "4").attr('x', 5);
// nodeTitle.on('mouseover', (evt, n) =>{
// console.log(evt,n)
// });
nodeTitle nodeTitle
.each(function (node, i, nodes) { .each(function (node, i, nodes) {
var textLength = void 0; var textLength = void 0;
@ -790,7 +899,7 @@ class NodeMap {
titleTexts = splitText(titleText); titleTexts = splitText(titleText);
} }
if (titleTexts !== false) { if (titleTexts !== false) {
const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "3").attr("x", "5"); const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "4").attr("x", "5");
const tspan = self.append("tspan").text(titleTexts[1]).attr("dy", "1em").attr("x", "5"); 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();
@ -873,8 +982,8 @@ class NodeMap {
group.append("path") group.append("path")
.attr("marker-end", "url(#arrowHead)") .attr("marker-end", "url(#arrowHead)")
.attr('id', (d, i) => 'linkpath_' + i) .attr('id', (d, i) => 'linkpath_' + i)
.on("mouseover", function (ev, link) { .on("mouseover", (ev, link) => {
d3.select(this).classed('hover', true); d3.select(ev.target).classed('hover', true);
const nodes = document.getElementsByClassName('node'); const nodes = document.getElementsByClassName('node');
for (let n of nodes) { for (let n of nodes) {
const d = d3.select(n).datum(); const d = d3.select(n).datum();
@ -882,9 +991,13 @@ class NodeMap {
n.classList.add('linkHover'); n.classList.add('linkHover');
} }
} }
this.showRelationTooltip(link, ev);
// console.log(l); // console.log(l);
}).on("mouseout", function (ev, link) { }).on("mouseout", (ev, link) => {
d3.select(this).classed('hover', false); this.hideTooltip();
d3.select(ev.target).classed('hover', false);
const nodes = document.getElementsByClassName('linkHover'); const nodes = document.getElementsByClassName('linkHover');
while (nodes.length) { while (nodes.length) {
nodes[0].classList.remove('linkHover'); nodes[0].classList.remove('linkHover');
@ -1120,7 +1233,7 @@ class AlluvialMap {
render() { render() {
this.svg = this.root.append('svg') this.svg = this.root.append('svg')
this.resize(); this.resize();
this.sankey = d3.sankey() this.sankey = d3.sankey()
@ -1581,9 +1694,9 @@ class Store {
} }
isFiltered(node) { isFiltered(node) {
if(this.filters.categories.includes(node.printouts['Category'][0].fulltext.split(':')[1])) if (this.filters.categories.includes(node.printouts['Category'][0].fulltext.split(':')[1]))
return true; return true;
if(node.printouts['Institution Type'].length && this.filters.institution_types.includes(node.printouts['Institution Type'][0].fulltext)) if (node.printouts['Institution Type'].length && this.filters.institution_types.includes(node.printouts['Institution Type'][0].fulltext))
return true; return true;
return false; return false;
} }
@ -1634,14 +1747,14 @@ class Store {
Object.keys(CONFIG.filters).forEach(f => { Object.keys(CONFIG.filters).forEach(f => {
const settings = CONFIG.filters[f]; const settings = CONFIG.filters[f];
if(settings.type == 'institution_types') if (settings.type == 'institution_types')
// TODO; For now, skip // TODO; For now, skip
return; return;
let categories = [f]; let categories = [f];
if( settings.type == 'institution_types') if (settings.type == 'institution_types')
categories = ['Institution', getInstitutionClass(f)]; categories = ['Institution', getInstitutionClass(f)];
let labelEl = document.createElement('label') let labelEl = document.createElement('label')
let inputEl = document.createElement('input') let inputEl = document.createElement('input')
let textEl = document.createElement('span'); let textEl = document.createElement('span');

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" href="graph.css"> <link rel="stylesheet" href="graph.css">
<title>Remote Biometric Identification | A survey of the European Union</title>
</head> </head>
<body> <body>
@ -18,7 +18,7 @@
<aside id="filters"> <aside id="filters">
<h3>Legend</h3> <h3>Filter</h3>
</aside> </aside>
</header> </header>