
echarts
You MUST use this when building, styling, debugging, or optimizing Apache ECharts charts in JavaScript, React, or Vue - setup, lifecycle, responsive resizing, theming, large datasets, streaming, SSR, and symptoms like a blank chart or broken resize. Not for choosing chart types or for other charting libraries.
You MUST use this when building, styling, debugging, or optimizing Apache ECharts charts in JavaScript, React, or Vue - setup, lifecycle, responsive resizing, theming, large datasets, streaming, SSR, and symptoms like a blank chart or broken resize. Not for choosing chart types or for other charting libraries.
ECharts
Use this skill to build, audit, or fix Apache ECharts charts without turning the task into an option-reference lookup. Match the project's existing setup first; only introduce wrappers or new dependencies when the project has none.
Decision Tree
User task -> Does the project already use ECharts?
- Yes -> Find existing chart components/helpers, reuse their init, theme,
and resize patterns. Match import style (full vs echarts/core).
- No -> Pick integration by framework:
- React -> echarts-for-react wrapper, or a small hook around
init/dispose if the project avoids extra deps
- Vue 3 -> vue-echarts wrapper, or composable around init/dispose
- Vanilla / other -> echarts.init on a sized container
Next -> Bundle size a concern (app ships to users)?
- Yes -> Import from 'echarts/core' and register only the used charts,
components, and renderer (tree-shaking)
- No / internal tool / prototype -> import * as echarts from 'echarts'
Then -> Build the smallest working option, render it, then layer on
interactivity (tooltip, dataZoom, toolbox) and theming.
Core Workflow
- Inspect first: find existing ECharts usage, themes, and shared option helpers before writing a new chart.
- Size the container: the container element must have non-zero width and height before
echarts.initruns; a chart in a display:none or unmounted tab renders blank. - Own the lifecycle: one
initper container,resize()on container size change,dispose()on unmount. Wrappers handle this; hand-rolled code must. - Update via
setOption: default merge mode for incremental updates (streaming, new data);notMerge: truewhen the chart type or structure changes. - Verify visually: render the chart and check axes, labels, and tooltip against real data before polishing.
Setup
npm install echarts # core library (always)
npm install echarts-for-react # React wrapper (optional)
npm install vue-echarts # Vue 3 wrapper (optional)
Tree-shakeable imports for production bundles:
import * as echarts from 'echarts/core';
import { LineChart, BarChart } from 'echarts/charts';
import { GridComponent, TooltipComponent, DataZoomComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([LineChart, BarChart, GridComponent, TooltipComponent, DataZoomComponent, CanvasRenderer]);
A missing registration fails at runtime with a console error naming the missing chart/component; register it, do not switch to full import to silence the error. It is a console.error, not a thrown exception, so unit tests pass silently over it; catch it by asserting on the console or the rendered output.
With multiple chart components in one codebase, prefer a shared registration module (one echarts.use([...]) call imported everywhere) over per-component use lists; per-component lists drift out of sync and hide missing registrations until a component renders alone. Deliberate feature-specific registration in code-split routes is a valid exception for lazy-loaded dashboards.
Type imports: import type { ... } from 'echarts' is erased at compile time and does not affect the bundle; only value imports from the root package pull everything in. Some types (XAXisComponentOption, DefaultLabelFormatterCallbackParams) are exported only from the root, so mixing import type from 'echarts' with values from 'echarts/core' is normal; prefer ComposeOption from 'echarts/core' for option types:
import type { ComposeOption } from 'echarts/core';
import type { LineSeriesOption } from 'echarts/charts';
import type { GridComponentOption, TooltipComponentOption } from 'echarts/components';
type ChartOption = ComposeOption<LineSeriesOption | GridComponentOption | TooltipComponentOption>;
Lifecycle Rules
- Vanilla: keep the chart instance; call
chart.resize()from aResizeObserveron the container; callchart.dispose()before removing the container. - React (echarts-for-react): pass
optionas a prop; usenotMergeprop when replacing structure; get the instance viaref.getEchartsInstance()only for imperative needs (streamingsetOption,dispatchAction). - React (hand-rolled hook):
initin an effect,disposein its cleanup; keepoptionupdates in a separate effect so the chart is not re-created on every render. - Vue (vue-echarts): use
:optionbinding withautoresize; access the instance via template ref fordispatchAction. Pass:update-options="{ notMerge: true }"for structural option changes (chart type, series count, removing axes/series); merge mode keeps stale series. Switch themes via thethemeprop orTHEME_KEYinjection, notupdate-options(on older ECharts/vue-echarts versions, remount/re-init instead). Use thegroupprop to link charts (equivalent toecharts.connect). - Never call
echarts.inittwice on the same DOM node; reuse the instance or dispose first (echarts.getInstanceByDomto check).
Data and Options
- Prefer the
datasetcomponent (source+encode) when multiple series or charts share one table of data; use per-seriesdatafor simple single-series charts. - Time series: use
xAxis: { type: 'time' }with[timestamp, value]pairs instead of pre-formatting date strings into a category axis. - Large categorical axes: set
axisLabel.interval/rotatedeliberately instead of accepting overlap. - Tooltips:
trigger: 'axis'for line/bar time series,trigger: 'item'for pie/scatter/map. - Use
valueFormatterortooltip.formatterfor units; keep number formatting in one shared helper when the dashboard has many charts. - HTML tooltip
formatteroutput is injected as HTML: escape untrusted data (series names, user-generated labels) with a shared escape helper, or usetooltip.renderMode: 'richText'to opt out of HTML entirely.
Performance
- Choose Canvas, SVG, or WebGL from measured workload rather than a fixed point threshold. Measure the dataset, device/browser, interaction latency, and SVG output size; see the audit reference when reviewing an existing chart.
- For large line/scatter series: enable
large: trueandsampling: 'lttb'on the series; turn offanimationfor initial render of big datasets. - Millions of points: use
echarts-gl(WebGL), a separate dependency; add it only when actually needed. - Streaming: call
setOption({ series: [{ data }] })on the existing instance (merge mode); do not re-init or passnotMergeper tick. - Many charts on one page: share a single
ResizeObserver/resize handler and useecharts.connectfor linked tooltips/dataZoom instead of duplicating handlers.connectis also a UX feature for dashboards:chart.group = 'name'; echarts.connect('name')(or the vue-echartsgroupprop) syncs tooltips and dataZoom across related charts. Only link charts with compatible axis semantics (same x-axis type and domain); a chart with a different axis belongs in its own group or unlinked.
Theming
- Register a theme once (
echarts.registerTheme('name', themeObject)) and pass the name to everyinit; do not copy color arrays into each chart's option. - Dark mode: prefer
init(el, null, ...)plus a registered dark theme, ordarkMode: truein the option. Switch themes at runtime withchart.setTheme(...)(ECharts 6) or the vue-echartsthemeprop; on ECharts 5 themes are fixed at init time; re-init (dispose + init) there. - Keep chart-independent styling (font family, palette) in the theme; keep data-dependent styling (visualMap ranges, markLines) in the option.
SSR and Export
- Server-side rendering (reports, emails, OG images):
echarts.init(null, null, { renderer: 'svg', ssr: true, width, height })thenrenderToSVGString()- Node only, no DOM needed. - If option builders are shared between the browser and a Node SVG renderer, keep both
echarts.use([...])registration points covering the same set; a narrower server-side list silently renders without the missing components. - Client image export: enable
toolbox.feature.saveAsImage, or callchart.getDataURL({ pixelRatio: 2 })programmatically.
ECharts 6 Migration Notes
grid.containLabelis deprecated. The semantics-preserving migration iscontainLabel: true→{ outerBoundsMode: 'same', outerBoundsContain: 'axisLabel' }; setgrid.outerBoundsonly when you need a custom constraint rect (it is a separate part of the new layout API). The legacy behavior still works only ifLegacyGridContainLabel(from'echarts/features') is registered; treat remainingcontainLabel: trueusages as tech debt when auditing.- The default theme changed in v6 (palette and component layout). To keep the v5 look during migration:
import 'echarts/theme/v5'and pass'v5'as the theme toinit. - Axis label overflow prevention and axis-name overlap prevention are on by default in v6, which can shift layouts slightly; disable with
grid.outerBoundsMode: 'none'andxAxis/yAxis.nameMoveOverlap: falsewhen pixel-parity with v5 matters. - Check the installed major version (
node_modules/echarts/package.json) before recommending options; deprecations surface as console warnings, not errors.
Auditing Existing Usage
For a code-and-browser audit, read references/audit.md before writing findings. It is the required full checklist for dashboard growth, tree-shaken registrations, interactive state, HTML tooltip trust, large-data cardinality, zero-size failures, and browser evidence.
Quick triage still starts with the shared registration module, lifecycle ownership, structural setOption updates, root value imports, and ECharts-version migration debt. Treat repeated formatter/options as extraction debt; centralized design tokens passed directly to options are a valid alternative to registerTheme when that is the project's deliberate convention.
Common Failure Modes
- Blank chart, no error: container had zero size at init (hidden tab, flex parent without height, init before mount). Fix sizing/timing, then call
resize(). - Chart does not update: a new option object with merge mode silently keeps stale series/axes; use
notMerge: truewhen removing series or changing chart type. - Legend/dataZoom selection lost after update:
notMerge: truecan reset interactive state, depending on the wrapper, versions, and update path. Capture the state you need to survive (chart.getOption().legend[0].selected, the dataZoom range) and pass it back, or give it an explicit app-side owner. Do not report a reset from static inspection alone; prove it on the installed ECharts/wrapper versions. The ECharts instance is a valid owner for session-only state when browser evidence shows it survives and the product does not require it to survive a remount or navigation. notMerge: trueeverywhere: forfeits ECharts' diff optimization and risks resetting legend/dataZoom selection on structural updates. Reserve it for structural changes (chart type, series count, removed axes/series); keep merge mode for data-only updates.- "Component xxx not exists" / missing chart: tree-shaken build without the registration; add it to
echarts.use([...]). - Memory growth in SPA: instances not disposed on route change; verify
dispose()runs in unmount cleanup. - Chart wrong size after sidebar/panel toggle: window
resizeevent never fired; observe the container (ResizeObserver /autoresize), not the window. - Tooltip clipped: set
tooltip.confine: trueorappendToBody-styletooltip.appendTowhen the chart sits in an overflow-hidden container. - Sluggish with big data: animation on + no sampling; set
animation: false,sampling: 'lttb',large: truebefore reaching for WebGL.
Reference Examples
examples/vanilla_line.html- Vanilla JS time-series line chart with resize handlingexamples/react_chart.tsx- React component with tree-shaken imports and echarts-for-reactexamples/vue_chart.vue- Vue 3 component using vue-echarts with autoresize





