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]
