charts/src/js/utils/intervals.js
2020-08-30 21:12:53 +02:00

240 lines
5.9 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 => 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;
}
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;
}