mrkeyoor.com_
Thu 06 Aug 10:54 UTC
npmWeb Frontendupdated 06 Aug 2026

chart.js

A charting library that draws into a single HTML canvas element instead of building SVG nodes. You hand it a config object with a type, a data object of labels and datasets, and an options object, and it renders line, bar, pie, doughnut, radar, polar area, scatter, or bubble charts with animations, tooltips, and a legend already wired up. Version 4 is built from registerable pieces: controllers, scales, elements, and plugins are separate exports, so you either import 'chart.js/auto' and get everything or register only the parts you use and ship less code.

Verdict

Still the default pick for ordinary line, bar, and pie charts in a web app: cheap to learn, good defaults, and every framework has a wrapper. Reach for ECharts or ApexCharts once you need exotic chart types or interaction that Chart.js only gets through third-party plugins.

API stability4/5The v4 config shape has held since December 2022 and 4.x releases have been additive; the deduction is history, since both the v2 to v3 and v3 to v4 jumps rewrote option names and forced component registration
Docs4/5chartjs.org/docs covers every option with a live samples gallery and a migration guide; the thin spots are the plugin authoring API and the TypeScript generics, where the source is often faster to read than the docs
Maintenance3/5Latest release 4.5.1 in October 2025 and last repo push in late May 2026, with roughly 500 open issues and PRs against a small volunteer team; the project is alive but slow rather than actively developed
Ecosystem5/5About 12.8M weekly npm downloads, 67k stars, first-party wrappers for React, Vue, Angular, and Svelte, and mature plugins for datalabels, zoom, annotation, and financial charts

Use it if

  • You need the standard business chart types (line, bar, pie, doughnut, radar, scatter, bubble, polar area) with sensible defaults, configured through a JSON-shaped object rather than drawing code
  • You are plotting a few thousand points and want canvas rendering: SVG-based chart libraries start creating one DOM node per point and stutter well before canvas does
  • You want a maintained framework wrapper instead of writing one: react-chartjs-2, vue-chartjs, and ng2-charts all follow the Chart.js majors
  • You need charts on a page with no build step, since dist/chart.umd.js works from a plain script tag or a CDN
  • You already have chartjs-plugin-datalabels, chartjs-plugin-zoom, or chartjs-plugin-annotation in mind, because that plugin ecosystem is the main reason to pick it over newer libraries
Skip it if

Setup reality

npm install chart.js is the whole install, no peer dependencies and types are bundled. The friction starts at the first import. Pick 'chart.js/auto' and you pull the entire library into your bundle; pick the named imports and you must call Chart.register() with every controller, scale, element, and plugin you touch, or you get a runtime error like "line is not a registered controller". A time axis needs a separate adapter package (chartjs-adapter-date-fns plus date-fns) or it throws about a missing date adapter. Sizing bites next: never set width and height on the canvas, wrap it in a positioned parent with a real height, and set maintainAspectRatio to false. In React, StrictMode's double effect will hit "Canvas is already in use" unless you destroy the chart in the cleanup function. If you are porting v2 config, most option keys moved: tooltip and legend now live under options.plugins, and scales are an object keyed by axis id instead of xAxes and yAxes arrays.

Patterns

Render a chart with the auto bundleminimal-chart

import Chart from 'chart.js/auto';

const chart = new Chart(document.getElementById('sales'), {
  type: 'bar',
  data: {
    labels: ['Jan', 'Feb', 'Mar'],
    datasets: [{ label: 'Units', data: [12, 19, 7] }],
  },
  options: {
    scales: { y: { beginAtZero: true } },
  },
});

chart.js/auto registers every controller, scale, and plugin for you, so nothing is tree-shaken; fine for a prototype or a script tag, expensive in a shipped bundle.

Register only the pieces you usetree-shaken-setup

import {
  Chart,
  LineController,
  LineElement,
  PointElement,
  LinearScale,
  CategoryScale,
  Filler,
  Tooltip,
  Legend,
} from 'chart.js';

Chart.register(
  LineController,
  LineElement,
  PointElement,
  LinearScale,
  CategoryScale,
  Filler,
  Tooltip,
  Legend,
);

const chart = new Chart(canvas, {
  type: 'line',
  data: { labels, datasets: [{ label: 'Load', data, fill: true }] },
});

Forget one import and you get a runtime error naming the missing piece, for example "line is not a registered controller". Filler is its own plugin, so fill: true does nothing until it is registered.

Push new data into a live chartupdate-data

chart.data.labels.push('Apr');
chart.data.datasets[0].data.push(23);
chart.update();

// swap a whole series and skip the animation
chart.data.datasets[0].data = [5, 6, 7, 8];
chart.update('none');

Mutate chart.data in place and then call update(); nothing redraws on assignment alone. Passing 'none' skips animation, which matters for polling or streaming updates.

Destroy a chart before reusing the canvasdestroy-before-reuse

let chart = null;

function render(canvas, data) {
  if (chart) chart.destroy();
  chart = new Chart(canvas, { type: 'doughnut', data });
  return chart;
}

// React
useEffect(() => {
  const c = new Chart(ref.current, config);
  return () => c.destroy();
}, []);

Creating a second chart on the same canvas throws "Canvas is already in use". React StrictMode runs effects twice in development, so the cleanup destroy() is required, not optional.

Make the chart fill a sized containerresponsive-sizing

<!-- parent must have a real height -->
<div style="position: relative; height: 320px; width: 100%">
  <canvas id="c"></canvas>
</div>

<script type="module">
  new Chart(document.getElementById('c'), {
    type: 'line',
    data,
    options: { responsive: true, maintainAspectRatio: false },
  });
