Skip to content

Commit 7b3bd48

Browse files
mbostockFil
andauthored
consume bin options; allow bin options on output (#565)
* consume options * Update bin.js * don’t propagate count input label * remove trailing comma * all bin options on outputs * rename test * regenerate test output * consume stack options, too * lift default * same with groups (#569) * same with groups * utc * prettier Co-authored-by: Mike Bostock <[email protected]> Co-authored-by: Philippe Rivière <[email protected]>
1 parent ec55d5e commit 7b3bd48

File tree

10 files changed

+393
-32
lines changed

10 files changed

+393
-32
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1152,7 +1152,7 @@ To control how the quantitative dimensions *x* and *y* are divided into bins, th
11521152
* **domain** - values outside the domain will be omitted
11531153
* **cumulative** - if positive, each bin will contain all lesser bins
11541154

1155-
If the **domain** option is not specified, it defaults to the minimum and maximum of the corresponding dimension (*x* or *y*), possibly niced to match the threshold interval to ensure that the first and last bin have the same width as other bins. If **cumulative** is negative (-1 by convention), each bin will contain all *greater* bins rather than all *lesser* bins, representing the [complementary cumulative distribution](https://en.wikipedia.org/wiki/Cumulative_distribution_function#Complementary_cumulative_distribution_function_.28tail_distribution.29).
1155+
These options may be specified either on the *options* or *outputs* object. If the **domain** option is not specified, it defaults to the minimum and maximum of the corresponding dimension (*x* or *y*), possibly niced to match the threshold interval to ensure that the first and last bin have the same width as other bins. If **cumulative** is negative (-1 by convention), each bin will contain all *greater* bins rather than all *lesser* bins, representing the [complementary cumulative distribution](https://en.wikipedia.org/wiki/Cumulative_distribution_function#Complementary_cumulative_distribution_function_.28tail_distribution.29).
11561156

11571157
To pass separate binning options for *x* and *y*, the **x** and **y** input channels can be specified as an object with the options above and a **value** option to specify the input channel values.
11581158

src/transforms/bin.js

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
import {bin as binner, extent, thresholdFreedmanDiaconis, thresholdScott, thresholdSturges, utcTickInterval} from "d3";
22
import {valueof, range, identity, maybeLazyChannel, maybeTuple, maybeColor, maybeValue, mid, labelof, isTemporal} from "../mark.js";
33
import {basic} from "./basic.js";
4-
import {maybeEvaluator, maybeGroup, maybeOutput, maybeOutputs, maybeReduce, maybeSort, maybeSubgroup, reduceCount, reduceIdentity} from "./group.js";
4+
import {hasOutput, maybeEvaluator, maybeGroup, maybeOutput, maybeOutputs, maybeReduce, maybeSort, maybeSubgroup, reduceCount, reduceIdentity} from "./group.js";
55
import {maybeInsetX, maybeInsetY} from "./inset.js";
66

77
// Group on {z, fill, stroke}, then optionally on y, then bin x.
88
export function binX(outputs = {y: "count"}, options = {}) {
9+
([outputs, options] = mergeOptions(outputs, options));
910
const {x, y} = options;
1011
return binn(maybeBinValue(x, options, identity), null, null, y, outputs, maybeInsetX(options));
1112
}
1213

1314
// Group on {z, fill, stroke}, then optionally on x, then bin y.
1415
export function binY(outputs = {x: "count"}, options = {}) {
16+
([outputs, options] = mergeOptions(outputs, options));
1517
const {x, y} = options;
1618
return binn(null, maybeBinValue(y, options, identity), x, null, outputs, maybeInsetY(options));
1719
}
1820

1921
// Group on {z, fill, stroke}, then bin on x and y.
2022
export function bin(outputs = {fill: "count"}, options = {}) {
23+
([outputs, options] = mergeOptions(outputs, options));
2124
const {x, y} = maybeBinValueTuple(options);
2225
return binn(x, y, null, null, outputs, maybeInsetX(maybeInsetY(options)));
2326
}
@@ -61,8 +64,21 @@ function binn(
6164

6265
// Greedily materialize the z, fill, and stroke channels (if channels and not
6366
// constants) so that we can reference them for subdividing groups without
64-
// computing them more than once.
65-
const {x, y, z, fill, stroke, ...options} = inputs;
67+
// computing them more than once. We also want to consume options that should
68+
// only apply to this transform rather than passing them through to the next.
69+
const {
70+
x,
71+
y,
72+
z,
73+
fill,
74+
stroke,
75+
x1, x2, // consumed if x is an output
76+
y1, y2, // consumed if y is an output
77+
domain, // eslint-disable-line no-unused-vars
78+
cumulative, // eslint-disable-line no-unused-vars
79+
thresholds, // eslint-disable-line no-unused-vars
80+
...options
81+
} = inputs;
6682
const [GZ, setGZ] = maybeLazyChannel(z);
6783
const [vfill] = maybeColor(fill);
6884
const [vstroke] = maybeColor(stroke);
@@ -127,14 +143,19 @@ function binn(
127143
maybeSort(groupFacets, sort, reverse);
128144
return {data: groupData, facets: groupFacets};
129145
}),
130-
...BX1 && !hasOutput(outputs, "x") ? {x1: BX1, x2: BX2, x: mid(BX1, BX2)} : {x},
131-
...BY1 && !hasOutput(outputs, "y") ? {y1: BY1, y2: BY2, y: mid(BY1, BY2)} : {y},
146+
...!hasOutput(outputs, "x") && (BX1 ? {x1: BX1, x2: BX2, x: mid(BX1, BX2)} : {x, x1, x2}),
147+
...!hasOutput(outputs, "y") && (BY1 ? {y1: BY1, y2: BY2, y: mid(BY1, BY2)} : {y, y1, y2}),
132148
...GK && {[gk]: GK},
133149
...Object.fromEntries(outputs.map(({name, output}) => [name, output]))
134150
};
135151
}
136152

137-
function maybeBinValue(value, {cumulative, domain, thresholds} = {}, defaultValue) {
153+
// Allow bin options to be specified as part of outputs; merge them into options.
154+
function mergeOptions({cumulative, domain, thresholds, ...outputs}, options) {
155+
return [outputs, {cumulative, domain, thresholds, ...options}];
156+
}
157+
158+
function maybeBinValue(value, {cumulative, domain, thresholds}, defaultValue) {
138159
value = {...maybeValue(value)};
139160
if (value.domain === undefined) value.domain = domain;
140161
if (value.cumulative === undefined) value.cumulative = cumulative;
@@ -144,7 +165,7 @@ function maybeBinValue(value, {cumulative, domain, thresholds} = {}, defaultValu
144165
return value;
145166
}
146167

147-
function maybeBinValueTuple(options = {}) {
168+
function maybeBinValueTuple(options) {
148169
let {x, y} = options;
149170
x = maybeBinValue(x, options);
150171
y = maybeBinValue(y, options);
@@ -202,15 +223,6 @@ function isTimeInterval(t) {
202223
return t ? typeof t.range === "function" : false;
203224
}
204225

205-
function hasOutput(outputs, ...names) {
206-
for (const {name} of outputs) {
207-
if (names.includes(name)) {
208-
return true;
209-
}
210-
}
211-
return false;
212-
}
213-
214226
function binset(bin) {
215227
return [bin, new Set(bin)];
216228
}

src/transforms/group.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,14 @@ function groupn(
5757
// Greedily materialize the z, fill, and stroke channels (if channels and not
5858
// constants) so that we can reference them for subdividing groups without
5959
// computing them more than once.
60-
const {z, fill, stroke, ...options} = inputs;
60+
const {
61+
z,
62+
fill,
63+
stroke,
64+
x1, x2, // consumed if x is an output
65+
y1, y2, // consumed if y is an output
66+
...options
67+
} = inputs;
6168
const [GZ, setGZ] = maybeLazyChannel(z);
6269
const [vfill] = maybeColor(fill);
6370
const [vstroke] = maybeColor(stroke);
@@ -112,12 +119,21 @@ function groupn(
112119
maybeSort(groupFacets, sort, reverse);
113120
return {data: groupData, facets: groupFacets};
114121
}),
115-
...GX && {x: GX},
116-
...GY && {y: GY},
122+
...!hasOutput(outputs, "x") && (GX ? {x: GX} : {x1, x2}),
123+
...!hasOutput(outputs, "y") && (GY ? {y: GY} : {y1, y2}),
117124
...Object.fromEntries(outputs.map(({name, output}) => [name, output]))
118125
};
119126
}
120127

128+
export function hasOutput(outputs, ...names) {
129+
for (const {name} of outputs) {
130+
if (names.includes(name)) {
131+
return true;
132+
}
133+
}
134+
return false;
135+
}
136+
121137
export function maybeOutputs(outputs, inputs) {
122138
return Object.entries(outputs).map(([name, reduce]) => {
123139
return reduce == null
@@ -151,7 +167,7 @@ export function maybeEvaluator(name, reduce, inputs) {
151167
const reducer = maybeReduce(reduce, input);
152168
let V, context;
153169
return {
154-
label: labelof(input, reducer.label),
170+
label: labelof(reducer === reduceCount ? null : input, reducer.label),
155171
initialize(data) {
156172
V = input === undefined ? data : valueof(data, input);
157173
if (reducer.scope === "data") {

src/transforms/interval.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ function maybeInterval(interval) {
1717

1818
// The interval may be specified either as x: {value, interval} or as {x,
1919
// interval}. The former is used, for example, for Plot.rect.
20-
function maybeIntervalValue(value, {interval} = {}) {
20+
function maybeIntervalValue(value, {interval}) {
2121
value = {...maybeValue(value)};
2222
value.interval = maybeInterval(value.interval === undefined ? interval : value.interval);
2323
return value;
2424
}
2525

26-
function maybeIntervalK(k, maybeInsetK, options = {}) {
26+
function maybeIntervalK(k, maybeInsetK, options) {
2727
const {[k]: v, [`${k}1`]: v1, [`${k}2`]: v2} = options;
2828
const {value, interval} = maybeIntervalValue(v, options);
2929
if (value == null || interval == null) return options;
@@ -38,7 +38,7 @@ function maybeIntervalK(k, maybeInsetK, options = {}) {
3838
});
3939
}
4040

41-
export function maybeIntervalX(options) {
41+
export function maybeIntervalX(options = {}) {
4242
return maybeIntervalK("x", maybeInsetX, options);
4343
}
4444

src/transforms/stack.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,42 @@ import {field, lazyChannel, maybeLazyChannel, maybeZ, mid, range, valueof, maybe
44
import {basic} from "./basic.js";
55

66
export function stackX(stackOptions = {}, options = {}) {
7-
if (arguments.length === 1) options = mergeOptions(stackOptions);
7+
if (arguments.length === 1) ([stackOptions, options] = mergeOptions(stackOptions));
88
const {y1, y = y1, x, ...rest} = options; // note: consumes x!
99
const [transform, Y, x1, x2] = stack(y, x, "x", stackOptions, rest);
1010
return {...transform, y1, y: Y, x1, x2, x: mid(x1, x2)};
1111
}
1212

1313
export function stackX1(stackOptions = {}, options = {}) {
14-
if (arguments.length === 1) options = mergeOptions(stackOptions);
14+
if (arguments.length === 1) ([stackOptions, options] = mergeOptions(stackOptions));
1515
const {y1, y = y1, x} = options;
1616
const [transform, Y, X] = stack(y, x, "x", stackOptions, options);
1717
return {...transform, y1, y: Y, x: X};
1818
}
1919

2020
export function stackX2(stackOptions = {}, options = {}) {
21-
if (arguments.length === 1) options = mergeOptions(stackOptions);
21+
if (arguments.length === 1) ([stackOptions, options] = mergeOptions(stackOptions));
2222
const {y1, y = y1, x} = options;
2323
const [transform, Y,, X] = stack(y, x, "x", stackOptions, options);
2424
return {...transform, y1, y: Y, x: X};
2525
}
2626

2727
export function stackY(stackOptions = {}, options = {}) {
28-
if (arguments.length === 1) options = mergeOptions(stackOptions);
28+
if (arguments.length === 1) ([stackOptions, options] = mergeOptions(stackOptions));
2929
const {x1, x = x1, y, ...rest} = options; // note: consumes y!
3030
const [transform, X, y1, y2] = stack(x, y, "y", stackOptions, rest);
3131
return {...transform, x1, x: X, y1, y2, y: mid(y1, y2)};
3232
}
3333

3434
export function stackY1(stackOptions = {}, options = {}) {
35-
if (arguments.length === 1) options = mergeOptions(stackOptions);
35+
if (arguments.length === 1) ([stackOptions, options] = mergeOptions(stackOptions));
3636
const {x1, x = x1, y} = options;
3737
const [transform, X, Y] = stack(x, y, "y", stackOptions, options);
3838
return {...transform, x1, x: X, y: Y};
3939
}
4040

4141
export function stackY2(stackOptions = {}, options = {}) {
42-
if (arguments.length === 1) options = mergeOptions(stackOptions);
42+
if (arguments.length === 1) ([stackOptions, options] = mergeOptions(stackOptions));
4343
const {x1, x = x1, y} = options;
4444
const [transform, X,, Y] = stack(x, y, "y", stackOptions, options);
4545
return {...transform, x1, x: X, y: Y};
@@ -73,8 +73,8 @@ function aliasSort(options, name) {
7373
// transform. If only one options object is specified, we interpret it as a
7474
// stack option, and therefore must remove it from the propagated options.
7575
function mergeOptions(options) {
76-
const {reverse} = options;
77-
return reverse ? {...options, reverse: false} : options;
76+
const {offset, order, reverse, ...rest} = options;
77+
return [{offset, order, reverse}, rest];
7878
}
7979

8080
function stack(x, y = () => 1, ky, {offset, order, reverse}, options) {

test/output/stargazersHourly.svg

Lines changed: 112 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)