Skip to content

Commit d168bf2

Browse files
committed
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.
1 parent 479dcd4 commit d168bf2

8 files changed

Lines changed: 270 additions & 146 deletions

File tree

‎src/component/dataZoom/AxisProxy.ts‎

Lines changed: 134 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,24 @@
1717
* under the License.
1818
*/
1919

20-
import*aszrUtilfrom'zrender/src/core/util';
21-
import*asnumberUtilfrom'../../util/number';
20+
import{clone,defaults,each,map}from'zrender/src/core/util';
21+
import{
22+
asc,getAcceptableTickPrecision,linearMap,mathAbs,mathCeil,mathFloor,mathMax,mathMin,round
23+
}from'../../util/number';
2224
importsliderMovefrom'../helper/sliderMove';
2325
importGlobalModelfrom'../../model/Global';
2426
importSeriesModelfrom'../../model/Series';
2527
importExtensionAPIfrom'../../core/ExtensionAPI';
26-
import{Dictionary}from'../../util/types';
28+
import{Dictionary,NullUndefined}from'../../util/types';
2729
// TODO Polar?
2830
importDataZoomModelfrom'./DataZoomModel';
2931
import{AxisBaseModel}from'../../coord/AxisBaseModel';
3032
import{unionAxisExtentFromData}from'../../coord/axisHelper';
3133
import{ensureScaleRawExtentInfo}from'../../coord/scaleRawExtentInfo';
3234
import{getAxisMainType,isCoordSupported,DataZoomAxisDimension}from'./helper';
3335
import{SINGLE_REFERRING}from'../../util/model';
36+
import{isOrdinalScale,isTimeScale}from'../../scale/helper';
3437

35-
consteach=zrUtil.each;
36-
constasc=numberUtil.asc;
3738

