Skip to content

Commit de66b18

Browse files
mbostockFil
andauthored
transforms can declare channels (#899)
* transforms can declare channels * default dodge to sort by reverse r (if r is a channel) * derive scale labels after the reinitializers have run (#900) * derive scale labels after the reinitializers have run * tweak style Co-authored-by: Mike Bostock <[email protected]> * only if both sort and reverse are undefined * fix tests * Update README Co-authored-by: Philippe Rivière <[email protected]>
1 parent 7338c4c commit de66b18

File tree

11 files changed

+752
-308
lines changed

11 files changed

+752
-308
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2213,6 +2213,8 @@ Initializers can be used to transform and derive new channels prior to rendering
22132213
22142214
The **anchor** option may one of *middle*, *right*, and *left* for dodgeX, and one of *middle*, *top*, and *bottom* for dodgeY. With the *middle* anchor the piles will grow from the center in both directions; with the other anchors, the piles will grow from the specified anchor towards the opposite direction.
22152215
2216+
The dodge layout is highly dependent on the input data order: the circles placed first will be closest to the dodge anchor. When using the dodge layout with circles of varying radius, the data is sorted by descending radius by default; you can disable this behavior by setting the **sort** or **reverse** option.
2217+
22162218
#### Plot.dodgeY([*layoutOptions*, ]*options*)
22172219
22182220
```js
@@ -2262,7 +2264,9 @@ See also the [hexgrid](#hexgrid) mark.
22622264
22632265
### Custom initializers
22642266
2265-
You can specify a custom initializer by specifying a function as the mark *initializer* option. This function is called after the scales have been computed, and receives as inputs the (possibly transformed) array of *data*, the *facets* index of elements of this array that belong to each facet, the input *channels* (as an object of named channels), the *scales*, and the *dimensions*. The mark itself is the *this* context. The initializer function must return an object with *data*, *facets*, and new *channels*. Any new channels are merged with existing channels, replacing channels of the same name.
2267+
You can specify a custom initializer by specifying a function as the mark **initializer** option. This function is called after the scales have been computed, and receives as inputs the (possibly transformed) array of *data*, the *facets* index of elements of this array that belong to each facet, the input *channels* (as an object of named channels), the *scales*, and the *dimensions*. The mark itself is the *this* context. The initializer function must return an object with *data*, *facets*, and new *channels*. Any new channels are merged with existing channels, replacing channels of the same name.
2268+
2269+
If an initializer desires a channel that is not supported by the downstream mark, additional channels can be declared using the mark **channels** option.
22662270
22672271
## Curves
22682272

src/plot.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ export function plot(options = {}) {
8787
const dimensions = Dimensions(scaleDescriptors, axes, options);
8888

8989
autoScaleRange(scaleDescriptors, dimensions);
90-
autoScaleLabels(channelsByScale, scaleDescriptors, axes, dimensions, options);
9190
autoAxisTicks(scaleDescriptors, axes);
9291

9392
const {fx, fy} = scales;
@@ -118,6 +117,8 @@ export function plot(options = {}) {
118117
Object.assign(scales, newScales);
119118
}
120119

120+
autoScaleLabels(channelsByScale, scaleDescriptors, axes, dimensions, options);
121+
121122
// Compute value objects, applying scales as needed.
122123
for (const state of stateByMark.values()) {
123124
state.values = valueObject(state.channels, scales);
@@ -247,15 +248,16 @@ export function plot(options = {}) {
247248

248249
export class Mark {
249250
constructor(data, channels = [], options = {}, defaults) {
250-
const {facet = "auto", sort, dx, dy, clip, initializer} = options;
251+
const {facet = "auto", sort, dx, dy, clip, initializer, channels: extraChannels} = options;
251252
const names = new Set();
252253
this.data = data;
253254
this.sort = isOptions(sort) ? sort : null;
254255
this.initializer = this.sort?.channel == null ? initializer : channelSort(initializer, this.sort);
255256
this.facet = facet == null || facet === false ? null : keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]);
256257
const {transform} = basic(options);
257258
this.transform = transform;
258-
if (defaults !== undefined) channels = styles(this, options, channels, defaults);
259+
if (extraChannels !== undefined) channels = [...channels, ...extraChannels.filter(e => !channels.some(c => c.name === e.name))];
260+
if (defaults !== undefined) channels = [...channels, ...styles(this, options, defaults)];
259261
this.channels = channels.filter(channel => {
260262
const {name, value, optional} = channel;
261263
if (value == null) {

src/style.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export function styles(
3232
paintOrder,
3333
shapeRendering
3434
},
35-
channels,
3635
{
3736
ariaLabel: cariaLabel,
3837
fill: defaultFill = "currentColor",
@@ -125,7 +124,6 @@ export function styles(
125124
mark.shapeRendering = impliedString(shapeRendering, "auto");
126125

127126
return [
128-
...channels,
129127
{name: "title", value: title, optional: true},
130128
{name: "href", value: href, optional: true},
131129
{name: "ariaLabel", value: variaLabel, optional: true},

src/transforms/dodge.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function maybeAnchor(anchor) {
1717
}
1818

1919
export function dodgeX(dodgeOptions = {}, options = {}) {
20-
if (arguments.length === 1) [options, dodgeOptions] = [dodgeOptions, options];
20+
if (arguments.length === 1) ([dodgeOptions, options] = mergeOptions(dodgeOptions));
2121
let {anchor = "left", padding = 1} = maybeAnchor(dodgeOptions);
2222
switch (`${anchor}`.toLowerCase()) {
2323
case "left": anchor = anchorXLeft; break;
@@ -29,7 +29,7 @@ export function dodgeX(dodgeOptions = {}, options = {}) {
2929
}
3030

3131
export function dodgeY(dodgeOptions = {}, options = {}) {
32-
if (arguments.length === 1) [options, dodgeOptions] = [dodgeOptions, options];
32+
if (arguments.length === 1) ([dodgeOptions, options] = mergeOptions(dodgeOptions));
3333
let {anchor = "bottom", padding = 1} = maybeAnchor(dodgeOptions);
3434
switch (`${anchor}`.toLowerCase()) {
3535
case "top": anchor = anchorYTop; break;
@@ -40,7 +40,18 @@ export function dodgeY(dodgeOptions = {}, options = {}) {
4040
return dodge("y", "x", anchor, number(padding), options);
4141
}
4242

43+
function mergeOptions(options) {
44+
const {padding, ...rest} = options;
45+
return [{padding}, rest];
46+
}
47+
4348
function dodge(y, x, anchor, padding, options) {
49+
const {r} = options;
50+
if (r != null && typeof r !== "number") {
51+
const {channels, sort, reverse} = options;
52+
options = {...options, channels: [...channels ?? [], {name: "r", value: r, scale: "r"}]};
53+
if (sort === undefined && reverse === undefined) options.sort = r, options.reverse = true;
54+
}
4455
return initializer(options, function(data, facets, {[x]: X, r: R}, scales, dimensions) {
4556
if (!X) throw new Error(`missing channel: ${x}`);
4657
X = coerceNumbers(valueof(X.value, scales[X.scale] || identity));

0 commit comments

Comments
 (0)