D3 renders of the test data
This commit is contained in:
parent
3312cb6f23
commit
1d94684461
5 changed files with 429 additions and 3 deletions
240
js/viz.jsx
Normal file
240
js/viz.jsx
Normal file
|
@ -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(<h2>Hello</h2>);
|
||||
|
||||
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)
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
6
vite.config.js
Normal file
6
vite.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
esbuild: {
|
||||
jsxFactory: 'h',
|
||||
jsxFragment: 'Fragment'
|
||||
}
|
||||
}
|
174
viz.html
Normal file
174
viz.html
Normal file
|
@ -0,0 +1,174 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UvA UB - movements</title>
|
||||
<style>
|
||||
#canvas {
|
||||
background: white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform-origin: top left;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fluxisch Mono', sans-serif;
|
||||
src:
|
||||
url(assets/fonts/FluxischElse-Regular.woff) format('woff');
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fluxisch Mono', sans-serif;
|
||||
src:
|
||||
url(assets/fonts/FluxischElse-Light.woff) format('woff');
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fluxisch Mono', sans-serif;
|
||||
src:
|
||||
url(assets/fonts/FluxischElse-Bold.woff) format('woff');
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
/* animation root */
|
||||
#viz {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "Fluxisch Else";
|
||||
font-weight: 200;
|
||||
color: black;
|
||||
/* background: linear-gradient(180deg, #3a294c 0%, #62487f 100%); */
|
||||
background: rgb(64, 64, 64)
|
||||
}
|
||||
|
||||
h1 {
|
||||
position: absolute;
|
||||
top: calc(50% - 3em);
|
||||
text-align: center;
|
||||
font-weight: 200;
|
||||
font-size: 140pt;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#viz #about {
|
||||
position: absolute;
|
||||
font-size: 8pt;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 450px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#lines {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#motiontitle {
|
||||
/* animation: jitter 5s infinite; */
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes jitter {
|
||||
0% {
|
||||
transform: skew(0, 0);
|
||||
}
|
||||
|
||||
10% {
|
||||
transform: skew(10deg, 2deg);
|
||||
}
|
||||
|
||||
15% {
|
||||
transform: skew(13deg, -2deg);
|
||||
}
|
||||
|
||||
45% {
|
||||
transform: skew(-3deg, -4deg);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: skew(-20deg, 4deg);
|
||||
}
|
||||
|
||||
95% {
|
||||
transform: skew(20deg, 1deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: skew(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
svg path {
|
||||
fill: none;
|
||||
/* stroke: rgba(255, 255, 0, .1); */
|
||||
stroke: #bc89ff10;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
svg circle{
|
||||
fill: white
|
||||
}
|
||||
svg text{
|
||||
fill: white;
|
||||
font-size: 30pt;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function scaleToFit() {
|
||||
const el = document.getElementById('canvas');
|
||||
|
||||
|
||||
const sw = window.innerWidth / parseInt(el.style.width);
|
||||
const sh = window.innerHeight / parseInt(el.style.height);
|
||||
|
||||
const s = Math.min(sw, sh);
|
||||
|
||||
canvas.style.transform = `scale(${s * 100}%)`;
|
||||
}
|
||||
window.addEventListener('resize', scaleToFit);
|
||||
window.addEventListener('load', scaleToFit);
|
||||
|
||||
</script>
|
||||
<script type="module">
|
||||
import { viz } from './js/viz.jsx';
|
||||
|
||||
viz(document.getElementById('viz'));
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="canvas" style="width: 2160px;height: 3840px;">
|
||||
<div id="viz">
|
||||
|
||||
<h1>library of <span id="motiontitle">motions</span></h1>
|
||||
<div id="about">
|
||||
Work by <em>Ruben van de Ven</em> for the <em>University of Amsterdam Library</em>. Fonts by <em>Open
|
||||
Source Publishing</em>, map by <em>OpenStreetMap</em>.
|
||||
</div>
|
||||
|
||||
<svg id="lines" height="3840" width="2160"></svg>
|
||||
|
||||
<!--<img src="assets/map-amsterdam-colored.png">-->
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue