import * as d3 from "d3"; // use jsx templating import { h, Fragment } from 'start-dom-jsx'; import anime from 'animejs/lib/anime.es.js'; console.log(anime) // 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); } interface IndexObject { [index: string]: Array; } /** * A scene */ class Scene { // setup: CallableFunction; animation: CallableFunction; // the function that returns an anime.js object constructor(animation: CallableFunction) { // this.setup = setup; this.animation = animation; } async run () { const anim = this.animation(); console.log('anim', anim); await anim.finished } } class Sequence { scenes: Array; constructor() { this.scenes = []; } async run() { while(true) { for(const scene of this.scenes){ console.log('scene', scene); await scene.run(); console.log('ran') } } } } function buildIndex(objectList: Array, field: string): IndexObject { const index = {} for (const idx in objectList) { const v = objectList[idx][field] if (!index.hasOwnProperty(v)) { index[v] = [idx] } else { index[v].push(idx) } } return index; } function start(parsed_requests) { const nodes = parsed_requests.nodes; const edges: Array = 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) // filter nodes with only having both Latitude and Longitude. // then map these coordinates to the canvas space 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 }) // create an index to access the node objects by their name const nodeMap = Object.fromEntries(valid_nodes.map(d => [d['name'], d])); const edgeIndexBarcode = buildIndex(edges, 'Barcode'); // console.log(edgeIndexBarcode); 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('circle').attr("r", 20 /*this.nodeSize*/).style("fill", "none").style("stroke", "white").style("stroke-width", "10"); // group.append('path') // .attr('d', (n) => { // return getSymbolForNode(n)(n); // }) var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "4").attr('x', 35).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}`) }) // const edgesToHighlight = edgeIndexBarcode["0139900930"].map((i) => edges[i]) // const edgesToHighlight = edgeIndexBarcode["0182126011"].map((i) => edges[i]) // const edgesToHighlight = edgeIndexBarcode["0139204135"].map((i) => edges[i]) let edgesToHighlight: Array = []; for (const code in edgeIndexBarcode) { if (edgeIndexBarcode[code].length < 4) { continue; } for (const i of edgeIndexBarcode[code]) { // console.log(i) edgesToHighlight.push(edges[i]); } } // const edgeToSelect = edges.filter(e => e.nr == 530)[0] const edgeToSelect = edges.filter(e => e.Barcode == "0133040931")[0] edgesToHighlight = edges.filter(e => e.Barcode == edgeToSelect.Barcode) // edgeToSelect. // console.log(530, edges[530].nr, edges[530]) // edgesToHighlight = []; // edgesToHighlight.push(edges[530]) highlightTrajectory(edgesToHighlight); // console.log(libraries) function scene_01_FollowItem() { const edgeToSelect = edges.filter(e => e.Barcode == "0133040931")[0] edgesToHighlight = edges.filter(e => e.Barcode == edgeToSelect.Barcode) const tl = anime.timeline({}) console.log(tl) for(const edge of edgesToHighlight) { tl.add({ targets: `#linkpath_${edge.nr}`, stroke: ['rgb(255, 0, 0)', 'rgb(0, 255, 255)'], // strokeDashoffset: [anime.setDashoffset, 0], duration: 500, // easing: 'easeOutElastic(1, .8)', // direction: 'alternate' }) .add({ targets: `#linkpath_${edge.nr}`, // stroke: ['rgb(255, 0, 0)', 'rgb(0, 255, 255)'], strokeDashoffset: [anime.setDashoffset, 0], easing: 'easeInOutSine', duration: 8000, // easing: 'easeOutElastic(1, .8)', // direction: 'alternate' }) } tl.add({ targets: edgesToHighlight.map(edge => `#linkpath_${edge.nr}`), stroke: ['rgb(0, 255, 255)', 'rgb(255, 0, 0)'], duration: 3000 }) return tl; } const sequence = new Sequence(); sequence.scenes.push(new Scene(scene_01_FollowItem)) console.log(sequence) sequence.run() // document.getElementById('linkpath_3903').style.stroke = 'orange'; // sequence.scenes.push(new Scene(() => { // const tl = anime.timeline({}) // tl.add({ // targets: '#linkpath_532', // stroke: ['rgb(255, 0, 0)', 'rgb(0, 255, 255)'], // duration: 2000, // // easing: 'easeOutElastic(1, .8)', // direction: 'alternate' // }) // tl.add({ // targets: '#linkpath_532', // // stroke: ['rgb(255, 255, 0)', 'rgb(255, 0, 255)'], // translateX: [0, -100, 0], // duration: 4000, // easing: 'easeOutElastic(1, .8)', // // direction: 'alternate' // }) // return tl // })); // sequence.scenes.push(new Scene(() => { // return anime({ // targets: '#linkpath_530', // translateX: [0, 500], // duration: 2000, // direction: 'alternate' // }) // })); // sequence.run() } // TODO: this is currently by barcode, but this should be the object itself. function highlightTrajectory(edges: Array) { // console.log(edges) for (const edge of edges) { console.log(`#linkpath_${edge.nr}`) const selection = d3.select(`#linkpath_${edge.nr}`); // console.log(selection); selection.style("stroke", "red"); selection.style("stroke-width", "5"); selection.classed("selected", true); } }