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

sushuang pushed a commit to branch PR/plainheart_fix/alignTicks-precision
in repository https://gitbox.apache.org/repos/asf/echarts.git

commit d47ea4ad7bea2aa8f595dcebf1454f1512dfeb98
Author: 100pah <[email protected]>
AuthorDate: Thu Feb 19 21:04:10 2026 +0800

    fix(dataZoom): Do not display values outside of effective extent.
---
 src/component/dataZoom/AxisProxy.ts      | 50 +++++++++++++-------
 src/component/dataZoom/SliderZoomView.ts | 79 +++++++++++++++++++-------------
 src/coord/scaleRawExtentInfo.ts          | 21 +++++++--
 test/bar-overflow-plot2.html             |  1 +
 4 files changed, 98 insertions(+), 53 deletions(-)

diff --git a/src/component/dataZoom/AxisProxy.ts 
b/src/component/dataZoom/AxisProxy.ts
index 3bcf7a23a..fee554bcc 100644
--- a/src/component/dataZoom/AxisProxy.ts
+++ b/src/component/dataZoom/AxisProxy.ts
@@ -34,6 +34,7 @@ import { SINGLE_REFERRING } from '../../util/model';
 import { isOrdinalScale, isTimeScale } from '../../scale/helper';
 import {
     AXIS_EXTENT_INFO_BUILD_FROM_DATA_ZOOM, scaleRawExtentInfoReallyCreate,
+    ScaleRawExtentResultForZoom,
 } from '../../coord/scaleRawExtentInfo';
 import { suppressOnAxisZero } from '../../coord/axisHelper';
 
@@ -45,20 +46,20 @@ interface MinMaxSpan {
     maxValueSpan: number
 }
 
