From cc0ebd2845781021cb05287fded7c94b26174619 Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 28 Dec 2021 21:47:14 +0800 Subject: [PATCH 1/7] feat(axis): add alignTicks --- src/component/axis/CartesianAxisView.ts | 4 +- src/component/timeline/SliderTimelineView.ts | 2 +- src/coord/axisCommonTypes.ts | 10 +- src/coord/axisHelper.ts | 17 +- src/coord/cartesian/Grid.ts | 82 +- src/coord/radar/Radar.ts | 72 +- src/scale/Interval.ts | 10 +- src/scale/Log.ts | 9 +- src/scale/Ordinal.ts | 4 +- src/scale/Scale.ts | 6 +- src/scale/Time.ts | 9 +- src/scale/helper.ts | 114 ++ src/util/number.ts | 14 + test/auto-align.html | 1270 ++++++++++++++++++ test/axis-align-ticks-random.html | 186 +++ 15 files changed, 1694 insertions(+), 115 deletions(-) create mode 100644 test/auto-align.html create mode 100644 test/axis-align-ticks-random.html diff --git a/src/component/axis/CartesianAxisView.ts b/src/component/axis/CartesianAxisView.ts index 20005749f8..e040398902 100644 --- a/src/component/axis/CartesianAxisView.ts +++ b/src/component/axis/CartesianAxisView.ts @@ -28,6 +28,7 @@ import ExtensionAPI from '../../core/ExtensionAPI'; import CartesianAxisModel from '../../coord/cartesian/AxisModel'; import GridModel from '../../coord/cartesian/GridModel'; import { Payload } from '../../util/types'; +import { isIntervalOrLogScale } from '../../scale/helper'; const axisBuilderAttrs = [ 'axisLine', 'axisTickLabel', 'axisName' @@ -69,8 +70,7 @@ class CartesianAxisView extends AxisView { handleAutoShown(elementType) { const cartesians = gridModel.coordinateSystem.getCartesians(); for (let i = 0; i < cartesians.length; i++) { - const otherAxisType = cartesians[i].getOtherAxis(axisModel.axis).type; - if (otherAxisType === 'value' || otherAxisType === 'log') { + if (isIntervalOrLogScale(cartesians[i].getOtherAxis(axisModel.axis).scale)) { // Still show axis tick or axisLine if other axis is value / log return true; } diff --git a/src/component/timeline/SliderTimelineView.ts b/src/component/timeline/SliderTimelineView.ts index b49bc95620..32eb7d6fc2 100644 --- a/src/component/timeline/SliderTimelineView.ts +++ b/src/component/timeline/SliderTimelineView.ts @@ -351,7 +351,7 @@ class SliderTimelineView extends TimelineView { const dataExtent = data.getDataExtent('value'); scale.setExtent(dataExtent[0], dataExtent[1]); - scale.niceTicks(); + scale.calcNiceTicks(); const axis = new TimelineAxis('value', scale, layoutInfo.axisExtent as [number, number], axisType); axis.model = timelineModel; diff --git a/src/coord/axisCommonTypes.ts b/src/coord/axisCommonTypes.ts index fac7d7c00a..e3241cb02e 100644 --- a/src/coord/axisCommonTypes.ts +++ b/src/coord/axisCommonTypes.ts @@ -81,7 +81,7 @@ export interface AxisBaseOptionCommon extends ComponentOption, } -interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon { +export interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon { /* * The gap at both ends of the axis. * [GAP, GAP], where @@ -105,6 +105,14 @@ interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon { * Specify max interval when auto calculate tick interval. */ maxInterval?: number; + + /** + * If align ticks to the first axis that is not use alignTicks + * If all axes has alignTicks: true. The first one will be applied. + * + * Will be ignored if interval is set. + */ + alignTicks?: boolean } export interface CategoryAxisBaseOption extends AxisBaseOptionCommon { diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 53a60b6f5b..e8bf8a64e4 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -149,7 +149,10 @@ function adjustScaleForOverflow( // Precondition of calling this method: // The scale extent has been initailized using series data extent via // `scale.setExtent` or `scale.unionExtentFromData`; -export function niceScaleExtent(scale: Scale, inModel: AxisBaseModel) { +export function niceScaleExtent( + scale: Scale, + inModel: AxisBaseModel +) { const model = inModel as AxisBaseModel; const extentInfo = getScaleExtent(scale, model); const extent = extentInfo.extent; @@ -160,15 +163,16 @@ export function niceScaleExtent(scale: Scale, inModel: AxisBaseModel) { } const scaleType = scale.type; + const interval = model.get('interval'); + const isIntervalOrTime = scaleType === 'interval' || scaleType === 'time'; + scale.setExtent(extent[0], extent[1]); - scale.niceExtent({ + scale.calcNiceExtent({ splitNumber: splitNumber, fixMin: extentInfo.fixMin, fixMax: extentInfo.fixMax, - minInterval: (scaleType === 'interval' || scaleType === 'time') - ? model.get('minInterval') : null, - maxInterval: (scaleType === 'interval' || scaleType === 'time') - ? model.get('maxInterval') : null + minInterval: isIntervalOrTime ? model.get('minInterval') : null, + maxInterval: isIntervalOrTime ? model.get('maxInterval') : null }); // If some one specified the min, max. And the default calculated interval @@ -176,7 +180,6 @@ export function niceScaleExtent(scale: Scale, inModel: AxisBaseModel) { // in angle axis with angle 0 - 360. Interval calculated in interval scale is hard // to be 60. // FIXME - const interval = model.get('interval'); if (interval != null) { (scale as IntervalScale).setInterval && (scale as IntervalScale).setInterval(interval); } diff --git a/src/coord/cartesian/Grid.ts b/src/coord/cartesian/Grid.ts index aba0b732c0..d9c6de68f8 100644 --- a/src/coord/cartesian/Grid.ts +++ b/src/coord/cartesian/Grid.ts @@ -23,7 +23,7 @@ * TODO Default cartesian */ -import {isObject, each, indexOf, retrieve3} from 'zrender/src/core/util'; +import {isObject, each, indexOf, retrieve3, map, keys} from 'zrender/src/core/util'; import {getLayoutRect, LayoutRect} from '../../util/layout'; import { createScaleByModel, @@ -47,14 +47,20 @@ import { ScaleDataValue } from '../../util/types'; import SeriesData from '../../data/SeriesData'; import OrdinalScale from '../../scale/Ordinal'; import { isCartesian2DSeries, findAxisModels } from './cartesianAxisHelper'; -import { CategoryAxisBaseOption } from '../axisCommonTypes'; +import { CategoryAxisBaseOption, NumericAxisBaseOptionCommon } from '../axisCommonTypes'; import { AxisBaseModel } from '../AxisBaseModel'; +import { alignScaleTicks, isIntervalOrLogScale } from '../../scale/helper'; +import IntervalScale from '../../scale/Interval'; +import LogScale from '../../scale/Log'; type Cartesian2DDimensionName = 'x' | 'y'; type FinderAxisIndex = {xAxisIndex?: number, yAxisIndex?: number}; -type AxesMap = {x: Axis2D[], y: Axis2D[]}; +type AxesMap = { + x: Axis2D[], + y: Axis2D[] +}; class Grid implements CoordinateSystemMaster { @@ -92,12 +98,55 @@ class Grid implements CoordinateSystemMaster { this._updateScale(ecModel, this.model); - each(axesMap.x, function (xAxis) { - niceScaleExtent(xAxis.scale, xAxis.model); - }); - each(axesMap.y, function (yAxis) { - niceScaleExtent(yAxis.scale, yAxis.model); - }); + function updateAxisTicks(axes: Record) { + let alignTo: Axis2D; + // Axis is added in order of axisIndex. + const axesIndices = keys(axes); + const len = axesIndices.length; + if (!len) { + return; + } + const axisNeedsAlign: Axis2D[] = []; + // Process once and calculate the ticks for those don't use alignTicks. + for (let i = len - 1; i >= 0; i--) { + const idx = +axesIndices[i]; // Convert to number. + const axis = axes[idx]; + const model = axis.model as AxisBaseModel; + const scale = axis.scale; + if (// Only value and log axis without interval support alignTicks. + isIntervalOrLogScale(scale) + && model.get('alignTicks') + && model.get('interval') == null + ) { + axisNeedsAlign.push(axis); + } + else { + niceScaleExtent(scale, model); + if (isIntervalOrLogScale(scale)) { // Can only align to interval or log axis. + alignTo = axis; + } + } + }; + // All axes has set alignTicks. Pick the first one. + // PENDING. Should we find the axis that both set interval, min, max and align to this one? + if (axisNeedsAlign.length) { + if (!alignTo) { + alignTo = axisNeedsAlign.pop(); + niceScaleExtent(alignTo.scale, alignTo.model); + } + + each(axisNeedsAlign, axis => { + alignScaleTicks( + axis.scale as IntervalScale | LogScale, + axis.model, + alignTo.scale as IntervalScale | LogScale + ); + }); + } + } + + updateAxisTicks(axesMap.x); + updateAxisTicks(axesMap.y); // Key: axisDim_axisIndex, value: boolean, whether onZero target. const onZeroRecords = {} as Dictionary; @@ -177,15 +226,6 @@ class Grid implements CoordinateSystemMaster { const axesMapOnDim = this._axesMap[dim]; if (axesMapOnDim != null) { return axesMapOnDim[axisIndex || 0]; - // if (axisIndex == null) { - // Find first axis - // for (let name in axesMapOnDim) { - // if (axesMapOnDim.hasOwnProperty(name)) { - // return axesMapOnDim[name]; - // } - // } - // } - // return axesMapOnDim[axisIndex]; } } @@ -445,10 +485,8 @@ class Grid implements CoordinateSystemMaster { const xAxis = cartesian.getAxis('x'); const yAxis = cartesian.getAxis('y'); - if (data.type === 'list') { - unionExtent(data, xAxis); - unionExtent(data, yAxis); - } + unionExtent(data, xAxis); + unionExtent(data, yAxis); } }, this); diff --git a/src/coord/radar/Radar.ts b/src/coord/radar/Radar.ts index b98c3af869..f80ec8ef80 100644 --- a/src/coord/radar/Radar.ts +++ b/src/coord/radar/Radar.ts @@ -34,6 +34,7 @@ import { ScaleDataValue } from '../../util/types'; import { ParsedModelFinder } from '../../util/model'; import { parseAxisModelMinMax } from '../scaleRawExtentInfo'; import { map, each } from 'zrender/src/core/util'; +import { alignScaleTicks } from '../../scale/helper'; class Radar implements CoordinateSystem, CoordinateSystemMaster { @@ -172,71 +173,16 @@ class Radar implements CoordinateSystem, CoordinateSystemMaster { }, this); const splitNumber = radarModel.get('splitNumber'); - - function increaseInterval(interval: number) { - const exp10 = Math.pow(10, Math.floor(Math.log(interval) / Math.LN10)); - // Increase interval - let f = interval / exp10; - if (f === 2) { - f = 5; - } - else { // f is 2 or 5 - f *= 2; - } - return f * exp10; - } + const dummyScale = new IntervalScale(); + dummyScale.setExtent(0, splitNumber); + dummyScale.setInterval(1); // Force all the axis fixing the maxSplitNumber. each(indicatorAxes, function (indicatorAxis, idx) { - const rawExtent = getScaleExtent(indicatorAxis.scale, indicatorAxis.model).extent; - niceScaleExtent(indicatorAxis.scale, indicatorAxis.model); - - const axisModel = indicatorAxis.model; - const scale = indicatorAxis.scale as IntervalScale; - const fixedMin = parseAxisModelMinMax(scale, axisModel.get('min', true) as ScaleDataValue); - const fixedMax = parseAxisModelMinMax(scale, axisModel.get('max', true) as ScaleDataValue); - let interval = scale.getInterval(); - - if (fixedMin != null && fixedMax != null) { - // User set min, max, divide to get new interval - scale.setExtent(+fixedMin, +fixedMax); - scale.setInterval( - (fixedMax - fixedMin) / splitNumber - ); - } - else if (fixedMin != null) { - let max; - // User set min, expand extent on the other side - do { - max = fixedMin + interval * splitNumber; - scale.setExtent(+fixedMin, max); - // Interval must been set after extent - // FIXME - scale.setInterval(interval); - - interval = increaseInterval(interval); - } while (max < rawExtent[1] && isFinite(max) && isFinite(rawExtent[1])); - } - else if (fixedMax != null) { - let min; - // User set min, expand extent on the other side - do { - min = fixedMax - interval * splitNumber; - scale.setExtent(min, +fixedMax); - scale.setInterval(interval); - interval = increaseInterval(interval); - } while (min > rawExtent[0] && isFinite(min) && isFinite(rawExtent[0])); - } - else { - const nicedSplitNumber = scale.getTicks().length - 1; - if (nicedSplitNumber > splitNumber) { - interval = increaseInterval(interval); - } - // TODO - const max = Math.ceil(rawExtent[1] / interval) * interval; - const min = numberUtil.round(max - interval * splitNumber); - scale.setExtent(min, max); - scale.setInterval(interval); - } + alignScaleTicks( + indicatorAxis.scale as IntervalScale, + indicatorAxis.model, + dummyScale + ); }); } diff --git a/src/scale/Interval.ts b/src/scale/Interval.ts index a1102fe237..1f4a7f98e8 100644 --- a/src/scale/Interval.ts +++ b/src/scale/Interval.ts @@ -215,7 +215,7 @@ class IntervalScale = Dictionary> e /** * @param splitNumber By default `5`. */ - niceTicks(splitNumber?: number, minInterval?: number, maxInterval?: number): void { + calcNiceTicks(splitNumber?: number, minInterval?: number, maxInterval?: number): void { splitNumber = splitNumber || 5; const extent = this._extent; let span = extent[1] - extent[0]; @@ -238,7 +238,7 @@ class IntervalScale = Dictionary> e this._niceExtent = result.niceTickExtent; } - niceExtent(opt: { + calcNiceExtent(opt: { splitNumber: number, // By default 5. fixMin?: boolean, fixMax?: boolean, @@ -275,8 +275,7 @@ class IntervalScale = Dictionary> e extent[1] = 1; } - this.niceTicks(opt.splitNumber, opt.minInterval, opt.maxInterval); - + this.calcNiceTicks(opt.splitNumber, opt.minInterval, opt.maxInterval); // let extent = this._extent; const interval = this._interval; @@ -288,6 +287,9 @@ class IntervalScale = Dictionary> e } } + setNiceExtent(min: number, max: number) { + this._niceExtent = [min, max]; + } } Scale.registerClass(IntervalScale); diff --git a/src/scale/Log.ts b/src/scale/Log.ts index f8c3258b49..41586e8775 100644 --- a/src/scale/Log.ts +++ b/src/scale/Log.ts @@ -59,7 +59,7 @@ class LogScale extends Scale { /** * @param Whether expand the ticks to niced extent. */ - getTicks(expandToNicedExtent: boolean): ScaleTick[] { + getTicks(expandToNicedExtent?: boolean): ScaleTick[] { const originalScale = this._originalScale; const extent = this._extent; const originalExtent = originalScale.getExtent(); @@ -128,7 +128,7 @@ class LogScale extends Scale { * Update interval and extent of intervals for nice ticks * @param approxTickNum default 10 Given approx tick number */ - niceTicks(approxTickNum: number): void { + calcNiceTicks(approxTickNum: number): void { approxTickNum = approxTickNum || 10; const extent = this._extent; const span = extent[1] - extent[0]; @@ -158,14 +158,14 @@ class LogScale extends Scale { this._niceExtent = niceExtent; } - niceExtent(opt: { + calcNiceExtent(opt: { splitNumber: number, // By default 5. fixMin?: boolean, fixMax?: boolean, minInterval?: number, maxInterval?: number }): void { - intervalScaleProto.niceExtent.call(this, opt); + intervalScaleProto.calcNiceExtent.call(this, opt); this._fixMin = opt.fixMin; this._fixMax = opt.fixMax; @@ -198,7 +198,6 @@ const proto = LogScale.prototype; proto.getMinorTicks = intervalScaleProto.getMinorTicks; proto.getLabel = intervalScaleProto.getLabel; - function fixRoundingError(val: number, originalVal: number): number { return roundingErrorFix(val, numberUtil.getPrecision(originalVal)); } diff --git a/src/scale/Ordinal.ts b/src/scale/Ordinal.ts index 01139da56c..13ee2b1f68 100644 --- a/src/scale/Ordinal.ts +++ b/src/scale/Ordinal.ts @@ -280,9 +280,9 @@ class OrdinalScale extends Scale { return this._ordinalMeta; } - niceTicks() {} + calcNiceTicks() {} - niceExtent() {} + calcNiceExtent() {} } diff --git a/src/scale/Scale.ts b/src/scale/Scale.ts index afae3f46b7..143f4c9b74 100644 --- a/src/scale/Scale.ts +++ b/src/scale/Scale.ts @@ -149,14 +149,14 @@ abstract class Scale = Dictionary> * @param minInterval Optional. * @param maxInterval Optional. */ - abstract niceTicks( + abstract calcNiceTicks( // FIXME:TS make them in a "opt", the same with `niceExtent`? splitNumber?: number, minInterval?: number, maxInterval?: number ): void; - abstract niceExtent( + abstract calcNiceExtent( opt?: { splitNumber?: number, fixMin?: boolean, @@ -171,7 +171,7 @@ abstract class Scale = Dictionary> */ abstract getLabel(tick: ScaleTick): string; - abstract getTicks(expandToNicedExtent?: boolean): ScaleTick[]; + abstract getTicks(): ScaleTick[]; abstract getMinorTicks(splitNumber: number): number[][]; diff --git a/src/scale/Time.ts b/src/scale/Time.ts index d9e1b945eb..b64567bec9 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -144,9 +144,8 @@ class TimeScale extends IntervalScale { /** * @override - * @param expandToNicedExtent Whether expand the ticks to niced extent. */ - getTicks(expandToNicedExtent?: boolean): TimeScaleTick[] { + getTicks(): TimeScaleTick[] { const interval = this._interval; const extent = this._extent; @@ -180,7 +179,7 @@ class TimeScale extends IntervalScale { return ticks; } - niceExtent( + calcNiceExtent( opt?: { splitNumber?: number, fixMin?: boolean, @@ -203,10 +202,10 @@ class TimeScale extends IntervalScale { extent[0] = extent[1] - ONE_DAY; } - this.niceTicks(opt.splitNumber, opt.minInterval, opt.maxInterval); + this.calcNiceTicks(opt.splitNumber, opt.minInterval, opt.maxInterval); } - niceTicks(approxTickNum: number, minInterval: number, maxInterval: number): void { + calcNiceTicks(approxTickNum: number, minInterval: number, maxInterval: number): void { approxTickNum = approxTickNum || 10; const extent = this._extent; diff --git a/src/scale/helper.ts b/src/scale/helper.ts index 4965dc2fc8..ff8923d491 100644 --- a/src/scale/helper.ts +++ b/src/scale/helper.ts @@ -17,15 +17,29 @@ * under the License. */ +import { NumericAxisBaseOptionCommon } from '../coord/axisCommonTypes'; import * as numberUtil from '../util/number'; +import IntervalScale from './Interval'; +import { parseAxisModelMinMax } from '../coord/scaleRawExtentInfo'; +import { ScaleDataValue } from '../util/types'; +import { getScaleExtent, niceScaleExtent } from '../coord/axisHelper'; +import { AxisBaseModel } from '../coord/AxisBaseModel'; +import LogScale from './Log'; +import Scale from './Scale'; +import { warn } from '../util/log'; const roundNumber = numberUtil.round; +const mathLog = Math.log; type intervalScaleNiceTicksResult = { interval: number, intervalPrecision: number, niceTickExtent: [number, number] }; + +export function isIntervalOrLogScale(scale: Scale): scale is LogScale | IntervalScale { + return scale.type === 'interval' || scale.type === 'log'; +} /** * @param extent Both extent[0] and extent[1] should be valid number. * Should be extent[0] < extent[1]. @@ -61,6 +75,106 @@ export function intervalScaleNiceTicks( return result; } +function increaseInterval(interval: number) { + const exp10 = Math.pow(10, numberUtil.quantityExponent(interval)); + // Increase interval + let f = interval / exp10; + if (f === 2) { + f = 3; + } + else if (f === 3) { + f = 5; + } + else { // f is 1 or 5 + f *= 2; + } + return f * exp10; +} + +export function alignScaleTicks( + scale: IntervalScale | LogScale, + axisModel: AxisBaseModel>, + alignToScale: IntervalScale | LogScale +) { + + const intervalScaleProto = IntervalScale.prototype; + + // NOTE: There is a precondition for log scale here: + // In log scale we store _interval and _extent of exponent value. + // So if we use the method of InternalScale to set/get these data. + // It process the exponent value, which is linear and what we want here. + const alignToTicks = intervalScaleProto.getTicks.call(alignToScale); + const alignToNicedTicks = intervalScaleProto.getTicks.call(alignToScale, true); + const alignToSplitNumber = alignToTicks.length - 1; + const alignToInterval = intervalScaleProto.getInterval.call(alignToScale); + + const fixedMin = parseAxisModelMinMax(scale, axisModel.get('min', true) as ScaleDataValue); + const fixedMax = parseAxisModelMinMax(scale, axisModel.get('max', true) as ScaleDataValue); + + niceScaleExtent(scale, axisModel); + const rawExtent = intervalScaleProto.getExtent.call(scale); + const isMinFixed = fixedMin != null; + const isMaxFixed = fixedMax != null; + + let interval = intervalScaleProto.getInterval.call(scale); + let min: number = rawExtent[0]; + let max: number = rawExtent[1]; + + if (isMinFixed && isMaxFixed) { + // User set min, max, divide to get new interval + interval = (max - min) / alignToSplitNumber; + } + else if (isMinFixed) { + max = rawExtent[0] + interval * alignToSplitNumber; + // User set min, expand extent on the other side + while (max < rawExtent[1] && isFinite(max) && isFinite(rawExtent[1])) { + interval = increaseInterval(interval); + max = rawExtent[0] + interval * alignToSplitNumber; + } + } + else if (isMaxFixed) { + // User set max, expand extent on the other side + min = rawExtent[1] - interval * alignToSplitNumber; + while (min > rawExtent[0] && isFinite(min) && isFinite(rawExtent[0])) { + interval = increaseInterval(interval); + min = rawExtent[1] - interval * alignToSplitNumber; + } + } + else { + const nicedSplitNumber = scale.getTicks().length - 1; + if (nicedSplitNumber > alignToSplitNumber) { + interval = increaseInterval(interval); + } + // PENDING + max = Math.ceil(rawExtent[1] / interval) * interval; + min = numberUtil.round(max - interval * alignToSplitNumber); + } + + // Adjust min, max based on the extent of alignTo. When min or max is set in alignTo scale + const t0 = (alignToTicks[0].value - alignToNicedTicks[0].value) / alignToInterval; + const t1 = (alignToTicks[alignToSplitNumber].value - alignToNicedTicks[alignToSplitNumber].value) / alignToInterval; + + // NOTE: Must in setExtent -> setInterval -> setNiceExtent order. + intervalScaleProto.setExtent.call(scale, min + interval * t0, max + interval * t1); + intervalScaleProto.setInterval.call(scale, interval); + if (t0 || t1) { + intervalScaleProto.setNiceExtent.call(scale, min + interval, max - interval); + } + + if (__DEV__) { + const ticks = intervalScaleProto.getTicks.call(scale); + // if (ticks.length !== alignToScale.getTicks().length) { + // debugger + // } + if (ticks[1] && ticks[1].value % interval) { + warn( + // eslint-disable-next-line + `The ticks may be not displayed nice if when set min: ${fixedMin}, max: ${fixedMax} and alignTicks: true` + ); + } + } +} + /** * @return interval precision */ diff --git a/src/util/number.ts b/src/util/number.ts index 2ec02f3f8d..20c1ac9cb2 100644 --- a/src/util/number.ts +++ b/src/util/number.ts @@ -483,6 +483,20 @@ export function nice(val: number, round?: boolean): number { return exponent >= -20 ? +val.toFixed(exponent < 0 ? -exponent : 0) : val; } +// /** +// * Check if value is a nice number. +// * @param val +// */ +// export function isValueNice(val: number) { +// const exp10 = Math.pow(10, quantityExponent(Math.abs(val))); +// const f = Math.abs(val / exp10); +// return f === 0 +// || f === 1 +// || f === 2 +// || f === 3 +// || f === 5; +// } + /** * This code was copied from "d3.js" * . diff --git a/test/auto-align.html b/test/auto-align.html new file mode 100644 index 0000000000..97b21d01b9 --- /dev/null +++ b/test/auto-align.html @@ -0,0 +1,1270 @@ + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/test/axis-align-ticks-random.html b/test/axis-align-ticks-random.html new file mode 100644 index 0000000000..42e0858dba --- /dev/null +++ b/test/axis-align-ticks-random.html @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + +

Ticks of right axis should be aligned and the ticks looks nice. All data are random

+
+

Min is fixed on the left axis

+
+

Max is fixed on the left axis

+
+

Min, max are both fixed on the left axis.

+
+

Min is fixed to -50 on the right axis

+
+

Max is fixed to 50 on the right axis

+
+

Min, max are both fixed to -50/50 on the right axis.

+
+

Min is fixed to 'dataMin' on the right axis

+
+

Max is fixed to 'dataMax' on the right axis

+
+

Min, max are both fixed to 'dataMin'/'dataMax' on the right axis.

+
+

Right axis should follow split numbers from left axis.

+
+ + + + + + + + + + + + From 9b0a076f2b76aed30243296958e4da40f5c6779f Mon Sep 17 00:00:00 2001 From: pissang Date: Tue, 28 Dec 2021 22:09:49 +0800 Subject: [PATCH 2/7] feat(alignTicks): optimize ticks not crossing zero --- src/scale/helper.ts | 27 +++++- test/axis-align-ticks.html | 177 +++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 test/axis-align-ticks.html diff --git a/src/scale/helper.ts b/src/scale/helper.ts index ff8923d491..bd1ce7829c 100644 --- a/src/scale/helper.ts +++ b/src/scale/helper.ts @@ -79,7 +79,10 @@ function increaseInterval(interval: number) { const exp10 = Math.pow(10, numberUtil.quantityExponent(interval)); // Increase interval let f = interval / exp10; - if (f === 2) { + if (!f) { + f = 1; + } + else if (f === 2) { f = 3; } else if (f === 3) { @@ -112,9 +115,18 @@ export function alignScaleTicks( const fixedMax = parseAxisModelMinMax(scale, axisModel.get('max', true) as ScaleDataValue); niceScaleExtent(scale, axisModel); - const rawExtent = intervalScaleProto.getExtent.call(scale); + const extent = intervalScaleProto.getExtent.call(scale); + const rawExtent = getScaleExtent(scale, axisModel).extent; const isMinFixed = fixedMin != null; const isMaxFixed = fixedMax != null; + // Need to update the rawExtent. + // Because value in rawExtent may be not parsed. e.g. 'dataMin', 'dataMax' + if (isMinFixed) { + rawExtent[0] = extent[0]; + } + if (isMaxFixed) { + rawExtent[1] = extent[1]; + } let interval = intervalScaleProto.getInterval.call(scale); let min: number = rawExtent[0]; @@ -145,9 +157,18 @@ export function alignScaleTicks( if (nicedSplitNumber > alignToSplitNumber) { interval = increaseInterval(interval); } - // PENDING max = Math.ceil(rawExtent[1] / interval) * interval; min = numberUtil.round(max - interval * alignToSplitNumber); + + // Not change the result that crossing zero. + if (min < 0 && rawExtent[0] >= 0) { + min = 0; + max = numberUtil.round(interval * alignToSplitNumber); + } + else if (max > 0 && rawExtent[1] <= 0) { + max = 0; + min = numberUtil.round((-interval * alignToSplitNumber)); + } } // Adjust min, max based on the extent of alignTo. When min or max is set in alignTo scale diff --git a/test/axis-align-ticks.html b/test/axis-align-ticks.html new file mode 100644 index 0000000000..1b38f8f154 --- /dev/null +++ b/test/axis-align-ticks.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + From f862adc182f2726a8dd84ff2691b8cfa8009f646 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 29 Dec 2021 14:12:32 +0800 Subject: [PATCH 3/7] feat(alignTicks): fix log axis align. add more test cases --- src/coord/radar/Radar.ts | 5 - src/scale/helper.ts | 68 +++++--- src/util/number.ts | 14 -- test/axis-align-ticks.html | 332 +++++++++++++++++++++++++++++++------ 4 files changed, 326 insertions(+), 93 deletions(-) diff --git a/src/coord/radar/Radar.ts b/src/coord/radar/Radar.ts index f80ec8ef80..68fb878153 100644 --- a/src/coord/radar/Radar.ts +++ b/src/coord/radar/Radar.ts @@ -22,17 +22,12 @@ import IndicatorAxis from './IndicatorAxis'; import IntervalScale from '../../scale/Interval'; import * as numberUtil from '../../util/number'; -import { - getScaleExtent, - niceScaleExtent -} from '../axisHelper'; import { CoordinateSystemMaster, CoordinateSystem } from '../CoordinateSystem'; import RadarModel from './RadarModel'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { ScaleDataValue } from '../../util/types'; import { ParsedModelFinder } from '../../util/model'; -import { parseAxisModelMinMax } from '../scaleRawExtentInfo'; import { map, each } from 'zrender/src/core/util'; import { alignScaleTicks } from '../../scale/helper'; diff --git a/src/scale/helper.ts b/src/scale/helper.ts index bd1ce7829c..8c752fc8cf 100644 --- a/src/scale/helper.ts +++ b/src/scale/helper.ts @@ -18,17 +18,14 @@ */ import { NumericAxisBaseOptionCommon } from '../coord/axisCommonTypes'; -import * as numberUtil from '../util/number'; +import {getPrecisionSafe, getPrecision, round, nice, quantityExponent} from '../util/number'; import IntervalScale from './Interval'; -import { parseAxisModelMinMax } from '../coord/scaleRawExtentInfo'; -import { ScaleDataValue } from '../util/types'; -import { getScaleExtent, niceScaleExtent } from '../coord/axisHelper'; +import { getScaleExtent } from '../coord/axisHelper'; import { AxisBaseModel } from '../coord/AxisBaseModel'; import LogScale from './Log'; import Scale from './Scale'; import { warn } from '../util/log'; -const roundNumber = numberUtil.round; const mathLog = Math.log; type intervalScaleNiceTicksResult = { @@ -37,6 +34,16 @@ type intervalScaleNiceTicksResult = { niceTickExtent: [number, number] }; +function isValueNice(val: number) { + const exp10 = Math.pow(10, quantityExponent(Math.abs(val))); + const f = Math.abs(val / exp10); + return f === 0 + || f === 1 + || f === 2 + || f === 3 + || f === 5; +} + export function isIntervalOrLogScale(scale: Scale): scale is LogScale | IntervalScale { return scale.type === 'interval' || scale.type === 'log'; } @@ -55,7 +62,7 @@ export function intervalScaleNiceTicks( const result = {} as intervalScaleNiceTicksResult; const span = extent[1] - extent[0]; - let interval = result.interval = numberUtil.nice(span / splitNumber, true); + let interval = result.interval = nice(span / splitNumber, true); if (minInterval != null && interval < minInterval) { interval = result.interval = minInterval; } @@ -66,8 +73,8 @@ export function intervalScaleNiceTicks( const precision = result.intervalPrecision = getIntervalPrecision(interval); // Niced extent inside original extent const niceTickExtent = result.niceTickExtent = [ - roundNumber(Math.ceil(extent[0] / interval) * interval, precision), - roundNumber(Math.floor(extent[1] / interval) * interval, precision) + round(Math.ceil(extent[0] / interval) * interval, precision), + round(Math.floor(extent[1] / interval) * interval, precision) ]; fixExtent(niceTickExtent, extent); @@ -76,7 +83,7 @@ export function intervalScaleNiceTicks( } function increaseInterval(interval: number) { - const exp10 = Math.pow(10, numberUtil.quantityExponent(interval)); + const exp10 = Math.pow(10, quantityExponent(interval)); // Increase interval let f = interval / exp10; if (!f) { @@ -111,14 +118,24 @@ export function alignScaleTicks( const alignToSplitNumber = alignToTicks.length - 1; const alignToInterval = intervalScaleProto.getInterval.call(alignToScale); - const fixedMin = parseAxisModelMinMax(scale, axisModel.get('min', true) as ScaleDataValue); - const fixedMax = parseAxisModelMinMax(scale, axisModel.get('max', true) as ScaleDataValue); + const scaleExtent = getScaleExtent(scale, axisModel); + let rawExtent = scaleExtent.extent; + const isMinFixed = scaleExtent.fixMin; + const isMaxFixed = scaleExtent.fixMax; + + if (scale.type === 'log') { + const logBase = mathLog((scale as LogScale).base); + rawExtent = [mathLog(rawExtent[0]) / logBase, mathLog(rawExtent[1]) / logBase]; + } - niceScaleExtent(scale, axisModel); + scale.setExtent(rawExtent[0], rawExtent[1]); + scale.calcNiceExtent({ + splitNumber: alignToSplitNumber, + fixMin: isMinFixed, + fixMax: isMaxFixed + }); const extent = intervalScaleProto.getExtent.call(scale); - const rawExtent = getScaleExtent(scale, axisModel).extent; - const isMinFixed = fixedMin != null; - const isMaxFixed = fixedMax != null; + // Need to update the rawExtent. // Because value in rawExtent may be not parsed. e.g. 'dataMin', 'dataMax' if (isMinFixed) { @@ -157,18 +174,20 @@ export function alignScaleTicks( if (nicedSplitNumber > alignToSplitNumber) { interval = increaseInterval(interval); } - max = Math.ceil(rawExtent[1] / interval) * interval; - min = numberUtil.round(max - interval * alignToSplitNumber); + const range = interval * alignToSplitNumber; + max = Math.ceil(rawExtent[1] / interval) * interval; + min = round(max - range); // Not change the result that crossing zero. if (min < 0 && rawExtent[0] >= 0) { min = 0; - max = numberUtil.round(interval * alignToSplitNumber); + max = round(range); } else if (max > 0 && rawExtent[1] <= 0) { max = 0; - min = numberUtil.round((-interval * alignToSplitNumber)); + min = -round(range); } + } // Adjust min, max based on the extent of alignTo. When min or max is set in alignTo scale @@ -184,13 +203,12 @@ export function alignScaleTicks( if (__DEV__) { const ticks = intervalScaleProto.getTicks.call(scale); - // if (ticks.length !== alignToScale.getTicks().length) { - // debugger - // } - if (ticks[1] && ticks[1].value % interval) { + if (ticks[1] + && (!isValueNice(interval) || getPrecisionSafe(ticks[1].value) > getPrecisionSafe(interval)) + ) { warn( // eslint-disable-next-line - `The ticks may be not displayed nice if when set min: ${fixedMin}, max: ${fixedMax} and alignTicks: true` + `The ticks may be not displayed nice if when set min: ${axisModel.get('min')}, max: ${axisModel.get('max')} and alignTicks: true` ); } } @@ -201,7 +219,7 @@ export function alignScaleTicks( */ export function getIntervalPrecision(interval: number): number { // Tow more digital for tick. - return numberUtil.getPrecision(interval) + 2; + return getPrecision(interval) + 2; } function clamp( diff --git a/src/util/number.ts b/src/util/number.ts index 20c1ac9cb2..2ec02f3f8d 100644 --- a/src/util/number.ts +++ b/src/util/number.ts @@ -483,20 +483,6 @@ export function nice(val: number, round?: boolean): number { return exponent >= -20 ? +val.toFixed(exponent < 0 ? -exponent : 0) : val; } -// /** -// * Check if value is a nice number. -// * @param val -// */ -// export function isValueNice(val: number) { -// const exp10 = Math.pow(10, quantityExponent(Math.abs(val))); -// const f = Math.abs(val / exp10); -// return f === 0 -// || f === 1 -// || f === 2 -// || f === 3 -// || f === 5; -// } - /** * This code was copied from "d3.js" * . diff --git a/test/axis-align-ticks.html b/test/axis-align-ticks.html index 1b38f8f154..c1e62500be 100644 --- a/test/axis-align-ticks.html +++ b/test/axis-align-ticks.html @@ -38,44 +38,28 @@
- - - +
+
+
+
+
+
+ + + + + + + + + + + + + From b8f1d34246f956f942db4a86939c94fb08cbb096 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 29 Dec 2021 14:14:14 +0800 Subject: [PATCH 4/7] feat(alignTicks): fix unit test --- test/ut/spec/scale/interval.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ut/spec/scale/interval.test.ts b/test/ut/spec/scale/interval.test.ts index 67965d7792..60481156b8 100755 --- a/test/ut/spec/scale/interval.test.ts +++ b/test/ut/spec/scale/interval.test.ts @@ -135,7 +135,7 @@ describe('scale_interval', function () { const interval = new IntervalScale(); interval.setExtent(extent[0], extent[1]); - interval.niceExtent({ + interval.calcNiceExtent({ fixMin: true, fixMax: true, splitNumber From d9a6ba8ee6b9e193023630bcccf2670db9583a44 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 29 Dec 2021 14:18:58 +0800 Subject: [PATCH 5/7] test(alignTicks): add vrt recording --- test/runTest/actions/__meta__.json | 1 + test/runTest/actions/axis-align-ticks.json | 1 + 2 files changed, 2 insertions(+) create mode 100644 test/runTest/actions/axis-align-ticks.json diff --git a/test/runTest/actions/__meta__.json b/test/runTest/actions/__meta__.json index 047c3de96c..60cd43e8c9 100644 --- a/test/runTest/actions/__meta__.json +++ b/test/runTest/actions/__meta__.json @@ -8,6 +8,7 @@ "aria-pie": 2, "axes": 0, "axis": 1, + "axis-align-ticks": 4, "axis-boundaryGap": 1, "axis-interval": 3, "axis-interval2": 3, diff --git a/test/runTest/actions/axis-align-ticks.json b/test/runTest/actions/axis-align-ticks.json new file mode 100644 index 0000000000..e69e61501f --- /dev/null +++ b/test/runTest/actions/axis-align-ticks.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousedown","time":331,"x":288,"y":71},{"type":"mouseup","time":425,"x":288,"y":71},{"time":426,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":570,"x":288,"y":71},{"type":"mousemove","time":770,"x":375,"y":70},{"type":"mousedown","time":976,"x":381,"y":70},{"type":"mousemove","time":981,"x":381,"y":70},{"type":"mouseup","time":1065,"x":381,"y":70},{"time":1066,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1337,"x":383,"y":70},{"type":"mousemove","time":1537,"x":482,"y":69},{"type":"mousemove","time":1742,"x":485,"y":69},{"type":"mousedown","time":1758,"x":485,"y":69},{"type":"mouseup","time":1832,"x":485,"y":69},{"time":1833,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2087,"x":484,"y":69},{"type":"mousemove","time":2287,"x":345,"y":61},{"type":"mousemove","time":2487,"x":346,"y":64},{"type":"mousemove","time":2692,"x":354,"y":66},{"type":"mousedown","time":2726,"x":354,"y":66},{"type":"mouseup","time":2790,"x":354,"y":66},{"time":2791,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2903,"x":352,"y":66},{"type":"mousemove","time":3103,"x":330,"y":68},{"type":"mousemove","time":3308,"x":311,"y":65},{"type":"mousedown","time":3629,"x":311,"y":65},{"type":"mouseup","time":3692,"x":311,"y":65},{"time":3693,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4104,"x":312,"y":65},{"type":"mousemove","time":4307,"x":423,"y":68},{"type":"mousemove","time":4520,"x":474,"y":67},{"type":"mousedown","time":4595,"x":474,"y":67},{"type":"mouseup","time":4676,"x":474,"y":67},{"time":4677,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5703,"x":474,"y":67}],"scrollY":0,"scrollX":0,"timestamp":1640758619162},{"name":"Action 2","ops":[{"type":"mousedown","time":292,"x":300,"y":139},{"type":"mouseup","time":409,"x":300,"y":139},{"time":410,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":663,"x":301,"y":139},{"type":"mousemove","time":867,"x":466,"y":141},{"type":"mousemove","time":1165,"x":473,"y":139},{"type":"mousedown","time":1192,"x":473,"y":139},{"type":"mouseup","time":1298,"x":473,"y":139},{"time":1299,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1512,"x":472,"y":139},{"type":"mousemove","time":1717,"x":405,"y":136},{"type":"mousedown","time":1875,"x":405,"y":136},{"type":"mouseup","time":1950,"x":405,"y":136},{"time":1951,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":2676,"x":405,"y":136},{"type":"mouseup","time":2784,"x":405,"y":136},{"time":2785,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2962,"x":404,"y":136},{"type":"mousemove","time":3162,"x":346,"y":137},{"type":"mousemove","time":3368,"x":309,"y":138},{"type":"mousedown","time":3489,"x":309,"y":138},{"type":"mouseup","time":3592,"x":309,"y":138},{"time":3593,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3896,"x":309,"y":138},{"type":"mousemove","time":4096,"x":425,"y":135},{"type":"mousemove","time":4301,"x":465,"y":137},{"type":"mousemove","time":4516,"x":475,"y":137},{"type":"mousedown","time":4636,"x":475,"y":137},{"type":"mouseup","time":4717,"x":475,"y":137},{"time":4718,"delay":400,"type":"screenshot-auto"}],"scrollY":408,"scrollX":0,"timestamp":1640758633004},{"name":"Action 3","ops":[{"type":"mousedown","time":430,"x":301,"y":94},{"type":"mouseup","time":535,"x":301,"y":94},{"time":536,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":821,"x":302,"y":94},{"type":"mousemove","time":1021,"x":378,"y":95},{"type":"mousemove","time":1221,"x":385,"y":96},{"type":"mousedown","time":1297,"x":385,"y":96},{"type":"mousemove","time":1338,"x":385,"y":96},{"type":"mouseup","time":1369,"x":385,"y":97},{"time":1370,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1543,"x":386,"y":97},{"type":"mousemove","time":1621,"x":386,"y":96},{"type":"mousemove","time":1827,"x":402,"y":95},{"type":"mousedown","time":2243,"x":402,"y":95},{"type":"mouseup","time":2325,"x":402,"y":95},{"time":2326,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2455,"x":402,"y":95},{"type":"mousemove","time":2655,"x":365,"y":103},{"type":"mousemove","time":2859,"x":331,"y":104},{"type":"mousemove","time":3072,"x":323,"y":103},{"type":"mousedown","time":3297,"x":323,"y":103},{"type":"mouseup","time":3376,"x":323,"y":103},{"time":3377,"delay":400,"type":"screenshot-auto"}],"scrollY":907,"scrollX":0,"timestamp":1640758671814},{"name":"Action 4","ops":[{"type":"mousedown","time":230,"x":553,"y":567},{"type":"mousemove","time":324,"x":553,"y":567},{"type":"mousemove","time":525,"x":425,"y":566},{"type":"mouseup","time":617,"x":420,"y":566},{"time":618,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":733,"x":420,"y":566},{"type":"mousedown","time":1117,"x":420,"y":566},{"type":"mousemove","time":1191,"x":420,"y":566},{"type":"mousemove","time":1398,"x":250,"y":569},{"type":"mousemove","time":1622,"x":202,"y":572},{"type":"mouseup","time":1683,"x":202,"y":572},{"time":1684,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2241,"x":201,"y":572},{"type":"mousedown","time":2267,"x":201,"y":572},{"type":"mousemove","time":2441,"x":148,"y":576},{"type":"mousemove","time":2641,"x":120,"y":576},{"type":"mousemove","time":2892,"x":118,"y":576},{"type":"mousemove","time":3096,"x":67,"y":580},{"type":"mouseup","time":3183,"x":67,"y":580},{"time":3184,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3275,"x":67,"y":580},{"type":"mousemove","time":3475,"x":66,"y":581},{"type":"mousemove","time":3675,"x":162,"y":586},{"type":"mousemove","time":3875,"x":196,"y":581},{"type":"mousemove","time":4075,"x":214,"y":577},{"type":"mousemove","time":4276,"x":207,"y":578},{"type":"mousemove","time":4541,"x":206,"y":578},{"type":"mousemove","time":4742,"x":202,"y":579},{"type":"mousedown","time":4807,"x":202,"y":579},{"type":"mousemove","time":4858,"x":202,"y":579},{"type":"mousemove","time":5058,"x":712,"y":572},{"type":"mousemove","time":5259,"x":766,"y":570}],"scrollY":2166,"scrollX":0,"timestamp":1640758695695}] \ No newline at end of file From 8be090782d1e806fc1cceaa9ab0a451986febff6 Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 29 Dec 2021 16:30:41 +0800 Subject: [PATCH 6/7] test: remove case --- test/auto-align.html | 1270 ------------------------------------------ 1 file changed, 1270 deletions(-) delete mode 100644 test/auto-align.html diff --git a/test/auto-align.html b/test/auto-align.html deleted file mode 100644 index 97b21d01b9..0000000000 --- a/test/auto-align.html +++ /dev/null @@ -1,1270 +0,0 @@ - - - - - - - - - - - - - - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - - \ No newline at end of file From 26230d24754235dfc55952a0e902349e69b30e8d Mon Sep 17 00:00:00 2001 From: pissang Date: Wed, 29 Dec 2021 16:59:10 +0800 Subject: [PATCH 7/7] style: tweak warn log --- src/scale/helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scale/helper.ts b/src/scale/helper.ts index 8c752fc8cf..a04ec3899a 100644 --- a/src/scale/helper.ts +++ b/src/scale/helper.ts @@ -208,7 +208,7 @@ export function alignScaleTicks( ) { warn( // eslint-disable-next-line - `The ticks may be not displayed nice if when set min: ${axisModel.get('min')}, max: ${axisModel.get('max')} and alignTicks: true` + `The ticks may be not readable when set min: ${axisModel.get('min')}, max: ${axisModel.get('max')} and alignTicks: true` ); } }