/
githubmirror
/
echarts
Обзор
Документация
Войти
/
githubmirror
/
echarts
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/coord/axisStatistics.ts
501 строка
19 KB
100pah
fix regression: Fix appendData broken by previous commits.
09 май 2026, 20:40
09 май 2026, 20:40
be4cc54
Код
Авторство
О чём код?
/* * 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();