Skip to content

Commit cade7e7

Browse files
Filmbostock
andauthored
pass the bins' extent (x1, x2, y1, y2) to the reducer (#437)
* pass the bins' extent (x1, x2, y1, y2) to the reducer * allow outputs to be suppressed * remove unused maybeLabel closes #435 Co-authored-by: Mike Bostock <[email protected]>
1 parent 8b47987 commit cade7e7

File tree

11 files changed

+1398
-25
lines changed

11 files changed

+1398
-25
lines changed

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,27 @@ The constant *dx* and *dy* options have been extended to all marks, allowing to
1212

1313
Quantitative scales, as well as identity position scales, now coerce channel values to numbers; both null and undefined are coerced to NaN. Similarly, time scales coerce channel values to dates; numbers are assumed to be milliseconds since UNIX epoch, while strings are assumed to be in [ISO 8601 format](https://github.com/mbostock/isoformat/blob/main/README.md#parsedate-fallback).
1414

15+
### Transforms
16+
17+
#### Plot.bin
18+
19+
The reducers now receive the extent of the current bin as an argument after the data. For example, it allows to create meaningful titles:
20+
```js
21+
Plot.rect(
22+
athletes,
23+
Plot.bin(
24+
{
25+
fill: "count",
26+
title: (bin, { x1, x2, y1, y2 }) =>
27+
`${bin.length} athletes weighing between ${x1} and ${x2} and with a height between ${y1} and ${y2}`
28+
},
29+
{ x: "weight", y: "height", inset: 0 }
30+
)
31+
).plot()
32+
```
33+
34+
The *x1* and *x2* outputs now default to undefined if *x* is explicitly defined; similary, the *y1* and *y2* outputs now default to undefined if *y* is explicitly defined.
35+
1536
## 0.2.0
1637

1738
Released August 20, 2021.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,7 +1051,7 @@ While it is possible to compute channel values on the binned data by defining ch
10511051
* **fill** - the first value of the *fill* channel, if any
10521052
* **stroke** - the first value of the *stroke* channel, if any
10531053

1054-
The **x1**, **x2**, and **x** output channels are only computed by the Plot.binX and Plot.bin transform; similarly the **y1**, **y2**, and **y** output channels are only computed by the Plot.binY and Plot.bin transform. The **x** and **y** output channels are lazy: they are only computed if needed by a downstream mark or transform.
1054+
The **x1**, **x2**, and **x** output channels are only computed by the Plot.binX and Plot.bin transform; similarly the **y1**, **y2**, and **y** output channels are only computed by the Plot.binY and Plot.bin transform. The **x** and **y** output channels are lazy: they are only computed if needed by a downstream mark or transform. Conversely, the *x1* and *x2* outputs default to undefined if *x* is explicitly defined; and the *y1* and *y2* outputs default to undefined if *y* is explicitly defined.
10551055

10561056
You can declare additional channels to aggregate by specifying the channel name and desired aggregation method in the *outputs* object which is the first argument to the transform. For example, to use [Plot.binX](#plotbinxoutputs-options) to generate a **y** channel of bin counts as in a frequency histogram:
10571057

@@ -1077,10 +1077,10 @@ The following aggregation methods are supported:
10771077
* *deviation* - the standard deviation
10781078
* *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm)
10791079
* *mode* - the value with the most occurrences
1080-
* a function to be passed the array of values for each bin
1080+
* a function to be passed the array of values for each bin and the extent of the bin
10811081
* an object with a *reduce* method
10821082

1083-
The *reduce* method is repeatedly passed the index for each bin (an array of integers) and the corresponding input channel’s array of values; it must then return the corresponding aggregate value for the bin.
1083+
The *reduce* method is repeatedly passed the index for each bin (an array of integers), the corresponding input channel’s array of values, and the extent of the bin; it must then return the corresponding aggregate value for the bin.
10841084

10851085
Most aggregation methods 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*.
10861086

src/mark.js

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,6 @@ export function maybeNumber(value, defaultValue) {
151151
: [value, undefined];
152152
}
153153

154-
// If the channel value is specified as a string, indicating a named field, this
155-
// wraps the specified function f with another function with the corresponding
156-
// label property, such that the associated axis inherits the label by default.
157-
export function maybeLabel(f, value) {
158-
const label = typeof value === "string" ? value
159-
: typeof value === "function" ? value.label
160-
: undefined;
161-
return label === undefined ? f : Object.assign(d => f(d), {label});
162-
}
163-
164154
// Validates the specified optional string against the allowed list of keywords.
165155
export function maybeKeyword(input, name, allowed) {
166156
if (input != null) return keyword(input, name, allowed);

src/transforms/bin.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,18 @@ function binn(
111111
for (const [x1, x2, fx] of BX) {
112112
const bb = fx(g);
113113
for (const [y1, y2, fy] of BY) {
114+
const extent = {x1, x2, y1, y2};
114115
const b = fy(bb);
115-
if (filter && !filter.reduce(b)) continue;
116+
if (filter && !filter.reduce(b, extent)) continue;
116117
groupFacet.push(i++);
117-
groupData.push(reduceData.reduce(b, data));
118+
groupData.push(reduceData.reduce(b, data, extent));
118119
if (K) GK.push(k);
119120
if (Z) GZ.push(G === Z ? f : Z[b[0]]);
120121
if (F) GF.push(G === F ? f : F[b[0]]);
121122
if (S) GS.push(G === S ? f : S[b[0]]);
122123
if (BX1) BX1.push(x1), BX2.push(x2);
123124
if (BY1) BY1.push(y1), BY2.push(y2);
124-
for (const o of outputs) o.reduce(b);
125+
for (const o of outputs) o.reduce(b, extent);
125126
if (sort) sort.reduce(b);
126127
}
127128
}
@@ -132,8 +133,8 @@ function binn(
132133
maybeSort(groupFacets, sort, reverse);
133134
return {data: groupData, facets: groupFacets};
134135
}),
135-
...BX1 ? {x1: BX1, x2: BX2, x: mid(BX1, BX2)} : {x},
136-
...BY1 ? {y1: BY1, y2: BY2, y: mid(BY1, BY2)} : {y},
136+
...BX1 && !hasOutput(outputs, "x") ? {x1: BX1, x2: BX2, x: mid(BX1, BX2)} : {x},
137+
...BY1 && !hasOutput(outputs, "y") ? {y1: BY1, y2: BY2, y: mid(BY1, BY2)} : {y},
137138
...GK && {[gk]: GK},
138139
...Object.fromEntries(outputs.map(({name, output}) => [name, output]))
139140
};

src/transforms/group.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ function groupn(
119119
}
120120

121121
export function maybeOutputs(outputs, inputs) {
122-
return Object.entries(outputs).map(([name, reduce]) => maybeOutput(name, reduce, inputs));
122+
return Object.entries(outputs).map(([name, reduce]) => {
123+
return reduce == null
124+
? {name, initialize() {}, scope() {}, reduce() {}}
125+
: maybeOutput(name, reduce, inputs);
126+
});
123127
}
124128

125129
export function maybeOutput(name, reduce, inputs) {
@@ -136,8 +140,8 @@ export function maybeOutput(name, reduce, inputs) {
136140
scope(scope, I) {
137141
evaluator.scope(scope, I);
138142
},
139-
reduce(I) {
140-
O.push(evaluator.reduce(I));
143+
reduce(I, extent) {
144+
O.push(evaluator.reduce(I, extent));
141145
}
142146
};
143147
}
@@ -159,8 +163,10 @@ export function maybeEvaluator(name, reduce, inputs) {
159163
context = reducer.reduce(I, V);
160164
}
161165
},
162-
reduce(I) {
163-
return reducer.reduce(I, V, context);
166+
reduce(I, extent) {
167+
return reducer.scope == null
168+
? reducer.reduce(I, V, extent)
169+
: reducer.reduce(I, V, context, extent);
164170
}
165171
};
166172
}
@@ -214,8 +220,8 @@ export function maybeSort(facets, sort, reverse) {
214220

215221
function reduceFunction(f) {
216222
return {
217-
reduce(I, X) {
218-
return f(take(X, I));
223+
reduce(I, X, extent) {
224+
return f(take(X, I), extent);
219225
}
220226
};
221227
}

0 commit comments

Comments
 (0)