Skip to content

Explicit transform options #494

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 8 commits into from
Aug 11, 2021
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
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions src/transforms/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
58 changes: 39 additions & 19 deletions src/transforms/stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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} = {}) {
Expand All @@ -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);
Expand Down
14 changes: 9 additions & 5 deletions src/transforms/window.js
Original file line number Diff line number Diff line change
@@ -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));
}
Expand Down
2 changes: 1 addition & 1 deletion test/plots/cars-parcoords.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
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({x: "Date", y: "Anomaly", k: 24}))
Plot.line(data, Plot.windowY(24, {x: "Date", y: "Anomaly"}))
]
});
}
2 changes: 1 addition & 1 deletion test/plots/metro-unemployment-moving.js
Original file line number Diff line number Diff line change
Expand Up @@ -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])
]
});
Expand Down
2 changes: 1 addition & 1 deletion test/plots/us-population-state-age-dots.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion test/plots/us-population-state-age.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"}))
]
});
}