Compare commits
2 Commits
54e26f170a
...
8a38d407c4
Author | SHA1 | Date | |
---|---|---|---|
8a38d407c4 | |||
e80a13b1d8 |
232
site/src/components/FlowArea.svelte
Normal file
232
site/src/components/FlowArea.svelte
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
type FuncBase = (n: number) => number;
|
||||||
|
|
||||||
|
let {
|
||||||
|
grid = true,
|
||||||
|
grid_size = 35,
|
||||||
|
grid_scale_size = 1,
|
||||||
|
show_controll_buttons = false,
|
||||||
|
controll_buttons,
|
||||||
|
children,
|
||||||
|
onmousedown: ext_onmousedown,
|
||||||
|
onmouseup: ext_onmouseup,
|
||||||
|
onmouseleave: ext_onmouseleave,
|
||||||
|
onmousemove: ext_onmousemove
|
||||||
|
}: {
|
||||||
|
children?: Snippet<[FuncBase, FuncBase, FuncBase, FuncBase]>;
|
||||||
|
grid?: boolean;
|
||||||
|
grid_size?: number;
|
||||||
|
grid_scale_size?: number;
|
||||||
|
controll_buttons?: Snippet;
|
||||||
|
show_controll_buttons?: boolean;
|
||||||
|
onmousedown?: (e: MouseEvent) => void;
|
||||||
|
onmouseup?: (e: MouseEvent) => void;
|
||||||
|
onmouseleave?: (e: MouseEvent) => void;
|
||||||
|
onmousemove?: (e: MouseEvent) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Display stuff
|
||||||
|
//
|
||||||
|
|
||||||
|
// The canvas is not really a canvas but a div will kinda act like a canvas
|
||||||
|
let canvas: HTMLDivElement | undefined = $state();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Position + movement + interation
|
||||||
|
//
|
||||||
|
//
|
||||||
|
let y = $state(0);
|
||||||
|
let x = $state(0);
|
||||||
|
|
||||||
|
let initialGridOffsetX = $state(0);
|
||||||
|
let initialGridOffsetY = $state(0);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
let box = canvas?.getBoundingClientRect();
|
||||||
|
if (!box) return;
|
||||||
|
let w = box.width;
|
||||||
|
let h = box.height;
|
||||||
|
initialGridOffsetX = Math.round(w / 2 - Math.round(w / 2 / grid_size) * grid_size);
|
||||||
|
initialGridOffsetY = Math.round(h / 2 - Math.floor(h / 2 / grid_size) * grid_size);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Holdes the position for pan and select
|
||||||
|
let startMX = $state(0);
|
||||||
|
let startMY = $state(0);
|
||||||
|
let startX = $state(0);
|
||||||
|
let startY = $state(0);
|
||||||
|
let isDragging = $state(false);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Mouse interation
|
||||||
|
//
|
||||||
|
// Right click and drag to move around
|
||||||
|
function onmousedown(e: MouseEvent) {
|
||||||
|
ext_onmousedown?.(e);
|
||||||
|
if (e.defaultPrevented) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let box = canvas?.getBoundingClientRect();
|
||||||
|
const mouseX = e.x - (box?.left ?? 0);
|
||||||
|
const mouseY = e.y - (box?.top ?? 0);
|
||||||
|
|
||||||
|
startMX = mouseX;
|
||||||
|
startMY = mouseY;
|
||||||
|
if (e.button === 2 || e.button === 1) {
|
||||||
|
isDragging = true;
|
||||||
|
startX = x;
|
||||||
|
startY = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onmouseup(e: MouseEvent) {
|
||||||
|
ext_onmouseup?.(e);
|
||||||
|
if (e.defaultPrevented) return;
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
isDragging = false;
|
||||||
|
startX = 0;
|
||||||
|
startY = 0;
|
||||||
|
startMX = 0;
|
||||||
|
startMY = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the mouse leaves the area clear all the stuffs
|
||||||
|
function onmouseleave(e: MouseEvent) {
|
||||||
|
ext_onmouseleave?.(e);
|
||||||
|
if (e.defaultPrevented) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onmousemove(e: MouseEvent) {
|
||||||
|
ext_onmousemove?.(e);
|
||||||
|
if (e.defaultPrevented) return;
|
||||||
|
if (isDragging) {
|
||||||
|
let box = canvas?.getBoundingClientRect();
|
||||||
|
let mouseX = e.x - (box?.left ?? 0);
|
||||||
|
let mouseY = e.y - (box?.top ?? 0);
|
||||||
|
x = startX + (startMX - mouseX);
|
||||||
|
y = startY + (startMY - mouseY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For now disable the right click menu
|
||||||
|
*/
|
||||||
|
function oncontextmenu(e: MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Utils
|
||||||
|
//
|
||||||
|
//
|
||||||
|
function canvasX(inX: number) {
|
||||||
|
let box = canvas?.getBoundingClientRect();
|
||||||
|
return inX * grid_size + Math.round((box?.width ?? 0) / 2) - x;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canvasY(inY: number) {
|
||||||
|
let box = canvas?.getBoundingClientRect();
|
||||||
|
return Math.round((box?.height ?? 0) / 2) - y - inY * grid_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function worldX(canvasX: number) {
|
||||||
|
let box = canvas?.getBoundingClientRect();
|
||||||
|
return (canvasX + x - Math.round((box?.width ?? 0) / 2)) / grid_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function worldY(canvasY: number) {
|
||||||
|
let box = canvas?.getBoundingClientRect();
|
||||||
|
return (Math.round((box?.height ?? 0) / 2) - y - canvasY) / grid_size;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full flex-grow p-5 px-7">
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-lg w-full h-full grid relative overflow-hidden"
|
||||||
|
style="--grid-size: {grid_size * grid_scale_size}px;"
|
||||||
|
role="none"
|
||||||
|
bind:this={canvas}
|
||||||
|
{onmousedown}
|
||||||
|
{onmouseup}
|
||||||
|
{oncontextmenu}
|
||||||
|
{onmousemove}
|
||||||
|
{onmouseleave}
|
||||||
|
>
|
||||||
|
{#if grid}
|
||||||
|
<!-- Background grid -->
|
||||||
|
<!-- x -->
|
||||||
|
<div class="absolute background" style="--offset-y: {-y + initialGridOffsetY}px;"></div>
|
||||||
|
<!-- y -->
|
||||||
|
<div
|
||||||
|
class="absolute background"
|
||||||
|
style="--grid-angle: 90deg; --offset-x: {-x + initialGridOffsetX}px;"
|
||||||
|
></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if show_controll_buttons}
|
||||||
|
<!-- Control buttons -->
|
||||||
|
<div class="absolute left-5 top-5 flex gap-5 z-50">
|
||||||
|
<!-- Reset view port -->
|
||||||
|
{#if x !== 0 || y !== 0}
|
||||||
|
<button
|
||||||
|
class="bg-white p-2 shadow rounded-lg shadow-violet-500 h-[40px] w-[40px] text-violet-500"
|
||||||
|
onclick={() => {
|
||||||
|
x = 0;
|
||||||
|
y = 0;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="bi bi-arrow-clockwise"></span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if controll_buttons}
|
||||||
|
{@render controll_buttons()}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if children}
|
||||||
|
{@render children(canvasX, canvasY, worldX, worldY)}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--grid-color: #c1c1c1;
|
||||||
|
--grid-size: 35px;
|
||||||
|
--border-size: 1px;
|
||||||
|
--grid-angle: 0deg;
|
||||||
|
--offset-x: 0px;
|
||||||
|
--offset-y: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: -1px;
|
||||||
|
bottom: -1px;
|
||||||
|
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
var(--grid-angle),
|
||||||
|
transparent,
|
||||||
|
transparent calc(var(--grid-size) - var(--border-size)),
|
||||||
|
var(--grid-color) calc(var(--grid-size) - var(--border-size)),
|
||||||
|
var(--grid-color) var(--grid-size)
|
||||||
|
);
|
||||||
|
|
||||||
|
background-position: var(--offset-x) var(--offset-y);
|
||||||
|
background-size: var(--grid-size) var(--grid-size);
|
||||||
|
}
|
||||||
|
</style>
|
29
site/src/components/types.ts
Normal file
29
site/src/components/types.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
export interface Point {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rect extends Point {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractLinkNodePosX(canvasX: (x: number) => number, rect: Rect, grid_size: number) {
|
||||||
|
if (rect.x === -1) {
|
||||||
|
return canvasX(rect.x);
|
||||||
|
}
|
||||||
|
if (rect.x === rect.width) {
|
||||||
|
return canvasX(rect.x) + rect.width * grid_size;
|
||||||
|
}
|
||||||
|
return canvasX(rect.x) + rect.x * grid_size + grid_size / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractLinkNodePosY(canvasY: (x: number) => number, node: Rect, grid_size: number) {
|
||||||
|
if (node.y === -1) {
|
||||||
|
return canvasY(node.y);
|
||||||
|
}
|
||||||
|
if (node.y === node.height) {
|
||||||
|
return canvasY(node.y) + node.height * grid_size;
|
||||||
|
}
|
||||||
|
return canvasY(node.y) + node.y * grid_size + grid_size / 2;
|
||||||
|
}
|
@ -47,7 +47,21 @@ function createStatusStore() {
|
|||||||
nodesR[null as any] = {
|
nodesR[null as any] = {
|
||||||
icon: 'plus',
|
icon: 'plus',
|
||||||
id: null as any,
|
id: null as any,
|
||||||
name: 'Created'
|
name: 'Created',
|
||||||
|
x: -4,
|
||||||
|
y: 10,
|
||||||
|
width: 8,
|
||||||
|
height: 4
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
nodesR['null'] = {
|
||||||
|
icon: 'plus',
|
||||||
|
id: null as any,
|
||||||
|
name: 'Created',
|
||||||
|
x: -4,
|
||||||
|
y: 10,
|
||||||
|
width: 8,
|
||||||
|
height: 4
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
for (const nodeId of nodesId) {
|
for (const nodeId of nodesId) {
|
||||||
|
@ -9,7 +9,8 @@
|
|||||||
import PayRange from './PayRange.svelte';
|
import PayRange from './PayRange.svelte';
|
||||||
import LineGraphs, { type LineGraphData } from './LineGraph.svelte';
|
import LineGraphs, { type LineGraphData } from './LineGraph.svelte';
|
||||||
import * as d3 from 'd3';
|
import * as d3 from 'd3';
|
||||||
import { countReducer } from './utils';
|
import { countReducer, type EventsStat } from './utils';
|
||||||
|
import CoolGraph from './CoolGraph/CoolGraph.svelte';
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
applicationStore.loadAll();
|
applicationStore.loadAll();
|
||||||
@ -22,6 +23,8 @@
|
|||||||
let viewGraph: { xs: number[]; ys: number[] } = $state({ xs: [], ys: [] });
|
let viewGraph: { xs: number[]; ys: number[] } = $state({ xs: [], ys: [] });
|
||||||
let statusGraph: LineGraphData = $state([]);
|
let statusGraph: LineGraphData = $state([]);
|
||||||
|
|
||||||
|
let events: EventsStat[] = $state([]);
|
||||||
|
|
||||||
let width: number = $state(300);
|
let width: number = $state(300);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
@ -37,19 +40,7 @@
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
type EventsStat = {
|
events = await get('events/');
|
||||||
// application_id
|
|
||||||
a: string;
|
|
||||||
// created time
|
|
||||||
c: number;
|
|
||||||
// id
|
|
||||||
i: string;
|
|
||||||
// new_status_id
|
|
||||||
s?: string;
|
|
||||||
// Type
|
|
||||||
t: number;
|
|
||||||
};
|
|
||||||
const events: EventsStat[] = await get('events/');
|
|
||||||
|
|
||||||
const pre_created_graph = events
|
const pre_created_graph = events
|
||||||
.filter((a) => a.t === 0)
|
.filter((a) => a.t === 0)
|
||||||
@ -125,16 +116,12 @@
|
|||||||
data={[...statusStore.nodes, 'Created' as const].reduce(
|
data={[...statusStore.nodes, 'Created' as const].reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
if (item === 'Created') {
|
if (item === 'Created') {
|
||||||
const count = applicationStore.all.filter(
|
const count = applicationStore.all.filter((a) => a.status_id === null).length;
|
||||||
(a) => a.status_id === null
|
|
||||||
).length;
|
|
||||||
if (count === 0) return acc;
|
if (count === 0) return acc;
|
||||||
acc[item] = count;
|
acc[item] = count;
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
const count = applicationStore.all.filter(
|
const count = applicationStore.all.filter((a) => item.id === a.status_id).length;
|
||||||
(a) => item.id === a.status_id
|
|
||||||
).length;
|
|
||||||
if (count === 0) return acc;
|
if (count === 0) return acc;
|
||||||
acc[item.name] = count;
|
acc[item.name] = count;
|
||||||
return acc;
|
return acc;
|
||||||
@ -237,8 +224,7 @@
|
|||||||
sensitivity={0.01}
|
sensitivity={0.01}
|
||||||
data={applicationStore.all.reduce(
|
data={applicationStore.all.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
const l =
|
const l = item.inperson_type === '' ? 'Unknown' : item.inperson_type;
|
||||||
item.inperson_type === '' ? 'Unknown' : item.inperson_type;
|
|
||||||
if (acc[l]) {
|
if (acc[l]) {
|
||||||
acc[l] += 1;
|
acc[l] += 1;
|
||||||
} else {
|
} else {
|
||||||
@ -326,9 +312,7 @@
|
|||||||
.replace(/[^\d\-–]/g, '')
|
.replace(/[^\d\-–]/g, '')
|
||||||
.replace(/–/g, '-')
|
.replace(/–/g, '-')
|
||||||
.split('-');
|
.split('-');
|
||||||
return (
|
return Number(payrange[0]) + Number(payrange[1] ?? payrange[0]);
|
||||||
Number(payrange[0]) + Number(payrange[1] ?? payrange[0])
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.reduce((acc, a) => acc + a, 0) /
|
.reduce((acc, a) => acc + a, 0) /
|
||||||
(fapps.length * 2)
|
(fapps.length * 2)
|
||||||
@ -406,11 +390,7 @@
|
|||||||
.replace(/[^\d\-–]/g, '')
|
.replace(/[^\d\-–]/g, '')
|
||||||
.replace(/–/g, '-')
|
.replace(/–/g, '-')
|
||||||
.split('-');
|
.split('-');
|
||||||
return (
|
return (Number(payrange[0]) + Number(payrange[1] ?? payrange[0])) / 2;
|
||||||
(Number(payrange[0]) +
|
|
||||||
Number(payrange[1] ?? payrange[0])) /
|
|
||||||
2
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.reduce(
|
.reduce(
|
||||||
(acc, a) => {
|
(acc, a) => {
|
||||||
@ -432,6 +412,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
<h1>Cool graph</h1>
|
||||||
|
<CoolGraph {events} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
196
site/src/routes/graphs/CoolGraph/CoolGraph.svelte
Normal file
196
site/src/routes/graphs/CoolGraph/CoolGraph.svelte
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { statusStore } from '$lib/Types.svelte';
|
||||||
|
import FlowArea from '../../../components/FlowArea.svelte';
|
||||||
|
import type { PrevType } from './types';
|
||||||
|
import { type EventsStat } from '../utils';
|
||||||
|
import CoolGraphNode from './CoolGraphNode.svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
events
|
||||||
|
}: {
|
||||||
|
events: EventsStat[];
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
// Remove view events
|
||||||
|
const internalEvs = $derived(events.filter((a) => a.t !== 2).toSorted((a, b) => a.c - b.c));
|
||||||
|
|
||||||
|
let depth = $state(0);
|
||||||
|
|
||||||
|
let applicationsTimelines = $derived.by(() => {
|
||||||
|
let appState: Record<string, EventsStat[]> = {};
|
||||||
|
for (const ev of internalEvs) {
|
||||||
|
if (appState[ev.a] !== undefined) {
|
||||||
|
if (ev.t == 0) continue;
|
||||||
|
appState[ev.a].push(ev);
|
||||||
|
} else {
|
||||||
|
if (ev.t == 1) continue;
|
||||||
|
appState[ev.a] = [ev];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.values(appState);
|
||||||
|
});
|
||||||
|
|
||||||
|
let figHeight = $state(0);
|
||||||
|
let figWidth = $state(0);
|
||||||
|
|
||||||
|
type Pos = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
count: number;
|
||||||
|
from: PrevType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const posByDepth = $derived.by(() => {
|
||||||
|
if (applicationsTimelines.length === 0) return [];
|
||||||
|
|
||||||
|
const levels: Record<string, Pos>[] = [];
|
||||||
|
|
||||||
|
let timelines = [...applicationsTimelines];
|
||||||
|
while (timelines.length > 0) {
|
||||||
|
const s: Record<string, Pos> = {};
|
||||||
|
|
||||||
|
const nextTurnTimeLines: typeof timelines = [];
|
||||||
|
|
||||||
|
const thisX = levels.length;
|
||||||
|
|
||||||
|
// Deepclone prev
|
||||||
|
if (thisX > 0) {
|
||||||
|
for (const e of Object.keys(levels[thisX - 1])) {
|
||||||
|
s[e] = {
|
||||||
|
...levels[thisX - 1][e],
|
||||||
|
from: {
|
||||||
|
prev: levels[thisX - 1][e].from,
|
||||||
|
cur: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependencies: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
const thisLine = new Set<string>();
|
||||||
|
|
||||||
|
for (const t of timelines) {
|
||||||
|
const cur = t[thisX];
|
||||||
|
// Only allow create as the first event of the timeline
|
||||||
|
if ((cur.t === 0 && thisX !== 0) || (cur.t === 1 && thisX === 0)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const st = cur.s ?? 'null';
|
||||||
|
thisLine.add(st);
|
||||||
|
|
||||||
|
let prev: string | undefined = undefined;
|
||||||
|
if (thisX > 0) {
|
||||||
|
prev = t[thisX - 1].s ?? 'null';
|
||||||
|
if (dependencies[st]) {
|
||||||
|
dependencies[st].push(prev);
|
||||||
|
} else {
|
||||||
|
dependencies[st] = [prev];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s[st]) {
|
||||||
|
s[st].count += 1;
|
||||||
|
s[st].x = thisX;
|
||||||
|
if (prev) {
|
||||||
|
if (s[st].from.cur[prev]) {
|
||||||
|
s[st].from.cur[prev] += 1;
|
||||||
|
} else {
|
||||||
|
s[st].from.cur[prev] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s[st] = {
|
||||||
|
x: thisX,
|
||||||
|
y: 0,
|
||||||
|
from: { cur: {} },
|
||||||
|
count: 1
|
||||||
|
};
|
||||||
|
if (prev) s[st].from.cur[prev] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.length - 1 !== thisX) {
|
||||||
|
nextTurnTimeLines.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outer: for (let i = 0; i < thisX + 1; i++) {
|
||||||
|
for (const v of Object.values(s)) {
|
||||||
|
if (v.x === i) {
|
||||||
|
continue outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const v of Object.values(s)) {
|
||||||
|
if (v.x > i) {
|
||||||
|
v.x -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const max = Object.values(s).reduce((acc, a) => Math.max(acc, a.x), 0);
|
||||||
|
|
||||||
|
for (const tName of Object.keys(s)) {
|
||||||
|
const t = s[tName];
|
||||||
|
if (t.x != max) continue;
|
||||||
|
const thisLineDeps = dependencies[tName]?.filter((d) => thisLine.has(d)) ?? [];
|
||||||
|
if (thisLineDeps.length == 0) continue;
|
||||||
|
// If no cross dependencies
|
||||||
|
if (thisLineDeps.every((d) => !dependencies[d].includes(tName))) {
|
||||||
|
t.x += 1;
|
||||||
|
thisLine.delete(tName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO look for deps in the same line
|
||||||
|
|
||||||
|
const findY: Record<number, number> = {};
|
||||||
|
for (const target of Object.keys(s)) {
|
||||||
|
if (findY[s[target].x] === undefined) {
|
||||||
|
s[target].y = 0;
|
||||||
|
findY[s[target].x] = 1;
|
||||||
|
} else {
|
||||||
|
s[target].y = findY[s[target].x];
|
||||||
|
findY[s[target].x] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
levels.push(s);
|
||||||
|
|
||||||
|
timelines = nextTurnTimeLines;
|
||||||
|
}
|
||||||
|
return levels;
|
||||||
|
});
|
||||||
|
|
||||||
|
const levelSize = $derived(Math.floor(figWidth / posByDepth.length));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if statusStore.nodesR && posByDepth.length > 0}
|
||||||
|
<div>
|
||||||
|
<div class="flex">
|
||||||
|
<span>{depth}</span>
|
||||||
|
<input
|
||||||
|
class="flex-grow"
|
||||||
|
min="0"
|
||||||
|
max={posByDepth.length - 1}
|
||||||
|
type="range"
|
||||||
|
step="1"
|
||||||
|
bind:value={depth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col h-[1200px]">
|
||||||
|
<FlowArea grid_size={25 / 2} grid_scale_size={2}>
|
||||||
|
{#snippet children(canvasX, canvasY)}
|
||||||
|
{#each Object.keys(posByDepth[depth]) as nodeId}
|
||||||
|
{@const cur = posByDepth[depth][nodeId]}
|
||||||
|
{@const node = statusStore.nodesR[nodeId === 'null' ? (null as any) : nodeId]}
|
||||||
|
{#if node}
|
||||||
|
<CoolGraphNode {canvasX} {canvasY} {node} from={cur.from} count={cur.count} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/snippet}
|
||||||
|
</FlowArea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
182
site/src/routes/graphs/CoolGraph/CoolGraphNode.svelte
Normal file
182
site/src/routes/graphs/CoolGraph/CoolGraphNode.svelte
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { statusStore } from '$lib/Types.svelte';
|
||||||
|
import type { Node } from '../../flow/types';
|
||||||
|
import type { PrevType } from './types';
|
||||||
|
|
||||||
|
type FuncBase = (x: number) => number;
|
||||||
|
|
||||||
|
let {
|
||||||
|
node,
|
||||||
|
canvasX,
|
||||||
|
canvasY,
|
||||||
|
count,
|
||||||
|
from
|
||||||
|
}: {
|
||||||
|
node: Node;
|
||||||
|
canvasX: FuncBase;
|
||||||
|
canvasY: FuncBase;
|
||||||
|
from: PrevType;
|
||||||
|
count: number;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let fromText = $derived(
|
||||||
|
Object.keys(from.cur).reduce(
|
||||||
|
(acc, i) =>
|
||||||
|
`${acc}\n\t${i === 'null' ? 'Created' : statusStore.nodesR[i].name}: ${from.cur[i]}`,
|
||||||
|
'From:'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
type Particle = {
|
||||||
|
to: string;
|
||||||
|
percentage: number;
|
||||||
|
rand: number;
|
||||||
|
randX: number;
|
||||||
|
randY: number;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let particles: Particle[] = $state([]);
|
||||||
|
let particlesState: Record<string, number> = $state({});
|
||||||
|
|
||||||
|
let MAX_PARTICLES = 200;
|
||||||
|
|
||||||
|
function spwanParticle(key: string) {
|
||||||
|
if (particles.length > MAX_PARTICLES) return;
|
||||||
|
if (from.cur[key] <= particlesState[key]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
particles.push({
|
||||||
|
percentage: 0,
|
||||||
|
to: key,
|
||||||
|
rand: Math.random(),
|
||||||
|
randX: Math.random(),
|
||||||
|
randY: Math.random(),
|
||||||
|
color: [
|
||||||
|
'oklch(68.5% 0.169 237.323)',
|
||||||
|
'oklch(62.3% 0.214 259.815)',
|
||||||
|
'oklch(62.7% 0.265 303.9)'
|
||||||
|
][Math.floor(Math.random() * 3)]
|
||||||
|
});
|
||||||
|
particlesState[key] = (particlesState[key] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_ROUNDS = 40;
|
||||||
|
let timeout: number | undefined = undefined;
|
||||||
|
|
||||||
|
function t(k: number) {
|
||||||
|
if (k > MAX_ROUNDS || !from) {
|
||||||
|
timeout = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(from.cur)) {
|
||||||
|
spwanParticle(key);
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => t(k + 1), Math.random() * 1500) as unknown as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (timeout !== undefined) clearTimeout(timeout);
|
||||||
|
const _ = from;
|
||||||
|
particles = [];
|
||||||
|
timeout = setTimeout(() => t(0), 10) as unknown as number;
|
||||||
|
return () => {
|
||||||
|
if (timeout !== undefined) clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="shadow-sm shadow-violet-500 border rounded-full min-w-[50px] min-h-[50px] grid place-items-center bg-white z-40 absolute"
|
||||||
|
style="left: {canvasX(node.x + node.width / 2)}px; top: {canvasY(node.y - node.height / 2)}px;"
|
||||||
|
title={`${node?.name ?? ''}: ${count}\n${fromText}`}
|
||||||
|
>
|
||||||
|
<span class={`bi bi-${node?.icon ?? ''}`}> </span>
|
||||||
|
</div>
|
||||||
|
{#each particles as particle}
|
||||||
|
{@const to = statusStore.nodesR[particle.to]}
|
||||||
|
{@const f_x = to.x + to.width / 2 + particle.randX}
|
||||||
|
{@const f_y = to.y - to.height / 2 + particle.randY}
|
||||||
|
{@const x = (p: number) => f_x + (node.x + node.width / 2 - f_x) * p}
|
||||||
|
{@const y = (p: number) => f_y + (node.y - node.height / 2 - f_y) * p}
|
||||||
|
<div
|
||||||
|
class="animateMove absolute w-2 h-2 text-black rounded-full shadow-lg z-20"
|
||||||
|
style="--start-x: {canvasX(x(0)) + 25}px; --start-y: {canvasY(y(0)) + 25}px; --end-x: {canvasX(
|
||||||
|
x(1)
|
||||||
|
) + 25}px; --end-y: {canvasY(y(1)) + 25}px; background: {particle.color};"
|
||||||
|
></div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#each Object.keys(from.cur) as link}
|
||||||
|
{@const size = 20}
|
||||||
|
{@const to = statusStore.nodesR[link]}
|
||||||
|
{@const f_x = node.x + node.width / 2}
|
||||||
|
{@const f_y = node.y - node.height / 2}
|
||||||
|
{@const t_x = to.x + to.width / 2}
|
||||||
|
{@const t_y = to.y - to.height / 2}
|
||||||
|
{@const diff = Math.sqrt((f_x - t_x) ** 2 + (f_y - t_y) ** 2)}
|
||||||
|
{@const theta = Math.atan2(f_x - t_x, f_y - t_y)}
|
||||||
|
<div
|
||||||
|
class="absolute bg-gradient-to-t from-blue-400/70 to-blue-200/70 z-10"
|
||||||
|
style="height: {(25 / 2) *
|
||||||
|
diff}px; width: {size}px; transform-origin: top center; transform: translate({canvasX(f_x) +
|
||||||
|
25 -
|
||||||
|
size / 2}px, {canvasY(f_y) + 25 + size / 8}px) rotate({theta}rad);"
|
||||||
|
></div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if from.prev}
|
||||||
|
{#each Object.keys(from.prev.cur) as link}
|
||||||
|
{@const size = 10}
|
||||||
|
{@const to = statusStore.nodesR[link]}
|
||||||
|
{@const f_x = node.x + node.width / 2}
|
||||||
|
{@const f_y = node.y - node.height / 2}
|
||||||
|
{@const t_x = to.x + to.width / 2}
|
||||||
|
{@const t_y = to.y - to.height / 2}
|
||||||
|
{@const diff = Math.sqrt((f_x - t_x) ** 2 + (f_y - t_y) ** 2)}
|
||||||
|
{@const theta = Math.atan2(f_x - t_x, f_y - t_y)}
|
||||||
|
<div
|
||||||
|
class="absolute bg-gradient-to-t from-violet-400/70 to-violet-200/70"
|
||||||
|
style="height: {(25 / 2) *
|
||||||
|
diff}px; width: {size}px; transform-origin: top center; transform: translate({canvasX(f_x) +
|
||||||
|
25 -
|
||||||
|
size / 2}px, {canvasY(f_y) + 25 - size / 4}px) rotate({theta}rad);"
|
||||||
|
></div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if from.prev && from.prev.prev}
|
||||||
|
{#each Object.keys(from.prev.prev.cur) as link}
|
||||||
|
{@const size = 5}
|
||||||
|
{@const to = statusStore.nodesR[link]}
|
||||||
|
{@const f_x = node.x + node.width / 2}
|
||||||
|
{@const f_y = node.y - node.height / 2}
|
||||||
|
{@const t_x = to.x + to.width / 2}
|
||||||
|
{@const t_y = to.y - to.height / 2}
|
||||||
|
{@const diff = Math.sqrt((f_x - t_x) ** 2 + (f_y - t_y) ** 2)}
|
||||||
|
{@const theta = Math.atan2(f_x - t_x, f_y - t_y)}
|
||||||
|
<div
|
||||||
|
class="absolute bg-gradient-to-t from-violet-400/50 to-violet-200/50"
|
||||||
|
style="height: {(25 / 2) *
|
||||||
|
diff}px; width: {size}px; transform-origin: top center; transform: translate({canvasX(f_x) +
|
||||||
|
25 -
|
||||||
|
size / 2}px, {canvasY(f_y) + 25 - size / 4}px) rotate({theta}rad);"
|
||||||
|
></div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.animateMove {
|
||||||
|
animation: animateMoves 5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes animateMoves {
|
||||||
|
0% {
|
||||||
|
top: var(--start-y);
|
||||||
|
left: var(--start-x);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: var(--end-y);
|
||||||
|
left: var(--end-x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
1
site/src/routes/graphs/CoolGraph/types.ts
Normal file
1
site/src/routes/graphs/CoolGraph/types.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type PrevType = { prev?: PrevType; cur: Record<string, number> };
|
@ -1,489 +1,475 @@
|
|||||||
|
export type EventsStat = {
|
||||||
|
/** application_id */
|
||||||
|
a: string;
|
||||||
|
// created time
|
||||||
|
c: number;
|
||||||
|
// id
|
||||||
|
i: string;
|
||||||
|
// new_status_id
|
||||||
|
s?: string;
|
||||||
|
/** Type */
|
||||||
|
t: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type Sides = 'left' | 'right' | 'bot' | 'top';
|
export type Sides = 'left' | 'right' | 'bot' | 'top';
|
||||||
|
|
||||||
export type AxisProps = {
|
export type AxisProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
titleFontSize?: number;
|
titleFontSize?: number;
|
||||||
titlePos?: Sides;
|
titlePos?: Sides;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Axis {
|
export class Axis {
|
||||||
private _title?: string;
|
private _title?: string;
|
||||||
private _titleFontSize: number;
|
private _titleFontSize: number;
|
||||||
private _titlePos: Sides;
|
private _titlePos: Sides;
|
||||||
|
|
||||||
constructor({ title, titleFontSize, titlePos }: AxisProps = {}) {
|
constructor({ title, titleFontSize, titlePos }: AxisProps = {}) {
|
||||||
this._title = title;
|
this._title = title;
|
||||||
this._titleFontSize = titleFontSize ?? 15;
|
this._titleFontSize = titleFontSize ?? 15;
|
||||||
this._titlePos = titlePos ?? 'left';
|
this._titlePos = titlePos ?? 'left';
|
||||||
}
|
}
|
||||||
|
|
||||||
getPadding() {
|
getPadding() {
|
||||||
if (!this._title) return 0;
|
if (!this._title) return 0;
|
||||||
return this._titleFontSize + 4;
|
return this._titleFontSize + 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_title(
|
apply_title(
|
||||||
svg: d3.Selection<KnownAny, KnownAny, null, KnownAny>,
|
svg: d3.Selection<KnownAny, KnownAny, null, KnownAny>,
|
||||||
height: number,
|
height: number,
|
||||||
width: number,
|
width: number,
|
||||||
padding: number
|
padding: number
|
||||||
) {
|
) {
|
||||||
if (!this._title) return undefined;
|
if (!this._title) return undefined;
|
||||||
if (this._titlePos === 'left') {
|
if (this._titlePos === 'left') {
|
||||||
return (
|
return (
|
||||||
svg
|
svg
|
||||||
.append('text')
|
.append('text')
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
.attr('transform', 'rotate(-90)')
|
.attr('transform', 'rotate(-90)')
|
||||||
.attr('font-size', `${this._titleFontSize}px`)
|
.attr('font-size', `${this._titleFontSize}px`)
|
||||||
.attr('font-family', 'Open sans')
|
.attr('font-family', 'Open sans')
|
||||||
// y becomes x
|
// y becomes x
|
||||||
.attr('x', -height / 2)
|
.attr('x', -height / 2)
|
||||||
.attr('y', -padding + this._titleFontSize + 4)
|
.attr('y', -padding + this._titleFontSize + 4)
|
||||||
.text(this._title)
|
.text(this._title)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this._titlePos === 'right') {
|
if (this._titlePos === 'right') {
|
||||||
return (
|
return (
|
||||||
svg
|
svg
|
||||||
.append('text')
|
.append('text')
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
.attr('transform', 'rotate(90)')
|
.attr('transform', 'rotate(90)')
|
||||||
.attr('font-size', `${this._titleFontSize}px`)
|
.attr('font-size', `${this._titleFontSize}px`)
|
||||||
.attr('font-family', 'Open sans')
|
.attr('font-family', 'Open sans')
|
||||||
// y becomes x
|
// y becomes x
|
||||||
.attr('x', height / 2)
|
.attr('x', height / 2)
|
||||||
.attr('y', -width - padding + this._titleFontSize + 4)
|
.attr('y', -width - padding + this._titleFontSize + 4)
|
||||||
.text(this._title)
|
.text(this._title)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this._titlePos === 'bot') {
|
if (this._titlePos === 'bot') {
|
||||||
return svg
|
return svg
|
||||||
.append('text')
|
.append('text')
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
.attr('font-size', `${this._titleFontSize}px`)
|
.attr('font-size', `${this._titleFontSize}px`)
|
||||||
.attr('font-family', 'Open sans')
|
.attr('font-family', 'Open sans')
|
||||||
.attr('x', width / 2)
|
.attr('x', width / 2)
|
||||||
.attr('y', height + padding - 4)
|
.attr('y', height + padding - 4)
|
||||||
.text(this._title);
|
.text(this._title);
|
||||||
}
|
}
|
||||||
if (this._titlePos === 'top') {
|
if (this._titlePos === 'top') {
|
||||||
return svg
|
return svg
|
||||||
.append('text')
|
.append('text')
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
.attr('font-size', `${this._titleFontSize}px`)
|
.attr('font-size', `${this._titleFontSize}px`)
|
||||||
.attr('font-family', 'Open sans')
|
.attr('font-family', 'Open sans')
|
||||||
.attr('x', width / 2)
|
.attr('x', width / 2)
|
||||||
.attr('y', -padding + this._titleFontSize)
|
.attr('y', -padding + this._titleFontSize)
|
||||||
.text(this._title);
|
.text(this._title);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('Unknown title pos', this.titlePos);
|
console.error('Unknown title pos', this.titlePos);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Builder pattern functions
|
// Builder pattern functions
|
||||||
//
|
//
|
||||||
title(title?: string) {
|
title(title?: string) {
|
||||||
this._title = title;
|
this._title = title;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
titlePos(pos: Sides) {
|
titlePos(pos: Sides) {
|
||||||
this._titlePos = pos;
|
this._titlePos = pos;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
titleFontSize(size: number) {
|
titleFontSize(size: number) {
|
||||||
this._titleFontSize = size;
|
this._titleFontSize = size;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
enforcePos(pos: 'vert' | 'hoz', defaultPos: Sides) {
|
enforcePos(pos: 'vert' | 'hoz', defaultPos: Sides) {
|
||||||
if (pos === 'vert') {
|
if (pos === 'vert') {
|
||||||
if (this._titlePos !== 'top' && this._titlePos !== 'bot') {
|
if (this._titlePos !== 'top' && this._titlePos !== 'bot') {
|
||||||
return this.titlePos(defaultPos);
|
return this.titlePos(defaultPos);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this._titlePos !== 'left' && this._titlePos !== 'right') {
|
if (this._titlePos !== 'left' && this._titlePos !== 'right') {
|
||||||
return this.titlePos(defaultPos);
|
return this.titlePos(defaultPos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EnforceSizeType = [number, number];
|
export type EnforceSizeType = [number, number];
|
||||||
|
|
||||||
export function enforceSizeHelper<
|
export function enforceSizeHelper<H extends Record<NamesH, number>, NamesH extends string = ''>(
|
||||||
H extends Record<NamesH, number>,
|
width: number,
|
||||||
NamesH extends string = ''
|
height: number,
|
||||||
>(width: number, height: number, enforce: boolean): EnforceHelper<NamesH, H> {
|
enforce: boolean
|
||||||
const c = document.createElement('canvas');
|
): EnforceHelper<NamesH, H> {
|
||||||
const ctx = c.getContext('2d');
|
const c = document.createElement('canvas');
|
||||||
if (!ctx) throw new Error('Failed to get ctx');
|
const ctx = c.getContext('2d');
|
||||||
|
if (!ctx) throw new Error('Failed to get ctx');
|
||||||
|
|
||||||
const r = {
|
const r = {
|
||||||
_width: width,
|
_width: width,
|
||||||
_height: height,
|
_height: height,
|
||||||
_enforce: enforce,
|
_enforce: enforce,
|
||||||
_w_p: 0,
|
_w_p: 0,
|
||||||
|
|
||||||
_h_indicators: [] as Indicator<NamesH>[]
|
_h_indicators: [] as Indicator<NamesH>[]
|
||||||
} as EnforceHelper<NamesH, H>;
|
} as EnforceHelper<NamesH, H>;
|
||||||
|
|
||||||
r.h = (name, h, type, data, font) => {
|
r.h = (name, h, type, data, font) => {
|
||||||
const nr = r as EnforceHelper<
|
const nr = r as EnforceHelper<NamesH | typeof name, Record<NamesH | typeof name, number>>;
|
||||||
NamesH | typeof name,
|
|
||||||
Record<NamesH | typeof name, number>
|
|
||||||
>;
|
|
||||||
|
|
||||||
// TODO maybe update the values
|
// TODO maybe update the values
|
||||||
if (nr[name]) {
|
if (nr[name]) {
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
let size = 0;
|
let size = 0;
|
||||||
if (typeof h === 'string') {
|
if (typeof h === 'string') {
|
||||||
if (h.endsWith('rem')) {
|
if (h.endsWith('rem')) {
|
||||||
const rem = Number(h.substring(0, h.length - 3));
|
const rem = Number(h.substring(0, h.length - 3));
|
||||||
if (Number.isNaN(rem)) {
|
if (Number.isNaN(rem)) {
|
||||||
throw new Error('h is not a valid rem value');
|
throw new Error('h is not a valid rem value');
|
||||||
}
|
}
|
||||||
size =
|
size = rem * Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
rem *
|
} else {
|
||||||
Number.parseFloat(
|
const n = Number(h);
|
||||||
getComputedStyle(document.documentElement).fontSize
|
if (Number.isNaN(n)) {
|
||||||
);
|
throw new Error('h is not a number');
|
||||||
} else {
|
}
|
||||||
const n = Number(h);
|
size = n;
|
||||||
if (Number.isNaN(n)) {
|
}
|
||||||
throw new Error('h is not a number');
|
} else {
|
||||||
}
|
size = h;
|
||||||
size = n;
|
}
|
||||||
}
|
|
||||||
} else {
|
|
||||||
size = h;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size < 0) throw Error('h is negative');
|
if (size < 0) throw Error('h is negative');
|
||||||
|
|
||||||
nr._h_indicators.push({
|
nr._h_indicators.push({
|
||||||
name,
|
name,
|
||||||
size,
|
size,
|
||||||
type: type ?? 'fixed',
|
type: type ?? 'fixed',
|
||||||
data,
|
data,
|
||||||
font
|
font
|
||||||
});
|
});
|
||||||
|
|
||||||
(nr as KnownAny)[name] = size;
|
(nr as KnownAny)[name] = size;
|
||||||
|
|
||||||
return nr as KnownAny;
|
return nr as KnownAny;
|
||||||
};
|
};
|
||||||
|
|
||||||
r.w_p = (h) => {
|
r.w_p = (h) => {
|
||||||
let size = 0;
|
let size = 0;
|
||||||
if (typeof h === 'string') {
|
if (typeof h === 'string') {
|
||||||
if (h.endsWith('rem')) {
|
if (h.endsWith('rem')) {
|
||||||
const rem = Number(h.substring(0, h.length - 3));
|
const rem = Number(h.substring(0, h.length - 3));
|
||||||
if (Number.isNaN(rem)) {
|
if (Number.isNaN(rem)) {
|
||||||
throw new Error('h is not a valid rem value');
|
throw new Error('h is not a valid rem value');
|
||||||
}
|
}
|
||||||
size =
|
size = rem * Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
rem *
|
} else {
|
||||||
Number.parseFloat(
|
const n = Number(h);
|
||||||
getComputedStyle(document.documentElement).fontSize
|
if (Number.isNaN(n)) {
|
||||||
);
|
throw new Error('h is not a number');
|
||||||
} else {
|
}
|
||||||
const n = Number(h);
|
size = n;
|
||||||
if (Number.isNaN(n)) {
|
}
|
||||||
throw new Error('h is not a number');
|
} else {
|
||||||
}
|
size = h;
|
||||||
size = n;
|
}
|
||||||
}
|
r._w_p = size * 2;
|
||||||
} else {
|
return r;
|
||||||
size = h;
|
};
|
||||||
}
|
|
||||||
r._w_p = size * 2;
|
|
||||||
return r;
|
|
||||||
};
|
|
||||||
|
|
||||||
r.enforce = (inEnforce) => {
|
r.enforce = (inEnforce) => {
|
||||||
const e = inEnforce ?? r._enforce;
|
const e = inEnforce ?? r._enforce;
|
||||||
if (!e) return r;
|
if (!e) return r;
|
||||||
|
|
||||||
let h_sum = 0;
|
let h_sum = 0;
|
||||||
for (const i of r._h_indicators) {
|
for (const i of r._h_indicators) {
|
||||||
h_sum += i.size;
|
h_sum += i.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO handle width
|
// TODO handle width
|
||||||
if (h_sum < r._height && !r._h_indicators.some((i) => i.type === 'left'))
|
if (h_sum < r._height && !r._h_indicators.some((i) => i.type === 'left')) return r;
|
||||||
return r;
|
|
||||||
|
|
||||||
let fSize = r._h_indicators.reduce((acc, i) => {
|
let fSize = r._h_indicators.reduce((acc, i) => {
|
||||||
if (i.type !== 'fixed') return acc;
|
if (i.type !== 'fixed') return acc;
|
||||||
return acc + i.size;
|
return acc + i.size;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// you are fucked anyway
|
// you are fucked anyway
|
||||||
if (fSize > r._height) return r;
|
if (fSize > r._height) return r;
|
||||||
|
|
||||||
const h_leftover = h_sum - fSize;
|
const h_leftover = h_sum - fSize;
|
||||||
const th_leftover = r._height - fSize;
|
const th_leftover = r._height - fSize;
|
||||||
|
|
||||||
const pr = r._h_indicators
|
const pr = r._h_indicators
|
||||||
.filter((i) => i.type === 'dyanmic' || i.type === 'text')
|
.filter((i) => i.type === 'dyanmic' || i.type === 'text')
|
||||||
.map((i) => {
|
.map((i) => {
|
||||||
return [i, (i.size / h_leftover) * th_leftover] as const;
|
return [i, (i.size / h_leftover) * th_leftover] as const;
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const i of pr) {
|
for (const i of pr) {
|
||||||
let s = i[1];
|
let s = i[1];
|
||||||
if (i[0].type === 'text') {
|
if (i[0].type === 'text') {
|
||||||
s = Math.floor(
|
s = Math.floor(
|
||||||
getFitSizeForList(
|
getFitSizeForList([i[0].data ?? ''], r._width - r._w_p, i[1], 0.5, ctx, i[0].font) ?? i[1]
|
||||||
[i[0].data ?? ''],
|
);
|
||||||
r._width - r._w_p,
|
}
|
||||||
i[1],
|
|
||||||
0.5,
|
|
||||||
ctx,
|
|
||||||
i[0].font
|
|
||||||
) ?? i[1]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fSize += s;
|
fSize += s;
|
||||||
(r as KnownAny)[i[0].name] = s;
|
(r as KnownAny)[i[0].name] = s;
|
||||||
}
|
}
|
||||||
|
|
||||||
const left = r._h_indicators.filter((i) => i.type === 'left');
|
const left = r._h_indicators.filter((i) => i.type === 'left');
|
||||||
const len_left = left.length;
|
const len_left = left.length;
|
||||||
|
|
||||||
const rest = r._height - fSize;
|
const rest = r._height - fSize;
|
||||||
|
|
||||||
for (const i of left) {
|
for (const i of left) {
|
||||||
(r as KnownAny)[i.name] = rest / len_left;
|
(r as KnownAny)[i.name] = rest / len_left;
|
||||||
}
|
}
|
||||||
|
|
||||||
return r as KnownAny;
|
return r as KnownAny;
|
||||||
};
|
};
|
||||||
|
|
||||||
r.toEnfoceSize = (name) => {
|
r.toEnfoceSize = (name) => {
|
||||||
if (!r._enforce) return;
|
if (!r._enforce) return;
|
||||||
return [r._width - r._w_p, r[name]];
|
return [r._width - r._w_p, r[name]];
|
||||||
};
|
};
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
enforceSizeHelper.fromEnforceSize = (enforceSize?: EnforceSizeType) => {
|
enforceSizeHelper.fromEnforceSize = (enforceSize?: EnforceSizeType) => {
|
||||||
return enforceSizeHelper(
|
return enforceSizeHelper(enforceSize?.[0] ?? 0, enforceSize?.[1] ?? 0, !!enforceSize);
|
||||||
enforceSize?.[0] ?? 0,
|
|
||||||
enforceSize?.[1] ?? 0,
|
|
||||||
!!enforceSize
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PaddingManagerProps = {
|
export type PaddingManagerProps = {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Paddable = number | Axis | string[] | string | undefined;
|
export type Paddable = number | Axis | string[] | string | undefined;
|
||||||
|
|
||||||
export class PaddingManager {
|
export class PaddingManager {
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
ctx: CanvasRenderingContext2D;
|
ctx: CanvasRenderingContext2D;
|
||||||
|
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
|
||||||
private _paddingLeft = 0;
|
private _paddingLeft = 0;
|
||||||
private _paddingRight = 0;
|
private _paddingRight = 0;
|
||||||
private _paddingTop = 0;
|
private _paddingTop = 0;
|
||||||
private _paddingBot = 0;
|
private _paddingBot = 0;
|
||||||
|
|
||||||
private _fontSize = 15;
|
private _fontSize = 15;
|
||||||
|
|
||||||
private _enforceSize?: EnforceSizeType;
|
private _enforceSize?: EnforceSizeType;
|
||||||
|
|
||||||
constructor({ width, height, fontSize }: PaddingManagerProps) {
|
constructor({ width, height, fontSize }: PaddingManagerProps) {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
|
|
||||||
if (fontSize !== undefined) {
|
if (fontSize !== undefined) {
|
||||||
this._fontSize = fontSize;
|
this._fontSize = fontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is used to calculate the size of text
|
// This is used to calculate the size of text
|
||||||
this.canvas = document.createElement('canvas');
|
this.canvas = document.createElement('canvas');
|
||||||
const ctx = this.canvas.getContext('2d');
|
const ctx = this.canvas.getContext('2d');
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
throw new Error('Failed to create context for the internal canvas');
|
throw new Error('Failed to create context for the internal canvas');
|
||||||
}
|
}
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.ctx.font = `${this._fontSize}px Open sans`;
|
this.ctx.font = `${this._fontSize}px Open sans`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get paddedHeight() {
|
get paddedHeight() {
|
||||||
return this.height + this._paddingTop + this._paddingBot;
|
return this.height + this._paddingTop + this._paddingBot;
|
||||||
}
|
}
|
||||||
|
|
||||||
get paddedWidth() {
|
get paddedWidth() {
|
||||||
return this.width + this._paddingLeft + this._paddingRight;
|
return this.width + this._paddingLeft + this._paddingRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
set fontSize(size: number) {
|
set fontSize(size: number) {
|
||||||
this._fontSize = size;
|
this._fontSize = size;
|
||||||
this.ctx.font = `${this._fontSize}px Open sans`;
|
this.ctx.font = `${this._fontSize}px Open sans`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get fontSize() {
|
get fontSize() {
|
||||||
return this._fontSize;
|
return this._fontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
get left() {
|
get left() {
|
||||||
return this._paddingLeft;
|
return this._paddingLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
get right() {
|
get right() {
|
||||||
return this._paddingRight;
|
return this._paddingRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
get top() {
|
get top() {
|
||||||
return this._paddingTop;
|
return this._paddingTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
get bot() {
|
get bot() {
|
||||||
return this._paddingBot;
|
return this._paddingBot;
|
||||||
}
|
}
|
||||||
|
|
||||||
get translateString() {
|
get translateString() {
|
||||||
return `translate(${this.left},${this.top})`;
|
return `translate(${this.left},${this.top})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Add padding
|
// Add padding
|
||||||
//
|
//
|
||||||
pad(side: Sides, padding: Paddable, angle?: number) {
|
pad(side: Sides, padding: Paddable, angle?: number) {
|
||||||
let pn = 0;
|
let pn = 0;
|
||||||
|
|
||||||
if (padding === undefined) {
|
if (padding === undefined) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
if (typeof padding === 'number') {
|
if (typeof padding === 'number') {
|
||||||
pn = padding;
|
pn = padding;
|
||||||
} else if (typeof padding === 'string') {
|
} else if (typeof padding === 'string') {
|
||||||
let a: number | undefined = undefined;
|
let a: number | undefined = undefined;
|
||||||
if (angle !== undefined) {
|
if (angle !== undefined) {
|
||||||
a = angle * (Math.PI / 180);
|
a = angle * (Math.PI / 180);
|
||||||
}
|
}
|
||||||
pn = this.ctx.measureText(padding).width * Math.sin(a ?? Math.PI / 2);
|
pn = this.ctx.measureText(padding).width * Math.sin(a ?? Math.PI / 2);
|
||||||
} else if (Array.isArray(padding)) {
|
} else if (Array.isArray(padding)) {
|
||||||
pn = padding.reduce(
|
pn = padding.reduce((acc, s) => Math.max(this.ctx.measureText(s).width, acc), 0);
|
||||||
(acc, s) => Math.max(this.ctx.measureText(s).width, acc),
|
} else {
|
||||||
0
|
pn = padding.getPadding();
|
||||||
);
|
}
|
||||||
} else {
|
|
||||||
pn = padding.getPadding();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (side) {
|
switch (side) {
|
||||||
case 'left':
|
case 'left':
|
||||||
this._paddingLeft += pn;
|
this._paddingLeft += pn;
|
||||||
return this;
|
return this;
|
||||||
case 'right':
|
case 'right':
|
||||||
this._paddingRight += pn;
|
this._paddingRight += pn;
|
||||||
return this;
|
return this;
|
||||||
case 'top':
|
case 'top':
|
||||||
this._paddingTop += pn;
|
this._paddingTop += pn;
|
||||||
return this;
|
return this;
|
||||||
case 'bot':
|
case 'bot':
|
||||||
this._paddingBot += pn;
|
this._paddingBot += pn;
|
||||||
return this;
|
return this;
|
||||||
default:
|
default:
|
||||||
throw new Error(`unknown side: ${side}`);
|
throw new Error(`unknown side: ${side}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
padHoz(padding: Paddable) {
|
padHoz(padding: Paddable) {
|
||||||
return this.pad('left', padding).pad('right', padding);
|
return this.pad('left', padding).pad('right', padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
padLeft(padding: Paddable, angle?: number) {
|
padLeft(padding: Paddable, angle?: number) {
|
||||||
return this.pad('left', padding, angle);
|
return this.pad('left', padding, angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
padRight(padding: Paddable, angle?: number) {
|
padRight(padding: Paddable, angle?: number) {
|
||||||
return this.pad('right', padding, angle);
|
return this.pad('right', padding, angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
padTop(padding: Paddable, angle?: number) {
|
padTop(padding: Paddable, angle?: number) {
|
||||||
return this.pad('top', padding, angle);
|
return this.pad('top', padding, angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
padBot(padding: Paddable, angle?: number) {
|
padBot(padding: Paddable, angle?: number) {
|
||||||
return this.pad('bot', padding, angle);
|
return this.pad('bot', padding, angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetPadding(side: Sides) {
|
resetPadding(side: Sides) {
|
||||||
switch (side) {
|
switch (side) {
|
||||||
case 'left':
|
case 'left':
|
||||||
this._paddingLeft = 0;
|
this._paddingLeft = 0;
|
||||||
return this;
|
return this;
|
||||||
case 'right':
|
case 'right':
|
||||||
this._paddingRight = 0;
|
this._paddingRight = 0;
|
||||||
return this;
|
return this;
|
||||||
case 'top':
|
case 'top':
|
||||||
this._paddingTop = 0;
|
this._paddingTop = 0;
|
||||||
return this;
|
return this;
|
||||||
case 'bot':
|
case 'bot':
|
||||||
this._paddingBot = 0;
|
this._paddingBot = 0;
|
||||||
return this;
|
return this;
|
||||||
default:
|
default:
|
||||||
throw new Error(`unknown side: ${side}`);
|
throw new Error(`unknown side: ${side}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
padAll(n: number) {
|
padAll(n: number) {
|
||||||
this._paddingLeft += n;
|
this._paddingLeft += n;
|
||||||
this._paddingRight += n;
|
this._paddingRight += n;
|
||||||
this._paddingTop += n;
|
this._paddingTop += n;
|
||||||
this._paddingBot += n;
|
this._paddingBot += n;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
enforce(inEnforce?: EnforceSizeType) {
|
enforce(inEnforce?: EnforceSizeType) {
|
||||||
const enforce = this._enforceSize ?? inEnforce;
|
const enforce = this._enforceSize ?? inEnforce;
|
||||||
if (enforce === undefined) return this;
|
if (enforce === undefined) return this;
|
||||||
|
|
||||||
if (this.paddedWidth !== enforce[0]) {
|
if (this.paddedWidth !== enforce[0]) {
|
||||||
this.width = enforce[0] - this.left - this.right;
|
this.width = enforce[0] - this.left - this.right;
|
||||||
}
|
}
|
||||||
if (this.paddedHeight !== enforce[1]) {
|
if (this.paddedHeight !== enforce[1]) {
|
||||||
this.height = enforce[1] - this.top - this.bot;
|
this.height = enforce[1] - this.top - this.bot;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function countReducer(acc: Record<number, number>, a: { c: number | string | Date }) {
|
export function countReducer(acc: Record<number, number>, a: { c: number | string | Date }) {
|
||||||
const c = new Date(a.c);
|
const c = new Date(a.c);
|
||||||
c.setHours(0);
|
c.setHours(0);
|
||||||
c.setMinutes(0);
|
c.setMinutes(0);
|
||||||
c.setSeconds(0);
|
c.setSeconds(0);
|
||||||
c.setMilliseconds(0);
|
c.setMilliseconds(0);
|
||||||
const ct = c.getTime();
|
const ct = c.getTime();
|
||||||
|
|
||||||
if (acc[ct]) {
|
if (acc[ct]) {
|
||||||
acc[ct] += 1;
|
acc[ct] += 1;
|
||||||
} else {
|
} else {
|
||||||
acc[ct] = 1;
|
acc[ct] = 1;
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog class="card max-w-[50vw]" bind:this={dialog}>
|
<dialog class="card max-w-[50vw]" bind:this={dialog}>
|
||||||
<ApplicationSearchBar bind:result={internal} bind:filter />
|
<ApplicationSearchBar bind:result={internal} bind:filter excludeApplication={application} />
|
||||||
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
|
<div class="overflow-y-auto overflow-x-hidden flex-grow p-2">
|
||||||
{#each internal as item}
|
{#each internal as item}
|
||||||
<div class="card p-2 my-2 bg-slate-100 max-w-full" role="none">
|
<div class="card p-2 my-2 bg-slate-100 max-w-full" role="none">
|
||||||
|
Loading…
Reference in New Issue
Block a user