From 17d5584eb791244a65588c1b2767e32e317f09be Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Fri, 16 Apr 2021 11:15:57 +0200 Subject: [PATCH] WIP messy graph --- index.html | 2 +- www/graph.css | 8 ++ www/graph.js | 369 ++++++++++++++++++++++++++++++++----------------- www/index.html | 6 +- 4 files changed, 256 insertions(+), 129 deletions(-) diff --git a/index.html b/index.html index 23ec9e2..42bd4a5 100644 --- a/index.html +++ b/index.html @@ -169,7 +169,7 @@ - + \ No newline at end of file diff --git a/www/graph.css b/www/graph.css index 97eb88b..c2fa212 100644 --- a/www/graph.css +++ b/www/graph.css @@ -148,4 +148,12 @@ a, a:link{ } a:hover{ text-decoration: underline; +} + +#filters{ + position: fixed; + left: 0; + top: 0; + background: white; + padding: 10px; } \ No newline at end of file diff --git a/www/graph.js b/www/graph.js index 2a83bb5..c0266a4 100644 --- a/www/graph.js +++ b/www/graph.js @@ -7,12 +7,13 @@ const CONFIG = { 'labels': { 'rotate': true, }, - 'countries': ['Austria', 'Italy', 'Belgium', 'Latvia', 'Bulgaria', 'Lithuania', 'Croatia', 'Luxembourg', 'Cyprus', 'Malta', 'Czechia', 'Netherlands', 'Denmark', 'Poland', 'Estonia ', 'Portugal', 'Finland ', 'Romania', 'France', 'Slovakia', 'Germany', 'Slovenia', 'Greece', 'Spain', 'Hungary', 'Sweden', 'Ireland'], + 'countries': ['Austria', 'Italy', 'Belgium', 'Latvia', 'Bulgaria', 'Lithuania', 'Croatia', 'Luxembourg', 'Cyprus', 'Malta', 'Czechia', 'Netherlands', 'Denmark', 'Poland', 'Estonia', 'Portugal', 'Finland ', 'Romania', 'France', 'Slovakia', 'Germany', 'Slovenia', 'Greece', 'Spain', 'Hungary', 'Sweden', 'Ireland'], 'eu': { 'lonMin': -10, 'lonMax': 35, 'center': [11, 47], - } + }, + "filters": ["Institution", "Deployments", "Technology", "Dataset"], }; @@ -20,6 +21,41 @@ const CONFIG = { var graph, store; +var filteredCategories = [];//= ["Institution"]; + +CONFIG.filters.forEach(f => { + let labelEl = document.createElement('label') + let inputEl = document.createElement('input') + let textEl = document.createElement('span'); + inputEl.type = "checkbox"; + textEl.innerText = f; + labelEl.appendChild(inputEl); + labelEl.appendChild(textEl); + + if(!filteredCategories.includes(f)) { + inputEl.checked = true; + } + + inputEl.addEventListener('change', function(e){ + if(e.target.checked) { + filteredCategories.forEach((d, i) => { + if(d == f) { + filteredCategories.splice(i, 1); + } + }); + } else { + if(!filteredCategories.includes(f)) { + filteredCategories.push(f); + } + } + filter(); + update(); + }) + + document.getElementById('filters').appendChild(labelEl); +}) + + function getSizeForNode(node) { // if (node.hasOwnProperty('https://schema.org/thumbnailUrl')) return nodeSize; // if (weights[node['@id']]) return nodeSize * weights[node['@id']]; @@ -130,12 +166,12 @@ const svg = d3.select("svg") // SET UP MAP: const projection = d3.geoHill() .rotate([-12, 0, 0]) - // .translate([width / 2, height * 1.5]) - // .scale(height * 1.5); + .translate([width / 2, height * 1.5]) + .scale(height * 1.5); const proj = d3.geoPath().projection(projection); const graticule = d3.geoGraticule10(); -let euCenter; +const euCenter = projection(CONFIG.eu.center); const container = svg.append("g").attr("id", "container"); // container.append("circle").attr("cx", euCenter[0]).attr("cy", euCenter[1]).attr("r", 500).attr("fill","red") @@ -146,6 +182,7 @@ const g_graticule = container.append("g") .append('path') .attr("class", "graticule") .attr("fill", "none") + .attr('d', proj(graticule)) .attr("stroke-width", "!px") .attr("stroke", (n) => { return "lightgray"; @@ -160,17 +197,32 @@ function sizeWindow() { .attr("viewBox", [0, 0, width, height]) .attr("width", width) .attr("height", height); - projection - .translate([width / 2, height * 1.5]) - .scale(height * 1.5); - - g_graticule.attr('d', proj(graticule)) - euCenter = projection(CONFIG.eu.center); + + // + // // update(); } sizeWindow() window.addEventListener('resize', sizeWindow); +svg.call(d3.zoom().scaleExtent([0.3, 8]).on("start", function () { + svg.node().classList.add("dragging"); +}).on("end", function () { + svg.node().classList.remove("dragging"); +}).on("zoom", function ({ transform }) { + container.attr("transform", transform); +})); + + + +var node = container.append("g") + .attr('class', 'nodes') + .selectAll(".node"); +var link = container.append("g") + .attr('class', 'links') + .selectAll(".link"); +// let linkLines = link.selectAll('line'); + // REQUEST ATLAS & GRAPH const req_data = new Request(CONFIG.dataUrl, { method: 'GET' }); @@ -224,7 +276,7 @@ const simulation = d3.forceSimulation() .force("collision", d3.forceCollide(function (d) { return getSizeForNode(d) * 1.5; // avoid overlapping nodes })) - .force("outsideEu", d3.forceRadial(500, euCenter[0], euCenter[1]) + .force("outsideEu", d3.forceRadial(height/2.7, euCenter[0], euCenter[1]) .strength(function (node, idx) { // return 1; if (store.nodesBorderingEu.indexOf(node) !== -1) { @@ -245,10 +297,11 @@ function buildGraph(data, world) { console.log('all nodes', nodes); const nodeMap = Object.fromEntries(nodes.map(d => [d['@id'], d])); const links = data.links.filter(l => nodeMap[l.source] && nodeMap[l.target]).map(d => Object.create(d)); + console.log(nodeMap, links); graph = { - "nodes": [...nodes], - "links": [...links], + "nodes": [], + "links": [], } store = { "nodes": [...nodes], @@ -256,12 +309,14 @@ function buildGraph(data, world) { "nodesBorderingEu": links.filter(l => l.name == 'City' && (nodeMap[l.target].lon < CONFIG.eu.lonMin || nodeMap[l.target].lon > CONFIG.eu.lonMax)).map(l => nodeMap[l.source]), } + filter(); + // the .source and .target attributes are still ID's. Only after initialisation of the force are they replaced with their representative objects const nodesEastOfEu = links.filter(l => l.name == 'City' && (nodeMap[l.target].lon < CONFIG.eu.lonMin)).map(l => nodeMap[l.source]); - g_countries.selectAll("path") + var c = g_countries.selectAll("path") .data(countries) .enter() .append("path") @@ -271,12 +326,10 @@ function buildGraph(data, world) { if (CONFIG.countries.indexOf(n.properties.name) !== -1) { return ''; } - return "lightgray"; + return "rgba(200,200,200,.7)"; }); - g_borders//.selectAll("path") - // .data(borders) - // .enter() + g_borders .append("path") .attr("class", "borders") .attr("d", proj(borders)) @@ -286,23 +339,6 @@ function buildGraph(data, world) { return "white"; }); - - const link = container.append("g") - .attr('class', 'links') - .selectAll(".link") - .data(links) - .join("g") - .attr("class", "link") - ; - // link.exit().remove(); - - const linkLine = link - .append("line"). - attr("marker-end", "url(#arrowHead)"); - const linkText = link.filter((l) => l.name != "City").append("text").text(function (l) { - return l.name; - }); - nodes.forEach(function (d) { d.x = euCenter[0]; d.y = euCenter[1]; @@ -327,81 +363,103 @@ function buildGraph(data, world) { d.fy = p[1]; } }) - const node = container.append("g") - .attr('class', 'nodes') - .selectAll(".node") - .data(nodes) - .join("g") - .attr('class', getClasses) - .call(drag(simulation)) - .on("click", (evt, n) => selectNode(evt, n, node)) - ; + + update(); + return svg.node(); +} - node - .append('circle') - .attr("r", getSizeForNode) - // .call(drag(simulation)); +function update() { - var nodeTitle = node.append('text').attr("class", "nodeTitle").attr("y", "5"); - nodeTitle - .each(function (node, nodes) { - var textLength = void 0; - const self = d3.select(this); - const titleText = getTitle(node); - var titleTexts = false; - if (titleText.length > 20) { - titleTexts = splitText(titleText); - } - if (titleTexts !== false) { - const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "-10").attr("x", "0"); - const tspan = self.append("tspan").text(titleTexts[1]).attr("y", "10").attr("x", "0"); - const textLength1 = tspan.node().getComputedTextLength(); - const textLength2 = tspan.node().getComputedTextLength(); - textLength = Math.max(textLength1, textLength2); - } else { - self.text(titleText); - textLength = self.node().getComputedTextLength(); - } - // scale according to text length: - if (textLength > getSizeForNode(node) * 2) { - self.attr('transform', 'scale(' + getSizeForNode(node) * 2 / textLength / 1.05 + ')'); - } + + // see also: https://www.createwithdata.com/enter-exit-with-d3-join/ + node = node.data(graph.nodes, d => d.id) + .join(function (enter) { + let group = enter.append("g").attr("class", getClasses); + group.call(drag(simulation)); + group.on("click", (evt, n) => selectNode(evt, n, node)); + group.append('circle').attr("r", getSizeForNode); + var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "5"); + nodeTitle + .each(function (node, nodes) { + var textLength = void 0; + const self = d3.select(this); + const titleText = getTitle(node); + var titleTexts = false; + if (titleText.length > 20) { + titleTexts = splitText(titleText); + } + if (titleTexts !== false) { + const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "-10").attr("x", "0"); + const tspan = self.append("tspan").text(titleTexts[1]).attr("y", "10").attr("x", "0"); + const textLength1 = tspan.node().getComputedTextLength(); + const textLength2 = tspan.node().getComputedTextLength(); + textLength = Math.max(textLength1, textLength2); + } else { + self.text(titleText); + textLength = self.node().getComputedTextLength(); + } + // scale according to text length: + if (textLength > getSizeForNode(node) * 2) { + self.attr('transform', 'scale(' + getSizeForNode(node) * 2 / textLength / 1.05 + ')'); + } + }); + return group; }); - // node.append("title") - // .text(d => d['@id']); + // let linkText; + // let linkLine; + link = link + .data(graph.links) + .join( + enter => { + console.log(enter); + let group = enter.append("g").attr("class", "link"); + group.append("line").attr("marker-end", "url(#arrowHead)").attr('id', (d,i) => 'linkid_' + i); + group.filter((l) => l.name != "City").append("text").attr("class", "labelText").text(function (l) { + return l.name; + }); + // group.append("text") + // .attr("class", "labelText") + // .attr("dx", 20) + // .attr("dy", 0) + // .style("fill", "red") + // .append("textPath") + // .attr("xlink:href", function (d, i) { return "#linkid_" + i; }) + // .attr("startOffset","50%") + // .text((d,i) => d.name ); + return group; + } + ) + ; + // let linkEnter = link.enter() + // let linkLine = link.selectAll('line'); + // let newLinks = link.enter().append("g").attr("class","link"); + // newLinks.append("line").attr("marker-end", "url(#arrowHead)"); + // newLinks.filter((l) => l.name != "City").append("text").text(function (l) { + // return l.name; + // }); - svg.call(d3.zoom().scaleExtent([0.3, 6]).on("start", function () { - svg.node().classList.add("dragging"); - }).on("end", function () { - svg.node().classList.remove("dragging"); - }).on("zoom", function ({ transform }) { - container.attr("transform", transform); - })); + // link.exit().remove(); + // let linkLine = link.selectAll('line'); + // let linkText = link.selectAll('text'); + // // }) function(update){ return update }, function(exit) { + // // exit.remove(); + // // }) + // ; + + // console.log(link, linkText, linkLine) + + simulation.nodes(graph.nodes); simulation.on("tick", () => { + // console.log('t', link._groups[0].length); - data.nodes.forEach(function (d, idx) { - d.leftX = d.rightX = d.x; - // fix first node on center - // if(idx === 0) { - // d.fx = width/2; - // d.fy = height/2; - // return; - // } - }); - link - .attr("x1", d => d.source.x) - .attr("y1", d => d.source.y) - .attr("x2", d => d.target.x) - .attr("y2", d => d.target.y); - - linkLine.each(function (d) { - var sourceX, targetX, midX, dx, dy, angle; + link.each(function (d) { + let sourceX, targetX, midX, dx, dy, angle; // This mess makes the arrows exactly perfect. // thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4 @@ -446,7 +504,31 @@ function buildGraph(data, world) { d.targetX = targetX - Math.sin(angle) * tgtSize; d.sourceY = d.source.y + Math.cos(angle) * srcSize; d.targetY = d.target.y - Math.cos(angle) * tgtSize; - }).attr("x1", function (d) { + + let rel = d3.select(this); + rel.select('line') + .attr("x1", d.sourceX) + .attr("y1", d.sourceY) + .attr("x2", d.targetX) + .attr("y2", d.targetY) + + rel.select('text') + .attr("transform", function (d) { + const dx = (d.target.x - d.source.x) / 2; + const dy = (d.target.y - d.source.y) / 2; + const x = d.source.x + dx; + const y = d.source.y + dy; + const deg = Math.atan(dy / dx) * 180 / Math.PI; + // if dx/dy == 0/0 -> deg == NaN + if (isNaN(deg)) { + return ""; + } + // return ""; + return "translate(" + x + " " + y + ") rotate(" + (CONFIG.labels.rotate ? deg : 0) + ")"; + }); + + + })/*.attr("x1", function (d) { return d.sourceX; }).attr("y1", function (d) { return d.sourceY; @@ -454,34 +536,13 @@ function buildGraph(data, world) { return d.targetX; }).attr("y2", function (d) { return d.targetY; - }); - linkText.attr("transform", function (d) { - const dx = (d.target.x - d.source.x) / 2; - const dy = (d.target.y - d.source.y) / 2; - const x = d.source.x + dx; - const y = d.source.y + dy; - const deg = Math.atan(dy / dx) * 180 / Math.PI; - // if dx/dy == 0/0 -> deg == NaN - if (isNaN(deg)) { - return ""; - } - // return ""; - return "translate(" + x + " " + y + ") rotate(" + (CONFIG.labels.rotate ? deg : 0) + ")"; - }); + });*/ + // linkText node .attr("transform", d => `translate(${d.x}, ${d.y})`); }); - update(); - - - return svg.node(); -} - -function update() { - simulation.nodes(graph.nodes) - // .on("tick", ticked) simulation.force("link") .links(graph.links); @@ -493,6 +554,55 @@ function update() { } } +} + +// filter function +function filter() { + // add and remove nodes from data based on type filters + store.nodes.forEach(function (n) { + const cats = getCategories(n); + if (cats.every(c => filteredCategories.includes(c))) { + // hide + graph.nodes.forEach(function (d, i) { + if (n['@id'] === d['@id']) { + graph.nodes.splice(i, 1); + } + }); + } else { + // add back + if (!graph.nodes.includes(n)) { + graph.nodes.push(n); + } + } + + }); + + // add and remove links from data based on availability of nodes + let checkOn; + if(typeof store.links[0].source !== 'object') { + checkOn = graph.nodes.map(n => n['@id']) + } else { + checkOn = graph.nodes + } + + store.links.forEach(function (l) { + + if (!checkOn.includes(l.source) || !checkOn.includes(l.target)) { + // hide + console.log('hide!') + graph.links.forEach(function (d, i) { + if (l === d) { + graph.links.splice(i, 1); + } + }); + + } else if (!graph.links.includes(l)) { + graph.links[graph.links.length] = l; + // console.log('add', l, graph.links) + } + + }); + // console.log(graph.links) } @@ -544,7 +654,12 @@ function selectNode(evt, node, d3Node) { } -document.getElementById('closeInfo').addEventListener('click', (evt) => { - document.querySelectorAll('svg .node').forEach(n => n.classList.remove('selected')); - document.getElementById('nodeInfo').classList.add('hidden'); -}) +const closeEl = document.getElementById('closeInfo'); +if(closeEl) { + + document.getElementById('closeInfo').addEventListener('click', (evt) => { + document.querySelectorAll('svg .node').forEach(n => n.classList.remove('selected')); + document.getElementById('nodeInfo').classList.add('hidden'); + }) + +} \ No newline at end of file diff --git a/www/index.html b/www/index.html index e370166..757806e 100644 --- a/www/index.html +++ b/www/index.html @@ -15,10 +15,14 @@ -