DataZoom, toolbox and large-data performance
Add inside and slider zoom, expose toolbox actions, and keep the frame rate acceptable when a series has hundreds of thousands of points.
DataZoom and the toolbox
const option = {
tooltip: { trigger: 'axis' },
grid: { left: 56, right: 24, top: 32, bottom: 88 },
xAxis: { type: 'time' },
yAxis: { type: 'value' },
dataZoom: [
{
type: 'inside', // wheel and pinch
xAxisIndex: 0,
zoomOnMouseWheel: true,
moveOnMouseMove: true,
moveOnMouseWheel: false, // keep the wheel for zoom, not pan
throttle: 50 // ms between zoom events: cheap re-renders
},
{
type: 'slider', // the draggable bar under the chart
xAxisIndex: 0,
height: 24,
bottom: 32,
brushSelect: false,
showDataShadow: true,
start: 0,
end: 20,
labelFormatter: (value) => new Date(value).toLocaleDateString()
}
],
toolbox: {
right: 16,
top: 0,
feature: {
dataZoom: { yAxisIndex: 'none' }, // drag-to-zoom on the plot area
restore: {}, // reset to the initial option
saveAsImage: { name: 'revenue', pixelRatio: 2, backgroundColor: '#fff' },
dataView: { readOnly: true, lang: ['Data', 'Close', 'Refresh'] },
magicType: { type: ['line', 'bar'] }
}
},
series: [{
type: 'line',
showSymbol: false, // 20,000 points cannot each have a symbol
sampling: 'lttb', // Largest-Triangle-Three-Buckets downsampling
data: largeSeries
}]
};| Option | Effect | When it matters |
|---|---|---|
dataZoom.inside.throttle | Limits zoom event rate | Dense series where each zoom re-renders |
showSymbol: false | No per-point marker | Any line with more than a few hundred points |
sampling: 'lttb' | Keeps the visual shape while dropping points | Time series that are visually smooth |
large: true | Optimised rendering path | Scatter and bar with tens of thousands of items |
largeThreshold | Point count at which large mode engages | Tuning the trade-off between fidelity and speed |
progressive / progressiveThreshold | Renders in chunks so the page stays responsive | Very large scatter or graph series |
useDirtyRect at init | Repaints only changed regions | Frequent updates on a static chart |
💡
Zoom state lives in the option. Calling
setOption with a fresh option while the user is zoomed in resets the window unless the new option carries the current dataZoom.start and end. Merge rather than replace when updating a zoomable chart.Making a huge series usable
// 1. Downsample before it reaches the chart when the data is not
// already aggregated. 200,000 points at one per second is more than a
// screen pixel can show anyway.
function bucket(points, bucketSize) {
const out = [];
for (let i = 0; i < points.length; i += bucketSize) {
const slice = points.slice(i, i + bucketSize);
if (!slice.length) break;
const sum = slice.reduce((acc, p) => acc + p[1], 0);
out.push([slice[0][0], sum / slice.length]); // mean per bucket
}
return out;
}
// 2. Or let ECharts do it, which keeps the original detail available on zoom.
const series = [{
type: 'line',
showSymbol: false,
sampling: 'lttb',
progressive: 2000, // draw 2,000 points per frame
large: true,
largeThreshold: 5000,
lineStyle: { width: 1 },
data: hugeSeries
}];
// 3. Typed-array datasets avoid per-point object allocation entirely.
const typed = {
dataset: {
dimensions: ['time', 'value'],
source: {
// one flat numeric array, indexed by the dimensions
}
}
};
// 4. Disable animation when the chart updates on a timer.
chart.setOption({ animation: false }, { lazyUpdate: true });
// 5. lazyUpdate batches the render to the next frame instead of rendering
// synchronously inside a tight update loop.
for (const point of stream) {
pushToSeries(point);
}
chart.setOption({ series: [{ data: current }] }, { lazyUpdate: true });
// 6. notMerge vs merge: only use notMerge when the structure changed.
// Merging keeps the zoom, the legend selection and the animation state.
chart.setOption({ series: [{ data: next }] });- The cheapest optimisation is almost always to send fewer points. Aggregate on the server per zoom level, or bucket in the client before the first render.
lttbsampling preserves peaks and shapes well; average-based sampling smooths them away. For monitoring data where spikes matter, LTTB is the safer choice.large: truechanges the rendering path but does not reduce the data. Combine it with sampling, not instead of it.- A chart that updates from a WebSocket should throttle its
setOptioncalls to the animation frame rate. Rendering faster than the screen refreshes only wastes work.
Incremental updates and zoom-aware tooltips
// A streaming chart: shift the window instead of growing the array forever.
const WINDOW = 600;
const buffer = [];
function push(point) {
buffer.push(point);
if (buffer.length > WINDOW) buffer.shift();
chart.setOption({
series: [{ data: buffer }],
xAxis: { min: buffer[0]?.[0], max: buffer[buffer.length - 1]?.[0] }
}, { lazyUpdate: true });
}
// When the user has zoomed, respect their window rather than snapping back.
let userZoom = null;
chart.on('dataZoom', () => {
const model = chart.getModel().getComponent('dataZoom', 0);
userZoom = { start: model.option.start, end: model.option.end };
});
chart.on('restore', () => { userZoom = null; });
function pushRespectingZoom(point) {
buffer.push(point);
if (buffer.length > WINDOW) buffer.shift();
const option = { series: [{ data: buffer }] };
if (userZoom) option.dataZoom = [{ start: userZoom.start, end: userZoom.end }];
chart.setOption(option, { lazyUpdate: true });
}
// Reading the visible range for a custom aggregation or an API call
function visibleRange() {
const axis = chart.getModel().getComponent('xAxis', 0).axis;
return { min: axis.scale.getExtent()[0], max: axis.scale.getExtent()[1] };
}| Problem | Cause | Fix |
|---|---|---|
| Zoom resets on update | setOption replaced the option | Carry start and end in the update |
| Update stutters | Rendering on every message | Throttle to a frame, use lazyUpdate |
| Memory grows without bound | An append-only data array | Keep a fixed window and shift |
| Filtering forces a new chart | Data was filtered in JavaScript | Use a filter transform on a dataset |
| Tooltip is slow on a big series | trigger: 'item' with 100k points | Use trigger: 'axis' |
| Panning is sluggish | Wheel bound to both zoom and pan | Set moveOnMouseWheel: false |
FAQ
How many points can ECharts render?
A line chart with
showSymbol: false and sampling handles hundreds of thousands of points smoothly on canvas. A scatter with symbols is far more expensive, so enable large mode and expect a few tens of thousands before it feels heavy.Why does the chart reset when I update the data?
The update replaced the option object, including the current zoom. Either merge the update (pass only the
series you changed) or explicitly carry dataZoom.start and end from the current state into the new option.Related
Interaction, resize and data updates Custom series and the matrix coordinate system
Last refreshed 2026-09-18.