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 d168bf237a442c2254fd494d6ae76e4e4712deff
Author: 100pah <[email protected]>
AuthorDate: Fri Jan 9 16:44:16 2026 +0800

    fix(axisTick&dataZoom): (1) Apply a better auto-precision method. (2) Make 
the rounding result consistent between dataZoom calculated window and specified 
axis `determinedMin/Max`. (3) Fix unexpected behaviors when dataZoom controls 
axes with `alignTicks: true` - previous they are not precisely aligned and the 
ticks jump significantly due to inappropriate rounding when dataZoom dragging.
---
 src/component/dataZoom/AxisProxy.ts         | 202 ++++++++++++++++++----------
 src/component/dataZoom/DataZoomModel.ts     |  26 ++--
 src/component/dataZoom/SliderZoomView.ts    |  58 ++++----
 src/component/dataZoom/dataZoomProcessor.ts |  31 +++--
 src/component/dataZoom/helper.ts            |  26 +++-
 src/component/helper/sliderMove.ts          |  19 ++-
 src/component/toolbox/feature/DataZoom.ts   |  39 ++++--
 src/coord/Axis.ts                           |  15 +--
 8 files changed, 270 insertions(+), 146 deletions(-)

diff --git a/src/component/dataZoom/AxisProxy.ts 
b/src/component/dataZoom/AxisProxy.ts
index 11d5f6c74..cabe1db8c 100644
--- a/src/component/dataZoom/AxisProxy.ts
+++ b/src/component/dataZoom/AxisProxy.ts
@@ -17,13 +17,15 @@
 * under the License.
 */
 
-import * as zrUtil from 'zrender/src/core/util';
-import * as numberUtil from '../../util/number';
+import {clone, defaults, each, map} from 'zrender/src/core/util';
+import {
+    asc, getAcceptableTickPrecision, linearMap, mathAbs, mathCeil, mathFloor, 
mathMax, mathMin, round
+} from '../../util/number';
 import sliderMove from '../helper/sliderMove';
 import GlobalModel from '../../model/Global';
 import SeriesModel from '../../model/Series';
 import ExtensionAPI from '../../core/ExtensionAPI';
-import { Dictionary } from '../../util/types';
+import { Dictionary, NullUndefined } from '../../util/types';
 // TODO Polar?
 import DataZoomModel from './DataZoomModel';
 import { AxisBaseModel } from '../../coord/AxisBaseModel';
@@ -31,9 +33,8 @@ import { unionAxisExtentFromData } from 
'../../coord/axisHelper';
 import { ensureScaleRawExtentInfo } from '../../coord/scaleRawExtentInfo';
 import { getAxisMainType, isCoordSupported, DataZoomAxisDimension } from 
'./helper';
 import { SINGLE_REFERRING } from '../../util/model';
+import { isOrdinalScale, isTimeScale } from '../../scale/helper';
 
