You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by su...@apache.org on 2026/03/15 20:09:33 UTC
(echarts) 04/32: fix(axisTick&dataZoom): (1) Apply a better auto-precision method. (2) Make the rounding result consistent between dataZoom calculated window and specified axis `determinedMin/Max`. (3) Fix unexpected behaviors when dataZoom controls axes with `alignTicks: true` - previous they are not precisely aligned and the ticks jump significantly due to inappropriate rounding when dataZoom dragging.
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 d168bf237a442c2254fd494d6ae76e4e4712deff
Author: 100pah <su...@gmail.com>
AuthorDate: Fri Jan 9 16:44:16 2026 +0800
fix(axisTick&dataZoom): (1) Apply a better auto-precision method. (2) Make the rounding result consistent between dataZoom calculated window and specified axis `determinedMin/Max`. (3) Fix unexpected behaviors when dataZoom controls axes with `alignTicks: true` - previous they are not precisely aligned and the ticks jump significantly due to inappropriate rounding when dataZoom dragging.
---
src/component/dataZoom/AxisProxy.ts | 202 ++++++++++++++++++----------
src/component/dataZoom/DataZoomModel.ts | 26 ++--
src/component/dataZoom/SliderZoomView.ts | 58 ++++----
src/component/dataZoom/dataZoomProcessor.ts | 31 +++--
src/component/dataZoom/helper.ts | 26 +++-
src/component/helper/sliderMove.ts | 19 ++-
src/component/toolbox/feature/DataZoom.ts | 39 ++++--
src/coord/Axis.ts | 15 +--
8 files changed, 270 insertions(+), 146 deletions(-)
diff --git a/src/component/dataZoom/AxisProxy.ts b/src/component/dataZoom/AxisProxy.ts
index 11d5f6c74..cabe1db8c 100644
--- a/src/component/dataZoom/AxisProxy.ts
+++ b/src/component/dataZoom/AxisProxy.ts
@@ -17,13 +17,15 @@
* under the License.
*/
-import * as zrUtil from 'zrender/src/core/util';
-import * as numberUtil from '../../util/number';
+import {clone, defaults, each, map} from 'zrender/src/core/util';
+import {
+ asc, getAcceptableTickPrecision, linearMap, mathAbs, mathCeil, mathFloor, mathMax, mathMin, round
+} from '../../util/number';
import sliderMove from '../helper/sliderMove';
import GlobalModel from '../../model/Global';
import SeriesModel from '../../model/Series';
import ExtensionAPI from '../../core/ExtensionAPI';
-import { Dictionary } from '../../util/types';
+import { Dictionary, NullUndefined } from '../../util/types';
// TODO Polar?
import DataZoomModel from './DataZoomModel';
import { AxisBaseModel } from '../../coord/AxisBaseModel';
@@ -31,9 +33,8 @@ import { unionAxisExtentFromData } from '../../coord/axisHelper';
import { ensureScaleRawExtentInfo } from '../../coord/scaleRawExtentInfo';
import { getAxisMainType, isCoordSupported, DataZoomAxisDimension } from './helper';
import { SINGLE_REFERRING } from '../../util/model';
+import { isOrdinalScale, isTimeScale } from '../../scale/helper';
-const each = zrUtil.each;
-const asc = numberUtil.asc;
interface MinMaxSpan {
minSpan: number
@@ -42,6 +43,17 @@ interface MinMaxSpan {
maxValueSpan: number
}
+interface AxisProxyWindow {
+ value: [number, number];
+ percent: [number, number];
+ // Percent invert from "value window", which may be slightly different from "percent window" due to some
+ // handling such as rounding. The difference may be magnified in cases like "alignTicks", so we use
+ // `percentInverted` in these cases.
+ // But we retain the original input percent in `percent` whenever possible, since they have been used in views.
+ percentInverted: [number, number];
+ valuePrecision: number;
+}
+
/**
* Operate single axis.
* One axis can only operated by one axis operator.
@@ -56,13 +68,16 @@ class AxisProxy {
private _dimName: DataZoomAxisDimension;
private _axisIndex: number;
- private _valueWindow: [number, number];
- private _percentWindow: [number, number];
+ private _window: AxisProxyWindow;
private _dataExtent: [number, number];
private _minMaxSpan: MinMaxSpan;
+ /**
+ * The host `dataZoom` model. An axis may be controlled by multiple `dataZoom`s,
+ * but only the first declared `dataZoom` is the host.
+ */
private _dataZoomModel: DataZoomModel;
constructor(
@@ -94,17 +109,10 @@ class AxisProxy {
}
/**
- * @return Value can only be NaN or finite value.
+ * @return `getWindow().value` can only have NaN or finite value.
*/
- getDataValueWindow() {
- return this._valueWindow.slice() as [number, number];
- }
-
- /**
- * @return {Array.<number>}
- */
- getDataPercentWindow() {
- return this._percentWindow.slice() as [number, number];
+ getWindow(): AxisProxyWindow {
+ return clone(this._window);
}
getTargetSeriesModels() {
@@ -128,26 +136,31 @@ class AxisProxy {
}
getMinMaxSpan() {
- return zrUtil.clone(this._minMaxSpan);
+ return clone(this._minMaxSpan);
}
/**
+ * [CAVEAT] Keep this method pure, so that it can be called multiple times.
+ *
* Only calculate by given range and this._dataExtent, do not change anything.
*/
- calculateDataWindow(opt?: {
- start?: number
- end?: number
- startValue?: number | string | Date
- endValue?: number | string | Date
- }) {
+ calculateDataWindow(
+ opt: {
+ start?: number // percent, 0 ~ 100
+ end?: number // percent, 0 ~ 100
+ startValue?: number | string | Date
+ endValue?: number | string | Date
+ }
+ ): AxisProxyWindow {
const dataExtent = this._dataExtent;
- const axisModel = this.getAxisModel();
- const scale = axisModel.axis.scale;
+ const axis = this.getAxisModel().axis;
+ const scale = axis.scale;
const rangePropMode = this._dataZoomModel.getRangePropMode();
const percentExtent = [0, 100];
const percentWindow = [] as unknown as [number, number];
const valueWindow = [] as unknown as [number, number];
let hasPropModeValue;
+ const needRound = [false, false];
each(['start', 'end'] as const, function (prop, idx) {
let boundPercent = opt[prop];
@@ -169,24 +182,19 @@ class AxisProxy {
if (rangePropMode[idx] === 'percent') {
boundPercent == null && (boundPercent = percentExtent[idx]);
- // Use scale.parse to math round for category or time axis.
- boundValue = scale.parse(numberUtil.linearMap(
- boundPercent, percentExtent, dataExtent
- ));
+ boundValue = linearMap(boundPercent, percentExtent, dataExtent);
+ needRound[idx] = true;
}
else {
hasPropModeValue = true;
+ // NOTE: `scale.parse` can also round input for 'time' or 'ordinal' scale.
boundValue = boundValue == null ? dataExtent[idx] : scale.parse(boundValue);
// Calculating `percent` from `value` may be not accurate, because
- // This calculation can not be inversed, because all of values that
+ // This calculation can not be inverted, because all of values that
// are overflow the `dataExtent` will be calculated to percent '100%'
- boundPercent = numberUtil.linearMap(
- boundValue, dataExtent, percentExtent
- );
+ boundPercent = linearMap(boundValue, dataExtent, percentExtent);
}
- // valueWindow[idx] = round(boundValue);
- // percentWindow[idx] = round(boundPercent);
// fallback to extent start/end when parsed value or percent is invalid
valueWindow[idx] = boundValue == null || isNaN(boundValue)
? dataExtent[idx]
@@ -199,11 +207,17 @@ class AxisProxy {
asc(valueWindow);
asc(percentWindow);
- // The windows from user calling of `dispatchAction` might be out of the extent,
- // or do not obey the `min/maxSpan`, `min/maxValueSpan`. But we don't restrict window
- // by `zoomLock` here, because we see `zoomLock` just as a interaction constraint,
- // where API is able to initialize/modify the window size even though `zoomLock`
- // specified.
+ // The windows specified from `dispatchAction` or `setOption` may:
+ // (1) be out of the extent, or
+ // (2) do not comply with `minSpan/maxSpan`, `minValueSpan/maxValueSpan`.
+ // So we clamp them here.
+ // But we don't restrict window by `zoomLock` here, because we see `zoomLock` just as a
+ // interaction constraint, where API is able to initialize/modify the window size even
+ // though `zoomLock` specified.
+ // PENDING: For historical reason, the option design is partially incompatible:
+ // If `option.start` and `option.endValue` are specified, and when we choose whether
+ // `min/maxValueSpan` or `minSpan/maxSpan` is applied, neither one is intuitive.
+ // (Currently using `minValueSpan/maxValueSpan`.)
const spans = this._minMaxSpan;
hasPropModeValue
? restrictSet(valueWindow, percentWindow, dataExtent, percentExtent, false)
@@ -223,14 +237,67 @@ class AxisProxy {
spans['max' + suffix as 'maxSpan' | 'maxValueSpan']
);
for (let i = 0; i < 2; i++) {
- toWindow[i] = numberUtil.linearMap(fromWindow[i], fromExtent, toExtent, true);
- toValue && (toWindow[i] = scale.parse(toWindow[i]));
+ toWindow[i] = linearMap(fromWindow[i], fromExtent, toExtent, true);
+ if (toValue) {
+ toWindow[i] = toWindow[i];
+ needRound[i] = true;
+ }
+ }
+ simplyEnsureAsc(toWindow);
+ }
+
+ // - In 'time' and 'ordinal' scale, rounding by 0 is required.
+ // - In 'interval' and 'log' scale, we round values for acceptable display with acceptable accuracy loose.
+ // "Values" can be rounded only if they are generated from `percent`, since user-specified "value"
+ // should be respected, and `DataZoomSelect` already performs its own rounding.
+ // - Currently we only round "value" but not "percent", since there is no need so far.
+ // - MEMO: See also #3228 and commit a89fd0d7f1833ecf08a4a5b7ecf651b4a0d8da41
+ // - PENDING: The rounding result may slightly overflow the restriction from `min/maxSpan`,
+ // but it is acceptable so far.
+ const isScaleOrdinalOrTime = isOrdinalScale(scale) || isTimeScale(scale);
+ // Typically pxExtent has been ready in coordSys create. (See `create` of `Grid.ts`)
+ const pxExtent = axis.getExtent();
+ // NOTICE: this pxSpan may be not accurate yet due to "outerBounds" logic, but acceptable.
+ const pxSpan = mathAbs(pxExtent[1] - pxExtent[0]);
+ const precision = isScaleOrdinalOrTime
+ ? 0
+ : getAcceptableTickPrecision(valueWindow[1] - valueWindow[0], pxSpan, 0.5);
+ each([[0, mathCeil], [1, mathFloor]] as const, function ([idx, ceilOrFloor]) {
+ if (!needRound[idx] || !isFinite(precision)) {
+ return;
+ }
+ valueWindow[idx] = round(valueWindow[idx], precision);
+ valueWindow[idx] = mathMin(dataExtent[1], mathMax(dataExtent[0], valueWindow[idx])); // Clamp.
+ if (percentWindow[idx] === percentExtent[idx]) {
+ // When `percent` is 0 or 100, `value` must be `dataExtent[0]` or `dataExtent[1]`
+ // regardless of the calculated precision.
+ // NOTE: `percentWindow` is never over [0, 100] at this moment.
+ valueWindow[idx] = dataExtent[idx];
+ if (isScaleOrdinalOrTime) {
+ // In case that dataExtent[idx] is not an integer (may occur since it comes from user input)
+ valueWindow[idx] = ceilOrFloor(valueWindow[idx]);
+ }
+ }
+ });
+ simplyEnsureAsc(valueWindow);
+
+ const percentInvertedWindow = [
+ linearMap(valueWindow[0], dataExtent, percentExtent, true),
+ linearMap(valueWindow[1], dataExtent, percentExtent, true),
+ ] as [number, number];
+ simplyEnsureAsc(percentInvertedWindow);
+
+ function simplyEnsureAsc(window: number[]): void {
+ if (window[0] > window[1]) {
+ window[0] = window[1];
}
}
return {
- valueWindow: valueWindow,
- percentWindow: percentWindow
+ value: valueWindow,
+ percent: percentWindow,
+ percentInverted: percentInvertedWindow,
+ valuePrecision: precision,
};
}
@@ -239,8 +306,8 @@ class AxisProxy {
* so it is recommended to be called in "process stage" but not "model init
* stage".
*/
- reset(dataZoomModel: DataZoomModel) {
- if (dataZoomModel !== this._dataZoomModel) {
+ reset(dataZoomModel: DataZoomModel, alignToPercentInverted: [number, number] | NullUndefined) {
+ if (!this.hostedBy(dataZoomModel)) {
return;
}
@@ -251,24 +318,28 @@ class AxisProxy {
// `calculateDataWindow` uses min/maxSpan.
this._updateMinMaxSpan();
- const dataWindow = this.calculateDataWindow(dataZoomModel.settledOption);
-
- this._valueWindow = dataWindow.valueWindow;
- this._percentWindow = dataWindow.percentWindow;
+ let opt = dataZoomModel.settledOption;
+ if (alignToPercentInverted) {
+ opt = defaults({
+ start: alignToPercentInverted[0],
+ end: alignToPercentInverted[1],
+ }, opt);
+ }
+ this._window = this.calculateDataWindow(opt);
// Update axis setting then.
this._setAxisModel();
}
filterData(dataZoomModel: DataZoomModel, api: ExtensionAPI) {
- if (dataZoomModel !== this._dataZoomModel) {
+ if (!this.hostedBy(dataZoomModel)) {
return;
}
const axisDim = this._dimName;
const seriesModels = this.getTargetSeriesModels();
const filterMode = dataZoomModel.get('filterMode');
- const valueWindow = this._valueWindow;
+ const valueWindow = this._window.value;
if (filterMode === 'none') {
return;
@@ -305,7 +376,7 @@ class AxisProxy {
if (filterMode === 'weakFilter') {
const store = seriesData.getStore();
- const dataDimIndices = zrUtil.map(dataDims, dim => seriesData.getDimensionIndex(dim), seriesData);
+ const dataDimIndices = map(dataDims, dim => seriesData.getDimensionIndex(dim), seriesData);
seriesData.filterSelf(function (dataIndex) {
let leftOut;
let rightOut;
@@ -368,12 +439,12 @@ class AxisProxy {
// minValueSpan and maxValueSpan has higher priority than minSpan and maxSpan
if (valueSpan != null) {
- percentSpan = numberUtil.linearMap(
+ percentSpan = linearMap(
dataExtent[0] + valueSpan, dataExtent, [0, 100], true
);
}
else if (percentSpan != null) {
- valueSpan = numberUtil.linearMap(
+ valueSpan = linearMap(
percentSpan, [0, 100], dataExtent, true
) - dataExtent[0];
}
@@ -387,27 +458,22 @@ class AxisProxy {
const axisModel = this.getAxisModel();
- const percentWindow = this._percentWindow;
- const valueWindow = this._valueWindow;
-
- if (!percentWindow) {
+ const window = this._window;
+ if (!window) {
return;
}
-
- // [0, 500]: arbitrary value, guess axis extent.
- let precision = numberUtil.getPixelPrecision(valueWindow, [0, 500]);
- precision = Math.min(precision, 20);
+ const {percent, value} = window;
// For value axis, if min/max/scale are not set, we just use the extent obtained
// by series data, which may be a little different from the extent calculated by
// `axisHelper.getScaleExtent`. But the different just affects the experience a
// little when zooming. So it will not be fixed until some users require it strongly.
const rawExtentInfo = axisModel.axis.scale.rawExtentInfo;
- if (percentWindow[0] !== 0) {
- rawExtentInfo.setDeterminedMinMax('min', +valueWindow[0].toFixed(precision));
+ if (percent[0] !== 0) {
+ rawExtentInfo.setDeterminedMinMax('min', value[0]);
}
- if (percentWindow[1] !== 100) {
- rawExtentInfo.setDeterminedMinMax('max', +valueWindow[1].toFixed(precision));
+ if (percent[1] !== 100) {
+ rawExtentInfo.setDeterminedMinMax('max', value[1]);
}
rawExtentInfo.freeze();
}
diff --git a/src/component/dataZoom/DataZoomModel.ts b/src/component/dataZoom/DataZoomModel.ts
index 279c06a13..c83e166cb 100644
--- a/src/component/dataZoom/DataZoomModel.ts
+++ b/src/component/dataZoom/DataZoomModel.ts
@@ -23,13 +23,15 @@ import ComponentModel from '../../model/Component';
import {
LayoutOrient,
ComponentOption,
- LabelOption
+ LabelOption,
+ NullUndefined
} from '../../util/types';
import Model from '../../model/Model';
import GlobalModel from '../../model/Global';
import { AxisBaseModel } from '../../coord/AxisBaseModel';
import {
- getAxisMainType, DATA_ZOOM_AXIS_DIMENSIONS, DataZoomAxisDimension
+ getAxisMainType, DATA_ZOOM_AXIS_DIMENSIONS, DataZoomAxisDimension,
+ getAxisProxyFromModel
} from './helper';
import SingleAxisModel from '../../coord/single/AxisModel';
import { MULTIPLE_REFERRING, SINGLE_REFERRING, ModelFinderIndexQuery, ModelFinderIdQuery } from '../../util/model';
@@ -131,15 +133,11 @@ export interface DataZoomOption extends ComponentOption {
type RangeOption = Pick<DataZoomOption, 'start' | 'end' | 'startValue' | 'endValue'>;
-export type DataZoomExtendedAxisBaseModel = AxisBaseModel & {
- __dzAxisProxy: AxisProxy
-};
-
class DataZoomAxisInfo {
indexList: number[] = [];
indexMap: boolean[] = [];
- add(axisCmptIdx: number) {
+ add(axisCmptIdx: ComponentModel['componentIndex']): void {
// Remove duplication.
if (!this.indexMap[axisCmptIdx]) {
this.indexList.push(axisCmptIdx);
@@ -456,10 +454,7 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
* @return If not found, return null/undefined.
*/
getAxisProxy(axisDim: DataZoomAxisDimension, axisIndex: number): AxisProxy {
- const axisModel = this.getAxisModel(axisDim, axisIndex);
- if (axisModel) {
- return (axisModel as DataZoomExtendedAxisBaseModel).__dzAxisProxy;
- }
+ return getAxisProxyFromModel(this.getAxisModel(axisDim, axisIndex));
}
/**
@@ -510,7 +505,7 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
getPercentRange(): number[] {
const axisProxy = this.findRepresentativeAxisProxy();
if (axisProxy) {
- return axisProxy.getDataPercentWindow();
+ return axisProxy.getWindow().percent;
}
}
@@ -523,11 +518,11 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
if (axisDim == null && axisIndex == null) {
const axisProxy = this.findRepresentativeAxisProxy();
if (axisProxy) {
- return axisProxy.getDataValueWindow();
+ return axisProxy.getWindow().value;
}
}
else {
- return this.getAxisProxy(axisDim, axisIndex).getDataValueWindow();
+ return this.getAxisProxy(axisDim, axisIndex).getWindow().value;
}
}
@@ -537,7 +532,7 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
*/
findRepresentativeAxisProxy(axisModel?: AxisBaseModel): AxisProxy {
if (axisModel) {
- return (axisModel as DataZoomExtendedAxisBaseModel).__dzAxisProxy;
+ return getAxisProxyFromModel(axisModel);
}
// Find the first hosted axisProxy
@@ -576,6 +571,7 @@ class DataZoomModel<Opts extends DataZoomOption = DataZoomOption> extends Compon
}
}
+
/**
* Retrieve those raw params from option, which will be cached separately,
* because they will be overwritten by normalized/calculated values in the main
diff --git a/src/component/dataZoom/SliderZoomView.ts b/src/component/dataZoom/SliderZoomView.ts
index 30422c9c5..ad82f0fca 100644
--- a/src/component/dataZoom/SliderZoomView.ts
+++ b/src/component/dataZoom/SliderZoomView.ts
@@ -22,7 +22,7 @@ import * as eventTool from 'zrender/src/core/event';
import * as graphic from '../../util/graphic';
import * as throttle from '../../util/throttle';
import DataZoomView from './DataZoomView';
-import { linearMap, asc, parsePercent } from '../../util/number';
+import { linearMap, asc, parsePercent, round } from '../../util/number';
import * as layout from '../../util/layout';
import sliderMove from '../helper/sliderMove';
import GlobalModel from '../../model/Global';
@@ -35,7 +35,7 @@ import { RectLike } from 'zrender/src/core/BoundingRect';
import Axis from '../../coord/Axis';
import SeriesModel from '../../model/Series';
import { AxisBaseModel } from '../../coord/AxisBaseModel';
-import { getAxisMainType, collectReferCoordSysModelInfo } from './helper';
+import { getAxisMainType, collectReferCoordSysModelInfo, getAlignTo } from './helper';
import { enableHoverEmphasis } from '../../util/states';
import { createSymbol, symbolBuildProxies } from '../../util/symbol';
import { deprecateLog } from '../../util/log';
@@ -44,6 +44,8 @@ import Displayable from 'zrender/src/graphic/Displayable';
import { createTextStyle } from '../../label/labelStyle';
import SeriesData from '../../data/SeriesData';
import tokens from '../../visual/tokens';
+import type AxisProxy from './AxisProxy';
+import { isOrdinalScale, isTimeScale } from '../../scale/helper';
const Rect = graphic.Rect;
@@ -816,30 +818,35 @@ class SliderZoomView extends DataZoomView {
private _updateDataInfo(nonRealtime?: boolean) {
const dataZoomModel = this.dataZoomModel;
- const displaybles = this._displayables;
- const handleLabels = displaybles.handleLabels;
+ const displayables = this._displayables;
+ const handleLabels = displayables.handleLabels;
const orient = this._orient;
let labelTexts = ['', ''];
- // FIXME
- // date型,支持formatter,autoformatter(ec2 date.getAutoFormatter)
if (dataZoomModel.get('showDetail')) {
const axisProxy = dataZoomModel.findRepresentativeAxisProxy();
if (axisProxy) {
- const axis = axisProxy.getAxisModel().axis;
const range = this._range;
- const dataInterval = nonRealtime
+ let dataInterval: [number, number];
+ if (nonRealtime) {
// See #4434, data and axis are not processed and reset yet in non-realtime mode.
- ? axisProxy.calculateDataWindow({
- start: range[0], end: range[1]
- }).valueWindow
- : axisProxy.getDataValueWindow();
+ let calcWinInput = {start: range[0], end: range[1]};
+ const alignTo = getAlignTo(dataZoomModel, axisProxy);
+ if (alignTo) {
+ const alignToWindow = alignTo.calculateDataWindow(calcWinInput).percentInverted;
+ calcWinInput = {start: alignToWindow[0], end: alignToWindow[1]};
+ }
+ dataInterval = axisProxy.calculateDataWindow(calcWinInput).value;
+ }
+ else {
+ dataInterval = axisProxy.getWindow().value;
+ }
labelTexts = [
- this._formatLabel(dataInterval[0], axis),
- this._formatLabel(dataInterval[1], axis)
+ this._formatLabel(dataInterval[0], axisProxy),
+ this._formatLabel(dataInterval[1], axisProxy)
];
}
}
@@ -854,7 +861,7 @@ class SliderZoomView extends DataZoomView {
// Text should not transform by barGroup.
// Ignore handlers transform
const barTransform = graphic.getTransform(
- displaybles.handles[handleIndex].parent, this.group
+ displayables.handles[handleIndex].parent, this.group
);
const direction = graphic.transformDirection(
handleIndex === 0 ? 'right' : 'left', barTransform
@@ -877,27 +884,26 @@ class SliderZoomView extends DataZoomView {
}
}
- private _formatLabel(value: ParsedValue, axis: Axis) {
+ private _formatLabel(value: number, axisProxy: AxisProxy) {
const dataZoomModel = this.dataZoomModel;
const labelFormatter = dataZoomModel.get('labelFormatter');
let labelPrecision = dataZoomModel.get('labelPrecision');
if (labelPrecision == null || labelPrecision === 'auto') {
- labelPrecision = axis.getPixelPrecision();
+ labelPrecision = axisProxy.getWindow().valuePrecision;
}
- const valueStr = (value == null || isNaN(value as number))
+ const scale = axisProxy.getAxisModel().axis.scale;
+ const valueStr = (value == null || isNaN(value))
? ''
- // FIXME Glue code
- : (axis.type === 'category' || axis.type === 'time')
- ? axis.scale.getLabel({
- value: Math.round(value as number)
- })
- // param of toFixed should less then 20.
- : (value as number).toFixed(Math.min(labelPrecision as number, 20));
+ : (isOrdinalScale(scale) || isTimeScale(scale))
+ ? scale.getLabel({value: Math.round(value)})
+ : isFinite(labelPrecision)
+ ? round(value, labelPrecision, true)
+ : value + '';
return isFunction(labelFormatter)
- ? labelFormatter(value as number, valueStr)
+ ? labelFormatter(value, valueStr)
: isString(labelFormatter)
? labelFormatter.replace('{value}', valueStr)
: valueStr;
diff --git a/src/component/dataZoom/dataZoomProcessor.ts b/src/component/dataZoom/dataZoomProcessor.ts
index f511aaed3..d64269295 100644
--- a/src/component/dataZoom/dataZoomProcessor.ts
+++ b/src/component/dataZoom/dataZoomProcessor.ts
@@ -19,11 +19,12 @@
import {createHashMap, each} from 'zrender/src/core/util';
import SeriesModel from '../../model/Series';
-import DataZoomModel, { DataZoomExtendedAxisBaseModel } from './DataZoomModel';
-import { getAxisMainType, DataZoomAxisDimension } from './helper';
+import DataZoomModel from './DataZoomModel';
+import { getAxisMainType, DataZoomAxisDimension, DataZoomExtendedAxisBaseModel, getAlignTo } from './helper';
import AxisProxy from './AxisProxy';
import { StageHandler } from '../../util/types';
+
const dataZoomProcessor: StageHandler = {
// `dataZoomProcessor` will only be performed in needed series. Consider if
@@ -54,7 +55,7 @@ const dataZoomProcessor: StageHandler = {
});
const proxyList: AxisProxy[] = [];
eachAxisModel(function (axisDim, axisIndex, axisModel, dataZoomModel) {
- // Different dataZooms may constrol the same axis. In that case,
+ // Different dataZooms may control the same axis. In that case,
// an axisProxy serves both of them.
if (!axisModel.__dzAxisProxy) {
// Use the first dataZoomModel as the main model of axisProxy.
@@ -82,8 +83,19 @@ const dataZoomProcessor: StageHandler = {
// We calculate window and reset axis here but not in model
// init stage and not after action dispatch handler, because
// reset should be called after seriesData.restoreData.
+ const axisProxyNeedAlign: [AxisProxy, AxisProxy][] = [];
dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
- dataZoomModel.getAxisProxy(axisDim, axisIndex).reset(dataZoomModel);
+ const axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex);
+ const alignToAxisProxy = getAlignTo(dataZoomModel, axisProxy);
+ if (alignToAxisProxy) {
+ axisProxyNeedAlign.push([axisProxy, alignToAxisProxy]);
+ }
+ else {
+ axisProxy.reset(dataZoomModel, null);
+ }
+ });
+ each(axisProxyNeedAlign, function (item) {
+ item[0].reset(dataZoomModel, item[1].getWindow().percentInverted);
});
// Caution: data zoom filtering is order sensitive when using
@@ -110,14 +122,13 @@ const dataZoomProcessor: StageHandler = {
// is able to get them from chart.getOption().
const axisProxy = dataZoomModel.findRepresentativeAxisProxy();
if (axisProxy) {
- const percentRange = axisProxy.getDataPercentWindow();
- const valueRange = axisProxy.getDataValueWindow();
+ const {percent, value} = axisProxy.getWindow();
dataZoomModel.setCalculatedRange({
- start: percentRange[0],
- end: percentRange[1],
- startValue: valueRange[0],
- endValue: valueRange[1]
+ start: percent[0],
+ end: percent[1],
+ startValue: value[0],
+ endValue: value[1]
});
}
});
diff --git a/src/component/dataZoom/helper.ts b/src/component/dataZoom/helper.ts
index 4b0a8a31e..e2009cb76 100644
--- a/src/component/dataZoom/helper.ts
+++ b/src/component/dataZoom/helper.ts
@@ -17,13 +17,14 @@
* under the License.
*/
-import { Payload } from '../../util/types';
+import { NullUndefined, Payload } from '../../util/types';
import GlobalModel from '../../model/Global';
import DataZoomModel from './DataZoomModel';
import { indexOf, createHashMap, assert, HashMap } from 'zrender/src/core/util';
import SeriesModel from '../../model/Series';
import { CoordinateSystemHostModel } from '../../coord/CoordinateSystem';
import { AxisBaseModel } from '../../coord/AxisBaseModel';
+import type AxisProxy from './AxisProxy';
export interface DataZoomPayloadBatchItem {
@@ -43,6 +44,10 @@ export interface DataZoomReferCoordSysInfo {
axisModels: AxisBaseModel[];
}
+export type DataZoomExtendedAxisBaseModel = AxisBaseModel & {
+ __dzAxisProxy: AxisProxy
+};
+
export const DATA_ZOOM_AXIS_DIMENSIONS = [
'x', 'y', 'radius', 'angle', 'single'
] as const;
@@ -205,3 +210,22 @@ export function collectReferCoordSysModelInfo(dataZoomModel: DataZoomModel): {
return coordSysInfoWrap;
}
+
+export function getAxisProxyFromModel(axisModel: AxisBaseModel): AxisProxy | NullUndefined {
+ return axisModel && (axisModel as DataZoomExtendedAxisBaseModel).__dzAxisProxy;
+}
+
+/**
+ * NOTICE: If `axis_a` aligns to `axis_b`, but they are not controlled by
+ * the same `dataZoom`, do not consider `axis_b` as `alignTo` and
+ * then do not input it into `AxisProxy#reset`.
+ */
+export function getAlignTo(dataZoomModel: DataZoomModel, axisProxy: AxisProxy): AxisProxy | NullUndefined {
+ const alignToAxis = axisProxy.getAxisModel().axis.alignTo;
+ return (
+ alignToAxis && dataZoomModel.getAxisProxy(
+ alignToAxis.dim as DataZoomAxisDimension,
+ alignToAxis.model.componentIndex
+ )
+ ) ? getAxisProxyFromModel(alignToAxis.model) : null;
+}
diff --git a/src/component/helper/sliderMove.ts b/src/component/helper/sliderMove.ts
index 902312d30..721f6704a 100644
--- a/src/component/helper/sliderMove.ts
+++ b/src/component/helper/sliderMove.ts
@@ -17,6 +17,8 @@
* under the License.
*/
+import { addSafe } from '../../util/number';
+
/**
* Calculate slider move result.
* Usage:
@@ -24,6 +26,9 @@
* maxSpan and the same as `Math.abs(handleEnd[1] - handleEnds[0])`.
* (2) If handle0 is forbidden to cross handle1, set minSpan as `0`.
*
+ * [CAVEAT]
+ * This method is inefficient due to the use of `addSafe`.
+ *
* @param delta Move length.
* @param handleEnds handleEnds[0] can be bigger then handleEnds[1].
* handleEnds will be modified in this method.
@@ -48,7 +53,9 @@ export default function sliderMove(
delta = delta || 0;
- const extentSpan = extent[1] - extent[0];
+ // Consider `7.1e-9 - 7e-9` get `1.0000000000000007e-10`, so use `addSafe`
+ // to remove rounding error whenever possible.
+ const extentSpan = addSafe(extent[1], -extent[0]);
// Notice maxSpan and minSpan can be null/undefined.
if (minSpan != null) {
@@ -58,7 +65,7 @@ export default function sliderMove(
maxSpan = Math.max(maxSpan, minSpan != null ? minSpan : 0);
}
if (handleIndex === 'all') {
- let handleSpan = Math.abs(handleEnds[1] - handleEnds[0]);
+ let handleSpan = Math.abs(addSafe(handleEnds[1], -handleEnds[0]));
handleSpan = restrict(handleSpan, [0, extentSpan]);
minSpan = maxSpan = restrict(handleSpan, [minSpan, maxSpan]);
handleIndex = 0;
@@ -74,7 +81,9 @@ export default function sliderMove(
// Restrict in extent.
const extentMinSpan = minSpan || 0;
const realExtent = extent.slice();
- originalDistSign.sign < 0 ? (realExtent[0] += extentMinSpan) : (realExtent[1] -= extentMinSpan);
+ originalDistSign.sign < 0
+ ? (realExtent[0] = addSafe(realExtent[0], extentMinSpan))
+ : (realExtent[1] = addSafe(realExtent[1], -extentMinSpan));
handleEnds[handleIndex] = restrict(handleEnds[handleIndex], realExtent);
// Expand span.
@@ -84,13 +93,13 @@ export default function sliderMove(
currDistSign.sign !== originalDistSign.sign || currDistSign.span < minSpan
)) {
// If minSpan exists, 'cross' is forbidden.
- handleEnds[1 - handleIndex] = handleEnds[handleIndex] + originalDistSign.sign * minSpan;
+ handleEnds[1 - handleIndex] = addSafe(handleEnds[handleIndex], originalDistSign.sign * minSpan);
}
// Shrink span.
currDistSign = getSpanSign(handleEnds, handleIndex);
if (maxSpan != null && currDistSign.span > maxSpan) {
- handleEnds[1 - handleIndex] = handleEnds[handleIndex] + currDistSign.sign * maxSpan;
+ handleEnds[1 - handleIndex] = addSafe(handleEnds[handleIndex], currDistSign.sign * maxSpan);
}
return handleEnds;
diff --git a/src/component/toolbox/feature/DataZoom.ts b/src/component/toolbox/feature/DataZoom.ts
index e51ace23c..85a1727e8 100644
--- a/src/component/toolbox/feature/DataZoom.ts
+++ b/src/component/toolbox/feature/DataZoom.ts
@@ -35,9 +35,7 @@ import { Payload, Dictionary, ComponentOption, ItemStyleOption } from '../../../
import Cartesian2D from '../../../coord/cartesian/Cartesian2D';
import CartesianAxisModel from '../../../coord/cartesian/AxisModel';
import DataZoomModel from '../../dataZoom/DataZoomModel';
-import {
- DataZoomPayloadBatchItem, DataZoomAxisDimension
-} from '../../dataZoom/helper';
+import {DataZoomPayloadBatchItem} from '../../dataZoom/helper';
import {
ModelFinderObject, ModelFinderIndexQuery, makeInternalComponentId,
ModelFinderIdQuery, parseFinder, ParsedModelFinderKnown
@@ -46,6 +44,8 @@ import ToolboxModel from '../ToolboxModel';
import { registerInternalOptionCreator } from '../../../model/internalComponentCreator';
import ComponentModel from '../../../model/Component';
import tokens from '../../../visual/tokens';
+import BoundingRect from 'zrender/src/core/BoundingRect';
+import { getAcceptableTickPrecision, round } from '../../../util/number';
const each = zrUtil.each;
@@ -55,6 +55,9 @@ const DATA_ZOOM_ID_BASE = makeInternalComponentId('toolbox-dataZoom_');
const ICON_TYPES = ['zoom', 'back'] as const;
type IconType = typeof ICON_TYPES[number];
+const XY2WH = {x: 'width', y: 'height'} as const;
+
+
export interface ToolboxDataZoomFeatureOption extends ToolboxFeatureOption {
type?: IconType[]
icon?: {[key in IconType]?: string}
@@ -135,15 +138,18 @@ class DataZoomFeature extends ToolboxFeature<ToolboxDataZoomFeatureOption> {
return;
}
+ const coordSysRect = coordSys.master.getRect().clone();
+
const brushType = area.brushType;
if (brushType === 'rect') {
- setBatch('x', coordSys, (coordRange as BrushDimensionMinMax[])[0]);
- setBatch('y', coordSys, (coordRange as BrushDimensionMinMax[])[1]);
+ setBatch('x', coordSys, coordSysRect, (coordRange as BrushDimensionMinMax[])[0]);
+ setBatch('y', coordSys, coordSysRect, (coordRange as BrushDimensionMinMax[])[1]);
}
else {
setBatch(
({lineX: 'x', lineY: 'y'} as const)[brushType as 'lineX' | 'lineY'],
coordSys,
+ coordSysRect,
coordRange as BrushDimensionMinMax
);
}
@@ -153,29 +159,42 @@ class DataZoomFeature extends ToolboxFeature<ToolboxDataZoomFeatureOption> {
this._dispatchZoomAction(snapshot);
- function setBatch(dimName: DataZoomAxisDimension, coordSys: Cartesian2D, minMax: number[]) {
+ function setBatch(
+ dimName: 'x' | 'y',
+ coordSys: Cartesian2D,
+ coordSysRect: BoundingRect,
+ minMax: number[]
+ ) {
const axis = coordSys.getAxis(dimName);
const axisModel = axis.model;
const dataZoomModel = findDataZoom(dimName, axisModel, ecModel);
// Restrict range.
const minMaxSpan = dataZoomModel.findRepresentativeAxisProxy(axisModel).getMinMaxSpan();
+ const scaleExtent = axis.scale.getExtent();
if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) {
minMax = sliderMove(
- 0, minMax.slice(), axis.scale.getExtent(), 0,
+ 0, minMax.slice(), scaleExtent, 0,
minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan
);
}
+ // Round for displayable.
+ const precision = getAcceptableTickPrecision(
+ scaleExtent[1] - scaleExtent[0],
+ coordSysRect[XY2WH[dimName]],
+ 0.5
+ );
+
dataZoomModel && (snapshot[dataZoomModel.id] = {
dataZoomId: dataZoomModel.id,
- startValue: minMax[0],
- endValue: minMax[1]
+ startValue: isFinite(precision) ? round(minMax[0], precision) : minMax[0],
+ endValue: isFinite(precision) ? round(minMax[1], precision) : minMax[1]
});
}
function findDataZoom(
- dimName: DataZoomAxisDimension, axisModel: CartesianAxisModel, ecModel: GlobalModel
+ dimName: 'x' | 'y', axisModel: CartesianAxisModel, ecModel: GlobalModel
): DataZoomModel {
let found;
ecModel.eachComponent({mainType: 'dataZoom', subType: 'select'}, function (dzModel: DataZoomModel) {
diff --git a/src/coord/Axis.ts b/src/coord/Axis.ts
index 6226e96b2..0390875e2 100644
--- a/src/coord/Axis.ts
+++ b/src/coord/Axis.ts
@@ -18,7 +18,7 @@
*/
import {each, map} from 'zrender/src/core/util';
-import {linearMap, getPixelPrecision, round} from '../util/number';
+import {linearMap, round} from '../util/number';
import {
createAxisTicks,
createAxisLabels,
@@ -80,6 +80,9 @@ class Axis {
// `inverse` can be inferred by `extent` unless `extent[0] === extent[1]`.
inverse: AxisBaseOption['inverse'] = false;
+ // Injected outside
+ alignTo: Axis;
+
constructor(dim: DimensionName, scale: Scale, extent: [number, number]) {
this.dim = dim;
@@ -111,16 +114,6 @@ class Axis {
return this._extent.slice() as [number, number];
}
- /**
- * Get precision used for formatting
- */
- getPixelPrecision(dataExtent?: [number, number]): number {
- return getPixelPrecision(
- dataExtent || this.scale.getExtent(),
- this._extent
- );
- }
-
/**
* Set coord extent
*/
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org