Skip to content

find reducer #1914

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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