Skip to content

Commit 479dcd4

Browse files
committed
fix&feat: Change and clarify the rounding error and auto-precision utils and solutions.
1 parent d013d5e commit 479dcd4

3 files changed

Lines changed: 332 additions & 63 deletions

File tree

src/scale/helper.ts

Lines changed: 120 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,54 @@
1717
* under the License.
1818
*/
1919

20-
import {getPrecision, round, nice, quantityExponent} from '../util/number';
20+
import {getPrecision, round, nice, quantityExponent, mathPow, mathMax, mathRound} from '../util/number';
2121
import IntervalScale from './Interval';
2222
import LogScale from './Log';
2323
import type Scale from './Scale';
2424
import { bind } from 'zrender/src/core/util';
2525
import type { ScaleBreakContext } from './break';
26+
import TimeScale from './Time';
27+
import { NullUndefined } from '../util/types';
2628

2729
type intervalScaleNiceTicksResult = {
2830
interval: number,
2931
intervalPrecision: number,
3032
niceTickExtent: [number, number]
3133
};
3234

33-
export function isValueNice(val: number) {
34-
const exp10 = Math.pow(10, quantityExponent(Math.abs(val)));
35-
const f = Math.abs(val / exp10);
36-
return f === 0
37-
|| f === 1
38-
|| f === 2
39-
|| f === 3
40-
|| f === 5;
41-
}
35+
/**
36+
* See also method `nice` in `src/util/number.ts`.
37+
*/
38+
// export function isValueNice(val: number) {
39+
// const exp10 = Math.pow(10, quantityExponent(Math.abs(val)));
40+
// const f = Math.abs(round(val / exp10, 0));
41+
// return f === 0
42+
// || f === 1
43+
// || f === 2
44+
// || f === 3
45+
// || f === 5;
46+
// }
4247

