Skip to content

Commit 21a0caa

Browse files
mbostockFil
andauthored
filter, sort, and reverse transforms (#472)
* filter, sort, and reverse transforms * document Plot.sort, Plot.reverse and Plot.filter * update README * update README * update README * update README Co-authored-by: Philippe Rivière <[email protected]>
1 parent 2d1c317 commit 21a0caa

File tree

13 files changed

+126
-68
lines changed

13 files changed

+126
-68
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,32 @@ The basic transforms are composable: the *filter* transform is applied first, th
955955

956956
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.
957957

958+
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.
959+
960+
### Plot.sort(*order*, *options*)
961+
962+
```js
963+
Plot.sort(d => d.value, options) // show data in ascending value order
964+
```
965+
966+
Sorts the data by the specified *order*, which can be an acessor function, a comparator function, or a channel value definition.
967+
968+
### Plot.reverse(*options*)
969+
970+
```js
971+
Plot.reverse(options) // reverse the input order
972+
```
973+
974+
Reverses the order of the data.
975+
976+
### Plot.filter(*test*, *options*)
977+
978+
```js
979+
Plot.filter(d => d.value > 3, options) // show data whose value is greater than three
980+
```
981+
982+
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.
983+
958984
### Bin
959985

960986
[<img src="./img/bin.png" width="320" height="198" alt="a histogram of athletes by weight">](https://observablehq.com/@observablehq/plot-bin)

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export {Rect, rect, rectX, rectY} from "./marks/rect.js";
1111
export {RuleX, RuleY, ruleX, ruleY} from "./marks/rule.js";
1212
export {Text, text, textX, textY} from "./marks/text.js";
1313
export {TickX, TickY, tickX, tickY} from "./marks/tick.js";
14+
export {filter} from "./transforms/filter.js";
15+
export {reverse} from "./transforms/reverse.js";
16+
export {sort} from "./transforms/sort.js";
1417
export {bin, binX, binY} from "./transforms/bin.js";
1518
export {group, groupX, groupY, groupZ} from "./transforms/group.js";
1619
export {normalizeX, normalizeY} from "./transforms/normalize.js";

src/mark.js

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {color} from "d3";
2-
import {ascendingDefined, nonempty} from "./defined.js";
2+
import {nonempty} from "./defined.js";
33
import {plot} from "./plot.js";
44
import {styles} from "./style.js";
5+
import {basic} from "./transforms/basic.js";
56

67
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
78
const TypedArray = Object.getPrototypeOf(Uint8Array);
@@ -13,7 +14,7 @@ export class Mark {
1314
const names = new Set();
1415
this.data = data;
1516
this.facet = facet ? keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]) : null;
16-
const {transform} = maybeTransform(options);
17+
const {transform} = basic(options);
1718
this.transform = transform;
1819
if (defaults !== undefined) channels = styles(this, options, channels, defaults);
1920
this.channels = channels.filter(channel => {
@@ -225,23 +226,6 @@ export function maybeLazyChannel(source) {
225226
return source == null ? [source] : lazyChannel(source);
226227
}
227228

228-
// If both t1 and t2 are defined, returns a composite transform that first
229-
// applies t1 and then applies t2.
230-
export function maybeTransform({
231-
filter: f1,
232-
sort: s1,
233-
reverse: r1,
234-
transform: t1,
235-
...options
236-
} = {}, t2) {
237-
if (t1 === undefined) {
238-
if (f1 != null) t1 = filter(f1);
239-
if (s1 != null) t1 = compose(t1, sort(s1));
240-
if (r1) t1 = compose(t1, reverse);
241-
}
242-
return {...options, transform: compose(t1, t2)};
243-
}
244-
245229
// Assuming that both x1 and x2 and lazy channels (per above), this derives a
246230
// new a channel that’s the average of the two, and which inherits the channel
247231
// label (if any). Both input channels are assumed to be quantitative. If either
@@ -266,45 +250,6 @@ export function maybeValue(value) {
266250
typeof value.transform !== "function") ? value : {value};
267251
}
268252

269-
function compose(t1, t2) {
270-
if (t1 == null) return t2 === null ? undefined : t2;
271-
if (t2 == null) return t1 === null ? undefined : t1;
272-
return (data, facets) => {
273-
({data, facets} = t1(data, facets));
274-
return t2(arrayify(data), facets);
275-
};
276-
}
277-
278-
function sort(value) {
279-
return (typeof value === "function" && value.length !== 1 ? sortCompare : sortValue)(value);
280-
}
281-
282-
function sortCompare(compare) {
283-
return (data, facets) => {
284-
const compareData = (i, j) => compare(data[i], data[j]);
285-
return {data, facets: facets.map(I => I.slice().sort(compareData))};
286-
};
287-
}
288-
289-
function sortValue(value) {
290-
return (data, facets) => {
291-
const V = valueof(data, value);
292-
const compareValue = (i, j) => ascendingDefined(V[i], V[j]);
293-
return {data, facets: facets.map(I => I.slice().sort(compareValue))};
294-
};
295-
}
296-
297-
function filter(value) {
298-
return (data, facets) => {
299-
const V = valueof(data, value);
300-
return {data, facets: facets.map(I => I.filter(i => V[i]))};
301-
};
302-
}
303-
304-
function reverse(data, facets) {
305-
return {data, facets: facets.map(I => I.slice().reverse())};
306-
}
307-
308253
export function numberChannel(source) {
309254
return {
310255
transform: data => valueof(data, source, Float64Array),

src/transforms/basic.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {composeTransform} from "./compose.js";
2+
import {filterTransform} from "./filter.js";
3+
import {reverseTransform} from "./reverse.js";
4+
import {sortTransform} from "./sort.js";
5+
6+
// If both t1 and t2 are defined, returns a composite transform that first
7+
// applies t1 and then applies t2.
8+
export function basic({
9+
filter: f1,
10+
sort: s1,
11+
reverse: r1,
12+
transform: t1,
13+
...options
14+
} = {}, t2) {
15+
if (t1 === undefined) { // explicit transform overrides filter, sort, and reverse
16+
if (f1 != null) t1 = filterTransform(f1);
17+
if (s1 != null) t1 = composeTransform(t1, sortTransform(s1));
18+
if (r1) t1 = composeTransform(t1, reverseTransform);
19+
}
20+
return {...options, transform: composeTransform(t1, t2)};
21+
}

src/transforms/bin.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {bin as binner, extent, thresholdFreedmanDiaconis, thresholdScott, thresholdSturges, utcTickInterval} from "d3";
2-
import {valueof, range, identity, maybeLazyChannel, maybeTransform, maybeTuple, maybeColor, maybeValue, mid, labelof, isTemporal} from "../mark.js";
2+
import {valueof, range, identity, maybeLazyChannel, maybeTuple, maybeColor, maybeValue, mid, labelof, isTemporal} from "../mark.js";
33
import {offset} from "../style.js";
4+
import {basic} from "./basic.js";
45
import {maybeGroup, maybeOutputs, maybeReduce, maybeSubgroup, reduceIdentity} from "./group.js";
56

67
// Group on {z, fill, stroke}, then optionally on y, then bin x.
@@ -69,7 +70,7 @@ function binn(
6970
..."z" in inputs && {z: GZ || z},
7071
..."fill" in inputs && {fill: GF || fill},
7172
..."stroke" in inputs && {stroke: GS || stroke},
72-
...maybeTransform(options, (data, facets) => {
73+
...basic(options, (data, facets) => {
7374
const K = valueof(data, k);
7475
const Z = valueof(data, z);
7576
const F = valueof(data, vfill);

src/transforms/compose.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {arrayify} from "../mark.js";
2+
3+
export function composeTransform(t1, t2) {
4+
if (t1 == null) return t2 === null ? undefined : t2;
5+
if (t2 == null) return t1 === null ? undefined : t1;
6+
return (data, facets) => {
7+
({data, facets} = t1(data, facets));
8+
return t2(arrayify(data), facets);
9+
};
10+
}

src/transforms/filter.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {valueof} from "../mark.js";
2+
import {basic} from "./basic.js";
3+
4+
export function filter(value, options) {
5+
return basic(options, filterTransform(value));
6+
}
7+
8+
export function filterTransform(value) {
9+
return (data, facets) => {
10+
const V = valueof(data, value);
11+
return {data, facets: facets.map(I => I.filter(i => V[i]))};
12+
};
13+
}

src/transforms/group.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {group as grouper, sort, sum, deviation, min, max, mean, median, mode, variance, InternSet} from "d3";
22
import {firstof} from "../defined.js";
3-
import {valueof, maybeColor, maybeInput, maybeTransform, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take, labelof, range} from "../mark.js";
3+
import {valueof, maybeColor, maybeInput, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take, labelof, range} from "../mark.js";
4+
import {basic} from "./basic.js";
45

56
// Group on {z, fill, stroke}.
67
export function groupZ(outputs, options) {
@@ -57,7 +58,7 @@ function groupn(
5758
..."z" in inputs && {z: GZ || z},
5859
..."fill" in inputs && {fill: GF || fill},
5960
..."stroke" in inputs && {stroke: GS || stroke},
60-
...maybeTransform(options, (data, facets) => {
61+
...basic(options, (data, facets) => {
6162
const X = valueof(data, x);
6263
const Y = valueof(data, y);
6364
const Z = valueof(data, z);

src/transforms/map.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {group} from "d3";
2-
import {maybeTransform, maybeZ, take, valueof, maybeInput, lazyChannel} from "../mark.js";
2+
import {maybeZ, take, valueof, maybeInput, lazyChannel} from "../mark.js";
3+
import {basic} from "./basic.js";
34

45
export function mapX(m, options = {}) {
56
return map(Object.fromEntries(["x", "x1", "x2"]
@@ -22,7 +23,7 @@ export function map(outputs = {}, options = {}) {
2223
return {key, input, output, setOutput, map: maybeMap(map)};
2324
});
2425
return {
25-
...maybeTransform(options, (data, facets) => {
26+
...basic(options, (data, facets) => {
2627
const Z = valueof(data, z);
2728
const X = channels.map(({input}) => valueof(data, input));
2829
const MX = channels.map(({setOutput}) => setOutput(new Array(data.length)));

src/transforms/reverse.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {basic} from "./basic.js";
2+
3+
export function reverse(options) {
4+
return basic(options, reverseTransform);
5+
}
6+
7+
export function reverseTransform(data, facets) {
8+
return {data, facets: facets.map(I => I.slice().reverse())};
9+
}

src/transforms/select.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {greatest, group, least} from "d3";
2-
import {maybeTransform, maybeZ, valueof} from "../mark.js";
2+
import {maybeZ, valueof} from "../mark.js";
3+
import {basic} from "./basic.js";
34

45
export function selectFirst(options) {
56
return select(first, undefined, options);
@@ -53,7 +54,7 @@ function* max(I, X) {
5354

5455
function select(selectIndex, v, options) {
5556
const z = maybeZ(options);
56-
return maybeTransform(options, (data, facets) => {
57+
return basic(options, (data, facets) => {
5758
const Z = valueof(data, z);
5859
const V = valueof(data, v);
5960
const selectFacets = [];

src/transforms/sort.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {ascendingDefined} from "../defined.js";
2+
import {valueof} from "../mark.js";
3+
import {basic} from "./basic.js";
4+
5+
export function sort(value, options) {
6+
return basic(options, sortTransform(value));
7+
}
8+
9+
export function sortTransform(value) {
10+
return (typeof value === "function" && value.length !== 1 ? sortCompare : sortValue)(value);
11+
}
12+
13+
function sortCompare(compare) {
14+
return (data, facets) => {
15+
const compareData = (i, j) => compare(data[i], data[j]);
16+
return {data, facets: facets.map(I => I.slice().sort(compareData))};
17+
};
18+
}
19+
20+
function sortValue(value) {
21+
return (data, facets) => {
22+
const V = valueof(data, value);
23+
const compareValue = (i, j) => ascendingDefined(V[i], V[j]);
24+
return {data, facets: facets.map(I => I.slice().sort(compareValue))};
25+
};
26+
}

src/transforms/stack.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {InternMap, cumsum, group, groupSort, greatest, rollup, sum, min} from "d3";
22
import {ascendingDefined} from "../defined.js";
3-
import {field, lazyChannel, maybeTransform, maybeLazyChannel, maybeZ, mid, range, valueof, identity, maybeZero} from "../mark.js";
3+
import {field, lazyChannel, maybeLazyChannel, maybeZ, mid, range, valueof, identity, maybeZero} from "../mark.js";
4+
import {basic} from "./basic.js";
45

56
export function stackX({y1, y = y1, x, ...options} = {}) {
67
const [transform, Y, x1, x2] = stack(y, x, "x", options);
@@ -58,7 +59,7 @@ function stack(x, y = () => 1, ky, {offset, order, reverse, ...options} = {}) {
5859
offset = maybeOffset(offset);
5960
order = maybeOrder(order, offset, ky);
6061
return [
61-
maybeTransform(options, (data, facets) => {
62+
basic(options, (data, facets) => {
6263
const X = x == null ? undefined : setX(valueof(data, x));
6364
const Y = valueof(data, y, Float64Array);
6465
const Z = valueof(data, z);

0 commit comments

Comments
 (0)