Skip to content

Commit 9d9ba91

Browse files
Filmbostock
andauthored
window skipNaN (#996)
* skipNaN * generalize non-strict reducers (#997) * generalize non-strict reducers * tweak * don’t coerce for mode * non-strict reducers * DRY * mode can be undefined, not NaN * fix first/last defined logic * don’t coerce for first and last * numbers cannot be null * shorter * fix comment Co-authored-by: Philippe Rivière <[email protected]> * Update README * Update README * don’t coerce for min and max Co-authored-by: Mike Bostock <[email protected]>
1 parent ddf3db9 commit 9d9ba91

File tree

6 files changed

+385
-154
lines changed

6 files changed

+385
-154
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1890,9 +1890,9 @@ The Plot.windowX and Plot.windowY transforms compute a moving window around each
18901890
* **k** - the window size (the number of elements in the window)
18911891
* **anchor** - how to align the window: *start*, *middle*, or *end*
18921892
* **reduce** - the aggregation method (window reducer)
1893-
* **strict** - if true, disallow window truncation; defaults to false
1893+
* **strict** - if true, output undefined if any window value is undefined; defaults to false
18941894
1895-
If the **strict** option is true, the resulting start values or end values or both (depending on the **anchor**) of each series may be undefined since there are not enough elements to create a window of size **k**. If the **strict** option is false (the default), the window will be automatically truncated as needed. For example, if **k** is 24 and **anchor** is *middle*, then the initial 11 values have effective window sizes of 13, 14, 15, … 23, and likewise the last 12 values have effective window sizes of 23, 22, 21, … 12. Values computed with a truncated window may be noiser; if you would prefer to not show this data, set the **strict** option to true.
1895+
If the **strict** option is true, the output start values or end values or both (depending on the **anchor**) of each series may be undefined since there are not enough elements to create a window of size **k**; output values may also be undefined if some of the input values in the corresponding window are undefined. If the **strict** option is false (the default), the window will be automatically truncated as needed, and undefined input values are ignored. For example, if **k** is 24 and **anchor** is *middle*, then the initial 11 values have effective window sizes of 13, 14, 15, … 23, and likewise the last 12 values have effective window sizes of 23, 22, 21, … 12. Values computed with a truncated window may be noisy; if you would prefer to not show this data, set the **strict** option to true.
18961896
18971897
The following window reducers are supported:
18981898

src/transforms/window.js

Lines changed: 161 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {mapX, mapY} from "./map.js";
21
import {deviation, max, min, median, mode, variance} from "d3";
2+
import {defined} from "../defined.js";
3+
import {percentile, take} from "../options.js";
34
import {warn} from "../warnings.js";
4-
import {percentile} from "../options.js";
5+
import {mapX, mapY} from "./map.js";
56

67
export function windowX(windowOptions = {}, options) {
78
if (arguments.length === 1) options = windowOptions;
@@ -21,9 +22,7 @@ export function window(options = {}) {
2122
warn(`Warning: the shift option is deprecated; please use anchor "${anchor}" instead.`);
2223
}
2324
if (!((k = Math.floor(k)) > 0)) throw new Error(`invalid k: ${k}`);
24-
const r = maybeReduce(reduce);
25-
const s = maybeAnchor(anchor, k);
26-
return (strict ? r : looseReducer(r))(k, s);
25+
return maybeReduce(reduce)(k, maybeAnchor(anchor, k), strict);
2726
}
2827

2928
function maybeAnchor(anchor = "middle", k) {
@@ -46,52 +45,36 @@ function maybeShift(shift) {
4645

4746
function maybeReduce(reduce = "mean") {
4847
if (typeof reduce === "string") {
49-
if (/^p\d{2}$/i.test(reduce)) return reduceSubarray(percentile(reduce));
48+
if (/^p\d{2}$/i.test(reduce)) return reduceNumbers(percentile(reduce));
5049
switch (reduce.toLowerCase()) {
51-
case "deviation": return reduceSubarray(deviation);
52-
case "max": return reduceSubarray(max);
50+
case "deviation": return reduceNumbers(deviation);
51+
case "max": return reduceArray(max);
5352
case "mean": return reduceMean;
54-
case "median": return reduceSubarray(median);
55-
case "min": return reduceSubarray(min);
56-
case "mode": return reduceSubarray(mode);
53+
case "median": return reduceNumbers(median);
54+
case "min": return reduceArray(min);
55+
case "mode": return reduceArray(mode);
5756
case "sum": return reduceSum;
58-
case "variance": return reduceSubarray(variance);
57+
case "variance": return reduceNumbers(variance);
5958
case "difference": return reduceDifference;
6059
case "ratio": return reduceRatio;
6160
case "first": return reduceFirst;
6261
case "last": return reduceLast;
6362
}
6463
}
6564
if (typeof reduce !== "function") throw new Error(`invalid reduce: ${reduce}`);
66-
return reduceSubarray(reduce);
67-
}
68-
69-
function looseReducer(reducer) {
70-
return (k, s) => {
71-
const reduce = reducer(k, s);
72-
return {
73-
map(I, S, T) {
74-
const n = I.length;
75-
reduce.map(I, S, T);
76-
for (let i = 0; i < s; ++i) {
77-
const j = Math.min(n, i + k - s);
78-
reducer(j, i).map(slice(I, 0, j), S, T);
79-
}
80-
for (let i = n - k + s + 1; i < n; ++i) {
81-
const j = Math.max(0, i - s);
82-
reducer(n - j, i - j).map(slice(I, j, n), S, T);
83-
}
84-
}
85-
};
86-
};
65+
return reduceArray(reduce);
8766
}
8867

8968
function slice(I, i, j) {
9069
return I.subarray ? I.subarray(i, j) : I.slice(i, j);
9170
}
9271

93-
function reduceSubarray(f) {
94-
return (k, s) => ({
72+
// Note that the subarray may include NaN in the non-strict case; we expect the
73+
// function f to handle that itself (e.g., by filtering as needed). The D3
74+
// reducers (e.g., min, max, mean, median) do, and it’s faster to avoid
75+
// redundant filtering.
76+
function reduceNumbers(f) {
77+
return (k, s, strict) => strict ? ({
9578
map(I, S, T) {
9679
const C = Float64Array.from(I, i => S[i] === null ? NaN : S[i]);
9780
let nans = 0;
@@ -102,11 +85,44 @@ function reduceSubarray(f) {
10285
if (isNaN(C[i])) --nans;
10386
}
10487
}
88+
}) : ({
89+
map(I, S, T) {
90+
const C = Float64Array.from(I, i => S[i] === null ? NaN : S[i]);
91+
for (let i = -s; i < 0; ++i) {
92+
T[I[i + s]] = f(C.subarray(0, i + k));
93+
}
94+
for (let i = 0, n = I.length - s; i < n; ++i) {
95+
T[I[i + s]] = f(C.subarray(i, i + k));
96+
}
97+
}
98+
});
99+
}
100+
101+
function reduceArray(f) {
102+
return (k, s, strict) => strict ? ({
103+
map(I, S, T) {
104+
let count = 0;
105+
for (let i = 0; i < k - 1; ++i) count += defined(S[I[i]]);
106+
for (let i = 0, n = I.length - k + 1; i < n; ++i) {
107+
count += defined(S[I[i + k - 1]]);
108+
if (count === k) T[I[i + s]] = f(take(S, slice(I, i, i + k)));
109+
count -= defined(S[I[i]]);
110+
}
111+
}
112+
}) : ({
113+
map(I, S, T) {
114+
for (let i = -s; i < 0; ++i) {
115+
T[I[i + s]] = f(take(S, slice(I, 0, i + k)));
116+
}
117+
for (let i = 0, n = I.length - s; i < n; ++i) {
118+
T[I[i + s]] = f(take(S, slice(I, i, i + k)));
119+
}
120+
}
105121
});
106122
}
107123

108-
function reduceSum(k, s) {
109-
return {
124+
function reduceSum(k, s, strict) {
125+
return strict ? ({
110126
map(I, S, T) {
111127
let nans = 0;
112128
let sum = 0;
@@ -125,61 +141,147 @@ function reduceSum(k, s) {
125141
else sum -= +a;
126142
}
127143
}
128-
};
129-
}
130-
131-
function reduceMean(k, s) {
132-
const sum = reduceSum(k, s);
133-
return {
144+
}) : ({
134145
map(I, S, T) {
135-
sum.map(I, S, T);
136-
for (let i = 0, n = I.length - k + 1; i < n; ++i) {
137-
T[I[i + s]] /= k;
146+
let sum = 0;
147+
const n = I.length;
148+
for (let i = 0, j = Math.min(n, k - s - 1); i < j; ++i) {
149+
sum += +S[I[i]] || 0;
150+
}
151+
for (let i = -s, j = n - s; i < j; ++i) {
152+
sum += +S[I[i + k - 1]] || 0;
153+
T[I[i + s]] = sum;
154+
sum -= +S[I[i]] || 0;
138155
}
139156
}
140-
};
157+
});
158+
}
159+
160+
function reduceMean(k, s, strict) {
161+
if (strict) {
162+
const sum = reduceSum(k, s, strict);
163+
return {
164+
map(I, S, T) {
165+
sum.map(I, S, T);
166+
for (let i = 0, n = I.length - k + 1; i < n; ++i) {
167+
T[I[i + s]] /= k;
168+
}
169+
}
170+
};
171+
} else {
172+
return {
173+
map(I, S, T) {
174+
let sum = 0;
175+
let count = 0;
176+
const n = I.length;
177+
for (let i = 0, j = Math.min(n, k - s - 1); i < j; ++i) {
178+
let v = S[I[i]];
179+
if (v !== null && !isNaN(v = +v)) sum += v, ++count;
180+
}
181+
for (let i = -s, j = n - s; i < j; ++i) {
182+
let a = S[I[i + k - 1]];
183+
let b = S[I[i]];
184+
if (a !== null && !isNaN(a = +a)) sum += a, ++count;
185+
T[I[i + s]] = sum / count;
186+
if (b !== null && !isNaN(b = +b)) sum -= b, --count;
187+
}
188+
}
189+
};
190+
}
191+
}
192+
193+
function firstDefined(S, I, i, k) {
194+
for (let j = i + k; i < j; ++i) {
195+
const v = S[I[i]];
196+
if (defined(v)) return v;
197+
}
198+
}
199+
200+
function lastDefined(S, I, i, k) {
201+
for (let j = i + k - 1; j >= i; --j) {
202+
const v = S[I[j]];
203+
if (defined(v)) return v;
204+
}
205+
}
206+
207+
function firstNumber(S, I, i, k) {
208+
for (let j = i + k; i < j; ++i) {
209+
let v = S[I[i]];
210+
if (v !== null && !isNaN(v = +v)) return v;
211+
}
141212
}
142213

143-
function reduceDifference(k, s) {
144-
return {
214+
function lastNumber(S, I, i, k) {
215+
for (let j = i + k - 1; j >= i; --j) {
216+
let v = S[I[j]];
217+
if (v !== null && !isNaN(v = +v)) return v;
218+
}
219+
}
220+
221+
function reduceDifference(k, s, strict) {
222+
return strict ? ({
145223
map(I, S, T) {
146224
for (let i = 0, n = I.length - k; i < n; ++i) {
147225
const a = S[I[i]];
148226
const b = S[I[i + k - 1]];
149227
T[I[i + s]] = a === null || b === null ? NaN : b - a;
150228
}
151229
}
152-
};
230+
}) : ({
231+
map(I, S, T) {
232+
for (let i = -s, n = I.length - k + s + 1; i < n; ++i) {
233+
T[I[i + s]] = lastNumber(S, I, i, k) - firstNumber(S, I, i, k);
234+
}
235+
}
236+
});
153237
}
154238

155-
function reduceRatio(k, s) {
156-
return {
239+
function reduceRatio(k, s, strict) {
240+
return strict ? ({
157241
map(I, S, T) {
158242
for (let i = 0, n = I.length - k; i < n; ++i) {
159243
const a = S[I[i]];
160244
const b = S[I[i + k - 1]];
161245
T[I[i + s]] = a === null || b === null ? NaN : b / a;
162246
}
163247
}
164-
};
248+
}) : ({
249+
map(I, S, T) {
250+
for (let i = -s, n = I.length - k + s + 1; i < n; ++i) {
251+
T[I[i + s]] = lastNumber(S, I, i, k) / firstNumber(S, I, i, k);
252+
}
253+
}
254+
});
165255
}
166256

167-
function reduceFirst(k, s) {
168-
return {
257+
function reduceFirst(k, s, strict) {
258+
return strict ? ({
169259
map(I, S, T) {
170260
for (let i = 0, n = I.length - k; i < n; ++i) {
171261
T[I[i + s]] = S[I[i]];
172262
}
173263
}
174-
};
264+
}) : ({
265+
map(I, S, T) {
266+
for (let i = -s, n = I.length - k + s + 1; i < n; ++i) {
267+
T[I[i + s]] = firstDefined(S, I, i, k);
268+
}
269+
}
270+
});
175271
}
176272

177-
function reduceLast(k, s) {
178-
return {
273+
function reduceLast(k, s, strict) {
274+
return strict ? ({
179275
map(I, S, T) {
180276
for (let i = 0, n = I.length - k; i < n; ++i) {
181277
T[I[i + s]] = S[I[i + k - 1]];
182278
}
183279
}
184-
};
280+
}) : ({
281+
map(I, S, T) {
282+
for (let i = -s, n = I.length - k + s + 1; i < n; ++i) {
283+
T[I[i + s]] = lastDefined(S, I, i, k);
284+
}
285+
}
286+
});
185287
}

test/output/gistempAnomalyMoving.svg

Lines changed: 1 addition & 1 deletion
Loading

0 commit comments

Comments
 (0)