From 8a38d407c4b4e3c2b27fcc0870546abae5b15e35 Mon Sep 17 00:00:00 2001 From: Andre Henriques Date: Mon, 7 Apr 2025 11:16:10 +0100 Subject: [PATCH] feat: added cool graph --- site/src/components/FlowArea.svelte | 232 ++++++ site/src/components/types.ts | 29 + site/src/lib/Types.svelte.ts | 16 +- site/src/routes/graphs/+page.svelte | 42 +- .../routes/graphs/CoolGraph/CoolGraph.svelte | 196 +++++ .../graphs/CoolGraph/CoolGraphNode.svelte | 182 ++++ site/src/routes/graphs/CoolGraph/types.ts | 1 + site/src/routes/graphs/utils.ts | 788 +++++++++--------- 8 files changed, 1054 insertions(+), 432 deletions(-) create mode 100644 site/src/components/FlowArea.svelte create mode 100644 site/src/components/types.ts create mode 100644 site/src/routes/graphs/CoolGraph/CoolGraph.svelte create mode 100644 site/src/routes/graphs/CoolGraph/CoolGraphNode.svelte create mode 100644 site/src/routes/graphs/CoolGraph/types.ts diff --git a/site/src/components/FlowArea.svelte b/site/src/components/FlowArea.svelte new file mode 100644 index 0000000..ddc30ae --- /dev/null +++ b/site/src/components/FlowArea.svelte @@ -0,0 +1,232 @@ + + +
+
+ {#if grid} + + +
+ +
+ {/if} + + {#if show_controll_buttons} + +
+ + {#if x !== 0 || y !== 0} + + {/if} +
+ + {#if controll_buttons} + {@render controll_buttons()} + {/if} + {/if} + + {#if children} + {@render children(canvasX, canvasY, worldX, worldY)} + {/if} +
+
+ + diff --git a/site/src/components/types.ts b/site/src/components/types.ts new file mode 100644 index 0000000..ca67397 --- /dev/null +++ b/site/src/components/types.ts @@ -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; +} diff --git a/site/src/lib/Types.svelte.ts b/site/src/lib/Types.svelte.ts index dd1aa69..e85c77f 100644 --- a/site/src/lib/Types.svelte.ts +++ b/site/src/lib/Types.svelte.ts @@ -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) { diff --git a/site/src/routes/graphs/+page.svelte b/site/src/routes/graphs/+page.svelte index d3110b0..27d423e 100644 --- a/site/src/routes/graphs/+page.svelte +++ b/site/src/routes/graphs/+page.svelte @@ -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 @@ {/each} +

Cool graph

+ diff --git a/site/src/routes/graphs/CoolGraph/CoolGraph.svelte b/site/src/routes/graphs/CoolGraph/CoolGraph.svelte new file mode 100644 index 0000000..613837e --- /dev/null +++ b/site/src/routes/graphs/CoolGraph/CoolGraph.svelte @@ -0,0 +1,196 @@ + + +{#if statusStore.nodesR && posByDepth.length > 0} +
+
+ {depth} + +
+
+ + {#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} + + {/if} + {/each} + {/snippet} + +
+
+{/if} diff --git a/site/src/routes/graphs/CoolGraph/CoolGraphNode.svelte b/site/src/routes/graphs/CoolGraph/CoolGraphNode.svelte new file mode 100644 index 0000000..d63d3e8 --- /dev/null +++ b/site/src/routes/graphs/CoolGraph/CoolGraphNode.svelte @@ -0,0 +1,182 @@ + + +
+ +
+{#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} +
+{/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)} +
+{/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)} +
+ {/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)} +
+ {/each} +{/if} + + diff --git a/site/src/routes/graphs/CoolGraph/types.ts b/site/src/routes/graphs/CoolGraph/types.ts new file mode 100644 index 0000000..bc058a4 --- /dev/null +++ b/site/src/routes/graphs/CoolGraph/types.ts @@ -0,0 +1 @@ +export type PrevType = { prev?: PrevType; cur: Record }; diff --git a/site/src/routes/graphs/utils.ts b/site/src/routes/graphs/utils.ts index 3aa3667..23d58d7 100644 --- a/site/src/routes/graphs/utils.ts +++ b/site/src/routes/graphs/utils.ts @@ -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, - 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, + 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 extends string = '' ->(width: number, height: number, enforce: boolean): EnforceHelper { - const c = document.createElement('canvas'); - const ctx = c.getContext('2d'); - if (!ctx) throw new Error('Failed to get ctx'); +export function enforceSizeHelper, NamesH extends string = ''>( + width: number, + height: number, + enforce: boolean +): EnforceHelper { + 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[] - } as EnforceHelper; + _h_indicators: [] as Indicator[] + } as EnforceHelper; - r.h = (name, h, type, data, font) => { - const nr = r as EnforceHelper< - NamesH | typeof name, - Record - >; + r.h = (name, h, type, data, font) => { + const nr = r as EnforceHelper>; - // 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, 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; } -