Version
6.1.0 (also on master @ 30076ae)
Link to Minimal Reproduction
https://github.com/JoshuaKGoldberg/repros/tree/echarts-tooltip-stale-series-index
Steps to Reproduce
npm install
npm run build
npm run repro:headless
Or npm run repro:browser to drive it by hand. The repro is:
echarts.init a chart with tooltip: {trigger: 'axis'}, a category xAxis, and 5 line series (animation: false).
- Hover the middle of the chart so the axis tooltip shows.
- Without moving the pointer off the chart,
setOption the same option with only 1 series, in merge mode:
chart.setOption(optionWithOneSeries, {
lazyUpdate: false,
notMerge: false,
replaceMerge: ['series', 'xAxis', 'yAxis'],
});
Current Behavior
An uncaught TypeError is thrown a tick later:
Uncaught TypeError: Cannot read properties of undefined (reading 'getDataParams')
at TooltipView._showAxisTooltip
at TooltipView._tryShow
at TooltipView.manuallyShowTip
at <setTimeout callback in TooltipView._keepShow>
Poking deeper:
TooltipView caches the last hovered pointer state in _lastX / _lastY / _lastDataByCoordSys, and _lastDataByCoordSys holds raw seriesIndex numbers.
- Every
setOption runs TooltipView.render() → _keepShow(), which, when _lastX/_lastY are set and triggerOn is neither 'none' nor 'click', schedules setTimeout(() => self.manuallyShowTip(..., {x: _lastX, y: _lastY, dataByCoordSys: _lastDataByCoordSys})) to re-show the tooltip after the update.
- The new option has fewer series than those cached indices refer to.
GlobalModel.getSeriesByIndex is just this._componentsMap.get('series')[seriesIndex], so an out-of-range index yields undefined, and _showAxisTooltip dereferences it unguarded:
each(axisItem.seriesDataIndices, function (idxItem) {
const series = ecModel.getSeriesByIndex(idxItem.seriesIndex);
const dataIndex = idxItem.dataIndexInside;
const cbParams = series.getDataParams(dataIndex) as TooltipCallbackDataParams; // series is undefined
This only reproduces under merge mode. With setOption(option, true) the tooltip component view is disposed and recreated, so _lastX is cleared and nothing throws. It needs the combination the repro uses: the tooltip component is merged (the view survives with its stale cache) while series is replaced (the cached indices go out of range).
It is not fatal but:
- The stale tooltip stays on screen listing all 5 removed series with their old values.
- Every subsequent
setOption throws again while the pointer sits still (3 updates → 3 errors).
- Moving the pointer recovers it: the tooltip re-renders with the remaining series and no further errors occur.
Because the throw comes from a setTimeout, it can't be caught by application code.
It lands as an uncaught error, so any app with error reporting sees it as a user-visible crash even though the chart survives.
That's how I landed here!
Expected Behavior
The re-shown tooltip skips series that no longer exist instead of throwing.
ECharts already applies exactly this guard at the sibling call site that builds axis-pointer label params, src/component/axisPointer/viewHelper.ts#L177-L180:
zrUtil.each(seriesDataIndices, function (idxItem) {
const series = ecModel.getSeriesByIndex(idxItem.seriesIndex);
const dataIndex = idxItem.dataIndexInside;
const dataParams = series && series.getDataParams(dataIndex);
dataParams && params.seriesData.push(dataParams);
});
Environment
- OS: macOS 15
- Browser: Chromium 151 (also reproduces in headless Chromium via Playwright)
- Framework: none (plain
echarts.init)
Any additional comments?
A few lines above the crash, TooltipView.ts#L556-L562 dereferences axisModel before its own null check:
const axisModel = ecModel.getComponent(axisItem.axisDim + 'Axis', axisItem.axisIndex) as AxisBaseModel;
const axisValue = axisItem.value;
const axis = axisModel.axis; // throws if axisModel is undefined
const axisValueParsed = axis.scale.parse(axisValue);
if (!axisModel || axisValue == null) { // too late
return;
}
The same staleness that removes a series can remove an axis, so getComponent can return undefined here.
Related but distinct:
Version
6.1.0 (also on master @ 30076ae)
Link to Minimal Reproduction
https://github.com/JoshuaKGoldberg/repros/tree/echarts-tooltip-stale-series-index
Steps to Reproduce
Or
npm run repro:browserto drive it by hand. The repro is:echarts.inita chart withtooltip: {trigger: 'axis'}, a categoryxAxis, and 5lineseries (animation: false).setOptionthe same option with only 1 series, in merge mode:Current Behavior
An uncaught
TypeErroris thrown a tick later:Poking deeper:
TooltipViewcaches the last hovered pointer state in_lastX/_lastY/_lastDataByCoordSys, and_lastDataByCoordSysholds rawseriesIndexnumbers.setOptionrunsTooltipView.render()→_keepShow(), which, when_lastX/_lastYare set andtriggerOnis neither'none'nor'click', schedulessetTimeout(() => self.manuallyShowTip(..., {x: _lastX, y: _lastY, dataByCoordSys: _lastDataByCoordSys}))to re-show the tooltip after the update.GlobalModel.getSeriesByIndexis justthis._componentsMap.get('series')[seriesIndex], so an out-of-range index yieldsundefined, and_showAxisTooltipdereferences it unguarded:This only reproduces under merge mode. With
setOption(option, true)the tooltip component view is disposed and recreated, so_lastXis cleared and nothing throws. It needs the combination the repro uses: thetooltipcomponent is merged (the view survives with its stale cache) whileseriesis replaced (the cached indices go out of range).It is not fatal but:
setOptionthrows again while the pointer sits still (3 updates → 3 errors).Because the throw comes from a
setTimeout, it can't be caught by application code.It lands as an uncaught error, so any app with error reporting sees it as a user-visible crash even though the chart survives.
That's how I landed here!
Expected Behavior
The re-shown tooltip skips series that no longer exist instead of throwing.
ECharts already applies exactly this guard at the sibling call site that builds axis-pointer label params,
src/component/axisPointer/viewHelper.ts#L177-L180:Environment
echarts.init)Any additional comments?
A few lines above the crash,
TooltipView.ts#L556-L562dereferencesaxisModelbefore its own null check:The same staleness that removes a series can remove an axis, so
getComponentcan returnundefinedhere.Related but distinct:
_keepShowre-show path, but a null tooltip DOM on the item tooltip pathgetDataParamscrashing fromfindEventDispatchermousemove on a disposed series