From 1d94684461ab77ccf9757e49bd2eee77e33228bc Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Fri, 12 Apr 2024 15:33:39 +0200 Subject: [PATCH] D3 renders of the test data --- js/viz.jsx | 240 +++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 7 +- vite.config.js | 6 ++ viz.html | 174 +++++++++++++++++++++++++++++++++++ yarn.lock | 5 ++ 5 files changed, 429 insertions(+), 3 deletions(-) create mode 100644 js/viz.jsx create mode 100644 vite.config.js create mode 100644 viz.html diff --git a/js/viz.jsx b/js/viz.jsx new file mode 100644 index 0000000..61026af --- /dev/null +++ b/js/viz.jsx @@ -0,0 +1,240 @@ +import * as d3 from "d3"; +import { h, Fragment } from 'start-dom-jsx' + +// const projection = d3.geoAitoff(); + +function degreesToRadians(degrees) { + return (degrees * Math.PI) / 180; +} + +function latLonToOffsets(latitude, longitude, mapWidth, mapHeight) { + const FE = 180; // false easting + const radius = mapWidth / (2 * Math.PI); + + const latRad = degreesToRadians(latitude); + const lonRad = degreesToRadians(longitude + FE); + + const x = lonRad * radius; + + const yFromEquator = radius * Math.log(Math.tan(Math.PI / 4 + latRad / 2)); + const y = mapHeight / 2 - yFromEquator; + + return { x, y }; +} + +export function viz(vizEl) { + console.log(d3) + // vizEl.appendChild(

