-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
getNiceTickValues.ts
249 lines (214 loc) · 8.31 KB
/
getNiceTickValues.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
/**
* @fileOverview calculate tick values of scale
* @author xile611, arcthur
* @date 2015-09-17
*/
import Decimal from 'decimal.js-light';
import { compose, range, memoize, map, reverse } from './util/utils';
import { getDigitCount, rangeStep } from './util/arithmetic';
export type StepRatioControl = 0.05 | 0.03 | 0.01;
/**
* Calculate a interval of a minimum value and a maximum value
*
* @param {Number} min The minimum value
* @param {Number} max The maximum value
* @return {Array} An interval
*/
export const getValidInterval = ([min, max]: [number, number]) => {
let [validMin, validMax] = [min, max];
// exchange
if (min > max) {
[validMin, validMax] = [max, min];
}
return [validMin, validMax];
};
/**
* Calculate the step which is easy to understand between ticks, like 10, 20, 25
*
* @param {Decimal} roughStep The rough step calculated by deviding the
* difference by the tickCount
* @param {Boolean} allowDecimals Allow the ticks to be decimals or not
* @param {Integer} correctionFactor A correction factor
* @param {StepRatioControl} stepRatioControl The value to control the step of y domain
* @return {Decimal} The step which is easy to understand between two ticks
*/
export const getFormatStep = (
roughStep: Decimal,
allowDecimals: boolean,
correctionFactor: number,
stepRatioControl: StepRatioControl = 0.05,
) => {
if (roughStep.lte(0)) {
return new Decimal(0);
}
const digitCount = getDigitCount(roughStep.toNumber());
// The ratio between the rough step and the smallest number which has a bigger
// order of magnitudes than the rough step
const digitCountValue = new Decimal(10).pow(digitCount);
const stepRatio = roughStep.div(digitCountValue);
// When an integer and a float multiplied, the accuracy of result may be wrong
const stepRatioScale = digitCount !== 1 ? stepRatioControl : 0.1;
const amendStepRatio = new Decimal(Math.ceil(stepRatio.div(stepRatioScale).toNumber()))
.add(correctionFactor)
.mul(stepRatioScale);
const formatStep = amendStepRatio.mul(digitCountValue);
return allowDecimals ? new Decimal(formatStep.toNumber()) : new Decimal(Math.ceil(formatStep.toNumber()));
};
/**
* calculate the ticks when the minimum value equals to the maximum value
*
* @param {Number} value The minimum valuue which is also the maximum value
* @param {Integer} tickCount The count of ticks
* @param {Boolean} allowDecimals Allow the ticks to be decimals or not
* @return {Array} ticks
*/
export const getTickOfSingleValue = (value: number, tickCount: number, allowDecimals: boolean) => {
let step: Decimal = new Decimal(1);
// calculate the middle value of ticks
let middle = new Decimal(value);
if (!middle.isint() && allowDecimals) {
const absVal = Math.abs(value);
if (absVal < 1) {
// The step should be a float number when the difference is smaller than 1
step = new Decimal(10).pow(getDigitCount(value) - 1);
middle = new Decimal(Math.floor(middle.div(step).toNumber())).mul(step);
} else if (absVal > 1) {
// Return the maximum integer which is smaller than 'value' when 'value' is greater than 1
middle = new Decimal(Math.floor(value));
}
} else if (value === 0) {
middle = new Decimal(Math.floor((tickCount - 1) / 2));
} else if (!allowDecimals) {
middle = new Decimal(Math.floor(value));
}
const middleIndex = Math.floor((tickCount - 1) / 2);
const fn = compose(
map((n: number) => middle.add(new Decimal(n - middleIndex).mul(step)).toNumber()),
range,
);
return fn(0, tickCount);
};
/**
* Calculate the step
*
* @param {Number} min The minimum value of an interval
* @param {Number} max The maximum value of an interval
* @param {Integer} tickCount The count of ticks
* @param {Boolean} allowDecimals Allow the ticks to be decimals or not
* @param {Number} correctionFactor A correction factor
* @param {StepRatioControl} stepRatioControl The value to control the step of y domain
* @return {Object} The step, minimum value of ticks, maximum value of ticks
*/
export const calculateStep = (
min: number,
max: number,
tickCount: number,
allowDecimals: boolean,
correctionFactor = 0,
stepRatioControl: StepRatioControl = 0.05,
): any => {
// dirty hack (for recharts' test)
if (!Number.isFinite((max - min) / (tickCount - 1))) {
return {
step: new Decimal(0),
tickMin: new Decimal(0),
tickMax: new Decimal(0),
};
}
// The step which is easy to understand between two ticks
const step = getFormatStep(
new Decimal(max).sub(min).div(tickCount - 1),
allowDecimals,
correctionFactor,
stepRatioControl,
);
// A medial value of ticks
let middle;
// When 0 is inside the interval, 0 should be a tick
if (min <= 0 && max >= 0) {
middle = new Decimal(0);
} else {
// calculate the middle value
middle = new Decimal(min).add(max).div(2);
// minus modulo value
middle = middle.sub(new Decimal(middle).mod(step));
}
let belowCount = Math.ceil(middle.sub(min).div(step).toNumber());
let upCount = Math.ceil(new Decimal(max).sub(middle).div(step).toNumber());
const scaleCount = belowCount + upCount + 1;
if (scaleCount > tickCount) {
// When more ticks need to cover the interval, step should be bigger.
return calculateStep(min, max, tickCount, allowDecimals, correctionFactor + 1, stepRatioControl);
}
if (scaleCount < tickCount) {
// When less ticks can cover the interval, we should add some additional ticks
upCount = max > 0 ? upCount + (tickCount - scaleCount) : upCount;
belowCount = max > 0 ? belowCount : belowCount + (tickCount - scaleCount);
}
return {
step,
tickMin: middle.sub(new Decimal(belowCount).mul(step)),
tickMax: middle.add(new Decimal(upCount).mul(step)),
};
};
/**
* Calculate the ticks of an interval, the count of ticks will be guraranteed
*
* @param {Number} min, max min: The minimum value, max: The maximum value
* @param {Integer} tickCount The count of ticks
* @param {Boolean} allowDecimals Allow the ticks to be decimals or not
* @param {StepRatioControl} stepRatioControl The value to control the step of y domain
* @return {Array} ticks
*/
function getNiceTickValuesFn(
[min, max]: [number, number],
tickCount = 6,
allowDecimals = true,
stepRatioControl: StepRatioControl = 0.05,
) {
// More than two ticks should be return
const count = Math.max(tickCount, 2);
const [cormin, cormax] = getValidInterval([min, max]);
if (cormin === -Infinity || cormax === Infinity) {
const values =
cormax === Infinity
? [cormin, ...range(0, tickCount - 1).map(() => Infinity)]
: [...range(0, tickCount - 1).map(() => -Infinity), cormax];
return min > max ? reverse(values) : values;
}
if (cormin === cormax) {
return getTickOfSingleValue(cormin, tickCount, allowDecimals);
}
// Get the step between two ticks
const { step, tickMin, tickMax } = calculateStep(cormin, cormax, count, allowDecimals, 0, stepRatioControl);
const values = rangeStep(tickMin, tickMax.add(new Decimal(0.1).mul(step)), step);
return min > max ? reverse(values) : values;
}
/**
* Calculate the ticks of an interval, the count of ticks won't be guraranteed,
* but the domain will be guaranteed
*
* @param {Number} min, max min: The minimum value, max: The maximum value
* @param {Integer} tickCount The count of ticks
* @param {Boolean} allowDecimals Allow the ticks to be decimals or not
* @return {Array} ticks
*/
function getTickValuesFixedDomainFn([min, max]: [number, number], tickCount: number, allowDecimals = true) {
// More than two ticks should be return
const [cormin, cormax] = getValidInterval([min, max]);
if (cormin === -Infinity || cormax === Infinity) {
return [min, max];
}
if (cormin === cormax) {
return [cormin];
}
const count = Math.max(tickCount, 2);
const step = getFormatStep(new Decimal(cormax).sub(cormin).div(count - 1), allowDecimals, 0);
const values = [
...rangeStep(new Decimal(cormin), new Decimal(cormax).sub(new Decimal(0.99).mul(step)), step),
cormax,
];
return min > max ? reverse(values) : values;
}
export const getNiceTickValues = memoize(getNiceTickValuesFn);
export const getTickValuesFixedDomain = memoize(getTickValuesFixedDomainFn);