From 086cf6014d5aa8212cbe40da3bdee21107508e77 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 21 Mar 2021 11:31:36 -0700 Subject: [PATCH 1/5] improve group --- .eslintrc.json | 1 + src/mark.js | 4 +- src/transforms/group.js | 173 ++++++++++++---------------- test/output/penguinSpeciesGroup.svg | 57 +++++++++ test/plots/index.js | 1 + test/plots/penguin-species-group.js | 15 +++ 6 files changed, 149 insertions(+), 102 deletions(-) create mode 100644 test/output/penguinSpeciesGroup.svg create mode 100644 test/plots/penguin-species-group.js diff --git a/.eslintrc.json b/.eslintrc.json index 3b9bd641e4..48aa23da01 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,6 +12,7 @@ "rules": { "no-cond-assign": 0, "no-constant-condition": 0, + "no-sparse-arrays": 0, "no-unexpected-multiline": 0, "comma-dangle": ["error", "never"], "semi": [2, "always"] diff --git a/src/mark.js b/src/mark.js index bdc2792c0f..8791c6ccae 100644 --- a/src/mark.js +++ b/src/mark.js @@ -230,7 +230,9 @@ export function mid(x1, x2) { // This distinguishes between per-dimension options and a standalone value. export function maybeValue(value) { - return typeof value === "undefined" || (value && value.toString === objectToString) ? value : {value}; + return value === undefined || (value && + value.toString === objectToString && + typeof value.transform !== "function") ? value : {value}; } function compose(t1, t2) { diff --git a/src/transforms/group.js b/src/transforms/group.js index 7dd4dd9911..ed40125837 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -2,59 +2,87 @@ import {group as grouper, sort, sum, InternSet} from "d3"; import {defined, firstof} from "../defined.js"; import {valueof, maybeColor, maybeTransform, maybeValue, maybeLazyChannel, lazyChannel, first, identity, take, maybeTuple, labelof} from "../mark.js"; -// Group on y, z, fill, or stroke, if any, then group on x. -export function groupX({x, y, out = y == null ? "y" : "fill", ...options} = {}) { - const [transform, X, l] = group1(x, "y", {y, ...options}); - return {x: X, ...transform, [out]: l}; +// Group on z, fill, or stroke, if any, then on x (optionally). +export function groupX({out = "y", ...options} = {}) { + const {x = identity} = options; + if (x == null) { + const [transform, L] = group2(null, null, options); + return {...transform, [out]: L}; + } + const [transform, L, X] = group2(x, null, options); + return {...transform, x: X, [out]: L}; } -// Group on x, z, fill, or stroke, if any, then group on y. -export function groupY({y, x, out = x == null ? "x" : "fill", ...options} = {}) { - const [transform, Y, l] = group1(y, "x", {x, ...options}); - return {y: Y, ...transform, [out]: l}; +// Group on z, fill, or stroke, if any, then on y (optionally). +export function groupY({out = "x", ...options} = {}) { + const {y = identity} = options; + if (y == null) { + const [transform, L] = group2(null, null, options); + return {...transform, [out]: L}; + } + const [transform, L, Y] = group2(y, null, options); + return {...transform, y: Y, [out]: L}; } -// Group on z, fill, or stroke, if any. +// Group on z, fill, or stroke, if any, then on x and y (optionally). export function groupR(options) { return group({...options, out: "r"}); } -export function group({x, y, out = "fill", ...options} = {}) { - const [transform, X, Y, L] = group2(x, y, options); - return {x: X, y: Y, ...transform, [out]: L}; +// Group on z, fill, or stroke, if any, then on x and y (optionally). +export function group({out = "fill", ...options} = {}) { + let {x, y} = options; + ([x, y] = maybeTuple(x, y)); + if (x == null && y == null) { + const [transform, L] = group2(null, null, options); + return {...transform, [out]: L}; + } + if (x == null) { + const [transform, L, X] = group2(x, null, options); + return {...transform, x: X, [out]: L}; + } + if (y == null) { + const [transform, L, Y] = group2(y, null, options); + return {...transform, y: Y, [out]: L}; + } + const [transform, L, X, Y] = group2(x, y, options); + return {...transform, x: X, y: Y, [out]: L}; } -function group1(x = identity, key, {[key]: k, weight, domain, normalize, z, fill, stroke, ...options} = {}) { - const m = normalize === true || normalize === "z" ? 100 : +normalize; - const [X, setX] = lazyChannel(x); - const [L, setL] = lazyChannel(`${labelof(weight, "Frequency")}${m === 100 ? " (%)" : ""}`); +function group2(xv, yv, {z, fill, stroke, weight, domain, normalize, ...options} = {}) { + let {value: x, domain: xdomain} = {domain, ...maybeValue(xv)}; + let {value: y, domain: ydomain} = {domain, ...maybeValue(yv)}; + ([x, y] = maybeTuple(x, y)); + const m = maybeNormalize(normalize); + const [BL, setBL] = lazyChannel(`${labelof(weight, "Frequency")}${m === 100 ? " (%)" : ""}`); + const [BX, setBX] = maybeLazyChannel(x); + const [BY, setBY] = maybeLazyChannel(y); + const [BZ, setBZ] = maybeLazyChannel(z); const [vfill] = maybeColor(fill); const [vstroke] = maybeColor(stroke); - const [BK, setBK] = maybeLazyChannel(k); - const [BZ, setBZ] = maybeLazyChannel(z); const [BF = fill, setBF] = maybeLazyChannel(vfill); const [BS = stroke, setBS] = maybeLazyChannel(vstroke); - const defined = maybeDomain(domain); + const xdefined = BX && maybeDomain(xdomain); + const ydefined = BY && maybeDomain(ydomain); return [ { - ...key && {[key]: BK}, z: BZ, fill: BF, stroke: BS, ...options, transform: maybeTransform(options, (data, facets) => { const X = valueof(data, x); - const K = valueof(data, k); + const Y = valueof(data, y); const Z = valueof(data, z); const F = valueof(data, vfill); const S = valueof(data, vstroke); const W = valueof(data, weight); const groupFacets = []; const groupData = []; - const BX = setX([]); - const L = setL([]); - const G = firstof(K, Z, F, S); - const BK = K && setBK([]); + const G = firstof(Z, F, S); + const BL = setBL([]); + const BX = X && setBX([]); + const BY = Y && setBY([]); const BZ = Z && setBZ([]); const BF = F && setBF([]); const BS = S && setBS([]); @@ -62,84 +90,17 @@ function group1(x = identity, key, {[key]: k, weight, domain, normalize, z, fill let i = 0; for (const facet of facets) { const groupFacet = []; + if (normalize === "facet") n = W ? sum(facet, i => W[i]) : facet.length; for (const I of G ? grouper(facet, i => G[i]).values() : [facet]) { if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; - for (const [x, f] of sort(grouper(I, i => X[i]), first)) { - if (!defined(x)) continue; - const l = W ? sum(f, i => W[i]) : f.length; - groupFacet.push(i++); - groupData.push(take(data, f)); - BX.push(x); - L.push(m ? l * m / n : l); - if (K) BK.push(K[f[0]]); - if (Z) BZ.push(Z[f[0]]); - if (F) BF.push(F[f[0]]); - if (S) BS.push(S[f[0]]); - } - } - groupFacets.push(groupFacet); - } - return {data: groupData, facets: groupFacets}; - }) - }, - X, - L - ]; -} - -function group2(xv, yv, {z, fill, stroke, weight, domain, normalize, ...options} = {}) { - let {value: x, domain: xdomain} = {domain, ...maybeValue(xv)}; - let {value: y, domain: ydomain} = {domain, ...maybeValue(yv)}; - ([x, y] = maybeTuple(x, y)); - const m = normalize === true || normalize === "z" ? 100 : +normalize; - const [X, setX] = lazyChannel(x); - const [Y, setY] = lazyChannel(y); - const [L, setL] = lazyChannel(`${labelof(weight, "Frequency")}${m === 100 ? " (%)" : ""}`); - const [Z, setZ] = maybeLazyChannel(z); - const [vfill] = maybeColor(fill); - const [vstroke] = maybeColor(stroke); - const [F = fill, setF] = maybeLazyChannel(vfill); - const [S = stroke, setS] = maybeLazyChannel(vstroke); - const xdefined = maybeDomain(xdomain); - const ydefined = maybeDomain(ydomain); - return [ - { - z: Z, - fill: F, - stroke: S, - ...options, - transform: maybeTransform(options, (data, facets) => { - const X = valueof(data, x); - const Y = valueof(data, y); - const Z = valueof(data, z); - const F = valueof(data, vfill); - const S = valueof(data, vstroke); - const W = valueof(data, weight); - const groupFacets = []; - const groupData = []; - const G = firstof(Z, F, S); - const BX = setX([]); - const BY = setY([]); - const BL = setL([]); - const BZ = Z && setZ([]); - const BF = F && setF([]); - const BS = S && setS([]); - let n = W ? sum(W) : data.length; - let i = 0; - for (const facet of facets) { - const groupFacet = []; - for (const I of G ? grouper(facet, i => G[i]).values() : [facet]) { - if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; - for (const [y, fy] of sort(grouper(I, i => Y[i]), first)) { - if (!ydefined(y)) continue; - for (const [x, f] of sort(grouper(fy, i => X[i]), first)) { - if (!xdefined(x)) continue; + for (const [y, fy] of Y ? sort(grouper(I, i => Y[i]), first).filter(ydefined) : [[, I]]) { + for (const [x, f] of X ? sort(grouper(fy, i => X[i]), first).filter(xdefined) : [[, fy]]) { const l = W ? sum(f, i => W[i]) : f.length; groupFacet.push(i++); groupData.push(take(data, f)); - BX.push(x); - BY.push(y); BL.push(m ? l * m / n : l); + if (X) BX.push(x); + if (Y) BY.push(y); if (Z) BZ.push(Z[f[0]]); if (F) BF.push(F[f[0]]); if (S) BS.push(S[f[0]]); @@ -151,9 +112,9 @@ function group2(xv, yv, {z, fill, stroke, weight, domain, normalize, ...options} return {data: groupData, facets: groupFacets}; }) }, - X, - Y, - L + BL, + BX, + BY ]; } @@ -161,5 +122,15 @@ function maybeDomain(domain) { if (domain === undefined) return defined; if (domain === null) return () => false; domain = new InternSet(domain); - return value => domain.has(value); + return ([key]) => domain.has(key); +} + +function maybeNormalize(normalize) { + if (!normalize) return; + if (normalize === true) return 100; + if (typeof normalize === "number") return normalize; + switch ((normalize + "").toLowerCase()) { + case "facet": case "z": return 100; + } + throw new Error("invalid normalize"); } diff --git a/test/output/penguinSpeciesGroup.svg b/test/output/penguinSpeciesGroup.svg new file mode 100644 index 0000000000..a159847956 --- /dev/null +++ b/test/output/penguinSpeciesGroup.svg @@ -0,0 +1,57 @@ + + + + + 0 + + + + 10 + + + + 20 + + + + 30 + + + + 40 + + + + 50 + + + + 60 + + + + 70 + + + + 80 + + + + 90 + + + + 100 + Frequency (%) → + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/index.js b/test/plots/index.js index 0665482814..130cf515bc 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -51,6 +51,7 @@ export {default as penguinMassSex} from "./penguin-mass-sex.js"; export {default as penguinMassSexSpecies} from "./penguin-mass-sex-species.js"; export {default as penguinMassSpecies} from "./penguin-mass-species.js"; export {default as penguinSexMassCulmenSpecies} from "./penguin-sex-mass-culmen-species.js"; +export {default as penguinSpeciesGroup} from "./penguin-species-group.js"; export {default as penguinSpeciesIsland} from "./penguin-species-island.js"; export {default as policeDeaths} from "./police-deaths.js"; export {default as policeDeathsBar} from "./police-deaths-bar.js"; diff --git a/test/plots/penguin-species-group.js b/test/plots/penguin-species-group.js new file mode 100644 index 0000000000..eaee717b22 --- /dev/null +++ b/test/plots/penguin-species-group.js @@ -0,0 +1,15 @@ +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: { + grid: true + }, + marks: [ + Plot.barX(penguins, Plot.stackX(Plot.groupY({y: null, fill: "species", normalize: true}))), + Plot.ruleX([0, 100]) + ] + }); +} From d42f85205d6726584086212634668a9aaa98549c Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 21 Mar 2021 11:46:44 -0700 Subject: [PATCH 2/5] groupZ --- src/index.js | 2 +- src/transforms/group.js | 33 ++++++++++++++++++++++------- test/plots/penguin-species-group.js | 2 +- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/index.js b/src/index.js index 283568e6b3..5f646f3628 100644 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,7 @@ export {RuleX, RuleY, ruleX, ruleY} from "./marks/rule.js"; export {Text, text, textX, textY} from "./marks/text.js"; export {TickX, TickY, tickX, tickY} from "./marks/tick.js"; export {bin, binX, binY, binXMid, binYMid, binR} from "./transforms/bin.js"; -export {group, groupX, groupY, groupR} from "./transforms/group.js"; +export {group, groupX, groupY, groupR, groupZ, groupZX, groupZY, groupZR} from "./transforms/group.js"; export {normalizeX, normalizeY} from "./transforms/normalize.js"; export {map, mapX, mapY} from "./transforms/map.js"; export {reduce, reduceX, reduceY} from "./transforms/reduce.js"; diff --git a/src/transforms/group.js b/src/transforms/group.js index ed40125837..2e89d49f66 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -2,7 +2,13 @@ import {group as grouper, sort, sum, InternSet} from "d3"; import {defined, firstof} from "../defined.js"; import {valueof, maybeColor, maybeTransform, maybeValue, maybeLazyChannel, lazyChannel, first, identity, take, maybeTuple, labelof} from "../mark.js"; -// Group on z, fill, or stroke, if any, then on x (optionally). +// Group on {z, fill, stroke}. +export function groupZ({out = "fill", ...options} = {}) { + const [transform, L] = group2(null, null, options); + return {...transform, [out]: L}; +} + +// Group on {z, fill, stroke}, then on x (optionally). export function groupX({out = "y", ...options} = {}) { const {x = identity} = options; if (x == null) { @@ -13,7 +19,7 @@ export function groupX({out = "y", ...options} = {}) { return {...transform, x: X, [out]: L}; } -// Group on z, fill, or stroke, if any, then on y (optionally). +// Group on {z, fill, stroke}, then on y (optionally). export function groupY({out = "x", ...options} = {}) { const {y = identity} = options; if (y == null) { @@ -24,12 +30,7 @@ export function groupY({out = "x", ...options} = {}) { return {...transform, y: Y, [out]: L}; } -// Group on z, fill, or stroke, if any, then on x and y (optionally). -export function groupR(options) { - return group({...options, out: "r"}); -} - -// Group on z, fill, or stroke, if any, then on x and y (optionally). +// Group on {z, fill, stroke}, then on x and y (optionally). export function group({out = "fill", ...options} = {}) { let {x, y} = options; ([x, y] = maybeTuple(x, y)); @@ -49,6 +50,22 @@ export function group({out = "fill", ...options} = {}) { return {...transform, x: X, y: Y, [out]: L}; } +export function groupZX(options) { + return groupZ({...options, out: "x"}); +} + +export function groupZY(options) { + return groupZ({...options, out: "y"}); +} + +export function groupZR(options) { + return groupZ({...options, out: "r"}); +} + +export function groupR(options) { + return group({...options, out: "r"}); +} + function group2(xv, yv, {z, fill, stroke, weight, domain, normalize, ...options} = {}) { let {value: x, domain: xdomain} = {domain, ...maybeValue(xv)}; let {value: y, domain: ydomain} = {domain, ...maybeValue(yv)}; diff --git a/test/plots/penguin-species-group.js b/test/plots/penguin-species-group.js index eaee717b22..d6b8fdc034 100644 --- a/test/plots/penguin-species-group.js +++ b/test/plots/penguin-species-group.js @@ -8,7 +8,7 @@ export default async function() { grid: true }, marks: [ - Plot.barX(penguins, Plot.stackX(Plot.groupY({y: null, fill: "species", normalize: true}))), + Plot.barX(penguins, Plot.stackX(Plot.groupZX({fill: "species", normalize: true}))), Plot.ruleX([0, 100]) ] }); From 5bebd4cd0c548f51dcd3d29040224eae551c1ac8 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 22 Mar 2021 14:09:04 -0700 Subject: [PATCH 3/5] simplify --- src/transforms/group.js | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 2e89d49f66..5343750d35 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -9,43 +9,20 @@ export function groupZ({out = "fill", ...options} = {}) { } // Group on {z, fill, stroke}, then on x (optionally). -export function groupX({out = "y", ...options} = {}) { - const {x = identity} = options; - if (x == null) { - const [transform, L] = group2(null, null, options); - return {...transform, [out]: L}; - } +export function groupX({x = identity, out = "y", ...options} = {}) { const [transform, L, X] = group2(x, null, options); return {...transform, x: X, [out]: L}; } // Group on {z, fill, stroke}, then on y (optionally). -export function groupY({out = "x", ...options} = {}) { - const {y = identity} = options; - if (y == null) { - const [transform, L] = group2(null, null, options); - return {...transform, [out]: L}; - } +export function groupY({y = identity, out = "x", ...options} = {}) { const [transform, L, Y] = group2(y, null, options); return {...transform, y: Y, [out]: L}; } // Group on {z, fill, stroke}, then on x and y (optionally). -export function group({out = "fill", ...options} = {}) { - let {x, y} = options; +export function group({x, y, out = "fill", ...options} = {}) { ([x, y] = maybeTuple(x, y)); - if (x == null && y == null) { - const [transform, L] = group2(null, null, options); - return {...transform, [out]: L}; - } - if (x == null) { - const [transform, L, X] = group2(x, null, options); - return {...transform, x: X, [out]: L}; - } - if (y == null) { - const [transform, L, Y] = group2(y, null, options); - return {...transform, y: Y, [out]: L}; - } const [transform, L, X, Y] = group2(x, y, options); return {...transform, x: X, y: Y, [out]: L}; } From 6735c91e094725273996d778839f0f96e19c5968 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 22 Mar 2021 14:09:50 -0700 Subject: [PATCH 4/5] reorder --- src/transforms/group.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 5343750d35..37d9419a27 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -8,6 +8,18 @@ export function groupZ({out = "fill", ...options} = {}) { return {...transform, [out]: L}; } +export function groupZX(options) { + return groupZ({...options, out: "x"}); +} + +export function groupZY(options) { + return groupZ({...options, out: "y"}); +} + +export function groupZR(options) { + return groupZ({...options, out: "r"}); +} + // Group on {z, fill, stroke}, then on x (optionally). export function groupX({x = identity, out = "y", ...options} = {}) { const [transform, L, X] = group2(x, null, options); @@ -27,18 +39,6 @@ export function group({x, y, out = "fill", ...options} = {}) { return {...transform, x: X, y: Y, [out]: L}; } -export function groupZX(options) { - return groupZ({...options, out: "x"}); -} - -export function groupZY(options) { - return groupZ({...options, out: "y"}); -} - -export function groupZR(options) { - return groupZ({...options, out: "r"}); -} - export function groupR(options) { return group({...options, out: "r"}); } From 02baf4a82436ad1b555dbd42b683d8ced5e98108 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 22 Mar 2021 14:13:03 -0700 Subject: [PATCH 5/5] cleaner --- src/transforms/group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index 37d9419a27..695a3f88eb 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -28,7 +28,7 @@ export function groupX({x = identity, out = "y", ...options} = {}) { // Group on {z, fill, stroke}, then on y (optionally). export function groupY({y = identity, out = "x", ...options} = {}) { - const [transform, L, Y] = group2(y, null, options); + const [transform, L,, Y] = group2(null, y, options); return {...transform, y: Y, [out]: L}; }