Skip to content

Commit cbb673d

Browse files
mbostockFil
andauthored
find reducer (#1914)
* find transform * find reducer * prefilter data * extent * find documentation --------- Co-authored-by: Philippe Rivière <[email protected]>
1 parent 2b1a694 commit cbb673d

File tree

12 files changed

+19228
-16
lines changed

12 files changed

+19228
-16
lines changed

docs/transforms/bin.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ In addition, a reducer may be specified as:
280280
* a function to be passed the array of values for each bin and the extent of the bin
281281
* an object with a **reduceIndex** method, and optionally a **scope**
282282

283-
In the last case, the **reduceIndex** method is repeatedly passed three arguments: the index for each bin (an array of integers), the input channel’s array of values, and the extent of the bin (an object {x1, x2, y1, y2}); it must then return the corresponding aggregate value for the bin.
283+
In the last case, the **reduceIndex** method is repeatedly passed three arguments: the index for each bin (an array of integers), the input channel’s array of values, and the extent of the bin (an object {data, x1, x2, y1, y2}); it must then return the corresponding aggregate value for the bin.
284284

285285
If the reducer object’s **scope** is *data*, then the **reduceIndex** method is first invoked for the full data; the return value of the **reduceIndex** method is then made available as a third argument (making the extent the fourth argument). Similarly if the **scope** is *facet*, then the **reduceIndex** method is invoked for each facet, and the resulting reduce value is made available while reducing the facet’s bins. (This optional **scope** is used by the *proportion* and *proportion-facet* reducers.)
286286

docs/transforms/group.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -369,12 +369,12 @@ The following named reducers are supported:
369369

370370
In addition, a reducer may be specified as:
371371

372-
* a function - passed the array of values for each group
372+
* a function to be passed the array of values for each group and the extent of the group
373373
* an object with a **reduceIndex** method, an optionally a **scope**
374374

375-
In the last case, the **reduceIndex** method is repeatedly passed two arguments: the index for each group (an array of integers), and the input channel’s array of values; it must then return the corresponding aggregate value for the group.
375+
In the last case, the **reduceIndex** method is repeatedly passed three arguments: the index for each group (an array of integers), the input channel’s array of values, and the extent of the group (an object {data, x, y}); it must then return the corresponding aggregate value for the group.
376376

377-
If the reducer object’s **scope** is *data*, then the **reduceIndex** method is first invoked for the full data; the return value of the **reduceIndex** method is then made available as a third argument. Similarly if the **scope** is *facet*, then the **reduceIndex** method is invoked for each facet, and the resulting reduce value is made available while reducing the facet’s groups. (This optional **scope** is used by the *proportion* and *proportion-facet* reducers.)
377+
If the reducer object’s **scope** is *data*, then the **reduceIndex** method is first invoked for the full data; the return value of the **reduceIndex** method is then made available as a third argument (making the extent the fourth argument). Similarly if the **scope** is *facet*, then the **reduceIndex** method is invoked for each facet, and the resulting reduce value is made available while reducing the facet’s groups. (This optional **scope** is used by the *proportion* and *proportion-facet* reducers.)
378378

379379
Most reducers require binding the output channel to an input channel; for example, if you want the **y** output channel to be a *sum* (not merely a count), there should be a corresponding **y** input channel specifying which values to sum. If there is not, *sum* will be equivalent to *count*.
380380

@@ -435,3 +435,14 @@ Plot.groupZ({x: "proportion"}, {fill: "species"})
435435
```
436436

437437
Groups on the first channel of **z**, **fill**, or **stroke**, if any. If none of **z**, **fill**, or **stroke** are channels, then all data (within each facet) is placed into a single group.
438+
439+
## find(*test*) {#find}
440+
441+
```js
442+
Plot.groupX(
443+
{y1: Plot.find((d) => d.sex === "F"), y2: Plot.find((d) => d.sex === "M")},
444+
{x: "date", y: "value"}
445+
)
446+
```
447+
448+
Returns a reducer that finds the first datum for which the given *test* function returns a truthy value, and returns the corresponding channel value. This may be used with the group or bin transform to implement a “pivot wider” transform; for example, a “tall” dataset with separate rows for male and female observations may be transformed into a “wide” dataset with separate columns for male and female values.

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export {filter, reverse, sort, shuffle, basic as transform, initializer} from ".
3434
export {bin, binX, binY} from "./transforms/bin.js";
3535
export {centroid, geoCentroid} from "./transforms/centroid.js";
3636
export {dodgeX, dodgeY} from "./transforms/dodge.js";
37-
export {group, groupX, groupY, groupZ} from "./transforms/group.js";
37+
export {find, group, groupX, groupY, groupZ} from "./transforms/group.js";
3838
export {hexbin} from "./transforms/hexbin.js";
3939
export {normalize, normalizeX, normalizeY} from "./transforms/normalize.js";
4040
export {map, mapX, mapY} from "./transforms/map.js";

src/reducer.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export interface ReducerImplementation<S = any, T = S> {
6767
* value. If no input channel is supplied (e.g., as with the *count* reducer)
6868
* then *values* may be undefined.
6969
*/
70-
reduceIndex(index: number[], values: S[]): T;
70+
reduceIndex(index: number[], values: S[], extent: {data: any[]}): T;
7171
// TODO scope
7272
// TODO label
7373
}

src/transforms/bin.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ function binn(
167167
const BX2 = bx && setBX2([]);
168168
const BY1 = by && setBY1([]);
169169
const BY2 = by && setBY2([]);
170-
const bin = bing(bx?.(data), by?.(data));
170+
const bin = bing(bx, by, data);
171171
let i = 0;
172172
for (const o of outputs) o.initialize(data);
173173
if (sort) sort.initialize(data);
@@ -367,28 +367,30 @@ function isTimeThresholds(t) {
367367
return isTimeInterval(t) || (isIterable(t) && isTemporal(t));
368368
}
369369

370-
function bing(EX, EY) {
370+
function bing(bx, by, data) {
371+
const EX = bx?.(data);
372+
const EY = by?.(data);
371373
return EX && EY
372374
? function* (I) {
373375
const X = EX.bin(I); // first bin on x
374376
for (const [ix, [x1, x2]] of EX.entries()) {
375377
const Y = EY.bin(X[ix]); // then bin on y
376378
for (const [iy, [y1, y2]] of EY.entries()) {
377-
yield [Y[iy], {x1, y1, x2, y2}];
379+
yield [Y[iy], {data, x1, y1, x2, y2}];
378380
}
379381
}
380382
}
381383
: EX
382384
? function* (I) {
383385
const X = EX.bin(I);
384386
for (const [i, [x1, x2]] of EX.entries()) {
385-
yield [X[i], {x1, x2}];
387+
yield [X[i], {data, x1, x2}];
386388
}
387389
}
388390
: function* (I) {
389391
const Y = EY.bin(I);
390392
for (const [i, [y1, y2]] of EY.entries()) {
391-
yield [Y[i], {y1, y2}];
393+
yield [Y[i], {data, y1, y2}];
392394
}
393395
};
394396
}

src/transforms/group.d.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {ChannelReducers, ChannelValue} from "../channel.js";
2-
import type {Reducer} from "../reducer.js";
2+
import type {Reducer, ReducerImplementation} from "../reducer.js";
33
import type {Transformed} from "./basic.js";
44

55
/** Options for outputs of the group (and bin) transform. */
@@ -143,3 +143,10 @@ export function groupY<T>(outputs?: GroupOutputs, options?: T): Transformed<T>;
143143
* *options*.
144144
*/
145145
export function group<T>(outputs?: GroupOutputs, options?: T): Transformed<T>;
146+
147+
/**
148+
* Given the specified *test* function, returns a corresponding reducer
149+
* implementation for use with the group or bin transform. The reducer returns
150+
* the first channel value for which the *test* function returns a truthy value.
151+
*/
152+
export function find<T = any>(test: (d: T, index: number, data: T[]) => unknown): ReducerImplementation;

src/transforms/group.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,19 @@ function groupn(
134134
for (const [f, I] of maybeGroup(facet, G)) {
135135
for (const [y, gg] of maybeGroup(I, Y)) {
136136
for (const [x, g] of maybeGroup(gg, X)) {
137-
if (filter && !filter.reduce(g)) continue;
137+
const extent = {data};
138+
if (X) extent.x = x;
139+
if (Y) extent.y = y;
140+
if (filter && !filter.reduce(g, extent)) continue;
138141
groupFacet.push(i++);
139-
groupData.push(reduceData.reduceIndex(g, data));
142+
groupData.push(reduceData.reduceIndex(g, data, extent));
140143
if (X) GX.push(x);
141144
if (Y) GY.push(y);
142145
if (Z) GZ.push(G === Z ? f : Z[g[0]]);
143146
if (F) GF.push(G === F ? f : F[g[0]]);
144147
if (S) GS.push(G === S ? f : S[g[0]]);
145-
for (const o of outputs) o.reduce(g);
146-
if (sort) sort.reduce(g);
148+
for (const o of outputs) o.reduce(g, extent);
149+
if (sort) sort.reduce(g, extent);
147150
}
148151
}
149152
}
@@ -395,3 +398,12 @@ function reduceProportion(value, scope) {
395398
? {scope, label: "Frequency", reduceIndex: (I, V, basis = 1) => I.length / basis}
396399
: {scope, reduceIndex: (I, V, basis = 1) => sum(I, (i) => V[i]) / basis};
397400
}
401+
402+
export function find(test) {
403+
if (typeof test !== "function") throw new Error(`invalid test function: ${test}`);
404+
return {
405+
reduceIndex(I, V, {data}) {
406+
return V[I.find((i) => test(data[i], i, data))];
407+
}
408+
};
409+
}

test/data/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ https://data.giss.nasa.gov/gistemp/
102102
Met Office Hadley Centre
103103
https://www.metoffice.gov.uk/hadobs/hadcrut4/data/current/series_format.html
104104

105+
## ilc_lvps08.csv
106+
“Share of young adults aged 18-34 living with their parents”, Eurostat
107+
https://ec.europa.eu/eurostat/databrowser/view/ILC_LVPS08__custom_7530569/default/table?lang=en
108+
105109
## ipos.csv
106110
“The Facebook Offering: How It Compares”, The New York Times
107111
https://archive.nytimes.com/www.nytimes.com/interactive/2012/05/17/business/dealbook/how-the-facebook-offering-compares.html?hp

0 commit comments

Comments
 (0)