ub-movements-test/js/viz.jsx
2024-04-12 15:33:39 +02:00

240 lines
No EOL
7.2 KiB
JavaScript

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)
}