/
githubmirror
/
echarts
Обзор
Документация
Войти
/
githubmirror
/
echarts
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/scale/Ordinal.ts
338 строк
11 KB
100pah
fix: Simplify and clarify the code about axis ticks regarding ordinal scale sorting. Fix axis label and sorting bug introduced by preceding commits.
28 мар 2026, 21:57
28 мар 2026, 21:57
8cd5c80
Код
Авторство
О чём код?
/* * 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. */ /** * Linear continuous scale * http://en.wikipedia.org/wiki/Level_of_measurement */ import Scale from './Scale'; import OrdinalMeta from '../data/OrdinalMeta'; import { OrdinalRawValue, OrdinalNumber, OrdinalSortInfo, ScaleTick, } from '../util/types'; import { CategoryAxisBaseOption } from '../coord/axisCommonTypes'; import { isArray, map, isObject, isString } from 'zrender/src/core/util'; import { mathMin, mathRound } from '../util/number'; import { DecoratedScaleMapperMethods, decorateScaleMapper, enableScaleMapperFreeze, getScaleExtentForTickUnsafe, initBreakOrLinearMapper, ScaleMapper, ScaleMapperGeneric } from './scaleMapper'; import { ordinalScaleCreateTicks } from './helper'; type OrdinalScaleSetting = { ordinalMeta?: OrdinalMeta | CategoryAxisBaseOption['data']; extent?: number[]; }; /** * @final NEVER inherit me! */ interface OrdinalScale extends ScaleMapperGeneric<OrdinalScale> { _mapper: ScaleMapper; } class OrdinalScale extends Scale<OrdinalScale> { static type = 'ordinal'; readonly type = 'ordinal' as const; private _ordinalMeta: OrdinalMeta; /** * For example: * Given original ordinal data: * ```js * option = { * xAxis: { * // Their raw ordinal numbers are: * // 0 1 2 3 4 5 * data: ['a', 'b', 'c', 'd', 'e', 'f'] * }, * yAxis: {} * series: { * type: 'bar', * data: [ * ['d', 110], // ordinalNumber: 3 * ['c', 660], // ordinalNumber: 2 * ['f', 220], // ordinalNumber: 5 * ['e', 550] // ordinalNumber: 4 * ], * realtimeSort: true * } * }; * ``` * After realtime sorted (order by yValue desc): * ```js * _ordinalNumbersByTick: [ * 2, // tick: 0, yValue: 660 * 5, // tick: 1, yValue: 220 * 3, // tick: 2, yValue: 110 * 4, // tick: 3, yValue: 550 * 0, // tick: 4, yValue: - * 1, // tick: 5, yValue: - * ], * _ticksByOrdinalNumber: [ * 4, // ordinalNumber: 0, yValue: - * 5, // ordinalNumber: 1, yValue: - * 0, // ordinalNumber: 2, yValue: 660 * 2, // ordinalNumber: 3, yValue: 110 * 3, // ordinalNumber: 4, yValue: 550 * 1, // ordinalNumber: 5, yValue: 220 * ] * ``` * NOTICE: * - The index of `_ordinalNumbersByTick` is "tick number", i.e., `tick.value`, * rather than the index of `scale.getTicks()`. They are not the same when * `_extent[0]` is delibrately set to be not zero, or `axisTick/axisLabel.interval` > 0. * - Currently we only support that the index of `_ordinalNumbersByTick` is * from `0` to `ordinalMeta.categories.length - 1`. * - `OrdinalNumber` is always from `0` to `ordinalMeta.categories.length - 1`. * * @see `Ordinal['getRawOrdinalNumber']` * @see `OrdinalSortInfo` */ private _ordinalNumbersByTick: OrdinalNumber[]; /** * This is the inverted map of `_ordinalNumbersByTick`. * The index is `OrdinalNumber`, which is from `0` to `ordinalMeta.categories.length - 1`. * after `_ticksByOrdinalNumber` is initialized. * * @see `Ordinal['_ordinalNumbersByTick']` * @see `Ordinal['_getTickNumber']` * @see `OrdinalSortInfo` */ private _ticksByOrdinalNumber: number[]; constructor(setting: OrdinalScaleSetting) { super(); this.parse = OrdinalScale.parse; decorateScaleMapper(this, OrdinalScale.decoratedMethods); let ordinalMeta = setting.ordinalMeta; // Caution: Should not use instanceof, consider ec-extensions using // import approach to get OrdinalMeta class. if (!ordinalMeta) { ordinalMeta = new OrdinalMeta({}); } if (isArray(ordinalMeta)) { ordinalMeta = new OrdinalMeta({ categories: map(ordinalMeta, item => (isObject(item) ? item.value : item)) }); } this._ordinalMeta = ordinalMeta as OrdinalMeta; // Create an interval LinearScaleMapper, and decorate it. const res = initBreakOrLinearMapper( null, null, // Do not support break in OrdinalScale yet. setting.extent || [0, ordinalMeta.categories.length - 1] ); this._mapper = res.mapper; enableScaleMapperFreeze(this, res.mapper); } private static parse(this: OrdinalScale, val: OrdinalRawValue | OrdinalNumber): OrdinalNumber { // Caution: Math.round(null) will return `0` rather than `NaN` if (val == null) { val = NaN; } else if (isString(val)) { val = this._ordinalMeta.getOrdinal(val); if (val == null) { val = NaN; } } else { // The val from user input might be float. val = mathRound(val); } return val; } static decoratedMethods: DecoratedScaleMapperMethods<OrdinalScale> = { needTransform() { return this._mapper.needTransform(); }, contain(this: OrdinalScale, val: OrdinalNumber): boolean { return this._mapper.contain(this._getTickNumber(val)) && val >= 0 && val < this._ordinalMeta.categories.length; }, normalize(this: OrdinalScale, val: OrdinalNumber): number { return this._mapper.normalize(this._getTickNumber(val)); }, scale(this: OrdinalScale, val: number): OrdinalNumber { return this.getRawOrdinalNumber(mathRound(this._mapper.scale(val))); }, transformIn(val, opt) { return this._mapper.transformIn(this._getTickNumber(val), opt); }, transformOut(val, opt) { return this.getRawOrdinalNumber(this._mapper.transformOut(val, opt)); }, getExtent() { return this._mapper.getExtent(); }, getExtentUnsafe(kind, depth) { return this._mapper.getExtentUnsafe(kind, depth); }, /** * NOTICE: OrdinalScale extent should always originates from * `[0, ordinalMeta.categories.length - 1]`, regardless of min/max of `series.data`. * But settings like `xxxAxis.min/max` can still modify the extent. * It is handled by constructor of `ScaleRawExtentInfo`. */ setExtent(start, end) { return this._mapper.setExtent(start, end); }, setExtent2(kind, start, end) { return this._mapper.setExtent2(kind, start, end); }, }; /** * PENDING: currently this method is not used. * `makeCategoryTicks` is effectively used. */ getTicks(): ScaleTick[] { const ticks: ScaleTick[] = []; ordinalScaleCreateTicks(this, 0, function (tick) { ticks.push(tick); }); return ticks; } getMinorTicks(splitNumber: number): number[][] { // Not support. return; } /** * @see `Ordinal['_ordinalNumbersByTick']` */ setSortInfo(info: OrdinalSortInfo): void { if (info == null) { this._ordinalNumbersByTick = this._ticksByOrdinalNumber = null; return; } const infoOrdinalNumbers = info.ordinalNumbers; const ordinalsByTick = this._ordinalNumbersByTick = [] as OrdinalNumber[]; const ticksByOrdinal = this._ticksByOrdinalNumber = [] as number[]; // Unnecessary support negative tick in `realtimeSort`. let tickNum = 0; const allCategoryLen = this._ordinalMeta.categories.length; for (const len = mathMin(allCategoryLen, infoOrdinalNumbers.length); tickNum < len; ++tickNum) { const ordinalNumber = ordinalsByTick[tickNum] = infoOrdinalNumbers[tickNum]; ticksByOrdinal[ordinalNumber] = tickNum; } // Handle that `series.data` only covers part of the `axis.category.data`. let unusedOrdinal = 0; for (; tickNum < allCategoryLen; ++tickNum) { while (ticksByOrdinal[unusedOrdinal] != null) { unusedOrdinal++; }; ordinalsByTick[tickNum] = unusedOrdinal; ticksByOrdinal[unusedOrdinal] = tickNum; } } private _getTickNumber(ordinal: OrdinalNumber): number { const ticksByOrdinalNumber = this._ticksByOrdinalNumber; // also support ordinal out of range of `ordinalMeta.categories.length`, // where ordinal numbers are used as tick value directly. return (ticksByOrdinalNumber && ordinal >= 0 && ordinal < ticksByOrdinalNumber.length) ? ticksByOrdinalNumber[ordinal] : ordinal; } /** * @usage * ```js * const ordinalNumber = ordinalScale.getRawOrdinalNumber(tick.value); * // case0 * const rawOrdinalValue = axisModel.getCategories()[ordinalNumber]; * // case1 * const rawOrdinalValue = this._ordinalMeta.categories[ordinalNumber]; * // case2 * const coord = axis.dataToCoord(ordinalNumber); * ``` * * value may be out of range, e.g., when axis max is larger than `ordinalMeta.categories.length`, * where ordinal numbers are used as tick value directly. */ getRawOrdinalNumber(tickValue: ScaleTick['value']): OrdinalNumber { const ordinalNumbersByTick = this._ordinalNumbersByTick; return (ordinalNumbersByTick && tickValue >= 0 && tickValue < ordinalNumbersByTick.length) ? ordinalNumbersByTick[tickValue] : tickValue; } /** * Get item on tick */ getLabel(tick: ScaleTick): string { if (!this.isBlank()) { const ordinalNumber = this.getRawOrdinalNumber(tick.value); const category = this._ordinalMeta.categories[ordinalNumber]; // Note that if no data, ordinalMeta.categories is an empty array. // Return empty if it's not exist. return category == null ? '' : category + ''; } } /** * NOTICE: This is different from `.getOrdinalMeta().length` when extent * is specified by `xxxAxis.min/max` or by `dataZoom`. */ count(): number { const extent = getScaleExtentForTickUnsafe(this._mapper); return extent[1] - extent[0] + 1; } getOrdinalMeta(): OrdinalMeta { return this._ordinalMeta; } } Scale.registerClass(OrdinalScale); export default OrdinalScale;