npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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.ts for every option and event.
  • Sharp on any screen. Handles device pixel ratio and container resizing for you.

Contents


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:

  1. new Chart(canvas, container, options?)
  2. setData(data) whenever the data changes
  3. setOptions(options) whenever the look changes
  4. destroy() 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

  • data is what to draw: categories, series, slices. Call setData on every change. It's cheap.
  • options is how to draw it: axes, animation, labels, theme, tooltip. Pass them to the constructor and/or setOptions.

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 } or tooltip: { render });
  • or render your own from onHoverChange with any UI library. The event carries everything you need, including canvas-relative x/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 sparkline

A 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 snap

Custom 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 floor

Time-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 halo

Names 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, yAxis is the category axis and xAxis is the value axis.
  • Per-bar styling: colors[i] and fillModes[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.
  • bands tint a run of categories. markers are pills above a category's bars. subBars is 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 again

setIndicators 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 ariaLabel with a short summary, and consider a visually-hidden data table next to important charts. Motion follows prefers-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 use CanvasRenderingContext2D.roundRect and 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: false for 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 typecheck

Releases go through Changesets (pnpm changeset at the repo root).