Getting Started
Setup & First Chart
ECharts is initialized on a DOM element with explicit width/height. setOption configures the entire chart. Apache ECharts (formerly Baidu ECharts) is one of the most powerful JS charting libraries.
<!-- include ECharts -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<div id="chart" style="width: 600px; height: 400px;"></div>
<script>
const chart = echarts.init(document.getElementById('chart'));
chart.setOption({
title: { text: 'Sales' },
xAxis: { data: ['Q1', 'Q2', 'Q3', 'Q4'] },
yAxis: {},
series: [{
type: 'bar',
data: [100, 200, 150, 300]
}]
});
</script>Init with Theme & Renderer
init takes (dom, theme, opts). The 'svg' renderer is sharper at any zoom and produces smaller DOM for simple charts, while 'canvas' is faster for large datasets. devicePixelRatio controls crispness on retina. Always call dispose() when removing a chart to free memory.
// register a theme (defined elsewhere or imported)
echarts.registerTheme('myTheme', {
color: ['#5470c6', '#91cc75'],
backgroundColor: '#f5f5f5'
});
// renderer: 'canvas' (default, faster) or 'svg' (sharper, lighter)
const chart = echarts.init(document.getElementById('chart'), 'myTheme', {
renderer: 'svg',
devicePixelRatio: 2,
width: 800,
height: 600,
locale: 'EN'
});
// detached from DOM later
chart.dispose();Responsive Resize
ECharts doesn't auto-resize when its container changes — you must call chart.resize(). ResizeObserver is the modern way to watch container size (better than window resize for layout-driven changes). Always dispose() on teardown to prevent leaks in SPAs.
const chart = echarts.init(document.getElementById('chart'));
chart.setOption({ /* ... */ });
// resize when window changes
window.addEventListener('resize', () => chart.resize());
// or observe the container
const ro = new ResizeObserver(() => chart.resize());
ro.observe(document.getElementById('chart'));
// resize with explicit size
chart.resize({ width: 500, height: 300, silent: false });
// cleanup
window.addEventListener('unload', () => chart.dispose());setOption Merge Modes
By default setOption deep-merges new options into existing ones, so you can update individual pieces. notMerge: true replaces the whole option (useful when series count changes). replaceMerge targets specific component types (series, xAxis) for partial replacement. lazyUpdate batches multiple setOption calls into one render.
const chart = echarts.init(document.getElementById('chart'));
// merge (default): deep-merges with existing option
chart.setOption({ xAxis: { data: ['A', 'B'] } });
// notMerge: replace entire option (old config discarded)
chart.setOption({ xAxis: { data: ['X', 'Y'] } }, { notMerge: true });
// replaceMerge: replace specific component types only
chart.setOption(
{ series: [{ type: 'bar', data: [5, 6] }] },
{ replaceMerge: ['series'] }
);
// lazyUpdate: batch updates, apply on next frame
chart.setOption({ series: [{ data: [1, 2] }] }, { lazyUpdate: true });Lifecycle: getInstanceByDom & dispose
Calling init on a DOM that already has a chart throws. Use getInstanceByDom to check. dispose() frees memory and removes the chart; the DOM element stays. The attribute _echarts_instance_ marks chart containers — query it to find and clean up stray charts. Always dispose in framework unmount hooks.
function mountChart(dom) {
// avoid double-init on the same DOM node
const existing = echarts.getInstanceByDom(dom);
if (existing) return existing;
const chart = echarts.init(dom);
chart.setOption({ /* ... */ });
return chart;
}
// in a framework cleanup (React useEffect / Vue onUnmounted):
function unmount(dom) {
const chart = echarts.getInstanceByDom(dom);
if (chart) chart.dispose(); // free memory + remove DOM
}
// dispose all charts on a page
echarts.disposeAll?.(); // not built-in; iterate manually
Array.from(document.querySelectorAll('[_echarts_instance_]'))
.forEach(dom => echarts.dispose(dom));Bar Charts
Basic Bar
A bar chart needs a category xAxis and a value yAxis. Each series.data value maps to one category. itemStyle controls bar appearance. For vertical bars, the category axis is x; for horizontal bars, swap x and y axis types.
chart.setOption({
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
},
yAxis: { type: 'value' },
series: [{
type: 'bar',
data: [120, 200, 150, 80, 70],
itemStyle: { color: '#5470c6' }
}]
});Grouped & Stacked Bar
Multiple bar series with the same xAxis are grouped by default. To stack them, give each series the same stack name. Stacking accumulates values per category. Use stack: 'total' on all series that should stack together — series without a stack name still group.
chart.setOption({
xAxis: { type: 'category', data: ['Q1', 'Q2', 'Q3', 'Q4'] },
yAxis: { type: 'value' },
// GROUPED: multiple series render side by side
series: [
{ type: 'bar', name: 'A', data: [10, 20, 30, 40] },
{ type: 'bar', name: 'B', data: [15, 25, 35, 45] }
]
});
// STACKED: add stack with same name to each series
chart.setOption({
series: [
{ type: 'bar', name: 'A', stack: 'total', data: [10, 20, 30, 40] },
{ type: 'bar', name: 'B', stack: 'total', data: [15, 25, 35, 45] }
]
});Horizontal Bar
A horizontal bar is just a bar chart with the category axis on yAxis and value axis on xAxis. Labels with position: 'right' sit at the bar end. Horizontal bars are better for long category names or when comparing many categories — they avoid rotated labels.
chart.setOption({
// swap: category axis becomes yAxis
yAxis: {
type: 'category',
data: ['Apple', 'Banana', 'Cherry', 'Date']
},
xAxis: { type: 'value' },
series: [{
type: 'bar',
data: [120, 200, 150, 80],
label: {
show: true,
position: 'right' // label at end of bar
}
}]
});Bar with Background & Rounded Corners
showBackground draws a track behind each bar (progress-bar style). itemStyle.borderRadius rounds bar corners — pass [topLeft, topRight, bottomRight, bottomLeft] for vertical bars; reverse order for horizontal. borderRadius can be a number (all corners) or array.
chart.setOption({
xAxis: { type: 'category', data: ['A', 'B', 'C', 'D'] },
yAxis: { type: 'value' },
series: [{
type: 'bar',
data: [50, 80, 60, 90],
showBackground: true,
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.2)',
borderRadius: [4, 4, 0, 0]
},
itemStyle: {
borderRadius: [8, 8, 0, 0] // round top corners
}
}]
});Bar Race (Dynamic)
A bar race animates reordering. Sort the data array, then update both the yAxis categories and series data. animationDurationUpdate controls transition smoothness. yAxis.inverse: true puts the largest value at the top. This is popular for animated data stories.
const data = [
{ name: 'A', value: 10 },
{ name: 'B', value: 20 }
];
chart.setOption({
xAxis: { type: 'value', max: 100 },
yAxis: { type: 'category', inverse: true },
series: [{
type: 'bar',
data: data.map(d => ({ value: d.value, name: d.name })),
label: { show: true, position: 'right' },
// sort bars by value on each update
animationDuration: 500,
animationDurationUpdate: 500
}]
});
// update data on interval
setInterval(() => {
data.forEach(d => d.value += Math.random() * 10);
data.sort((a, b) => b.value - a.value);
chart.setOption({
yAxis: { data: data.map(d => d.name) },
series: [{ data: data.map(d => d.value) }]
});
}, 1000);Bar Width & Gap
barWidth accepts pixels or percentage of category band. barGap is the gap between bars within a group (only matters with multiple series). barCategoryGap is the gap between groups. Percentages are relative to the category band, so they stay proportional on resize.
chart.setOption({
xAxis: { type: 'category', data: ['A', 'B', 'C', 'D'] },
yAxis: { type: 'value' },
series: [{
type: 'bar',
data: [50, 80, 60, 90],
barWidth: '40%', // width of each bar
barGap: '20%', // gap between bars in same group
barCategoryGap: '40%', // gap between category groups
// fixed pixel width also works:
// barWidth: 30
}]
});Line Charts
Basic Line
A line chart uses type: 'line'. Points are shown by default with symbol markers; set symbol: 'none' to hide them. The line connects points in data order. For time series, use type: 'time' on xAxis with [timestamp, value] pairs.
chart.setOption({
xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'] },
yAxis: { type: 'value' },
series: [{
type: 'line',
data: [120, 200, 150, 80, 70],
symbol: 'circle', // point marker
symbolSize: 8
}]
});Smooth Line & Area
smooth: true curves the line (Catmull-Rom interpolation). areaStyle fills the area under the line; a gradient (linear color stops) gives a faded look. boundaryGap: false on the category axis starts the line at the y-axis edge instead of leaving a gap.
chart.setOption({
xAxis: { type: 'category', boundaryGap: false, data: ['Mon','Tue','Wed','Thu','Fri'] },
yAxis: { type: 'value' },
series: [{
type: 'line',
data: [120, 200, 150, 80, 70],
smooth: true, // curved line
areaStyle: { // fill under line
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(84,112,198,0.5)' },
{ offset: 1, color: 'rgba(84,112,198,0.05)' }
]
}
}
}]
});Multi-Series Line
Multiple series render as separate lines, distinguished by color. The legend toggles visibility. lineStyle.type accepts 'solid', 'dashed', 'dotted'. Each series can have its own style. The name in legend must match series.name.
chart.setOption({
legend: { data: ['Email', 'Search'] },
xAxis: { type: 'category', data: ['Mon','Tue','Wed','Thu','Fri'] },
yAxis: { type: 'value' },
series: [
{
type: 'line',
name: 'Email',
data: [120, 132, 101, 134, 90],
lineStyle: { width: 3, type: 'solid' }
},
{
type: 'line',
name: 'Search',
data: [220, 182, 191, 234, 290],
lineStyle: { width: 2, type: 'dashed' }
}
]
});Stacked Area
Stacked area charts use type: 'line' with the same stack name and an areaStyle. Each series stacks on top of the previous. emphasis.focus: 'series' highlights the whole series on hover. Useful for showing composition over time (e.g. traffic by source).
chart.setOption({
xAxis: { type: 'category', boundaryGap: false, data: ['Mon','Tue','Wed','Thu','Fri'] },
yAxis: { type: 'value' },
series: [
{
type: 'line',
name: 'A',
stack: 'total',
areaStyle: {},
data: [120, 132, 101, 134, 90],
emphasis: { focus: 'series' }
},
{
type: 'line',
name: 'B',
stack: 'total',
areaStyle: {},
data: [220, 182, 191, 234, 290]
}
]
});Step Line
step turns a line into a staircase. 'start' steps before the point, 'middle' (symmetric) at the point, 'end' after. Step charts are great for values that change discretely (inventory levels, server status) rather than continuously.
chart.setOption({
xAxis: { type: 'category', data: ['Mon','Tue','Wed','Thu','Fri'] },
yAxis: { type: 'value' },
series: [{
type: 'line',
data: [120, 200, 150, 80, 70],
step: 'middle', // 'start' | 'middle' | 'end'
symbol: 'none',
lineStyle: { width: 2 }
}]
});Line with Time Axis
With type: 'time', the axis auto-formats dates and handles irregular intervals. Data points are [timestamp, value] pairs. ECharts spaces points by their actual time, so gaps in data show correctly. This is the right choice for time series with varying intervals.
chart.setOption({
xAxis: { type: 'time' }, // auto-parses timestamps
yAxis: { type: 'value' },
series: [{
type: 'line',
data: [
[new Date('2024-01-01').getTime(), 100],
[new Date('2024-01-02').getTime(), 200],
[new Date('2024-01-05').getTime(), 150],
[new Date('2024-01-10').getTime(), 300]
],
smooth: true
}]
});Pie Charts
Basic Pie
Pie charts don't use axes — series.data is an array of {value, name}. radius controls pie size as a percentage of the container's smaller dimension. emphasis styles apply on hover. The legend auto-derives labels from data.name.
chart.setOption({
series: [{
type: 'pie',
radius: '60%', // radius of pie
data: [
{ value: 1048, name: 'Search' },
{ value: 735, name: 'Direct' },
{ value: 580, name: 'Email' },
{ value: 484, name: 'Union' },
{ value: 300, name: 'Video' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0,0,0,0.5)'
}
}
}]
});Doughnut (Ring) Pie
Setting radius to [inner, outer] creates a doughnut. A center label is a common pattern (show total or a KPI). avoidLabelOverlap: false keeps labels where they are. The inner radius controls ring thickness — [50%, 70%] is a thin ring; [0%, 70%] is a full pie.
chart.setOption({
series: [{
type: 'pie',
radius: ['40%', '70%'], // [inner, outer] = ring
avoidLabelOverlap: false,
label: {
show: true,
position: 'center', // center label
formatter: 'Total\n{c}'
},
data: [
{ value: 1048, name: 'A' },
{ value: 735, name: 'B' },
{ value: 580, name: 'C' }
]
}]
});Rose (Nightingale) Chart
roseType makes slice radius proportional to value (Nightingale rose). 'area' uses sqrt(value) for radius (area-proportional), 'radius' uses value directly. Each slice has the same angle but different radius. Good for comparing magnitudes where angle alone is hard to judge.
chart.setOption({
series: [{
type: 'pie',
radius: ['10%', '70%'],
roseType: 'area', // or 'radius'
data: [
{ value: 40, name: 'A' },
{ value: 38, name: 'B' },
{ value: 32, name: 'C' },
{ value: 30, name: 'D' },
{ value: 28, name: 'E' }
],
label: { show: true }
}]
});Pie with Custom Labels
label.formatter supports template vars: {a} (series name), {b} (data name), {c} (value), {d} (percentage). labelLine controls the connector line. length is the segment near the slice, length2 the segment near the label. Custom labels make pie charts much more readable.
chart.setOption({
series: [{
type: 'pie',
radius: '60%',
label: {
formatter: '{b}: {d}%', // name: percentage
color: '#333',
fontSize: 14
},
labelLine: {
show: true,
length: 15, // first segment
length2: 20, // second segment
smooth: true
},
data: [
{ value: 1048, name: 'Search' },
{ value: 735, name: 'Direct' }
]
}]
});Semi-circle Pie
Combining startAngle and endAngle creates partial pies (half, quarter, gauge-like). center positions the pie — ['50%','70%'] pushes it down so a 180° pie sits at the bottom. Partial pies are often used as gauges or for stylized dashboards.
chart.setOption({
series: [{
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '70%'], // move down
startAngle: 180, // start at left
endAngle: 360, // end at right -> semicircle
data: [
{ value: 60, name: 'A' },
{ value: 30, name: 'B' },
{ value: 10, name: 'C' }
]
}]
});Scatter Charts
Basic Scatter
Scatter charts use two value axes; each data point is [x, y]. symbolSize controls point size (fixed or per-point via a function). Scatter reveals correlation between two variables. Both axes are type: 'value' (numeric), unlike category-based bar/line.
chart.setOption({
xAxis: { type: 'value' },
yAxis: { type: 'value' },
series: [{
type: 'scatter',
symbolSize: 12,
data: [
[10, 20], [15, 25], [20, 30], [25, 35],
[30, 40], [35, 45], [40, 50], [45, 55]
],
itemStyle: { color: '#5470c6' }
}]
});Bubble Chart (Size by Value)
A bubble chart is a scatter where point size encodes a third dimension. Pass [x, y, size] per point and set symbolSize to a function returning the size. The function runs per point, so each bubble scales independently. Useful for 3-variable data without a 3D chart.
chart.setOption({
xAxis: { type: 'value' },
yAxis: { type: 'value' },
series: [{
type: 'scatter',
// each point: [x, y, size]
data: [
[10, 20, 30],
[15, 25, 60],
[20, 30, 12],
[25, 35, 45]
],
symbolSize: function (data) {
return data[2]; // use 3rd element as size
}
}]
});Scatter with VisualMap (Color by Value)
visualMap maps a data dimension to color. dimension: 2 colors by the 3rd element (index 2) of each point. inRange.color defines the gradient. This adds a fourth dimension (color) to a scatter. The visualMap component shows the legend with a slider.
chart.setOption({
xAxis: { type: 'value' },
yAxis: { type: 'value' },
visualMap: {
min: 0,
max: 100,
dimension: 2, // map color to 3rd element
inRange: { color: ['#50a3ba', '#eac736', '#d94e5d'] },
right: 10,
top: 'center'
},
series: [{
type: 'scatter',
data: [[10,20,30], [15,25,60], [20,30,12]],
symbolSize: 15
}]
});Effect Scatter (Animated)
effectScatter adds a ripple animation around each point — great for highlighting key data points (alerts, top cities). showEffectOn controls when the ripple plays. brushType: 'stroke' is lighter; 'fill' is more prominent. Use sparingly — too many ripples distract.
chart.setOption({
xAxis: { type: 'value' },
yAxis: { type: 'value' },
series: [{
type: 'effectScatter', // animated ripple
symbolSize: function (data) { return data[2]; },
data: [[10, 20, 30], [15, 25, 60]],
showEffectOn: 'render', // 'render' or 'emphasis'
rippleEffect: {
brushType: 'stroke', // 'stroke' or 'fill'
period: 4,
scale: 3
}
}]
});Large-scale Scatter
For thousands of points, enable progressive rendering — ECharts splits data into chunks and renders them across frames to avoid freezing. large: true uses a single optimized draw call. Combine with small symbolSize and opacity to handle overplotting. This keeps 50k+ points smooth.
// for thousands of points, use progressive rendering
chart.setOption({
xAxis: { type: 'value' },
yAxis: { type: 'value' },
series: [{
type: 'scatter',
data: largeDataArray, // 50k+ points
symbolSize: 3,
progressive: 2000, // chunk size for progressive render
progressiveThreshold: 5000, // enable above this count
large: true, // optimize for many points
largeThreshold: 2000,
itemStyle: { opacity: 0.6 }
}]
});Radar Charts
Basic Radar
Radar charts need a radar.indicator array defining each axis (name and max). Series data is an array of values matching the indicators. Radar is great for comparing multiple attributes of a few items (player stats, product comparison). max sets the scale of each axis.
chart.setOption({
radar: {
indicator: [
{ name: 'Speed', max: 100 },
{ name: 'Power', max: 100 },
{ name: 'Range', max: 100 },
{ name: 'Comfort', max: 100 },
{ name: 'Price', max: 100 }
]
},
series: [{
type: 'radar',
data: [{ value: [85, 70, 90, 65, 80], name: 'Car A' }]
}]
});Multi-series Radar
Multiple data entries render overlapping polygons, ideal for comparison. Each entry needs a unique name matching the legend. Too many series (5+) makes radar charts unreadable — the polygons overlap into a blob. Stick to 2-4 items for clarity.
chart.setOption({
legend: { data: ['Car A', 'Car B'] },
radar: {
indicator: [
{ name: 'Speed', max: 100 },
{ name: 'Power', max: 100 },
{ name: 'Range', max: 100 }
]
},
series: [{
type: 'radar',
data: [
{ value: [85, 70, 90], name: 'Car A' },
{ value: [60, 95, 75], name: 'Car B' }
]
}]
});Radar with Area
areaStyle fills the radar polygon with semi-transparent color, making the shape easier to read. lineStyle emphasizes the outline. Keep areaStyle opacity low (0.2-0.4) so overlapping series stay visible. Symbols mark each vertex.
chart.setOption({
radar: {
indicator: [
{ name: 'A', max: 100 },
{ name: 'B', max: 100 },
{ name: 'C', max: 100 }
]
},
series: [{
type: 'radar',
data: [{ value: [85, 70, 90], name: 'Score' }],
areaStyle: { opacity: 0.3 },
lineStyle: { width: 2 },
symbol: 'circle',
symbolSize: 6
}]
});Radar Shape & Axis
shape: 'polygon' (default) gives angular axes; 'circle' makes smooth rings. splitNumber controls grid density. splitArea alternates colors for a zebra effect. axisName styles the axis labels. These options control the chart's visual structure without touching data.
chart.setOption({
radar: {
shape: 'polygon', // 'polygon' or 'circle'
radius: '65%',
splitNumber: 5, // grid ring count
axisName: {
color: '#333',
fontSize: 12
},
splitLine: { lineStyle: { color: '#ccc' } },
splitArea: {
areaStyle: { color: ['#fafafa', '#fff'] }
},
indicator: [
{ name: 'Speed', max: 100 },
{ name: 'Power', max: 100 }
]
},
series: [{ type: 'radar', data: [{ value: [80, 70] }] }]
});Radar Polar Radius
radius: [inner, outer] creates a ring-shaped radar (like a doughnut). center positions the chart. min on an indicator sets where the axis starts — useful when all values are high (avoids a tiny polygon in the center). Adjust center to leave room for axis labels.
chart.setOption({
radar: {
indicator: [
{ name: 'A', max: 100, min: 0 }, // min sets the inner bound
{ name: 'B', max: 100 },
{ name: 'C', max: 100 }
],
radius: ['20%', '70%'], // [inner, outer] for ring radar
center: ['50%', '55%']
},
series: [{
type: 'radar',
data: [{ value: [60, 70, 80] }]
}]
});Heatmaps
Cartesian Heatmap
A cartesian heatmap needs a visualMap to color cells. Data is [x, y, value]. label: { show: true } prints values in cells (only good for small grids). calculable: true adds a slider handle. Heatmaps reveal density patterns (e.g. hourly activity).
const hours = ['12a','1a','2a','3a','4a','5a','6a','7a','8a','9a','10a','11a'];
const days = ['Sat','Sun','Mon','Tue','Wed','Thu','Fri'];
// data: [x, y, value]
const data = [[0,0,5],[0,1,1],[1,0,8] /* ... */];
chart.setOption({
tooltip: {},
xAxis: { type: 'category', data: hours },
yAxis: { type: 'category', data: days },
visualMap: {
min: 0, max: 10,
calculable: true,
orient: 'horizontal',
left: 'center', bottom: '5%'
},
series: [{
type: 'heatmap',
data: data,
label: { show: true }
}]
});Calendar Heatmap
Calendar heatmaps show daily values over a year (GitHub-style contribution graph). calendar.range sets the year or date range. Data points are [dateString, value]. The visualMap gradient defines color stops. cellSize controls square size — 'auto' fits the container.
const data = [];
// generate one year of data
for (let i = 0; i < 365; i++) {
const date = new Date(2024, 0, 1);
date.setDate(date.getDate() + i);
data.push([date.toISOString().slice(0,10), Math.floor(Math.random() * 100)]);
}
chart.setOption({
tooltip: {},
visualMap: {
min: 0, max: 100,
inRange: { color: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'] }
},
calendar: {
range: '2024',
cellSize: ['auto', 13],
yearLabel: { show: true }
},
series: [{ type: 'heatmap', data: data }]
});Heatmap Item Style
itemStyle.borderColor and borderWidth create a grid separator effect (white borders make cells distinct). borderRadius rounds cell corners. emphasis styles apply on hover. A hidden visualMap (show: false) still drives colors without showing the legend.
chart.setOption({
xAxis: { type: 'category', data: ['A','B','C','D'] },
yAxis: { type: 'category', data: ['X','Y','Z'] },
visualMap: { min: 0, max: 100, show: false },
series: [{
type: 'heatmap',
data: [[0,0,80],[0,1,30],[1,0,50]],
itemStyle: {
borderColor: '#fff', // white grid lines
borderWidth: 2,
borderRadius: 4
},
emphasis: {
itemStyle: { shadowBlur: 10 }
}
}]
});Punch Card Heatmap
A punch card is a 7x24 heatmap showing activity by day-of-week and hour. Each cell is a time slot. The rounded itemStyle makes it look like dots. Punch cards reveal weekly patterns (e.g. when a server is busiest). The data is a complete 168-cell grid.
const data = [];
for (let day = 0; day < 7; day++) {
for (let hour = 0; hour < 24; hour++) {
data.push([hour, day, Math.floor(Math.random() * 50) + 1]);
}
}
chart.setOption({
xAxis: { type: 'category', data: Array.from({length:24}, (_,i) => i + 'h') },
yAxis: { type: 'category', data: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'] },
visualMap: { max: 50, inRange: { color: ['#fff', '#5470c6'] } },
series: [{
type: 'heatmap',
data: data,
itemStyle: { borderRadius: 10, borderWidth: 2, borderColor: '#fff' }
}]
});Heatmap on Geo Map
Combining geo with a map series colors regions by value. visualMap drives the color scale. You must register a map first via echarts.registerMap('china', geoJson). roam: true enables pan/zoom. This is how choropleth (color-by-region) maps are built.
// requires a registered geo map (e.g. echarts.registerMap)
chart.setOption({
geo: { map: 'china', roam: true },
visualMap: {
min: 0, max: 1000,
inRange: { color: ['#e0ecf4', '#005824'] }
},
series: [{
type: 'map',
map: 'china',
data: [
{ name: 'Beijing', value: 800 },
{ name: 'Shanghai', value: 950 }
]
}]
});Tree Charts
Basic Tree (Vertical)
Tree charts render hierarchical data. layout: 'orthogonal' with orient: 'LR' makes a left-to-right tree. expandAndCollapse lets users click to fold/unfold subtrees. leaves configures leaf node styling. Tree is perfect for org charts, file trees, and taxonomies.
const data = {
name: 'Root',
children: [
{
name: 'Child A',
children: [{ name: 'A1' }, { name: 'A2' }]
},
{
name: 'Child B',
children: [{ name: 'B1' }]
}
]
};
chart.setOption({
series: [{
type: 'tree',
data: [data],
top: '5%', left: '10%', bottom: '5%', right: '20%',
layout: 'orthogonal', // 'orthogonal' or 'radial'
orient: 'LR', // 'LR','RL','TB','BT'
symbolSize: 10,
label: { position: 'left' },
leaves: { label: { position: 'right' } },
expandAndCollapse: true,
animationDuration: 550
}]
});Horizontal Tree
orient: 'LR' produces a horizontal tree (root on left, leaves on right). Internal node labels sit to the LEFT of the node; leaf labels to the RIGHT — this prevents overlap with branches. curveness on lineStyle creates curved connectors for a softer look.
chart.setOption({
series: [{
type: 'tree',
data: [treeData],
layout: 'orthogonal',
orient: 'LR', // left to right
symbol: 'emptyCircle',
symbolSize: 7,
label: { position: 'left', verticalAlign: 'middle', align: 'right' },
leaves: { label: { position: 'right', align: 'left' } },
lineStyle: { color: '#999', width: 1, curveness: 0.5 }
}]
});Radial Tree
layout: 'radial' places the root at the center and radiates children outward in a circle. This works well for large trees where a horizontal layout would be too wide. emphasis.focus: 'descendant' highlights all descendants on hover, helping users trace branches.
chart.setOption({
series: [{
type: 'tree',
data: [treeData],
layout: 'radial', // circular layout
symbol: 'circle',
symbolSize: 8,
label: {
position: 'inside',
rotate: 0
},
leaves: { label: { position: 'right' } },
emphasis: { focus: 'descendant' },
lineStyle: { curveness: 0.5 }
}]
});Tree with Custom Symbols
symbol and symbolSize accept functions for per-node customization. Here internal nodes (with children) are larger rectangles while leaves are small circles. This visually distinguishes hierarchy levels. Combine with orient: 'TB' for a classic top-down org chart.
const treeData = {
name: 'CEO',
value: 'Boss',
children: [
{ name: 'VP Engineering', children: [{ name: 'Eng Lead' }] },
{ name: 'VP Sales', children: [{ name: 'Sales Lead' }] }
]
};
chart.setOption({
series: [{
type: 'tree',
data: [treeData],
symbol: function (params) {
// custom symbol per node
return params.data.children ? 'rect' : 'circle';
},
symbolSize: function (params) {
return params.data.children ? 15 : 8;
},
orient: 'TB',
label: { position: 'top', distance: 8 }
}]
});Tree Expand/Collapse Control
initialTreeDepth controls how many levels are open initially (null = all, 0 = root only). Clicking a node with children toggles its subtree. The click event lets you react to node selection — useful for drill-down UIs. animationDurationUpdate smooths expand/collapse.
chart.setOption({
series: [{
type: 'tree',
data: [treeData],
expandAndCollapse: true,
initialTreeDepth: 2, // expand first 2 levels
collapse: 'collapseBtn',
symbol: 'circle',
animationDurationUpdate: 750
}]
});
// programmatic control via API
chart.on('click', function (params) {
if (params.componentType === 'series') {
console.log('Node clicked:', params.data.name);
console.log('Collapsed:', params.data.collapsed);
}
});Sunburst Charts
Basic Sunburst
Sunburst is a radial tree where each ring represents a hierarchy level. Inner = parent, outer = children. Each node's value drives its angular size. radius: [0, '90%'] makes a full disc; [inner, outer] creates a ring sunburst. Great for hierarchical proportion visualization.
const data = [{
name: 'Root',
children: [
{
name: 'A',
value: 15,
children: [{ name: 'A1', value: 5 }, { name: 'A2', value: 10 }]
},
{
name: 'B',
value: 20,
children: [{ name: 'B1', value: 20 }]
}
]
}];
chart.setOption({
series: [{
type: 'sunburst',
data: data,
radius: [0, '90%'],
label: { rotate: 'radial' }
}]
});Sunburst with Levels
levels configures each ring separately — the first entry is the root, subsequent entries map to deeper levels. r0/r set inner/outer radius for that level. Different label rotations (radial, tangential, 0) keep text readable at each depth. This gives fine-grained styling control.
chart.setOption({
series: [{
type: 'sunburst',
data: sunburstData,
radius: ['15%', '90%'],
levels: [
{}, // root level (default)
{
r0: '15%',
r: '45%',
label: { rotate: 0, fontSize: 14 },
itemStyle: { borderWidth: 2 }
},
{
r0: '45%',
r: '70%',
label: { rotate: 'tangential', fontSize: 12 }
},
{
r0: '70%',
r: '90%',
label: { rotate: 'radial', fontSize: 10 }
}
]
}]
});Sunburst Item Style & Highlight
emphasis.focus: 'ancestor' highlights the path from hovered node back to root — users see where a slice fits in the hierarchy. borderWidth separates slices visually. Sunburst is interactive: clicking usually drills down. Use ancestor highlighting to maintain context in deep trees.
chart.setOption({
series: [{
type: 'sunburst',
data: data,
itemStyle: {
borderColor: '#fff',
borderWidth: 1
},
emphasis: {
focus: 'ancestor' // highlight ancestors on hover
},
highlight: {
itemStyle: { color: '#ffb' }
}
}]
});Sunburst with Value & Sorting
sort controls slice ordering within a parent. nodeClick: 'zoomToNode' makes a click zoom into that subtree (great for exploration). minAngle hides labels on slices smaller than 5 degrees, avoiding clutter. The value of parent nodes is auto-summed from children if omitted.
chart.setOption({
series: [{
type: 'sunburst',
data: [{
name: 'Root',
children: [
{ name: 'A', value: 30, children: [/* ... */] },
{ name: 'B', value: 70, children: [/* ... */] }
]
}],
nodeClick: 'zoomToNode', // click to zoom into subtree
sort: 'desc', // or 'asc', null
label: { minAngle: 5 } // hide labels on tiny slices
}]
});Sunburst Drill-down
Custom click handlers let you build your own drill-down UX. Replace the data with the clicked node to zoom in. Keep a reference to the parent if you want a 'back' button. Sunburst drill-down is excellent for exploring deep hierarchies without overwhelming the screen.
const data = [/* full tree */];
let currentRoot = data[0];
chart.setOption({
series: [{
type: 'sunburst',
data: data,
nodeClick: false
}]
});
chart.on('click', function (params) {
if (params.componentType === 'series' && params.data.children) {
// manually drill down
currentRoot = params.data;
chart.setOption({
series: [{ data: [currentRoot] }]
}, { notMerge: true });
}
});Sankey Diagrams
Basic Sankey
Sankey shows flow between nodes. data is nodes (with names); links is the flow (source, target, value). The width of each link is proportional to value. emphasis.focus: 'adjacency' highlights connected nodes on hover. Great for energy, money, or user flow visualization.
chart.setOption({
series: [{
type: 'sankey',
data: [
{ name: 'Source A' },
{ name: 'Source B' },
{ name: 'Mid C' },
{ name: 'Sink D' }
],
links: [
{ source: 'Source A', target: 'Mid C', value: 5 },
{ source: 'Source B', target: 'Mid C', value: 3 },
{ source: 'Mid C', target: 'Sink D', value: 8 }
],
emphasis: { focus: 'adjacency' },
lineStyle: { color: 'gradient', curveness: 0.5 }
}]
});Sankey Node Levels
levels styles nodes by depth (column). lineStyle.color: 'source' colors links to match the source node; 'target' matches the target; 'gradient' blends. nodeAlign positions nodes vertically within a column. Setting depth explicitly forces column placement, overriding auto-layout.
chart.setOption({
series: [{
type: 'sankey',
data: [
{ name: 'A', depth: 0 },
{ name: 'B', depth: 1 },
{ name: 'C', depth: 2 }
],
links: [
{ source: 'A', target: 'B', value: 10 },
{ source: 'B', target: 'C', value: 10 }
],
levels: [
{ depth: 0, itemStyle: { color: '#fbb' }, lineStyle: { color: 'source' } },
{ depth: 1, itemStyle: { color: '#bfb' }, lineStyle: { color: 'target' } },
{ depth: 2, itemStyle: { color: '#bbf' } }
],
nodeAlign: 'justify' // 'left','right','justify'
}]
});Sankey Node Style & Gap
nodeWidth sets how thick each node rectangle is. nodeGap controls vertical spacing. Smaller gaps show more nodes but can overlap. lineStyle.opacity makes flows subtle so nodes stand out. label.position: 'right' puts labels next to nodes — common for left-to-right flows.
chart.setOption({
series: [{
type: 'sankey',
data: nodes,
links: links,
nodeWidth: 20, // width of each node rectangle
nodeGap: 10, // vertical gap between nodes
nodeAlign: 'justify',
label: {
fontSize: 12,
color: '#333',
position: 'right'
},
lineStyle: { opacity: 0.4 },
itemStyle: { borderWidth: 0 }
}]
});Sankey with Cycle (Drain)
True cycles cause ECharts Sankey to fail. The workaround is to split a recirculating node into separate 'in' and 'out' nodes (e.g. 'Recycle') so the flow is acyclic. This visually represents feedback loops. Always verify flow conservation: a node's input total should equal output total.
// Sankey normally forbids cycles, but you can show feedback loops
// by splitting a node into two (in & out) with the same name suffix
chart.setOption({
series: [{
type: 'sankey',
data: [
{ name: 'Input' },
{ name: 'Process' },
{ name: 'Output' },
{ name: 'Recycle' }
],
links: [
{ source: 'Input', target: 'Process', value: 100 },
{ source: 'Process', target: 'Output', value: 80 },
{ source: 'Process', target: 'Recycle', value: 20 },
{ source: 'Recycle', target: 'Process', value: 20 }
],
emphasis: { focus: 'adjacency' }
}]
});Sankey Interaction
Sankey click events distinguish nodes from edges via dataType. Use this to build detail panels showing flow specifics. label.formatter: '{b}: {c}' shows name and value. The adjacency focus highlights the full path through a hovered node, revealing cause-and-effect chains.
chart.setOption({
series: [{
type: 'sankey',
data: nodes,
links: links,
emphasis: {
focus: 'adjacency',
lineStyle: { opacity: 0.8 }
},
label: { formatter: '{b}: {c}' },
tooltip: { trigger: 'item' }
}]
});
chart.on('click', function (params) {
if (params.dataType === 'edge') {
console.log('Flow from', params.data.source, 'to', params.data.target);
console.log('Value:', params.data.value);
} else if (params.dataType === 'node') {
console.log('Node:', params.data.name);
}
});Funnel Charts
Basic Funnel
A funnel shows stages of a process where each stage has fewer items. sort: 'descending' (default) puts the largest stage at top. Data should be ordered by the process flow. Funnels are perfect for sales pipelines, conversion analysis, and onboarding steps.
chart.setOption({
series: [{
type: 'funnel',
data: [
{ value: 100, name: 'Visit' },
{ value: 75, name: 'Sign Up' },
{ value: 50, name: 'Trial' },
{ value: 25, name: 'Purchase' }
],
sort: 'descending' // 'descending','ascending','none'
}]
});Funnel with Sort & Gap
sort: 'ascending' inverts the funnel (smallest at top, like a growth chart). gap separates slices for readability. funnelAlign positions slices horizontally. minSize/maxSize bound the slice widths as percentages, ensuring tiny stages stay visible.
chart.setOption({
series: [{
type: 'funnel',
data: data,
sort: 'ascending', // smallest at top
gap: 4, // pixel gap between slices
funnelAlign: 'center', // 'left','center','right'
width: '60%',
minSize: '20%',
maxSize: '100%'
}]
});Funnel Labels
position: 'inside' overlays labels on slices; 'left'/'right' places them outside with connector lines (labelLine). formatter: '{b}: {c}' shows name and value. Use outside labels when slices are too thin for inside text. Emphasis enlarges the label on hover.
chart.setOption({
series: [{
type: 'funnel',
data: data,
label: {
show: true,
position: 'inside', // 'inside','left','right'
formatter: '{b}: {c}'
},
labelLine: {
show: true,
length: 20,
lineStyle: { width: 1, type: 'solid' }
},
emphasis: {
label: { fontSize: 16 }
}
}]
});Funnel Item Style
Per-slice itemStyle overrides the default color cycle. borderColor separates slices visually. You can also set color on individual data items. For a polished look, use a sequential color palette (lighter for early stages, darker for converted) to reinforce the funnel metaphor.
chart.setOption({
series: [{
type: 'funnel',
data: [
{ value: 100, name: 'A', itemStyle: { color: '#5470c6' } },
{ value: 75, name: 'B', itemStyle: { color: '#91cc75' } },
{ value: 50, name: 'C', itemStyle: { color: '#fac858' } }
],
itemStyle: {
borderColor: '#fff',
borderWidth: 2,
borderRadius: 0
}
}]
});Comparison Funnel
Multiple funnel series can sit side-by-side for period comparison. Give each series its own left/width to position them. This is great for A/B test visualization or year-over-year conversion comparison. Use contrasting colors per series to distinguish them.
chart.setOption({
series: [
{
type: 'funnel',
name: '2023',
data: [{ value: 100, name: 'A' }, { value: 40, name: 'B' }],
sort: 'descending',
left: '5%', width: '40%',
label: { position: 'inside' }
},
{
type: 'funnel',
name: '2024',
data: [{ value: 120, name: 'A' }, { value: 60, name: 'B' }],
sort: 'descending',
left: '55%', width: '40%',
label: { position: 'inside' }
}
]
});Gauge Charts
Basic Gauge
A gauge shows a single value on an arc. min/max set the scale. progress shows a colored arc up to the value (with axisLine as the track). detail renders the value text in the center. valueAnimation smoothly transitions the number. Great for KPIs and single-metric dashboards.
chart.setOption({
series: [{
type: 'gauge',
min: 0,
max: 100,
progress: { show: true, width: 18 },
axisLine: { lineStyle: { width: 18 } },
detail: {
valueAnimation: true,
formatter: '{value}%'
},
data: [{ value: 70, name: 'Score' }]
}]
});Gauge with Pointer
The pointer style is the classic gauge look. anchor is the center pin. Negative distance values push ticks/labels inward (toward center). radius scales the whole gauge. Combine pointer with splitLine to mimic a physical instrument. Useful when you want an analog feel.
chart.setOption({
series: [{
type: 'gauge',
radius: '60%',
min: 0, max: 100,
pointer: { show: true, length: '60%', width: 5 },
anchor: { show: true, size: 12, itemStyle: { color: '#333' } },
axisTick: { distance: -15, length: 5 },
splitLine: { distance: -20, length: 10 },
axisLabel: { distance: -25 },
data: [{ value: 42 }]
}]
});Gauge with Color Bands
axisLine.lineStyle.color accepts an array of [threshold, color] pairs to create colored zones (e.g. red/yellow/green for risk levels). Thresholds are fractions (0-1). This is the standard way to show 'safe/warning/danger' ranges on a gauge. The pointer or progress arc indicates the current value within these zones.
chart.setOption({
series: [{
type: 'gauge',
min: 0, max: 100,
axisLine: {
lineStyle: {
width: 20,
color: [
[0.3, '#fd666d'], // 0-30% red
[0.7, '#37a2da'], // 30-70% blue
[1, '#91cc75'] // 70-100% green
]
}
},
data: [{ value: 85 }]
}]
});Multi-pointer Gauge
Multiple data entries create multiple pointers on the same gauge. Useful for comparing related metrics (CPU vs Memory vs Disk). Each value animates independently. Keep the count low (3-5 max) or the gauge becomes cluttered. Names appear as labels near each pointer.
chart.setOption({
series: [{
type: 'gauge',
min: 0, max: 100,
data: [
{ value: 60, name: 'CPU' },
{ value: 40, name: 'Memory' },
{ value: 80, name: 'Disk' }
],
detail: { formatter: '{value}%' },
title: { fontSize: 14 }
}]
});Custom Gauge (Round Progress)
Setting startAngle/endAngle to span 360° creates a circular progress ring (like an Apple Watch ring). Hiding pointer/ticks/labels gives a clean minimalist look. roundCap rounds the progress ends. This style is popular for modern dashboards — it's a gauge disguised as a ring.
chart.setOption({
series: [{
type: 'gauge',
startAngle: 90,
endAngle: -270, // full circle
pointer: { show: false },
progress: {
show: true,
overlap: false,
roundCap: true,
clip: false
},
axisLine: { lineStyle: { width: 20, color: [[1, 'rgba(0,0,0,0.1)']] } },
splitLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false },
detail: {
valueAnimation: true,
offsetCenter: ['0%', '0%'],
fontSize: 30,
formatter: '{value}%'
},
data: [{ value: 65 }]
}]
});Coordinate Systems
Multiple X Axes
xAxis is an array — each entry is a separate axis. Series bind to an axis via xAxisIndex (0-based). This lets one chart show two metrics with different scales or units. Use position to place axes on top/bottom. Common for stock charts (price + volume) or weather (temp + humidity).
chart.setOption({
xAxis: [
{ type: 'category', data: ['A','B','C'], position: 'bottom' },
{ type: 'category', data: ['A','B','C'], position: 'top', axisLine: { onZero: false } }
],
yAxis: { type: 'value' },
series: [
{ type: 'line', data: [10, 20, 30], xAxisIndex: 0 },
{ type: 'line', data: [50, 60, 70], xAxisIndex: 1 }
]
});Dual Y Axes
Two y-axes (left and right) let you plot series with different units (e.g. revenue in $ vs units sold). Each series binds to an axis via yAxisIndex. Color-code the right axis line to match its series so users know which line reads against which axis. Avoid 3+ axes — they get confusing.
chart.setOption({
xAxis: { type: 'category', data: ['Mon','Tue','Wed','Thu'] },
yAxis: [
{ type: 'value', name: 'Sales', position: 'left' },
{ type: 'value', name: 'Orders', position: 'right', axisLine: { lineStyle: { color: '#ee6666' } } }
],
series: [
{ type: 'bar', data: [200, 180, 250, 210], yAxisIndex: 0 },
{ type: 'line', data: [40, 35, 50, 42], yAxisIndex: 1, smooth: true }
]
});Polar Coordinates
Polar coordinates use angleAxis (around the circle) and radiusAxis (out from center). Series set coordinateSystem: 'polar'. startAngle: 90 puts 0° at the top (north). Polar is great for directional data (wind rose), cyclical patterns, or rose/nightingale variants.
chart.setOption({
polar: {},
angleAxis: {
type: 'category',
data: ['N','NE','E','SE','S','SW','W','NW'],
startAngle: 90
},
radiusAxis: { type: 'value' },
series: [{
type: 'bar',
data: [1, 2, 3, 4, 5, 6, 7, 8],
coordinateSystem: 'polar'
}]
});Calendar Coordinates
The calendar coordinate system maps dates to cells. Series with coordinateSystem: 'calendar' bind to it. cellSize can be pixels or 'auto'. orient: 'horizontal' is the standard week-row layout. dayLabel/monthLabel nameMap localizes the labels. Combine with heatmap for contribution graphs.
chart.setOption({
calendar: {
range: ['2024-01-01', '2024-12-31'],
cellSize: ['auto', 15],
left: 50, right: 30,
orient: 'horizontal',
dayLabel: { nameMap: 'en' },
monthLabel: { nameMap: 'en' },
yearLabel: { show: false }
},
series: [{
type: 'heatmap',
coordinateSystem: 'calendar',
data: calendarData
}]
});Parallel Coordinates
Parallel coordinates plot multi-dimensional data with one axis per dimension. Each data row becomes a polyline crossing all axes. This reveals correlations and clusters in high-dimensional data (e.g. comparing cars across price, mileage, year, safety). Useful for data exploration.
chart.setOption({
parallelAxis: [
{ dim: 0, name: 'Price' },
{ dim: 1, name: 'Mileage' },
{ dim: 2, name: 'Year' },
{ dim: 3, name: 'Safety', type: 'category', data: ['Low','Med','High'] }
],
parallel: {
parallelAxisDefault: {
axisLine: { lineStyle: { color: '#999' } },
nameTextStyle: { color: '#333' }
}
},
series: [{
type: 'parallel',
data: [[20000, 30, 2020, 'High'], [25000, 25, 2021, 'Med']]
}]
});Geo Map Setup
registerMap binds a GeoJSON to a name for use in geo and map series. roam enables mouse pan/zoom. itemStyle.areaColor fills regions; borderColor outlines them. emphasis styles apply on hover. Geo alone shows a map; pair with a map series to color regions by value (choropleth).
// load a map GeoJSON (must be registered before use)
fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json')
.then(r => r.json())
.then(geoJson => {
echarts.registerMap('china', geoJson);
chart.setOption({
geo: {
map: 'china',
roam: true, // pan & zoom
zoom: 1.2,
label: { show: false },
itemStyle: {
areaColor: '#eee',
borderColor: '#fff'
},
emphasis: {
label: { show: true },
itemStyle: { areaColor: '#f5f5f5' }
}
}
});
});Dataset
Basic Dataset
Dataset separates data from series config. The source is a 2D array (header row first). seriesLayoutBy: 'column' makes each series take a column. encode maps dataset columns to x/y. This decouples data from presentation — useful when data is shared across multiple charts.
chart.setOption({
dataset: {
source: [
['product', '2023', '2024'],
['Apple', 41, 56],
['Banana', 67, 28],
['Cherry', 89, 76]
]
},
xAxis: { type: 'category' },
yAxis: {},
series: [
{ type: 'bar', seriesLayoutBy: 'column', encode: { x: 'product', y: '2023' } },
{ type: 'bar', seriesLayoutBy: 'column', encode: { x: 'product', y: '2024' } }
]
});Dataset with Object Rows
Dataset source can be an array of objects (key-value rows). dimensions optionally declares columns (useful when rows might be missing keys). encode uses dimension names instead of indices for readability. Object rows are easier to author by hand and more self-documenting.
chart.setOption({
dataset: {
dimensions: ['product', 'count', 'score'],
source: [
{ product: 'Apple', count: 41, score: 90 },
{ product: 'Banana', count: 67, score: 75 },
{ product: 'Cherry', count: 89, score: 88 }
]
},
xAxis: { type: 'category' },
yAxis: {},
series: [
{ type: 'bar', encode: { x: 'product', y: 'count' } },
{ type: 'bar', encode: { x: 'product', y: 'score' } }
]
});Dataset Transform (Filter/Sort)
Transforms chain datasets: filter removes rows, sort orders them. Each transform reads from fromDatasetId and exposes a new dataset. Series bind via datasetId. Transforms let you derive views from one source without copying data. Supported transforms: filter, sort, and custom via registerTransform.
chart.setOption({
dataset: [
{
id: 'raw',
source: [
{ product: 'A', sales: 100 },
{ product: 'B', sales: 50 },
{ product: 'C', sales: 200 }
]
},
{
id: 'filtered',
fromDatasetId: 'raw',
transform: {
type: 'filter',
config: { and: [{ gte: { sales: 80 } }] }
}
},
{
id: 'sorted',
fromDatasetId: 'filtered',
transform: { type: 'sort', config: { order: 'desc', main: 'sales' } }
}
],
xAxis: { type: 'category' },
yAxis: {},
series: [{ type: 'bar', datasetId: 'sorted', encode: { x: 'product', y: 'sales' } }]
});Dataset Encode & Dimensions
encode maps dataset columns (by index or name) to chart dimensions. encode.tooltip lists columns to show in the tooltip. Here we use columns 0/1 for position, 2 for size, 3 for color (via itemStyle). Encoding makes the same dataset reusable across different chart types.
chart.setOption({
dataset: {
source: [
[10, 20, 30, 'red'],
[15, 25, 35, 'blue'],
[20, 30, 40, 'green']
]
},
xAxis: { type: 'value' },
yAxis: { type: 'value' },
series: [{
type: 'scatter',
encode: {
x: 0, // first column -> x
y: 1, // second column -> y
tooltip: [0, 1, 2] // show these in tooltip
},
symbolSize: function (d) { return d[2]; }
}]
});Dataset with Pie
Pie charts need itemName and value mappings via encode. The same dataset could feed a bar chart (encode x:name, y:value) and a pie chart with different encodings. This is the strength of dataset: one source, multiple views. Switching chart types only changes the series type and encode.
chart.setOption({
dataset: {
source: [
{ name: 'Search', value: 1048 },
{ name: 'Direct', value: 735 },
{ name: 'Email', value: 580 }
]
},
series: [{
type: 'pie',
radius: '60%',
encode: { itemName: 'name', value: 'value' }
}]
});VisualMap
Continuous VisualMap
Continuous visualMap maps a numeric range to color (and optionally size). inRange.color is the gradient (interpolated). calculable: true adds draggable handles for filtering. orient and left/top position it. The visualMap drives series colors — you don't set itemStyle.color manually.
chart.setOption({
visualMap: {
type: 'continuous',
min: 0,
max: 100,
inRange: {
color: ['#50a3ba', '#eac736', '#d94e5d'],
symbolSize: [10, 50]
},
calculable: true, // show slider handles
orient: 'vertical',
left: 'left', top: 'bottom'
},
series: [{
type: 'scatter',
data: [[10,20,30], [50,60,80], [80,90,95]]
}]
});Piecewise VisualMap
Piecewise visualMap bins values into discrete ranges, each with its own color. pieces defines the bins (min/max/label/color). This is clearer than a gradient when you want categorical buckets (e.g. low/medium/high risk). Useful for choropleth maps and categorized scatter plots.
chart.setOption({
visualMap: {
type: 'piecewise',
pieces: [
{ min: 0, max: 30, label: 'Low', color: '#91cc75' },
{ min: 30, max: 70, label: 'Mid', color: '#fac858' },
{ min: 70, max: 100, label: 'High', color: '#ee6666' }
],
orient: 'horizontal',
left: 'center', bottom: 10
},
series: [{
type: 'scatter',
data: data
}]
});Category VisualMap
Category visualMap maps string categories to colors. categories lists the values; inRange.color lists the matching colors in order. dimension selects which data column drives the mapping (default 0). Great when your data has a categorical attribute (e.g. priority, status) you want to color-code.
chart.setOption({
visualMap: {
type: 'piecewise',
categories: ['Low', 'Medium', 'High'],
inRange: { color: ['#91cc75', '#fac858', '#ee6666'] },
dimension: 2 // which data column to map
},
series: [{
type: 'scatter',
data: [
[10, 20, 'Low'],
[50, 60, 'Medium'],
[80, 90, 'High']
]
}]
});VisualMap Inline & Out of Range
outOfRange styles data points outside the visualMap's selected range (e.g. greyed out when filtered). controller styles the slider itself. When a user drags the slider to filter, out-of-range points fade to outOfRange.color. This makes visualMap an interactive filter, not just a legend.
chart.setOption({
visualMap: {
min: 0, max: 100,
inRange: { color: ['#5470c6', '#91cc75'] },
outOfRange: { color: '#ccc', symbolSize: 5 }, // filtered out style
controller: {
inRange: { color: ['#5470c6', '#91cc75'] }
},
right: 10, top: 'center'
},
series: [{
type: 'scatter',
data: data
}]
});VisualMap on Multiple Series
By default visualMap affects all series. Use seriesIndex (number or array) to target specific series. This lets you color-code one series while leaving others unchanged — useful when comparing a highlighted dataset against a reference. A single chart can even have multiple visualMaps for different series.
chart.setOption({
visualMap: {
min: 0, max: 100,
inRange: { color: ['#5470c6', '#91cc75', '#fac858'] }
},
series: [
{ type: 'scatter', name: 'A', data: dataA },
{ type: 'scatter', name: 'B', data: dataB }
]
});
// bind visualMap to specific series only
chart.setOption({
visualMap: { seriesIndex: 1 } // only affects second series
});Legend
Basic Legend
Legend lists series names for toggling visibility. data must match series.name (or be auto-derived). icon styles the marker. Clicking a legend item toggles its series. The legend auto-updates when series are added/removed via setOption.
chart.setOption({
legend: {
data: ['Email', 'Search', 'Direct'],
show: true,
icon: 'roundRect', // 'circle','rect','roundRect','triangle','diamond','pin','arrow'
itemWidth: 20,
itemHeight: 14,
textStyle: { color: '#333', fontSize: 12 }
},
series: [
{ type: 'line', name: 'Email', data: [120, 200, 150] },
{ type: 'line', name: 'Search', data: [220, 180, 90] }
]
});Legend Position & Layout
orient and top/left position the legend. Common layouts: top-center (default), bottom-center, right-vertical. itemGap controls spacing between legend items. A subtle background/border visually separates the legend from the chart. Position matters — bottom legends free up vertical space.
chart.setOption({
legend: {
orient: 'horizontal', // 'horizontal' or 'vertical'
top: 'top', // 'top','bottom','middle' or px
left: 'center', // 'left','center','right' or px
itemGap: 20, // gap between items
padding: 5,
backgroundColor: '#f5f5f5',
borderColor: '#ddd',
borderWidth: 1,
borderRadius: 4
},
series: [/* ... */]
});Legend Selection & Default
selected controls initial visibility. selectedMode: 'single' makes the legend act like radio buttons (only one series visible). The legendselectchanged event fires on user toggles — useful for persisting preferences or syncing multiple charts. Default mode is 'multiple'.
chart.setOption({
legend: {
selected: {
'Email': true, // visible by default
'Search': false, // hidden by default
'Direct': true
},
selectedMode: 'multiple' // 'single' or 'multiple'
},
series: [
{ type: 'line', name: 'Email', data: [120, 200, 150] },
{ type: 'line', name: 'Search', data: [220, 180, 90] },
{ type: 'line', name: 'Direct', data: [100, 90, 80] }
]
});
chart.on('legendselectchanged', function (params) {
console.log('Selected:', params.selected);
});Scrollable Legend
type: 'scroll' paginates the legend when there are too many items — page buttons appear. This is essential for charts with 10+ series that would otherwise overflow. Page icon/text styles customize the navigation. Vertical scroll legends fit well in the right margin.
chart.setOption({
legend: {
type: 'scroll', // 'plain' (default) or 'scroll'
orient: 'vertical',
right: 10, top: 20, bottom: 20,
pageButtonItemStyle: { color: '#333' },
pageIconColor: '#5470c6',
pageIconInactiveColor: '#ccc',
pageTextStyle: { color: '#333' },
data: Array.from({length: 30}, (_, i) => 'Series ' + i)
},
series: [/* many series */]
});Legend Formatter & Rich Text
formatter customizes legend labels (e.g. uppercase, add counts). rich text lets you style parts of the label differently — define styles in textStyle.rich and reference them in the formatter with {style|text} syntax. This enables legends like 'A {count|42}' with mixed styling.
chart.setOption({
legend: {
data: ['A', 'B'],
formatter: function (name) {
return name.toUpperCase();
},
textStyle: {
rich: {
a: { color: '#5470c6', fontSize: 14, fontWeight: 'bold' },
b: { color: '#91cc75', fontSize: 12 }
}
}
},
series: [/* ... */]
});
// legend can also show values via legendselectchanged + setOption
chart.on('legendselectchanged', () => {
// dynamic label updates
});Tooltip
Basic Tooltip
trigger: 'item' shows tooltip per data point (scatter, pie). 'axis' shows it for the whole category (bar, line). axisPointer draws a guide line — 'shadow' is a translucent band (good for bars), 'line' is a thin line (good for line charts), 'cross' shows both x and y lines.
chart.setOption({
tooltip: {
trigger: 'axis', // 'item','axis','none'
show: true,
backgroundColor: 'rgba(50,50,50,0.9)',
borderColor: '#333',
borderWidth: 1,
padding: 10,
textStyle: { color: '#fff', fontSize: 13 },
axisPointer: { type: 'shadow' } // 'line','shadow','cross'
},
xAxis: { type: 'category', data: ['A','B','C'] },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: [10, 20, 30] }]
});Tooltip Formatter
formatter customizes tooltip content. A function receives params (with name, value, seriesName, color, etc.) and returns HTML. String templates use {a} (series name), {b} (category/x), {c} (value), {d} (pie percentage). For multi-series axis tooltips, params is an array — iterate it.