248 lines
6.5 KiB
JavaScript
248 lines
6.5 KiB
JavaScript
import { floatTwo } from "./helpers";
|
|
|
|
function normalize(x) {
|
|
// Calculates mantissa and exponent of a number
|
|
// Returns normalized number and exponent
|
|
// https://stackoverflow.com/q/9383593/6495043
|
|
|
|
if (x === 0) {
|
|
return [0, 0];
|
|
}
|
|
if (isNaN(x)) {
|
|
return { mantissa: -6755399441055744, exponent: 972 };
|
|
}
|
|
var sig = x > 0 ? 1 : -1;
|
|
if (!isFinite(x)) {
|
|
return { mantissa: sig * 4503599627370496, exponent: 972 };
|
|
}
|
|
|
|
x = Math.abs(x);
|
|
var exp = Math.floor(Math.log10(x));
|
|
var man = x / Math.pow(10, exp);
|
|
|
|
return [sig * man, exp];
|
|
}
|
|
|
|
function getChartRangeIntervals(max, min = 0) {
|
|
let upperBound = Math.ceil(max);
|
|
let lowerBound = Math.floor(min);
|
|
let range = upperBound - lowerBound;
|
|
|
|
let noOfParts = range;
|
|
let partSize = 1;
|
|
|
|
// To avoid too many partitions
|
|
if (range > 5) {
|
|
if (range % 2 !== 0) {
|
|
upperBound++;
|
|
// Recalc range
|
|
range = upperBound - lowerBound;
|
|
}
|
|
noOfParts = range / 2;
|
|
partSize = 2;
|
|
}
|
|
|
|
// Special case: 1 and 2
|
|
if (range <= 2) {
|
|
noOfParts = 4;
|
|
partSize = range / noOfParts;
|
|
}
|
|
|
|
// Special case: 0
|
|
if (range === 0) {
|
|
noOfParts = 5;
|
|
partSize = 1;
|
|
}
|
|
|
|
let intervals = [];
|
|
for (var i = 0; i <= noOfParts; i++) {
|
|
intervals.push(lowerBound + partSize * i);
|
|
}
|
|
return intervals;
|
|
}
|
|
|
|
function getChartIntervals(maxValue, minValue = 0) {
|
|
let [normalMaxValue, exponent] = normalize(maxValue);
|
|
let normalMinValue = minValue ? minValue / Math.pow(10, exponent) : 0;
|
|
|
|
// Allow only 7 significant digits
|
|
normalMaxValue = normalMaxValue.toFixed(6);
|
|
|
|
let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue);
|
|
intervals = intervals.map((value) => {
|
|
// For negative exponents we want to divide by 10^-exponent to avoid
|
|
// floating point arithmetic bugs. For instance, in javascript
|
|
// 6 * 10^-1 == 0.6000000000000001, we instead want 6 / 10^1 == 0.6
|
|
if (exponent < 0) {
|
|
return value / Math.pow(10, -exponent);
|
|
}
|
|
return value * Math.pow(10, exponent);
|
|
});
|
|
return intervals;
|
|
}
|
|
|
|
export function calcChartIntervals(values, withMinimum = false) {
|
|
//*** Where the magic happens ***
|
|
|
|
// Calculates best-fit y intervals from given values
|
|
// and returns the interval array
|
|
|
|
let maxValue = Math.max(...values);
|
|
let minValue = Math.min(...values);
|
|
|
|
// Exponent to be used for pretty print
|
|
let exponent = 0,
|
|
intervals = []; // eslint-disable-line no-unused-vars
|
|
|
|
function getPositiveFirstIntervals(maxValue, absMinValue) {
|
|
let intervals = getChartIntervals(maxValue);
|
|
|
|
let intervalSize = intervals[1] - intervals[0];
|
|
|
|
// Then unshift the negative values
|
|
let value = 0;
|
|
for (var i = 1; value < absMinValue; i++) {
|
|
value += intervalSize;
|
|
intervals.unshift(-1 * value);
|
|
}
|
|
return intervals;
|
|
}
|
|
|
|
// CASE I: Both non-negative
|
|
|
|
if (maxValue >= 0 && minValue >= 0) {
|
|
exponent = normalize(maxValue)[1];
|
|
if (!withMinimum) {
|
|
intervals = getChartIntervals(maxValue);
|
|
} else {
|
|
intervals = getChartIntervals(maxValue, minValue);
|
|
}
|
|
}
|
|
|
|
// CASE II: Only minValue negative
|
|
else if (maxValue > 0 && minValue < 0) {
|
|
// `withMinimum` irrelevant in this case,
|
|
// We'll be handling both sides of zero separately
|
|
// (both starting from zero)
|
|
// Because ceil() and floor() behave differently
|
|
// in those two regions
|
|
|
|
let absMinValue = Math.abs(minValue);
|
|
|
|
if (maxValue >= absMinValue) {
|
|
exponent = normalize(maxValue)[1];
|
|
intervals = getPositiveFirstIntervals(maxValue, absMinValue);
|
|
} else {
|
|
// Mirror: maxValue => absMinValue, then change sign
|
|
exponent = normalize(absMinValue)[1];
|
|
let posIntervals = getPositiveFirstIntervals(absMinValue, maxValue);
|
|
intervals = posIntervals.reverse().map((d) => d * -1);
|
|
}
|
|
}
|
|
|
|
// CASE III: Both non-positive
|
|
else if (maxValue <= 0 && minValue <= 0) {
|
|
// Mirrored Case I:
|
|
// Work with positives, then reverse the sign and array
|
|
|
|
let pseudoMaxValue = Math.abs(minValue);
|
|
let pseudoMinValue = Math.abs(maxValue);
|
|
|
|
exponent = normalize(pseudoMaxValue)[1];
|
|
if (!withMinimum) {
|
|
intervals = getChartIntervals(pseudoMaxValue);
|
|
} else {
|
|
intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue);
|
|
}
|
|
|
|
intervals = intervals.reverse().map((d) => d * -1);
|
|
}
|
|
|
|
return intervals.sort((a, b) => a - b);
|
|
}
|
|
|
|
export function getZeroIndex(yPts) {
|
|
let zeroIndex;
|
|
let interval = getIntervalSize(yPts);
|
|
if (yPts.indexOf(0) >= 0) {
|
|
// the range has a given zero
|
|
// zero-line on the chart
|
|
zeroIndex = yPts.indexOf(0);
|
|
} else if (yPts[0] > 0) {
|
|
// Minimum value is positive
|
|
// zero-line is off the chart: below
|
|
let min = yPts[0];
|
|
zeroIndex = (-1 * min) / interval;
|
|
} else {
|
|
// Maximum value is negative
|
|
// zero-line is off the chart: above
|
|
let max = yPts[yPts.length - 1];
|
|
zeroIndex = (-1 * max) / interval + (yPts.length - 1);
|
|
}
|
|
return zeroIndex;
|
|
}
|
|
|
|
export function getRealIntervals(max, noOfIntervals, min = 0, asc = 1) {
|
|
let range = max - min;
|
|
let part = (range * 1.0) / noOfIntervals;
|
|
let intervals = [];
|
|
|
|
for (var i = 0; i <= noOfIntervals; i++) {
|
|
intervals.push(min + part * i);
|
|
}
|
|
|
|
return asc ? intervals : intervals.reverse();
|
|
}
|
|
|
|
export function getIntervalSize(orderedArray) {
|
|
return orderedArray[1] - orderedArray[0];
|
|
}
|
|
|
|
export function getValueRange(orderedArray) {
|
|
return orderedArray[orderedArray.length - 1] - orderedArray[0];
|
|
}
|
|
|
|
export function scale(val, yAxis) {
|
|
return floatTwo(yAxis.zeroLine - val * yAxis.scaleMultiplier);
|
|
}
|
|
|
|
export function isInRange(val, min, max) {
|
|
return val > min && val < max;
|
|
}
|
|
|
|
export function isInRange2D(coord, minCoord, maxCoord) {
|
|
return (
|
|
isInRange(coord[0], minCoord[0], maxCoord[0]) &&
|
|
isInRange(coord[1], minCoord[1], maxCoord[1])
|
|
);
|
|
}
|
|
|
|
export function getClosestInArray(goal, arr, index = false) {
|
|
let closest = arr.reduce(function (prev, curr) {
|
|
return Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev;
|
|
}, []);
|
|
|
|
return index ? arr.indexOf(closest) : closest;
|
|
}
|
|
|
|
export function calcDistribution(values, distributionSize) {
|
|
// Assume non-negative values,
|
|
// implying distribution minimum at zero
|
|
|
|
let dataMaxValue = Math.max(...values);
|
|
|
|
let distributionStep = 1 / (distributionSize - 1);
|
|
let distribution = [];
|
|
|
|
for (var i = 0; i < distributionSize; i++) {
|
|
let checkpoint = dataMaxValue * (distributionStep * i);
|
|
distribution.push(checkpoint);
|
|
}
|
|
|
|
return distribution;
|
|
}
|
|
|
|
export function getMaxCheckpoint(value, distribution) {
|
|
return distribution.filter((d) => d < value).length;
|
|
}
|