@zvk/charts
v0.1.3
Published
Dependency-free chart utilities and server-safe SVG React components for ZVK dashboards.
Readme
@zvk/charts
Dependency-free chart utilities and server-safe SVG React components for ZVK dashboards.
import { createLinearScale, deriveBarChartModel, deriveSparklineModel, deriveSparklineSummary, linePath, planChartTicks } from "@zvk/charts";
import { AreaChart, BarChart, BulletChart, GroupedBarChart, Histogram, LineChart, ScatterPlot, Sparkline, StackedBarChart } from "@zvk/charts/react";
import "@zvk/charts/styles.css";@zvk/charts is static-first. The root package exports pure chart math, layout, bar, sparkline, line-detail, axis, diagnostics, and table-model helpers. @zvk/charts/react exports deterministic React 19 components that render SVG figures without browser APIs at module initialization. @zvk/charts/styles.css provides the .zvk-chart* class and --zvk-chart-* variable contract.
The package has no runtime dependencies. React and React DOM are peers for the React subpath.
Chart Types
v1 includes:
BarChartfor categorical comparison.GroupedBarChartfor comparing independent series inside each category.StackedBarChartfor additive part-to-whole bar segments.LineChartfor ordered or time-like trends.AreaChartfor filled trend volume.Sparklinefor compact KPI trends.BulletChartfor progress against target ranges.ScatterPlotfor relationships between two numeric measures.Histogramfor numeric distributions.ChartFrame,ChartLegend, andChartDataTablefor static accessible chart composition.- Utilities for linear scales, band scales, nice numeric ticks, UTC millisecond normalization, bar layout, line paths, line detail models, bar models, grouped/stacked/area/bullet/scatter/histogram models, annotations, sparkline summaries, small-multiple layout planning, density planning, axis tick planning, reference marks, diagnostics, and table models.
Deferred by design:
- rich tooltips;
- animation;
- pie/donut, stacked area, brush/zoom, and graph/node-link visualization;
- canvas/WebGL renderers;
- D3, Recharts, Chart.js, Nivo, ECharts, visx, or Tremor dependencies.
Accessibility
Charts are complex images. Every chart component requires:
title;description;- readable series/category labels;
- formatters for values whose raw numbers are not user-facing;
- a visible summary or table fallback for decision-making charts.
Static React components render a <figure> and an SVG with role="img", visible title/description IDs, and SVG <title> / <desc> fallbacks. Use showDataTable when exact values matter.
type RevenueRow = {
month: "Jan" | "Feb" | "Mar";
revenue: number;
};
const data = [
{ month: "Jan", revenue: 12000 },
{ month: "Feb", revenue: 14200 },
{ month: "Mar", revenue: 13800 }
] satisfies RevenueRow[];
<BarChart
data={data}
description="Revenue peaked in February and stayed above January in March."
showDataTable
title="Monthly revenue"
valueFormatter={(value) => `$${value.toLocaleString("en-US")}`}
x="month"
y="revenue"
/>;Grouped and stacked bars are separate exports so their multi-series semantics stay explicit:
<GroupedBarChart
category="quarter"
data={[
{ quarter: "Q1", enterprise: 124, selfServe: 66 },
{ quarter: "Q2", enterprise: 148, selfServe: 72 }
]}
description="Enterprise and self-serve revenue compared by quarter."
series={[
{ id: "enterprise", label: "Enterprise", value: "enterprise" },
{ id: "self-serve", label: "Self serve", value: "selfServe" }
]}
showDataTable
title="Segment revenue"
/>;
<StackedBarChart
category="quarter"
data={[
{ quarter: "Q1", new: 42, expansion: 18, churn: -9 },
{ quarter: "Q2", new: 48, expansion: 24, churn: -12 }
]}
description="New and expansion revenue are stacked against churn."
series={[
{ id: "new", label: "New", value: "new" },
{ id: "expansion", label: "Expansion", value: "expansion" },
{ id: "churn", label: "Churn", tone: "negative", value: "churn" }
]}
showDataTable
title="ARR movement"
/>;Area, bullet, scatter, and histogram components cover common static dashboard foundations:
<AreaChart
data={data}
description="Revenue filled to the baseline for the quarter."
series={[{ id: "revenue", label: "Revenue", x: "month", y: "revenue" }]}
showDataTable
title="Revenue area"
xScale="linear"
/>;
<BulletChart
data={[{ label: "Revenue", value: 72, target: 80, warning: 60, good: 90 }]}
description="Progress against target and qualitative ranges."
label="label"
ranges={[
{ id: "warning", label: "Warning", value: "warning", tone: "warning" },
{ id: "good", label: "Good", value: "good", tone: "positive" }
]}
target="target"
title="KPI health"
value="value"
/>;LineChart can render static point markers and latest/extrema value labels without adding client JavaScript:
<LineChart
data={data}
description="Revenue peaked in February and stayed above January in March."
pointLabels="latest"
series={[{ id: "revenue", label: "Revenue", x: "month", y: "revenue", unit: "USD" }]}
showPoints
title="Revenue trend"
xScale="linear"
valueFormatter={(value) => `$${value.toLocaleString("en-US")}`}
/>;Line charts can also render static x-axis labels and labeled reference marks:
<LineChart
data={data}
description="Revenue stayed near the target range through the quarter."
referenceBands={[{ axis: "y", from: 12000, id: "target-range", label: "Target range", to: 15000 }]}
referenceLines={[{ axis: "y", id: "goal", label: "Goal", value: 14000, tone: "warning" }]}
series={[{ id: "revenue", label: "Revenue", x: "month", y: "revenue", unit: "USD" }]}
title="Revenue trend"
xAxis={{ showLabels: true, tickFormatter: (value) => String(value) }}
xScale="linear"
/>;Use annotations for static callouts anchored to points or bars:
<LineChart
annotations={[{ anchor: { kind: "point", seriesId: "revenue", datumIndex: 2 }, id: "latest", label: "Latest" }]}
data={data}
description="Revenue increased through the quarter."
series={[{ id: "revenue", label: "Revenue", x: "month", y: "revenue" }]}
title="Revenue trend"
xScale="linear"
/>;Use deterministic axis options when dashboard copy needs stable labels. tickCount is a hint, tickValues is exact, and tickFormatter should be SSR-stable. Dense labels and duplicate formatted labels are reported through diagnostics instead of browser measurement.
For app-owned details panels, derive the same server-safe models used by the React charts:
import { createLinearScale, deriveBarChartModel, deriveLineChartDetailModel, deriveSparklineModel, deriveSparklineSummary, planChartTicks } from "@zvk/charts";
const lineModel = deriveLineChartDetailModel({
data,
plotHeight: 240,
plotWidth: 480,
series: [{ id: "revenue", label: "Revenue", x: "month", y: "revenue", unit: "USD" }],
xScale: "linear"
});
const barModel = deriveBarChartModel({
data,
height: 260,
title: "Monthly revenue",
width: 480,
x: "month",
y: "revenue"
});
const sparklineModel = deriveSparklineModel({
data,
value: "revenue",
valueFormatter: (value) => `$${value.toLocaleString("en-US")}`
});
const sparklineSummary = deriveSparklineSummary({
includePercentDelta: true,
values: data.map((datum) => datum.revenue),
valueFormatter: (value) => `$${value.toLocaleString("en-US")}`
});
const tickPlan = planChartTicks({
axis: "y",
maxLabelCount: 4,
scale: createLinearScale({ domain: [0, 20000], range: [240, 0] }),
tickValues: [0, 5000, 10000, 15000, 20000]
});deriveBarChartModel exposes resolved categories, bars, value ticks, reference marks, table rows, and diagnostics before React renders. Grouped, stacked, area, bullet, scatter, and histogram models expose geometry and table metadata for persistent details panels. deriveSparklineModel exposes the path, latest value, min/max, delta, direction, and formatter-backed summary for compact KPI cards. deriveSparklineSummary creates formatter-driven KPI copy, including optional percent delta. planChartTicks, planVisiblePointLabels, and planSmallMultiples return visibility/layout state and diagnostics for omitted dense labels or repeated chart panels.
Pure helpers return diagnostics for invalid data, duplicate IDs, unresolved annotation anchors, omitted dense labels or markers, duplicate formatted tick labels, and out-of-domain reference marks. Static React components surface diagnostic count and highest severity as data-chart-diagnostic-count and data-chart-diagnostic-severity attributes instead of invoking callbacks during render.
import { deriveReferenceMarks } from "@zvk/charts";BarChart supports categoryId and categoryLabel when stable layout identity differs from the visible label. Without categoryId, bars use deterministic index-based IDs so repeated labels do not collapse into one band.
<BarChart
categoryId="id"
categoryLabel="quarter"
data={[
{ id: "north-q1", quarter: "Q1", revenue: 10 },
{ id: "south-q1", quarter: "Q1", revenue: 12 }
]}
description="Both regions share the same displayed quarter label."
title="Quarter revenue"
x="quarter"
y="revenue"
/>;Styling
Load chart styles after @zvk/ui/styles.css when an app uses both packages:
import "@zvk/ui/styles.css";
import "@zvk/charts/styles.css";The chart stylesheet still works without @zvk/ui; chart variables fall back to browser colors and package defaults. Customize through --zvk-chart-* variables instead of importing @zvk/ui internals.
Chart typography uses --zvk-chart-font-primary, --zvk-chart-font-secondary, and --zvk-chart-font-tertiary. These variables inherit --zvk-ui-font-family-primary, --zvk-ui-font-family-secondary, and --zvk-ui-font-family-tertiary when UI styles are present.
Server Safety
The root and React subpaths do not read window, document, ResizeObserver, matchMedia, or layout state at module initialization. Browser-only behavior belongs in a future client subpath with its own accessibility and interaction contract. Diagnostic callbacks are intentionally not part of the static React API because effects would make charts client-bound and render-time callbacks would be side effects.
Repo Skill
Use .codex/skills/use-zvk-charts/SKILL.md when maintaining this package.
