From 388c3d171b2548d1d11fedcbcea157c8a63dd4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 27 May 2022 13:30:05 +0200 Subject: [PATCH] Fix Plot.hexbin default reducer, and simplify --- src/transforms/hexbin.js | 46 ++- test/output/hexbinZ.html | 282 +++++++++++++ test/output/{hexbinZ.svg => hexbinZNull.svg} | 410 +++++++++---------- test/plots/hexbin-z-null.js | 23 ++ test/plots/hexbin-z.js | 18 +- test/plots/index.js | 1 + 6 files changed, 530 insertions(+), 250 deletions(-) create mode 100644 test/output/hexbinZ.html rename test/output/{hexbinZ.svg => hexbinZNull.svg} (58%) create mode 100644 test/plots/hexbin-z-null.js diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index e3ca03b827..acea8e2988 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -1,7 +1,6 @@ -import {group} from "d3"; import {sqrt3} from "../symbols.js"; -import {identity, maybeColumn, maybeColorChannel, valueof} from "../options.js"; -import {hasOutput, maybeOutputs} from "./group.js"; +import {identity, maybeColorChannel, valueof} from "../options.js"; +import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js"; import {initialize} from "./initialize.js"; // 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 = {}) { // TODO filter e.g. to show empty hexbins? // TODO disallow x, x1, x2, y, y1, y2 reducers? -function hexbinn(outputs, {binWidth = 20, fill, stroke, z, ...options}) { +function hexbinn(outputs, {binWidth = 20, z, fill, stroke, ...options}) { binWidth = +binWidth; - const [GZ, setGZ] = maybeColumn(z); const [vfill] = maybeColorChannel(fill); const [vstroke] = maybeColorChannel(stroke); - const [GF = fill, setGF] = maybeColumn(vfill); - const [GS = stroke, setGS] = maybeColumn(vstroke); - outputs = maybeOutputs({ - ...setGF && {fill: "first"}, - ...setGS && {stroke: "first"}, - ...outputs - }, {fill, stroke, ...options}); + outputs = maybeOutputs(outputs, {z, fill, stroke, ...options}); return { symbol: "hexagon", ...!hasOutput(outputs, "r") && {r: binWidth / 2}, - ...!setGF && {fill}, - ...((hasOutput(outputs, "fill") || setGF) && stroke === undefined) ? {stroke: "none"} : {stroke}, + ...!hasOutput(outputs, "fill") && {fill}, + ...((hasOutput(outputs, "fill") || vstroke != null) && stroke === undefined) ? {stroke: "none"} : {stroke}, ...initialize(options, function(data, facets, {x: X, y: Y}, scales) { - if (setGF) setGF(valueof(data, vfill)); - if (setGS) setGS(valueof(data, vstroke)); - if (setGZ) setGZ(valueof(data, z)); - for (const o of outputs) o.initialize(data); if (X === undefined) throw new Error("missing channel: x"); if (Y === undefined) throw new Error("missing channel: y"); const x = X.scale !== undefined ? scales[X.scale] : identity.transform; const y = Y.scale !== undefined ? scales[Y.scale] : identity.transform; X = X.value.map(x); Y = Y.value.map(y); - const F = setGF && GF.transform(); - const S = setGS && GS.transform(); - const Z = setGZ ? GZ.transform() : (F || S); + const Z = valueof(data, z); + const F = valueof(data, vfill); + const S = valueof(data, vstroke); + const G = maybeSubgroup(outputs, Z, F, S); + if (Z && !outputs.find(r => r.name === "z")) { + outputs.push(...maybeOutputs({z: "first"}, {z: Z})); + } + if (F && !outputs.find(r => r.name === "fill")) { + outputs.push(...maybeOutputs({fill: "first"}, {fill: F})); + } + if (S && !outputs.find(r => r.name === "stroke")) { + outputs.push(...maybeOutputs({stroke: "first"}, {stroke: S})); + } const binFacets = []; const BX = []; const BY = []; let i = -1; + for (const o of outputs) o.initialize(data); for (const facet of facets) { const binFacet = []; for (const o of outputs) o.scope("facet", facet); - for (const index of Z ? group(facet, i => Z[i]).values() : [facet]) { + for (const [, index] of maybeGroup(facet, G)) { for (const bin of hbin(index, X, Y, binWidth)) { binFacet.push(++i); BX.push(bin.x); @@ -69,6 +68,9 @@ function hexbinn(outputs, {binWidth = 20, fill, stroke, z, ...options}) { const channels = { x: {value: BX}, y: {value: BY}, + ...Z && {z: {value: Z}}, + ...F && {fill: {value: F, scale: true}}, + ...S && {stroke: {value: S, scale: true}}, ...Object.fromEntries(outputs.map(({name, output}) => [name, {scale: true, binWidth: name === "r" ? binWidth : undefined, value: output.transform()}])) }; if ("r" in channels) { diff --git a/test/output/hexbinZ.html b/test/output/hexbinZ.html new file mode 100644 index 0000000000..198c51cd95 --- /dev/null +++ b/test/output/hexbinZ.html @@ -0,0 +1,282 @@ +
+
+ AdelieChinstrapGentoo +
+ + + + 3,000 + + + 3,500 + + + 4,000 + + + 4,500 + + + 5,000 + + + 5,500 + + + 6,000 + ↑ body_mass_g + + + + 35 + + + 40 + + + 45 + + + 50 + + + 55 + culmen_length_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/output/hexbinZ.svg b/test/output/hexbinZNull.svg similarity index 58% rename from test/output/hexbinZ.svg rename to test/output/hexbinZNull.svg index f6bf1f9f9b..495effc6c0 100644 --- a/test/output/hexbinZ.svg +++ b/test/output/hexbinZNull.svg @@ -87,224 +87,196 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/hexbin-z-null.js b/test/plots/hexbin-z-null.js new file mode 100644 index 0000000000..3680146c8a --- /dev/null +++ b/test/plots/hexbin-z-null.js @@ -0,0 +1,23 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const penguins = await d3.csv("data/penguins.csv", d3.autoType); + return Plot.plot({ + x: {inset: 10}, + y: {inset: 10}, + marks: [ + Plot.frame(), + Plot.hexgrid(), + Plot.dot(penguins, Plot.hexbin({r: "count"}, { + x: "culmen_depth_mm", + y: "culmen_length_mm", + stroke: "species", + fill: "island", + z: null, + fillOpacity: 0.5, + symbol: "dot" + })) + ] + }); +} diff --git a/test/plots/hexbin-z.js b/test/plots/hexbin-z.js index 0ac30caff3..e6adc73976 100644 --- a/test/plots/hexbin-z.js +++ b/test/plots/hexbin-z.js @@ -9,14 +9,14 @@ export default async function() { marks: [ Plot.frame(), Plot.hexgrid(), - Plot.dot(penguins, Plot.hexbin({r: "count"}, { - x: "culmen_depth_mm", - y: "culmen_length_mm", - strokeWidth: 2, - stroke: "sex", - fill: "sex", - fillOpacity: 0.5 - })) - ] + Plot.dot( + penguins, + Plot.hexbin( + { r: "count" }, + { x: "culmen_length_mm", y: "body_mass_g", stroke: "species", strokeOpacity: 0.8 } + ) + ) + ], + color: {legend: true} }); } diff --git a/test/plots/index.js b/test/plots/index.js index 12f9611a39..1d49ca4d0f 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -75,6 +75,7 @@ export {default as hexbinR} from "./hexbin-r.js"; export {default as hexbinSymbol} from "./hexbin-symbol.js"; export {default as hexbinText} from "./hexbin-text.js"; export {default as hexbinZ} from "./hexbin-z.js"; +export {default as hexbinZNull} from "./hexbin-z-null.js"; export {default as highCardinalityOrdinal} from "./high-cardinality-ordinal.js"; export {default as identityScale} from "./identity-scale.js"; export {default as industryUnemployment} from "./industry-unemployment.js";