Public
Edited
Dec 1, 2022
1 fork
Calculating binSizes [Max count]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function histogram(
tableColumn,
bucketWidth,
bucketCount,
fixedTicks,
maxBucketCount
) {
// console.log('-------------')
// ---- Commented for testing --- //
// absMin, absMax] = getDataRange(tableColumn);
// q1 = getQuantile(tableColumn, 0.25);
// q3 = getQuantile(tableColumn, 0.75);
// iqr = q3-q1; // assuming q1 is smaller than q3
// // regular data range (ie. excluding outliers) goes from regularMin to regularMax
// // bounded by absMin and absMax
// // extend by fixedTicks if needed
// const smallestFixedTick = Math.min(...fixedTicks);
// const biggestFixedTick = Math.max(...fixedTicks);
// regularMin = Math.min(smallestFixedTick, Math.max(absMin, q1 - 1.5 * iqr));
// regularMax = Math.max(biggestFixedTick, Math.min(absMax, q3 + 1.5 * iqr));


// Mocked data values for testing
const [absMin, absMax] = [form.absMin, form.absMax];
let regularMin, regularMax
// regular data range (ie. excluding outliers) goes from regularMin to regularMax
// bounded by absMin and absMax
// extend by fixedTicks if needed
regularMin = Math.max(absMin, form.regularMin);
regularMax = Math.min(absMax, form.regularMax);
let regMinWithRequired = regularMin;
let regMaxWithRequired = regularMax;
if(fixedTicks) {
const smallestFixedTick = Math.min(...fixedTicks);
const biggestFixedTick = Math.max(...fixedTicks);
regMinWithRequired = Math.min(smallestFixedTick, Math.max(absMin, form.regularMin));
regMaxWithRequired = Math.max(biggestFixedTick, Math.min(absMax, form.regularMax));
}
const distance = regMaxWithRequired - regMinWithRequired;
const MAX_PRECISION = 5

if (distance === 0) {
let power = getPower(regularMin, MAX_PRECISION);

//step
const intValue = Math.floor(regularMin * Math.pow(10, power));
const lowerBound = intValue / Math.pow(10, power);
const upperBound = (intValue + 1) / Math.pow(10, power);
const boundaries = [absMin, lowerBound, upperBound,absMax];
const step = +Math.pow(10, -power).toFixed(countDecimals(upperBound));

return boundaries;
}

// figure out suggestedStep based on bucketWidth or bucketCount
let suggestedStep;
if (bucketWidth) {
suggestedStep = bucketWidth;
} else {
// derive suggestedStep from given bucketCount or use Sturges' formula
const count = Math.ceil(Math.log2(2)) + 1;
suggestedStep = distance / count;
}

let step;
let ticks = [];
let power;
let firstTick;
let indexDiff;

// calculate suggested step
step = tickIncrement(suggestedStep, fixedTicks);

//calculate min nice step based on maxCount
const minStepAllowed = minTickIncrement(distance / (maxBucketCount - 2)) // - 2 to make space for outliers

// is suggested step is smaller than allowd we use min nice step
if (step < minStepAllowed) {
step = minStepAllowed
}

if (step <= Math.pow(10, -MAX_PRECISION)) {
step = +Math.pow(10, -MAX_PRECISION).toFixed(MAX_PRECISION);
}

const precision = getPrecision(step, fixedTicks, MAX_PRECISION);
if (fixedTicks) {
firstTick = fixedTicks[0];
} else {
// if no required ticks, center around the 0
firstTick = 0;
}

// calculate first tick
indexDiff = Math.ceil((firstTick - regMinWithRequired) / step);
firstTick = firstTick - (indexDiff * step)

//fill tick array
let index = 0;
let currentTick = firstTick;
while (currentTick <= regularMax || currentTick < regMaxWithRequired) {
currentTick = (firstTick + index * step);
ticks.push(+currentTick.toFixed(precision))
index++
}

const newAbsMin = Math.min(absMin, Math.min(...ticks));
const newAbsMax = Math.max(absMax, Math.max(...ticks));
const bucketBoundaries = [newAbsMin, ...ticks, newAbsMax];

return { boundaries: bucketBoundaries, step: step };
}
Insert cell
function getPrecision(step, fixedTicks, maxPrecision) {
let highestPrecision = countDecimals(step)

if(fixedTicks && fixedTicks.length > 0) {
const allValues = [step, ...fixedTicks];
const allPrecisions = allValues.map(value => countDecimals(value))
highestPrecision = Math.max(...allPrecisions)
}

return highestPrecision > maxPrecision ? maxPrecision : highestPrecision;
}
Insert cell
function countDecimals(number){
const maxDigits = 20;
if (Math.floor(number.valueOf()) === number.valueOf()) {
return 0;
}

//make sure that numbers with scientific notation are turned into decimals
let newNumber = number.toLocaleString("en-US", { useGrouping: false, maximumFractionDigits: maxDigits });
return newNumber.split(".")[1].length || 0;
}
Insert cell
/**
* Rounds the given step to the next smaller power of ten multiplied by 1, 2 or 5.
* @returns step: the new calculated step
*
* @example - integer suggested step
* tickIncrement(90) = 100
* tickIncrement(80) = 100
* tickIncrement(70) = 50
* tickIncrement(60) = 50
* tickIncrement(50) = 50
* tickIncrementp(40) = 40
* tickIncrement(30) = 20
* tickIncrement(20) = 20
* tickIncrement(10) = 10
* tickIncrement(9) = 10
* @example - float suggested step
* tickIncrement(0.9) = 1
* tickIncrement(0.8) = 1
* tickIncrement(0.7) = 0.5
* tickIncrement(0.6) = 0.5
* tickIncrement(0.5) = 0.5
* tickIncrement(0.4) = 0.5
* tickIncrement(0.3) = 0.2
* tickIncrement(0.2) = 0.2
* tickIncrement(0.1) = 0.1
* tickIncrement(0.01) = 0.01
*
* @example - with required ticks
* tickIncrement(0.5, [3, 1.5]) = 0.5
* tickIncrement(0.2, [3, 1.5]) = 0.3
* tickIncrement(1.2, [3, 1.5]) = 1.5
*
* @param step has to be positive number
* @param requiredTicks are significant values (eg. thresholds) that should be marked as a tick
*/
function tickIncrement(step, requiredTicks) {
let newStep;

if (requiredTicks && requiredTicks.length > 0) {
const possibleBinSizes = getPossibleBinSizes(requiredTicks);
return getClosestToTarget(possibleBinSizes, step);
}

// nextSmallestPowerOf10 == 10^power <= step
const power = Math.floor(Math.log(step) / Math.LN10);

// error thresholds for finding the right tick steps
const e10 = Math.sqrt(50); // 7.07
const e5 = Math.sqrt(10); // 3.16
const e2 = Math.sqrt(2); // 1.41
const error = step / Math.pow(10, power);

// step is >= 1
if (power >= 0) {
if (error >= e10) {
newStep = 10 * Math.pow(10, power);
} else if (error >= e5) {
newStep = 5 * Math.pow(10, power);
} else if (error >= e2) {
newStep = 2 * Math.pow(10, power);
} else {
newStep = 1 * Math.pow(10, power);
}
// step is between 0 and 1 (eg. step is 0.1, ie. power is -1, meaning: 0.1 = 10^-1 )
} else {
if (error >= e10) {
newStep = -Math.pow(10, -power) / 10;
} else if (error >= e5) {
newStep = -Math.pow(10, -power) / 5;
} else if (error >= e2) {
newStep = -Math.pow(10, -power) / 2;
} else {
newStep = -Math.pow(10, -power) / 1;
}

//get actual step
newStep = 1 / -newStep;
}

return newStep
}
Insert cell
/**
* Rounds UP the given step to the next smaller power of ten multiplied by 1, 2 or 5.
* @returns step: the new calculated step
*
* @example - integer suggested step
* minTickIncrement(100) = 100
* minTickIncrement(90) = 50
* minTickIncrement(80) = 50
* minTickIncrement(70) = 50
* minTickIncrement(60) = 50
* minTickIncrement(50) = 50
* minTickIncrement(40) = 20
* minTickIncrement(30) = 20
* minTickIncrement(20) = 20
* minTickIncrement(10) = 10
*
* @param step has to be positive number
*/
function minTickIncrement(step) {
let minNiceStep;

// nextSmallestPowerOf10 == 10^power <= step
const power = Math.floor(Math.log(step) / Math.LN10);

// error thresholds for finding the right tick steps
const e5 = 5
const e2 = 2
const e1 = 1
const error = step / Math.pow(10, power);

// step is >= 1
if (power >= 0) {
if (error <= e1) {
minNiceStep = 1 * Math.pow(10, power);
} else if (error <= e2) {
minNiceStep = 2 * Math.pow(10, power);
} else if (error <= e5) {
minNiceStep = 5 * Math.pow(10, power);
} else {
minNiceStep = 10 * Math.pow(10, power);
}
} else {

if (error <= e1) {
minNiceStep = -Math.pow(10, -power) / 1;
} else if (error <= e2) {
minNiceStep = -Math.pow(10, -power) / 2;
} else if (error <= e5) {
minNiceStep = -Math.pow(10, -power) / 5;
} else {
minNiceStep = -Math.pow(10, -power) / 10;
}
//get actual step
minNiceStep = 1 / -minNiceStep;
}

return minNiceStep
}
Insert cell
/**
* Calculates the number in an array closer to a target value
*
* @param array array of numbers to iterate
* @param targetValue - target value
* @returns the closest number to the target value from the array
*/
function getClosestToTarget(array, targetValue) {
let result
let lastDelta;

array.some(function (item) {
let delta = Math.abs(targetValue - item);
if (delta >= lastDelta) return true;
result = item;
lastDelta = delta;
});
return result;
}
Insert cell
getPossibleBinSizes([0.0000001, 0.00009])
Insert cell
/**
* Calculates all the common factors of a list of numbers
*
* @param values list of numbers to iterate
* @returns the intersection of all the factors of the numbers on the list
*
*@example getPossibleBinSizes(12, 30) => [1, 2, 3, 6]
*/
function getPossibleBinSizes(values) {
const MAX_PRECISION = 5;
let uniqueValues = [...new Set(values)].sort((a, b) => a - b);
let integerFactors = [];
let power = 0;

// Multiply by ten until all values are integers, keep track of the number of multiplications
while (uniqueValues.some((d) => !Number.isInteger(d))) {
if(power >= MAX_PRECISION) {
return [+(Math.pow(10, -MAX_PRECISION)).toFixed(MAX_PRECISION)]
}
power++;
uniqueValues = uniqueValues.map((d) => d * Math.pow(10, 1));
}

if(uniqueValues.length === 1) {
integerFactors.push(factors(uniqueValues[0]))
} else {
for (let i = 1; i < uniqueValues.length; i++) {
let leftValue = uniqueValues[i-1];
let rightValue = uniqueValues[i];
integerFactors.push(factors(rightValue - leftValue));
}
}



const intersectionValues = intersection(integerFactors);

//divide all factors so we get to the initial state
return intersectionValues.map((d) => d / Math.pow(10, power));
}
Insert cell
factorsOf = factors(11)
Insert cell
/**
* Returns all the factors or divisors for a given number
*
* @param number number to analyze E.g 6
* @returns a list of all the factors for the given number E.g [1, 2, 3, 6]
*/
function factors(number) {

if(number == 0) {
number = 1;
}

let result = [];
for (let i = 1; i <= Math.abs(number); i++) {
if (Math.abs(number) % i === 0) {
result.push(i);
}
}
result.sort((a, b) => a - b);
return result;
}

Insert cell
inter = intersection([
[1, 2, 4, 6],
[1, 2, 4, 8, 12],
])
Insert cell
/** DONE DONE DONE
* Returns all the common values in a series of lists
*
* @param lists a list of lists of numbers. E.g [[1, 2, 4, 6], [1, 2, 4, 8, 12]]
* @returns the intersection of the lists. E.g [1, 2, 4]
*/
function intersection(lists) {
let result = [];

if (!lists.length > 0) {
return result;
}

for (let currentList of lists) {
for (let currentValue of currentList) {
if (result.indexOf(currentValue) === -1) {
if (lists.filter((obj) => obj.indexOf(currentValue) === -1).length === 0) {
result.push(currentValue);
}
}
}
}
result.sort((a, b) => a - b);
return result;
}

Insert cell
//getPowerOf = getPower(1.00000000000006, 10)
Insert cell
function getPower(value, maxPower) {
let power = 0;
let decimals = countDecimals(value)
if (Number.isInteger(value)) {
return power;
}

while (!Number.isInteger(value)) {
power++;
decimals--
if (power === maxPower) {
return power;
}

value = +(value*10).toFixed(decimals);
}
return power
}
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more