Tooltips, legends, labels and visual maps
Write formatter callbacks that produce useful tooltips, manage label overlap, and drive colour from data with visualMap, markLine and markArea.
Tooltips that a reader can use
const option = {
tooltip: {
trigger: 'axis', // 'item', 'axis' or 'none'
axisPointer: {
type: 'cross', // 'line', 'shadow' or 'cross'
label: { backgroundColor: '#334155' }
},
confine: true, // never overflow the container
enterable: false, // true only if the tooltip has a link inside
appendToBody: true, // escapes overflow: hidden ancestors
backgroundColor: 'rgba(15, 23, 42, 0.92)',
borderWidth: 0,
padding: [8, 12],
textStyle: { color: '#f8fafc', fontSize: 12 },
extraCssText: 'border-radius: 8px; box-shadow: 0 6px 24px rgba(0,0,0,.18);',
formatter: (params) => {
// With trigger: 'axis', params is an array of one entry per series.
const rows = params
.map((p) => `<div style="display:flex;gap:12px;justify-content:space-between">
<span>${p.marker}${p.seriesName}</span>
<strong>${Number(p.value).toLocaleString()}</strong>
</div>`)
.join('');
const total = params.reduce((sum, p) => sum + Number(p.value || 0), 0);
return `<div style="min-width:160px">
<div style="opacity:.7;margin-bottom:6px">${params[0].axisValueLabel}</div>
${rows}
<div style="border-top:1px solid rgba(255,255,255,.15);margin-top:6px;padding-top:6px">
Total <strong>${total.toLocaleString()}</strong>
</div>
</div>`;
}
}
};| Setting | Effect | Use when |
|---|---|---|
trigger: 'axis' | One tooltip for all series at an x position | Time series and bar charts |
trigger: 'item' | One tooltip per mark | Scatter, pie, map |
confine: true | Keeps it inside the container | Small containers and dashboards |
appendToBody: true | Renders outside the chart element | The chart sits in a scrolling or clipped panel |
enterable: true | The pointer can move onto the tooltip | Only when it contains a link or button |
valueFormatter | Formats values without a full formatter | Simple unit suffixes |
renderMode: 'html' | 'richText' | HTML or canvas text | richText when HTML is unavailable or unwanted |
⚠️
The tooltip
formatter returns HTML that ECharts inserts into the page. Any value taken from the data — a series name, a label, a unit read from the server — must be escaped, or a crafted dataset becomes stored cross-site scripting. Escape the strings you interpolate, or use renderMode: 'richText'.Legend and label layout
const option = {
legend: {
type: 'scroll', // many series: scroll instead of wrapping off-screen
top: 0,
left: 'center',
itemWidth: 12,
itemHeight: 8,
icon: 'roundRect',
orient: 'horizontal',
selectedMode: true, // click to toggle a series
inactiveColor: '#cbd5e1',
textStyle: { color: '#334155' },
// Start with one series hidden
selected: { 'Forecast': false }
},
// Keep labels readable: layout, hide overlap, or move them outside for bars.
series: [{
type: 'bar',
label: {
show: true,
position: 'top',
formatter: (p) => Number(p.value).toLocaleString(),
color: '#475569',
fontSize: 11
},
labelLayout: {
hideOverlap: true, // drop labels that would collide
moveOverlap: 'shiftY', // or nudge them instead of hiding
draggable: false,
// align: 'right'
},
emphasis: { label: { show: true, fontWeight: 'bold' } }
}, {
type: 'line',
label: { show: true, position: 'top', distance: 8 },
labelLine: { show: true, length: 8, length2: 8, lineStyle: { color: '#94a3b8' } }
}]
};
// A pie's labels are a different problem: put them outside with leader lines.
const pie = {
series: [{
type: 'pie',
radius: ['45%', '70%'],
label: { position: 'outside', formatter: '{b}
{d}%' },
labelLine: { show: true, length: 12, length2: 12 },
labelLayout: { hideOverlap: true },
avoidLabelOverlap: true
}]
};labelLayoutapplies to all series at once and is the modern replacement for per-series label guesswork.hideOverlapsilently removes labels. If every label matters, usemoveOverlapor reduce the data instead — a chart that hides its own values is a chart that lies.- A legend with
selectedpre-set to hide a series is a good default for forecast or comparison lines that would otherwise dominate the view. legend.type: 'scroll'is essential once the series count exceeds the width; without it the legend simply overflows.
visualMap, markLine and markArea
const option = {
// Continuous colour mapping for a scatter: value drives colour.
visualMap: {
type: 'continuous',
min: 0,
max: 100,
dimension: 2, // which data dimension drives the colour
calculable: true, // draggable handles
orient: 'horizontal',
left: 'center',
bottom: 0,
text: ['High', 'Low'],
inRange: { color: ['#dbeafe', '#3b82f6', '#1e3a8a'], symbolSize: [8, 28] }
},
// Piecewise mapping with explicit buckets: better for discrete categories.
// visualMap: {
// type: 'piecewise',
// splitNumber: 5,
// pieces: [
// { min: 0, max: 60, label: 'Under target', color: '#f97316' },
// { min: 60, max: 90, label: 'Near target', color: '#eab308' },
// { min: 90, max: 100, label: 'At target', color: '#22c55e' }
// ]
// },
series: [{
type: 'line',
data: values,
markLine: {
silent: true,
symbol: 'none',
lineStyle: { type: 'dashed', color: '#ef4444' },
label: { formatter: 'Target {c}', position: 'insideEndTop' },
data: [{ yAxis: 5000 }]
},
markArea: {
silent: true,
itemStyle: { color: 'rgba(239, 68, 68, 0.08)' },
label: { show: true, position: 'insideTop', formatter: 'Incident window' },
data: [[{ xAxis: 'Mar' }, { xAxis: 'Apr' }]]
}
}]
};| Feature | Answers | Careful about |
|---|---|---|
visualMap continuous | "How does this value map to intensity?" | Needs a legend or the colour is unreadable |
visualMap piecewise | "Which bucket is this in?" | The buckets are a judgement; label them |
markLine | "Where is the threshold?" | One line per meaning; not a substitute for an axis |
markArea | "Which window does this cover?" | Only meaningful on a category or time axis |
markPoint | "What is the extreme value?" | Clutters a dense series |
| Colour-only encoding | Nothing for a colour-blind reader | Always pair colour with shape or text |
// A richer label formatter: rich text lets one label mix styles.
const richLabel = {
label: {
show: true,
formatter: '{name|{b}}
{value|{c} ms}',
rich: {
name: { color: '#64748b', fontSize: 11, lineHeight: 16 },
value: { color: '#0f172a', fontSize: 14, fontWeight: 600, lineHeight: 20 }
}
}
};
// Combining a tooltip formatter with a visualMap: read the mapped dimension
// in the tooltip so the reader can see why the colour is what it is.
const tooltipWithDimension = {
tooltip: {
trigger: 'item',
formatter: (p) => `${p.data[3]} - value ${p.data[2]}, weight ${p.data[4]}`
}
};FAQ
Why is my tooltip cut off at the chart edge?
Set
confine: true so it stays inside the container. If the chart itself sits in a clipped panel, add appendToBody: true so the tooltip element is not a child of the clipped box.How do I stop labels overlapping?
Use
labelLayout: { hideOverlap: true } for a quick fix, or moveOverlap: 'shiftY' when every label must remain visible. For rotated category labels, combine rotate with hideOverlap on the axis.Related
Coordinate systems, axes and scales Accessibility, export and printing
Last refreshed 2026-09-18.