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 ffcc636fbbe3d46107f70f41dbe2df1d33fe1e88
Author: 100pah <[email protected]>
AuthorDate: Sat Jan 10 00:53:28 2026 +0800

    fix(alignTicks): Change alignTick strategy: (1) Previously some series data 
may be out of the calculated extent and can not be displayed. (2) Previously 
the precision is incorrect for small float number (fixed at 10 rather than 
based on the magnitude of the value). (3) Make the tick precision more 
acceptable when min/max of axis is fixed, and remove console warning, because 
whey can be specified when dataZoom dragging. (4) Clarify the related code for 
LogScale.
---
 src/coord/axisAlignTicks.ts                      | 334 +++++++---
 src/coord/axisCommonTypes.ts                     |   4 +-
 src/coord/axisHelper.ts                          | 125 ++--
 src/coord/axisModelCommonMixin.ts                |   3 +-
 src/coord/cartesian/Grid.ts                      | 108 +++-
 src/coord/cartesian/defaultAxisExtentFromData.ts |   2 +-
 src/coord/parallel/Parallel.ts                   |  21 +-
 src/coord/polar/polarCreator.ts                  |  14 +-
 src/coord/radar/Radar.ts                         |   3 +-
 src/coord/scaleRawExtentInfo.ts                  |  60 +-
 src/coord/single/Single.ts                       |   8 +-
 src/export/api/helper.ts                         |  11 +-
 src/scale/Interval.ts                            | 174 ++++--
 src/scale/Log.ts                                 | 172 ++++--
 src/scale/Scale.ts                               |   2 +-
 src/scale/helper.ts                              |   9 +-
 src/util/number.ts                               |  11 +-
 test/axis-align-edge-cases.html                  | 746 +++++++++++++++++++++++
 test/axis-align-ticks-random.html                |   4 +
 test/runTest/actions/__meta__.json               |   1 +
 test/runTest/actions/axis-align-edge-cases.json  |   1 +
 test/runTest/marks/axis-align-edge-cases.json    |  10 +
 test/runTest/marks/axis-align-lastLabel.json     |   8 +
 test/runTest/marks/axis-align-ticks-random.json  |  10 +
 24 files changed, 1459 insertions(+), 382 deletions(-)

diff --git a/src/coord/axisAlignTicks.ts b/src/coord/axisAlignTicks.ts
index 3080cf69d..4c4a6f8ac 100644
--- a/src/coord/axisAlignTicks.ts
+++ b/src/coord/axisAlignTicks.ts
@@ -17,124 +17,270 @@
 * under the License.
 */
 
-import { NumericAxisBaseOptionCommon } from './axisCommonTypes';
-import { getPrecisionSafe, round } from '../util/number';
+import {
+    getAcceptableTickPrecision,
+    mathAbs, mathCeil, mathFloor, mathMax, nice, NICE_MODE_MIN, round
+} from '../util/number';
 import IntervalScale from '../scale/Interval';
-import { getScaleExtent, retrieveAxisBreaksOption } from './axisHelper';
+import { adoptScaleExtentOptionAndPrepare } from './axisHelper';
 import { AxisBaseModel } from './AxisBaseModel';
 import LogScale from '../scale/Log';
 import { warn } from '../util/log';
