This is an automated email from the ASF dual-hosted git repository. susiwen8 pushed a commit to branch codex/fix-21512-datazoom-sampling in repository https://gitbox.apache.org/repos/asf/echarts.git
commit 11ba3e300d191deb1c202392bd6b8f4dec9135da Author: susiwen8 <[email protected]> AuthorDate: Sat May 2 23:38:57 2026 +0800 Preserve line sampling detail inside dataZoom windows Line sampling previously used the processed series count as its sampling base. When dataZoom used filterMode none or empty, that count still represented the full dataset, so a narrow zoom could remain downsampled as if the whole dataset were visible. This bases the sampling rate on points contained by the current base-axis extent while keeping the no-sampling path out of the processor. The regression HTML case compares filter, none, empty, and raw modes around a narrow zoom window. Constraint: dataZoom filterMode none and empty intentionally keep full SeriesData rather than filtering rows before sampling Rejected: Change dataZoom filtering semantics for none/empty | would break the documented promise that data is not filtered Confidence: high Scope-risk: narrow Directive: Sampling rate should follow the visible base-axis extent, not only the post-filter SeriesData count Tested: npm run checktype -- --pretty false Tested: npm run lint -- --quiet src/processor/dataSample.ts Tested: test/line-sampling-dataZoom-filterMode.html via Chrome, PASS with none/empty counts matching raw Not-tested: Full visual regression suite --- src/processor/dataSample.ts | 17 ++- test/line-sampling-dataZoom-filterMode.html | 215 ++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+), 2 deletions(-) diff --git a/src/processor/dataSample.ts b/src/processor/dataSample.ts index 3c19572e9..e23e1d32b 100644 --- a/src/processor/dataSample.ts +++ b/src/processor/dataSample.ts @@ -21,6 +21,8 @@ import { StageHandler, SeriesOption, SeriesSamplingOptionMixin } from '../util/t import { Dictionary } from 'zrender/src/core/types'; import SeriesModel from '../model/Series'; import { isFunction, isString } from 'zrender/src/core/util'; +import type SeriesData from '../data/SeriesData'; +import type Axis from '../coord/Axis'; type Sampler = (frame: ArrayLike<number>) => number; @@ -72,6 +74,16 @@ const indexSampler = function (frame: ArrayLike<number>) { return Math.round(frame.length / 2); }; +function countDataInAxisExtent(data: SeriesData, baseAxis: Axis, baseDim: string) { + let count = 0; + data.each(baseDim, function (value: number) { + if (baseAxis.containData(value)) { + count++; + } + }); + return count; +} + export default function dataSample(seriesType: string): StageHandler { return { @@ -86,14 +98,15 @@ export default function dataSample(seriesType: string): StageHandler { const coordSys = seriesModel.coordinateSystem; const count = data.count(); // Only cartesian2d support down sampling. Disable it when there is few data. - if (count > 10 && coordSys.type === 'cartesian2d' && sampling) { + if (count > 10 && coordSys.type === 'cartesian2d' && sampling && sampling !== 'none') { const baseAxis = coordSys.getBaseAxis(); const valueAxis = coordSys.getOtherAxis(baseAxis); const extent = baseAxis.getExtent(); const dpr = api.getDevicePixelRatio(); // Coordinste system has been resized const size = Math.abs(extent[1] - extent[0]) * (dpr || 1); - const rate = Math.round(count / size); + const dataCount = countDataInAxisExtent(data, baseAxis, data.mapDimension(baseAxis.dim)); + const rate = Math.round(dataCount / size); if (isFinite(rate) && rate > 1) { if (sampling === 'lttb') { diff --git a/test/line-sampling-dataZoom-filterMode.html b/test/line-sampling-dataZoom-filterMode.html new file mode 100644 index 000000000..4bd316e89 --- /dev/null +++ b/test/line-sampling-dataZoom-filterMode.html @@ -0,0 +1,215 @@ +<!DOCTYPE html> +<!-- +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +--> + +<html> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="icon" href="data:,"> + <script src="lib/simpleRequire.js"></script> + <script src="lib/config.js"></script> + <script> + (function () { + var echartsPath = new URLSearchParams(location.search).get('__ECHARTS_PATH__'); + if (echartsPath && typeof require !== 'undefined') { + require.config({ + paths: { + echarts: echartsPath + } + }); + } + })(); + </script> + <script src="lib/testHelper.js"></script> + <link rel="stylesheet" href="lib/reset.css" /> + </head> + <body> + <style> + body { + font-family: sans-serif; + padding: 16px; + } + h1 { + font-size: 18px; + margin: 0 0 12px; + } + #status { + display: inline-block; + margin-bottom: 14px; + padding: 6px 10px; + border-radius: 3px; + background: #fee; + color: #a40000; + font-size: 13px; + font-weight: bold; + } + #status.pass { + background: #e8f6ed; + color: #176a32; + } + .grid { + display: grid; + grid-template-columns: repeat(2, 580px); + gap: 16px; + align-items: start; + } + .panel { + border: 1px solid #ddd; + padding: 10px; + } + .panel h2 { + font-size: 14px; + margin: 0 0 8px; + } + .chart { + width: 560px; + height: 260px; + } + .count { + margin-top: 6px; + color: #555; + font-size: 12px; + } + </style> + + <h1>Issue #21512: dataZoom filterMode none/empty should not lock line sampling to full data extent</h1> + <div id="status">Running</div> + <div class="grid"> + <div class="panel"> + <h2>filterMode: filter, sampling: lttb</h2> + <div id="chart-filter" class="chart"></div> + <div id="count-filter" class="count"></div> + </div> + <div class="panel"> + <h2>filterMode: none, sampling: lttb</h2> + <div id="chart-none" class="chart"></div> + <div id="count-none" class="count"></div> + </div> + <div class="panel"> + <h2>filterMode: empty, sampling: lttb</h2> + <div id="chart-empty" class="chart"></div> + <div id="count-empty" class="count"></div> + </div> + <div class="panel"> + <h2>filterMode: none, sampling: none</h2> + <div id="chart-raw" class="chart"></div> + <div id="count-raw" class="count"></div> + </div> + </div> + + <script> + require(['echarts'], function (echarts) { + var rawCount = 5000; + var zoomStart = 2400; + var zoomEnd = 2499; + var data = []; + + for (var i = 0; i < rawCount; i++) { + var detail = i >= zoomStart && i <= zoomEnd + ? (i % 2 ? 0.95 : -0.95) + : Math.sin(i / 23) * 0.15; + data.push([i, detail + Math.sin(i / 7) * 0.08]); + } + + function makeOption(filterMode, sampling) { + return { + animation: false, + grid: { + top: 16, + right: 20, + bottom: 54, + left: 45 + }, + dataZoom: [ + { + id: 'zoom', + type: 'slider', + filterMode: filterMode, + startValue: zoomStart, + endValue: zoomEnd, + height: 22, + bottom: 14 + } + ], + xAxis: { + type: 'value', + min: 'dataMin', + max: 'dataMax' + }, + yAxis: { + type: 'value', + min: -1.2, + max: 1.2 + }, + series: [ + { + type: 'line', + showSymbol: false, + sampling: sampling, + data: data, + lineStyle: { + width: 1 + } + } + ] + }; + } + + function render(domId, countId, filterMode, sampling) { + var chart = echarts.init(document.getElementById(domId), null, { + renderer: 'canvas' + }); + chart.setOption(makeOption(filterMode, sampling)); + var count = chart.getModel().getSeriesByIndex(0).getData().count(); + document.getElementById(countId).innerHTML = 'processed data count: ' + count; + return { + chart: chart, + count: count + }; + } + + var results = { + filter: render('chart-filter', 'count-filter', 'filter', 'lttb').count, + none: render('chart-none', 'count-none', 'none', 'lttb').count, + empty: render('chart-empty', 'count-empty', 'empty', 'lttb').count, + raw: render('chart-raw', 'count-raw', 'none', 'none').count + }; + + var visibleCount = zoomEnd - zoomStart + 1; + var pass = results.filter === visibleCount + && results.none === rawCount + && results.empty === rawCount + && results.raw === rawCount; + var statusEl = document.getElementById('status'); + statusEl.className = pass ? 'pass' : ''; + statusEl.innerHTML = (pass ? 'PASS' : 'FAIL') + + ' - visible window has ' + visibleCount + + ' points, so none/empty+lttb should preserve raw detail in this zoom.'; + + window.__issue21512Result = { + pass: pass, + rawCount: rawCount, + visibleCount: visibleCount, + counts: results + }; + }); + </script> + </body> +</html> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
