Skip to content

Commit d98275a

Browse files
committed
compute the scaled coordinates of bars (and cell) before filtering, allowing NaN, null and undefined as (ordinal) classes
groups respect the domain option fixes #52 fixes #45 fixes #255 supersedes #271
2 parents f08e17f + 79221f1 commit d98275a

File tree

6 files changed

+228
-17
lines changed

6 files changed

+228
-17
lines changed

src/marks/bar.js

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ export class AbstractBar extends Mark {
4848
const {rx, ry} = this;
4949
const {color} = scales;
5050
const {z: Z, title: L, fill: F, stroke: S} = channels;
51-
const index = filter(I, ...this._positions(channels), F, S);
51+
const [X, vx] = maybeCoords(this._x(scales, channels, dimensions), I);
52+
const [Y, vy] = maybeCoords(this._y(scales, channels, dimensions), I);
53+
const [W, vw] = maybeCoords(this._width(scales, channels, dimensions), I);
54+
const [H, vh] = maybeCoords(this._height(scales, channels, dimensions), I);
55+
const index = filter(I, X, Y, W, H, F, S);
5256
if (Z) index.sort((i, j) => ascending(Z[i], Z[j]));
5357
return create("svg:g")
5458
.call(applyIndirectStyles, this)
@@ -57,10 +61,10 @@ export class AbstractBar extends Mark {
5761
.data(index)
5862
.join("rect")
5963
.call(applyDirectStyles, this)
60-
.attr("x", this._x(scales, channels, dimensions))
61-
.attr("width", this._width(scales, channels, dimensions))
62-
.attr("y", this._y(scales, channels, dimensions))
63-
.attr("height", this._height(scales, channels, dimensions))
64+
.attr("x", X ? i => X[i] : vx)
65+
.attr("width", W ? i => W[i] : vw)
66+
.attr("y", Y ? i => Y[i] : vy)
67+
.attr("height", H ? i => H[i] : vh)
6468
.attr("fill", F && (i => color(F[i])))
6569
.attr("stroke", S && (i => color(S[i])))
6670
.call(rx != null ? rect => rect.attr("rx", rx) : () => {})
@@ -88,6 +92,17 @@ export class AbstractBar extends Mark {
8892
}
8993
}
9094

95+
function maybeCoords(x, I) {
96+
if (typeof x === "function") {
97+
const X = [];
98+
for (const i of I) {
99+
X[i] = x(i);
100+
}
101+
return [X, undefined];
102+
}
103+
return [undefined, x];
104+
}
105+
91106
export class BarX extends AbstractBar {
92107
constructor(data, {x1, x2, y, ...options} = {}) {
93108
super(
@@ -103,9 +118,6 @@ export class BarX extends AbstractBar {
103118
_transform(selection, {x}) {
104119
selection.call(applyTransform, x, null);
105120
}
106-
_positions({x1: X1, x2: X2, y: Y}) {
107-
return [X1, X2, Y];
108-
}
109121
_x({x}, {x1: X1, x2: X2}) {
110122
const {insetLeft} = this;
111123
return i => Math.min(x(X1[i]), x(X2[i])) + insetLeft;
@@ -131,9 +143,6 @@ export class BarY extends AbstractBar {
131143
_transform(selection, {y}) {
132144
selection.call(applyTransform, null, y);
133145
}
134-
_positions({y1: Y1, y2: Y2, x: X}) {
135-
return [Y1, Y2, X];
136-
}
137146
_y({y}, {y1: Y1, y2: Y2}) {
138147
const {insetTop} = this;
139148
return i => Math.min(y(Y1[i]), y(Y2[i])) + insetTop;

src/transforms/group.js

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {group as grouper, sort, sum, deviation, min, max, mean, median, variance} from "d3";
1+
import {InternSet, group as grouper, sort, sum, deviation, min, max, mean, median, variance} from "d3";
22
import {firstof} from "../defined.js";
33
import {valueof, maybeColor, maybeInput, maybeTransform, maybeTuple, maybeLazyChannel, lazyChannel, first, identity, take, labelof, range} from "../mark.js";
44

@@ -34,7 +34,7 @@ function groupn(
3434
x, // optionally group on x
3535
y, // optionally group on y
3636
{data: reduceData = reduceIdentity, ...outputs} = {}, // output channel definitions
37-
inputs = {} // input channels and options
37+
{domain, xdomain = domain, ydomain = domain, ...inputs} = {} // input channels and options
3838
) {
3939
reduceData = maybeReduce(reduceData, identity);
4040
outputs = maybeOutputs(outputs, inputs);
@@ -53,6 +53,9 @@ function groupn(
5353
const [GF = fill, setGF] = maybeLazyChannel(vfill);
5454
const [GS = stroke, setGS] = maybeLazyChannel(vstroke);
5555

56+
// Only return groups that belong to the domain
57+
const xdefined = GX && maybeDomain(xdomain);
58+
const ydefined = GY && maybeDomain(ydomain);
5659
return {
5760
z: GZ,
5861
fill: GF,
@@ -82,8 +85,8 @@ function groupn(
8285
for (const o of outputs) o.scope("facet", facet);
8386
for (const [, I] of maybeGroup(facet, G)) {
8487
for (const o of outputs) o.scope("group", I);
85-
for (const [y, gg] of maybeGroup(I, Y)) {
86-
for (const [x, g] of maybeGroup(gg, X)) {
88+
for (const [y, gg] of maybeGroup(I, Y, ydefined, ydomain)) {
89+
for (const [x, g] of maybeGroup(gg, X, xdefined, xdomain)) {
8790
groupFacet.push(i++);
8891
groupData.push(reduceData.reduce(g, data));
8992
if (X) GX.push(x);
@@ -130,8 +133,12 @@ export function maybeOutputs(outputs, inputs) {
130133
});
131134
}
132135

133-
export function maybeGroup(I, X) {
134-
return X ? sort(grouper(I, i => X[i]), first) : [[, I]];
136+
export function maybeGroup(I, X, filter, domain) {
137+
if (!X) return [[, I]];
138+
const G = grouper(I, i => X[i]);
139+
return domain
140+
? domain.map(x => [x, G.has(x) ? G.get(x) : []])
141+
: sort(filter ? Array.from(G).filter(filter) : G, first);
135142
}
136143

137144
export function maybeReduce(reduce, value) {
@@ -211,3 +218,10 @@ function reduceProportion(value, scope) {
211218
? {scope, label: "Frequency", reduce: (I, V, basis = 1) => I.length / basis}
212219
: {scope, reduce: (I, V, basis = 1) => sum(I, i => V[i]) / basis};
213220
}
221+
222+
function maybeDomain(domain) {
223+
if (domain === undefined) return;
224+
if (domain === null) return () => false;
225+
domain = new InternSet(domain);
226+
return ([key]) => domain.has(key);
227+
}
Lines changed: 122 additions & 0 deletions
Loading

test/plots/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export {default as penguinSexMassCulmenSpecies} from "./penguin-sex-mass-culmen-
6161
export {default as penguinSpeciesGroup} from "./penguin-species-group.js";
6262
export {default as penguinSpeciesIsland} from "./penguin-species-island.js";
6363
export {default as penguinSpeciesIslandRelative} from "./penguin-species-island-relative.js";
64+
export {default as penguinSpeciesIslandSex} from "./penguin-species-island-sex.js";
6465
export {default as policeDeaths} from "./police-deaths.js";
6566
export {default as policeDeathsBar} from "./police-deaths-bar.js";
6667
export {default as randomWalk} from "./random-walk.js";
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as Plot from "@observablehq/plot";
2+
import * as d3 from "d3";
3+
4+
export default async function() {
5+
const data = await d3.csv("data/penguins.csv", d3.autoType);
6+
return Plot.plot({
7+
facet: {
8+
data,
9+
x: "species"
10+
},
11+
fx: {
12+
domain: d3.groupSort(data, ({length}) => length, d => d.species).reverse()
13+
},
14+
x: {
15+
domain: ["MALE", "FEMALE", null],
16+
tickFormat: d => d === null ? "N/A" : d
17+
},
18+
y: {
19+
grid: true
20+
},
21+
color: {
22+
scheme: "greys"
23+
},
24+
marks: [
25+
Plot.barY(data, Plot.stackY(Plot.groupX({y: "count"}, {x: "sex", fill: "island", stroke: "black"}))),
26+
Plot.ruleY([0])
27+
]
28+
});
29+
}

test/transforms/group-test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as Plot from "@observablehq/plot";
2+
import tape from "tape-await";
3+
4+
tape("groupX respects the domain option (#255)", test => {
5+
const data = ["A", "A", "C"];
6+
const options = {x: d => d, domain: ["C", "B", "A"]};
7+
const mark = Plot.dot(data, Plot.groupX({y: "count"}, options));
8+
const A = mark.initialize();
9+
test.deepEqual(A.index, [0, 1, 2]);
10+
test.deepEqual(A.channels.find(d => d[0] === "x")[1].value, ["C", "B", "A"]);
11+
test.deepEqual(A.channels.find(d => d[0] === "y")[1].value, [1, 0, 2]);
12+
});
13+
14+
tape("groupY respects the domain option (#255)", test => {
15+
const data = ["A", "A", "C"];
16+
const options = {x: d => d, domain: ["C", "B", "A"]};
17+
const mark = Plot.dot(data, Plot.groupY({x: "count"}, options));
18+
const A = mark.initialize();
19+
test.deepEqual(A.index, [0, 1, 2]);
20+
test.deepEqual(A.channels.find(d => d[0] === "y")[1].value, ["C", "B", "A"]);
21+
test.deepEqual(A.channels.find(d => d[0] === "x")[1].value, [1, 0, 2]);
22+
});
23+
24+
tape("group respects the domain option (#255)", test => {
25+
const data = ["A", "A", "C", "A", "C"];
26+
const options = {x: d => d, y: d => d, domain: ["C", "B", "A"]};
27+
const mark = Plot.dot(data, Plot.group({r: "count"}, options));
28+
const A = mark.initialize();
29+
test.deepEqual(A.index, [0, 1, 2, 3, 4, 5, 6, 7, 8]);
30+
test.deepEqual(A.channels.find(d => d[0] === "r")[1].value, [
31+
//C, B, A
32+
2, 0, 0, // C
33+
0, 0, 0, // B
34+
0, 0, 3 // A
35+
]);
36+
});

0 commit comments

Comments
 (0)