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);
--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;

View file

@ -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(`<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:-->
<defs>
<filter id="tint">
@ -358,7 +421,7 @@ class NodeMap {
</filter>
</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;
// 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 = `
<span class='category'>${getCategories(node)[0]}</span>
<h3>${node.fulltext}</h3>
@ -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 = `
<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() {
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');

View file

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