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 * as modelUtil from '../../util/model';
2828
import { ECPolyline, ECPolygon } from './poly';
2929
import ChartView from '../../view/Chart';
30-
import { prepareDataCoordInfo, getStackedOnPoint } from './helper';
30+
import { prepareDataCoordInfo, getStackedOnPoint, isPointIllegal } from './helper';
3131
import { createGridClipPath, createPolarClipPath } from '../helper/createClipPathFromCoordSys';
3232
import LineSeriesModel, { LineSeriesOption } from './LineSeries';
3333
import type GlobalModel from '../../model/Global';
@@ -84,42 +84,33 @@ function isPointsSame(points1: ArrayLike<number>, points2: ArrayLike<number>) {
8484
return true;
8585
}
8686

87-
function bboxFromPoints(points: ArrayLike<number>) {
88-
let minX = Infinity;
89-
let minY = Infinity;
90-
let maxX = -Infinity;
91-
let maxY = -Infinity;
87+
function xyExtentFromPoints(points: ArrayLike<number>) {
88+
const xExtent = modelUtil.initExtentForUnion();
89+
const yExtent = modelUtil.initExtentForUnion();
9290

9391
for (let i = 0; i < points.length;) {
9492
const x = points[i++];
9593
const y = 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
function getBoundingDiff(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
return Math.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
const reference = 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
return true;
448439
}
449440

450-
451-
function isPointNull(x: number, y: number) {
452-
return isNaN(x) || isNaN(y);
453-
}
454-
455441
function getLastIndexNotNull(points: ArrayLike<number>) {
456442
let len = 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
let nextIndex = -1;
478464
for (let i = 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
const x = points[dataIndex * 2];
967953
const y = 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
return coordSys.dataToPoint(stackedData);
134134
}
135+
136+
export function isPointIllegal(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
import PathProxy from 'zrender/src/core/PathProxy';
2424
import { cubicRootAt, cubicAt } from 'zrender/src/core/curve';
2525
import tokens from '../../visual/tokens';
26+
import { isPointIllegal } from './helper';
2627

2728
const mathMin = Math.min;
2829
const mathMax = Math.max;
2930

30-
function isPointNull(x: number, y: number) {
31-
return isNaN(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
let tmpK = 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
let nextCpx0;
121118
let nextCpy0;
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
const targetRawPowExtent = 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
const targetExtent = 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
import CartesianAxisModel from './cartesian/AxisModel';
4747
import SeriesData from '../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
return zrUtil.keys(dataDimMap);
307312
}
308313

309-
export function unionExtent(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
export function isNameLocationCenter(nameLocation: AxisBaseOptionCommon['nameLocation']) {
316315
return nameLocation === '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
import type Axis from '../Axis';
2929
import GlobalModel from '../../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
import type Axis from './Axis';
3030
import type SeriesModel from '../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
export class ScaleRawExtentInfo {
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-
const isOrdinal = 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-
const isOrdinal = this._isOrdinal;
218+
const scale = this._scale;
219+
const isOrdinal = isOrdinalScale(scale);
231220
let dataMin = this._dataMin;
232221
let dataMax = 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
const data = seriesModel.getData();
459456
if (data) {
457+
const filter = isLogScale(scale) ? {g: 0} : null;
460458
each(getDataDimensionsOnAxis(data, axis.dim), function (dim) {
461-
const seriesExtent = data.getApproximateExtent(dim);
459+
const seriesExtent = 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+
export function clampForLogScale(val: number) {
506+
// Avoid `NaN` for log scale.
507+
// See also `DataStore#getDataExtent`.
508+
return val < 0 ? 0 : val;
509+
}

0 commit comments

Comments
 (0)