diff --git a/README.md b/README.md index 9512711e57..20c7f37edf 100644 --- a/README.md +++ b/README.md @@ -1258,6 +1258,8 @@ The following window reducers are supported: * *difference* - the difference between the last and first window value * *ratio* - the ratio of the last and first window value +By default, **shift** is *centered* and **reduce** is *mean*. + #### Plot.map(*outputs*, *options*) ```js @@ -1282,37 +1284,37 @@ Plot.mapY("cumsum", {y: d3.randomNormal()}) Equivalent to Plot.map({y: *map*, y1: *map*, y2: *map*}, *options*), but ignores any of **y**, **y1**, and **y2** not present in *options*. -#### Plot.normalizeX(*options*) +#### Plot.normalizeX(*basis*, *options*) ```js -Plot.normalizeX({y: "Date", x: "Close", stroke: "Symbol"}) +Plot.normalizeX("first", {y: "Date", x: "Close", stroke: "Symbol"}) ``` -Like [Plot.mapX](#plotmapxmap-options), but applies the normalize map method with the given *options*. +Like [Plot.mapX](#plotmapxmap-options), but applies the normalize map method with the given *basis*. -#### Plot.normalizeY(*options*) +#### Plot.normalizeY(*basis*, *options*) ```js -Plot.normalizeY({x: "Date", y: "Close", stroke: "Symbol"}) +Plot.normalizeY("first", {x: "Date", y: "Close", stroke: "Symbol"}) ``` -Like [Plot.mapY](#plotmapymap-options), but applies the normalize map method with the given *options*. +Like [Plot.mapY](#plotmapymap-options), but applies the normalize map method with the given *basis*. -#### Plot.windowX(*options*) +#### Plot.windowX(*k*, *options*) ```js -Plot.windowX({y: "Date", x: "Anomaly", k: 24}) +Plot.windowX(24, {y: "Date", x: "Anomaly"}) ``` -Like [Plot.mapX](#plotmapxmap-options), but applies the window map method with the given *options*. +Like [Plot.mapX](#plotmapxmap-options), but applies the window map method with the given window size *k*. For additional options to the window transform, replace the number *k* with an object with properties *k*, *shift*, or *reduce*. -#### Plot.windowY(*options*) +#### Plot.windowY(*k*, *options*) ```js -Plot.windowY({x: "Date", y: "Anomaly", k: 24}) +Plot.windowY(24, {x: "Date", y: "Anomaly"}) ``` -Like [Plot.mapY](#plotmapymap-options), but applies the window map method with the given *options*. +Like [Plot.mapY](#plotmapymap-options), but applies the window map method with the given window size *k*. For additional options to the window transform, replace the number *k* with an object with properties *k*, *shift*, or *reduce*. ### Select @@ -1384,6 +1386,8 @@ If a given stack has zero total value, the *expand* offset will not adjust the s In addition to the **y1** and **y2** output channels, Plot.stackY computers a **y** output channel that represents the midpoint of **y1** and **y2**. Plot.stackX does the same for **x**. This can be used to position a label or a dot in the center of a stacked layer. The **x** and **y** output channels are lazy: they are only computed if needed by a downstream mark or transform. +If two arguments are passed to the stack transform functions below, the stack-specific options (**offset**, **order**, and **reverse**) are pulled exclusively from the first *options* argument, while any channels (*e.g.*, **x**, **y**, and **z**) are pulled from second *options* argument. Options from the second argument that are not consumed by the stack transform will be passed through. Using two arguments is sometimes necessary is disambiguate the option recipient when chaining transforms. + #### Plot.stackY(*options*) ```js diff --git a/src/transforms/normalize.js b/src/transforms/normalize.js index 222125520e..f55460b132 100644 --- a/src/transforms/normalize.js +++ b/src/transforms/normalize.js @@ -3,11 +3,13 @@ import {defined} from "../defined.js"; import {take} from "../mark.js"; import {mapX, mapY} from "./map.js"; -export function normalizeX({basis, ...options} = {}) { +export function normalizeX(basis, options) { + if (arguments.length === 1) ({basis, ...options} = basis); return mapX(normalize(basis), options); } -export function normalizeY({basis, ...options} = {}) { +export function normalizeY(basis, options) { + if (arguments.length === 1) ({basis, ...options} = basis); return mapY(normalize(basis), options); } diff --git a/src/transforms/stack.js b/src/transforms/stack.js index b7c1fce8a0..2207357171 100644 --- a/src/transforms/stack.js +++ b/src/transforms/stack.js @@ -3,34 +3,46 @@ import {ascendingDefined} from "../defined.js"; import {field, lazyChannel, maybeLazyChannel, maybeZ, mid, range, valueof, identity, maybeZero} from "../mark.js"; import {basic} from "./basic.js"; -export function stackX({y1, y = y1, x, ...options} = {}) { - const [transform, Y, x1, x2] = stack(y, x, "x", options); - return {y1, y: Y, x1, x2, x: mid(x1, x2), ...transform}; +export function stackX(stackOptions = {}, options = {}) { + if (arguments.length === 1) options = mergeOptions(stackOptions); + const {y1, y = y1, x, ...rest} = options; // note: consumes x! + const [transform, Y, x1, x2] = stack(y, x, "x", stackOptions, rest); + return {...transform, y1, y: Y, x1, x2, x: mid(x1, x2)}; } -export function stackX1({y1, y = y1, x, ...options} = {}) { - const [transform, Y, X] = stack(y, x, "x", options); - return {y1, y: Y, x: X, ...transform}; +export function stackX1(stackOptions = {}, options = {}) { + if (arguments.length === 1) options = mergeOptions(stackOptions); + const {y1, y = y1, x} = options; + const [transform, Y, X] = stack(y, x, "x", stackOptions, options); + return {...transform, y1, y: Y, x: X}; } -export function stackX2({y1, y = y1, x, ...options} = {}) { - const [transform, Y,, X] = stack(y, x, "x", options); - return {y1, y: Y, x: X, ...transform}; +export function stackX2(stackOptions = {}, options = {}) { + if (arguments.length === 1) options = mergeOptions(stackOptions); + const {y1, y = y1, x} = options; + const [transform, Y,, X] = stack(y, x, "x", stackOptions, options); + return {...transform, y1, y: Y, x: X}; } -export function stackY({x1, x = x1, y, ...options} = {}) { - const [transform, X, y1, y2] = stack(x, y, "y", options); - return {x1, x: X, y1, y2, y: mid(y1, y2), ...transform}; +export function stackY(stackOptions = {}, options = {}) { + if (arguments.length === 1) options = mergeOptions(stackOptions); + const {x1, x = x1, y, ...rest} = options; // note: consumes y! + const [transform, X, y1, y2] = stack(x, y, "y", stackOptions, rest); + return {...transform, x1, x: X, y1, y2, y: mid(y1, y2)}; } -export function stackY1({x1, x = x1, y, ...options} = {}) { - const [transform, X, Y] = stack(x, y, "y", options); - return {x1, x: X, y: Y, ...transform}; +export function stackY1(stackOptions = {}, options = {}) { + if (arguments.length === 1) options = mergeOptions(stackOptions); + const {x1, x = x1, y} = options; + const [transform, X, Y] = stack(x, y, "y", stackOptions, options); + return {...transform, x1, x: X, y: Y}; } -export function stackY2({x1, x = x1, y, ...options} = {}) { - const [transform, X,, Y] = stack(x, y, "y", options); - return {x1, x: X, y: Y, ...transform}; +export function stackY2(stackOptions = {}, options = {}) { + if (arguments.length === 1) options = mergeOptions(stackOptions); + const {x1, x = x1, y} = options; + const [transform, X,, Y] = stack(x, y, "y", stackOptions, options); + return {...transform, x1, x: X, y: Y}; } export function maybeStackX({x, x1, x2, ...options} = {}) { @@ -51,7 +63,15 @@ export function maybeStackY({y, y1, y2, ...options} = {}) { return {...options, y1, y2}; } -function stack(x, y = () => 1, ky, {offset, order, reverse, ...options} = {}) { +// The reverse option is ambiguous: it is both a stack option and a basic +// transform. If only one options object is specified, we interpret it as a +// stack option, and therefore must remove it from the propagated options. +function mergeOptions(options) { + const {reverse} = options; + return reverse ? {...options, reverse: false} : options; +} + +function stack(x, y = () => 1, ky, {offset, order, reverse}, options) { const z = maybeZ(options); const [X, setX] = maybeLazyChannel(x); const [Y1, setY1] = lazyChannel(y); diff --git a/src/transforms/window.js b/src/transforms/window.js index 1ca59f1e24..c790d578a8 100644 --- a/src/transforms/window.js +++ b/src/transforms/window.js @@ -1,15 +1,19 @@ import {mapX, mapY} from "./map.js"; import {deviation, max, min, median, variance} from "d3"; -export function windowX({k, reduce, shift, ...options} = {}) { - return mapX(window(k, reduce, shift), options); +export function windowX(windowOptions = {}, options) { + if (arguments.length === 1) options = windowOptions; + return mapX(window(windowOptions), options); } -export function windowY({k, reduce, shift, ...options} = {}) { - return mapY(window(k, reduce, shift), options); +export function windowY(windowOptions = {}, options) { + if (arguments.length === 1) options = windowOptions; + return mapY(window(windowOptions), options); } -function window(k, reduce, shift) { +function window(options = {}) { + if (typeof options === "number") options = {k: options}; + let {k, reduce, shift} = options; if (!((k = Math.floor(k)) > 0)) throw new Error("invalid k"); return maybeReduce(reduce)(k, maybeShift(shift, k)); } diff --git a/test/plots/cars-parcoords.js b/test/plots/cars-parcoords.js index 48f19fc3a6..1288efcc72 100644 --- a/test/plots/cars-parcoords.js +++ b/test/plots/cars-parcoords.js @@ -20,7 +20,7 @@ export default async function() { }); // Normalize the x-position based on the extent for each dimension. - const xy = Plot.normalizeX({basis: "extent", x: "value", y: "dimension", z: "dimension"}); + const xy = Plot.normalizeX("extent", {x: "value", y: "dimension", z: "dimension"}); return Plot.plot({ marginLeft: 100, diff --git a/test/plots/gistemp-anomaly-moving.js b/test/plots/gistemp-anomaly-moving.js index 3860b299ef..e38614ccd5 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({x: "Date", y: "Anomaly", k: 24})) + Plot.line(data, Plot.windowY(24, {x: "Date", y: "Anomaly"})) ] }); } diff --git a/test/plots/metro-unemployment-moving.js b/test/plots/metro-unemployment-moving.js index 40c716e3b2..5a56fea716 100644 --- a/test/plots/metro-unemployment-moving.js +++ b/test/plots/metro-unemployment-moving.js @@ -5,7 +5,7 @@ export default async function() { const data = await d3.csv("data/bls-metro-unemployment.csv", d3.autoType); return Plot.plot({ marks: [ - Plot.line(data, Plot.windowY({x: "date", y: "unemployment", z: "division", k: 12})), + Plot.line(data, Plot.windowY(12, {x: "date", y: "unemployment", z: "division"})), Plot.ruleY([0]) ] }); diff --git a/test/plots/us-population-state-age-dots.js b/test/plots/us-population-state-age-dots.js index 33e5c5d160..cc859c1f03 100644 --- a/test/plots/us-population-state-age-dots.js +++ b/test/plots/us-population-state-age-dots.js @@ -5,7 +5,7 @@ export default async function() { const states = await d3.csv("data/us-population-state-age.csv", d3.autoType); const ages = states.columns.slice(1); const stateage = ages.flatMap(age => states.map(d => ({state: d.name, age, population: d[age]}))); - const position = Plot.normalizeX({basis: "sum", z: "state", x: "population", y: "state"}); + const position = Plot.normalizeX("sum", {z: "state", x: "population", y: "state"}); return Plot.plot({ height: 660, grid: true, diff --git a/test/plots/us-population-state-age.js b/test/plots/us-population-state-age.js index 591ab99703..b5e0988754 100644 --- a/test/plots/us-population-state-age.js +++ b/test/plots/us-population-state-age.js @@ -19,7 +19,7 @@ export default async function() { }, marks: [ Plot.ruleX([0]), - Plot.tickX(stateage, Plot.normalizeX({basis: "sum", z: "state", x: "population", y: "age"})) + Plot.tickX(stateage, Plot.normalizeX("sum", {z: "state", x: "population", y: "age"})) ] }); }