/
githubmirror
/
echarts
Обзор
Документация
Войти
/
githubmirror
/
echarts
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/scale/helper.ts
339 строк
11 KB
100pah
test: fix UT.
11 апр 2026, 16:50
11 апр 2026, 16:50
e548306
Код
Авторство
О чём код?
/* * 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 { getPrecision, round, nice, quantityExponent, mathPow, mathMax, mathRound, mathLog, mathAbs, mathFloor, mathCeil } from '../util/number'; import type IntervalScale from './Interval'; import type LogScale from './Log'; import type Scale from './Scale'; import type TimeScale from './Time'; import { NullUndefined, ScaleTick } from '../util/types'; import type OrdinalScale from './Ordinal'; import { ScaleExtentFixMinMax, ScaleRawExtentResultFinal } from '../coord/scaleRawExtentInfo'; import { isValidNumberForExtent } from '../util/model'; import { getScaleExtentForTickUnsafe } from './scaleMapper'; type intervalScaleNiceTicksResult = { interval: number, intervalPrecision: number, niceTickExtent: [number, number] }; export type IntervalScaleGetLabelOpt = { // If 'auto', use nice precision. precision?: 'auto' | number, // `true`: returns 1.50 but not 1.5 if precision is 2. pad?: boolean }; /** * See also method `nice` in `src/util/number.ts`. */ // export function isValueNice(val: number) { // const exp10 = Math.pow(10, quantityExponent(Math.abs(val))); // const f = Math.abs(round(val / exp10, 0)); // return f === 0 // || f === 1 // || f === 2 // || f === 3 // || f === 5; // } export function isIntervalOrLogScale(scale: Scale): scale is (LogScale | IntervalScale) { return isIntervalScale(scale) || isLogScale(scale); } export function isIntervalOrTimeScale(scale: Scale): scale is (IntervalScale | TimeScale) { return isIntervalScale(scale) || isTimeScale(scale); } export function isIntervalScale(scale: Scale): scale is IntervalScale { return scale.type === 'interval'; } export function isTimeScale(scale: Scale): scale is TimeScale { return scale.type === 'time'; } export function isLogScale(scale: Scale): scale is LogScale { return scale.type === 'log'; } export function isOrdinalScale(scale: Scale): scale is OrdinalScale { return scale.type === 'ordinal'; } /** * @param extent Both extent[0] and extent[1] should be valid number. * Should be extent[0] < extent[1]. * @param splitNumber splitNumber should be >= 1. */ export function intervalScaleNiceTicks( extent: number[], spanWithBreaks: number, splitNumber: number, minInterval?: number, maxInterval?: number ): intervalScaleNiceTicksResult { const result = {} as intervalScaleNiceTicksResult; let interval = result.interval = nice(spanWithBreaks / splitNumber, true); if (minInterval != null && interval < minInterval) { interval = result.interval = minInterval; } if (maxInterval != null && interval > maxInterval) { interval = result.interval = maxInterval; } const precision = result.intervalPrecision = getIntervalPrecision(interval); // Niced extent inside original extent result.niceTickExtent = [ round(mathCeil(extent[0] / interval) * interval, precision), round(mathFloor(extent[1] / interval) * interval, precision) ]; return result; } /** * The input `niceInterval` should be generated * from `nice` method in `src/util/number.ts`, or * from `increaseInterval` itself. */ export function increaseInterval(niceInterval: number) { const exponent = quantityExponent(niceInterval); // No rounding error in Math.pow(10, integer). const exp10 = mathPow(10, exponent); // Fix IEEE 754 float rounding error let f = mathRound(niceInterval / exp10); if (!f) { f = 1; } else if (f === 2) { f = 3; } else if (f === 3) { f = 5; } else { // f is 1 or 5 f *= 2; } // Fix IEEE 754 float rounding error return round(f * exp10, -exponent); } export function getIntervalPrecision(niceInterval: number): number { // Tow more digital for tick. // NOTE: `2` was introduced in commit `af2a2a9f6303081d7c3b52f0a38add07b4c6e0c7`; // it works on "nice" interval, but seems not necessarily mathematically required. return getPrecision(niceInterval) + 2; } /** * Lookup table to avoid rounding error - if the value before transformed is in `lookup.from[i]`, * return `lookup.to[i]` directly without transform. * Rounding errors typically arise in logarithm transform, which can cause the tick to be displayed * like `5.999999999999999` when it is expected to be `6`. */ export type ValueTransformLookupOpt = { lookup?: { from: number[]; to: number[]; } | NullUndefined; }; /** * NOTE: * - If `val` is `NaN`, return `NaN`. * - If `val` is `0`, return `-Infinity`. * - If `val` is negative, return `NaN`. * * @see {DataStore#getDataExtent} It handles non-positive values for logarithm scale. */ export function logScaleLogTick( val: number, base: number, ): number { // NOTE: // - rounding error may happen above, typically expecting `log10(1000)` but actually // getting `2.9999999999999996`, but generally it does not matter since they are not // used to display. // - Consider backward compatibility and other log bases, do not use `Math.log10`. return mathLog(val) / mathLog(base); } /** * Cumulative rounding errors cause the logarithm operation to become non-invertible by simply exponentiation. * - `Math.pow(10, integer)` itself has no rounding error. But, * - If `linearTickVal` is generated internally by `calcNiceTicks`, it may be still "not nice" (not an integer) * when it is `extent[i]`. * - If `linearTickVal` is generated outside (e.g., by `scaleCalcAlign`) and set by `setExtent`, * `logScaleLogTick` may already have introduced rounding errors even for "nice" values. * But invertible is required when the original `extent[i]` need to be respected, or "nice" ticks need to be * displayed instead of something like `5.999999999999999`, which is addressed in this function. * See also `#4158`. * * [CAUTION]: * Monotonicity may be broken on extent ends - callers must make sure it does not matter. */ export function logScalePowTick( // `tickVal` should be in the linear space. linearTickVal: number, base: number, opt: ValueTransformLookupOpt | NullUndefined ): number { const lookup = opt && opt.lookup; if (lookup) { for (let i = 0; i < lookup.from.length; i++) { if (linearTickVal === lookup.from[i]) { return lookup.to[i]; } } } return mathPow(base, linearTickVal); } /** * For `IntervalScale`, convert `rawExtent` to: * - Be no non-finite number. * - Be `extent[0] < extent[1]`- no equal; otherwise, additional handling is required * in "nice" and "align" ticks. */ export function intervalScaleEnsureValidExtent( rawExtent: number[], fixMinMax: ScaleExtentFixMinMax, rawExtentResult?: ScaleRawExtentResultFinal | NullUndefined ): number[] { const extent = rawExtent.slice(); // PENDING: // This implementation is not rigorous, but has long been in use. // If extent start and end are same, expand them if (extent[0] === extent[1]) { // If `containShape`, the extent must be evenly distributed to both sides; // otherwise, shape (e.g., bars) may be overflow and clipped. const containShapeRequired = rawExtentResult && rawExtentResult.ctnShp; if (extent[0] !== 0) { // Expand extent // Note that extents can be both negative. See #13154 const expandSize = mathAbs(extent[0]); // In the fowllowing case // Axis has been fixed max 100 // Plus data are all 100 and axis extent are [100, 100]. // Extend to the both side will cause expanded max is larger than fixed max. // So only expand to the smaller side. if (!fixMinMax[1]) { extent[1] += expandSize / 2; extent[0] -= expandSize / 2; } else { extent[0] -= expandSize / 2; } } else { if (containShapeRequired) { extent[0] = -1; extent[1] = 1; } else { extent[1] = 1; } } } // For example, if there are no series data, extent may be `[Infinity, -Infinity]` here. if (!isValidNumberForExtent(extent[0]) || !isValidNumberForExtent(extent[1])) { extent[0] = 0; extent[1] = 1; } if (extent[1] < extent[0]) { extent.reverse(); } return extent; } export function extentDiffers(extent1: number[], extent2: number[]): boolean[] { return [extent1[0] !== extent2[0], extent1[1] !== extent2[1]]; } export function ensureValidSplitNumber( rawSplitNumber: number | NullUndefined, defaultSplitNumber: number ): number { rawSplitNumber = rawSplitNumber || defaultSplitNumber; return mathRound(mathMax(rawSplitNumber, 1)); } /** * NOTE: The result can have only one item, e.g., when `extent[0] === extent[1]` * and `categoryInterval === 0`. */ export function ordinalScaleCreateTicks( ordinalScale: OrdinalScale, // `categoryInterval` is the number part of `CategoryTickLabelSplitIntervalOption`. categoryInterval: number, addItem: ( tick: ScaleTick, isExtentBoundary: boolean ) => void, ) { const extent = getScaleExtentForTickUnsafe(ordinalScale); let startTick = extent[0]; const tickCount = ordinalScale.count(); const step = Math.max((categoryInterval || 0) + 1, 1); // Calculate start tick based on zero if possible to keep label consistent // while zooming and moving while interval > 0. Otherwise the selection // of displayable ticks and symbols probably keep changing. if (startTick !== 0 && step > 1 && tickCount / step > 2) { startTick = Math.round(Math.ceil(startTick / step) * step); } // min max labels may be excluded if `startTick > 0`, but they should be always // included and the label display strategy is adopted uniformly later in `AxisBuilder`. if (startTick !== extent[0]) { addItemInternally(extent[0], true, true); } let tickValue = startTick; for (; tickValue <= extent[1]; tickValue += step) { addItemInternally(tickValue, false, tickValue === extent[0] || tickValue === extent[1]); } if (tickValue - step !== extent[1]) { addItemInternally(extent[1], true, true); } function addItemInternally(tickValue: number, offInterval: boolean, isExtentBoundary: boolean): void { addItem( { value: tickValue, offInterval, }, isExtentBoundary ); } }