@indiminds/charts
v2.2.0
Published
Dependency-free canvas charts — line, bar (vertical/horizontal), combo, pie/donut and candlestick — with configurable axes, animations and tooltips. Works with React, Next.js, Vue or plain JS.
Maintainers
Readme
@indiminds/charts
Fast, dependency-free canvas charts for trading and analytics dashboards. Line, bar (vertical or horizontal), combo, pie/donut and a full candlestick engine. Axes, animation, tooltips and theming are all configurable, and the same API works in React, Next.js, Vue, Svelte or plain JavaScript.
npm install @indiminds/charts- Zero runtime dependencies. No React, no D3, no charting library underneath.
- Tree-shakeable entry points. Import only the chart you use (4–10 kB gzipped each).
- Configurable. Hide or restyle any axis, add value labels, turn bars sideways, make a donut into a gauge.
- Smooth by default. Intro animations, tweened live updates, respects
prefers-reduced-motion. - Typed. Written in TypeScript, and ships
.d.tsfor every option and event. - Sharp on any screen. Handles device pixel ratio and container resizing for you.
Contents
- Charts at a glance
- Quick start
- Using with React / Next.js / Vue
- Core concepts
- Recipes
- Chart guides: Line · Bar · Combo · Pie & donut · Candlestick
- API reference
- Theming
- Accessibility, performance & browser support
- Migrating from 2.0
- License
Charts at a glance
| Import | Class | Use it for | Size (min / gzip) |
|---|---|---|---|
| @indiminds/charts/line | LineChart | Trends, one or two value axes | 15.8 kB / 6.3 kB |
| @indiminds/charts/bar | BarChart | Grouped, horizontal, before/after (delta) bars | 25.9 kB / 9.4 kB |
| @indiminds/charts/combo | ComboChart | Bars + line overlays on one category axis | 20.0 kB / 7.8 kB |
| @indiminds/charts/pie | PieChart | Pie, donut, half-ring gauge | 11.7 kB / 4.7 kB |
| @indiminds/charts | CandlestickChart | OHLC candles, indicators, panes, pan/zoom | 26.9 kB / 9.1 kB |
Each entry point is self-contained. Importing /bar never pulls in the
candlestick engine.
Quick start
Every chart draws into a <canvas> inside a container you give a height.
The examples use Tailwind CSS; any CSS that sets a height works the same.
<div id="chart" class="relative h-64"><canvas></canvas></div>import { BarChart } from "@indiminds/charts/bar";
const container = document.getElementById("chart")!;
const chart = new BarChart(container.querySelector("canvas")!, container, {
yAxis: { formatter: (v) => `${v}Cr` },
dataLabels: true,
tooltip: true,
});
chart.setData({
categories: ["NSEEQ", "BSEEQ", "NSEFO", "MCX"],
series: [{ id: "turnover", name: "Turnover", color: "#52667a", values: [2156, 1023, 648, 408] }],
});
// Later, when the element goes away:
chart.destroy();That's the whole lifecycle. Every chart has the same shape:
new Chart(canvas, container, options?)setData(data)whenever the data changessetOptions(options)whenever the look changesdestroy()when you're done
Using with React / Next.js / Vue
The package has no framework dependency. Wrap it in a few lines of your own.
React / Next.js
"use client"; // Next.js App Router: canvas needs the browser
import { useEffect, useRef } from "react";
import { BarChart, type BarChartData, type BarChartOptions, type BarHoverEvent } from "@indiminds/charts/bar";
interface Props {
data: BarChartData;
options?: BarChartOptions;
onHover?: (event: BarHoverEvent | null) => void;
className?: string; // sizes the container, e.g. "h-64" or "h-full"
}
export function Bars({ data, options, onHover, className = "h-64" }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const chartRef = useRef<BarChart | null>(null);
// Create once, destroy on unmount.
useEffect(() => {
const chart = new BarChart(canvasRef.current!, containerRef.current!, options);
chartRef.current = chart;
return () => chart.destroy();
}, []);
useEffect(() => { chartRef.current?.setData(data); }, [data]);
// "replace" so removing a prop also removes the option.
useEffect(() => { chartRef.current?.setOptions(options ?? {}, "replace"); }, [options]);
useEffect(() => { if (chartRef.current) chartRef.current.onHoverChange = onHover; }, [onHover]);
return (
<div ref={containerRef} className={`relative ${className}`}>
<canvas ref={canvasRef} />
</div>
);
}<Bars data={data} className="h-72" /> // fixed height
<div className="flex h-full flex-col">
<Bars data={data} className="min-h-0 flex-1" /> {/* fills a panel */}
</div>relative keeps the built-in tooltip positioned inside the chart. Use a
class merge helper like cn() if your project has one. Memoize data and
options (useMemo) so the chart isn't re-fed on every render. Importing the package on the server is safe. Only construct charts
in the browser, which useEffect already guarantees.
Core concepts
Canvas + container
The chart watches the container with a ResizeObserver and keeps the
canvas exactly that size, at your screen's pixel density. Give the container
a height (h-64, h-full, flex-1 with min-h-0), because an auto-height
container with only a canvas inside collapses to zero. Several charts can share a page; each owns its own canvas.
Data vs. options
datais what to draw: categories, series, slices. CallsetDataon every change. It's cheap.optionsis how to draw it: axes, animation, labels, theme, tooltip. Pass them to the constructor and/orsetOptions.
setOptions(partial) deep-merges by default, so you only pass what changes.
setOptions(all, "replace") swaps the whole set. Use that to unset
something, and in declarative wrappers.
Updates and animation
| setData with… | What you see |
|---|---|
| First data, or different categories/series | The chart's intro: lines sweep in, bars grow, the pie sweeps round |
| Same categories and series, new values | Values tween to their new positions |
| animation: false | Instant redraw, always |
Animations are skipped entirely when the user's OS asks for reduced motion.
A chart created inside a hidden tab or collapsed panel holds its intro
until it's visible. Call chart.replayAnimation() to play it again, e.g.
when a tab is re-opened.
Hover events and tooltips
Every chart fires onHoverChange(event | null) as the cursor moves, with the
hovered index, category and screen coordinates. You can:
- use the built-in tooltip (
tooltip: true), a small DOM box that follows the cursor; - customize it (
tooltip: { formatter }ortooltip: { render }); - or render your own from
onHoverChangewith any UI library. The event carries everything you need, including canvas-relativex/y.
To link an external legend, call chart.highlight(index) on legend hover and
chart.highlight(null) on leave. It emphasizes that bar, slice or category
without firing onHoverChange, so there's no feedback loop.
onClick(event, mouseEvent) fires when a click lands on something the chart
would report on hover, with the same event. Use it to open a drill-down.
Colors
Every color option takes any CSS color, including CSS variables:
"hsl(var(--primary) / 0.5)", "var(--destructive)". The chart resolves
them against its own container and again whenever the theme changes, so you
don't need to convert them yourself.
Custom drawing (layers)
Line, bar and combo charts take layers: { below?, above? }. Each is a
function that gets the chart's geometry and draws with the plain canvas API,
for marks no option covers. below runs over the grid, above over the data.
layers: {
above: ({ ctx, frame, theme, indexToPixel, valueToPixel, activeIndex }) => {
ctx.fillStyle = theme.text;
ctx.fillText("Target", frame.right - 40, valueToPixel(100) - 4);
},
}The bar chart also passes bars: where every bar landed (index,
seriesIndex, value, x, y, width, height, direction). Canvas
state is saved and restored around each layer.
Recipes
Y axis only (no X axis)
new BarChart(canvas, container, {
xAxis: { visible: false },
yAxis: { position: "left", formatter: (v) => `${v}Cr` },
});No axes at all
new LineChart(canvas, container, {
xAxis: { visible: false },
yAxis: { visible: false },
}); // a sparklineA hidden axis gives its space back to the plot. Hide just parts with
labels: false, line: false or gridLines: false.
Horizontal bars with value labels
const chart = new BarChart(canvas, container, {
orientation: "horizontal",
xAxis: { visible: false },
barRadius: 3,
dataLabels: { formatter: (v) => `${v.toLocaleString("en-IN")}Cr` },
});
chart.setData({
categories: ["10.67L\nNSEEQ", "4.30L\nBSEEQ", "21.77K\nMCX"], // "\n" = second line
series: [{
id: "turnover",
color: "#52667a",
values: [2156.09, 1023.28, 407.58],
colors: [null, null, "#16a34a"], // per-bar override
}],
});With position: "auto" (the default), each label sits inside the end of its
bar when it fits and just outside when it doesn't.
Donut with a two-line center label
const pie = new PieChart(canvas, container, {
innerRadiusRatio: 0.6,
sliceGap: 1.5,
centerLabel: { value: "3.18", caption: "Cr" },
tooltip: { formatter: (v) => v.toLocaleString("en-IN") },
});
pie.setData({ slices: accounts.map((a) => ({ id: a.id, label: a.name, value: a.loss, color: a.color })) });Half-ring gauge
new PieChart(canvas, container, {
innerRadiusRatio: 0.7,
startAngle: -90, // start at 9 o'clock
sweepAngle: 180,
centerLabel: "72%",
});Legend hover → highlight a slice
legendRow.addEventListener("mouseenter", () => pie.highlight(i));
legendRow.addEventListener("mouseleave", () => pie.highlight(null));Tune or disable animation
{ animation: false } // instant
{ animation: { duration: 1200, easing: "easeInOutCubic" } } // slower
{ animation: { stagger: 60 } } // bars grow one after another
{ animation: { onUpdate: false } } // intro only; live updates snapCustom tooltip content
new LineChart(canvas, container, {
tooltip: {
render: (e) => {
const el = document.createElement("div");
el.textContent = `${e.category}: ${e.points.map((p) => p.value).join(" / ")}`;
return el; // or return a plain string, or null to hide
},
},
});Strings are shown as plain text, never parsed as HTML, so data can't inject markup. Return an element for rich content.
Fixed axis range, axis titles
{
yAxis: { min: 0, max: 100, title: "Utilisation %", line: true },
xAxis: { title: "Time" },
}Exact ticks and reference lines
{
yAxis: { min: 0, max: 100, ticks: [20, 40, 60, 80, 100] },
referenceLines: [{ value: 80, color: "var(--destructive)", dash: [4, 4], label: "Limit" }],
}Area chart
series: [{ id: "pnl", color: "#3b82f6", values, fill: true, curve: "monotone" }]
// fill: { opacity: 0.25, baseline: 0 } fills down (or up) to a value instead of the plot's floorTime-based x axis
Pass x (ascending numbers, e.g. epoch ms) and points are spaced by value,
so an hour sampled twice takes as much width as an hour sampled sixty times.
chart.setData({ x: times, series: [{ id: "idx", color, values }] });
chart.setOptions({ xAxis: { formatter: (t) => new Date(t).toTimeString().slice(0, 5) } });Without xAxis.ticks, the range is cut evenly with both ends labelled, as
many ticks as fit. Leave some padding.right for half of the last label.
Color a line by stretch
colors gives each point its own color, which runs until the next point.
Line and area both follow it.
series: [{ id: "risk", color, values, colors: values.map((v) => (v >= 70 ? red : v >= 40 ? amber : green)), fill: true }]Point markers
series: [{
id: "used", color, values,
lineWidth: 0, // markers only
points: (index, value) => ({ radius: 8, fill: value >= 100 ? red : green, text: "N" }),
}],
xAxis: { labelAngle: -60 }, // long category names
activePoint: { radius: 10 }, // drawn under the marker, so it reads as a haloNames inside bars
series: [{ id: "turnover", color, values, labels: { text: symbols } }]The text runs along the bar from its base and is cut with an ellipsis to what fits.
Touching pairs with an open seam
{
barGapRatio: 0, // bars of one category touch
maxBarThickness: 24,
}
// per series:
{ radius: [4, 0, 0, 0], strokes: values.map(() => ({ color: red, width: 2, sides: ["top", "left", "bottom"] })) }Chart guides
LineChart
import { LineChart } from "@indiminds/charts/line";
chart.setData({
categories: ["9:15", "9:30", "9:45"],
series: [
{ id: "price", name: "Price", color: "#111827", values: [8300, 8312, 8309], emphasis: true },
{ id: "oi", name: "OI", color: "#3b82f6", values: [12000, 15500, 14800], axis: "right" },
],
});Series with axis: "right" get their own scale on a right-hand axis
(y2Axis), which only appears when such a series exists. null values leave
gaps. The right axis always includes zero.
Hover event: { index, category, x, points: [{ id, value, x, y }] }, one
point per series with a value at that index.
BarChart
import { BarChart } from "@indiminds/charts/bar";- Grouped bars: several
series, one bar each per category. - Horizontal:
orientation: "horizontal". Categories run top to bottom,yAxisis the category axis andxAxisis the value axis. - Per-bar styling:
colors[i]andfillModes[i]("solid" | "hatched" | "outline"). - Before/after (delta) bars: give every series
baseValues. Each bar is solid up to the smaller value, then hatched (grew) or outlined (shrank). - Reference line:
referenceLine: { category, label, color? }draws a labelled vertical guide at a category. - Annotations:
annotations: [{ index, seriesId, label, background, color }]hangs pill labels above specific bars.
Delta bars, reference lines and annotations are vertical-only.
Hover event: { index, category, x, y }.
ComboChart
import { ComboChart } from "@indiminds/charts/combo";
chart.setData({
categories,
bars: [{ id: "oi", name: "Open interest", color: "#3b82f6", values: oi }],
lineOverlays: [
{ id: "price", name: "Price", color: "#111827", values: price, axis: "right" },
{ id: "pcr", color: "#f59e0b", values: pcr, axis: "auto", style: "dashed" },
],
markers: [{ index: 5, label: "+12%", background: "#dcfce7", color: "#16a34a" }],
bands: [{ from: 3, to: 9, color: "#16a34a", label: "Long buildup" }],
subBars: { id: "vol", name: "Volume", color: "#a855f7", values: volume },
});axis: "right"lines share a labelled right axis.axis: "auto"lines each get a private, unlabelled scale. You see the shape, and read the value on hover.bandstint a run of categories.markersare pills above a category's bars.subBarsis a companion series on its own small scale beside the main bars.
Hover event: { index, category, x, bars: [{ id, value, color }], lines: [...] }.
PieChart
import { PieChart } from "@indiminds/charts/pie";
chart.setData({
slices: [
{ id: "a", label: "Alpha", value: 42, color: "#3b82f6" },
{ id: "b", label: "Beta", value: 27, color: "#f59e0b" },
],
});Zero and negative values draw nothing. Partial rings (sweepAngle < 360)
are scaled to fill the canvas.
Hover event: { index, slice, fraction, x, y }. fraction is the
slice's share of the total, 0–1, and x/y is the midpoint of its outer arc.
CandlestickChart
The full trading chart: candles, OHLC bars, Heikin Ashi, line or area price styles, indicator panes, pan/zoom and infinite history. It has its own API, separate from the options system above.
import { CandlestickChart } from "@indiminds/charts";
const chart = new CandlestickChart(canvas, container);
chart.setData(candles); // { time, open, high, low, close, volume? }[]
chart.setPriceStyle("heikin-ashi"); // "candles" | "bars" | "heikin-ashi" | "line" | "area"
chart.setIndicators(indicators); // RenderableIndicator[] from @indiminds/chart-types
chart.onCrosshairMove = (e) => {}; // { candle, index } | null
chart.onLayoutChange = (layout) => {}; // pane geometry, for positioning DOM overlays
chart.onLoadMore = (oldestTime) => {}; // fetch older candles, then setData againsetIndicators takes plain numbers. Computing them (RSI, EMA, …) is up to
you. This package only turns numbers into pixels.
API reference
Methods (Line, Bar, Combo, Pie)
| Member | Description |
|---|---|
| new XChart(canvas, container, options?) | Creates the chart. container is the element whose size the chart follows. |
| setData(data) | Draws new data, animating per the rules in Updates and animation. |
| setOptions(options, mode?) | "merge" (default) deep-merges; "replace" swaps the whole set. |
| getOptions() | The current options. |
| highlight(index \| null) | Emphasizes one category, bar or slice, e.g. from a legend. Doesn't fire onHoverChange. |
| replayAnimation() | Plays the intro again. |
| destroy() | Stops observers, animation and listeners, and removes the tooltip. Call it on unmount. |
| onHoverChange | (event \| null) => void. null when the cursor leaves. |
| onClick | (event, mouseEvent) => void. A click on a category, point or slice. |
Shared options (ChartOptions)
| Option | Type | Default | Description |
|---|---|---|---|
| padding | { top?, right?, bottom?, left? } | per chart | Space between the canvas edge and the plot, outside any axis. |
| animation | boolean \| AnimationOptions | true | See below. |
| theme | ChartTheme | from CSS variables | Per-chart colors and font. See Theming. |
| tooltip | boolean \| TooltipOptions | off | Built-in tooltip. |
| crosshair | boolean | true | The dashed guide at the hovered category. |
| ariaLabel | string | none | Accessible name. Also sets role="img" on the canvas. |
AxisOptions
Used by xAxis, yAxis and y2Axis.
| Option | Type | Default | Description |
|---|---|---|---|
| visible | boolean | true | false hides the axis and frees its space. |
| line | boolean | varies | The axis line itself. |
| labels | boolean | true | Tick labels. Hiding them also frees their gutter. |
| gridLines | boolean | varies | Gridlines at this axis' ticks. Setting true draws them even on a hidden axis. |
| title | string | none | Axis name, along the outer edge. |
| size | number | per chart | Px reserved for the labels. |
| formatter | (value: number) => string | per chart | Numeric axes only. Also the default for data labels and tooltips. |
| min / max | number | fitted | Numeric axes only. Fixes the range. |
| ticks | number[] | auto | Numeric axes only. Ticks at exactly these values. |
| labelAngle | number (deg) | 0 | Rotates category (and numeric x) labels, e.g. -45 or 90. The axis grows to fit. |
Line, bar and combo charts also take referenceLines: { value, axis?, color?,
width?, dash?, label? }[] and layers: { below?, above? }.
Category labels are drawn as given. Put "\n" in one for a second line,
and the axis grows to fit.
AnimationOptions
| Option | Type | Default | Description |
|---|---|---|---|
| enabled | boolean | true | |
| duration | number (ms) | 800 (combo 700) | The intro. Value updates take ¾ of this. |
| delay | number (ms) | 0 | Before the intro starts. |
| easing | "linear" \| "easeOutQuad" \| "easeOutCubic" \| "easeInOutCubic" \| "easeOutQuart" \| (t) => number | "easeOutCubic" | |
| stagger | number (ms) | 0 | Bar only: each category starts this much after the previous one. |
| onUpdate | boolean | true | Tween same-shape updates. false snaps them. |
TooltipOptions<Event>
| Option | Type | Description |
|---|---|---|
| enabled | boolean | Default true once tooltip is set. |
| formatter | (value: number) => string | Value format in the default tooltip. Defaults to the axis formatter. |
| render | (event) => HTMLElement \| string \| null | Replaces the default content. null hides it for that event. |
The tooltip is appended to the container. If the container is
position: static, it's switched to relative while the chart exists.
LineChart
Data: { categories?: string[], x?: number[], series: LineSeriesSpec[] }.
Give categories, or x for a numeric axis (then categories only titles
the tooltip).
| LineSeriesSpec | Type | Description |
|---|---|---|
| id | string | Stable key. |
| name | string? | Tooltip label. Defaults to id. |
| color | string | Any CSS color. |
| values | (number \| null)[] | One per category. null is a gap. |
| colors | (string \| null)[]? | Per-point color, running to the next point. |
| axis | "left" \| "right" | Default "left". |
| emphasis | boolean? | Slightly thicker line. |
| lineWidth | number? | Default 1.5. 0 hides the line. |
| curve | "linear" \| "monotone" | Default "linear". Monotone never overshoots the data. |
| fill | boolean \| { opacity?, baseline? } | Area under the line. Default opacity 0.15, down to the plot's floor. |
| points | boolean \| LinePointStyle \| (index, value) => LinePointStyle \| null | Markers: { radius?, fill?, stroke?, strokeWidth?, text?, textColor?, font? }. |
Options: ChartOptions plus xAxis, yAxis (left), y2Axis (right),
referenceLines, layers and activePoint (boolean \| { radius? }, a dot
on each line at the hovered point). Default formatters: left v.toFixed(2),
right v.toLocaleString().
BarChart
Data: { categories, series: BarSeries[], referenceLine?, annotations? }
| BarSeries | Type | Description |
|---|---|---|
| id, name?, color, values | | As for line series. |
| colors | (string \| null)[]? | Per-bar color override. |
| fillModes | ("solid" \| "hatched" \| "outline")[]? | Per-bar style. |
| baseValues | (number \| null)[]? | Previous values, for delta bars. Needed on every series. |
| strokes | ({ color, width?, sides? } \| null)[]? | Per-bar outline inside the bar, over its fill. Leave a side out to keep that edge open. |
| radius | number \| [tl, tr, br, bl] | Overrides barRadius for this series. The array sets each corner as drawn. |
| labels | { text, color?, font? } | Text inside each bar, along it from the base. color is one color or one per bar. |
Options: ChartOptions plus:
| Option | Type | Default | Description |
|---|---|---|---|
| orientation | "vertical" \| "horizontal" | "vertical" | |
| xAxis | BarAxisOptions | | The horizontal axis: categories when vertical, values when horizontal. |
| yAxis | BarAxisOptions | | The vertical axis. position: "left" \| "right" picks its side (default: right for values, left for categories). |
| dataLabels | boolean \| DataLabelOptions | off | Value labels on bars. |
| barRadius | number | 0 | Rounds the bar's far end, in px. |
| barWidthRatio | number | 0.7 | Share of the category slot the bars fill. |
| barGapRatio | number (0–0.9) | 0.15 | Gap between bars of one category, as a share of each bar's slot. 0 makes them touch. |
| maxBarThickness | number | none | Upper limit on bar thickness, in px. A capped group stays together. |
| referenceLines | ReferenceLineOptions[] | none | Lines at fixed values on the value axis. |
| layers | { below?, above? } | none | Custom drawing. The scene also has bars and orientation. |
| DataLabelOptions | Type | Default | Description |
|---|---|---|---|
| position | "auto" \| "inside-end" \| "outside-end" \| "center" | "auto" | auto goes inside when it fits. |
| formatter | (value, index, seriesId) => string | axis formatter | |
| color | string | theme text | Labels outside a bar. |
| insideColor | string | #fff | Labels on top of a bar. |
| font | string | bold axis font | A canvas font string. |
| visible | boolean | true | |
ComboChart
Data: { categories, bars, lineOverlays?, markers?, bands?, subBars? }.
See the guide. Series take an optional name for the tooltip.
Options: ChartOptions plus xAxis, yAxis (bars), y2Axis (right-axis
lines), referenceLines and layers. The category axis draws no vertical
gridlines by default.
PieChart
Data: { slices: { id, label, value, color }[] }
Options: ChartOptions plus:
| Option | Type | Default | Description |
|---|---|---|---|
| innerRadiusRatio | number (0–1) | 0 | > 0 makes a donut. |
| centerLabel | string \| { value, caption?, color?, captionColor? } | none | Text in the donut hole. |
| startAngle | number (deg) | 0 | Clockwise from 12 o'clock. |
| sweepAngle | number (deg) | 360 | 180 draws a half ring. |
| sliceGap | number (px) | 0 | Transparent gap between slices. |
| hoverOffset | number (px) | 6 | How far a hovered/highlighted slice pops out. |
Exported types
Every entry point exports its chart's data, options and event types, plus the
shared AnimationOptions, AxisOptions, ChartOptions, ChartTheme,
Padding, ReferenceLineOptions, ResolvedTheme, CartesianScene,
ChartLayers, PlotFrame, Easing, EasingName, TooltipOptions,
TooltipModel and TooltipRow.
Theming
Charts read their colors from CSS custom properties on <html>, and redraw
by themselves when its class, data-theme or style changes. Set these
once and light/dark mode just works:
| CSS variable | Theme key | Used for |
|---|---|---|
| --chart-grid | grid | Gridlines and axis lines |
| --chart-axis | text | Axis labels, titles, outside data labels |
| --chart-crosshair | crosshair | Hover guide line |
| --chart-pill | pill | Tooltip and reference-line pill background |
| --chart-pill-foreground | pillText | Tooltip and pill text |
| --chart-1 | priceLine | Candlestick line/area style |
With Tailwind CSS v4, define them in your main stylesheet next to your other
theme colors, and map them in @theme inline so they're also available as
utilities:
/* app.css */
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
:root {
--chart-grid: #e5e7eb;
--chart-axis: #6b7280;
--chart-crosshair: #9ca3af;
--chart-pill: #111827;
--chart-pill-foreground: #ffffff;
}
.dark {
--chart-grid: #313150;
--chart-axis: #8b8b93;
--chart-crosshair: #a1a1aa;
--chart-pill: #313150;
}
@theme inline {
--color-chart-grid: var(--chart-grid);
--color-chart-axis: var(--chart-axis);
--color-chart-pill: var(--chart-pill);
--color-chart-pill-foreground: var(--chart-pill-foreground);
}Toggling dark on <html> re-themes every chart. The utilities keep DOM
around the chart (legends, headers, custom tooltips) in the same colors:
<div class="flex items-center gap-2 border-b border-chart-grid text-xs text-chart-axis">
<span class="size-2 rounded-full bg-[#3b82f6]"></span> Open interest
</div>Keep the variables on :root/.dark rather than only inside @theme,
since Tailwind drops theme variables no utility uses and the charts read
them at runtime.
Override per chart with theme, which wins over the CSS variables:
{ theme: { grid: "#eee", text: "#374151", fontFamily: "Inter, sans-serif", fontSize: 12 } }Series and slice colors always come from your data.
Accessibility, performance & browser support
- Accessibility. Canvas content is invisible to screen readers, so pass
ariaLabelwith a short summary, and consider a visually-hidden data table next to important charts. Motion followsprefers-reduced-motion. - Performance. A chart redraws only when its data, options, size, theme
or the pointer changes, and animations run on
requestAnimationFrame. Nothing runs while idle. Live updates of the same shape tween without re-running the intro. - SSR. Importing is safe on the server, since nothing touches the DOM at import time. Construct charts in the browser only.
- Browsers. Evergreen browsers (ES2020,
ResizeObserver, Canvas 2D). Rounded bar ends useCanvasRenderingContext2D.roundRectand fall back to square corners where it's missing.
Migrating from 2.0
2.x code keeps working. The fields below still work but are deprecated in
favor of options:
| 2.0 (on data) | Now (on options) |
|---|---|
| Line leftFormatter / rightFormatter | yAxis.formatter / y2Axis.formatter |
| Bar valueFormatter | yAxis.formatter (xAxis when horizontal) |
| Combo leftFormatter, overlay formatter | yAxis.formatter, y2Axis.formatter |
| Combo leftAxisLabel / rightAxisLabel | yAxis.title / y2Axis.title |
| Pie innerRadiusRatio, centerLabel | same names on options |
| Pie valueFormatter | tooltip.formatter |
With default options, every chart settles on the same pixels as 2.0. Two things are new by default:
- Bar and pie charts play an intro animation. Pass
animation: falsefor the old instant draw. - Same-shape line updates tween instead of snapping.
Also fixed: value axes no longer print "-0".
License
Proprietary. Evaluation use only. See LICENSE for the full
terms. Production or commercial use requires a separate agreement with
Indiminds Technologies LLP ([email protected]).
Contributing
This package lives in the Indiminds Back-Testing-UI monorepo, under
packages/charting-engine. See the repo root's CLAUDE.md for the internal
structure.
pnpm --filter @indiminds/charts build # dist/ + .d.ts
pnpm --filter @indiminds/charts typecheckReleases go through Changesets (pnpm changeset at the repo root).
