Skip to content

Commit 6986367

Browse files
committed
select edits (#664)
* select edits * update README
1 parent 2880ed1 commit 6986367

File tree

2 files changed

+75
-51
lines changed

2 files changed

+75
-51
lines changed

README.md

Lines changed: 32 additions & 29 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.
@@ -1633,35 +1665,6 @@ Selects the rightmost point of each series.
16331665

16341666
Selects the highest point of each series.
16351667

1636-
### Plot.select(*selector*, *options*)
1637-
1638-
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 short-hand "min" and "max" which respectively select the least and greatest points for the specified channel.
1639-
1640-
For example, to select the point within each series that is the closest to the median of the *y* channel:
1641-
1642-
```js
1643-
Plot.select({
1644-
y: (I, V) => {
1645-
const median = d3.median(I, i => V[i]);
1646-
const i = d3.least(I, (i) => Math.abs(V[i] - median));
1647-
return [i];
1648-
}, {
1649-
x: "year",
1650-
y: "revenue",
1651-
fill: "format"
1652-
})
1653-
```
1654-
1655-
To pick three points at random in each series:
1656-
```js
1657-
Plot.select(I => d3.shuffle(I).slice(0, 3), {z: "year", ...})
1658-
```
1659-
1660-
To pick the point in each city with the highest temperature:
1661-
```js
1662-
Plot.select({fill: "max"}, {x: "date", y: "city", fill: "temperature", z: "city"})
1663-
```
1664-
16651668
### Stack
16661669

16671670
[<img src="./img/stack.png" width="320" height="198" alt="a stacked area chart of revenue by category">](https://observablehq.com/@observablehq/plot-stack)

src/transforms/select.js

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,62 +3,83 @@ import {maybeZ, valueof} from "../options.js";
33
import {basic} from "./basic.js";
44

55
export function select(selector, options = {}) {
6-
if (typeof selector === "function") return selectAny(selector, undefined, options);
7-
const k = Object.keys(selector);
8-
if (k.length !== 1) throw new Error("select one channel");
9-
const channel = k[0];
10-
selector = selector[channel];
11-
const x = options[channel];
12-
if (x == null) throw new Error(`missing channel: ${channel}`);
13-
if (typeof selector === "function") return selectAny(selector, x, 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;
1431
switch (`${selector}`.toLowerCase()) {
15-
case "min": return selectAny(min, x, options);
16-
case "max": return selectAny(max, x, options);
32+
case "min": return selectorMin;
33+
case "max": return selectorMax;
1734
}
1835
throw new Error(`unknown selector: ${selector}`);
1936
}
2037

2138
export function selectFirst(options) {
22-
return selectAny(first, undefined, options);
39+
return selectChannel(null, selectorFirst, options);
2340
}
2441

2542
export function selectLast(options) {
26-
return selectAny(last, undefined, options);
43+
return selectChannel(null, selectorLast, options);
2744
}
2845

2946
export function selectMinX(options) {
30-
return select({x: "min"}, options);
47+
return selectChannel("x", selectorMin, options);
3148
}
3249

3350
export function selectMinY(options) {
34-
return selectAny({y: "min"}, options);
51+
return selectChannel("y", selectorMin, options);
3552
}
3653

3754
export function selectMaxX(options) {
38-
return selectAny({x: "max"}, options);
55+
return selectChannel("x", selectorMax, options);
3956
}
4057

4158
export function selectMaxY(options) {
42-
return selectAny({y: "max"}, options);
59+
return selectChannel("y", selectorMax, options);
4360
}
4461

45-
function* first(I) {
62+
function* selectorFirst(I) {
4663
yield I[0];
4764
}
4865

49-
function* last(I) {
66+
function* selectorLast(I) {
5067
yield I[I.length - 1];
5168
}
5269

53-
function* min(I, X) {
70+
function* selectorMin(I, X) {
5471
yield least(I, i => X[i]);
5572
}
5673

57-
function* max(I, X) {
74+
function* selectorMax(I, X) {
5875
yield greatest(I, i => X[i]);
5976
}
6077

61-
function selectAny(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+
}
6283
const z = maybeZ(options);
6384
return basic(options, (data, facets) => {
6485
const Z = valueof(data, z);
@@ -67,7 +88,7 @@ function selectAny(selectIndex, v, options) {
6788
for (const facet of facets) {
6889
const selectFacet = [];
6990
for (const I of Z ? group(facet, i => Z[i]).values() : [facet]) {
70-
for (const i of selectIndex(I, V)) {
91+
for (const i of selector(I, V)) {
7192
selectFacet.push(i);
7293
}
7394
}

0 commit comments

Comments
 (0)