This is an automated email from the ASF dual-hosted git repository.

wangzx pushed a commit to branch fix/scatter-large-stuck
in repository https://gitbox.apache.org/repos/asf/echarts.git

commit bbe3591a8a946b62a16e2ee6e254d8d1ed0c2741
Author: plainheart <[email protected]>
AuthorDate: Fri Dec 19 03:18:24 2025 +0800

    fix(scatter): fix jitter layout does not support progressive rendering and 
causes chart to stuck and potential NPE
---
 src/chart/scatter/install.ts             |   2 +-
 src/chart/scatter/jitterLayout.ts        | 110 +++++++++++++-------
 src/util/jitter.ts                       |  14 ++-
 test/runTest/actions/__meta__.json       |   1 +
 test/runTest/actions/scatter-jitter.json |   1 +
 test/scatter-jitter.html                 | 173 +++++++++++++++++++++++++++++++
 6 files changed, 253 insertions(+), 48 deletions(-)

diff --git a/src/chart/scatter/install.ts b/src/chart/scatter/install.ts
index e4257b582..308fc3103 100644
--- a/src/chart/scatter/install.ts
+++ b/src/chart/scatter/install.ts
@@ -36,5 +36,5 @@ export function install(registers: 
EChartsExtensionInstallRegisters) {
 }
 
 export function installScatterJitter(registers: 
EChartsExtensionInstallRegisters) {
-    registers.registerLayout(registers.PRIORITY.VISUAL.POST_CHART_LAYOUT, 
jitterLayout);
+    registers.registerLayout(registers.PRIORITY.VISUAL.POST_CHART_LAYOUT, 
jitterLayout());
 }
diff --git a/src/chart/scatter/jitterLayout.ts 
b/src/chart/scatter/jitterLayout.ts
index b3f88284b..753cda5e9 100644
--- a/src/chart/scatter/jitterLayout.ts
+++ b/src/chart/scatter/jitterLayout.ts
@@ -17,53 +17,85 @@
 * under the License.
 */
 
-import type GlobalModel from '../../model/Global';
 import type ScatterSeriesModel from './ScatterSeries';
 import { needFixJitter, fixJitter } from '../../util/jitter';
 import type SingleAxis from '../../coord/single/SingleAxis';
 import type Axis2D from '../../coord/cartesian/Axis2D';
+import type SeriesData from '../../data/SeriesData';
+import type { StageHandler } from '../../util/types';
+import createRenderPlanner from '../helper/createRenderPlanner';
 
