export class RelativeScale { static scale(data, tickCount, maxFactor) { const { min, max } = RelativeScale.calculateBounds(data); let factor = 1; while (true) { const scale = Math.pow(10, factor); const scaledMin = min - (min % scale); let scaledMax = max + (max % scale === 0 ? 0 : scale - (max % scale)); // 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; } const ticks = (scaledMax - scaledMin) / scale; if ( ticks <= tickCount || (typeof maxFactor === "number" && factor === maxFactor) ) { return { scaledMin, scaledMax, scale, }; } else { // Too many steps between min/max, increase factor and try again factor++; } } } static scaleMatrix(data, tickCount, maxFactor) { const nonNullData = data.flat().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 max = nonNullData.reduce((a, b) => { return Math.max(a, b); }, Number.NEGATIVE_INFINITY); return RelativeScale.scale( [0, RelativeScale.isFiniteOrZero(max)], tickCount, maxFactor ); } 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) { return { min: 0, max: 0, }; } else { 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); }, Number.POSITIVE_INFINITY); const max = nonNullData.reduce((a, b) => { return Math.max(a, b); }, Number.NEGATIVE_INFINITY); return { min: RelativeScale.isFiniteOrZero(min), max: RelativeScale.isFiniteOrZero(max), }; } } static isFiniteOrZero(val) { return Number.isFinite(val) ? val : 0; } }