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 479dcd454f0f49c70177284a67c490d624bef0c3
Author: 100pah <[email protected]>
AuthorDate: Fri Jan 9 16:24:14 2026 +0800

    fix&feat: Change and clarify the rounding error and auto-precision utils 
and solutions.
---
 src/scale/helper.ts              | 144 +++++++++++++++++++++++++------
 src/util/number.ts               | 177 ++++++++++++++++++++++++++++++---------
 test/ut/spec/util/number.test.ts |  74 +++++++++++++++-
 3 files changed, 332 insertions(+), 63 deletions(-)

diff --git a/src/scale/helper.ts b/src/scale/helper.ts
index 31f41945e..c5143d530 100644
--- a/src/scale/helper.ts
+++ b/src/scale/helper.ts
@@ -17,12 +17,14 @@
 * under the License.
 */
 
-import {getPrecision, round, nice, quantityExponent} from '../util/number';
+import {getPrecision, round, nice, quantityExponent, mathPow, mathMax, 
mathRound} from '../util/number';
 import IntervalScale from './Interval';
 import LogScale from './Log';
 import type Scale from './Scale';
 import { bind } from 'zrender/src/core/util';
 import type { ScaleBreakContext } from './break';
+import TimeScale from './Time';
+import { NullUndefined } from '../util/types';
 
 type intervalScaleNiceTicksResult = {
     interval: number,
@@ -30,19 +32,39 @@ type intervalScaleNiceTicksResult = {
     niceTickExtent: [number, number]
 };
 
-export function isValueNice(val: number) {
-    const exp10 = Math.pow(10, quantityExponent(Math.abs(val)));
-    const f = Math.abs(val / exp10);
-    return f === 0
-        || f === 1
-        || f === 2
-        || f === 3
-        || f === 5;
-}
+/**
+ * See also method `nice` in `src/util/number.ts`.
+ */
+// export function isValueNice(val: number) {
+//     const exp10 = Math.pow(10, quantityExponent(Math.abs(val)));
+//     const f = Math.abs(round(val / exp10, 0));
+//     return f === 0
+//         || f === 1
+//         || f === 2
+//         || f === 3
+//         || f === 5;
+// }
 
 export function isIntervalOrLogScale(scale: Scale): scale is LogScale | 
IntervalScale {
-    return scale.type === 'interval' || scale.type === 'log';
+    return isIntervalScale(scale) || isLogScale(scale);
+}
+
+export function isIntervalScale(scale: Scale): scale is IntervalScale {
+    return scale.type === 'interval';
 }
+
+export function isTimeScale(scale: Scale): scale is TimeScale {
+    return scale.type === 'time';
+}
+
+export function isLogScale(scale: Scale): scale is LogScale {
+    return scale.type === 'log';
+}
+
+export function isOrdinalScale(scale: Scale): boolean {
+    return scale.type === 'ordinal';
+}
+
 /**
  * @param extent Both extent[0] and extent[1] should be valid number.
  *               Should be extent[0] < extent[1].
@@ -65,7 +87,6 @@ export function intervalScaleNiceTicks(
     if (maxInterval != null && interval > maxInterval) {
         interval = result.interval = maxInterval;
     }
-    // Tow more digital for tick.
     const precision = result.intervalPrecision = 
getIntervalPrecision(interval);
     // Niced extent inside original extent
     const niceTickExtent = result.niceTickExtent = [
@@ -73,15 +94,22 @@ export function intervalScaleNiceTicks(
         round(Math.floor(extent[1] / interval) * interval, precision)
     ];
 
-    fixExtent(niceTickExtent, extent);
+    fixNiceExtent(niceTickExtent, extent);
 
     return result;
 }
 
-export function increaseInterval(interval: number) {
-    const exp10 = Math.pow(10, quantityExponent(interval));
-    // Increase interval
-    let f = interval / exp10;
+/**
+ * The input `niceInterval` should be generated
+ * from `nice` method in `src/util/number.ts`, or
+ * from `increaseInterval` itself.
+ */
+export function increaseInterval(niceInterval: number) {
+    const exponent = quantityExponent(niceInterval);
+    // No rounding error in Math.pow(10, xxx).
+    const exp10 = mathPow(10, exponent);
+    // Fix IEEE 754 float rounding error
+    let f = mathRound(niceInterval / exp10);
     if (!f) {
         f = 1;
     }
@@ -94,17 +122,18 @@ export function increaseInterval(interval: number) {
     else { // f is 1 or 5
         f *= 2;
     }
-    return round(f * exp10);
+    // Fix IEEE 754 float rounding error
+    return round(f * exp10, -exponent);
 }
 
-/**
- * @return interval precision
- */
-export function getIntervalPrecision(interval: number): number {
+export function getIntervalPrecision(niceInterval: number): number {
     // Tow more digital for tick.
-    return getPrecision(interval) + 2;
+    // NOTE: `2` was introduced in commit 
`af2a2a9f6303081d7c3b52f0a38add07b4c6e0c7`;
+    // it works on "nice" interval, but seems not necessarily mathematically 
required.
+    return getPrecision(niceInterval) + 2;
 }
 
+
 function clamp(
     niceTickExtent: [number, number], idx: number, extent: [number, number]
 ): void {
@@ -112,7 +141,7 @@ function clamp(
 }
 
 // In some cases (e.g., splitNumber is 1), niceTickExtent may be out of extent.
-export function fixExtent(
+export function fixNiceExtent(
     niceTickExtent: [number, number], extent: [number, number]
 ): void {
     !isFinite(niceTickExtent[0]) && (niceTickExtent[0] = extent[0]);
@@ -173,3 +202,70 @@ export function logTransform(base: number, extent: 
number[], noClampNegative?: b
         Math.log(noClampNegative ? extent[1] : Math.max(0, extent[1])) / 
loggedBase
     ];
 }
+
+export function powTransform(base: number, extent: number[]): [number, number] 
{
+    return [
+        mathPow(base, extent[0]),
+        mathPow(base, extent[1])
+    ];
+}
+
+/**
+ * A valid extent is:
+ *  - No non-finite number.
+ *  - `extent[0] < extent[1]`.
+ *
+ * [NOTICE]: The input `rawExtent` can only be:
+ *  - All non-finite numbers or `NaN`; or
+ *  - `[Infinity, -Infinity]` (A typical initial extent with no data.)
+ *  (Improve it when needed.)
+ */
+export function intervalScaleEnsureValidExtent(
+    rawExtent: number[],
+    opt: {
+        fixMax?: boolean
+    }
+): number[] {
+    const extent = rawExtent.slice();
+    // If extent start and end are same, expand them
+    if (extent[0] === extent[1]) {
+        if (extent[0] !== 0) {
+            // Expand extent
+            // Note that extents can be both negative. See #13154
+            const expandSize = Math.abs(extent[0]);
+            // In the fowllowing case
+            //      Axis has been fixed max 100
+            //      Plus data are all 100 and axis extent are [100, 100].
+            // Extend to the both side will cause expanded max is larger than 
fixed max.
+            // So only expand to the smaller side.
+            if (!opt.fixMax) {
+                extent[1] += expandSize / 2;
+                extent[0] -= expandSize / 2;
+            }
+            else {
+                extent[0] -= expandSize / 2;
+            }
+        }
+        else {
+            extent[1] = 1;
+        }
+    }
+    const span = extent[1] - extent[0];
+    // If there are no data and extent are [Infinity, -Infinity]
+    if (!isFinite(span)) {
+        extent[0] = 0;
+        extent[1] = 1;
+    }
+    else if (span < 0) {
+        extent.reverse();
+    }
+
+    return extent;
+}
+
+export function ensureValidSplitNumber(
+    rawSplitNumber: number | NullUndefined, defaultSplitNumber: number
+): number {
+    rawSplitNumber = rawSplitNumber || defaultSplitNumber;
+    return mathRound(mathMax(rawSplitNumber, 1));
+}
diff --git a/src/util/number.ts b/src/util/number.ts
index f156e6d3d..074374351 100644
--- a/src/util/number.ts
+++ b/src/util/number.ts
@@ -27,11 +27,14 @@
 */
 
 import * as zrUtil from 'zrender/src/core/util';
+import { NullUndefined } from './types';
 
 const RADIAN_EPSILON = 1e-4;
-// Although chrome already enlarge this number to 100 for `toFixed`, but
-// we sill follow the spec for compatibility.
-const ROUND_SUPPORTED_PRECISION_MAX = 20;
+
+// A `RangeError` may be thrown if `n` is out of this range when calling 
`toFixed(n)`.
+// Although Chrome and ES2017+ have enlarged this number to 100, but we sill 
follow
+// the ES3~ES6 spec (0 <= n <= 20) for backward and cross-platform 
compatibility.
+const TO_FIXED_SUPPORTED_PRECISION_MAX = 20;
 
 function _trim(str: string): string {
     return str.replace(/^\s+|\s+$/g, '');
@@ -40,6 +43,14 @@ function _trim(str: string): string {
 export const mathMin = Math.min;
 export const mathMax = Math.max;
 export const mathAbs = Math.abs;
+export const mathRound = Math.round;
+export const mathFloor = Math.floor;
+export const mathCeil = Math.ceil;
+export const mathPow = Math.pow;
+export const mathLog = Math.log;
+export const mathLN10 = Math.LN10;
+export const mathPI = Math.PI;
+export const mathRandom = Math.random;
 
 /**
  * Linear mapping a value from domain to range
@@ -149,8 +160,35 @@ export function parsePositionSizeOption(option: unknown, 
percentBase: number, pe
 }
 
 /**
- * (1) Fix rounding error of float numbers.
- * (2) Support return string to avoid scientific notation like '3.5e-7'.
+ * [Feature_1] Round at specified precision.
+ *  FIXME: this is not a general-purpose rounding implementation yet due to 
`TO_FIXED_SUPPORTED_PRECISION_MAX`.
+ *  e.g., `round(1.25 * 1e-150, 151)` has no overflow in IEEE754 64bit float, 
but can not be handled by
+ *  this method.
+ *
+ * [Feature_2] Support return string to avoid scientific notation like 
'3.5e-7'.
+ *
+ * [Feature_3] Fix rounding error of float numbers !!!ONLY SUITABLE FOR 
SPECIAL CASES!!!.
+ *  [CAVEAT]:
+ *      Rounding is NEVER a general-purpose solution for rounding errors.
+ *      Consider a case: `expect=123.99994999`, `actual=123.99995000` (suppose 
rounding error occurs).
+ *          Calling `round(expect, 4)` gets `123.9999`.
+ *          Calling `round(actual, 4)` gets `124.0000`.
+ *          A unacceptable result arises, even if the original difference is 
only `0.00000001` (tiny
+ *          and not strongly correlated with the digit pattern).
+ *      So the rounding approach works only if:
+ *          The digit next to the `precision` won't cross the rounding 
boundary. Typically, it works if
+ *          the digit next to the `precision` is expected to be `0`, and the 
rounding error is small
+ *          enough and impossible to affect that digit (`roundingError < 
Math.pow(10, -precision) / 2`).
+ *      The quantity of a rounding error can be roughly estimated by formula:
+ *          `minPrecisionRoundingErrorMayOccur ~= max(0, floor(14 - 
quantityExponent(val)))`
+ *          MEMO: This is derived from:
+ *              Let ` EXP52B10 = log10(pow(2, 52)) = 15.65355977452702 `
+ *                  (`52` is IEEE754 float64 mantissa bits count)
+ *              We require: ` abs(val) * pow(10, precision) < pow(10, 
EXP52B10) `
+ *              Hence: ` precision < EXP52B10 - log10(abs(val)) `
+ *              Hence: ` precision = floor( EXP52B10 - log10(abs(val)) ) `
+ *              Since: ` quantityExponent(val) = floor(log10(abs(val))) `
+ *              Hence: ` precision ~= floor(EXP52B10 - 1 - 
quantityExponent(val))
  */
 export function round(x: number | string, precision?: number): number;
 export function round(x: number | string, precision: number, returnStr: 
false): number;
@@ -162,7 +200,7 @@ export function round(x: number | string, precision?: 
number, returnStr?: boolea
         precision = 10;
     }
     // Avoid range error
-    precision = Math.min(Math.max(0, precision), 
ROUND_SUPPORTED_PRECISION_MAX);
+    precision = mathMin(mathMax(0, precision), 
TO_FIXED_SUPPORTED_PRECISION_MAX);
     // PENDING: 1.005.toFixed(2) is '1.00' rather than '1.01'
     x = (+x).toFixed(precision);
     return (returnStr ? x : +x);
@@ -181,6 +219,8 @@ export function asc<T extends number[]>(arr: T): T {
 
 /**
  * Get precision.
+ * e.g. `getPrecisionSafe(100.123)` return `3`.
+ * e.g. `getPrecisionSafe(100)` return `0`.
  */
 export function getPrecision(val: string | number): number {
     val = +val;
@@ -200,7 +240,7 @@ export function getPrecision(val: string | number): number {
     if (val > 1e-14) {
         let e = 1;
         for (let i = 0; i < 15; i++, e *= 10) {
-            if (Math.round(val * e) / e === val) {
+            if (mathRound(val * e) / e === val) {
                 return i;
             }
         }
@@ -211,6 +251,8 @@ export function getPrecision(val: string | number): number {
 
 /**
  * Get precision with slow but safe method
+ * e.g. `getPrecisionSafe(100.123)` return `3`.
+ * e.g. `getPrecisionSafe(100)` return `0`.
  */
 export function getPrecisionSafe(val: string | number): number {
     // toLowerCase for: '3.4E-12'
@@ -222,20 +264,57 @@ export function getPrecisionSafe(val: string | number): 
number {
     const significandPartLen = eIndex > 0 ? eIndex : str.length;
     const dotIndex = str.indexOf('.');
     const decimalPartLen = dotIndex < 0 ? 0 : significandPartLen - 1 - 
dotIndex;
-    return Math.max(0, decimalPartLen - exp);
+    return mathMax(0, decimalPartLen - exp);
 }
 
 /**
- * Minimal dicernible data precisioin according to a single pixel.
+ * @deprecated Use `getAcceptableTickPrecision` instead. See bad case in 
`test/ut/spec/util/number.test.ts`
+ * NOTE: originally introduced in commit 
`ff93e3e7f9ff24902e10d4469fd3187393b05feb`
+ *
+ * Minimal discernible data precision according to a single pixel.
  */
 export function getPixelPrecision(dataExtent: [number, number], pixelExtent: 
[number, number]): number {
-    const log = Math.log;
-    const LN10 = Math.LN10;
-    const dataQuantity = Math.floor(log(dataExtent[1] - dataExtent[0]) / LN10);
-    const sizeQuantity = Math.round(log(mathAbs(pixelExtent[1] - 
pixelExtent[0])) / LN10);
+    const dataQuantity = mathFloor(mathLog(dataExtent[1] - dataExtent[0]) / 
mathLN10);
+    const sizeQuantity = mathRound(mathLog(mathAbs(pixelExtent[1] - 
pixelExtent[0])) / mathLN10);
     // toFixed() digits argument must be between 0 and 20.
-    const precision = Math.min(Math.max(-dataQuantity + sizeQuantity, 0), 20);
-    return !isFinite(precision) ? 20 : precision;
+    const precision = mathMin(mathMax(-dataQuantity + sizeQuantity, 0), 
TO_FIXED_SUPPORTED_PRECISION_MAX);
+    return !isFinite(precision) ? TO_FIXED_SUPPORTED_PRECISION_MAX : precision;
+}
+
+/**
+ * This method chooses a reasonable "data" precision that can be used in 
`round` method.
+ * A reasonable precision is suitable for display; it may cause cumulative 
error but acceptable.
+ *
+ * "data" is linearly mapped to pixel according to the ratio determined by 
`dataSpan` and `pxSpan`.
+ * The diff from the original "data" to the rounded "data" (with the result 
precision) should be
+ * equal or less than `pxDiffAcceptable`, which is typically `1` pixel.
+ * And the result precision should be as small as possible.
+ *
+ * [NOTICE]: using arbitrary parameters is not preferable -- a discernible 
misalign (e.g., over 1px)
+ *  may occur, especially when `splitLine` displayed.
+ */
+export function getAcceptableTickPrecision(
+    // Typically, `Math.abs(dataExtent[1] - dataExtent[0])`.
+    dataSpan: number,
+    // Typically, `Math.abs(pixelExtent[1] - pixelExtent[0])`.
+    pxSpan: number,
+    // By default, `1`.
+    pxDiffAcceptable: number | NullUndefined
+    // Return a precision >= 0
+    // This precision can be used in method `round`.
+    // Return `NaN` for illegal inputs, such as `0`/`NaN`/`Infinity`.
+): number {
+    // Formula for choosing an acceptable precision:
+    //  Let `pxDiff = abs(dataSpan - round(dataSpan, precision))`.
+    //  We require `pxDiff <= dataSpan * pxDiffAcceptable / pxSpan`.
+    //  Consider the nature of "round", the max `pxDiff` is: `pow(10, 
-precision) / 2`,
+    //  Hence: `pow(10, -precision) / 2 <= dataSpan * pxDiffAcceptable / 
pxSpan`
+    //  Hence: `precision >= -log10(2 * dataSpan * pxDiffAcceptable / pxSpan)`
+    const dataExp2 = mathLog(2 * mathAbs(pxDiffAcceptable || 1) * 
mathAbs(dataSpan)) / mathLN10;
+    const pxExp = mathLog(mathAbs(pxSpan)) / mathLN10;
+    // PENDING: Rounding error generally does not matter; do not fix it before 
`Math.ceil`
+    // until bad case occur.
+    return mathMax(0, mathCeil(-dataExp2 + pxExp));
 }
 
 /**
@@ -277,7 +356,7 @@ export function getPercentSeats(valueList: number[], 
precision: number): number[
         return [];
     }
 
-    const digits = Math.pow(10, precision);
+    const digits = mathPow(10, precision);
     const votesPerQuota = zrUtil.map(valueList, function (val) {
         return (isNaN(val) ? 0 : val) / sum * digits * 100;
     });
@@ -285,7 +364,7 @@ export function getPercentSeats(valueList: number[], 
precision: number): number[
 
     const seats = zrUtil.map(votesPerQuota, function (votes) {
         // Assign automatic seats.
-        return Math.floor(votes);
+        return mathFloor(votes);
     });
     let currentSum = zrUtil.reduce(seats, function (acc, val) {
         return acc + val;
@@ -322,12 +401,12 @@ export function getPercentSeats(valueList: number[], 
precision: number): number[
  * See <http://0.30000000000000004.com/>
  */
 export function addSafe(val0: number, val1: number): number {
-    const maxPrecision = Math.max(getPrecision(val0), getPrecision(val1));
+    const maxPrecision = mathMax(getPrecision(val0), getPrecision(val1));
     // const multiplier = Math.pow(10, maxPrecision);
-    // return (Math.round(val0 * multiplier) + Math.round(val1 * multiplier)) 
/ multiplier;
+    // return (mathRound(val0 * multiplier) + mathRound(val1 * multiplier)) / 
multiplier;
     const sum = val0 + val1;
     // // PENDING: support more?
-    return maxPrecision > ROUND_SUPPORTED_PRECISION_MAX
+    return maxPrecision > TO_FIXED_SUPPORTED_PRECISION_MAX
         ? sum : round(sum, maxPrecision);
 }
 
@@ -338,7 +417,7 @@ export const MAX_SAFE_INTEGER = 9007199254740991;
  * To 0 - 2 * PI, considering negative radian.
  */
 export function remRadian(radian: number): number {
-    const pi2 = Math.PI * 2;
+    const pi2 = mathPI * 2;
     return (radian % pi2 + pi2) % pi2;
 }
 
@@ -427,7 +506,7 @@ export function parseDate(value: unknown): Date {
         return new Date(NaN);
     }
 
-    return new Date(Math.round(value as number));
+    return new Date(mathRound(value as number));
 }
 
 /**
@@ -437,50 +516,73 @@ export function parseDate(value: unknown): Date {
  * @return
  */
 export function quantity(val: number): number {
-    return Math.pow(10, quantityExponent(val));
+    return mathPow(10, quantityExponent(val));
 }
 
 /**
  * Exponent of the quantity of a number
- * e.g., 1234 equals to 1.234*10^3, so quantityExponent(1234) is 3
+ * e.g., 9876 equals to 9.876*10^3, so quantityExponent(9876) is 3
+ * e.g., 0.09876 equals to 9.876*10^-2, so quantityExponent(0.09876) is -2
  *
  * @param val non-negative value
  * @return
  */
 export function quantityExponent(val: number): number {
     if (val === 0) {
+        // PENDING: like IEEE754 use exponent `0` in this case.
+        // but methematically, exponent of zero is `-Infinity`.
         return 0;
     }
 
-    let exp = Math.floor(Math.log(val) / Math.LN10);
+    let exp = mathFloor(mathLog(val) / mathLN10);
     /**
      * exp is expected to be the rounded-down result of the base-10 log of val.
      * But due to the precision loss with Math.log(val), we need to restore it
      * using 10^exp to make sure we can get val back from exp. #11249
      */
-    if (val / Math.pow(10, exp) >= 10) {
+    if (val / mathPow(10, exp) >= 10) {
         exp++;
     }
     return exp;
 }
 
+export const NICE_MODE_ROUND = 1 as const;
+export const NICE_MODE_MIN = 2 as const;
+
 /**
- * find a “nice” number approximately equal to x. Round the number if round = 
true,
- * take ceiling if round = false. The primary observation is that the “nicest”
+ * find a “nice” number approximately equal to x. Round the number if 'round',
+ * take ceiling if 'round'. The primary observation is that the “nicest”
  * numbers in decimal are 1, 2, and 5, and all power-of-ten multiples of these 
numbers.
  *
  * See "Nice Numbers for Graph Labels" of Graphic Gems.
  *
  * @param  val Non-negative value.
- * @param  round
  * @return Niced number
  */
-export function nice(val: number, round?: boolean): number {
+export function nice(
+    val: number,
+    // All non-`NICE_MODE_MIN`-truthy values means `NICE_MODE_ROUND`, for 
backward compatibility.
+    mode?: boolean | typeof NICE_MODE_ROUND | typeof NICE_MODE_MIN
+): number {
+    // Consider the scientific notation of `val`:
+    //  - `exponent` is its exponent.
+    //  - `f` is its coefficient. `1 <= f < 10`.
+    //  e.g., if `val` is `0.0054321`, `exponent` is `-3`, `f` is `5.4321`,
+    //      The result is `0.005` on NICE_MODE_ROUND.
+    //  e.g., if `val` is `987.12345`, `exponent` is `2`, `f` is `9.8712345`,
+    //      The result is `1000` on NICE_MODE_ROUND.
+    //  e.g., if `val` is `0`,
+    //      The result is `1`.
     const exponent = quantityExponent(val);
-    const exp10 = Math.pow(10, exponent);
-    const f = val / exp10; // 1 <= f < 10
+    // No rounding error in Math.pow(10, xxx).
+    const exp10 = mathPow(10, exponent);
+    const f = val / exp10;
+
     let nf;
-    if (round) {
+    if (mode === NICE_MODE_MIN) {
+        nf = 1;
+    }
+    else if (mode) {
         if (f < 1.5) {
             nf = 1;
         }
@@ -516,9 +618,8 @@ export function nice(val: number, round?: boolean): number {
     }
     val = nf * exp10;
 
-    // Fix 3 * 0.1 === 0.30000000000000004 issue (see IEEE 754).
-    // 20 is the uppper bound of toFixed.
-    return exponent >= -20 ? +val.toFixed(exponent < 0 ? -exponent : 0) : val;
+    // Fix IEEE 754 float rounding error
+    return round(val, -exponent);
 }
 
 /**
@@ -529,7 +630,7 @@ export function nice(val: number, round?: boolean): number {
  */
 export function quantile(ascArr: number[], p: number): number {
     const H = (ascArr.length - 1) * p + 1;
-    const h = Math.floor(H);
+    const h = mathFloor(H);
     const v = +ascArr[h - 1];
     const e = H - h;
     return e ? v + e * (ascArr[h] - v) : v;
@@ -640,7 +741,7 @@ export function isNumeric(val: unknown): val is number {
  * @return An positive integer.
  */
 export function getRandomIdBase(): number {
-    return Math.round(Math.random() * 9);
+    return mathRound(mathRandom() * 9);
 }
 
 /**
diff --git a/test/ut/spec/util/number.test.ts b/test/ut/spec/util/number.test.ts
index 52d872130..25bb4502a 100755
--- a/test/ut/spec/util/number.test.ts
+++ b/test/ut/spec/util/number.test.ts
@@ -21,7 +21,9 @@
 import {
     linearMap, parseDate, reformIntervals, getPrecisionSafe, getPrecision,
     getPercentWithPrecision, quantityExponent, quantity, nice,
-    isNumeric, numericToNumber, addSafe
+    isNumeric, numericToNumber, addSafe,
+    getPixelPrecision,
+    getAcceptablePrecision
 } from '@/src/util/number';
 
 
@@ -1015,4 +1017,74 @@ describe('util/number', function () {
         testNumeric(function () {}, NaN, false);
     });
 
+    describe('getAcceptablePrecision', function () {
+        // NOTICE: These cases fail in `getPixelPrecision` (pxDiff1 > 1)
+        const CASES = [
+            // dataExtent                      pixelExtent                  
precision1 precision2 diff1 diff2
+            [ [ 0, 1e-3 ],                     [ 0, 100 ] ],                // 
1 0
+            [ [ 0, 1e5 ],                      [ 0, 100 ] ],                // 
1 0
+            [ [ 0, 816.2050883836147 ],        [ 0, 914.7923109827166 ] ],  // 
1 0
+            [ [ 0, 132.4279201671552 ],        [ 0, 267.9399859644955 ] ],  // 
0 1 1.011644620055545 0.1011644620055545
+            [ [ 0, 100.34020279327427 ],       [ 0, 287.77043437322726 ] ], // 
0 1 1.433973753103259 0.14339737531032593
+            [ [ 0, 131.76288568225613 ],       [ 0, 268.9583525845105 ] ],  // 
0 1 1.020614990298174 0.10206149902981741
+            [ [ 0, 100.28571954202148 ],       [ 0, 256.9972613326965 ] ],  // 
0 1 1.2813253098563555 0.12813253098563557
+            [ [ 0, 104.20905450687412 ],       [ 0, 301.06863468545566 ] ], // 
0 1 1.4445416288926973 0.14445416288926974
+            [ [ 0, 0.0000012212958760328775 ], [ 0, 30.161832948821356 ] ], // 
7 8 1.234829067252553 0.12348290672525532 fail1
+            [ [ 0, 1.0169256034269881e-7 ],    [ 0, 293.5116394339741 ] ],  // 
9 10 1.4431323119648805 0.14431323119648806 fail1
+            [ [ 0, 0.0011105264071798859 ],    [ 0, 222.30675252167865 ] ], // 
5 6 1.0009070972306418 0.10009070972306418 fail1
+            [ [ 0, 0.00010498610084514804 ],   [ 0, 264.6383246939843 ] ],  // 
6 7 1.260349334643447 0.1260349334643447 fail1
+        ];
+        const NAN_CASES = [
+            [ [ 0, 0 ],                        [ 0, 100 ] ],
+        ];
+
+        // We require `diff * pxSpan / dataSpan <= pxDiffAcceptable`.
+        // The max `diff` is: `pow(10, -precision) / 2`.
+        function calcMaxPxDiff(dataExtent: number[], pxExtent: number[], 
precision: number): number {
+            return Math.pow(10, -precision) / 2
+                * Math.abs(pxExtent[1] - pxExtent[0])
+                / Math.abs(dataExtent[1] - dataExtent[0]);
+        }
+
+        for (let idx = 0; idx < CASES.length; idx++) {
+            const caseItem = CASES[idx];
+            const dataExtent = caseItem[0];
+            const pxExtent = caseItem[1];
+            const precision1 = getPixelPrecision(
+                dataExtent.slice() as [number, number],
+                pxExtent.slice() as [number, number]
+            );
+            const precision2 = getAcceptablePrecision(
+                dataExtent[1] - dataExtent[0],
+                pxExtent[1] - pxExtent[0],
+                null
+            );
+            const pxDiff1 = calcMaxPxDiff(dataExtent, pxExtent, precision1);
+            const pxDiff2 = calcMaxPxDiff(dataExtent, pxExtent, precision2);
+            expect(pxDiff1).toBeFinite(); // May > 1 (bad case).
+            expect(pxDiff2).toBeLessThanOrEqual(1);
+
+            // if (precision1 > 1) {
+            //     console.log(
+            //         dataExtent, pxExtent, '---',
+            //         precision1, precision2, pxDiff1, pxDiff2
+            //     );
+            // }
+        }
+
+        for (let idx = 0; idx < NAN_CASES.length; idx++) {
+            const caseItem = NAN_CASES[idx];
+            const dataExtent = caseItem[0];
+            const pxExtent = caseItem[1];
+            const precision2 = getAcceptablePrecision(
+                dataExtent[1] - dataExtent[0],
+                pxExtent[1] - pxExtent[0],
+                null
+            );
+            const pxDiff2 = calcMaxPxDiff(dataExtent, pxExtent, precision2);
+            expect(pxDiff2).toBeNaN();
+        }
+
+    });
+
 });
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to