This is an automated email from the ASF dual-hosted git repository. sushuang pushed a commit to branch PR/plainheart_fix/alignTicks-precision in repository https://gitbox.apache.org/repos/asf/echarts.git
commit a6ab2458f32ff0b2cf657f72c58f3267236d75eb Author: 100pah <[email protected]> AuthorDate: Wed Jan 14 16:24:13 2026 +0800 feat(alignTicks): (1) Fix LogScale precision. (2) Tweak align ticks layout. (3) Remove unreasonable clamp in Interval calcNiceExtent, and clarify the definition of `_niceExtent`. --- src/coord/axisAlignTicks.ts | 200 +++++++++++++++++++++++++--------------- src/coord/axisHelper.ts | 6 +- src/coord/scaleRawExtentInfo.ts | 14 ++- src/scale/Interval.ts | 94 +++++++++++-------- src/scale/Log.ts | 128 ++++++++++--------------- src/scale/Scale.ts | 8 +- src/scale/Time.ts | 2 - src/scale/break.ts | 9 +- src/scale/breakImpl.ts | 86 ++++++++--------- src/scale/helper.ts | 128 +++++++++++++++++-------- test/axis-align-edge-cases.html | 97 ++++++++++++++++++- test/axis-align-ticks.html | 5 +- 12 files changed, 479 insertions(+), 298 deletions(-) diff --git a/src/coord/axisAlignTicks.ts b/src/coord/axisAlignTicks.ts index 4c4a6f8ac..f392c74eb 100644 --- a/src/coord/axisAlignTicks.ts +++ b/src/coord/axisAlignTicks.ts @@ -19,7 +19,8 @@ import { getAcceptableTickPrecision, - mathAbs, mathCeil, mathFloor, mathMax, nice, NICE_MODE_MIN, round + getPrecision, + mathAbs, mathCeil, mathFloor, mathMax, mathRound, nice, NICE_MODE_MIN, quantity, round } from '../util/number'; import IntervalScale from '../scale/Interval'; import { adoptScaleExtentOptionAndPrepare } from './axisHelper'; @@ -28,7 +29,7 @@ import LogScale from '../scale/Log'; import { warn } from '../util/log'; import { increaseInterval, isLogScale, getIntervalPrecision, intervalScaleEnsureValidExtent, - logTransform, + logScaleLogTickPair, } from '../scale/helper'; import { assert } from 'zrender/src/core/util'; import { NullUndefined } from '../util/types'; @@ -42,7 +43,9 @@ export function alignScaleTicks( ): void { const isTargetLogScale = isLogScale(targetScale); const alignToScaleLinear = isLogScale(alignToScale) ? alignToScale.linearStub : alignToScale; + const targetScaleLinear = isTargetLogScale ? targetScale.linearStub : targetScale; + const targetLogScaleBase = (targetScale as LogScale).base; const alignToTicks = alignToScaleLinear.getTicks(); const alignToExpNiceTicks = alignToScaleLinear.getTicks({expandToNicedExtent: true}); const alignToSegCount = alignToTicks.length - 1; @@ -67,16 +70,16 @@ export function alignScaleTicks( // matching exactly to ticks of `alignTo` scale. // Adjust min, max based on the extent of alignTo. When min or max is set in alignTo scale - let t0: number; // diff ratio on min irregular segment. 0 <= t0 < 1 - let t1: number; // diff ratio on max irregular segment. 0 <= t1 < 1 - let alignToRegularSegCount: number; // >= 1 + let t0: number; // diff ratio on min not-nice segment. 0 <= t0 < 1 + let t1: number; // diff ratio on max not-nice segment. 0 <= t1 < 1 + let alignToNiceSegCount: number; // >= 1 // Consider ticks of `alignTo`, only these cases below may occur: if (alignToSegCount === 1) { // `alignToTicks` is like: // |--| // In this case, we make the corresponding 2 target ticks "nice". t0 = t1 = 0; - alignToRegularSegCount = 1; + alignToNiceSegCount = 1; } else if (alignToSegCount === 2) { // `alignToTicks` is like: @@ -84,16 +87,16 @@ export function alignScaleTicks( // |-----|-| or // |-----|-----| // Notices that nice ticks do not necessarily exist in this case. - // In this case, we choose the larger segment as the "regular segment" and + // In this case, we choose the larger segment as the "nice segment" and // the corresponding target ticks are made "nice". const interval0 = mathAbs(alignToTicks[0].value - alignToTicks[1].value); const interval1 = mathAbs(alignToTicks[1].value - alignToTicks[2].value); t0 = t1 = 0; if (interval0 === interval1) { - alignToRegularSegCount = 2; + alignToNiceSegCount = 2; } else { - alignToRegularSegCount = 1; + alignToNiceSegCount = 1; if (interval0 < interval1) { t0 = interval0 / interval1; } @@ -107,9 +110,9 @@ export function alignScaleTicks( // |-|-----|-----|-| or // |-----|-----|-| or // |-|-----|-----| or ... - // At least one regular segment is present, and irregular segments are only present on + // At least one nice segment is present, and not-nice segments are only present on // the start and/or the end. - // In this case, ticks corresponding to regular segments are made "nice". + // In this case, ticks corresponding to nice segments are made "nice". const alignToInterval = alignToScaleLinear.getInterval(); t0 = ( 1 - (alignToTicks[0].value - alignToExpNiceTicks[0].value) / alignToInterval @@ -117,28 +120,36 @@ export function alignScaleTicks( t1 = ( 1 - (alignToExpNiceTicks[alignToSegCount].value - alignToTicks[alignToSegCount].value) / alignToInterval ) % 1; - alignToRegularSegCount = alignToSegCount - (t0 ? 1 : 0) - (t1 ? 1 : 0); + alignToNiceSegCount = alignToSegCount - (t0 ? 1 : 0) - (t1 ? 1 : 0); } if (__DEV__) { - assert(alignToRegularSegCount >= 1); + assert(alignToNiceSegCount >= 1); } const targetExtentInfo = adoptScaleExtentOptionAndPrepare(targetScale, targetAxisModel, targetDataExtent); - // NOTE: If `dataZoom` has either start/end not 0% or 100% (indicated by `min/maxDetermined`), we consider - // both min and max fixed; otherwise the result is probably unexpected if we expand the extent out of - // the original min/max, e.g., the expanded extent may cross zero. + // NOTE: + // Consider a case: + // dataZoom controls all Y axes; + // dataZoom end is 90% (maxFixed: true, maxDetermined: true); + // but dataZoom start is 0% (minFixed: false, minDetermined: false); + // In this case, + // `Interval#calcNiceTicks` only uses `targetExtentInfo.max` as the upper bound but may expand the + // lower bound to a "nice" tick and can get an acceptable result. + // But `alignScaleTicks` has to use both `targetExtentInfo.min/max` as the bounds without any expansion, + // otherwise the lower bound may become negative unexpectedly for all positive series data. const hasMinMaxDetermined = targetExtentInfo.minDetermined || targetExtentInfo.maxDetermined; - const targetMinFixed = targetExtentInfo.minFixed || hasMinMaxDetermined; - const targetMaxFixed = targetExtentInfo.maxFixed || hasMinMaxDetermined; - // MEMO: - // - When only `xxxAxis.min` or `xxxAxis.max` is fixed, even "nice" interval can be calculated, ticks - // accumulated based on `min`/`max` can be "nice" only if `min` or `max` is "nice". - // - Generating a "nice" interval in this case may cause the extent have both positive and negative ticks, - // which may be not preferable for all positive (very common) or all negative series data. But it can be - // simply resolved by specifying `xxxAxis.min: 0`/`xxxAxis.max: 0`, so we do not specially handle this - // case here. + const targetMinMaxFixed = [ + targetExtentInfo.minFixed || hasMinMaxDetermined, + targetExtentInfo.maxFixed || hasMinMaxDetermined + ]; + // MEMO: When only `xxxAxis.min` or `xxxAxis.max` is fixed, + // - Even a "nice" interval can be calculated, ticks accumulated based on `min`/`max` can be "nice" only if + // `min` or `max` is a "nice" number. + // - Generating a "nice" interval may cause the extent have both positive and negative ticks, which may be + // not preferable for all positive (very common) or all negative series data. But it can be simply resolved + // by specifying `xxxAxis.min: 0`/`xxxAxis.max: 0`, so we do not specially handle this case here. // Therefore, we prioritize generating "nice" interval over preventing from crossing zero. // e.g., if series data are all positive and the max data is `11739`, // If setting `yAxis.max: 'dataMax'`, ticks may be like: @@ -148,11 +159,15 @@ export function alignScaleTicks( // If setting `yAxis.max: 12000, yAxis.min: 0`, ticks may be like: // `12000, 9000, 6000, 3000, 0` ("nice") - let targetExtent = [targetExtentInfo.min, targetExtentInfo.max]; + let targetRawExtent = [targetExtentInfo.min, targetExtentInfo.max]; if (isTargetLogScale) { - targetExtent = logTransform(targetScale.base, targetExtent); + targetRawExtent = logScaleLogTickPair(targetRawExtent, targetLogScaleBase); } - targetExtent = intervalScaleEnsureValidExtent(targetExtent, {fixMax: targetMaxFixed}); + const targetExtent = intervalScaleEnsureValidExtent(targetRawExtent, targetMinMaxFixed); + const targetMinMaxChanged = [ + targetExtent[0] !== targetRawExtent[0], + targetExtent[1] !== targetRawExtent[1] + ]; let min: number; let max: number; @@ -172,9 +187,9 @@ export function alignScaleTicks( break; } interval = isTargetLogScale - // TODO: A guardcode to avoid infinite loop, but probably it - // should be guranteed by `LogScale` itself. - ? interval * mathMax(targetScale.base, 2) + // TODO: `mathMax(base, 2)` is a guardcode to avoid infinite loop, + // but probably it should be guranteed by `LogScale` itself. + ? interval * mathMax(targetLogScaleBase, 2) : increaseInterval(interval); intervalPrecision = getIntervalPrecision(interval); } @@ -185,11 +200,24 @@ export function alignScaleTicks( } } + function updateMinFromMinNice() { + min = round(minNice - interval * t0, intervalPrecision); + } + function updateMaxFromMaxNice() { + max = round(maxNice + interval * t1, intervalPrecision); + } + function updateMinNiceFromMinT0Interval() { + minNice = t0 ? round(min + interval * t0, intervalPrecision) : min; + } + function updateMaxNiceFromMaxT1Interval() { + maxNice = t1 ? round(max - interval * t1, intervalPrecision) : max; + } + // NOTE: The new calculated `min`/`max` must NOT shrink the original extent; otherwise some series // data may be outside of the extent. They can expand the original extent slightly to align with // ticks of `alignTo`. In this case, more blank space is added but visually fine. - if (targetMinFixed && targetMaxFixed) { + if (targetMinMaxFixed[0] && targetMinMaxFixed[1]) { // Both `min` and `max` are specified (via dataZoom or ec option; consider both Cartesian, radar and // other possible axes). In this case, "nice" ticks can hardly be calculated, but reasonable ticks should // still be calculated whenever possible, especially `intervalPrecision` should be tuned for better @@ -197,90 +225,114 @@ export function alignScaleTicks( min = targetExtent[0]; max = targetExtent[1]; - intervalCount = alignToRegularSegCount; - const rawInterval = (max - min) / (alignToRegularSegCount + t0 + t1); + intervalCount = alignToNiceSegCount; + interval = (max - min) / (alignToNiceSegCount + t0 + t1); // Typically axis pixel extent is ready here. See `create` in `Grid.ts`. const axisPxExtent = targetAxisModel.axis.getExtent(); // NOTICE: this pxSpan may be not accurate yet due to "outerBounds" logic, but acceptable so far. const pxSpan = mathAbs(axisPxExtent[1] - axisPxExtent[0]); - // We imperically choose `pxDiffAcceptable` as `0.5 / alignToRegularSegCount` for reduce cumulative + // We imperically choose `pxDiffAcceptable` as `0.5 / alignToNiceSegCount` for reduce cumulative // error, otherwise a discernible misalign (> 1px) may occur. // PENDING: We do not find a acceptable precision for LogScale here. // Theoretically it can be addressed but introduce more complexity. Is it necessary? - intervalPrecision = getAcceptableTickPrecision(max - min, pxSpan, 0.5 / alignToRegularSegCount); - interval = round(rawInterval, intervalPrecision); - maxNice = t1 ? round(max - rawInterval * t1, intervalPrecision) : max; - minNice = t0 ? round(min + rawInterval * t0, intervalPrecision) : min; + intervalPrecision = getAcceptableTickPrecision(max - min, pxSpan, 0.5 / alignToNiceSegCount); + updateMinNiceFromMinT0Interval(); + updateMaxNiceFromMaxT1Interval(); + interval = round(interval, intervalPrecision); } else { // Make a minimal enough `interval`, increase it later. // It is a similar logic as `IntervalScale#calcNiceTicks` and `LogScale#calcNiceTicks`. // Axis break is not supported, which is guranteed by the caller of this function. - interval = nice((targetExtent[1] - targetExtent[0]) / alignToRegularSegCount, NICE_MODE_MIN); + const targetSpan = targetExtent[1] - targetExtent[0]; + interval = isTargetLogScale + ? mathMax(quantity(targetSpan), 1) + : nice(targetSpan / alignToNiceSegCount, NICE_MODE_MIN); intervalPrecision = getIntervalPrecision(interval); - if (targetMinFixed) { + if (targetMinMaxFixed[0]) { min = targetExtent[0]; loopIncreaseInterval(function () { - minNice = t0 ? round(min + interval * t0, intervalPrecision) : min; - maxNice = round(minNice + interval * alignToRegularSegCount, intervalPrecision); - max = round(maxNice + interval * t1, intervalPrecision); + updateMinNiceFromMinT0Interval(); + maxNice = round(minNice + interval * alignToNiceSegCount, intervalPrecision); + updateMaxFromMaxNice(); if (max >= targetExtent[1]) { return true; } }); } - else if (targetMaxFixed) { + else if (targetMinMaxFixed[1]) { max = targetExtent[1]; loopIncreaseInterval(function () { - maxNice = t1 ? round(max - interval * t1, intervalPrecision) : max; - minNice = round(maxNice - interval * alignToRegularSegCount, intervalPrecision); - min = round(minNice - interval * t0, intervalPrecision); + updateMaxNiceFromMaxT1Interval(); + minNice = round(maxNice - interval * alignToNiceSegCount, intervalPrecision); + updateMinFromMinNice(); if (min <= targetExtent[0]) { return true; } }); } else { - // Currently we simply lay out ticks of the target scale to the "regular segments" of `alignTo` - // scale for "nice". If unexpected cases occur in future, the strategy can be tuned precisely - // (e.g., make use of irregular segments). loopIncreaseInterval(function () { - // Consider cases that all positive or all negative, try not to cross zero, which is - // preferable in most cases. - if (targetExtent[1] <= 0) { - maxNice = round(mathCeil(targetExtent[1] / interval) * interval, intervalPrecision); - minNice = round(maxNice - interval * alignToRegularSegCount, intervalPrecision); - if (minNice <= targetExtent[0]) { - return true; + minNice = round(mathCeil(targetExtent[0] / interval) * interval, intervalPrecision); + maxNice = round(mathFloor(targetExtent[1] / interval) * interval, intervalPrecision); + // NOTE: + // - `maxNice - minNice >= -interval` here. + // - While `interval` increases, `currIntervalCount` decreases, minimum `-1`. + const currIntervalCount = mathRound((maxNice - minNice) / interval); + if (currIntervalCount <= alignToNiceSegCount) { + const moreCount = alignToNiceSegCount - currIntervalCount; + // Consider cases that negative series data do not make sense (or vice versa), users can + // simply specify `xxxAxis.min/max: 0` to achieve that. But we still optimize it for some + // common default cases whenever possible, especially when ec option `xxxAxis.scale: false` + // (the default), it is usually unexpected if negative (or positive) ticks are introduced. + let moreCountPair: number[]; + const needCrossZero = targetExtentInfo.needCrossZero; + if (needCrossZero && targetExtent[0] === 0) { + // 0 has been included in extent and all positive. + moreCountPair = [0, moreCount]; } - } - else { - minNice = round(mathFloor(targetExtent[0] / interval) * interval, intervalPrecision); - maxNice = round(minNice + interval * alignToRegularSegCount, intervalPrecision); - if (maxNice >= targetExtent[1]) { + else if (needCrossZero && targetExtent[1] === 0) { + // 0 has been included in extent and all negative. + moreCountPair = [moreCount, 0]; + } + else { + // Try to arrange tick in the middle as possible corresponding to the given `alignTo` + // ticks, which is especially preferable in `LogScale`. + const lessHalfCount = mathFloor(moreCount / 2); + moreCountPair = moreCount % 2 === 0 ? [lessHalfCount, lessHalfCount] + : (min + max) < (targetExtent[0] + targetExtent[1]) ? [lessHalfCount, lessHalfCount + 1] + : [lessHalfCount + 1, lessHalfCount]; + } + minNice = round(minNice - interval * moreCountPair[0], intervalPrecision); + maxNice = round(maxNice + interval * moreCountPair[1], intervalPrecision); + updateMinFromMinNice(); + updateMaxFromMaxNice(); + if (min <= targetExtent[0] && max >= targetExtent[1]) { return true; } } }); - min = round(minNice - interval * t0, intervalPrecision); - max = round(maxNice + interval * t1, intervalPrecision); } - - intervalPrecision = null; // Clear for the calling of `setInterval`. } - if (isTargetLogScale) { - min = targetScale.powTick(min, 0, null); - max = targetScale.powTick(max, 1, null); - } + const extentPrecision = isTargetLogScale + ? [ + (targetMinMaxFixed[0] && !targetMinMaxChanged[0]) + ? getPrecision(min) : null, + (targetMinMaxFixed[1] && !targetMinMaxChanged[1]) + ? getPrecision(max) : null + ] + : []; + // NOTE: Must in setExtent -> setInterval order. - targetScale.setExtent(min, max); - targetScale.setInterval({ + targetScaleLinear.setExtent(min, max); + targetScaleLinear.setInterval({ // Even in LogScale, `interval` should not be in log space. interval, intervalCount, intervalPrecision, - niceExtent: [minNice, maxNice] + extentPrecision, + niceExtent: [minNice, maxNice], }); } diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 301bab7a9..4fa38b5ae 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -174,8 +174,7 @@ export function niceScaleExtent( scale.setExtent(extentInfo.min, extentInfo.max); scale.calcNiceExtent({ splitNumber: model.get('splitNumber'), - fixMin: extentInfo.minFixed, - fixMax: extentInfo.maxFixed, + fixMinMax: [extentInfo.minFixed, extentInfo.maxFixed], minInterval: isIntervalOrTime ? model.get('minInterval') : null, maxInterval: isIntervalOrTime ? model.get('maxInterval') : null }); @@ -337,6 +336,9 @@ export function getDataDimensionsOnAxis(data: SeriesData, axisDim: string): Dime return zrUtil.keys(dataDimMap); } +/** + * FIXME: refactor - merge with `Scale#unionExtentFromData` + */ export function unionAxisExtentFromData(dataExtent: number[], data: SeriesData, axisDim: string): void { if (data) { zrUtil.each(getDataDimensionsOnAxis(data, axisDim), function (dim) { diff --git a/src/coord/scaleRawExtentInfo.ts b/src/coord/scaleRawExtentInfo.ts index 8ff4e78a0..f3ae0b923 100644 --- a/src/coord/scaleRawExtentInfo.ts +++ b/src/coord/scaleRawExtentInfo.ts @@ -38,10 +38,11 @@ export interface ScaleRawExtentResult { min: number; max: number; - // `minFixed`/`maxFixed` marks that: - // - `xxxAxis.min/max` are user specified, or - // - `minDetermined/maxDetermined` are `true` - // so it should be used directly in the final extent without any other "nice strategy". + // `minFixed`/`maxFixed` is `true` iff: + // - ec option `xxxAxis.min/max` are specified, or + // - `scaleRawExtentResult.minDetermined/maxDetermined` are `true` + // They typically suggest axes to use `scaleRawExtentResult.min/max` directly + // as their bounds, instead of expanding the extent by some "nice strategy". readonly minFixed: boolean; readonly maxFixed: boolean; @@ -51,6 +52,7 @@ export interface ScaleRawExtentResult { // Mark that the axis should be blank. readonly isBlank: boolean; + readonly needCrossZero: boolean; } export class ScaleRawExtentInfo { @@ -250,7 +252,8 @@ export class ScaleRawExtentInfo { || (isOrdinal && !axisDataLen); // If data extent modified, need to recalculated to ensure cross zero. - if (this._needCrossZero) { + const needCrossZero = this._needCrossZero; + if (needCrossZero) { // Axis is over zero and min is not set if (min > 0 && max > 0 && !minFixed) { min = 0; @@ -295,6 +298,7 @@ export class ScaleRawExtentInfo { minDetermined: minDetermined, maxDetermined: maxDetermined, isBlank: isBlank, + needCrossZero: needCrossZero, }; } diff --git a/src/scale/Interval.ts b/src/scale/Interval.ts index 98ca9daec..a2b7cf9a3 100644 --- a/src/scale/Interval.ts +++ b/src/scale/Interval.ts @@ -24,7 +24,7 @@ import Scale, { ScaleGetTicksOpt, ScaleSettingDefault } from './Scale'; import * as helper from './helper'; import {ScaleTick, ParsedAxisBreakList, ScaleDataValue, NullUndefined} from '../util/types'; import { getScaleBreakHelper } from './break'; -import { assert } from 'zrender/src/core/util'; +import { assert, retrieve2 } from 'zrender/src/core/util'; class IntervalScale<SETTING extends ScaleSettingDefault = ScaleSettingDefault> extends Scale<SETTING> { @@ -34,13 +34,30 @@ class IntervalScale<SETTING extends ScaleSettingDefault = ScaleSettingDefault> e // Step is calculated in adjustExtent. protected _interval: number = 0; protected _intervalPrecision: number = 2; - // `_intervalCount` effectively specifies the number of "nice segment". This is for special cases, - // such as `alignTo: true` and min max are fixed. In this case, `_interval` may be specified with - // a "not-nice" value and needs to be rounded with `_intervalPrecision` for better appearance. Then - // merely accumulating `_interval` may generate incorrect number of ticks. So `_intervalCount` is - // required to specify the expected tick number. + protected _extentPrecision: number[] = []; + /** + * `_intervalCount` effectively specifies the number of "nice segments". This is for special cases, + * such as `alignTicks: true` and min max are fixed. In this case, `_interval` may be specified with + * a "not-nice" value and needs to be rounded with `_intervalPrecision` for better appearance. Then + * merely accumulating `_interval` may generate incorrect number of ticks due to cumulative errors. + * So `_intervalCount` is required to specify the expected nice ticks number. + * Should ensure `_intervalCount >= -1`, + * where `-1` means no nice tick (e.g., `_extent: [5.2, 5.8], _interval: 1`), + * and `0` means only one nice tick (e.g., `_extent: [5, 5.8], _interval: 1`). + * @see setInterval + */ private _intervalCount: number | NullUndefined = undefined; - // Should ensure: `_extent[0] <= _niceExtent[0] && _niceExtent[1] <= _extent[1]` + /** + * Should ensure: + * `_extent[0] <= _niceExtent[0] && _niceExtent[1] <= _extent[1]` + * But NOTICE: + * `_niceExtent[0] - _niceExtent[1] <= _interval`, rather than always `< 0`, + * because `_niceExtent` is typically calculated by + * `[ Math.ceil(_extent[0] / _interval) * _interval, Math.floor(_extent[1] / _interval) * _interval ]`. + * e.g., `_extent: [5.2, 5.8]` with interval `1` will get `_niceExtent: [6, 5]`. + * e.g., `_extent: [5, 5.8]` with interval `1` will get `_niceExtent: [5, 5]`. + * @see setInterval + */ protected _niceExtent: [number, number]; @@ -90,53 +107,43 @@ class IntervalScale<SETTING extends ScaleSettingDefault = ScaleSettingDefault> e /** * @final override is DISALLOWED. */ - setInterval({interval, intervalCount, intervalPrecision, niceExtent}: { + setInterval({interval, intervalCount, intervalPrecision, extentPrecision, niceExtent}: { interval?: number | NullUndefined; + // See comments of `_intervalCount`. intervalCount?: number | NullUndefined; intervalPrecision?: number | NullUndefined; + extentPrecision?: number[] | NullUndefined; niceExtent?: number[]; }): void { - const intervalCountSpecified = intervalCount != null; + const extent = this._extent; + if (__DEV__) { assert(interval != null); - if (intervalCountSpecified) { + if (intervalCount != null) { assert( - intervalCount > 0 + intervalCount >= -1 && intervalPrecision != null // Do not support intervalCount on axis break currently. && !this.hasBreaks() ); } - } - - const extent = this._extent; - if (__DEV__) { if (niceExtent != null) { - assert( - isFinite(niceExtent[0]) && isFinite(niceExtent[1]) - && extent[0] <= niceExtent[0] && niceExtent[1] <= extent[1] - ); + assert(isFinite(niceExtent[0]) && isFinite(niceExtent[1])); + assert(extent[0] <= niceExtent[0] && niceExtent[1] <= extent[1]); + assert(round(niceExtent[0] - niceExtent[1], getPrecision(interval)) <= interval); } } - niceExtent = this._niceExtent = niceExtent != null - ? niceExtent.slice() as [number, number] + + // Set or clear + this._niceExtent = + niceExtent != null ? niceExtent.slice() as [number, number] // Dropped the auto calculated niceExtent and use user-set extent. // We assume users want to set both interval and extent to get a better result. : extent.slice() as [number, number]; - this._interval = interval; - - if (!intervalCountSpecified) { - // This is for cases of "nice" interval. - this._intervalCount = undefined; // Clear - this._intervalPrecision = helper.getIntervalPrecision(interval); - } - else { - // This is for cases of "not-nice" interval, typically min max are fixed and - // axis alignment is required. - this._intervalCount = intervalCount; - this._intervalPrecision = intervalPrecision; - } + this._intervalCount = intervalCount; + this._intervalPrecision = retrieve2(intervalPrecision, helper.getIntervalPrecision(interval)); + this._extentPrecision = extentPrecision || []; } /** @@ -191,13 +198,17 @@ class IntervalScale<SETTING extends ScaleSettingDefault = ScaleSettingDefault> e ; niceTickIdx++ ) { + // Consider case `_extent: [5.2, 5.8], _niceExtent: [6, 5], interval: 1`, + // `_intervalCount` makes sense iff `-1`. + // Consider case `_extent: [5, 5.8], _niceExtent: [5, 5], interval: 1`, + // `_intervalCount` makes sense iff `0`. if (intervalCount == null) { - if (tick > niceTickExtent[1]) { + if (tick > niceTickExtent[1] || !isFinite(tick) || !isFinite(niceTickExtent[1])) { break; } } else { - if (niceTickIdx > intervalCount) { // ticks number should be `intervalCount + 1` + if (niceTickIdx > intervalCount) { // nice ticks number should be `intervalCount + 1` break; } // Consider cumulative error, especially caused by rounding, the last nice @@ -398,12 +409,13 @@ class IntervalScale<SETTING extends ScaleSettingDefault = ScaleSettingDefault> e calcNiceExtent(opt: { splitNumber: number, // By default 5. // Do not modify the original extent[0]/extent[1] except for an invalid extent. - fixMin?: boolean, - fixMax?: boolean, + fixMinMax?: boolean[], // [fixMin, fixMax] minInterval?: number, maxInterval?: number }): void { - let extent = helper.intervalScaleEnsureValidExtent(this._extent, opt); + const fixMinMax = opt.fixMinMax || []; + + let extent = helper.intervalScaleEnsureValidExtent(this._extent, fixMinMax); this._innerSetExtent(extent[0], extent[1]); extent = this._extent.slice() as [number, number]; @@ -412,10 +424,10 @@ class IntervalScale<SETTING extends ScaleSettingDefault = ScaleSettingDefault> e const interval = this._interval; const intervalPrecition = this._intervalPrecision; - if (!opt.fixMin) { + if (!fixMinMax[0]) { extent[0] = round(mathFloor(extent[0] / interval) * interval, intervalPrecition); } - if (!opt.fixMax) { + if (!fixMinMax[1]) { extent[1] = round(mathCeil(extent[1] / interval) * interval, intervalPrecition); } this._innerSetExtent(extent[0], extent[1]); diff --git a/src/scale/Log.ts b/src/scale/Log.ts index d7be77057..ea32d8005 100644 --- a/src/scale/Log.ts +++ b/src/scale/Log.ts @@ -21,7 +21,8 @@ import * as zrUtil from 'zrender/src/core/util'; import Scale, { ScaleGetTicksOpt, ScaleSettingDefault } from './Scale'; import { mathFloor, mathCeil, mathPow, mathLog, - round, quantity, getPrecision + round, quantity, getPrecision, + mathMax, } from '../util/number'; // Use some method of IntervalScale @@ -31,14 +32,18 @@ import { ScaleTick, NullUndefined } from '../util/types'; -import { ensureValidSplitNumber, fixNiceExtent, getIntervalPrecision, logTransform } from './helper'; +import { + ensureValidSplitNumber, getIntervalPrecision, + logScalePowTickPair, logScalePowTick, logScaleLogTickPair, + getExtentPrecision +} from './helper'; import SeriesData from '../data/SeriesData'; import { getScaleBreakHelper } from './break'; const LINEAR_STUB_METHODS = [ - 'getExtent', 'getTicks', 'getInterval' - // Keep no setting method to mitigate vulnerability. + 'getExtent', 'getTicks', 'getInterval', + 'setExtent', 'setInterval', ] as const; /** @@ -46,7 +51,8 @@ const LINEAR_STUB_METHODS = [ * - The supper class (`IntervalScale`) and its member fields (such as `this._extent`, * `this._interval`, `this._niceExtent`) provides linear tick arrangement (logarithm applied). * - `_originalScale` (`IntervalScale`) is used to save some original info - * (before logarithm applied, such as raw extent). + * (before logarithm applied, such as raw extent; but may be still invalid, and not sync to the + * calculated ("nice") extent). */ class LogScale extends IntervalScale { @@ -57,9 +63,6 @@ class LogScale extends IntervalScale { private _originalScale = new IntervalScale(); - // `[fixMin, fixMax]` - private _fixMinMax: boolean[] = [false, false]; - linearStub: Pick<IntervalScale, (typeof LINEAR_STUB_METHODS)[number]>; constructor(logBase: number | NullUndefined, settings?: ScaleSettingDefault) { @@ -84,30 +87,35 @@ class LogScale extends IntervalScale { * @param Whether expand the ticks to niced extent. */ getTicks(opt?: ScaleGetTicksOpt): ScaleTick[] { - const extent = this._extent; + const base = this.base; + const originalScale = this._originalScale; const scaleBreakHelper = getScaleBreakHelper(); + const extent = this._extent; + const extentPrecision = this._extentPrecision; return zrUtil.map(super.getTicks(opt || {}), function (tick) { + const val = tick.value; + let powVal = logScalePowTick( + val, + base, + getExtentPrecision(val, extent, extentPrecision) + ); + let vBreak; - let brkRoundingCriterion; if (scaleBreakHelper) { - const transformed = scaleBreakHelper.getTicksLogTransformBreak( + const brkPowResult = scaleBreakHelper.getTicksPowBreak( tick, - this.base, - this._originalScale._innerGetBreaks(), - fixRoundingError + base, + originalScale._innerGetBreaks(), + extent, + extentPrecision ); - vBreak = transformed.vBreak; - brkRoundingCriterion = transformed.brkRoundingCriterion; + if (brkPowResult) { + vBreak = brkPowResult.vBreak; + powVal = brkPowResult.tickPowValue; + } } - const val = tick.value; - const powVal = this.powTick( - val, - val === extent[1] ? 1 : val === extent[0] ? 0 : null, - brkRoundingCriterion - ); - return { value: powVal, break: vBreak, @@ -122,55 +130,25 @@ class LogScale extends IntervalScale { setExtent(start: number, end: number): void { // [CAVEAT]: If modifying this logic, must sync to `_initLinearStub`. this._originalScale.setExtent(start, end); - const loggedExtent = logTransform(this.base, [start, end]); + const loggedExtent = logScaleLogTickPair([start, end], this.base); super.setExtent(loggedExtent[0], loggedExtent[1]); } getExtent() { const extent = super.getExtent(); - return [ - this.powTick(extent[0], 0, null), - this.powTick(extent[1], 1, null) - ] as [number, number]; + return logScalePowTickPair( + extent, + this.base, + this._extentPrecision + ); } unionExtentFromData(data: SeriesData, dim: DimensionName | DimensionLoose): void { this._originalScale.unionExtentFromData(data, dim); - const loggedOther = logTransform(this.base, data.getApproximateExtent(dim), true); + const loggedOther = logScaleLogTickPair(data.getApproximateExtent(dim), this.base, true); this._innerUnionExtent(loggedOther); } - /** - * fixMin/Max and rounding error are addressed. - */ - powTick( - // `val` should be in the linear space. - val: number, - // `0`: `value` is `min`; - // `1`: `value` is `max`; - // `NullUndefined`: others. - extentIdx: 0 | 1 | NullUndefined, - fallbackRoundingCriterion: number | NullUndefined - ): number { - // NOTE: `Math.pow(10, integer)` has no rounding error. - // PENDING: other base? - let powVal = mathPow(this.base, val); - - // Fix #4158 - // NOTE: Even when `fixMin/Max` is `true`, `pow(base, this._extent[0]/[1])` may be still - // not equal to `this._originalScale.getExtent()[0]`/`[1]` in invalid extent case. - // So we always call `Math.pow`. - const roundingCriterion = this._fixMinMax[extentIdx] - ? this._originalScale.getExtent()[extentIdx] - : fallbackRoundingCriterion; - - if (roundingCriterion != null) { - powVal = fixRoundingError(powVal, roundingCriterion); - } - - return powVal; - } - /** * Update interval and extent of intervals for nice ticks * @param splitNumber default 10 Given approx tick number @@ -183,7 +161,9 @@ class LogScale extends IntervalScale { return; } - let interval = quantity(span); + // Interval should be integer + let interval = mathMax(quantity(span), 1); + const err = splitNumber / span * interval; // Filter ticks to get closer to the desired count. @@ -192,19 +172,12 @@ class LogScale extends IntervalScale { interval *= 10; } - // Interval should be integer - while (!isNaN(interval) && Math.abs(interval) < 1 && Math.abs(interval) > 0) { - interval *= 10; - } - const intervalPrecision = getIntervalPrecision(interval); const niceExtent = [ round(mathCeil(extent[0] / interval) * interval, intervalPrecision), round(mathFloor(extent[1] / interval) * interval, intervalPrecision) ] as [number, number]; - fixNiceExtent(niceExtent, extent); - this._interval = interval; this._intervalPrecision = intervalPrecision; this._niceExtent = niceExtent; @@ -214,14 +187,20 @@ class LogScale extends IntervalScale { calcNiceExtent(opt: { splitNumber: number, - fixMin?: boolean, - fixMax?: boolean, + fixMinMax?: boolean[], minInterval?: number, maxInterval?: number }): void { + const oldExtent = this._extent.slice() as [number, number]; super.calcNiceExtent(opt); - - this._fixMinMax = [!!opt.fixMin, !!opt.fixMax]; + const newExtent = this._extent; + + this._extentPrecision = [ + (opt.fixMinMax && opt.fixMinMax[0] && oldExtent[0] === newExtent[0]) + ? getPrecision(newExtent[0]) : null, + (opt.fixMinMax && opt.fixMinMax[1] && oldExtent[1] === newExtent[1]) + ? getPrecision(newExtent[1]) : null + ]; } contain(val: number): boolean { @@ -257,11 +236,6 @@ class LogScale extends IntervalScale { } -function fixRoundingError(val: number, originalVal: number): number { - return round(val, getPrecision(originalVal)); -} - - Scale.registerClass(LogScale); export default LogScale; diff --git a/src/scale/Scale.ts b/src/scale/Scale.ts index 801e2a2eb..f46266572 100644 --- a/src/scale/Scale.ts +++ b/src/scale/Scale.ts @@ -244,13 +244,7 @@ abstract class Scale<SETTING extends ScaleSettingDefault = ScaleSettingDefault> ): void; abstract calcNiceExtent( - opt?: { - splitNumber?: number, - fixMin?: boolean, - fixMax?: boolean, - minInterval?: number, - maxInterval?: number - } + opt?: {} ): void; /** diff --git a/src/scale/Time.ts b/src/scale/Time.ts index eb682440c..3549384d6 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -254,8 +254,6 @@ class TimeScale extends IntervalScale<TimeScaleSetting> { calcNiceExtent( opt?: { splitNumber?: number, - fixMin?: boolean, - fixMax?: boolean, minInterval?: number, maxInterval?: number } diff --git a/src/scale/break.ts b/src/scale/break.ts index ccac04680..2f994b6f0 100644 --- a/src/scale/break.ts +++ b/src/scale/break.ts @@ -114,15 +114,16 @@ export type ScaleBreakHelper = { ): ( TReturnIdx extends false ? TItem[][] : number[][] ); - getTicksLogTransformBreak( + getTicksPowBreak( tick: ScaleTick, logBase: number, logOriginalBreaks: ParsedAxisBreakList, - fixRoundingError: (val: number, originalVal: number) => number + extent: number[], + extentPrecision: (number | NullUndefined)[], ): { - brkRoundingCriterion: number | NullUndefined; + tickPowValue: number | NullUndefined; vBreak: VisualAxisBreak | NullUndefined; - }; + } | NullUndefined; logarithmicParseBreaksFromOption( breakOptionList: AxisBreakOption[], logBase: number, diff --git a/src/scale/breakImpl.ts b/src/scale/breakImpl.ts index 67d2eaa65..8c5938f45 100644 --- a/src/scale/breakImpl.ts +++ b/src/scale/breakImpl.ts @@ -17,7 +17,7 @@ * under the License. */ -import { assert, clone, each, find, isString, map, trim } from 'zrender/src/core/util'; +import { assert, clone, each, find, isString, map, retrieve2, trim } from 'zrender/src/core/util'; import { NullUndefined, ParsedAxisBreak, ParsedAxisBreakList, AxisBreakOption, AxisBreakOptionIdentifierInAxis, ScaleTick, VisualAxisBreak, @@ -25,8 +25,9 @@ import { import { error } from '../util/log'; import type Scale from './Scale'; import { ScaleBreakContext, AxisBreakParsingResult, registerScaleBreakHelperImpl, ParamPruneByBreak } from './break'; -import { round as fixRound } from '../util/number'; +import { getPrecision, mathMax, mathMin, mathRound } from '../util/number'; import { AxisLabelFormatterExtraParams } from '../coord/axisCommonTypes'; +import { getExtentPrecision, logScaleLogTick, logScaleLogTickPair, logScalePowTick } from './helper'; /** * @caution @@ -83,7 +84,7 @@ class ScaleBreakContextImpl implements ScaleBreakContext { const multiple = estimateNiceMultiple(tickVal, brk.vmax); if (__DEV__) { // If not, it may cause dead loop or not nice tick. - assert(multiple >= 0 && Math.round(multiple) === multiple); + assert(multiple >= 0 && mathRound(multiple) === multiple); } return multiple; } @@ -320,7 +321,7 @@ function updateAxisBreakGapReal( if (gapParsed.type === 'tpPrct') { brk.gapReal = gapPrctSum !== 0 // prctBrksGapRealSum is supposed to be non-negative but add a safe guard - ? Math.max(prctBrksGapRealSum, 0) * gapParsed.val / gapPrctSum : 0; + ? mathMax(prctBrksGapRealSum, 0) * gapParsed.val / gapPrctSum : 0; } if (gapParsed.type === 'tpAbs') { brk.gapReal = gapParsed.val; @@ -430,8 +431,8 @@ function clampBreakByExtent( brk: ParsedAxisBreak, scaleExtent: [number, number] ): NullUndefined | ParsedAxisBreak { - const vmin = Math.max(brk.vmin, scaleExtent[0]); - const vmax = Math.min(brk.vmax, scaleExtent[1]); + const vmin = mathMax(brk.vmin, scaleExtent[0]); + const vmax = mathMin(brk.vmax, scaleExtent[1]); return ( vmin < vmax || (vmin === vmax && vmin > scaleExtent[0] && vmin < scaleExtent[1]) @@ -619,46 +620,47 @@ function retrieveAxisBreakPairs<TItem, TReturnIdx extends boolean>( return result; } -function getTicksLogTransformBreak( +function getTicksPowBreak( tick: ScaleTick, logBase: number, logOriginalBreaks: ParsedAxisBreakList, - fixRoundingError: (val: number, originalVal: number) => number + extent: number[], + extentPrecision: (number | NullUndefined)[], ): { - brkRoundingCriterion: number; + tickPowValue: number; vBreak: VisualAxisBreak | NullUndefined; -} { - let vBreak: VisualAxisBreak | NullUndefined; - let brkRoundingCriterion: number; - - if (tick.break) { - const brk = tick.break.parsedBreak; - const originalBreak = find(logOriginalBreaks, brk => identifyAxisBreak( - brk.breakOption, tick.break.parsedBreak.breakOption - )); - const vmin = fixRoundingError(Math.pow(logBase, brk.vmin), originalBreak.vmin); - const vmax = fixRoundingError(Math.pow(logBase, brk.vmax), originalBreak.vmax); - const gapParsed = { - type: brk.gapParsed.type, - val: brk.gapParsed.type === 'tpAbs' - ? fixRound(Math.pow(logBase, brk.vmin + brk.gapParsed.val)) - vmin - : brk.gapParsed.val, - }; - vBreak = { - type: tick.break.type, - parsedBreak: { - breakOption: brk.breakOption, - vmin, - vmax, - gapParsed, - gapReal: brk.gapReal, - } - }; - brkRoundingCriterion = originalBreak[tick.break.type]; + // Return: If not found, return null/undefined. +} | NullUndefined { + if (!tick.break) { + return; } + const brk = tick.break.parsedBreak; + const originalBreak = find(logOriginalBreaks, brk => identifyAxisBreak( + brk.breakOption, tick.break.parsedBreak.breakOption + )); + const minPrecision = getExtentPrecision(brk.vmin, extent, extentPrecision); + const maxPrecision = getExtentPrecision(brk.vmax, extent, extentPrecision); + // NOTE: `tick.break` may be clamped by scale extent. For consistency we always + // pow back, or heuristically use the user input original break to obtain an + // acceptable rounding precision for display. + const vmin = logScalePowTick(brk.vmin, logBase, retrieve2(minPrecision, getPrecision(originalBreak.vmin))); + const vmax = logScalePowTick(brk.vmax, logBase, retrieve2(maxPrecision, getPrecision(originalBreak.vmax))); + const parsedBreak = { + vmin, + vmax, + // They are not changed by extent clamping. + breakOption: brk.breakOption, + gapParsed: clone(originalBreak.gapParsed), + gapReal: brk.gapReal, + }; + const vBreak = { + type: tick.break.type, + parsedBreak, + }; + return { - brkRoundingCriterion, + tickPowValue: parsedBreak[vBreak.type], vBreak, }; } @@ -675,14 +677,12 @@ function logarithmicParseBreaksFromOption( const parsedOriginal = parseAxisBreakOption(breakOptionList, parse, opt); const parsedLogged = parseAxisBreakOption(breakOptionList, parse, opt); - const loggedBase = Math.log(logBase); parsedLogged.breaks = map(parsedLogged.breaks, brk => { - const vmin = Math.log(brk.vmin) / loggedBase; - const vmax = Math.log(brk.vmax) / loggedBase; + const [vmin, vmax] = logScaleLogTickPair([brk.vmin, brk.vmax], logBase, true); const gapParsed = { type: brk.gapParsed.type, val: brk.gapParsed.type === 'tpAbs' - ? (Math.log(brk.vmin + brk.gapParsed.val) / loggedBase) - vmin + ? logScaleLogTick(brk.vmin + brk.gapParsed.val, logBase, true) - vmin : brk.gapParsed.val, }; return { @@ -722,7 +722,7 @@ export function installScaleBreakHelper(): void { identifyAxisBreak, serializeAxisBreakIdentifier, retrieveAxisBreakPairs, - getTicksLogTransformBreak, + getTicksPowBreak, logarithmicParseBreaksFromOption, makeAxisLabelFormatterParamBreak, }); diff --git a/src/scale/helper.ts b/src/scale/helper.ts index f89abca5d..bb10ae051 100644 --- a/src/scale/helper.ts +++ b/src/scale/helper.ts @@ -17,7 +17,11 @@ * under the License. */ -import {getPrecision, round, nice, quantityExponent, mathPow, mathMax, mathRound} from '../util/number'; +import { + getPrecision, round, nice, quantityExponent, + mathPow, mathMax, mathRound, + mathLog, mathAbs, mathFloor, mathCeil, mathMin +} from '../util/number'; import IntervalScale from './Interval'; import LogScale from './Log'; import type Scale from './Scale'; @@ -89,13 +93,11 @@ export function intervalScaleNiceTicks( } const precision = result.intervalPrecision = getIntervalPrecision(interval); // Niced extent inside original extent - const niceTickExtent = result.niceTickExtent = [ - round(Math.ceil(extent[0] / interval) * interval, precision), - round(Math.floor(extent[1] / interval) * interval, precision) + result.niceTickExtent = [ + round(mathCeil(extent[0] / interval) * interval, precision), + round(mathFloor(extent[1] / interval) * interval, precision) ]; - fixNiceExtent(niceTickExtent, extent); - return result; } @@ -133,26 +135,6 @@ export function getIntervalPrecision(niceInterval: number): number { return getPrecision(niceInterval) + 2; } - -function clamp( - niceTickExtent: [number, number], idx: number, extent: [number, number] -): void { - niceTickExtent[idx] = Math.max(Math.min(niceTickExtent[idx], extent[1]), extent[0]); -} - -// In some cases (e.g., splitNumber is 1), niceTickExtent may be out of extent. -export function fixNiceExtent( - niceTickExtent: [number, number], extent: [number, number] -): void { - !isFinite(niceTickExtent[0]) && (niceTickExtent[0] = extent[0]); - !isFinite(niceTickExtent[1]) && (niceTickExtent[1] = extent[1]); - clamp(niceTickExtent, 0, extent); - clamp(niceTickExtent, 1, extent); - if (niceTickExtent[0] > niceTickExtent[1]) { - niceTickExtent[0] = niceTickExtent[1]; - } -} - export function contain(val: number, extent: [number, number]): boolean { return val >= extent[0] && val <= extent[1]; } @@ -192,17 +174,79 @@ function scale( return val * (extent[1] - extent[0]) + extent[0]; } -export function logTransform(base: number, extent: number[], noClampNegative?: boolean): [number, number] { - const loggedBase = Math.log(base); +/** + * @see logScaleLogTick + */ +export function logScaleLogTickPair( + pair: number[], + base: number, + noClampNegative?: boolean +): [number, number] { return [ - // log(negative) is NaN, so safe guard here. - // PENDING: But even getting a -Infinity still does not make sense in extent. - // Just keep it as is, getting a NaN to make some previous cases works by coincidence. - Math.log(noClampNegative ? extent[0] : Math.max(0, extent[0])) / loggedBase, - Math.log(noClampNegative ? extent[1] : Math.max(0, extent[1])) / loggedBase + logScaleLogTick(pair[0], base, noClampNegative), + logScaleLogTick(pair[1], base, noClampNegative) ]; } +export function logScaleLogTick( + val: number, + base: number, + noClampNegative?: boolean +): number { + // log(negative) is NaN, so safe guard here. + // PENDING: But even getting a -Infinity still does not make sense in extent. + // Just keep it as is, getting a NaN to make some previous cases works by coincidence. + return mathLog(noClampNegative ? val : mathMax(0, val)) / mathLog(base); + // 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. +} + +/** + * @see logScalePowTick + */ +export function logScalePowTickPair( + linearPair: number[], + base: number, + precisionPair: (number | NullUndefined)[], +): [number, number] { + return [ + logScalePowTick(linearPair[0], base, precisionPair[0]), + logScalePowTick(linearPair[1], base, precisionPair[1]) + ] as [number, number]; +} + +/** + * 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 `alignScaleTicks`) and set by `setExtent`, + * `logScaleLogTickPair` 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 by providing + * a `precision`. + * See also `#4158`. + */ +export function logScalePowTick( + // `tickVal` should be in the linear space. + linearTickVal: number, + base: number, + precision: number | NullUndefined, +): number { + + // NOTE: Even when min/max is required to be fixed, `pow(base, tickVal)` is not necessarily equal to + // `originalPowExtent[0]`/`[1]`. e.g., when `originalPowExtent` is a invalid extent but + // `tickVal` has been adjusted to make it valid. So we always use `Math.pow`. + let powVal = mathPow(base, linearTickVal); + + if (precision != null) { + powVal = round(powVal, precision); + } + + return powVal; +} + /** * A valid extent is: * - No non-finite number. @@ -215,9 +259,7 @@ export function logTransform(base: number, extent: number[], noClampNegative?: b */ export function intervalScaleEnsureValidExtent( rawExtent: number[], - opt: { - fixMax?: boolean - } + fixMinMax: boolean[], ): number[] { const extent = rawExtent.slice(); // If extent start and end are same, expand them @@ -225,13 +267,13 @@ export function intervalScaleEnsureValidExtent( if (extent[0] !== 0) { // Expand extent // Note that extents can be both negative. See #13154 - const expandSize = Math.abs(extent[0]); + 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 (!opt.fixMax) { + if (!fixMinMax[1]) { extent[1] += expandSize / 2; extent[0] -= expandSize / 2; } @@ -262,3 +304,13 @@ export function ensureValidSplitNumber( rawSplitNumber = rawSplitNumber || defaultSplitNumber; return mathRound(mathMax(rawSplitNumber, 1)); } + +export function getExtentPrecision( + val: number, + extent: number[], + extentPrecision: (number | NullUndefined)[], +): number | NullUndefined { + return val === extent[0] ? extentPrecision[0] + : val === extent[1] ? extentPrecision[1] + : null; +} diff --git a/test/axis-align-edge-cases.html b/test/axis-align-edge-cases.html index d851f1bdd..4c1bc4b88 100644 --- a/test/axis-align-edge-cases.html +++ b/test/axis-align-edge-cases.html @@ -42,12 +42,13 @@ under the License. </style> - <!-- <div id="main_cartesian_0_integerData"></div> + <div id="main_cartesian_0_integerData"></div> <div id="main_cartesian_0_floatData"></div> <div id="main_cartesian_fix_min_max"></div> <div id="main_cartesian_yAxis_dataZoom"></div> - <div id="main_radar_0"></div> --> + <div id="main_radar_0"></div> <div id="main_cartesian_0_logIntegerData"></div> + <div id="main_cartesian_middle_preferable"></div> @@ -734,13 +735,103 @@ under the License. create_case_main_cartesian_0( echarts, 'main_cartesian_0_logIntegerData', - 'The right yAxis is "log"; the right series are integer Data', + 'The right yAxis is **"log"**; the right series are integer Data', TEST_DATA_INTEGER ); }); // End of `require` </script> + + <script> + require([ + 'echarts', + ], function (echarts /*, data */) { + _ctx = { + rightYAxisScale: true, + rightSeriesMax: 4000, + rightSeriesMin: 3099, + }; + + function createOption() { + return { + tooltip: {}, + grid: { + top: 50, + bottom: 50, + }, + xAxis: { + max: 1300 + }, + yAxis: [{ + id: 'left', + splitNumber: 5, + }, { + id: 'right', + alignTicks: true, + scale: _ctx.rightYAxisScale, + }], + legend: { + top: 5 + }, + series: [{ + type: 'line', + name: 'lineLeft', + label: {show: true, position: 'top'}, + yAxisIndex: 0, + data: [[10, 0], [20, 10], [30, 20], [40, 30]] + }, { + type: 'line', + name: 'lineRight', + label: {show: true, position: 'top'}, + yAxisIndex: 1, + data: [[1000, _ctx.rightSeriesMin], [1200, _ctx.rightSeriesMax]] + }] + }; + } + function updateOption() { + chart.setOption(createOption(), {notMerge: true}); + } + + var chart = testHelper.create(echarts, 'main_cartesian_middle_preferable', { + title: [ + `The right yAxis aligns to the left yAxis.`, + `The right series should be laid out at the **middle of Y** whenever possible.`, + ], + option: createOption(), + height: 300, + inputsStyle: 'compact', + inputs: [{ + type: 'select', + text: 'rightYAxis.scale:', + values: [true, false], + onchange: function () { + _ctx.rightYAxisScale = this.value; + updateOption(); + } + }, { + type: 'select', + text: 'rightSeries.min:', + values: [_ctx.rightSeriesMin, 3000, 3010], + onchange: function () { + _ctx.rightSeriesMin = this.value; + updateOption(); + } + }, { + type: 'select', + text: 'rightSeries.max:', + values: [_ctx.rightSeriesMax, 4001, 4190], + onchange: function () { + _ctx.rightSeriesMax = this.value; + updateOption(); + } + }] + }); // End of `testHelper.create` + }); // End of `require` + </script> + + + </body> </html> diff --git a/test/axis-align-ticks.html b/test/axis-align-ticks.html index b0a761a84..e22f2a99c 100644 --- a/test/axis-align-ticks.html +++ b/test/axis-align-ticks.html @@ -40,7 +40,7 @@ under the License. <div id="main0"></div> <div id="main1"></div> <div id="main2"></div> - <div id="main3"></div> + <div id="main_Log_axis_can_alignTicks_to_value_axis"></div> <div id="main4"></div> <div id="main5"></div> <div id="main6"></div> @@ -228,6 +228,7 @@ under the License. var option = { legend: {}, + tooltip: {}, xAxis: { type: 'category', name: 'x', @@ -258,7 +259,7 @@ under the License. ] } - var chart = testHelper.create(echarts, 'main3', { + var chart = testHelper.create(echarts, 'main_Log_axis_can_alignTicks_to_value_axis', { title: [ 'Log axis can alignTicks to value axis' ], --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
