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 18a23a87585cc8a1575b63dcc4060af5007330fc
Author: 100pah <[email protected]>
AuthorDate: Mon Jan 19 02:30:47 2026 +0800

    refactor(scale): For readability and maintainability (1) Migrate 
`calcNiceTicks` and `calcNiceExtent` from Scale class override member functions 
to plain functions, similar to `axisAlignTicks`. Previously it's hard to modify 
and error-prone. (2) Remove unnecessary override on Scale class hierarchy and 
limit override usage, which is difficult to understand and error-prone. (3) 
Simplify the inheritance - shift `LogScale` and `TimeScale` inheritance from 
`IntervalScale` to `Scale`. (4) C [...]
---
 src/component/dataZoom/AxisProxy.ts              |   6 +-
 src/component/timeline/SliderTimelineView.ts     |  40 +---
 src/component/timeline/TimelineAxis.ts           |   6 +-
 src/component/timeline/TimelineModel.ts          |   4 -
 src/coord/axisAlignTicks.ts                      |  29 ++-
 src/coord/axisCommonTypes.ts                     |   3 +
 src/coord/axisHelper.ts                          |  61 ++---
 src/coord/axisNiceTicks.ts                       | 272 ++++++++++++++++++++++
 src/coord/cartesian/Grid.ts                      |   6 +-
 src/coord/cartesian/defaultAxisExtentFromData.ts |   4 +-
 src/coord/parallel/Parallel.ts                   |   3 +-
 src/coord/polar/polarCreator.ts                  |   6 +-
 src/coord/radar/Radar.ts                         |   2 +-
 src/coord/single/Single.ts                       |   3 +-
 src/export/api/helper.ts                         |   3 +-
 src/scale/Interval.ts                            | 279 ++++++++---------------
 src/scale/Log.ts                                 | 165 +++++---------
 src/scale/Ordinal.ts                             |   8 +-
 src/scale/Scale.ts                               | 104 ++++-----
 src/scale/Time.ts                                | 122 +++++-----
 src/scale/helper.ts                              |  11 +-
 src/scale/minorTicks.ts                          |  81 +++++++
 src/util/jitter.ts                               |   2 +-
 test/axis-align-ticks.html                       |   4 +-
 test/runTest/actions/axis-align-ticks.json       |   2 +-
 25 files changed, 703 insertions(+), 523 deletions(-)

