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 a6ab2458f32ff0b2cf657f72c58f3267236d75eb
Author: 100pah <[email protected]>
AuthorDate: Wed Jan 14 16:24:13 2026 +0800

    feat(alignTicks): (1) Fix LogScale precision. (2) Tweak align ticks layout. 
(3) Remove unreasonable clamp in Interval calcNiceExtent, and clarify the 
definition of `_niceExtent`.
---
 src/coord/axisAlignTicks.ts     | 200 +++++++++++++++++++++++++---------------
 src/coord/axisHelper.ts         |   6 +-
 src/coord/scaleRawExtentInfo.ts |  14 ++-
 src/scale/Interval.ts           |  94 +++++++++++--------
 src/scale/Log.ts                | 128 ++++++++++---------------
 src/scale/Scale.ts              |   8 +-
 src/scale/Time.ts               |   2 -
 src/scale/break.ts              |   9 +-
 src/scale/breakImpl.ts          |  86 ++++++++---------
 src/scale/helper.ts             | 128 +++++++++++++++++--------
 test/axis-align-edge-cases.html |  97 ++++++++++++++++++-
 test/axis-align-ticks.html      |   5 +-
 12 files changed, 479 insertions(+), 298 deletions(-)

diff --git a/src/coord/axisAlignTicks.ts b/src/coord/axisAlignTicks.ts
index 4c4a6f8ac..f392c74eb 100644
--- a/src/coord/axisAlignTicks.ts
+++ b/src/coord/axisAlignTicks.ts
@@ -19,7 +19,8 @@
 
 import {
     getAcceptableTickPrecision,
-    mathAbs, mathCeil, mathFloor, mathMax, nice, NICE_MODE_MIN, round
+    getPrecision,
+    mathAbs, mathCeil, mathFloor, mathMax, mathRound, nice, NICE_MODE_MIN, 
quantity, round
 } from '../util/number';
 import IntervalScale from '../scale/Interval';
 import { adoptScaleExtentOptionAndPrepare } from './axisHelper';
@@ -28,7 +29,7 @@ import LogScale from '../scale/Log';
 import { warn } from '../util/log';
 import {
     increaseInterval, isLogScale, getIntervalPrecision, 
intervalScaleEnsureValidExtent,
-    logTransform,
+    logScaleLogTickPair,
 } from '../scale/helper';
 import { assert } from 'zrender/src/core/util';
 import { NullUndefined } from '../util/types';