3839
interfaceMinMaxSpan{
3940
minSpan: number
@@ -42,6 +43,17 @@ interface MinMaxSpan {
4243
maxValueSpan: number
4344
}
4445

46+
interfaceAxisProxyWindow{
47+
value: [number,number];
48+
percent: [number,number];
49+
// Percent invert from "value window", which may be slightly different from "percent window" due to some
50+
// handling such as rounding. The difference may be magnified in cases like "alignTicks", so we use
51+
// `percentInverted` in these cases.
52+
// But we retain the original input percent in `percent` whenever possible, since they have been used in views.
53+
percentInverted: [number,number];
54+
valuePrecision: number;
55+
}
56+
4557
/**
4658
* Operate single axis.
4759
* One axis can only operated by one axis operator.
@@ -56,13 +68,16 @@ class AxisProxy {
5668
private_dimName: DataZoomAxisDimension;
5769
private_axisIndex: number;
5870

59-
private_valueWindow: [number,number];
60-
private_percentWindow: [number,number];
71+
private_window: AxisProxyWindow;
6172

6273
private_dataExtent: [number,number];
6374

6475
private_minMaxSpan: MinMaxSpan;
6576

77+
/**
78+
* The host `dataZoom` model. An axis may be controlled by multiple `dataZoom`s,
79+
* but only the first declared `dataZoom` is the host.
80+
*/
6681
private_dataZoomModel: DataZoomModel;
6782

6883
constructor(
@@ -94,17 +109,10 @@ class AxisProxy {
94109
}
95110

96111
/**
97-
* @returnValue can only be NaN or finite value.
112+
* @return`getWindow().value` can only have NaN or finite value.
98113
*/
99-
getDataValueWindow(){
100-
returnthis._valueWindow.slice()as[number,number];
101-
}
102-
103-
/**
104-
* @return {Array.<number>}
105-
*/
106-
getDataPercentWindow(){
107-
returnthis._percentWindow.slice()as[number,number];
114+
getWindow(): AxisProxyWindow{
115+
returnclone(this._window);
108116
}
109117

110118
getTargetSeriesModels(){
@@ -128,26 +136,31 @@ class AxisProxy {
128136
}
129137

130138
getMinMaxSpan(){
131-
returnzrUtil.clone(this._minMaxSpan);
139+
returnclone(this._minMaxSpan);
132140
}
133141

134142
/**
143+
* [CAVEAT] Keep this method pure, so that it can be called multiple times.
144+
*
135145
* Only calculate by given range and this._dataExtent, do not change anything.
136146
*/
137-
calculateDataWindow(opt?: {
138-
start?: number
139-
end?: number
140-
startValue?: number|string|Date
141-
endValue?: number|string|Date
142-
}){
147+
calculateDataWindow(
148+
opt: {
149+
start?: number// percent, 0 ~ 100
150+
end?: number// percent, 0 ~ 100
151+
startValue?: number|string|Date
152+
endValue?: number|string|Date
153+
}
154+
): AxisProxyWindow{
143155
constdataExtent=this._dataExtent;
144-
constaxisModel=this.getAxisModel();
145-
constscale=axisModel.axis.scale;
156+
constaxis=this.getAxisModel().axis;
157+
constscale=axis.scale;
146158
constrangePropMode=this._dataZoomModel.getRangePropMode();
147159
constpercentExtent=[0,100];
148160
constpercentWindow=[]asunknownas[number,number];
149161
constvalueWindow=[]asunknownas[number,number];
150162
lethasPropModeValue;
163+
constneedRound=[false,false];
151164

152165
each(['start','end']asconst,function(prop,idx){
153166
letboundPercent=opt[prop];
@@ -169,24 +182,19 @@ class AxisProxy {
169182

170183
if(rangePropMode[idx]==='percent'){
171184
boundPercent==null&&(boundPercent=percentExtent[idx]);
172-
// Use scale.parse to math round for category or time axis.
173-
boundValue=scale.parse(numberUtil.linearMap(
174-
boundPercent,percentExtent,dataExtent
175-
));
185+
boundValue=linearMap(boundPercent,percentExtent,dataExtent);
186+
needRound[idx]=true;
176187
}
177188
else{
178189
hasPropModeValue=true;
190+
// NOTE: `scale.parse` can also round input for 'time' or 'ordinal' scale.
179191
boundValue=boundValue==null ? dataExtent[idx] : scale.parse(boundValue);
180192
// Calculating `percent` from `value` may be not accurate, because
181-
// This calculation can not be inversed, because all of values that
193+
// This calculation can not be inverted, because all of values that
182194
// are overflow the `dataExtent` will be calculated to percent '100%'
183-
boundPercent=numberUtil.linearMap(
184-
boundValue,dataExtent,percentExtent
185-
);
195+
boundPercent=linearMap(boundValue,dataExtent,percentExtent);
186196
}
187197

188-
// valueWindow[idx] = round(boundValue);
189-
// percentWindow[idx] = round(boundPercent);
190198
// fallback to extent start/end when parsed value or percent is invalid
191199
valueWindow[idx]=boundValue==null||isNaN(boundValue)
192200
? dataExtent[idx]
@@ -199,11 +207,17 @@ class AxisProxy {
199207
asc(valueWindow);
200208
asc(percentWindow);
201209

202-
// The windows from user calling of `dispatchAction` might be out of the extent,
203-
// or do not obey the `min/maxSpan`, `min/maxValueSpan`. But we don't restrict window
204-
// by `zoomLock` here, because we see `zoomLock` just as a interaction constraint,
205-
// where API is able to initialize/modify the window size even though `zoomLock`
206-
// specified.
210+
// The windows specified from `dispatchAction` or `setOption` may:
211+
// (1) be out of the extent, or
212+
// (2) do not comply with `minSpan/maxSpan`, `minValueSpan/maxValueSpan`.
213+
// So we clamp them here.
214+
// But we don't restrict window by `zoomLock` here, because we see `zoomLock` just as a
215+
// interaction constraint, where API is able to initialize/modify the window size even
216+
// though `zoomLock` specified.
217+
// PENDING: For historical reason, the option design is partially incompatible:
218+
// If `option.start` and `option.endValue` are specified, and when we choose whether
219+
// `min/maxValueSpan` or `minSpan/maxSpan` is applied, neither one is intuitive.
220+
// (Currently using `minValueSpan/maxValueSpan`.)
207221
constspans=this._minMaxSpan;
208222
hasPropModeValue
209223
? restrictSet(valueWindow,percentWindow,dataExtent,percentExtent,false)
@@ -223,14 +237,67 @@ class AxisProxy {
223237
spans['max'+suffixas'maxSpan'|'maxValueSpan']
224238
);
225239
for(leti=0;i<2;i++){
226-
toWindow[i]=numberUtil.linearMap(fromWindow[i],fromExtent,toExtent,true);
227-
toValue&&(toWindow[i]=scale.parse(toWindow[i]));
240+
toWindow[i]=linearMap(fromWindow[i],fromExtent,toExtent,true);
241+
if(toValue){
242+
toWindow[i]=toWindow[i];
243+
needRound[i]=true;
244+
}
245+
}
246+
simplyEnsureAsc(toWindow);
247+
}
248+
249+
// - In 'time' and 'ordinal' scale, rounding by 0 is required.
250+
// - In 'interval' and 'log' scale, we round values for acceptable display with acceptable accuracy loose.
251+
// "Values" can be rounded only if they are generated from `percent`, since user-specified "value"
252+
// should be respected, and `DataZoomSelect` already performs its own rounding.
253+
// - Currently we only round "value" but not "percent", since there is no need so far.
254+
// - MEMO: See also #3228 and commit a89fd0d7f1833ecf08a4a5b7ecf651b4a0d8da41
255+
// - PENDING: The rounding result may slightly overflow the restriction from `min/maxSpan`,
256+
// but it is acceptable so far.
257+
constisScaleOrdinalOrTime=isOrdinalScale(scale)||isTimeScale(scale);
258+
// Typically pxExtent has been ready in coordSys create. (See `create` of `Grid.ts`)
259+
constpxExtent=axis.getExtent();
260+
// NOTICE: this pxSpan may be not accurate yet due to "outerBounds" logic, but acceptable.
261+
constpxSpan=mathAbs(pxExtent[1]-pxExtent[0]);
262+
constprecision=isScaleOrdinalOrTime
263+
? 0
264+
: getAcceptableTickPrecision(valueWindow[1]-valueWindow[0],pxSpan,0.5);
265+
each([[0,mathCeil],[1,mathFloor]]asconst,function([idx,ceilOrFloor]){
266+
if(!needRound[idx]||!isFinite(precision)){
267+
return;
268+
}
269+
valueWindow[idx]=round(valueWindow[idx],precision);
270+
valueWindow[idx]=mathMin(dataExtent[1],mathMax(dataExtent[0],valueWindow[idx]));// Clamp.
271+
if(percentWindow[idx]===percentExtent[idx]){
272+
// When `percent` is 0 or 100, `value` must be `dataExtent[0]` or `dataExtent[1]`
273+
// regardless of the calculated precision.
274+
// NOTE: `percentWindow` is never over [0, 100] at this moment.
275+
valueWindow[idx]=dataExtent[idx];
276+
if(isScaleOrdinalOrTime){
277+
// In case that dataExtent[idx] is not an integer (may occur since it comes from user input)
278+
valueWindow[idx]=ceilOrFloor(valueWindow[idx]);
279+
}
280+
}
281+
});
282+
simplyEnsureAsc(valueWindow);
283+
284+
constpercentInvertedWindow=[
285+
linearMap(valueWindow[0],dataExtent,percentExtent,true),
286+
linearMap(valueWindow[1],dataExtent,percentExtent,true),
287+
]as[number,number];
288+
simplyEnsureAsc(percentInvertedWindow);
289+
290+
functionsimplyEnsureAsc(window: number[]): void{
291+
if(window[0]>window[1]){
292+
window[0]=window[1];
228293
}
229294
}
230295

231296
return{
232-
valueWindow: valueWindow,
233-
percentWindow: percentWindow
297+
value: valueWindow,
298+
percent: percentWindow,
299+
percentInverted: percentInvertedWindow,
300+
valuePrecision: precision,
234301
};
235302
}
236303

@@ -239,8 +306,8 @@ class AxisProxy {
239306
* so it is recommended to be called in "process stage" but not "model init
240307
* stage".
241308
*/
242-
reset(dataZoomModel: DataZoomModel){
243-
if(dataZoomModel!==this._dataZoomModel){
309+
reset(dataZoomModel: DataZoomModel,alignToPercentInverted: [number,number]|NullUndefined){
310+
if(!this.hostedBy(dataZoomModel)){
244311
return;
245312
}
246313

@@ -251,24 +318,28 @@ class AxisProxy {
251318
// `calculateDataWindow` uses min/maxSpan.
252319
this._updateMinMaxSpan();
253320

254-
constdataWindow=this.calculateDataWindow(dataZoomModel.settledOption);
255-
256-
this._valueWindow=dataWindow.valueWindow;
257-
this._percentWindow=dataWindow.percentWindow;
321+
letopt=dataZoomModel.settledOption;
322+
if(alignToPercentInverted){
323+
opt=defaults({
324+
start: alignToPercentInverted[0],
325+
end: alignToPercentInverted[1],
326+
},opt);
327+
}
328+
this._window=this.calculateDataWindow(opt);
258329

259330
// Update axis setting then.
260331
this._setAxisModel();
261332
}
262333

263334
filterData(dataZoomModel: DataZoomModel,api: ExtensionAPI){
264-
if(dataZoomModel!==this._dataZoomModel){
335+
if(!this.hostedBy(dataZoomModel)){
265336
return;
266337
}
267338

268339
constaxisDim=this._dimName;
269340
constseriesModels=this.getTargetSeriesModels();
270341
constfilterMode=dataZoomModel.get('filterMode');
271-
constvalueWindow=this._valueWindow;
342+
constvalueWindow=this._window.value;
272343

273344
if(filterMode==='none'){
274345
return;
@@ -305,7 +376,7 @@ class AxisProxy {
305376

306377
if(filterMode==='weakFilter'){
307378
conststore=seriesData.getStore();
308-
constdataDimIndices=zrUtil.map(dataDims,dim=>seriesData.getDimensionIndex(dim),seriesData);
379+
constdataDimIndices=map(dataDims,dim=>seriesData.getDimensionIndex(dim),seriesData);
309380
seriesData.filterSelf(function(dataIndex){
310381
letleftOut;
311382
letrightOut;
@@ -368,12 +439,12 @@ class AxisProxy {
368439

369440
// minValueSpan and maxValueSpan has higher priority than minSpan and maxSpan
370441
if(valueSpan!=null){
371-
percentSpan=numberUtil.linearMap(
442+
percentSpan=linearMap(
372443
dataExtent[0]+valueSpan,dataExtent,[0,100],true
373444
);
374445
}
375446
elseif(percentSpan!=null){
376-
valueSpan=numberUtil.linearMap(
447+
valueSpan=linearMap(
377448
percentSpan,[0,100],dataExtent,true
378449
)-dataExtent[0];
379450
}
@@ -387,27 +458,22 @@ class AxisProxy {
387458

388459
constaxisModel=this.getAxisModel();
389460

390-
constpercentWindow=this._percentWindow;
391-
constvalueWindow=this._valueWindow;
392-
393-
if(!percentWindow){
461+
constwindow=this._window;
462+
if(!window){
394463
return;
395464
}
396-
397-
// [0, 500]: arbitrary value, guess axis extent.
398-
letprecision=numberUtil.getPixelPrecision(valueWindow,[0,500]);
399-
precision=Math.min(precision,20);
465+
const{percent, value}=window;
400466

401467
// For value axis, if min/max/scale are not set, we just use the extent obtained
402468
// by series data, which may be a little different from the extent calculated by
403469
// `axisHelper.getScaleExtent`. But the different just affects the experience a
404470
// little when zooming. So it will not be fixed until some users require it strongly.
405471
constrawExtentInfo=axisModel.axis.scale.rawExtentInfo;
406-
if(percentWindow[0]!==0){
407-
rawExtentInfo.setDeterminedMinMax('min',+valueWindow[0].toFixed(precision));
472+
if(percent[0]!==0){
473+
rawExtentInfo.setDeterminedMinMax('min',value[0]);
408474
}
409-
if(percentWindow[1]!==100){
410-
rawExtentInfo.setDeterminedMinMax('max',+valueWindow[1].toFixed(precision));
475+
if(percent[1]!==100){
476+
rawExtentInfo.setDeterminedMinMax('max',value[1]);
411477
}
412478
rawExtentInfo.freeze();
413479
}

0 commit comments

Comments
 (0)