4348
export function isIntervalOrLogScale(scale: Scale): scale is LogScale | IntervalScale {
44-
return scale.type === 'interval' || scale.type === 'log';
49+
return isIntervalScale(scale) || isLogScale(scale);
50+
}
51+
52+
export function isIntervalScale(scale: Scale): scale is IntervalScale {
53+
return scale.type === 'interval';
4554
}
55+
56+
export function isTimeScale(scale: Scale): scale is TimeScale {
57+
return scale.type === 'time';
58+
}
59+
60+
export function isLogScale(scale: Scale): scale is LogScale {
61+
return scale.type === 'log';
62+
}
63+
64+
export function isOrdinalScale(scale: Scale): boolean {
65+
return scale.type === 'ordinal';
66+
}
67+
4668
/**
4769
* @param extent Both extent[0] and extent[1] should be valid number.
4870
* Should be extent[0] < extent[1].
@@ -65,23 +87,29 @@ export function intervalScaleNiceTicks(
6587
if (maxInterval != null && interval > maxInterval) {
6688
interval = result.interval = maxInterval;
6789
}
68-
// Tow more digital for tick.
6990
const precision = result.intervalPrecision = getIntervalPrecision(interval);
7091
// Niced extent inside original extent
7192
const niceTickExtent = result.niceTickExtent = [
7293
round(Math.ceil(extent[0] / interval) * interval, precision),
7394
round(Math.floor(extent[1] / interval) * interval, precision)
7495
];
7596

76-
fixExtent(niceTickExtent, extent);
97+
fixNiceExtent(niceTickExtent, extent);
7798

7899
return result;
79100
}
80101

81-
export function increaseInterval(interval: number) {
82-
const exp10 = Math.pow(10, quantityExponent(interval));
83-
// Increase interval
84-
let f = interval / exp10;
102+
/**
103+
* The input `niceInterval` should be generated
104+
* from `nice` method in `src/util/number.ts`, or
105+
* from `increaseInterval` itself.
106+
*/
107+
export function increaseInterval(niceInterval: number) {
108+
const exponent = quantityExponent(niceInterval);
109+
// No rounding error in Math.pow(10, xxx).
110+
const exp10 = mathPow(10, exponent);
111+
// Fix IEEE 754 float rounding error
112+
let f = mathRound(niceInterval / exp10);
85113
if (!f) {
86114
f = 1;
87115
}
@@ -94,25 +122,26 @@ export function increaseInterval(interval: number) {
94122
else { // f is 1 or 5
95123
f *= 2;
96124
}
97-
return round(f * exp10);
125+
// Fix IEEE 754 float rounding error
126+
return round(f * exp10, -exponent);
98127
}
99128

100-
/**
101-
* @return interval precision
102-
*/
103-
export function getIntervalPrecision(interval: number): number {
129+
export function getIntervalPrecision(niceInterval: number): number {
104130
// Tow more digital for tick.
105-
return getPrecision(interval) + 2;
131+
// NOTE: `2` was introduced in commit `af2a2a9f6303081d7c3b52f0a38add07b4c6e0c7`;
132+
// it works on "nice" interval, but seems not necessarily mathematically required.
133+
return getPrecision(niceInterval) + 2;
106134
}
107135

136+
108137
function clamp(
109138
niceTickExtent: [number, number], idx: number, extent: [number, number]
110139
): void {
111140
niceTickExtent[idx] = Math.max(Math.min(niceTickExtent[idx], extent[1]), extent[0]);
112141
}
113142

114143
// In some cases (e.g., splitNumber is 1), niceTickExtent may be out of extent.
115-
export function fixExtent(
144+
export function fixNiceExtent(
116145
niceTickExtent: [number, number], extent: [number, number]
117146
): void {
118147
!isFinite(niceTickExtent[0]) && (niceTickExtent[0] = extent[0]);
@@ -173,3 +202,70 @@ export function logTransform(base: number, extent: number[], noClampNegative?: b
173202
Math.log(noClampNegative ? extent[1] : Math.max(0, extent[1])) / loggedBase
174203
];
175204
}
205+
206+
export function powTransform(base: number, extent: number[]): [number, number] {
207+
return [
208+
mathPow(base, extent[0]),
209+
mathPow(base, extent[1])
210+
];
211+
}
212+
213+
/**
214+
* A valid extent is:
215+
* - No non-finite number.
216+
* - `extent[0] < extent[1]`.
217+
*
218+
* [NOTICE]: The input `rawExtent` can only be:
219+
* - All non-finite numbers or `NaN`; or
220+
* - `[Infinity, -Infinity]` (A typical initial extent with no data.)
221+
* (Improve it when needed.)
222+
*/
223+
export function intervalScaleEnsureValidExtent(
224+
rawExtent: number[],
225+
opt: {
226+
fixMax?: boolean
227+
}
228+
): number[] {
229+
const extent = rawExtent.slice();
230+
// If extent start and end are same, expand them
231+
if (extent[0] === extent[1]) {
232+
if (extent[0] !== 0) {
233+
// Expand extent
234+
// Note that extents can be both negative. See #13154
235+
const expandSize = Math.abs(extent[0]);
236+
// In the fowllowing case
237+
// Axis has been fixed max 100
238+
// Plus data are all 100 and axis extent are [100, 100].
239+
// Extend to the both side will cause expanded max is larger than fixed max.
240+
// So only expand to the smaller side.
241+
if (!opt.fixMax) {
242+
extent[1] += expandSize / 2;
243+
extent[0] -= expandSize / 2;
244+
}
245+
else {
246+
extent[0] -= expandSize / 2;
247+
}
248+
}
249+
else {
250+
extent[1] = 1;
251+
}
252+
}
253+
const span = extent[1] - extent[0];
254+
// If there are no data and extent are [Infinity, -Infinity]
255+
if (!isFinite(span)) {
256+
extent[0] = 0;
257+
extent[1] = 1;
258+
}
259+
else if (span < 0) {
260+
extent.reverse();
261+
}
262+
263+
return extent;
264+
}
265+
266+
export function ensureValidSplitNumber(
267+
rawSplitNumber: number | NullUndefined, defaultSplitNumber: number
268+
): number {
269+
rawSplitNumber = rawSplitNumber || defaultSplitNumber;
270+
return mathRound(mathMax(rawSplitNumber, 1));
271+
}

0 commit comments

Comments
 (0)