Hello

); + + fetch("data/parsed_requests.json") + .then((response) => response.json()) + .then(start) + .catch((error) => { + console.error(error); + // alert("Data could not be loaded.") + }); + + // d3.queue() + // .defer(d3.json, "data/parsed_requests.json") // World shape + // .await(start) +} + +function lerp(X, Y, t) { + return X * t + Y * (1 - t) +} + +function inverse_lerp(a, b, x) { + return (x - a) / (b - a); +} + +function start(parsed_requests) { + const nodes = parsed_requests.nodes; + const edges = parsed_requests.edges; + + const svg = d3.select('svg'); + const g_libraries = svg.append("g").attr("id", "libraries"); + const g_movements = svg.append("g").attr("id", "movements"); + const width = +svg.attr("width"), + height = +svg.attr("height"); + + // let libraries = {} + // console.log(nodes, edges) + const valid_nodes = nodes.filter((n) => n.lat && n.lon).map((node) => { + const { x: x1, y: y1 } = latLonToOffsets(node.lat, node.lon, 100000, 100000) + console.log(x1, y1) + const x2 = inverse_lerp(1358, 1380, x1); + const y2 = inverse_lerp(32862, 32900, y1); + + const margin = 200; + const x = x2 * (width - 2 * margin) + margin; + const y = y2 * (height - 2 * margin) + margin; + // console.log(x2, y2) + // console.log(node) + + node['x'] = x; + node['y'] = y; + return node + }) + + const nodeMap = Object.fromEntries(valid_nodes.map(d => [d['name'], d])); + + const links = edges.filter(l => nodeMap[l['Owning Library Name']] && nodeMap[l['Pickup Location']]).map((l, idx) => { + l.source = nodeMap[l['Owning Library Name']]; + l.target = nodeMap[l['Pickup Location']]; + l.nr = idx; + return l; + }); + // .filter((link, index, self) => + // // remove incidental duplicates + // index === self.findIndex((l) => ( + // l.source.id === link.source.id && l.target.id === link.target.id + // )) + // ); + + // Reformat the list of link. Note that columns in csv file are called long1, long2, lat1, lat2 + // var link = [] + // edges.forEach(function (row) { + // source = [+row.long1, +row.lat1] + // target = [+row.long2, +row.lat2] + // topush = { type: "LineString", coordinates: [source, target] } + // link.push(topush) + // }) + + // Add the path + const libraries = g_libraries.selectAll(".library"); + libraries.data(valid_nodes, d => d.code) + .join((enter) => { + let group = enter.append("g") + // .attr("class", getClasses) + // .attr("id", (n) => getIdForTitle(n.fulltext)); + .attr("id", (n) => n.code) + .attr("transform", (n) => `translate(${n.x}, ${n.y})`); + + // group.on("click", (evt, n) => { + // evt.stopPropagation(); this.selectNode(n); + // }); + // group.on("mouseover", (evt, n) => { + // this.hoverNode(evt, n); + // }); + // group.on("mouseout", (evt, n) => { + // this.endHoverNode(n); + // }); + group.append('circle').attr("r", 5 /*this.nodeSize*/); + // group.append('path') + // .attr('d', (n) => { + // return getSymbolForNode(n)(n); + // }) + var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "4").attr('x', 5).text((n) => n.name); + return group + }) + + const movements_a = g_movements.selectAll(".movement"); + const movements = movements_a + .data(links) + .join( + enter => { + let group = enter.append("g") + .attr("class", (l) => "link " /* TODO type of movement */) + .attr("id", (l) => `link${l.nr}`); + group.append("path") + // .attr("marker-end", "url(#arrowHead)") + .attr('id', (d, i) => 'linkpath_' + i) + // .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(); + // if (d == link.target || d == link.source) { + // n.classList.add('linkHover'); + // } + // } + + // this.showRelationTooltip(link, ev); + // }).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'); + // } + // }).on("click", (ev, link) => { + // ev.stopPropagation(); + // this.selectNode(link.source); + // }) + ; + // 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; + } + ) + ; + + movements.each(function (l) { + let sourceX, targetX, midX, dx, dy, angle; + + // This mess makes the arrows exactly perfect. + // thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4 + if (l.source.x < l.target.x) { + sourceX = l.source.x; + targetX = l.target.x; + } else if (l.target.x < l.source.x) { + targetX = l.target.x; + sourceX = l.source.x; + } else if (l.target.isCircle) { + targetX = sourceX = l.target.x; + } else if (l.source.isCircle) { + targetX = sourceX = l.source.x; + } else { + midX = (l.source.x + l.target.x) / 2; + if (midX > l.target.x) { + midX = l.target.x; + } else if (midX > l.source.x) { + midX = l.source.x; + } else if (midX < l.target.x) { + midX = l.target.x; + } else if (midX < l.source.x) { + midX = l.source.x; + } + targetX = sourceX = midX; + } + + dx = targetX - sourceX; + dy = l.target.y - l.source.y; + angle = Math.atan2(dx, dy); + + var srcSize = 5; //_mapGraph.getSizeForNode(l.source); + var tgtSize = 5; //_mapGraph.getSizeForNode(l.target); + + // Compute the line endpoint such that the arrow + // it not in the center, but rather slightly out of it + // use a small ofset for the angle to compensate roughly for the curve + l.sourceX = sourceX + Math.sin(angle+.5) * srcSize; + l.targetX = targetX - Math.sin(angle-.5) * tgtSize; + l.sourceY = l.source.y + Math.cos(angle+.5) * srcSize; + l.targetY = l.target.y - Math.cos(angle-.5) * tgtSize; + + + // find radius of arc based on distance between points + // add a jitter to spread out the lines when links are stacked + const dr = Math.sqrt(dx * dx + dy * dy) * (.7 + Math.random()*.6); + + // "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y + let rel = d3.select(this); + rel.select("path") + .attr('d', `M ${l.sourceX},${l.sourceY} A ${dr},${dr} 0 0,1 ${l.targetX},${l.targetY}`) + + + }) + + console.log(libraries) +} \ No newline at end of file diff --git a/package.json b/package.json index 7c12141..aefce5a 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,13 @@ "private": true, "type": "module", "scripts": { - "dev": "vite", // start dev server, aliases: `vite dev`, `vite serve` - "build": "vite build", // build for production - "preview": "vite preview" // locally preview production build + "dev": "vite", + "build": "vite build", + "preview": "vite preview" }, "dependencies": { "d3": "^7.9.0", + "start-dom-jsx": "^1.0.0-beta.1", "vite": "^5.2.8" } } diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..bd00335 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,6 @@ +export default { + esbuild: { + jsxFactory: 'h', + jsxFragment: 'Fragment' + } +} \ No newline at end of file diff --git a/viz.html b/viz.html new file mode 100644 index 0000000..4afaca1 --- /dev/null +++ b/viz.html @@ -0,0 +1,174 @@ + + + + + + + UvA UB - movements + + + + + + +
+
+ +

library of motions

+
+ Work by Ruben van de Ven for the University of Amsterdam Library. Fonts by Open + Source Publishing, map by OpenStreetMap. +
+ + + + +
+
+ + + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index afed0ae..94dcdfb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -562,6 +562,11 @@ source-map-js@^1.2.0: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== +start-dom-jsx@^1.0.0-beta.1: + version "1.0.0-beta.1" + resolved "https://registry.yarnpkg.com/start-dom-jsx/-/start-dom-jsx-1.0.0-beta.1.tgz#f286af1ac7eebe086e820b643bcd25158d322a8d" + integrity sha512-1zz3GPeHxO8+HRYo9ZBzmxZ8MRvG5c0yxB3iVryOX6sOWaW+PPdUR1U0sP8yMoELauTy0SrH0768yOqCKiwDsw== + vite@^5.2.8: version "5.2.8" resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.8.tgz#a99e09939f1a502992381395ce93efa40a2844aa"