Skip to content

Commit 587420d

Browse files
committed
projection + hexbin
1 parent 8027a85 commit 587420d

File tree

9 files changed

+5211
-29
lines changed

9 files changed

+5211
-29
lines changed

src/channel.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,28 @@ export function Channels(descriptors, data) {
2626

2727
// TODO Use Float64Array for scales with numeric ranges, e.g. position?
2828
export function valueObject(channels, scales, {projection}) {
29+
let scaledX, scaledY;
30+
2931
const values = Object.fromEntries(
3032
Object.entries(channels).map(([name, {scale: scaleName, value}]) => {
31-
const scale = scales[scaleName];
33+
let scale;
34+
if (scaleName !== undefined) {
35+
if (name === "x") scaledX = true;
36+
else if (name === "y") scaledY = true;
37+
scale = scales[scaleName];
38+
}
3239
return [name, scale === undefined ? value : map(value, scale)];
3340
})
3441
);
35-
if (projection) {
42+
43+
// If there is a projection, and there are both x and y channels, and those x
44+
// and y channels are associated with a scale (presumably the x and y scale)
45+
// rather than being in screen coordinates (as with an initializer), then
46+
// apply the projection, replacing the x and y values.
47+
if (projection && scaledX && scaledY) {
3648
applyProjection(values, projection);
3749
}
50+
3851
return values;
3952
}
4053

src/plot.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,17 @@ export function plot(options = {}) {
134134
const newByScale = new Set();
135135
for (const [mark, state] of stateByMark) {
136136
if (mark.initializer != null) {
137-
const {facets, channels} = mark.initializer(state.data, state.facets, state.channels, scales, subdimensions);
138-
if (facets !== undefined) state.facets = facets;
137+
const {facets, channels} = mark.initializer(
138+
state.data,
139+
state.facets,
140+
state.channels,
141+
scales,
142+
subdimensions,
143+
context
144+
);
145+
if (facets !== undefined) {
146+
state.facets = facets;
147+
}
139148
if (channels !== undefined) {
140149
inferChannelScale(channels, mark);
141150
applyScaleTransforms(channels, options);

src/projection.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,12 @@ export function maybeProjection(projection, dimensions) {
7373

7474
export function applyProjection(values, projection) {
7575
const {x, y} = values;
76-
if (x && y) {
77-
const n = x.length;
78-
const X = (values.x = new Float64Array(n));
79-
const Y = (values.y = new Float64Array(n));
80-
for (let i = 0; i < n; ++i) {
81-
const p = projection([x[i], y[i]]);
82-
if (p) (X[i] = p[0]), (Y[i] = p[1]);
83-
else X[i] = Y[i] = NaN;
84-
}
76+
const n = x.length;
77+
const X = (values.x = new Float64Array(n));
78+
const Y = (values.y = new Float64Array(n));
79+
for (let i = 0; i < n; ++i) {
80+
const p = projection([x[i], y[i]]);
81+
if (p) (X[i] = p[0]), (Y[i] = p[1]);
82+
else X[i] = Y[i] = NaN;
8583
}
8684
}

src/transforms/basic.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ function composeTransform(t1, t2) {
5151
function composeInitializer(i1, i2) {
5252
if (i1 == null) return i2 === null ? undefined : i2;
5353
if (i2 == null) return i1 === null ? undefined : i1;
54-
return function (data, facets, channels, scales, dimensions) {
54+
return function (data, facets, channels, ...args) {
5555
let c1, d1, f1, c2, d2, f2;
56-
({data: d1 = data, facets: f1 = facets, channels: c1} = i1.call(this, data, facets, channels, scales, dimensions));
57-
({data: d2 = d1, facets: f2 = f1, channels: c2} = i2.call(this, d1, f1, {...channels, ...c1}, scales, dimensions));
56+
({data: d1 = data, facets: f1 = facets, channels: c1} = i1.call(this, data, facets, channels, ...args));
57+
({data: d2 = d1, facets: f2 = f1, channels: c2} = i2.call(this, d1, f1, {...channels, ...c1}, ...args));
5858
return {data: d2, facets: f2, channels: {...c1, ...c2}};
5959
};
6060
}

src/transforms/hexbin.js

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import {valueObject} from "../channel.js";
12
import {coerceNumbers} from "../scales.js";
23
import {sqrt3} from "../symbols.js";
3-
import {identity, isNoneish, number, valueof} from "../options.js";
4+
import {isNoneish, number, valueof} from "../options.js";
45
import {initializer} from "./basic.js";
56
import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js";
67

@@ -13,32 +14,34 @@ export const ox = 0.5,
1314
oy = 0;
1415

1516
/** @jsdoc hexbin */
16-
export function hexbin(outputs = {fill: "count"}, options = {}) {
17+
export function hexbin(outputs = {fill: "count"}, {binWidth, ...options} = {}) {
1718
// TODO filter e.g. to show empty hexbins?
1819
// TODO disallow x, x1, x2, y, y1, y2 reducers?
19-
let {binWidth, ...remainingOptions} = options;
2020
binWidth = binWidth === undefined ? 20 : number(binWidth);
21-
outputs = maybeOutputs(outputs, remainingOptions);
21+
outputs = maybeOutputs(outputs, options);
2222

2323
// A fill output means a fill channel, and hence the stroke should default to
2424
// none (assuming a mark that defaults to fill and no stroke, such as dot).
2525
// Note that it’s safe to mutate options here because we just created it with
2626
// the rest operator above.
27-
const {z, fill, stroke} = remainingOptions;
28-
if (stroke === undefined && isNoneish(fill) && hasOutput(outputs, "fill")) remainingOptions.stroke = "none";
27+
const {z, fill, stroke} = options;
28+
if (stroke === undefined && isNoneish(fill) && hasOutput(outputs, "fill")) options.stroke = "none";
2929

3030
// Populate default values for the r and symbol options, as appropriate.
31-
if (remainingOptions.symbol === undefined) remainingOptions.symbol = "hexagon";
32-
if (remainingOptions.r === undefined && !hasOutput(outputs, "r")) remainingOptions.r = binWidth / 2;
31+
if (options.symbol === undefined) options.symbol = "hexagon";
32+
if (options.r === undefined && !hasOutput(outputs, "r")) options.r = binWidth / 2;
3333

34-
return initializer(remainingOptions, (data, facets, {x: X, y: Y, z: Z, fill: F, stroke: S, symbol: Q}, scales) => {
34+
return initializer(options, (data, facets, {x: X, y: Y, z: Z, fill: F, stroke: S, symbol: Q}, scales, _, context) => {
3535
if (X === undefined) throw new Error("missing channel: x");
3636
if (Y === undefined) throw new Error("missing channel: y");
3737

38-
// Coerce the X and Y channels to numbers (so that null is properly treated
39-
// as an undefined value rather than being coerced to zero).
40-
X = coerceNumbers(valueof(X.value, scales[X.scale] || identity));
41-
Y = coerceNumbers(valueof(Y.value, scales[Y.scale] || identity));
38+
// Extract the scaled (or projected!) values for the x and y channels.
39+
({x: X, y: Y} = valueObject({x: X, y: Y}, scales, context));
40+
41+
// Coerce the x and y channels to numbers (so that null is properly
42+
// treated as an undefined value rather than being coerced to zero).
43+
X = coerceNumbers(X);
44+
Y = coerceNumbers(Y);
4245

4346
// Extract the values for channels that are eligible for grouping; not all
4447
// marks define a z channel, so compute one if it not already computed. If z

0 commit comments

Comments
 (0)