@@ -42,7 +43,9 @@ export function alignScaleTicks(
 ): void {
     const isTargetLogScale = isLogScale(targetScale);
     const alignToScaleLinear = isLogScale(alignToScale) ? 
alignToScale.linearStub : alignToScale;
+    const targetScaleLinear = isTargetLogScale ? targetScale.linearStub : 
targetScale;
 
+    const targetLogScaleBase = (targetScale as LogScale).base;
     const alignToTicks = alignToScaleLinear.getTicks();
     const alignToExpNiceTicks = 
alignToScaleLinear.getTicks({expandToNicedExtent: true});
     const alignToSegCount = alignToTicks.length - 1;
@@ -67,16 +70,16 @@ export function alignScaleTicks(
     // 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
+    let t0: number; // diff ratio on min not-nice segment. 0 <= t0 < 1
+    let t1: number; // diff ratio on max not-nice segment. 0 <= t1 < 1
+    let alignToNiceSegCount: 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;
+        alignToNiceSegCount = 1;
     }
     else if (alignToSegCount === 2) {
         // `alignToTicks` is like:
@@ -84,16 +87,16 @@ export function alignScaleTicks(
         //  |-----|-| 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
+        // In this case, we choose the larger segment as the "nice 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;
+            alignToNiceSegCount = 2;
         }
         else {
-            alignToRegularSegCount = 1;
+            alignToNiceSegCount = 1;
             if (interval0 < interval1) {
                 t0 = interval0 / interval1;
             }
@@ -107,9 +110,9 @@ export function alignScaleTicks(
         //  |-|-----|-----|-| or
         //  |-----|-----|-| or
         //  |-|-----|-----| or ...
-        // At least one regular segment is present, and irregular segments are 
only present on
+        // At least one nice segment is present, and not-nice segments are 
only present on
         // the start and/or the end.
-        // In this case, ticks corresponding to regular segments are made 
"nice".
+        // In this case, ticks corresponding to nice segments are made "nice".
         const alignToInterval = alignToScaleLinear.getInterval();
         t0 = (
             1 - (alignToTicks[0].value - alignToExpNiceTicks[0].value) / 
alignToInterval
@@ -117,28 +120,36 @@ export function alignScaleTicks(
         t1 = (
             1 - (alignToExpNiceTicks[alignToSegCount].value - 
alignToTicks[alignToSegCount].value) / alignToInterval
         ) % 1;
-        alignToRegularSegCount = alignToSegCount - (t0 ? 1 : 0) - (t1 ? 1 : 0);
+        alignToNiceSegCount = alignToSegCount - (t0 ? 1 : 0) - (t1 ? 1 : 0);
     }
 
     if (__DEV__) {
-        assert(alignToRegularSegCount >= 1);
+        assert(alignToNiceSegCount >= 1);
     }
 
     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.
+    // NOTE:
+    //  Consider a case:
+    //      dataZoom controls all Y axes;
+    //      dataZoom end is 90% (maxFixed: true, maxDetermined: true);
+    //      but dataZoom start is 0% (minFixed: false, minDetermined: false);
+    //  In this case,
+    //      `Interval#calcNiceTicks` only uses `targetExtentInfo.max` as the 
upper bound but may expand the
+    //      lower bound to a "nice" tick and can get an acceptable result.
+    //      But `alignScaleTicks` has to use both `targetExtentInfo.min/max` 
as the bounds without any expansion,
+    //      otherwise the lower bound may become negative unexpectedly for all 
positive series data.
     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.
+    const targetMinMaxFixed = [
+        targetExtentInfo.minFixed || hasMinMaxDetermined,
+        targetExtentInfo.maxFixed || hasMinMaxDetermined
+    ];
+    // MEMO: When only `xxxAxis.min` or `xxxAxis.max` is fixed,
+    //  - Even a "nice" interval can be calculated, ticks accumulated based on 
`min`/`max` can be "nice" only if
+    //    `min` or `max` is a "nice" number.
+    //  - Generating a "nice" interval 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:
@@ -148,11 +159,15 @@ export function alignScaleTicks(
     //      If setting `yAxis.max: 12000, yAxis.min: 0`, ticks may be like:
     //          `12000, 9000, 6000, 3000, 0` ("nice")
 
-    let targetExtent = [targetExtentInfo.min, targetExtentInfo.max];
+    let targetRawExtent = [targetExtentInfo.min, targetExtentInfo.max];
     if (isTargetLogScale) {
-        targetExtent = logTransform(targetScale.base, targetExtent);
+        targetRawExtent = logScaleLogTickPair(targetRawExtent, 
targetLogScaleBase);
     }
-    targetExtent = intervalScaleEnsureValidExtent(targetExtent, {fixMax: 
targetMaxFixed});
+    const targetExtent = intervalScaleEnsureValidExtent(targetRawExtent, 
targetMinMaxFixed);
+    const targetMinMaxChanged = [
+        targetExtent[0] !== targetRawExtent[0],
+        targetExtent[1] !== targetRawExtent[1]
+    ];
 
     let min: number;
     let max: number;
@@ -172,9 +187,9 @@ export function alignScaleTicks(
                 break;
             }
             interval = isTargetLogScale
-                // TODO: A guardcode to avoid infinite loop, but probably it
-                // should be guranteed by `LogScale` itself.
-                ? interval * mathMax(targetScale.base, 2)
+                // TODO: `mathMax(base, 2)` is a guardcode to avoid infinite 
loop,
+                // but probably it should be guranteed by `LogScale` itself.
+                ? interval * mathMax(targetLogScaleBase, 2)
                 : increaseInterval(interval);
             intervalPrecision = getIntervalPrecision(interval);
         }
@@ -185,11 +200,24 @@ export function alignScaleTicks(
         }
     }
 
+    function updateMinFromMinNice() {
+        min = round(minNice - interval * t0, intervalPrecision);
+    }
+    function updateMaxFromMaxNice() {
+        max = round(maxNice + interval * t1, intervalPrecision);
+    }
+    function updateMinNiceFromMinT0Interval() {
+        minNice = t0 ? round(min + interval * t0, intervalPrecision) : min;
+    }
+    function updateMaxNiceFromMaxT1Interval() {
+        maxNice = t1 ? round(max - interval * t1, intervalPrecision) : max;
+    }
+
     // 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) {
+    if (targetMinMaxFixed[0] && targetMinMaxFixed[1]) {
         // 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
@@ -197,90 +225,114 @@ export function alignScaleTicks(
 
         min = targetExtent[0];
         max = targetExtent[1];
-        intervalCount = alignToRegularSegCount;
-        const rawInterval = (max - min) / (alignToRegularSegCount + t0 + t1);
+        intervalCount = alignToNiceSegCount;
+        interval = (max - min) / (alignToNiceSegCount + 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
+        // We imperically choose `pxDiffAcceptable` as `0.5 / 
alignToNiceSegCount` 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;
+        intervalPrecision = getAcceptableTickPrecision(max - min, pxSpan, 0.5 
/ alignToNiceSegCount);
+        updateMinNiceFromMinT0Interval();
+        updateMaxNiceFromMaxT1Interval();
+        interval = round(interval, intervalPrecision);
     }
     else {
         // 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);
+        const targetSpan = targetExtent[1] - targetExtent[0];
+        interval = isTargetLogScale
+            ? mathMax(quantity(targetSpan), 1)
+            : nice(targetSpan / alignToNiceSegCount, NICE_MODE_MIN);
         intervalPrecision = getIntervalPrecision(interval);
 
-        if (targetMinFixed) {
+        if (targetMinMaxFixed[0]) {
             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);
+                updateMinNiceFromMinT0Interval();
+                maxNice = round(minNice + interval * alignToNiceSegCount, 
intervalPrecision);
+                updateMaxFromMaxNice();
                 if (max >= targetExtent[1]) {
                     return true;
                 }
             });
         }
-        else if (targetMaxFixed) {
+        else if (targetMinMaxFixed[1]) {
             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);
+                updateMaxNiceFromMaxT1Interval();
+                minNice = round(maxNice - interval * alignToNiceSegCount, 
intervalPrecision);
+                updateMinFromMinNice();
                 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;
+                minNice = round(mathCeil(targetExtent[0] / interval) * 
interval, intervalPrecision);
+                maxNice = round(mathFloor(targetExtent[1] / interval) * 
interval, intervalPrecision);
+                // NOTE:
+                //  - `maxNice - minNice >= -interval` here.
+                //  - While `interval` increases, `currIntervalCount` 
decreases, minimum `-1`.
+                const currIntervalCount = mathRound((maxNice - minNice) / 
interval);
+                if (currIntervalCount <= alignToNiceSegCount) {
+                    const moreCount = alignToNiceSegCount - currIntervalCount;
+                    // Consider cases that negative series data do not make 
sense (or vice versa), users can
+                    // simply specify `xxxAxis.min/max: 0` to achieve that. 
But we still optimize it for some
+                    // common default cases whenever possible, especially when 
ec option `xxxAxis.scale: false`
+                    // (the default), it is usually unexpected if negative (or 
positive) ticks are introduced.
+                    let moreCountPair: number[];
+                    const needCrossZero = targetExtentInfo.needCrossZero;
+                    if (needCrossZero && targetExtent[0] === 0) {
+                        // 0 has been included in extent and all positive.
+                        moreCountPair = [0, moreCount];
                     }
-                }
-                else {
-                    minNice = round(mathFloor(targetExtent[0] / interval) * 
interval, intervalPrecision);
-                    maxNice = round(minNice + interval * 
alignToRegularSegCount, intervalPrecision);
-                    if (maxNice >= targetExtent[1]) {
+                    else if (needCrossZero && targetExtent[1] === 0) {
+                        // 0 has been included in extent and all negative.
+                        moreCountPair = [moreCount, 0];
+                    }
+                    else {
+                        // Try to arrange tick in the middle as possible 
corresponding to the given `alignTo`
+                        // ticks, which is especially preferable in `LogScale`.
+                        const lessHalfCount = mathFloor(moreCount / 2);
+                        moreCountPair = moreCount % 2 === 0 ? [lessHalfCount, 
lessHalfCount]
+                            : (min + max) < (targetExtent[0] + 
targetExtent[1]) ? [lessHalfCount, lessHalfCount + 1]
+                            : [lessHalfCount + 1, lessHalfCount];
+                    }
+                    minNice = round(minNice - interval * moreCountPair[0], 
intervalPrecision);
+                    maxNice = round(maxNice + interval * moreCountPair[1], 
intervalPrecision);
+                    updateMinFromMinNice();
+                    updateMaxFromMaxNice();
+                    if (min <= targetExtent[0] && max >= targetExtent[1]) {
                         return true;
                     }
                 }
             });
-            min = round(minNice - interval * t0, intervalPrecision);
-            max = round(maxNice + interval * t1, intervalPrecision);
         }
-
-        intervalPrecision = null; // Clear for the calling of `setInterval`.
     }
 
-    if (isTargetLogScale) {
-        min = targetScale.powTick(min, 0, null);
-        max = targetScale.powTick(max, 1, null);
-    }
+    const extentPrecision = isTargetLogScale
+        ? [
+            (targetMinMaxFixed[0] && !targetMinMaxChanged[0])
+                ? getPrecision(min) : null,
+            (targetMinMaxFixed[1] && !targetMinMaxChanged[1])
+                ? getPrecision(max) : null
+        ]
+        : [];
+
     // NOTE: Must in setExtent -> setInterval order.
-    targetScale.setExtent(min, max);
-    targetScale.setInterval({
+    targetScaleLinear.setExtent(min, max);
+    targetScaleLinear.setInterval({
         // Even in LogScale, `interval` should not be in log space.
         interval,
         intervalCount,
         intervalPrecision,
-        niceExtent: [minNice, maxNice]
+        extentPrecision,
+        niceExtent: [minNice, maxNice],
     });
 }
diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts
index 301bab7a9..4fa38b5ae 100644
--- a/src/coord/axisHelper.ts
+++ b/src/coord/axisHelper.ts
@@ -174,8 +174,7 @@ export function niceScaleExtent(
     scale.setExtent(extentInfo.min, extentInfo.max);
     scale.calcNiceExtent({
         splitNumber: model.get('splitNumber'),
-        fixMin: extentInfo.minFixed,
-        fixMax: extentInfo.maxFixed,
+        fixMinMax: [extentInfo.minFixed, extentInfo.maxFixed],
         minInterval: isIntervalOrTime ? model.get('minInterval') : null,
         maxInterval: isIntervalOrTime ? model.get('maxInterval') : null
     });
@@ -337,6 +336,9 @@ export function getDataDimensionsOnAxis(data: SeriesData, 
axisDim: string): Dime
     return zrUtil.keys(dataDimMap);
 }
 
+/**
+ * FIXME: refactor - merge with `Scale#unionExtentFromData`
+ */
 export function unionAxisExtentFromData(dataExtent: number[], data: 
SeriesData, axisDim: string): void {
     if (data) {
         zrUtil.each(getDataDimensionsOnAxis(data, axisDim), function (dim) {
diff --git a/src/coord/scaleRawExtentInfo.ts b/src/coord/scaleRawExtentInfo.ts
index 8ff4e78a0..f3ae0b923 100644
--- a/src/coord/scaleRawExtentInfo.ts
+++ b/src/coord/scaleRawExtentInfo.ts
@@ -38,10 +38,11 @@ export interface ScaleRawExtentResult {
     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".
+    // `minFixed`/`maxFixed` is `true` iff:
+    //  - ec option `xxxAxis.min/max` are specified, or
+    //  - `scaleRawExtentResult.minDetermined/maxDetermined` are `true`
+    // They typically suggest axes to use `scaleRawExtentResult.min/max` 
directly
+    // as their bounds, instead of expanding the extent by some "nice 
strategy".
     readonly minFixed: boolean;
     readonly maxFixed: boolean;
 
@@ -51,6 +52,7 @@ export interface ScaleRawExtentResult {
 
     // Mark that the axis should be blank.
     readonly isBlank: boolean;
+    readonly needCrossZero: boolean;
 }
 
 export class ScaleRawExtentInfo {
@@ -250,7 +252,8 @@ export class ScaleRawExtentInfo {
             || (isOrdinal && !axisDataLen);
 
         // If data extent modified, need to recalculated to ensure cross zero.
-        if (this._needCrossZero) {
+        const needCrossZero = this._needCrossZero;
+        if (needCrossZero) {
             // Axis is over zero and min is not set
             if (min > 0 && max > 0 && !minFixed) {
                 min = 0;
@@ -295,6 +298,7 @@ export class ScaleRawExtentInfo {
             minDetermined: minDetermined,
             maxDetermined: maxDetermined,
             isBlank: isBlank,
+            needCrossZero: needCrossZero,
         };
     }
 
diff --git a/src/scale/Interval.ts b/src/scale/Interval.ts
index 98ca9daec..a2b7cf9a3 100644
--- a/src/scale/Interval.ts
+++ b/src/scale/Interval.ts
@@ -24,7 +24,7 @@ import Scale, { ScaleGetTicksOpt, ScaleSettingDefault } from 
'./Scale';
 import * as helper from './helper';
 import {ScaleTick, ParsedAxisBreakList, ScaleDataValue, NullUndefined} from 
'../util/types';
 import { getScaleBreakHelper } from './break';
-import { assert } from 'zrender/src/core/util';
+import { assert, retrieve2 } from 'zrender/src/core/util';
 
 class IntervalScale<SETTING extends ScaleSettingDefault = ScaleSettingDefault> 
extends Scale<SETTING> {
 
@@ -34,13 +34,30 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
     // Step is calculated in adjustExtent.
     protected _interval: number = 0;
     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.
+    protected _extentPrecision: number[] = [];
+    /**
+     * `_intervalCount` effectively specifies the number of "nice segments". 
This is for special cases,
+     * such as `alignTicks: 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 
due to cumulative errors.
+     * So `_intervalCount` is required to specify the expected nice ticks 
number.
+     * Should ensure `_intervalCount >= -1`,
+     *  where `-1` means no nice tick (e.g., `_extent: [5.2, 5.8], _interval: 
1`),
+     *  and `0` means only one nice tick (e.g., `_extent: [5, 5.8], _interval: 
1`).
+     * @see setInterval
+     */
     private _intervalCount: number | NullUndefined = undefined;
-    // Should ensure: `_extent[0] <= _niceExtent[0] && _niceExtent[1] <= 
_extent[1]`
+    /**
+     * Should ensure:
+     *  `_extent[0] <= _niceExtent[0] && _niceExtent[1] <= _extent[1]`
+     * But NOTICE:
+     *  `_niceExtent[0] - _niceExtent[1] <= _interval`, rather than always `< 
0`,
+     *  because `_niceExtent` is typically calculated by
+     *  `[ Math.ceil(_extent[0] / _interval) * _interval, 
Math.floor(_extent[1] / _interval) * _interval ]`.
+     *  e.g., `_extent: [5.2, 5.8]` with interval `1` will get `_niceExtent: 
[6, 5]`.
+     *  e.g., `_extent: [5, 5.8]` with interval `1` will get `_niceExtent: [5, 
5]`.
+     * @see setInterval
+     */
     protected _niceExtent: [number, number];
 
 
@@ -90,53 +107,43 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
     /**
      * @final override is DISALLOWED.
      */
-    setInterval({interval, intervalCount, intervalPrecision, niceExtent}: {
+    setInterval({interval, intervalCount, intervalPrecision, extentPrecision, 
niceExtent}: {
         interval?: number | NullUndefined;
+        // See comments of `_intervalCount`.
         intervalCount?: number | NullUndefined;
         intervalPrecision?: number | NullUndefined;
+        extentPrecision?: number[] | NullUndefined;
         niceExtent?: number[];
     }): void {
-        const intervalCountSpecified = intervalCount != null;
+        const extent = this._extent;
+
         if (__DEV__) {
             assert(interval != null);
-            if (intervalCountSpecified) {
+            if (intervalCount != null) {
                 assert(
-                    intervalCount > 0
+                    intervalCount >= -1
                     && 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]
-                );
+                assert(isFinite(niceExtent[0]) && isFinite(niceExtent[1]));
+                assert(extent[0] <= niceExtent[0] && niceExtent[1] <= 
extent[1]);
+                assert(round(niceExtent[0] - niceExtent[1], 
getPrecision(interval)) <= interval);
             }
         }
-        niceExtent = this._niceExtent = niceExtent != null
-            ? niceExtent.slice() as [number, number]
+
+        // Set or clear
+        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;
-
-        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;
-        }
+        this._intervalCount = intervalCount;
+        this._intervalPrecision = retrieve2(intervalPrecision, 
helper.getIntervalPrecision(interval));
+        this._extentPrecision = extentPrecision || [];
     }
 
     /**
@@ -191,13 +198,17 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
             ;
             niceTickIdx++
         ) {
+            // Consider case `_extent: [5.2, 5.8], _niceExtent: [6, 5], 
interval: 1`,
+            //  `_intervalCount` makes sense iff `-1`.
+            // Consider case `_extent: [5, 5.8], _niceExtent: [5, 5], 
interval: 1`,
+            //  `_intervalCount` makes sense iff `0`.
             if (intervalCount == null) {
-                if (tick > niceTickExtent[1]) {
+                if (tick > niceTickExtent[1] || !isFinite(tick) || 
!isFinite(niceTickExtent[1])) {
                     break;
                 }
             }
             else {
-                if (niceTickIdx > intervalCount) { // ticks number should be 
`intervalCount + 1`
+                if (niceTickIdx > intervalCount) { // nice ticks number should 
be `intervalCount + 1`
                     break;
                 }
                 // Consider cumulative error, especially caused by rounding, 
the last nice
@@ -398,12 +409,13 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
     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,
+        fixMinMax?: boolean[], // [fixMin, fixMax]
         minInterval?: number,
         maxInterval?: number
     }): void {
-        let extent = helper.intervalScaleEnsureValidExtent(this._extent, opt);
+        const fixMinMax = opt.fixMinMax || [];
+
+        let extent = helper.intervalScaleEnsureValidExtent(this._extent, 
fixMinMax);
 
         this._innerSetExtent(extent[0], extent[1]);
         extent = this._extent.slice() as [number, number];
@@ -412,10 +424,10 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
         const interval = this._interval;
         const intervalPrecition = this._intervalPrecision;
 
-        if (!opt.fixMin) {
+        if (!fixMinMax[0]) {
             extent[0] = round(mathFloor(extent[0] / interval) * interval, 
intervalPrecition);
         }
-        if (!opt.fixMax) {
+        if (!fixMinMax[1]) {
             extent[1] = round(mathCeil(extent[1] / interval) * interval, 
intervalPrecition);
         }
         this._innerSetExtent(extent[0], extent[1]);
diff --git a/src/scale/Log.ts b/src/scale/Log.ts
index d7be77057..ea32d8005 100644
--- a/src/scale/Log.ts
+++ b/src/scale/Log.ts
@@ -21,7 +21,8 @@ import * as zrUtil from 'zrender/src/core/util';
 import Scale, { ScaleGetTicksOpt, ScaleSettingDefault } from './Scale';
 import {
     mathFloor, mathCeil, mathPow, mathLog,
-    round, quantity, getPrecision
+    round, quantity, getPrecision,
+    mathMax,
 } from '../util/number';
 
 // Use some method of IntervalScale
@@ -31,14 +32,18 @@ import {
     ScaleTick,
     NullUndefined
 } from '../util/types';
-import { ensureValidSplitNumber, fixNiceExtent, getIntervalPrecision, 
logTransform } from './helper';
+import {
+    ensureValidSplitNumber, getIntervalPrecision,
+    logScalePowTickPair, logScalePowTick, logScaleLogTickPair,
+    getExtentPrecision
+} from './helper';
 import SeriesData from '../data/SeriesData';
 import { getScaleBreakHelper } from './break';
 
 
 const LINEAR_STUB_METHODS = [
-    'getExtent', 'getTicks', 'getInterval'
-    // Keep no setting method to mitigate vulnerability.
+    'getExtent', 'getTicks', 'getInterval',
+    'setExtent', 'setInterval',
 ] as const;
 
 /**
@@ -46,7 +51,8 @@ const LINEAR_STUB_METHODS = [
  *  - 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).
+ *    (before logarithm applied, such as raw extent; but may be still invalid, 
and not sync to the
+ *     calculated ("nice") extent).
  */
 class LogScale extends IntervalScale {
 
@@ -57,9 +63,6 @@ class LogScale extends IntervalScale {
 
     private _originalScale = new IntervalScale();
 
-    // `[fixMin, fixMax]`
-    private _fixMinMax: boolean[] = [false, false];
-
     linearStub: Pick<IntervalScale, (typeof LINEAR_STUB_METHODS)[number]>;
 
     constructor(logBase: number | NullUndefined, settings?: 
ScaleSettingDefault) {
@@ -84,30 +87,35 @@ class LogScale extends IntervalScale {
      * @param Whether expand the ticks to niced extent.
      */
     getTicks(opt?: ScaleGetTicksOpt): ScaleTick[] {
-        const extent = this._extent;
+        const base = this.base;
+        const originalScale = this._originalScale;
         const scaleBreakHelper = getScaleBreakHelper();
+        const extent = this._extent;
+        const extentPrecision = this._extentPrecision;
 
         return zrUtil.map(super.getTicks(opt || {}), function (tick) {
+            const val = tick.value;
+            let powVal = logScalePowTick(
+                val,
+                base,
+                getExtentPrecision(val, extent, extentPrecision)
+            );
+
             let vBreak;
-            let brkRoundingCriterion;
             if (scaleBreakHelper) {
-                const transformed = scaleBreakHelper.getTicksLogTransformBreak(
+                const brkPowResult = scaleBreakHelper.getTicksPowBreak(
                     tick,
-                    this.base,
-                    this._originalScale._innerGetBreaks(),
-                    fixRoundingError
+                    base,
+                    originalScale._innerGetBreaks(),
+                    extent,
+                    extentPrecision
                 );
-                vBreak = transformed.vBreak;
-                brkRoundingCriterion = transformed.brkRoundingCriterion;
+                if (brkPowResult) {
+                    vBreak = brkPowResult.vBreak;
+                    powVal = brkPowResult.tickPowValue;
+                }
             }
 
-            const val = tick.value;
-            const powVal = this.powTick(
-                val,
-                val === extent[1] ? 1 : val === extent[0] ? 0 : null,
-                brkRoundingCriterion
-            );
-
             return {
                 value: powVal,
                 break: vBreak,
@@ -122,55 +130,25 @@ 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]);
+        const loggedExtent = logScaleLogTickPair([start, end], this.base);
         super.setExtent(loggedExtent[0], loggedExtent[1]);
     }
 
     getExtent() {
         const extent = super.getExtent();
-        return [
-            this.powTick(extent[0], 0, null),
-            this.powTick(extent[1], 1, null)
-        ] as [number, number];
+        return logScalePowTickPair(
+            extent,
+            this.base,
+            this._extentPrecision
+        );
     }
 
     unionExtentFromData(data: SeriesData, dim: DimensionName | 
DimensionLoose): void {
         this._originalScale.unionExtentFromData(data, dim);
-        const loggedOther = logTransform(this.base, 
data.getApproximateExtent(dim), true);
+        const loggedOther = 
logScaleLogTickPair(data.getApproximateExtent(dim), this.base, true);
         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 splitNumber default 10 Given approx tick number
@@ -183,7 +161,9 @@ class LogScale extends IntervalScale {
             return;
         }
 
-        let interval = quantity(span);
+        // Interval should be integer
+        let interval = mathMax(quantity(span), 1);
+
         const err = splitNumber / span * interval;
 
         // Filter ticks to get closer to the desired count.
@@ -192,19 +172,12 @@ class LogScale extends IntervalScale {
             interval *= 10;
         }
 
-        // Interval should be integer
-        while (!isNaN(interval) && Math.abs(interval) < 1 && 
Math.abs(interval) > 0) {
-            interval *= 10;
-        }
-
         const intervalPrecision = getIntervalPrecision(interval);
         const niceExtent = [
             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 = intervalPrecision;
         this._niceExtent = niceExtent;
@@ -214,14 +187,20 @@ class LogScale extends IntervalScale {
 
     calcNiceExtent(opt: {
         splitNumber: number,
-        fixMin?: boolean,
-        fixMax?: boolean,
+        fixMinMax?: boolean[],
         minInterval?: number,
         maxInterval?: number
     }): void {
+        const oldExtent = this._extent.slice() as [number, number];
         super.calcNiceExtent(opt);
-
-        this._fixMinMax = [!!opt.fixMin, !!opt.fixMax];
+        const newExtent = this._extent;
+
+        this._extentPrecision = [
+            (opt.fixMinMax && opt.fixMinMax[0] && oldExtent[0] === 
newExtent[0])
+                ? getPrecision(newExtent[0]) : null,
+            (opt.fixMinMax && opt.fixMinMax[1] && oldExtent[1] === 
newExtent[1])
+                ? getPrecision(newExtent[1]) : null
+        ];
     }
 
     contain(val: number): boolean {
@@ -257,11 +236,6 @@ class LogScale extends IntervalScale {
 
 }
 
-function fixRoundingError(val: number, originalVal: number): number {
-    return round(val, getPrecision(originalVal));
-}
-
-
 Scale.registerClass(LogScale);
 
 export default LogScale;
diff --git a/src/scale/Scale.ts b/src/scale/Scale.ts
index 801e2a2eb..f46266572 100644
--- a/src/scale/Scale.ts
+++ b/src/scale/Scale.ts
@@ -244,13 +244,7 @@ abstract class Scale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault>
     ): void;
 
     abstract calcNiceExtent(
-        opt?: {
-            splitNumber?: number,
-            fixMin?: boolean,
-            fixMax?: boolean,
-            minInterval?: number,
-            maxInterval?: number
-        }
+        opt?: {}
     ): void;
 
     /**
diff --git a/src/scale/Time.ts b/src/scale/Time.ts
index eb682440c..3549384d6 100644
--- a/src/scale/Time.ts
+++ b/src/scale/Time.ts
@@ -254,8 +254,6 @@ class TimeScale extends IntervalScale<TimeScaleSetting> {
     calcNiceExtent(
         opt?: {
             splitNumber?: number,
-            fixMin?: boolean,
-            fixMax?: boolean,
             minInterval?: number,
             maxInterval?: number
         }
diff --git a/src/scale/break.ts b/src/scale/break.ts
index ccac04680..2f994b6f0 100644
--- a/src/scale/break.ts
+++ b/src/scale/break.ts
@@ -114,15 +114,16 @@ export type ScaleBreakHelper = {
     ): (
         TReturnIdx extends false ? TItem[][] : number[][]
     );
-    getTicksLogTransformBreak(
+    getTicksPowBreak(
         tick: ScaleTick,
         logBase: number,
         logOriginalBreaks: ParsedAxisBreakList,
-        fixRoundingError: (val: number, originalVal: number) => number
+        extent: number[],
+        extentPrecision: (number | NullUndefined)[],
     ): {
-        brkRoundingCriterion: number | NullUndefined;
+        tickPowValue: number | NullUndefined;
         vBreak: VisualAxisBreak | NullUndefined;
-    };
+    } | NullUndefined;
     logarithmicParseBreaksFromOption(
         breakOptionList: AxisBreakOption[],
         logBase: number,
diff --git a/src/scale/breakImpl.ts b/src/scale/breakImpl.ts
index 67d2eaa65..8c5938f45 100644
--- a/src/scale/breakImpl.ts
+++ b/src/scale/breakImpl.ts
@@ -17,7 +17,7 @@
 * under the License.
 */
 
-import { assert, clone, each, find, isString, map, trim } from 
'zrender/src/core/util';
+import { assert, clone, each, find, isString, map, retrieve2, trim } from 
'zrender/src/core/util';
 import {
     NullUndefined, ParsedAxisBreak, ParsedAxisBreakList, AxisBreakOption,
     AxisBreakOptionIdentifierInAxis, ScaleTick, VisualAxisBreak,
@@ -25,8 +25,9 @@ import {
 import { error } from '../util/log';
 import type Scale from './Scale';
 import { ScaleBreakContext, AxisBreakParsingResult, 
registerScaleBreakHelperImpl, ParamPruneByBreak } from './break';
-import { round as fixRound } from '../util/number';
+import { getPrecision, mathMax, mathMin, mathRound } from '../util/number';
 import { AxisLabelFormatterExtraParams } from '../coord/axisCommonTypes';
+import { getExtentPrecision, logScaleLogTick, logScaleLogTickPair, 
logScalePowTick } from './helper';
 
 /**
  * @caution
@@ -83,7 +84,7 @@ class ScaleBreakContextImpl implements ScaleBreakContext {
                 const multiple = estimateNiceMultiple(tickVal, brk.vmax);
                 if (__DEV__) {
                     // If not, it may cause dead loop or not nice tick.
-                    assert(multiple >= 0 && Math.round(multiple) === multiple);
+                    assert(multiple >= 0 && mathRound(multiple) === multiple);
                 }
                 return multiple;
             }
@@ -320,7 +321,7 @@ function updateAxisBreakGapReal(
         if (gapParsed.type === 'tpPrct') {
             brk.gapReal = gapPrctSum !== 0
                 // prctBrksGapRealSum is supposed to be non-negative but add a 
safe guard
-                ? Math.max(prctBrksGapRealSum, 0) * gapParsed.val / gapPrctSum 
: 0;
+                ? mathMax(prctBrksGapRealSum, 0) * gapParsed.val / gapPrctSum 
: 0;
         }
         if (gapParsed.type === 'tpAbs') {
             brk.gapReal = gapParsed.val;
@@ -430,8 +431,8 @@ function clampBreakByExtent(
     brk: ParsedAxisBreak,
     scaleExtent: [number, number]
 ): NullUndefined | ParsedAxisBreak {
-    const vmin = Math.max(brk.vmin, scaleExtent[0]);
-    const vmax = Math.min(brk.vmax, scaleExtent[1]);
+    const vmin = mathMax(brk.vmin, scaleExtent[0]);
+    const vmax = mathMin(brk.vmax, scaleExtent[1]);
     return (
             vmin < vmax
             || (vmin === vmax && vmin > scaleExtent[0] && vmin < 
scaleExtent[1])
@@ -619,46 +620,47 @@ function retrieveAxisBreakPairs<TItem, TReturnIdx extends 
boolean>(
     return result;
 }
 
-function getTicksLogTransformBreak(
+function getTicksPowBreak(
     tick: ScaleTick,
     logBase: number,
     logOriginalBreaks: ParsedAxisBreakList,
-    fixRoundingError: (val: number, originalVal: number) => number
+    extent: number[],
+    extentPrecision: (number | NullUndefined)[],
 ): {
-    brkRoundingCriterion: number;
+    tickPowValue: number;
     vBreak: VisualAxisBreak | NullUndefined;
-} {
-    let vBreak: VisualAxisBreak | NullUndefined;
-    let brkRoundingCriterion: number;
-
-    if (tick.break) {
-        const brk = tick.break.parsedBreak;
-        const originalBreak = find(logOriginalBreaks, brk => identifyAxisBreak(
-            brk.breakOption, tick.break.parsedBreak.breakOption
-        ));
-        const vmin = fixRoundingError(Math.pow(logBase, brk.vmin), 
originalBreak.vmin);
-        const vmax = fixRoundingError(Math.pow(logBase, brk.vmax), 
originalBreak.vmax);
-        const gapParsed = {
-            type: brk.gapParsed.type,
-            val: brk.gapParsed.type === 'tpAbs'
-                ? fixRound(Math.pow(logBase, brk.vmin + brk.gapParsed.val)) - 
vmin
-                : brk.gapParsed.val,
-        };
-        vBreak = {
-            type: tick.break.type,
-            parsedBreak: {
-                breakOption: brk.breakOption,
-                vmin,
-                vmax,
-                gapParsed,
-                gapReal: brk.gapReal,
-            }
-        };
-        brkRoundingCriterion = originalBreak[tick.break.type];
+    // Return: If not found, return null/undefined.
+} | NullUndefined {
+    if (!tick.break) {
+        return;
     }
 
+    const brk = tick.break.parsedBreak;
+    const originalBreak = find(logOriginalBreaks, brk => identifyAxisBreak(
+        brk.breakOption, tick.break.parsedBreak.breakOption
+    ));
+    const minPrecision = getExtentPrecision(brk.vmin, extent, extentPrecision);
+    const maxPrecision = getExtentPrecision(brk.vmax, extent, extentPrecision);
+    // NOTE: `tick.break` may be clamped by scale extent. For consistency we 
always
+    // pow back, or heuristically use the user input original break to obtain 
an
+    // acceptable rounding precision for display.
+    const vmin = logScalePowTick(brk.vmin, logBase, retrieve2(minPrecision, 
getPrecision(originalBreak.vmin)));
+    const vmax = logScalePowTick(brk.vmax, logBase, retrieve2(maxPrecision, 
getPrecision(originalBreak.vmax)));
+    const parsedBreak = {
+        vmin,
+        vmax,
+        // They are not changed by extent clamping.
+        breakOption: brk.breakOption,
+        gapParsed: clone(originalBreak.gapParsed),
+        gapReal: brk.gapReal,
+    };
+    const vBreak = {
+        type: tick.break.type,
+        parsedBreak,
+    };
+
     return {
-        brkRoundingCriterion,
+        tickPowValue: parsedBreak[vBreak.type],
         vBreak,
     };
 }
@@ -675,14 +677,12 @@ function logarithmicParseBreaksFromOption(
     const parsedOriginal = parseAxisBreakOption(breakOptionList, parse, opt);
 
     const parsedLogged = parseAxisBreakOption(breakOptionList, parse, opt);
-    const loggedBase = Math.log(logBase);
     parsedLogged.breaks = map(parsedLogged.breaks, brk => {
-        const vmin = Math.log(brk.vmin) / loggedBase;
-        const vmax = Math.log(brk.vmax) / loggedBase;
+        const [vmin, vmax] = logScaleLogTickPair([brk.vmin, brk.vmax], 
logBase, true);
         const gapParsed = {
             type: brk.gapParsed.type,
             val: brk.gapParsed.type === 'tpAbs'
-                ? (Math.log(brk.vmin + brk.gapParsed.val) / loggedBase) - vmin
+                ? logScaleLogTick(brk.vmin + brk.gapParsed.val, logBase, true) 
- vmin
                 : brk.gapParsed.val,
         };
         return {
@@ -722,7 +722,7 @@ export function installScaleBreakHelper(): void {
         identifyAxisBreak,
         serializeAxisBreakIdentifier,
         retrieveAxisBreakPairs,
-        getTicksLogTransformBreak,
+        getTicksPowBreak,
         logarithmicParseBreaksFromOption,
         makeAxisLabelFormatterParamBreak,
     });
diff --git a/src/scale/helper.ts b/src/scale/helper.ts
index f89abca5d..bb10ae051 100644
--- a/src/scale/helper.ts
+++ b/src/scale/helper.ts
@@ -17,7 +17,11 @@
 * under the License.
 */
 
-import {getPrecision, round, nice, quantityExponent, mathPow, mathMax, 
mathRound} from '../util/number';
+import {
+    getPrecision, round, nice, quantityExponent,
+    mathPow, mathMax, mathRound,
+    mathLog, mathAbs, mathFloor, mathCeil, mathMin
+} from '../util/number';
 import IntervalScale from './Interval';
 import LogScale from './Log';
 import type Scale from './Scale';
@@ -89,13 +93,11 @@ export function intervalScaleNiceTicks(
     }
     const precision = result.intervalPrecision = 
getIntervalPrecision(interval);
     // Niced extent inside original extent
-    const niceTickExtent = result.niceTickExtent = [
-        round(Math.ceil(extent[0] / interval) * interval, precision),
-        round(Math.floor(extent[1] / interval) * interval, precision)
+    result.niceTickExtent = [
+        round(mathCeil(extent[0] / interval) * interval, precision),
+        round(mathFloor(extent[1] / interval) * interval, precision)
     ];
 
-    fixNiceExtent(niceTickExtent, extent);
-
     return result;
 }
 
@@ -133,26 +135,6 @@ export function getIntervalPrecision(niceInterval: 
number): number {
     return getPrecision(niceInterval) + 2;
 }
 
-
-function clamp(
-    niceTickExtent: [number, number], idx: number, extent: [number, number]
-): void {
-    niceTickExtent[idx] = Math.max(Math.min(niceTickExtent[idx], extent[1]), 
extent[0]);
-}
-
-// In some cases (e.g., splitNumber is 1), niceTickExtent may be out of extent.
-export function fixNiceExtent(
-    niceTickExtent: [number, number], extent: [number, number]
-): void {
-    !isFinite(niceTickExtent[0]) && (niceTickExtent[0] = extent[0]);
-    !isFinite(niceTickExtent[1]) && (niceTickExtent[1] = extent[1]);
-    clamp(niceTickExtent, 0, extent);
-    clamp(niceTickExtent, 1, extent);
-    if (niceTickExtent[0] > niceTickExtent[1]) {
-        niceTickExtent[0] = niceTickExtent[1];
-    }
-}
-
 export function contain(val: number, extent: [number, number]): boolean {
     return val >= extent[0] && val <= extent[1];
 }
@@ -192,17 +174,79 @@ function scale(
     return val * (extent[1] - extent[0]) + extent[0];
 }
 
-export function logTransform(base: number, extent: number[], noClampNegative?: 
boolean): [number, number] {
-    const loggedBase = Math.log(base);
+/**
+ * @see logScaleLogTick
+ */
+export function logScaleLogTickPair(
+    pair: number[],
+    base: number,
+    noClampNegative?: boolean
+): [number, number] {
     return [
-        // log(negative) is NaN, so safe guard here.
-        // PENDING: But even getting a -Infinity still does not make sense in 
extent.
-        //  Just keep it as is, getting a NaN to make some previous cases 
works by coincidence.
-        Math.log(noClampNegative ? extent[0] : Math.max(0, extent[0])) / 
loggedBase,
-        Math.log(noClampNegative ? extent[1] : Math.max(0, extent[1])) / 
loggedBase
+        logScaleLogTick(pair[0], base, noClampNegative),
+        logScaleLogTick(pair[1], base, noClampNegative)
     ];
 }
 
+export function logScaleLogTick(
+    val: number,
+    base: number,
+    noClampNegative?: boolean
+): number {
+    // log(negative) is NaN, so safe guard here.
+    // PENDING: But even getting a -Infinity still does not make sense in 
extent.
+    //  Just keep it as is, getting a NaN to make some previous cases works by 
coincidence.
+    return mathLog(noClampNegative ? val : mathMax(0, val)) / mathLog(base);
+    // NOTE: rounding error may happen above, typically expecting 
`log10(1000)` but actually
+    // getting `2.9999999999999996`, but generally it does not matter since 
they are not
+    // used to display.
+}
+
+/**
+ * @see logScalePowTick
+ */
+export function logScalePowTickPair(
+    linearPair: number[],
+    base: number,
+    precisionPair: (number | NullUndefined)[],
+): [number, number] {
+    return [
+        logScalePowTick(linearPair[0], base, precisionPair[0]),
+        logScalePowTick(linearPair[1], base, precisionPair[1])
+    ] as [number, number];
+}
+
+/**
+ * Cumulative rounding errors cause the logarithm operation to become 
non-invertible by simply exponentiation.
+ *  - `Math.pow(10, integer)` itself has no rounding error. But,
+ *  - If `linearTickVal` is generated internally by `calcNiceTicks`, it may be 
still "not nice" (not an integer)
+ *    when it is `extent[i]`.
+ *  - If `linearTickVal` is generated outside (e.g., by `alignScaleTicks`) and 
set by `setExtent`,
+ *    `logScaleLogTickPair` may already have introduced rounding errors even 
for "nice" values.
+ * But invertible is required when the original `extent[i]` need to be 
respected, or "nice" ticks need to be
+ * displayed instead of something like `5.999999999999999`, which is addressed 
in this function by providing
+ * a `precision`.
+ * See also `#4158`.
+ */
+export function logScalePowTick(
+    // `tickVal` should be in the linear space.
+    linearTickVal: number,
+    base: number,
+    precision: number | NullUndefined,
+): number {
+
+    // NOTE: Even when min/max is required to be fixed, `pow(base, tickVal)` 
is not necessarily equal to
+    // `originalPowExtent[0]`/`[1]`. e.g., when `originalPowExtent` is a 
invalid extent but
+    // `tickVal` has been adjusted to make it valid. So we always use 
`Math.pow`.
+    let powVal = mathPow(base, linearTickVal);
+
+    if (precision != null) {
+        powVal = round(powVal, precision);
+    }
+
+    return powVal;
+}
+
 /**
  * A valid extent is:
  *  - No non-finite number.
@@ -215,9 +259,7 @@ export function logTransform(base: number, extent: 
number[], noClampNegative?: b
  */
 export function intervalScaleEnsureValidExtent(
     rawExtent: number[],
-    opt: {
-        fixMax?: boolean
-    }
+    fixMinMax: boolean[],
 ): number[] {
     const extent = rawExtent.slice();
     // If extent start and end are same, expand them
@@ -225,13 +267,13 @@ export function intervalScaleEnsureValidExtent(
         if (extent[0] !== 0) {
             // Expand extent
             // Note that extents can be both negative. See #13154
-            const expandSize = Math.abs(extent[0]);
+            const expandSize = mathAbs(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) {
+            if (!fixMinMax[1]) {
                 extent[1] += expandSize / 2;
                 extent[0] -= expandSize / 2;
             }
@@ -262,3 +304,13 @@ export function ensureValidSplitNumber(
     rawSplitNumber = rawSplitNumber || defaultSplitNumber;
     return mathRound(mathMax(rawSplitNumber, 1));
 }
+
+export function getExtentPrecision(
+    val: number,
+    extent: number[],
+    extentPrecision: (number | NullUndefined)[],
+): number | NullUndefined {
+    return val === extent[0] ? extentPrecision[0]
+        : val === extent[1] ? extentPrecision[1]
+        : null;
+}
diff --git a/test/axis-align-edge-cases.html b/test/axis-align-edge-cases.html
index d851f1bdd..4c1bc4b88 100644
--- a/test/axis-align-edge-cases.html
+++ b/test/axis-align-edge-cases.html
@@ -42,12 +42,13 @@ under the License.
         </style>
 
 
-        <!-- <div id="main_cartesian_0_integerData"></div>
+        <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_radar_0"></div>
         <div id="main_cartesian_0_logIntegerData"></div>
+        <div id="main_cartesian_middle_preferable"></div>
 
 
 
@@ -734,13 +735,103 @@ under the License.
                 create_case_main_cartesian_0(
                     echarts,
                     'main_cartesian_0_logIntegerData',
-                    'The right yAxis is "log"; the right series are integer 
Data',
+                    'The right yAxis is **"log"**; the right series are 
integer Data',
                     TEST_DATA_INTEGER
                 );
             }); // End of `require`
         </script>
 
 
+
+        <script>
+            require([
+                'echarts',
+            ], function (echarts /*, data */) {
+                _ctx = {
+                    rightYAxisScale: true,
+                    rightSeriesMax: 4000,
+                    rightSeriesMin: 3099,
+                };
+
+                function createOption() {
+                    return {
+                        tooltip: {},
+                        grid: {
+                            top: 50,
+                            bottom: 50,
+                        },
+                        xAxis: {
+                            max: 1300
+                        },
+                        yAxis: [{
+                            id: 'left',
+                            splitNumber: 5,
+                        }, {
+                            id: 'right',
+                            alignTicks: true,
+                            scale: _ctx.rightYAxisScale,
+                        }],
+                        legend: {
+                            top: 5
+                        },
+                        series: [{
+                            type: 'line',
+                            name: 'lineLeft',
+                            label: {show: true, position: 'top'},
+                            yAxisIndex: 0,
+                            data: [[10, 0], [20, 10], [30, 20], [40, 30]]
+                        }, {
+                            type: 'line',
+                            name: 'lineRight',
+                            label: {show: true, position: 'top'},
+                            yAxisIndex: 1,
+                            data: [[1000, _ctx.rightSeriesMin], [1200, 
_ctx.rightSeriesMax]]
+                        }]
+                    };
+                }
+                function updateOption() {
+                    chart.setOption(createOption(), {notMerge: true});
+                }
+
+                var chart = testHelper.create(echarts, 
'main_cartesian_middle_preferable', {
+                    title: [
+                        `The right yAxis aligns to the left yAxis.`,
+                        `The right series should be laid out at the **middle 
of Y** whenever possible.`,
+                    ],
+                    option: createOption(),
+                    height: 300,
+                    inputsStyle: 'compact',
+                    inputs: [{
+                        type: 'select',
+                        text: 'rightYAxis.scale:',
+                        values: [true, false],
+                        onchange: function () {
+                            _ctx.rightYAxisScale = this.value;
+                            updateOption();
+                        }
+                    }, {
+                        type: 'select',
+                        text: 'rightSeries.min:',
+                        values: [_ctx.rightSeriesMin, 3000, 3010],
+                        onchange: function () {
+                            _ctx.rightSeriesMin = this.value;
+                            updateOption();
+                        }
+                    }, {
+                        type: 'select',
+                        text: 'rightSeries.max:',
+                        values: [_ctx.rightSeriesMax, 4001, 4190],
+                        onchange: function () {
+                            _ctx.rightSeriesMax = this.value;
+                            updateOption();
+                        }
+                    }]
+                }); // End of `testHelper.create`
+            }); // End of `require`
+        </script>
+
+
+
     </body>
 </html>
 
diff --git a/test/axis-align-ticks.html b/test/axis-align-ticks.html
index b0a761a84..e22f2a99c 100644
--- a/test/axis-align-ticks.html
+++ b/test/axis-align-ticks.html
@@ -40,7 +40,7 @@ under the License.
         <div id="main0"></div>
         <div id="main1"></div>
         <div id="main2"></div>
-        <div id="main3"></div>
+        <div id="main_Log_axis_can_alignTicks_to_value_axis"></div>
         <div id="main4"></div>
         <div id="main5"></div>
         <div id="main6"></div>
@@ -228,6 +228,7 @@ under the License.
 
                 var option = {
                     legend: {},
+                    tooltip: {},
                     xAxis: {
                         type: 'category',
                         name: 'x',
@@ -258,7 +259,7 @@ under the License.
                     ]
                 }
 
-                var chart = testHelper.create(echarts, 'main3', {
+                var chart = testHelper.create(echarts, 
'main_Log_axis_can_alignTicks_to_value_axis', {
                     title: [
                         'Log axis can alignTicks to value axis'
                     ],


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

Reply via email to