blob: 0be88b9a45c4d1c8479bf52bdaba7577faf8a7d9 [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, bind, each, extend, keys, noop } from 'zrender/src/core/util';
import { initExtentForUnion, isValidBoundsForExtent } from '../util/model';
import { NullUndefined } from '../util/types';
import { AxisBreakParsingResult, BreakScaleMapper, getScaleBreakHelper } from './break';
import { error } from '../util/log';
import type { ValueTransformLookupOpt } from './helper';
import { DataSanitizationFilter } from '../data/helper/dataValueHelper';
// ------ START: Scale Mapper Core ------
/**
* Illustration:
* SCALE_EXTENT_KIND_EFFECTIVE: |------------| (always exist)
* SCALE_EXTENT_KIND_MAPPING: |---|------------|--| (present only when it is specified by `setExtent2`)
*
* - [SCALE_EXTENT_KIND_EFFECTIVE]:
* It is a portion of a scale extent that is functional on most features, including:
* - All tick/label-related calculation.
* - `dataZoom` controlled ends.
* - Cartesian2D `clampData`.
* - line series start.
* - heatmap series range.
* - markerArea range.
* - etc.
* `SCALE_EXTENT_KIND_EFFECTIVE` always exists.
*
* - [SCALE_EXTENT_KIND_MAPPING]:
* It is an expanded extent from ends of `SCALE_EXTENT_KIND_EFFECTIVE` to accommodate shapes at edges to
* avoid overflow. They can be typically used by bar/candlestick series on category axis with
* `boundaryGap: false`, or on other numeric axes. ec option `xxxAxis.containShape` is the switch.
* In this case, we need to:
* - Do not render ticks and labels in the portion "SCALE_EXTENT_KIND_MAPPING - SCALE_EXTENT_KIND_EFFECTIVE",
* since they are considered meaningless there.
* - Prevent "nice strategy" from triggering unexpectedly by the "contain shape expansion".
* Otherwise, for example, the original extent is `[0, 1000]`, then the expanded
* extent, say `[-5, 1000]`, can cause a considerable negative expansion by "nice",
* like `[-200, 10000]`, which is commonly unexpected. And it is exacerbated in LogScale.
* - Prevent the min/max tick label from displaying, since they are commonly meaningless
* and probably misleading.
* Therefore, `SCALE_EXTENT_KIND_MAPPING` is only used for:
* - mapping between data and pixel, such as,
* - `scaleMapper.normalize/scale`;
* - Cartesian2D `calcAffineTransform` (a quick path of `scaleMapper.normalize/scale`).
* - `grid` boundary related calculation in view rendering, such as, `barGrid` calculates
* `barWidth` for numeric scales based on the data extent.
* - Axis line position determination (such as `canOnZeroToAxis`);
* - `axisPointer` triggering (otherwise users may be confused if using `SCALE_EXTENT_KIND_EFFECTIVE`).
* `SCALE_EXTENT_KIND_MAPPING` can be absent, which can be used to determine whether it is used.
*
* @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
*/
export type ScaleExtentKind =
typeof SCALE_EXTENT_KIND_EFFECTIVE
| typeof SCALE_EXTENT_KIND_MAPPING;
export const SCALE_EXTENT_KIND_EFFECTIVE = 0;
export const SCALE_EXTENT_KIND_MAPPING = 1;
const SCALE_MAPPER_METHOD_NAMES_MAP: Record<keyof ScaleMapper, 1> = {
needTransform: 1,
normalize: 1,
scale: 1,
transformIn: 1,
transformOut: 1,
contain: 1,
getExtent: 1,
getExtentUnsafe: 1,
setExtent: 1,
setExtent2: 1,
getFilter: 1,
sanitize: 1,
getDefaultStartValue: 1,
freeze: 1,
};
const SCALE_MAPPER_METHOD_NAMES = keys(SCALE_MAPPER_METHOD_NAMES_MAP);
/**
* - [SCALE_MAPPER_DEPTH_OUT_OF_BREAK]:
* In `transformIn`, it transforms a value from the outermost space to the space before break being applied.
* In `transformOut`, it transforms a value from the space before break being applied to the outermost space.
* Typically nice axis ticks are picked in that space due to the current design of nice ticks
* algorithm, while size related features may use `SCALE_MAPPER_DEPTH_INNERMOST`.
* - [SCALE_MAPPER_DEPTH_INNERMOST]:
* Currently only linear space is used as the innermost space.
*/
export type ScaleMapperDepthOpt = {
depth?: NullUndefined
| typeof SCALE_MAPPER_DEPTH_OUT_OF_BREAK
| typeof SCALE_MAPPER_DEPTH_INNERMOST;
};
export const SCALE_MAPPER_DEPTH_OUT_OF_BREAK = 2;
export const SCALE_MAPPER_DEPTH_INNERMOST = 3;
export type ScaleMapperTransformOutOpt = (
// depth: NullUndefined means SCALE_MAPPER_DEPTH_INNERMOST.
ScaleMapperDepthOpt
& ValueTransformLookupOpt
);
export type ScaleMapperTransformInOpt =
// depth: NullUndefined means SCALE_MAPPER_DEPTH_INNERMOST.
ScaleMapperDepthOpt;
/**
* @tutorial [SCALE_COMPOSITION_AND_TRANSFORMATION]:
* `ScaleMapper` is designed for multiple steps of numeric transformations from a certain space to a linear space,
* or vice versa. Each steps is implemented as a `ScaleMapper`, and composed like a decorator pattern. And some
* steps, such as "axis breaks transfromation", can be skipped when no breaks for performance consideration.
* Currently we support:
* - step#0: extent based linear scaling.
* This is implemented in `LinearScaleMapper`.
* It is mixed into `IntervalScale`, `TimeScale`;
* and it is also composited into `BreakScaleMapper`, `OrdinalScale`, `LogScale`.
* - step#1: axis breaks.
* This is implemented in `BreakScaleMapper`.
* This step may be absent if no breaks.
* - step#2: logarithmic (implemented in `LogScale`), or
* ordinal-related handling, or
* others to be supported, such as asinh ...
* Illustration of some currently supported cases:
* - linear_space(in an IntervalScale)
* - break_space(in an IntervalScale method bound by a BreakScaleMapper)
* └─break_transform─► linear_space(in a LinearScaleMapper owned by a BreakScaleMapper)
* - log_space(in a LogScale)
* └─log_transform─► linear_space(in an IntervalScale)
* - log_space(in a LogScale)
* └─log_transform─► break_space(in an IntervalScale method bound by a BreakScaleMapper)
* └─break_transform─► linear_space(in a LinearScaleMapper owned by a BreakScaleMapper)
* - linear_space(in a TimeScale)
* - break_space(in a TimeScale method bound by a BreakScaleMapper)
* └─break_transform─► linear_space(in a LinearScaleMapper owned by a BreakScaleMapper)
* - category_values(in a OrdinalScale)
* └─category_to_numeric─► linear_space(in a LinearScaleMapper owned by a BreakScaleMapper)
*
*
* @tutorial [SCALE_EXTENT_CONSTRUCTION]:
* The full construction processing of the scale extent in EC_FULL_UPDATE_CYCLE:
* - step#1. At `CoordinateSystem#create` stage, requirements of collecting series data extents are
* committed to `associateSeriesWithAxis`, and `Scale` instances are created.
* - step#2. Call `scaleRawExtentInfoCreate` to really collect series data extent and create
* `ScaleRawExtentInfo` instances to manage extent related configurations
* - at "data processing" stage for dataZoom controlled axes, if any, or
* - at "CoordinateSystem#update" stage for all other axes.
* - step#3. Perform "nice" (see `scaleCalcNice`) or "align" (see `scaleCalcAlign`) strategies to
* modify the original extent from `ScaleRawExtentInfo` instance, if needed, at
* "CoordinateSystem#update" stage.
* - step#4. `calcContainShape` is performed. Set `SCALE_EXTENT_KIND_MAPPING` if needed.
* See AXIS_CONTAIN_SHAPE_PROCESSING_ORDER for more details.
*
*/
export interface ScaleMapper extends ScaleMapperGeneric<ScaleMapper> {}
export interface ScaleMapperGeneric<This> {
/**
* Enable a fast path in large data traversal - the call of `transformIn`/`transformOut`
* can be omitted, and this is the most case.
*/
needTransform(this: This): boolean;
/**
* Normalize a value to linear [0, 1], return 0.5 if extent span is 0.
* The typical logic is:
* `transformIn_self` -> `transformIn_inner` -> ... to the innermost space,
* then do linear normalization based on innermost extent.
*/
normalize: (this: This, val: number) => number;
/**
* Scale a normalized value to extent. It's the inverse of `normalize`.
*/
scale: (this: This, val: number) => number;
/**
* [NOTICE]:
* - This method must be available since the instance is constructed.
* - This method has nothing to do with extent - transforming out of extent is supported.
*
* This method transforms a value forward into a inner space.
* The typical logic is:
* `transformIn_self` -> `transformIn_inner` -> ... to the innermost space.
* In most cases axis ticks are laid out in linear space, and some features
* (such as LogScale, axis breaks) transform values from their own spaces into linear space.
*/
transformIn: (
this: This,
val: number,
opt: ScaleMapperTransformInOpt | NullUndefined
) => number;
/**
* [NOTICE]:
* - This method must be available since the instance is constructed.
* - This method has nothing to do with extent - transforming out of extent is supported.
*
* The inverse method of `transformIn`.
*/
transformOut: (
this: This,
val: number,
opt: ScaleMapperTransformOutOpt | NullUndefined
) => number;
/**
* Whether the extent contains the given value.
*/
contain: (this: This, val: number) => boolean;
/**
* [NOTICE]:
* In EC_FULL_UPDATE_CYCLE, scale extent is finally determined at `coordSys#update` stage.
*
* Get a clone of the scale extent.
* An extent is always in an increase order.
* It always returns an array - never be a null/undefined.
*
* @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
*/
getExtent(this: This): number[];
/**
* [NOTICE]:
* Callers must NOT modify the return.
*
* @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
*/
getExtentUnsafe(
this: This,
kind: ScaleExtentKind,
// NullUndefined means the outermost space.
depth: ScaleMapperDepthOpt['depth'] | NullUndefined
): number[] | NullUndefined;
/**
* [NOTICE]:
* The caller must ensure `start <= end` and both are finite number!
*
* `setExtent` is identical to `setExtent2(SCALE_EXTENT_KIND_EFFECTIVE)`.
*
* @see SCALE_EXTENT_CONSTRUCTION for the full processing flow.
*/
setExtent(this: This, start: number, end: number): void;
setExtent2(this: This, kind: ScaleExtentKind, start: number, end: number): void;
/**
* Filter for sanitization.
*/
getFilter?: () => DataSanitizationFilter;
/**
* NOTICE:
* - Should not sanitize invalid values (e.g., NaN, Infinity, null, undefined),
* since it probably has special meaning, and always properly handled in every Scale.
* - Should not depend on the current `extent`, since it can be used before `extent` determined.
*
* Sanitize a value if possible. For example, for LogScale, the negative part will be clampped.
* This provides some permissiveness to ec option like `xxxAxis.min/max`.
*/
sanitize?: (
(this: This, value: number | NullUndefined, dataExtent: number[]) => number | NullUndefined
) | NullUndefined;
/**
* If not provided, use `0`.
*/
getDefaultStartValue?: (() => number) | NullUndefined;
/**
* Restrict the modification behavior of a scale for robustness. e.g., avoid subsequently
* modifying `SCALE_EXTENT_KIND_EFFECTIVE` but no sync to `SCALE_EXTENT_KIND_MAPPING`.
*/
freeze(this: This): void;
}
export function initBreakOrLinearMapper(
// If input `null/undefined`, a mapper will be created.
mapper: ScaleMapper | NullUndefined,
breakParsed: AxisBreakParsingResult | NullUndefined,
initialExtent: number[] | NullUndefined,
): {
// If breaks are not available, `brk` is `null/undefined`.
brk: BreakScaleMapper | NullUndefined;
// Never be `null/undefined`.
mapper: ScaleMapper;
} {
let brk: BreakScaleMapper | NullUndefined;
mapper = mapper || {} as ScaleMapper;
const scaleBreakHelper = getScaleBreakHelper();
if (scaleBreakHelper) {
const brkMapper = scaleBreakHelper.createBreakScaleMapper(breakParsed, initialExtent);
if (brkMapper.hasBreaks()) {
// Some `ScaleMapper` methods (such as `normalize`) needs to be fast for large data
// when no breaks, so mount break methods only when breaks really exist.
each(SCALE_MAPPER_METHOD_NAMES, function (methodName) {
if (brkMapper[methodName]) {
(mapper as any)[methodName] = bind(brkMapper[methodName], brkMapper);
}
});
brk = brkMapper;
}
}
if (brk == null) {
initLinearScaleMapper(mapper, initialExtent);
}
return {brk, mapper};
}
export type DecoratedScaleMapperMethods<THost extends ScaleMapper> = Omit<ScaleMapperGeneric<THost>, 'freeze'>;
export function decorateScaleMapper<THost extends ScaleMapper>(
host: THost,
decoratedMapperMethods: Omit<ScaleMapperGeneric<THost>, 'freeze'>
): void {
each(SCALE_MAPPER_METHOD_NAMES, function (methodName) {
(host as any)[methodName] = (decoratedMapperMethods as ScaleMapperGeneric<THost>)[methodName];
});
}
export function enableScaleMapperFreeze(host: ScaleMapper, subMapper: ScaleMapper): void {
host.freeze = noop;
if (__DEV__) {
host.freeze = function () {
subMapper.freeze();
};
};
}
export function getScaleExtentForTickUnsafe(mapper: ScaleMapper): number[] {
return mapper.getExtentUnsafe(SCALE_EXTENT_KIND_EFFECTIVE, SCALE_MAPPER_DEPTH_OUT_OF_BREAK);
}
export function getScaleExtentForMappingUnsafe(
mapper: ScaleMapper,
// NullUndefined means the outermost space.
depth: ScaleMapperDepthOpt['depth'] | NullUndefined
): number[] {
return mapper.getExtentUnsafe(SCALE_EXTENT_KIND_MAPPING, depth)
|| mapper.getExtentUnsafe(SCALE_EXTENT_KIND_EFFECTIVE, depth);
}
export function getScaleLinearSpanForMapping(mapper: ScaleMapper): number {
const extent = getScaleExtentForMappingUnsafe(mapper, SCALE_MAPPER_DEPTH_INNERMOST);
return extent[1] - extent[0];
}
export function getScaleLinearSpanEffective(mapper: ScaleMapper): number {
const extent = mapper.getExtentUnsafe(SCALE_EXTENT_KIND_EFFECTIVE, SCALE_MAPPER_DEPTH_INNERMOST);
return extent[1] - extent[0];
}
// ------ END: Scale Mapper Core ------
// ------ START: Linear Scale Mapper ------
/**
* Generally, no need to export `LinearScaleMapper` and not recommended
* to visit `_extent` directly outside, otherwise it may be incorrect
* due to possible polymorphism - use `getExtentUnsafe()` instead.
*/
interface LinearScaleMapper extends ScaleMapper {
/**
* [CAVEAT]:
* - Should update only by `setExtent` or `setExtent2`!
* - The caller of `setExtent()` should ensure `extent[0] <= extent[1]`,
* but it is initialized as `[Infinity, -Infinity]`.
* With these restriction, `extent` can only be either:
* + `extent[0] < extent[1]` and both finite, or
* + `extent[0] === extent[1]` and both finite, or
* + `extent[0] === Infinity && extent[1] === -Infinity`
*
* Structure: `_extent[ScaleExtentKind][]`
*/
readonly _extents: number[][];
readonly _frozen: boolean;
}
export function initLinearScaleMapper(
// If input `null/undefined`, a mapper will be created.
mapper: ScaleMapper | NullUndefined,
initialExtent: number[] | NullUndefined
): ScaleMapper {
const linearMapper = (mapper || {}) as LinearScaleMapper;
const extendList: number[][] = [];
// @ts-ignore
linearMapper._extents = extendList;
extendList[SCALE_EXTENT_KIND_EFFECTIVE] = initialExtent ? initialExtent.slice() : initExtentForUnion();
extend(linearMapper, linearScaleMapperMethods);
return linearMapper;
}
const linearScaleMapperMethods: ScaleMapperGeneric<LinearScaleMapper> = {
needTransform() {
return false;
},
normalize(val) {
const extent = this._extents[SCALE_EXTENT_KIND_MAPPING] || this._extents[SCALE_EXTENT_KIND_EFFECTIVE];
if (extent[1] === extent[0]) {
return 0.5;
}
return (val - extent[0]) / (extent[1] - extent[0]);
},
scale(val) {
const extent = this._extents[SCALE_EXTENT_KIND_MAPPING] || this._extents[SCALE_EXTENT_KIND_EFFECTIVE];
return val * (extent[1] - extent[0]) + extent[0];
},
transformIn(val) {
return val;
},
transformOut(val) {
return val;
},
contain(val) {
// This method is typically used in axis trigger and markers.
// Users may be confused if the extent is restricted to `SCALE_EXTENT_KIND_EFFECTIVE`.
const extent = getScaleExtentForMappingUnsafe(this, null);
return val >= extent[0] && val <= extent[1];
},
getExtent() {
return this._extents[SCALE_EXTENT_KIND_EFFECTIVE].slice();
},
getExtentUnsafe(kind) {
return this._extents[kind];
},
setExtent(start, end) {
if (__DEV__) {
assert(!this._frozen);
}
writeExtent(this._extents, SCALE_EXTENT_KIND_EFFECTIVE, start, end);
},
setExtent2(kind, start, end) {
if (__DEV__) {
assert(!this._frozen);
}
const extentList = this._extents;
if (!extentList[kind]) {
extentList[kind] = extentList[SCALE_EXTENT_KIND_EFFECTIVE].slice();
}
writeExtent(extentList, kind, start, end);
},
freeze() {
if (__DEV__) {
// @ts-ignore
this._frozen = true;
}
}
};
function writeExtent(
extentList: number[][], kind: ScaleExtentKind, start: number, end: number
): void {
// NOTE: `NaN` should be excluded. e.g., `scaleRawExtentInfo.resultMinMax` may be `[NaN, NaN]`.
if (isValidBoundsForExtent(start, end)) {
extentList[kind][0] = start;
extentList[kind][1] = end;
}
else {
if (__DEV__) {
// PENDING: should use `assert` after fixing all invalid calls.
if (start != null && end != null && start <= end) {
error(`Invalid setExtent call - start: ${start}, end: ${end}`);
}
}
}
}
// ------ END: Linear Scale Mapper ------