charts/src/js/utils/intervals.js
2022-11-24 12:54:27 +05:30

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;
}