Skip to content

non-strict window by default #993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1890,9 +1890,9 @@ The Plot.windowX and Plot.windowY transforms compute a moving window around each
* **k** - the window size (the number of elements in the window)
* **anchor** - how to align the window: *start*, *middle*, or *end*
* **reduce** - the aggregation method (window reducer)
* **extend** - whether to extend output values by truncating the window; defaults to false
* **strict** - if true, disallow window truncation; defaults to false

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.
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.

The following window reducers are supported:

Expand Down
14 changes: 9 additions & 5 deletions src/transforms/window.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ export function windowY(windowOptions = {}, options) {

export function window(options = {}) {
if (typeof options === "number") options = {k: options};
let {k, reduce, shift, anchor, extend} = options;
let {k, reduce, shift, anchor, strict} = options;
if (anchor === undefined && shift !== undefined) {
anchor = maybeShift(shift);
warn(`Warning: the shift option is deprecated; please use anchor "${anchor}" instead.`);
}
if (!((k = Math.floor(k)) > 0)) throw new Error(`invalid k: ${k}`);
const r = maybeReduce(reduce);
const s = maybeAnchor(anchor, k);
return (extend ? extendReducer(r) : r)(k, s);
return (strict ? r : looseReducer(r))(k, s);
}

function maybeAnchor(anchor = "middle", k) {
Expand Down Expand Up @@ -66,7 +66,7 @@ function maybeReduce(reduce = "mean") {
return reduceSubarray(reduce);
}

function extendReducer(reducer) {
function looseReducer(reducer) {
return (k, s) => {
const reduce = reducer(k, s);
return {
Expand All @@ -75,17 +75,21 @@ function extendReducer(reducer) {
reduce.map(I, S, T);
for (let i = 0; i < s; ++i) {
const j = Math.min(n, i + k - s);
reducer(j, i).map(I.subarray(0, j), S, T);
reducer(j, i).map(slice(I, 0, j), S, T);
}
for (let i = n - k + s + 1; i < n; ++i) {
const j = Math.max(0, i - s);
reducer(n - j, i - j).map(I.subarray(j, n), S, T);
reducer(n - j, i - j).map(slice(I, j, n), S, T);
}
}
};
};
}

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

function reduceSubarray(f) {
return (k, s) => ({
map(I, S, T) {
Expand Down
90 changes: 45 additions & 45 deletions test/output/metroUnemploymentMoving.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion test/plots/aapl-bollinger.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ export default async function() {
}

function bollinger(N, K) {
return Plot.window({k: N, reduce: Y => d3.mean(Y) + K * d3.deviation(Y), anchor: "end"});
return Plot.window({k: N, reduce: Y => d3.mean(Y) + K * d3.deviation(Y), strict: true, anchor: "end"});
}
2 changes: 1 addition & 1 deletion test/plots/gistemp-anomaly-moving.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default async function() {
marks: [
Plot.ruleY([0]),
Plot.dot(data, {x: "Date", y: "Anomaly", stroke: "Anomaly"}),
Plot.line(data, Plot.windowY({k: 24, extend: true}, {x: "Date", y: "Anomaly"}))
Plot.line(data, Plot.windowY({k: 24}, {x: "Date", y: "Anomaly"}))
]
});
}
4 changes: 2 additions & 2 deletions test/plots/seattle-precipitation-sum.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import * as d3 from "d3";

export default async function() {
const weather = (await d3.csv("data/seattle-weather.csv", d3.autoType)).slice(-28);
const y = Plot.window({k: 7, reduce: "sum", anchor: "end"});
const text = Plot.window({k: 7, reduce: V => Math.round(d3.sum(V)), anchor: "end"});
const y = Plot.window({k: 7, strict: true, reduce: "sum", anchor: "end"});
const text = Plot.window({k: 7, strict: true, reduce: V => Math.round(d3.sum(V)), anchor: "end"});
return Plot.plot({
marks: [
Plot.rectY(weather, Plot.map({y}, {x: "date", y: "precipitation", interval: d3.utcDay})),
Expand Down
4 changes: 2 additions & 2 deletions test/plots/sf-temperature-band-area.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export default async function() {
label: "↑ Daily temperature range (°F)"
},
marks: [
Plot.areaY(temperatures, Plot.windowY({k: 7, x: "date", y1: "low", y2: "high", curve: "step", fill: "#ccc"})),
Plot.line(temperatures, Plot.windowY({k: 7, x: "date", y: d => (d.low + d.high) / 2, curve: "step"}))
Plot.areaY(temperatures, Plot.windowY({k: 7, strict: true, x: "date", y1: "low", y2: "high", curve: "step", fill: "#ccc"})),
Plot.line(temperatures, Plot.windowY({k: 7, strict: true, x: "date", y: d => (d.low + d.high) / 2, curve: "step"}))
],
width: 960
});
Expand Down
4 changes: 2 additions & 2 deletions test/plots/sf-temperature-band.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export default async function() {
},
marks: [
Plot.areaY(temperatures, {x: "date", y1: "low", y2: "high", curve: "step", fill: "#ccc"}),
Plot.line(temperatures, Plot.windowY({x: "date", y: "low", k: 7, curve: "step", stroke: "blue"})),
Plot.line(temperatures, Plot.windowY({x: "date", y: "high", k: 7, curve: "step", stroke: "red"}))
Plot.line(temperatures, Plot.windowY({x: "date", y: "low", k: 7, strict: true, curve: "step", stroke: "blue"})),
Plot.line(temperatures, Plot.windowY({x: "date", y: "high", k: 7, strict: true, curve: "step", stroke: "red"}))
],
width: 960
});
Expand Down
2 changes: 1 addition & 1 deletion test/plots/travelers-covid-drop.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default async function() {
},
marks: [
Plot.lineY(travelers, {x: "date", y: d => d.current / d.previous - 1, strokeWidth: 0.25, curve: "step"}),
Plot.lineY(travelers, Plot.windowY({x: "date", y: d => d.current / d.previous - 1, k: 7, stroke: "steelblue"}))
Plot.lineY(travelers, Plot.windowY({x: "date", y: d => d.current / d.previous - 1, k: 7, strict: true, stroke: "steelblue"}))
]
});
}
104 changes: 52 additions & 52 deletions test/transforms/window-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,130 +28,130 @@ it(`windowX(k, options) is equivalent to windowX({k, anchor: "middle", reduce: "
assert.deepStrictEqual(m1.x.transform(), m2.x.transform());
});

it(`windowX(k, options) computes a moving average of window size k`, () => {
it(`windowX({k, strict: true}, options) computes a moving average of window size k`, () => {
const data = range(6);
const m1 = applyTransform(Plot.windowX(1, {x: d => d}), data);
const m1 = applyTransform(Plot.windowX({k: 1, strict: true}, {x: d => d}), data);
assert.deepStrictEqual(m1.x.transform(), [0, 1, 2, 3, 4, 5]);
const m2 = applyTransform(Plot.windowX(2, {x: d => d}), data);
const m2 = applyTransform(Plot.windowX({k: 2, strict: true}, {x: d => d}), data);
assert.deepStrictEqual(m2.x.transform(), [0.5, 1.5, 2.5, 3.5, 4.5,, ]);
const m3 = applyTransform(Plot.windowX(3, {x: d => d}), data);
const m3 = applyTransform(Plot.windowX({k: 3, strict: true}, {x: d => d}), data);
assert.deepStrictEqual(m3.x.transform(), [, 1, 2, 3, 4,, ]);
const m4 = applyTransform(Plot.windowX(4, {x: d => d}), data);
const m4 = applyTransform(Plot.windowX({k: 4, strict: true}, {x: d => d}), data);
assert.deepStrictEqual(m4.x.transform(), [, 1.5, 2.5, 3.5,,, ]);
});

it(`windowX({reduce: "mean"}) produces NaN if the current window contains NaN`, () => {
it(`windowX({k, strict: true}) produces NaN if the current window contains NaN`, () => {
const data = [1, 1, 1, null, 1, 1, 1, 1, 1, NaN, 1, 1, 1];
const m1 = applyTransform(Plot.windowX({reduce: "mean", k: 1, x: d => d}), data);
const m1 = applyTransform(Plot.windowX({k: 1, strict: true, x: d => d}), data);
assert.deepStrictEqual(m1.x.transform(), [1, 1, 1, NaN, 1, 1, 1, 1, 1, NaN, 1, 1, 1]);
const m2 = applyTransform(Plot.windowX({reduce: "mean", k: 2, x: d => d}), data);
const m2 = applyTransform(Plot.windowX({k: 2, strict: true, x: d => d}), data);
assert.deepStrictEqual(m2.x.transform(), [1, 1, NaN, NaN, 1, 1, 1, 1, NaN, NaN, 1, 1,, ]);
const m3 = applyTransform(Plot.windowX({reduce: "mean", k: 3, x: d => d}), data);
const m3 = applyTransform(Plot.windowX({k: 3, strict: true, x: d => d}), data);
assert.deepStrictEqual(m3.x.transform(), [, 1, NaN, NaN, NaN, 1, 1, 1, NaN, NaN, NaN, 1,, ]);
});

it(`windowX({reduce: "mean"}) treats null as NaN`, () => {
it(`windowX({k, strict: true}) treats null as NaN`, () => {
const data = [1, 1, 1, null, 1, 1, 1, 1, 1, null, 1, 1, 1];
const m3 = applyTransform(Plot.windowX({reduce: "mean", k: 3, x: d => d}), data);
const m3 = applyTransform(Plot.windowX({k: 3, strict: true, x: d => d}), data);
assert.deepStrictEqual(m3.x.transform(), [, 1, NaN, NaN, NaN, 1, 1, 1, NaN, NaN, NaN, 1,, ]);
});

it(`windowX({reduce: "mean", anchor}) respects the given anchor`, () => {
it(`windowX({k, strict: true, anchor}) respects the given anchor`, () => {
const data = [0, 1, 2, 3, 4, 5];
const mc = applyTransform(Plot.windowX({reduce: "mean", k: 3, anchor: "middle", x: d => d}), data);
const mc = applyTransform(Plot.windowX({k: 3, strict: true, anchor: "middle", x: d => d}), data);
assert.deepStrictEqual(mc.x.transform(), [, 1, 2, 3, 4,, ]);
const ml = applyTransform(Plot.windowX({reduce: "mean", k: 3, anchor: "start", x: d => d}), data);
const ml = applyTransform(Plot.windowX({k: 3, strict: true, anchor: "start", x: d => d}), data);
assert.deepStrictEqual(ml.x.transform(), [1, 2, 3, 4,,, ]);
const mt = applyTransform(Plot.windowX({reduce: "mean", k: 3, anchor: "end", x: d => d}), data);
const mt = applyTransform(Plot.windowX({k: 3, strict: true, anchor: "end", x: d => d}), data);
assert.deepStrictEqual(mt.x.transform(), [,, 1, 2, 3, 4]);
});

it(`windowX({reduce: "mean", k, extend: true}) truncates the window at the start and end`, () => {
it(`windowX(k) truncates the window at the start and end`, () => {
const data = range(6);
const m1 = applyTransform(Plot.windowX({k: 1, extend: true}, {x: d => d}), data);
const m1 = applyTransform(Plot.windowX(1, {x: d => d}), data);
assert.deepStrictEqual(m1.x.transform(), [0, 1, 2, 3, 4, 5]);
const m2 = applyTransform(Plot.windowX({k: 2, extend: true}, {x: d => d}), data);
const m2 = applyTransform(Plot.windowX(2, {x: d => d}), data);
assert.deepStrictEqual(m2.x.transform(), [0.5, 1.5, 2.5, 3.5, 4.5, 5]);
const m3 = applyTransform(Plot.windowX({k: 3, extend: true}, {x: d => d}), data);
const m3 = applyTransform(Plot.windowX(3, {x: d => d}), data);
assert.deepStrictEqual(m3.x.transform(), [0.5, 1, 2, 3, 4, 4.5]);
const m4 = applyTransform(Plot.windowX({k: 4, extend: true}, {x: d => d}), data);
const m4 = applyTransform(Plot.windowX(4, {x: d => d}), data);
assert.deepStrictEqual(m4.x.transform(), [1, 1.5, 2.5, 3.5, 4, 4.5]);
});

it(`windowX({reduce: "mean", k, extend: true, anchor: "start"}) truncates the window at the end`, () => {
it(`windowX({k, anchor: "start"}) truncates the window at the end`, () => {
const data = range(6);
const m1 = applyTransform(Plot.windowX({k: 1, extend: true, anchor: "start"}, {x: d => d}), data);
const m1 = applyTransform(Plot.windowX({k: 1, anchor: "start"}, {x: d => d}), data);
assert.deepStrictEqual(m1.x.transform(), [0, 1, 2, 3, 4, 5]);
const m2 = applyTransform(Plot.windowX({k: 2, extend: true, anchor: "start"}, {x: d => d}), data);
const m2 = applyTransform(Plot.windowX({k: 2, anchor: "start"}, {x: d => d}), data);
assert.deepStrictEqual(m2.x.transform(), [0.5, 1.5, 2.5, 3.5, 4.5, 5]);
const m3 = applyTransform(Plot.windowX({k: 3, extend: true, anchor: "start"}, {x: d => d}), data);
const m3 = applyTransform(Plot.windowX({k: 3, anchor: "start"}, {x: d => d}), data);
assert.deepStrictEqual(m3.x.transform(), [1, 2, 3, 4, 4.5, 5]);
const m4 = applyTransform(Plot.windowX({k: 4, extend: true, anchor: "start"}, {x: d => d}), data);
const m4 = applyTransform(Plot.windowX({k: 4, anchor: "start"}, {x: d => d}), data);
assert.deepStrictEqual(m4.x.transform(), [1.5, 2.5, 3.5, 4, 4.5, 5]);
});

it(`windowX({reduce: "mean", k, extend: true, anchor: "end"}) truncates the window at the start`, () => {
it(`windowX({k, anchor: "end"}) truncates the window at the start`, () => {
const data = range(6);
const m1 = applyTransform(Plot.windowX({k: 1, extend: true, anchor: "end"}, {x: d => d}), data);
const m1 = applyTransform(Plot.windowX({k: 1, anchor: "end"}, {x: d => d}), data);
assert.deepStrictEqual(m1.x.transform(), [0, 1, 2, 3, 4, 5]);
const m2 = applyTransform(Plot.windowX({k: 2, extend: true, anchor: "end"}, {x: d => d}), data);
const m2 = applyTransform(Plot.windowX({k: 2, anchor: "end"}, {x: d => d}), data);
assert.deepStrictEqual(m2.x.transform(), [0, 0.5, 1.5, 2.5, 3.5, 4.5]);
const m3 = applyTransform(Plot.windowX({k: 3, extend: true, anchor: "end"}, {x: d => d}), data);
const m3 = applyTransform(Plot.windowX({k: 3, anchor: "end"}, {x: d => d}), data);
assert.deepStrictEqual(m3.x.transform(), [0, 0.5, 1, 2, 3, 4]);
const m4 = applyTransform(Plot.windowX({k: 4, extend: true, anchor: "end"}, {x: d => d}), data);
const m4 = applyTransform(Plot.windowX({k: 4, anchor: "end"}, {x: d => d}), data);
assert.deepStrictEqual(m4.x.transform(), [0, 0.5, 1, 1.5, 2.5, 3.5]);
});

it(`windowX({reduce: "mean", k, extend: true}) handles k being bigger than the data size`, () => {
it(`windowX(k) handles k being bigger than the data size`, () => {
const data = range(6);
const m3 = applyTransform(Plot.windowX({k: 3, extend: true}, {x: d => d}), data);
const m3 = applyTransform(Plot.windowX(3, {x: d => d}), data);
assert.deepStrictEqual(m3.x.transform(), [0.5, 1, 2, 3, 4, 4.5]);
const m5 = applyTransform(Plot.windowX({k: 5, extend: true}, {x: d => d}), data);
const m5 = applyTransform(Plot.windowX(5, {x: d => d}), data);
assert.deepStrictEqual(m5.x.transform(), [1, 1.5, 2, 3, 3.5, 4]);
const m6 = applyTransform(Plot.windowX({k: 6, extend: true}, {x: d => d}), data);
const m6 = applyTransform(Plot.windowX(6, {x: d => d}), data);
assert.deepStrictEqual(m6.x.transform(), [1.5, 2, 2.5, 3, 3.5, 4]);
const m7 = applyTransform(Plot.windowX({k: 7, extend: true}, {x: d => d}), data);
const m7 = applyTransform(Plot.windowX(7, {x: d => d}), data);
assert.deepStrictEqual(m7.x.transform(), [1.5, 2, 2.5, 2.5, 3, 3.5]);
const m8 = applyTransform(Plot.windowX({k: 8, extend: true}, {x: d => d}), data);
const m8 = applyTransform(Plot.windowX(8, {x: d => d}), data);
assert.deepStrictEqual(m8.x.transform(), [2, 2.5, 2.5, 2.5, 3, 3.5]);
const m9 = applyTransform(Plot.windowX({k: 9, extend: true}, {x: d => d}), data);
const m9 = applyTransform(Plot.windowX(9, {x: d => d}), data);
assert.deepStrictEqual(m9.x.transform(), [2, 2.5, 2.5, 2.5, 2.5, 3]);
const m10 = applyTransform(Plot.windowX({k: 10, extend: true}, {x: d => d}), data);
const m10 = applyTransform(Plot.windowX(10, {x: d => d}), data);
assert.deepStrictEqual(m10.x.transform(), [2.5, 2.5, 2.5, 2.5, 2.5, 3]);
const m11 = applyTransform(Plot.windowX({k: 11, extend: true}, {x: d => d}), data);
const m11 = applyTransform(Plot.windowX(11, {x: d => d}), data);
assert.deepStrictEqual(m11.x.transform(), [2.5, 2.5, 2.5, 2.5, 2.5, 2.5]);
});

it(`windowX({reduce: "max", k}) computes a moving maximum of window size k`, () => {
it(`windowX({reduce: "max", k, strict: true}) computes a moving maximum of window size k`, () => {
const data = [0, 1, 2, 3, 4, 5];
const m1 = applyTransform(Plot.windowX({reduce: "max", k: 1, x: d => d}), data);
const m1 = applyTransform(Plot.windowX({reduce: "max", k: 1, strict: true, x: d => d}), data);
assert.deepStrictEqual(m1.x.transform(), [0, 1, 2, 3, 4, 5]);
const m2 = applyTransform(Plot.windowX({reduce: "max", k: 2, x: d => d}), data);
const m2 = applyTransform(Plot.windowX({reduce: "max", k: 2, strict: true, x: d => d}), data);
assert.deepStrictEqual(m2.x.transform(), [1, 2, 3, 4, 5,, ]);
const m3 = applyTransform(Plot.windowX({reduce: "max", k: 3, x: d => d}), data);
const m3 = applyTransform(Plot.windowX({reduce: "max", k: 3, strict: true, x: d => d}), data);
assert.deepStrictEqual(m3.x.transform(), [, 2, 3, 4, 5,, ]);
const m4 = applyTransform(Plot.windowX({reduce: "max", k: 4, x: d => d}), data);
const m4 = applyTransform(Plot.windowX({reduce: "max", k: 4, strict: true, x: d => d}), data);
assert.deepStrictEqual(m4.x.transform(), [, 3, 4, 5,,, ]);
});

it(`windowX({reduce: "max"}) produces NaN if the current window contains NaN`, () => {
it(`windowX({reduce: "max", k, strict: true}) produces NaN if the current window contains NaN`, () => {
const data = [1, 1, 1, NaN, 1, 1, 1, 1, 1, NaN, NaN, NaN, NaN, 1];
const m3 = applyTransform(Plot.windowX({reduce: "max", k: 3, x: d => d}), data);
const m3 = applyTransform(Plot.windowX({reduce: "max", k: 3, strict: true, x: d => d}), data);
assert.deepStrictEqual(m3.x.transform(), [, 1, NaN, NaN, NaN, 1, 1, 1, NaN, NaN, NaN, NaN, NaN,, ]);
});

it(`windowX({reduce: "max"}) treats null as NaN`, () => {
it(`windowX({reduce: "max", k, strict: true}) treats null as NaN`, () => {
const data = [1, 1, 1, null, 1, 1, 1, 1, 1, null, null, null, null, 1];
const m3 = applyTransform(Plot.windowX({reduce: "max", k: 3, x: d => d}), data);
const m3 = applyTransform(Plot.windowX({reduce: "max", k: 3, strict: true, x: d => d}), data);
assert.deepStrictEqual(m3.x.transform(), [, 1, NaN, NaN, NaN, 1, 1, 1, NaN, NaN, NaN, NaN, NaN,, ]);
});

it(`windowX({reduce: "max", anchor}) respects the given anchor`, () => {
it(`windowX({reduce: "max", k, strict: true, anchor}) respects the given anchor`, () => {
const data = [0, 1, 2, 3, 4, 5];
const mc = applyTransform(Plot.windowX({reduce: "max", k: 3, anchor: "middle", x: d => d}), data);
const mc = applyTransform(Plot.windowX({reduce: "max", k: 3, strict: true, anchor: "middle", x: d => d}), data);
assert.deepStrictEqual(mc.x.transform(), [, 2, 3, 4, 5,, ]);
const ml = applyTransform(Plot.windowX({reduce: "max", k: 3, anchor: "start", x: d => d}), data);
const ml = applyTransform(Plot.windowX({reduce: "max", k: 3, strict: true, anchor: "start", x: d => d}), data);
assert.deepStrictEqual(ml.x.transform(), [2, 3, 4, 5,,, ]);
const mt = applyTransform(Plot.windowX({reduce: "max", k: 3, anchor: "end", x: d => d}), data);
const mt = applyTransform(Plot.windowX({reduce: "max", k: 3, strict: true, anchor: "end", x: d => d}), data);
assert.deepStrictEqual(mt.x.transform(), [,, 2, 3, 4, 5]);
});