From 1b79f0d29436e37cd87540540152e9473adaa064 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 14 Jul 2022 12:57:56 -0700 Subject: [PATCH] non-strict window by default --- README.md | 4 +- src/transforms/window.js | 14 ++-- test/output/metroUnemploymentMoving.svg | 90 ++++++++++---------- test/plots/aapl-bollinger.js | 2 +- test/plots/gistemp-anomaly-moving.js | 2 +- test/plots/seattle-precipitation-sum.js | 4 +- test/plots/sf-temperature-band-area.js | 4 +- test/plots/sf-temperature-band.js | 4 +- test/plots/travelers-covid-drop.js | 2 +- test/transforms/window-test.js | 104 ++++++++++++------------ 10 files changed, 117 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 1dbf241cfb..0e817f17b7 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/src/transforms/window.js b/src/transforms/window.js index 3a4127646d..9eab504a8c 100644 --- a/src/transforms/window.js +++ b/src/transforms/window.js @@ -15,7 +15,7 @@ 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.`); @@ -23,7 +23,7 @@ export function window(options = {}) { 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) { @@ -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 { @@ -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) { diff --git a/test/output/metroUnemploymentMoving.svg b/test/output/metroUnemploymentMoving.svg index 392a4b6a3f..442f1706c0 100644 --- a/test/output/metroUnemploymentMoving.svg +++ b/test/output/metroUnemploymentMoving.svg @@ -66,51 +66,51 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/plots/aapl-bollinger.js b/test/plots/aapl-bollinger.js index ad5423a123..60aa9b838f 100644 --- a/test/plots/aapl-bollinger.js +++ b/test/plots/aapl-bollinger.js @@ -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"}); } diff --git a/test/plots/gistemp-anomaly-moving.js b/test/plots/gistemp-anomaly-moving.js index 1f3838bc9e..9f73329a43 100644 --- a/test/plots/gistemp-anomaly-moving.js +++ b/test/plots/gistemp-anomaly-moving.js @@ -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"})) ] }); } diff --git a/test/plots/seattle-precipitation-sum.js b/test/plots/seattle-precipitation-sum.js index ee7bbb6f91..2900a1f4cf 100644 --- a/test/plots/seattle-precipitation-sum.js +++ b/test/plots/seattle-precipitation-sum.js @@ -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})), diff --git a/test/plots/sf-temperature-band-area.js b/test/plots/sf-temperature-band-area.js index 8730d1f111..3c03a10f6c 100644 --- a/test/plots/sf-temperature-band-area.js +++ b/test/plots/sf-temperature-band-area.js @@ -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 }); diff --git a/test/plots/sf-temperature-band.js b/test/plots/sf-temperature-band.js index b20a1bdab0..996d7cc7c4 100644 --- a/test/plots/sf-temperature-band.js +++ b/test/plots/sf-temperature-band.js @@ -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 }); diff --git a/test/plots/travelers-covid-drop.js b/test/plots/travelers-covid-drop.js index 1568074109..162ac1402f 100644 --- a/test/plots/travelers-covid-drop.js +++ b/test/plots/travelers-covid-drop.js @@ -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"})) ] }); } diff --git a/test/transforms/window-test.js b/test/transforms/window-test.js index ede2a7a117..1fb8198076 100644 --- a/test/transforms/window-test.js +++ b/test/transforms/window-test.js @@ -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]); });