diff --git a/src/component/dataZoom/AxisProxy.ts 
b/src/component/dataZoom/AxisProxy.ts
index cabe1db8c..4ba157c7e 100644
--- a/src/component/dataZoom/AxisProxy.ts
+++ b/src/component/dataZoom/AxisProxy.ts
@@ -458,11 +458,7 @@ class AxisProxy {
 
         const axisModel = this.getAxisModel();
 
-        const window = this._window;
-        if (!window) {
-            return;
-        }
-        const {percent, value} = window;
+        const {percent, value} = this._window;
 
         // For value axis, if min/max/scale are not set, we just use the 
extent obtained
         // by series data, which may be a little different from the extent 
calculated by
diff --git a/src/component/timeline/SliderTimelineView.ts 
b/src/component/timeline/SliderTimelineView.ts
index 0a547abd7..23fea2bb7 100644
--- a/src/component/timeline/SliderTimelineView.ts
+++ b/src/component/timeline/SliderTimelineView.ts
@@ -23,7 +23,7 @@ import * as graphic from '../../util/graphic';
 import { createTextStyle } from '../../label/labelStyle';
 import * as layout from '../../util/layout';
 import TimelineView from './TimelineView';
-import TimelineAxis from './TimelineAxis';
+import TimelineAxis, { TimelineAxisType } from './TimelineAxis';
 import {createSymbol, normalizeSymbolOffset, normalizeSymbolSize} from 
'../../util/symbol';
 import * as numberUtil from '../../util/number';
 import GlobalModel from '../../model/Global';
@@ -35,10 +35,6 @@ import TimelineModel, { TimelineDataItemOption, 
TimelineCheckpointStyle } from '
 import { TimelineChangePayload, TimelinePlayChangePayload } from 
'./timelineAction';
 import Model from '../../model/Model';
 import { PathProps, PathStyleProps } from 'zrender/src/graphic/Path';
-import Scale from '../../scale/Scale';
-import OrdinalScale from '../../scale/Ordinal';
-import TimeScale from '../../scale/Time';
-import IntervalScale from '../../scale/Interval';
 import { VectorArray } from 'zrender/src/core/vector';
 import { parsePercent } from 'zrender/src/contain/text';
 import { makeInner } from '../../util/model';
@@ -46,6 +42,9 @@ import { getECData } from '../../util/innerStore';
 import { enableHoverEmphasis } from '../../util/states';
 import { createTooltipMarkup } from '../tooltip/tooltipMarkup';
 import Displayable from 'zrender/src/graphic/Displayable';
+import { createScaleByModel } from '../../coord/axisHelper';
+import { OptionAxisType } from '../../coord/axisCommonTypes';
+import { scaleCalcNiceReal } from '../../coord/axisNiceTicks';
 
 const PI = Math.PI;
 
@@ -338,9 +337,12 @@ class SliderTimelineView extends TimelineView {
 
     private _createAxis(layoutInfo: LayoutInfo, timelineModel: 
SliderTimelineModel) {
         const data = timelineModel.getData();
-        const axisType = timelineModel.get('axisType');
+        let axisType = timelineModel.get('axisType') || 
timelineModel.get('type') as TimelineAxisType;
+        if (axisType !== 'category' && axisType !== 'time') {
+            axisType = 'value';
+        }
 
-        const scale = createScaleByModel(timelineModel, axisType);
+        const scale = createScaleByModel(timelineModel, axisType as 
OptionAxisType);
 
         // Customize scale. The `tickValue` is `dataIndex`.
         scale.getTicks = function () {
@@ -351,7 +353,7 @@ class SliderTimelineView extends TimelineView {
 
         const dataExtent = data.getDataExtent('value');
         scale.setExtent(dataExtent[0], dataExtent[1]);
-        scale.calcNiceTicks();
+        scaleCalcNiceReal(scale, {fixMinMax: [true, true]});
 
         const axis = new TimelineAxis('value', scale, layoutInfo.axisExtent as 
[number, number], axisType);
         axis.model = timelineModel;
@@ -730,28 +732,6 @@ class SliderTimelineView extends TimelineView {
     }
 }
 
-function createScaleByModel(model: SliderTimelineModel, axisType?: string): 
Scale {
-    axisType = axisType || model.get('type');
-    if (axisType) {
-        switch (axisType) {
-            // Buildin scale
-            case 'category':
-                return new OrdinalScale({
-                    ordinalMeta: model.getCategories(),
-                    extent: [Infinity, -Infinity]
-                });
-            case 'time':
-                return new TimeScale({
-                    locale: model.ecModel.getLocaleModel(),
-                    useUTC: model.ecModel.get('useUTC')
-                });
-            default:
-                // default to be value
-                return new IntervalScale();
-        }
-    }
-}
-
 
 function getViewRect(model: SliderTimelineModel, api: ExtensionAPI) {
     return layout.getLayoutRect(
diff --git a/src/component/timeline/TimelineAxis.ts 
b/src/component/timeline/TimelineAxis.ts
index e0f21156a..59e9284c5 100644
--- a/src/component/timeline/TimelineAxis.ts
+++ b/src/component/timeline/TimelineAxis.ts
@@ -23,12 +23,14 @@ import TimelineModel from './TimelineModel';
 import { LabelOption } from '../../util/types';
 import Model from '../../model/Model';
 
+export type TimelineAxisType = 'category' | 'time' | 'value';
+
 /**
  * Extend axis 2d
  */
 class TimelineAxis extends Axis {
 
-    type: 'category' | 'time' | 'value';
+    type: TimelineAxisType;
 
     // @ts-ignore
     model: TimelineModel;
@@ -37,7 +39,7 @@ class TimelineAxis extends Axis {
         dim: string,
         scale: Scale,
         coordExtent: [number, number],
-        axisType: 'category' | 'time' | 'value'
+        axisType: TimelineAxisType
     ) {
         super(dim, scale, coordExtent);
         this.type = axisType || 'value';
diff --git a/src/component/timeline/TimelineModel.ts 
b/src/component/timeline/TimelineModel.ts
index d366589d9..ad6de3166 100644
--- a/src/component/timeline/TimelineModel.ts
+++ b/src/component/timeline/TimelineModel.ts
@@ -292,10 +292,6 @@ class TimelineModel extends ComponentModel<TimelineOption> 
{
         return this._data;
     }
 
-    /**
-     * @public
-     * @return {Array.<string>} categoreis
-     */
     getCategories() {
         if (this.get('axisType') === 'category') {
             return this._names.slice();
diff --git a/src/coord/axisAlignTicks.ts b/src/coord/axisAlignTicks.ts
index f392c74eb..fc871006a 100644
--- a/src/coord/axisAlignTicks.ts
+++ b/src/coord/axisAlignTicks.ts
@@ -113,7 +113,7 @@ export function alignScaleTicks(
         // 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 nice segments are made "nice".
-        const alignToInterval = alignToScaleLinear.getInterval();
+        const alignToInterval = alignToScaleLinear.getConfig().interval;
         t0 = (
             1 - (alignToTicks[0].value - alignToExpNiceTicks[0].value) / 
alignToInterval
         ) % 1;
@@ -282,23 +282,28 @@ export function alignScaleTicks(
                 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.
+                    // Consider cases that negative tick do not make sense (or 
vice versa), users can simply
+                    // specify `xxxAxis.min/max: 0` to avoid negative. But we 
still automatically handle it
+                    // for some common cases whenever possible:
+                    //  - When ec option is `xxxAxis.scale: false` (the 
default), it is usually unexpected if
+                    //    negative (or positive) ticks are introduced.
+                    //  - In LogScale, series data are usually either all > 1 
or all < 1, rather than both,
+                    //    that is, logarithm result is typically either all 
positive or all negative.
                     let moreCountPair: number[];
-                    const needCrossZero = targetExtentInfo.needCrossZero;
-                    if (needCrossZero && targetExtent[0] === 0) {
+                    const mayEnhanceZero = targetExtentInfo.needCrossZero || 
isTargetLogScale;
+                    // `bounds < 0` or `bounds > 0` may require more complex 
handling, so we only auto handle
+                    // `bounds === 0`.
+                    if (mayEnhanceZero && targetExtent[0] === 0) {
                         // 0 has been included in extent and all positive.
                         moreCountPair = [0, moreCount];
                     }
-                    else if (needCrossZero && targetExtent[1] === 0) {
+                    else if (mayEnhanceZero && 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`.
+                        // Try to center ticks in axis space whenever 
possible, 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]
@@ -325,9 +330,9 @@ export function alignScaleTicks(
         ]
         : [];
 
-    // NOTE: Must in setExtent -> setInterval order.
+    // NOTE: Must in setExtent -> setConfigs order.
     targetScaleLinear.setExtent(min, max);
-    targetScaleLinear.setInterval({
+    targetScaleLinear.setConfig({
         // Even in LogScale, `interval` should not be in log space.
         interval,
         intervalCount,
diff --git a/src/coord/axisCommonTypes.ts b/src/coord/axisCommonTypes.ts
index 944ad5949..bd4df4391 100644
--- a/src/coord/axisCommonTypes.ts
+++ b/src/coord/axisCommonTypes.ts
@@ -34,6 +34,9 @@ import type { PrimaryTimeUnit } from '../util/time';
 export const AXIS_TYPES = {value: 1, category: 1, time: 1, log: 1} as const;
 export type OptionAxisType = keyof typeof AXIS_TYPES;
 
+// `scale/Ordinal` | `scale/Interval` | `scale/Log` | `scale/Time`
+export type AxisScaleType = 'ordinal' | 'interval' | 'log' | 'time';
+
 export interface AxisBaseOptionCommon extends ComponentOption,
     AnimationOptionMixin {
     type?: OptionAxisType;
diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts
index 4fa38b5ae..b0771dbe5 100644
--- a/src/coord/axisHelper.ts
+++ b/src/coord/axisHelper.ts
@@ -41,6 +41,7 @@ import {
     AxisLabelCategoryFormatter,
     AxisLabelValueFormatter,
     AxisLabelFormatterExtraParams,
+    OptionAxisType,
 } from './axisCommonTypes';
 import CartesianAxisModel from './cartesian/AxisModel';
 import SeriesData from '../data/SeriesData';
@@ -50,7 +51,8 @@ import { ensureScaleRawExtentInfo, ScaleRawExtentResult } 
from './scaleRawExtent
 import { parseTimeAxisLabelFormatter } from '../util/time';
 import { getScaleBreakHelper } from '../scale/break';
 import { error } from '../util/log';
-import { isIntervalScale, isTimeScale } from '../scale/helper';
+import { isTimeScale } from '../scale/helper';
+import { AxisModelExtendedInCreator } from './axisModelCreator';
 
 
 type BarWidthAndOffset = ReturnType<typeof makeColumnLayout>;
@@ -157,43 +159,22 @@ function adjustScaleForOverflow(
     return {min: min, max: max};
 }
 
-export function niceScaleExtent(
-    scale: Scale,
-    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 = adoptScaleExtentOptionAndPrepare(scale, model, 
dataExtent);
-
-    const isInterval = isIntervalScale(scale);
-    const isIntervalOrTime = isInterval || isTimeScale(scale);
-
-    scale.setBreaksFromOption(retrieveAxisBreaksOption(model));
-    scale.setExtent(extentInfo.min, extentInfo.max);
-    scale.calcNiceExtent({
-        splitNumber: model.get('splitNumber'),
-        fixMinMax: [extentInfo.minFixed, extentInfo.maxFixed],
-        minInterval: isIntervalOrTime ? model.get('minInterval') : null,
-        maxInterval: isIntervalOrTime ? model.get('maxInterval') : null
-    });
-
-    // If some one specified the min, max. And the default calculated interval
-    // 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.
-    // 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});
-    }
-}
-
-export function createScaleByModel(model: AxisBaseModel): Scale {
-    const axisType = model.get('type');
-    switch (axisType) {
+export function createScaleByModel(
+    model:
+        Model<
+            // Expect `Pick<AxisBaseOptionCommon, 'type'>`,
+            // but be lenient for user's invalid input.
+            {type?: string}
+            & Pick<LogAxisBaseOption, 'logBase'>
+        >
+        & Partial<Pick<
+            AxisModelExtendedInCreator,
+            'getOrdinalMeta' | 'getCategories'
+        >>,
+    axisType?: OptionAxisType
+): Scale {
+    const type = axisType || model.get('type');
+    switch (type) {
         case 'category':
             return new OrdinalScale({
                 ordinalMeta: model.getOrdinalMeta
@@ -208,10 +189,10 @@ export function createScaleByModel(model: AxisBaseModel): 
Scale {
             });
         case 'log':
             // See also #3749
-            return new LogScale((model as 
AxisBaseModel<LogAxisBaseOption>).get('logBase'));
+            return new LogScale(model.get('logBase'));
         default:
             // case 'value'/'interval', or others.
-            return new (Scale.getClass(axisType) || IntervalScale)();
+            return new (Scale.getClass(type) || IntervalScale)();
     }
 }
 
diff --git a/src/coord/axisNiceTicks.ts b/src/coord/axisNiceTicks.ts
new file mode 100644
index 000000000..ce1f8bfe5
--- /dev/null
+++ b/src/coord/axisNiceTicks.ts
@@ -0,0 +1,272 @@
+/*
+* 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.
+*/
+
+import { assert, noop } from 'zrender/src/core/util';
+import {
+    ensureValidSplitNumber, getIntervalPrecision,
+    intervalScaleEnsureValidExtent,
+    isIntervalScale, isTimeScale
+} from '../scale/helper';
+import IntervalScale from '../scale/Interval';
+import { getPrecision, mathCeil, mathFloor, mathMax, nice, quantity, round } 
from '../util/number';
+import type { AxisBaseModel } from './AxisBaseModel';
+import type { AxisScaleType, LogAxisBaseOption } from './axisCommonTypes';
+import { adoptScaleExtentOptionAndPrepare, retrieveAxisBreaksOption } from 
'./axisHelper';
+import { timeScaleCalcNice } from '../scale/Time';
+import type LogScale from '../scale/Log';
+import { NullUndefined } from '../util/types';
+import Scale from '../scale/Scale';
+
+
+// ------ START: LinearIntervalScaleStub Nice ------
+
+type LinearIntervalScaleStubCalcNiceTicks = (
+    scale: IntervalScale,
+    opt: Pick<ScaleCalcNiceMethodOpt, 'splitNumber' | 'minInterval' | 
'maxInterval'>
+) => {
+    intervalPrecision: number;
+    interval: number;
+    niceExtent: number[];
+};
+
+type LinearIntervalScaleStubCalcExtentPrecision = (
+    oldExtent: number[],
+    newExtent: number[],
+    opt: Pick<ScaleCalcNiceMethodOpt, 'fixMinMax'>
+) => (
+    (number | NullUndefined)[] | NullUndefined
+);
+
+function linearIntervalScaleStubCalcNice(
+    linearIntervalScaleStub: IntervalScale,
+    opt: ScaleCalcNiceMethodOpt,
+    opt2: {
+        calcNiceTicks: LinearIntervalScaleStubCalcNiceTicks;
+        calcExtentPrecision: LinearIntervalScaleStubCalcExtentPrecision;
+    }
+): void {
+    // [CAVEAT]: If updating this impl, need to sync it to `axisAlignTicks.ts`.
+
+    const fixMinMax = opt.fixMinMax || [];
+    const oldExtent = linearIntervalScaleStub.getExtent();
+
+    let extent = intervalScaleEnsureValidExtent(oldExtent.slice(), fixMinMax);
+
+    linearIntervalScaleStub.setExtent(extent[0], extent[1]);
+    extent = linearIntervalScaleStub.getExtent();
+
+    const {
+        interval,
+        intervalPrecision,
+        niceExtent,
+    } = opt2.calcNiceTicks(linearIntervalScaleStub, opt);
+
+    if (!fixMinMax[0]) {
+        extent[0] = round(mathFloor(extent[0] / interval) * interval, 
intervalPrecision);
+    }
+    if (!fixMinMax[1]) {
+        extent[1] = round(mathCeil(extent[1] / interval) * interval, 
intervalPrecision);
+    }
+
+    const extentPrecision = opt2.calcExtentPrecision(oldExtent, extent, opt);
+
+    linearIntervalScaleStub.setExtent(extent[0], extent[1]);
+    linearIntervalScaleStub.setConfig({
+        interval,
+        intervalPrecision,
+        niceExtent,
+        extentPrecision
+    });
+}
+
+// ------ END: LinearIntervalScaleStub Nice ------
+
+
+// ------ START: IntervalScale Nice ------
+
+const intervalScaleCalcNiceTicks: LinearIntervalScaleStubCalcNiceTicks = 
function (scale, opt) {
+    const splitNumber = ensureValidSplitNumber(opt.splitNumber, 5);
+    const extent = scale.getExtent();
+    const span = scale.getBreaksElapsedExtentSpan();
+
+    if (__DEV__) {
+        assert(isFinite(span) && span > 0); // It should be ensured by 
`intervalScaleEnsureValidExtent`.
+    }
+
+    const minInterval = opt.minInterval;
+    const maxInterval = opt.maxInterval;
+
+    let interval = nice(span / splitNumber, true);
+    if (minInterval != null && interval < minInterval) {
+        interval = minInterval;
+    }
+    if (maxInterval != null && interval > maxInterval) {
+        interval = maxInterval;
+    }
+    const intervalPrecision = getIntervalPrecision(interval);
+    // Nice extent inside original extent
+    const niceExtent = [
+        round(mathCeil(extent[0] / interval) * interval, intervalPrecision),
+        round(mathFloor(extent[1] / interval) * interval, intervalPrecision)
+    ];
+
+    return {interval, intervalPrecision, niceExtent};
+};
+
+const intervalScaleCalcNice: ScaleCalcNiceMethod = function (
+    scale: IntervalScale, opt
+) {
+    linearIntervalScaleStubCalcNice(scale, opt, {
+        calcNiceTicks: intervalScaleCalcNiceTicks,
+        calcExtentPrecision: noop as unknown as 
LinearIntervalScaleStubCalcExtentPrecision,
+    });
+};
+
+// ------ END: IntervalScale Nice ------
+
+
+// ------ START: LogScale Nice ------
+
+const logScaleCalcNiceTicks: LinearIntervalScaleStubCalcNiceTicks = function (
+    linearStub: IntervalScale, opt
+) {
+    // [CAVEAT]: If updating this impl, need to sync it to `axisAlignTicks.ts`.
+
+    const splitNumber = ensureValidSplitNumber(opt.splitNumber, 10);
+    const extent = linearStub.getExtent();
+    const span = linearStub.getBreaksElapsedExtentSpan();
+
+    if (__DEV__) {
+        assert(isFinite(span) && span > 0); // It should be ensured by 
`intervalScaleEnsureValidExtent`.
+    }
+
+    // Interval should be integer
+    let interval = mathMax(quantity(span), 1);
+
+    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;
+    }
+
+    const intervalPrecision = getIntervalPrecision(interval);
+    const niceExtent = [
+        round(mathCeil(extent[0] / interval) * interval, intervalPrecision),
+        round(mathFloor(extent[1] / interval) * interval, intervalPrecision)
+    ] as [number, number];
+
+    return {intervalPrecision, interval, niceExtent};
+};
+
+const logScaleCalcExtentPrecision: LinearIntervalScaleStubCalcExtentPrecision 
= function (
+    oldExtent, newExtent, opt
+) {
+    return [
+        (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
+    ];
+};
+
+const logScaleCalcNice: ScaleCalcNiceMethod = function (scale: LogScale, opt): 
void {
+    // NOTE: Calculate nice only on linearStub of LogScale.
+    linearIntervalScaleStubCalcNice(scale.linearStub, opt, {
+        calcNiceTicks: logScaleCalcNiceTicks,
+        calcExtentPrecision: logScaleCalcExtentPrecision,
+    });
+};
+
+// ------ END: LogScale Nice ------
+
+
+// ------ START: scaleCalcNice Entry ------
+
+export type ScaleCalcNiceMethod = (
+    scale: ScaleForCalcNice,
+    opt: ScaleCalcNiceMethodOpt
+) => void;
+
+type ScaleForCalcNice = Pick<
+    Scale,
+    'type' | 'setExtent' | 'getExtent' | 'getBreaksElapsedExtentSpan'
+>;
+
+type ScaleCalcNiceMethodOpt = {
+    splitNumber?: number;
+    minInterval?: number;
+    maxInterval?: number;
+    // `[fixMin, fixMax]`. If `true`, the original `extent[0]`/`extent[1]`
+    // will not be modified, except for an invalid extent.
+    fixMinMax?: boolean[];
+};
+
+export function scaleCalcNice(
+    scale: Scale,
+    // scale: Scale,
+    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 = adoptScaleExtentOptionAndPrepare(scale, model, 
dataExtent);
+
+    const isInterval = isIntervalScale(scale);
+    const isIntervalOrTime = isInterval || isTimeScale(scale);
+
+    scale.setBreaksFromOption(retrieveAxisBreaksOption(model));
+    scale.setExtent(extentInfo.min, extentInfo.max);
+
+    scaleCalcNiceReal(scale, {
+        splitNumber: model.get('splitNumber'),
+        fixMinMax: [extentInfo.minFixed, extentInfo.maxFixed],
+        minInterval: isIntervalOrTime ? model.get('minInterval') : null,
+        maxInterval: isIntervalOrTime ? model.get('maxInterval') : null
+    });
+
+    // If some one specified the min, max. And the default calculated interval
+    // 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.
+    // 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).setConfig) {
+        (scale as IntervalScale).setConfig({interval});
+    }
+}
+
+export function scaleCalcNiceReal(
+    scale: ScaleForCalcNice,
+    opt: ScaleCalcNiceMethodOpt
+): void {
+    scaleCalcNiceMethods[scale.type](scale, opt);
+}
+
+const scaleCalcNiceMethods: Record<AxisScaleType, ScaleCalcNiceMethod> = {
+    interval: intervalScaleCalcNice,
+    log: logScaleCalcNice,
+    time: timeScaleCalcNice,
+    ordinal: noop,
+};
+
+// ------ END: scaleCalcNice Entry ------
diff --git a/src/coord/cartesian/Grid.ts b/src/coord/cartesian/Grid.ts
index 2b695c3df..3d7b067c5 100644
--- a/src/coord/cartesian/Grid.ts
+++ b/src/coord/cartesian/Grid.ts
@@ -28,7 +28,6 @@ import {BoxLayoutReferenceResult, createBoxLayoutReference, 
getLayoutRect, Layou
 import {
     createScaleByModel,
     ifAxisCrossZero,
-    niceScaleExtent,
     getDataDimensionsOnAxis,
     isNameLocationCenter,
     shouldAxisShow,
@@ -71,6 +70,7 @@ import { error, log } from '../../util/log';
 import { AxisTickLabelComputingKind } from '../axisTickLabelBuilder';
 import { injectCoordSysByOption } from '../../core/CoordinateSystem';
 import { mathMax, parsePositionSizeOption } from '../../util/number';
+import { scaleCalcNice } from '../axisNiceTicks';
 
 type Cartesian2DDimensionName = 'x' | 'y';
 
@@ -135,12 +135,12 @@ class Grid implements CoordinateSystemMaster {
                     axisNeedsAlign.push(axis);
                 }
                 else {
-                    niceScaleExtent(axis.scale, axis.model, 
axis.scale.getExtent());
+                    scaleCalcNice(axis.scale, axis.model, 
axis.scale.getExtent());
                 }
             };
             each(axisNeedsAlign, axis => {
                 if (incapableOfAlignNeedFallback(axis, axis.alignTo as 
Axis2D)) {
-                    niceScaleExtent(axis.scale, axis.model, 
axis.scale.getExtent());
+                    scaleCalcNice(axis.scale, axis.model, 
axis.scale.getExtent());
                 }
                 else {
                     alignScaleTicks(
diff --git a/src/coord/cartesian/defaultAxisExtentFromData.ts 
b/src/coord/cartesian/defaultAxisExtentFromData.ts
index 76e0cdac9..a2c78eeeb 100644
--- a/src/coord/cartesian/defaultAxisExtentFromData.ts
+++ b/src/coord/cartesian/defaultAxisExtentFromData.ts
@@ -204,7 +204,7 @@ function calculateFilteredExtent(
         if (singleCondDim && singleTarDim) {
             for (let dataIdx = 0; dataIdx < dataLen; dataIdx++) {
                 const condVal = data.get(singleCondDim, dataIdx) as number;
-                if (condAxis.scale.isInExtentRange(condVal)) {
+                if (condAxis.scale.isInExtent(condVal)) {
                     unionExtent(tarDimExtents[0], data.get(singleTarDim, 
dataIdx) as number);
                 }
             }
@@ -213,7 +213,7 @@ function calculateFilteredExtent(
             for (let dataIdx = 0; dataIdx < dataLen; dataIdx++) {
                 for (let j = 0; j < condDimsLen; j++) {
                     const condVal = data.get(condDims[j], dataIdx) as number;
-                    if (condAxis.scale.isInExtentRange(condVal)) {
+                    if (condAxis.scale.isInExtent(condVal)) {
                         for (let k = 0; k < tarDimsLen; k++) {
                             unionExtent(tarDimExtents[k], data.get(tarDims[k], 
dataIdx) as number);
                         }
diff --git a/src/coord/parallel/Parallel.ts b/src/coord/parallel/Parallel.ts
index 38ad006ca..48a7a3aa8 100644
--- a/src/coord/parallel/Parallel.ts
+++ b/src/coord/parallel/Parallel.ts
@@ -40,6 +40,7 @@ import ParallelAxisModel, { ParallelActiveState } from 
'./AxisModel';
 import SeriesData from '../../data/SeriesData';
 import { AxisBaseModel } from '../AxisBaseModel';
 import { CategoryAxisBaseOption } from '../axisCommonTypes';
+import { scaleCalcNice } from '../axisNiceTicks';
 
 
 interface ParallelCoordinateSystemLayoutInfo {
@@ -184,7 +185,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, 
axis.scale.getExtent());
+            scaleCalcNice(axis.scale, axis.model, axis.scale.getExtent());
         }, this);
     }
 
diff --git a/src/coord/polar/polarCreator.ts b/src/coord/polar/polarCreator.ts
index 8a7515896..5a5a7e53a 100644
--- a/src/coord/polar/polarCreator.ts
+++ b/src/coord/polar/polarCreator.ts
@@ -24,7 +24,6 @@ import Polar, { polarDimensions } from './Polar';
 import {parsePercent} from '../../util/number';
 import {
     createScaleByModel,
-    niceScaleExtent,
     getDataDimensionsOnAxis
 } from '../../coord/axisHelper';
 
@@ -41,6 +40,7 @@ import { SINGLE_REFERRING } from '../../util/model';
 import { AxisBaseModel } from '../AxisBaseModel';
 import { CategoryAxisBaseOption } from '../axisCommonTypes';
 import { createBoxLayoutReference } from '../../util/layout';
+import { scaleCalcNice } from '../axisNiceTicks';
 
 /**
  * Resize method bound to the polar
@@ -99,8 +99,8 @@ function updatePolarScale(this: Polar, ecModel: GlobalModel, 
api: ExtensionAPI)
         }
     });
 
-    niceScaleExtent(angleScale, angleAxis.model, angleScale.getExtent());
-    niceScaleExtent(radiusScale, radiusAxis.model, radiusScale.getExtent());
+    scaleCalcNice(angleScale, angleAxis.model, angleScale.getExtent());
+    scaleCalcNice(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 02d046585..46360af7c 100644
--- a/src/coord/radar/Radar.ts
+++ b/src/coord/radar/Radar.ts
@@ -174,7 +174,7 @@ class Radar implements CoordinateSystem, 
CoordinateSystemMaster {
         const splitNumber = radarModel.get('splitNumber');
         const dummyScale = new IntervalScale();
         dummyScale.setExtent(0, splitNumber);
-        dummyScale.setInterval({interval: 1});
+        dummyScale.setConfig({interval: 1});
         // Force all the axis fixing the maxSplitNumber.
         each(indicatorAxes, function (indicatorAxis, idx) {
             alignScaleTicks(
diff --git a/src/coord/single/Single.ts b/src/coord/single/Single.ts
index 5610fe7d2..eb1e2f50b 100644
--- a/src/coord/single/Single.ts
+++ b/src/coord/single/Single.ts
@@ -34,6 +34,7 @@ import { ParsedModelFinder, ParsedModelFinderKnown } from 
'../../util/model';
 import { ScaleDataValue } from '../../util/types';
 import { AxisBaseModel } from '../AxisBaseModel';
 import { CategoryAxisBaseOption } from '../axisCommonTypes';
+import { scaleCalcNice } from '../axisNiceTicks';
 
 export const singleDimensions = ['single'];
 /**
@@ -104,7 +105,7 @@ class Single implements CoordinateSystem, 
CoordinateSystemMaster {
                 each(data.mapDimensionsAll(this.dimension), function (dim) {
                     scale.unionExtentFromData(data, dim);
                 });
-                axisHelper.niceScaleExtent(scale, axis.model, 
scale.getExtent());
+                scaleCalcNice(scale, axis.model, scale.getExtent());
             }
         }, this);
     }
diff --git a/src/export/api/helper.ts b/src/export/api/helper.ts
index 7ef631e78..803c0f674 100644
--- a/src/export/api/helper.ts
+++ b/src/export/api/helper.ts
@@ -38,6 +38,7 @@ import { AxisBaseModel } from '../../coord/AxisBaseModel';
 import { getECData } from '../../util/innerStore';
 import { createTextStyle as innerCreateTextStyle } from 
'../../label/labelStyle';
 import { DisplayState, TextCommonOption } from '../../util/types';
+import { scaleCalcNice } from '../../coord/axisNiceTicks';
 
 /**
  * Create a multi dimension List structure from seriesModel.
@@ -96,7 +97,7 @@ 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, 
scale.getExtent());
+    scaleCalcNice(scale, axisModel as AxisBaseModel, scale.getExtent());
     return scale;
 }
 
diff --git a/src/scale/Interval.ts b/src/scale/Interval.ts
index a2b7cf9a3..78babe18b 100644
--- a/src/scale/Interval.ts
+++ b/src/scale/Interval.ts
@@ -18,23 +18,34 @@
 */
 
 
-import {round, mathRound, mathMin, getPrecision, mathCeil, mathFloor} from 
'../util/number';
+import {round, mathRound, mathMin, getPrecision} from '../util/number';
 import {addCommas} from '../util/format';
 import Scale, { ScaleGetTicksOpt, ScaleSettingDefault } from './Scale';
 import * as helper from './helper';
-import {ScaleTick, ParsedAxisBreakList, ScaleDataValue, NullUndefined} from 
'../util/types';
+import {ScaleTick, ScaleDataValue, NullUndefined} from '../util/types';
 import { getScaleBreakHelper } from './break';
-import { assert, retrieve2 } from 'zrender/src/core/util';
+import { assert, clone } from 'zrender/src/core/util';
+import { getMinorTicks } from './minorTicks';
 
-class IntervalScale<SETTING extends ScaleSettingDefault = ScaleSettingDefault> 
extends Scale<SETTING> {
 
-    static type = 'interval';
-    type = 'interval';
+type IntervalScaleConfig = {
+    interval: IntervalScaleConfigParsed['interval'];
+    intervalPrecision?: IntervalScaleConfigParsed['intervalPrecision'] | 
NullUndefined;
+    extentPrecision?: IntervalScaleConfigParsed['extentPrecision'] | 
NullUndefined;
+    intervalCount?: IntervalScaleConfigParsed['intervalCount'] | NullUndefined;
+    niceExtent?: IntervalScaleConfigParsed['niceExtent'] | NullUndefined;
+};
 
-    // Step is calculated in adjustExtent.
-    protected _interval: number = 0;
-    protected _intervalPrecision: number = 2;
-    protected _extentPrecision: number[] = [];
+type IntervalScaleConfigParsed = {
+    /**
+     * Step of ticks.
+     */
+    interval: number;
+    intervalPrecision: number;
+    /**
+     * Precisions of `_extent[0]` and `_extent[1]`.
+     */
+    extentPrecision: (number | NullUndefined)[];
     /**
      * `_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
@@ -46,7 +57,7 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
      *  and `0` means only one nice tick (e.g., `_extent: [5, 5.8], _interval: 
1`).
      * @see setInterval
      */
-    private _intervalCount: number | NullUndefined = undefined;
+    intervalCount: number | NullUndefined;
     /**
      * Should ensure:
      *  `_extent[0] <= _niceExtent[0] && _niceExtent[1] <= _extent[1]`
@@ -58,9 +69,29 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
      *  e.g., `_extent: [5, 5.8]` with interval `1` will get `_niceExtent: [5, 
5]`.
      * @see setInterval
      */
-    protected _niceExtent: [number, number];
+    niceExtent: number[] | NullUndefined;
+};
 
 
+class IntervalScale<SETTING extends ScaleSettingDefault = ScaleSettingDefault> 
extends Scale<SETTING> {
+
+    static type = 'interval';
+    type = 'interval' as const;
+
+    private _cfg: IntervalScaleConfigParsed;
+
+
+    constructor(setting?: SETTING) {
+        super(setting);
+        this._cfg = {
+            interval: 0,
+            intervalPrecision: 2,
+            extentPrecision: [],
+            intervalCount: undefined,
+            niceExtent: undefined,
+        };
+    }
+
     parse(val: ScaleDataValue): number {
         // `Scale#parse` (and its overrids) are typically applied at the axis 
values input
         // in echarts option. e.g., `axis.min/max`, `dataZoom.min/max`, etc.
@@ -100,63 +131,58 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
         return this._calculator.scale(val, this._extent);
     }
 
-    getInterval(): number {
-        return this._interval;
+    getConfig(): IntervalScaleConfigParsed {
+        return clone(this._cfg);
     }
 
     /**
      * @final override is DISALLOWED.
      */
-    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 {
+    setConfig(cfg: IntervalScaleConfig): void {
         const extent = this._extent;
 
         if (__DEV__) {
-            assert(interval != null);
-            if (intervalCount != null) {
+            assert(cfg.interval != null);
+            if (cfg.intervalCount != null) {
                 assert(
-                    intervalCount >= -1
-                    && intervalPrecision != null
+                    cfg.intervalCount >= -1
+                    && cfg.intervalPrecision != null
                     // Do not support intervalCount on axis break currently.
                     && !this.hasBreaks()
                 );
             }
-            if (niceExtent != null) {
-                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);
+            if (cfg.niceExtent != null) {
+                assert(isFinite(cfg.niceExtent[0]) && 
isFinite(cfg.niceExtent[1]));
+                assert(extent[0] <= cfg.niceExtent[0] && cfg.niceExtent[1] <= 
extent[1]);
+                assert(round(cfg.niceExtent[0] - cfg.niceExtent[1], 
getPrecision(cfg.interval)) <= cfg.interval);
             }
         }
 
-        // Set or clear
-        this._niceExtent =
-            niceExtent != null ? niceExtent.slice() as [number, number]
+        // Reset all.
+        this._cfg = cfg = clone(cfg) as IntervalScaleConfigParsed;
+        if (cfg.niceExtent == null) {
             // 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;
-        this._intervalCount = intervalCount;
-        this._intervalPrecision = retrieve2(intervalPrecision, 
helper.getIntervalPrecision(interval));
-        this._extentPrecision = extentPrecision || [];
+            cfg.niceExtent = extent.slice() as [number, number];
+        }
+        if (cfg.intervalPrecision == null) {
+            cfg.intervalPrecision = helper.getIntervalPrecision(cfg.interval);
+        }
+        cfg.extentPrecision = cfg.extentPrecision || [];
     }
 
     /**
      * In ascending order.
      *
-     * @override
+     * @final override is DISALLOWED.
      */
     getTicks(opt?: ScaleGetTicksOpt): ScaleTick[] {
         opt = opt || {};
-        const interval = this._interval;
+        const cfg = this._cfg;
+        const interval = cfg.interval;
         const extent = this._extent;
-        const niceTickExtent = this._niceExtent;
-        const intervalPrecision = this._intervalPrecision;
+        const niceExtent = cfg.niceExtent;
+        const intervalPrecision = cfg.intervalPrecision;
         const scaleBreakHelper = getScaleBreakHelper();
 
         const ticks = [] as ScaleTick[];
@@ -170,15 +196,19 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
             return ticks;
         }
 
+        if (__DEV__) {
+            assert(niceExtent != null);
+        }
+
         // [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 (extent[0] < niceExtent[0]) {
             if (opt.expandToNicedExtent) {
                 ticks.push({
-                    value: round(niceTickExtent[0] - interval, 
intervalPrecision)
+                    value: round(niceExtent[0] - interval, intervalPrecision)
                 });
             }
             else {
@@ -192,9 +222,9 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
             return mathRound((targetTick - tickVal) / interval);
         };
 
-        const intervalCount = this._intervalCount;
+        const intervalCount = cfg.intervalCount;
         for (
-            let tick = niceTickExtent[0], niceTickIdx = 0;
+            let tick = niceExtent[0], niceTickIdx = 0;
             ;
             niceTickIdx++
         ) {
@@ -203,7 +233,7 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
             // Consider case `_extent: [5, 5.8], _niceExtent: [5, 5], 
interval: 1`,
             //  `_intervalCount` makes sense iff `0`.
             if (intervalCount == null) {
-                if (tick > niceTickExtent[1] || !isFinite(tick) || 
!isFinite(niceTickExtent[1])) {
+                if (tick > niceExtent[1] || !isFinite(tick) || 
!isFinite(niceExtent[1])) {
                     break;
                 }
             }
@@ -212,10 +242,10 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
                     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]);
+                // `tick` may be less than or greater than `niceExtent[1]` 
slightly.
+                tick = mathMin(tick, niceExtent[1]);
                 if (niceTickIdx === intervalCount) {
-                    tick = niceTickExtent[1];
+                    tick = niceExtent[1];
                 }
             }
 
@@ -244,8 +274,8 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
         }
 
         // 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];
+        // than niceExtent[1] and niceExtent[1] === extent[1].
+        const lastNiceTick = ticks.length ? ticks[ticks.length - 1].value : 
niceExtent[1];
         if (extent[1] > lastNiceTick) {
             if (opt.expandToNicedExtent) {
                 ticks.push({
@@ -265,7 +295,7 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
                 ticks,
                 this._brkCtx!.breaks,
                 item => item.value,
-                this._interval,
+                cfg.interval,
                 this._extent
             );
         }
@@ -276,71 +306,24 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
         return ticks;
     }
 
+    /**
+     * @final override is DISALLOWED.
+     */
     getMinorTicks(splitNumber: number): number[][] {
-        const ticks = this.getTicks({
-            expandToNicedExtent: true,
-        });
-        // NOTE: In log-scale, do not support minor ticks when breaks exist.
-        //  because currently log-scale minor ticks is calculated based on raw 
values
-        //  rather than log-transformed value, due to an odd effect when 
breaks exist.
-        const minorTicks = [];
-        const extent = this.getExtent();
-
-        for (let i = 1; i < ticks.length; i++) {
-            const nextTick = ticks[i];
-            const prevTick = ticks[i - 1];
-
-            if (prevTick.break || nextTick.break) {
-                // Do not build minor ticks to the adjacent ticks to breaks 
ticks,
-                // since the interval might be irregular.
-                continue;
-            }
-
-            let count = 0;
-            const minorTicksGroup = [];
-            const interval = nextTick.value - prevTick.value;
-            const minorInterval = interval / splitNumber;
-            const minorIntervalPrecision = 
helper.getIntervalPrecision(minorInterval);
-
-            while (count < splitNumber - 1) {
-                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]) {
-                    minorTicksGroup.push(minorTick);
-                }
-                count++;
-            }
-
-            const scaleBreakHelper = getScaleBreakHelper();
-            scaleBreakHelper && scaleBreakHelper.pruneTicksByBreak(
-                'auto',
-                minorTicksGroup,
-                this._getNonTransBreaks(),
-                value => value,
-                this._interval,
-                extent
-            );
-            minorTicks.push(minorTicksGroup);
-        }
-
-        return minorTicks;
-    }
-
-    protected _getNonTransBreaks(): ParsedAxisBreakList {
-        return this._brkCtx ? this._brkCtx.breaks : [];
+        return getMinorTicks(
+            this,
+            splitNumber,
+            this.innerGetBreaks(),
+            this._cfg.interval
+        );
     }
 
     /**
-     * @param opt.precision If 'auto', use nice presision.
-     * @param opt.pad returns 1.50 but not 1.5 if precision is 2.
+     * @final override is DISALLOWED.
      */
     getLabel(
         data: ScaleTick,
-        opt?: {
-            precision?: 'auto' | number,
-            pad?: boolean
-        }
+        opt?: helper.IntervalScaleGetLabelOpt
     ): string {
         if (data == null) {
             return '';
@@ -353,7 +336,7 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
         }
         else if (precision === 'auto') {
             // Should be more precise then tick.
-            precision = this._intervalPrecision;
+            precision = this._cfg.intervalPrecision;
         }
 
         // (1) If `precision` is set, 12.005 should be display as '12.00500'.
@@ -363,78 +346,6 @@ class IntervalScale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> e
         return addCommas(dataNum);
     }
 
-    /**
-     * FIXME: refactor - disallow override, use composition instead.
-     *
-     * The override of `calcNiceTicks` should ensure these members are 
provided:
-     *  this._intervalPrecision
-     *  this._interval
-     *
-     * @param splitNumber By default `5`.
-     */
-    calcNiceTicks(splitNumber?: number, minInterval?: number, maxInterval?: 
number): void {
-        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) {
-            span = -span;
-            extent.reverse();
-            this._innerSetExtent(extent[0], extent[1]);
-            extent = this._extent.slice() as [number, number];
-        }
-
-        const result = helper.intervalScaleNiceTicks(
-            extent, span, splitNumber, minInterval, maxInterval
-        );
-
-        this._intervalPrecision = result.intervalPrecision;
-        this._interval = result.interval;
-        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.
-        fixMinMax?: boolean[], // [fixMin, fixMax]
-        minInterval?: number,
-        maxInterval?: number
-    }): void {
-        const fixMinMax = opt.fixMinMax || [];
-
-        let extent = helper.intervalScaleEnsureValidExtent(this._extent, 
fixMinMax);
-
-        this._innerSetExtent(extent[0], extent[1]);
-        extent = this._extent.slice() as [number, number];
-
-        this.calcNiceTicks(opt.splitNumber, opt.minInterval, opt.maxInterval);
-        const interval = this._interval;
-        const intervalPrecition = this._intervalPrecision;
-
-        if (!fixMinMax[0]) {
-            extent[0] = round(mathFloor(extent[0] / interval) * interval, 
intervalPrecition);
-        }
-        if (!fixMinMax[1]) {
-            extent[1] = round(mathCeil(extent[1] / interval) * interval, 
intervalPrecition);
-        }
-        this._innerSetExtent(extent[0], extent[1]);
-
-        // [CAVEAT]: If updating this impl, need to sync it to 
`axisAlignTicks.ts`.
-    }
-
 }
 
 Scale.registerClass(IntervalScale);
diff --git a/src/scale/Log.ts b/src/scale/Log.ts
index ea32d8005..5481003bf 100644
--- a/src/scale/Log.ts
+++ b/src/scale/Log.ts
@@ -20,80 +20,58 @@
 import * as zrUtil from 'zrender/src/core/util';
 import Scale, { ScaleGetTicksOpt, ScaleSettingDefault } from './Scale';
 import {
-    mathFloor, mathCeil, mathPow, mathLog,
-    round, quantity, getPrecision,
-    mathMax,
+    mathPow, mathLog,
 } from '../util/number';
 
 // Use some method of IntervalScale
 import IntervalScale from './Interval';
 import {
-    DimensionLoose, DimensionName, ParsedAxisBreakList, AxisBreakOption,
+    DimensionLoose, DimensionName, AxisBreakOption,
     ScaleTick,
-    NullUndefined
+    NullUndefined,
+    ScaleDataValue
 } from '../util/types';
 import {
-    ensureValidSplitNumber, getIntervalPrecision,
     logScalePowTickPair, logScalePowTick, logScaleLogTickPair,
-    getExtentPrecision
+    getExtentPrecision,
+    IntervalScaleGetLabelOpt,
+    logScaleLogTick,
 } from './helper';
 import SeriesData from '../data/SeriesData';
 import { getScaleBreakHelper } from './break';
+import { getMinorTicks } from './minorTicks';
 
 
-const LINEAR_STUB_METHODS = [
-    'getExtent', 'getTicks', 'getInterval',
-    'setExtent', 'setInterval',
-] 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; but may be still invalid, 
and not sync to the
- *     calculated ("nice") extent).
- */
-class LogScale extends IntervalScale {
+class LogScale extends Scale {
 
     static type = 'log';
-    readonly type = 'log';
+    readonly type = 'log' as const;
 
     readonly base: number;
 
-    private _originalScale = new IntervalScale();
-
-    linearStub: Pick<IntervalScale, (typeof LINEAR_STUB_METHODS)[number]>;
+    // `_originalScale` is used to save some original info (before logarithm
+    // applied, such as raw extent; but may be still invalid, and not sync
+    // to the calculated ("nice") extent).
+    private _originalScale: IntervalScale;
+    // `linearStub` provides linear tick arrangement (logarithm applied).
+    readonly linearStub: IntervalScale;
 
     constructor(logBase: number | NullUndefined, settings?: 
ScaleSettingDefault) {
-        super(settings);
+        super();
+        this._originalScale = new IntervalScale();
+        this.linearStub = new IntervalScale(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[] {
         const base = this.base;
         const originalScale = this._originalScale;
         const scaleBreakHelper = getScaleBreakHelper();
-        const extent = this._extent;
-        const extentPrecision = this._extentPrecision;
+        const linearStub = this.linearStub;
+        const extent = linearStub.getExtent();
+        const extentPrecision = linearStub.getConfig().extentPrecision;
 
-        return zrUtil.map(super.getTicks(opt || {}), function (tick) {
+        return zrUtil.map(linearStub.getTicks(opt || {}), function (tick) {
             const val = tick.value;
             let powVal = logScalePowTick(
                 val,
@@ -106,7 +84,7 @@ class LogScale extends IntervalScale {
                 const brkPowResult = scaleBreakHelper.getTicksPowBreak(
                     tick,
                     base,
-                    originalScale._innerGetBreaks(),
+                    originalScale.innerGetBreaks(),
                     extent,
                     extentPrecision
                 );
@@ -123,98 +101,65 @@ class LogScale extends IntervalScale {
         }, this);
     }
 
-    protected _getNonTransBreaks(): ParsedAxisBreakList {
-        return this._originalScale._innerGetBreaks();
+    getMinorTicks(splitNumber: number): number[][] {
+        return getMinorTicks(
+            this,
+            splitNumber,
+            this._originalScale.innerGetBreaks(),
+            // NOTE: minor ticks are in the log scale value to visually hint 
users "logarithm".
+            this.linearStub.getConfig().interval
+        );
+    }
+
+    getLabel(
+        data: ScaleTick,
+        opt?: IntervalScaleGetLabelOpt
+    ) {
+        return this.linearStub.getLabel(data, opt);
     }
 
     setExtent(start: number, end: number): void {
         // [CAVEAT]: If modifying this logic, must sync to `_initLinearStub`.
         this._originalScale.setExtent(start, end);
         const loggedExtent = logScaleLogTickPair([start, end], this.base);
-        super.setExtent(loggedExtent[0], loggedExtent[1]);
+        this.linearStub.setExtent(loggedExtent[0], loggedExtent[1]);
     }
 
     getExtent() {
-        const extent = super.getExtent();
+        const linearStub = this.linearStub;
         return logScalePowTickPair(
-            extent,
+            linearStub.getExtent(),
             this.base,
-            this._extentPrecision
+            linearStub.getConfig().extentPrecision
         );
     }
 
+    isInExtent(value: number): boolean {
+        return this.linearStub.isInExtent(logScaleLogTick(value, this.base));
+    }
+
     unionExtentFromData(data: SeriesData, dim: DimensionName | 
DimensionLoose): void {
         this._originalScale.unionExtentFromData(data, dim);
         const loggedOther = 
logScaleLogTickPair(data.getApproximateExtent(dim), this.base, true);
-        this._innerUnionExtent(loggedOther);
-    }
-
-    /**
-     * Update interval and extent of intervals for nice ticks
-     * @param splitNumber default 10 Given approx tick number
-     */
-    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;
-        }
-
-        // Interval should be integer
-        let interval = mathMax(quantity(span), 1);
-
-        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;
-        }
-
-        const intervalPrecision = getIntervalPrecision(interval);
-        const niceExtent = [
-            round(mathCeil(extent[0] / interval) * interval, 
intervalPrecision),
-            round(mathFloor(extent[1] / interval) * interval, 
intervalPrecision)
-        ] as [number, number];
-
-        this._interval = interval;
-        this._intervalPrecision = intervalPrecision;
-        this._niceExtent = niceExtent;
-
-        // [CAVEAT]: If updating this impl, need to sync it to 
`axisAlignTicks.ts`.
+        this.linearStub.innerUnionExtent(loggedOther);
     }
 
-    calcNiceExtent(opt: {
-        splitNumber: number,
-        fixMinMax?: boolean[],
-        minInterval?: number,
-        maxInterval?: number
-    }): void {
-        const oldExtent = this._extent.slice() as [number, number];
-        super.calcNiceExtent(opt);
-        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
-        ];
+    parse(val: ScaleDataValue): number {
+        return this.linearStub.parse(val);
     }
 
     contain(val: number): boolean {
         val = mathLog(val) / mathLog(this.base);
-        return super.contain(val);
+        return this.linearStub.contain(val);
     }
 
     normalize(val: number): number {
         val = mathLog(val) / mathLog(this.base);
-        return super.normalize(val);
+        return this.linearStub.normalize(val);
     }
 
     scale(val: number): number {
-        val = super.scale(val);
+        val = this.linearStub.scale(val);
         return mathPow(this.base, val);
     }
 
@@ -230,8 +175,8 @@ class LogScale extends IntervalScale {
             this.base,
             zrUtil.bind(this.parse, this)
         );
-        this._originalScale._innerSetBreak(parsedOriginal);
-        this._innerSetBreak(parsedLogged);
+        this._originalScale.innerSetBreak(parsedOriginal);
+        this.linearStub.innerSetBreak(parsedLogged);
     }
 
 }
diff --git a/src/scale/Ordinal.ts b/src/scale/Ordinal.ts
index cf41374ff..b7ab881a6 100644
--- a/src/scale/Ordinal.ts
+++ b/src/scale/Ordinal.ts
@@ -45,7 +45,7 @@ type OrdinalScaleSetting = {
 class OrdinalScale extends Scale<OrdinalScaleSetting> {
 
     static type = 'ordinal';
-    readonly type = 'ordinal';
+    readonly type = 'ordinal' as const;
 
     private _ordinalMeta: OrdinalMeta;
 
@@ -268,7 +268,7 @@ class OrdinalScale extends Scale<OrdinalScaleSetting> {
      * @override
      * If value is in extent range
      */
-    isInExtentRange(value: OrdinalNumber): boolean {
+    isInExtent(value: OrdinalNumber): boolean {
         value = this._getTickNumber(value);
         return this._extent[0] <= value && this._extent[1] >= value;
     }
@@ -277,10 +277,6 @@ class OrdinalScale extends Scale<OrdinalScaleSetting> {
         return this._ordinalMeta;
     }
 
-    calcNiceTicks() {}
-
-    calcNiceExtent() {}
-
 }
 
 Scale.registerClass(OrdinalScale);
diff --git a/src/scale/Scale.ts b/src/scale/Scale.ts
index f46266572..3dfe36195 100644
--- a/src/scale/Scale.ts
+++ b/src/scale/Scale.ts
@@ -36,9 +36,10 @@ import {
 import { ScaleRawExtentInfo } from '../coord/scaleRawExtentInfo';
 import { bind } from 'zrender/src/core/util';
 import { ScaleBreakContext, AxisBreakParsingResult, getScaleBreakHelper, 
ParamPruneByBreak } from './break';
+import { AxisScaleType } from '../coord/axisCommonTypes';
 
 export type ScaleGetTicksOpt = {
-    // Whether expand the ticks to niced extent.
+    // Whether expand the ticks to nice extent.
     expandToNicedExtent?: boolean;
     pruneByBreak?: ParamPruneByBreak;
     // - not specified or undefined(default): insert the breaks as items into 
the tick array.
@@ -54,16 +55,16 @@ export type ScaleSettingDefault = Dictionary<unknown>;
 
 abstract class Scale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault> {
 
-    type: string;
+    type: AxisScaleType;
 
     private _setting: SETTING;
 
-    // [CAVEAT]: Should update only by `_innerSetExtent`!
+    // [CAVEAT]: Should update only by `setExtent`!
     // Make sure that extent[0] always <= extent[1].
     protected _extent: [number, number];
 
-    // FIXME: Effectively, both logorithmic scale and break scale are numeric 
axis transformation
-    //  mechanisms. However, for historical reason, logorithmic scale is 
implemented as a subclass,
+    // FIXME: Effectively, both logarithmic scale and break scale are numeric 
axis transformation
+    //  mechanisms. However, for historical reason, logarithmic scale is 
implemented as a subclass,
     //  while break scale is implemented inside the base class `Scale`. If 
more transformations
     //  need to be introduced in futher, we should probably refactor them for 
better orthogonal
     //  composition. (e.g. use decorator-like patterns rather than the current 
class inheritance?)
@@ -86,6 +87,9 @@ abstract class Scale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault>
         }
     }
 
+    /**
+     * @final NEVER override!
+     */
     getSetting<KEY extends keyof SETTING>(name: KEY): SETTING[KEY] {
         return this._setting[name];
     }
@@ -115,22 +119,19 @@ abstract class Scale<SETTING extends ScaleSettingDefault 
= ScaleSettingDefault>
     abstract scale(val: number): number;
 
     /**
-     * [CAVEAT]: It should not be overridden!
+     * @final NEVER override!
      */
-    _innerUnionExtent(other: 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(
+        this.setExtent(
             other[0] < extent[0] ? other[0] : extent[0],
             other[1] > extent[1] ? other[1] : extent[1]
         );
     }
 
-    /**
-     * Set extent from data
-     */
     unionExtentFromData(data: SeriesData, dim: DimensionName | 
DimensionLoose): void {
-        this._innerUnionExtent(data.getApproximateExtent(dim));
+        this.innerUnionExtent(data.getApproximateExtent(dim));
     }
 
     /**
@@ -142,13 +143,6 @@ abstract class Scale<SETTING extends ScaleSettingDefault = 
ScaleSettingDefault>
     }
 
     setExtent(start: number, end: number): void {
-        this._innerSetExtent(start, end);
-    }
-
-    /**
-     * [CAVEAT]: It should not be overridden!
-     */
-    protected _innerSetExtent(start: number, end: number): void {
         const thisExtent = this._extent;
         if (!isNaN(start)) {
             thisExtent[0] = start;
@@ -167,53 +161,63 @@ abstract class Scale<SETTING extends ScaleSettingDefault 
= ScaleSettingDefault>
     ): void {
         const scaleBreakHelper = getScaleBreakHelper();
         if (scaleBreakHelper) {
-            this._innerSetBreak(
+            this.innerSetBreak(
                 scaleBreakHelper.parseAxisBreakOption(breakOptionList, 
bind(this.parse, this))
             );
         }
     }
 
     /**
-     * [CAVEAT]: It should not be overridden!
+     * @final NEVER override!
      */
-    _innerSetBreak(parsed: AxisBreakParsingResult) {
-        if (this._brkCtx) {
-            this._brkCtx.setBreaks(parsed);
-            this._calculator.updateMethods(this._brkCtx);
-            this._brkCtx.update(this._extent);
+    innerSetBreak(parsed: AxisBreakParsingResult) {
+        const brkCtx = this._brkCtx;
+        if (brkCtx) {
+            brkCtx.setBreaks(parsed);
+            this._calculator.updateMethods(brkCtx);
+            brkCtx.update(this._extent);
         }
     }
 
     /**
-     * [CAVEAT]: It should not be overridden!
+     * @final NEVER override!
      */
-    _innerGetBreaks(): ParsedAxisBreakList {
-        return this._brkCtx ? this._brkCtx.breaks : [];
+    innerGetBreaks(): ParsedAxisBreakList {
+        const brkCtx = this._brkCtx;
+        return brkCtx ? brkCtx.breaks : [];
     }
 
     /**
      * Do not expose the internal `_breaks` unless necessary.
+     *
+     * @final NEVER override!
      */
     hasBreaks(): boolean {
-        return this._brkCtx ? this._brkCtx.hasBreaks() : false;
-    }
-
-    protected _getExtentSpanWithBreaks() {
-        return (this._brkCtx && this._brkCtx.hasBreaks())
-            ? this._brkCtx.getExtentSpan()
-            : this._extent[1] - this._extent[0];
+        const brkCtx = this._brkCtx;
+        return brkCtx ? brkCtx.hasBreaks() : false;
     }
 
     /**
-     * If value is in extent range
+     * @final NEVER override!
      */
-    isInExtentRange(value: number): boolean {
-        return this._extent[0] <= value && this._extent[1] >= value;
+    getBreaksElapsedExtentSpan() {
+        const brkCtx = this._brkCtx;
+        const extent = this._extent;
+        return (brkCtx && brkCtx.hasBreaks())
+            ? brkCtx.getExtentSpan()
+            : extent[1] - extent[0];
+    }
+
+    isInExtent(value: number): boolean {
+        const extent = this._extent;
+        return extent[0] <= value && extent[1] >= value;
     }
 
     /**
      * When axis extent depends on data and no data exists,
      * axis ticks should not be drawn, which is named 'blank'.
+     *
+     * @final NEVER override!
      */
     isBlank(): boolean {
         return this._isBlank;
@@ -222,31 +226,13 @@ abstract class Scale<SETTING extends ScaleSettingDefault 
= ScaleSettingDefault>
     /**
      * When axis extent depends on data and no data exists,
      * axis ticks should not be drawn, which is named 'blank'.
+     *
+     * @final NEVER override!
      */
     setBlank(isBlank: boolean) {
         this._isBlank = isBlank;
     }
 
-    /**
-     * Update interval and extent of intervals for nice ticks
-     *
-     * @param splitNumber Approximated tick numbers. Optional.
-     *        The implementation of `niceTicks` should decide tick numbers
-     *        whether `splitNumber` is given.
-     * @param minInterval Optional.
-     * @param maxInterval Optional.
-     */
-    abstract calcNiceTicks(
-        // FIXME:TS make them in a "opt", the same with `niceExtent`?
-        splitNumber?: number,
-        minInterval?: number,
-        maxInterval?: number
-    ): void;
-
-    abstract calcNiceExtent(
-        opt?: {}
-    ): void;
-
     /**
      * @return label of the tick.
      */
diff --git a/src/scale/Time.ts b/src/scale/Time.ts
index 3549384d6..5785dc16b 100644
--- a/src/scale/Time.ts
+++ b/src/scale/Time.ts
@@ -75,7 +75,6 @@ import {
     roundTime
 } from '../util/time';
 import * as scaleHelper from './helper';
-import IntervalScale from './Interval';
 import Scale, { ScaleGetTicksOpt } from './Scale';
 import {TimeScaleTick, ScaleTick, AxisBreakOption, NullUndefined} from 
'../util/types';
 import {TimeAxisLabelFormatterParsed} from '../coord/axisCommonTypes';
@@ -84,6 +83,8 @@ import { LocaleOption } from '../core/locale';
 import Model from '../model/Model';
 import { each, filter, indexOf, isNumber, map } from 'zrender/src/core/util';
 import { ScaleBreakContext, getScaleBreakHelper } from './break';
+import type { ScaleCalcNiceMethod } from '../coord/axisNiceTicks';
+import { getMinorTicks } from './minorTicks';
 
 // FIXME 公用?
 const bisect = function (
@@ -110,12 +111,13 @@ type TimeScaleSetting = {
     modelAxisBreaks?: AxisBreakOption[];
 };
 
-class TimeScale extends IntervalScale<TimeScaleSetting> {
+class TimeScale extends Scale<TimeScaleSetting> {
 
     static type = 'time';
-    readonly type = 'time';
+    readonly type = 'time' as const;
 
     private _approxInterval: number;
+    private _interval: number = 0;
 
     private _minLevelUnit: TimeUnit;
 
@@ -186,7 +188,7 @@ class TimeScale extends IntervalScale<TimeScaleSetting> {
             this._approxInterval,
             useUTC,
             extent,
-            this._getExtentSpanWithBreaks(),
+            this.getBreaksElapsedExtentSpan(),
             this._brkCtx
         );
 
@@ -251,56 +253,23 @@ class TimeScale extends IntervalScale<TimeScaleSetting> {
         return ticks;
     }
 
-    calcNiceExtent(
-        opt?: {
-            splitNumber?: number,
-            minInterval?: number,
-            maxInterval?: number
-        }
-    ): void {
-        const extent = this.getExtent();
-        // If extent start and end are same, expand them
-        if (extent[0] === extent[1]) {
-            // Expand extent
-            extent[0] -= ONE_DAY;
-            extent[1] += ONE_DAY;
-        }
-        // If there are no data and extent are [Infinity, -Infinity]
-        if (extent[1] === -Infinity && extent[0] === Infinity) {
-            const d = new Date();
-            extent[1] = +new Date(d.getFullYear(), d.getMonth(), d.getDate());
-            extent[0] = extent[1] - ONE_DAY;
-        }
-        this._innerSetExtent(extent[0], extent[1]);
-
-        this.calcNiceTicks(opt.splitNumber, opt.minInterval, opt.maxInterval);
-    }
-
-    calcNiceTicks(approxTickNum: number, minInterval: number, maxInterval: 
number): void {
-        approxTickNum = approxTickNum || 10;
-
-        const span = this._getExtentSpanWithBreaks();
-        this._approxInterval = span / approxTickNum;
-
-        if (minInterval != null && this._approxInterval < minInterval) {
-            this._approxInterval = minInterval;
-        }
-        if (maxInterval != null && this._approxInterval > maxInterval) {
-            this._approxInterval = maxInterval;
-        }
-
-        const scaleIntervalsLen = scaleIntervals.length;
-        const idx = Math.min(
-            bisect(scaleIntervals, this._approxInterval, 0, scaleIntervalsLen),
-            scaleIntervalsLen - 1
+    getMinorTicks(splitNumber: number): number[][] {
+        return getMinorTicks(
+            this,
+            splitNumber,
+            this.innerGetBreaks(),
+            this._interval
         );
+    }
 
-        // Interval that can be used to calculate ticks
-        this._interval = scaleIntervals[idx][1];
-        this._intervalPrecision = 
scaleHelper.getIntervalPrecision(this._interval);
-        // Min level used when picking ticks from top down.
-        // We check one more level to avoid the ticks are to sparse in some 
case.
-        this._minLevelUnit = scaleIntervals[Math.max(idx - 1, 0)][0];
+    setTimeInterval(opt: {
+        interval: number;
+        approxInterval: number;
+        minLevelUnit: TimeUnit;
+    }): void {
+        this._interval = opt.interval;
+        this._approxInterval = opt.approxInterval;
+        this._minLevelUnit = opt.minLevelUnit;
     }
 
     parse(val: number | string | Date): number {
@@ -576,7 +545,7 @@ function getIntervalTicks(
             }
         }
 
-        // This extra tick is for calcuating ticks of next level. Will not 
been added to the final result
+        // This extra tick is for calculating ticks of next level. Will not 
been added to the final result
         out.push({
             value: dateTime,
             notAdd: true
@@ -764,6 +733,53 @@ function getIntervalTicks(
     return result;
 }
 
+export const timeScaleCalcNice: ScaleCalcNiceMethod = function (scale: 
TimeScale, opt) {
+    const extent = scale.getExtent();
+    // If extent start and end are same, expand them
+    if (extent[0] === extent[1]) {
+        // Expand extent
+        extent[0] -= ONE_DAY;
+        extent[1] += ONE_DAY;
+    }
+    // If there are no data and extent are [Infinity, -Infinity]
+    if (extent[1] === -Infinity && extent[0] === Infinity) {
+        const d = new Date();
+        extent[1] = +new Date(d.getFullYear(), d.getMonth(), d.getDate());
+        extent[0] = extent[1] - ONE_DAY;
+    }
+    scale.setExtent(extent[0], extent[1]);
+
+    const splitNumber = scaleHelper.ensureValidSplitNumber(opt.splitNumber, 
10);
+    const span = scale.getBreaksElapsedExtentSpan();
+    let approxInterval = span / splitNumber;
+
+    const minInterval = opt.minInterval;
+    const maxInterval = opt.maxInterval;
+    if (minInterval != null && approxInterval < minInterval) {
+        approxInterval = minInterval;
+    }
+    if (maxInterval != null && approxInterval > maxInterval) {
+        approxInterval = maxInterval;
+    }
+
+    const scaleIntervalsLen = scaleIntervals.length;
+    const idx = Math.min(
+        bisect(scaleIntervals, approxInterval, 0, scaleIntervalsLen),
+        scaleIntervalsLen - 1
+    );
+
+    // Interval that can be used to calculate ticks
+    const interval = scaleIntervals[idx][1];
+    // Min level used when picking ticks from top down.
+    // We check one more level to avoid the ticks are to sparse in some case.
+    const minLevelUnit = scaleIntervals[Math.max(idx - 1, 0)][0];
+
+    scale.setTimeInterval({
+        approxInterval,
+        interval,
+        minLevelUnit
+    });
+};
 
 Scale.registerClass(TimeScale);
 
diff --git a/src/scale/helper.ts b/src/scale/helper.ts
index bb10ae051..b29028281 100644
--- a/src/scale/helper.ts
+++ b/src/scale/helper.ts
@@ -20,7 +20,7 @@
 import {
     getPrecision, round, nice, quantityExponent,
     mathPow, mathMax, mathRound,
-    mathLog, mathAbs, mathFloor, mathCeil, mathMin
+    mathLog, mathAbs, mathFloor, mathCeil
 } from '../util/number';
 import IntervalScale from './Interval';
 import LogScale from './Log';
@@ -36,6 +36,13 @@ type intervalScaleNiceTicksResult = {
     niceTickExtent: [number, number]
 };
 
+export type IntervalScaleGetLabelOpt = {
+    // If 'auto', use nice precision.
+    precision?: 'auto' | number,
+    // `true`: returns 1.50 but not 1.5 if precision is 2.
+    pad?: boolean
+};
+
 /**
  * See also method `nice` in `src/util/number.ts`.
  */
@@ -286,7 +293,7 @@ export function intervalScaleEnsureValidExtent(
         }
     }
     const span = extent[1] - extent[0];
-    // If there are no data and extent are [Infinity, -Infinity]
+    // If there are no series data, extent may be `[Infinity, -Infinity]` here.
     if (!isFinite(span)) {
         extent[0] = 0;
         extent[1] = 1;
diff --git a/src/scale/minorTicks.ts b/src/scale/minorTicks.ts
new file mode 100644
index 000000000..85b8cf953
--- /dev/null
+++ b/src/scale/minorTicks.ts
@@ -0,0 +1,81 @@
+/*
+* 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.
+*/
+
+import { round } from '../util/number';
+import { ParsedAxisBreakList } from '../util/types';
+import { getScaleBreakHelper } from './break';
+import { getIntervalPrecision } from './helper';
+import Scale from './Scale';
+
+
+export function getMinorTicks(
+    scale: Scale,
+    splitNumber: number,
+    breaks: ParsedAxisBreakList,
+    scaleInterval: number
+): number[][] {
+    const ticks = scale.getTicks({
+        expandToNicedExtent: true,
+    });
+    // NOTE: In log-scale, do not support minor ticks when breaks exist.
+    //  because currently log-scale minor ticks is calculated based on raw 
values
+    //  rather than log-transformed value, due to an odd effect when breaks 
exist.
+    const minorTicks = [];
+    const extent = scale.getExtent();
+
+    for (let i = 1; i < ticks.length; i++) {
+        const nextTick = ticks[i];
+        const prevTick = ticks[i - 1];
+
+        if (prevTick.break || nextTick.break) {
+            // Do not build minor ticks to the adjacent ticks to breaks ticks,
+            // since the interval might be irregular.
+            continue;
+        }
+
+        let count = 0;
+        const minorTicksGroup = [];
+        const interval = nextTick.value - prevTick.value;
+        const minorInterval = interval / splitNumber;
+        const minorIntervalPrecision = getIntervalPrecision(minorInterval);
+
+        while (count < splitNumber - 1) {
+            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]) {
+                minorTicksGroup.push(minorTick);
+            }
+            count++;
+        }
+
+        const scaleBreakHelper = getScaleBreakHelper();
+        scaleBreakHelper && scaleBreakHelper.pruneTicksByBreak(
+            'auto',
+            minorTicksGroup,
+            breaks,
+            value => value,
+            scaleInterval,
+            extent
+        );
+        minorTicks.push(minorTicksGroup);
+    }
+
+    return minorTicks;
+}
diff --git a/src/util/jitter.ts b/src/util/jitter.ts
index 42690c1dd..a98a5d579 100644
--- a/src/util/jitter.ts
+++ b/src/util/jitter.ts
@@ -61,7 +61,7 @@ export function fixJitter(
 ): number {
     if (fixedAxis instanceof Axis2D) {
         const scaleType = fixedAxis.scale.type;
-        if (scaleType !== 'category' && scaleType !== 'ordinal') {
+        if (scaleType !== 'ordinal') {
             return floatCoord;
         }
     }
diff --git a/test/axis-align-ticks.html b/test/axis-align-ticks.html
index e22f2a99c..8b98a1b4f 100644
--- a/test/axis-align-ticks.html
+++ b/test/axis-align-ticks.html
@@ -41,7 +41,7 @@ under the License.
         <div id="main1"></div>
         <div id="main2"></div>
         <div id="main_Log_axis_can_alignTicks_to_value_axis"></div>
-        <div id="main4"></div>
+        <div id="main_Value_axis_can_alignTicks_to_log_axis"></div>
         <div id="main5"></div>
         <div id="main6"></div>
         <div id="main7"></div>
@@ -305,7 +305,7 @@ under the License.
                     ]
                 }
 
-                var chart = testHelper.create(echarts, 'main4', {
+                var chart = testHelper.create(echarts, 
'main_Value_axis_can_alignTicks_to_log_axis', {
                     title: [
                         'Value axis can alignTicks to log axis'
                     ],
diff --git a/test/runTest/actions/axis-align-ticks.json 
b/test/runTest/actions/axis-align-ticks.json
index e69e61501..b4d7a94ea 100644
--- a/test/runTest/actions/axis-align-ticks.json
+++ b/test/runTest/actions/axis-align-ticks.json
@@ -1 +1 @@
-[{"name":"Action 
1","ops":[{"type":"mousedown","time":331,"x":288,"y":71},{"type":"mouseup","time":425,"x":288,"y":71},{"time":426,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":570,"x":288,"y":71},{"type":"mousemove","time":770,"x":375,"y":70},{"type":"mousedown","time":976,"x":381,"y":70},{"type":"mousemove","time":981,"x":381,"y":70},{"type":"mouseup","time":1065,"x":381,"y":70},{"time":1066,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1337,"x":38
 [...]
\ No newline at end of file
+[{"name":"Action 
1","ops":[{"type":"mousedown","time":331,"x":288,"y":71},{"type":"mouseup","time":425,"x":288,"y":71},{"time":426,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":570,"x":288,"y":71},{"type":"mousemove","time":770,"x":375,"y":70},{"type":"mousedown","time":976,"x":381,"y":70},{"type":"mousemove","time":981,"x":381,"y":70},{"type":"mouseup","time":1065,"x":381,"y":70},{"time":1066,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1337,"x":38
 [...]
\ No newline at end of file


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

Reply via email to