Skip to content

Commit c77b252

Browse files
Filmbostock
andauthored
generalize Plot.select (#656)
* generalize Plot.select closes #515 * Plot.select({x: "min"}, options) etc. * select edits (#664) * select edits * update README Co-authored-by: Mike Bostock <[email protected]>
1 parent 98c027a commit c77b252

File tree

8 files changed

+6200
-27
lines changed

8 files changed

+6200
-27
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1609,6 +1609,38 @@ Like [Plot.mapY](#plotmapymap-options), but applies the window map method with t
16091609

16101610
The select transform derives a filtered mark index; it does not affect the mark’s data or channels. It is similar to the basic [filter transform](#transforms) except that provides convenient shorthand for pulling a single value out of each series. The data are grouped into series using the *z*, *fill*, or *stroke* channel in the same fashion as the [area](#area) and [line](#line) marks.
16111611

1612+
#### Plot.select(*selector*, *options*)
1613+
1614+
Selects the points of each series selected by the *selector*, which can be specified either as a function which receives as input the index of the series, or as a {key: value} object with exactly one key representing a channel and the value being a function which receives as inputs the index of the series and the channel or the shorthand “min” and “max” which respectively select the least and greatest points for the specified channel.
1615+
1616+
For example, to select the point within each series that is the closest to the median of the *y* channel:
1617+
1618+
```js
1619+
Plot.select({
1620+
y: (I, V) => {
1621+
const median = d3.median(I, i => V[i]);
1622+
const i = d3.least(I, i => Math.abs(V[i] - median));
1623+
return [i];
1624+
}
1625+
}, {
1626+
x: "year",
1627+
y: "revenue",
1628+
fill: "format"
1629+
})
1630+
```
1631+
1632+
To pick three points at random in each series:
1633+
1634+
```js
1635+
Plot.select(I => d3.shuffle(I.slice()).slice(0, 3), {z: "year", ...})
1636+
```
1637+
1638+
To pick the point in each city with the highest temperature:
1639+
1640+
```js
1641+
Plot.select({fill: "max"}, {x: "date", y: "city", fill: "temperature", z: "city"})
1642+
```
1643+
16121644
#### Plot.selectFirst(*options*)
16131645

16141646
Selects the first point of each series according to input order.

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export {group, groupX, groupY, groupZ} from "./transforms/group.js";
1919
export {normalize, normalizeX, normalizeY} from "./transforms/normalize.js";
2020
export {map, mapX, mapY} from "./transforms/map.js";
2121
export {window, windowX, windowY} from "./transforms/window.js";
22-
export {selectFirst, selectLast, selectMaxX, selectMaxY, selectMinX, selectMinY} from "./transforms/select.js";
22+
export {select, selectFirst, selectLast, selectMaxX, selectMaxY, selectMinX, selectMinY} from "./transforms/select.js";
2323
export {stackX, stackX1, stackX2, stackY, stackY1, stackY2} from "./transforms/stack.js";
2424
export {formatIsoDate, formatWeekday, formatMonth} from "./format.js";
2525
export {scale} from "./scales.js";

src/transforms/select.js

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,84 @@ import {greatest, group, least} from "d3";
22
import {maybeZ, valueof} from "../options.js";
33
import {basic} from "./basic.js";
44

5+
export function select(selector, options = {}) {
6+
// If specified selector is a string or function, it’s a selector without an
7+
// input channel such as first or last.
8+
if (typeof selector === "string") {
9+
switch (selector.toLowerCase()) {
10+
case "first": return selectFirst(options);
11+
case "last": return selectLast(options);
12+
}
13+
}
14+
if (typeof selector === "function") {
15+
return selectChannel(null, selector, options);
16+
}
17+
// Otherwise the selector is an option {name: value} where name is a channel
18+
// name and value is a selector definition that additionally takes the given
19+
// channel values as input. The selector object must have exactly one key.
20+
let key, value;
21+
for (key in selector) {
22+
if (value !== undefined) throw new Error("ambiguous select definition");
23+
value = maybeSelector(selector[key]);
24+
}
25+
if (value === undefined) throw new Error("invalid select definition");
26+
return selectChannel(key, value, options);
27+
}
28+
29+
function maybeSelector(selector) {
30+
if (typeof selector === "function") return selector;
31+
switch (`${selector}`.toLowerCase()) {
32+
case "min": return selectorMin;
33+
case "max": return selectorMax;
34+
}
35+
throw new Error(`unknown selector: ${selector}`);
36+
}
37+
538
export function selectFirst(options) {
6-
return select(first, undefined, options);
39+
return selectChannel(null, selectorFirst, options);
740
}
841

942
export function selectLast(options) {
10-
return select(last, undefined, options);
43+
return selectChannel(null, selectorLast, options);
1144
}
1245

13-
export function selectMinX(options = {}) {
14-
const x = options.x;
15-
if (x == null) throw new Error("missing channel: x");
16-
return select(min, x, options);
46+
export function selectMinX(options) {
47+
return selectChannel("x", selectorMin, options);
1748
}
1849

19-
export function selectMinY(options = {}) {
20-
const y = options.y;
21-
if (y == null) throw new Error("missing channel: y");
22-
return select(min, y, options);
50+
export function selectMinY(options) {
51+
return selectChannel("y", selectorMin, options);
2352
}
2453

25-
export function selectMaxX(options = {}) {
26-
const x = options.x;
27-
if (x == null) throw new Error("missing channel: x");
28-
return select(max, x, options);
54+
export function selectMaxX(options) {
55+
return selectChannel("x", selectorMax, options);
2956
}
3057

31-
export function selectMaxY(options = {}) {
32-
const y = options.y;
33-
if (y == null) throw new Error("missing channel: y");
34-
return select(max, y, options);
58+
export function selectMaxY(options) {
59+
return selectChannel("y", selectorMax, options);
3560
}
3661

37-
// TODO If the value (for some required channel) is undefined, scan forward?
38-
function* first(I) {
62+
function* selectorFirst(I) {
3963
yield I[0];
4064
}
4165

42-
// TODO If the value (for some required channel) is undefined, scan backward?
43-
function* last(I) {
66+
function* selectorLast(I) {
4467
yield I[I.length - 1];
4568
}
4669

47-
function* min(I, X) {
70+
function* selectorMin(I, X) {
4871
yield least(I, i => X[i]);
4972
}
5073

51-
function* max(I, X) {
74+
function* selectorMax(I, X) {
5275
yield greatest(I, i => X[i]);
5376
}
5477

55-
function select(selectIndex, v, options) {
78+
function selectChannel(v, selector, options) {
79+
if (v != null) {
80+
if (options[v] == null) throw new Error(`missing channel: ${v}`);
81+
v = options[v];
82+
}
5683
const z = maybeZ(options);
5784
return basic(options, (data, facets) => {
5885
const Z = valueof(data, z);
@@ -61,7 +88,7 @@ function select(selectIndex, v, options) {
6188
for (const facet of facets) {
6289
const selectFacet = [];
6390
for (const I of Z ? group(facet, i => Z[i]).values() : [facet]) {
64-
for (const i of selectIndex(I, V)) {
91+
for (const i of selector(I, V)) {
6592
selectFacet.push(i);
6693
}
6794
}

0 commit comments

Comments
 (0)