Chart
A self-built canvas time-series chart engine sharing the chart-kit math layer — automatic canvas theming, responsive width, interactive legend and hover tooltip
Chart is a self-built canvas time-series chart powered by the chart-kit math layer (scaleLinear, linePath / areaPath, monotone smoothing, nice ticks, LTTB downsampling) and useChartPalette. It colors data series automatically with the 8-color palette, follows theme variables (--color-border / --color-muted-foreground) for grid and axis text, redraws responsively via ResizeObserver when the container resizes, and ships an interactive legend (click to toggle series) plus a cursor-following tooltip.
Within the chart family, Chart is the canvas-engine member: Donut, Radar and Sparkline are zero-dependency SVG components for categorical and small datasets, while Chart owns large time series — keep it whenever a series reaches thousands of points or needs box-zoom on a time axis. Rendering is DPR-aware canvas 2D; everything (crosshair, selection box) is drawn in-canvas and the component needs no external CSS.
Installation
Chart depends on the useChartPalette hook and the chart-kit / chart-paint utilities — litefy add chart installs them automatically:
useChartPalette can also be used standalone for custom visualizations — call useChartPalette({ mode: "wheel" | "mono" }) to get 8 theme-synced colors. See use-chart-palette for details.
Usage
Line Chart
"use client";
import { Chart } from "@/ui";
import type { ChartSeriesConfig } from "@/ui";
function seeded(seed: number) {
let state = seed;
return () => {
state = (state * 1664525 + 1013904223) % 4294967296;
return state / 4294967296;
};
}
const random = seeded(42);
const day = 86400;
const points = 30;
const now = Math.floor(Date.now() / 1000);
const x = Array.from({ length: points }, (_, i) => now - (points - 1 - i) * day);
const visits = x.map((_, i) => Math.round(700 + Math.sin(i / 4) * 150 + random() * 80));
const orders = x.map((_, i) => Math.round(450 + Math.cos(i / 5) * 80 + random() * 40));
const revenue = x.map((_, i) => Math.round(1000 + Math.sin(i / 6) * 250 + random() * 120));
const signups = x.map((_, i) => Math.round(200 + Math.sin(i / 3) * 50 + random() * 30));
const data: [number[], ...number[][]] = [x, visits, orders, revenue, signups];
const series: ChartSeriesConfig[] = [
{ label: "Visits" },
{ label: "Orders" },
{ label: "Revenue", width: 2 },
{ label: "Signups" },
];
export default function ChartLineDemo() {
return (
<div className="w-full max-w-2xl">
<Chart data={data} series={series} height={260} />
</div>
);
}
data is ChartData ([number[], ...number[][]]): the first array holds x values (time defaults to true, so pass Unix timestamps in seconds), and each following array maps to the corresponding entry in series. Series without an explicit stroke pick colors from the palette by index (cycling).
Area Chart
"use client";
import { Chart } from "@/ui";
import type { ChartSeriesConfig } from "@/ui";
function seeded(seed: number) {
let state = seed;
return () => {
state = (state * 1664525 + 1013904223) % 4294967296;
return state / 4294967296;
};
}
const random = seeded(7);
const day = 86400;
const points = 20;
const now = Math.floor(Date.now() / 1000);
const x = Array.from({ length: points }, (_, i) => now - (points - 1 - i) * day);
const high = x.map((_, i) => Math.round(8 + Math.sin(i / 3) * 6 + random() * 3));
const avg = x.map((_, i) => Math.round(1 + Math.sin(i / 5) * 4 + random() * 2));
const low = x.map((_, i) => Math.round(-6 + Math.sin(i / 4) * 5 + random() * 3));
const data: [number[], ...number[][]] = [x, high, avg, low];
const series: ChartSeriesConfig[] = [
{ label: "High", fill: true },
{ label: "Avg" },
{ label: "Low", fill: true },
];
export default function ChartAreaDemo() {
return (
<div className="w-full max-w-2xl">
<Chart data={data} series={series} height={220} />
</div>
);
}
fill: true (or type: "area") fills from the line down to the lower edge of the plot area, so mixed positive/negative data like temperatures needs no extra configuration.
Bar Chart
"use client";
import { Chart } from "@/ui";
import { useChartPalette } from "@/ui";
import type { ChartSeriesConfig } from "@/ui";
const x = Array.from({ length: 12 }, (_, i) => i + 1);
const returning = [420, 480, 510, 640, 720, 810, 940, 880, 1020, 1150, 1240, 1380];
const fresh = [180, 220, 260, 310, 380, 420, 460, 440, 520, 590, 660, 740];
const data: [number[], ...number[][]] = [x, returning, fresh];
export default function ChartBarsDemo() {
const palette = useChartPalette({ mode: "mono" });
const series: ChartSeriesConfig[] = [
{
label: "Returning",
type: "bar",
stroke: palette[0],
fill: palette[0],
},
{
label: "New",
type: "bar",
stroke: palette[1],
fill: palette[1],
value: (v) => `${v} users`,
},
];
return (
<div className="w-full max-w-2xl">
<Chart data={data} series={series} height={220} time={false} />
</div>
);
}
Bars are a native series type: pass type: "bar" and the renderer draws grouped bars (70% of each x band, split evenly between bar series). Bar colors come from useChartPalette explicitly, assigned to both stroke and fill. Bar charts usually pair with time={false} for a numeric x axis — in that mode the component automatically extends the x range by half a unit on both ends so edge bars are not clipped.
API
ChartProps
| Prop | Type | Default | Description |
|---|---|---|---|
| data | ChartData ([number[], ...number[][]]) | - | Aligned chart data, first array is x values |
| series | ChartSeriesConfig[] | - | Series configs, one per y array in data |
| height | number | 240 | Chart height in pixels; width always adapts to the container |
| time | boolean | true | Whether the x axis is a time scale; when false it is treated as plain numbers |
| className | ClassNameValue | - | Extra classes appended to the root element |
| onReady | () => void | - | Called once after the first render |
ChartSeriesConfig
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | - | Series name, shown in the legend and tooltip |
| type | "line" | "area" | "bar" | "line" | How the series is drawn |
| stroke | string | Auto from palette | Line color, any valid CSS color |
| width | number | 1.5 | Line width in pixels (line / area only) |
| fill | string | boolean | - | Area fill; true uses the line color at 18% opacity, a string is used as a custom fill color |
| value | (v: number) => string | - | Value formatter used in the tooltip |
| show | boolean | true | Whether the series is initially visible |
Behavior Notes
- Legend: rendered above the chart; click to toggle a series off/on.
- Zoom: drag a box in the plot area to zoom into that x region (the y range auto-fits the visible window); double-click to reset to the full data range.
- Tooltip: shows every visible series' value at the cursor's x position, follows the cursor and clamps to the container bounds.
- Theme sync: when the theme changes primary/background colors,
useChartPalettetriggers a rebuild so series and grid colors update accordingly. - Palette modes:
useChartPalette()picks hues around the color wheel by default (high contrast);useChartPalette({ mode: "mono" })stays within the primary hue family (series are distinguished by lightness — great for theme-consistent bar/area charts). When the primary is achromatic, both modes fall back to a blue hue. - Time axis: tick positions and labels are generated by
niceTimeTicks(second / minute / hour / day / month / year boundaries, locale-formatted) and re-computed on zoom. - Large datasets: series with more than 2000 points in view are reduced with the LTTB algorithm to roughly one point per pixel, keeping hover and drag interactions smooth.
- Data updates: the canvas redraws whenever
datachanges — suitable for polling and realtime streams.