-const each = zrUtil.each;
-const asc = numberUtil.asc;
 
 interface MinMaxSpan {
     minSpan: number
@@ -42,6 +43,17 @@ interface MinMaxSpan {
     maxValueSpan: number
 }
 
+interface AxisProxyWindow {
+    value: [number, number];
+    percent: [number, 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];
+    valuePrecision: number;
+}
+
 /**
  * Operate single axis.
  * One axis can only operated by one axis operator.
@@ -56,13 +68,16 @@ class AxisProxy {
     private _dimName: DataZoomAxisDimension;
     private _axisIndex: number;
 
-    private _valueWindow: [number, number];
-    private _percentWindow: [number, number];
+    private _window: AxisProxyWindow;
 
     private _dataExtent: [number, number];
 
     private _minMaxSpan: MinMaxSpan;
 
+    /**
+     * The host `dataZoom` model. An axis may be controlled by multiple 
`dataZoom`s,
+     * but only the first declared `dataZoom` is the host.
+     */
     private _dataZoomModel: DataZoomModel;
 
     constructor(
@@ -94,17 +109,10 @@ class AxisProxy {
     }
 
     /**
-     * @return Value can only be NaN or finite value.
+     * @return `getWindow().value` can only have NaN or finite value.
      */
-    getDataValueWindow() {
-        return this._valueWindow.slice() as [number, number];
-    }
-
-    /**
-     * @return {Array.<number>}
-     */
-    getDataPercentWindow() {
-        return this._percentWindow.slice() as [number, number];
+    getWindow(): AxisProxyWindow {
+        return clone(this._window);
     }
 
     getTargetSeriesModels() {
@@ -128,26 +136,31 @@ class AxisProxy {
     }
 
     getMinMaxSpan() {
-        return zrUtil.clone(this._minMaxSpan);
+        return clone(this._minMaxSpan);
     }
 
     /**
+     * [CAVEAT] Keep this method pure, so that it can be called multiple times.
+     *
      * Only calculate by given range and this._dataExtent, do not change 
anything.
      */
-    calculateDataWindow(opt?: {
-        start?: number
-        end?: number
-        startValue?: number | string | Date
-        endValue?: number | string | Date
-    }) {
+    calculateDataWindow(
+        opt: {
+            start?: number // percent, 0 ~ 100
+            end?: number // percent, 0 ~ 100
+            startValue?: number | string | Date
+            endValue?: number | string | Date
+        }
+    ): AxisProxyWindow {
         const dataExtent = this._dataExtent;
-        const axisModel = this.getAxisModel();
-        const scale = axisModel.axis.scale;
+        const axis = this.getAxisModel().axis;
+        const scale = axis.scale;
         const rangePropMode = this._dataZoomModel.getRangePropMode();
         const percentExtent = [0, 100];
         const percentWindow = [] as unknown as [number, number];
         const valueWindow = [] as unknown as [number, number];
         let hasPropModeValue;
+        const needRound = [false, false];
 
         each(['start', 'end'] as const, function (prop, idx) {
             let boundPercent = opt[prop];
@@ -169,24 +182,19 @@ class AxisProxy {
 
             if (rangePropMode[idx] === 'percent') {
                 boundPercent == null && (boundPercent = percentExtent[idx]);
-                // Use scale.parse to math round for category or time axis.
-                boundValue = scale.parse(numberUtil.linearMap(
-                    boundPercent, percentExtent, dataExtent
-                ));
+                boundValue = linearMap(boundPercent, percentExtent, 
dataExtent);
+                needRound[idx] = true;
             }
             else {
                 hasPropModeValue = true;
+                // NOTE: `scale.parse` can also round input for 'time' or 
'ordinal' scale.
                 boundValue = boundValue == null ? dataExtent[idx] : 
scale.parse(boundValue);
                 // Calculating `percent` from `value` may be not accurate, 
because
-                // This calculation can not be inversed, because all of values 
that
+                // This calculation can not be inverted, because all of values 
that
                 // are overflow the `dataExtent` will be calculated to percent 
'100%'
-                boundPercent = numberUtil.linearMap(
-                    boundValue, dataExtent, percentExtent
-                );
+                boundPercent = linearMap(boundValue, dataExtent, 
percentExtent);
             }
 
-            // valueWindow[idx] = round(boundValue);
-            // percentWindow[idx] = round(boundPercent);
             // fallback to extent start/end when parsed value or percent is 
invalid
             valueWindow[idx] = boundValue == null || isNaN(boundValue)
                 ? dataExtent[idx]
@@ -199,11 +207,17 @@ class AxisProxy {
         asc(valueWindow);
         asc(percentWindow);
 
-        // The windows from user calling of `dispatchAction` might be out of 
the extent,
-        // or do not obey the `min/maxSpan`, `min/maxValueSpan`. But we don't 
restrict window
-        // by `zoomLock` here, because we see `zoomLock` just as a interaction 
constraint,
-        // where API is able to initialize/modify the window size even though 
`zoomLock`
-        // specified.
+        // The windows specified from `dispatchAction` or `setOption` may:
+        //  (1) be out of the extent, or
+        //  (2) do not comply with `minSpan/maxSpan`, 
`minValueSpan/maxValueSpan`.
+        // So we clamp them here.
+        // But we don't restrict window by `zoomLock` here, because we see 
`zoomLock` just as a
+        // interaction constraint, where API is able to initialize/modify the 
window size even
+        // though `zoomLock` specified.
+        // PENDING: For historical reason, the option design is partially 
incompatible:
+        //  If `option.start` and `option.endValue` are specified, and when we 
choose whether
+        //  `min/maxValueSpan` or `minSpan/maxSpan` is applied, neither one is 
intuitive.
+        //  (Currently using `minValueSpan/maxValueSpan`.)
         const spans = this._minMaxSpan;
         hasPropModeValue
             ? restrictSet(valueWindow, percentWindow, dataExtent, 
percentExtent, false)
@@ -223,14 +237,67 @@ class AxisProxy {
                 spans['max' + suffix as 'maxSpan' | 'maxValueSpan']
             );
             for (let i = 0; i < 2; i++) {
-                toWindow[i] = numberUtil.linearMap(fromWindow[i], fromExtent, 
toExtent, true);
-                toValue && (toWindow[i] = scale.parse(toWindow[i]));
+                toWindow[i] = linearMap(fromWindow[i], fromExtent, toExtent, 
true);
+                if (toValue) {
+                    toWindow[i] = toWindow[i];
+                    needRound[i] = true;
+                }
+            }
+            simplyEnsureAsc(toWindow);
+        }
+
+        // - In 'time' and 'ordinal' scale, rounding by 0 is required.
+        // - In 'interval' and 'log' scale, we round values for acceptable 
display with acceptable accuracy loose.
+        //  "Values" can be rounded only if they are generated from `percent`, 
since user-specified "value"
+        //  should be respected, and `DataZoomSelect` already performs its own 
rounding.
+        // - Currently we only round "value" but not "percent", since there is 
no need so far.
+        // - MEMO: See also #3228 and commit 
a89fd0d7f1833ecf08a4a5b7ecf651b4a0d8da41
+        // - PENDING: The rounding result may slightly overflow the 
restriction from `min/maxSpan`,
+        //  but it is acceptable so far.
+        const isScaleOrdinalOrTime = isOrdinalScale(scale) || 
isTimeScale(scale);
+        // Typically pxExtent has been ready in coordSys create. (See `create` 
of `Grid.ts`)
+        const pxExtent = axis.getExtent();
+        // NOTICE: this pxSpan may be not accurate yet due to "outerBounds" 
logic, but acceptable.
+        const pxSpan = mathAbs(pxExtent[1] - pxExtent[0]);
+        const precision = isScaleOrdinalOrTime
+            ? 0
+            : getAcceptableTickPrecision(valueWindow[1] - valueWindow[0], 
pxSpan, 0.5);
+        each([[0, mathCeil], [1, mathFloor]] as const, function ([idx, 
ceilOrFloor]) {
+            if (!needRound[idx] || !isFinite(precision)) {
+                return;
+            }
+            valueWindow[idx] = round(valueWindow[idx], precision);
+            valueWindow[idx] = mathMin(dataExtent[1], mathMax(dataExtent[0], 
valueWindow[idx])); // Clamp.
+            if (percentWindow[idx] === percentExtent[idx]) {
+                // When `percent` is 0 or 100, `value` must be `dataExtent[0]` 
or `dataExtent[1]`
+                // regardless of the calculated precision.
+                // NOTE: `percentWindow` is never over [0, 100] at this moment.
+                valueWindow[idx] = dataExtent[idx];
+                if (isScaleOrdinalOrTime) {
+                    // In case that dataExtent[idx] is not an integer (may 
occur since it comes from user input)
+                    valueWindow[idx] = ceilOrFloor(valueWindow[idx]);
+                }
+            }
+        });
+        simplyEnsureAsc(valueWindow);
+
+        const percentInvertedWindow = [
+            linearMap(valueWindow[0], dataExtent, percentExtent, true),
+            linearMap(valueWindow[1], dataExtent, percentExtent, true),
+        ] as [number, number];
+        simplyEnsureAsc(percentInvertedWindow);
+
+        function simplyEnsureAsc(window: number[]): void {
+            if (window[0] > window[1]) {
+                window[0] = window[1];
             }
         }
 
         return {
-            valueWindow: valueWindow,
-            percentWindow: percentWindow
+            value: valueWindow,
+            percent: percentWindow,
+            percentInverted: percentInvertedWindow,
+            valuePrecision: precision,
         };
     }
 
@@ -239,8 +306,8 @@ class AxisProxy {
      * so it is recommended to be called in "process stage" but not "model init
      * stage".
      */
-    reset(dataZoomModel: DataZoomModel) {
-        if (dataZoomModel !== this._dataZoomModel) {
+    reset(dataZoomModel: DataZoomModel, alignToPercentInverted: [number, 
number] | NullUndefined) {
+        if (!this.hostedBy(dataZoomModel)) {
             return;
         }
 
@@ -251,24 +318,28 @@ class AxisProxy {
         // `calculateDataWindow` uses min/maxSpan.
         this._updateMinMaxSpan();
 
-        const dataWindow = 
this.calculateDataWindow(dataZoomModel.settledOption);
-
-        this._valueWindow = dataWindow.valueWindow;
-        this._percentWindow = dataWindow.percentWindow;
+        let opt = dataZoomModel.settledOption;
+        if (alignToPercentInverted) {
+            opt = defaults({
+                start: alignToPercentInverted[0],
+                end: alignToPercentInverted[1],
+            }, opt);
+        }
+        this._window = this.calculateDataWindow(opt);
 
         // Update axis setting then.
         this._setAxisModel();
     }
 
     filterData(dataZoomModel: DataZoomModel, api: ExtensionAPI) {
-        if (dataZoomModel !== this._dataZoomModel) {
+        if (!this.hostedBy(dataZoomModel)) {
             return;
         }
 
         const axisDim = this._dimName;
         const seriesModels = this.getTargetSeriesModels();
         const filterMode = dataZoomModel.get('filterMode');
-        const valueWindow = this._valueWindow;
+        const valueWindow = this._window.value;
 
         if (filterMode === 'none') {
             return;
@@ -305,7 +376,7 @@ class AxisProxy {
 
             if (filterMode === 'weakFilter') {
                 const store = seriesData.getStore();
-                const dataDimIndices = zrUtil.map(dataDims, dim => 
seriesData.getDimensionIndex(dim), seriesData);
+                const dataDimIndices = map(dataDims, dim => 
seriesData.getDimensionIndex(dim), seriesData);
                 seriesData.filterSelf(function (dataIndex) {
                     let leftOut;
                     let rightOut;
@@ -368,12 +439,12 @@ class AxisProxy {
 
             // minValueSpan and maxValueSpan has higher priority than minSpan 
and maxSpan
             if (valueSpan != null) {
-                percentSpan = numberUtil.linearMap(
+                percentSpan = linearMap(
                     dataExtent[0] + valueSpan, dataExtent, [0, 100], true
                 );
             }
             else if (percentSpan != null) {
-                valueSpan = numberUtil.linearMap(
+                valueSpan = linearMap(
                     percentSpan, [0, 100], dataExtent, true
                 ) - dataExtent[0];
             }
@@ -387,27 +458,22 @@ class AxisProxy {
 
         const axisModel = this.getAxisModel();
 
-        const percentWindow = this._percentWindow;
-        const valueWindow = this._valueWindow;
-
-        if (!percentWindow) {
+        const window = this._window;
+        if (!window) {
             return;
         }
-
-        // [0, 500]: arbitrary value, guess axis extent.
-        let precision = numberUtil.getPixelPrecision(valueWindow, [0, 500]);
-        precision = Math.min(precision, 20);
+        const {percent, value} = window;
 
         // For value axis, if min/max/scale are not set, we just use the 
extent obtained
         // by series data, which may be a little different from the extent 
calculated by
         // `axisHelper.getScaleExtent`. But the different just affects the 
experience a
         // little when zooming. So it will not be fixed until some users 
require it strongly.
         const rawExtentInfo = axisModel.axis.scale.rawExtentInfo;
-        if (percentWindow[0] !== 0) {
-            rawExtentInfo.setDeterminedMinMax('min', 
+valueWindow[0].toFixed(precision));
+        if (percent[0] !== 0) {
+            rawExtentInfo.setDeterminedMinMax('min', value[0]);
         }
-        if (percentWindow[1] !== 100) {
-            rawExtentInfo.setDeterminedMinMax('max', 
+valueWindow[1].toFixed(precision));
+        if (percent[1] !== 100) {
+            rawExtentInfo.setDeterminedMinMax('max', value[1]);
         }
         rawExtentInfo.freeze();
     }
diff --git a/src/component/dataZoom/DataZoomModel.ts 
b/src/component/dataZoom/DataZoomModel.ts
index 279c06a13..c83e166cb 100644
--- a/src/component/dataZoom/DataZoomModel.ts
+++ b/src/component/dataZoom/DataZoomModel.ts
@@ -23,13 +23,15 @@ import ComponentModel from '../../model/Component';
 import {
     LayoutOrient,
     ComponentOption,
-    LabelOption
+    LabelOption,
+    NullUndefined
 } from '../../util/types';
 import Model from '../../model/Model';
 import GlobalModel from '../../model/Global';
 import { AxisBaseModel } from '../../coord/AxisBaseModel';
 import {
-    getAxisMainType, DATA_ZOOM_AXIS_DIMENSIONS, DataZoomAxisDimension
+    getAxisMainType, DATA_ZOOM_AXIS_DIMENSIONS, DataZoomAxisDimension,
+    getAxisProxyFromModel
 } from './helper';
 import SingleAxisModel from '../../coord/single/AxisModel';
 import { MULTIPLE_REFERRING, SINGLE_REFERRING, ModelFinderIndexQuery, 
ModelFinderIdQuery } from '../../util/model';
@@ -131,15 +133,11 @@ export interface DataZoomOption extends ComponentOption {
 
 type RangeOption = Pick<DataZoomOption, 'start' | 'end' | 'startValue' | 
'endValue'>;
 
-export type DataZoomExtendedAxisBaseModel = AxisBaseModel & {
-    __dzAxisProxy: AxisProxy
-};
-
 class DataZoomAxisInfo {
     indexList: number[] = [];
     indexMap: boolean[] = [];
 
-    add(axisCmptIdx: number) {
+    add(axisCmptIdx: ComponentModel['componentIndex']): void {
         // Remove duplication.
         if (!this.indexMap[axisCmptIdx]) {
             this.indexList.push(axisCmptIdx);
@@ -456,10 +454,7 @@ class DataZoomModel<Opts extends DataZoomOption = 
DataZoomOption> extends Compon
      * @return If not found, return null/undefined.
      */
     getAxisProxy(axisDim: DataZoomAxisDimension, axisIndex: number): AxisProxy 
{
-        const axisModel = this.getAxisModel(axisDim, axisIndex);
-        if (axisModel) {
-            return (axisModel as DataZoomExtendedAxisBaseModel).__dzAxisProxy;
-        }
+        return getAxisProxyFromModel(this.getAxisModel(axisDim, axisIndex));
     }
 
     /**
@@ -510,7 +505,7 @@ class DataZoomModel<Opts extends DataZoomOption = 
DataZoomOption> extends Compon
     getPercentRange(): number[] {
         const axisProxy = this.findRepresentativeAxisProxy();
         if (axisProxy) {
-            return axisProxy.getDataPercentWindow();
+            return axisProxy.getWindow().percent;
         }
     }
 
@@ -523,11 +518,11 @@ class DataZoomModel<Opts extends DataZoomOption = 
DataZoomOption> extends Compon
         if (axisDim == null && axisIndex == null) {
             const axisProxy = this.findRepresentativeAxisProxy();
             if (axisProxy) {
-                return axisProxy.getDataValueWindow();
+                return axisProxy.getWindow().value;
             }
         }
         else {
-            return this.getAxisProxy(axisDim, axisIndex).getDataValueWindow();
+            return this.getAxisProxy(axisDim, axisIndex).getWindow().value;
         }
     }
 
@@ -537,7 +532,7 @@ class DataZoomModel<Opts extends DataZoomOption = 
DataZoomOption> extends Compon
      */
     findRepresentativeAxisProxy(axisModel?: AxisBaseModel): AxisProxy {
         if (axisModel) {
-            return (axisModel as DataZoomExtendedAxisBaseModel).__dzAxisProxy;
+            return getAxisProxyFromModel(axisModel);
         }
 
         // Find the first hosted axisProxy
@@ -576,6 +571,7 @@ class DataZoomModel<Opts extends DataZoomOption = 
DataZoomOption> extends Compon
     }
 
 }
+
 /**
  * Retrieve those raw params from option, which will be cached separately,
  * because they will be overwritten by normalized/calculated values in the main
diff --git a/src/component/dataZoom/SliderZoomView.ts 
b/src/component/dataZoom/SliderZoomView.ts
index 30422c9c5..ad82f0fca 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 } from '../../util/number';
+import { linearMap, asc, parsePercent, round } from '../../util/number';
 import * as layout from '../../util/layout';
 import sliderMove from '../helper/sliderMove';
 import GlobalModel from '../../model/Global';
@@ -35,7 +35,7 @@ import { RectLike } from 'zrender/src/core/BoundingRect';
 import Axis from '../../coord/Axis';
 import SeriesModel from '../../model/Series';
 import { AxisBaseModel } from '../../coord/AxisBaseModel';
-import { getAxisMainType, collectReferCoordSysModelInfo } from './helper';
+import { getAxisMainType, collectReferCoordSysModelInfo, getAlignTo } from 
'./helper';
 import { enableHoverEmphasis } from '../../util/states';
 import { createSymbol, symbolBuildProxies } from '../../util/symbol';
 import { deprecateLog } from '../../util/log';
@@ -44,6 +44,8 @@ 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';
 
 const Rect = graphic.Rect;
 
@@ -816,30 +818,35 @@ class SliderZoomView extends DataZoomView {
 
     private _updateDataInfo(nonRealtime?: boolean) {
         const dataZoomModel = this.dataZoomModel;
-        const displaybles = this._displayables;
-        const handleLabels = displaybles.handleLabels;
+        const displayables = this._displayables;
+        const handleLabels = displayables.handleLabels;
         const orient = this._orient;
         let labelTexts = ['', ''];
 
-        // FIXME
-        // date型,支持formatter,autoformatter(ec2 date.getAutoFormatter)
         if (dataZoomModel.get('showDetail')) {
             const axisProxy = dataZoomModel.findRepresentativeAxisProxy();
 
             if (axisProxy) {
-                const axis = axisProxy.getAxisModel().axis;
                 const range = this._range;
 
-                const dataInterval = nonRealtime
+                let dataInterval: [number, number];
+                if (nonRealtime) {
                     // See #4434, data and axis are not processed and reset 
yet in non-realtime mode.
-                    ? axisProxy.calculateDataWindow({
-                        start: range[0], end: range[1]
-                    }).valueWindow
-                    : axisProxy.getDataValueWindow();
+                    let calcWinInput = {start: range[0], end: range[1]};
+                    const alignTo = getAlignTo(dataZoomModel, axisProxy);
+                    if (alignTo) {
+                        const alignToWindow = 
alignTo.calculateDataWindow(calcWinInput).percentInverted;
+                        calcWinInput = {start: alignToWindow[0], end: 
alignToWindow[1]};
+                    }
+                    dataInterval = 
axisProxy.calculateDataWindow(calcWinInput).value;
+                }
+                else {
+                    dataInterval = axisProxy.getWindow().value;
+                }
 
                 labelTexts = [
-                    this._formatLabel(dataInterval[0], axis),
-                    this._formatLabel(dataInterval[1], axis)
+                    this._formatLabel(dataInterval[0], axisProxy),
+                    this._formatLabel(dataInterval[1], axisProxy)
                 ];
             }
         }
@@ -854,7 +861,7 @@ class SliderZoomView extends DataZoomView {
             // Text should not transform by barGroup.
             // Ignore handlers transform
             const barTransform = graphic.getTransform(
-                displaybles.handles[handleIndex].parent, this.group
+                displayables.handles[handleIndex].parent, this.group
             );
             const direction = graphic.transformDirection(
                 handleIndex === 0 ? 'right' : 'left', barTransform
@@ -877,27 +884,26 @@ class SliderZoomView extends DataZoomView {
         }
     }
 
-    private _formatLabel(value: ParsedValue, axis: Axis) {
+    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 = axis.getPixelPrecision();
+            labelPrecision = axisProxy.getWindow().valuePrecision;
         }
 
-        const valueStr = (value == null || isNaN(value as number))
+        const scale = axisProxy.getAxisModel().axis.scale;
+        const valueStr = (value == null || isNaN(value))
             ? ''
-            // FIXME Glue code
-            : (axis.type === 'category' || axis.type === 'time')
-                ? axis.scale.getLabel({
-                    value: Math.round(value as number)
-                })
-                // param of toFixed should less then 20.
-                : (value as number).toFixed(Math.min(labelPrecision as number, 
20));
+            : (isOrdinalScale(scale) || isTimeScale(scale))
+            ? scale.getLabel({value: Math.round(value)})
+            : isFinite(labelPrecision)
+            ? round(value, labelPrecision, true)
+            : value + '';
 
         return isFunction(labelFormatter)
-            ? labelFormatter(value as number, valueStr)
+            ? labelFormatter(value, valueStr)
             : isString(labelFormatter)
                 ? labelFormatter.replace('{value}', valueStr)
                 : valueStr;
diff --git a/src/component/dataZoom/dataZoomProcessor.ts 
b/src/component/dataZoom/dataZoomProcessor.ts
index f511aaed3..d64269295 100644
--- a/src/component/dataZoom/dataZoomProcessor.ts
+++ b/src/component/dataZoom/dataZoomProcessor.ts
@@ -19,11 +19,12 @@
 
 import {createHashMap, each} from 'zrender/src/core/util';
 import SeriesModel from '../../model/Series';
-import DataZoomModel, { DataZoomExtendedAxisBaseModel } from './DataZoomModel';
-import { getAxisMainType, DataZoomAxisDimension } from './helper';
+import DataZoomModel from './DataZoomModel';
+import { getAxisMainType, DataZoomAxisDimension, 
DataZoomExtendedAxisBaseModel, getAlignTo } from './helper';
 import AxisProxy from './AxisProxy';
 import { StageHandler } from '../../util/types';
 
+
 const dataZoomProcessor: StageHandler = {
 
     // `dataZoomProcessor` will only be performed in needed series. Consider if
@@ -54,7 +55,7 @@ const dataZoomProcessor: StageHandler = {
         });
         const proxyList: AxisProxy[] = [];
         eachAxisModel(function (axisDim, axisIndex, axisModel, dataZoomModel) {
-            // Different dataZooms may constrol the same axis. In that case,
+            // Different dataZooms may control the same axis. In that case,
             // an axisProxy serves both of them.
             if (!axisModel.__dzAxisProxy) {
                 // Use the first dataZoomModel as the main model of axisProxy.
@@ -82,8 +83,19 @@ const dataZoomProcessor: StageHandler = {
             // We calculate window and reset axis here but not in model
             // init stage and not after action dispatch handler, because
             // reset should be called after seriesData.restoreData.
+            const axisProxyNeedAlign: [AxisProxy, AxisProxy][] = [];
             dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
-                dataZoomModel.getAxisProxy(axisDim, 
axisIndex).reset(dataZoomModel);
+                const axisProxy = dataZoomModel.getAxisProxy(axisDim, 
axisIndex);
+                const alignToAxisProxy = getAlignTo(dataZoomModel, axisProxy);
+                if (alignToAxisProxy) {
+                    axisProxyNeedAlign.push([axisProxy, alignToAxisProxy]);
+                }
+                else {
+                    axisProxy.reset(dataZoomModel, null);
+                }
+            });
+            each(axisProxyNeedAlign, function (item) {
+                item[0].reset(dataZoomModel, 
item[1].getWindow().percentInverted);
             });
 
             // Caution: data zoom filtering is order sensitive when using
@@ -110,14 +122,13 @@ const dataZoomProcessor: StageHandler = {
             // is able to get them from chart.getOption().
             const axisProxy = dataZoomModel.findRepresentativeAxisProxy();
             if (axisProxy) {
-                const percentRange = axisProxy.getDataPercentWindow();
-                const valueRange = axisProxy.getDataValueWindow();
+                const {percent, value} = axisProxy.getWindow();
 
                 dataZoomModel.setCalculatedRange({
-                    start: percentRange[0],
-                    end: percentRange[1],
-                    startValue: valueRange[0],
-                    endValue: valueRange[1]
+                    start: percent[0],
+                    end: percent[1],
+                    startValue: value[0],
+                    endValue: value[1]
                 });
             }
         });
diff --git a/src/component/dataZoom/helper.ts b/src/component/dataZoom/helper.ts
index 4b0a8a31e..e2009cb76 100644
--- a/src/component/dataZoom/helper.ts
+++ b/src/component/dataZoom/helper.ts
@@ -17,13 +17,14 @@
 * under the License.
 */
 
-import { Payload } from '../../util/types';
+import { NullUndefined, Payload } from '../../util/types';
 import GlobalModel from '../../model/Global';
 import DataZoomModel from './DataZoomModel';
 import { indexOf, createHashMap, assert, HashMap } from 
'zrender/src/core/util';
 import SeriesModel from '../../model/Series';
 import { CoordinateSystemHostModel } from '../../coord/CoordinateSystem';
 import { AxisBaseModel } from '../../coord/AxisBaseModel';
+import type AxisProxy from './AxisProxy';
 
 
 export interface DataZoomPayloadBatchItem {
@@ -43,6 +44,10 @@ export interface DataZoomReferCoordSysInfo {
     axisModels: AxisBaseModel[];
 }
 
+export type DataZoomExtendedAxisBaseModel = AxisBaseModel & {
+    __dzAxisProxy: AxisProxy
+};
+
 export const DATA_ZOOM_AXIS_DIMENSIONS = [
     'x', 'y', 'radius', 'angle', 'single'
 ] as const;
@@ -205,3 +210,22 @@ export function 
collectReferCoordSysModelInfo(dataZoomModel: DataZoomModel): {
 
     return coordSysInfoWrap;
 }
+
+export function getAxisProxyFromModel(axisModel: AxisBaseModel): AxisProxy | 
NullUndefined {
+    return axisModel && (axisModel as 
DataZoomExtendedAxisBaseModel).__dzAxisProxy;
+}
+
+/**
+ * NOTICE: If `axis_a` aligns to `axis_b`, but they are not controlled by
+ * the same `dataZoom`, do not consider `axis_b` as `alignTo` and
+ * then do not input it into `AxisProxy#reset`.
+ */
+export function getAlignTo(dataZoomModel: DataZoomModel, axisProxy: 
AxisProxy): AxisProxy | NullUndefined {
+    const alignToAxis = axisProxy.getAxisModel().axis.alignTo;
+    return (
+        alignToAxis && dataZoomModel.getAxisProxy(
+            alignToAxis.dim as DataZoomAxisDimension,
+            alignToAxis.model.componentIndex
+        )
+    ) ? getAxisProxyFromModel(alignToAxis.model) : null;
+}
diff --git a/src/component/helper/sliderMove.ts 
b/src/component/helper/sliderMove.ts
index 902312d30..721f6704a 100644
--- a/src/component/helper/sliderMove.ts
+++ b/src/component/helper/sliderMove.ts
@@ -17,6 +17,8 @@
 * under the License.
 */
 
+import { addSafe } from '../../util/number';
+
 /**
  * Calculate slider move result.
  * Usage:
@@ -24,6 +26,9 @@
  * maxSpan and the same as `Math.abs(handleEnd[1] - handleEnds[0])`.
  * (2) If handle0 is forbidden to cross handle1, set minSpan as `0`.
  *
+ * [CAVEAT]
+ *  This method is inefficient due to the use of `addSafe`.
+ *
  * @param delta Move length.
  * @param handleEnds handleEnds[0] can be bigger then handleEnds[1].
  *              handleEnds will be modified in this method.
@@ -48,7 +53,9 @@ export default function sliderMove(
 
     delta = delta || 0;
 
-    const extentSpan = extent[1] - extent[0];
+    // Consider `7.1e-9 - 7e-9` get `1.0000000000000007e-10`, so use `addSafe`
+    // to remove rounding error whenever possible.
+    const extentSpan = addSafe(extent[1], -extent[0]);
 
     // Notice maxSpan and minSpan can be null/undefined.
     if (minSpan != null) {
@@ -58,7 +65,7 @@ export default function sliderMove(
         maxSpan = Math.max(maxSpan, minSpan != null ? minSpan : 0);
     }
     if (handleIndex === 'all') {
-        let handleSpan = Math.abs(handleEnds[1] - handleEnds[0]);
+        let handleSpan = Math.abs(addSafe(handleEnds[1], -handleEnds[0]));
         handleSpan = restrict(handleSpan, [0, extentSpan]);
         minSpan = maxSpan = restrict(handleSpan, [minSpan, maxSpan]);
         handleIndex = 0;
@@ -74,7 +81,9 @@ export default function sliderMove(
     // Restrict in extent.
     const extentMinSpan = minSpan || 0;
     const realExtent = extent.slice();
-    originalDistSign.sign < 0 ? (realExtent[0] += extentMinSpan) : 
(realExtent[1] -= extentMinSpan);
+    originalDistSign.sign < 0
+        ? (realExtent[0] = addSafe(realExtent[0], extentMinSpan))
+        : (realExtent[1] = addSafe(realExtent[1], -extentMinSpan));
     handleEnds[handleIndex] = restrict(handleEnds[handleIndex], realExtent);
 
     // Expand span.
@@ -84,13 +93,13 @@ export default function sliderMove(
         currDistSign.sign !== originalDistSign.sign || currDistSign.span < 
minSpan
     )) {
         // If minSpan exists, 'cross' is forbidden.
-        handleEnds[1 - handleIndex] = handleEnds[handleIndex] + 
originalDistSign.sign * minSpan;
+        handleEnds[1 - handleIndex] = addSafe(handleEnds[handleIndex], 
originalDistSign.sign * minSpan);
     }
 
     // Shrink span.
     currDistSign = getSpanSign(handleEnds, handleIndex);
     if (maxSpan != null && currDistSign.span > maxSpan) {
-        handleEnds[1 - handleIndex] = handleEnds[handleIndex] + 
currDistSign.sign * maxSpan;
+        handleEnds[1 - handleIndex] = addSafe(handleEnds[handleIndex], 
currDistSign.sign * maxSpan);
     }
 
     return handleEnds;
diff --git a/src/component/toolbox/feature/DataZoom.ts 
b/src/component/toolbox/feature/DataZoom.ts
index e51ace23c..85a1727e8 100644
--- a/src/component/toolbox/feature/DataZoom.ts
+++ b/src/component/toolbox/feature/DataZoom.ts
@@ -35,9 +35,7 @@ import { Payload, Dictionary, ComponentOption, 
ItemStyleOption } from '../../../
 import Cartesian2D from '../../../coord/cartesian/Cartesian2D';
 import CartesianAxisModel from '../../../coord/cartesian/AxisModel';
 import DataZoomModel from '../../dataZoom/DataZoomModel';
-import {
-    DataZoomPayloadBatchItem, DataZoomAxisDimension
-} from '../../dataZoom/helper';
+import {DataZoomPayloadBatchItem} from '../../dataZoom/helper';
 import {
     ModelFinderObject, ModelFinderIndexQuery, makeInternalComponentId,
     ModelFinderIdQuery, parseFinder, ParsedModelFinderKnown
@@ -46,6 +44,8 @@ import ToolboxModel from '../ToolboxModel';
 import { registerInternalOptionCreator } from 
'../../../model/internalComponentCreator';
 import ComponentModel from '../../../model/Component';
 import tokens from '../../../visual/tokens';
+import BoundingRect from 'zrender/src/core/BoundingRect';
+import { getAcceptableTickPrecision, round } from '../../../util/number';
 
 
 const each = zrUtil.each;
@@ -55,6 +55,9 @@ const DATA_ZOOM_ID_BASE = 
makeInternalComponentId('toolbox-dataZoom_');
 const ICON_TYPES = ['zoom', 'back'] as const;
 type IconType = typeof ICON_TYPES[number];
 
+const XY2WH = {x: 'width', y: 'height'} as const;
+
+
 export interface ToolboxDataZoomFeatureOption extends ToolboxFeatureOption {
     type?: IconType[]
     icon?: {[key in IconType]?: string}
@@ -135,15 +138,18 @@ class DataZoomFeature extends 
ToolboxFeature<ToolboxDataZoomFeatureOption> {
                 return;
             }
 
+            const coordSysRect = coordSys.master.getRect().clone();
+
             const brushType = area.brushType;
             if (brushType === 'rect') {
-                setBatch('x', coordSys, (coordRange as 
BrushDimensionMinMax[])[0]);
-                setBatch('y', coordSys, (coordRange as 
BrushDimensionMinMax[])[1]);
+                setBatch('x', coordSys, coordSysRect, (coordRange as 
BrushDimensionMinMax[])[0]);
+                setBatch('y', coordSys, coordSysRect, (coordRange as 
BrushDimensionMinMax[])[1]);
             }
             else {
                 setBatch(
                     ({lineX: 'x', lineY: 'y'} as const)[brushType as 'lineX' | 
'lineY'],
                     coordSys,
+                    coordSysRect,
                     coordRange as BrushDimensionMinMax
                 );
             }
@@ -153,29 +159,42 @@ class DataZoomFeature extends 
ToolboxFeature<ToolboxDataZoomFeatureOption> {
 
         this._dispatchZoomAction(snapshot);
 
-        function setBatch(dimName: DataZoomAxisDimension, coordSys: 
Cartesian2D, minMax: number[]) {
+        function setBatch(
+            dimName: 'x' | 'y',
+            coordSys: Cartesian2D,
+            coordSysRect: BoundingRect,
+            minMax: number[]
+        ) {
             const axis = coordSys.getAxis(dimName);
             const axisModel = axis.model;
             const dataZoomModel = findDataZoom(dimName, axisModel, ecModel);
 
             // Restrict range.
             const minMaxSpan = 
dataZoomModel.findRepresentativeAxisProxy(axisModel).getMinMaxSpan();
+            const scaleExtent = axis.scale.getExtent();
             if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != 
null) {
                 minMax = sliderMove(
-                    0, minMax.slice(), axis.scale.getExtent(), 0,
+                    0, minMax.slice(), scaleExtent, 0,
                     minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan
                 );
             }
 
+            // Round for displayable.
+            const precision = getAcceptableTickPrecision(
+                scaleExtent[1] - scaleExtent[0],
+                coordSysRect[XY2WH[dimName]],
+                0.5
+            );
+
             dataZoomModel && (snapshot[dataZoomModel.id] = {
                 dataZoomId: dataZoomModel.id,
-                startValue: minMax[0],
-                endValue: minMax[1]
+                startValue: isFinite(precision) ? round(minMax[0], precision) 
: minMax[0],
+                endValue: isFinite(precision) ? round(minMax[1], precision) : 
minMax[1]
             });
         }
 
         function findDataZoom(
-            dimName: DataZoomAxisDimension, axisModel: CartesianAxisModel, 
ecModel: GlobalModel
+            dimName: 'x' | 'y', axisModel: CartesianAxisModel, ecModel: 
GlobalModel
         ): DataZoomModel {
             let found;
             ecModel.eachComponent({mainType: 'dataZoom', subType: 'select'}, 
function (dzModel: DataZoomModel) {
diff --git a/src/coord/Axis.ts b/src/coord/Axis.ts
index 6226e96b2..0390875e2 100644
--- a/src/coord/Axis.ts
+++ b/src/coord/Axis.ts
@@ -18,7 +18,7 @@
 */
 
 import {each, map} from 'zrender/src/core/util';
-import {linearMap, getPixelPrecision, round} from '../util/number';
+import {linearMap, round} from '../util/number';
 import {
     createAxisTicks,
     createAxisLabels,
@@ -80,6 +80,9 @@ class Axis {
     // `inverse` can be inferred by `extent` unless `extent[0] === extent[1]`.
     inverse: AxisBaseOption['inverse'] = false;
 
+    // Injected outside
+    alignTo: Axis;
+
 
     constructor(dim: DimensionName, scale: Scale, extent: [number, number]) {
         this.dim = dim;
@@ -111,16 +114,6 @@ class Axis {
         return this._extent.slice() as [number, number];
     }
 
-    /**
-     * Get precision used for formatting
-     */
-    getPixelPrecision(dataExtent?: [number, number]): number {
-        return getPixelPrecision(
-            dataExtent || this.scale.getExtent(),
-            this._extent
-        );
-    }
-
     /**
      * Set coord extent
      */


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


Reply via email to