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]
