Skip to content

Commit dedc5dc

Browse files
committed
fix(logScale): (1) Thoroughly resolve a long-standing issue of non-positive data on LogScale - exclude non-positive series data items when calculate dataExtent on LogScale. (2) Include Infinite into connectNulls handling on line series; the Infinite value may be generated by log(0) and previously the corresponding effect in unpredictable on line series (sometimes display as connected but sometimes not).
1 parent 0f4561d commit dedc5dc

19 files changed

Lines changed: 341 additions & 235 deletions

‎src/chart/line/LineSeries.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ class LineSeriesModel extends SeriesModel<LineSeriesOption> {
209209
// follow the label interval strategy.
210210
showAllSymbol: 'auto',
211211

212-
// Whether to connect break point.
212+
// Whether to connect break point. (non-finite values)
213213
connectNulls: false,
214214

215215
// Sampling for large data. Can be: 'average', 'max', 'min', 'sum', 'lttb'.

‎src/chart/line/LineView.ts‎

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import * as graphic from '../../util/graphic';
2727
import*asmodelUtilfrom'../../util/model';
2828
import{ECPolyline,ECPolygon}from'./poly';
2929
importChartViewfrom'../../view/Chart';
30-
import{prepareDataCoordInfo,getStackedOnPoint}from'./helper';
30+
import{prepareDataCoordInfo,getStackedOnPoint,isPointIllegal}from'./helper';
3131
import{createGridClipPath,createPolarClipPath}from'../helper/createClipPathFromCoordSys';
3232
importLineSeriesModel,{LineSeriesOption}from'./LineSeries';
3333
importtypeGlobalModelfrom'../../model/Global';
@@ -84,42 +84,33 @@ function isPointsSame(points1: ArrayLike<number>, points2: ArrayLike<number>) {
8484
returntrue;
8585
}
8686

87-
functionbboxFromPoints(points: ArrayLike<number>){
88-
letminX=Infinity;
89-
letminY=Infinity;
90-
letmaxX=-Infinity;
91-
letmaxY=-Infinity;
87+
functionxyExtentFromPoints(points: ArrayLike<number>){
88+
constxExtent=modelUtil.initExtentForUnion();
89+
constyExtent=modelUtil.initExtentForUnion();
9290

9391
for(leti=0;i<points.length;){
9492
constx=points[i++];
9593
consty=points[i++];
96-
if(!isNaN(x)){
97-
minX=Math.min(x,minX);
98-
maxX=Math.max(x,maxX);
99-
}
100-
if(!isNaN(y)){
101-
minY=Math.min(y,minY);
102-
maxY=Math.max(y,maxY);
94+
if(!isPointIllegal(x,y)){
95+
modelUtil.unionExtent(xExtent,x);
96+
modelUtil.unionExtent(yExtent,y);
10397
}
10498
}
105-
return[
106-
[minX,minY],
107-
[maxX,maxY]
108-
];
99+
return[xExtent,yExtent];
109100
}
110101

111102
functiongetBoundingDiff(points1: ArrayLike<number>,points2: ArrayLike<number>): number{
112103

113-
const[min1,max1]=bboxFromPoints(points1);
114-
const[min2,max2]=bboxFromPoints(points2);
104+
const[xExtent1,yExtent1]=xyExtentFromPoints(points1);
105+
const[xExtent2,yExtent2]=xyExtentFromPoints(points2);
115106

116107
// Get a max value from each corner of two boundings.
117108
returnMath.max(
118-
Math.abs(min1[0]-min2[0]),
119-
Math.abs(min1[1]-min2[1]),
109+
Math.abs(xExtent1[0]-xExtent2[0]),
110+
Math.abs(yExtent1[0]-yExtent2[0]),
120111

121-
Math.abs(max1[0]-max2[0]),
122-
Math.abs(max1[1]-max2[1])
112+
Math.abs(xExtent1[1]-xExtent2[1]),
113+
Math.abs(yExtent1[1]-yExtent2[1])
123114
);
124115
}
125116

@@ -181,7 +172,7 @@ function turnPointsIntoStep(
181172
* should stay the same as the lines above. See #20021
182173
*/
183174
constreference=basePoints||points;
184-
if(!isNaN(reference[i])&&!isNaN(reference[i+1])){
175+
if(!isPointIllegal(reference[i],reference[i+1])){
185176
filteredPoints.push(points[i],points[i+1]);
186177
}
187178
}
@@ -447,15 +438,10 @@ function canShowAllSymbolForCategory(
447438
returntrue;
448439
}
449440

450-
451-
functionisPointNull(x: number,y: number){
452-
returnisNaN(x)||isNaN(y);
453-
}
454-
455441
functiongetLastIndexNotNull(points: ArrayLike<number>){
456442
letlen=points.length/2;
457443
for(;len>0;len--){
458-
if(!isPointNull(points[len*2-2],points[len*2-1])){
444+
if(!isPointIllegal(points[len*2-2],points[len*2-1])){
459445
break;
460446
}
461447
}
@@ -477,7 +463,7 @@ function getIndexRange(points: ArrayLike<number>, xOrY: number, dim: 'x' | 'y')
477463
letnextIndex=-1;
478464
for(leti=0;i<len;i++){
479465
b=points[i*2+dimIdx];
480-
if(isNaN(b)||isNaN(points[i*2+1-dimIdx])){
466+
if(isPointIllegal(b,points[i*2+1-dimIdx])){
481467
continue;
482468
}
483469
if(i===0){
@@ -965,7 +951,7 @@ class LineView extends ChartView {
965951
// Create a temporary symbol if it is not exists
966952
constx=points[dataIndex*2];
967953
consty=points[dataIndex*2+1];
968-
if(isNaN(x)||isNaN(y)){
954+
if(isPointIllegal(x,y)){
969955
// Null data
970956
return;
971957
}

‎src/chart/line/helper.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,13 @@ export function getStackedOnPoint(
132132

133133
returncoordSys.dataToPoint(stackedData);
134134
}
135+
136+
exportfunctionisPointIllegal(xOrY: number,yOrX: number){
137+
// NOTE:
138+
// - `NaN` point x/y may be generated by, e.g.,
139+
// original series data `NaN`, '-', `null`, `undefined`,
140+
// negative values in LogScale.
141+
// - `Infinite` point x/y may be generated by, e.g.,
142+
// original series data `Infinite`, `0` in LogScale.
143+
return!isFinite(xOrY)||!isFinite(yOrX);
144+
}

‎src/chart/line/poly.ts‎

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,11 @@ import Path, { PathProps } from 'zrender/src/graphic/Path';
2323
importPathProxyfrom'zrender/src/core/PathProxy';
2424
import{cubicRootAt,cubicAt}from'zrender/src/core/curve';
2525
importtokensfrom'../../visual/tokens';
26+
import{isPointIllegal}from'./helper';
2627

2728
constmathMin=Math.min;
2829
constmathMax=Math.max;
2930

30-
functionisPointNull(x: number,y: number){
31-
returnisNaN(x)||isNaN(y);
32-
}
33-
3431
/**
3532
* Draw smoothed line in non-monotone, in may cause undesired curve in extreme
3633
* situations. This should be used when points are non-monotone neither in x or
@@ -63,7 +60,7 @@ function drawSegment(
6360
if(idx>=allLen||idx<0){
6461
break;
6562
}
66-
if(isPointNull(x,y)){
63+
if(isPointIllegal(x,y)){
6764
if(connectNulls){
6865
idx+=dir;
6966
continue;
@@ -106,7 +103,7 @@ function drawSegment(
106103
lettmpK=k+1;
107104
if(connectNulls){
108105
// Find next point not null
109-
while(isPointNull(nextX,nextY)&&tmpK<segLen){
106+
while(isPointIllegal(nextX,nextY)&&tmpK<segLen){
110107
tmpK++;
111108
nextIdx+=dir;
112109
nextX=points[nextIdx*2];
@@ -120,7 +117,7 @@ function drawSegment(
120117
letnextCpx0;
121118
letnextCpy0;
122119
// Is last point
123-
if(tmpK>=segLen||isPointNull(nextX,nextY)){
120+
if(tmpK>=segLen||isPointIllegal(nextX,nextY)){
124121
cpx1=x;
125122
cpy1=y;
126123
}
@@ -256,12 +253,12 @@ export class ECPolyline extends Path<ECPolylineProps> {
256253
if(shape.connectNulls){
257254
// Must remove first and last null values avoid draw error in polygon
258255
for(;len>0;len--){
259-
if(!isPointNull(points[len*2-2],points[len*2-1])){
256+
if(!isPointIllegal(points[len*2-2],points[len*2-1])){
260257
break;
261258
}
262259
}
263260
for(;i<len;i++){
264-
if(!isPointNull(points[i*2],points[i*2+1])){
261+
if(!isPointIllegal(points[i*2],points[i*2+1])){
265262
break;
266263
}
267264
}
@@ -380,12 +377,12 @@ export class ECPolygon extends Path {
380377
if(shape.connectNulls){
381378
// Must remove first and last null values avoid draw error in polygon
382379
for(;len>0;len--){
383-
if(!isPointNull(points[len*2-2],points[len*2-1])){
380+
if(!isPointIllegal(points[len*2-2],points[len*2-1])){
384381
break;
385382
}
386383
}
387384
for(;i<len;i++){
388-
if(!isPointNull(points[i*2],points[i*2+1])){
385+
if(!isPointIllegal(points[i*2],points[i*2+1])){
389386
break;
390387
}
391388
}

‎src/coord/axisAlignTicks.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ export function alignScaleTicks(
159159
consttargetRawPowExtent=targetRawExtent;
160160
if(isTargetLogScale){
161161
targetRawExtent=[
162-
logScaleLogTick(targetRawExtent[0],targetLogScaleBase,false),
163-
logScaleLogTick(targetRawExtent[1],targetLogScaleBase,false)
162+
logScaleLogTick(targetRawExtent[0],targetLogScaleBase),
163+
logScaleLogTick(targetRawExtent[1],targetLogScaleBase)
164164
];
165165
}
166166
consttargetExtent=intervalScaleEnsureValidExtent(targetRawExtent,targetMinMaxFixed);

‎src/coord/axisHelper.ts‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ import {
4646
importCartesianAxisModelfrom'./cartesian/AxisModel';
4747
importSeriesDatafrom'../data/SeriesData';
4848
import{getStackedDimension}from'../data/helper/dataStackHelper';
49-
import{Dictionary,DimensionName,NullUndefined,ScaleTick}from'../util/types';
50-
import{ensureScaleRawExtentInfo,ScaleRawExtentResult}from'./scaleRawExtentInfo';
49+
import{Dictionary,DimensionName,ScaleTick}from'../util/types';
50+
import{clampForLogScale,ensureScaleRawExtentInfo,ScaleRawExtentResult}from'./scaleRawExtentInfo';
5151
import{parseTimeAxisLabelFormatter}from'../util/time';
5252
import{getScaleBreakHelper}from'../scale/break';
5353
import{error}from'../util/log';
@@ -104,6 +104,11 @@ export function adoptScaleExtentOptionAndPrepare(
104104
}
105105
}
106106

107+
if(isLogScale(scale)){
108+
min=clampForLogScale(min);
109+
max=clampForLogScale(max);
110+
}
111+
107112
rawExtentResult.min=min;
108113
rawExtentResult.max=max;
109114

@@ -306,12 +311,6 @@ export function getDataDimensionsOnAxis(data: SeriesData, axisDim: string): Dime
306311
returnzrUtil.keys(dataDimMap);
307312
}
308313

309-
exportfunctionunionExtent(dataExtent: number[],val: number|NullUndefined): void{
310-
// Considered that number could be NaN and should not write into the extent.
311-
val<dataExtent[0]&&(dataExtent[0]=val);
312-
val>dataExtent[1]&&(dataExtent[1]=val);
313-
}
314-
315314
exportfunctionisNameLocationCenter(nameLocation: AxisBaseOptionCommon['nameLocation']){
316315
returnnameLocation==='middle'||nameLocation==='center';
317316
}

‎src/coord/cartesian/defaultAxisExtentFromData.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ import SeriesModel from '../../model/Series';
2323
import{
2424
isCartesian2DDeclaredSeries,findAxisModels,isCartesian2DInjectedAsDataCoordSys
2525
}from'./cartesianAxisHelper';
26-
import{getDataDimensionsOnAxis,unionExtent}from'../axisHelper';
26+
import{getDataDimensionsOnAxis}from'../axisHelper';
2727
import{AxisBaseModel}from'../AxisBaseModel';
2828
importtypeAxisfrom'../Axis';
2929
importGlobalModelfrom'../../model/Global';
3030
import{Dictionary}from'../../util/types';
3131
import{
3232
AXIS_EXTENT_INFO_BUILD_FROM_DATA_ZOOM,ensureScaleRawExtentInfo,ScaleRawExtentInfo,ScaleRawExtentResult
3333
}from'../scaleRawExtentInfo';
34-
import{initExtentForUnion}from'../../util/model';
34+
import{initExtentForUnion,unionExtent}from'../../util/model';
3535

3636
/**
3737
* @obsolete

‎src/coord/scaleRawExtentInfo.ts‎

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ import { DimensionIndex, DimensionName, NullUndefined, ScaleDataValue } from '..
2828
import{isIntervalScale,isLogScale,isOrdinalScale,isTimeScale}from'../scale/helper';
2929
importtypeAxisfrom'./Axis';
3030
importtypeSeriesModelfrom'../model/Series';
31-
import{makeInner,initExtentForUnion}from'../util/model';
32-
import{getDataDimensionsOnAxis,unionExtent}from'./axisHelper';
31+
import{makeInner,initExtentForUnion,unionExtent}from'../util/model';
32+
import{getDataDimensionsOnAxis}from'./axisHelper';
3333
import{
3434
getCoordForCoordSysUsageKindBox
3535
}from'../core/CoordinateSystem';
@@ -83,7 +83,7 @@ export interface ScaleRawExtentResult {
8383
exportclassScaleRawExtentInfo{
8484

8585
private_needCrossZero: ValueAxisBaseOption['scale'];
86-
private_isOrdinal: boolean;
86+
private_scale: Scale;
8787
private_axisDataLen: number;
8888
private_boundaryGapInner: number[];
8989

@@ -118,26 +118,14 @@ export class ScaleRawExtentInfo {
118118
// Typically: data extent from all series on this axis.
119119
dataExtent: number[]
120120
){
121-
this._prepareParams(scale,model,dataExtent);
122-
}
121+
this._scale=scale;
123122

124-
/**
125-
* Parameters depending on outside (like model, user callback)
126-
* are prepared and fixed here.
127-
*/
128-
private_prepareParams(
129-
scale: Scale,
130-
model: AxisBaseModel,
131-
// Usually: data extent from all series on this axis.
132-
dataExtent: number[]
133-
){
134123
if(dataExtent[1]<dataExtent[0]){
135124
dataExtent=[NaN,NaN];
136125
}
137126
this._dataMin=dataExtent[0];
138127
this._dataMax=dataExtent[1];
139128

140-
constisOrdinal=this._isOrdinal=isOrdinalScale(scale);
141129
this._needCrossZero=isIntervalScale(scale)&&model.getNeedCrossZero&&model.getNeedCrossZero();
142130

143131
if(isIntervalScale(scale)||isLogScale(scale)||isTimeScale(scale)){
@@ -181,7 +169,7 @@ export class ScaleRawExtentInfo {
181169
this._modelMaxNum=parseAxisModelMinMax(scale,modelMaxRaw);
182170
}
183171

184-
if(isOrdinal){
172+
if(isOrdinalScale(scale)){
185173
// FIXME: there is a flaw here: if there is no "block" data processor like `dataZoom`,
186174
// and progressive rendering is using, here the category result might just only contain
187175
// the processed chunk rather than the entire result.
@@ -227,7 +215,8 @@ export class ScaleRawExtentInfo {
227215
// be the result that originalExtent enlarged by boundaryGap.
228216
// (3) If no data, it should be ensured that `scale.setBlank` is set.
229217

230-
constisOrdinal=this._isOrdinal;
218+
constscale=this._scale;
219+
constisOrdinal=isOrdinalScale(scale);
231220
letdataMin=this._dataMin;
232221
letdataMax=this._dataMax;
233222

@@ -313,6 +302,11 @@ export class ScaleRawExtentInfo {
313302
maxFixed=maxDetermined=true;
314303
}
315304

305+
if(isLogScale(scale)){
306+
min=clampForLogScale(min);
307+
max=clampForLogScale(max);
308+
}
309+
316310
// Ensure min/max be finite number or NaN here. (not to be null/undefined)
317311
// `NaN` means min/max axis is blank.
318312
return{
@@ -338,6 +332,9 @@ export class ScaleRawExtentInfo {
338332
if(__DEV__){
339333
assert(this[attr]==null);
340334
}
335+
if(isLogScale(this._scale)){
336+
val=clampForLogScale(val);
337+
}
341338
this[attr]=val;
342339
}
343340
}
@@ -457,8 +454,9 @@ export function axisExtentInfoFinalBuild(
457454
// NOTE: This data may have been filtered by dataZoom on orthogonal axes.
458455
constdata=seriesModel.getData();
459456
if(data){
457+
constfilter=isLogScale(scale) ? {g: 0} : null;
460458
each(getDataDimensionsOnAxis(data,axis.dim),function(dim){
461-
constseriesExtent=data.getApproximateExtent(dim);
459+
constseriesExtent=data.getApproximateExtent(dim,filter);
462460
unionExtent(extent,seriesExtent[0]);
463461
unionExtent(extent,seriesExtent[1]);
464462
});
@@ -503,3 +501,9 @@ function injectScaleRawExtentInfo(
503501
// @ts-ignore
504502
scaleRawExtentInfo.from=from;
505503
}
504+
505+
exportfunctionclampForLogScale(val: number){
506+
// Avoid `NaN` for log scale.
507+
// See also `DataStore#getDataExtent`.
508+
returnval<0 ? 0 : val;
509+
}

0 commit comments

Comments
 (0)