echarts
Apache ECharts is a browser visualization engine that turns a declarative option object into interactive Canvas or SVG charts. It covers familiar bars, lines, pies, and scatter plots as well as heatmaps, graphs, trees, treemaps, sankeys, gauges, maps, brushing, data zoom, timelines, datasets, and custom series. The same instance API handles rendering, events, animated updates, export, linked charts, and server-rendered SVG. Version 6 is framework-neutral JavaScript with TypeScript declarations, built on the separate zrender graphics engine.
ECharts is the best open-source default when a dashboard will outgrow basic charts and you are willing to manage its instance lifecycle and option model. Use tree-shaken imports from day one; for a handful of simple charts, Chart.js or uPlot costs less in bundle size and mental overhead.
Use it if
- You need many chart types, interactions, annotations, coordinate systems, and visual encodings behind one consistent option model
- Your dashboard needs data zoom, brushing, linked charts, animated transitions, large-series optimizations, or custom rendering
- You want to choose Canvas or SVG per chart and may also render SVG strings on the server
- You need a permissive Apache-2.0 library with a large community, extensive examples, and no commercial feature tier
- You need one small line or bar chart: the complete import is 368.0 KB gzipped, and even a tree-shaken core setup carries more concepts than Chart.js or uPlot
- You want charts expressed as ordinary React or Vue components: ECharts owns an imperative instance, so a wrapper must synchronize options, resizing, events, and disposal with framework lifecycle
- You require a chart to communicate its full data accessibly without extra work: ARIA is opt-in, the AriaComponent is not imported by default, and an aria-label is not a substitute for a data table or keyboard-designed controls
- You need WebGL or 3D from the base package: those capabilities live in the separate echarts-gl extension rather than ECharts itself
- You cannot budget for major-version visual regression checks: version 6 changed the default theme, legend placement, label-overflow handling, and other defaults even though much of the API stayed compatible
Setup reality
npm install echarts gives you ESM, CommonJS, TypeScript declarations, zrender, and tslib with no peer dependencies. The tempting import * as echarts from 'echarts' includes every chart and component and measures 368.0 KB gzipped. Production apps should import echarts/core, register only the chart types and components they use with echarts.use(), and explicitly register either CanvasRenderer or SVGRenderer because the tree-shakeable core includes neither. A chart container must already have nonzero width and height before echarts.init(); flex and hidden-tab layouts are frequent sources of blank charts. ECharts does not automatically follow every container resize, so attach ResizeObserver and call chart.resize(), then disconnect the observer and chart.dispose() when the view unmounts. setOption merges by default. That is convenient for incremental data, but removed series can survive unless you use replaceMerge: ['series'] or notMerge for a full replacement. Stable series IDs make animated updates more predictable. Framework wrappers need to avoid initializing twice on the same DOM node and must unregister event handlers or dispose the instance. Tree-shaken builds also need every optional feature registered: setting aria.show does nothing without AriaComponent, dataZoom needs its component, and an SVG chart needs SVGRenderer. Maps are not bundled as geographic data; load GeoJSON and call registerMap yourself. Server-side SVG requires renderer: 'svg', ssr: true, explicit dimensions, renderToSVGString(), and disposal. Canvas server rendering additionally needs a Canvas implementation such as node-canvas. Version 6 changed visual defaults, including theme colors and legend placement, so screenshot-test a v5 migration or initialize with the supplied v5 theme when preserving appearance is more important than adopting the new defaults.
Patterns
Register only the features you useconfigure-tree-shaking
import * as echarts from 'echarts/core';
import { BarChart, LineChart } from 'echarts/charts';
import {
AriaComponent,
DatasetComponent,
DataZoomComponent,
GridComponent,
LegendComponent,
TitleComponent,
TooltipComponent,
} from 'echarts/components';
import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([
BarChart, LineChart, AriaComponent, DatasetComponent, DataZoomComponent,
GridComponent, LegendComponent, TitleComponent, TooltipComponent, LabelLayout,
UniversalTransition, CanvasRenderer,
]);The core entry point registers nothing automatically, including the renderer. Missing registrations usually produce an empty feature rather than a useful compile error.
Render a basic bar chartrender-bar-chart
const element = document.getElementById('sales-chart');
if (!element) throw new Error('chart container is missing');
const chart = echarts.init(element);
chart.setOption({
title: { text: 'Weekly sales' },
tooltip: {},
xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'] },
yAxis: { type: 'value' },
series: [{ id: 'sales', name: 'Sales', type: 'bar', data: [12, 20, 15, 8, 17] }],
});The container needs a real width and height before init. Register BarChart, GridComponent, TitleComponent, TooltipComponent, and a renderer when using the core build.
Plot timestamped line datarender-time-series
chart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'time' },
yAxis: { type: 'value', scale: true },
series: [{
id: 'temperature',
type: 'line',
showSymbol: false,
data: readings.map((row) => [row.timestamp, row.value]),
}],
});Use timestamps or parseable dates on a time axis. scale: true prevents a value axis from always forcing zero into the visible range.
Share one dataset across several seriesbind-tabular-dataset
chart.setOption({
legend: {},
tooltip: { trigger: 'axis' },
dataset: {
source: [
['month', 'Revenue', 'Cost'],
['Jan', 120, 80],
['Feb', 150, 92],
['Mar', 170, 110],
],
},
xAxis: { type: 'category' },
yAxis: {},
series: [
{ id: 'revenue', type: 'bar', encode: { x: 'month', y: 'Revenue' } },
{ id: 'cost', type: 'line', encode: { x: 'month', y: 'Cost' } },
],
});Register DatasetComponent in a core build. Named dimensions make encode mappings easier to review than positional column numbers.
Replace live series without leaving stale onesreplace-live-series
chart.setOption(
{
series: streams.map((stream) => ({
id: stream.id,
name: stream.label,
type: 'line',
showSymbol: false,
data: stream.points,
})),
},
{ replaceMerge: ['series'], lazyUpdate: true },
);setOption merges by default. replaceMerge removes series no longer present, while stable IDs preserve sensible matching and transitions for retained series.
Handle clicks and remove the handler laterhandle-chart-click
const onBarClick = (params) => {
if (params.componentType === 'series' && params.seriesType === 'bar') {
openDetails(params.name, params.value);
}
};
chart.on('click', onBarClick);
// During teardown when the chart instance is retained:
chart.off('click', onBarClick);Register the same function reference with off. Calling on again after every render without cleanup stacks duplicate handlers.
Resize when the chart container changesresize-with-container
const observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
if (width > 0 && height > 0) chart.resize();
});
observer.observe(chartElement);
function cleanup() {
observer.disconnect();
chart.dispose();
}A window resize listener misses sidebar, grid, and tab layout changes. Do not call resize with positional width and height arguments; use resize() or an options object.
Add slider and wheel zoomingadd-data-zoom
chart.setOption({
xAxis: { type: 'time' },
yAxis: { type: 'value' },
dataZoom: [
{ type: 'inside', xAxisIndex: 0, filterMode: 'filter' },
{ type: 'slider', xAxisIndex: 0, bottom: 8 },
],
grid: { bottom: 70 },
series: [{ id: 'requests', type: 'line', data: points }],
});Register DataZoomComponent in the core build. Leave enough grid space for the slider or it will overlap axis labels.
Downsample a dense line seriesdownsample-large-line
chart.setOption({
animation: false,
xAxis: { type: 'time' },
yAxis: { type: 'value' },
series: [{
id: 'telemetry',
type: 'line',
data: points,
showSymbol: false,
sampling: 'lttb',
large: true,
largeThreshold: 2000,
}],
});Sampling changes the rendered representation, not the source array. Test tooltips and visual fidelity because large mode disables or simplifies some item-level effects.
Enable an ARIA description and decalsenable-accessibility
chart.setOption({
title: { text: 'Revenue by quarter' },
aria: {
enabled: true,
decal: { show: true },
description: 'Quarterly revenue in thousands of dollars for 2025.',
},
xAxis: { type: 'category', data: ['Q1', 'Q2', 'Q3', 'Q4'] },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: [120, 135, 128, 160] }],
});Register AriaComponent first. Add an adjacent HTML table or equivalent for exact values and test keyboard and screen-reader flows rather than relying on one generated label.
Render a chart to an SVG string on the serverrender-svg-on-server
import * as echarts from 'echarts/core';
import { BarChart } from 'echarts/charts';
import { GridComponent } from 'echarts/components';
import { SVGRenderer } from 'echarts/renderers';
echarts.use([BarChart, GridComponent, SVGRenderer]);
const chart = echarts.init(null, null, {
renderer: 'svg',
ssr: true,
width: 800,
height: 400,
});
try {
chart.setOption(option);
return chart.renderToSVGString();
} finally {
chart.dispose();
}SSR requires explicit dimensions and SVGRenderer. The returned SVG can animate initially, but ordinary browser interactions and tooltips need client code.
Dispose an instance during view teardowndispose-chart-instance
let chart = echarts.getInstanceByDom(chartElement);
if (!chart) chart = echarts.init(chartElement);
// when the route, component, or DOM node is removed
chart.dispose();Initializing twice on the same element causes warnings and leaks. Dispose releases event handlers, rendering resources, and the DOM-to-instance association.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| chart.js | npm | You need common responsive chart types with a smaller API and a widely understood plugin model |
| vega-lite | npm | You want a grammar of graphics and reproducible declarative specifications rather than an imperative chart instance |
| uplot | npm | You need very fast, compact time-series plotting and can accept far fewer chart types and built-in interactions |