-import { logTransform, increaseInterval, isValueNice } from '../scale/helper';
+import {
+    increaseInterval, isLogScale, getIntervalPrecision, 
intervalScaleEnsureValidExtent,
+    logTransform,
+} from '../scale/helper';
+import { assert } from 'zrender/src/core/util';
+import { NullUndefined } from '../util/types';
 
 
 export function alignScaleTicks(
-    scale: IntervalScale | LogScale,
-    axisModel: AxisBaseModel<Pick<NumericAxisBaseOptionCommon, 'min' | 'max' | 
'breaks'>>,
+    targetScale: IntervalScale | LogScale,
+    targetDataExtent: number[],
+    targetAxisModel: AxisBaseModel,
     alignToScale: IntervalScale | LogScale
-) {
-
-    const intervalScaleProto = IntervalScale.prototype;
-
-    // NOTE: There is a precondition for log scale  here:
-    // In log scale we store _interval and _extent of exponent value.
-    // So if we use the method of InternalScale to set/get these data.
-    // It process the exponent value, which is linear and what we want here.
-    const alignToTicks = intervalScaleProto.getTicks.call(alignToScale);
-    const alignToNicedTicks = intervalScaleProto.getTicks.call(alignToScale, 
{expandToNicedExtent: true});
-    const alignToSplitNumber = alignToTicks.length - 1;
-    const alignToInterval = intervalScaleProto.getInterval.call(alignToScale);
-
-    const scaleExtent = getScaleExtent(scale, axisModel);
-    let rawExtent = scaleExtent.extent;
-    const isMinFixed = scaleExtent.fixMin;
-    const isMaxFixed = scaleExtent.fixMax;
-
-    if (scale.type === 'log') {
-        rawExtent = logTransform((scale as LogScale).base, rawExtent, true);
+): void {
+    const isTargetLogScale = isLogScale(targetScale);
+    const alignToScaleLinear = isLogScale(alignToScale) ? 
alignToScale.linearStub : alignToScale;
+
+    const alignToTicks = alignToScaleLinear.getTicks();
+    const alignToExpNiceTicks = 
alignToScaleLinear.getTicks({expandToNicedExtent: true});
+    const alignToSegCount = alignToTicks.length - 1;
+
+    if (__DEV__) {
+        // This is guards for future changes of `Interval#getTicks`.
+        assert(!alignToScale.hasBreaks() && !targetScale.hasBreaks());
+        assert(alignToSegCount > 0); // Ticks length >= 2 even on a blank 
scale.
+        assert(alignToExpNiceTicks.length === alignToTicks.length);
+        assert(alignToTicks[0].value <= alignToTicks[alignToSegCount].value);
+        assert(
+            alignToExpNiceTicks[0].value <= alignToTicks[0].value
+            && alignToTicks[alignToSegCount].value <= 
alignToExpNiceTicks[alignToSegCount].value
+        );
+        if (alignToSegCount >= 2) {
+            assert(alignToExpNiceTicks[1].value === alignToTicks[1].value);
+            assert(alignToExpNiceTicks[alignToSegCount - 1].value === 
alignToTicks[alignToSegCount - 1].value);
+        }
     }
-    scale.setBreaksFromOption(retrieveAxisBreaksOption(axisModel));
-    scale.setExtent(rawExtent[0], rawExtent[1]);
-    scale.calcNiceExtent({
-        splitNumber: alignToSplitNumber,
-        fixMin: isMinFixed,
-        fixMax: isMaxFixed
-    });
-    const extent = intervalScaleProto.getExtent.call(scale);
 
-    // Need to update the rawExtent.
-    // Because value in rawExtent may be not parsed. e.g. 'dataMin', 'dataMax'
-    if (isMinFixed) {
-        rawExtent[0] = extent[0];
+    // The Current strategy: Find a proper interval and an extent for the 
target scale to derive ticks
+    // matching exactly to ticks of `alignTo` scale.
+
+    // Adjust min, max based on the extent of alignTo. When min or max is set 
in alignTo scale
+    let t0: number; // diff ratio on min irregular segment. 0 <= t0 < 1
+    let t1: number; // diff ratio on max irregular segment. 0 <= t1 < 1
+    let alignToRegularSegCount: number; // >= 1
+    // Consider ticks of `alignTo`, only these cases below may occur:
+    if (alignToSegCount === 1) {
+        // `alignToTicks` is like:
+        //  |--|
+        // In this case, we make the corresponding 2 target ticks "nice".
+        t0 = t1 = 0;
+        alignToRegularSegCount = 1;
     }
-    if (isMaxFixed) {
-        rawExtent[1] = extent[1];
+    else if (alignToSegCount === 2) {
+        // `alignToTicks` is like:
+        //  |-|-----| or
+        //  |-----|-| or
+        //  |-----|-----|
+        // Notices that nice ticks do not necessarily exist in this case.
+        // In this case, we choose the larger segment as the "regular segment" 
and
+        // the corresponding target ticks are made "nice".
+        const interval0 = mathAbs(alignToTicks[0].value - 
alignToTicks[1].value);
+        const interval1 = mathAbs(alignToTicks[1].value - 
alignToTicks[2].value);
+        t0 = t1 = 0;
+        if (interval0 === interval1) {
+            alignToRegularSegCount = 2;
+        }
+        else {
+            alignToRegularSegCount = 1;
+            if (interval0 < interval1) {
+                t0 = interval0 / interval1;
+            }
+            else {
+                t1 = interval1 / interval0;
+            }
+        }
+    }
+    else { // alignToSegCount >= 3
+        // `alignToTicks` is like:
+        //  |-|-----|-----|-| or
+        //  |-----|-----|-| or
+        //  |-|-----|-----| or ...
+        // At least one regular segment is present, and irregular segments are 
only present on
+        // the start and/or the end.
+        // In this case, ticks corresponding to regular segments are made 
"nice".
+        const alignToInterval = alignToScaleLinear.getInterval();
+        t0 = (
+            1 - (alignToTicks[0].value - alignToExpNiceTicks[0].value) / 
alignToInterval
+        ) % 1;
+        t1 = (
+            1 - (alignToExpNiceTicks[alignToSegCount].value - 
alignToTicks[alignToSegCount].value) / alignToInterval
+        ) % 1;
+        alignToRegularSegCount = alignToSegCount - (t0 ? 1 : 0) - (t1 ? 1 : 0);
     }
 
-    let interval = intervalScaleProto.getInterval.call(scale);
-    let min: number = rawExtent[0];
-    let max: number = rawExtent[1];
+    if (__DEV__) {
+        assert(alignToRegularSegCount >= 1);
+    }
 
-    if (isMinFixed && isMaxFixed) {
-        // User set min, max, divide to get new interval
-        interval = (max - min) / alignToSplitNumber;
+    const targetExtentInfo = adoptScaleExtentOptionAndPrepare(targetScale, 
targetAxisModel, targetDataExtent);
+
+    // NOTE: If `dataZoom` has either start/end not 0% or 100% (indicated by 
`min/maxDetermined`), we consider
+    // both min and max fixed; otherwise the result is probably unexpected if 
we expand the extent out of
+    // the original min/max, e.g., the expanded extent may cross zero.
+    const hasMinMaxDetermined = targetExtentInfo.minDetermined || 
targetExtentInfo.maxDetermined;
+    const targetMinFixed = targetExtentInfo.minFixed || hasMinMaxDetermined;
+    const targetMaxFixed = targetExtentInfo.maxFixed || hasMinMaxDetermined;
+    // MEMO:
+    //  - When only `xxxAxis.min` or `xxxAxis.max` is fixed, even "nice" 
interval can be calculated, ticks
+    //    accumulated based on `min`/`max` can be "nice" only if `min` or 
`max` is "nice".
+    //  - Generating a "nice" interval in this case may cause the extent have 
both positive and negative ticks,
+    //    which may be not preferable for all positive (very common) or all 
negative series data. But it can be
+    //    simply resolved by specifying `xxxAxis.min: 0`/`xxxAxis.max: 0`, so 
we do not specially handle this
+    //    case here.
+    //  Therefore, we prioritize generating "nice" interval over preventing 
from crossing zero.
+    //  e.g., if series data are all positive and the max data is `11739`,
+    //      If setting `yAxis.max: 'dataMax'`, ticks may be like:
+    //          `11739, 8739, 5739, 2739, -1739` (not "nice" enough)
+    //      If setting `yAxis.max: 'dataMax', yAxis.min: 0`, ticks may be like:
+    //          `11739, 8805, 5870, 2935, 0` (not "nice" enough but may be 
acceptable)
+    //      If setting `yAxis.max: 12000, yAxis.min: 0`, ticks may be like:
+    //          `12000, 9000, 6000, 3000, 0` ("nice")
+
+    let targetExtent = [targetExtentInfo.min, targetExtentInfo.max];
+    if (isTargetLogScale) {
+        targetExtent = logTransform(targetScale.base, targetExtent);
     }
-    else if (isMinFixed) {
-        max = rawExtent[0] + interval * alignToSplitNumber;
-        // User set min, expand extent on the other side
-        while (max < rawExtent[1] && isFinite(max) && isFinite(rawExtent[1])) {
-            interval = increaseInterval(interval);
-            max = rawExtent[0] + interval * alignToSplitNumber;
+    targetExtent = intervalScaleEnsureValidExtent(targetExtent, {fixMax: 
targetMaxFixed});
+
+    let min: number;
+    let max: number;
+    let interval: number;
+    let intervalPrecision: number;
+    let intervalCount: number | NullUndefined;
+    let maxNice: number;
+    let minNice: number;
+
+    function loopIncreaseInterval(cb: () => boolean) {
+        // Typically this loop runs less than 5 times. But we still
+        // use a safeguard for future changes.
+        const LOOP_MAX = 50;
+        let loopGuard = 0;
+        for (; loopGuard < LOOP_MAX; loopGuard++) {
+            if (cb()) {
+                break;
+            }
+            interval = isTargetLogScale
+                // TODO: A guardcode to avoid infinite loop, but probably it
+                // should be guranteed by `LogScale` itself.
+                ? interval * mathMax(targetScale.base, 2)
+                : increaseInterval(interval);
+            intervalPrecision = getIntervalPrecision(interval);
         }
-    }
-    else if (isMaxFixed) {
-        // User set max, expand extent on the other side
-        min = rawExtent[1] - interval * alignToSplitNumber;
-        while (min > rawExtent[0] && isFinite(min) && isFinite(rawExtent[0])) {
-            interval = increaseInterval(interval);
-            min = rawExtent[1] - interval * alignToSplitNumber;
+        if (__DEV__) {
+            if (loopGuard >= LOOP_MAX) {
+                warn('incorrect impl in `alignScaleTicks`.');
+            }
         }
     }
+
+    // NOTE: The new calculated `min`/`max` must NOT shrink the original 
extent; otherwise some series
+    // data may be outside of the extent. They can expand the original extent 
slightly to align with
+    // ticks of `alignTo`. In this case, more blank space is added but 
visually fine.
+
+    if (targetMinFixed && targetMaxFixed) {
+        // Both `min` and `max` are specified (via dataZoom or ec option; 
consider both Cartesian, radar and
+        // other possible axes). In this case, "nice" ticks can hardly be 
calculated, but reasonable ticks should
+        // still be calculated whenever possible, especially 
`intervalPrecision` should be tuned for better
+        // appearance and lower cumulative error.
+
+        min = targetExtent[0];
+        max = targetExtent[1];
+        intervalCount = alignToRegularSegCount;
+        const rawInterval = (max - min) / (alignToRegularSegCount + t0 + t1);
+        // Typically axis pixel extent is ready here. See `create` in 
`Grid.ts`.
+        const axisPxExtent = targetAxisModel.axis.getExtent();
+        // NOTICE: this pxSpan may be not accurate yet due to "outerBounds" 
logic, but acceptable so far.
+        const pxSpan = mathAbs(axisPxExtent[1] - axisPxExtent[0]);
+        // We imperically choose `pxDiffAcceptable` as `0.5 / 
alignToRegularSegCount` for reduce cumulative
+        // error, otherwise a discernible misalign (> 1px) may occur.
+        // PENDING: We do not find a acceptable precision for LogScale here.
+        //  Theoretically it can be addressed but introduce more complexity. 
Is it necessary?
+        intervalPrecision = getAcceptableTickPrecision(max - min, pxSpan, 0.5 
/ alignToRegularSegCount);
+        interval = round(rawInterval, intervalPrecision);
+        maxNice = t1 ? round(max - rawInterval * t1, intervalPrecision) : max;
+        minNice = t0 ? round(min + rawInterval * t0, intervalPrecision) : min;
+    }
     else {
-        const nicedSplitNumber = scale.getTicks().length - 1;
-        if (nicedSplitNumber > alignToSplitNumber) {
-            interval = increaseInterval(interval);
-        }
+        // Make a minimal enough `interval`, increase it later.
+        // It is a similar logic as `IntervalScale#calcNiceTicks` and 
`LogScale#calcNiceTicks`.
+        // Axis break is not supported, which is guranteed by the caller of 
this function.
+        interval = nice((targetExtent[1] - targetExtent[0]) / 
alignToRegularSegCount, NICE_MODE_MIN);
+        intervalPrecision = getIntervalPrecision(interval);
 
-        const range = interval * alignToSplitNumber;
-        max = round(Math.ceil(rawExtent[1] / interval) * interval);
-        min = round(max - range);
-        // Not change the result that crossing zero.
-        if (min < 0 && rawExtent[0] >= 0) {
-            min = 0;
-            max = round(range);
+        if (targetMinFixed) {
+            min = targetExtent[0];
+            loopIncreaseInterval(function () {
+                minNice = t0 ? round(min + interval * t0, intervalPrecision) : 
min;
+                maxNice = round(minNice + interval * alignToRegularSegCount, 
intervalPrecision);
+                max = round(maxNice + interval * t1, intervalPrecision);
+                if (max >= targetExtent[1]) {
+                    return true;
+                }
+            });
         }
-        else if (max > 0 && rawExtent[1] <= 0) {
-            max = 0;
-            min = -round(range);
+        else if (targetMaxFixed) {
+            max = targetExtent[1];
+            loopIncreaseInterval(function () {
+                maxNice = t1 ? round(max - interval * t1, intervalPrecision) : 
max;
+                minNice = round(maxNice - interval * alignToRegularSegCount, 
intervalPrecision);
+                min = round(minNice - interval * t0, intervalPrecision);
+                if (min <= targetExtent[0]) {
+                    return true;
+                }
+            });
+        }
+        else {
+            // Currently we simply lay out ticks of the target scale to the 
"regular segments" of `alignTo`
+            // scale for "nice". If unexpected cases occur in future, the 
strategy can be tuned precisely
+            // (e.g., make use of irregular segments).
+            loopIncreaseInterval(function () {
+                // Consider cases that all positive or all negative, try not 
to cross zero, which is
+                // preferable in most cases.
+                if (targetExtent[1] <= 0) {
+                    maxNice = round(mathCeil(targetExtent[1] / interval) * 
interval, intervalPrecision);
+                    minNice = round(maxNice - interval * 
alignToRegularSegCount, intervalPrecision);
+                    if (minNice <= targetExtent[0]) {
+                        return true;
+                    }
+                }
+                else {
+                    minNice = round(mathFloor(targetExtent[0] / interval) * 
interval, intervalPrecision);
+                    maxNice = round(minNice + interval * 
alignToRegularSegCount, intervalPrecision);
+                    if (maxNice >= targetExtent[1]) {
+                        return true;
+                    }
+                }
+            });
+            min = round(minNice - interval * t0, intervalPrecision);
+            max = round(maxNice + interval * t1, intervalPrecision);
         }
 
+        intervalPrecision = null; // Clear for the calling of `setInterval`.
     }
 
-    // Adjust min, max based on the extent of alignTo. When min or max is set 
in alignTo scale
-    const t0 = (alignToTicks[0].value - alignToNicedTicks[0].value) / 
alignToInterval;
-    const t1 = (alignToTicks[alignToSplitNumber].value - 
alignToNicedTicks[alignToSplitNumber].value) / alignToInterval;
-
-    // NOTE: Must in setExtent -> setInterval -> setNiceExtent order.
-    intervalScaleProto.setExtent.call(scale, min + interval * t0, max + 
interval * t1);
-    intervalScaleProto.setInterval.call(scale, interval);
-    if (t0 || t1) {
-        intervalScaleProto.setNiceExtent.call(scale, min + interval, max - 
interval);
-    }
-
-    if (__DEV__) {
-        const ticks = intervalScaleProto.getTicks.call(scale);
-        if (ticks[1]
-            && (!isValueNice(interval) || getPrecisionSafe(ticks[1].value) > 
getPrecisionSafe(interval))) {
-            warn(
-                `The ticks may be not readable when set min: 
${axisModel.get('min')}, max: ${axisModel.get('max')}`
-                + ` and alignTicks: true. (${axisModel.axis?.dim}AxisIndex: 
${axisModel.componentIndex})`,
-                true
-            );
-        }
+    if (isTargetLogScale) {
+        min = targetScale.powTick(min, 0, null);
+        max = targetScale.powTick(max, 1, null);
     }
+    // NOTE: Must in setExtent -> setInterval order.
+    targetScale.setExtent(min, max);
+    targetScale.setInterval({
+        // Even in LogScale, `interval` should not be in log space.
+        interval,
+        intervalCount,
+        intervalPrecision,
+        niceExtent: [minNice, maxNice]
+    });
 }
diff --git a/src/coord/axisCommonTypes.ts b/src/coord/axisCommonTypes.ts
index 4291a0729..944ad5949 100644
--- a/src/coord/axisCommonTypes.ts
+++ b/src/coord/axisCommonTypes.ts
@@ -210,10 +210,10 @@ export interface ValueAxisBaseOption extends 
NumericAxisBaseOptionCommon {
 
     /**
      * Optional value can be:
-     * + `false`: always include value 0.
+     * + `false`: always include value 0 if not conflict with `axis.min/max` 
setting.
      * + `true`: the axis may not contain zero position.
      */
-     scale?: boolean;
+    scale?: boolean;
 }
 export interface LogAxisBaseOption extends NumericAxisBaseOptionCommon {
     type?: 'log';
diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts
index 24cb773ee..301bab7a9 100644
--- a/src/coord/axisHelper.ts
+++ b/src/coord/axisHelper.ts
@@ -46,26 +46,35 @@ import CartesianAxisModel from './cartesian/AxisModel';
 import SeriesData from '../data/SeriesData';
 import { getStackedDimension } from '../data/helper/dataStackHelper';
 import { Dictionary, DimensionName, ScaleTick } from '../util/types';
-import { ensureScaleRawExtentInfo } from './scaleRawExtentInfo';
+import { ensureScaleRawExtentInfo, ScaleRawExtentResult } from 
'./scaleRawExtentInfo';
 import { parseTimeAxisLabelFormatter } from '../util/time';
 import { getScaleBreakHelper } from '../scale/break';
 import { error } from '../util/log';
+import { isIntervalScale, isTimeScale } from '../scale/helper';
 
 
 type BarWidthAndOffset = ReturnType<typeof makeColumnLayout>;
 
 /**
- * Get axis scale extent before niced.
+ * Prepare axis scale extent before niced.
  * Item of returned array can only be number (including Infinity and NaN).
  *
- * Caution:
- * Precondition of calling this method:
- * The scale extent has been initialized using series data extent via
- * `scale.setExtent` or `scale.unionExtentFromData`;
+ * CAVEAT:
+ *  This function has side-effect.
+ *
+ * FIXME:
+ *  Refector to decouple `unionExtentFromData` and irregular value handling 
from `scale`.
+ *  Merge `unionAxisExtentFromData` and `unionExtentFromData`.
+ *  Refector `ensureScaleRawExtentInfo`.
  */
-export function getScaleExtent(scale: Scale, model: AxisBaseModel) {
-    const scaleType = scale.type;
-    const rawExtentResult = ensureScaleRawExtentInfo(scale, model, 
scale.getExtent()).calculate();
+export function adoptScaleExtentOptionAndPrepare(
+    scale: Scale,
+    model: AxisBaseModel,
+    // Typically: data extent from all series on this axis.
+    // Can be obtained by `scale.unionExtentFromData(); scale.getExtent()`;
+    dataExtent: number[]
+): ScaleRawExtentResult {
+    const rawExtentResult = ensureScaleRawExtentInfo(scale, model, 
dataExtent).calculate();
 
     scale.setBlank(rawExtentResult.isBlank);
 
@@ -82,7 +91,7 @@ export function getScaleExtent(scale: Scale, model: 
AxisBaseModel) {
     // (4) Consider other chart types using `barGrid`?
     // See #6728, #4862, `test/bar-overflow-time-plot.html`
     const ecModel = model.ecModel;
-    if (ecModel && (scaleType === 'time' /* || scaleType === 'interval' */)) {
+    if (ecModel && (isTimeScale(scale) /* || scaleType === 'interval' */)) {
         const barSeriesModels = prepareLayoutBarSeries('bar', ecModel);
         let isBaseAxisAndHasBarSeries = false;
 
@@ -102,13 +111,10 @@ export function getScaleExtent(scale: Scale, model: 
AxisBaseModel) {
         }
     }
 
-    return {
-        extent: [min, max],
-        // "fix" means "fixed", the value should not be
-        // changed in the subsequent steps.
-        fixMin: rawExtentResult.minFixed,
-        fixMax: rawExtentResult.maxFixed
-    };
+    rawExtentResult.min = min;
+    rawExtentResult.max = max;
+
+    return rawExtentResult;
 }
 
 function adjustScaleForOverflow(
@@ -151,32 +157,25 @@ function adjustScaleForOverflow(
     return {min: min, max: max};
 }
 
-// Precondition of calling this method:
-// The scale extent has been initialized using series data extent via
-// `scale.setExtent` or `scale.unionExtentFromData`;
 export function niceScaleExtent(
     scale: Scale,
-    inModel: AxisBaseModel
-) {
+    inModel: AxisBaseModel,
+    // Typically: data extent from all series on this axis, which can be 
obtained by
+    //  `scale.unionExtentFromData(...); scale.getExtent();`.
+    dataExtent: number[],
+): void {
     const model = inModel as AxisBaseModel<LogAxisBaseOption>;
-    const extentInfo = getScaleExtent(scale, model);
-    const extent = extentInfo.extent;
-    const splitNumber = model.get('splitNumber');
-
-    if (scale instanceof LogScale) {
-        scale.base = model.get('logBase');
-    }
+    const extentInfo = adoptScaleExtentOptionAndPrepare(scale, model, 
dataExtent);
 
-    const scaleType = scale.type;
-    const interval = model.get('interval');
-    const isIntervalOrTime = scaleType === 'interval' || scaleType === 'time';
+    const isInterval = isIntervalScale(scale);
+    const isIntervalOrTime = isInterval || isTimeScale(scale);
 
     scale.setBreaksFromOption(retrieveAxisBreaksOption(model));
-    scale.setExtent(extent[0], extent[1]);
+    scale.setExtent(extentInfo.min, extentInfo.max);
     scale.calcNiceExtent({
-        splitNumber: splitNumber,
-        fixMin: extentInfo.fixMin,
-        fixMax: extentInfo.fixMax,
+        splitNumber: model.get('splitNumber'),
+        fixMin: extentInfo.minFixed,
+        fixMax: extentInfo.maxFixed,
         minInterval: isIntervalOrTime ? model.get('minInterval') : null,
         maxInterval: isIntervalOrTime ? model.get('maxInterval') : null
     });
@@ -185,36 +184,35 @@ export function niceScaleExtent(
     // is not good enough. He can specify the interval. It is often appeared
     // in angle axis with angle 0 - 360. Interval calculated in interval scale 
is hard
     // to be 60.
-    // FIXME
-    if (interval != null) {
-        (scale as IntervalScale).setInterval && (scale as 
IntervalScale).setInterval(interval);
+    // In `xxxAxis.type: 'log'`, ec option `xxxAxis.interval` requires a 
logarithm-applied
+    // value rather than a value in the raw scale.
+    const interval = model.get('interval');
+    if (interval != null && (scale as IntervalScale).setInterval) {
+        (scale as IntervalScale).setInterval({interval});
     }
 }
 
-/**
- * @param axisType Default retrieve from model.type
- */
-export function createScaleByModel(model: AxisBaseModel, axisType?: string): 
Scale {
-    axisType = axisType || model.get('type');
-    if (axisType) {
-        switch (axisType) {
-            // Buildin scale
-            case 'category':
-                return new OrdinalScale({
-                    ordinalMeta: model.getOrdinalMeta
-                        ? model.getOrdinalMeta()
-                        : model.getCategories(),
-                    extent: [Infinity, -Infinity]
-                });
-            case 'time':
-                return new TimeScale({
-                    locale: model.ecModel.getLocaleModel(),
-                    useUTC: model.ecModel.get('useUTC'),
-                });
-            default:
-                // case 'value'/'interval', 'log', or others.
-                return new (Scale.getClass(axisType) || IntervalScale)();
-        }
+export function createScaleByModel(model: AxisBaseModel): Scale {
+    const axisType = model.get('type');
+    switch (axisType) {
+        case 'category':
+            return new OrdinalScale({
+                ordinalMeta: model.getOrdinalMeta
+                    ? model.getOrdinalMeta()
+                    : model.getCategories(),
+                extent: [Infinity, -Infinity]
+            });
+        case 'time':
+            return new TimeScale({
+                locale: model.ecModel.getLocaleModel(),
+                useUTC: model.ecModel.get('useUTC'),
+            });
+        case 'log':
+            // See also #3749
+            return new LogScale((model as 
AxisBaseModel<LogAxisBaseOption>).get('logBase'));
+        default:
+            // case 'value'/'interval', or others.
+            return new (Scale.getClass(axisType) || IntervalScale)();
     }
 }
 
@@ -303,7 +301,6 @@ export function getAxisRawValue<TIsCategory extends 
boolean>(axis: Axis, tick: S
 
 /**
  * @param model axisLabelModel or axisTickModel
- * @return {number|String} Can be null|'auto'|number|function
  */
 export function getOptionCategoryInterval(
     model: Model<AxisBaseOption['axisLabel']>
diff --git a/src/coord/axisModelCommonMixin.ts 
b/src/coord/axisModelCommonMixin.ts
index 57adacfb1..ed8b7c45a 100644
--- a/src/coord/axisModelCommonMixin.ts
+++ b/src/coord/axisModelCommonMixin.ts
@@ -31,8 +31,7 @@ interface AxisModelCommonMixin<Opt extends AxisBaseOption> 
extends Pick<Model<Op
 class AxisModelCommonMixin<Opt extends AxisBaseOption> {
 
     getNeedCrossZero(): boolean {
-        const option = this.option as ValueAxisBaseOption;
-        return !option.scale;
+        return !(this.option as ValueAxisBaseOption).scale;
     }
 
     /**
diff --git a/src/coord/cartesian/Grid.ts b/src/coord/cartesian/Grid.ts
index 97ac081bf..2b695c3df 100644
--- a/src/coord/cartesian/Grid.ts
+++ b/src/coord/cartesian/Grid.ts
@@ -32,6 +32,7 @@ import {
     getDataDimensionsOnAxis,
     isNameLocationCenter,
     shouldAxisShow,
+    retrieveAxisBreaksOption,
 } from '../../coord/axisHelper';
 import Cartesian2D, {cartesian2DDimensions} from './Cartesian2D';
 import Axis2D from './Axis2D';
@@ -124,50 +125,32 @@ class Grid implements CoordinateSystemMaster {
         this._updateScale(ecModel, this.model);
 
         function updateAxisTicks(axes: Record<number, Axis2D>) {
-            let alignTo: Axis2D;
             // Axis is added in order of axisIndex.
             const axesIndices = keys(axes);
-            const len = axesIndices.length;
-            if (!len) {
-                return;
-            }
             const axisNeedsAlign: Axis2D[] = [];
-            // Process once and calculate the ticks for those don't use 
alignTicks.
-            for (let i = len - 1; i >= 0; i--) {
-                const idx = +axesIndices[i];    // Convert to number.
-                const axis = axes[idx];
-                const model = axis.model as 
AxisBaseModel<NumericAxisBaseOptionCommon>;
-                const scale = axis.scale;
-                if (// Only value and log axis without interval support 
alignTicks.
-                    isIntervalOrLogScale(scale)
-                    && model.get('alignTicks')
-                    && model.get('interval') == null
-                ) {
+
+            for (let i = axesIndices.length - 1; i >= 0; i--) { // Reverse 
order
+                const axis = axes[+axesIndices[i]];
+                if (axis.alignTo) {
                     axisNeedsAlign.push(axis);
                 }
                 else {
-                    niceScaleExtent(scale, model);
-                    if (isIntervalOrLogScale(scale)) {  // Can only align to 
interval or log axis.
-                        alignTo = axis;
-                    }
+                    niceScaleExtent(axis.scale, axis.model, 
axis.scale.getExtent());
                 }
             };
-            // All axes has set alignTicks. Pick the first one.
-            // PENDING. Should we find the axis that both set interval, min, 
max and align to this one?
-            if (axisNeedsAlign.length) {
-                if (!alignTo) {
-                    alignTo = axisNeedsAlign.pop();
-                    niceScaleExtent(alignTo.scale, alignTo.model);
+            each(axisNeedsAlign, axis => {
+                if (incapableOfAlignNeedFallback(axis, axis.alignTo as 
Axis2D)) {
+                    niceScaleExtent(axis.scale, axis.model, 
axis.scale.getExtent());
                 }
-
-                each(axisNeedsAlign, axis => {
+                else {
                     alignScaleTicks(
                         axis.scale as IntervalScale | LogScale,
+                        axis.scale.getExtent(),
                         axis.model,
-                        alignTo.scale as IntervalScale | LogScale
+                        axis.alignTo.scale as IntervalScale | LogScale
                     );
-                });
-            }
+                }
+            });
         }
 
         updateAxisTicks(axesMap.x);
@@ -450,6 +433,9 @@ class Grid implements CoordinateSystemMaster {
             });
         });
 
+        prepareAlignToInCoordSysCreate(axesMap.x);
+        prepareAlignToInCoordSysCreate(axesMap.y);
+
         function createAxisCreator(dimName: Cartesian2DDimensionName) {
             return function (axisModel: CartesianAxisModel, idx: number): void 
{
                 if (!isAxisUsedInTheGrid(axisModel, gridModel)) {
@@ -698,6 +684,66 @@ function canOnZeroToAxis(axis: Axis2D): boolean {
     return axis && axis.type !== 'category' && axis.type !== 'time' && 
ifAxisCrossZero(axis);
 }
 
+/**
+ * [CAVEAT] This method is called before data processing stage.
+ *  Do not rely on any info that is determined afterward.
+ */
+function prepareAlignToInCoordSysCreate(axes: Record<number, Axis2D>): void {
+    // Axis is added in order of axisIndex.
+    const axesIndices = keys(axes);
+
+    let alignTo: Axis2D;
+    const axisNeedsAlign: Axis2D[] = [];
+
+    for (let i = axesIndices.length - 1; i >= 0; i--) { // Reverse order
+        const axis = axes[+axesIndices[i]];
+        if (
+            isIntervalOrLogScale(axis.scale)
+            // NOTE: `scale.hasBreaks()` is not available at this moment. 
Check it later.
+            && retrieveAxisBreaksOption(axis.model) == null
+            // NOTE: `scale.getTicks()` is not available at this moment. Check 
it later.
+        ) {
+            // Request `alignTicks`.
+            if ((axis.model as 
AxisBaseModel<NumericAxisBaseOptionCommon>).get('alignTicks')
+                && (axis.model as 
AxisBaseModel<NumericAxisBaseOptionCommon>).get('interval') == null
+            ) {
+                axisNeedsAlign.push(axis);
+            }
+            else {
+                // `alignTo` the last one that does not request `alignTicks`
+                // (This rule is retained for backward compat).
+                alignTo = axis;
+            }
+        }
+    };
+    // If all axes has set alignTicks, pick the first one as alignTo.
+    // PENDING. Should we find the axis that both set interval, min, max and 
align to this one?
+    // PENDING. Should we allow specifying alignTo via ec option?
+    if (!alignTo) {
+        alignTo = axisNeedsAlign.pop();
+    }
+    if (alignTo) {
+        each(axisNeedsAlign, function (axis) {
+            axis.alignTo = alignTo;
+        });
+    }
+}
+
+/**
+ *  This is just a defence code. They are unlikely to be actually `true`,
+ *  since these cases have been addressed in `prepareAlignToInCoordSysCreate`.
+ *
+ *  Can not be called BEFORE "nice" performed.
+ */
+function incapableOfAlignNeedFallback(targetAxis: Axis2D, alignTo: Axis2D): 
boolean {
+    return targetAxis.scale.hasBreaks()
+        || alignTo.scale.hasBreaks()
+        // Normally ticks length are more than 2 even when axis is blank.
+        // But still guard for corner cases and possible changes.
+        || alignTo.scale.getTicks().length < 2;
+}
+
+
 function updateAxisTransform(axis: Axis2D, coordBase: number) {
     const axisExtent = axis.getExtent();
     const axisExtentSum = axisExtent[0] + axisExtent[1];
diff --git a/src/coord/cartesian/defaultAxisExtentFromData.ts 
b/src/coord/cartesian/defaultAxisExtentFromData.ts
index ca4bdcbac..76e0cdac9 100644
--- a/src/coord/cartesian/defaultAxisExtentFromData.ts
+++ b/src/coord/cartesian/defaultAxisExtentFromData.ts
@@ -241,7 +241,7 @@ function shrinkAxisExtent(axisRecordMap: 
HashMap<AxisRecord>) {
         if (tarAxisExtent) {
             const rawExtentResult = axisRecord.rawExtentResult;
             const rawExtentInfo = axisRecord.rawExtentInfo;
-            // Shink the original extent.
+            // Shrink the original extent.
             if (!rawExtentResult.minFixed && tarAxisExtent[0] > 
rawExtentResult.min) {
                 rawExtentInfo.modifyDataMinMax('min', tarAxisExtent[0]);
             }
diff --git a/src/coord/parallel/Parallel.ts b/src/coord/parallel/Parallel.ts
index 6566cb121..38ad006ca 100644
--- a/src/coord/parallel/Parallel.ts
+++ b/src/coord/parallel/Parallel.ts
@@ -23,13 +23,13 @@
  * <https://en.wikipedia.org/wiki/Parallel_coordinates>
  */
 
-import * as zrUtil from 'zrender/src/core/util';
+import {each, createHashMap, clone} from 'zrender/src/core/util';
 import * as matrix from 'zrender/src/core/matrix';
 import * as layoutUtil from '../../util/layout';
 import * as axisHelper from '../../coord/axisHelper';
 import ParallelAxis from './ParallelAxis';
 import * as graphic from '../../util/graphic';
-import * as numberUtil from '../../util/number';
+import {mathCeil, mathFloor, mathMax, mathMin, mathPI, round} from 
'../../util/number';
 import sliderMove from '../../component/helper/sliderMove';
 import ParallelModel, { ParallelLayoutDirection } from './ParallelModel';
 import GlobalModel from '../../model/Global';
@@ -41,13 +41,6 @@ import SeriesData from '../../data/SeriesData';
 import { AxisBaseModel } from '../AxisBaseModel';
 import { CategoryAxisBaseOption } from '../axisCommonTypes';
 
-const each = zrUtil.each;
-const mathMin = Math.min;
-const mathMax = Math.max;
-const mathFloor = Math.floor;
-const mathCeil = Math.ceil;
-const round = numberUtil.round;
-const PI = Math.PI;
 
 interface ParallelCoordinateSystemLayoutInfo {
     layout: ParallelLayoutDirection;
@@ -85,7 +78,7 @@ class Parallel implements CoordinateSystemMaster, 
CoordinateSystem {
     /**
      * key: dimension
      */
-    private _axesMap = zrUtil.createHashMap<ParallelAxis>();
+    private _axesMap = createHashMap<ParallelAxis>();
 
     /**
      * key: dimension
@@ -191,7 +184,7 @@ class Parallel implements CoordinateSystemMaster, 
CoordinateSystem {
         // do after all series processed
         each(this.dimensions, function (dim) {
             const axis = this._axesMap.get(dim);
-            axisHelper.niceScaleExtent(axis.scale, axis.model);
+            axisHelper.niceScaleExtent(axis.scale, axis.model, 
axis.scale.getExtent());
         }, this);
     }
 
@@ -304,7 +297,7 @@ class Parallel implements CoordinateSystemMaster, 
CoordinateSystem {
                 }
             };
             const rotationTable = {
-                horizontal: PI / 2,
+                horizontal: mathPI / 2,
                 vertical: 0
             };
 
@@ -373,7 +366,7 @@ class Parallel implements CoordinateSystemMaster, 
CoordinateSystem {
         const dataDimensions = [] as DimensionName[];
         const axisModels = [] as ParallelAxisModel[];
 
-        zrUtil.each(dimensions, function (axisDim) {
+        each(dimensions, function (axisDim) {
             dataDimensions.push(data.mapDimension(axisDim));
             axisModels.push(axesMap.get(axisDim).model);
         });
@@ -433,7 +426,7 @@ class Parallel implements CoordinateSystemMaster, 
CoordinateSystem {
      * Get axis layout.
      */
     getAxisLayout(dim: DimensionName): ParallelAxisLayoutInfo {
-        return zrUtil.clone(this._axesLayout[dim]);
+        return clone(this._axesLayout[dim]);
     }
 
     /**
diff --git a/src/coord/polar/polarCreator.ts b/src/coord/polar/polarCreator.ts
index 224de9c3b..8a7515896 100644
--- a/src/coord/polar/polarCreator.ts
+++ b/src/coord/polar/polarCreator.ts
@@ -81,24 +81,26 @@ function updatePolarScale(this: Polar, ecModel: 
GlobalModel, api: ExtensionAPI)
     const polar = this;
     const angleAxis = polar.getAngleAxis();
     const radiusAxis = polar.getRadiusAxis();
+    const angleScale = angleAxis.scale;
+    const radiusScale = radiusAxis.scale;
     // Reset scale
-    angleAxis.scale.setExtent(Infinity, -Infinity);
-    radiusAxis.scale.setExtent(Infinity, -Infinity);
+    angleScale.setExtent(Infinity, -Infinity);
+    radiusScale.setExtent(Infinity, -Infinity);
 
     ecModel.eachSeries(function (seriesModel) {
         if (seriesModel.coordinateSystem === polar) {
             const data = seriesModel.getData();
             zrUtil.each(getDataDimensionsOnAxis(data, 'radius'), function 
(dim) {
-                radiusAxis.scale.unionExtentFromData(data, dim);
+                radiusScale.unionExtentFromData(data, dim);
             });
             zrUtil.each(getDataDimensionsOnAxis(data, 'angle'), function (dim) 
{
-                angleAxis.scale.unionExtentFromData(data, dim);
+                angleScale.unionExtentFromData(data, dim);
             });
         }
     });
 
-    niceScaleExtent(angleAxis.scale, angleAxis.model);
-    niceScaleExtent(radiusAxis.scale, radiusAxis.model);
+    niceScaleExtent(angleScale, angleAxis.model, angleScale.getExtent());
+    niceScaleExtent(radiusScale, radiusAxis.model, radiusScale.getExtent());
 
     // Fix extent of category angle axis
     if (angleAxis.type === 'category' && !angleAxis.onBand) {
diff --git a/src/coord/radar/Radar.ts b/src/coord/radar/Radar.ts
index c0fb4df29..02d046585 100644
--- a/src/coord/radar/Radar.ts
+++ b/src/coord/radar/Radar.ts
@@ -174,11 +174,12 @@ class Radar implements CoordinateSystem, 
CoordinateSystemMaster {
         const splitNumber = radarModel.get('splitNumber');
         const dummyScale = new IntervalScale();
         dummyScale.setExtent(0, splitNumber);
-        dummyScale.setInterval(1);
+        dummyScale.setInterval({interval: 1});
         // Force all the axis fixing the maxSplitNumber.
         each(indicatorAxes, function (indicatorAxis, idx) {
             alignScaleTicks(
                 indicatorAxis.scale as IntervalScale,
+                indicatorAxis.scale.getExtent(),
                 indicatorAxis.model,
                 dummyScale
             );
diff --git a/src/coord/scaleRawExtentInfo.ts b/src/coord/scaleRawExtentInfo.ts
index 389b98c3b..8ff4e78a0 100644
--- a/src/coord/scaleRawExtentInfo.ts
+++ b/src/coord/scaleRawExtentInfo.ts
@@ -21,8 +21,11 @@ import { assert, isArray, eqNaN, isFunction } from 
'zrender/src/core/util';
 import Scale from '../scale/Scale';
 import { AxisBaseModel } from './AxisBaseModel';
 import { parsePercent } from 'zrender/src/contain/text';
-import { AxisBaseOption, CategoryAxisBaseOption, NumericAxisBaseOptionCommon } 
from './axisCommonTypes';
+import {
+    AxisBaseOption, CategoryAxisBaseOption, NumericAxisBaseOptionCommon, 
ValueAxisBaseOption
+} from './axisCommonTypes';
 import { ScaleDataValue } from '../util/types';
+import { isIntervalScale, isLogScale, isOrdinalScale, isTimeScale } from 
'../scale/helper';
 
 
 export interface ScaleRawExtentResult {
@@ -32,19 +35,27 @@ export interface ScaleRawExtentResult {
     // a little (say, "nice strategy", e.g., niceScale, boundaryGap).
     // Ensure `min`/`max` be finite number or NaN here.
     // (not to be null/undefined) `NaN` means min/max axis is blank.
-    readonly min: number;
-    readonly max: number;
-    // `minFixed`/`maxFixed` marks that `min`/`max` should be used
-    // in the final extent without other "nice strategy".
+    min: number;
+    max: number;
+
+    // `minFixed`/`maxFixed` marks that:
+    //  - `xxxAxis.min/max` are user specified, or
+    //  - `minDetermined/maxDetermined` are `true`
+    // so it should be used directly in the final extent without any other 
"nice strategy".
     readonly minFixed: boolean;
     readonly maxFixed: boolean;
+
+    // Typically set by `dataZoom` when its start/end is not 0%/100%.
+    readonly minDetermined: boolean;
+    readonly maxDetermined: boolean;
+
     // Mark that the axis should be blank.
     readonly isBlank: boolean;
 }
 
 export class ScaleRawExtentInfo {
 
-    private _needCrossZero: boolean;
+    private _needCrossZero: ValueAxisBaseOption['scale'];
     private _isOrdinal: boolean;
     private _axisDataLen: number;
     private _boundaryGapInner: number[];
@@ -62,6 +73,7 @@ export class ScaleRawExtentInfo {
     private _dataMin: number;
     private _dataMax: number;
 
+    // Typically specified by `dataZoom` when its start/end is not 0%/100%.
     // Highest priority if specified.
     private _determinedMin: number;
     private _determinedMax: number;
@@ -76,10 +88,10 @@ export class ScaleRawExtentInfo {
     constructor(
         scale: Scale,
         model: AxisBaseModel,
-        // Usually: data extent from all series on this axis.
-        originalExtent: number[]
+        // Typically: data extent from all series on this axis.
+        dataExtent: number[]
     ) {
-        this._prepareParams(scale, model, originalExtent);
+        this._prepareParams(scale, model, dataExtent);
     }
 
     /**
@@ -98,10 +110,10 @@ export class ScaleRawExtentInfo {
         this._dataMin = dataExtent[0];
         this._dataMax = dataExtent[1];
 
-        const isOrdinal = this._isOrdinal = scale.type === 'ordinal';
-        this._needCrossZero = scale.type === 'interval' && 
model.getNeedCrossZero && model.getNeedCrossZero();
+        const isOrdinal = this._isOrdinal = isOrdinalScale(scale);
+        this._needCrossZero = isIntervalScale(scale) && model.getNeedCrossZero 
&& model.getNeedCrossZero();
 
-        if (scale.type === 'interval' || scale.type === 'log' || scale.type 
=== 'time') {
+        if (isIntervalScale(scale) || isLogScale(scale) || isTimeScale(scale)) 
{
             // Process custom dataMin/dataMax
             const dataMinRaw = (model as 
AxisBaseModel<NumericAxisBaseOptionCommon>).get('dataMin', true);
             if (dataMinRaw != null) {
@@ -255,15 +267,22 @@ export class ScaleRawExtentInfo {
             // If so, here `minFixed`/`maxFixed` need to be set.
         }
 
+        // NOTE: Switching `min/maxFixed` probably leads to abrupt extent 
changes when draging a `dataZoom`
+        // handle, since minFixed/maxFixed impact the "nice extent" and "nice 
ticks" calculation. Consider
+        // the case that dataZoom `start` is greater than 0% but its `end` is 
100%, (or vice versa), we
+        // currently only set `minFixed` as `true` but remain `maxFixed` as 
`false` to avoid unnecessary
+        // abrupt change. Incidentally, the effect is not unacceptable if we 
set both `min/maxFixed` as `true`.
         const determinedMin = this._determinedMin;
         const determinedMax = this._determinedMax;
+        let minDetermined = false;
+        let maxDetermined = false;
         if (determinedMin != null) {
             min = determinedMin;
-            minFixed = true;
+            minFixed = minDetermined = true;
         }
         if (determinedMax != null) {
             max = determinedMax;
-            maxFixed = true;
+            maxFixed = maxDetermined = true;
         }
 
         // Ensure min/max be finite number or NaN here. (not to be 
null/undefined)
@@ -273,7 +292,9 @@ export class ScaleRawExtentInfo {
             max: max,
             minFixed: minFixed,
             maxFixed: maxFixed,
-            isBlank: isBlank
+            minDetermined: minDetermined,
+            maxDetermined: maxDetermined,
+            isBlank: isBlank,
         };
     }
 
@@ -323,8 +344,11 @@ const DATA_MIN_MAX_ATTR = { min: '_dataMin', max: 
'_dataMax' } as const;
 export function ensureScaleRawExtentInfo(
     scale: Scale,
     model: AxisBaseModel,
-    // Usually: data extent from all series on this axis.
-    originalExtent: number[]
+    // Typically: data extent from all series on this axis.
+    // FIXME:
+    //  Refactor: only the first input `dataExtent` is used but it is 
determined by the
+    //  caller, which is error-prone.
+    dataExtent: number[]
 ): ScaleRawExtentInfo {
 
     // Do not permit to recreate.
@@ -333,7 +357,7 @@ export function ensureScaleRawExtentInfo(
         return rawExtentInfo;
     }
 
-    rawExtentInfo = new ScaleRawExtentInfo(scale, model, originalExtent);
+    rawExtentInfo = new ScaleRawExtentInfo(scale, model, dataExtent);
     // @ts-ignore
     scale.rawExtentInfo = rawExtentInfo;
 
diff --git a/src/coord/single/Single.ts b/src/coord/single/Single.ts
index 80e2ac04c..5610fe7d2 100644
--- a/src/coord/single/Single.ts
+++ b/src/coord/single/Single.ts
@@ -99,10 +99,12 @@ class Single implements CoordinateSystem, 
CoordinateSystemMaster {
         ecModel.eachSeries(function (seriesModel) {
             if (seriesModel.coordinateSystem === this) {
                 const data = seriesModel.getData();
+                const axis = this._axis;
+                const scale = axis.scale;
                 each(data.mapDimensionsAll(this.dimension), function (dim) {
-                    this._axis.scale.unionExtentFromData(data, dim);
-                }, this);
-                axisHelper.niceScaleExtent(this._axis.scale, this._axis.model);
+                    scale.unionExtentFromData(data, dim);
+                });
+                axisHelper.niceScaleExtent(scale, axis.model, 
scale.getExtent());
             }
         }, this);
     }
diff --git a/src/export/api/helper.ts b/src/export/api/helper.ts
index 92a4ddf66..7ef631e78 100644
--- a/src/export/api/helper.ts
+++ b/src/export/api/helper.ts
@@ -96,19 +96,12 @@ export function createScale(dataExtent: number[], option: 
object | AxisBaseModel
     const scale = axisHelper.createScaleByModel(axisModel as AxisBaseModel);
     scale.setExtent(dataExtent[0], dataExtent[1]);
 
-    axisHelper.niceScaleExtent(scale, axisModel as AxisBaseModel);
+    axisHelper.niceScaleExtent(scale, axisModel as AxisBaseModel, 
scale.getExtent());
     return scale;
 }
 
 /**
- * Mixin common methods to axis model,
- *
- * Include methods
- * `getFormattedLabels() => Array.<string>`
- * `getCategories() => Array.<string>`
- * `getMin(origin: boolean) => number`
- * `getMax(origin: boolean) => number`
- * `getNeedCrossZero() => boolean`
+ * Mixin common methods to axis model
  */
 export function mixinAxisModelCommonMethods(Model: Model) {
     zrUtil.mixin(Model, AxisModelCommonMixin);
diff --git a/src/scale/Interval.ts b/src/scale/Interval.ts
index 917aa3f53..98ca9daec 100644
--- a/src/scale/Interval.ts
+++ b/src/scale/Interval.ts
@@ -18,14 +18,13 @@
 */
 
 
-import * as numberUtil from '../util/number';
-import * as formatUtil from '../util/format';
+import {round, mathRound, mathMin, getPrecision, mathCeil, mathFloor} from 
'../util/number';
+import {addCommas} from '../util/format';
 import Scale, { ScaleGetTicksOpt, ScaleSettingDefault } from './Scale';
 import * as helper from './helper';
-import {ScaleTick, ParsedAxisBreakList, ScaleDataValue} from '../util/types';
+import {ScaleTick, ParsedAxisBreakList, ScaleDataValue, NullUndefined} from 
'../util/types';
 import { getScaleBreakHelper } from './break';
-
-const roundNumber = numberUtil.round;
+import { assert } from 'zrender/src/core/util';
 
 class IntervalScale<SETTING extends ScaleSettingDefault = ScaleSettingDefault> 
extends Scale<SETTING> {
 
@@ -34,8 +33,15 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
 
     // Step is calculated in adjustExtent.
     protected _interval: number = 0;
-    protected _niceExtent: [number, number];
     protected _intervalPrecision: number = 2;
+    // `_intervalCount` effectively specifies the number of "nice segment". 
This is for special cases,
+    // such as `alignTo: true` and min max are fixed. In this case, 
`_interval` may be specified with
+    // a "not-nice" value and needs to be rounded with `_intervalPrecision` 
for better appearance. Then
+    // merely accumulating `_interval` may generate incorrect number of ticks. 
So `_intervalCount` is
+    // required to specify the expected tick number.
+    private _intervalCount: number | NullUndefined = undefined;
+    // Should ensure: `_extent[0] <= _niceExtent[0] && _niceExtent[1] <= 
_extent[1]`
+    protected _niceExtent: [number, number];
 
 
     parse(val: ScaleDataValue): number {
@@ -81,16 +87,61 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
         return this._interval;
     }
 
-    setInterval(interval: number): void {
+    /**
+     * @final override is DISALLOWED.
+     */
+    setInterval({interval, intervalCount, intervalPrecision, niceExtent}: {
+        interval?: number | NullUndefined;
+        intervalCount?: number | NullUndefined;
+        intervalPrecision?: number | NullUndefined;
+        niceExtent?: number[];
+    }): void {
+        const intervalCountSpecified = intervalCount != null;
+        if (__DEV__) {
+            assert(interval != null);
+            if (intervalCountSpecified) {
+                assert(
+                    intervalCount > 0
+                    && intervalPrecision != null
+                    // Do not support intervalCount on axis break currently.
+                    && !this.hasBreaks()
+                );
+            }
+        }
+
+        const extent = this._extent;
+        if (__DEV__) {
+            if (niceExtent != null) {
+                assert(
+                    isFinite(niceExtent[0]) && isFinite(niceExtent[1])
+                    && extent[0] <= niceExtent[0] && niceExtent[1] <= extent[1]
+                );
+            }
+        }
+        niceExtent = this._niceExtent = niceExtent != null
+            ? niceExtent.slice() as [number, number]
+            // Dropped the auto calculated niceExtent and use user-set extent.
+            // We assume users want to set both interval and extent to get a 
better result.
+            : extent.slice() as [number, number];
+
         this._interval = interval;
-        // Dropped auto calculated niceExtent and use user-set extent.
-        // We assume user wants to set both interval, min, max to get a better 
result.
-        this._niceExtent = this._extent.slice() as [number, number];
 
-        this._intervalPrecision = helper.getIntervalPrecision(interval);
+        if (!intervalCountSpecified) {
+            // This is for cases of "nice" interval.
+            this._intervalCount = undefined; // Clear
+            this._intervalPrecision = helper.getIntervalPrecision(interval);
+        }
+        else {
+            // This is for cases of "not-nice" interval, typically min max are 
fixed and
+            // axis alignment is required.
+            this._intervalCount = intervalCount;
+            this._intervalPrecision = intervalPrecision;
+        }
     }
 
     /**
+     * In ascending order.
+     *
      * @override
      */
     getTicks(opt?: ScaleGetTicksOpt): ScaleTick[] {
@@ -112,13 +163,15 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
             return ticks;
         }
 
+        // [CAVEAT]: If changing this logic, must sync it to 
`axisAlignTicks.ts`.
+
         // Consider this case: using dataZoom toolbox, zoom and zoom.
         const safeLimit = 10000;
 
         if (extent[0] < niceTickExtent[0]) {
             if (opt.expandToNicedExtent) {
                 ticks.push({
-                    value: roundNumber(niceTickExtent[0] - interval, 
intervalPrecision)
+                    value: round(niceTickExtent[0] - interval, 
intervalPrecision)
                 });
             }
             else {
@@ -129,21 +182,43 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
         }
 
         const estimateNiceMultiple = (tickVal: number, targetTick: number) => {
-            return Math.round((targetTick - tickVal) / interval);
+            return mathRound((targetTick - tickVal) / interval);
         };
 
-        let tick = niceTickExtent[0];
-        while (tick <= niceTickExtent[1]) {
+        const intervalCount = this._intervalCount;
+        for (
+            let tick = niceTickExtent[0], niceTickIdx = 0;
+            ;
+            niceTickIdx++
+        ) {
+            if (intervalCount == null) {
+                if (tick > niceTickExtent[1]) {
+                    break;
+                }
+            }
+            else {
+                if (niceTickIdx > intervalCount) { // ticks number should be 
`intervalCount + 1`
+                    break;
+                }
+                // Consider cumulative error, especially caused by rounding, 
the last nice
+                // `tick` may be less than or greater than `niceTickExtent[1]` 
slightly.
+                tick = mathMin(tick, niceTickExtent[1]);
+                if (niceTickIdx === intervalCount) {
+                    tick = niceTickExtent[1];
+                }
+            }
+
             ticks.push({
                 value: tick
             });
 
             // Avoid rounding error
-            tick = roundNumber(tick + interval, intervalPrecision);
+            tick = round(tick + interval, intervalPrecision);
+
             if (this._brkCtx) {
                 const moreMultiple = this._brkCtx.calcNiceTickMultiple(tick, 
estimateNiceMultiple);
                 if (moreMultiple >= 0) {
-                    tick = roundNumber(tick + moreMultiple * interval, 
intervalPrecision);
+                    tick = round(tick + moreMultiple * interval, 
intervalPrecision);
                 }
             }
 
@@ -156,13 +231,14 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
                 return [];
             }
         }
+
         // Consider this case: the last item of ticks is smaller
         // than niceTickExtent[1] and niceTickExtent[1] === extent[1].
         const lastNiceTick = ticks.length ? ticks[ticks.length - 1].value : 
niceTickExtent[1];
         if (extent[1] > lastNiceTick) {
             if (opt.expandToNicedExtent) {
                 ticks.push({
-                    value: roundNumber(lastNiceTick + interval, 
intervalPrecision)
+                    value: round(lastNiceTick + interval, intervalPrecision)
                 });
             }
             else {
@@ -216,7 +292,7 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
             const minorIntervalPrecision = 
helper.getIntervalPrecision(minorInterval);
 
             while (count < splitNumber - 1) {
-                const minorTick = roundNumber(prevTick.value + (count + 1) * 
minorInterval, minorIntervalPrecision);
+                const minorTick = round(prevTick.value + (count + 1) * 
minorInterval, minorIntervalPrecision);
 
                 // For the first and last interval. The count may be less than 
splitNumber.
                 if (minorTick > extent[0] && minorTick < extent[1]) {
@@ -262,7 +338,7 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
         let precision = opt && opt.precision;
 
         if (precision == null) {
-            precision = numberUtil.getPrecision(data.value) || 0;
+            precision = getPrecision(data.value) || 0;
         }
         else if (precision === 'auto') {
             // Should be more precise then tick.
@@ -270,10 +346,10 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
         }
 
         // (1) If `precision` is set, 12.005 should be display as '12.00500'.
-        // (2) Use roundNumber (toFixed) to avoid scientific notation like 
'3.5e-7'.
-        const dataNum = roundNumber(data.value, precision as number, true);
+        // (2) Use `round` (toFixed) to avoid scientific notation like 
'3.5e-7'.
+        const dataNum = round(data.value, precision as number, true);
 
-        return formatUtil.addCommas(dataNum);
+        return addCommas(dataNum);
     }
 
     /**
@@ -286,12 +362,16 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
      * @param splitNumber By default `5`.
      */
     calcNiceTicks(splitNumber?: number, minInterval?: number, maxInterval?: 
number): void {
-        splitNumber = splitNumber || 5;
+        splitNumber = helper.ensureValidSplitNumber(splitNumber, 5);
         let extent = this._extent.slice() as [number, number];
         let span = this._getExtentSpanWithBreaks();
+
         if (!isFinite(span)) {
+            // FIXME: Check and refactor this branch -- this return should 
never happen;
+            //  otherwise the subsequent logic may be incorrect.
             return;
         }
+
         // User may set axis min 0 and data are all negative
         // FIXME If it needs to reverse ?
         if (span < 0) {
@@ -310,43 +390,21 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
         this._niceExtent = result.niceTickExtent;
     }
 
+    /**
+     * FIXME: refactor - disallow override for readability; use composition 
instead.
+     *  `calcNiceExtent` and `alignScaleTicks` both implement tick arrangement 
(for
+     *  two scenarios), but they are implemented in two different code styles.
+     */
     calcNiceExtent(opt: {
         splitNumber: number, // By default 5.
+        // Do not modify the original extent[0]/extent[1] except for an 
invalid extent.
         fixMin?: boolean,
         fixMax?: boolean,
         minInterval?: number,
         maxInterval?: number
     }): void {
-        let extent = this._extent.slice() as [number, number];
-        // If extent start and end are same, expand them
-        if (extent[0] === extent[1]) {
-            if (extent[0] !== 0) {
-                // Expand extent
-                // Note that extents can be both negative. See #13154
-                const expandSize = Math.abs(extent[0]);
-                // In the fowllowing case
-                //      Axis has been fixed max 100
-                //      Plus data are all 100 and axis extent are [100, 100].
-                // Extend to the both side will cause expanded max is larger 
than fixed max.
-                // So only expand to the smaller side.
-                if (!opt.fixMax) {
-                    extent[1] += expandSize / 2;
-                    extent[0] -= expandSize / 2;
-                }
-                else {
-                    extent[0] -= expandSize / 2;
-                }
-            }
-            else {
-                extent[1] = 1;
-            }
-        }
-        const span = extent[1] - extent[0];
-        // If there are no data and extent are [Infinity, -Infinity]
-        if (!isFinite(span)) {
-            extent[0] = 0;
-            extent[1] = 1;
-        }
+        let extent = helper.intervalScaleEnsureValidExtent(this._extent, opt);
+
         this._innerSetExtent(extent[0], extent[1]);
         extent = this._extent.slice() as [number, number];
 
@@ -355,16 +413,14 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
         const intervalPrecition = this._intervalPrecision;
 
         if (!opt.fixMin) {
-            extent[0] = roundNumber(Math.floor(extent[0] / interval) * 
interval, intervalPrecition);
+            extent[0] = round(mathFloor(extent[0] / interval) * interval, 
intervalPrecition);
         }
         if (!opt.fixMax) {
-            extent[1] = roundNumber(Math.ceil(extent[1] / interval) * 
interval, intervalPrecition);
+            extent[1] = round(mathCeil(extent[1] / interval) * interval, 
intervalPrecition);
         }
         this._innerSetExtent(extent[0], extent[1]);
-    }
 
-    setNiceExtent(min: number, max: number): void {
-        this._niceExtent = [min, max];
+        // [CAVEAT]: If updating this impl, need to sync it to 
`axisAlignTicks.ts`.
     }
 
 }
diff --git a/src/scale/Log.ts b/src/scale/Log.ts
index bdd799056..d7be77057 100644
--- a/src/scale/Log.ts
+++ b/src/scale/Log.ts
@@ -18,81 +18,95 @@
 */
 
 import * as zrUtil from 'zrender/src/core/util';
-import Scale, { ScaleGetTicksOpt } from './Scale';
-import * as numberUtil from '../util/number';
+import Scale, { ScaleGetTicksOpt, ScaleSettingDefault } from './Scale';
+import {
+    mathFloor, mathCeil, mathPow, mathLog,
+    round, quantity, getPrecision
+} from '../util/number';
 
 // Use some method of IntervalScale
 import IntervalScale from './Interval';
 import {
     DimensionLoose, DimensionName, ParsedAxisBreakList, AxisBreakOption,
-    ScaleTick
+    ScaleTick,
+    NullUndefined
 } from '../util/types';
-import { getIntervalPrecision, logTransform } from './helper';
+import { ensureValidSplitNumber, fixNiceExtent, getIntervalPrecision, 
logTransform } from './helper';
 import SeriesData from '../data/SeriesData';
 import { getScaleBreakHelper } from './break';
 
-const fixRound = numberUtil.round;
-const mathFloor = Math.floor;
-const mathCeil = Math.ceil;
-const mathPow = Math.pow;
-const mathLog = Math.log;
 
+const LINEAR_STUB_METHODS = [
+    'getExtent', 'getTicks', 'getInterval'
+    // Keep no setting method to mitigate vulnerability.
+] as const;
+
+/**
+ * IMPL_MEMO:
+ *  - The supper class (`IntervalScale`) and its member fields (such as 
`this._extent`,
+ *    `this._interval`, `this._niceExtent`) provides linear tick arrangement 
(logarithm applied).
+ *  - `_originalScale` (`IntervalScale`) is used to save some original info
+ *    (before logarithm applied, such as raw extent).
+ */
 class LogScale extends IntervalScale {
 
     static type = 'log';
     readonly type = 'log';
 
-    base = 10;
+    readonly base: number;
 
     private _originalScale = new IntervalScale();
 
-    private _fixMin: boolean;
-    private _fixMax: boolean;
+    // `[fixMin, fixMax]`
+    private _fixMinMax: boolean[] = [false, false];
+
+    linearStub: Pick<IntervalScale, (typeof LINEAR_STUB_METHODS)[number]>;
+
+    constructor(logBase: number | NullUndefined, settings?: 
ScaleSettingDefault) {
+        super(settings);
+        this.base = zrUtil.retrieve2(logBase, 10);
+        this._initLinearStub();
+    }
+
+    private _initLinearStub(): void {
+        // TODO: Refactor -- This impl is error-prone. And the use of 
`prototype` should be removed.
+        const intervalScaleProto = IntervalScale.prototype;
+        const logScale = this;
+        const stub = logScale.linearStub = {} as LogScale['linearStub'];
+        zrUtil.each(LINEAR_STUB_METHODS, function (methodName) {
+            stub[methodName] = function () {
+                return (intervalScaleProto[methodName] as any).apply(logScale, 
arguments);
+            };
+        });
+    }
 
     /**
      * @param Whether expand the ticks to niced extent.
      */
     getTicks(opt?: ScaleGetTicksOpt): ScaleTick[] {
-        opt = opt || {};
-        const extent = this._extent.slice() as [number, number];
-        const originalExtent = this._originalScale.getExtent();
-
-        const ticks = super.getTicks(opt);
-        const base = this.base;
-        const originalBreaks = this._originalScale._innerGetBreaks();
+        const extent = this._extent;
         const scaleBreakHelper = getScaleBreakHelper();
 
-        return zrUtil.map(ticks, function (tick) {
-            const val = tick.value;
-            let roundingCriterion = null;
-
-            let powVal = mathPow(base, val);
-
-            // Fix #4158
-            if (val === extent[0] && this._fixMin) {
-                roundingCriterion = originalExtent[0];
-            }
-            else if (val === extent[1] && this._fixMax) {
-                roundingCriterion = originalExtent[1];
-            }
-
+        return zrUtil.map(super.getTicks(opt || {}), function (tick) {
             let vBreak;
+            let brkRoundingCriterion;
             if (scaleBreakHelper) {
                 const transformed = scaleBreakHelper.getTicksLogTransformBreak(
                     tick,
-                    base,
-                    originalBreaks,
+                    this.base,
+                    this._originalScale._innerGetBreaks(),
                     fixRoundingError
                 );
                 vBreak = transformed.vBreak;
-                if (roundingCriterion == null) {
-                    roundingCriterion = transformed.brkRoundingCriterion;
-                }
+                brkRoundingCriterion = transformed.brkRoundingCriterion;
             }
 
-            if (roundingCriterion != null) {
-                powVal = fixRoundingError(powVal, roundingCriterion);
-            }
+            const val = tick.value;
+            const powVal = this.powTick(
+                val,
+                val === extent[1] ? 1 : val === extent[0] ? 0 : null,
+                brkRoundingCriterion
+            );
 
             return {
                 value: powVal,
@@ -106,26 +120,18 @@ class LogScale extends IntervalScale {
     }
 
     setExtent(start: number, end: number): void {
+        // [CAVEAT]: If modifying this logic, must sync to `_initLinearStub`.
         this._originalScale.setExtent(start, end);
         const loggedExtent = logTransform(this.base, [start, end]);
         super.setExtent(loggedExtent[0], loggedExtent[1]);
     }
 
-    /**
-     * @return {number} end
-     */
     getExtent() {
-        const base = this.base;
         const extent = super.getExtent();
-        extent[0] = mathPow(base, extent[0]);
-        extent[1] = mathPow(base, extent[1]);
-
-        // Fix #4158
-        const originalExtent = this._originalScale.getExtent();
-        this._fixMin && (extent[0] = fixRoundingError(extent[0], 
originalExtent[0]));
-        this._fixMax && (extent[1] = fixRoundingError(extent[1], 
originalExtent[1]));
-
-        return extent;
+        return [
+            this.powTick(extent[0], 0, null),
+            this.powTick(extent[1], 1, null)
+        ] as [number, number];
     }
 
     unionExtentFromData(data: SeriesData, dim: DimensionName | 
DimensionLoose): void {
@@ -134,23 +140,55 @@ class LogScale extends IntervalScale {
         this._innerUnionExtent(loggedOther);
     }
 
+    /**
+     * fixMin/Max and rounding error are addressed.
+     */
+    powTick(
+        // `val` should be in the linear space.
+        val: number,
+        // `0`: `value` is `min`;
+        // `1`: `value` is `max`;
+        // `NullUndefined`: others.
+        extentIdx: 0 | 1 | NullUndefined,
+        fallbackRoundingCriterion: number | NullUndefined
+    ): number {
+        // NOTE: `Math.pow(10, integer)` has no rounding error.
+        // PENDING: other base?
+        let powVal = mathPow(this.base, val);
+
+        // Fix #4158
+        // NOTE: Even when `fixMin/Max` is `true`, `pow(base, 
this._extent[0]/[1])` may be still
+        // not equal to `this._originalScale.getExtent()[0]`/`[1]` in invalid 
extent case.
+        // So we always call `Math.pow`.
+        const roundingCriterion = this._fixMinMax[extentIdx]
+            ? this._originalScale.getExtent()[extentIdx]
+            : fallbackRoundingCriterion;
+
+        if (roundingCriterion != null) {
+            powVal = fixRoundingError(powVal, roundingCriterion);
+        }
+
+        return powVal;
+    }
+
     /**
      * Update interval and extent of intervals for nice ticks
-     * @param approxTickNum default 10 Given approx tick number
+     * @param splitNumber default 10 Given approx tick number
      */
-    calcNiceTicks(approxTickNum: number): void {
-        approxTickNum = approxTickNum || 10;
+    calcNiceTicks(splitNumber: number): void {
+        splitNumber = ensureValidSplitNumber(splitNumber, 10);
         const extent = this._extent.slice() as [number, number];
         const span = this._getExtentSpanWithBreaks();
         if (!isFinite(span) || span <= 0) {
             return;
         }
 
-        let interval = numberUtil.quantity(span);
-        const err = approxTickNum / span * interval;
+        let interval = quantity(span);
+        const err = splitNumber / span * interval;
 
         // Filter ticks to get closer to the desired count.
         if (err <= 0.5) {
+            // TODO: support other bases other than 10?
             interval *= 10;
         }
 
@@ -159,14 +197,19 @@ class LogScale extends IntervalScale {
             interval *= 10;
         }
 
+        const intervalPrecision = getIntervalPrecision(interval);
         const niceExtent = [
-            fixRound(mathCeil(extent[0] / interval) * interval),
-            fixRound(mathFloor(extent[1] / interval) * interval)
+            round(mathCeil(extent[0] / interval) * interval, 
intervalPrecision),
+            round(mathFloor(extent[1] / interval) * interval, 
intervalPrecision)
         ] as [number, number];
 
+        fixNiceExtent(niceExtent, extent);
+
         this._interval = interval;
-        this._intervalPrecision = getIntervalPrecision(interval);
+        this._intervalPrecision = intervalPrecision;
         this._niceExtent = niceExtent;
+
+        // [CAVEAT]: If updating this impl, need to sync it to 
`axisAlignTicks.ts`.
     }
 
     calcNiceExtent(opt: {
@@ -178,8 +221,7 @@ class LogScale extends IntervalScale {
     }): void {
         super.calcNiceExtent(opt);
 
-        this._fixMin = opt.fixMin;
-        this._fixMax = opt.fixMax;
+        this._fixMinMax = [!!opt.fixMin, !!opt.fixMax];
     }
 
     contain(val: number): boolean {
@@ -216,7 +258,7 @@ class LogScale extends IntervalScale {
 }
 
 function fixRoundingError(val: number, originalVal: number): number {
-    return fixRound(val, numberUtil.getPrecision(originalVal));
+    return round(val, getPrecision(originalVal));
 }
 
 
diff --git a/src/scale/Scale.ts b/src/scale/Scale.ts
index 1f1fbdecf..801e2a2eb 100644
--- a/src/scale/Scale.ts
+++ b/src/scale/Scale.ts
@@ -117,7 +117,7 @@ abstract class Scale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault>
     /**
      * [CAVEAT]: It should not be overridden!
      */
-    _innerUnionExtent(other: [number, number]): void {
+    _innerUnionExtent(other: number[]): void {
         const extent = this._extent;
         // Considered that number could be NaN and should not write into the 
extent.
         this._innerSetExtent(
diff --git a/src/scale/helper.ts b/src/scale/helper.ts
index c5143d530..f89abca5d 100644
--- a/src/scale/helper.ts
+++ b/src/scale/helper.ts
@@ -106,7 +106,7 @@ export function intervalScaleNiceTicks(
  */
 export function increaseInterval(niceInterval: number) {
     const exponent = quantityExponent(niceInterval);
-    // No rounding error in Math.pow(10, xxx).
+    // No rounding error in Math.pow(10, integer).
     const exp10 = mathPow(10, exponent);
     // Fix IEEE 754 float rounding error
     let f = mathRound(niceInterval / exp10);
@@ -203,13 +203,6 @@ export function logTransform(base: number, extent: 
number[], noClampNegative?: b
     ];
 }
 
-export function powTransform(base: number, extent: number[]): [number, number] 
{
-    return [
-        mathPow(base, extent[0]),
-        mathPow(base, extent[1])
-    ];
-}
-
 /**
  * A valid extent is:
  *  - No non-finite number.
diff --git a/src/util/number.ts b/src/util/number.ts
index 074374351..552a9ec83 100644
--- a/src/util/number.ts
+++ b/src/util/number.ts
@@ -288,10 +288,13 @@ export function getPixelPrecision(dataExtent: [number, 
number], pixelExtent: [nu
  * "data" is linearly mapped to pixel according to the ratio determined by 
`dataSpan` and `pxSpan`.
  * The diff from the original "data" to the rounded "data" (with the result 
precision) should be
  * equal or less than `pxDiffAcceptable`, which is typically `1` pixel.
- * And the result precision should be as small as possible.
+ * And the result precision should be as small as possible for a concise 
display.
  *
- * [NOTICE]: using arbitrary parameters is not preferable -- a discernible 
misalign (e.g., over 1px)
- *  may occur, especially when `splitLine` displayed.
+ * [NOTICE]: using arbitrary parameters is NOT preferable - a discernible 
misalign (e.g., over 1px)
+ *  may occur, especially when `splitLine` is displayed.
+ *
+ * PENDING: Only linear case is addressed for now; other mapping methods (like 
log) will not be
+ *  covered until necessary.
  */
 export function getAcceptableTickPrecision(
     // Typically, `Math.abs(dataExtent[1] - dataExtent[0])`.
@@ -574,7 +577,7 @@ export function nice(
     //  e.g., if `val` is `0`,
     //      The result is `1`.
     const exponent = quantityExponent(val);
-    // No rounding error in Math.pow(10, xxx).
+    // No rounding error in Math.pow(10, integer).
     const exp10 = mathPow(10, exponent);
     const f = val / exp10;
 
diff --git a/test/axis-align-edge-cases.html b/test/axis-align-edge-cases.html
new file mode 100644
index 000000000..d851f1bdd
--- /dev/null
+++ b/test/axis-align-edge-cases.html
@@ -0,0 +1,746 @@
+<!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" />
+        <script src="lib/simpleRequire.js"></script>
+        <script src="lib/config.js"></script>
+        <script src="lib/jquery.min.js"></script>
+        <script src="lib/facePrint.js"></script>
+        <script src="lib/testHelper.js"></script>
+        <!-- <script src="lib/canteen.js"></script> -->
+        <!-- <script src="lib/draggable.js"></script> -->
+        <link rel="stylesheet" href="lib/reset.css" />
+    </head>
+    <body>
+        <style>
+            html {
+                /* Fix the line-height to integer to avoid it varying across 
clients and
+                   causing visual test failures. Some clients may not support 
fractional px. */
+                line-height: 18px;
+            }
+        </style>
+
+
+        <!-- <div id="main_cartesian_0_integerData"></div>
+        <div id="main_cartesian_0_floatData"></div>
+        <div id="main_cartesian_fix_min_max"></div>
+        <div id="main_cartesian_yAxis_dataZoom"></div>
+        <div id="main_radar_0"></div> -->
+        <div id="main_cartesian_0_logIntegerData"></div>
+
+
+
+        <script>
+            function create_case_main_cartesian_0(echarts, domId, title, 
testData) {
+
+                const scatterLeftData = [[1, 35], [100, 200], [500, 50]];
+                let _ctx = {
+                    persistent_option: {}
+                };
+
+                function resetCtx() {
+                    _ctx.case_option = {};
+                }
+
+                function createOption() {
+                    return {
+                        tooltip: {},
+                        grid: {
+                            top: 50,
+                            bottom: 50,
+                            // outerBoundsMode: 'none',
+                        },
+                        xAxis: {
+                            max: 1300
+                        },
+                        yAxis: [{
+                            id: 'left',
+                            min: _ctx.case_option.yAxis__left__min,
+                            max: _ctx.case_option.yAxis__left__max,
+                            scale: _ctx.case_option.yAxis__left__scale,
+                            splitNumber: 
_ctx.persistent_option.yAxis__left__splitNumber,
+                            // alignTicks: true,
+                        }, {
+                            id: 'right',
+                            type: testData.rightYAxisType || 'value',
+                            alignTicks: true,
+                            min: _ctx.case_option.yAxis__right__min,
+                            max: _ctx.case_option.yAxis__right__max,
+                            scale: _ctx.case_option.yAxis__right__scale,
+                            splitLine: {show: false},
+                        }],
+                        legend: {
+                            top: 5
+                        },
+                        series: [{
+                            type: 'scatter',
+                            name: 'scatterLeft',
+                            label: {show: true, position: 'top'},
+                            yAxisIndex: 0,
+                            data: scatterLeftData,
+                        }, {
+                            type: 'scatter',
+                            name: 'scatterRight',
+                            label: {show: true, position: 'top'},
+                            yAxisIndex: 1,
+                            data: testData.scatterRightData,
+                        }]
+                    };
+                }
+
+                let _recreateChartTimeout;
+                function scheduleRecreateChart() {
+                    if (_recreateChartTimeout != null) {
+                        return;
+                    }
+                    _recreateChartTimeout = setTimeout(function () {
+                        _recreateChartTimeout = null;
+                        chart.setOption(createOption(), {notMerge: true});
+                    }, 0);
+                }
+
+                const TEST_CASE_LIST = [
+                    {
+                        text: 'defaults',
+                        value: {
+                            version: '1.0.0',
+                            operations: [],
+                        },
+                    },
+                    {
+                        text: 'only rightYAxis.max is set',
+                        value: {
+                            version: '1.0.0',
+                            startTime: 1767869651040,
+                            operations: [{id: '__inputs|right yAxis max:|', 
op: 'select', args: [1]}],
+                            endTime: 1767869653618
+                        }
+                    },
+                    {
+                        text: 'only rightYAxis.min is set positive',
+                        value: {
+                            version: '1.0.0',
+                            startTime: 1767869713876,
+                            operations: [{id: '__inputs|right yAxis min:|', 
op: 'select', args: [1]}],
+                            endTime: 1767869718229
+                        }
+                    },
+                    {
+                        text: 'only rightYAxis.min is set negative',
+                        value: {
+                            version: '1.0.0',
+                            startTime: 1767869748667,
+                            operations: [{id: '__inputs|right yAxis min:|', 
op: 'select', args: [2]}],
+                            endTime: 1767869753233
+                        }
+                    },
+                    {
+                        text: 'only rightYAxis.scale is true',
+                        value: {
+                            version: '1.0.0',
+                            startTime: 1767870099308,
+                            operations: [{id: '__inputs|right yAxis scale:|', 
op: 'select', args: [1]}],
+                            endTime: 1767870104613
+                        }
+                    },
+                    {
+                        text: 'only leftYAxis.min is set',
+                        value: {
+                            version: '1.0.0',
+                            startTime: 1766947619748,
+                            operations: [{id: '__inputs|left yAxis min:|', op: 
'select', args: [1]}],
+                            endTime: 1766947634626
+                        }
+                    },
+                    {
+                        text: 'only leftYAxis.max is set',
+                        value: {
+                            version: '1.0.0',
+                            startTime: 1766907841989,
+                            operations: [{id: '__inputs|left yAxis max:|', op: 
'select', args: [1]}],
+                            endTime: 1766907871908
+                        }
+                    },
+                    {
+                        text: 'only leftYAxis.max, rightYAxis.min are set',
+                        value: {
+                            version: '1.0.0',
+                            startTime: 1766907958986,
+                            operations: [
+                                {id: '__inputs|left yAxis max:|', op: 
'select', args: [1]},
+                                {id: '__inputs|right yAxis min:|', op: 
'select', args: [1]}
+                            ],
+                            endTime: 1766907973682
+                        }
+                    },
+                    {
+                        text: 'only rightYAxis.min/max are set',
+                        value: {
+                            version: '1.0.0',
+                            startTime: 1766951919872,
+                            operations: [
+                                {id: '__inputs|right yAxis min:|', op: 
'select', args: [1]},
+                                {id: '__inputs|right yAxis max:|', op: 
'select', args: [1]}
+                            ],
+                            endTime: 1766951924713
+                        }
+                    },
+                    {
+                        text: 'only leftYAxis.max, rightYAxis.min/max are set',
+                        value: {
+                            version: '1.0.0',
+                            startTime: 1766908018145,
+                            operations: [
+                                {id: '__inputs|left yAxis max:|', op: 
'select', args: [1]},
+                                {id: '__inputs|right yAxis min:|', op: 
'select', args: [1]},
+                                {id: '__inputs|right yAxis max:|', op: 
'select', args: [1]}
+                            ],
+                            endTime: 1766908026098
+                        }
+                    },
+                    {
+                        text: 'only leftYAxis.min/max, rightYAxis.minmax are 
set',
+                        value: {
+                            version: '1.0.0',
+                            startTime: 1766948681354,
+                            operations: [
+                                {id: '__inputs|left yAxis min:|', op: 
'select', args: [1]},
+                                {id: '__inputs|left yAxis max:|', op: 
'select', args: [1]},
+                                {id: '__inputs|right yAxis min:|', op: 
'select', args: [1]},
+                                {id: '__inputs|right yAxis max:|', op: 
'select', args: [1]}
+                            ],
+                            endTime: 1766948688107
+                        }
+                    },
+                ];
+
+                resetCtx();
+
+                var chart = testHelper.create(echarts, domId, {
+                    title: [
+                        title,
+                        `The right yAxis aligns to the left yAxis`,
+                        `Both left/right yAxis may have **min/max/scale** 
specified.`,
+                        `scatterLeft 
**${testHelper.printObject(scatterLeftData)}** should display.`,
+                        `scatterRight 
**${testHelper.printObject(testData.scatterRightData)}** should display.`,
+                        'xAxis should be in a proper position.'
+                    ],
+                    option: createOption(),
+                    height: 300,
+                    saveInputsInitialState: true,
+                    inputsStyle: 'compact',
+                    inputs: [{
+                        type: 'select',
+                        text: 'quick test cases:',
+                        prevent: {
+                            recordInputs: true,
+                            inputsState: true
+                        },
+                        options: TEST_CASE_LIST,
+                        onchange() {
+                            chart.__testHelper.restoreInputsToInitialState();
+                            resetCtx();
+                            scheduleRecreateChart();
+                            chart.__testHelper.replayInputs(this.value);
+                        }
+                    }, {
+                        type: 'br'
+                    }, {
+                        text: 'stop recording inputs and copy to clipboard',
+                        prevent: {
+                            recordInputs: true,
+                            inputsState: true
+                        },
+                        onclick() {
+                            chart.__testHelper.recordInputs({
+                                action: 'stop',
+                                printObjectOpt: {
+                                    marginLeft: 8,
+                                    lineBreakMaxColumn: 100
+                                }
+                            });
+                            chart.__testHelper.restoreInputsToInitialState();
+                            resetCtx();
+                            chart.__testHelper.recordInputs({
+                                action: 'start',
+                            });
+                        }
+                    }, {
+                        text: 'test quick cases for each left yAxis 
splitNumber:',
+                        type: 'select',
+                        values: [undefined, 0, 1, 2, 3, 4, 9],
+                        prevent: {
+                            recordInputs: true,
+                            inputsState: true
+                        },
+                        onchange: function () {
+                            _ctx.persistent_option.yAxis__left__splitNumber = 
this.value;
+                            scheduleRecreateChart();
+                        }
+                    }, {
+                        type: 'hr'
+                    }, {
+                        text: 'left yAxis min:',
+                        type: 'select',
+                        values: testData.inputs.leftYAxisMin,
+                        onchange: function () {
+                            _ctx.case_option.yAxis__left__min = this.value;
+                            scheduleRecreateChart();
+                        }
+                    }, {
+                        text: 'left yAxis max:',
+                        type: 'select',
+                        values: testData.inputs.leftYAxisMax,
+                        onchange: function () {
+                            _ctx.case_option.yAxis__left__max = this.value;
+                            scheduleRecreateChart();
+                        }
+                    }, {
+                        text: 'left yAxis scale:',
+                        type: 'select',
+                        values: [false, true],
+                        onchange: function () {
+                            _ctx.case_option.yAxis__left__scale = this.value;
+                            scheduleRecreateChart();
+                        }
+                    }, {
+                        type: 'hr'
+                    }, {
+                        text: 'right yAxis min:',
+                        type: 'select',
+                        values: testData.inputs.rightYAxisMin,
+                        onchange: function () {
+                            _ctx.case_option.yAxis__right__min = this.value;
+                            scheduleRecreateChart();
+                        }
+                    }, {
+                        text: 'right yAxis max:',
+                        type: 'select',
+                        values: testData.inputs.rightYAxisMax,
+                        onchange: function () {
+                            _ctx.case_option.yAxis__right__max = this.value;
+                            scheduleRecreateChart();
+                        }
+                    }, {
+                        text: 'right yAxis scale:',
+                        type: 'select',
+                        values: [false, true],
+                        onchange: function () {
+                            _ctx.case_option.yAxis__right__scale = this.value;
+                            scheduleRecreateChart();
+                        }
+                    }]
+
+                }); // End of `testHelper.create`
+
+                if (chart) {
+                    chart.__testHelper.recordInputs({
+                        action: 'start',
+                    });
+                }
+
+            } // End of `create_case_main_cartesian_0`
+        </script>
+
+
+        <script>
+            require(['echarts'], function (echarts) {
+                const TEST_DATA_INTEGER = {
+                    scatterRightData: [[1000, 201212], [1200, 211200]],
+                    inputs: {
+                        leftYAxisMin: [undefined, 12.379],
+                        leftYAxisMax: [undefined, 219.791],
+                        rightYAxisMin: [undefined, 1013.33, -1000],
+                        rightYAxisMax: [undefined, 231739],
+                    },
+                };
+                create_case_main_cartesian_0(
+                    echarts,
+                    'main_cartesian_0_integerData',
+                    'The right series are integer Data',
+                    TEST_DATA_INTEGER
+                );
+            }); // End of `require`
+        </script>
+
+
+        <script>
+            require(['echarts'], function (echarts) {
+                const TEST_DATA_FLOAT = {
+                    // float data less than 1e-10, to test the precision of 
axis tick calculation.
+                    scatterRightData: [[1000, 0.00000000000201212], [1200, 
0.00000000000211200]],
+                    inputs: {
+                        leftYAxisMin: [undefined, 12.379],
+                        leftYAxisMax: [undefined, 219.791],
+                        rightYAxisMin: [undefined, 0.0000000000000101333, 
-0.00000000000001000],
+                        rightYAxisMax: [undefined, 0.00000000000231739],
+                    },
+                };
+                create_case_main_cartesian_0(
+                    echarts,
+                    'main_cartesian_0_floatData',
+                    'The right series are Float Data',
+                    TEST_DATA_FLOAT
+                );
+            }); // End of `require`
+        </script>
+
+
+
+        <script>
+            require([
+                'echarts',
+            ], function (echarts /*, data */) {
+                var yAxisMin = 2000;
+                var yAxisMax = 5000.3747;
+
+                function createOption() {
+                    return {
+                        tooltip: {},
+                        grid: {
+                            top: 50,
+                            bottom: 50,
+                        },
+                        xAxis: {
+                            max: 1300
+                        },
+                        yAxis: [{
+                            id: 'left',
+                            splitNumber: 3,
+                        }, {
+                            id: 'right',
+                            min: yAxisMin,
+                            max: yAxisMax,
+                            // axisLabel: {hideOverlap: false},
+                            alignTicks: true
+                        }],
+                        legend: {
+                            top: 5
+                        },
+                        series: [{
+                            type: 'scatter',
+                            name: 'scatterLeft',
+                            label: {show: true, position: 'top'},
+                            yAxisIndex: 0,
+                            data: [[1, 0], [1, 10], [1, 20], [1, 30]]
+                        }, {
+                            type: 'scatter',
+                            name: 'scatterRight',
+                            label: {show: true, position: 'top'},
+                            yAxisIndex: 1,
+                            data: [[1000, 3000], [1200, yAxisMax]]
+                        }]
+                    };
+                }
+
+                var chart = testHelper.create(echarts, 
'main_cartesian_fix_min_max', {
+                    title: [
+                        `The right yAxis aligns to the left yAxis.`,
+                        `The right yAxis has **min: ${yAxisMin}, max: 
${yAxisMax}**.`,
+                        `The right yAxis should display min/max correctly with 
proper percision.`,
+                    ],
+                    option: createOption(),
+                    height: 300,
+                }); // End of `testHelper.create`
+            }); // End of `require`
+        </script>
+
+
+
+        <script>
+            require([
+                'echarts',
+            ], function (echarts /*, data */) {
+                var RIGHT_Y_AXIS_LABEL_FORMATTER = {
+                    'precision_8': function (value) {
+                        return value.toFixed(8);
+                    }
+                };
+                var dataMap = {
+                    big_data: [[1000, 3000], [1200, 50003747]],
+                    small_data: [[1000, 0.0003000], [1200, 0.00050003747]]
+                };
+
+                var _ctx = {
+                    seriesData: dataMap.big_data,
+                    rightYAxisFormatterKind: undefined,
+                    dataZoomEnd: undefined,
+                    realtime: true,
+                    gridHeight: 260,
+                };
+
+                function createOption() {
+
+                    return {
+                        tooltip: {},
+                        grid: {
+                            top: 50,
+                            height: _ctx.gridHeight,
+                            right: 140,
+                        },
+                        xAxis: {
+                            max: 1300
+                        },
+                        yAxis: [{
+                            id: 'left',
+                            splitNumber: 3,
+                        }, {
+                            id: 'right',
+                            // axisLabel: {hideOverlap: false},
+                            axisLabel: {
+                                formatter: 
RIGHT_Y_AXIS_LABEL_FORMATTER[_ctx.rightYAxisFormatterKind]
+                            },
+                            alignTicks: true
+                        }],
+                        legend: {
+                            top: 5
+                        },
+                        dataZoom: {
+                            yAxisIndex: 'all',
+                            end: _ctx.dataZoomEnd,
+                            realtime: _ctx.realtime,
+                        },
+                        series: [{
+                            type: 'scatter',
+                            name: 'scatterLeft',
+                            label: {show: true, position: 'top'},
+                            yAxisIndex: 0,
+                            data: [[1, 0], [1, 10], [1, 20], [1, 30]]
+                        }, {
+                            type: 'scatter',
+                            name: 'scatterRight',
+                            label: {show: true, position: 'top'},
+                            yAxisIndex: 1,
+                            data: _ctx.seriesData
+                        }]
+                    };
+                }
+
+                var chart = testHelper.create(echarts, 
'main_cartesian_yAxis_dataZoom', {
+                    title: [
+                        `dataZoom controls all yAxes.`,
+                        `The right yAxis aligns to the left yAxis.`,
+                        `All series data should be displayed when dataZoom is 
full.`,
+                    ],
+                    option: createOption(),
+                    height: 300,
+                    inputsStyle: 'compact',
+                    inputs: [{
+                        type: 'select',
+                        text: 'dataZoom.realtime:',
+                        values: [true, false],
+                        onchange: function () {
+                            _ctx.realtime = this.value;
+                            chart.setOption(createOption(), {notMerge: true});
+                        }
+                    }, {
+                        type: 'select',
+                        text: 'seriesData:',
+                        values: [dataMap.big_data, dataMap.small_data],
+                        onchange: function () {
+                            _ctx.seriesData = this.value;
+                            chart.setOption(createOption(), {notMerge: true});
+                        }
+                    }, {
+                        type: 'select',
+                        text: 'grid.height:',
+                        values: [_ctx.gridHeight, 5, 0],
+                        onchange: function () {
+                            _ctx.gridHeight = this.value;
+                            chart.setOption(createOption(), {notMerge: true});
+                        }
+                    }, {
+                        type: 'br'
+                    }, {
+                        type: 'select',
+                        text: 'rightYAxisFormatter:',
+                        values: [undefined, 'precision_8'],
+                        onchange: function () {
+                            _ctx.rightYAxisFormatterKind = this.value;
+                            chart.setOption(createOption(), {notMerge: true});
+                        }
+                    }, {
+                        type: 'select',
+                        text: 'dataZoomEnd:',
+                        values: [undefined, 91, 90.5],
+                        onchange: function () {
+                            _ctx.dataZoomEnd = this.value;
+                            chart.setOption(createOption(), {notMerge: true});
+                        }
+                    }]
+                }); // End of `testHelper.create`
+            }); // End of `require`
+        </script>
+
+
+
+
+
+
+        <script>
+            require(['echarts'], function (echarts) {
+                const RAW_INDICATOR = [
+                    {name: 'Zt'},
+                    {name: 'Uy'},
+                    {name: 'Sw'},
+                    {name: 'Lk'},
+                    {name: 'Eq'},
+                    {name: 'Pr'}
+                ];
+                const INDICATOR_MIN = {
+                    'Zt': 26,
+                    'Uy': 0,
+                    'Sw': 0.000035987,
+                    'Lk': 17,
+                    'Eq': 76,
+                    'Pr': -28,
+                };
+                const INDICATOR_MAX = {
+                    'Zt': 28,
+                    'Uy': 10,
+                    'Sw': 0.000095,
+                    'Lk': 18,
+                    'Eq': 82,
+                    'Pr': 35,
+                };
+                const DEFAULT_SPLIT_NUMBER = 5;
+
+                const _ctx = {
+                    min: INDICATOR_MIN,
+                    max: INDICATOR_MAX,
+                    splitNumber: DEFAULT_SPLIT_NUMBER,
+                };
+
+                function createOption() {
+                    // This case is originally from #3753.
+                    const indicator = RAW_INDICATOR.map(rawItem => {
+                        const item = {name: rawItem.name};
+                        if (_ctx.min) {
+                            item.min = _ctx.min[item.name];
+                        }
+                        if (_ctx.max) {
+                            item.max = _ctx.max[item.name];
+                        }
+                        return item;
+                    });
+
+                    return {
+                        legend: {
+                            left: 20,
+                            top: 'middle',
+                            data: ['Result']
+                        },
+                        radar: {
+                            radius: '80%',
+                            indicator: indicator,
+                            splitNumber: _ctx.splitNumber,
+                            axisLabel: {
+                                show: true,
+                            },
+                        },
+                        series: [{
+                            type: 'radar',
+                            data: [{
+                                value: [28, 9.49, 0.000078219876, 17.84, 76, 
34.57],
+                                name: 'Result',
+                                label: {
+                                    show: true,
+                                    color: 'red',
+                                    formatter:function(params) {
+                                        return params.value;
+                                    }
+                                }
+                            }]
+                        }]
+                    };
+                }
+
+                var chart = testHelper.create(echarts, 'main_radar_0', {
+                    title: [
+                        `All radar axes should be aligned.`,
+                        `All radar axes may have **min/max** specified.`,
+                        `min max of each axes should be displayed correctly.`,
+                        `axis tick precision should be proper.`,
+                    ],
+                    option: createOption(),
+                    height: 500,
+                    saveInputsInitialState: true,
+                    inputsStyle: 'compact',
+                    inputs: [{
+                        text: 'indicator.min:',
+                        type: 'select',
+                        values: [INDICATOR_MIN, undefined],
+                        onchange: function () {
+                            _ctx.min = this.value;
+                            chart.setOption(createOption());
+                        }
+                    }, {
+                        text: 'indicator.max:',
+                        type: 'select',
+                        values: [INDICATOR_MAX, undefined],
+                        onchange: function () {
+                            _ctx.max = this.value;
+                            chart.setOption(createOption());
+                        }
+                    }, {
+                        text: 'splitNumber:',
+                        type: 'select',
+                        values: [DEFAULT_SPLIT_NUMBER, 4, 3],
+                        onchange: function () {
+                            _ctx.splitNumber = this.value;
+                            chart.setOption(createOption());
+                        }
+                    }]
+
+                }); // End of `testHelper.create`
+
+            }); // End of `require`
+        </script>
+
+
+        <script>
+            require(['echarts'], function (echarts) {
+                const TEST_DATA_INTEGER = {
+                    scatterRightData: [[1000, 201212], [1200, 211200]],
+                    rightYAxisType: 'log',
+                    inputs: {
+                        leftYAxisMin: [undefined, 12.379],
+                        leftYAxisMax: [undefined, 219.791],
+                        rightYAxisMin: [undefined, 1013.33, -1000],
+                        rightYAxisMax: [undefined, 231739],
+                    },
+                };
+                create_case_main_cartesian_0(
+                    echarts,
+                    'main_cartesian_0_logIntegerData',
+                    'The right yAxis is "log"; the right series are integer 
Data',
+                    TEST_DATA_INTEGER
+                );
+            }); // End of `require`
+        </script>
+
+
+    </body>
+</html>
+
diff --git a/test/axis-align-ticks-random.html 
b/test/axis-align-ticks-random.html
index 42e0858db..8b502e713 100644
--- a/test/axis-align-ticks-random.html
+++ b/test/axis-align-ticks-random.html
@@ -83,6 +83,8 @@ under the License.
             'echarts'
         ], function (echarts) {
 
+            const __EC_OPTIONS_FOR_DEBUG = window.__EC_OPTIONS_FOR_DEBUG = {};
+
 
             function makeOption(leftMin, leftMax, rightMin, rightMax, 
splitNumber) {
 
@@ -145,11 +147,13 @@ under the License.
             }
 
             function makeTestCharts(containerId, leftMin, leftMax, rightMin, 
rightMax) {
+                __EC_OPTIONS_FOR_DEBUG[containerId] = [];
                 const container = document.querySelector(containerId);
                 for (let i = 0; i < 15; i++) {
                     const dom = document.createElement('div');
                     dom.className = 'chart';
                     const option = makeOption(leftMin, leftMax, rightMin, 
rightMax);
+                    __EC_OPTIONS_FOR_DEBUG[containerId].push(option);
                     container.appendChild(dom);
                     const chart = echarts.init(dom);
                     chart.setOption(option);
diff --git a/test/runTest/actions/__meta__.json 
b/test/runTest/actions/__meta__.json
index 6d7272302..a4e134f69 100644
--- a/test/runTest/actions/__meta__.json
+++ b/test/runTest/actions/__meta__.json
@@ -8,6 +8,7 @@
   "aria-pie": 2,
   "axes": 0,
   "axis": 1,
+  "axis-align-edge-cases": 5,
   "axis-align-ticks": 4,
   "axis-boundaryGap": 1,
   "axis-break": 7,
diff --git a/test/runTest/actions/axis-align-edge-cases.json 
b/test/runTest/actions/axis-align-edge-cases.json
new file mode 100644
index 000000000..aea4cbc16
--- /dev/null
+++ b/test/runTest/actions/axis-align-edge-cases.json
@@ -0,0 +1 @@
+[{"name":"Action 
1","ops":[{"type":"mousemove","time":286,"x":586,"y":156},{"type":"mousedown","time":434,"x":583,"y":154},{"type":"mousemove","time":492,"x":583,"y":154},{"type":"mouseup","time":517,"x":583,"y":154},{"time":518,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":735,"x":582,"y":157},{"type":"mousemove","time":935,"x":519,"y":321},{"type":"mousemove","time":1135,"x":474,"y":318},{"type":"mousemove","time":1343,"x":455,"y":298},{"type":"mousedown","time":1484
 [...]
\ No newline at end of file
diff --git a/test/runTest/marks/axis-align-edge-cases.json 
b/test/runTest/marks/axis-align-edge-cases.json
new file mode 100644
index 000000000..826d66dad
--- /dev/null
+++ b/test/runTest/marks/axis-align-edge-cases.json
@@ -0,0 +1,10 @@
+[
+  {
+    "link": "https://github.com/apache/echarts/issues/21430";,
+    "comment": "The diff is introduced by changing the \"alignTicks\" logic 
and provided a better precision strategy in both ticks and dataZoom. Expected.",
+    "type": "Bug Fixing",
+    "markedBy": "100pah",
+    "lastVersion": "6.0.0",
+    "markTime": 1767946162935
+  }
+]
\ No newline at end of file
diff --git a/test/runTest/marks/axis-align-lastLabel.json 
b/test/runTest/marks/axis-align-lastLabel.json
index 22f0ac687..aa980855d 100644
--- a/test/runTest/marks/axis-align-lastLabel.json
+++ b/test/runTest/marks/axis-align-lastLabel.json
@@ -1,4 +1,12 @@
 [
+  {
+    "link": "https://github.com/apache/echarts/issues/21430";,
+    "comment": "The diff is introduced by changing the precision choosing of 
dataZoom. Intentional.",
+    "type": "Bug Fixing",
+    "markedBy": "100pah",
+    "lastVersion": "6.0.0",
+    "markTime": 1767944513407
+  },
   {
     "link": "https://github.com/apache/echarts/pull/21059";,
     "comment": "Introduce by the `outerBounds` feature that avoid axis name 
overflowing the canvas by default. In the previous result the label touches the 
canvas edge by coincidence.",
diff --git a/test/runTest/marks/axis-align-ticks-random.json 
b/test/runTest/marks/axis-align-ticks-random.json
new file mode 100644
index 000000000..f1d2b750d
--- /dev/null
+++ b/test/runTest/marks/axis-align-ticks-random.json
@@ -0,0 +1,10 @@
+[
+  {
+    "link": "https://github.com/apache/echarts/issues/21430";,
+    "comment": "The diff is introduced by changes of alignTicks strategy. 
Acceptable.",
+    "type": "Bug Fixing",
+    "markedBy": "100pah",
+    "lastVersion": "6.0.0",
+    "markTime": 1767944488907
+  }
+]
\ No newline at end of file


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

Reply via email to