From 925a4f238d8375b09aad111d1d24d33a2e1a2344 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 4 Aug 2021 08:11:47 -0700 Subject: [PATCH 1/8] normalize shorthand basis --- src/transforms/normalize.js | 6 ++++-- test/plots/cars-parcoords.js | 2 +- test/plots/us-population-state-age-dots.js | 2 +- test/plots/us-population-state-age.js | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/transforms/normalize.js b/src/transforms/normalize.js index 222125520e..0a918eb815 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 !== undefined) ({basis, ...options} = basis); return mapX(normalize(basis), options); } -export function normalizeY({basis, ...options} = {}) { +export function normalizeY(basis, options) { + if (arguments.length === 1 && basis !== undefined) ({basis, ...options} = basis); return mapY(normalize(basis), options); } 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/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"})) ] }); } From b3ee575d6b55d07898a1a621ee8548dbfc0ce184 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 10 Aug 2021 14:42:50 -0700 Subject: [PATCH 2/8] window shorthand --- src/transforms/window.js | 14 +++++++++----- test/plots/gistemp-anomaly-moving.js | 2 +- test/plots/metro-unemployment-moving.js | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) 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/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]) ] }); From 51ba14b05e7fa09825cde559cc526720a96453de Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 10 Aug 2021 15:07:39 -0700 Subject: [PATCH 3/8] stack shorthand --- src/transforms/stack.js | 58 +++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 19 deletions(-) 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); From addf7fef3bd4ffb5b42c78050d45faaa6cdb48d3 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 10 Aug 2021 15:24:59 -0700 Subject: [PATCH 4/8] update README --- README.md | 22 ++++++++++++---------- src/transforms/normalize.js | 4 ++-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9512711e57..0c6df8a8b3 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"}) ``` -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"}) ``` -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*. If *k* is an object, separate *shift* and *reduce* window options can also be specified. -#### 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*. If *k* is an object, separate *shift* and *reduce* window options can also be specified. ### Select diff --git a/src/transforms/normalize.js b/src/transforms/normalize.js index 0a918eb815..f55460b132 100644 --- a/src/transforms/normalize.js +++ b/src/transforms/normalize.js @@ -4,12 +4,12 @@ import {take} from "../mark.js"; import {mapX, mapY} from "./map.js"; export function normalizeX(basis, options) { - if (arguments.length === 1 && basis !== undefined) ({basis, ...options} = basis); + if (arguments.length === 1) ({basis, ...options} = basis); return mapX(normalize(basis), options); } export function normalizeY(basis, options) { - if (arguments.length === 1 && basis !== undefined) ({basis, ...options} = basis); + if (arguments.length === 1) ({basis, ...options} = basis); return mapY(normalize(basis), options); } From 0436d850aa41b95c88560b67d82ad09bc8e8e1ee Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 10 Aug 2021 16:28:39 -0700 Subject: [PATCH 5/8] update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0c6df8a8b3..414403467c 100644 --- a/README.md +++ b/README.md @@ -1287,7 +1287,7 @@ Equivalent to Plot.map({y: *map*, y1: *map*, y2: *map*}, *options*), but ignores #### 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 *basis*. @@ -1295,7 +1295,7 @@ Like [Plot.mapX](#plotmapxmap-options), but applies the normalize map method wit #### 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 *basis*. From 8e77ccbd710179320902d53803d34040478bebfd Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 10 Aug 2021 16:33:43 -0700 Subject: [PATCH 6/8] update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 414403467c..7a2877fd36 100644 --- a/README.md +++ b/README.md @@ -1386,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 From 2dd145d40afdef9f78d9e1301b44f61f9b9937af Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 10 Aug 2021 16:34:16 -0700 Subject: [PATCH 7/8] update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7a2877fd36..e69fa323d6 100644 --- a/README.md +++ b/README.md @@ -1386,7 +1386,7 @@ 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. +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*) From da23e0c0c01840ad04fdeb20b6459dc5326c1f76 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 11 Aug 2021 08:19:05 -0700 Subject: [PATCH 8/8] update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e69fa323d6..20c7f37edf 100644 --- a/README.md +++ b/README.md @@ -1306,7 +1306,7 @@ Like [Plot.mapY](#plotmapymap-options), but applies the normalize map method wit Plot.windowX(24, {y: "Date", x: "Anomaly"}) ``` -Like [Plot.mapX](#plotmapxmap-options), but applies the window map method with the given window size *k*. If *k* is an object, separate *shift* and *reduce* window options can also be specified. +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(*k*, *options*) @@ -1314,7 +1314,7 @@ Like [Plot.mapX](#plotmapxmap-options), but applies the window map method with t Plot.windowY(24, {x: "Date", y: "Anomaly"}) ``` -Like [Plot.mapY](#plotmapymap-options), but applies the window map method with the given window size *k*. If *k* is an object, separate *shift* and *reduce* window options can also be specified. +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