@sgratzl/boxplots
Advanced tools
Comparing version 1.2.2 to 1.3.0
@@ -8,2 +8,9 @@ /** | ||
declare type KernelDensityEstimator = (v: number) => number; | ||
declare function kde(stats: { | ||
items: ArrayLike<number>; | ||
iqr: number; | ||
variance: number; | ||
}): KernelDensityEstimator; | ||
interface IBoxPlot { | ||
@@ -53,2 +60,6 @@ /** | ||
/** | ||
* variance | ||
*/ | ||
readonly variance: number; | ||
/** | ||
* number of missing values (NaN, null, undefined) in the data | ||
@@ -65,2 +76,3 @@ */ | ||
readonly items: ArrayLike<number>; | ||
readonly kde: KernelDensityEstimator; | ||
} | ||
@@ -106,2 +118,7 @@ declare interface QuantileMethod { | ||
interface QuantilesResult { | ||
q1: number; | ||
median: number; | ||
q3: number; | ||
} | ||
/** | ||
@@ -112,7 +129,3 @@ * computes the boxplot stats using the given interpolation function if needed | ||
*/ | ||
declare function quantilesInterpolate(arr: ArrayLike<number>, length: number, interpolate: (i: number, j: number, fraction: number) => number): { | ||
q1: number; | ||
median: number; | ||
q3: number; | ||
}; | ||
declare function quantilesInterpolate(arr: ArrayLike<number>, length: number, interpolate: (i: number, j: number, fraction: number) => number): QuantilesResult; | ||
/** | ||
@@ -122,7 +135,3 @@ * Uses R's quantile algorithm type=7. | ||
*/ | ||
declare function quantilesType7(arr: ArrayLike<number>, length?: number): { | ||
q1: number; | ||
median: number; | ||
q3: number; | ||
}; | ||
declare function quantilesType7(arr: ArrayLike<number>, length?: number): QuantilesResult; | ||
/** | ||
@@ -132,39 +141,19 @@ * ‘linear’: i + (j - i) * fraction, where fraction is the fractional part of the index surrounded by i and j. | ||
*/ | ||
declare function quantilesLinear(arr: ArrayLike<number>, length?: number): { | ||
q1: number; | ||
median: number; | ||
q3: number; | ||
}; | ||
declare function quantilesLinear(arr: ArrayLike<number>, length?: number): QuantilesResult; | ||
/** | ||
* ‘lower’: i. | ||
*/ | ||
declare function quantilesLower(arr: ArrayLike<number>, length?: number): { | ||
q1: number; | ||
median: number; | ||
q3: number; | ||
}; | ||
declare function quantilesLower(arr: ArrayLike<number>, length?: number): QuantilesResult; | ||
/** | ||
* 'higher': j. | ||
*/ | ||
declare function quantilesHigher(arr: ArrayLike<number>, length?: number): { | ||
q1: number; | ||
median: number; | ||
q3: number; | ||
}; | ||
declare function quantilesHigher(arr: ArrayLike<number>, length?: number): QuantilesResult; | ||
/** | ||
* ‘nearest’: i or j, whichever is nearest | ||
*/ | ||
declare function quantilesNearest(arr: ArrayLike<number>, length?: number): { | ||
q1: number; | ||
median: number; | ||
q3: number; | ||
}; | ||
declare function quantilesNearest(arr: ArrayLike<number>, length?: number): QuantilesResult; | ||
/** | ||
* ‘midpoint’: (i + j) / 2 | ||
*/ | ||
declare function quantilesMidpoint(arr: ArrayLike<number>, length?: number): { | ||
q1: number; | ||
median: number; | ||
q3: number; | ||
}; | ||
declare function quantilesMidpoint(arr: ArrayLike<number>, length?: number): QuantilesResult; | ||
/** | ||
@@ -177,7 +166,3 @@ * The hinges equal the quartiles for odd n (where n <- length(x)) | ||
*/ | ||
declare function quantilesFivenum(arr: ArrayLike<number>, length?: number): { | ||
q1: number; | ||
median: number; | ||
q3: number; | ||
}; | ||
declare function quantilesFivenum(arr: ArrayLike<number>, length?: number): QuantilesResult; | ||
/** | ||
@@ -188,10 +173,5 @@ * alias for quantilesFivenum | ||
*/ | ||
declare function quantilesHinges(arr: ArrayLike<number>, length?: number): { | ||
q1: number; | ||
median: number; | ||
q3: number; | ||
}; | ||
declare function quantilesHinges(arr: ArrayLike<number>, length?: number): QuantilesResult; | ||
export default boxplot; | ||
export { BoxplotStatsOptions, IBoxPlot, QuantileMethod, boxplot, quantilesFivenum, quantilesHigher, quantilesHinges, quantilesInterpolate, quantilesLinear, quantilesLower, quantilesMidpoint, quantilesNearest, quantilesType7 }; | ||
export { BoxplotStatsOptions, IBoxPlot, KernelDensityEstimator, QuantileMethod, QuantilesResult, boxplot, boxplot as default, kde, quantilesFivenum, quantilesHigher, quantilesHinges, quantilesInterpolate, quantilesLinear, quantilesLower, quantilesMidpoint, quantilesNearest, quantilesType7 }; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -8,6 +8,30 @@ /** | ||
'use strict'; | ||
const HELPER = Math.sqrt(2 * Math.PI); | ||
function gaussian(u) { | ||
return Math.exp(-0.5 * u * u) / HELPER; | ||
} | ||
function toSampleVariance(variance, len) { | ||
return (variance * len) / (len - 1); | ||
} | ||
function nrd(iqr, variance, len) { | ||
let s = Math.sqrt(toSampleVariance(variance, len)); | ||
if (typeof iqr === 'number') { | ||
s = Math.min(s, iqr / 1.34); | ||
} | ||
return 1.06 * s * Math.pow(len, -0.2); | ||
} | ||
function kde(stats) { | ||
const len = stats.items.length; | ||
const bandwidth = nrd(stats.iqr, stats.variance, len); | ||
return (x) => { | ||
let i = 0; | ||
let sum = 0; | ||
for (i = 0; i < len; i++) { | ||
const v = stats.items[i]; | ||
sum += gaussian((x - v) / bandwidth); | ||
} | ||
return sum / bandwidth / len; | ||
}; | ||
} | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
function quantilesInterpolate(arr, length, interpolate) { | ||
@@ -61,9 +85,6 @@ const n1 = length - 1; | ||
function createSortedData(data) { | ||
let min = Number.POSITIVE_INFINITY; | ||
let max = Number.NEGATIVE_INFINITY; | ||
let sum = 0; | ||
let valid = 0; | ||
const length = data.length; | ||
const { length } = data; | ||
const vs = data instanceof Float64Array ? new Float64Array(length) : new Float32Array(length); | ||
for (let i = 0; i < length; ++i) { | ||
for (let i = 0; i < length; i += 1) { | ||
const v = data[i]; | ||
@@ -74,10 +95,3 @@ if (v == null || Number.isNaN(v)) { | ||
vs[valid] = v; | ||
valid++; | ||
if (v < min) { | ||
min = v; | ||
} | ||
if (v > max) { | ||
max = v; | ||
} | ||
sum += v; | ||
valid += 1; | ||
} | ||
@@ -87,5 +101,4 @@ const missing = length - valid; | ||
return { | ||
sum, | ||
min, | ||
max, | ||
min: Number.NaN, | ||
max: Number.NaN, | ||
missing, | ||
@@ -95,10 +108,11 @@ s: [], | ||
} | ||
const s = valid === length ? vs : vs.subarray(0, valid); | ||
s.sort((a, b) => (a === b ? 0 : a < b ? -1 : 1)); | ||
const validData = valid === length ? vs : vs.subarray(0, valid); | ||
validData.sort((a, b) => (a === b ? 0 : a < b ? -1 : 1)); | ||
const min = validData[0]; | ||
const max = validData[validData.length - 1]; | ||
return { | ||
sum, | ||
min: s[0], | ||
max: s[s.length - 1], | ||
min, | ||
max, | ||
missing, | ||
s, | ||
s: validData, | ||
}; | ||
@@ -109,3 +123,2 @@ } | ||
return { | ||
sum: Number.NaN, | ||
min: Number.NaN, | ||
@@ -119,10 +132,3 @@ max: Number.NaN, | ||
const max = data[data.length - 1]; | ||
const red = (acc, v) => acc + v; | ||
const sum = data instanceof Float32Array | ||
? data.reduce(red, 0) | ||
: data instanceof Float64Array | ||
? data.reduce(red, 0) | ||
: data.reduce(red, 0); | ||
return { | ||
sum, | ||
min, | ||
@@ -134,4 +140,67 @@ max, | ||
} | ||
function computeWhiskers(s, valid, min, max, { eps, quantiles, coef, whiskersMode }) { | ||
const same = (a, b) => Math.abs(a - b) < eps; | ||
const { median, q1, q3 } = quantiles(s, valid); | ||
const iqr = q3 - q1; | ||
const isCoefValid = typeof coef === 'number' && coef > 0; | ||
let whiskerLow = isCoefValid ? Math.max(min, q1 - coef * iqr) : min; | ||
let whiskerHigh = isCoefValid ? Math.min(max, q3 + coef * iqr) : max; | ||
const outlierLow = []; | ||
for (let i = 0; i < valid; i += 1) { | ||
const v = s[i]; | ||
if (v >= whiskerLow || same(v, whiskerLow)) { | ||
if (whiskersMode === 'nearest') { | ||
whiskerLow = v; | ||
} | ||
break; | ||
} | ||
if (outlierLow.length === 0 || !same(outlierLow[outlierLow.length - 1], v)) { | ||
outlierLow.push(v); | ||
} | ||
} | ||
const reversedOutlierHigh = []; | ||
for (let i = valid - 1; i >= 0; i -= 1) { | ||
const v = s[i]; | ||
if (v <= whiskerHigh || same(v, whiskerHigh)) { | ||
if (whiskersMode === 'nearest') { | ||
whiskerHigh = v; | ||
} | ||
break; | ||
} | ||
if ((reversedOutlierHigh.length === 0 || !same(reversedOutlierHigh[reversedOutlierHigh.length - 1], v)) && | ||
(outlierLow.length === 0 || !same(outlierLow[outlierLow.length - 1], v))) { | ||
reversedOutlierHigh.push(v); | ||
} | ||
} | ||
const outlier = outlierLow.concat(reversedOutlierHigh.reverse()); | ||
return { | ||
median, | ||
q1, | ||
q3, | ||
iqr, | ||
outlier, | ||
whiskerHigh, | ||
whiskerLow, | ||
}; | ||
} | ||
function computeStats(s, valid) { | ||
let mean = 0; | ||
for (let i = 0; i < valid; i++) { | ||
const v = s[i]; | ||
mean += v; | ||
} | ||
mean /= valid; | ||
let variance = 0; | ||
for (let i = 0; i < valid; i++) { | ||
const v = s[i]; | ||
variance += (v - mean) * (v - mean); | ||
} | ||
variance /= valid; | ||
return { | ||
mean, | ||
variance, | ||
}; | ||
} | ||
function boxplot(data, options = {}) { | ||
const { quantiles, validAndSorted, coef, whiskersMode, eps } = Object.assign({ | ||
const fullOptions = { | ||
coef: 1.5, | ||
@@ -142,4 +211,5 @@ eps: 10e-3, | ||
whiskersMode: 'nearest', | ||
}, options); | ||
const { missing, s, min, max, sum } = validAndSorted ? withSortedData(data) : createSortedData(data); | ||
...options, | ||
}; | ||
const { missing, s, min, max } = fullOptions.validAndSorted ? withSortedData(data) : createSortedData(data); | ||
const invalid = { | ||
@@ -158,5 +228,6 @@ min: Number.NaN, | ||
q3: Number.NaN, | ||
variance: 0, | ||
items: [], | ||
kde: () => 0, | ||
}; | ||
const same = (a, b) => Math.abs(a - b) < eps; | ||
const valid = data.length - missing; | ||
@@ -166,35 +237,3 @@ if (valid === 0) { | ||
} | ||
const { median, q1, q3 } = quantiles(s, valid); | ||
const iqr = q3 - q1; | ||
const isCoefValid = typeof coef === 'number' && coef > 0; | ||
let whiskerLow = isCoefValid ? Math.max(min, q1 - coef * iqr) : min; | ||
let whiskerHigh = isCoefValid ? Math.min(max, q3 + coef * iqr) : max; | ||
const outlier = []; | ||
for (let i = 0; i < valid; ++i) { | ||
const v = s[i]; | ||
if (v >= whiskerLow || same(v, whiskerLow)) { | ||
if (whiskersMode === 'nearest') { | ||
whiskerLow = v; | ||
} | ||
break; | ||
} | ||
if (outlier.length === 0 || !same(outlier[outlier.length - 1], v)) { | ||
outlier.push(v); | ||
} | ||
} | ||
const reversedOutliers = []; | ||
for (let i = valid - 1; i >= 0; --i) { | ||
const v = s[i]; | ||
if (v <= whiskerHigh || same(v, whiskerHigh)) { | ||
if (whiskersMode === 'nearest') { | ||
whiskerHigh = v; | ||
} | ||
break; | ||
} | ||
if ((reversedOutliers.length === 0 || !same(reversedOutliers[reversedOutliers.length - 1], v)) && | ||
(outlier.length === 0 || !same(outlier[outlier.length - 1], v))) { | ||
reversedOutliers.push(v); | ||
} | ||
} | ||
return { | ||
const result = { | ||
min, | ||
@@ -204,25 +243,13 @@ max, | ||
missing, | ||
mean: sum / valid, | ||
whiskerHigh, | ||
whiskerLow, | ||
outlier: outlier.concat(reversedOutliers.reverse()), | ||
median, | ||
q1, | ||
q3, | ||
iqr, | ||
items: s, | ||
...computeStats(s, valid), | ||
...computeWhiskers(s, valid, min, max, fullOptions), | ||
}; | ||
return { | ||
...result, | ||
kde: kde(result), | ||
}; | ||
} | ||
exports.boxplot = boxplot; | ||
exports.default = boxplot; | ||
exports.quantilesFivenum = quantilesFivenum; | ||
exports.quantilesHigher = quantilesHigher; | ||
exports.quantilesHinges = quantilesHinges; | ||
exports.quantilesInterpolate = quantilesInterpolate; | ||
exports.quantilesLinear = quantilesLinear; | ||
exports.quantilesLower = quantilesLower; | ||
exports.quantilesMidpoint = quantilesMidpoint; | ||
exports.quantilesNearest = quantilesNearest; | ||
exports.quantilesType7 = quantilesType7; | ||
export { boxplot, boxplot as default, kde, quantilesFivenum, quantilesHigher, quantilesHinges, quantilesInterpolate, quantilesLinear, quantilesLower, quantilesMidpoint, quantilesNearest, quantilesType7 }; | ||
//# sourceMappingURL=index.js.map |
@@ -1,2 +0,2 @@ | ||
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).BoxPlots={})}(this,(function(e){"use strict";function n(e,n,t){const r=n-1,i=n=>{const i=n*r,s=Math.floor(i),o=i-s,u=e[s];return 0===o?u:t(u,e[Math.min(s+1,r)],o)};return{q1:i(.25),median:i(.5),q3:i(.75)}}function t(e,t=e.length){return n(e,t,(e,n,t)=>e+t*(n-e))}function r(e,n=e.length){const t=n,r=Math.floor((t+3)/2)/2,i=n=>.5*(e[Math.floor(n)-1]+e[Math.ceil(n)-1]);return{q1:i(r),median:i((t+1)/2),q3:i(t+1-r)}}function i(e,n={}){const{quantiles:r,validAndSorted:i,coef:s,whiskersMode:o,eps:u}=Object.assign({coef:1.5,eps:.01,quantiles:t,validAndSorted:!1,whiskersMode:"nearest"},n),{missing:a,s:l,min:m,max:N,sum:c}=i?function(e){if(0===e.length)return{sum:Number.NaN,min:Number.NaN,max:Number.NaN,missing:0,s:[]};const n=e[0],t=e[e.length-1],r=(e,n)=>e+n;return{sum:(e instanceof Float32Array||Float64Array,e.reduce(r,0)),min:n,max:t,missing:0,s:e}}(e):function(e){let n=Number.POSITIVE_INFINITY,t=Number.NEGATIVE_INFINITY,r=0,i=0;const s=e.length,o=e instanceof Float64Array?new Float64Array(s):new Float32Array(s);for(let u=0;u<s;++u){const s=e[u];null==s||Number.isNaN(s)||(o[i]=s,i++,s<n&&(n=s),s>t&&(t=s),r+=s)}const u=s-i;if(0===i)return{sum:r,min:n,max:t,missing:u,s:[]};const a=i===s?o:o.subarray(0,i);return a.sort((e,n)=>e===n?0:e<n?-1:1),{sum:r,min:a[0],max:a[a.length-1],missing:u,s:a}}(e),f={min:Number.NaN,max:Number.NaN,mean:Number.NaN,missing:a,iqr:Number.NaN,count:e.length,whiskerHigh:Number.NaN,whiskerLow:Number.NaN,outlier:[],median:Number.NaN,q1:Number.NaN,q3:Number.NaN,items:[]},h=(e,n)=>Math.abs(e-n)<u,g=e.length-a;if(0===g)return f;const{median:d,q1:b,q3:q}=r(l,g),p=q-b,x="number"==typeof s&&s>0;let y=x?Math.max(m,b-s*p):m,M=x?Math.min(N,q+s*p):N;const w=[];for(let e=0;e<g;++e){const n=l[e];if(n>=y||h(n,y)){"nearest"===o&&(y=n);break}0!==w.length&&h(w[w.length-1],n)||w.push(n)}const I=[];for(let e=g-1;e>=0;--e){const n=l[e];if(n<=M||h(n,M)){"nearest"===o&&(M=n);break}0!==I.length&&h(I[I.length-1],n)||0!==w.length&&h(w[w.length-1],n)||I.push(n)}return{min:m,max:N,count:e.length,missing:a,mean:c/g,whiskerHigh:M,whiskerLow:y,outlier:w.concat(I.reverse()),median:d,q1:b,q3:q,iqr:p,items:l}}e.boxplot=i,e.default=i,e.quantilesFivenum=r,e.quantilesHigher=function(e,t=e.length){return n(e,t,(e,n)=>n)},e.quantilesHinges=function(e,n=e.length){return r(e,n)},e.quantilesInterpolate=n,e.quantilesLinear=function(e,t=e.length){return n(e,t,(e,n,t)=>e+(n-e)*t)},e.quantilesLower=function(e,t=e.length){return n(e,t,e=>e)},e.quantilesMidpoint=function(e,t=e.length){return n(e,t,(e,n)=>.5*(e+n))},e.quantilesNearest=function(e,t=e.length){return n(e,t,(e,n,t)=>t<.5?e:n)},e.quantilesType7=t,Object.defineProperty(e,"__esModule",{value:!0})})); | ||
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).BoxPlots={})}(this,(function(e){"use strict";const n=Math.sqrt(2*Math.PI);function t(e){const t=e.items.length,r=function(e,n,t){let r=Math.sqrt(function(e,n){return e*n/(n-1)}(n,t));return"number"==typeof e&&(r=Math.min(r,e/1.34)),1.06*r*Math.pow(t,-.2)}(e.iqr,e.variance,t);return i=>{let o=0,s=0;for(o=0;o<t;o++){const t=e.items[o];s+=(a=(i-t)/r,Math.exp(-.5*a*a)/n)}var a;return s/r/t}}function r(e,n,t){const r=n-1,i=n=>{const i=n*r,o=Math.floor(i),s=i-o,a=e[o];return 0===s?a:t(a,e[Math.min(o+1,r)],s)};return{q1:i(.25),median:i(.5),q3:i(.75)}}function i(e,n=e.length){return r(e,n,((e,n,t)=>e+t*(n-e)))}function o(e,n=e.length){const t=n,r=Math.floor((t+3)/2)/2,i=n=>.5*(e[Math.floor(n)-1]+e[Math.ceil(n)-1]);return{q1:i(r),median:i((t+1)/2),q3:i(t+1-r)}}function s(e,n,t,r,{eps:i,quantiles:o,coef:s,whiskersMode:a}){const u=(e,n)=>Math.abs(e-n)<i,{median:l,q1:m,q3:c}=o(e,n),f=c-m,h="number"==typeof s&&s>0;let N=h?Math.max(t,m-s*f):t,g=h?Math.min(r,c+s*f):r;const d=[];for(let t=0;t<n;t+=1){const n=e[t];if(n>=N||u(n,N)){"nearest"===a&&(N=n);break}0!==d.length&&u(d[d.length-1],n)||d.push(n)}const q=[];for(let t=n-1;t>=0;t-=1){const n=e[t];if(n<=g||u(n,g)){"nearest"===a&&(g=n);break}0!==q.length&&u(q[q.length-1],n)||0!==d.length&&u(d[d.length-1],n)||q.push(n)}return{median:l,q1:m,q3:c,iqr:f,outlier:d.concat(q.reverse()),whiskerHigh:g,whiskerLow:N}}function a(e,n){let t=0;for(let r=0;r<n;r++){t+=e[r]}t/=n;let r=0;for(let i=0;i<n;i++){const n=e[i];r+=(n-t)*(n-t)}return r/=n,{mean:t,variance:r}}function u(e,n={}){const r={coef:1.5,eps:.01,quantiles:i,validAndSorted:!1,whiskersMode:"nearest",...n},{missing:o,s:u,min:l,max:m}=r.validAndSorted?function(e){return 0===e.length?{min:Number.NaN,max:Number.NaN,missing:0,s:[]}:{min:e[0],max:e[e.length-1],missing:0,s:e}}(e):function(e){let n=0;const{length:t}=e,r=e instanceof Float64Array?new Float64Array(t):new Float32Array(t);for(let i=0;i<t;i+=1){const t=e[i];null==t||Number.isNaN(t)||(r[n]=t,n+=1)}const i=t-n;if(0===n)return{min:Number.NaN,max:Number.NaN,missing:i,s:[]};const o=n===t?r:r.subarray(0,n);return o.sort(((e,n)=>e===n?0:e<n?-1:1)),{min:o[0],max:o[o.length-1],missing:i,s:o}}(e),c={min:Number.NaN,max:Number.NaN,mean:Number.NaN,missing:o,iqr:Number.NaN,count:e.length,whiskerHigh:Number.NaN,whiskerLow:Number.NaN,outlier:[],median:Number.NaN,q1:Number.NaN,q3:Number.NaN,variance:0,items:[],kde:()=>0},f=e.length-o;if(0===f)return c;const h={min:l,max:m,count:e.length,missing:o,items:u,...a(u,f),...s(u,f,l,m,r)};return{...h,kde:t(h)}}e.boxplot=u,e.default=u,e.kde=t,e.quantilesFivenum=o,e.quantilesHigher=function(e,n=e.length){return r(e,n,((e,n)=>n))},e.quantilesHinges=function(e,n=e.length){return o(e,n)},e.quantilesInterpolate=r,e.quantilesLinear=function(e,n=e.length){return r(e,n,((e,n,t)=>e+(n-e)*t))},e.quantilesLower=function(e,n=e.length){return r(e,n,(e=>e))},e.quantilesMidpoint=function(e,n=e.length){return r(e,n,((e,n)=>.5*(e+n)))},e.quantilesNearest=function(e,n=e.length){return r(e,n,((e,n,t)=>t<.5?e:n))},e.quantilesType7=i,Object.defineProperty(e,"__esModule",{value:!0})})); | ||
//# sourceMappingURL=index.umd.min.js.map |
{ | ||
"name": "@sgratzl/boxplots", | ||
"description": "simple boxplot stats helper", | ||
"version": "1.2.2", | ||
"version": "1.3.0", | ||
"publishConfig": { | ||
@@ -21,2 +21,3 @@ "access": "public" | ||
"whisker", | ||
"kde", | ||
"quantiles" | ||
@@ -29,4 +30,4 @@ ], | ||
"global": "BoxPlots", | ||
"module": "build/index.esm.js", | ||
"main": "build/index.js", | ||
"module": "build/index.js", | ||
"main": "build/index.cjs", | ||
"unpkg": "build/index.umd.min.js", | ||
@@ -41,36 +42,38 @@ "jsdelivr": "build/index.umd.min.js", | ||
"devDependencies": { | ||
"@rollup/plugin-commonjs": "^17.0.0", | ||
"@rollup/plugin-node-resolve": "^11.0.0", | ||
"@rollup/plugin-replace": "^2.3.4", | ||
"@rollup/plugin-typescript": "^8.0.0", | ||
"@types/jest": "^26.0.19", | ||
"@typescript-eslint/eslint-plugin": "^4.9.1", | ||
"@typescript-eslint/parser": "^4.9.1", | ||
"@yarnpkg/pnpify": "^2.4.0", | ||
"eslint": "^7.15.0", | ||
"eslint-config-prettier": "^7.0.0", | ||
"@babel/core": "^7.16.0", | ||
"@babel/preset-env": "^7.16.0", | ||
"@rollup/plugin-babel": "^5.3.0", | ||
"@rollup/plugin-commonjs": "^21.0.1", | ||
"@rollup/plugin-node-resolve": "^13.0.6", | ||
"@rollup/plugin-replace": "^3.0.0", | ||
"@rollup/plugin-typescript": "^8.3.0", | ||
"@types/jest": "^27.0.2", | ||
"@typescript-eslint/eslint-plugin": "^5.3.0", | ||
"@typescript-eslint/parser": "^5.3.0", | ||
"@yarnpkg/sdks": "^2.5.0", | ||
"eslint": "^8.2.0", | ||
"eslint-config-airbnb-typescript": "^15.0.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint-config-react-app": "^6.0.0", | ||
"eslint-plugin-flowtype": "^5.2.0", | ||
"eslint-plugin-import": "^2.22.1", | ||
"eslint-plugin-flowtype": "^8.0.3", | ||
"eslint-plugin-import": "^2.25.2", | ||
"eslint-plugin-jsx-a11y": "^6.4.1", | ||
"eslint-plugin-prettier": "^3.2.0", | ||
"eslint-plugin-react": "^7.21.5", | ||
"eslint-plugin-prettier": "^4.0.0", | ||
"eslint-plugin-react": "^7.26.1", | ||
"eslint-plugin-react-hooks": "^4.2.0", | ||
"jest": "^26.6.3", | ||
"prettier": "^2.2.1", | ||
"release-it": "^14.2.2", | ||
"jest": "^27.3.1", | ||
"prettier": "^2.4.1", | ||
"rimraf": "^3.0.2", | ||
"rollup": "^2.34.2", | ||
"rollup-plugin-cleanup": "^3.2.1", | ||
"rollup-plugin-dts": "^2.0.1", | ||
"rollup": "^2.59.0", | ||
"rollup-plugin-dts": "^4.0.1", | ||
"rollup-plugin-terser": "^7.0.2", | ||
"ts-jest": "^26.4.4", | ||
"tslib": "^2.0.3", | ||
"typedoc": "^0.19.2", | ||
"typescript": "^4.1.2" | ||
"ts-jest": "^27.0.7", | ||
"tslib": "^2.3.1", | ||
"typedoc": "^0.22.8", | ||
"typescript": "^4.4.4" | ||
}, | ||
"scripts": { | ||
"clean": "rimraf build node_modules \"*.tgz\"", | ||
"compile": "tsc -p tsconfig.json --noEmit", | ||
"start": "npm run watch", | ||
"clean": "rimraf build docs node_modules \"*.tgz\" \"*.tsbuildinfo\"", | ||
"compile": "tsc -b tsconfig.c.json", | ||
"start": "yarn run watch", | ||
"watch": "rollup -c -w", | ||
@@ -81,13 +84,12 @@ "build": "rollup -c", | ||
"test:coverage": "jest --passWithNoTests --coverage", | ||
"lint": "npm run eslint && npm run prettier", | ||
"fix": "npm run eslint:fix && npm run prettier:write", | ||
"prettier:write": "prettier */** --write", | ||
"prettier": "prettier */** --check", | ||
"lint": "yarn run eslint && yarn run prettier", | ||
"fix": "yarn run eslint:fix && yarn run prettier:write", | ||
"prettier:write": "prettier \"*\" \"*/**\" --write", | ||
"prettier": "prettier \"*\" \"*/**\" --check", | ||
"eslint": "eslint src --ext .ts,.tsx", | ||
"eslint:fix": "npm run eslint -- --fix", | ||
"docs": "typedoc", | ||
"prepare": "npm run build", | ||
"release": "release-it --disable-metrics --npm.skipChecks", | ||
"release:pre": "release-it --disable-metrics --npm.skipChecks --preRelease=alpha --npm.tag=next" | ||
} | ||
"eslint:fix": "yarn run eslint --fix", | ||
"docs": "typedoc src/index.ts", | ||
"prepare": "yarn run build" | ||
}, | ||
"packageManager": "yarn@3.1.0" | ||
} |
@@ -19,12 +19,3 @@ # BoxPlots | ||
const arr = [ | ||
-0.402253, | ||
-1.4521869, | ||
0.135228, | ||
-1.8620118, | ||
-0.5687531, | ||
0.4218371, | ||
-1.1165662, | ||
0.5960255, | ||
-0.5008038, | ||
-0.394178, | ||
-0.402253, -1.4521869, 0.135228, -1.8620118, -0.5687531, 0.4218371, -1.1165662, 0.5960255, -0.5008038, -0.394178, | ||
1.3709885, | ||
@@ -31,0 +22,0 @@ ]; |
@@ -11,12 +11,3 @@ /// <reference types="jest" /> | ||
const arr = [ | ||
-0.402253, | ||
-1.4521869, | ||
0.135228, | ||
-1.8620118, | ||
-0.5687531, | ||
0.4218371, | ||
-1.1165662, | ||
0.5960255, | ||
-0.5008038, | ||
-0.394178, | ||
-0.402253, -1.4521869, 0.135228, -1.8620118, -0.5687531, 0.4218371, -1.1165662, 0.5960255, -0.5008038, -0.394178, | ||
1.3709885, | ||
@@ -23,0 +14,0 @@ ]; |
@@ -0,1 +1,2 @@ | ||
import kde, { KernelDensityEstimator } from './kde'; | ||
import { quantilesType7 } from './quantiles'; | ||
@@ -50,2 +51,7 @@ | ||
/** | ||
* variance | ||
*/ | ||
readonly variance: number; | ||
/** | ||
* number of missing values (NaN, null, undefined) in the data | ||
@@ -62,2 +68,4 @@ */ | ||
readonly items: ArrayLike<number>; | ||
readonly kde: KernelDensityEstimator; | ||
} | ||
@@ -105,25 +113,15 @@ | ||
function createSortedData(data: readonly number[] | Float32Array | Float64Array) { | ||
let min = Number.POSITIVE_INFINITY; | ||
let max = Number.NEGATIVE_INFINITY; | ||
let sum = 0; | ||
let valid = 0; | ||
const length = data.length; | ||
const { length } = data; | ||
const vs = data instanceof Float64Array ? new Float64Array(length) : new Float32Array(length); | ||
for (let i = 0; i < length; ++i) { | ||
for (let i = 0; i < length; i += 1) { | ||
const v = data[i]; | ||
if (v == null || Number.isNaN(v)) { | ||
// eslint-disable-next-line no-continue | ||
continue; | ||
} | ||
vs[valid] = v; | ||
valid++; | ||
if (v < min) { | ||
min = v; | ||
} | ||
if (v > max) { | ||
max = v; | ||
} | ||
sum += v; | ||
valid += 1; | ||
} | ||
@@ -135,5 +133,4 @@ | ||
return { | ||
sum, | ||
min, | ||
max, | ||
min: Number.NaN, | ||
max: Number.NaN, | ||
missing, | ||
@@ -145,13 +142,16 @@ s: [], | ||
// add comparator since the polyfill doesn't to a real sorting | ||
const s = valid === length ? vs : vs.subarray(0, valid); | ||
const validData = valid === length ? vs : vs.subarray(0, valid); | ||
// sort in place | ||
s.sort((a, b) => (a === b ? 0 : a < b ? -1 : 1)); | ||
// eslint-disable-next-line no-nested-ternary | ||
validData.sort((a, b) => (a === b ? 0 : a < b ? -1 : 1)); | ||
// use real number for better precision | ||
const min = validData[0]; | ||
const max = validData[validData.length - 1]; | ||
return { | ||
sum, | ||
min: s[0], | ||
max: s[s.length - 1], | ||
min, | ||
max, | ||
missing, | ||
s, | ||
s: validData, | ||
}; | ||
@@ -163,3 +163,2 @@ } | ||
return { | ||
sum: Number.NaN, | ||
min: Number.NaN, | ||
@@ -173,12 +172,4 @@ max: Number.NaN, | ||
const max = data[data.length - 1]; | ||
const red = (acc: number, v: number) => acc + v; | ||
const sum = | ||
data instanceof Float32Array | ||
? data.reduce(red, 0) | ||
: data instanceof Float64Array | ||
? data.reduce(red, 0) | ||
: data.reduce(red, 0); | ||
return { | ||
sum, | ||
min, | ||
@@ -191,52 +182,21 @@ max, | ||
export default function boxplot( | ||
data: readonly number[] | Float32Array | Float64Array, | ||
options: BoxplotStatsOptions = {} | ||
): IBoxPlot { | ||
const { quantiles, validAndSorted, coef, whiskersMode, eps }: Required<BoxplotStatsOptions> = Object.assign( | ||
{ | ||
coef: 1.5, | ||
eps: 10e-3, | ||
quantiles: quantilesType7, | ||
validAndSorted: false, | ||
whiskersMode: 'nearest', | ||
}, | ||
options | ||
); | ||
const { missing, s, min, max, sum } = validAndSorted ? withSortedData(data) : createSortedData(data); | ||
const invalid = { | ||
min: Number.NaN, | ||
max: Number.NaN, | ||
mean: Number.NaN, | ||
missing, | ||
iqr: Number.NaN, | ||
count: data.length, | ||
whiskerHigh: Number.NaN, | ||
whiskerLow: Number.NaN, | ||
outlier: [], | ||
median: Number.NaN, | ||
q1: Number.NaN, | ||
q3: Number.NaN, | ||
items: [], | ||
}; | ||
function computeWhiskers( | ||
s: ArrayLike<number>, | ||
valid: number, | ||
min: number, | ||
max: number, | ||
{ eps, quantiles, coef, whiskersMode }: Required<BoxplotStatsOptions> | ||
) { | ||
const same = (a: number, b: number) => Math.abs(a - b) < eps; | ||
const valid = data.length - missing; | ||
if (valid === 0) { | ||
return invalid; | ||
} | ||
const { median, q1, q3 } = quantiles(s, valid); | ||
const iqr = q3 - q1; | ||
const isCoefValid = typeof coef === 'number' && coef > 0; | ||
let whiskerLow = isCoefValid ? Math.max(min, q1 - coef * iqr) : min; | ||
let whiskerHigh = isCoefValid ? Math.min(max, q3 + coef * iqr) : max; | ||
const outlier: number[] = []; | ||
const outlierLow: number[] = []; | ||
// look for the closest value which is bigger than the computed left | ||
for (let i = 0; i < valid; ++i) { | ||
for (let i = 0; i < valid; i += 1) { | ||
const v = s[i]; | ||
@@ -250,9 +210,9 @@ if (v >= whiskerLow || same(v, whiskerLow)) { | ||
// outlier | ||
if (outlier.length === 0 || !same(outlier[outlier.length - 1], v)) { | ||
outlier.push(v); | ||
if (outlierLow.length === 0 || !same(outlierLow[outlierLow.length - 1], v)) { | ||
outlierLow.push(v); | ||
} | ||
} | ||
// look for the closest value which is smaller than the computed right | ||
const reversedOutliers: number[] = []; | ||
for (let i = valid - 1; i >= 0; --i) { | ||
const reversedOutlierHigh: number[] = []; | ||
for (let i = valid - 1; i >= 0; i -= 1) { | ||
const v = s[i]; | ||
@@ -267,10 +227,81 @@ if (v <= whiskerHigh || same(v, whiskerHigh)) { | ||
if ( | ||
(reversedOutliers.length === 0 || !same(reversedOutliers[reversedOutliers.length - 1], v)) && | ||
(outlier.length === 0 || !same(outlier[outlier.length - 1], v)) | ||
(reversedOutlierHigh.length === 0 || !same(reversedOutlierHigh[reversedOutlierHigh.length - 1], v)) && | ||
(outlierLow.length === 0 || !same(outlierLow[outlierLow.length - 1], v)) | ||
) { | ||
reversedOutliers.push(v); | ||
reversedOutlierHigh.push(v); | ||
} | ||
} | ||
const outlier = outlierLow.concat(reversedOutlierHigh.reverse()); | ||
return { | ||
median, | ||
q1, | ||
q3, | ||
iqr, | ||
outlier, | ||
whiskerHigh, | ||
whiskerLow, | ||
}; | ||
} | ||
function computeStats(s: ArrayLike<number>, valid: number) { | ||
let mean = 0; | ||
for (let i = 0; i < valid; i++) { | ||
const v = s[i]; | ||
mean += v; | ||
} | ||
mean /= valid; | ||
let variance = 0; | ||
for (let i = 0; i < valid; i++) { | ||
const v = s[i]; | ||
variance += (v - mean) * (v - mean); | ||
} | ||
variance /= valid; | ||
return { | ||
mean, | ||
variance, | ||
}; | ||
} | ||
export default function boxplot( | ||
data: readonly number[] | Float32Array | Float64Array, | ||
options: BoxplotStatsOptions = {} | ||
): IBoxPlot { | ||
const fullOptions: Required<BoxplotStatsOptions> = { | ||
coef: 1.5, | ||
eps: 10e-3, | ||
quantiles: quantilesType7, | ||
validAndSorted: false, | ||
whiskersMode: 'nearest', | ||
...options, | ||
}; | ||
const { missing, s, min, max } = fullOptions.validAndSorted ? withSortedData(data) : createSortedData(data); | ||
const invalid: IBoxPlot = { | ||
min: Number.NaN, | ||
max: Number.NaN, | ||
mean: Number.NaN, | ||
missing, | ||
iqr: Number.NaN, | ||
count: data.length, | ||
whiskerHigh: Number.NaN, | ||
whiskerLow: Number.NaN, | ||
outlier: [], | ||
median: Number.NaN, | ||
q1: Number.NaN, | ||
q3: Number.NaN, | ||
variance: 0, | ||
items: [], | ||
kde: () => 0, | ||
}; | ||
const valid = data.length - missing; | ||
if (valid === 0) { | ||
return invalid; | ||
} | ||
const result: Omit<IBoxPlot, 'kde'> = { | ||
min, | ||
@@ -280,12 +311,10 @@ max, | ||
missing, | ||
mean: sum / valid, | ||
whiskerHigh, | ||
whiskerLow, | ||
outlier: outlier.concat(reversedOutliers.reverse()), | ||
median, | ||
q1, | ||
q3, | ||
iqr, | ||
items: s, | ||
...computeStats(s, valid), | ||
...computeWhiskers(s, valid, min, max, fullOptions), | ||
}; | ||
return { | ||
...result, | ||
kde: kde(result), | ||
}; | ||
} |
export * from './boxplot'; | ||
export { default as boxplot, default } from './boxplot'; | ||
export { default as kde, KernelDensityEstimator } from './kde'; | ||
export * from './quantiles'; |
@@ -20,12 +20,3 @@ /// <reference types="jest" /> | ||
const arr = [ | ||
-0.402253, | ||
-1.4521869, | ||
0.135228, | ||
-1.8620118, | ||
-0.5687531, | ||
0.4218371, | ||
-1.1165662, | ||
0.5960255, | ||
-0.5008038, | ||
-0.394178, | ||
-0.402253, -1.4521869, 0.135228, -1.8620118, -0.5687531, 0.4218371, -1.1165662, 0.5960255, -0.5008038, -0.394178, | ||
1.3709885, | ||
@@ -48,14 +39,4 @@ ].sort(asc); | ||
const arr = [ | ||
1.086657167, | ||
0.294672807, | ||
1.462293013, | ||
0.485641706, | ||
1.57748264, | ||
0.827809286, | ||
-0.397192557, | ||
-1.222111542, | ||
1.071236583, | ||
-1.182959319, | ||
-0.003749222, | ||
-0.360759239, | ||
1.086657167, 0.294672807, 1.462293013, 0.485641706, 1.57748264, 0.827809286, -0.397192557, -1.222111542, | ||
1.071236583, -1.182959319, -0.003749222, -0.360759239, | ||
].sort(asc); | ||
@@ -62,0 +43,0 @@ it('type7', () => { |
@@ -0,1 +1,7 @@ | ||
export interface QuantilesResult { | ||
q1: number; | ||
median: number; | ||
q3: number; | ||
} | ||
/** | ||
@@ -10,3 +16,3 @@ * computes the boxplot stats using the given interpolation function if needed | ||
interpolate: (i: number, j: number, fraction: number) => number | ||
) { | ||
): QuantilesResult { | ||
const n1 = length - 1; | ||
@@ -33,3 +39,3 @@ const compute = (q: number) => { | ||
*/ | ||
export function quantilesType7(arr: ArrayLike<number>, length = arr.length) { | ||
export function quantilesType7(arr: ArrayLike<number>, length = arr.length): QuantilesResult { | ||
return quantilesInterpolate(arr, length, (a, b, alpha) => a + alpha * (b - a)); | ||
@@ -42,3 +48,3 @@ } | ||
*/ | ||
export function quantilesLinear(arr: ArrayLike<number>, length = arr.length) { | ||
export function quantilesLinear(arr: ArrayLike<number>, length = arr.length): QuantilesResult { | ||
return quantilesInterpolate(arr, length, (i, j, fraction) => i + (j - i) * fraction); | ||
@@ -50,3 +56,3 @@ } | ||
*/ | ||
export function quantilesLower(arr: ArrayLike<number>, length = arr.length) { | ||
export function quantilesLower(arr: ArrayLike<number>, length = arr.length): QuantilesResult { | ||
return quantilesInterpolate(arr, length, (i) => i); | ||
@@ -58,3 +64,3 @@ } | ||
*/ | ||
export function quantilesHigher(arr: ArrayLike<number>, length = arr.length) { | ||
export function quantilesHigher(arr: ArrayLike<number>, length = arr.length): QuantilesResult { | ||
return quantilesInterpolate(arr, length, (_, j) => j); | ||
@@ -66,3 +72,3 @@ } | ||
*/ | ||
export function quantilesNearest(arr: ArrayLike<number>, length = arr.length) { | ||
export function quantilesNearest(arr: ArrayLike<number>, length = arr.length): QuantilesResult { | ||
return quantilesInterpolate(arr, length, (i, j, fraction) => (fraction < 0.5 ? i : j)); | ||
@@ -74,3 +80,3 @@ } | ||
*/ | ||
export function quantilesMidpoint(arr: ArrayLike<number>, length = arr.length) { | ||
export function quantilesMidpoint(arr: ArrayLike<number>, length = arr.length): QuantilesResult { | ||
return quantilesInterpolate(arr, length, (i, j) => (i + j) * 0.5); | ||
@@ -86,3 +92,3 @@ } | ||
*/ | ||
export function quantilesFivenum(arr: ArrayLike<number>, length = arr.length) { | ||
export function quantilesFivenum(arr: ArrayLike<number>, length = arr.length): QuantilesResult { | ||
// based on R fivenum | ||
@@ -107,4 +113,4 @@ const n = length; | ||
*/ | ||
export function quantilesHinges(arr: ArrayLike<number>, length = arr.length) { | ||
export function quantilesHinges(arr: ArrayLike<number>, length = arr.length): QuantilesResult { | ||
return quantilesFivenum(arr, length); | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
67276
31
17
1229
61