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,3 +1,15 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
@ -117,10 +129,11 @@ export class Axis {
|
|||||||
|
|
||||||
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
|
||||||
|
): EnforceHelper<NamesH, H> {
|
||||||
const c = document.createElement('canvas');
|
const c = document.createElement('canvas');
|
||||||
const ctx = c.getContext('2d');
|
const ctx = c.getContext('2d');
|
||||||
if (!ctx) throw new Error('Failed to get ctx');
|
if (!ctx) throw new Error('Failed to get ctx');
|
||||||
@ -135,10 +148,7 @@ export function enforceSizeHelper<
|
|||||||
} 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]) {
|
||||||
@ -152,11 +162,7 @@ export function enforceSizeHelper<
|
|||||||
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 *
|
|
||||||
Number.parseFloat(
|
|
||||||
getComputedStyle(document.documentElement).fontSize
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const n = Number(h);
|
const n = Number(h);
|
||||||
if (Number.isNaN(n)) {
|
if (Number.isNaN(n)) {
|
||||||
@ -191,11 +197,7 @@ export function enforceSizeHelper<
|
|||||||
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 *
|
|
||||||
Number.parseFloat(
|
|
||||||
getComputedStyle(document.documentElement).fontSize
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const n = Number(h);
|
const n = Number(h);
|
||||||
if (Number.isNaN(n)) {
|
if (Number.isNaN(n)) {
|
||||||
@ -220,8 +222,7 @@ export function enforceSizeHelper<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
@ -244,14 +245,7 @@ export function enforceSizeHelper<
|
|||||||
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]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,11 +273,7 @@ export function enforceSizeHelper<
|
|||||||
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 = {
|
||||||
@ -383,10 +373,7 @@ export class PaddingManager {
|
|||||||
}
|
}
|
||||||
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),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
pn = padding.getPadding();
|
pn = padding.getPadding();
|
||||||
}
|
}
|
||||||
@ -486,4 +473,3 @@ export function countReducer(acc: Record<number, number>, a: { c: number | strin
|
|||||||
}
|
}
|
||||||
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