JoshuaKGoldberg opened a new issue, #21732: URL: https://github.com/apache/echarts/issues/21732
### 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 ```shell npm install npm run build npm run repro:headless ``` Or `npm run repro:browser` to drive it by hand. The repro is: 1. `echarts.init` a chart with `tooltip: {trigger: 'axis'}`, a category `xAxis`, and 5 `line` series (`animation: false`). 2. Hover the middle of the chart so the axis tooltip shows. 3. Without moving the pointer off the chart, `setOption` the same option with only 1 series, in merge mode: ```js chart.setOption(optionWithOneSeries, { lazyUpdate: false, notMerge: false, replaceMerge: ['series', 'xAxis', 'yAxis'], }); ``` ### Current Behavior An uncaught `TypeError` is thrown a tick later: ```plaintext 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: 1. `TooltipView` caches the last hovered pointer state in `_lastX` / `_lastY` / `_lastDataByCoordSys`, and `_lastDataByCoordSys` holds raw `seriesIndex` numbers. 2. Every `setOption` runs `TooltipView.render()` → [`_keepShow()`](https://github.com/apache/echarts/blob/6.1.0/src/component/tooltip/TooltipView.ts#L240-L274), 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. 3. The new option has fewer series than those cached indices refer to. [`GlobalModel.getSeriesByIndex`](https://github.com/apache/echarts/blob/6.1.0/src/model/Global.ts#L764-L766) is just `this._componentsMap.get('series')[seriesIndex]`, so an out-of-range index yields `undefined`, and [`_showAxisTooltip`](https://github.com/apache/echarts/blob/6.1.0/src/component/tooltip/TooltipView.ts#L579-L582) dereferences it unguarded: ```ts 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`](https://github.com/apache/echarts/blob/6.1.0/src/component/axisPointer/viewHelper.ts#L177-L180): ```ts 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`](https://github.com/apache/echarts/blob/6.1.0/src/component/tooltip/TooltipView.ts#L556-L562) dereferences `axisModel` *before* its own null check: ```ts 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: * #19827: same `_keepShow` re-show path, but a null tooltip DOM on the *item* tooltip path * #21535: `getDataParams` crashing from `findEventDispatcher` mousemove on a disposed series -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
