From 1650efae49f86d9c1952f0bc4862c464f365d87f Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Thu, 12 Aug 2021 09:00:05 +0200 Subject: [PATCH] Relation tooltip, filter, page title --- www/graph.css | 76 ++++++++++++++--------- www/graph.js | 161 +++++++++++++++++++++++++++++++++++++++++-------- www/index.html | 4 +- 3 files changed, 185 insertions(+), 56 deletions(-) diff --git a/www/graph.css b/www/graph.css index ac71581..c4cad0f 100644 --- a/www/graph.css +++ b/www/graph.css @@ -22,11 +22,13 @@ --selected-color: var(--color1); --link-color: rgba(255,255,255,0.5); --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); --body-back: #96a7b7; /*#9cb3c9; /*#8195a7; /*#9cb3c9; #b9cada*/ --title-color: #1c1c1c; + + --zoom: 1; /* to be overriden by js */ } body { @@ -63,33 +65,33 @@ svg .links line, svg .links path { /* stroke: #f3722c; */ /* stroke: #9df32c; */ stroke: var(--link-color); - stroke-width: 6; + stroke-width: calc(10px / var(--zoom)); fill: none; - transition: stroke-width 1s; + /* transition: stroke-width 1s; */ cursor: pointer; } svg .links line.hover, svg .links path.hover { - stroke: var(--link-hover-color); + stroke: var(--link-hover-color) !important; /* stroke-width: 12; */ - marker-end: url(#arrowHeadSelected); + marker-end: url(#arrowHeadSelected) !important; } svg .links .linkedHover path{ stroke: var(--link-hover-related-color); - stroke-width: 12; + stroke-width: calc(12px / var(--zoom)); marker-end: url(#arrowHeadSelectedRelated); } svg .links .linkedSelected path{ - stroke: var(--link-focus-color); - stroke-width: 12; - marker-end: url(#arrowHeadSelected); + stroke: var(--link-hover-related-color); + stroke-width: calc(12px / var(--zoom)); + marker-end: url(#arrowHeadSelectedRelated); } -svg.zoomed .links line, svg.zoomed .links path { +/* svg.zoomed .links line, svg.zoomed .links path { stroke-width: 2; -} +} */ /* svg.zoomed .links line, svg.zoomed .links path.hover { stroke-width: 4; @@ -154,16 +156,18 @@ svg #header text#subtitle { cursor: pointer; } + .node text.nodeTitle { text-anchor: start; dominant-baseline: hanging; + font-size: calc(20pt / var(--zoom)) !important; /*achieves a 'text-anchor: top'*/ /* font-size: 16pt; */ /*Set this in JS*/ - transition: font-size .4s, opacity 1s; + transition: opacity 1s; fill: #5d5d5f; /*also when hovering node*/ opacity: 1; - pointer-events: none; + /* pointer-events: none; */ /*prevent mouse glitches*/ } @@ -171,6 +175,7 @@ svg #header text#subtitle { .node text.nodeTitle.overlapping { /* used to be shown on hover, but disabled now that we have a tooltip */ opacity: 0; + pointer-events: none; } 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 { fill: var(--hover-related-color) !important; stroke: var(--hover-related-color); - stroke-width: 5px; + stroke-width: 2px; } -.node.linkedSelected path { - fill: var(--hover-related-color) !important; +.selectedNode .node:not(.linkedSelected) path { + fill: lightgray !important; /* 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 { fill: var(--hover-color) !important; stroke: var(--hover-color); - stroke-width: 5px; + stroke-width: 2px; } /* .node:hover text { @@ -212,7 +217,11 @@ svg.zoomed.zoomed2 .node text.nodeTitle { 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; stroke: var(--selected-color); stroke-width: 5px; @@ -245,7 +254,7 @@ svg.zoomed.zoomed2 .node text.nodeTitle { .node.Institution circle, .node.Institution path { /* fill: lightcoral; */ - fill: red; + fill: #11F999; } .node.Dataset circle, .node.Dataset path { @@ -258,37 +267,37 @@ svg.zoomed.zoomed2 .node text.nodeTitle { } .node.Institution.Institution-company path{ - fill: blue; + fill: #45F68A; } .node.Institution.Institution-government path{ - fill: orange; + fill: #60F37B; } .node.Institution.Institution-local-government path{ - fill: purple; + fill: #75F06D; } .node.Institution.Institution-law-enforcement path{ - fill: seagreen; + fill: #87EC60; } .node.Institution.Institution-ngo path{ - fill: yellow; + fill: #97E853; } .node.Institution.Institution-university path{ - fill: pink; + fill: #A6E447; } .node.Institution.Institution-research path{ - fill: fuchsia; + fill: #B4E03C; } .node.Institution.Institution-project path{ - fill: rgb(0, 255, 255); + fill: #C1DB31; } .node.Institution.Institution-watchdog path{ - fill: rgb(145, 255, 0); + fill: #CED628; } .node.Institution.Institution-expert-group path{ - fill: rgb(149, 87, 161); + fill: #DAD121; } .node.Institution.Institution-foundation path{ - fill: brown; + fill: #E5CB1E; } .node.Institution.Institution-international-organization path{ fill: gray; @@ -348,6 +357,13 @@ svg.zoomed.zoomed2 .node text.nodeTitle { border-radius: 5px; box-shadow: 2px 2px 5px rgba(0, 0, 0, .5); } +#tooltip.link{ + +} + +#tooltip .entity{ + font-weight: bold; +} #tooltip:not(.visible){ position:absolute; diff --git a/www/graph.js b/www/graph.js index 07302cb..e025dd0 100644 --- a/www/graph.js +++ b/www/graph.js @@ -22,6 +22,13 @@ const CONFIG = { "label": "Institution", "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": { // "label": "Institution", "type": "institution_types", @@ -95,8 +102,53 @@ const CONFIG = { "Software Deployed", "Software 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": [ "Geolocation", "City Coordinates", @@ -113,7 +165,7 @@ const CONFIG = { "Country": ["Country"], "City": ["City"], // ["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"], "Company": ["Managed by", "Provided by", "Developped by (institutions)"], "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 height = window.innerHeight; @@ -264,7 +327,7 @@ function getTitle(obj) { function getCategories(obj) { // console.log(obj); 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 => { cats.push(getInstitutionClass(type.fulltext)); }); @@ -272,7 +335,7 @@ function getCategories(obj) { return cats; } function getInstitutionClass(name) { - return "Institution-"+ slugify(name); + return "Institution-" + slugify(name); } function getClasses(obj) { const classes = getCategories(obj); @@ -339,7 +402,7 @@ class NodeMap { render() { this.svg = this.root.append('svg') - this.svg.append('defs').html(` + this.svg.append('defs').html(` @@ -358,7 +421,7 @@ class NodeMap { `); - this.svg.on('click', (e) => { console.log(e); this.deselectNode()}) + this.svg.on('click', (e) => { console.log(e); this.deselectNode() }) // const noise = 0.001; // this.svg.append('defs').append('filter').attr('id', 'splotch').html( `${ @@ -421,7 +484,7 @@ class NodeMap { return "country"; }) .attr("d", this.proj) - // .attr("filter", 'url(#splotch)') + // .attr("filter", 'url(#splotch)') // .attr("fill", ); this.g_borders @@ -443,6 +506,7 @@ class NodeMap { if (zoomTimeout) { clearTimeout(zoomTimeout) } + document.querySelector(':root').style.setProperty('--zoom', evt.transform.k); zoomTimeout = setTimeout(() => { this.g_nodes.attr('style', `font-size:${22000 / this.height / evt.transform.k}pt`) setTimeout(() => { @@ -517,7 +581,6 @@ class NodeMap { this.graph.nodes.forEach((d) => { for (const prop of CONFIG.geo_properties) { - // console.log(this,d.printouts, prop) if (d.printouts[prop].length) { // console.log("fix node", d); @@ -667,8 +730,15 @@ class NodeMap { } 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) - this.tooltipEl.innerHTML = ` ${getCategories(node)[0]}

