Use SCSS as preprocessor and WIP 'scenes' setup
This commit is contained in:
parent
070c3ef848
commit
1789731ab9
6 changed files with 452 additions and 131 deletions
41
package-lock.json
generated
41
package-lock.json
generated
|
@ -9,9 +9,12 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||||
|
"@sveu/browser": "^1.0.1",
|
||||||
"@tsconfig/svelte": "^5.0.2",
|
"@tsconfig/svelte": "^5.0.2",
|
||||||
|
"sass": "^1.77.2",
|
||||||
"svelte": "^4.2.12",
|
"svelte": "^4.2.12",
|
||||||
"svelte-check": "^3.6.7",
|
"svelte-check": "^3.6.7",
|
||||||
|
"svelte-preprocess": "^5.1.4",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.2.0"
|
"vite": "^5.2.0"
|
||||||
|
@ -728,6 +731,21 @@
|
||||||
"vite": "^5.0.0"
|
"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": {
|
"node_modules/@tsconfig/svelte": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.4.tgz",
|
||||||
|
@ -1095,6 +1113,12 @@
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||||
|
@ -1546,6 +1570,23 @@
|
||||||
"rimraf": "^2.5.2"
|
"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": {
|
"node_modules/sorcery": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
|
||||||
|
|
|
@ -11,9 +11,12 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||||
|
"@sveu/browser": "^1.0.1",
|
||||||
"@tsconfig/svelte": "^5.0.2",
|
"@tsconfig/svelte": "^5.0.2",
|
||||||
|
"sass": "^1.77.2",
|
||||||
"svelte": "^4.2.12",
|
"svelte": "^4.2.12",
|
||||||
"svelte-check": "^3.6.7",
|
"svelte-check": "^3.6.7",
|
||||||
|
"svelte-preprocess": "^5.1.4",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.2.0"
|
"vite": "^5.2.0"
|
||||||
|
|
319
src/Viz.svelte
319
src/Viz.svelte
|
@ -1,11 +1,23 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import parsed_requests from "/data/parsed_requests.json";
|
import parsed_requests from "/data/parsed_requests.json";
|
||||||
|
import { draw, slide } from "svelte/transition";
|
||||||
|
import { fps } from "@sveu/browser";
|
||||||
|
|
||||||
import { draw } from "svelte/transition";
|
import { All, Scene, Timeline } from "./scenes/Scenes";
|
||||||
|
import {
|
||||||
|
type Location,
|
||||||
|
type Movement,
|
||||||
|
type Item,
|
||||||
|
type Motion,
|
||||||
|
get_path_d,
|
||||||
|
} from "./lib/types";
|
||||||
|
|
||||||
// these are passed from main.ts (or vice versaas)
|
// these are passed from main.ts (or vice versaas)
|
||||||
export let width = 0;
|
export let width = 0;
|
||||||
export let height = 0;
|
export let height = 0;
|
||||||
|
export let dev: Boolean = false;
|
||||||
|
|
||||||
|
const _fps = fps();
|
||||||
|
|
||||||
const node_positions_percentages = {
|
const node_positions_percentages = {
|
||||||
APM: [13.829853909464873, 11.277305564859935],
|
APM: [13.829853909464873, 11.277305564859935],
|
||||||
|
@ -26,65 +38,69 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const node_positions = Object.fromEntries(
|
const node_positions = Object.fromEntries(
|
||||||
Object.entries(node_positions_percentages).map(([k, p]) => [
|
Object.entries(node_positions_percentages).map(([k, p]) => [
|
||||||
k,
|
k,
|
||||||
[(p[0] / 100) * width, (p[1] / 100) * height],
|
[(p[0] / 100) * width, (p[1] / 100) * height],
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
$: console.log(parsed_requests);
|
$: console.log(parsed_requests);
|
||||||
|
|
||||||
// preprocess data
|
// preprocess data
|
||||||
const nodes = parsed_requests.nodes;
|
const _nodes: Location[] = parsed_requests.nodes;
|
||||||
const edges: Array<Object> = parsed_requests.edges;
|
const _requests: Array<Object> = parsed_requests.edges;
|
||||||
|
|
||||||
|
const items = new Map<string, Item>();
|
||||||
|
const movements: Array<Movement> = [];
|
||||||
|
|
||||||
// filter nodes with only having both Latitude and Longitude.
|
// filter nodes with only having both Latitude and Longitude.
|
||||||
// then map these coordinates to the canvas space
|
// then map these coordinates to the canvas space
|
||||||
const valid_nodes = nodes
|
const locations = _nodes
|
||||||
.filter((n) => n.lat && n.lon)
|
.filter((n) => n.lat && n.lon)
|
||||||
.map((node) => {
|
.map((node) => {
|
||||||
node["x"] = node_positions[node.code][0];
|
node["x"] = node_positions[node.code][0];
|
||||||
node["y"] = node_positions[node.code][1];
|
node["y"] = node_positions[node.code][1];
|
||||||
// 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;
|
return node;
|
||||||
});
|
});
|
||||||
|
|
||||||
// create an index to access the node objects by their name
|
// create an index to access the node objects by their name
|
||||||
|
const nodeMap: Map<String, Location> = new Map(
|
||||||
|
locations.map((d) => [d["name"], d]),
|
||||||
|
);
|
||||||
|
|
||||||
const nodeMap = Object.fromEntries(valid_nodes.map((d) => [d["name"], d]));
|
_requests
|
||||||
const edgeIndexBarcode = buildIndex(edges, "Barcode");
|
.filter((n) => n['Owning Library Name'] != n['Pickup Location'])
|
||||||
// console.log(edgeIndexBarcode);
|
|
||||||
|
|
||||||
const movements = edges
|
|
||||||
.filter(
|
.filter(
|
||||||
(l) =>
|
(l) =>
|
||||||
nodeMap[l["Owning Library Name"]] &&
|
nodeMap.has(l["Owning Library Name"]) &&
|
||||||
nodeMap[l["Pickup Location"]],
|
nodeMap.has(l["Pickup Location"]),
|
||||||
)
|
)
|
||||||
.map((l, idx) => {
|
.forEach((r, idx) => {
|
||||||
l.source = nodeMap[l["Owning Library Name"]];
|
const identifier: String = r["Barcode"];
|
||||||
l.target = nodeMap[l["Pickup Location"]];
|
if (!items.has(identifier)) {
|
||||||
l.nr = idx;
|
items.set(identifier, {
|
||||||
return l;
|
title: r["Title (Complete)"],
|
||||||
|
MMS: r["MMS Id"],
|
||||||
|
Barcode: r["Barcode"],
|
||||||
|
Publisher: r["Publisher"],
|
||||||
|
_original: r,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let movement: Movement = {
|
||||||
|
source: nodeMap.get(r["Owning Library Name"]),
|
||||||
|
target: nodeMap.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");
|
||||||
|
// console.log(edgeIndexBarcode);
|
||||||
|
|
||||||
function degreesToRadians(degrees: number) {
|
function degreesToRadians(degrees: number) {
|
||||||
return (degrees * Math.PI) / 180;
|
return (degrees * Math.PI) / 180;
|
||||||
|
@ -118,118 +134,107 @@
|
||||||
[index: string]: Array<number>;
|
[index: string]: Array<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildIndex(objectList: Array<Object>, field: string): IndexObject {
|
// function buildIndex(objectList: Array<Object>, field: string): IndexObject {
|
||||||
const index = {};
|
// const index = {};
|
||||||
for (const idx in objectList) {
|
// for (const idx in objectList) {
|
||||||
const v = objectList[idx][field];
|
// const v = objectList[idx][field];
|
||||||
if (!index.hasOwnProperty(v)) {
|
// if (!index.hasOwnProperty(v)) {
|
||||||
index[v] = [idx];
|
// index[v] = [idx];
|
||||||
} else {
|
// } else {
|
||||||
index[v].push(idx);
|
// index[v].push(idx);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return index;
|
// return index;
|
||||||
}
|
// }
|
||||||
|
|
||||||
function get_path_d(movement) {
|
const occurences = new Map<Item, Movement[]>();
|
||||||
const m = movement;
|
movements.forEach((movement, idx) => {
|
||||||
let sourceX, targetX, midX, dx, dy, angle;
|
let movements = occurences.get(movement.item) ?? []
|
||||||
|
movements.push(movement)
|
||||||
|
occurences.set(movement.item, movements);
|
||||||
|
});
|
||||||
|
|
||||||
// This mess makes the arrows exactly perfect.
|
let drawn_motions = writable(<Motion[]>[]); //.filter((m, i) => i < 100);
|
||||||
// thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4
|
$: opacity = Math.max(0.055, Math.min(1, 200 / $drawn_motions.length));
|
||||||
if (m.source.x < m.target.x) {
|
let overlay_motions = writable(<Motion[]>[]); //.filter((m, i) => i < 100);
|
||||||
sourceX = m.source.x;
|
let events = writable(<Event[]>[]); //.filter((m, i) => i < 100);
|
||||||
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 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.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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(movements);
|
|
||||||
|
|
||||||
let drawn_movements = movements; //.filter((m, i) => i < 100);
|
|
||||||
|
|
||||||
import { onMount, onDestroy } from "svelte";
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Next scene", currentSceneI);
|
||||||
|
currentScene = new scenes[currentSceneI](
|
||||||
|
movements,
|
||||||
|
drawn_motions,
|
||||||
|
nextScene,
|
||||||
|
occurences,
|
||||||
|
);
|
||||||
|
currentSceneI = (currentSceneI + 1) % scenes.length;
|
||||||
|
}
|
||||||
|
|
||||||
let paused = false;
|
let paused = false;
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let interval = null;
|
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,
|
||||||
|
// );
|
||||||
|
|
||||||
interval = setInterval(() => {
|
// // if (remainingTime === 0) {
|
||||||
if (paused) {
|
// // clearInterval(interval);
|
||||||
return;
|
// // deleteTimer();
|
||||||
}
|
// // }
|
||||||
// TODO: change scene
|
// }, 2000);
|
||||||
// timeElapsed += 1;
|
|
||||||
|
|
||||||
// if (remainingTime === 0) {
|
|
||||||
// clearInterval(interval);
|
|
||||||
// deleteTimer();
|
|
||||||
// }
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
currentScene.stop();
|
||||||
}
|
// clearInterval(interval);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>library of <span id="motiontitle">motions</span></h1>
|
<h1>library of <span id="motiontitle">motions</span></h1>
|
||||||
<div id="about">
|
<div id="about">
|
||||||
Work by <em>Ruben van de Ven</em> for the
|
<span>Work by <em>Ruben van de Ven</em> for the
|
||||||
<em>University of Amsterdam Library</em>. Fonts by
|
<em>University of Amsterdam Library</em>.</span>
|
||||||
<em>Open Source Publishing</em>, map by <em>OpenStreetMap</em>.
|
<span>Fonts by <em>Open Source Publishing</em>.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<svg {width} {height}>
|
<svg {width} {height}>
|
||||||
<g id="movements">
|
<g id="movements">
|
||||||
{#each drawn_movements as m}
|
{#each $drawn_motions as m}
|
||||||
<path
|
<path
|
||||||
in:draw|global={{ duration: 5000 + Math.random() * 10000 }}
|
in:draw|global={{ duration: m.duration }}
|
||||||
d={get_path_d(m)}
|
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>
|
></path>
|
||||||
{/each}
|
{/each}
|
||||||
</g>
|
</g>
|
||||||
<g id="libraries">
|
<g id="libraries">
|
||||||
{#each valid_nodes as node}
|
{#each locations as node}
|
||||||
<g id={node.code} transform="translate({node.x}, {node.y})">
|
<g id={node.code} transform="translate({node.x}, {node.y})">
|
||||||
<circle r="5"></circle>
|
<circle r="5"></circle>
|
||||||
<circle r="20"></circle>
|
<circle r="20"></circle>
|
||||||
|
@ -239,7 +244,52 @@
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<style>
|
{#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; */
|
||||||
|
}
|
||||||
#about {
|
#about {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 8pt;
|
font-size: 8pt;
|
||||||
|
@ -247,6 +297,10 @@
|
||||||
right: 20px;
|
right: 20px;
|
||||||
width: 450px;
|
width: 450px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
|
span{
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#libraries circle:first-child {
|
#libraries circle:first-child {
|
||||||
fill: white;
|
fill: white;
|
||||||
|
@ -258,10 +312,15 @@
|
||||||
stroke-width: 10;
|
stroke-width: 10;
|
||||||
}
|
}
|
||||||
path {
|
path {
|
||||||
stroke: #bc89ff15;
|
stroke: #bc89ff;
|
||||||
stroke-width: 2px;
|
stroke-width: 2px;
|
||||||
fill: none;
|
fill: none;
|
||||||
}
|
}
|
||||||
|
#overlay_motions path {
|
||||||
|
stroke: red;
|
||||||
|
stroke-width: 4;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
text {
|
text {
|
||||||
fill: white;
|
fill: white;
|
||||||
font-size: 30pt;
|
font-size: 30pt;
|
||||||
|
|
89
src/lib/types.ts
Normal file
89
src/lib/types.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// An event to trigger drawing an Edge
|
||||||
|
export type Motion = {
|
||||||
|
duration: number,
|
||||||
|
movement: Movement,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Event = {
|
||||||
|
date: Date,
|
||||||
|
title: String,
|
||||||
|
description: String
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
}
|
|
@ -11,7 +11,8 @@ const app = new Viz({
|
||||||
target: document.getElementById('viz')!,
|
target: document.getElementById('viz')!,
|
||||||
props: {
|
props: {
|
||||||
width: width,
|
width: width,
|
||||||
height: height
|
height: height,
|
||||||
|
dev: true
|
||||||
},
|
},
|
||||||
// intro: true,
|
// intro: true,
|
||||||
})
|
})
|
||||||
|
|
128
src/scenes/Scenes.ts
Normal file
128
src/scenes/Scenes.ts
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
import type { Item, Motion, Movement } 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(movements: Movement[], motions: Writable<Motion[]>, nextScene: CallableFunction, occurences: Map<Item, Movements[]>) {
|
||||||
|
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 = movements;
|
||||||
|
|
||||||
|
this.motions = 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(allMovements: Movement[], motions: Writable<Motion[]>, nextScene: CallableFunction, occurences: Map<Item, Movements[]>) {
|
||||||
|
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',occurences)
|
||||||
|
const [ item, movements ]= this.pickMovements(occurences);
|
||||||
|
this.item = item
|
||||||
|
this.movements = movements
|
||||||
|
console.log(item, movements)
|
||||||
|
|
||||||
|
this.motions = 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