feat: added cool graph
This commit is contained in:
parent
e80a13b1d8
commit
8a38d407c4
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] = {
|
||||
icon: 'plus',
|
||||
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;
|
||||
|
||||
for (const nodeId of nodesId) {
|
||||
|
@ -9,7 +9,8 @@
|
||||
import PayRange from './PayRange.svelte';
|
||||
import LineGraphs, { type LineGraphData } from './LineGraph.svelte';
|
||||
import * as d3 from 'd3';
|
||||
import { countReducer } from './utils';
|
||||
import { countReducer, type EventsStat } from './utils';
|
||||
import CoolGraph from './CoolGraph/CoolGraph.svelte';
|
||||
|
||||
onMount(() => {
|
||||
applicationStore.loadAll();
|
||||
@ -22,6 +23,8 @@
|
||||
let viewGraph: { xs: number[]; ys: number[] } = $state({ xs: [], ys: [] });
|
||||
let statusGraph: LineGraphData = $state([]);
|
||||
|
||||
let events: EventsStat[] = $state([]);
|
||||
|
||||
let width: number = $state(300);
|
||||
|
||||
onMount(async () => {
|
||||
@ -37,19 +40,7 @@
|
||||
})();
|
||||
|
||||
(async () => {
|
||||
type EventsStat = {
|
||||
// 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/');
|
||||
events = await get('events/');
|
||||
|
||||
const pre_created_graph = events
|
||||
.filter((a) => a.t === 0)
|
||||
@ -125,16 +116,12 @@
|
||||
data={[...statusStore.nodes, 'Created' as const].reduce(
|
||||
(acc, item) => {
|
||||
if (item === 'Created') {
|
||||
const count = applicationStore.all.filter(
|
||||
(a) => a.status_id === null
|
||||
).length;
|
||||
const count = applicationStore.all.filter((a) => a.status_id === null).length;
|
||||
if (count === 0) return acc;
|
||||
acc[item] = count;
|
||||
return acc;
|
||||
}
|
||||
const count = applicationStore.all.filter(
|
||||
(a) => item.id === a.status_id
|
||||
).length;
|
||||
const count = applicationStore.all.filter((a) => item.id === a.status_id).length;
|
||||
if (count === 0) return acc;
|
||||
acc[item.name] = count;
|
||||
return acc;
|
||||
@ -237,8 +224,7 @@
|
||||
sensitivity={0.01}
|
||||
data={applicationStore.all.reduce(
|
||||
(acc, item) => {
|
||||
const l =
|
||||
item.inperson_type === '' ? 'Unknown' : item.inperson_type;
|
||||
const l = item.inperson_type === '' ? 'Unknown' : item.inperson_type;
|
||||
if (acc[l]) {
|
||||
acc[l] += 1;
|
||||
} else {
|
||||
@ -326,9 +312,7 @@
|
||||
.replace(/[^\d\-–]/g, '')
|
||||
.replace(/–/g, '-')
|
||||
.split('-');
|
||||
return (
|
||||
Number(payrange[0]) + Number(payrange[1] ?? payrange[0])
|
||||
);
|
||||
return Number(payrange[0]) + Number(payrange[1] ?? payrange[0]);
|
||||
})
|
||||
.reduce((acc, a) => acc + a, 0) /
|
||||
(fapps.length * 2)
|
||||
@ -406,11 +390,7 @@
|
||||
.replace(/[^\d\-–]/g, '')
|
||||
.replace(/–/g, '-')
|
||||
.split('-');
|
||||
return (
|
||||
(Number(payrange[0]) +
|
||||
Number(payrange[1] ?? payrange[0])) /
|
||||
2
|
||||
);
|
||||
return (Number(payrange[0]) + Number(payrange[1] ?? payrange[0])) / 2;
|
||||
})
|
||||
.reduce(
|
||||
(acc, a) => {
|
||||
@ -432,6 +412,8 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<h1>Cool graph</h1>
|
||||
<CoolGraph {events} />
|
||||
</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 AxisProps = {
|
||||
title?: string;
|
||||
titleFontSize?: number;
|
||||
titlePos?: Sides;
|
||||
title?: string;
|
||||
titleFontSize?: number;
|
||||
titlePos?: Sides;
|
||||
};
|
||||
|
||||
export class Axis {
|
||||
private _title?: string;
|
||||
private _titleFontSize: number;
|
||||
private _titlePos: Sides;
|
||||
private _title?: string;
|
||||
private _titleFontSize: number;
|
||||
private _titlePos: Sides;
|
||||
|
||||
constructor({ title, titleFontSize, titlePos }: AxisProps = {}) {
|
||||
this._title = title;
|
||||
this._titleFontSize = titleFontSize ?? 15;
|
||||
this._titlePos = titlePos ?? 'left';
|
||||
}
|
||||
constructor({ title, titleFontSize, titlePos }: AxisProps = {}) {
|
||||
this._title = title;
|
||||
this._titleFontSize = titleFontSize ?? 15;
|
||||
this._titlePos = titlePos ?? 'left';
|
||||
}
|
||||
|
||||
getPadding() {
|
||||
if (!this._title) return 0;
|
||||
return this._titleFontSize + 4;
|
||||
}
|
||||
getPadding() {
|
||||
if (!this._title) return 0;
|
||||
return this._titleFontSize + 4;
|
||||
}
|
||||
|
||||
apply_title(
|
||||
svg: d3.Selection<KnownAny, KnownAny, null, KnownAny>,
|
||||
height: number,
|
||||
width: number,
|
||||
padding: number
|
||||
) {
|
||||
if (!this._title) return undefined;
|
||||
if (this._titlePos === 'left') {
|
||||
return (
|
||||
svg
|
||||
.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('transform', 'rotate(-90)')
|
||||
.attr('font-size', `${this._titleFontSize}px`)
|
||||
.attr('font-family', 'Open sans')
|
||||
// y becomes x
|
||||
.attr('x', -height / 2)
|
||||
.attr('y', -padding + this._titleFontSize + 4)
|
||||
.text(this._title)
|
||||
);
|
||||
}
|
||||
if (this._titlePos === 'right') {
|
||||
return (
|
||||
svg
|
||||
.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('transform', 'rotate(90)')
|
||||
.attr('font-size', `${this._titleFontSize}px`)
|
||||
.attr('font-family', 'Open sans')
|
||||
// y becomes x
|
||||
.attr('x', height / 2)
|
||||
.attr('y', -width - padding + this._titleFontSize + 4)
|
||||
.text(this._title)
|
||||
);
|
||||
}
|
||||
if (this._titlePos === 'bot') {
|
||||
return svg
|
||||
.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', `${this._titleFontSize}px`)
|
||||
.attr('font-family', 'Open sans')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', height + padding - 4)
|
||||
.text(this._title);
|
||||
}
|
||||
if (this._titlePos === 'top') {
|
||||
return svg
|
||||
.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', `${this._titleFontSize}px`)
|
||||
.attr('font-family', 'Open sans')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', -padding + this._titleFontSize)
|
||||
.text(this._title);
|
||||
}
|
||||
apply_title(
|
||||
svg: d3.Selection<KnownAny, KnownAny, null, KnownAny>,
|
||||
height: number,
|
||||
width: number,
|
||||
padding: number
|
||||
) {
|
||||
if (!this._title) return undefined;
|
||||
if (this._titlePos === 'left') {
|
||||
return (
|
||||
svg
|
||||
.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('transform', 'rotate(-90)')
|
||||
.attr('font-size', `${this._titleFontSize}px`)
|
||||
.attr('font-family', 'Open sans')
|
||||
// y becomes x
|
||||
.attr('x', -height / 2)
|
||||
.attr('y', -padding + this._titleFontSize + 4)
|
||||
.text(this._title)
|
||||
);
|
||||
}
|
||||
if (this._titlePos === 'right') {
|
||||
return (
|
||||
svg
|
||||
.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('transform', 'rotate(90)')
|
||||
.attr('font-size', `${this._titleFontSize}px`)
|
||||
.attr('font-family', 'Open sans')
|
||||
// y becomes x
|
||||
.attr('x', height / 2)
|
||||
.attr('y', -width - padding + this._titleFontSize + 4)
|
||||
.text(this._title)
|
||||
);
|
||||
}
|
||||
if (this._titlePos === 'bot') {
|
||||
return svg
|
||||
.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', `${this._titleFontSize}px`)
|
||||
.attr('font-family', 'Open sans')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', height + padding - 4)
|
||||
.text(this._title);
|
||||
}
|
||||
if (this._titlePos === 'top') {
|
||||
return svg
|
||||
.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', `${this._titleFontSize}px`)
|
||||
.attr('font-family', 'Open sans')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', -padding + this._titleFontSize)
|
||||
.text(this._title);
|
||||
}
|
||||
|
||||
console.error('Unknown title pos', this.titlePos);
|
||||
return undefined;
|
||||
}
|
||||
console.error('Unknown title pos', this.titlePos);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
//
|
||||
// Builder pattern functions
|
||||
//
|
||||
title(title?: string) {
|
||||
this._title = title;
|
||||
return this;
|
||||
}
|
||||
//
|
||||
// Builder pattern functions
|
||||
//
|
||||
title(title?: string) {
|
||||
this._title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
titlePos(pos: Sides) {
|
||||
this._titlePos = pos;
|
||||
return this;
|
||||
}
|
||||
titlePos(pos: Sides) {
|
||||
this._titlePos = pos;
|
||||
return this;
|
||||
}
|
||||
|
||||
titleFontSize(size: number) {
|
||||
this._titleFontSize = size;
|
||||
return this;
|
||||
}
|
||||
titleFontSize(size: number) {
|
||||
this._titleFontSize = size;
|
||||
return this;
|
||||
}
|
||||
|
||||
enforcePos(pos: 'vert' | 'hoz', defaultPos: Sides) {
|
||||
if (pos === 'vert') {
|
||||
if (this._titlePos !== 'top' && this._titlePos !== 'bot') {
|
||||
return this.titlePos(defaultPos);
|
||||
}
|
||||
} else {
|
||||
if (this._titlePos !== 'left' && this._titlePos !== 'right') {
|
||||
return this.titlePos(defaultPos);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
enforcePos(pos: 'vert' | 'hoz', defaultPos: Sides) {
|
||||
if (pos === 'vert') {
|
||||
if (this._titlePos !== 'top' && this._titlePos !== 'bot') {
|
||||
return this.titlePos(defaultPos);
|
||||
}
|
||||
} else {
|
||||
if (this._titlePos !== 'left' && this._titlePos !== 'right') {
|
||||
return this.titlePos(defaultPos);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export type EnforceSizeType = [number, number];
|
||||
|
||||
export function enforceSizeHelper<
|
||||
H extends Record<NamesH, number>,
|
||||
NamesH extends string = ''
|
||||
>(width: number, height: number, enforce: boolean): EnforceHelper<NamesH, H> {
|
||||
const c = document.createElement('canvas');
|
||||
const ctx = c.getContext('2d');
|
||||
if (!ctx) throw new Error('Failed to get ctx');
|
||||
export function enforceSizeHelper<H extends Record<NamesH, number>, NamesH extends string = ''>(
|
||||
width: number,
|
||||
height: number,
|
||||
enforce: boolean
|
||||
): EnforceHelper<NamesH, H> {
|
||||
const c = document.createElement('canvas');
|
||||
const ctx = c.getContext('2d');
|
||||
if (!ctx) throw new Error('Failed to get ctx');
|
||||
|
||||
const r = {
|
||||
_width: width,
|
||||
_height: height,
|
||||
_enforce: enforce,
|
||||
_w_p: 0,
|
||||
const r = {
|
||||
_width: width,
|
||||
_height: height,
|
||||
_enforce: enforce,
|
||||
_w_p: 0,
|
||||
|
||||
_h_indicators: [] as Indicator<NamesH>[]
|
||||
} as EnforceHelper<NamesH, H>;
|
||||
_h_indicators: [] as Indicator<NamesH>[]
|
||||
} as EnforceHelper<NamesH, H>;
|
||||
|
||||
r.h = (name, h, type, data, font) => {
|
||||
const nr = r as EnforceHelper<
|
||||
NamesH | typeof name,
|
||||
Record<NamesH | typeof name, number>
|
||||
>;
|
||||
r.h = (name, h, type, data, font) => {
|
||||
const nr = r as EnforceHelper<NamesH | typeof name, Record<NamesH | typeof name, number>>;
|
||||
|
||||
// TODO maybe update the values
|
||||
if (nr[name]) {
|
||||
return r;
|
||||
}
|
||||
// TODO maybe update the values
|
||||
if (nr[name]) {
|
||||
return r;
|
||||
}
|
||||
|
||||
let size = 0;
|
||||
if (typeof h === 'string') {
|
||||
if (h.endsWith('rem')) {
|
||||
const rem = Number(h.substring(0, h.length - 3));
|
||||
if (Number.isNaN(rem)) {
|
||||
throw new Error('h is not a valid rem value');
|
||||
}
|
||||
size =
|
||||
rem *
|
||||
Number.parseFloat(
|
||||
getComputedStyle(document.documentElement).fontSize
|
||||
);
|
||||
} else {
|
||||
const n = Number(h);
|
||||
if (Number.isNaN(n)) {
|
||||
throw new Error('h is not a number');
|
||||
}
|
||||
size = n;
|
||||
}
|
||||
} else {
|
||||
size = h;
|
||||
}
|
||||
let size = 0;
|
||||
if (typeof h === 'string') {
|
||||
if (h.endsWith('rem')) {
|
||||
const rem = Number(h.substring(0, h.length - 3));
|
||||
if (Number.isNaN(rem)) {
|
||||
throw new Error('h is not a valid rem value');
|
||||
}
|
||||
size = rem * Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
} else {
|
||||
const n = Number(h);
|
||||
if (Number.isNaN(n)) {
|
||||
throw new Error('h is not a number');
|
||||
}
|
||||
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({
|
||||
name,
|
||||
size,
|
||||
type: type ?? 'fixed',
|
||||
data,
|
||||
font
|
||||
});
|
||||
nr._h_indicators.push({
|
||||
name,
|
||||
size,
|
||||
type: type ?? 'fixed',
|
||||
data,
|
||||
font
|
||||
});
|
||||
|
||||
(nr as KnownAny)[name] = size;
|
||||
(nr as KnownAny)[name] = size;
|
||||
|
||||
return nr as KnownAny;
|
||||
};
|
||||
return nr as KnownAny;
|
||||
};
|
||||
|
||||
r.w_p = (h) => {
|
||||
let size = 0;
|
||||
if (typeof h === 'string') {
|
||||
if (h.endsWith('rem')) {
|
||||
const rem = Number(h.substring(0, h.length - 3));
|
||||
if (Number.isNaN(rem)) {
|
||||
throw new Error('h is not a valid rem value');
|
||||
}
|
||||
size =
|
||||
rem *
|
||||
Number.parseFloat(
|
||||
getComputedStyle(document.documentElement).fontSize
|
||||
);
|
||||
} else {
|
||||
const n = Number(h);
|
||||
if (Number.isNaN(n)) {
|
||||
throw new Error('h is not a number');
|
||||
}
|
||||
size = n;
|
||||
}
|
||||
} else {
|
||||
size = h;
|
||||
}
|
||||
r._w_p = size * 2;
|
||||
return r;
|
||||
};
|
||||
r.w_p = (h) => {
|
||||
let size = 0;
|
||||
if (typeof h === 'string') {
|
||||
if (h.endsWith('rem')) {
|
||||
const rem = Number(h.substring(0, h.length - 3));
|
||||
if (Number.isNaN(rem)) {
|
||||
throw new Error('h is not a valid rem value');
|
||||
}
|
||||
size = rem * Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
} else {
|
||||
const n = Number(h);
|
||||
if (Number.isNaN(n)) {
|
||||
throw new Error('h is not a number');
|
||||
}
|
||||
size = n;
|
||||
}
|
||||
} else {
|
||||
size = h;
|
||||
}
|
||||
r._w_p = size * 2;
|
||||
return r;
|
||||
};
|
||||
|
||||
r.enforce = (inEnforce) => {
|
||||
const e = inEnforce ?? r._enforce;
|
||||
if (!e) return r;
|
||||
r.enforce = (inEnforce) => {
|
||||
const e = inEnforce ?? r._enforce;
|
||||
if (!e) return r;
|
||||
|
||||
let h_sum = 0;
|
||||
for (const i of r._h_indicators) {
|
||||
h_sum += i.size;
|
||||
}
|
||||
let h_sum = 0;
|
||||
for (const i of r._h_indicators) {
|
||||
h_sum += i.size;
|
||||
}
|
||||
|
||||
// TODO handle width
|
||||
if (h_sum < r._height && !r._h_indicators.some((i) => i.type === 'left'))
|
||||
return r;
|
||||
// TODO handle width
|
||||
if (h_sum < r._height && !r._h_indicators.some((i) => i.type === 'left')) return r;
|
||||
|
||||
let fSize = r._h_indicators.reduce((acc, i) => {
|
||||
if (i.type !== 'fixed') return acc;
|
||||
return acc + i.size;
|
||||
}, 0);
|
||||
let fSize = r._h_indicators.reduce((acc, i) => {
|
||||
if (i.type !== 'fixed') return acc;
|
||||
return acc + i.size;
|
||||
}, 0);
|
||||
|
||||
// you are fucked anyway
|
||||
if (fSize > r._height) return r;
|
||||
// you are fucked anyway
|
||||
if (fSize > r._height) return r;
|
||||
|
||||
const h_leftover = h_sum - fSize;
|
||||
const th_leftover = r._height - fSize;
|
||||
const h_leftover = h_sum - fSize;
|
||||
const th_leftover = r._height - fSize;
|
||||
|
||||
const pr = r._h_indicators
|
||||
.filter((i) => i.type === 'dyanmic' || i.type === 'text')
|
||||
.map((i) => {
|
||||
return [i, (i.size / h_leftover) * th_leftover] as const;
|
||||
});
|
||||
const pr = r._h_indicators
|
||||
.filter((i) => i.type === 'dyanmic' || i.type === 'text')
|
||||
.map((i) => {
|
||||
return [i, (i.size / h_leftover) * th_leftover] as const;
|
||||
});
|
||||
|
||||
for (const i of pr) {
|
||||
let s = i[1];
|
||||
if (i[0].type === 'text') {
|
||||
s = Math.floor(
|
||||
getFitSizeForList(
|
||||
[i[0].data ?? ''],
|
||||
r._width - r._w_p,
|
||||
i[1],
|
||||
0.5,
|
||||
ctx,
|
||||
i[0].font
|
||||
) ?? i[1]
|
||||
);
|
||||
}
|
||||
for (const i of pr) {
|
||||
let s = i[1];
|
||||
if (i[0].type === 'text') {
|
||||
s = Math.floor(
|
||||
getFitSizeForList([i[0].data ?? ''], r._width - r._w_p, i[1], 0.5, ctx, i[0].font) ?? i[1]
|
||||
);
|
||||
}
|
||||
|
||||
fSize += s;
|
||||
(r as KnownAny)[i[0].name] = s;
|
||||
}
|
||||
fSize += s;
|
||||
(r as KnownAny)[i[0].name] = s;
|
||||
}
|
||||
|
||||
const left = r._h_indicators.filter((i) => i.type === 'left');
|
||||
const len_left = left.length;
|
||||
const left = r._h_indicators.filter((i) => i.type === 'left');
|
||||
const len_left = left.length;
|
||||
|
||||
const rest = r._height - fSize;
|
||||
const rest = r._height - fSize;
|
||||
|
||||
for (const i of left) {
|
||||
(r as KnownAny)[i.name] = rest / len_left;
|
||||
}
|
||||
for (const i of left) {
|
||||
(r as KnownAny)[i.name] = rest / len_left;
|
||||
}
|
||||
|
||||
return r as KnownAny;
|
||||
};
|
||||
return r as KnownAny;
|
||||
};
|
||||
|
||||
r.toEnfoceSize = (name) => {
|
||||
if (!r._enforce) return;
|
||||
return [r._width - r._w_p, r[name]];
|
||||
};
|
||||
r.toEnfoceSize = (name) => {
|
||||
if (!r._enforce) return;
|
||||
return [r._width - r._w_p, r[name]];
|
||||
};
|
||||
|
||||
return r;
|
||||
return r;
|
||||
}
|
||||
enforceSizeHelper.fromEnforceSize = (enforceSize?: EnforceSizeType) => {
|
||||
return enforceSizeHelper(
|
||||
enforceSize?.[0] ?? 0,
|
||||
enforceSize?.[1] ?? 0,
|
||||
!!enforceSize
|
||||
);
|
||||
return enforceSizeHelper(enforceSize?.[0] ?? 0, enforceSize?.[1] ?? 0, !!enforceSize);
|
||||
};
|
||||
|
||||
export type PaddingManagerProps = {
|
||||
width: number;
|
||||
height: number;
|
||||
fontSize?: number;
|
||||
width: number;
|
||||
height: number;
|
||||
fontSize?: number;
|
||||
};
|
||||
|
||||
export type Paddable = number | Axis | string[] | string | undefined;
|
||||
|
||||
export class PaddingManager {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
|
||||
width: number;
|
||||
height: number;
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
private _paddingLeft = 0;
|
||||
private _paddingRight = 0;
|
||||
private _paddingTop = 0;
|
||||
private _paddingBot = 0;
|
||||
private _paddingLeft = 0;
|
||||
private _paddingRight = 0;
|
||||
private _paddingTop = 0;
|
||||
private _paddingBot = 0;
|
||||
|
||||
private _fontSize = 15;
|
||||
private _fontSize = 15;
|
||||
|
||||
private _enforceSize?: EnforceSizeType;
|
||||
private _enforceSize?: EnforceSizeType;
|
||||
|
||||
constructor({ width, height, fontSize }: PaddingManagerProps) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
constructor({ width, height, fontSize }: PaddingManagerProps) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
if (fontSize !== undefined) {
|
||||
this._fontSize = fontSize;
|
||||
}
|
||||
if (fontSize !== undefined) {
|
||||
this._fontSize = fontSize;
|
||||
}
|
||||
|
||||
// This is used to calculate the size of text
|
||||
this.canvas = document.createElement('canvas');
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Failed to create context for the internal canvas');
|
||||
}
|
||||
this.ctx = ctx;
|
||||
this.ctx.font = `${this._fontSize}px Open sans`;
|
||||
}
|
||||
// This is used to calculate the size of text
|
||||
this.canvas = document.createElement('canvas');
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Failed to create context for the internal canvas');
|
||||
}
|
||||
this.ctx = ctx;
|
||||
this.ctx.font = `${this._fontSize}px Open sans`;
|
||||
}
|
||||
|
||||
get paddedHeight() {
|
||||
return this.height + this._paddingTop + this._paddingBot;
|
||||
}
|
||||
get paddedHeight() {
|
||||
return this.height + this._paddingTop + this._paddingBot;
|
||||
}
|
||||
|
||||
get paddedWidth() {
|
||||
return this.width + this._paddingLeft + this._paddingRight;
|
||||
}
|
||||
get paddedWidth() {
|
||||
return this.width + this._paddingLeft + this._paddingRight;
|
||||
}
|
||||
|
||||
set fontSize(size: number) {
|
||||
this._fontSize = size;
|
||||
this.ctx.font = `${this._fontSize}px Open sans`;
|
||||
}
|
||||
set fontSize(size: number) {
|
||||
this._fontSize = size;
|
||||
this.ctx.font = `${this._fontSize}px Open sans`;
|
||||
}
|
||||
|
||||
get fontSize() {
|
||||
return this._fontSize;
|
||||
}
|
||||
get fontSize() {
|
||||
return this._fontSize;
|
||||
}
|
||||
|
||||
get left() {
|
||||
return this._paddingLeft;
|
||||
}
|
||||
get left() {
|
||||
return this._paddingLeft;
|
||||
}
|
||||
|
||||
get right() {
|
||||
return this._paddingRight;
|
||||
}
|
||||
get right() {
|
||||
return this._paddingRight;
|
||||
}
|
||||
|
||||
get top() {
|
||||
return this._paddingTop;
|
||||
}
|
||||
get top() {
|
||||
return this._paddingTop;
|
||||
}
|
||||
|
||||
get bot() {
|
||||
return this._paddingBot;
|
||||
}
|
||||
get bot() {
|
||||
return this._paddingBot;
|
||||
}
|
||||
|
||||
get translateString() {
|
||||
return `translate(${this.left},${this.top})`;
|
||||
}
|
||||
get translateString() {
|
||||
return `translate(${this.left},${this.top})`;
|
||||
}
|
||||
|
||||
//
|
||||
// Add padding
|
||||
//
|
||||
pad(side: Sides, padding: Paddable, angle?: number) {
|
||||
let pn = 0;
|
||||
//
|
||||
// Add padding
|
||||
//
|
||||
pad(side: Sides, padding: Paddable, angle?: number) {
|
||||
let pn = 0;
|
||||
|
||||
if (padding === undefined) {
|
||||
return this;
|
||||
}
|
||||
if (typeof padding === 'number') {
|
||||
pn = padding;
|
||||
} else if (typeof padding === 'string') {
|
||||
let a: number | undefined = undefined;
|
||||
if (angle !== undefined) {
|
||||
a = angle * (Math.PI / 180);
|
||||
}
|
||||
pn = this.ctx.measureText(padding).width * Math.sin(a ?? Math.PI / 2);
|
||||
} else if (Array.isArray(padding)) {
|
||||
pn = padding.reduce(
|
||||
(acc, s) => Math.max(this.ctx.measureText(s).width, acc),
|
||||
0
|
||||
);
|
||||
} else {
|
||||
pn = padding.getPadding();
|
||||
}
|
||||
if (padding === undefined) {
|
||||
return this;
|
||||
}
|
||||
if (typeof padding === 'number') {
|
||||
pn = padding;
|
||||
} else if (typeof padding === 'string') {
|
||||
let a: number | undefined = undefined;
|
||||
if (angle !== undefined) {
|
||||
a = angle * (Math.PI / 180);
|
||||
}
|
||||
pn = this.ctx.measureText(padding).width * Math.sin(a ?? Math.PI / 2);
|
||||
} else if (Array.isArray(padding)) {
|
||||
pn = padding.reduce((acc, s) => Math.max(this.ctx.measureText(s).width, acc), 0);
|
||||
} else {
|
||||
pn = padding.getPadding();
|
||||
}
|
||||
|
||||
switch (side) {
|
||||
case 'left':
|
||||
this._paddingLeft += pn;
|
||||
return this;
|
||||
case 'right':
|
||||
this._paddingRight += pn;
|
||||
return this;
|
||||
case 'top':
|
||||
this._paddingTop += pn;
|
||||
return this;
|
||||
case 'bot':
|
||||
this._paddingBot += pn;
|
||||
return this;
|
||||
default:
|
||||
throw new Error(`unknown side: ${side}`);
|
||||
}
|
||||
}
|
||||
switch (side) {
|
||||
case 'left':
|
||||
this._paddingLeft += pn;
|
||||
return this;
|
||||
case 'right':
|
||||
this._paddingRight += pn;
|
||||
return this;
|
||||
case 'top':
|
||||
this._paddingTop += pn;
|
||||
return this;
|
||||
case 'bot':
|
||||
this._paddingBot += pn;
|
||||
return this;
|
||||
default:
|
||||
throw new Error(`unknown side: ${side}`);
|
||||
}
|
||||
}
|
||||
|
||||
padHoz(padding: Paddable) {
|
||||
return this.pad('left', padding).pad('right', padding);
|
||||
}
|
||||
padHoz(padding: Paddable) {
|
||||
return this.pad('left', padding).pad('right', padding);
|
||||
}
|
||||
|
||||
padLeft(padding: Paddable, angle?: number) {
|
||||
return this.pad('left', padding, angle);
|
||||
}
|
||||
padLeft(padding: Paddable, angle?: number) {
|
||||
return this.pad('left', padding, angle);
|
||||
}
|
||||
|
||||
padRight(padding: Paddable, angle?: number) {
|
||||
return this.pad('right', padding, angle);
|
||||
}
|
||||
padRight(padding: Paddable, angle?: number) {
|
||||
return this.pad('right', padding, angle);
|
||||
}
|
||||
|
||||
padTop(padding: Paddable, angle?: number) {
|
||||
return this.pad('top', padding, angle);
|
||||
}
|
||||
padTop(padding: Paddable, angle?: number) {
|
||||
return this.pad('top', padding, angle);
|
||||
}
|
||||
|
||||
padBot(padding: Paddable, angle?: number) {
|
||||
return this.pad('bot', padding, angle);
|
||||
}
|
||||
padBot(padding: Paddable, angle?: number) {
|
||||
return this.pad('bot', padding, angle);
|
||||
}
|
||||
|
||||
resetPadding(side: Sides) {
|
||||
switch (side) {
|
||||
case 'left':
|
||||
this._paddingLeft = 0;
|
||||
return this;
|
||||
case 'right':
|
||||
this._paddingRight = 0;
|
||||
return this;
|
||||
case 'top':
|
||||
this._paddingTop = 0;
|
||||
return this;
|
||||
case 'bot':
|
||||
this._paddingBot = 0;
|
||||
return this;
|
||||
default:
|
||||
throw new Error(`unknown side: ${side}`);
|
||||
}
|
||||
}
|
||||
resetPadding(side: Sides) {
|
||||
switch (side) {
|
||||
case 'left':
|
||||
this._paddingLeft = 0;
|
||||
return this;
|
||||
case 'right':
|
||||
this._paddingRight = 0;
|
||||
return this;
|
||||
case 'top':
|
||||
this._paddingTop = 0;
|
||||
return this;
|
||||
case 'bot':
|
||||
this._paddingBot = 0;
|
||||
return this;
|
||||
default:
|
||||
throw new Error(`unknown side: ${side}`);
|
||||
}
|
||||
}
|
||||
|
||||
padAll(n: number) {
|
||||
this._paddingLeft += n;
|
||||
this._paddingRight += n;
|
||||
this._paddingTop += n;
|
||||
this._paddingBot += n;
|
||||
return this;
|
||||
}
|
||||
padAll(n: number) {
|
||||
this._paddingLeft += n;
|
||||
this._paddingRight += n;
|
||||
this._paddingTop += n;
|
||||
this._paddingBot += n;
|
||||
return this;
|
||||
}
|
||||
|
||||
enforce(inEnforce?: EnforceSizeType) {
|
||||
const enforce = this._enforceSize ?? inEnforce;
|
||||
if (enforce === undefined) return this;
|
||||
enforce(inEnforce?: EnforceSizeType) {
|
||||
const enforce = this._enforceSize ?? inEnforce;
|
||||
if (enforce === undefined) return this;
|
||||
|
||||
if (this.paddedWidth !== enforce[0]) {
|
||||
this.width = enforce[0] - this.left - this.right;
|
||||
}
|
||||
if (this.paddedHeight !== enforce[1]) {
|
||||
this.height = enforce[1] - this.top - this.bot;
|
||||
}
|
||||
if (this.paddedWidth !== enforce[0]) {
|
||||
this.width = enforce[0] - this.left - this.right;
|
||||
}
|
||||
if (this.paddedHeight !== enforce[1]) {
|
||||
this.height = enforce[1] - this.top - this.bot;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export function countReducer(acc: Record<number, number>, a: { c: number | string | Date }) {
|
||||
const c = new Date(a.c);
|
||||
c.setHours(0);
|
||||
c.setMinutes(0);
|
||||
c.setSeconds(0);
|
||||
c.setMilliseconds(0);
|
||||
const ct = c.getTime();
|
||||
const c = new Date(a.c);
|
||||
c.setHours(0);
|
||||
c.setMinutes(0);
|
||||
c.setSeconds(0);
|
||||
c.setMilliseconds(0);
|
||||
const ct = c.getTime();
|
||||
|
||||
if (acc[ct]) {
|
||||
acc[ct] += 1;
|
||||
} else {
|
||||
acc[ct] = 1;
|
||||
}
|
||||
return acc;
|
||||
if (acc[ct]) {
|
||||
acc[ct] += 1;
|
||||
} else {
|
||||
acc[ct] = 1;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user