From d75a4d4ca04346593349425800264410db0cbaf0 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 1 Aug 2021 07:54:18 -0700 Subject: [PATCH 1/6] filter, sort, and reverse transforms --- src/index.js | 3 ++ src/mark.js | 61 ++------------------------------------- src/transforms/basic.js | 21 ++++++++++++++ src/transforms/bin.js | 5 ++-- src/transforms/compose.js | 10 +++++++ src/transforms/filter.js | 13 +++++++++ src/transforms/group.js | 5 ++-- src/transforms/map.js | 5 ++-- src/transforms/reverse.js | 9 ++++++ src/transforms/select.js | 5 ++-- src/transforms/sort.js | 26 +++++++++++++++++ src/transforms/stack.js | 5 ++-- 12 files changed, 100 insertions(+), 68 deletions(-) create mode 100644 src/transforms/basic.js create mode 100644 src/transforms/compose.js create mode 100644 src/transforms/filter.js create mode 100644 src/transforms/reverse.js create mode 100644 src/transforms/sort.js diff --git a/src/index.js b/src/index.js index 1583d4a352..9a4360df2c 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,9 @@ export {Rect, rect, rectX, rectY} from "./marks/rect.js"; 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 {filter} from "./transforms/filter.js"; +export {reverse} from "./transforms/reverse.js"; +export {sort} from "./transforms/sort.js"; export {bin, binX, binY} from "./transforms/bin.js"; export {group, groupX, groupY, groupZ} from "./transforms/group.js"; export {normalizeX, normalizeY} from "./transforms/normalize.js"; diff --git a/src/mark.js b/src/mark.js index 662749574a..45cd137353 100644 --- a/src/mark.js +++ b/src/mark.js @@ -1,7 +1,8 @@ import {color} from "d3"; -import {ascendingDefined, nonempty} from "./defined.js"; +import {nonempty} from "./defined.js"; import {plot} from "./plot.js"; import {styles} from "./style.js"; +import {basic} from "./transforms/basic.js"; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray const TypedArray = Object.getPrototypeOf(Uint8Array); @@ -13,7 +14,7 @@ export class Mark { const names = new Set(); this.data = data; this.facet = facet ? keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]) : null; - const {transform} = maybeTransform(options); + const {transform} = basic(options); this.transform = transform; if (defaults !== undefined) channels = styles(this, options, channels, defaults); this.channels = channels.filter(channel => { @@ -225,23 +226,6 @@ export function maybeLazyChannel(source) { return source == null ? [source] : lazyChannel(source); } -// If both t1 and t2 are defined, returns a composite transform that first -// applies t1 and then applies t2. -export function maybeTransform({ - filter: f1, - sort: s1, - reverse: r1, - transform: t1, - ...options -} = {}, t2) { - if (t1 === undefined) { - if (f1 != null) t1 = filter(f1); - if (s1 != null) t1 = compose(t1, sort(s1)); - if (r1) t1 = compose(t1, reverse); - } - return {...options, transform: compose(t1, t2)}; -} - // 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 @@ -266,45 +250,6 @@ export function maybeValue(value) { typeof value.transform !== "function") ? value : {value}; } -function compose(t1, t2) { - if (t1 == null) return t2 === null ? undefined : t2; - if (t2 == null) return t1 === null ? undefined : t1; - return (data, facets) => { - ({data, facets} = t1(data, facets)); - return t2(arrayify(data), facets); - }; -} - -function sort(value) { - return (typeof value === "function" && value.length !== 1 ? sortCompare : sortValue)(value); -} - -function sortCompare(compare) { - return (data, facets) => { - const compareData = (i, j) => compare(data[i], data[j]); - return {data, facets: facets.map(I => I.slice().sort(compareData))}; - }; -} - -function sortValue(value) { - return (data, facets) => { - const V = valueof(data, value); - const compareValue = (i, j) => ascendingDefined(V[i], V[j]); - return {data, facets: facets.map(I => I.slice().sort(compareValue))}; - }; -} - -function filter(value) { - return (data, facets) => { - const V = valueof(data, value); - return {data, facets: facets.map(I => I.filter(i => V[i]))}; - }; -} - -function reverse(data, facets) { - return {data, facets: facets.map(I => I.slice().reverse())}; -} - export function numberChannel(source) { return { transform: data => valueof(data, source, Float64Array), diff --git a/src/transforms/basic.js b/src/transforms/basic.js new file mode 100644 index 0000000000..213206422e --- /dev/null +++ b/src/transforms/basic.js @@ -0,0 +1,21 @@ +import {composeTransform} from "./compose.js"; +import {filterTransform} from "./filter.js"; +import {reverseTransform} from "./reverse.js"; +import {sortTransform} from "./sort.js"; + +// If both t1 and t2 are defined, returns a composite transform that first +// applies t1 and then applies t2. +export function basic({ + filter: f1, + sort: s1, + reverse: r1, + transform: t1, + ...options +} = {}, t2) { + if (t1 === undefined) { // explicit transform overrides filter, sort, and reverse + if (f1 != null) t1 = filterTransform(f1); + if (s1 != null) t1 = composeTransform(t1, sortTransform(s1)); + if (r1) t1 = composeTransform(t1, reverseTransform); + } + return {...options, transform: composeTransform(t1, t2)}; +} diff --git a/src/transforms/bin.js b/src/transforms/bin.js index b3e068cc3f..3b65b9d2f0 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -1,6 +1,7 @@ import {bin as binner, extent, thresholdFreedmanDiaconis, thresholdScott, thresholdSturges, utcTickInterval} from "d3"; -import {valueof, range, identity, maybeLazyChannel, maybeTransform, maybeTuple, maybeColor, maybeValue, mid, labelof, isTemporal} from "../mark.js"; +import {valueof, range, identity, maybeLazyChannel, maybeTuple, maybeColor, maybeValue, mid, labelof, isTemporal} from "../mark.js"; import {offset} from "../style.js"; +import {basic} from "./basic.js"; import {maybeGroup, maybeOutputs, maybeReduce, maybeSubgroup, reduceIdentity} from "./group.js"; // Group on {z, fill, stroke}, then optionally on y, then bin x. @@ -69,7 +70,7 @@ function binn( ..."z" in inputs && {z: GZ || z}, ..."fill" in inputs && {fill: GF || fill}, ..."stroke" in inputs && {stroke: GS || stroke}, - ...maybeTransform(options, (data, facets) => { + ...basic(options, (data, facets) => { const K = valueof(data, k); const Z = valueof(data, z); const F = valueof(data, vfill); diff --git a/src/transforms/compose.js b/src/transforms/compose.js new file mode 100644 index 0000000000..7691d5f4f0 --- /dev/null +++ b/src/transforms/compose.js @@ -0,0 +1,10 @@ +import {arrayify} from "../mark.js"; + +export function composeTransform(t1, t2) { + if (t1 == null) return t2 === null ? undefined : t2; + if (t2 == null) return t1 === null ? undefined : t1; + return (data, facets) => { + ({data, facets} = t1(data, facets)); + return t2(arrayify(data), facets); + }; +} diff --git a/src/transforms/filter.js b/src/transforms/filter.js new file mode 100644 index 0000000000..ec4f9cf5ee --- /dev/null +++ b/src/transforms/filter.js @@ -0,0 +1,13 @@ +import {valueof} from "../mark.js"; +import {basic} from "./basic.js"; + +export function filter(value, options) { + return basic(options, filterTransform(value)); +} + +export function filterTransform(value) { + return (data, facets) => { + const V = valueof(data, value); + return {data, facets: facets.map(I => I.filter(i => V[i]))}; + }; +} diff --git a/src/transforms/group.js b/src/transforms/group.js index b3d7bcb5ae..553012cd36 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -1,6 +1,7 @@ import {group as grouper, sort, sum, deviation, min, max, mean, median, mode, variance, InternSet} from "d3"; import {firstof} from "../defined.js"; -import {valueof, maybeColor, maybeInput, maybeTransform, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take, labelof, range} from "../mark.js"; +import {valueof, maybeColor, maybeInput, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take, labelof, range} from "../mark.js"; +import {basic} from "./basic.js"; // Group on {z, fill, stroke}. export function groupZ(outputs, options) { @@ -57,7 +58,7 @@ function groupn( ..."z" in inputs && {z: GZ || z}, ..."fill" in inputs && {fill: GF || fill}, ..."stroke" in inputs && {stroke: GS || stroke}, - ...maybeTransform(options, (data, facets) => { + ...basic(options, (data, facets) => { const X = valueof(data, x); const Y = valueof(data, y); const Z = valueof(data, z); diff --git a/src/transforms/map.js b/src/transforms/map.js index 416b51712a..5f3d6c431f 100644 --- a/src/transforms/map.js +++ b/src/transforms/map.js @@ -1,5 +1,6 @@ import {group} from "d3"; -import {maybeTransform, maybeZ, take, valueof, maybeInput, lazyChannel} from "../mark.js"; +import {maybeZ, take, valueof, maybeInput, lazyChannel} from "../mark.js"; +import {basic} from "./basic.js"; export function mapX(m, options = {}) { return map(Object.fromEntries(["x", "x1", "x2"] @@ -22,7 +23,7 @@ export function map(outputs = {}, options = {}) { return {key, input, output, setOutput, map: maybeMap(map)}; }); return { - ...maybeTransform(options, (data, facets) => { + ...basic(options, (data, facets) => { const Z = valueof(data, z); const X = channels.map(({input}) => valueof(data, input)); const MX = channels.map(({setOutput}) => setOutput(new Array(data.length))); diff --git a/src/transforms/reverse.js b/src/transforms/reverse.js new file mode 100644 index 0000000000..adbc9701e7 --- /dev/null +++ b/src/transforms/reverse.js @@ -0,0 +1,9 @@ +import {basic} from "./basic.js"; + +export function reverse(options) { + return basic(options, reverseTransform); +} + +export function reverseTransform(data, facets) { + return {data, facets: facets.map(I => I.slice().reverse())}; +} diff --git a/src/transforms/select.js b/src/transforms/select.js index c42dfc4e44..f70f3729ea 100644 --- a/src/transforms/select.js +++ b/src/transforms/select.js @@ -1,5 +1,6 @@ import {greatest, group, least} from "d3"; -import {maybeTransform, maybeZ, valueof} from "../mark.js"; +import {maybeZ, valueof} from "../mark.js"; +import {basic} from "./basic.js"; export function selectFirst(options) { return select(first, undefined, options); @@ -53,7 +54,7 @@ function* max(I, X) { function select(selectIndex, v, options) { const z = maybeZ(options); - return maybeTransform(options, (data, facets) => { + return basic(options, (data, facets) => { const Z = valueof(data, z); const V = valueof(data, v); const selectFacets = []; diff --git a/src/transforms/sort.js b/src/transforms/sort.js new file mode 100644 index 0000000000..b36dee21f9 --- /dev/null +++ b/src/transforms/sort.js @@ -0,0 +1,26 @@ +import {ascendingDefined} from "../defined.js"; +import {valueof} from "../mark.js"; +import {basic} from "./basic.js"; + +export function sort(value, options) { + return basic(options, sortTransform(value)); +} + +export function sortTransform(value) { + return (typeof value === "function" && value.length !== 1 ? sortCompare : sortValue)(value); +} + +function sortCompare(compare) { + return (data, facets) => { + const compareData = (i, j) => compare(data[i], data[j]); + return {data, facets: facets.map(I => I.slice().sort(compareData))}; + }; +} + +function sortValue(value) { + return (data, facets) => { + const V = valueof(data, value); + const compareValue = (i, j) => ascendingDefined(V[i], V[j]); + return {data, facets: facets.map(I => I.slice().sort(compareValue))}; + }; +} diff --git a/src/transforms/stack.js b/src/transforms/stack.js index ba6c213997..b7c1fce8a0 100644 --- a/src/transforms/stack.js +++ b/src/transforms/stack.js @@ -1,6 +1,7 @@ import {InternMap, cumsum, group, groupSort, greatest, rollup, sum, min} from "d3"; import {ascendingDefined} from "../defined.js"; -import {field, lazyChannel, maybeTransform, maybeLazyChannel, maybeZ, mid, range, valueof, identity, maybeZero} from "../mark.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); @@ -58,7 +59,7 @@ function stack(x, y = () => 1, ky, {offset, order, reverse, ...options} = {}) { offset = maybeOffset(offset); order = maybeOrder(order, offset, ky); return [ - maybeTransform(options, (data, facets) => { + basic(options, (data, facets) => { const X = x == null ? undefined : setX(valueof(data, x)); const Y = valueof(data, y, Float64Array); const Z = valueof(data, z); From 99da733239450d1aac91b9f1d1e0ab12289701b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 4 Aug 2021 11:08:57 +0200 Subject: [PATCH 2/6] document Plot.sort, Plot.reverse and Plot.filter --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 4dd68991aa..77f1b4e06c 100644 --- a/README.md +++ b/README.md @@ -955,6 +955,28 @@ The basic transforms are composable: the *filter* transform is applied first, th 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. +The *filter*, *sort* and *reverse* transforms are also available as functions, allowing to specify the order of operations when the shorthand notation might be ambiguous. In the following example, the sort transform is made explicitly after binning and before stacking: + +```js +Plot.barY(data, Plot.stackX(Plot.reverse(Plot.sort("length", Plot.binX({y: "count"}, {x: "date"}))))) +``` + +(Sorting before binning would result in sorted data inside bins; sorting after stacking would result in a different z-order of the marks.) + +### Sort and reverse + +The Plot.sort(*sort*, *options*) transform will return data sorted by *sort*, which can be an acessor function, a comparator function, or a channel value definition. + +Plot.reverse(*options*) reverses the order of the data. + +### Filter + +Given a *filter* and *options*, Plot.filter passes the data for which *filter* is truthy. The filter can be given as an accessor function (which receives the datum and index), or as a channel value definition. + +```js +Plot.filter([0, 1, 1, ], options) // returns the second and third data points +``` + ### Bin [a histogram of athletes by weight](https://observablehq.com/@observablehq/plot-bin) From 595376c91e1c60ccaa66255d0f8354a196cf7710 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 10 Aug 2021 15:31:48 -0700 Subject: [PATCH 3/6] update README --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 77f1b4e06c..11c961e201 100644 --- a/README.md +++ b/README.md @@ -963,18 +963,20 @@ Plot.barY(data, Plot.stackX(Plot.reverse(Plot.sort("length", Plot.binX({y: "coun (Sorting before binning would result in sorted data inside bins; sorting after stacking would result in a different z-order of the marks.) -### Sort and reverse +### Plot.sort(*order*, *options) -The Plot.sort(*sort*, *options*) transform will return data sorted by *sort*, which can be an acessor function, a comparator function, or a channel value definition. +Sorts the data by the specified *order*, which can be an acessor function, a comparator function, or a channel value definition. -Plot.reverse(*options*) reverses the order of the data. +### Plot.reverse(*options) -### Filter +Reverses the order of the data. -Given a *filter* and *options*, Plot.filter passes the data for which *filter* is truthy. The filter can be given as an accessor function (which receives the datum and index), or as a channel value definition. +### Plot.filter(*test*, *options*) + +Filters the data given the specified *test*. The test can be given as an accessor function (which receives the datum and index), or as a channel value definition; truthy values are retained. ```js -Plot.filter([0, 1, 1, ], options) // returns the second and third data points +Plot.filter(d => d.value > 3, options) // show data whose value is greater than three ``` ### Bin From 8b8228a79e451573488fd0925b80bfc0da2bdf8f Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 10 Aug 2021 15:32:10 -0700 Subject: [PATCH 4/6] update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 11c961e201..6932740ca3 100644 --- a/README.md +++ b/README.md @@ -963,11 +963,11 @@ Plot.barY(data, Plot.stackX(Plot.reverse(Plot.sort("length", Plot.binX({y: "coun (Sorting before binning would result in sorted data inside bins; sorting after stacking would result in a different z-order of the marks.) -### Plot.sort(*order*, *options) +### Plot.sort(*order*, *options*) Sorts the data by the specified *order*, which can be an acessor function, a comparator function, or a channel value definition. -### Plot.reverse(*options) +### Plot.reverse(*options*) Reverses the order of the data. From fa347b81912de1dcd28699e4b3748660ca7537cb Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 10 Aug 2021 15:33:22 -0700 Subject: [PATCH 5/6] update README --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6932740ca3..16845d19fe 100644 --- a/README.md +++ b/README.md @@ -965,20 +965,28 @@ Plot.barY(data, Plot.stackX(Plot.reverse(Plot.sort("length", Plot.binX({y: "coun ### Plot.sort(*order*, *options*) +```js +Plot.sort(d => d.value, options) // show data in ascending value order +``` + Sorts the data by the specified *order*, which can be an acessor function, a comparator function, or a channel value definition. ### Plot.reverse(*options*) +```js +Plot.reverse(options) // reverse the input order +``` + Reverses the order of the data. ### Plot.filter(*test*, *options*) -Filters the data given the specified *test*. The test can be given as an accessor function (which receives the datum and index), or as a channel value definition; truthy values are retained. - ```js Plot.filter(d => d.value > 3, options) // show data whose value is greater than three ``` +Filters the data given the specified *test*. The test can be given as an accessor function (which receives the datum and index), or as a channel value definition; truthy values are retained. + ### Bin [a histogram of athletes by weight](https://observablehq.com/@observablehq/plot-bin) From 425ace4e33f4e2a4c81878769a4731ee5a2741b1 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 10 Aug 2021 15:36:43 -0700 Subject: [PATCH 6/6] update README --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index 16845d19fe..9512711e57 100644 --- a/README.md +++ b/README.md @@ -955,13 +955,7 @@ The basic transforms are composable: the *filter* transform is applied first, th 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. -The *filter*, *sort* and *reverse* transforms are also available as functions, allowing to specify the order of operations when the shorthand notation might be ambiguous. In the following example, the sort transform is made explicitly after binning and before stacking: - -```js -Plot.barY(data, Plot.stackX(Plot.reverse(Plot.sort("length", Plot.binX({y: "count"}, {x: "date"}))))) -``` - -(Sorting before binning would result in sorted data inside bins; sorting after stacking would result in a different z-order of the marks.) +The *filter*, *sort* and *reverse* transforms are also available as functions, allowing the order of operations to be specified explicitly. For example, sorting before binning results in sorted data inside bins, whereas sorting after binning results affects the *z*-order of rendered marks. ### Plot.sort(*order*, *options*)