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]

Reply via email to