Skip to content

Commit c6c1bcd

Browse files
authored
x and y reducers for group and hexbin (#1916)
* x and y reducers for group * x and y reducers for hexbin
1 parent 4cf4d73 commit c6c1bcd

File tree

10 files changed

+554
-29
lines changed

10 files changed

+554
-29
lines changed

docs/transforms/group.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,8 @@ The following named reducers are supported:
366366
* *deviation* - the standard deviation
367367
* *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm)
368368
* *identity* - the array of values
369+
* *x* - the group’s *x* value (when grouping on *x*)
370+
* *y* - the group’s *y* value (when grouping on *y*)
369371

370372
In addition, a reducer may be specified as:
371373

docs/transforms/hexbin.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,9 @@ Plot.plot({
174174

175175
The *options* must specify the **x** and **y** channels. The **binWidth** option (default 20) defines the distance between centers of neighboring hexagons in pixels. If any of **z**, **fill**, or **stroke** is a channel, the first of these channels will be used to subdivide bins.
176176

177-
The *outputs* options are similar to the [bin transform](./bin.md); each output channel receives as input, for each hexagon, the subset of the data which has been matched to its center. The outputs object specifies the aggregation method for each output channel.
177+
The *outputs* options are similar to the [bin transform](./bin.md); for each hexagon, an output channel value is derived by reducing the corresponding binned input channel values. The *outputs* object specifies the reducer for each output channel.
178178

179-
The following aggregation methods are supported:
179+
The following named reducers are supported:
180180

181181
* *first* - the first value, in input order
182182
* *last* - the last value, in input order
@@ -195,13 +195,22 @@ The following aggregation methods are supported:
195195
* *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm)
196196
* *mode* - the value with the most occurrences
197197
* *identity* - the array of values
198-
* a function to be passed the array of values for each bin and the extent of the bin
198+
* *x* - the hexagon’s *x* center
199+
* *y* - the hexagon’s *y* center
200+
201+
In addition, a reducer may be specified as:
202+
203+
* a function to be passed the array of values for each bin and the center of the bin
199204
* an object with a *reduceIndex* method
200205

206+
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 center of the bin (an object {data, x, y}); it must then return the corresponding aggregate value for the bin.
207+
208+
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*.
209+
201210
## hexbin(*outputs*, *options*) {#hexbin}
202211

203212
```js
204213
Plot.dot(olympians, Plot.hexbin({fill: "count"}, {x: "weight", y: "height"}))
205214
```
206215

207-
Bins (hexagonally) on **x** and **y**. Also groups on the first channel of **z**, **fill**, or **stroke**, if any.
216+
Bins hexagonally on **x** and **y**. Also groups on the first channel of **z**, **fill**, or **stroke**, if any.

src/transforms/group.d.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,42 @@ export interface GroupOutputOptions<T = Reducer> {
3838
z?: ChannelValue;
3939
}
4040

41+
/**
42+
* How to reduce grouped values; one of:
43+
*
44+
* - a generic reducer name, such as *count* or *first*
45+
* - *x* - the group’s **x** value (when grouping on **x**)
46+
* - *y* - the group’s **y** value (when grouping on **y**)
47+
* - a function that takes an array of values and returns the reduced value
48+
* - an object that implements the *reduceIndex* method
49+
*
50+
* When a reducer function or implementation is used with the group transform,
51+
* it is passed the group extent {x, y} as an additional argument.
52+
*/
53+
export type GroupReducer = Reducer | GroupReducerFunction | GroupReducerImplementation | "x" | "y";
54+
55+
/**
56+
* A shorthand functional group reducer implementation: given an array of input
57+
* channel *values*, and the current group’s *extent*, returns the corresponding
58+
* reduced output value.
59+
*/
60+
export type GroupReducerFunction<S = any, T = S> = (values: S[], extent: {x: any; y: any}) => T;
61+
62+
/** A group reducer implementation. */
63+
export interface GroupReducerImplementation<S = any, T = S> {
64+
/**
65+
* Given an *index* representing the contents of the current group, the input
66+
* channel’s array of *values*, and the current group’s *extent*, returns the
67+
* corresponding reduced output value. If no input channel is supplied (e.g.,
68+
* as with the *count* reducer) then *values* may be undefined.
69+
*/
70+
reduceIndex(index: number[], values: S[], extent: {x: any; y: any}): T;
71+
// TODO scope
72+
// TODO label
73+
}
74+
4175
/** Output channels (and options) for the group transform. */
42-
export type GroupOutputs = ChannelReducers | GroupOutputOptions;
76+
export type GroupOutputs = ChannelReducers<GroupReducer> | GroupOutputOptions<GroupReducer>;
4377

4478
/**
4579
* Groups on the first channel of **z**, **fill**, or **stroke**, if any, and

src/transforms/group.js

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ function groupn(
7676
inputs = {} // input channels and options
7777
) {
7878
// Compute the outputs.
79-
outputs = maybeOutputs(outputs, inputs);
80-
reduceData = maybeReduce(reduceData, identity);
81-
sort = sort == null ? undefined : maybeOutput("sort", sort, inputs);
82-
filter = filter == null ? undefined : maybeEvaluator("filter", filter, inputs);
79+
outputs = maybeGroupOutputs(outputs, inputs);
80+
reduceData = maybeGroupReduce(reduceData, identity);
81+
sort = sort == null ? undefined : maybeGroupOutput("sort", sort, inputs);
82+
filter = filter == null ? undefined : maybeGroupEvaluator("filter", filter, inputs);
8383

8484
// Produce x and y output channels as appropriate.
8585
const [GX, setGX] = maybeColumn(x);
@@ -287,6 +287,32 @@ function invalidReduce(reduce) {
287287
throw new Error(`invalid reduce: ${reduce}`);
288288
}
289289

290+
export function maybeGroupOutputs(outputs, inputs) {
291+
return maybeOutputs(outputs, inputs, maybeGroupOutput);
292+
}
293+
294+
function maybeGroupOutput(name, reduce, inputs) {
295+
return maybeOutput(name, reduce, inputs, maybeGroupEvaluator);
296+
}
297+
298+
function maybeGroupEvaluator(name, reduce, inputs) {
299+
return maybeEvaluator(name, reduce, inputs, maybeGroupReduce);
300+
}
301+
302+
function maybeGroupReduce(reduce, value) {
303+
return maybeReduce(reduce, value, maybeGroupReduceFallback);
304+
}
305+
306+
function maybeGroupReduceFallback(reduce) {
307+
switch (`${reduce}`.toLowerCase()) {
308+
case "x":
309+
return reduceX;
310+
case "y":
311+
return reduceY;
312+
}
313+
throw new Error(`invalid group reduce: ${reduce}`);
314+
}
315+
290316
export function maybeSubgroup(outputs, inputs) {
291317
for (const name in inputs) {
292318
const value = inputs[name];
@@ -399,6 +425,18 @@ function reduceProportion(value, scope) {
399425
: {scope, reduceIndex: (I, V, basis = 1) => sum(I, (i) => V[i]) / basis};
400426
}
401427

428+
const reduceX = {
429+
reduceIndex(I, X, {x}) {
430+
return x;
431+
}
432+
};
433+
434+
const reduceY = {
435+
reduceIndex(I, X, {y}) {
436+
return y;
437+
}
438+
};
439+
402440
export function find(test) {
403441
if (typeof test !== "function") throw new Error(`invalid test function: ${test}`);
404442
return {

src/transforms/hexbin.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {ChannelReducers, ChannelValue} from "../channel.js";
22
import type {Initialized} from "./basic.js";
3+
import type {GroupReducer} from "./group.js";
34

45
/** Options for the hexbin transform. */
56
export interface HexbinOptions {
@@ -43,4 +44,4 @@ export interface HexbinOptions {
4344
*
4445
* To draw empty hexagons, see the hexgrid mark.
4546
*/
46-
export function hexbin<T>(outputs?: ChannelReducers, options?: T & HexbinOptions): Initialized<T>;
47+
export function hexbin<T>(outputs?: ChannelReducers<GroupReducer>, options?: T & HexbinOptions): Initialized<T>;

src/transforms/hexbin.js

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {map, number, valueof} from "../options.js";
22
import {applyPosition} from "../projection.js";
33
import {sqrt3} from "../symbol.js";
44
import {initializer} from "./basic.js";
5-
import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js";
5+
import {hasOutput, maybeGroup, maybeGroupOutputs, maybeSubgroup} from "./group.js";
66

77
// We don’t want the hexagons to align with the edges of the plot frame, as that
88
// would cause extreme x-values (the upper bound of the default x-scale domain)
@@ -16,9 +16,8 @@ export function hexbin(outputs = {fill: "count"}, {binWidth, ...options} = {}) {
1616
const {z} = options;
1717

1818
// TODO filter e.g. to show empty hexbins?
19-
// TODO disallow x, x1, x2, y, y1, y2 reducers?
2019
binWidth = binWidth === undefined ? 20 : number(binWidth);
21-
outputs = maybeOutputs(outputs, options);
20+
outputs = maybeGroupOutputs(outputs, options);
2221

2322
// A fill output means a fill channel; declaring the channel here instead of
2423
// waiting for the initializer allows the mark constructor to determine that
@@ -65,15 +64,15 @@ export function hexbin(outputs = {fill: "count"}, {binWidth, ...options} = {}) {
6564
const binFacet = [];
6665
for (const o of outputs) o.scope("facet", facet);
6766
for (const [f, I] of maybeGroup(facet, G)) {
68-
for (const bin of hbin(I, X, Y, binWidth)) {
67+
for (const {index: b, extent} of hbin(data, I, X, Y, binWidth)) {
6968
binFacet.push(++i);
70-
BX.push(bin.x);
71-
BY.push(bin.y);
72-
if (Z) GZ.push(G === Z ? f : Z[bin[0]]);
73-
if (F) GF.push(G === F ? f : F[bin[0]]);
74-
if (S) GS.push(G === S ? f : S[bin[0]]);
75-
if (Q) GQ.push(G === Q ? f : Q[bin[0]]);
76-
for (const o of outputs) o.reduce(bin);
69+
BX.push(extent.x);
70+
BY.push(extent.y);
71+
if (Z) GZ.push(G === Z ? f : Z[b[0]]);
72+
if (F) GF.push(G === F ? f : F[b[0]]);
73+
if (S) GS.push(G === S ? f : S[b[0]]);
74+
if (Q) GQ.push(G === Q ? f : Q[b[0]]);
75+
for (const o of outputs) o.reduce(b, extent);
7776
}
7877
}
7978
binFacets.push(binFacet);
@@ -106,7 +105,7 @@ export function hexbin(outputs = {fill: "count"}, {binWidth, ...options} = {}) {
106105
});
107106
}
108107

109-
function hbin(I, X, Y, dx) {
108+
function hbin(data, I, X, Y, dx) {
110109
const dy = dx * (1.5 / sqrt3);
111110
const bins = new Map();
112111
for (const i of I) {
@@ -127,11 +126,10 @@ function hbin(I, X, Y, dx) {
127126
const key = `${pi},${pj}`;
128127
let bin = bins.get(key);
129128
if (bin === undefined) {
130-
bins.set(key, (bin = []));
131-
bin.x = (pi + (pj & 1) / 2) * dx + ox;
132-
bin.y = pj * dy + oy;
129+
bin = {index: [], extent: {data, x: (pi + (pj & 1) / 2) * dx + ox, y: pj * dy + oy}};
130+
bins.set(key, bin);
133131
}
134-
bin.push(i);
132+
bin.index.push(i);
135133
}
136134
return bins.values();
137135
}

test/output/hexbinFillX.svg

Lines changed: 273 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)