@cloudglides/nox 1.1.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +9 -9
  2. package/example/src/App.css +89 -84
  3. package/example/src/App.jsx +375 -151
  4. package/package.json +7 -6
  5. package/src/core.browser.js +10 -2
  6. package/src/core.js +32 -4
  7. package/src/generators/logistic.js +30 -25
  8. package/src/generators/mixer.js +7 -7
  9. package/src/generators/mt19937.js +10 -7
  10. package/src/generators/pcg64.js +23 -12
  11. package/src/generators/splitmix64.js +12 -6
  12. package/src/generators/tent.js +12 -7
  13. package/src/generators/xorshift64.js +6 -3
  14. package/src/index.d.ts +68 -4
  15. package/src/index.js +154 -2
  16. package/src/rng.browser.js +21 -10
  17. package/src/rng.js +95 -82
  18. package/src/utils/arrays.js +149 -0
  19. package/src/utils/bits.js +146 -21
  20. package/src/utils/categorical.js +68 -31
  21. package/src/utils/combinatorics.js +113 -69
  22. package/src/utils/confidence.js +145 -0
  23. package/src/utils/decomposition.js +204 -0
  24. package/src/utils/distributions-advanced.js +122 -0
  25. package/src/utils/distributions-extra.js +102 -11
  26. package/src/utils/distributions-special.js +77 -20
  27. package/src/utils/distributions.js +99 -35
  28. package/src/utils/effects.js +172 -0
  29. package/src/utils/entropy.browser.js +29 -26
  30. package/src/utils/entropy.js +18 -8
  31. package/src/utils/helpers.js +64 -0
  32. package/src/utils/hypothesis.js +167 -0
  33. package/src/utils/integration.js +137 -0
  34. package/src/utils/interpolation.js +221 -0
  35. package/src/utils/matrix.js +242 -0
  36. package/src/utils/noise.js +36 -22
  37. package/src/utils/odesolvers.js +176 -0
  38. package/src/utils/optimization.js +215 -0
  39. package/src/utils/precomputed.js +166 -0
  40. package/src/utils/probability.js +199 -0
  41. package/src/utils/regression.js +170 -0
  42. package/src/utils/resampling.js +112 -0
  43. package/src/utils/rootfinding.js +158 -0
  44. package/src/utils/sampling.js +86 -77
  45. package/src/utils/seed.js +10 -4
  46. package/src/utils/seeding.js +24 -12
  47. package/src/utils/sequence.js +116 -32
  48. package/src/utils/state.js +48 -36
  49. package/src/utils/statistics.js +64 -2
  50. package/src/utils/stochastic.js +91 -31
  51. package/src/utils/stratified.js +108 -0
  52. package/src/utils/timeseries.js +166 -0
  53. package/src/utils/transforms.js +146 -0
  54. package/test/comprehensive-new.js +126 -0
  55. package/test/comprehensive.js +4 -3
  56. package/test/error-handling.js +49 -0
  57. package/test/new-features.js +52 -0
  58. package/IMPROVEMENTS.md +0 -58
