From 8dab9dca2f11d197c5dea65ab0cf019294733a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 30 Aug 2021 18:28:55 +0200 Subject: [PATCH 01/11] expose Plot.transform and Plot.column --- README.md | 25 +- src/index.js | 4 +- test/output/diamondsCaratSampling.svg | 2079 +++++++++++++++++++++++++ test/plots/diamonds-carat-sampling.js | 30 + test/plots/index.js | 1 + 5 files changed, 2133 insertions(+), 6 deletions(-) create mode 100644 test/output/diamondsCaratSampling.svg create mode 100644 test/plots/diamonds-carat-sampling.js diff --git a/README.md b/README.md index c593cef76b..e366a3b4c9 100644 --- a/README.md +++ b/README.md @@ -1331,6 +1331,7 @@ Plot’s transforms provide a convenient mechanism for transforming data as part * **filter** - filters data according to the specified accessor or values * **sort** - sorts data according to the specified comparator, accessor, or values * **reverse** - reverses the sorted (or if not sorted, the input) data order +* **transform** - a function that returns transformed *data* and *facets* For example, to draw bars only for letters that commonly form vowels: @@ -1346,10 +1347,6 @@ 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: - -* **transform** - a function that returns transformed *data* and *index* - The basic transforms are composable: the *filter* transform is applied first, then *sort*, then *reverse*. If a custom *transform* option is specified directly, it supersedes any basic transforms (*i.e.*, the *filter*, *sort* and *reverse* options are ignored). However, the *transform* option is rarely used directly; instead an option transform is used. These option transforms automatically compose with the basic *filter*, *sort* and *reverse* transforms. Plot’s option transforms, listed below, do more than populate the **transform** function: they derive new mark options and channels. These transforms take a mark’s *options* object (and possibly transform-specific options as the first argument) and return a new, transformed, *options*. Option transforms are composable: you can pass an *options* objects through more than one transform before passing it to a mark. You can also reuse the same transformed *options* on multiple marks. @@ -1974,6 +1971,26 @@ If *marker* is true, it defaults to *circle*. If *marker* is a function, it will The primary color of a marker is inherited from the *stroke* of the associated mark. The *arrow* marker is [automatically oriented](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/orient) such that it points in the tangential direction of the path at the position the marker is placed. The *circle* markers are centered around the given vertex. Note that lines whose curve is not *linear* (the default), markers are not necessarily drawn at the data positions given by *x* and *y*; marker placement is determined by the (possibly Bézier) path segments generated by the curve. To ensure that symbols are drawn at a given *x* and *y* position, consider using a [dot](#dot). +### Custom transforms + +For greater control, you can also implement a custom transform function, by defining the following option: + +* **transform** - a function that returns transformed *data* and *facets* + +The arguments of the *transform* function are *data* and *facets*. The incoming *facets* is an array of facets, each facet being an array of indices into the incoming *data* array. The transform must return an object with two properties *{data, facets}* with the same structure. (The new data and facets can, of course, be different from the incoming values.) + +A custom transform might also generate new channels (for example, the count of elements in a groupX transform might be returned as a new channel *y*). The following helpers are useful to build new custom transforms: + +#### Plot.transform(*options*, *transform*) + +Returns an options object with a new transform that corresponds to the specified *transform* function composed with any preceding transform. + +#### Plot.column(*x*) + +Returns [*X*, *setX*], a channel and a setter. *X* can be returned immediately as a new channel, and *setX* can be called to fill *X* when the transform is applied to the data. The resulting channel *X* is an object with a *transform* method that returns the materialized column of values instantiated by *setX*. + +If *x* is a string or has a label, a label is automatically created on the resulting channel. + ## Formats These helper functions are provided for use as a *scale*.tickFormat [axis option](#position-options), as the text option for [Plot.text](#plottextdata-options), or for general use. See also [d3-format](https://github.com/d3/d3-format), [d3-time-format](https://github.com/d3/d3-time-format), and JavaScript’s built-in [date formatting](https://observablehq.com/@mbostock/date-formatting) and [number formatting](https://observablehq.com/@mbostock/number-formatting). diff --git a/src/index.js b/src/index.js index 7109b319f7..0f45ee7b69 100644 --- a/src/index.js +++ b/src/index.js @@ -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, lazyChannel as column} 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"; diff --git a/test/output/diamondsCaratSampling.svg b/test/output/diamondsCaratSampling.svg new file mode 100644 index 0000000000..a3356a7a4d --- /dev/null +++ b/test/output/diamondsCaratSampling.svg @@ -0,0 +1,2079 @@ + + + + + 2,000 + + + 4,000 + + + 6,000 + + + 8,000 + + + 10,000 + + + 12,000 + + + 14,000 + + + 16,000 + + + 18,000 + ↑ price + + + + 0.5 + + + 1.0 + + + 1.5 + + + 2.0 + + + 2.5 + + + 3.0 + + + 3.5 + + + 4.0 + + + 4.5 + + + 5.0 + carat → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/diamonds-carat-sampling.js b/test/plots/diamonds-carat-sampling.js new file mode 100644 index 0000000000..6c9d7ef911 --- /dev/null +++ b/test/plots/diamonds-carat-sampling.js @@ -0,0 +1,30 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + + +function samples(array, m) { + if (!((m = Math.floor(m)) > 0)) return []; // return nothing + const n = array.length; + if (!(n > m)) return [...array]; // return everything + if (m === 1) return [array[n >> 1]]; // return the midpoint + return Array.from({length: m}, (_, i) => array[Math.round(i / (m - 1) * (n - 1))]); +} + +function sample({samples: n = 10, ...options}) { + return Plot.transform(options, (data, facets) => ({data, facets: Array.from(facets, I => samples(I, n))})); +} + +export default async function() { + const data = await d3.csv("data/diamonds.csv", d3.autoType); + return Plot.plot({ + marks: [ + Plot.dot(data, sample({ + x: "carat", + y: "price", + r: 1, + fill: "currentColor", + samples: 2000 + })) + ] + }); +} diff --git a/test/plots/index.js b/test/plots/index.js index 794e78b433..8c147c5c04 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -42,6 +42,7 @@ export {default as d3Survey2015Why} from "./d3-survey-2015-why.js"; export {default as decathlon} from "./decathlon.js"; export {default as diamondsCaratPrice} from "./diamonds-carat-price.js"; export {default as diamondsCaratPriceDots} from "./diamonds-carat-price-dots.js"; +export {default as diamondsCaratSampling} from "./diamonds-carat-sampling.js"; export {default as documentationLinks} from "./documentation-links.js"; export {default as downloads} from "./downloads.js"; export {default as driving} from "./driving.js"; From 069e16e3474dd6355921a513d0e057c52b137220 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 2 Mar 2022 08:13:50 -0800 Subject: [PATCH 02/11] Update diamonds-carat-sampling.js --- test/plots/diamonds-carat-sampling.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/plots/diamonds-carat-sampling.js b/test/plots/diamonds-carat-sampling.js index 6c9d7ef911..ddb19f8983 100644 --- a/test/plots/diamonds-carat-sampling.js +++ b/test/plots/diamonds-carat-sampling.js @@ -1,7 +1,7 @@ import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; - +// https://observablehq.com/@mbostock/evenly-spaced-sampling function samples(array, m) { if (!((m = Math.floor(m)) > 0)) return []; // return nothing const n = array.length; @@ -10,7 +10,7 @@ function samples(array, m) { return Array.from({length: m}, (_, i) => array[Math.round(i / (m - 1) * (n - 1))]); } -function sample({samples: n = 10, ...options}) { +function sample(n, options) { return Plot.transform(options, (data, facets) => ({data, facets: Array.from(facets, I => samples(I, n))})); } @@ -18,12 +18,11 @@ export default async function() { const data = await d3.csv("data/diamonds.csv", d3.autoType); return Plot.plot({ marks: [ - Plot.dot(data, sample({ + Plot.dot(data, sample(2000, { x: "carat", y: "price", r: 1, - fill: "currentColor", - samples: 2000 + fill: "currentColor" })) ] }); From a84c29b4f73dc62c07438b32d48056d3a71942e7 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 2 Mar 2022 09:48:45 -0800 Subject: [PATCH 03/11] rename to Plot.channel --- README.md | 16 +++++++--------- src/index.js | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e366a3b4c9..82271d8795 100644 --- a/README.md +++ b/README.md @@ -1973,23 +1973,21 @@ The primary color of a marker is inherited from the *stroke* of the associated m ### Custom transforms -For greater control, you can also implement a custom transform function, by defining the following option: +For greater control, you can also implement a custom transform function by defining the **transform** function option. This 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). -* **transform** - a function that returns transformed *data* and *facets* - -The arguments of the *transform* function are *data* and *facets*. The incoming *facets* is an array of facets, each facet being an array of indices into the incoming *data* array. The transform must return an object with two properties *{data, facets}* with the same structure. (The new data and facets can, of course, be different from the incoming values.) +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. -A custom transform might also generate new channels (for example, the count of elements in a groupX transform might be returned as a new channel *y*). The following helpers are useful to build new custom transforms: +Plot provides a few helpers for implementing transforms. #### Plot.transform(*options*, *transform*) -Returns an options object with a new transform that corresponds to the specified *transform* function composed with any preceding transform. +Given an *options* object that may specify some basic transforms (*filter*, *sort*, or *reverse*), composes those basic transforms if any with the given *transform* function, returning a new *options* object. 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.column(*x*) +#### Plot.channel([*source*]) -Returns [*X*, *setX*], a channel and a setter. *X* can be returned immediately as a new channel, and *setX* can be called to fill *X* when the transform is applied to the data. The resulting channel *X* is an object with a *transform* method that returns the materialized column of values instantiated by *setX*. +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. -If *x* is a string or has a label, a label is automatically created on the resulting channel. +Plot.channel is typically used by options transforms to define new channels; these channels are populated (derived) when the custom *transform* function is invoked. ## Formats diff --git a/src/index.js b/src/index.js index 0f45ee7b69..3f6d4a7902 100644 --- a/src/index.js +++ b/src/index.js @@ -14,7 +14,7 @@ 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, lazyChannel as column} from "./options.js"; +export {valueof, lazyChannel as 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"; From dcbfde8c06db630d9f1025e1c7265e94e4fead48 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 2 Mar 2022 09:49:56 -0800 Subject: [PATCH 04/11] revert change to README --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 82271d8795..2af9b1a0d1 100644 --- a/README.md +++ b/README.md @@ -1331,7 +1331,6 @@ Plot’s transforms provide a convenient mechanism for transforming data as part * **filter** - filters data according to the specified accessor or values * **sort** - sorts data according to the specified comparator, accessor, or values * **reverse** - reverses the sorted (or if not sorted, the input) data order -* **transform** - a function that returns transformed *data* and *facets* For example, to draw bars only for letters that commonly form vowels: @@ -1347,6 +1346,10 @@ 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: + +* **transform** - a function that returns transformed *data* and *index* + The basic transforms are composable: the *filter* transform is applied first, then *sort*, then *reverse*. If a custom *transform* option is specified directly, it supersedes any basic transforms (*i.e.*, the *filter*, *sort* and *reverse* options are ignored). However, the *transform* option is rarely used directly; instead an option transform is used. These option transforms automatically compose with the basic *filter*, *sort* and *reverse* transforms. Plot’s option transforms, listed below, do more than populate the **transform** function: they derive new mark options and channels. These transforms take a mark’s *options* object (and possibly transform-specific options as the first argument) and return a new, transformed, *options*. Option transforms are composable: you can pass an *options* objects through more than one transform before passing it to a mark. You can also reuse the same transformed *options* on multiple marks. From f91537f52b2824a4317e8740f3ceacd040f8e420 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 2 Mar 2022 09:52:42 -0800 Subject: [PATCH 05/11] Update README --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2af9b1a0d1..fca1f7a594 100644 --- a/README.md +++ b/README.md @@ -1976,9 +1976,11 @@ The primary color of a marker is inherited from the *stroke* of the associated m ### Custom transforms -For greater control, you can also implement a custom transform function by defining the **transform** function option. This 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). +The **transform** option represents a custom transform function, allowing data, indexes, or channels to be derived prior to rendering. Custom transforms are rarely implemented directly; see [Plot’s built-in transforms](#transforms). -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. +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. From b46923574dc8aef4c187837b62f0b429f6816adf Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 2 Mar 2022 09:56:11 -0800 Subject: [PATCH 06/11] Update README --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index fca1f7a594..4b05d430b9 100644 --- a/README.md +++ b/README.md @@ -1919,6 +1919,26 @@ 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 represents 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 below. + +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*), composes those basic transforms if any with the given *transform* function, returning a new *options* object. 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). @@ -1974,26 +1994,6 @@ If *marker* is true, it defaults to *circle*. If *marker* is a function, it will The primary color of a marker is inherited from the *stroke* of the associated mark. The *arrow* marker is [automatically oriented](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/orient) such that it points in the tangential direction of the path at the position the marker is placed. The *circle* markers are centered around the given vertex. Note that lines whose curve is not *linear* (the default), markers are not necessarily drawn at the data positions given by *x* and *y*; marker placement is determined by the (possibly Bézier) path segments generated by the curve. To ensure that symbols are drawn at a given *x* and *y* position, consider using a [dot](#dot). -### Custom transforms - -The **transform** option represents a custom transform function, allowing data, indexes, or channels to be derived prior to rendering. Custom transforms are rarely implemented directly; see [Plot’s built-in transforms](#transforms). - -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*), composes those basic transforms if any with the given *transform* function, returning a new *options* object. 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. - ## Formats These helper functions are provided for use as a *scale*.tickFormat [axis option](#position-options), as the text option for [Plot.text](#plottextdata-options), or for general use. See also [d3-format](https://github.com/d3/d3-format), [d3-time-format](https://github.com/d3/d3-time-format), and JavaScript’s built-in [date formatting](https://observablehq.com/@mbostock/date-formatting) and [number formatting](https://observablehq.com/@mbostock/number-formatting). From d6285c961795c99da9ea27512479c68e7a1270c0 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 2 Mar 2022 09:56:57 -0800 Subject: [PATCH 07/11] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b05d430b9..a5a5ca3039 100644 --- a/README.md +++ b/README.md @@ -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* From 7105fa0ce9c10ee821fe39786ea4ffd98a355226 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 2 Mar 2022 09:57:54 -0800 Subject: [PATCH 08/11] Update README --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index a5a5ca3039..90bbfe3fc8 100644 --- a/README.md +++ b/README.md @@ -1921,9 +1921,7 @@ Equivalent to [Plot.stackX](#plotstackxstack-options), except that the **x2** ch ### Custom transforms -The **transform** option represents 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 below. - -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). +The **transform** option represents 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. From 71bef8e9f3ea209847c7601febf82ac5d21482c2 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 2 Mar 2022 09:58:10 -0800 Subject: [PATCH 09/11] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 90bbfe3fc8..9f18e5948d 100644 --- a/README.md +++ b/README.md @@ -1921,7 +1921,7 @@ Equivalent to [Plot.stackX](#plotstackxstack-options), except that the **x2** ch ### Custom transforms -The **transform** option represents 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). +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. From dd86f7287049915f0739d9823715dc807a7156ff Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 2 Mar 2022 10:00:39 -0800 Subject: [PATCH 10/11] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f18e5948d..7851a0d5a5 100644 --- a/README.md +++ b/README.md @@ -1929,7 +1929,7 @@ 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*), composes those basic transforms if any with the given *transform* function, returning a new *options* object. 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. +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*]) From 113b2aa3f8b0b5ee045d9d9815175fe4e4fd8852 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 2 Mar 2022 10:18:06 -0800 Subject: [PATCH 11/11] rename lazyChannel to channel --- src/index.js | 2 +- src/options.js | 12 ++++++------ src/transforms/bin.js | 18 +++++++++--------- src/transforms/group.js | 14 +++++++------- src/transforms/map.js | 4 ++-- src/transforms/stack.js | 8 ++++---- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/index.js b/src/index.js index 3f6d4a7902..f815c6ba22 100644 --- a/src/index.js +++ b/src/index.js @@ -14,7 +14,7 @@ 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, lazyChannel as channel} from "./options.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"; diff --git a/src/options.js b/src/options.js index 78be2825c4..0984f98938 100644 --- a/src/options.js +++ b/src/options.js @@ -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 [ { @@ -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 diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 7432eda4d3..40631dde6a 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -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"; @@ -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 @@ -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}, diff --git a/src/transforms/group.js b/src/transforms/group.js index 45f461a58e..63a142f700 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -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}. @@ -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 @@ -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}, @@ -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, diff --git a/src/transforms/map.js b/src/transforms/map.js index 5ecc89ed2b..ed7450445a 100644 --- a/src/transforms/map.js +++ b/src/transforms/map.js @@ -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 = {}) { @@ -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 { diff --git a/src/transforms/stack.js b/src/transforms/stack.js index cc2baab7a0..6f5ad1c080 100644 --- a/src/transforms/stack.js +++ b/src/transforms/stack.js @@ -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 = {}) { @@ -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 [