From 13f0a83475ba11f7bbde20cba12ef3fe920113a5 Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Wed, 31 Mar 2021 17:02:00 +0200 Subject: [PATCH] Refactoring --- www/graph.js | 294 ++++++++++++++++++++++----------------------------- 1 file changed, 128 insertions(+), 166 deletions(-) diff --git a/www/graph.js b/www/graph.js index ba09440..2a83bb5 100644 --- a/www/graph.js +++ b/www/graph.js @@ -10,11 +10,16 @@ const CONFIG = { '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 + 'lonMax': 35, + 'center': [11, 47], } }; +// thanks to https://bl.ocks.org/denisemauldin/cdd667cbaf7b45d600a634c8ae32fae5 +var graph, store; + + function getSizeForNode(node) { // if (node.hasOwnProperty('https://schema.org/thumbnailUrl')) return nodeSize; // if (weights[node['@id']]) return nodeSize * weights[node['@id']]; @@ -119,10 +124,57 @@ function getUrl(obj) { let width = window.innerWidth; let height = window.innerHeight; +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); + +const proj = d3.geoPath().projection(projection); +const graticule = d3.geoGraticule10(); +let euCenter; + +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") + +const g_countries = container.append("g").attr("id", "countries"); +const g_borders = container.append("g").attr("id", "borders"); +const g_graticule = container.append("g") + .append('path') + .attr("class", "graticule") + .attr("fill", "none") + .attr("stroke-width", "!px") + .attr("stroke", (n) => { + return "lightgray"; + }); +; + +function sizeWindow() { + width = window.innerWidth; + height = window.innerHeight; + + svg + .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); + + +// REQUEST ATLAS & GRAPH const req_data = new Request(CONFIG.dataUrl, { method: 'GET' }); const req_world = new Request('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json', { method: 'GET' }); - Promise.all([fetch(req_data), fetch(req_world)]) .then(([res_data, res_world]) => { return Promise.all([res_data.json(), res_world.json()]); @@ -133,130 +185,80 @@ Promise.all([fetch(req_data), fetch(req_world)]) console.error(error); }); ; -// fetch(request) -// .then(response => { -// if (response.status === 200) { -// return response.json(); -// } else { -// throw new Error('Something went wrong on api server!'); -// } -// }) -// .then(data => { -// buildGraph(data); -// }).catch(error => { -// console.error(error); -// }); + +let linkCounts = []; +const simulation = d3.forceSimulation() + .force("link", d3.forceLink() + .id(d => d['@id']) + .iterations(2) // increase to make more rigid + .distance((l) => { + // if (getCategories(l.source).indexOf('City') || getCategories(l.target).indexOf('City')) { + // // if(l.source.lat || l.target.lat) { + // return 2; + // } + return 10; + }) + .strength((l) => { + if (linkCounts.length < 1) { + // replicate from d3-force/src/link.js so we have access to this in our own strength function + linkCounts = new Array(store.nodes.length) + for (let i = 0; i < store.links.length; ++i) { + let link = store.links[i]; + linkCounts[link.source.index] = (linkCounts[link.source.index] || 0) + 1; + linkCounts[link.target.index] = (linkCounts[link.target.index] || 0) + 1; + } + } + + if (store.nodesBorderingEu.indexOf(l.source) !== -1 || store.nodesBorderingEu.indexOf(l.target) !== -1) { + // console.log('outside', l.target) + return 0.0001; + } + // original: + return 1 / Math.min(linkCounts[l.source.index], linkCounts[l.target.index]); + }) + ) + // .force("charge", d3.forceManyBody() + // .strength(-10) + // ) + // .force("center", d3.forceCenter(width / 2, height / 2)) + .force("collision", d3.forceCollide(function (d) { + return getSizeForNode(d) * 1.5; // avoid overlapping nodes + })) + .force("outsideEu", d3.forceRadial(500, euCenter[0], euCenter[1]) + .strength(function (node, idx) { + // return 1; + if (store.nodesBorderingEu.indexOf(node) !== -1) { + // console.log(node, store.nodesBorderingEu.indexOf(node) !== -1); + return 1; + } + return 0; + }) + ); function buildGraph(data, world) { // land = topojson.feature(world, world.objects.land) const borders = topojson.mesh(world, world.objects.countries, (a, b) => a !== b) const countries = topojson.feature(world, world.objects.countries).features; + const nodes = data.nodes.filter(n => n._INST || n.parent).map(d => Object.create(d)); 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)); + graph = { + "nodes": [...nodes], + "links": [...links], + } + store = { + "nodes": [...nodes], + "links": [...links], + "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]), + } + // the .source and .target attributes are still ID's. Only after initialisation of the force are they replaced with their representative objects - const 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]); + const nodesEastOfEu = links.filter(l => l.name == 'City' && (nodeMap[l.target].lon < CONFIG.eu.lonMin)).map(l => nodeMap[l.source]); - // console.log(links,nodesBorderingEu, links.filter(l => l.name == 'City').map(l => [l.name, l.target])); - console.log(nodesBorderingEu) - - const projection = d3.geoHill() - // .center([0, 0]) - .translate([width / 2, height * 1.5]) - .rotate([-12, 0, 0]) - // .rotate([-12, -52, 0]) - // .clipAngle(170) - .scale(height * 1.5); - - const proj = d3.geoPath().projection(projection); - const graticule = d3.geoGraticule10(); - - - const euCenter = projection([11, 47]); - - - - // console.log(links); - - let linkCounts = []; - const linksWithForce = links;//.filter(l => ( nodesBorderingEu.indexOf(nodeMap[l.source]) === -1 && nodesBorderingEu.indexOf(nodeMap[l.target]) === -1 )) - console.log(linksWithForce); - - const simulation = d3.forceSimulation(nodes) - .force("link", d3.forceLink(linksWithForce) - .id(d => d['@id']) - .iterations(2) // increase to make more rigid - .distance((l) => { - // if (getCategories(l.source).indexOf('City') || getCategories(l.target).indexOf('City')) { - // // if(l.source.lat || l.target.lat) { - // return 2; - // } - return 10; - }) - .strength((l) => { - if (linkCounts.length < 1) { - // replicate from d3-force/src/link.js so we have access to this in our own strength function - linkCounts = new Array(nodes.length) - for (let i = 0; i < linksWithForce.length; ++i) { - let link = linksWithForce[i]; - linkCounts[link.source.index] = (linkCounts[link.source.index] || 0) + 1; - linkCounts[link.target.index] = (linkCounts[link.target.index] || 0) + 1; - } - } - // if(getTitle(l.source) == "NEC"){ - // console.log(l, l.target, nodesBorderingEu.indexOf(l.target) !== -1); - // } - // return 0; - - if(nodesBorderingEu.indexOf(l.source) !== -1 || nodesBorderingEu.indexOf(l.target) !== -1) { - // console.log('outside', l.target) - return 0.0001; - } - // original: - return 1 / Math.min(linkCounts[l.source.index], linkCounts[l.target.index]); - }) - ) - // .force("charge", d3.forceManyBody() - // .strength(-10) - // ) - // .force("center", d3.forceCenter(width / 2, height / 2)) - .force("collision", d3.forceCollide(function (d) { - return getSizeForNode(d) * 1.5; // avoid overlapping nodes - })) - .force("outsideEu", d3.forceRadial(500, euCenter[0], euCenter[1]) - .strength(function (node, idx) { - // return 1; - if (nodesBorderingEu.indexOf(node) !== -1) { - // console.log(node, nodesBorderingEu.indexOf(node) !== -1); - return 1; - } - return 0; - }) - ); - - const svg = d3.select("svg") - .attr("viewBox", [0, 0, width, height]); - 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") - - const g_countries = container.append("g").attr("id", "countries"); - const g_borders = container.append("g").attr("id", "borders"); - container.append("g") - .append('path') - .attr("class", "graticule") - .attr('d', proj(graticule)) - .attr("fill", "none") - .attr("stroke-width", "!px") - .attr("stroke", (n) => { - return "lightgray"; - }); - ; - - g_countries.selectAll("path") @@ -289,14 +291,10 @@ function buildGraph(data, world) { .attr('class', 'links') .selectAll(".link") .data(links) - ; - - console.log(links); - - link.enter().join("g") + .join("g") .attr("class", "link") ; - link.exit().remove(); + // link.exit().remove(); const linkLine = link .append("line"). @@ -309,15 +307,15 @@ function buildGraph(data, world) { d.x = euCenter[0]; d.y = euCenter[1]; - if(nodesBorderingEu.indexOf(d) !== -1) { - if(nodesEastOfEu.indexOf(d) !== -1) { + if (store.nodesBorderingEu.indexOf(d) !== -1) { + if (nodesEastOfEu.indexOf(d) !== -1) { d.x = 466.5836692678423; d.y = 466.3493609705728; } else { d.x = 1406.608195836305; d.y = 807.9332721328062; } - + } if (d.lon && d.lat) { @@ -475,47 +473,17 @@ function buildGraph(data, world) { .attr("transform", d => `translate(${d.x}, ${d.y})`); }); + update(); - function refresh() { - container.selectAll(".countries").attr("d", proj); - container.selectAll(".graticule").attr("d", proj(graticule)); - container.selectAll(".borders").attr("d", proj(borders)); - - // simulation.alpha = 0; - // simulation.restart(); - // nodes.forEach(function (d) { - // if (d.lon && d.lat) { - // var p = projection([d.lon, d.lat]); - // d.fx = p[0]; - // d.fy = p[1]; - // } - // }) - - } - - - // beautiful but causes issues with force - // d3.geoZoom() - // .northUp(true) - // .projection(projection) - // .scaleExtent([.3, 6]) - // .onMove(refresh)(svg.node()); - - - function resize() { - width = window.innerWidth; - height = window.innerHeight; - - d3.selectAll('svg') - .attr("width", width) - .attr("height", height); - projection.translate([width / 2, height / 2]); - projection.scale(height * 1.5); - // refresh(); - } - window.addEventListener('resize', resize); + return svg.node(); +} +function update() { + simulation.nodes(graph.nodes) + // .on("tick", ticked) + simulation.force("link") + .links(graph.links); // simulate the first bit without drawing, so we don't have the 'jumping' graph in the beginning @@ -526,14 +494,8 @@ function buildGraph(data, world) { } - return svg.node(); } -color = _ => { - const scale = d3.scaleOrdinal(d3.schemeCategory10); - return d => scale(d.group); -}; - const drag = simulation => { let ignoreDrag = false;