From 12795bbb80ded822d185f03a15ef4368e1a7c74d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 24 Mar 2021 12:25:01 +0100 Subject: [PATCH 1/2] =?UTF-8?q?An=20x-only=20plot=20should=20be=20taller?= =?UTF-8?q?=20if=20there=E2=80=99s=20an=20x-facet.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #125 --- src/plot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plot.js b/src/plot.js index 5ec84d2868..cc6f2bce26 100644 --- a/src/plot.js +++ b/src/plot.js @@ -95,7 +95,7 @@ export function plot(options = {}) { } function Dimensions( - {y, fy}, + {y, fy, fx}, { x: {axis: xAxis} = {}, y: {axis: yAxis} = {}, @@ -104,7 +104,7 @@ function Dimensions( }, { width = 640, - height = y || fy ? 396 : 60, + height = y || fy ? 396 : fx ? 90 : 60, facet: { marginTop: facetMarginTop = fxAxis === "top" ? 30 :0, marginRight: facetMarginRight = fyAxis === "right" ? 40 : 0, From 6dbe377b9077ca114b1c03dc854fc911a1596eea 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 2/2] 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]) + ] + }); +}