Skip to content

Expose Plot.channel and Plot.transform #411

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 11 commits into from
Mar 2, 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
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1346,7 +1346,7 @@ Plot.barY(alphabet.filter(d => /[aeiou]/i.test(d.letter)), {x: "letter", y: "fre

Together the **sort** and **reverse** transforms allow control over *z*-order, which can be important when addressing overplotting. If the sort option is a function but does not take exactly one argument, it is assumed to be a [comparator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#description); otherwise, the sort option is interpreted as a channel value definition and thus may be either as a column name, accessor function, or array of values.

For greater control, you can also implement a custom transform function:
For greater control, you can also implement a [custom transform function](#custom-transforms):

* **transform** - a function that returns transformed *data* and *index*

Expand Down Expand Up @@ -1919,6 +1919,24 @@ Plot.stackX2({y: "year", x: "revenue", z: "format", fill: "group"})

Equivalent to [Plot.stackX](#plotstackxstack-options), except that the **x2** channel is returned as the **x** channel. This can be used, for example, to draw a line at the right edge of each stacked area.

### Custom transforms

The **transform** option defines a custom transform function, allowing data, indexes, or channels to be derived prior to rendering. Custom transforms are rarely implemented directly; see the built-in transforms above. The transform function (if present) is passed two arguments, *data* and *facets*, representing the mark’s data and facet indexes; it must then return a {data, facets} object representing the resulting transformed data and facet indexes. The *facets* are represented as a nested array of arrays such as [[0, 1, 3, …], [2, 5, 10, …], …]; each element in *facets* specifies the zero-based indexes of elements in *data* that are in a given facet (*i.e.*, have a distinct value in the associated *fx* or *fy* dimension).

While transform functions often produce new *data* or *facets*, they may return the passed-in *data* and *facets* as-is, and often have a side-effect of constructing derived channels. For example, the count of elements in a [groupX transform](#group) might be returned as a new *y* channel. In this case, the transform is typically expressed as an options transform: a function that takes a mark options object and returns a new, transformed options object, where the returned options object implements a *transform* function option. Transform functions should not mutate the input *data* or *facets*. Likewise options transforms should not mutate the input *options* object.

Plot provides a few helpers for implementing transforms.

#### Plot.transform(*options*, *transform*)

Given an *options* object that may specify some basic transforms (*filter*, *sort*, or *reverse*) or a custom *transform* function, composes those transforms if any with the given *transform* function, returning a new *options* object. If a custom *transform* function is present on the given *options*, any basic transforms are ignored. Any additional input *options* are passed through in the returned *options* object. This method facilitates applying the basic transforms prior to applying the given custom *transform* and is used internally by Plot’s built-in transforms.

#### Plot.channel([*source*])

This helper for constructing derived channels returns a [*channel*, *setChannel*] array. The *channel* object implements *channel*.transform, returning whatever value was most recently passed to *setChannel*. If *setChannel* is not called, then *channel*.transform returns undefined. If a *source* is specified, then *channel*.label exposes the given *source*’s label, if any: if *source* is a string as when representing a named field of data, then *channel*.label is *source*; otherwise *channel*.label propagates *source*.label. This allows derived channels to propagate a human-readable axis or legend label.

Plot.channel is typically used by options transforms to define new channels; these channels are populated (derived) when the custom *transform* function is invoked.

## Curves

A curve defines how to turn a discrete representation of a line as a sequence of points [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] into a continuous path; *i.e.*, how to interpolate between points. Curves are used by the [line](#line), [area](#area), and [link](#link) mark, and are implemented by [d3-shape](https://github.com/d3/d3-shape/blob/master/README.md#curves).
Expand Down
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export {RuleX, RuleY, ruleX, ruleY} from "./marks/rule.js";
export {Text, text, textX, textY} from "./marks/text.js";
export {TickX, TickY, tickX, tickY} from "./marks/tick.js";
export {Vector, vector, vectorX, vectorY} from "./marks/vector.js";
export {valueof} from "./options.js";
export {filter, reverse, sort, shuffle} from "./transforms/basic.js";
export {valueof, channel} from "./options.js";
export {filter, reverse, sort, shuffle, basic as transform} from "./transforms/basic.js";
export {bin, binX, binY} from "./transforms/bin.js";
export {group, groupX, groupY, groupZ} from "./transforms/group.js";
export {normalize, normalizeX, normalizeY} from "./transforms/normalize.js";
Expand Down
12 changes: 6 additions & 6 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export function maybeInput(key, options) {
// Defines a channel whose values are lazily populated by calling the returned
// setter. If the given source is labeled, the label is propagated to the
// returned channel definition.
export function lazyChannel(source) {
export function channel(source) {
let value;
return [
{
Expand All @@ -167,17 +167,17 @@ export function lazyChannel(source) {
];
}

// Like channel, but allows the source to be null.
export function maybeChannel(source) {
return source == null ? [source] : channel(source);
}

export function labelof(value, defaultValue) {
return typeof value === "string" ? value
: value && value.label !== undefined ? value.label
: defaultValue;
}

// Like lazyChannel, but allows the source to be null.
export function maybeLazyChannel(source) {
return source == null ? [source] : lazyChannel(source);
}

// Assuming that both x1 and x2 and lazy channels (per above), this derives a
// new a channel that’s the average of the two, and which inherits the channel
// label (if any). Both input channels are assumed to be quantitative. If either
Expand Down
18 changes: 9 additions & 9 deletions src/transforms/bin.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {bin as binner, extent, thresholdFreedmanDiaconis, thresholdScott, thresholdSturges, utcTickInterval} from "d3";
import {valueof, range, identity, maybeLazyChannel, maybeTuple, maybeColorChannel, maybeValue, mid, labelof, isTemporal} from "../options.js";
import {valueof, range, identity, maybeChannel, maybeTuple, maybeColorChannel, maybeValue, mid, labelof, isTemporal} from "../options.js";
import {coerceDate, coerceNumber} from "../scales.js";
import {basic} from "./basic.js";
import {hasOutput, maybeEvaluator, maybeGroup, maybeOutput, maybeOutputs, maybeReduce, maybeSort, maybeSubgroup, reduceCount, reduceFirst, reduceIdentity} from "./group.js";
Expand Down Expand Up @@ -67,14 +67,14 @@ function binn(
if (gy != null && hasOutput(outputs, "y", "y1", "y2")) gy = null;

// Produce x1, x2, y1, and y2 output channels as appropriate (when binning).
const [BX1, setBX1] = maybeLazyChannel(bx);
const [BX2, setBX2] = maybeLazyChannel(bx);
const [BY1, setBY1] = maybeLazyChannel(by);
const [BY2, setBY2] = maybeLazyChannel(by);
const [BX1, setBX1] = maybeChannel(bx);
const [BX2, setBX2] = maybeChannel(bx);
const [BY1, setBY1] = maybeChannel(by);
const [BY2, setBY2] = maybeChannel(by);

// Produce x or y output channels as appropriate (when grouping).
const [k, gk] = gx != null ? [gx, "x"] : gy != null ? [gy, "y"] : [];
const [GK, setGK] = maybeLazyChannel(k);
const [GK, setGK] = maybeChannel(k);

// Greedily materialize the z, fill, and stroke channels (if channels and not
// constants) so that we can reference them for subdividing groups without
Expand All @@ -94,11 +94,11 @@ function binn(
interval, // eslint-disable-line no-unused-vars
...options
} = inputs;
const [GZ, setGZ] = maybeLazyChannel(z);
const [GZ, setGZ] = maybeChannel(z);
const [vfill] = maybeColorChannel(fill);
const [vstroke] = maybeColorChannel(stroke);
const [GF = fill, setGF] = maybeLazyChannel(vfill);
const [GS = stroke, setGS] = maybeLazyChannel(vstroke);
const [GF = fill, setGF] = maybeChannel(vfill);
const [GS = stroke, setGS] = maybeChannel(vstroke);

return {
..."z" in inputs && {z: GZ || z},
Expand Down
14 changes: 7 additions & 7 deletions src/transforms/group.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {group as grouper, sort, sum, deviation, min, max, mean, median, mode, variance, InternSet, minIndex, maxIndex, rollup} from "d3";
import {ascendingDefined, firstof} from "../defined.js";
import {valueof, maybeColorChannel, maybeInput, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take, labelof, range, second, percentile} from "../options.js";
import {valueof, maybeColorChannel, maybeInput, maybeTuple, maybeChannel, channel, first, identity, take, labelof, range, second, percentile} from "../options.js";
import {basic} from "./basic.js";

// Group on {z, fill, stroke}.
Expand Down Expand Up @@ -51,8 +51,8 @@ function groupn(
filter = filter == null ? undefined : maybeEvaluator("filter", filter, inputs);

// Produce x and y output channels as appropriate.
const [GX, setGX] = maybeLazyChannel(x);
const [GY, setGY] = maybeLazyChannel(y);
const [GX, setGX] = maybeChannel(x);
const [GY, setGY] = maybeChannel(y);

// Greedily materialize the z, fill, and stroke channels (if channels and not
// constants) so that we can reference them for subdividing groups without
Expand All @@ -65,11 +65,11 @@ function groupn(
y1, y2, // consumed if y is an output
...options
} = inputs;
const [GZ, setGZ] = maybeLazyChannel(z);
const [GZ, setGZ] = maybeChannel(z);
const [vfill] = maybeColorChannel(fill);
const [vstroke] = maybeColorChannel(stroke);
const [GF = fill, setGF] = maybeLazyChannel(vfill);
const [GS = stroke, setGS] = maybeLazyChannel(vstroke);
const [GF = fill, setGF] = maybeChannel(vfill);
const [GS = stroke, setGS] = maybeChannel(vstroke);

return {
..."z" in inputs && {z: GZ || z},
Expand Down Expand Up @@ -148,7 +148,7 @@ export function maybeOutputs(outputs, inputs) {

export function maybeOutput(name, reduce, inputs) {
const evaluator = maybeEvaluator(name, reduce, inputs);
const [output, setOutput] = lazyChannel(evaluator.label);
const [output, setOutput] = channel(evaluator.label);
let O;
return {
name,
Expand Down
4 changes: 2 additions & 2 deletions src/transforms/map.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {count, group, rank} from "d3";
import {maybeZ, take, valueof, maybeInput, lazyChannel} from "../options.js";
import {maybeZ, take, valueof, maybeInput, channel} from "../options.js";
import {basic} from "./basic.js";

export function mapX(m, options = {}) {
Expand All @@ -19,7 +19,7 @@ export function map(outputs = {}, options = {}) {
const channels = Object.entries(outputs).map(([key, map]) => {
const input = maybeInput(key, options);
if (input == null) throw new Error(`missing channel: ${key}`);
const [output, setOutput] = lazyChannel(input);
const [output, setOutput] = channel(input);
return {key, input, output, setOutput, map: maybeMap(map)};
});
return {
Expand Down
8 changes: 4 additions & 4 deletions src/transforms/stack.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {InternMap, cumsum, group, groupSort, greatest, max, min, rollup, sum} from "d3";
import {ascendingDefined} from "../defined.js";
import {field, lazyChannel, maybeLazyChannel, maybeZ, mid, range, valueof, maybeZero} from "../options.js";
import {field, channel, maybeChannel, maybeZ, mid, range, valueof, maybeZero} from "../options.js";
import {basic} from "./basic.js";

export function stackX(stackOptions = {}, options = {}) {
Expand Down Expand Up @@ -67,9 +67,9 @@ function mergeOptions(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);
const [Y2, setY2] = lazyChannel(y);
const [X, setX] = maybeChannel(x);
const [Y1, setY1] = channel(y);
const [Y2, setY2] = channel(y);
offset = maybeOffset(offset);
order = maybeOrder(order, offset, ky);
return [
Expand Down
Loading