Making Chart.js Line Charts Survive 50,000 Points with the Decimation Plugin
Chart.js's built-in decimation plugin downsamples huge line datasets to roughly the canvas pixel width. Here's how to enable it, choose between min-max and LTTB, and handle the fidelity trade-off.
20 May 2026, 10:42 UTC

Your monitoring dashboard works fine in development with a few hundred points. Then production data arrives — 50,000 sensor readings spanning a week — and the line chart takes seconds to draw, animations stutter, and hovering feels like wading through mud. The useful takeaway: Chart.js ships an official decimation plugin that downsamples your data to roughly the canvas pixel width before rendering, and enabling it is often a five-line config change. But it only works under specific conditions, and it trades exact fidelity for speed.
Why big datasets hurt
Chart.js draws on an HTML canvas. Every point in a line dataset becomes part of the path the browser has to compute and paint. The catch is that a canvas is typically only 800–1600 pixels wide, so plotting 50,000 points means dozens of points land on the same vertical pixel column. You are paying full rendering cost for detail nobody can see.
Decimation exploits that fact. It reduces the dataset to a much smaller representative set — on the order of the pixel width — so the drawn line looks nearly identical while the update pipeline has far less work to do.
Enabling the plugin
The plugin is opt-in and configured per chart. Assuming Chart.js 4.x (the plugin exists in 3.x too, but option defaults have shifted between majors, so check the docs for your installed version):
new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: 'Sensor temperature',
data: points, // [{x: timestampMs, y: value}, ...] sorted by x
borderColor: 'rgb(75, 192, 192)',
pointRadius: 0
}]
},
options: {
parsing: false, // data is already in {x, y} form
normalized: true, // data is already sorted by x
animation: false, // large updates don't need tweening
scales: {
x: { type: 'time' }
},
plugins: {
decimation: {
enabled: true,
algorithm: 'min-max'
}
}
}
});Three supporting choices matter as much as the plugin itself. parsing: false tells Chart.js to skip its data-normalization step because you are handing it ready-made {x, y} objects. normalized: true promises the data is sorted and deduplicated, skipping a sorting pass. animation: false avoids interpolating tens of thousands of positions on every update. None of these are required for decimation, but together they remove most of the remaining overhead.
Choosing an algorithm: min-max vs LTTB
The plugin offers two downsampling algorithms, and the choice depends on what your users look at:
- min-max buckets points along the x-axis and keeps the minimum and maximum of each bucket. It preserves peaks and troughs — spikes in sensor data, error bursts, latency outliers — at the cost of keeping up to twice as many points. For monitoring dashboards, this is usually the right default because the spikes are the story.
- LTTB (largest-triangle-three-buckets) picks points that best preserve the overall visual shape of the line. It produces smoother, more faithful curves for smooth signals, but a narrow spike can be averaged away if it doesn't contribute to the perceived shape.
If you are unsure, render both against a real extract of your data and zoom into the regions you care about. The right answer depends on your data's shape, not on a universal rule.
The conditions that silently disqualify you
Decimation is not a general-purpose fix. It only applies when all of the following hold:
- The chart is a line chart on a linear or time x-axis with numeric data. Bar, pie, and category-axis charts are ignored — the plugin does nothing rather than erroring.
- The x values are sorted. Unsorted input doesn't produce an error; it produces a misleading downsampled line. If your data arrives unordered, sort it before handing it to Chart.js.
Because failures are silent, verify rather than assume. A quick check: temporarily set algorithm to something invalid or log chart.data.datasets[0].data.length before and after render in a debug build, or simply compare render time in devtools with enabled: false versus true. If nothing changes, one of the preconditions isn't met.
The fidelity trade-off
Decimation is lossy. Tooltips and hover operate on the decimated dataset, so a user hovering over a peak may see an approximate bucket value rather than the exact raw reading. For many dashboards that's acceptable; for audit-style views it isn't.
The common pattern is two-tier rendering: show the decimated chart for the full range, and when the user zooms into a narrow window (via chartjs-plugin-zoom or your own range selector), re-render that window with the raw points. A 500-point visible window renders instantly and gives exact tooltips, while the overview stays fast.
What to do next
Profile before and after: record a performance trace in browser devtools while updating your chart with realistic data volume, with decimation off and on. You should see scripting and rendering time drop sharply. Then zoom in and confirm the peaks you care about survive your chosen algorithm. If they don't, switch algorithms or adopt the zoom-and-rerender pattern. And before shipping, confirm the option names and defaults against the official docs for your exact Chart.js major version — normalized and parsing behavior have changed between 3.x and 4.x.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.