From 24498f04046c59badc9a7d554ed7a789291f215c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 24 Mar 2021 15:07:19 +0100 Subject: [PATCH 1/4] compute the scaled coordinates of bars (and cell) before filtering, allowing NaN, null and undefined as (ordinal) classes fixes #52 fixes #45 --- src/marks/bar.js | 31 ++++-- src/transforms/group.js | 16 ++- test/output/penguinSpeciesIslandSex.svg | 122 +++++++++++++++++++++++ test/plots/index.js | 1 + test/plots/penguin-species-island-sex.js | 29 ++++++ 5 files changed, 179 insertions(+), 20 deletions(-) create mode 100644 test/output/penguinSpeciesIslandSex.svg create mode 100644 test/plots/penguin-species-island-sex.js diff --git a/src/marks/bar.js b/src/marks/bar.js index 57ff15a27c..241de10795 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -48,7 +48,11 @@ export class AbstractBar extends Mark { const {rx, ry} = this; const {color} = scales; const {z: Z, title: L, fill: F, stroke: S} = channels; - const index = filter(I, ...this._positions(channels), F, S); + const [X, vx] = maybeCoords(this._x(scales, channels, dimensions), I); + const [Y, vy] = maybeCoords(this._y(scales, channels, dimensions), I); + const [W, vw] = maybeCoords(this._width(scales, channels, dimensions), I); + const [H, vh] = maybeCoords(this._height(scales, channels, dimensions), I); + const index = filter(I, X, Y, W, H, F, S); if (Z) index.sort((i, j) => ascending(Z[i], Z[j])); return create("svg:g") .call(applyIndirectStyles, this) @@ -57,10 +61,10 @@ export class AbstractBar extends Mark { .data(index) .join("rect") .call(applyDirectStyles, this) - .attr("x", this._x(scales, channels, dimensions)) - .attr("width", this._width(scales, channels, dimensions)) - .attr("y", this._y(scales, channels, dimensions)) - .attr("height", this._height(scales, channels, dimensions)) + .attr("x", X ? i => X[i] : vx) + .attr("width", W ? i => W[i] : vw) + .attr("y", Y ? i => Y[i] : vy) + .attr("height", H ? i => H[i] : vh) .attr("fill", F && (i => color(F[i]))) .attr("stroke", S && (i => color(S[i]))) .call(rx != null ? rect => rect.attr("rx", rx) : () => {}) @@ -88,6 +92,17 @@ export class AbstractBar extends Mark { } } +function maybeCoords(x, I) { + if (typeof x === "function") { + const X = []; + for (const i of I) { + X[i] = x(i); + } + return [X, undefined]; + } + return [undefined, x]; +} + export class BarX extends AbstractBar { constructor(data, {x1, x2, y, ...options} = {}) { super( @@ -103,9 +118,6 @@ export class BarX extends AbstractBar { _transform(selection, {x}) { selection.call(applyTransform, x, null); } - _positions({x1: X1, x2: X2, y: Y}) { - return [X1, X2, Y]; - } _x({x}, {x1: X1, x2: X2}) { const {insetLeft} = this; return i => Math.min(x(X1[i]), x(X2[i])) + insetLeft; @@ -131,9 +143,6 @@ export class BarY extends AbstractBar { _transform(selection, {y}) { selection.call(applyTransform, null, y); } - _positions({y1: Y1, y2: Y2, x: X}) { - return [Y1, Y2, X]; - } _y({y}, {y1: Y1, y2: Y2}) { const {insetTop} = this; return i => Math.min(y(Y1[i]), y(Y2[i])) + insetTop; diff --git a/src/transforms/group.js b/src/transforms/group.js index 5f89f41ff0..db419d32d0 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -1,5 +1,5 @@ import {group as grouper, sort, sum, InternSet} from "d3"; -import {defined, firstof} from "../defined.js"; +import {firstof} from "../defined.js"; import {valueof, maybeColor, maybeTransform, maybeValue, maybeLazyChannel, lazyChannel, first, identity, take, maybeTuple, labelof} from "../mark.js"; // Group on {z, fill, stroke}. @@ -85,7 +85,7 @@ function group2(xv, yv, {z, fill, stroke, weight, domain, normalize, ...options} for (const facet of facets) { const groupFacet = []; if (normalize === "facet") n = W ? sum(facet, i => W[i]) : facet.length; - for (const [, I] of groups(facet, G, defined1)) { + for (const [, I] of groups(facet, G)) { if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; for (const [y, fy] of groups(I, Y, ydefined)) { for (const [x, f] of groups(fy, X, xdefined)) { @@ -113,7 +113,7 @@ function group2(xv, yv, {z, fill, stroke, weight, domain, normalize, ...options} } function maybeDomain(domain) { - if (domain === undefined) return defined1; + if (domain === undefined) return () => true; if (domain === null) return () => false; domain = new InternSet(domain); return ([key]) => domain.has(key); @@ -129,10 +129,8 @@ function maybeNormalize(normalize) { throw new Error("invalid normalize"); } -function defined1([key]) { - return defined(key); -} - -export function groups(I, X, defined = defined1) { - return X ? sort(grouper(I, i => X[i]), first).filter(defined) : [[, I]]; +export function groups(I, X, defined) { + if (!X) return [[, I]]; + const G = grouper(I, i => X[i]); + return sort(defined ? Array.from(G).filter(defined) : G, first); } diff --git a/test/output/penguinSpeciesIslandSex.svg b/test/output/penguinSpeciesIslandSex.svg new file mode 100644 index 0000000000..90995efe37 --- /dev/null +++ b/test/output/penguinSpeciesIslandSex.svg @@ -0,0 +1,122 @@ + + + + 0 + + + + 10 + + + + 20 + + + + 30 + + + + 40 + + + + 50 + + + + 60 + + + + 70 + + ↑ Frequency + + + + Adelie + + + Gentoo + + + Chinstrap + species + + + + + + MALE + + + FEMALE + + + N/A + + + + + + + MALE + + + FEMALE + + + N/A + sex + + + + + + MALE + + + FEMALE + + + N/A + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/index.js b/test/plots/index.js index 130cf515bc..e5811620c1 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -53,6 +53,7 @@ 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 penguinSpeciesIslandSex} from "./penguin-species-island-sex.js"; export {default as policeDeaths} from "./police-deaths.js"; export {default as policeDeathsBar} from "./police-deaths-bar.js"; export {default as randomWalk} from "./random-walk.js"; diff --git a/test/plots/penguin-species-island-sex.js b/test/plots/penguin-species-island-sex.js new file mode 100644 index 0000000000..df409b2cd5 --- /dev/null +++ b/test/plots/penguin-species-island-sex.js @@ -0,0 +1,29 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function() { + const data = await d3.csv("data/penguins.csv", d3.autoType); + return Plot.plot({ + facet: { + data, + x: "species" + }, + fx: { + domain: d3.groupSort(data, ({length}) => length, d => d.species).reverse() + }, + x: { + domain: ["MALE", "FEMALE", null], + tickFormat: d => d === null ? "N/A" : d + }, + y: { + grid: true + }, + color: { + scheme: "greys" + }, + marks: [ + Plot.barY(data, Plot.stackY(Plot.groupX({x: "sex", fill: "island", stroke: "black"}))), + Plot.ruleY([0]) + ] + }); +} From 96afbf6394c4ee99d63c32909e02a73943352301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 24 Mar 2021 15:22:16 +0100 Subject: [PATCH 2/4] groups respect the domain option fixes #255 --- src/transforms/group.js | 10 ++++++---- test/transforms/group-test.js | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 test/transforms/group-test.js diff --git a/src/transforms/group.js b/src/transforms/group.js index db419d32d0..a6f678e3f2 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -87,8 +87,8 @@ function group2(xv, yv, {z, fill, stroke, weight, domain, normalize, ...options} if (normalize === "facet") n = W ? sum(facet, i => W[i]) : facet.length; for (const [, I] of groups(facet, G)) { if (normalize === "z") n = W ? sum(I, i => W[i]) : I.length; - for (const [y, fy] of groups(I, Y, ydefined)) { - for (const [x, f] of groups(fy, X, xdefined)) { + for (const [y, fy] of groups(I, Y, ydefined, ydomain)) { + for (const [x, f] of groups(fy, X, xdefined, xdomain)) { const l = W ? sum(f, i => W[i]) : f.length; groupFacet.push(i++); groupData.push(take(data, f)); @@ -129,8 +129,10 @@ function maybeNormalize(normalize) { throw new Error("invalid normalize"); } -export function groups(I, X, defined) { +export function groups(I, X, defined, domain) { if (!X) return [[, I]]; const G = grouper(I, i => X[i]); - return sort(defined ? Array.from(G).filter(defined) : G, first); + return domain + ? domain.map(x => [x, G.has(x) ? G.get(x) : []]) + : sort(defined ? Array.from(G).filter(defined) : G, first); } diff --git a/test/transforms/group-test.js b/test/transforms/group-test.js new file mode 100644 index 0000000000..e06397533f --- /dev/null +++ b/test/transforms/group-test.js @@ -0,0 +1,36 @@ +import * as Plot from "@observablehq/plot"; +import tape from "tape-await"; + +tape("groupX respects the domain option (#255)", test => { + const data = ["A", "A", "C"]; + const options = {x: d => d, domain: ["C", "B", "A"]}; + const mark = Plot.dot(data, Plot.groupX(options)); + const A = mark.initialize(); + test.deepEqual(A.index, [0, 1, 2]); + test.deepEqual(A.channels.find(d => d[0] === "x")[1].value, ["C", "B", "A"]); + test.deepEqual(A.channels.find(d => d[0] === "y")[1].value, [1, 0, 2]); +}); + +tape("groupY respects the domain option (#255)", test => { + const data = ["A", "A", "C"]; + const options = {x: d => d, domain: ["C", "B", "A"]}; + const mark = Plot.dot(data, Plot.groupY(options)); + const A = mark.initialize(); + test.deepEqual(A.index, [0, 1, 2]); + test.deepEqual(A.channels.find(d => d[0] === "y")[1].value, ["C", "B", "A"]); + test.deepEqual(A.channels.find(d => d[0] === "x")[1].value, [1, 0, 2]); +}); + +tape("group respects the domain option (#255)", test => { + const data = ["A", "A", "C", "A", "C"]; + const options = {x: d => d, y: d => d, domain: ["C", "B", "A"]}; + const mark = Plot.dot(data, Plot.group(options)); + const A = mark.initialize(); + test.deepEqual(A.index, [0, 1, 2, 3, 4, 5, 6, 7, 8]); + test.deepEqual(A.channels.find(d => d[0] === "fill")[1].value, [ + //C, B, A + 2, 0, 0, // C + 0, 0, 0, // B + 0, 0, 3 // A + ]); +}); From 47e587f81d8c10dda0caf88288a9948826aca965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 24 Mar 2021 16:14:19 +0100 Subject: [PATCH 3/4] Update src/transforms/group.js Co-authored-by: Mike Bostock --- 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 a6f678e3f2..e5a1be65fd 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -113,7 +113,7 @@ function group2(xv, yv, {z, fill, stroke, weight, domain, normalize, ...options} } function maybeDomain(domain) { - if (domain === undefined) return () => true; + if (domain === undefined) return; if (domain === null) return () => false; domain = new InternSet(domain); return ([key]) => domain.has(key); From 79221f1f293e22c110158823e10b99da9c7a8964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 24 Mar 2021 17:44:27 +0100 Subject: [PATCH 4/4] rename filter --- src/transforms/group.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transforms/group.js b/src/transforms/group.js index e5a1be65fd..561f57ea91 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -129,10 +129,10 @@ function maybeNormalize(normalize) { throw new Error("invalid normalize"); } -export function groups(I, X, defined, domain) { +export function groups(I, X, filter, domain) { if (!X) return [[, I]]; const G = grouper(I, i => X[i]); return domain ? domain.map(x => [x, G.has(x) ? G.get(x) : []]) - : sort(defined ? Array.from(G).filter(defined) : G, first); + : sort(filter ? Array.from(G).filter(filter) : G, first); }