Skip to content

Commit

Permalink
find reducer (#1914)
Browse files Browse the repository at this point in the history
* find transform

* find reducer

* prefilter data

* extent

* find documentation

---------

Co-authored-by: Philippe Rivière <[email protected]>
  • Loading branch information
mbostock and Fil authored Nov 2, 2023
1 parent 2b1a694 commit cbb673d
Show file tree
Hide file tree
Showing 12 changed files with 19,228 additions and 16 deletions.
2 changes: 1 addition & 1 deletion docs/transforms/bin.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ In addition, a reducer may be specified as:
* a function to be passed the array of values for each bin and the extent of the bin
* an object with a **reduceIndex** method, and optionally a **scope**

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

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.)

Expand Down
17 changes: 14 additions & 3 deletions docs/transforms/group.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,12 +369,12 @@ The following named reducers are supported:

In addition, a reducer may be specified as:

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

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

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.)
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.)

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*.

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

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.

## find(*test*) {#find}

```js
Plot.groupX(
{y1: Plot.find((d) => d.sex === "F"), y2: Plot.find((d) => d.sex === "M")},
{x: "date", y: "value"}
)
```

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.
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export {filter, reverse, sort, shuffle, basic as transform, initializer} from ".
export {bin, binX, binY} from "./transforms/bin.js";
export {centroid, geoCentroid} from "./transforms/centroid.js";
export {dodgeX, dodgeY} from "./transforms/dodge.js";
export {group, groupX, groupY, groupZ} from "./transforms/group.js";
export {find, group, groupX, groupY, groupZ} from "./transforms/group.js";
export {hexbin} from "./transforms/hexbin.js";
export {normalize, normalizeX, normalizeY} from "./transforms/normalize.js";
export {map, mapX, mapY} from "./transforms/map.js";
Expand Down
2 changes: 1 addition & 1 deletion src/reducer.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export interface ReducerImplementation<S = any, T = S> {
* value. If no input channel is supplied (e.g., as with the *count* reducer)
* then *values* may be undefined.
*/
reduceIndex(index: number[], values: S[]): T;
reduceIndex(index: number[], values: S[], extent: {data: any[]}): T;
// TODO scope
// TODO label
}
Expand Down
12 changes: 7 additions & 5 deletions src/transforms/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ function binn(
const BX2 = bx && setBX2([]);
const BY1 = by && setBY1([]);
const BY2 = by && setBY2([]);
const bin = bing(bx?.(data), by?.(data));
const bin = bing(bx, by, data);
let i = 0;
for (const o of outputs) o.initialize(data);
if (sort) sort.initialize(data);
Expand Down Expand Up @@ -367,28 +367,30 @@ function isTimeThresholds(t) {
return isTimeInterval(t) || (isIterable(t) && isTemporal(t));
}

function bing(EX, EY) {
function bing(bx, by, data) {
const EX = bx?.(data);
const EY = by?.(data);
return EX && EY
? function* (I) {
const X = EX.bin(I); // first bin on x
for (const [ix, [x1, x2]] of EX.entries()) {
const Y = EY.bin(X[ix]); // then bin on y
for (const [iy, [y1, y2]] of EY.entries()) {
yield [Y[iy], {x1, y1, x2, y2}];
yield [Y[iy], {data, x1, y1, x2, y2}];
}
}
}
: EX
? function* (I) {
const X = EX.bin(I);
for (const [i, [x1, x2]] of EX.entries()) {
yield [X[i], {x1, x2}];
yield [X[i], {data, x1, x2}];
}
}
: function* (I) {
const Y = EY.bin(I);
for (const [i, [y1, y2]] of EY.entries()) {
yield [Y[i], {y1, y2}];
yield [Y[i], {data, y1, y2}];
}
};
}
Expand Down
9 changes: 8 additions & 1 deletion src/transforms/group.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {ChannelReducers, ChannelValue} from "../channel.js";
import type {Reducer} from "../reducer.js";
import type {Reducer, ReducerImplementation} from "../reducer.js";
import type {Transformed} from "./basic.js";

/** Options for outputs of the group (and bin) transform. */
Expand Down Expand Up @@ -143,3 +143,10 @@ export function groupY<T>(outputs?: GroupOutputs, options?: T): Transformed<T>;
* *options*.
*/
export function group<T>(outputs?: GroupOutputs, options?: T): Transformed<T>;

/**
* Given the specified *test* function, returns a corresponding reducer
* implementation for use with the group or bin transform. The reducer returns
* the first channel value for which the *test* function returns a truthy value.
*/
export function find<T = any>(test: (d: T, index: number, data: T[]) => unknown): ReducerImplementation;
20 changes: 16 additions & 4 deletions src/transforms/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,19 @@ function groupn(
for (const [f, I] of maybeGroup(facet, G)) {
for (const [y, gg] of maybeGroup(I, Y)) {
for (const [x, g] of maybeGroup(gg, X)) {
if (filter && !filter.reduce(g)) continue;
const extent = {data};
if (X) extent.x = x;
if (Y) extent.y = y;
if (filter && !filter.reduce(g, extent)) continue;
groupFacet.push(i++);
groupData.push(reduceData.reduceIndex(g, data));
groupData.push(reduceData.reduceIndex(g, data, extent));
if (X) GX.push(x);
if (Y) GY.push(y);
if (Z) GZ.push(G === Z ? f : Z[g[0]]);
if (F) GF.push(G === F ? f : F[g[0]]);
if (S) GS.push(G === S ? f : S[g[0]]);
for (const o of outputs) o.reduce(g);
if (sort) sort.reduce(g);
for (const o of outputs) o.reduce(g, extent);
if (sort) sort.reduce(g, extent);
}
}
}
Expand Down Expand Up @@ -395,3 +398,12 @@ function reduceProportion(value, scope) {
? {scope, label: "Frequency", reduceIndex: (I, V, basis = 1) => I.length / basis}
: {scope, reduceIndex: (I, V, basis = 1) => sum(I, (i) => V[i]) / basis};
}

export function find(test) {
if (typeof test !== "function") throw new Error(`invalid test function: ${test}`);
return {
reduceIndex(I, V, {data}) {
return V[I.find((i) => test(data[i], i, data))];
}
};
}
4 changes: 4 additions & 0 deletions test/data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ https://data.giss.nasa.gov/gistemp/
Met Office Hadley Centre
https://www.metoffice.gov.uk/hadobs/hadcrut4/data/current/series_format.html

## ilc_lvps08.csv
“Share of young adults aged 18-34 living with their parents”, Eurostat
https://ec.europa.eu/eurostat/databrowser/view/ILC_LVPS08__custom_7530569/default/table?lang=en

## ipos.csv
“The Facebook Offering: How It Compares”, The New York Times
https://archive.nytimes.com/www.nytimes.com/interactive/2012/05/17/business/dealbook/how-the-facebook-offering-compares.html?hp
Expand Down
Loading

0 comments on commit cbb673d

Please sign in to comment.