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 dedc5dc1857e74cdac8ec7a834242ae2ba286d72 Author: 100pah <[email protected]> AuthorDate: Sun Jan 25 22:56:30 2026 +0800 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). --- src/chart/line/LineSeries.ts | 2 +- src/chart/line/LineView.ts | 50 ++-- src/chart/line/helper.ts | 10 + src/chart/line/poly.ts | 19 +- src/coord/axisAlignTicks.ts | 4 +- src/coord/axisHelper.ts | 15 +- src/coord/cartesian/defaultAxisExtentFromData.ts | 4 +- src/coord/scaleRawExtentInfo.ts | 42 ++-- src/data/DataStore.ts | 87 +++++-- src/data/SeriesData.ts | 13 +- src/model/Series.ts | 1 + src/scale/Log.ts | 9 +- src/scale/breakImpl.ts | 6 +- src/scale/helper.ts | 21 +- src/util/model.ts | 9 + test/area-stack.html | 1 + test/logScale.html | 279 +++++++++++++---------- test/runTest/actions/__meta__.json | 2 +- test/runTest/actions/logScale.json | 2 +- 19 files changed, 341 insertions(+), 235 deletions(-) diff --git a/src/chart/line/LineSeries.ts b/src/chart/line/LineSeries.ts index 0cd0c9ef8..88dbdb404 100644 --- a/src/chart/line/LineSeries.ts +++ b/src/chart/line/LineSeries.ts @@ -209,7 +209,7 @@ class LineSeriesModel extends SeriesModel<LineSeriesOption> { // follow the label interval strategy. showAllSymbol: 'auto', - // Whether to connect break point. + // Whether to connect break point. (non-finite values) connectNulls: false, // Sampling for large data. Can be: 'average', 'max', 'min', 'sum', 'lttb'. diff --git a/src/chart/line/LineView.ts b/src/chart/line/LineView.ts index 3682d9651..32c6ee4df 100644 --- a/src/chart/line/LineView.ts +++ b/src/chart/line/LineView.ts @@ -27,7 +27,7 @@ import * as graphic from '../../util/graphic'; import * as modelUtil from '../../util/model'; import { ECPolyline, ECPolygon } from './poly'; import ChartView from '../../view/Chart'; -import { prepareDataCoordInfo, getStackedOnPoint } from './helper'; +import { prepareDataCoordInfo, getStackedOnPoint, isPointIllegal } from './helper'; import { createGridClipPath, createPolarClipPath } from '../helper/createClipPathFromCoordSys'; import LineSeriesModel, { LineSeriesOption } from './LineSeries'; import type GlobalModel from '../../model/Global'; @@ -84,42 +84,33 @@ function isPointsSame(points1: ArrayLike<number>, points2: ArrayLike<number>) { return true; } -function bboxFromPoints(points: ArrayLike<number>) { - let minX = Infinity; - let minY = Infinity; - let maxX = -Infinity; - let maxY = -Infinity; +function xyExtentFromPoints(points: ArrayLike<number>) { + const xExtent = modelUtil.initExtentForUnion(); + const yExtent = modelUtil.initExtentForUnion(); for (let i = 0; i < points.length;) { const x = points[i++]; const y = points[i++]; - if (!isNaN(x)) { - minX = Math.min(x, minX); - maxX = Math.max(x, maxX); - } - if (!isNaN(y)) { - minY = Math.min(y, minY); - maxY = Math.max(y, maxY); + if (!isPointIllegal(x, y)) { + modelUtil.unionExtent(xExtent, x); + modelUtil.unionExtent(yExtent, y); } } - return [ - [minX, minY], - [maxX, maxY] - ]; + return [xExtent, yExtent]; } function getBoundingDiff(points1: ArrayLike<number>, points2: ArrayLike<number>): number { - const [min1, max1] = bboxFromPoints(points1); - const [min2, max2] = bboxFromPoints(points2); + const [xExtent1, yExtent1] = xyExtentFromPoints(points1); + const [xExtent2, yExtent2] = xyExtentFromPoints(points2); // Get a max value from each corner of two boundings. return Math.max( - Math.abs(min1[0] - min2[0]), - Math.abs(min1[1] - min2[1]), + Math.abs(xExtent1[0] - xExtent2[0]), + Math.abs(yExtent1[0] - yExtent2[0]), - Math.abs(max1[0] - max2[0]), - Math.abs(max1[1] - max2[1]) + Math.abs(xExtent1[1] - xExtent2[1]), + Math.abs(yExtent1[1] - yExtent2[1]) ); } @@ -181,7 +172,7 @@ function turnPointsIntoStep( * should stay the same as the lines above. See #20021 */ const reference = basePoints || points; - if (!isNaN(reference[i]) && !isNaN(reference[i + 1])) { + if (!isPointIllegal(reference[i], reference[i + 1])) { filteredPoints.push(points[i], points[i + 1]); } } @@ -447,15 +438,10 @@ function canShowAllSymbolForCategory( return true; } - -function isPointNull(x: number, y: number) { - return isNaN(x) || isNaN(y); -} - function getLastIndexNotNull(points: ArrayLike<number>) { let len = points.length / 2; for (; len > 0; len--) { - if (!isPointNull(points[len * 2 - 2], points[len * 2 - 1])) { + if (!isPointIllegal(points[len * 2 - 2], points[len * 2 - 1])) { break; } } @@ -477,7 +463,7 @@ function getIndexRange(points: ArrayLike<number>, xOrY: number, dim: 'x' | 'y') let nextIndex = -1; for (let i = 0; i < len; i++) { b = points[i * 2 + dimIdx]; - if (isNaN(b) || isNaN(points[i * 2 + 1 - dimIdx])) { + if (isPointIllegal(b, points[i * 2 + 1 - dimIdx])) { continue; } if (i === 0) { @@ -965,7 +951,7 @@ class LineView extends ChartView { // Create a temporary symbol if it is not exists const x = points[dataIndex * 2]; const y = points[dataIndex * 2 + 1]; - if (isNaN(x) || isNaN(y)) { + if (isPointIllegal(x, y)) { // Null data return; } diff --git a/src/chart/line/helper.ts b/src/chart/line/helper.ts index 13a1c2c01..ba04dba5c 100644 --- a/src/chart/line/helper.ts +++ b/src/chart/line/helper.ts @@ -132,3 +132,13 @@ export function getStackedOnPoint( return coordSys.dataToPoint(stackedData); } + +export function isPointIllegal(xOrY: number, yOrX: number) { + // NOTE: + // - `NaN` point x/y may be generated by, e.g., + // original series data `NaN`, '-', `null`, `undefined`, + // negative values in LogScale. + // - `Infinite` point x/y may be generated by, e.g., + // original series data `Infinite`, `0` in LogScale. + return !isFinite(xOrY) || !isFinite(yOrX); +} diff --git a/src/chart/line/poly.ts b/src/chart/line/poly.ts index aa6826de8..10ca2a747 100644 --- a/src/chart/line/poly.ts +++ b/src/chart/line/poly.ts @@ -23,14 +23,11 @@ import Path, { PathProps } from 'zrender/src/graphic/Path'; import PathProxy from 'zrender/src/core/PathProxy'; import { cubicRootAt, cubicAt } from 'zrender/src/core/curve'; import tokens from '../../visual/tokens'; +import { isPointIllegal } from './helper'; const mathMin = Math.min; const mathMax = Math.max; -function isPointNull(x: number, y: number) { - return isNaN(x) || isNaN(y); -} - /** * Draw smoothed line in non-monotone, in may cause undesired curve in extreme * situations. This should be used when points are non-monotone neither in x or @@ -63,7 +60,7 @@ function drawSegment( if (idx >= allLen || idx < 0) { break; } - if (isPointNull(x, y)) { + if (isPointIllegal(x, y)) { if (connectNulls) { idx += dir; continue; @@ -106,7 +103,7 @@ function drawSegment( let tmpK = k + 1; if (connectNulls) { // Find next point not null - while (isPointNull(nextX, nextY) && tmpK < segLen) { + while (isPointIllegal(nextX, nextY) && tmpK < segLen) { tmpK++; nextIdx += dir; nextX = points[nextIdx * 2]; @@ -120,7 +117,7 @@ function drawSegment( let nextCpx0; let nextCpy0; // Is last point - if (tmpK >= segLen || isPointNull(nextX, nextY)) { + if (tmpK >= segLen || isPointIllegal(nextX, nextY)) { cpx1 = x; cpy1 = y; } @@ -256,12 +253,12 @@ export class ECPolyline extends Path<ECPolylineProps> { if (shape.connectNulls) { // Must remove first and last null values avoid draw error in polygon for (; len > 0; len--) { - if (!isPointNull(points[len * 2 - 2], points[len * 2 - 1])) { + if (!isPointIllegal(points[len * 2 - 2], points[len * 2 - 1])) { break; } } for (; i < len; i++) { - if (!isPointNull(points[i * 2], points[i * 2 + 1])) { + if (!isPointIllegal(points[i * 2], points[i * 2 + 1])) { break; } } @@ -380,12 +377,12 @@ export class ECPolygon extends Path { if (shape.connectNulls) { // Must remove first and last null values avoid draw error in polygon for (; len > 0; len--) { - if (!isPointNull(points[len * 2 - 2], points[len * 2 - 1])) { + if (!isPointIllegal(points[len * 2 - 2], points[len * 2 - 1])) { break; } } for (; i < len; i++) { - if (!isPointNull(points[i * 2], points[i * 2 + 1])) { + if (!isPointIllegal(points[i * 2], points[i * 2 + 1])) { break; } } diff --git a/src/coord/axisAlignTicks.ts b/src/coord/axisAlignTicks.ts index 75f2d8d55..75ba6fedb 100644 --- a/src/coord/axisAlignTicks.ts +++ b/src/coord/axisAlignTicks.ts @@ -159,8 +159,8 @@ export function alignScaleTicks( const targetRawPowExtent = targetRawExtent; if (isTargetLogScale) { targetRawExtent = [ - logScaleLogTick(targetRawExtent[0], targetLogScaleBase, false), - logScaleLogTick(targetRawExtent[1], targetLogScaleBase, false) + logScaleLogTick(targetRawExtent[0], targetLogScaleBase), + logScaleLogTick(targetRawExtent[1], targetLogScaleBase) ]; } const targetExtent = intervalScaleEnsureValidExtent(targetRawExtent, targetMinMaxFixed); diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 45e8790ae..d21af8084 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -46,8 +46,8 @@ import { import CartesianAxisModel from './cartesian/AxisModel'; import SeriesData from '../data/SeriesData'; import { getStackedDimension } from '../data/helper/dataStackHelper'; -import { Dictionary, DimensionName, NullUndefined, ScaleTick } from '../util/types'; -import { ensureScaleRawExtentInfo, ScaleRawExtentResult } from './scaleRawExtentInfo'; +import { Dictionary, DimensionName, ScaleTick } from '../util/types'; +import { clampForLogScale, ensureScaleRawExtentInfo, ScaleRawExtentResult } from './scaleRawExtentInfo'; import { parseTimeAxisLabelFormatter } from '../util/time'; import { getScaleBreakHelper } from '../scale/break'; import { error } from '../util/log'; @@ -104,6 +104,11 @@ export function adoptScaleExtentOptionAndPrepare( } } + if (isLogScale(scale)) { + min = clampForLogScale(min); + max = clampForLogScale(max); + } + rawExtentResult.min = min; rawExtentResult.max = max; @@ -306,12 +311,6 @@ export function getDataDimensionsOnAxis(data: SeriesData, axisDim: string): Dime return zrUtil.keys(dataDimMap); } -export function unionExtent(dataExtent: number[], val: number | NullUndefined): void { - // Considered that number could be NaN and should not write into the extent. - val < dataExtent[0] && (dataExtent[0] = val); - val > dataExtent[1] && (dataExtent[1] = val); -} - export function isNameLocationCenter(nameLocation: AxisBaseOptionCommon['nameLocation']) { return nameLocation === 'middle' || nameLocation === 'center'; } diff --git a/src/coord/cartesian/defaultAxisExtentFromData.ts b/src/coord/cartesian/defaultAxisExtentFromData.ts index a9d4950fa..c2db3b276 100644 --- a/src/coord/cartesian/defaultAxisExtentFromData.ts +++ b/src/coord/cartesian/defaultAxisExtentFromData.ts @@ -23,7 +23,7 @@ import SeriesModel from '../../model/Series'; import { isCartesian2DDeclaredSeries, findAxisModels, isCartesian2DInjectedAsDataCoordSys } from './cartesianAxisHelper'; -import { getDataDimensionsOnAxis, unionExtent } from '../axisHelper'; +import { getDataDimensionsOnAxis } from '../axisHelper'; import { AxisBaseModel } from '../AxisBaseModel'; import type Axis from '../Axis'; import GlobalModel from '../../model/Global'; @@ -31,7 +31,7 @@ import { Dictionary } from '../../util/types'; import { AXIS_EXTENT_INFO_BUILD_FROM_DATA_ZOOM, ensureScaleRawExtentInfo, ScaleRawExtentInfo, ScaleRawExtentResult } from '../scaleRawExtentInfo'; -import { initExtentForUnion } from '../../util/model'; +import { initExtentForUnion, unionExtent } from '../../util/model'; /** * @obsolete diff --git a/src/coord/scaleRawExtentInfo.ts b/src/coord/scaleRawExtentInfo.ts index 419df4915..8ce80ca31 100644 --- a/src/coord/scaleRawExtentInfo.ts +++ b/src/coord/scaleRawExtentInfo.ts @@ -28,8 +28,8 @@ import { DimensionIndex, DimensionName, NullUndefined, ScaleDataValue } from '.. import { isIntervalScale, isLogScale, isOrdinalScale, isTimeScale } from '../scale/helper'; import type Axis from './Axis'; import type SeriesModel from '../model/Series'; -import { makeInner, initExtentForUnion } from '../util/model'; -import { getDataDimensionsOnAxis, unionExtent } from './axisHelper'; +import { makeInner, initExtentForUnion, unionExtent } from '../util/model'; +import { getDataDimensionsOnAxis } from './axisHelper'; import { getCoordForCoordSysUsageKindBox } from '../core/CoordinateSystem'; @@ -83,7 +83,7 @@ export interface ScaleRawExtentResult { export class ScaleRawExtentInfo { private _needCrossZero: ValueAxisBaseOption['scale']; - private _isOrdinal: boolean; + private _scale: Scale; private _axisDataLen: number; private _boundaryGapInner: number[]; @@ -118,26 +118,14 @@ export class ScaleRawExtentInfo { // Typically: data extent from all series on this axis. dataExtent: number[] ) { - this._prepareParams(scale, model, dataExtent); - } + this._scale = scale; - /** - * Parameters depending on outside (like model, user callback) - * are prepared and fixed here. - */ - private _prepareParams( - scale: Scale, - model: AxisBaseModel, - // Usually: data extent from all series on this axis. - dataExtent: number[] - ) { if (dataExtent[1] < dataExtent[0]) { dataExtent = [NaN, NaN]; } this._dataMin = dataExtent[0]; this._dataMax = dataExtent[1]; - const isOrdinal = this._isOrdinal = isOrdinalScale(scale); this._needCrossZero = isIntervalScale(scale) && model.getNeedCrossZero && model.getNeedCrossZero(); if (isIntervalScale(scale) || isLogScale(scale) || isTimeScale(scale)) { @@ -181,7 +169,7 @@ export class ScaleRawExtentInfo { this._modelMaxNum = parseAxisModelMinMax(scale, modelMaxRaw); } - if (isOrdinal) { + if (isOrdinalScale(scale)) { // FIXME: there is a flaw here: if there is no "block" data processor like `dataZoom`, // and progressive rendering is using, here the category result might just only contain // the processed chunk rather than the entire result. @@ -227,7 +215,8 @@ export class ScaleRawExtentInfo { // be the result that originalExtent enlarged by boundaryGap. // (3) If no data, it should be ensured that `scale.setBlank` is set. - const isOrdinal = this._isOrdinal; + const scale = this._scale; + const isOrdinal = isOrdinalScale(scale); let dataMin = this._dataMin; let dataMax = this._dataMax; @@ -313,6 +302,11 @@ export class ScaleRawExtentInfo { maxFixed = maxDetermined = true; } + if (isLogScale(scale)) { + min = clampForLogScale(min); + max = clampForLogScale(max); + } + // Ensure min/max be finite number or NaN here. (not to be null/undefined) // `NaN` means min/max axis is blank. return { @@ -338,6 +332,9 @@ export class ScaleRawExtentInfo { if (__DEV__) { assert(this[attr] == null); } + if (isLogScale(this._scale)) { + val = clampForLogScale(val); + } this[attr] = val; } } @@ -457,8 +454,9 @@ export function axisExtentInfoFinalBuild( // NOTE: This data may have been filtered by dataZoom on orthogonal axes. const data = seriesModel.getData(); if (data) { + const filter = isLogScale(scale) ? {g: 0} : null; each(getDataDimensionsOnAxis(data, axis.dim), function (dim) { - const seriesExtent = data.getApproximateExtent(dim); + const seriesExtent = data.getApproximateExtent(dim, filter); unionExtent(extent, seriesExtent[0]); unionExtent(extent, seriesExtent[1]); }); @@ -503,3 +501,9 @@ function injectScaleRawExtentInfo( // @ts-ignore scaleRawExtentInfo.from = from; } + +export function clampForLogScale(val: number) { + // Avoid `NaN` for log scale. + // See also `DataStore#getDataExtent`. + return val < 0 ? 0 : val; +} diff --git a/src/data/DataStore.ts b/src/data/DataStore.ts index 4a3a0031e..f0280d06a 100644 --- a/src/data/DataStore.ts +++ b/src/data/DataStore.ts @@ -21,6 +21,7 @@ import { assert, clone, createHashMap, isFunction, keys, map, reduce } from 'zre import { DimensionIndex, DimensionName, + NullUndefined, OptionDataItem, ParsedValue, ParsedValueNumeric @@ -72,6 +73,9 @@ type FilterCb = (...args: any) => boolean; // type MapArrayCb = (...args: any) => any; type MapCb = (...args: any) => ParsedValue | ParsedValue[]; +// g: greater than, ge: greater equal, l: less than, le: less equal +export type DataStoreExtentFilter = {g?: number; ge?: number; l?: number; le?: number;}; + export type DimValueGetter = ( this: DataStore, dataItem: any, @@ -163,7 +167,9 @@ class DataStore { // It will not be calculated until needed. private _rawExtent: [number, number][] = []; - private _extent: [number, number][] = []; + // structure: + // `const extentOnFilterOnDimension = this._extent[dim][extentFilterKey]` + private _extent: Record<string, [number, number]>[] = []; // Indices stores the indices of data subset after filtered. // This data subset will be used in chart. @@ -829,7 +835,7 @@ class DataStore { let retValue = cb && cb.apply(null, values); if (retValue != null) { - // a number or string (in oridinal dimension)? + // a number or string (in ordinal dimension)? if (typeof retValue !== 'object') { tmpRetValue[0] = retValue; retValue = tmpRetValue; @@ -1126,10 +1132,10 @@ class DataStore { } } - /** - * Get extent of data in one dimension - */ - getDataExtent(dim: DimensionIndex): [number, number] { + getDataExtent( + dim: DimensionIndex, + filter: DataStoreExtentFilter | NullUndefined + ): [number, number] { // Make sure use concrete dim as cache name. const dimData = this._chunks[dim]; const initialExtent = initExtentForUnion(); @@ -1144,33 +1150,74 @@ class DataStore { // Consider the most cases when using data zoom, `getDataExtent` // happened before filtering. We cache raw extent, which is not // necessary to be cleared and recalculated when restore data. - const useRaw = !this._indices; - let dimExtent: [number, number]; - + const useRaw = !this._indices && !filter; if (useRaw) { return this._rawExtent[dim].slice() as [number, number]; } - dimExtent = this._extent[dim]; + + // NOTE: + // - In logarithm axis, zero should be excluded, therefore the `extent[0]` should be less or equal + // than the min positive data item, which requires the special handling here. + // - "Filter non-positive values for logarithm axis" can also be implemented in a data processor + // but that requires more complicated code to not break all streams under the current architecture, + // therefore we simply implement it here. + // - Performance is sensitive for large data, therefore inline filters rather than cb is used here. + + const thisExtent = this._extent; + const dimExtentRecord = thisExtent[dim] || (thisExtent[dim] = {}); + let filterKey = ''; + let filterG = -Infinity; + let filterGE = -Infinity; + let filterL = Infinity; + let filterLE = Infinity; + if (filter) { + if (filter.g != null) { + filterKey += 'G' + filter.g; + filterG = filter.g; + } + if (filter.ge != null) { + filterKey += 'GE' + filter.ge; + filterGE = filter.ge; + } + if (filter.l != null) { + filterKey += 'L' + filter.l; + filterL = filter.l; + } + if (filter.le != null) { + filterKey += 'LE' + filter.le; + filterLE = filter.le; + } + } + const dimExtent = dimExtentRecord[filterKey]; if (dimExtent) { return dimExtent.slice() as [number, number]; } - dimExtent = initialExtent; - let min = dimExtent[0]; - let max = dimExtent[1]; + let min = initialExtent[0]; + let max = initialExtent[1]; + // NOTICE: Performance sensitive on large data. for (let i = 0; i < currEnd; i++) { const rawIdx = this.getRawIndex(i); const value = dimData[rawIdx] as ParsedValueNumeric; - value < min && (min = value); - value > max && (max = value); + if (filter) { + if (value <= filterG + || value < filterGE + || value >= filterL + || value > filterLE + ) { + continue; + } + } + if (value < min) { + min = value; + } + if (value > max) { + max = value; + } } - dimExtent = [min, max]; - - this._extent[dim] = dimExtent; - - return dimExtent; + return (dimExtentRecord[filterKey] = [min, max]); } /** diff --git a/src/data/SeriesData.ts b/src/data/SeriesData.ts index f18aec6b7..c498abd6d 100644 --- a/src/data/SeriesData.ts +++ b/src/data/SeriesData.ts @@ -27,7 +27,7 @@ import DataDiffer from './DataDiffer'; import {DataProvider, DefaultDataProvider} from './helper/dataProvider'; import {summarizeDimensions, DimensionSummary} from './helper/dimensionHelper'; import SeriesDimensionDefine from './SeriesDimensionDefine'; -import {ArrayLike, Dictionary, FunctionPropertyNames} from 'zrender/src/core/types'; +import {ArrayLike, Dictionary, FunctionPropertyNames, NullUndefined} from 'zrender/src/core/types'; import Element from 'zrender/src/Element'; import { DimensionIndex, DimensionName, DimensionLoose, OptionDataItem, @@ -44,7 +44,7 @@ import type Tree from './Tree'; import type { VisualMeta } from '../component/visualMap/VisualMapModel'; import {isSourceInstance, Source} from './Source'; import { LineStyleProps } from '../model/mixin/lineStyle'; -import DataStore, { DataStoreDimensionDefine, DimValueGetter } from './DataStore'; +import DataStore, { DataStoreDimensionDefine, DataStoreExtentFilter, DimValueGetter } from './DataStore'; import { isSeriesDataSchema, SeriesDataSchema } from './helper/SeriesDataSchema'; const isObject = zrUtil.isObject; @@ -681,8 +681,11 @@ class SeriesData< * extent calculation will cost more than 10ms and the cache will * be erased because of the filtering. */ - getApproximateExtent(dim: SeriesDimensionLoose): [number, number] { - return this._approximateExtent[dim] || this._store.getDataExtent(this._getStoreDimIndex(dim)); + getApproximateExtent( + dim: SeriesDimensionLoose, + filter: DataStoreExtentFilter | NullUndefined + ): [number, number] { + return this._approximateExtent[dim] || this._store.getDataExtent(this._getStoreDimIndex(dim), filter); } /** @@ -789,7 +792,7 @@ class SeriesData< } getDataExtent(dim: DimensionLoose): [number, number] { - return this._store.getDataExtent(this._getStoreDimIndex(dim)); + return this._store.getDataExtent(this._getStoreDimIndex(dim), null); } getSum(dim: DimensionLoose): number { diff --git a/src/model/Series.ts b/src/model/Series.ts index 17779c912..f4fe5fb8d 100644 --- a/src/model/Series.ts +++ b/src/model/Series.ts @@ -537,6 +537,7 @@ class SeriesModel<Opt extends SeriesOption = SeriesOption> extends ComponentMode } restoreData() { + // See `dataTaskReset`. this.dataTask.dirty(); } diff --git a/src/scale/Log.ts b/src/scale/Log.ts index 0ce0b55d9..aae2f8e57 100644 --- a/src/scale/Log.ts +++ b/src/scale/Log.ts @@ -119,11 +119,14 @@ class LogScale extends Scale { return this.linearStub.getLabel(data, opt); } + /** + * NOTICE: The caller should ensure `start` and `end` are both non-negative. + */ setExtent(start: number, end: number): void { this.powStub.setExtent(start, end); this.linearStub.setExtent( - logScaleLogTick(start, this.base, false), - logScaleLogTick(end, this.base, false) + logScaleLogTick(start, this.base), + logScaleLogTick(end, this.base) ); } @@ -140,7 +143,7 @@ class LogScale extends Scale { } normalize(val: number): number { - return this.linearStub.normalize(logScaleLogTick(val, this.base, true)); + return this.linearStub.normalize(logScaleLogTick(val, this.base)); } scale(val: number): number { diff --git a/src/scale/breakImpl.ts b/src/scale/breakImpl.ts index 9f8869035..57da02a87 100644 --- a/src/scale/breakImpl.ts +++ b/src/scale/breakImpl.ts @@ -675,12 +675,12 @@ function logarithmicParseBreaksFromOption( const parsedLogged = parseAxisBreakOption(breakOptionList, parse, opt); parsedLogged.breaks = map(parsedLogged.breaks, brk => { - const vmin = logScaleLogTick(brk.vmin, logBase, true); - const vmax = logScaleLogTick(brk.vmax, logBase, true); + const vmin = logScaleLogTick(brk.vmin, logBase); + const vmax = logScaleLogTick(brk.vmax, logBase); const gapParsed = { type: brk.gapParsed.type, val: brk.gapParsed.type === 'tpAbs' - ? logScaleLogTick(brk.vmin + brk.gapParsed.val, logBase, true) - vmin + ? logScaleLogTick(brk.vmin + brk.gapParsed.val, logBase) - vmin : brk.gapParsed.val, }; return { diff --git a/src/scale/helper.ts b/src/scale/helper.ts index 4cddbc1aa..1d17d7d98 100644 --- a/src/scale/helper.ts +++ b/src/scale/helper.ts @@ -183,20 +183,23 @@ function scale( } /** - * NOTE: if `val` is `NaN`, return `NaN`. + * NOTE: + * - If `val` is `NaN`, return `NaN`. + * - If `val` is `0`, return `-Infinity`. + * - If `val` is negative, return `NaN`. + * + * @see {DataStore#getDataExtent} It handles non-positive values for logarithm scale. */ export function logScaleLogTick( val: number, base: number, - noClampNegative: boolean ): number { - // log(negative) is NaN, so safe guard here. - // PENDING: But even getting a -Infinity still does not make sense in extent. - // Just keep it as is, getting a NaN to make some previous cases works by coincidence. - return mathLog(noClampNegative ? val : mathMax(0, val)) / mathLog(base); - // NOTE: rounding error may happen above, typically expecting `log10(1000)` but actually - // getting `2.9999999999999996`, but generally it does not matter since they are not - // used to display. + // NOTE: + // - rounding error may happen above, typically expecting `log10(1000)` but actually + // getting `2.9999999999999996`, but generally it does not matter since they are not + // used to display. + // - Consider backward compatibility and other log bases, do not use `Math.log10`. + return mathLog(val) / mathLog(base); } /** diff --git a/src/util/model.ts b/src/util/model.ts index 4330f6d36..34ea2e39e 100644 --- a/src/util/model.ts +++ b/src/util/model.ts @@ -1177,6 +1177,15 @@ export function initExtentForUnion(): [number, number] { return [Infinity, -Infinity]; } +/** + * Suppose `extent` is initialized as `initExtentForUnion()`. + */ +export function unionExtent(extent: number[], val: number | NullUndefined): void { + // Considered that number could be NaN and should not write into the extent. + val < extent[0] && (extent[0] = val); + val > extent[1] && (extent[1] = val); +} + /** * A util for ensuring the callback is called only once. * @usage diff --git a/test/area-stack.html b/test/area-stack.html index c60eaa1b6..ceb63aace 100644 --- a/test/area-stack.html +++ b/test/area-stack.html @@ -126,6 +126,7 @@ under the License. var option = { legend: { + top: 5, }, toolbox: { feature: { diff --git a/test/logScale.html b/test/logScale.html index 028256139..113337e8d 100644 --- a/test/logScale.html +++ b/test/logScale.html @@ -39,6 +39,7 @@ under the License. <div id='main2_negative'></div> <div id='main_small_values'></div> <div id='main3'></div> + <div id='main_singleAxis_dataZoom'></div> <script> @@ -149,131 +150,113 @@ under the License. 'echarts', ], function (echarts /*, data */) { - var option = { - backgroundColor: 'rgba(0,0,0,0.1)', - legend: { - top: 3, - backgroundColor: 'rgba(250,0,0,0.4)', - borderRadius: 3, - borderWidth: 2, - borderColor: 'rgba(150,0,0,0.7)' - }, - tooltip: {}, - xAxis: [ - { - id: 0, - name: 'xAxis_0 long name', - nameTextStyle: { - rich: { - name_big_1: { - borderWidth: 4, - borderColor: 'rgba(0,180,0,0.5)', - backgroundColor: 'rgba(0,150,0,0.5)', - padding: [10, 20], - color: '#000', - fontSize: 20 - } - }, - color: '#555' - }, - nameMoveOverlap: true, - nameLocation: 'middle', - axisLabel: { - textStyle: { - rich: { - label_big_1: { - borderWidth: 4, - borderColor: 'rgba(0,180,0,0.5)', - backgroundColor: 'rgba(0,150,0,0.5)', - padding: [10, 20], - color: '#000', - fontSize: 20 - } - }, - color: '#555' - } - }, - axisLine: {onZero: false}, - position: ['bottom', 'top'] - } - ], - yAxis: [ - { - id: 0, - name: 'yAxis_0 long name', - type: 'log', - nameTextStyle: { - rich: { - name_big_1: { - borderWidth: 4, - borderColor: 'rgba(0,180,0,0.5)', - backgroundColor: 'rgba(0,150,0,0.5)', - padding: [10, 20], - color: '#000', - fontSize: 20 - } - }, - color: '#555' - }, - nameMoveOverlap: true, - nameLocation: 'middle', - axisLabel: { - textStyle: { - rich: { - label_big_1: { - borderWidth: 4, - borderColor: 'rgba(0,180,0,0.5)', - backgroundColor: 'rgba(0,150,0,0.5)', - padding: [10, 20], - color: '#000', - fontSize: 20 - } - }, - color: '#555' - } - }, - axisLine: {onZero: false}, - position: ['left', 'right'] - } - ], - grid: [ - { - left: 100, - right: 10, - top: 30, - bottom: 100, - show: true, - backgroundColor: 'rgba(150,0,0,0.2)' - } - ], - series: [ - { - xAxisIndex: 0, - yAxisIndex: 0, - symbolSize: 10, - name: 'series_small_0', - data: [[-50, 12]], - type: 'scatter' + const _ctx = { + yAxisMin: undefined, + lineConnectNulls: undefined, + }; + + function makeSeriesOption(seriesType, data) { + return { + type: seriesType, + symbolSize: 10, + name: [ + seriesType, + seriesType === 'line' ? ` connectNulls: ${_ctx.lineConnectNulls};` : '', + ` dataY: ${data.map(function (item) { return item[1]; }).join(', ')}` + ].join(''), + data: data, + connectNulls: _ctx.lineConnectNulls, + }; + } + + function makeOption() { + return { + legend: { }, - { - xAxisIndex: 0, - yAxisIndex: 0, - symbolSize: 10, - name: 'series_big_0', - data: [ + tooltip: {}, + xAxis: [ + { + id: 0, + nameMoveOverlap: true, + nameLocation: 'middle', + axisLine: {onZero: false}, + position: ['bottom', 'top'] + } + ], + yAxis: [ + { + id: 0, + name: 'yAxis is log', + type: 'log', + min: _ctx.yAxisMin, + nameMoveOverlap: true, + nameLocation: 'middle', + axisLine: {onZero: false}, + position: ['left', 'right'] + } + ], + grid: [ + { + left: 100, + right: 10, + top: 30, + bottom: 120, + show: true, + } + ], + series: [ + makeSeriesOption('scatter', [ + [-50, 0], [-50, 1219212121] + ]), + makeSeriesOption('scatter', [ [10000000000, -832], [55555550000, 31133232233] - ], - type: 'scatter' - } - ] - }; + ]), + makeSeriesOption('line', [ + [15555550000, 461293242], + [20555550000, 581293242], + [25000000000, -111832], + [30555550000, 719293242], + [35555550000, 519293242] + ]), + makeSeriesOption('line', [ + [15555550000, 251293242], + [20555550000, 231293242], + [25000000000, 0], + [30555550000, 319293242], + [35555550000, 219293242] + ]), + ] + }; + } var chart = testHelper.create(echarts, 'main2_negative', { title: [ - 'Test negative', + 'yAxis: non-positive points should be ignored; others are displayed.', + 'yAxis extent should exclude the non-positive values.', + 'When yAxis.min is non-positive, can show a defaults effect, but should be recoverable.', + 'When yAxis.min is "dataMin", non-positive values should be ignored.', + 'Check connectNulls.', ], - option: option, + option: makeOption(), + inputStyle: 'compact', + inputs: [{ + text: 'yAxis min:', + type: 'select', + values: [_ctx.yAxisMin, -1000000, 0, 'dataMin', 2000000], + onchange: function () { + _ctx.yAxisMin = this.value; + chart.setOption(makeOption()); + } + }, { + text: 'line connectNulls:', + type: 'select', + values: [_ctx.lineConnectNulls, false, true], + onchange: function () { + _ctx.lineConnectNulls = this.value; + chart.setOption(makeOption()); + } + }] }); }); @@ -391,5 +374,65 @@ under the License. }); </script> + + <script> + + require([ + 'echarts', + ], function (echarts) { + + var data2 = []; + for (var i = 0; i < 12; i++) { + data2.push([Math.random() * (i % 2 === 0 ? 100 : 1000000), Math.random() * 30]); + } + + option = { + tooltip: { + trigger: 'axis' + }, + dataZoom: [{ + type: 'inside', + singleAxisIndex: [0] + }, { + type: 'slider', + singleAxisIndex: [0] + }], + singleAxis: [{ + type: 'log', + id: 'c', + bottom: 80, + axisPointer: { + snap: false, + label: { + show: true + } + }, + splitArea: { + show: true + }, + }], + series: { + type: 'scatter', + coordinateSystem: 'singleAxis', + singleAxisId: 'c', + symbolSize: function (val) { + return val[1]; + }, + data: data2 + } + }; + + var chart = testHelper.create(echarts, 'main_singleAxis_dataZoom', { + title: [ + 'singleAxis log with dataZoom', + 'min/max precision should be proper after zooming', + ], + height: 300, + option: option, + }); + }); + </script> + + </body> </html> \ No newline at end of file diff --git a/test/runTest/actions/__meta__.json b/test/runTest/actions/__meta__.json index a4e134f69..b61d378e5 100644 --- a/test/runTest/actions/__meta__.json +++ b/test/runTest/actions/__meta__.json @@ -145,7 +145,7 @@ "line-visual2": 2, "lines-bus": 1, "lines-symbolSize-update": 1, - "logScale": 3, + "logScale": 4, "map": 3, "map-contour": 2, "map-default": 1, diff --git a/test/runTest/actions/logScale.json b/test/runTest/actions/logScale.json index 5115c2910..fd0c978cf 100644 --- a/test/runTest/actions/logScale.json +++ b/test/runTest/actions/logScale.json @@ -1 +1 @@ -[{"name":"Action 1","ops":[{"type":"mousemove","time":232,"x":764,"y":253},{"type":"mousemove","time":438,"x":142,"y":325},{"type":"mousemove","time":648,"x":79,"y":378},{"type":"mousemove","time":854,"x":58,"y":408},{"type":"mousemove","time":1073,"x":42,"y":435},{"type":"mousedown","time":1091,"x":42,"y":435},{"type":"mouseup","time":1207,"x":42,"y":435},{"time":1208,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1781,"x":43,"y":435},{"type":"mousemove","time":1981,"x [...] \ No newline at end of file +[{"name":"Action 1","ops":[{"type":"mousemove","time":232,"x":764,"y":253},{"type":"mousemove","time":438,"x":142,"y":325},{"type":"mousemove","time":648,"x":79,"y":378},{"type":"mousemove","time":854,"x":58,"y":408},{"type":"mousemove","time":1073,"x":42,"y":435},{"type":"mousedown","time":1091,"x":42,"y":435},{"type":"mouseup","time":1207,"x":42,"y":435},{"time":1208,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1781,"x":43,"y":435},{"type":"mousemove","time":1981,"x [...] \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
