Chart.js Decimation Plugin: Rendering 100k+ Points Without Freezing the Browser
Chart.js's built-in decimation plugin renders 100k+ point line charts interactively using min-max or LTTB sampling — but only if the axis type, data format, and sorting preconditions are met.
29 Aug 2025, 13:40 UTC

If a line chart with a few hundred thousand points takes seconds to render and stutters on every hover, the fix is usually not a different charting library. Chart.js ships an official decimation plugin that reduces the number of points actually drawn while preserving the visual shape of the line. For most large time series, enabling it with the right preconditions turns an unusable chart into an interactive one.
The useful answer first
Enable the plugin in your chart options and meet its three preconditions: a line chart, a linear or time/timeseries x axis, and data sorted by x supplied as parsed {x, y} objects. If any condition fails, the plugin silently does nothing — which is the single most common reason developers report "decimation is enabled but the chart is still slow."
A working configuration
The following configuration targets Chart.js v4 (the plugin exists in v3 as well; verify option names against your installed version). It runs in any browser environment where Chart.js is loaded — no build step or extra permissions required:
new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: 'Sensor readings',
data: points, // [{x: 1697000000000, y: 21.4}, ...] sorted by x
borderWidth: 1,
pointRadius: 0 // drawing 100k+ points as circles defeats the purpose
}]
},
options: {
parsing: true, // data is already {x, y} objects
animation: false, // re-animating on every update is expensive at this scale
scales: {
x: { type: 'time' } // 'linear' also works; 'category' does NOT
},
plugins: {
decimation: {
enabled: true,
algorithm: 'min-max'
}
}
}
});Two supporting choices matter as much as the plugin itself. Setting pointRadius: 0 avoids drawing hundreds of thousands of canvas circles, and animation: false removes per-frame interpolation cost on updates. Decimation plus disabled animations is the standard combination for dashboards plotting 100k or more points.
How the min-max algorithm works
Naive downsampling — keeping every Nth point — can erase spikes entirely, which is unacceptable for monitoring data where the spike is the thing you care about. The default min-max algorithm instead divides the x range into buckets roughly proportional to the canvas width in pixels, then keeps the minimum and maximum y value from each bucket. Because a screen pixel column cannot display more than a couple of distinct points anyway, the rendered line is visually near-identical while the element count drops from hundreds of thousands to roughly twice the canvas width.
The alternative lttb algorithm (largest-triangle-three-buckets) picks points that best preserve the line's visual shape and accepts a samples option controlling the target output size, e.g. { algorithm: 'lttb', samples: 200 }. It produces slightly better fidelity on jagged data at higher CPU cost per resample. Start with min-max; switch to lttb only if you can see distortion in the rendered line.
Verifying it actually engaged
Because the plugin no-ops silently when preconditions fail, always confirm the reduction. After the chart renders or updates, run this in the browser console:
console.log(chart.getDatasetMeta(0).data.length);This reports the number of point elements Chart.js actually created. With decimation working on a 100k-point dataset, expect a number in the low thousands (roughly twice the canvas width for min-max). If it still prints 100000, the plugin did not engage — check the axis type and data sorting first. For a fuller check, toggle enabled between true and false and compare first-render time in the browser's Performance profiler.
Limits and common mistakes
Silent no-ops
The three most frequent causes of decimation doing nothing:
- Category x axis. Labels like
['Jan', 'Feb', ...]use a category scale, which the plugin does not support. Convert to timestamps and atimescale. - Unsorted data. Points must be ordered by x. Sort once after fetching, not on every update.
- Wrong chart type. Only line charts are decimated; bar, scatter, and radar charts are unaffected.
Tooltips show the decimated subset
Decimation happens at render time, so hover interactions and tooltips operate on the reduced point set. Users cannot inspect every raw sample by hovering. If exact-value inspection matters, pair decimation with a zoom plugin so that zooming in narrows the visible range below the decimation threshold, or provide a detail view that queries the raw data.
Memory is untouched
Decimation reduces draw cost, not memory. The full data array still lives in the browser. For multi-million-point series, downsample on the server before sending, and treat client-side decimation as the final rendering optimization rather than the whole strategy.
Realtime updates resample every push
Each data change triggers re-sampling. Pushing points per millisecond from a websocket means paying the decimation cost constantly. Buffer incoming points and call chart.update() on a throttled interval (for example every 250–500 ms) instead.
Decision summary
| Situation | Recommendation |
|---|---|
| Under ~10k points | Skip decimation; native rendering is fine |
| 100k+ sorted time-series points | min-max, animation: false, pointRadius: 0 |
| Visible shape distortion with min-max | Try lttb with explicit samples |
| Millions of points / slow initial load | Server-side downsampling plus client decimation |
| Category axis or unsorted data | Fix preconditions first — plugin silently no-ops |
Option names and supported algorithms differ between Chart.js major versions, so confirm the configuration against the documentation for the version pinned in your project before shipping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.