-interface AxisProxyWindow {
-    value: [number, number];
-    percent: [number, number];
+export interface AxisProxyWindow {
+    // NOTE: May include non-effective portion.
+    value: number[];
+    noZoomEffMM: ScaleRawExtentResultForZoom['noZoomEffMM'];
+    percent: number[];
     // Percent invert from "value window", which may be slightly different 
from "percent window" due to some
     // handling such as rounding. The difference may be magnified in cases 
like "alignTicks", so we use
     // `percentInverted` in these cases.
     // But we retain the original input percent in `percent` whenever 
possible, since they have been used in views.
-    percentInverted: [number, number];
+    percentInverted: number[];
     valuePrecision: number;
 }
 
 /**
- * NOTICE: Its lifetime is different from `Axis` instance. It is recreated in 
each run of "ec prepare".
- *
  * Operate single axis.
  * One axis can only operated by one axis operator.
  * Different dataZoomModels may be defined to operate the same axis.
@@ -69,12 +70,15 @@ class AxisProxy {
 
     ecModel: GlobalModel;
 
+    // NOTICE: The lifetime of `AxisProxy` instance is different from `Axis` 
instance.
+    // It is recreated in each run of "ec prepare".
+
     private _dimName: DataZoomAxisDimension;
     private _axisIndex: number;
 
     private _window: AxisProxyWindow;
 
-    private _dataExtent: number[];
+    private _extent: ScaleRawExtentResultForZoom;
 
     private _minMaxSpan: MinMaxSpan;
 
@@ -156,7 +160,7 @@ class AxisProxy {
             endValue?: number | string | Date
         }
     ): AxisProxyWindow {
-        const dataExtent = this._dataExtent;
+        const {noZoomMapMM: dataExtent, noZoomEffMM} = this._extent;
         const axis = this.getAxisModel().axis;
         const scale = axis.scale;
         const dataZoomModel = this._dataZoomModel;
@@ -167,13 +171,26 @@ class AxisProxy {
         let hasPropModeValue;
         const needRound = [false, false];
 
+        // NOTE:
+        //  The current percentage base calculation strategy:
+        //    - If the window boundary is NOT at 0% or 100%, boundary values 
are derived from the raw extent
+        //      (series data + axis.min/max; see 
`ScaleRawExtentInfo['makeForZoom']`). Any subsequent "nice"
+        //      expansion are excluded.
+        //    - If the window boundary is at 0% or 100%, the "nice"-expanded 
portion is included.
+        //  Pros:
+        //    - The effect may be preferable when users intend to quickly 
narrow down to data details,
+        //      especially when "nice strategy" excessively expands the extent.
+        //    - It simplifies the logic, otherwise, "nice strategy" would need 
to be applied twice (full window
+        //      + current window).
+        //  Cons:
+        //    - This strategy causes jitter when switching dataZoom to/from 
0%/100% (though generally acceptable).
+
         each(['start', 'end'] as const, function (prop, idx) {
             let boundPercent = opt[prop];
             let boundValue = opt[prop + 'Value' as 'startValue' | 'endValue'];
 
-            // Notice: dataZoom is based either on `percentProp` ('start', 
'end') or
-            // on `valueProp` ('startValue', 'endValue'). (They are based on 
the data extent
-            // but not min/max of axis, which will be calculated by data 
window then).
+            // NOTE: dataZoom is based either on `percentProp` ('start', 
'end') or
+            // on `valueProp` ('startValue', 'endValue').
             // The former one is suitable for cases that a dataZoom component 
controls multiple
             // axes with different unit or extent, and the latter one is 
suitable for accurate
             // zoom by pixel (e.g., in dataZoomSelect).
@@ -307,6 +324,7 @@ class AxisProxy {
 
         return {
             value: valueWindow,
+            noZoomEffMM: noZoomEffMM.slice(),
             percent: percentWindow,
             percentInverted: percentInvertedWindow,
             valuePrecision: precision,
@@ -318,7 +336,7 @@ class AxisProxy {
      * so it is recommended to be called in "process stage" but not "model init
      * stage".
      */
-    reset(dataZoomModel: DataZoomModel, alignToPercentInverted: [number, 
number] | NullUndefined) {
+    reset(dataZoomModel: DataZoomModel, alignToPercentInverted: number[] | 
NullUndefined) {
         if (!this.hostedBy(dataZoomModel)) {
             return;
         }
@@ -338,7 +356,7 @@ class AxisProxy {
         suppressOnAxisZero(axis, {dz: true});
 
         const rawExtentInfo = axis.scale.rawExtentInfo;
-        this._dataExtent = rawExtentInfo.makeForZoom();
+        this._extent = rawExtentInfo.makeForZoom();
 
         // `calculateDataWindow` uses min/maxSpan.
         this._updateMinMaxSpan();
@@ -441,7 +459,7 @@ class AxisProxy {
                     }
                     else {
                         const range: Dictionary<[number, number]> = {};
-                        range[dim] = valueWindow;
+                        range[dim] = valueWindow as [number, number];
 
                         // console.time('select');
                         seriesData.selectRange(range);
@@ -451,7 +469,7 @@ class AxisProxy {
             }
 
             each(dataDims, function (dim) {
-                seriesData.setApproximateExtent(valueWindow, dim);
+                seriesData.setApproximateExtent(valueWindow as [number, 
number], dim);
             });
         });
 
@@ -463,7 +481,7 @@ class AxisProxy {
     private _updateMinMaxSpan() {
         const minMaxSpan = this._minMaxSpan = {} as MinMaxSpan;
         const dataZoomModel = this._dataZoomModel;
-        const dataExtent = this._dataExtent;
+        const dataExtent = this._extent.noZoomMapMM;
 
         each(['min', 'max'], function (minMax) {
             let percentSpan = dataZoomModel.get(minMax + 'Span' as 'minSpan' | 
'maxSpan');
diff --git a/src/component/dataZoom/SliderZoomView.ts 
b/src/component/dataZoom/SliderZoomView.ts
index 9cc8a447f..ef8ddc4d2 100644
--- a/src/component/dataZoom/SliderZoomView.ts
+++ b/src/component/dataZoom/SliderZoomView.ts
@@ -22,7 +22,7 @@ import * as eventTool from 'zrender/src/core/event';
 import * as graphic from '../../util/graphic';
 import * as throttle from '../../util/throttle';
 import DataZoomView from './DataZoomView';
-import { linearMap, asc, parsePercent, round } from '../../util/number';
+import { linearMap, asc, parsePercent, round, mathMax, mathMin } from 
'../../util/number';
 import * as layout from '../../util/layout';
 import sliderMove from '../helper/sliderMove';
 import GlobalModel from '../../model/Global';
@@ -44,8 +44,11 @@ import Displayable from 'zrender/src/graphic/Displayable';
 import { createTextStyle } from '../../label/labelStyle';
 import SeriesData from '../../data/SeriesData';
 import tokens from '../../visual/tokens';
-import type AxisProxy from './AxisProxy';
 import { isOrdinalScale, isTimeScale } from '../../scale/helper';
+import { AxisProxyWindow } from './AxisProxy';
+import type Scale from '../../scale/Scale';
+import { SCALE_EXTENT_KIND_EFFECTIVE } from '../../scale/scaleMapper';
+
 
 const Rect = graphic.Rect;
 
@@ -827,11 +830,12 @@ class SliderZoomView extends DataZoomView {
 
         if (dataZoomModel.get('showDetail')) {
             const axisProxy = dataZoomModel.findRepresentativeAxisProxy();
+            const scale = axisProxy.getAxisModel().axis.scale;
 
             if (axisProxy) {
                 const range = this._range;
 
-                let dataInterval: [number, number];
+                let window: AxisProxyWindow;
                 if (nonRealtime) {
                     // See #4434, data and axis are not processed and reset 
yet in non-realtime mode.
                     let calcWinInput = {start: range[0], end: range[1]};
@@ -840,15 +844,15 @@ class SliderZoomView extends DataZoomView {
                         const alignToWindow = 
alignTo.calculateDataWindow(calcWinInput).percentInverted;
                         calcWinInput = {start: alignToWindow[0], end: 
alignToWindow[1]};
                     }
-                    dataInterval = 
axisProxy.calculateDataWindow(calcWinInput).value;
+                    window = axisProxy.calculateDataWindow(calcWinInput);
                 }
                 else {
-                    dataInterval = axisProxy.getWindow().value;
+                    window = axisProxy.getWindow();
                 }
 
                 labelTexts = [
-                    this._formatLabel(dataInterval[0], axisProxy),
-                    this._formatLabel(dataInterval[1], axisProxy)
+                    formatLabel(dataZoomModel, 0, window, scale),
+                    formatLabel(dataZoomModel, 1, window, scale)
                 ];
             }
         }
@@ -886,31 +890,6 @@ class SliderZoomView extends DataZoomView {
         }
     }
 
-    private _formatLabel(value: number, axisProxy: AxisProxy) {
-        const dataZoomModel = this.dataZoomModel;
-        const labelFormatter = dataZoomModel.get('labelFormatter');
-
-        let labelPrecision = dataZoomModel.get('labelPrecision');
-        if (labelPrecision == null || labelPrecision === 'auto') {
-            labelPrecision = axisProxy.getWindow().valuePrecision;
-        }
-
-        const scale = axisProxy.getAxisModel().axis.scale;
-        const valueStr = (value == null || isNaN(value))
-            ? ''
-            : (isOrdinalScale(scale) || isTimeScale(scale))
-            ? scale.getLabel({value: Math.round(value)})
-            : isFinite(labelPrecision)
-            ? round(value, labelPrecision, true)
-            : value + '';
-
-        return isFunction(labelFormatter)
-            ? labelFormatter(value, valueStr)
-            : isString(labelFormatter)
-                ? labelFormatter.replace('{value}', valueStr)
-                : valueStr;
-    }
-
     private _onOverDataInfoTriggerArea(isOver: boolean): void {
         this._isOverDataInfoTriggerArea = isOver;
         this._showDataInfo(isOver);
@@ -1149,6 +1128,42 @@ class SliderZoomView extends DataZoomView {
 
 }
 
+function formatLabel(
+    dataZoomModel: SliderZoomModel,
+    extentIdx: 0 | 1,
+    window: AxisProxyWindow,
+    scale: Scale
+): string {
+    const labelFormatter = dataZoomModel.get('labelFormatter');
+
+    let labelPrecision = dataZoomModel.get('labelPrecision');
+    if (labelPrecision == null || labelPrecision === 'auto') {
+        labelPrecision = window.valuePrecision;
+    }
+
+    // Do not display values out of `SCALE_EXTENT_KIND_EFFECTIVE` - generally 
they are meaningless.
+    // For example, `scaleExtent[0]` is often `0`, and negative values are 
unlikely to be meaningful.
+    // That is, "nice" expansion and `SCALE_EXTENT_KIND_MAPPING` expansion are 
always not display in labels.
+    const value = (extentIdx ? mathMin : mathMax)(
+        window.value[extentIdx],
+        window.noZoomEffMM[extentIdx],
+    );
+
+    const valueStr = (value == null || isNaN(value))
+        ? ''
+        : (isOrdinalScale(scale) || isTimeScale(scale))
+        ? scale.getLabel({value: Math.round(value)})
+        : isFinite(labelPrecision)
+        ? round(value, labelPrecision, true)
+        : value + '';
+
+    return isFunction(labelFormatter)
+        ? labelFormatter(value, valueStr)
+        : isString(labelFormatter)
+            ? labelFormatter.replace('{value}', valueStr)
+            : valueStr;
+}
+
 function getOtherDim(thisDim: 'x' | 'y' | 'radius' | 'angle' | 'single' | 'z') 
{
     // FIXME
     // 这个逻辑和getOtherAxis里一致,但是写在这里是否不好
diff --git a/src/coord/scaleRawExtentInfo.ts b/src/coord/scaleRawExtentInfo.ts
index 3f64dcf6b..628ea1b1f 100644
--- a/src/coord/scaleRawExtentInfo.ts
+++ b/src/coord/scaleRawExtentInfo.ts
@@ -92,10 +92,14 @@ type ScaleRawExtentResultForContainShape = Pick<
 >;
 
 /**
- * Return the min max before `dataZoom` applied for mapping.
- * "mapping" means `SCALE_EXTENT_KIND_MAPPING`.
+ * Return the min max before `dataZoom` applied.
  */
-type ScaleRawExtentResultForZoom = number[];
+export type ScaleRawExtentResultForZoom = {
+    // "effective" means `SCALE_EXTENT_KIND_EFFECTIVE`.
+    noZoomEffMM: number[];
+    // "mapping" means `SCALE_EXTENT_KIND_MAPPING`.
+    noZoomMapMM: number[];
+};
 
 type ScaleRawExtentResultFinal = Pick<
     ScaleRawExtentInternal,
@@ -343,7 +347,10 @@ export class ScaleRawExtentInfo {
 
     makeForZoom(): ScaleRawExtentResultForZoom {
         const internal = this._i;
-        return (internal.noZoomEffMMExp || internal.noZoomEffMM).slice();
+        return {
+            noZoomEffMM: internal.noZoomEffMM.slice(),
+            noZoomMapMM: makeNoZoomMappingMM(internal),
+        };
     }
 
     makeFinal(): ScaleRawExtentResultFinal {
@@ -359,7 +366,7 @@ export class ScaleRawExtentInfo {
             needCrossZero: internal.needCrossZero,
             needToggleAxisInverse: internal.needToggleAxisInverse,
             effMM: noZoomEffMM.slice(),
-            mapMM: this.makeForZoom(),
+            mapMM: makeNoZoomMappingMM(internal),
         };
         const effMM = result.effMM;
         const mapMM = result.mapMM;
@@ -414,6 +421,10 @@ export class ScaleRawExtentInfo {
 
 }
 
+function makeNoZoomMappingMM(internal: ScaleRawExtentInternal): number[] {
+    return (internal.noZoomEffMMExp || internal.noZoomEffMM).slice();
+}
+
 /**
  * Should be called when a new extent is created or modified.
  */
diff --git a/test/bar-overflow-plot2.html b/test/bar-overflow-plot2.html
index cec30e22f..c297ef410 100644
--- a/test/bar-overflow-plot2.html
+++ b/test/bar-overflow-plot2.html
@@ -165,6 +165,7 @@ under the License.
                         'Bars must not overflow the xAxis.',
                         'Check yAxis onZero to xAxis (xAxis is value axis).',
                         `series data min value is 
**${testHelper.printObject(_data.minXSet)}**`,
+                        `dataZoom min label should **not below zero**`,
                     ],
                     option: createOption(),
                     inputsStyle: 'compact',


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

Reply via email to