-export default function jitterLayout(ecModel: GlobalModel) {
-    ecModel.eachSeriesByType('scatter', function (seriesModel: 
ScatterSeriesModel) {
-        const coordSys = seriesModel.coordinateSystem;
-        if (coordSys
-            && (coordSys.type === 'cartesian2d' || coordSys.type === 
'single')) {
-            const baseAxis = coordSys.getBaseAxis ? coordSys.getBaseAxis() : 
null;
+export default function jitterLayout(): StageHandler {
+    return {
+        seriesType: 'scatter',
+
+        plan: createRenderPlanner(),
+
+        reset(seriesModel: ScatterSeriesModel) {
+            const coordSys = seriesModel.coordinateSystem;
+            if (!coordSys || (coordSys.type !== 'cartesian2d' && coordSys.type 
!== 'single')) {
+                return;
+            }
+            const baseAxis = coordSys.getBaseAxis && coordSys.getBaseAxis();
             const hasJitter = baseAxis && needFixJitter(seriesModel, baseAxis);
+            if (!hasJitter) {
+                return;
+            }
 
-            if (hasJitter) {
-                const data = seriesModel.getData();
-                data.each(function (idx) {
-                    const dim = baseAxis.dim;
-                    const orient = (baseAxis as SingleAxis).orient;
-                    const isSingleY = orient === 'horizontal' && baseAxis.type 
!== 'category'
-                        || orient === 'vertical' && baseAxis.type === 
'category';
-                    const layout = data.getItemLayout(idx);
-                    const rawSize = data.getItemVisual(idx, 'symbolSize');
-                    const size = rawSize instanceof Array ? (rawSize[1] + 
rawSize[0]) / 2 : rawSize;
+            const dim = baseAxis.dim;
+            const orient = (baseAxis as SingleAxis).orient;
+            const isSingleY = orient === 'horizontal' && baseAxis.type !== 
'category'
+                || orient === 'vertical' && baseAxis.type === 'category';
 
-                    if (dim === 'y' || dim === 'single' && isSingleY) {
-                        // x is fixed, and y is floating
-                        const jittered = fixJitter(
-                            baseAxis as Axis2D | SingleAxis,
-                            layout[0],
-                            layout[1],
-                            size / 2
-                        );
-                        data.setItemLayout(idx, [layout[0], jittered]);
-                    }
-                    else if (dim === 'x' || dim === 'single' && !isSingleY) {
-                        // y is fixed, and x is floating
-                        const jittered = fixJitter(
-                            baseAxis as Axis2D | SingleAxis,
-                            layout[1],
-                            layout[0],
-                            size / 2
-                        );
-                        data.setItemLayout(idx, [jittered, layout[1]]);
+            return {
+                progress(params, data: SeriesData): void {
+                    const points = data.getLayout('points') as number[] | 
Float32Array;
+                    const chunkPointCount = (params.end - params.start) * 2;
+                    const hasPoints = !!points && points.length >= 
chunkPointCount;
+
+                    for (let i = params.start; i < params.end; i++) {
+                        const offset = hasPoints ? (i - params.start) * 2 : -1;
+                        const layout = hasPoints ? [points[offset], 
points[offset + 1]] : data.getItemLayout(i);
+                        if (!layout) {
+                            continue;
+                        }
+
+                        const rawSize = data.getItemVisual(i, 'symbolSize');
+                        const size = rawSize instanceof Array ? (rawSize[1] + 
rawSize[0]) / 2 : rawSize;
+
+                        if (dim === 'y' || (dim === 'single' && isSingleY)) {
+                            // x is fixed, and y is floating
+                            const jittered = fixJitter(
+                                baseAxis as Axis2D | SingleAxis,
+                                layout[0],
+                                layout[1],
+                                size / 2
+                            );
+                            if (hasPoints) {
+                                points[offset + 1] = jittered;
+                            }
+                            else {
+                                data.setItemLayout(i, [layout[0], jittered]);
+                            }
+                        }
+                        else if (dim === 'x' || (dim === 'single' && 
!isSingleY)) {
+                            // y is fixed, and x is floating
+                            const jittered = fixJitter(
+                                baseAxis as Axis2D | SingleAxis,
+                                layout[1],
+                                layout[0],
+                                size / 2
+                            );
+                            if (hasPoints) {
+                                points[offset] = jittered;
+                            }
+                            else {
+                                data.setItemLayout(i, [jittered, layout[1]]);
+                            }
+                        }
                     }
-                });
-            }
+                }
+            };
         }
-    });
+    };
 }
diff --git a/src/util/jitter.ts b/src/util/jitter.ts
index 42690c1dd..567a35968 100644
--- a/src/util/jitter.ts
+++ b/src/util/jitter.ts
@@ -67,21 +67,19 @@ export function fixJitter(
     }
     const axisModel = fixedAxis.model as AxisBaseModel;
     const jitter = axisModel.get('jitter');
+    if (!(jitter > 0)) {
+        return floatCoord;
+    }
     const jitterOverlap = axisModel.get('jitterOverlap');
     const jitterMargin = axisModel.get('jitterMargin') || 0;
     // Get band width to limit jitter range
     const bandWidth = fixedAxis.scale.type === 'ordinal'
         ? fixedAxis.getBandWidth()
         : null;
-    if (jitter > 0) {
-        if (jitterOverlap) {
-            return fixJitterIgnoreOverlaps(floatCoord, jitter, bandWidth, 
radius);
-        }
-        else {
-            return fixJitterAvoidOverlaps(fixedAxis, fixedCoord, floatCoord, 
radius, jitter, jitterMargin);
-        }
+    if (jitterOverlap) {
+        return fixJitterIgnoreOverlaps(floatCoord, jitter, bandWidth, radius);
     }
-    return floatCoord;
+    return fixJitterAvoidOverlaps(fixedAxis, fixedCoord, floatCoord, radius, 
jitter, jitterMargin);
 }
 
 function fixJitterIgnoreOverlaps(
diff --git a/test/runTest/actions/__meta__.json 
b/test/runTest/actions/__meta__.json
index c78118d8d..f75709f52 100644
--- a/test/runTest/actions/__meta__.json
+++ b/test/runTest/actions/__meta__.json
@@ -191,6 +191,7 @@
   "sankey-jump": 1,
   "sankey-level": 1,
   "scale-extreme-number": 2,
+  "scatter-jitter": 1,
   "scatter-random-stream-fix-axis": 1,
   "scatter-single-axis": 3,
   "scatterMatrix": 3,
diff --git a/test/runTest/actions/scatter-jitter.json 
b/test/runTest/actions/scatter-jitter.json
new file mode 100644
index 000000000..8b06e0f1b
--- /dev/null
+++ b/test/runTest/actions/scatter-jitter.json
@@ -0,0 +1 @@
+[{"name":"Action 
1","ops":[{"type":"mousemove","time":393,"x":92,"y":93},{"type":"mousemove","time":597,"x":99,"y":121},{"type":"mousedown","time":757,"x":101,"y":126},{"type":"mousemove","time":813,"x":101,"y":126},{"type":"mouseup","time":865,"x":101,"y":126},{"time":866,"delay":5000,"type":"screenshot-auto"}],"scrollY":6000,"scrollX":0,"timestamp":1766084422908}]
diff --git a/test/scatter-jitter.html b/test/scatter-jitter.html
index 01c5bb31e..e1355dc73 100644
--- a/test/scatter-jitter.html
+++ b/test/scatter-jitter.html
@@ -56,6 +56,8 @@ under the License.
         <div id="main3-boundary-test"></div>
         <div id="main3-boundary-test-no-overlap"></div>
 
+        <div id="main4"></div>
+        <div id="main5"></div>
 
 
         <script>
@@ -708,6 +710,177 @@ under the License.
                     });
                 });
                 </script>
+
+            <script>
+                require([
+                    'echarts'
+                ], function (echarts) {
+                    function genData(len, offset) {
+                        let arr = new Float32Array(len * 2);
+                        let off = 0;
+                        for (let i = 0; i < len; i++) {
+                            let x = +Math.random() * 10;
+                            let y = +Math.sin(x) - x * (len % 2 ? 0.1 : -0.1) 
* Math.random() + (offset || 0) / 10;
+                            arr[off++] = x;
+                            arr[off++] = y;
+                        }
+                        return arr;
+                    }
+                    const data1 = genData(5e5);
+                    const data2 = genData(5e5, 10);
+                    var option = {
+                        title: {
+                            text:
+                            echarts.format.addCommas(data1.length / 2 + 
data2.length / 2) + ' Points'
+                        },
+                        tooltip: {},
+                        xAxis: {},
+                        yAxis: {},
+                        dataZoom: [
+                            {
+                                type: 'inside'
+                            },
+                            {
+                                type: 'slider'
+                            }
+                        ],
+                        animation: true,
+                        series: [
+                            {
+                                name: 'A',
+                                type: 'scatter',
+                                data: data1,
+                                dimensions: ['x', 'y'],
+                                symbolSize: 3,
+                                itemStyle: {
+                                    opacity: 0.4
+                                },
+                                large: true
+                            },
+                            {
+                                name: 'B',
+                                type: 'scatter',
+                                data: data2,
+                                dimensions: ['x', 'y'],
+                                symbolSize: 3,
+                                itemStyle: {
+                                    opacity: 0.4
+                                },
+                                large: true
+                            }
+                        ]
+                    };
+
+                    // NOTE: Please manually test this case, as the chart may 
get stuck and cause the VRT to fail.
+                    var chart = testHelper.create(echarts, 'main4', {
+                        title: [
+                            'Large Dataset **without** Jittering',
+                            'The chart **SHOULD NOT** get stuck',
+                            '(Manual Test Only)'
+                        ],
+                        buttons: [
+                            {
+                                text: 'Click to load chart',
+                                onclick: function () {
+                                    const startTime = performance.now();
+                                    const elapsedTimeTip = 
document.createElement('div');
+                                    elapsedTimeTip.style.position = 'absolute';
+                                    elapsedTimeTip.style.top = '0';
+                                    elapsedTimeTip.style.left = '0';
+                                    elapsedTimeTip.style.backgroundColor = 
'rgba(0,0,0,0.7)';
+                                    elapsedTimeTip.style.color = '#fff';
+                                    elapsedTimeTip.style.padding = '4px 8px';
+                                    elapsedTimeTip.style.zIndex = '9999';
+                                    elapsedTimeTip.innerText = 'Rendering...';
+                                    chart.getDom().appendChild(elapsedTimeTip);
+                                    chart.setOption(option);
+                                    const onFinished = () => {
+                                        const elapsed = performance.now() - 
startTime;
+
+                                        elapsedTimeTip.innerText = `Rendered 
in ${elapsed.toFixed(2)} ms`;
+                                        chart.off('finished', onFinished);
+                                    }
+                                    chart.on('finished', onFinished);
+                                }
+                            }
+                        ]
+                    });
+                });
+            </script>
+
+            <script>
+                require([
+                    'echarts'
+                ], function (echarts) {
+                    function genData(len, offset) {
+                        let arr = new Float32Array(len * 2);
+                        let off = 0;
+                        for (let i = 0; i < len; i++) {
+                            let x = +Math.random() * 10;
+                            let y = +Math.sin(x) - x * (len % 2 ? 0.1 : -0.1) 
* Math.random() + (offset || 0) / 10;
+                            arr[off++] = x;
+                            arr[off++] = y;
+                        }
+                        return arr;
+                    }
+                    const data1 = genData(5e5);
+                    const data2 = genData(5e5, 10);
+                    var option = {
+                        title: {
+                            text:
+                            echarts.format.addCommas(data1.length / 2 + 
data2.length / 2) + ' Points'
+                        },
+                        tooltip: {},
+                        xAxis: {
+                            jitter: 1.5,
+                            type: 'category',
+                            data: Array.from({ length: 5 }, (_, idx) => idx)
+                        },
+                        yAxis: {},
+                        dataZoom: [
+                            {
+                                type: 'inside'
+                            },
+                            {
+                                type: 'slider'
+                            }
+                        ],
+                        animation: false,
+                        series: [
+                            {
+                                name: 'A',
+                                type: 'scatter',
+                                data: data1,
+                                dimensions: ['x', 'y'],
+                                symbolSize: 3,
+                                itemStyle: {
+                                    opacity: 0.4
+                                },
+                                large: true
+                            },
+                            {
+                                name: 'B',
+                                type: 'scatter',
+                                data: data2,
+                                dimensions: ['x', 'y'],
+                                symbolSize: 3,
+                                itemStyle: {
+                                    opacity: 0.4
+                                },
+                                large: true
+                            }
+                        ]
+                    };
+
+                    var chart = testHelper.create(echarts, 'main5', {
+                        title: [
+                            'Large Dataset **with** Jittering',
+                            'The chart **SHOULD NOT** throw errors'
+                        ],
+                        option: option
+                    });
+                });
+            </script>
     </body>
 </html>
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to