${node.fulltext}

@@ -685,11 +755,46 @@ class NodeMap { this.tooltipEl.style.left = (rect.left + rect.width / 2 - rectTT.width / 2) + 'px'; // 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 = ` + + ${link.target.fulltext} + ${label} + ${link.source.fulltext} + + `; + } else { + this.tooltipEl.innerHTML = ` + + ${link.source.fulltext} + ${label} + ${link.target.fulltext} + + `; + } + + 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() { - 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) { @@ -713,8 +818,8 @@ class NodeMap { 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')); - + this.container.classed('selectedNode', true); // TODO: show details; // alert('not yet implemented'); @@ -730,6 +835,7 @@ class NodeMap { while (els.length) { els[0].classList.remove('linkedSelected'); } + this.container.classed('selectedNode', false); } update() { @@ -779,7 +885,10 @@ class NodeMap { .attr('d', (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 .each(function (node, i, nodes) { var textLength = void 0; @@ -790,7 +899,7 @@ class NodeMap { titleTexts = splitText(titleText); } 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 textLength1 = tspan.node().getComputedTextLength(); // const textLength2 = tspan.node().getComputedTextLength(); @@ -873,8 +982,8 @@ class NodeMap { group.append("path") .attr("marker-end", "url(#arrowHead)") .attr('id', (d, i) => 'linkpath_' + i) - .on("mouseover", function (ev, link) { - d3.select(this).classed('hover', true); + .on("mouseover", (ev, link) => { + d3.select(ev.target).classed('hover', true); const nodes = document.getElementsByClassName('node'); for (let n of nodes) { const d = d3.select(n).datum(); @@ -882,9 +991,13 @@ class NodeMap { n.classList.add('linkHover'); } } + + this.showRelationTooltip(link, ev); + // console.log(l); - }).on("mouseout", function (ev, link) { - d3.select(this).classed('hover', false); + }).on("mouseout", (ev, link) => { + this.hideTooltip(); + d3.select(ev.target).classed('hover', false); const nodes = document.getElementsByClassName('linkHover'); while (nodes.length) { nodes[0].classList.remove('linkHover'); @@ -1120,7 +1233,7 @@ class AlluvialMap { render() { this.svg = this.root.append('svg') - + this.resize(); this.sankey = d3.sankey() @@ -1581,9 +1694,9 @@ class Store { } 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; - 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 false; } @@ -1634,14 +1747,14 @@ class Store { Object.keys(CONFIG.filters).forEach(f => { const settings = CONFIG.filters[f]; - if(settings.type == 'institution_types') - // TODO; For now, skip + if (settings.type == 'institution_types') + // TODO; For now, skip return; let categories = [f]; - if( settings.type == 'institution_types') + if (settings.type == 'institution_types') categories = ['Institution', getInstitutionClass(f)]; - + let labelEl = document.createElement('label') let inputEl = document.createElement('input') let textEl = document.createElement('span'); diff --git a/www/index.html b/www/index.html index 2740b6e..c5a2db1 100644 --- a/www/index.html +++ b/www/index.html @@ -3,7 +3,7 @@ - + Remote Biometric Identification | A survey of the European Union @@ -18,7 +18,7 @@