2020-05-11 00:37:22 -05:00
|
|
|
export class RelativeScale {
|
2020-06-05 17:02:31 -05:00
|
|
|
static scale (data, tickCount, maxFactor) {
|
2020-06-08 16:29:15 -05:00
|
|
|
const { min, max } = RelativeScale.calculateBounds(data)
|
2020-05-10 23:39:35 -05:00
|
|
|
|
|
|
|
let factor = 1
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
const scale = Math.pow(10, factor)
|
|
|
|
|
|
|
|
const scaledMin = min - (min % scale)
|
2021-05-31 01:27:20 -05:00
|
|
|
let scaledMax = max + (max % scale === 0 ? 0 : scale - (max % scale))
|
2020-06-17 22:22:29 -05:00
|
|
|
|
|
|
|
// Prevent min/max from being equal (and generating 0 ticks)
|
|
|
|
// This happens when all data points are products of scale value
|
|
|
|
if (scaledMin === scaledMax) {
|
|
|
|
scaledMax += scale
|
|
|
|
}
|
2020-05-10 23:39:35 -05:00
|
|
|
|
|
|
|
const ticks = (scaledMax - scaledMin) / scale
|
|
|
|
|
2020-06-08 16:29:15 -05:00
|
|
|
if (ticks <= tickCount || (typeof maxFactor === 'number' && factor === maxFactor)) {
|
|
|
|
return {
|
|
|
|
scaledMin,
|
|
|
|
scaledMax,
|
|
|
|
scale
|
|
|
|
}
|
2020-05-10 23:39:35 -05:00
|
|
|
} else {
|
|
|
|
// Too many steps between min/max, increase factor and try again
|
|
|
|
factor++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-05 17:02:31 -05:00
|
|
|
static scaleMatrix (data, tickCount, maxFactor) {
|
2021-05-31 01:27:20 -05:00
|
|
|
const nonNullData = data.flat().filter((val) => val !== null)
|
2020-05-11 03:10:23 -05:00
|
|
|
|
2021-05-31 01:27:20 -05:00
|
|
|
// when used with the spread operator large nonNullData/data arrays can reach the max call stack size
|
|
|
|
// use reduce calls to safely determine min/max values for any size of array
|
|
|
|
// https://stackoverflow.com/questions/63705432/maximum-call-stack-size-exceeded-when-using-the-dots-operator/63706516#63706516
|
|
|
|
const max = nonNullData.reduce((a, b) => {
|
|
|
|
return Math.max(a, b)
|
2021-06-07 11:59:56 -05:00
|
|
|
}, Number.NEGATIVE_INFINITY)
|
2021-05-31 01:27:20 -05:00
|
|
|
|
|
|
|
return RelativeScale.scale(
|
|
|
|
[0, RelativeScale.isFiniteOrZero(max)],
|
|
|
|
tickCount,
|
|
|
|
maxFactor
|
|
|
|
)
|
2020-05-11 02:28:41 -05:00
|
|
|
}
|
|
|
|
|
2020-05-10 23:39:35 -05:00
|
|
|
static generateTicks (min, max, step) {
|
|
|
|
const ticks = []
|
|
|
|
for (let i = min; i <= max; i += step) {
|
|
|
|
ticks.push(i)
|
|
|
|
}
|
|
|
|
return ticks
|
|
|
|
}
|
|
|
|
|
|
|
|
static calculateBounds (data) {
|
|
|
|
if (data.length === 0) {
|
2020-06-08 16:29:15 -05:00
|
|
|
return {
|
|
|
|
min: 0,
|
|
|
|
max: 0
|
2020-05-10 23:39:35 -05:00
|
|
|
}
|
2020-06-08 16:29:15 -05:00
|
|
|
} else {
|
2021-05-31 01:27:20 -05:00
|
|
|
const nonNullData = data.filter((val) => val !== null)
|
|
|
|
|
|
|
|
// when used with the spread operator large nonNullData/data arrays can reach the max call stack size
|
|
|
|
// use reduce calls to safely determine min/max values for any size of array
|
|
|
|
// https://stackoverflow.com/questions/63705432/maximum-call-stack-size-exceeded-when-using-the-dots-operator/63706516#63706516
|
|
|
|
const min = nonNullData.reduce((a, b) => {
|
|
|
|
return Math.min(a, b)
|
2021-06-07 11:59:56 -05:00
|
|
|
}, Number.POSITIVE_INFINITY)
|
2021-05-31 01:27:20 -05:00
|
|
|
const max = nonNullData.reduce((a, b) => {
|
|
|
|
return Math.max(a, b)
|
2021-06-07 11:59:56 -05:00
|
|
|
}, Number.NEGATIVE_INFINITY)
|
2020-05-10 23:39:35 -05:00
|
|
|
|
2020-06-08 16:29:15 -05:00
|
|
|
return {
|
|
|
|
min: RelativeScale.isFiniteOrZero(min),
|
|
|
|
max: RelativeScale.isFiniteOrZero(max)
|
2020-05-11 03:10:23 -05:00
|
|
|
}
|
2020-05-10 23:39:35 -05:00
|
|
|
}
|
|
|
|
}
|
2020-06-08 16:29:15 -05:00
|
|
|
|
|
|
|
static isFiniteOrZero (val) {
|
|
|
|
return Number.isFinite(val) ? val : 0
|
|
|
|
}
|
2020-05-10 23:39:35 -05:00
|
|
|
}
|