import type { NumberArray } from 'cheminfo-types'; import { xCheck } from './xCheck.ts'; export interface XBoxPlotOptions { /** * By default, there should be at least 5 elements. * @default false */ allowSmallArray?: boolean; } export interface XBoxPlot { min: number; q1: number; median: number; q3: number; max: number; } /** * Calculating the box plot of the array * This function will interpolate the values and use the inclusive algorithm * @param array - data * @param options * @returns - q1, median, q3, min, max */ export function xBoxPlot(array: NumberArray): XBoxPlot { xCheck(array); // duplicate the array to avoid modifying the original one // and sort typed array that is much faster than sorting a normal array array = Float64Array.from(array); array.sort(); // need to deal with very close points otherwise it yields to incorrect results if ((array.at(-1) as number) - array[0] <= Number.EPSILON) { // if one of the 2 numbers is an integer let's take this one const shortTestNumber = String(array[0]).length < String(array.at(-1)).length ? array[0] : (array.at(-1) as number); return { min: array[0], q1: shortTestNumber, median: shortTestNumber, q3: shortTestNumber, max: array.at(-1) as number, }; } const posQ1 = (array.length - 1) / 4; const posQ3 = (array.length - 1) * (3 / 4); const medianPos = (array.length - 1) / 2; const q1MinProportion = posQ1 % 1; const q3MinProportion = posQ3 % 1; const medianMinProportion = medianPos % 1; return { min: array[0], q1: q1MinProportion === 0 ? array[posQ1] : array[posQ1 >> 0] * (1 - q1MinProportion) + array[(posQ1 >> 0) + 1] * q1MinProportion, median: medianMinProportion === 0 ? array[medianPos] : array[medianPos >> 0] * (1 - medianMinProportion) + array[(medianPos >> 0) + 1] * medianMinProportion, q3: q3MinProportion === 0 ? array[posQ3] : array[posQ3 >> 0] * (1 - q3MinProportion) + array[(posQ3 >> 0) + 1] * q3MinProportion, max: array.at(-1) as number, }; }