@@ -139,5 +139,67 @@ export const varianceTest = (data, expectedVariance = 1 / 12) => {
139
139
  expectedVariance,
140
140
  chi2Statistic: chi2,
141
141
  degreesOfFreedom: data.length - 1
142
- };
143
- };
142
+ };
143
+ };
144
+
145
+ export const skewness = (data) => {
146
+ if (!Array.isArray(data) || data.length < 3) {
147
+ throw new Error('Data must be array with at least 3 elements');
148
+ }
149
+
150
+ const n = data.length;
151
+ const mean = data.reduce((a, b) => a + b, 0) / n;
152
+ const m2 = data.reduce((sum, x) => sum + (x - mean) ** 2, 0) / n;
153
+ const m3 = data.reduce((sum, x) => sum + (x - mean) ** 3, 0) / n;
154
+ const std = Math.sqrt(m2);
155
+
156
+ if (std === 0) return 0;
157
+ return m3 / (std ** 3);
158
+ };
159
+
160
+ export const kurtosis = (data) => {
161
+ if (!Array.isArray(data) || data.length < 4) {
162
+ throw new Error('Data must be array with at least 4 elements');
163
+ }
164
+
165
+ const n = data.length;
166
+ const mean = data.reduce((a, b) => a + b, 0) / n;
167
+ const m2 = data.reduce((sum, x) => sum + (x - mean) ** 2, 0) / n;
168
+ const m4 = data.reduce((sum, x) => sum + (x - mean) ** 4, 0) / n;
169
+ const std = Math.sqrt(m2);
170
+
171
+ if (std === 0) return 0;
172
+ return m4 / (std ** 4) - 3;
173
+ };
174
+
175
+ export const median = (data) => {
176
+ if (!Array.isArray(data) || data.length === 0) {
177
+ throw new Error('Data must be non-empty array');
178
+ }
179
+
180
+ const sorted = [...data].sort((a, b) => a - b);
181
+ const mid = Math.floor(sorted.length / 2);
182
+
183
+ if (sorted.length % 2 === 1) {
184
+ return sorted[mid];
185
+ }
186
+ return (sorted[mid - 1] + sorted[mid]) / 2;
187
+ };
188
+
189
+ export const quantile = (data, q) => {
190
+ if (!Array.isArray(data) || data.length === 0) {
191
+ throw new Error('Data must be non-empty array');
192
+ }
193
+ if (typeof q !== 'number' || q < 0 || q > 1) {
194
+ throw new RangeError('Quantile must be between 0 and 1');
195
+ }
196
+
197
+ const sorted = [...data].sort((a, b) => a - b);
198
+ const idx = q * (sorted.length - 1);
199
+ const lower = Math.floor(idx);
200
+ const upper = Math.ceil(idx);
201
+ const weight = idx % 1;
202
+
203
+ if (lower === upper) return sorted[lower];
204
+ return sorted[lower] * (1 - weight) + sorted[upper] * weight;
205
+ };
@@ -1,32 +1,92 @@
1
1
  export const brownianMotion = (rng, steps, dt = 1) => {
2
- const path = [0];
3
- for (let i = 0; i < steps; i++) {
4
- const drift = -0.5 * dt;
5
- const diffusion = Math.sqrt(dt) * (rng.nextFloat() * 2 - 1);
6
- const nextVal = path[i] + drift + diffusion;
7
- path.push(nextVal);
8
- }
9
- return path;
10
- };
11
-
12
- export const ornsteinUhlenbeck = (rng, steps, theta = 0.1, mu = 0, sigma = 1) => {
13
- const path = [mu];
14
- for (let i = 0; i < steps; i++) {
15
- const drift = theta * (mu - path[i]);
16
- const diffusion = sigma * (rng.nextFloat() * 2 - 1);
17
- const nextVal = path[i] + drift + diffusion;
18
- path.push(nextVal);
19
- }
20
- return path;
21
- };
22
-
23
- export const geometricBrownian = (rng, steps, mu = 0.05, sigma = 0.2, dt = 0.01) => {
24
- const path = [1];
25
- for (let i = 0; i < steps; i++) {
26
- const drift = (mu - 0.5 * sigma * sigma) * dt;
27
- const diffusion = sigma * Math.sqrt(dt) * (rng.nextFloat() * 2 - 1);
28
- const nextVal = path[i] * Math.exp(drift + diffusion);
29
- path.push(nextVal);
30
- }
31
- return path;
32
- };
2
+ if (!rng || typeof rng.nextFloat !== 'function') {
3
+ throw new TypeError('First argument must be RNG instance');
4
+ }
5
+ if (typeof steps !== 'number' || !Number.isInteger(steps)) {
6
+ throw new TypeError('steps must be an integer');
7
+ }
8
+ if (steps <= 0) {
9
+ throw new RangeError('steps must be positive');
10
+ }
11
+ if (typeof dt !== 'number') {
12
+ throw new TypeError('dt must be a number');
13
+ }
14
+ if (dt <= 0) {
15
+ throw new RangeError('dt must be positive');
16
+ }
17
+ const path = [0];
18
+ for (let i = 0; i < steps; i++) {
19
+ const drift = -0.5 * dt;
20
+ const diffusion = Math.sqrt(dt) * (rng.nextFloat() * 2 - 1);
21
+ const nextVal = path[i] + drift + diffusion;
22
+ path.push(nextVal);
23
+ }
24
+ return path;
25
+ };
26
+
27
+ export const ornsteinUhlenbeck = (rng, steps, theta = 0.1, mu = 0, sigma = 1) => {
28
+ if (!rng || typeof rng.nextFloat !== 'function') {
29
+ throw new TypeError('First argument must be RNG instance');
30
+ }
31
+ if (typeof steps !== 'number' || !Number.isInteger(steps)) {
32
+ throw new TypeError('steps must be an integer');
33
+ }
34
+ if (steps <= 0) {
35
+ throw new RangeError('steps must be positive');
36
+ }
37
+ if (typeof theta !== 'number') {
38
+ throw new TypeError('theta must be a number');
39
+ }
40
+ if (typeof mu !== 'number') {
41
+ throw new TypeError('mu must be a number');
42
+ }
43
+ if (typeof sigma !== 'number') {
44
+ throw new TypeError('sigma must be a number');
45
+ }
46
+ if (sigma <= 0) {
47
+ throw new RangeError('sigma must be positive');
48
+ }
49
+ const path = [mu];
50
+ for (let i = 0; i < steps; i++) {
51
+ const drift = theta * (mu - path[i]);
52
+ const diffusion = sigma * (rng.nextFloat() * 2 - 1);
53
+ const nextVal = path[i] + drift + diffusion;
54
+ path.push(nextVal);
55
+ }
56
+ return path;
57
+ };
58
+
59
+ export const geometricBrownian = (rng, steps, mu = 0.05, sigma = 0.2, dt = 0.01) => {
60
+ if (!rng || typeof rng.nextFloat !== 'function') {
61
+ throw new TypeError('First argument must be RNG instance');
62
+ }
63
+ if (typeof steps !== 'number' || !Number.isInteger(steps)) {
64
+ throw new TypeError('steps must be an integer');
65
+ }
66
+ if (steps <= 0) {
67
+ throw new RangeError('steps must be positive');
68
+ }
69
+ if (typeof mu !== 'number') {
70
+ throw new TypeError('mu must be a number');
71
+ }
72
+ if (typeof sigma !== 'number') {
73
+ throw new TypeError('sigma must be a number');
74
+ }
75
+ if (sigma <= 0) {
76
+ throw new RangeError('sigma must be positive');
77
+ }
78
+ if (typeof dt !== 'number') {
79
+ throw new TypeError('dt must be a number');
80
+ }
81
+ if (dt <= 0) {
82
+ throw new RangeError('dt must be positive');
83
+ }
84
+ const path = [1];
85
+ for (let i = 0; i < steps; i++) {
86
+ const drift = (mu - 0.5 * sigma * sigma) * dt;
87
+ const diffusion = sigma * Math.sqrt(dt) * (rng.nextFloat() * 2 - 1);
88
+ const nextVal = path[i] * Math.exp(drift + diffusion);
89
+ path.push(nextVal);
90
+ }
91
+ return path;
92
+ };
@@ -0,0 +1,108 @@
1
+ export function stratifiedSample(r, data, strata, n) {
2
+ if (!Array.isArray(data) || data.length === 0) {
3
+ throw new TypeError('data must be a non-empty array');
4
+ }
5
+ if (typeof strata !== 'function') {
6
+ throw new TypeError('strata must be a function that returns stratum key');
7
+ }
8
+ if (typeof n !== 'number' || n < 1) {
9
+ throw new RangeError('n must be a positive integer');
10
+ }
11
+
12
+ const groups = new Map();
13
+
14
+ for (let i = 0; i < data.length; i++) {
15
+ const key = strata(data[i]);
16
+ if (!groups.has(key)) {
17
+ groups.set(key, []);
18
+ }
19
+ groups.get(key).push(data[i]);
20
+ }
21
+
22
+ const result = [];
23
+ const groupCount = groups.size;
24
+ const samplesPerGroup = Math.floor(n / groupCount);
25
+ let remaining = n - samplesPerGroup * groupCount;
26
+
27
+ for (const [key, group] of groups) {
28
+ const k = samplesPerGroup + (remaining > 0 ? 1 : 0);
29
+ if (remaining > 0) remaining--;
30
+
31
+ if (k >= group.length) {
32
+ result.push(...group);
33
+ } else {
34
+ for (let i = 0; i < k; i++) {
35
+ const idx = r.nextInt(group.length);
36
+ result.push(group[idx]);
37
+ group[idx] = group[group.length - 1];
38
+ group.pop();
39
+ }
40
+ }
41
+ }
42
+
43
+ return result;
44
+ }
45
+
46
+ export function stratifiedSampleProportional(r, data, strata, n) {
47
+ if (!Array.isArray(data) || data.length === 0) {
48
+ throw new TypeError('data must be a non-empty array');
49
+ }
50
+ if (typeof strata !== 'function') {
51
+ throw new TypeError('strata must be a function');
52
+ }
53
+ if (typeof n !== 'number' || n < 1) {
54
+ throw new RangeError('n must be a positive integer');
55
+ }
56
+
57
+ const groups = new Map();
58
+
59
+ for (let i = 0; i < data.length; i++) {
60
+ const key = strata(data[i]);
61
+ if (!groups.has(key)) {
62
+ groups.set(key, []);
63
+ }
64
+ groups.get(key).push(data[i]);
65
+ }
66
+
67
+ const result = [];
68
+ const total = data.length;
69
+
70
+ for (const [key, group] of groups) {
71
+ const k = Math.round(n * group.length / total);
72
+
73
+ if (k >= group.length) {
74
+ result.push(...group);
75
+ } else if (k > 0) {
76
+ const copy = [...group];
77
+ for (let i = 0; i < k; i++) {
78
+ const idx = r.nextInt(copy.length);
79
+ result.push(copy[idx]);
80
+ copy[idx] = copy[copy.length - 1];
81
+ copy.pop();
82
+ }
83
+ }
84
+ }
85
+
86
+ return result;
87
+ }
88
+
89
+ export function stratify(data, strata) {
90
+ if (!Array.isArray(data) || data.length === 0) {
91
+ throw new TypeError('data must be a non-empty array');
92
+ }
93
+ if (typeof strata !== 'function') {
94
+ throw new TypeError('strata must be a function');
95
+ }
96
+
97
+ const groups = new Map();
98
+
99
+ for (let i = 0; i < data.length; i++) {
100
+ const key = strata(data[i]);
101
+ if (!groups.has(key)) {
102
+ groups.set(key, []);
103
+ }
104
+ groups.get(key).push(data[i]);
105
+ }
106
+
107
+ return Object.fromEntries(groups);
108
+ }
@@ -0,0 +1,166 @@
1
+ export function diff(data, order = 1) {
2
+ if (!Array.isArray(data) || data.length === 0) {
3
+ throw new TypeError('data must be non-empty array');
4
+ }
5
+ if (typeof order !== 'number' || order < 1) {
6
+ throw new RangeError('order must be positive integer');
7
+ }
8
+
9
+ let result = [...data];
10
+
11
+ for (let o = 0; o < order; o++) {
12
+ const prev = result;
13
+ result = new Array(prev.length - 1);
14
+ for (let i = 0; i < prev.length - 1; i++) {
15
+ result[i] = prev[i + 1] - prev[i];
16
+ }
17
+ }
18
+
19
+ return result;
20
+ }
21
+
22
+ export function lag(data, lags = 1) {
23
+ if (!Array.isArray(data) || data.length === 0) {
24
+ throw new TypeError('data must be non-empty array');
25
+ }
26
+ if (typeof lags !== 'number' || lags < 1) {
27
+ throw new RangeError('lags must be positive integer');
28
+ }
29
+
30
+ const result = new Array(data.length);
31
+
32
+ for (let i = 0; i < data.length; i++) {
33
+ result[i] = i < lags ? null : data[i - lags];
34
+ }
35
+
36
+ return result;
37
+ }
38
+
39
+ export function shift(data, periods = 1) {
40
+ if (!Array.isArray(data) || data.length === 0) {
41
+ throw new TypeError('data must be non-empty array');
42
+ }
43
+ if (typeof periods !== 'number') {
44
+ throw new RangeError('periods must be integer');
45
+ }
46
+
47
+ const result = new Array(data.length);
48
+
49
+ if (periods > 0) {
50
+ for (let i = 0; i < data.length; i++) {
51
+ result[i] = i < periods ? null : data[i - periods];
52
+ }
53
+ } else if (periods < 0) {
54
+ for (let i = 0; i < data.length; i++) {
55
+ result[i] = i < -periods ? null : data[i - periods];
56
+ }
57
+ } else {
58
+ return [...data];
59
+ }
60
+
61
+ return result;
62
+ }
63
+
64
+ export function sma(data, window) {
65
+ if (!Array.isArray(data) || data.length === 0) {
66
+ throw new TypeError('data must be non-empty array');
67
+ }
68
+ if (typeof window !== 'number' || window < 1) {
69
+ throw new RangeError('window must be positive integer');
70
+ }
71
+
72
+ const result = new Array(data.length);
73
+
74
+ for (let i = 0; i < data.length; i++) {
75
+ if (i < window - 1) {
76
+ result[i] = null;
77
+ } else {
78
+ let sum = 0;
79
+ for (let j = i - window + 1; j <= i; j++) {
80
+ sum += data[j];
81
+ }
82
+ result[i] = sum / window;
83
+ }
84
+ }
85
+
86
+ return result;
87
+ }
88
+
89
+ export function ema(data, window) {
90
+ if (!Array.isArray(data) || data.length === 0) {
91
+ throw new TypeError('data must be non-empty array');
92
+ }
93
+ if (typeof window !== 'number' || window < 1) {
94
+ throw new RangeError('window must be positive integer');
95
+ }
96
+
97
+ const alpha = 2 / (window + 1);
98
+ const result = new Array(data.length);
99
+
100
+ result[0] = data[0];
101
+
102
+ for (let i = 1; i < data.length; i++) {
103
+ result[i] = alpha * data[i] + (1 - alpha) * result[i - 1];
104
+ }
105
+
106
+ return result;
107
+ }
108
+
109
+ export function acf(data, maxLag = 40) {
110
+ if (!Array.isArray(data) || data.length === 0) {
111
+ throw new TypeError('data must be non-empty array');
112
+ }
113
+ if (typeof maxLag !== 'number' || maxLag < 1) {
114
+ throw new RangeError('maxLag must be positive integer');
115
+ }
116
+
117
+ const mean = data.reduce((a, b) => a + b, 0) / data.length;
118
+ const centered = data.map(x => x - mean);
119
+
120
+ let c0 = 0;
121
+ for (let i = 0; i < data.length; i++) {
122
+ c0 += centered[i] * centered[i];
123
+ }
124
+
125
+ const result = new Array(Math.min(maxLag + 1, data.length));
126
+ result[0] = 1;
127
+
128
+ for (let k = 1; k < result.length; k++) {
129
+ let ck = 0;
130
+ for (let i = 0; i < data.length - k; i++) {
131
+ ck += centered[i] * centered[i + k];
132
+ }
133
+ result[k] = ck / c0;
134
+ }
135
+
136
+ return result;
137
+ }
138
+
139
+ export function pacf(data, maxLag = 40) {
140
+ if (!Array.isArray(data) || data.length === 0) {
141
+ throw new TypeError('data must be non-empty array');
142
+ }
143
+ if (typeof maxLag !== 'number' || maxLag < 1) {
144
+ throw new RangeError('maxLag must be positive integer');
145
+ }
146
+
147
+ const correlations = acf(data, maxLag);
148
+ const result = [1];
149
+
150
+ for (let k = 1; k < correlations.length; k++) {
151
+ let numerator = correlations[k];
152
+
153
+ for (let j = 1; j < k; j++) {
154
+ numerator -= result[j] * correlations[k - j];
155
+ }
156
+
157
+ let denominator = 1;
158
+ for (let j = 1; j < k; j++) {
159
+ denominator -= result[j] * correlations[j];
160
+ }
161
+
162
+ result.push(numerator / denominator);
163
+ }
164
+
165
+ return result;
166
+ }
@@ -0,0 +1,146 @@
1
+ export function zscore(data) {
2
+ if (!Array.isArray(data) || data.length === 0) {
3
+ throw new TypeError('data must be non-empty array');
4
+ }
5
+
6
+ const mean = data.reduce((a, b) => a + b, 0) / data.length;
7
+ let variance = 0;
8
+
9
+ for (let i = 0; i < data.length; i++) {
10
+ variance += (data[i] - mean) ** 2;
11
+ }
12
+
13
+ const stddev = Math.sqrt(variance / data.length);
14
+
15
+ if (stddev === 0) {
16
+ throw new RangeError('cannot z-score data with zero variance');
17
+ }
18
+
19
+ return data.map(x => (x - mean) / stddev);
20
+ }
21
+
22
+ export function standardize(data) {
23
+ if (!Array.isArray(data) || data.length === 0) {
24
+ throw new TypeError('data must be non-empty array');
25
+ }
26
+
27
+ const mean = data.reduce((a, b) => a + b, 0) / data.length;
28
+ return data.map(x => x - mean);
29
+ }
30
+
31
+ export function minMaxScale(data, min = 0, max = 1) {
32
+ if (!Array.isArray(data) || data.length === 0) {
33
+ throw new TypeError('data must be non-empty array');
34
+ }
35
+ if (typeof min !== 'number' || typeof max !== 'number') {
36
+ throw new TypeError('min and max must be numbers');
37
+ }
38
+
39
+ const dataMin = Math.min(...data);
40
+ const dataMax = Math.max(...data);
41
+ const range = dataMax - dataMin;
42
+
43
+ if (range === 0) {
44
+ throw new RangeError('cannot scale data with zero range');
45
+ }
46
+
47
+ return data.map(x => ((x - dataMin) / range) * (max - min) + min);
48
+ }
49
+
50
+ export function logTransform(data, base = Math.E) {
51
+ if (!Array.isArray(data) || data.length === 0) {
52
+ throw new TypeError('data must be non-empty array');
53
+ }
54
+ if (typeof base !== 'number' || base <= 0 || base === 1) {
55
+ throw new RangeError('base must be positive number != 1');
56
+ }
57
+
58
+ for (let i = 0; i < data.length; i++) {
59
+ if (data[i] <= 0) {
60
+ throw new RangeError('all values must be positive');
61
+ }
62
+ }
63
+
64
+ const logBase = Math.log(base);
65
+ return data.map(x => Math.log(x) / logBase);
66
+ }
67
+
68
+ export function sqrtTransform(data) {
69
+ if (!Array.isArray(data) || data.length === 0) {
70
+ throw new TypeError('data must be non-empty array');
71
+ }
72
+
73
+ for (let i = 0; i < data.length; i++) {
74
+ if (data[i] < 0) {
75
+ throw new RangeError('all values must be non-negative');
76
+ }
77
+ }
78
+
79
+ return data.map(x => Math.sqrt(x));
80
+ }
81
+
82
+ export function rank(data) {
83
+ if (!Array.isArray(data) || data.length === 0) {
84
+ throw new TypeError('data must be non-empty array');
85
+ }
86
+
87
+ const indexed = data.map((val, i) => [val, i]);
88
+ indexed.sort((a, b) => a[0] - b[0]);
89
+
90
+ const ranks = new Array(data.length);
91
+ let i = 0;
92
+
93
+ while (i < indexed.length) {
94
+ let j = i;
95
+ while (j < indexed.length && indexed[j][0] === indexed[i][0]) {
96
+ j++;
97
+ }
98
+
99
+ const rank = (i + 1 + j) / 2;
100
+ for (let k = i; k < j; k++) {
101
+ ranks[indexed[k][1]] = rank;
102
+ }
103
+
104
+ i = j;
105
+ }
106
+
107
+ return ranks;
108
+ }
109
+
110
+ export function robustScale(data, center = 'median', scale = 'iqr') {
111
+ if (!Array.isArray(data) || data.length === 0) {
112
+ throw new TypeError('data must be non-empty array');
113
+ }
114
+
115
+ let centerVal;
116
+ if (center === 'median') {
117
+ const sorted = [...data].sort((a, b) => a - b);
118
+ centerVal = sorted.length % 2 === 0
119
+ ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
120
+ : sorted[Math.floor(sorted.length / 2)];
121
+ } else if (center === 'mean') {
122
+ centerVal = data.reduce((a, b) => a + b, 0) / data.length;
123
+ } else {
124
+ throw new RangeError('center must be "median" or "mean"');
125
+ }
126
+
127
+ let scaleVal;
128
+ if (scale === 'iqr') {
129
+ const sorted = [...data].sort((a, b) => a - b);
130
+ const q1 = sorted[Math.floor(sorted.length * 0.25)];
131
+ const q3 = sorted[Math.floor(sorted.length * 0.75)];
132
+ scaleVal = q3 - q1;
133
+ } else if (scale === 'mad') {
134
+ const centered = data.map(x => Math.abs(x - centerVal));
135
+ const sorted = [...centered].sort((a, b) => a - b);
136
+ scaleVal = sorted[Math.floor(sorted.length / 2)];
137
+ } else {
138
+ throw new RangeError('scale must be "iqr" or "mad"');
139
+ }
140
+
141
+ if (scaleVal === 0) {
142
+ throw new RangeError('scale value is zero');
143
+ }
144
+
145
+ return data.map(x => (x - centerVal) / scaleVal);
146
+ }