</script>

Do not set width or height attributes on the canvas; Chart.js manages them. With maintainAspectRatio false and an auto-height parent, the chart grows on every resize event.

Plot a time series on a date axistime-axis

// npm i chart.js chartjs-adapter-date-fns date-fns
import 'chartjs-adapter-date-fns';
import { Chart, LineController, LineElement, PointElement, LinearScale, TimeScale, Tooltip } from 'chart.js';

Chart.register(LineController, LineElement, PointElement, LinearScale, TimeScale, Tooltip);

new Chart(canvas, {
  type: 'line',
  data: {
    datasets: [{
      label: 'Signups',
      data: [
        { x: '2026-07-01', y: 12 },
        { x: '2026-07-02', y: 19 },
      ],
    }],
  },
  options: {
    scales: {
      x: { type: 'time', time: { unit: 'day', tooltipFormat: 'PP' } },
    },
  },
});

Chart.js bundles no date library. Without an adapter package the time scale throws about a missing date adapter implementation; the date-fns, Luxon, Moment, and Day.js adapters are separate npm packages.

Format tooltip labels and axis ticksformat-tooltip

const inr = new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0 });

options: {
  plugins: {
    tooltip: {
      callbacks: {
        label: (ctx) => `${ctx.dataset.label}: ${inr.format(ctx.parsed.y)}`,
      },
    },
    legend: { position: 'bottom' },
  },
  scales: {
    y: { ticks: { callback: (value) => `${value / 1000}k` } },
  },
}

Since v3 tooltip and legend live under options.plugins, not at the top level like v2. ctx.parsed holds the value after scale parsing, while ctx.raw is whatever you put in the data array.

Mix a line onto a bar chart with its own axissecond-y-axis

new Chart(canvas, {
  type: 'bar',
  data: {
    labels,
    datasets: [
      { label: 'Revenue', data: revenue, yAxisID: 'y' },
      { type: 'line', label: 'Margin %', data: margin, yAxisID: 'y1' },
    ],
  },
  options: {
    scales: {
      y: { position: 'left', beginAtZero: true },
      y1: {
        position: 'right',
        min: 0,
        max: 100,
        grid: { drawOnChartArea: false },
      },
    },
  },
});

The top-level type is only the default for datasets that do not set their own. With named imports you must register BarController and LineController, or the mixed dataset throws a not-registered error. drawOnChartArea false stops two grids from overlapping.

Stack bars and split them into groupsstacked-bar

options: {
  scales: {
    x: { stacked: true },
    y: { stacked: true, beginAtZero: true },
  },
}

// datasets can opt into separate stacks
datasets: [
  { label: 'Web', data: web, stack: 'traffic' },
  { label: 'App', data: app, stack: 'traffic' },
  { label: 'Target', data: target, stack: 'goal' },
]

Both axes need stacked: true; setting only y leaves the bars side by side. The dataset-level stack key is what separates groups, and datasets sharing a name stack together.

Downsample a large series before drawinglarge-dataset-decimation

import { Chart, LineController, LineElement, PointElement, LinearScale, Decimation } from 'chart.js';

Chart.register(LineController, LineElement, PointElement, LinearScale, Decimation);

new Chart(canvas, {
  type: 'line',
  data: { datasets: [{ data: points, borderWidth: 1, pointRadius: 0 }] },
  options: {
    parsing: false,
    normalized: true,
    animation: false,
    plugins: {
      decimation: { enabled: true, algorithm: 'lttb', samples: 500 },
    },
    scales: { x: { type: 'linear' } },
  },
});

Decimation is skipped unless parsing is false and the data is already an array of {x, y} sorted ascending on a linear or time index axis. pointRadius 0 and animation false do more for frame rate than the plugin on mid-size sets.

Write an inline plugin for a solid backgroundcustom-plugin-background

const solidBackground = {
  id: 'solidBackground',
  beforeDraw(chart, args, opts) {
    const { ctx } = chart;
    ctx.save();
    ctx.globalCompositeOperation = 'destination-over';
    ctx.fillStyle = opts.color || '#ffffff';
    ctx.fillRect(0, 0, chart.width, chart.height);
    ctx.restore();
  },
};

const chart = new Chart(canvas, {
  type: 'bar',
  data,
  options: { plugins: { solidBackground: { color: '#ffffff' } } },
  plugins: [solidBackground],
});

const png = chart.toBase64Image();

Plugins passed in the config plugins array apply to that chart only; Chart.register() makes them global. Without one, toBase64Image() returns a transparent PNG that turns black in some email clients and PDF viewers.

Handle a click on a bar or pointclick-datapoint

options: {
  onClick(event, elements, chart) {
    if (!elements.length) return;
    const { datasetIndex, index } = elements[0];
    const label = chart.data.labels[index];
    const value = chart.data.datasets[datasetIndex].data[index];
    console.log('clicked', label, value);
  },
}

// outside the config, from a raw DOM event
const hits = chart.getElementsAtEventForMode(
  domEvent,
  'nearest',
  { intersect: true },
  true,
);

elements is empty whenever the click misses a drawn element, including clicks in chart whitespace, so guard before indexing. Set intersect to false if you want the nearest point rather than a direct hit.

Alternatives

PackageRegistryPick it when
echartsnpmYou need chart types Chart.js does not have (sankey, treemap, candlestick, geo) in one maintained package
apexchartsnpmYou want SVG output, built-in toolbar, zoom, and CSV export without assembling plugins
uplotnpmTime series with tens of thousands of points where a tiny bundle and draw time beat feature count