blob: 3011d65936dd2005f68ab49fcd5c649a34364088 [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, createHashMap, each, HashMap, retrieve2 } from 'zrender/src/core/util';
import type GlobalModel from '../model/Global';
import type SeriesModel from '../model/Series';
import {
makeCallOnlyOnce, makeInner,
} from '../util/model';
import { ComponentSubType, NullUndefined } from '../util/types';
import type Axis from './Axis';
import type { AxisBaseModel } from './AxisBaseModel';
import { EChartsExtensionInstallRegisters } from '../extension';
import type ComponentModel from '../model/Component';
import {
getCachePerECFullUpdate, getCachePerECPrepare, GlobalModelCachePerECFullUpdate,
GlobalModelCachePerECPrepare
} from '../util/cycleCache';
import { CoordinateSystem } from './CoordinateSystem';
const callOnlyOnce = makeCallOnlyOnce();
// Ensure that it never appears in internal generated uid and pre-defined coordSysType.
export const AXIS_STAT_KEY_DELIMITER = '|&';
const ecModelCacheFullUpdateInner = makeInner<{
// It stores all <axis, series> pairs, aggregated by axis, based on which axis scale extent is calculated.
// NOTICE: series that has been filtered out are included.
// It is unrelated to `AxisStatKeyedClient`.
axSer: HashMap<SeriesModel[], AxisBaseModel['uid']>;
// AxisStatKey based statistics records.
// Only `AxisStatKeyedClient` concerned <axis, series> pairs are collected, based on which
// statistics are calculated.
keyed: AxisStatKeyed;
// `keys` is only used to quick travel.
keys: AxisStatKeys;
// Only used in dev mode for duplication checking.
axSerPairCheck?: HashMap<1, string>;
}, GlobalModelCachePerECFullUpdate>();
type AxisStatKeys = HashMap<AxisStatKey[], ComponentModel['uid']>;
type AxisStatKeyed = HashMap<AxisStatPerKey | NullUndefined, AxisStatKey>;
type AxisStatPerKey = HashMap<AxisStatPerKeyPerAxis | NullUndefined, AxisBaseModel['uid']>;
export type AxisStatPerKeyPerAxis = {
axis: Axis;
// This is series use this axis as base axis and need to be laid out.
// The order is determined by the client and must be respected.
// Never be null/undefined; Never be length === 0;
// series filtered out is included.
sers: SeriesModel[];
// For query. The array index is series index.
serByIdx: SeriesModel[];
// Minimal positive gap of values (in the linear space) of all relevant series (e.g. per `BaseBarSeriesSubType`)
// on this axis.
liPosMinGap?:
// Can only be a positive number rather than zero.
number
// In this case a positive min gap can not be calculated.
| typeof LINEAR_POSITIVE_MIN_GAP_NO_VALID_VALUE
// In this case a positive min gap can not be calculated.
| typeof LINEAR_POSITIVE_MIN_GAP_SINGLE_VALID_VALUE
// Be `null`/`undefined` if this metric is not required.
| NullUndefined;
// metrics corresponds to this record.
metrics?: AxisStatMetrics;
};
// In this case, there are one or multiple valid data value but all the same.
export const LINEAR_POSITIVE_MIN_GAP_SINGLE_VALID_VALUE = -2;
export const LINEAR_POSITIVE_MIN_GAP_NO_VALID_VALUE = -1;
const ecModelCachePrepareInner = makeInner<{
keyed: AxisStatECPrepareCacheKeyed | NullUndefined;
}, GlobalModelCachePerECPrepare>();
type AxisStatECPrepareCacheKeyed = HashMap<AxisStatECPrepareCachePerKey | NullUndefined, AxisStatKey>;
type AxisStatECPrepareCachePerKey = HashMap<AxisStatECPrepareCachePerKeyPerAxis | NullUndefined, AxisBaseModel['uid']>;
export type AxisStatECPrepareCachePerKeyPerAxis =
Pick<AxisStatPerKeyPerAxis, 'liPosMinGap'> & {
// Used for cache validity.
serUids?: HashMap<1, ComponentModel['uid']>
};
export type AxisStatKeyedClient = {
// A key for retrieving result.
key: AxisStatKey;
// Only the specific `seriesType` is covered.
seriesType: ComponentSubType;
// `true` by default - the <axis, series> pair is collected only if series's base axis is that axis.
baseAxis?: boolean | NullUndefined;
// `NullUndefined` by default - all coordinate systems are covered.
coordSysType?: CoordinateSystem['type'] | NullUndefined;
// `NullUndefined` return indicates this axis should be omitted.
getMetrics: (axis: Axis) => AxisStatMetrics | NullUndefined;
};
/**
* Within each individual axis, different groups of relevant series and statistics are
* designated by a `AxisStatKey`.
*
* `AxisStatKey` is a static definition.
* In most case a `seriesType` is used as a `AxisStatKey` (See `makeAxisStatKey`).
* Sometimes a `seriesType`+`coordSysType` is used as a `AxisStatKey` (See `makeAxisStatKey2`).
*
* A <axis, series> pair can only own to one `AxisStatKey`.
*/
export type AxisStatKey = string & {_: 'AxisStatKey'}; // Nominal to avoid misusing.
type ClientLookupKey = string & {_: 'ClientLookupKey'}; // Nominal to avoid misusing; internal usage.
export type AxisStatMetrics = {
// NOTICE:
// May be time-consuming in large data due to some metrics requiring travel and sort of
// series data, especially when axis break is used, so it is performed only if required.
liPosMinGap?: boolean
};
export type AxisStatisticsResult = Pick<AxisStatPerKeyPerAxis, 'liPosMinGap'>;
type AxisStatEachSeriesCb = (seriesModel: SeriesModel) => void;
let validateInputAxis: ((axis: Axis) => void) | NullUndefined;
if (__DEV__) {
validateInputAxis = function (axis) {
assert(axis && axis.model && axis.model.uid && axis.model.ecModel);
};
}
function getAxisStatPerKeyPerAxis(
axis: Axis,
axisStatKey: AxisStatKey
): AxisStatPerKeyPerAxis | NullUndefined {
const axisModel = axis.model;
const keyed = ecModelCacheFullUpdateInner(getCachePerECFullUpdate(axisModel.ecModel)).keyed;
const perKey = keyed && keyed.get(axisStatKey);
return perKey && perKey.get(axisModel.uid);
}
export function getAxisStat(
axis: Axis,
axisStatKey: AxisStatKey
// Return: Never return null/undefined.
): AxisStatisticsResult {
if (__DEV__) {
assert(axisStatKey != null);
validateInputAxis(axis);
}
return wrapStatResult(getAxisStatPerKeyPerAxis(axis, axisStatKey));
}
export function getAxisStatBySeries(
axis: Axis,
seriesList: (SeriesModel | NullUndefined)[]
// Return: Never be null/undefined; never contain null/undefined.
): AxisStatisticsResult[] {
if (__DEV__) {
validateInputAxis(axis);
}
const result: AxisStatisticsResult[] = [];
eachKeyEachAxis(axis.model.ecModel, function (perKeyPerAxis) {
for (let idx = 0; idx < seriesList.length; idx++) {
if (seriesList[idx] && perKeyPerAxis.serByIdx[seriesList[idx].seriesIndex]) {
result.push(wrapStatResult(perKeyPerAxis));
}
}
});
return result;
}
function eachKeyEachAxis(
ecModel: GlobalModel,
cb: (
perKeyPerAxis: AxisStatPerKeyPerAxis,
axisStatKey: AxisStatKey,
axisModelUid: AxisBaseModel['uid']
) => void
): void {
const keyed = ecModelCacheFullUpdateInner(getCachePerECFullUpdate(ecModel)).keyed;
keyed && keyed.each(function (perKey, axisStatKey) {
perKey.each(function (perKeyPerAxis, axisModelUid) {
cb(perKeyPerAxis, axisStatKey, axisModelUid);
});
});
}
function wrapStatResult(record: AxisStatPerKeyPerAxis | NullUndefined): AxisStatisticsResult {
return {
liPosMinGap: record ? record.liPosMinGap : undefined,
};
}
export function eachSeriesOnAxis(
axis: Axis,
cb: AxisStatEachSeriesCb
): void {
if (__DEV__) {
validateInputAxis(axis);
}
const ecModel = axis.model.ecModel;
const seriesOnAxisMap = ecModelCacheFullUpdateInner(getCachePerECFullUpdate(ecModel)).axSer;
seriesOnAxisMap && eachSeriesDealForAxisStat(ecModel, seriesOnAxisMap.get(axis.model.uid), cb);
}
/**
* NOTE:
* - series declaration order is respected (some ec option precedence matters, e.g., bar series).
* - series filtered out are excluded.
*/
export function eachSeriesOnAxisOnKey(
axis: Axis,
axisStatKey: AxisStatKey,
cb: AxisStatEachSeriesCb
): void {
if (__DEV__) {
assert(axisStatKey != null);
validateInputAxis(axis);
}
const perKeyPerAxis = getAxisStatPerKeyPerAxis(axis, axisStatKey);
perKeyPerAxis && eachSeriesDealForAxisStat(axis.model.ecModel, perKeyPerAxis.sers, cb);
}
export function eachSeriesDealForAxisStat(
ecModel: GlobalModel,
seriesList: SeriesModel[] | NullUndefined,
cb: AxisStatEachSeriesCb
): void {
if (!seriesList) {
return;
}
for (let i = 0; i < seriesList.length; i++) {
const seriesModel = seriesList[i];
// Legend-filtered series need to be ignored since series are registered before `legendFilter`.
if (!ecModel.isSeriesFiltered(seriesModel)) {
cb(seriesModel);
}
}
}
/**
* NOTE:
* - series filtered out are excluded.
*/
export function countSeriesOnAxisOnKey(
axis: Axis,
axisStatKey: AxisStatKey,
): number {
if (__DEV__) {
assert(axisStatKey != null);
validateInputAxis(axis);
}
const perKeyPerAxis = getAxisStatPerKeyPerAxis(axis, axisStatKey);
if (!perKeyPerAxis || !perKeyPerAxis.sers.length) {
return 0;
}
let count = 0;
eachSeriesDealForAxisStat(axis.model.ecModel, perKeyPerAxis.sers, function () {
count++;
});
return count;
}
/**
* NOTICE: Available after `CoordinateSystem['create']` (not included).
*
* Query all axes that have at least one associated series (via `associateSeriesWithAxis`)
* by the given key.
*/
export function eachAxisOnKey(
ecModel: GlobalModel,
axisStatKey: AxisStatKey,
cb: (axis: Axis) => void
): void {
if (__DEV__) {
assert(axisStatKey != null);
}
const keyed = ecModelCacheFullUpdateInner(getCachePerECFullUpdate(ecModel)).keyed;
const perKey = keyed && keyed.get(axisStatKey);
perKey && perKey.each(function (perKeyPerAxis) {
if (__DEV__) {
assert(perKeyPerAxis.sers.length > 0); // This is to avoid irrelevant axes to enter `cb`.
}
cb(perKeyPerAxis.axis);
});
}
/**
* NOTICE: Available after `CoordinateSystem['create']` (not included).
*
* Query all `AxisStatKey`s that have at least one associated series (via `associateSeriesWithAxis`)
* by the given axis.
*/
export function eachKeyOnAxis(
axis: Axis,
cb: (axisStatKey: AxisStatKey) => void
): void {
if (__DEV__) {
validateInputAxis(axis);
}
const model = axis.model;
const keysByAxisModelUid = ecModelCacheFullUpdateInner(getCachePerECFullUpdate(model.ecModel)).keys;
keysByAxisModelUid && each(keysByAxisModelUid.get(model.uid), function (axisStatKey) {
if (__DEV__) {
const stat = getAxisStatPerKeyPerAxis(axis, axisStatKey);
assert(stat && stat.sers.length > 0); // This is to avoid irrelevant `AxisStatKey` to enter `cb`.
}
cb(axisStatKey);
});
}
/**
* NOTICE: this processor may be omitted - it is registered only if required.
*/
function performAxisStatisticsOnOverallReset(ecModel: GlobalModel): void {
const ecPrepareCache = ecModelCachePrepareInner(getCachePerECPrepare(ecModel));
const ecPrepareCacheKeyed = ecPrepareCache.keyed || (ecPrepareCache.keyed = createHashMap());
eachKeyEachAxis(ecModel, function (perKeyPerAxis, axisStatKey, axisModelUid) {
const ecPrepareCachePerKey = ecPrepareCacheKeyed.get(axisStatKey)
|| ecPrepareCacheKeyed.set(axisStatKey, createHashMap());
const ecPreparePerKeyPerAxis = ecPrepareCachePerKey.get(axisModelUid)
|| ecPrepareCachePerKey.set(axisModelUid, {});
if (perKeyPerAxis.metrics.liPosMinGap) {
// We should assert the impl exists -- fail-fast if missing `registerMetricImpl`.
_metricImpl.liPosMinGap(ecModel, perKeyPerAxis, ecPreparePerKeyPerAxis);
}
});
}
// To reduce code size from unnecessary metrics.
export function registerMetricImpl(metricType: keyof AxisStatMetrics, impl: AxisStateMetricImpl): void {
_metricImpl[metricType] = impl;
}
const _metricImpl: Partial<Record<keyof AxisStatMetrics, AxisStateMetricImpl>> = {};
type AxisStateMetricImpl = (
ecModel: GlobalModel,
perKeyPerAxis: AxisStatPerKeyPerAxis,
ecPreparePerKeyPerAxis: AxisStatECPrepareCachePerKeyPerAxis
) => void;
/**
* NOTICE:
* - It must be called in `CoordinateSystem['create']`, before series filtering.
* - It must be called in `seriesIndex` ascending order (series declaration order).
* i.e., iterated by `ecModel.eachSeries`.
* - Every <axis, series> pair can only call this method once.
*
* @see scaleRawExtentInfoCreate in `scaleRawExtentInfo.ts`
*/
export function associateSeriesWithAxis(
axis: Axis | NullUndefined,
seriesModel: SeriesModel,
coordSysType: CoordinateSystem['type']
): void {
if (!axis) {
return;
}
const ecModel = seriesModel.ecModel;
const ecFullUpdateCache = ecModelCacheFullUpdateInner(getCachePerECFullUpdate(ecModel));
const axisModelUid = axis.model.uid;
if (__DEV__) {
validateInputAxis(axis);
// - An axis can be associated with multiple `axisStatKey`s. For example, if `axisStatKey`s are
// "candlestick" and "bar", they can be associated with the same "xAxis".
// - Within an individual axis, it is a typically incorrect usage if a <axis, series> pair is
// associated with multiple `perKeyPerAxis`, which may cause repeated calculation and
// performance degradation, had hard to be found without the checking below. For example, If
// `axisStatKey` are "grid-bar" (see `barGrid.ts`) and "polar-bar" (see `barPolar.ts`), and
// a <xAxis-series> pair is wrongly associated with both "polar-bar" and "grid-bar", the
// relevant statistics will be computed twice.
const axSerPairCheck = ecFullUpdateCache.axSerPairCheck
|| (ecFullUpdateCache.axSerPairCheck = createHashMap());
const pairKey = `${axisModelUid}${AXIS_STAT_KEY_DELIMITER}${seriesModel.uid}`;
assert(!axSerPairCheck.get(pairKey));
axSerPairCheck.set(pairKey, 1);
}
const seriesOnAxisMap = ecFullUpdateCache.axSer || (ecFullUpdateCache.axSer = createHashMap());
const seriesListPerAxis = seriesOnAxisMap.get(axisModelUid) || (seriesOnAxisMap.set(axisModelUid, []));
if (__DEV__) {
const lastSeries = seriesListPerAxis[seriesListPerAxis.length - 1];
if (lastSeries) {
// Series order should respect to the input order, since it matters in some cases
// (e.g., see `barGrid.ts` and `barPolar.ts` - ec option declaration order matters).
assert(lastSeries.seriesIndex < seriesModel.seriesIndex);
}
}
seriesListPerAxis.push(seriesModel);
const seriesType = seriesModel.subType;
const isBaseAxis = seriesModel.getBaseAxis() === axis;
const client = clientsForLookup.get(makeClientLookupKey(seriesType, isBaseAxis, coordSysType))
|| clientsForLookup.get(makeClientLookupKey(seriesType, isBaseAxis, null));
if (!client) {
return;
}
const keyed: AxisStatKeyed = ecFullUpdateCache.keyed || (ecFullUpdateCache.keyed = createHashMap());
const keys: AxisStatKeys = ecFullUpdateCache.keys || (ecFullUpdateCache.keys = createHashMap());
const axisStatKey = client.key;
const perKey = keyed.get(axisStatKey) || keyed.set(axisStatKey, createHashMap());
let perKeyPerAxis = perKey.get(axisModelUid);
if (!perKeyPerAxis) {
perKeyPerAxis = perKey.set(axisModelUid, {axis, sers: [], serByIdx: []});
// They should only be executed for each <key, axis> pair once:
perKeyPerAxis.metrics = client.getMetrics(axis);
(keys.get(axisModelUid) || keys.set(axisModelUid, []))
.push(axisStatKey);
}
// series order should respect to the input order.
perKeyPerAxis.sers.push(seriesModel);
perKeyPerAxis.serByIdx[seriesModel.seriesIndex] = seriesModel;
}
/**
* NOTE: Currently, the scenario is simple enough to look up clients by hash map.
* Otherwise, a caller-provided `filter` may be an alternative if more complex requirements arise.
*/
function makeClientLookupKey(
seriesType: ComponentSubType,
isBaseAxis: boolean | NullUndefined,
coordSysType: CoordinateSystem['type'] | NullUndefined
): ClientLookupKey {
return (
seriesType
+ AXIS_STAT_KEY_DELIMITER + retrieve2(isBaseAxis, true)
+ AXIS_STAT_KEY_DELIMITER + (coordSysType || '')
) as ClientLookupKey;
}
/**
* NOTICE: Can only be called in "install" stage.
*
* See `axisSnippets.ts` for some commonly used clients.
*/
export function requireAxisStatistics(
registers: EChartsExtensionInstallRegisters,
client: AxisStatKeyedClient
): void {
const clientKey = makeClientLookupKey(client.seriesType, client.baseAxis, client.coordSysType);
if (__DEV__) {
assert(client.seriesType
&& client.key
&& !clientsForCheckingStatKey.get(client.key)
&& !clientsForLookup.get(clientKey)
); // More checking is performed in `axSerPairCheck`.
clientsForCheckingStatKey.set(client.key, 1);
}
clientsForLookup.set(clientKey, client);
callOnlyOnce(registers, function () {
registers.registerProcessor(registers.PRIORITY.PROCESSOR.AXIS_STATISTICS, {
// NOTE: Theoretically, `appendData` requires `dirtyOnOverallProgress: true` here to re-calculate them.
// But this OVERALL_STAGE_TASK is applied to all series (no `getTargetSeries` specified),
// `dirtyOnOverallProgress: true` can cause irrelevant series (e.g., series on geo)
// to be re-rendered when `appendData` is called, which cause `appendData` meaningless,
// thereby not setting `dirtyOnOverallProgress: true`.
overallReset: performAxisStatisticsOnOverallReset
});
});
}
let clientsForCheckingStatKey: HashMap<1, AxisStatKey>;
if (__DEV__) {
clientsForCheckingStatKey = createHashMap();
}
const clientsForLookup: HashMap<AxisStatKeyedClient, ClientLookupKey> = createHashMap();