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"