Skip to content

Commit 027cdeb

Browse files
Filmbostock
authored andcommitted
Fix Plot.hexbin default reducer, and simplify (#884)
1 parent 5460c58 commit 027cdeb

File tree

6 files changed

+530
-250
lines changed

6 files changed

+530
-250
lines changed

src/transforms/hexbin.js

+24-22
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import {group} from "d3";
21
import {sqrt3} from "../symbols.js";
3-
import {identity, maybeColumn, maybeColorChannel, valueof} from "../options.js";
4-
import {hasOutput, maybeOutputs} from "./group.js";
2+
import {identity, maybeColorChannel, valueof} from "../options.js";
3+
import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js";
54
import {initialize} from "./initialize.js";
65

76
// We don’t want the hexagons to align with the edges of the plot frame, as that
@@ -18,45 +17,45 @@ export function hexbin(outputs = {fill: "count"}, options = {}) {
1817

1918
// TODO filter e.g. to show empty hexbins?
2019
// TODO disallow x, x1, x2, y, y1, y2 reducers?
21-
function hexbinn(outputs, {binWidth = 20, fill, stroke, z, ...options}) {
20+
function hexbinn(outputs, {binWidth = 20, z, fill, stroke, ...options}) {
2221
binWidth = +binWidth;
23-
const [GZ, setGZ] = maybeColumn(z);
2422
const [vfill] = maybeColorChannel(fill);
2523
const [vstroke] = maybeColorChannel(stroke);
26-
const [GF = fill, setGF] = maybeColumn(vfill);
27-
const [GS = stroke, setGS] = maybeColumn(vstroke);
28-
outputs = maybeOutputs({
29-
...setGF && {fill: "first"},
30-
...setGS && {stroke: "first"},
31-
...outputs
32-
}, {fill, stroke, ...options});
24+
outputs = maybeOutputs(outputs, {z, fill, stroke, ...options});
3325
return {
3426
symbol: "hexagon",
3527
...!hasOutput(outputs, "r") && {r: binWidth / 2},
36-
...!setGF && {fill},
37-
...((hasOutput(outputs, "fill") || setGF) && stroke === undefined) ? {stroke: "none"} : {stroke},
28+
...!hasOutput(outputs, "fill") && {fill},
29+
...((hasOutput(outputs, "fill") || vstroke != null) && stroke === undefined) ? {stroke: "none"} : {stroke},
3830
...initialize(options, function(data, facets, {x: X, y: Y}, scales) {
39-
if (setGF) setGF(valueof(data, vfill));
40-
if (setGS) setGS(valueof(data, vstroke));
41-
if (setGZ) setGZ(valueof(data, z));
42-
for (const o of outputs) o.initialize(data);
4331
if (X === undefined) throw new Error("missing channel: x");
4432
if (Y === undefined) throw new Error("missing channel: y");
4533
const x = X.scale !== undefined ? scales[X.scale] : identity.transform;
4634
const y = Y.scale !== undefined ? scales[Y.scale] : identity.transform;
4735
X = X.value.map(x);
4836
Y = Y.value.map(y);
49-
const F = setGF && GF.transform();
50-
const S = setGS && GS.transform();
51-
const Z = setGZ ? GZ.transform() : (F || S);
37+
const Z = valueof(data, z);
38+
const F = valueof(data, vfill);
39+
const S = valueof(data, vstroke);
40+
const G = maybeSubgroup(outputs, Z, F, S);
41+
if (Z && !outputs.find(r => r.name === "z")) {
42+
outputs.push(...maybeOutputs({z: "first"}, {z: Z}));
43+
}
44+
if (F && !outputs.find(r => r.name === "fill")) {
45+
outputs.push(...maybeOutputs({fill: "first"}, {fill: F}));
46+
}
47+
if (S && !outputs.find(r => r.name === "stroke")) {
48+
outputs.push(...maybeOutputs({stroke: "first"}, {stroke: S}));
49+
}
5250
const binFacets = [];
5351
const BX = [];
5452
const BY = [];
5553
let i = -1;
54+
for (const o of outputs) o.initialize(data);
5655
for (const facet of facets) {
5756
const binFacet = [];
5857
for (const o of outputs) o.scope("facet", facet);
59-
for (const index of Z ? group(facet, i => Z[i]).values() : [facet]) {
58+
for (const [, index] of maybeGroup(facet, G)) {
6059
for (const bin of hbin(index, X, Y, binWidth)) {
6160
binFacet.push(++i);
6261
BX.push(bin.x);
@@ -69,6 +68,9 @@ function hexbinn(outputs, {binWidth = 20, fill, stroke, z, ...options}) {
6968
const channels = {
7069
x: {value: BX},
7170
y: {value: BY},
71+
...Z && {z: {value: Z}},
72+
...F && {fill: {value: F, scale: true}},
73+
...S && {stroke: {value: S, scale: true}},
7274
...Object.fromEntries(outputs.map(({name, output}) => [name, {scale: true, binWidth: name === "r" ? binWidth : undefined, value: output.transform()}]))
7375
};
7476
if ("r" in channels) {

test/output/hexbinZ.html

+282
Large diffs are not rendered by default.

test/output/hexbinZ.svg renamed to test/output/hexbinZNull.svg

+191-219
Loading

test/plots/hexbin-z-null.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as Plot from "@observablehq/plot";
2+
import * as d3 from "d3";
3+
4+
export default async function() {
5+
const penguins = await d3.csv("data/penguins.csv", d3.autoType);
6+
return Plot.plot({
7+
x: {inset: 10},
8+
y: {inset: 10},
9+
marks: [
10+
Plot.frame(),
11+
Plot.hexgrid(),
12+
Plot.dot(penguins, Plot.hexbin({r: "count"}, {
13+
x: "culmen_depth_mm",
14+
y: "culmen_length_mm",
15+
stroke: "species",
16+
fill: "island",
17+
z: null,
18+
fillOpacity: 0.5,
19+
symbol: "dot"
20+
}))
21+
]
22+
});
23+
}

test/plots/hexbin-z.js

+9-9
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ export default async function() {
99
marks: [
1010
Plot.frame(),
1111
Plot.hexgrid(),
12-
Plot.dot(penguins, Plot.hexbin({r: "count"}, {
13-
x: "culmen_depth_mm",
14-
y: "culmen_length_mm",
15-
strokeWidth: 2,
16-
stroke: "sex",
17-
fill: "sex",
18-
fillOpacity: 0.5
19-
}))
20-
]
12+
Plot.dot(
13+
penguins,
14+
Plot.hexbin(
15+
{ r: "count" },
16+
{ x: "culmen_length_mm", y: "body_mass_g", stroke: "species", strokeOpacity: 0.8 }
17+
)
18+
)
19+
],
20+
color: {legend: true}
2121
});
2222
}

test/plots/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export {default as hexbinR} from "./hexbin-r.js";
7575
export {default as hexbinSymbol} from "./hexbin-symbol.js";
7676
export {default as hexbinText} from "./hexbin-text.js";
7777
export {default as hexbinZ} from "./hexbin-z.js";
78+
export {default as hexbinZNull} from "./hexbin-z-null.js";
7879
export {default as highCardinalityOrdinal} from "./high-cardinality-ordinal.js";
7980
export {default as identityScale} from "./identity-scale.js";
8081
export {default as industryUnemployment} from "./industry-unemployment.js";

0 commit comments

Comments
 (0)