blob: 563fbce7a6a1a406b6e70fd70e04fc2e75404c6e [file]
/*
* 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, isLogScale, isTimeScale,
} from '../scale/helper';
import IntervalScale, { IntervalScaleConfig } from '../scale/Interval';
import { mathCeil, mathFloor, mathMax, nice, quantity, round } from '../util/number';
import type { AxisBaseModel } from './AxisBaseModel';
import type { AxisScaleType, NumericAxisBaseOptionCommon } from './axisCommonTypes';
import {
updateIntervalOrLogScaleForNiceOrAligned
} from './axisHelper';
import { calcNiceForTimeScale } from '../scale/Time';
import type LogScale from '../scale/Log';
import Scale from '../scale/Scale';
import {
adoptScaleExtentKindMapping, adoptScaleRawExtentInfoAndPrepare,
ScaleExtentFixMinMax,
ScaleRawExtentResultFinal
} from './scaleRawExtentInfo';
import { getScaleLinearSpanEffective } from '../scale/scaleMapper';
import { NullUndefined } from '../util/types';
import type GlobalModel from '../model/Global';
import type Axis from './Axis';
// ------ START: LinearIntervalScaleStub Nice ------
function calcNiceForIntervalOrLogScale(
scale: (IntervalScale | LogScale) & Scale,
opt: ScaleCalcNiceMethodOpt,
): void {
// [CAVEAT]: If updating this impl, need to sync it to `axisAlignTicks.ts`.
const isTargetLogScale = isLogScale(scale);
const intervalStub = isTargetLogScale ? scale.intervalStub : scale;
const fixMinMax = opt.fixMinMax || [];
const oldOutermostExtent = isTargetLogScale ? scale.getExtent() : null;
const oldIntervalExtent = intervalStub.getExtent();
let newIntervalExtent = intervalScaleEnsureValidExtent(oldIntervalExtent, fixMinMax, opt.rawExtentResult);
intervalStub.setExtent(newIntervalExtent[0], newIntervalExtent[1]);
newIntervalExtent = intervalStub.getExtent();
const config = isTargetLogScale
? logScaleCalcNiceTicks(intervalStub, opt)
: intervalScaleCalcNiceTicks(intervalStub, opt);
const autoIntervalPrecision = config.intervalPrecision;
const autoInterval = config.interval;
// When auto calculated interval is not preferable, users are allowed to explicity specify
// `interval`, `min`, `max` to customize the axis. A typical case is, in angle axis with angle
// 0 - 360, where the internally calculated interval is not 60-based.
// NOTICE:
// - In `xxxAxis.type: 'log'`, ec option `xxxAxis.interval` requires a logarithm-applied
// value rather than a value in the raw scale.
// - Follow the historical behavior:
// - even `interval` is specified, the scale extent is still expanded based on the auto-calculated
// interval.
// - No validation to the specified `interval`.
const userInterval = opt.userInterval;
if (userInterval != null) {
config.interval = userInterval;
config.intervalPrecision = getIntervalPrecision(userInterval);
}
if (!fixMinMax[0]) {
newIntervalExtent[0] = round(
mathFloor(newIntervalExtent[0] / autoInterval) * autoInterval, autoIntervalPrecision
);
}
if (!fixMinMax[1]) {
newIntervalExtent[1] = round(
mathCeil(newIntervalExtent[1] / autoInterval) * autoInterval, autoIntervalPrecision
);
}
if (userInterval != null) { // Historical behavior.
config.niceExtent = newIntervalExtent.slice();
}
updateIntervalOrLogScaleForNiceOrAligned(
scale,
fixMinMax,
oldIntervalExtent,
newIntervalExtent,
oldOutermostExtent,
config,
);
}
// ------ END: LinearIntervalScaleStub Nice ------
// ------ START: IntervalScale Nice ------
function intervalScaleCalcNiceTicks(
scale: IntervalScale,
opt: Pick<ScaleCalcNiceMethodOpt, 'splitNumber' | 'minInterval' | 'maxInterval' | 'userInterval'>
): IntervalScaleConfig {
const splitNumber = ensureValidSplitNumber(opt.splitNumber, 5);
// Use the span in the innermost linear space to calculate nice ticks.
const span = getScaleLinearSpanEffective(scale);
if (__DEV__) {
assert(isFinite(span) && span > 0); // It should have been 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);
const extent = scale.getExtent();
// By design, the `niceExtent` is inside the original extent
const niceExtent = [
round(mathCeil(extent[0] / interval) * interval, intervalPrecision),
round(mathFloor(extent[1] / interval) * interval, intervalPrecision)
];
return {interval, intervalPrecision, niceExtent};
};
// ------ END: IntervalScale Nice ------
// ------ START: LogScale Nice ------
function logScaleCalcNiceTicks(
intervalStub: IntervalScale,
opt: Pick<
ScaleCalcNiceMethodOpt,
'splitNumber' | 'minInterval' | 'maxInterval' | 'userInterval'
>
): IntervalScaleConfig {
// [CAVEAT]: If updating this impl, need to sync it to `axisAlignTicks.ts`.
const splitNumber = ensureValidSplitNumber(opt.splitNumber, 10);
// Find nice ticks in the "logarithmic space". Notice that "logarithmic space" is a middle space
// rather than the innermost linear space when axis breaks exist.
const intervalExtent = intervalStub.getExtent();
// But use the span in the innermost linear space to calculate nice ticks.
const span = getScaleLinearSpanEffective(intervalStub);
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);
// For LogScale, we use a `niceExtent` in the "logarithmic space" rather than
// the original "pow space", because it is used in `intervalStub.getTicks()` thereafter.
const niceExtent = [
round(mathCeil(intervalExtent[0] / interval) * interval, intervalPrecision),
round(mathFloor(intervalExtent[1] / interval) * interval, intervalPrecision)
] as [number, number];
return {intervalPrecision, interval, niceExtent};
};
// ------ END: LogScale Nice ------
// ------ START: scaleCalcNice Entry ------
export type ScaleCalcNiceMethod = (
scale: Scale,
opt: ScaleCalcNiceMethodOpt
) => void;
type ScaleCalcNiceMethodOpt = {
splitNumber?: number | NullUndefined;
minInterval?: number | NullUndefined;
maxInterval?: number | NullUndefined;
userInterval?: number | NullUndefined;
userIntervalUseLegacy?: boolean | NullUndefined;
fixMinMax?: ScaleExtentFixMinMax | NullUndefined;
rawExtentResult?: ScaleRawExtentResultFinal | NullUndefined;
};
/**
* NOTE: See the summary of the process of extent determination in the comment of `scaleMapper.setExtent`.
*
* Calculate a "nice" extent and "nice" ticks configs based on the current scale extent and ec options.
* scale extent will be modified, and config may be set to the scale.
*
* @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
*/
export function scaleCalcNice(
axisLike: {
scale: Scale,
model: AxisBaseModel,
},
): void {
const scale = axisLike.scale;
const model = axisLike.model as AxisBaseModel<NumericAxisBaseOptionCommon>;
const axis = model.axis;
const ecModel = model.ecModel;
if (__DEV__) {
assert(axis && ecModel);
}
scaleCalcNice2(scale, model, axis, ecModel, null);
}
/**
* @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
*/
export function scaleCalcNice2(
scale: Scale,
model: AxisBaseModel<NumericAxisBaseOptionCommon>,
// Some call from external source, such as echarts-gl, may have no `axis` and `ecModel`,
// but has `externalDataExtent`.
axis: Axis | NullUndefined,
ecModel: GlobalModel | NullUndefined,
externalDataExtent: number[] | NullUndefined
): void {
const rawExtentResult = adoptScaleRawExtentInfoAndPrepare(scale, model, ecModel, axis, externalDataExtent);
const isIntervalOrTime = isIntervalScale(scale) || isTimeScale(scale);
scaleCalcNiceDirectly(scale, {
splitNumber: model.get('splitNumber'), // Backward compat - not get('xxx', true).
fixMinMax: rawExtentResult.fixMM,
userInterval: model.get('interval'), // Backward compat - not get('xxx', true).
minInterval: isIntervalOrTime ? model.get('minInterval') : null,
maxInterval: isIntervalOrTime ? model.get('maxInterval') : null,
rawExtentResult
});
if (axis && ecModel) {
adoptScaleExtentKindMapping(axis, scale, rawExtentResult, ecModel);
}
if (__DEV__) {
scale.freeze();
}
}
export function scaleCalcNiceDirectly(
scale: Scale,
opt: ScaleCalcNiceMethodOpt
): void {
scaleCalcNiceMethods[scale.type](scale, opt);
}
const scaleCalcNiceMethods: Record<AxisScaleType, ScaleCalcNiceMethod> = {
interval: calcNiceForIntervalOrLogScale,
log: calcNiceForIntervalOrLogScale,
time: calcNiceForTimeScale,
ordinal: noop,
};
// ------ END: scaleCalcNice Entry ------