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]


Reply via email to