| /* |
| * 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, mathRound, mathMin, getPrecision} from '../util/number'; |
| import {addCommas} from '../util/format'; |
| import Scale, { ScaleGetTicksOpt } from './Scale'; |
| import { getIntervalPrecision, IntervalScaleGetLabelOpt } from './helper'; |
| import {ScaleTick, ScaleDataValue, NullUndefined, AxisBreakOption} from '../util/types'; |
| import { |
| AxisBreakParsingResult, getBreaksUnsafe, getScaleBreakHelper, hasBreaks, simplyParseBreakOption |
| } from './break'; |
| import { assert, clone } from 'zrender/src/core/util'; |
| import { getMinorTicks } from './minorTicks'; |
| import { |
| getScaleExtentForTickUnsafe, |
| initBreakOrLinearMapper, ScaleMapperGeneric |
| } from './scaleMapper'; |
| import { warn } from '../util/log'; |
| |
| |
| export type IntervalScaleConfig = { |
| interval: IntervalScaleConfigParsed['interval']; |
| intervalPrecision?: IntervalScaleConfigParsed['intervalPrecision'] | NullUndefined; |
| intervalCount?: IntervalScaleConfigParsed['intervalCount'] | NullUndefined; |
| niceExtent?: IntervalScaleConfigParsed['niceExtent'] | NullUndefined; |
| }; |
| |
| type IntervalScaleConfigParsed = { |
| /** |
| * Step of ticks. |
| */ |
| interval: number; |
| intervalPrecision: number; |
| /** |
| * `_intervalCount` effectively specifies the number of "nice segments". This is for special cases, |
| * such as `alignTicks: true` and min max are fixed. In this case, `_interval` may be specified with |
| * a "not-nice" value and needs to be rounded with `_intervalPrecision` for better appearance. Then |
| * merely accumulating `_interval` may generate incorrect number of ticks due to cumulative errors. |
| * So `_intervalCount` is required to specify the expected nice ticks number. |
| * Should ensure `_intervalCount >= -1`, |
| * where `-1` means no nice tick (e.g., `_extent: [5.2, 5.8], _interval: 1`), |
| * and `0` means only one nice tick (e.g., `_extent: [5, 5.8], _interval: 1`). |
| * @see setInterval |
| */ |
| intervalCount: number | NullUndefined; |
| /** |
| * Should ensure: |
| * `_extent[0] <= _niceExtent[0] && _niceExtent[1] <= _extent[1]` |
| * But NOTICE: |
| * `_niceExtent[0] - _niceExtent[1] <= _interval`, rather than always `< 0`, |
| * because `_niceExtent` is typically calculated by |
| * `[ Math.ceil(_extent[0] / _interval) * _interval, Math.floor(_extent[1] / _interval) * _interval ]`. |
| * e.g., `_extent: [5.2, 5.8]` with interval `1` will get `_niceExtent: [6, 5]`. |
| * e.g., `_extent: [5, 5.8]` with interval `1` will get `_niceExtent: [5, 5]`. |
| * e.g., `_extent: [5.7, 5.7]` with interval `1` will get `_niceExtent: [6, 5]`. |
| * @see setInterval |
| */ |
| niceExtent: number[] | NullUndefined; |
| }; |
| |
| type IntervalScaleSetting = { |
| // Either `breakOption` or `parsedBreaks` can be specified. |
| breakOption?: AxisBreakOption[] | NullUndefined; |
| breakParsed?: AxisBreakParsingResult | NullUndefined; |
| }; |
| |
| /** |
| * @final NEVER inherit me! |
| */ |
| interface IntervalScale extends ScaleMapperGeneric<IntervalScale> {} |
| class IntervalScale extends Scale<IntervalScale> { |
| |
| static type = 'interval'; |
| type = 'interval' as const; |
| |
| private _cfg: IntervalScaleConfigParsed; |
| |
| |
| constructor(setting?: IntervalScaleSetting) { |
| super(); |
| |
| this.parse = IntervalScale.parse; |
| |
| setting = setting || {}; |
| |
| const breakParsed = simplyParseBreakOption(this, setting); |
| |
| const res = initBreakOrLinearMapper(this, breakParsed, null); |
| // @ts-ignore |
| this.brk = res.brk; |
| |
| this._cfg = { |
| interval: 0, |
| intervalPrecision: 2, |
| intervalCount: undefined, |
| niceExtent: undefined, |
| }; |
| } |
| |
| static 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. |
| // but `series.data` is not included, which uses `dataValueHelper.ts`#`parseDataValue`. |
| // `Scale#parse` originally introduced in fb8c813215098b9d2458966229bb95c510883d5e |
| // at 2016 for dataZoom start/end settings (See `parseAxisModelMinMax`). |
| // |
| // Historically `scale/Interval.ts` returns the input value directly. But numeric |
| // values (such as a number-like string '123') effectively passed through here and |
| // were involved in calculations, which was error-prone and inconsistent with the |
| // declared TS return type. Previously such issues are fixed separately in different |
| // places case by case (such as #2475). |
| // |
| // Now, we perform actual parse to ensure its `number` type here. The parsing rule |
| // follows the series data parsing rule (`dataValueHelper.ts`#`parseDataValue`) |
| // and maintains compatibility as much as possible (thus a more strict parsing |
| // `number.ts`#`numericToNumber` is not used here.) |
| // |
| // FIXME: `ScaleDataValue` also need to be modified to include numeric string type, |
| // since it effectively does. |
| return (val == null || val === '') |
| ? NaN |
| // If string (like '-'), using '+' parse to NaN |
| // If object, also parse to NaN |
| : Number(val); |
| } |
| |
| getConfig(): IntervalScaleConfigParsed { |
| return clone(this._cfg); |
| } |
| |
| setConfig(cfg: IntervalScaleConfig): void { |
| const extent = getScaleExtentForTickUnsafe(this); |
| |
| if (__DEV__) { |
| assert(cfg.interval != null); |
| if (cfg.intervalCount != null) { |
| assert( |
| cfg.intervalCount >= -1 |
| && cfg.intervalPrecision != null |
| // Do not support intervalCount on axis break currently. |
| && !hasBreaks(this) |
| ); |
| } |
| 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); |
| } |
| } |
| |
| // 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. |
| cfg.niceExtent = extent.slice() as [number, number]; |
| } |
| if (cfg.intervalPrecision == null) { |
| cfg.intervalPrecision = getIntervalPrecision(cfg.interval); |
| } |
| } |
| |
| /** |
| * In ascending order. |
| */ |
| getTicks(opt?: ScaleGetTicksOpt): ScaleTick[] { |
| opt = opt || {}; |
| const cfg = this._cfg; |
| const interval = cfg.interval; |
| const extent = getScaleExtentForTickUnsafe(this); |
| const niceExtent = cfg.niceExtent; |
| const intervalPrecision = cfg.intervalPrecision; |
| const scaleBreakHelper = getScaleBreakHelper(); |
| const brk = this.brk; |
| const brkAvailable = scaleBreakHelper && brk; |
| |
| const ticks = [] as ScaleTick[]; |
| // If interval is 0, return []; |
| if (!interval) { |
| return ticks; |
| } |
| |
| if (opt.breakTicks === 'only_break' && brkAvailable) { |
| scaleBreakHelper.addBreaksToTicks(ticks, brk.breaks, extent); |
| return ticks; |
| } |
| |
| if (__DEV__) { |
| assert(niceExtent != null); |
| } |
| |
| // [CAVEAT]: If changing this logic, must sync it to `axisAlignTicks.ts`. |
| |
| // A fail-safe is required since `interval` can be user specified, or for the case |
| // that using dataZoom toolbox and zoom repeatedly. |
| const safeLimit = 3000; |
| |
| if (extent[0] < niceExtent[0]) { |
| ticks.push({ |
| value: opt.expandToNicedExtent |
| ? round(niceExtent[0] - interval, intervalPrecision) |
| : extent[0] |
| }); |
| } |
| |
| const estimateNiceMultiple = (tickVal: number, targetTick: number) => { |
| return mathRound((targetTick - tickVal) / interval); |
| }; |
| |
| const intervalCount = cfg.intervalCount; |
| for ( |
| let tick = niceExtent[0], niceTickIdx = 0; |
| ; |
| niceTickIdx++ |
| ) { |
| // Consider case `_extent: [5.2, 5.8], _niceExtent: [6, 5], interval: 1`, |
| // `_intervalCount` makes sense iff `-1`. |
| // Consider case `_extent: [5, 5.8], _niceExtent: [5, 5], interval: 1`, |
| // `_intervalCount` makes sense iff `0`. |
| if (intervalCount == null) { |
| if (tick > niceExtent[1] || !isFinite(tick) || !isFinite(niceExtent[1])) { |
| break; |
| } |
| } |
| else { |
| if (niceTickIdx > intervalCount) { // nice ticks number should be `intervalCount + 1` |
| break; |
| } |
| // Consider cumulative error, especially caused by rounding, the last nice |
| // `tick` may be less than or greater than `niceExtent[1]` slightly. |
| tick = mathMin(tick, niceExtent[1]); |
| if (niceTickIdx === intervalCount) { |
| tick = niceExtent[1]; |
| } |
| } |
| |
| ticks.push({value: tick}); |
| |
| // Avoid rounding error |
| tick = round(tick + interval, intervalPrecision); |
| |
| if (brk) { |
| const moreMultiple = brk.calcNiceTickMultiple(tick, estimateNiceMultiple); |
| if (moreMultiple >= 0) { |
| tick = round(tick + moreMultiple * interval, intervalPrecision); |
| } |
| } |
| |
| if (ticks.length > 0 && tick === ticks[ticks.length - 1].value) { |
| // Consider out of safe float point, e.g., |
| // -3711126.9907707 + 2e-10 === -3711126.9907707 |
| break; |
| } |
| if (ticks.length > safeLimit) { |
| if (__DEV__) { |
| warn('Exceed safe limit in IntervalScale["getTicks"].'); |
| } |
| return []; |
| } |
| } |
| |
| // Consider this case: the last item of ticks is smaller |
| // than niceExtent[1] and niceExtent[1] === extent[1]. |
| const lastNiceTick = ticks.length ? ticks[ticks.length - 1].value : niceExtent[1]; |
| if (extent[1] > lastNiceTick) { |
| ticks.push({ |
| value: opt.expandToNicedExtent |
| ? round(lastNiceTick + interval, intervalPrecision) |
| : extent[1] |
| }); |
| } |
| |
| if (brkAvailable) { |
| scaleBreakHelper.pruneTicksByBreak( |
| opt.pruneByBreak, |
| ticks, |
| brk.breaks, |
| item => item.value, |
| cfg.interval, |
| extent |
| ); |
| } |
| if (brkAvailable && opt.breakTicks !== 'none') { |
| scaleBreakHelper.addBreaksToTicks(ticks, brk.breaks, extent); |
| } |
| |
| return ticks; |
| } |
| |
| getMinorTicks(splitNumber: number): number[][] { |
| return getMinorTicks( |
| this, |
| splitNumber, |
| getBreaksUnsafe(this), |
| this._cfg.interval |
| ); |
| } |
| |
| getLabel( |
| tick: ScaleTick, |
| opt?: IntervalScaleGetLabelOpt |
| ): string { |
| if (tick == null) { |
| return ''; |
| } |
| |
| let precision = opt && opt.precision; |
| |
| if (precision == null) { |
| precision = getPrecision(tick.value) || 0; |
| } |
| else if (precision === 'auto') { |
| // Should be more precise then tick. |
| precision = this._cfg.intervalPrecision; |
| } |
| |
| // (1) If `precision` is set, 12.005 should be display as '12.00500'. |
| // (2) Use `round` (toFixed) to avoid scientific notation like '3.5e-7'. |
| const dataNum = round(tick.value, precision as number, true); |
| |
| return addCommas(dataNum); |
| } |
| |
| } |
| |
| Scale.registerClass(IntervalScale); |
| |
| export default IntervalScale; |