Skip to content

Commit

Permalink
Merge branch 'main' into mbostock/difference
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Nov 7, 2023
2 parents 60b48f9 + c6c1bcd commit 36dd5ea
Show file tree
Hide file tree
Showing 14 changed files with 525 additions and 38 deletions.
2 changes: 2 additions & 0 deletions docs/transforms/group.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,8 @@ The following named reducers are supported:
* *deviation* - the standard deviation
* *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm)
* *identity* - the array of values
* *x* - the group’s *x* value (when grouping on *x*)
* *y* - the group’s *y* value (when grouping on *y*)

In addition, a reducer may be specified as:

Expand Down
17 changes: 13 additions & 4 deletions docs/transforms/hexbin.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,9 @@ Plot.plot({

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.

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.
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.

The following aggregation methods are supported:
The following named reducers are supported:

* *first* - the first value, in input order
* *last* - the last value, in input order
Expand All @@ -195,13 +195,22 @@ The following aggregation methods are supported:
* *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm)
* *mode* - the value with the most occurrences
* *identity* - the array of values
* a function to be passed the array of values for each bin and the extent of the bin
* *x* - the hexagon’s *x* center
* *y* - the hexagon’s *y* center

In addition, a reducer may be specified as:

* a function to be passed the array of values for each bin and the center of the bin
* an object with a *reduceIndex* method

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.

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*.

## hexbin(*outputs*, *options*) {#hexbin}

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

Bins (hexagonally) on **x** and **y**. Also groups on the first channel of **z**, **fill**, or **stroke**, if any.
Bins hexagonally on **x** and **y**. Also groups on the first channel of **z**, **fill**, or **stroke**, if any.
4 changes: 2 additions & 2 deletions src/transforms/dodge.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export interface DodgeYOptions extends DodgeOptions {
* If *dodgeOptions* is a string, it is shorthand for the dodge **anchor**.
*/
export function dodgeX<T>(options?: T & DodgeXOptions): Initialized<T>;
export function dodgeX<T>(dodgeOptions: DodgeXOptions | DodgeXOptions["anchor"], options?: T): Initialized<T>;
export function dodgeX<T>(dodgeOptions?: DodgeXOptions | DodgeXOptions["anchor"], options?: T): Initialized<T>;

/**
* Given an **x** position channel, derives a new **y** position channel that
Expand All @@ -72,4 +72,4 @@ export function dodgeX<T>(dodgeOptions: DodgeXOptions | DodgeXOptions["anchor"],
* If *dodgeOptions* is a string, it is shorthand for the dodge **anchor**.
*/
export function dodgeY<T>(options?: T & DodgeYOptions): Initialized<T>;
export function dodgeY<T>(dodgeOptions: DodgeYOptions | DodgeYOptions["anchor"], options?: T): Initialized<T>;
export function dodgeY<T>(dodgeOptions?: DodgeYOptions | DodgeYOptions["anchor"], options?: T): Initialized<T>;
36 changes: 35 additions & 1 deletion src/transforms/group.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,42 @@ export interface GroupOutputOptions<T = Reducer> {
z?: ChannelValue;
}

/**
* How to reduce grouped values; one of:
*
* - a generic reducer name, such as *count* or *first*
* - *x* - the group’s **x** value (when grouping on **x**)
* - *y* - the group’s **y** value (when grouping on **y**)
* - a function that takes an array of values and returns the reduced value
* - an object that implements the *reduceIndex* method
*
* When a reducer function or implementation is used with the group transform,
* it is passed the group extent {x, y} as an additional argument.
*/
export type GroupReducer = Reducer | GroupReducerFunction | GroupReducerImplementation | "x" | "y";

/**
* A shorthand functional group reducer implementation: given an array of input
* channel *values*, and the current group’s *extent*, returns the corresponding
* reduced output value.
*/
export type GroupReducerFunction<S = any, T = S> = (values: S[], extent: {x: any; y: any}) => T;

/** A group reducer implementation. */
export interface GroupReducerImplementation<S = any, T = S> {
/**
* Given an *index* representing the contents of the current group, the input
* channel’s array of *values*, and the current group’s *extent*, returns the
* corresponding reduced output value. If no input channel is supplied (e.g.,
* as with the *count* reducer) then *values* may be undefined.
*/
reduceIndex(index: number[], values: S[], extent: {x: any; y: any}): T;
// TODO scope
// TODO label
}

/** Output channels (and options) for the group transform. */
export type GroupOutputs = ChannelReducers | GroupOutputOptions;
export type GroupOutputs = ChannelReducers<GroupReducer> | GroupOutputOptions<GroupReducer>;

/**
* Groups on the first channel of **z**, **fill**, or **stroke**, if any, and
Expand Down
2 changes: 1 addition & 1 deletion src/transforms/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ function invalidReduce(reduce) {
throw new Error(`invalid reduce: ${reduce}`);
}

function maybeGroupOutputs(outputs, inputs) {
export function maybeGroupOutputs(outputs, inputs) {
return maybeOutputs(outputs, inputs, maybeGroupOutput);
}

Expand Down
3 changes: 2 additions & 1 deletion src/transforms/hexbin.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {ChannelReducers, ChannelValue} from "../channel.js";
import type {Initialized} from "./basic.js";
import type {GroupReducer} from "./group.js";

/** Options for the hexbin transform. */
export interface HexbinOptions {
Expand Down Expand Up @@ -43,4 +44,4 @@ export interface HexbinOptions {
*
* To draw empty hexagons, see the hexgrid mark.
*/
export function hexbin<T>(outputs?: ChannelReducers, options?: T & HexbinOptions): Initialized<T>;
export function hexbin<T>(outputs?: ChannelReducers<GroupReducer>, options?: T & HexbinOptions): Initialized<T>;
30 changes: 14 additions & 16 deletions src/transforms/hexbin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {map, number, valueof} from "../options.js";
import {applyPosition} from "../projection.js";
import {sqrt3} from "../symbol.js";
import {initializer} from "./basic.js";
import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js";
import {hasOutput, maybeGroup, maybeGroupOutputs, maybeSubgroup} from "./group.js";

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

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

// A fill output means a fill channel; declaring the channel here instead of
// waiting for the initializer allows the mark constructor to determine that
Expand Down Expand Up @@ -65,15 +64,15 @@ export function hexbin(outputs = {fill: "count"}, {binWidth, ...options} = {}) {
const binFacet = [];
for (const o of outputs) o.scope("facet", facet);
for (const [f, I] of maybeGroup(facet, G)) {
for (const bin of hbin(I, X, Y, binWidth)) {
for (const {index: b, extent} of hbin(data, I, X, Y, binWidth)) {
binFacet.push(++i);
BX.push(bin.x);
BY.push(bin.y);
if (Z) GZ.push(G === Z ? f : Z[bin[0]]);
if (F) GF.push(G === F ? f : F[bin[0]]);
if (S) GS.push(G === S ? f : S[bin[0]]);
if (Q) GQ.push(G === Q ? f : Q[bin[0]]);
for (const o of outputs) o.reduce(bin);
BX.push(extent.x);
BY.push(extent.y);
if (Z) GZ.push(G === Z ? f : Z[b[0]]);
if (F) GF.push(G === F ? f : F[b[0]]);
if (S) GS.push(G === S ? f : S[b[0]]);
if (Q) GQ.push(G === Q ? f : Q[b[0]]);
for (const o of outputs) o.reduce(b, extent);
}
}
binFacets.push(binFacet);
Expand Down Expand Up @@ -106,7 +105,7 @@ export function hexbin(outputs = {fill: "count"}, {binWidth, ...options} = {}) {
});
}

function hbin(I, X, Y, dx) {
function hbin(data, I, X, Y, dx) {
const dy = dx * (1.5 / sqrt3);
const bins = new Map();
for (const i of I) {
Expand All @@ -127,11 +126,10 @@ function hbin(I, X, Y, dx) {
const key = `${pi},${pj}`;
let bin = bins.get(key);
if (bin === undefined) {
bins.set(key, (bin = []));
bin.x = (pi + (pj & 1) / 2) * dx + ox;
bin.y = pj * dy + oy;
bin = {index: [], extent: {data, x: (pi + (pj & 1) / 2) * dx + ox, y: pj * dy + oy}};
bins.set(key, bin);
}
bin.push(i);
bin.index.push(i);
}
return bins.values();
}
4 changes: 2 additions & 2 deletions src/transforms/normalize.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export interface NormalizeOptions {
* *x₀*, …] as in an index chart.
*/
export function normalizeX<T>(options?: T & NormalizeOptions): Transformed<T>;
export function normalizeX<T>(basis: NormalizeBasis, options?: T): Transformed<T>;
export function normalizeX<T>(basis?: NormalizeBasis, options?: T): Transformed<T>;

/**
* Groups data into series using the first channel of **z**, **fill**, or
Expand All @@ -68,7 +68,7 @@ export function normalizeX<T>(basis: NormalizeBasis, options?: T): Transformed<T
* *y₀*, …] as in an index chart.
*/
export function normalizeY<T>(options?: T & NormalizeOptions): Transformed<T>;
export function normalizeY<T>(basis: NormalizeBasis, options?: T): Transformed<T>;
export function normalizeY<T>(basis?: NormalizeBasis, options?: T): Transformed<T>;

/**
* Given a normalize *basis*, returns a corresponding map implementation for use
Expand Down
12 changes: 6 additions & 6 deletions src/transforms/stack.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,23 +120,23 @@ export interface StackOptions {
* one.
*/
export function stackX<T>(options?: T & StackOptions): Transformed<T>;
export function stackX<T>(stackOptions: StackOptions, options?: T): Transformed<T>;
export function stackX<T>(stackOptions?: StackOptions, options?: T): Transformed<T>;

/**
* Like **stackX**, but returns the starting position **x1** as the **x**
* channel, for example to position a dot on the left-hand side of each element
* of a stack.
*/
export function stackX1<T>(options?: T & StackOptions): Transformed<T>;
export function stackX1<T>(stackOptions: StackOptions, options?: T): Transformed<T>;
export function stackX1<T>(stackOptions?: StackOptions, options?: T): Transformed<T>;

/**
* Like **stackX**, but returns the starting position **x2** as the **x**
* channel, for example to position a dot on the right-hand side of each element
* of a stack.
*/
export function stackX2<T>(options?: T & StackOptions): Transformed<T>;
export function stackX2<T>(stackOptions: StackOptions, options?: T): Transformed<T>;
export function stackX2<T>(stackOptions?: StackOptions, options?: T): Transformed<T>;

/**
* Transforms a length channel **y** into starting and ending position channels
Expand All @@ -148,19 +148,19 @@ export function stackX2<T>(stackOptions: StackOptions, options?: T): Transformed
* specified, the input channel **y** defaults to the constant one.
*/
export function stackY<T>(options?: T & StackOptions): Transformed<T>;
export function stackY<T>(stackOptions: StackOptions, options?: T): Transformed<T>;
export function stackY<T>(stackOptions?: StackOptions, options?: T): Transformed<T>;

/**
* Like **stackY**, but returns the starting position **y1** as the **y**
* channel, for example to position a dot at the bottom of each element of a
* stack.
*/
export function stackY1<T>(options?: T & StackOptions): Transformed<T>;
export function stackY1<T>(stackOptions: StackOptions, options?: T): Transformed<T>;
export function stackY1<T>(stackOptions?: StackOptions, options?: T): Transformed<T>;

/**
* Like **stackY**, but returns the ending position **y2** as the **y** channel,
* for example to position a dot at the top of each element of a stack.
*/
export function stackY2<T>(options?: T & StackOptions): Transformed<T>;
export function stackY2<T>(stackOptions: StackOptions, options?: T): Transformed<T>;
export function stackY2<T>(stackOptions?: StackOptions, options?: T): Transformed<T>;
4 changes: 2 additions & 2 deletions src/transforms/window.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export interface WindowOptions {
* If *windowOptions* is a number, it is shorthand for the window size **k**.
*/
export function windowX<T>(options?: T & WindowOptions): Transformed<T>;
export function windowX<T>(windowOptions: WindowOptions | WindowOptions["k"], options?: T): Transformed<T>;
export function windowX<T>(windowOptions?: WindowOptions | WindowOptions["k"], options?: T): Transformed<T>;

/**
* Groups data into series using the first channel of *z*, *fill*, or *stroke*
Expand All @@ -124,7 +124,7 @@ export function windowX<T>(windowOptions: WindowOptions | WindowOptions["k"], op
* If *windowOptions* is a number, it is shorthand for the window size **k**.
*/
export function windowY<T>(options?: T & WindowOptions): Transformed<T>;
export function windowY<T>(windowOptions: WindowOptions | WindowOptions["k"], options?: T): Transformed<T>;
export function windowY<T>(windowOptions?: WindowOptions | WindowOptions["k"], options?: T): Transformed<T>;

/**
* Given the specified window *options*, returns a corresponding map
Expand Down
273 changes: 273 additions & 0 deletions test/output/hexbinFillX.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 36dd5ea

Please sign in to comment.