apache / echarts

Apache ECharts is a powerful, interactive charting and data visualization library for browser
https://echarts.apache.org
Apache License 2.0
60.53k stars 19.61k forks source link

[Bug] series `sampling` breaks `updateAxisPointer` action #19814

Open mayfield opened 6 months ago

mayfield commented 6 months ago

Version

5.5.0

Link to Minimal Reproduction

https://jsfiddle.net/86yt7Lr1/

Steps to Reproduce

  1. Create chart with line type series and use sampling: 'lttb'.
  2. Have data size large enougth to trigger downsampling. i.e. ~ 2x pixel width
  3. Call chart.dispatchAction({type:'updateAxisPointer', dataIndex: <n>})
  4. Fails when dataIndex is "downsampled"

Current Behavior

The axispointer is not triggered some % of the time (depending on level of downsampling).

Expected Behavior

Work regardless of the use of sampling in the series options.

Environment

- OS: any
- Browser: any
- Framework: none

Any additional comments?

I think the issue might be in https://github.com/apache/echarts/blob/c576f0c395ef9af87461fe93bcaa4490d89a331a/src/data/DataStore.ts#L550-L565

There is an effort to binary search for the correct index but it only works if it finds an exact match. Otherwise it falls through and returns -1 to indicate the value was not found and nothing works. If this function returned the closest index instead, then it would work.

mayfield commented 6 months ago

Not a solution but I'm going to use this monkey patch to workaround the issue in my project until this gets a proper fix/release...

function monkeyPatchEchartsDownsampling(eChartsModule) {
    const dummy = eChartsModule.init(document.createElement('div'));
    dummy.setOption({xAxis: {}, yAxis: {}, series: {type: 'line'}});
    const model = dummy.getModel();
    const DataStore = model.getSeriesByIndex(0).getData().getStore().constructor;
    DataStore.prototype.indexOfRawIndex = function(rawIndex) {
        if (rawIndex >= this._rawCount || rawIndex < 0) {
            return -1;
        }
        if (!this._indices) {
            return rawIndex;
        }
        const indices = this._indices;
        const rawDataIndex = indices[rawIndex];
        if (rawDataIndex != null && rawDataIndex < this._count && rawDataIndex === rawIndex) {
            return rawIndex;
        }
        let left = 0;
        let right = this._count - 1;
        let mid = -1;
        while (left <= right) {
            mid = (left + right) / 2 | 0;
            if (indices[mid] < rawIndex) {
                left = mid + 1;
            } else if (indices[mid] > rawIndex) {
                right = mid - 1;
            } else {
                return mid;
            }
        }
        return mid;
    };
}

The function is a copy/paste of indexOfRawIndex but with the final bailout return value being the closet mid value instead of -1.

helgasoft commented 6 months ago

@mayfield, love those high-end code patches, adapted to Editor ❤️ Yet still scratching my head about the use-case...

mayfield commented 6 months ago

@helgasoft use-case is programatically triggering an axis pointer. I use it to link two disconnected charts that use different data sources and x axis types, but it really pertains to any use of DataStore.prototype.indexOfRawIndex when sampling is used (and data size exceeds viewport size).

https://github.com/apache/echarts/assets/139316/d9ebf9ae-06db-428a-9526-24911583181d

EDIT: Video is with my patch. Without it, the pointers are not linked properly and often just disappear.