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
This commit is contained in:
autocommit 2026-04-20 01:10:42 -07:00
commit ad912d4504
25 changed files with 739 additions and 0 deletions

View file

@ -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

39
dist/catmull-rom.d.ts vendored Normal file
View file

@ -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

1
dist/catmull-rom.d.ts.map vendored Normal file
View file

@ -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"}

62
dist/catmull-rom.js vendored Normal file
View file

@ -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

1
dist/catmull-rom.js.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"catmull-rom.js","sourceRoot":"","sources":["../src/catmull-rom.ts"],"names":[],"mappings":"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"}

94
dist/chart.d.ts vendored Normal file
View file

@ -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

1
dist/chart.d.ts.map vendored Normal file
View file

@ -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"}

155
dist/chart.js vendored Normal file
View file

@ -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

1
dist/chart.js.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"chart.js","sourceRoot":"","sources":["../src/chart.ts"],"names":[],"mappings":"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"}

3
dist/index.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
export * from './chart.ts';
export * from './catmull-rom.ts';
//# sourceMappingURL=index.d.ts.map

1
dist/index.d.ts.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC"}

3
dist/index.js vendored Normal file
View file

@ -0,0 +1,3 @@
export * from "./chart.js";
export * from "./catmull-rom.js";
//# sourceMappingURL=index.js.map

1
dist/index.js.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC"}

1
node_modules/.bin/vitest generated vendored Symbolic link
View file

@ -0,0 +1 @@
../vitest/vitest.mjs

1
node_modules/@types/react generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../../node_modules/.bun/@types+react@19.2.14/node_modules/@types/react

1
node_modules/@types/react-dom generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../../node_modules/.bun/@types+react-dom@19.2.3+273cdfb19a04c3e9/node_modules/@types/react-dom

1
node_modules/react generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../node_modules/.bun/react@19.2.5/node_modules/react

1
node_modules/react-dom generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../node_modules/.bun/react-dom@19.2.5+3f10a4be4e334a9b/node_modules/react-dom

1
node_modules/styled-components generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../node_modules/.bun/styled-components@6.4.0+21ccd8898788a04d/node_modules/styled-components

1
node_modules/vitest generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../node_modules/.bun/vitest@2.1.9+1f2148f19abc7b8c/node_modules/vitest

39
package.json Normal file
View file

@ -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
}
}

74
src/catmull-rom.ts Normal file
View file

@ -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`;
}

233
src/chart.ts Normal file
View file

@ -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];
}

2
src/index.ts Normal file
View file

@ -0,0 +1,2 @@
export * from './chart.ts';
export * from './catmull-rom.ts';

12
tsconfig.json Normal file
View file

@ -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"]
}