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>`;
    }
  }
};
SettingEffectUse when
trigger: 'axis'One tooltip for all series at an x positionTime series and bar charts
trigger: 'item'One tooltip per markScatter, pie, map
confine: trueKeeps it inside the containerSmall containers and dashboards
appendToBody: trueRenders outside the chart elementThe chart sits in a scrolling or clipped panel
enterable: trueThe pointer can move onto the tooltipOnly when it contains a link or button
valueFormatterFormats values without a full formatterSimple unit suffixes
renderMode: 'html' | 'richText'HTML or canvas textrichText 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
  }]
};
  • labelLayout applies to all series at once and is the modern replacement for per-series label guesswork.
  • hideOverlap silently removes labels. If every label matters, use moveOverlap or reduce the data instead — a chart that hides its own values is a chart that lies.
  • A legend with selected pre-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' }]]
    }
  }]
};
FeatureAnswersCareful 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 encodingNothing for a colour-blind readerAlways 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.

Coordinate systems, axes and scales Accessibility, export and printing

Last refreshed 2026-09-18.