Skip to content

Commit 3ef622f

Browse files
authored
extended window transform (#990)
* extended window transform * more window transform tests * Update README * handle k bigger than n
1 parent 420ad08 commit 3ef622f

File tree

7 files changed

+186
-100
lines changed

7 files changed

+186
-100
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1890,6 +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+
* **extend** - whether to extend output values by truncating the window; defaults to false
1894+
1895+
If the **extend** option is true, note that the resulting start values or end values or both (depending on the **anchor**) of each series may be noisy, as the window size will be truncated. 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. On the other hand, if the **extend** option is false, then some start values or end values will be undefined if **k** is greater than one.
18931896
18941897
The following window reducers are supported:
18951898

src/transforms/window.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ export function windowY(windowOptions = {}, options) {
1515

1616
export function window(options = {}) {
1717
if (typeof options === "number") options = {k: options};
18-
let {k, reduce, shift, anchor} = options;
18+
let {k, reduce, shift, anchor, extend} = options;
1919
if (anchor === undefined && shift !== undefined) {
2020
anchor = maybeShift(shift);
2121
warn(`Warning: the shift option is deprecated; please use anchor "${anchor}" instead.`);
2222
}
2323
if (!((k = Math.floor(k)) > 0)) throw new Error(`invalid k: ${k}`);
24-
return maybeReduce(reduce)(k, maybeAnchor(anchor, k));
24+
const r = maybeReduce(reduce);
25+
const s = maybeAnchor(anchor, k);
26+
return (extend ? extendReducer(r) : r)(k, s);
2527
}
2628

2729
function maybeAnchor(anchor = "middle", k) {
@@ -64,6 +66,26 @@ function maybeReduce(reduce = "mean") {
6466
return reduceSubarray(reduce);
6567
}
6668

69+
function extendReducer(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(I.subarray(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(I.subarray(j, n), S, T);
83+
}
84+
}
85+
};
86+
};
87+
}
88+
6789
function reduceSubarray(f) {
6890
return (k, s) => ({
6991
map(I, S, T) {

test/output/gistempAnomalyMoving.svg

Lines changed: 1 addition & 1 deletion
Loading

test/plots/gistemp-anomaly-moving.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default async function() {
1616
marks: [
1717
Plot.ruleY([0]),
1818
Plot.dot(data, {x: "Date", y: "Anomaly", stroke: "Anomaly"}),
19-
Plot.line(data, Plot.windowY(24, {x: "Date", y: "Anomaly"}))
19+
Plot.line(data, Plot.windowY({k: 24, extend: true}, {x: "Date", y: "Anomaly"}))
2020
]
2121
});
2222
}

test/transforms/window-test.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import * as Plot from "@observablehq/plot";
2+
import {range} from "d3";
3+
import assert from "assert";
4+
5+
function applyTransform(options, data) {
6+
options.transform(data, [Uint32Array.from(data, (_, i) => i)]);
7+
return options;
8+
}
9+
10+
it(`windowX(options) is equivalent to mapX(window(options), options)`, () => {
11+
const data = range(6);
12+
const m1 = applyTransform(Plot.windowX({k: 2, x: d => d}), data);
13+
const m2 = applyTransform(Plot.mapX(Plot.window({k: 2}), {x: d => d}), data);
14+
assert.deepStrictEqual(m1.x.transform(), m2.x.transform());
15+
});
16+
17+
it(`windowY(options) is equivalent to mapY(window(options), options)`, () => {
18+
const data = range(6);
19+
const m1 = applyTransform(Plot.windowY({k: 2, y: d => d}), data);
20+
const m2 = applyTransform(Plot.mapY(Plot.window({k: 2}), {y: d => d}), data);
21+
assert.deepStrictEqual(m1.y.transform(), m2.y.transform());
22+
});
23+
24+
it(`windowX(k, options) is equivalent to windowX({k, anchor: "middle", reduce: "mean", ...options})`, () => {
25+
const data = range(6);
26+
const m1 = applyTransform(Plot.windowX(2, {x: d => d}), data);
27+
const m2 = applyTransform(Plot.windowX({k: 2, anchor: "middle", reduce: "mean", x: d => d}), data);
28+
assert.deepStrictEqual(m1.x.transform(), m2.x.transform());
29+
});
30+
31+
it(`windowX(k, options) computes a moving average of window size k`, () => {
32+
const data = range(6);
33+
const m1 = applyTransform(Plot.windowX(1, {x: d => d}), data);
34+
assert.deepStrictEqual(m1.x.transform(), [0, 1, 2, 3, 4, 5]);
35+
const m2 = applyTransform(Plot.windowX(2, {x: d => d}), data);
36+
assert.deepStrictEqual(m2.x.transform(), [0.5, 1.5, 2.5, 3.5, 4.5,, ]);
37+
const m3 = applyTransform(Plot.windowX(3, {x: d => d}), data);
38+
assert.deepStrictEqual(m3.x.transform(), [, 1, 2, 3, 4,, ]);
39+
const m4 = applyTransform(Plot.windowX(4, {x: d => d}), data);
40+
assert.deepStrictEqual(m4.x.transform(), [, 1.5, 2.5, 3.5,,, ]);
41+
});
42+
43+
it(`windowX({reduce: "mean"}) produces NaN if the current window contains NaN`, () => {
44+
const data = [1, 1, 1, null, 1, 1, 1, 1, 1, NaN, 1, 1, 1];
45+
const m1 = applyTransform(Plot.windowX({reduce: "mean", k: 1, x: d => d}), data);
46+
assert.deepStrictEqual(m1.x.transform(), [1, 1, 1, NaN, 1, 1, 1, 1, 1, NaN, 1, 1, 1]);
47+
const m2 = applyTransform(Plot.windowX({reduce: "mean", k: 2, x: d => d}), data);
48+
assert.deepStrictEqual(m2.x.transform(), [1, 1, NaN, NaN, 1, 1, 1, 1, NaN, NaN, 1, 1,, ]);
49+
const m3 = applyTransform(Plot.windowX({reduce: "mean", k: 3, x: d => d}), data);
50+
assert.deepStrictEqual(m3.x.transform(), [, 1, NaN, NaN, NaN, 1, 1, 1, NaN, NaN, NaN, 1,, ]);
51+
});
52+
53+
it(`windowX({reduce: "mean"}) treats null as NaN`, () => {
54+
const data = [1, 1, 1, null, 1, 1, 1, 1, 1, null, 1, 1, 1];
55+
const m3 = applyTransform(Plot.windowX({reduce: "mean", k: 3, x: d => d}), data);
56+
assert.deepStrictEqual(m3.x.transform(), [, 1, NaN, NaN, NaN, 1, 1, 1, NaN, NaN, NaN, 1,, ]);
57+
});
58+
59+
it(`windowX({reduce: "mean", anchor}) respects the given anchor`, () => {
60+
const data = [0, 1, 2, 3, 4, 5];
61+
const mc = applyTransform(Plot.windowX({reduce: "mean", k: 3, anchor: "middle", x: d => d}), data);
62+
assert.deepStrictEqual(mc.x.transform(), [, 1, 2, 3, 4,, ]);
63+
const ml = applyTransform(Plot.windowX({reduce: "mean", k: 3, anchor: "start", x: d => d}), data);
64+
assert.deepStrictEqual(ml.x.transform(), [1, 2, 3, 4,,, ]);
65+
const mt = applyTransform(Plot.windowX({reduce: "mean", k: 3, anchor: "end", x: d => d}), data);
66+
assert.deepStrictEqual(mt.x.transform(), [,, 1, 2, 3, 4]);
67+
});
68+
69+
it(`windowX({reduce: "mean", k, extend: true}) truncates the window at the start and end`, () => {
70+
const data = range(6);
71+
const m1 = applyTransform(Plot.windowX({k: 1, extend: true}, {x: d => d}), data);
72+
assert.deepStrictEqual(m1.x.transform(), [0, 1, 2, 3, 4, 5]);
73+
const m2 = applyTransform(Plot.windowX({k: 2, extend: true}, {x: d => d}), data);
74+
assert.deepStrictEqual(m2.x.transform(), [0.5, 1.5, 2.5, 3.5, 4.5, 5]);
75+
const m3 = applyTransform(Plot.windowX({k: 3, extend: true}, {x: d => d}), data);
76+
assert.deepStrictEqual(m3.x.transform(), [0.5, 1, 2, 3, 4, 4.5]);
77+
const m4 = applyTransform(Plot.windowX({k: 4, extend: true}, {x: d => d}), data);
78+
assert.deepStrictEqual(m4.x.transform(), [1, 1.5, 2.5, 3.5, 4, 4.5]);
79+
});
80+
81+
it(`windowX({reduce: "mean", k, extend: true, anchor: "start"}) truncates the window at the end`, () => {
82+
const data = range(6);
83+
const m1 = applyTransform(Plot.windowX({k: 1, extend: true, anchor: "start"}, {x: d => d}), data);
84+
assert.deepStrictEqual(m1.x.transform(), [0, 1, 2, 3, 4, 5]);
85+
const m2 = applyTransform(Plot.windowX({k: 2, extend: true, anchor: "start"}, {x: d => d}), data);
86+
assert.deepStrictEqual(m2.x.transform(), [0.5, 1.5, 2.5, 3.5, 4.5, 5]);
87+
const m3 = applyTransform(Plot.windowX({k: 3, extend: true, anchor: "start"}, {x: d => d}), data);
88+
assert.deepStrictEqual(m3.x.transform(), [1, 2, 3, 4, 4.5, 5]);
89+
const m4 = applyTransform(Plot.windowX({k: 4, extend: true, anchor: "start"}, {x: d => d}), data);
90+
assert.deepStrictEqual(m4.x.transform(), [1.5, 2.5, 3.5, 4, 4.5, 5]);
91+
});
92+
93+
it(`windowX({reduce: "mean", k, extend: true, anchor: "end"}) truncates the window at the start`, () => {
94+
const data = range(6);
95+
const m1 = applyTransform(Plot.windowX({k: 1, extend: true, anchor: "end"}, {x: d => d}), data);
96+
assert.deepStrictEqual(m1.x.transform(), [0, 1, 2, 3, 4, 5]);
97+
const m2 = applyTransform(Plot.windowX({k: 2, extend: true, anchor: "end"}, {x: d => d}), data);
98+
assert.deepStrictEqual(m2.x.transform(), [0, 0.5, 1.5, 2.5, 3.5, 4.5]);
99+
const m3 = applyTransform(Plot.windowX({k: 3, extend: true, anchor: "end"}, {x: d => d}), data);
100+
assert.deepStrictEqual(m3.x.transform(), [0, 0.5, 1, 2, 3, 4]);
101+
const m4 = applyTransform(Plot.windowX({k: 4, extend: true, anchor: "end"}, {x: d => d}), data);
102+
assert.deepStrictEqual(m4.x.transform(), [0, 0.5, 1, 1.5, 2.5, 3.5]);
103+
});
104+
105+
it(`windowX({reduce: "mean", k, extend: true}) handles k being bigger than the data size`, () => {
106+
const data = range(6);
107+
const m3 = applyTransform(Plot.windowX({k: 3, extend: true}, {x: d => d}), data);
108+
assert.deepStrictEqual(m3.x.transform(), [0.5, 1, 2, 3, 4, 4.5]);
109+
const m5 = applyTransform(Plot.windowX({k: 5, extend: true}, {x: d => d}), data);
110+
assert.deepStrictEqual(m5.x.transform(), [1, 1.5, 2, 3, 3.5, 4]);
111+
const m6 = applyTransform(Plot.windowX({k: 6, extend: true}, {x: d => d}), data);
112+
assert.deepStrictEqual(m6.x.transform(), [1.5, 2, 2.5, 3, 3.5, 4]);
113+
const m7 = applyTransform(Plot.windowX({k: 7, extend: true}, {x: d => d}), data);
114+
assert.deepStrictEqual(m7.x.transform(), [1.5, 2, 2.5, 2.5, 3, 3.5]);
115+
const m8 = applyTransform(Plot.windowX({k: 8, extend: true}, {x: d => d}), data);
116+
assert.deepStrictEqual(m8.x.transform(), [2, 2.5, 2.5, 2.5, 3, 3.5]);
117+
const m9 = applyTransform(Plot.windowX({k: 9, extend: true}, {x: d => d}), data);
118+
assert.deepStrictEqual(m9.x.transform(), [2, 2.5, 2.5, 2.5, 2.5, 3]);
119+
const m10 = applyTransform(Plot.windowX({k: 10, extend: true}, {x: d => d}), data);
120+
assert.deepStrictEqual(m10.x.transform(), [2.5, 2.5, 2.5, 2.5, 2.5, 3]);
121+
const m11 = applyTransform(Plot.windowX({k: 11, extend: true}, {x: d => d}), data);
122+
assert.deepStrictEqual(m11.x.transform(), [2.5, 2.5, 2.5, 2.5, 2.5, 2.5]);
123+
});
124+
125+
it(`windowX({reduce: "max", k}) computes a moving maximum of window size k`, () => {
126+
const data = [0, 1, 2, 3, 4, 5];
127+
const m1 = applyTransform(Plot.windowX({reduce: "max", k: 1, x: d => d}), data);
128+
assert.deepStrictEqual(m1.x.transform(), [0, 1, 2, 3, 4, 5]);
129+
const m2 = applyTransform(Plot.windowX({reduce: "max", k: 2, x: d => d}), data);
130+
assert.deepStrictEqual(m2.x.transform(), [1, 2, 3, 4, 5,, ]);
131+
const m3 = applyTransform(Plot.windowX({reduce: "max", k: 3, x: d => d}), data);
132+
assert.deepStrictEqual(m3.x.transform(), [, 2, 3, 4, 5,, ]);
133+
const m4 = applyTransform(Plot.windowX({reduce: "max", k: 4, x: d => d}), data);
134+
assert.deepStrictEqual(m4.x.transform(), [, 3, 4, 5,,, ]);
135+
});
136+
137+
it(`windowX({reduce: "max"}) produces NaN if the current window contains NaN`, () => {
138+
const data = [1, 1, 1, NaN, 1, 1, 1, 1, 1, NaN, NaN, NaN, NaN, 1];
139+
const m3 = applyTransform(Plot.windowX({reduce: "max", k: 3, x: d => d}), data);
140+
assert.deepStrictEqual(m3.x.transform(), [, 1, NaN, NaN, NaN, 1, 1, 1, NaN, NaN, NaN, NaN, NaN,, ]);
141+
});
142+
143+
it(`windowX({reduce: "max"}) treats null as NaN`, () => {
144+
const data = [1, 1, 1, null, 1, 1, 1, 1, 1, null, null, null, null, 1];
145+
const m3 = applyTransform(Plot.windowX({reduce: "max", k: 3, x: d => d}), data);
146+
assert.deepStrictEqual(m3.x.transform(), [, 1, NaN, NaN, NaN, 1, 1, 1, NaN, NaN, NaN, NaN, NaN,, ]);
147+
});
148+
149+
it(`windowX({reduce: "max", anchor}) respects the given anchor`, () => {
150+
const data = [0, 1, 2, 3, 4, 5];
151+
const mc = applyTransform(Plot.windowX({reduce: "max", k: 3, anchor: "middle", x: d => d}), data);
152+
assert.deepStrictEqual(mc.x.transform(), [, 2, 3, 4, 5,, ]);
153+
const ml = applyTransform(Plot.windowX({reduce: "max", k: 3, anchor: "start", x: d => d}), data);
154+
assert.deepStrictEqual(ml.x.transform(), [2, 3, 4, 5,,, ]);
155+
const mt = applyTransform(Plot.windowX({reduce: "max", k: 3, anchor: "end", x: d => d}), data);
156+
assert.deepStrictEqual(mt.x.transform(), [,, 2, 3, 4, 5]);
157+
});

test/transforms/windowMax-test.js

Lines changed: 0 additions & 48 deletions
This file was deleted.

test/transforms/windowMean-test.js

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)