From ad912d45047c41f1d454d1a487dce77f0b568f41 Mon Sep 17 00:00:00 2001 From: autocommit Date: Mon, 20 Apr 2026 01:10:42 -0700 Subject: [PATCH] chore: initial package split from monorepo Package: @lilith/chart-math Split from: lilith/ui.git or lilith/build.git Publish workflow: calls lilith/workflows/.forgejo/workflows/publish-npm.yml@main --- .forgejo/workflows/publish.yml | 10 ++ dist/catmull-rom.d.ts | 39 ++++++ dist/catmull-rom.d.ts.map | 1 + dist/catmull-rom.js | 62 +++++++++ dist/catmull-rom.js.map | 1 + dist/chart.d.ts | 94 +++++++++++++ dist/chart.d.ts.map | 1 + dist/chart.js | 155 ++++++++++++++++++++++ dist/chart.js.map | 1 + dist/index.d.ts | 3 + dist/index.d.ts.map | 1 + dist/index.js | 3 + dist/index.js.map | 1 + node_modules/.bin/vitest | 1 + node_modules/@types/react | 1 + node_modules/@types/react-dom | 1 + node_modules/react | 1 + node_modules/react-dom | 1 + node_modules/styled-components | 1 + node_modules/vitest | 1 + package.json | 39 ++++++ src/catmull-rom.ts | 74 +++++++++++ src/chart.ts | 233 +++++++++++++++++++++++++++++++++ src/index.ts | 2 + tsconfig.json | 12 ++ 25 files changed, 739 insertions(+) create mode 100644 .forgejo/workflows/publish.yml create mode 100644 dist/catmull-rom.d.ts create mode 100644 dist/catmull-rom.d.ts.map create mode 100644 dist/catmull-rom.js create mode 100644 dist/catmull-rom.js.map create mode 100644 dist/chart.d.ts create mode 100644 dist/chart.d.ts.map create mode 100644 dist/chart.js create mode 100644 dist/chart.js.map create mode 100644 dist/index.d.ts create mode 100644 dist/index.d.ts.map create mode 100644 dist/index.js create mode 100644 dist/index.js.map create mode 120000 node_modules/.bin/vitest create mode 120000 node_modules/@types/react create mode 120000 node_modules/@types/react-dom create mode 120000 node_modules/react create mode 120000 node_modules/react-dom create mode 120000 node_modules/styled-components create mode 120000 node_modules/vitest create mode 100644 package.json create mode 100644 src/catmull-rom.ts create mode 100644 src/chart.ts create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/.forgejo/workflows/publish.yml b/.forgejo/workflows/publish.yml new file mode 100644 index 0000000..d9d5cdd --- /dev/null +++ b/.forgejo/workflows/publish.yml @@ -0,0 +1,10 @@ +name: Publish +on: + push: + branches: [main, master] + workflow_dispatch: +jobs: + publish: + uses: lilith/workflows/.forgejo/workflows/publish-npm.yml@main + secrets: inherit + diff --git a/dist/catmull-rom.d.ts b/dist/catmull-rom.d.ts new file mode 100644 index 0000000..f38c27c --- /dev/null +++ b/dist/catmull-rom.d.ts @@ -0,0 +1,39 @@ +/** + * Catmull-Rom spline → SVG cubic bezier conversion utilities. + * + * Generates C1-continuous curves through all control points, + * producing smoother interpolation than pairwise bezier. + */ +export interface Pt2 { + x: number; + y: number; +} +/** + * Generate SVG cubic bezier C commands for a Catmull-Rom spline through the given points. + * Does NOT include the initial M command — only the C segments. + * + * @param pts - Control points (minimum 2) + * @param tension - Spline tension (0 = linear, 0.5 = standard Catmull-Rom, 1 = tight) + * @returns SVG path string containing only C commands + */ +export declare function catmullRomSegments(pts: Pt2[], tension?: number): string; +/** + * Generate a complete SVG path (M + C commands) for a Catmull-Rom spline. + * + * @param pts - Control points (minimum 2) + * @param tension - Spline tension (default 0.5) + * @returns Complete SVG path string starting with M + */ +export declare function catmullRomPath(pts: Pt2[], tension?: number): string; +/** + * Generate a closed SVG area path for a stream ribbon. + * Traces the top edge left-to-right, then the bottom edge right-to-left, + * both as Catmull-Rom splines, closing with Z. + * + * @param top - Upper boundary points (left-to-right) + * @param bottom - Lower boundary points (left-to-right, will be reversed internally) + * @param tension - Spline tension (default 0.5) + * @returns Closed SVG path string + */ +export declare function catmullRomAreaPath(top: Pt2[], bottom: Pt2[], tension?: number): string; +//# sourceMappingURL=catmull-rom.d.ts.map \ No newline at end of file diff --git a/dist/catmull-rom.d.ts.map b/dist/catmull-rom.d.ts.map new file mode 100644 index 0000000..76259cf --- /dev/null +++ b/dist/catmull-rom.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"catmull-rom.d.ts","sourceRoot":"","sources":["../src/catmull-rom.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,GAAG;IAClB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,OAAO,SAAM,GAAG,MAAM,CAqBpE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,OAAO,SAAM,GAAG,MAAM,CAGhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,OAAO,SAAM,GAAG,MAAM,CAQnF"} \ No newline at end of file diff --git a/dist/catmull-rom.js b/dist/catmull-rom.js new file mode 100644 index 0000000..833fa17 --- /dev/null +++ b/dist/catmull-rom.js @@ -0,0 +1,62 @@ +/** + * Catmull-Rom spline → SVG cubic bezier conversion utilities. + * + * Generates C1-continuous curves through all control points, + * producing smoother interpolation than pairwise bezier. + */ +/** + * Generate SVG cubic bezier C commands for a Catmull-Rom spline through the given points. + * Does NOT include the initial M command — only the C segments. + * + * @param pts - Control points (minimum 2) + * @param tension - Spline tension (0 = linear, 0.5 = standard Catmull-Rom, 1 = tight) + * @returns SVG path string containing only C commands + */ +export function catmullRomSegments(pts, tension = 0.5) { + if (pts.length < 2) + return ''; + const out = []; + for (let i = 0; i < pts.length - 1; i++) { + const p0 = pts[Math.max(0, i - 1)]; + const p1 = pts[i]; + const p2 = pts[i + 1]; + const p3 = pts[Math.min(pts.length - 1, i + 2)]; + const c1x = p1.x + (tension * (p2.x - p0.x)) / 3; + const c1y = p1.y + (tension * (p2.y - p0.y)) / 3; + const c2x = p2.x - (tension * (p3.x - p1.x)) / 3; + const c2y = p2.y - (tension * (p3.y - p1.y)) / 3; + out.push(`C ${c1x.toFixed(1)} ${c1y.toFixed(1)},${c2x.toFixed(1)} ${c2y.toFixed(1)},${p2.x.toFixed(1)} ${p2.y.toFixed(1)}`); + } + return out.join(' '); +} +/** + * Generate a complete SVG path (M + C commands) for a Catmull-Rom spline. + * + * @param pts - Control points (minimum 2) + * @param tension - Spline tension (default 0.5) + * @returns Complete SVG path string starting with M + */ +export function catmullRomPath(pts, tension = 0.5) { + if (pts.length < 2) + return ''; + return `M ${pts[0].x.toFixed(1)} ${pts[0].y.toFixed(1)} ${catmullRomSegments(pts, tension)}`; +} +/** + * Generate a closed SVG area path for a stream ribbon. + * Traces the top edge left-to-right, then the bottom edge right-to-left, + * both as Catmull-Rom splines, closing with Z. + * + * @param top - Upper boundary points (left-to-right) + * @param bottom - Lower boundary points (left-to-right, will be reversed internally) + * @param tension - Spline tension (default 0.5) + * @returns Closed SVG path string + */ +export function catmullRomAreaPath(top, bottom, tension = 0.5) { + if (top.length < 2 || bottom.length < 2) + return ''; + const reversed = [...bottom].reverse(); + const topPath = catmullRomPath(top, tension); + const bottomSegs = catmullRomSegments(reversed, tension); + return `${topPath} L ${reversed[0].x.toFixed(1)} ${reversed[0].y.toFixed(1)} ${bottomSegs} Z`; +} +//# sourceMappingURL=catmull-rom.js.map \ No newline at end of file diff --git a/dist/catmull-rom.js.map b/dist/catmull-rom.js.map new file mode 100644 index 0000000..1664708 --- /dev/null +++ b/dist/catmull-rom.js.map @@ -0,0 +1 @@ +{"version":3,"file":"catmull-rom.js","sourceRoot":"","sources":["../src/catmull-rom.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/chart.d.ts b/dist/chart.d.ts new file mode 100644 index 0000000..7037d5e --- /dev/null +++ b/dist/chart.d.ts @@ -0,0 +1,94 @@ +/** + * Pure math utilities for SVG chart rendering. + * Scales, path generation, sparklines, and axis computation. + */ +export interface ChartDimensions { + width: number; + height: number; + padding: number; + chartWidth: number; + chartHeight: number; +} +export interface ScaleConfig { + min: number; + max: number; + range: number; +} +/** + * Calculate chart dimensions with padding + */ +export declare function calculateChartDimensions(width: number, height: number, padding: number): ChartDimensions; +/** + * Calculate scale configuration for a set of values + */ +export declare function calculateScale(values: number[], includeZero?: boolean): ScaleConfig; +/** + * Create a linear scale function + */ +export declare function createLinearScale(domain: [number, number], range: [number, number]): (value: number) => number; +/** + * Generate evenly spaced tick values + */ +export declare function generateTicks(min: number, max: number, count: number): number[]; +/** + * Generate SVG path for line chart + */ +export declare function generateLinePath(points: Array<{ + x: number; + y: number; +}>, curve?: boolean): string; +/** + * Generate SVG path for area under line + */ +export declare function generateAreaPath(linePath: string, firstX: number, lastX: number, baselineY: number): string; +/** + * Calculate points for a sparkline + */ +export declare function calculateSparklinePoints(data: number[], width: number, height: number): Array<{ + x: number; + y: number; +}>; +/** + * Format timestamp as HH:MM:SS for chart axis labels + * + * @param timestamp - Unix timestamp in milliseconds + * @returns Formatted time string (e.g., "14:30:05") + */ +export declare function formatChartTime(timestamp: number): string; +/** + * Auto-scale configuration for Y-axis + */ +export interface AutoYScaleConfig { + /** Minimum value on the axis */ + min: number; + /** Maximum value on the axis */ + max: number; + /** Range (max - min) */ + range: number; +} +/** + * Calculate auto-scaling Y-axis bounds based on visible data with padding. + * Automatically adds 10% padding above and below the data range. + * + * @param values - Array of numeric values to calculate scale for + * @param options - Optional configuration + * @param options.paddingPercent - Padding percentage (default: 0.1 = 10%) + * @param options.minPadding - Minimum padding in data units (default: 5) + * @param options.includeZero - Whether to always include zero in the scale (default: false) + * @returns Scale configuration with min, max, and range + */ +export declare function calculateAutoYScale(values: number[], options?: { + paddingPercent?: number; + minPadding?: number; + includeZero?: boolean; +}): AutoYScaleConfig; +/** + * Get color from chart series palette by index. + * Cycles through palette if index exceeds palette length. + * + * @param index - Zero-based index of the data series + * @param palette - Array of color hex strings + * @returns Hex color string + */ +export declare function getSeriesColor(index: number, palette: readonly string[]): string; +//# sourceMappingURL=chart.d.ts.map \ No newline at end of file diff --git a/dist/chart.d.ts.map b/dist/chart.d.ts.map new file mode 100644 index 0000000..0aac8ec --- /dev/null +++ b/dist/chart.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"chart.d.ts","sourceRoot":"","sources":["../src/chart.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd,eAAe,CAQjB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,WAAW,UAAO,GAAG,WAAW,CAMhF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EACxB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GACtB,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAW3B;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAI/E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EAAE,KAAK,UAAQ,GAAG,MAAM,CAwB/F;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,MAAM,CAER;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EAAE,EACd,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,KAAK,CAAC;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAajC;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMzD;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,GAAE;IACP,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CAClB,GACL,gBAAgB,CAmClB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAKhF"} \ No newline at end of file diff --git a/dist/chart.js b/dist/chart.js new file mode 100644 index 0000000..de6d54f --- /dev/null +++ b/dist/chart.js @@ -0,0 +1,155 @@ +/** + * Pure math utilities for SVG chart rendering. + * Scales, path generation, sparklines, and axis computation. + */ +/** + * Calculate chart dimensions with padding + */ +export function calculateChartDimensions(width, height, padding) { + return { + width, + height, + padding, + chartWidth: width - padding * 2, + chartHeight: height - padding * 2, + }; +} +/** + * Calculate scale configuration for a set of values + */ +export function calculateScale(values, includeZero = true) { + const min = includeZero ? Math.min(...values, 0) : Math.min(...values); + const max = Math.max(...values); + const range = max - min || 1; + return { min, max, range }; +} +/** + * Create a linear scale function + */ +export function createLinearScale(domain, range) { + const [domainMin, domainMax] = domain; + const [rangeMin, rangeMax] = range; + const domainRange = domainMax - domainMin || 1; + const rangeRange = rangeMax - rangeMin; + return (value) => { + const normalized = (value - domainMin) / domainRange; + return rangeMin + normalized * rangeRange; + }; +} +/** + * Generate evenly spaced tick values + */ +export function generateTicks(min, max, count) { + const step = (max - min) / (count - 1); + return Array.from({ length: count }, (_, i) => min + step * i); +} +/** + * Generate SVG path for line chart + */ +export function generateLinePath(points, curve = false) { + if (points.length === 0) { + return ''; + } + const pathSegments = points.map((point, i) => { + if (i === 0) { + return `M ${point.x} ${point.y}`; + } + if (curve && i > 0) { + const prev = points[i - 1]; + const cp1x = prev.x + (point.x - prev.x) / 3; + const cp1y = prev.y; + const cp2x = point.x - (point.x - prev.x) / 3; + const cp2y = point.y; + return `C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${point.x} ${point.y}`; + } + return `L ${point.x} ${point.y}`; + }); + return pathSegments.join(' '); +} +/** + * Generate SVG path for area under line + */ +export function generateAreaPath(linePath, firstX, lastX, baselineY) { + return `${linePath} L ${lastX} ${baselineY} L ${firstX} ${baselineY} Z`; +} +/** + * Calculate points for a sparkline + */ +export function calculateSparklinePoints(data, width, height) { + if (data.length === 0) { + return []; + } + const scale = calculateScale(data, false); + const xScale = createLinearScale([0, data.length - 1], [0, width]); + const yScale = createLinearScale([scale.min, scale.max], [height, 0]); + return data.map((value, index) => ({ + x: xScale(index), + y: yScale(value), + })); +} +/** + * Format timestamp as HH:MM:SS for chart axis labels + * + * @param timestamp - Unix timestamp in milliseconds + * @returns Formatted time string (e.g., "14:30:05") + */ +export function formatChartTime(timestamp) { + const date = new Date(timestamp); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const seconds = date.getSeconds().toString().padStart(2, '0'); + return `${hours}:${minutes}:${seconds}`; +} +/** + * Calculate auto-scaling Y-axis bounds based on visible data with padding. + * Automatically adds 10% padding above and below the data range. + * + * @param values - Array of numeric values to calculate scale for + * @param options - Optional configuration + * @param options.paddingPercent - Padding percentage (default: 0.1 = 10%) + * @param options.minPadding - Minimum padding in data units (default: 5) + * @param options.includeZero - Whether to always include zero in the scale (default: false) + * @returns Scale configuration with min, max, and range + */ +export function calculateAutoYScale(values, options = {}) { + const { paddingPercent = 0.1, minPadding = 5, includeZero = false, } = options; + // Default scale when no data + if (values.length === 0) { + return { min: 0, max: 100, range: 100 }; + } + const dataMin = Math.min(...values); + const dataMax = Math.max(...values); + const dataRange = dataMax - dataMin; + // Calculate padding (percentage of range, or minimum padding if range is small) + const padding = Math.max(dataRange * paddingPercent, minPadding); + let min = Math.floor(dataMin - padding); + const max = Math.ceil(dataMax + padding); + // Optionally include zero in the scale + if (includeZero) { + min = Math.min(min, 0); + } + else { + // Never go below zero for typical chart data + min = Math.max(0, min); + } + return { + min, + max, + range: max - min, + }; +} +/** + * Get color from chart series palette by index. + * Cycles through palette if index exceeds palette length. + * + * @param index - Zero-based index of the data series + * @param palette - Array of color hex strings + * @returns Hex color string + */ +export function getSeriesColor(index, palette) { + if (palette.length === 0) { + return '#3B82F6'; // Default blue + } + return palette[index % palette.length] ?? palette[0]; +} +//# sourceMappingURL=chart.js.map \ No newline at end of file diff --git a/dist/chart.js.map b/dist/chart.js.map new file mode 100644 index 0000000..183b842 --- /dev/null +++ b/dist/chart.js.map @@ -0,0 +1 @@ +{"version":3,"file":"chart.js","sourceRoot":"","sources":["../src/chart.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..530a337 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,3 @@ +export * from './chart.ts'; +export * from './catmull-rom.ts'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map new file mode 100644 index 0000000..d22ac5e --- /dev/null +++ b/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC"} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..0ca9c61 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,3 @@ +export * from "./chart.js"; +export * from "./catmull-rom.js"; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map new file mode 100644 index 0000000..fca3650 --- /dev/null +++ b/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC"} \ No newline at end of file diff --git a/node_modules/.bin/vitest b/node_modules/.bin/vitest new file mode 120000 index 0000000..2273497 --- /dev/null +++ b/node_modules/.bin/vitest @@ -0,0 +1 @@ +../vitest/vitest.mjs \ No newline at end of file diff --git a/node_modules/@types/react b/node_modules/@types/react new file mode 120000 index 0000000..d29c301 --- /dev/null +++ b/node_modules/@types/react @@ -0,0 +1 @@ +../../../../node_modules/.bun/@types+react@19.2.14/node_modules/@types/react \ No newline at end of file diff --git a/node_modules/@types/react-dom b/node_modules/@types/react-dom new file mode 120000 index 0000000..f615321 --- /dev/null +++ b/node_modules/@types/react-dom @@ -0,0 +1 @@ +../../../../node_modules/.bun/@types+react-dom@19.2.3+273cdfb19a04c3e9/node_modules/@types/react-dom \ No newline at end of file diff --git a/node_modules/react b/node_modules/react new file mode 120000 index 0000000..91a2b48 --- /dev/null +++ b/node_modules/react @@ -0,0 +1 @@ +../../../node_modules/.bun/react@19.2.5/node_modules/react \ No newline at end of file diff --git a/node_modules/react-dom b/node_modules/react-dom new file mode 120000 index 0000000..a0ee521 --- /dev/null +++ b/node_modules/react-dom @@ -0,0 +1 @@ +../../../node_modules/.bun/react-dom@19.2.5+3f10a4be4e334a9b/node_modules/react-dom \ No newline at end of file diff --git a/node_modules/styled-components b/node_modules/styled-components new file mode 120000 index 0000000..5ea5838 --- /dev/null +++ b/node_modules/styled-components @@ -0,0 +1 @@ +../../../node_modules/.bun/styled-components@6.4.0+21ccd8898788a04d/node_modules/styled-components \ No newline at end of file diff --git a/node_modules/vitest b/node_modules/vitest new file mode 120000 index 0000000..30aec3d --- /dev/null +++ b/node_modules/vitest @@ -0,0 +1 @@ +../../../node_modules/.bun/vitest@2.1.9+1f2148f19abc7b8c/node_modules/vitest \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..10209a4 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "@lilith/chart-math", + "version": "1.0.1", + "description": "Pure math utilities for SVG chart rendering — scales, paths, sparklines, Catmull-Rom splines", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": ["dist"], + "scripts": { + "build": "tsc --project tsconfig.json", + "typecheck": "tsc --noEmit", + "lint": "eslint src --fix", + "lint:check": "eslint src" + }, + "devDependencies": { + "@types/react": "^19.2.8", + "@types/react-dom": "^19.2.3", + "vitest": "^2.1.9" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "styled-components": "^6.0.0" + }, + "publishConfig": { + "registry": "http://forge.black.local/api/packages/lilith/npm/" + }, + "_": { + "registry": "forgejo", + "publish": true, + "build": true + } +} diff --git a/src/catmull-rom.ts b/src/catmull-rom.ts new file mode 100644 index 0000000..726d269 --- /dev/null +++ b/src/catmull-rom.ts @@ -0,0 +1,74 @@ +/** + * Catmull-Rom spline → SVG cubic bezier conversion utilities. + * + * Generates C1-continuous curves through all control points, + * producing smoother interpolation than pairwise bezier. + */ + +export interface Pt2 { + x: number; + y: number; +} + +/** + * Generate SVG cubic bezier C commands for a Catmull-Rom spline through the given points. + * Does NOT include the initial M command — only the C segments. + * + * @param pts - Control points (minimum 2) + * @param tension - Spline tension (0 = linear, 0.5 = standard Catmull-Rom, 1 = tight) + * @returns SVG path string containing only C commands + */ +export function catmullRomSegments(pts: Pt2[], tension = 0.5): string { + if (pts.length < 2) return ''; + + const out: string[] = []; + for (let i = 0; i < pts.length - 1; i++) { + const p0 = pts[Math.max(0, i - 1)]; + const p1 = pts[i]; + const p2 = pts[i + 1]; + const p3 = pts[Math.min(pts.length - 1, i + 2)]; + + const c1x = p1.x + (tension * (p2.x - p0.x)) / 3; + const c1y = p1.y + (tension * (p2.y - p0.y)) / 3; + const c2x = p2.x - (tension * (p3.x - p1.x)) / 3; + const c2y = p2.y - (tension * (p3.y - p1.y)) / 3; + + out.push( + `C ${c1x.toFixed(1)} ${c1y.toFixed(1)},${c2x.toFixed(1)} ${c2y.toFixed(1)},${p2.x.toFixed(1)} ${p2.y.toFixed(1)}`, + ); + } + + return out.join(' '); +} + +/** + * Generate a complete SVG path (M + C commands) for a Catmull-Rom spline. + * + * @param pts - Control points (minimum 2) + * @param tension - Spline tension (default 0.5) + * @returns Complete SVG path string starting with M + */ +export function catmullRomPath(pts: Pt2[], tension = 0.5): string { + if (pts.length < 2) return ''; + return `M ${pts[0].x.toFixed(1)} ${pts[0].y.toFixed(1)} ${catmullRomSegments(pts, tension)}`; +} + +/** + * Generate a closed SVG area path for a stream ribbon. + * Traces the top edge left-to-right, then the bottom edge right-to-left, + * both as Catmull-Rom splines, closing with Z. + * + * @param top - Upper boundary points (left-to-right) + * @param bottom - Lower boundary points (left-to-right, will be reversed internally) + * @param tension - Spline tension (default 0.5) + * @returns Closed SVG path string + */ +export function catmullRomAreaPath(top: Pt2[], bottom: Pt2[], tension = 0.5): string { + if (top.length < 2 || bottom.length < 2) return ''; + + const reversed = [...bottom].reverse(); + const topPath = catmullRomPath(top, tension); + const bottomSegs = catmullRomSegments(reversed, tension); + + return `${topPath} L ${reversed[0].x.toFixed(1)} ${reversed[0].y.toFixed(1)} ${bottomSegs} Z`; +} diff --git a/src/chart.ts b/src/chart.ts new file mode 100644 index 0000000..f681bf3 --- /dev/null +++ b/src/chart.ts @@ -0,0 +1,233 @@ +/** + * Pure math utilities for SVG chart rendering. + * Scales, path generation, sparklines, and axis computation. + */ + +export interface ChartDimensions { + width: number; + height: number; + padding: number; + chartWidth: number; + chartHeight: number; +} + +export interface ScaleConfig { + min: number; + max: number; + range: number; +} + +/** + * Calculate chart dimensions with padding + */ +export function calculateChartDimensions( + width: number, + height: number, + padding: number, +): ChartDimensions { + return { + width, + height, + padding, + chartWidth: width - padding * 2, + chartHeight: height - padding * 2, + }; +} + +/** + * Calculate scale configuration for a set of values + */ +export function calculateScale(values: number[], includeZero = true): ScaleConfig { + const min = includeZero ? Math.min(...values, 0) : Math.min(...values); + const max = Math.max(...values); + const range = max - min || 1; + + return { min, max, range }; +} + +/** + * Create a linear scale function + */ +export function createLinearScale( + domain: [number, number], + range: [number, number], +): (value: number) => number { + const [domainMin, domainMax] = domain; + const [rangeMin, rangeMax] = range; + const domainRange = domainMax - domainMin || 1; + const rangeRange = rangeMax - rangeMin; + + return (value: number) => { + const normalized = (value - domainMin) / domainRange; + + return rangeMin + normalized * rangeRange; + }; +} + +/** + * Generate evenly spaced tick values + */ +export function generateTicks(min: number, max: number, count: number): number[] { + const step = (max - min) / (count - 1); + + return Array.from({ length: count }, (_, i) => min + step * i); +} + +/** + * Generate SVG path for line chart + */ +export function generateLinePath(points: Array<{ x: number; y: number }>, curve = false): string { + if (points.length === 0) { + return ''; + } + + const pathSegments = points.map((point, i) => { + if (i === 0) { + return `M ${point.x} ${point.y}`; + } + + if (curve && i > 0) { + const prev = points[i - 1]; + const cp1x = prev.x + (point.x - prev.x) / 3; + const cp1y = prev.y; + const cp2x = point.x - (point.x - prev.x) / 3; + const cp2y = point.y; + + return `C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${point.x} ${point.y}`; + } + + return `L ${point.x} ${point.y}`; + }); + + return pathSegments.join(' '); +} + +/** + * Generate SVG path for area under line + */ +export function generateAreaPath( + linePath: string, + firstX: number, + lastX: number, + baselineY: number, +): string { + return `${linePath} L ${lastX} ${baselineY} L ${firstX} ${baselineY} Z`; +} + +/** + * Calculate points for a sparkline + */ +export function calculateSparklinePoints( + data: number[], + width: number, + height: number, +): Array<{ x: number; y: number }> { + if (data.length === 0) { + return []; + } + + const scale = calculateScale(data, false); + const xScale = createLinearScale([0, data.length - 1], [0, width]); + const yScale = createLinearScale([scale.min, scale.max], [height, 0]); + + return data.map((value, index) => ({ + x: xScale(index), + y: yScale(value), + })); +} + +/** + * Format timestamp as HH:MM:SS for chart axis labels + * + * @param timestamp - Unix timestamp in milliseconds + * @returns Formatted time string (e.g., "14:30:05") + */ +export function formatChartTime(timestamp: number): string { + const date = new Date(timestamp); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const seconds = date.getSeconds().toString().padStart(2, '0'); + return `${hours}:${minutes}:${seconds}`; +} + +/** + * Auto-scale configuration for Y-axis + */ +export interface AutoYScaleConfig { + /** Minimum value on the axis */ + min: number; + /** Maximum value on the axis */ + max: number; + /** Range (max - min) */ + range: number; +} + +/** + * Calculate auto-scaling Y-axis bounds based on visible data with padding. + * Automatically adds 10% padding above and below the data range. + * + * @param values - Array of numeric values to calculate scale for + * @param options - Optional configuration + * @param options.paddingPercent - Padding percentage (default: 0.1 = 10%) + * @param options.minPadding - Minimum padding in data units (default: 5) + * @param options.includeZero - Whether to always include zero in the scale (default: false) + * @returns Scale configuration with min, max, and range + */ +export function calculateAutoYScale( + values: number[], + options: { + paddingPercent?: number; + minPadding?: number; + includeZero?: boolean; + } = {}, +): AutoYScaleConfig { + const { + paddingPercent = 0.1, + minPadding = 5, + includeZero = false, + } = options; + + // Default scale when no data + if (values.length === 0) { + return { min: 0, max: 100, range: 100 }; + } + + const dataMin = Math.min(...values); + const dataMax = Math.max(...values); + const dataRange = dataMax - dataMin; + + // Calculate padding (percentage of range, or minimum padding if range is small) + const padding = Math.max(dataRange * paddingPercent, minPadding); + + let min = Math.floor(dataMin - padding); + const max = Math.ceil(dataMax + padding); + + // Optionally include zero in the scale + if (includeZero) { + min = Math.min(min, 0); + } else { + // Never go below zero for typical chart data + min = Math.max(0, min); + } + + return { + min, + max, + range: max - min, + }; +} + +/** + * Get color from chart series palette by index. + * Cycles through palette if index exceeds palette length. + * + * @param index - Zero-based index of the data series + * @param palette - Array of color hex strings + * @returns Hex color string + */ +export function getSeriesColor(index: number, palette: readonly string[]): string { + if (palette.length === 0) { + return '#3B82F6'; // Default blue + } + return palette[index % palette.length] ?? palette[0]; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d52496a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export * from './chart.ts'; +export * from './catmull-rom.ts'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c7e8496 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "noEmit": false, + "rewriteRelativeImportExtensions": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +}