Compare commits
No commits in common. "c8187f3f5e36d0f98d12e386524233e128b92a60" and "211792acdbf4bf225f306e16d9e89b7c4cf61fdf" have entirely different histories.
c8187f3f5e
...
211792acdb
6 changed files with 114 additions and 528 deletions
41
package-lock.json
generated
41
package-lock.json
generated
|
@ -9,12 +9,9 @@
|
|||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@sveu/browser": "^1.0.1",
|
||||
"@tsconfig/svelte": "^5.0.2",
|
||||
"sass": "^1.77.2",
|
||||
"svelte": "^4.2.12",
|
||||
"svelte-check": "^3.6.7",
|
||||
"svelte-preprocess": "^5.1.4",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.0"
|
||||
|
@ -731,21 +728,6 @@
|
|||
"vite": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveu/browser": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveu/browser/-/browser-1.0.1.tgz",
|
||||
"integrity": "sha512-Mz6Vx7erVabjL+yIM/9h9lqRjEbCiIRKjH9KD1VuR3xTTJdMiljoeWph9B+shg0n2VLhY2tz8PvTu6H3eqhF8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@sveu/shared": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveu/shared": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveu/shared/-/shared-1.0.1.tgz",
|
||||
"integrity": "sha512-waMJ+UXD5NV/M7L6lmXigY9Ry9TwhcBz+QDbYsDcq92bL7nlRUeh3ckvvAnxw1Nzw0T2JeEgmvZZblHuAHJL9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/svelte": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.4.tgz",
|
||||
|
@ -1113,12 +1095,6 @@
|
|||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz",
|
||||
"integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
|
@ -1570,23 +1546,6 @@
|
|||
"rimraf": "^2.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.77.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz",
|
||||
"integrity": "sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sorcery": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
|
||||
|
|
|
@ -11,12 +11,9 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@sveu/browser": "^1.0.1",
|
||||
"@tsconfig/svelte": "^5.0.2",
|
||||
"sass": "^1.77.2",
|
||||
"svelte": "^4.2.12",
|
||||
"svelte-check": "^3.6.7",
|
||||
"svelte-preprocess": "^5.1.4",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.0"
|
||||
|
|
355
src/Viz.svelte
355
src/Viz.svelte
|
@ -1,110 +1,62 @@
|
|||
<script lang="ts">
|
||||
import parsed_requests from "/data/parsed_requests.json";
|
||||
import { draw, slide } from "svelte/transition";
|
||||
import { fps } from "@sveu/browser";
|
||||
|
||||
import { All, Scene, Timeline } from "./scenes/Scenes";
|
||||
import {
|
||||
type Location,
|
||||
type Movement,
|
||||
type Item,
|
||||
type Motion,
|
||||
get_path_d,
|
||||
type Data,
|
||||
type VizData,
|
||||
type Log,
|
||||
} from "./lib/types";
|
||||
|
||||
// these are passed from main.ts (or vice versaas)
|
||||
export let width = 0;
|
||||
export let height = 0;
|
||||
export let dev: Boolean = false;
|
||||
|
||||
const _fps = fps();
|
||||
|
||||
const node_positions_percentages = {
|
||||
APM: [13.829853909464873, 11.277305564859935],
|
||||
IWO: [82.20349794238683, 92.52914963452248],
|
||||
BC: [13.829853909464873, 10.235638898193269],
|
||||
ARTIS: [37.36555349794344, 14.373622178489617],
|
||||
BHBPU: [18.895388888889407, 8.009932889956616],
|
||||
CEDLA: [32.88047119341627, 21.36735134591498],
|
||||
UBGW: [7.420491769548115, 13.544746441851984],
|
||||
IVIR: [33.838187242798554, 17.768099884380575],
|
||||
OTM: [80.81460905349795, 93.83123296785581],
|
||||
JB: [34.30115020576152, 19.538933217713907],
|
||||
REC: [32.38571810699577, 16.229534855698518],
|
||||
AMC: [81.82347530864213, 90.12737486852306],
|
||||
PCHH: [10.320697530863542, 3.927002725110166],
|
||||
BETA: [78.14333127572034, 26.257884003417413],
|
||||
UBB: [10.198269547325893, 16.40932977518532],
|
||||
};
|
||||
|
||||
const node_positions = Object.fromEntries(
|
||||
Object.entries(node_positions_percentages).map(([k, p]) => [
|
||||
k,
|
||||
[(p[0] / 100) * width, (p[1] / 100) * height],
|
||||
]),
|
||||
);
|
||||
|
||||
$: console.log(parsed_requests);
|
||||
|
||||
// preprocess data
|
||||
const _nodes: Location[] = parsed_requests.nodes;
|
||||
const _requests: Array<Object> = parsed_requests.edges;
|
||||
// TODO: pass these from main.ts (or vice versaas)
|
||||
const width = 2160;
|
||||
const height = 3840;
|
||||
|
||||
const items = new Map<string, Item>();
|
||||
const movements: Array<Movement> = [];
|
||||
// preprocess data
|
||||
const nodes = parsed_requests.nodes;
|
||||
const edges: Array<Object> = parsed_requests.edges;
|
||||
|
||||
// filter nodes with only having both Latitude and Longitude.
|
||||
// then map these coordinates to the canvas space
|
||||
const locations = new Map(_nodes
|
||||
const valid_nodes = nodes
|
||||
.filter((n) => n.lat && n.lon)
|
||||
.map((node) => {
|
||||
node["x"] = node_positions[node.code][0];
|
||||
node["y"] = node_positions[node.code][1];
|
||||
// const { x: x1, y: y1 } = latLonToOffsets(node.lat, node.lon, 100000, 100000)
|
||||
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;
|
||||
})
|
||||
.map((d) => [d["name"], d])
|
||||
);
|
||||
|
||||
|
||||
_requests
|
||||
// remove entries that stay at the same place
|
||||
.filter((n) => n['Owning Library Name'] != n['Pickup Location'])
|
||||
.filter(
|
||||
(l) =>
|
||||
locations.has(l["Owning Library Name"]) &&
|
||||
locations.has(l["Pickup Location"]),
|
||||
)
|
||||
.forEach((r, idx) => {
|
||||
const identifier: String = r["Barcode"];
|
||||
if (!items.has(identifier)) {
|
||||
items.set(identifier, {
|
||||
title: r["Title (Complete)"],
|
||||
MMS: r["MMS Id"],
|
||||
Barcode: r["Barcode"],
|
||||
Publisher: r["Publisher"],
|
||||
_original: r,
|
||||
});
|
||||
}
|
||||
let movement: Movement = {
|
||||
source: locations.get(r["Owning Library Name"]),
|
||||
target: locations.get(r["Pickup Location"]),
|
||||
nr: idx,
|
||||
item: items.get(identifier),
|
||||
_original: r,
|
||||
d: "", // bit of a hacky workaround, refactor in object
|
||||
};
|
||||
movement.d = get_path_d(movement);
|
||||
movements.push(movement);
|
||||
});
|
||||
console.log(items, movements);
|
||||
|
||||
// const edgeIndexBarcode = buildIndex(edges, "Barcode");
|
||||
// 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);
|
||||
|
||||
function degreesToRadians(degrees: number) {
|
||||
const movements = 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;
|
||||
});
|
||||
|
||||
function degreesToRadians(degrees) {
|
||||
return (degrees * Math.PI) / 180;
|
||||
}
|
||||
|
||||
|
@ -124,7 +76,7 @@
|
|||
return { x, y };
|
||||
}
|
||||
|
||||
function lerp(X: number, Y: number, t: number) {
|
||||
function lerp(X, Y, t) {
|
||||
return X * t + Y * (1 - t);
|
||||
}
|
||||
|
||||
|
@ -136,174 +88,104 @@
|
|||
[index: string]: Array<number>;
|
||||
}
|
||||
|
||||
// function buildIndex(objectList: Array<Object>, 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;
|
||||
// }
|
||||
|
||||
const occurences = new Map<Item, Movement[]>();
|
||||
movements.forEach((movement, idx) => {
|
||||
let movements = occurences.get(movement.item) ?? []
|
||||
movements.push(movement)
|
||||
occurences.set(movement.item, movements);
|
||||
});
|
||||
|
||||
const data: Data = {
|
||||
locations: locations,
|
||||
items: items,
|
||||
movements: movements,
|
||||
occurences: occurences
|
||||
function buildIndex(objectList: Array<Object>, 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;
|
||||
}
|
||||
|
||||
let drawn_motions = writable(<Motion[]>[]); //.filter((m, i) => i < 100);
|
||||
$: opacity = Math.max(0.055, Math.min(1, 200 / $drawn_motions.length));
|
||||
let overlay_motions = writable(<Motion[]>[]); //.filter((m, i) => i < 100);
|
||||
let events = writable(<Log[]>[]); //.filter((m, i) => i < 100);
|
||||
function get_path_d(movement) {
|
||||
const m = movement;
|
||||
let sourceX, targetX, midX, dx, dy, angle;
|
||||
|
||||
const viz_data: VizData = {
|
||||
drawn_motions: drawn_motions,
|
||||
overlay_motions: overlay_motions,
|
||||
events: events,
|
||||
}
|
||||
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
let scenes = [All, Timeline];
|
||||
let currentSceneI = 0;
|
||||
let currentScene: Scene;
|
||||
|
||||
function nextScene() {
|
||||
if (currentScene) {
|
||||
currentScene.stop();
|
||||
// This mess makes the arrows exactly perfect.
|
||||
// thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4
|
||||
if (m.source.x < m.target.x) {
|
||||
sourceX = m.source.x;
|
||||
targetX = m.target.x;
|
||||
} else if (m.target.x < m.source.x) {
|
||||
targetX = m.target.x;
|
||||
sourceX = m.source.x;
|
||||
} else if (m.target.isCircle) {
|
||||
targetX = sourceX = m.target.x;
|
||||
} else if (m.source.isCircle) {
|
||||
targetX = sourceX = m.source.x;
|
||||
} else {
|
||||
midX = (m.source.x + m.target.x) / 2;
|
||||
if (midX > m.target.x) {
|
||||
midX = m.target.x;
|
||||
} else if (midX > m.source.x) {
|
||||
midX = m.source.x;
|
||||
} else if (midX < m.target.x) {
|
||||
midX = m.target.x;
|
||||
} else if (midX < m.source.x) {
|
||||
midX = m.source.x;
|
||||
}
|
||||
targetX = sourceX = midX;
|
||||
}
|
||||
|
||||
console.log("Next scene", currentSceneI);
|
||||
currentScene = new scenes[currentSceneI](
|
||||
data,
|
||||
viz_data,
|
||||
nextScene,
|
||||
);
|
||||
currentSceneI = (currentSceneI + 1) % scenes.length;
|
||||
dx = targetX - sourceX;
|
||||
dy = m.target.y - m.source.y;
|
||||
angle = Math.atan2(dx, dy);
|
||||
|
||||
var srcSize = 5; //_mapGraph.getSizeForNode(m.source);
|
||||
var tgtSize = 5; //_mapGraph.getSizeForNode(m.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
|
||||
m.sourceX = sourceX + Math.sin(angle + 0.5) * srcSize;
|
||||
m.targetX = targetX - Math.sin(angle - 0.5) * tgtSize;
|
||||
m.sourceY = m.source.y + Math.cos(angle + 0.5) * srcSize;
|
||||
m.targetY = m.target.y - Math.cos(angle - 0.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) * (0.7 + Math.random() * 0.6);
|
||||
|
||||
// "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y
|
||||
return `M ${m.sourceX},${m.sourceY} A ${dr},${dr} 0 0,1 ${m.targetX},${m.targetY}`;
|
||||
}
|
||||
|
||||
let paused = false;
|
||||
onMount(() => {
|
||||
let interval = null;
|
||||
nextScene();
|
||||
// interval = setInterval(() => {
|
||||
// if (paused) {
|
||||
// return;
|
||||
// }
|
||||
// // TODO: change scene
|
||||
// // timeElapsed += 1;
|
||||
// const n = Math.ceil(Math.random() * 8);
|
||||
// const start_i = Math.floor((movements.length - n) * Math.random());
|
||||
// drawn_movements = movements.filter(
|
||||
// (m, i) => i >= start_i && i < start_i + n,
|
||||
// );
|
||||
console.log(movements);
|
||||
|
||||
// // if (remainingTime === 0) {
|
||||
// // clearInterval(interval);
|
||||
// // deleteTimer();
|
||||
// // }
|
||||
// }, 2000);
|
||||
|
||||
return () => {
|
||||
currentScene.stop();
|
||||
// clearInterval(interval);
|
||||
};
|
||||
});
|
||||
let drawn_movements = movements;
|
||||
</script>
|
||||
|
||||
<h1>library of <span id="motiontitle">motions</span></h1>
|
||||
<div id="about">
|
||||
<span>Work by <em>Ruben van de Ven</em> for the
|
||||
<em>University of Amsterdam Library</em>.</span>
|
||||
<span>Fonts by <em>Open Source Publishing</em>.</span>
|
||||
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 {width} {height}>
|
||||
<g id="movements">
|
||||
{#each $drawn_motions as m}
|
||||
<path
|
||||
in:draw|global={{ duration: m.duration }}
|
||||
d={m.movement.d}
|
||||
style="opacity: {opacity}"
|
||||
></path>
|
||||
{/each}
|
||||
</g>
|
||||
<g id="overlay_motions">
|
||||
{#each $overlay_motions as m}
|
||||
<path in:draw|global={{ duration: m.duration }} d={m.movement.d}
|
||||
></path>
|
||||
{#each drawn_movements as m}
|
||||
<path d={get_path_d(m)}></path>
|
||||
{/each}
|
||||
</g>
|
||||
<g id="libraries">
|
||||
{#each locations.values() as node}
|
||||
{#each valid_nodes as node}
|
||||
<g id={node.code} transform="translate({node.x}, {node.y})">
|
||||
<circle r="5"></circle>
|
||||
<circle r="20"></circle>
|
||||
<text class="nodeTitle" y="13" x="35">{node.name}</text>
|
||||
<text class="nodeTitle" y="4" x="35">{node.name}</text>
|
||||
</g>
|
||||
{/each}
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
{#if currentScene?.rendered_elements.indexOf("timeline") >= 0}
|
||||
<div id="timeline">
|
||||
{#each $events as m}
|
||||
<div class="entry" in:slide={{ duration: 200 }}>
|
||||
<!-- {m['Title (Complete)']} -->
|
||||
<span class="date"
|
||||
>{m.movement._original["Request Completion Date"]}</span
|
||||
>
|
||||
<span class="location">{m.movement.target.name}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if dev}
|
||||
<div id="fps">{$_fps}</div>
|
||||
<div id="controls">
|
||||
<button on:click={nextScene}>▷</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
* {
|
||||
color: white;
|
||||
}
|
||||
#timeline {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
}
|
||||
#fps {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 9999;
|
||||
font-size: 30px;
|
||||
/* color: white; */
|
||||
}
|
||||
#controls {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 40px;
|
||||
z-index: 9999;
|
||||
font-size: 30px;
|
||||
/* color: white; */
|
||||
}
|
||||
<style>
|
||||
#about {
|
||||
position: absolute;
|
||||
font-size: 8pt;
|
||||
|
@ -311,10 +193,6 @@
|
|||
right: 20px;
|
||||
width: 450px;
|
||||
text-align: right;
|
||||
|
||||
span{
|
||||
display:block;
|
||||
}
|
||||
}
|
||||
#libraries circle:first-child {
|
||||
fill: white;
|
||||
|
@ -326,15 +204,10 @@
|
|||
stroke-width: 10;
|
||||
}
|
||||
path {
|
||||
stroke: #bc89ff;
|
||||
stroke: #bc89ff10;
|
||||
stroke-width: 2px;
|
||||
fill: none;
|
||||
}
|
||||
#overlay_motions path {
|
||||
stroke: red;
|
||||
stroke-width: 4;
|
||||
opacity: 1;
|
||||
}
|
||||
text {
|
||||
fill: white;
|
||||
font-size: 30pt;
|
||||
|
|
107
src/lib/types.ts
107
src/lib/types.ts
|
@ -1,107 +0,0 @@
|
|||
import type { Writable } from "svelte/store";
|
||||
|
||||
export type Location = Object;
|
||||
|
||||
// disambiguated (physical or online) library object
|
||||
export type Item = {
|
||||
title: String,
|
||||
MMS: String,
|
||||
Barcode: String, // String because of preceding 0
|
||||
Publisher: String,
|
||||
_original: Object
|
||||
}
|
||||
|
||||
// A movement of the object (Request in Alma Analytics)
|
||||
export type Movement = {
|
||||
nr: number, // unique identifier in the set
|
||||
source: Location,
|
||||
target: Location,
|
||||
item: Item,
|
||||
_original: Object
|
||||
// also contains additional request data (see requests.csv)
|
||||
d: String
|
||||
};
|
||||
|
||||
export type Occurences = Map<Item, Movement[]>
|
||||
|
||||
export interface Data {
|
||||
locations: Map<String, Location>,
|
||||
items: Map<string, Item>,
|
||||
movements: Movement[],
|
||||
occurences: Occurences
|
||||
}
|
||||
|
||||
|
||||
// An event to trigger drawing an Edge
|
||||
export type Motion = {
|
||||
duration: number,
|
||||
movement: Movement,
|
||||
}
|
||||
|
||||
export type Log = {
|
||||
date: Date,
|
||||
title: String,
|
||||
description: String
|
||||
}
|
||||
|
||||
|
||||
// Type used by the scenes with all reactive Writables for drawable objects
|
||||
export type VizData = {
|
||||
drawn_motions: Writable<Motion[]>,
|
||||
overlay_motions: Writable<Motion[]>,
|
||||
events: Writable<Log[]>
|
||||
}
|
||||
|
||||
export function get_path_d(movement: Movement) {
|
||||
const m = movement;
|
||||
// console.log(m)
|
||||
let sourceX, targetX, midX, dx, dy, angle;
|
||||
|
||||
// This mess makes the arrows exactly perfect.
|
||||
// thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4
|
||||
if (m.source.x < m.target.x) {
|
||||
sourceX = m.source.x;
|
||||
targetX = m.target.x;
|
||||
} else if (m.target.x < m.source.x) {
|
||||
targetX = m.target.x;
|
||||
sourceX = m.source.x;
|
||||
} else if (m.target.isCircle) {
|
||||
targetX = sourceX = m.target.x;
|
||||
} else if (m.source.isCircle) {
|
||||
targetX = sourceX = m.source.x;
|
||||
} else {
|
||||
midX = (m.source.x + m.target.x) / 2;
|
||||
if (midX > m.target.x) {
|
||||
midX = m.target.x;
|
||||
} else if (midX > m.source.x) {
|
||||
midX = m.source.x;
|
||||
} else if (midX < m.target.x) {
|
||||
midX = m.target.x;
|
||||
} else if (midX < m.source.x) {
|
||||
midX = m.source.x;
|
||||
}
|
||||
targetX = sourceX = midX;
|
||||
}
|
||||
|
||||
dx = targetX - sourceX;
|
||||
dy = m.target.y - m.source.y;
|
||||
angle = Math.atan2(dx, dy);
|
||||
|
||||
var srcSize = 5; //_mapGraph.getSizeForNode(m.source);
|
||||
var tgtSize = 5; //_mapGraph.getSizeForNode(m.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 Movemente to compensate roughly for the curve
|
||||
let m_sourceX = sourceX + Math.sin(angle + 0.5) * srcSize;
|
||||
let m_targetX = targetX - Math.sin(angle - 0.5) * tgtSize;
|
||||
let m_sourceY = m.source.y + Math.cos(angle + 0.5) * srcSize;
|
||||
let m_targetY = m.target.y - Math.cos(angle - 0.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) * (0.7 + Math.random() * 0.5);
|
||||
|
||||
// "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y
|
||||
return `M ${m_sourceX},${m_sourceY} A ${dr},${dr} 0 0,1 ${m_targetX},${m_targetY}`;
|
||||
}
|
|
@ -7,15 +7,7 @@ document.getElementById('canvas')!.style.width = `${width}px`;
|
|||
document.getElementById('canvas')!.style.height = `${height}px`;
|
||||
|
||||
const app = new Viz({
|
||||
// see https://svelte.dev/docs/client-side-component-api
|
||||
target: document.getElementById('viz')!,
|
||||
props: {
|
||||
width: width,
|
||||
height: height,
|
||||
dev: true
|
||||
},
|
||||
// intro: true,
|
||||
})
|
||||
|
||||
|
||||
export default app
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
import type { Writable } from "svelte/store";
|
||||
import type { Data, Item, Motion, Movement, VizData } from "../lib/types";
|
||||
|
||||
export class Scene {
|
||||
rendered_elements: String[] = []
|
||||
stop() {
|
||||
console.log("TODO: stop timeout")
|
||||
}
|
||||
}
|
||||
|
||||
export class All extends Scene {
|
||||
allMovements: Movement[]
|
||||
motions: Writable<Motion[]>
|
||||
interval: number
|
||||
step: number = 0
|
||||
nextScene: CallableFunction
|
||||
locationCounts = new Map<Location, { in: number, out: number }>()
|
||||
|
||||
constructor(data: Data, viz_data: VizData, nextScene: CallableFunction) {
|
||||
super()
|
||||
|
||||
this.nextScene = nextScene
|
||||
|
||||
// start setInterval to trigger additions per 100 or so to drawn_movements (rendered on map)
|
||||
// when done, trigger parent.done()
|
||||
this.allMovements = data.movements;
|
||||
|
||||
this.motions = viz_data.drawn_motions
|
||||
// TODO: group by hour and have it last nr-of-hours * interval
|
||||
// TODO: then, add a timeline
|
||||
// TODO: then, add an overlay with relevant events, e.g.
|
||||
// TODO: when a the oldest object was moved, the newest object,
|
||||
// TODO: when a an object wasn't moved for very long,
|
||||
// TODO: .. and any other filter
|
||||
// TODO: then finish with a summary per location
|
||||
// TODO: .. (or add these in small prints as counters under the location names)
|
||||
this.interval = setInterval(this.tick.bind(this), 500);
|
||||
|
||||
// clear the motions when kicking off:
|
||||
this.motions.update(items => []);
|
||||
}
|
||||
|
||||
tick() {
|
||||
const n = 10
|
||||
if (this.step >= this.allMovements.length) {
|
||||
console.log('this', 'done')
|
||||
// todo: ease out all entries
|
||||
this.nextScene()
|
||||
return;
|
||||
}
|
||||
let movements: Movement[] = this.allMovements.slice(this.step, this.step + n);
|
||||
|
||||
// duration 5000 + Math.random() * 10000
|
||||
let motions: Motion[] = movements.map((m) => ({ duration: 5000, movement: m }));
|
||||
|
||||
this.motions.update(items => ([...items, ...motions]))
|
||||
this.step += n;
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearInterval(this.interval)
|
||||
}
|
||||
|
||||
scene_type = 'dedicated' //dedicated (like timeline) or overlay (like oldest)
|
||||
|
||||
}
|
||||
|
||||
export class Timeline extends Scene {
|
||||
movements: Movement[]
|
||||
item: Item
|
||||
motions: Writable<Motion[]>
|
||||
interval: number
|
||||
step: number = 0
|
||||
nextScene: CallableFunction
|
||||
locationCounts = new Map<Location, { in: number, out: number }>()
|
||||
|
||||
constructor(data: Data, viz_data: VizData, nextScene: CallableFunction) {
|
||||
super()
|
||||
|
||||
this.nextScene = nextScene
|
||||
|
||||
// start setInterval to trigger additions per 100 or so to drawn_movements (rendered on map)
|
||||
// when done, trigger parent.done()
|
||||
console.log('s',data.occurences)
|
||||
const [ item, movements ]= this.pickMovements(data.occurences);
|
||||
this.item = item
|
||||
this.movements = movements
|
||||
console.log(item, movements)
|
||||
|
||||
this.motions = viz_data.drawn_motions
|
||||
this.motions.update(items => [])
|
||||
|
||||
this.interval = setInterval(this.tick.bind(this), 3000);
|
||||
|
||||
|
||||
}
|
||||
|
||||
pickMovements(occurences: Map<Item, Movement[]>) {
|
||||
|
||||
const item_movements = [...occurences]
|
||||
// TODO: variable lenght, prefer with most steps
|
||||
.filter(([item, movements]) => movements.length > 1)
|
||||
|
||||
return item_movements[Math.floor(Math.random() * item_movements.length)]
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this.step >= this.movements.length) {
|
||||
console.log('this', 'done')
|
||||
// todo: ease out all entries
|
||||
this.nextScene()
|
||||
return;
|
||||
}
|
||||
|
||||
// duration 5000 + Math.random() * 10000
|
||||
const motion: Motion = { duration: 2000, movement: this.movements[this.step] };
|
||||
console.log(motion, motion.movement.source, motion.movement.target)
|
||||
this.motions.update(items => ([...items, motion]))
|
||||
this.step += 1;
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearInterval(this.interval)
|
||||
}
|
||||
|
||||
scene_type = 'dedicated' //dedicated (like timeline) or overlay (like oldest)
|
||||
|
||||
}
|
Loading…
Reference in a new issue