Skip to content

Commit 0863b76

Browse files
committed
facet
1 parent 10d2266 commit 0863b76

File tree

10 files changed

+113
-76
lines changed

10 files changed

+113
-76
lines changed

src/axes.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import {AxisX, AxisY} from "./marks/axis.js";
22

3-
export function Axes({x: xScale, y: yScale}, {x = {}, y = {}, grid} = {}) {
3+
export function Axes(
4+
{x: xScale, y: yScale, fx: fxScale, fy: fyScale},
5+
{x = {}, y = {}, fx = {}, fy = {}, grid} = {}
6+
) {
47
const {axis: xAxis = true} = x;
58
const {axis: yAxis = true} = y;
9+
const {axis: fxAxis = true} = fx;
10+
const {axis: fyAxis = true} = fy;
611
return {
712
x: xScale && xAxis ? new AxisX({grid, ...x}) : null,
8-
y: yScale && yAxis ? new AxisY({grid, ...y}) : null
13+
y: yScale && yAxis ? new AxisY({grid, ...y}) : null,
14+
fx: fxScale && fxAxis ? new AxisY({name: "fx", ...fx}) : null,
15+
fy: fyScale && fyAxis ? new AxisY({name: "fy", ...fy}) : null
916
};
1017
}
1118

src/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export {BarX, BarY, barX, barY} from "./marks/bar.js";
66
export {bin, binX, binY} from "./marks/bin.js";
77
export {Cell, cell} from "./marks/cell.js";
88
export {Dot, dot, dotX, dotY} from "./marks/dot.js";
9-
export {FacetY, facetY} from "./marks/facet.js";
109
export {group, groupX, groupY} from "./marks/group.js";
1110
export {Line, line, lineX, lineY} from "./marks/line.js";
1211
export {Link, link} from "./marks/link.js";

src/mark.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class Mark {
2121
});
2222
}
2323
initialize(data) {
24-
if (data !== undefined) data = this.transform(data);
24+
if (data !== undefined) data = this.transform(data, this.data);
2525
return {
2626
index: data === undefined ? undefined : Uint32Array.from(data, indexOf),
2727
channels: this.channels.map(channel => {

src/marks/axis.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {create} from "d3-selection";
44

55
export class AxisX {
66
constructor({
7+
name = "x",
78
axis = true,
89
ticks,
910
tickSize = 6,
@@ -13,6 +14,7 @@ export class AxisX {
1314
labelAnchor,
1415
labelOffset
1516
} = {}) {
17+
this.name = name;
1618
this.axis = axis = axis === true ? "bottom" : (axis + "").toLowerCase();
1719
if (!["top", "bottom"].includes(axis)) throw new Error(`invalid x-axis: ${axis}`);
1820
this.ticks = ticks;
@@ -25,7 +27,7 @@ export class AxisX {
2527
}
2628
render(
2729
index,
28-
{x},
30+
{[this.name]: x},
2931
channels,
3032
{width, height, marginTop, marginRight, marginBottom, marginLeft}
3133
) {
@@ -71,6 +73,7 @@ export class AxisX {
7173

7274
export class AxisY {
7375
constructor({
76+
name = "y",
7477
axis = true,
7578
ticks,
7679
tickSize = 6,
@@ -80,6 +83,7 @@ export class AxisY {
8083
labelAnchor,
8184
labelOffset
8285
} = {}) {
86+
this.name = name;
8387
this.axis = axis = axis === true ? "left" : (axis + "").toLowerCase();
8488
if (!["left", "right"].includes(axis)) throw new Error(`invalid y-axis: ${axis}`);
8589
this.ticks = ticks;
@@ -92,7 +96,7 @@ export class AxisY {
9296
}
9397
render(
9498
index,
95-
{y},
99+
{[this.name]: y},
96100
channels,
97101
{width, height, marginTop, marginRight, marginBottom, marginLeft}
98102
) {

src/marks/facet.js

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,26 @@ import {create} from "d3-selection";
33
import {Mark} from "../mark.js";
44
import {autoScaleRange} from "../scales.js";
55

6-
export class FacetY extends Mark {
6+
// TODO facet-x
7+
export class Facet extends Mark {
78
constructor(data, {y, transform} = {}, marks = []) {
89
super(
910
data,
1011
[
11-
{name: "y", value: y, scale: "y", type: "band"}
12+
{name: "fy", value: y, scale: "fy", type: "band"}
1213
],
1314
transform
1415
);
1516
this.marks = marks;
1617
this.facets = undefined; // set by initialize
1718
}
1819
initialize(data) {
19-
const {index, channels: [y]} = super.initialize(data);
20-
const [, {value: Y}] = y;
20+
const {index, channels: [fy]} = super.initialize(data);
21+
const [, {value: FY}] = fy;
2122
const subchannels = [];
2223
const facets = this.facets = new Map();
2324

24-
//
25-
for (const [facetKey, facetIndex] of group(index, i => Y[i])) {
25+
for (const [facetKey, facetIndex] of group(index, i => FY[i])) {
2626
const facetData = Array.from(facetIndex, i => data[i]);
2727
const markIndex = new Map();
2828
const markChannels = new Map();
@@ -33,43 +33,43 @@ export class FacetY extends Mark {
3333
const {index, channels} = mark.initialize(markData);
3434
for (const [name, channel] of channels) {
3535
if (name !== undefined) named[name] = channel.value;
36-
subchannels.push([undefined, facetYChannel(channel)]);
36+
subchannels.push([undefined, channel]);
3737
}
3838
markIndex.set(mark, index);
3939
markChannels.set(mark, named);
4040
}
4141
facets.set(facetKey, {markIndex, markChannels});
4242
}
4343

44-
return {index, channels: [y, ...subchannels]};
44+
return {index, channels: [fy, ...subchannels]};
4545
}
46-
render(index, {y, fy, ...scales}, channels, options) {
46+
render(index, scales, channels, options) {
4747
const {marks, facets} = this;
48-
const {marginRight, marginLeft, width} = options;
49-
const subscales = {y: fy, ...scales};
48+
const {fy} = scales;
49+
const {y, marginRight, marginLeft, width} = options;
5050

5151
const subdimensions = {
5252
marginTop: 0,
5353
marginRight,
5454
marginBottom: 0,
5555
marginLeft,
5656
width,
57-
height: y.bandwidth()
57+
height: fy.bandwidth()
5858
};
5959

60-
autoScaleRange({y: options.fy}, subdimensions);
60+
autoScaleRange({y}, subdimensions);
6161

6262
return create("svg:g")
6363
.call(g => g.selectAll()
64-
.data(y.domain())
64+
.data(fy.domain())
6565
.join("g")
66-
.attr("transform", (key) => `translate(0,${y(key)})`)
66+
.attr("transform", (key) => `translate(0,${fy(key)})`)
6767
.each(function(key) {
6868
const {markIndex, markChannels} = facets.get(key);
6969
for (const mark of marks) {
7070
const node = mark.render(
7171
markIndex.get(mark),
72-
subscales,
72+
scales,
7373
markChannels.get(mark),
7474
subdimensions
7575
);
@@ -79,11 +79,3 @@ export class FacetY extends Mark {
7979
.node();
8080
}
8181
}
82-
83-
export function facetY(data, options, marks) {
84-
return new FacetY(data, options, marks);
85-
}
86-
87-
function facetYChannel({scale, ...channel}) {
88-
return {...channel, scale: scale === "y" ? "fy" : scale};
89-
}

src/plot.js

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import {create} from "d3-selection";
22
import {Axes, autoAxisTicks, autoAxisLabels} from "./axes.js";
3+
import {Facet} from "./marks/facet.js";
34
import {Scales, autoScaleRange} from "./scales.js";
45

56
export function plot(options = {}) {
7+
const {facet} = options;
8+
9+
// When faceting, wrap all marks in a faceting mark.
10+
if (facet !== undefined) {
11+
const {marks} = options;
12+
const {data} = facet;
13+
options = {...options, marks: [new Facet(data, facet, marks)]};
14+
}
15+
616
const {
717
marks = [],
818
font = "10px sans-serif",
@@ -40,14 +50,41 @@ export function plot(options = {}) {
4050
const axes = Axes(scaleDescriptors, options);
4151
const dimensions = Dimensions(scaleDescriptors, axes, options);
4252

43-
autoScaleRange(scaleDescriptors, dimensions);
44-
autoAxisTicks(axes, dimensions);
45-
autoAxisLabels(scaleChannels, scaleDescriptors, axes, dimensions);
53+
// When faceting, layout fx and fy instead of x and y.
54+
// TODO cleaner
55+
if (facet !== undefined) {
56+
const x = scales.fx ? "fx" : "x";
57+
const y = scales.fy ? "fy" : "y";
58+
const facetScaleChannels = new Map([["x", scaleChannels.get(x)], ["y", scaleChannels.get(y)]]);
59+
const facetScaleDescriptors = {x: scaleDescriptors[x], y: scaleDescriptors[y]};
60+
const facetAxes = {x: axes[x], y: axes[y]};
61+
autoScaleRange(facetScaleDescriptors, dimensions);
62+
autoAxisTicks(facetAxes, dimensions);
63+
autoAxisLabels(facetScaleChannels, facetScaleDescriptors, facetAxes, dimensions);
64+
} else {
65+
autoScaleRange(scaleDescriptors, dimensions);
66+
autoAxisTicks(axes, dimensions);
67+
autoAxisLabels(scaleChannels, scaleDescriptors, axes, dimensions);
68+
}
4669

4770
// Normalize the options.
4871
options = {...scaleDescriptors, ...dimensions};
49-
if (axes.y) options.y = {...options.y, ...axes.y}, marks.unshift(axes.y);
50-
if (axes.x) options.x = {...options.x, ...axes.x}, marks.unshift(axes.x);
72+
if (axes.x) options.x = {...options.x, ...axes.x};
73+
if (axes.y) options.y = {...options.y, ...axes.y};
74+
if (axes.fx) options.fx = {...options.fx, ...axes.fx};
75+
if (axes.fy) options.fy = {...options.fy, ...axes.fy};
76+
77+
// When faceting, render axes for fx and fy instead of x and y.
78+
// TODO cleaner
79+
if (facet !== undefined) {
80+
const x = scales.fx ? "fx" : "x";
81+
const y = scales.fy ? "fy" : "y";
82+
if (axes[x]) marks.unshift(axes[x]);
83+
if (axes[y]) marks.unshift(axes[y]);
84+
} else {
85+
if (axes.x) marks.unshift(axes.x);
86+
if (axes.y) marks.unshift(axes.y);
87+
}
5188

5289
const {width, height} = dimensions;
5390

src/transforms/bin.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ export function bin1(options = {}) {
77
const bin = binner().value(value);
88
if (domain !== undefined) bin.domain(domain);
99
if (thresholds !== undefined) bin.thresholds(thresholds);
10-
return data => {
11-
const b = bin(data);
10+
return (facetData, data) => {
11+
let b;
1212
// We don’t want to choose thresholds dynamically for each facet; instead,
13-
// we extract the set of thresholds from an initial computation.
13+
// we extract the set of thresholds from an initial pass over all data.
1414
if (domain === undefined || thresholds === undefined) {
15+
b = bin(data);
1516
if (domain === undefined) bin.domain(domain = [b[0].x0, b[b.length - 1].x1]);
1617
if (thresholds === undefined) bin.thresholds(thresholds = b.slice(1).map(b => b.x0));
18+
if (facetData !== data) b = bin(facetData);
19+
} else {
20+
b = bin(facetData);
1721
}
1822
if (cumulative) {
1923
let sum = 0;

test/ballot-status-race.html

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,36 +56,34 @@
5656
});
5757

5858
document.body.appendChild(Plot.plot({
59+
marginLeft: 210,
5960
x: {
6061
grid: true,
6162
label: "Frequency (%) →"
6263
},
6364
y: {
65+
domain: ["ACCEPTED", "REJECTED", "PENDING"]
66+
},
67+
fy: {
6468
domain: rollup
6569
.filter(d => d.status === "ACCEPTED")
6670
.sort((a, b) => d3.descending(a.percent, b.percent))
6771
.map(d => d.race),
6872
label: null
6973
},
70-
fy: {
71-
domain: ["ACCEPTED", "REJECTED", "PENDING"]
72-
},
7374
color: {
7475
type: "ordinal",
7576
domain: ["ACCEPTED", "REJECTED", "PENDING"],
7677
range: ["currentColor", "brown", "gray"]
7778
},
79+
facet: {
80+
data: rollup,
81+
y: "race"
82+
},
7883
marks: [
79-
Plot.facetY(
80-
rollup,
81-
{y: "race"},
82-
[
83-
Plot.barX(rollup, {x: "percent", y: "status", fill: "status"}),
84-
Plot.ruleX([0])
85-
]
86-
)
87-
],
88-
marginLeft: 210
84+
Plot.barX(rollup, {x: "percent", y: "status", fill: "status"}),
85+
Plot.ruleX([0])
86+
]
8987
}));
9088
});
9189

test/metro-unemployment-ridgeline.html

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,30 @@
77

88
d3.csv("data/bls-metro-unemployment.csv", d3.autoType).then(data => {
99
document.body.appendChild(Plot.plot({
10+
width: 960,
11+
height: 1080,
12+
marginLeft: 300,
1013
x: {
1114
label: null
1215
},
1316
y: {
17+
range: [20, -40]
18+
},
19+
fy: {
1420
domain: d3.rollups(data, group => d3.max(group, d => d.unemployment), d => d.division)
1521
.sort(([, a], [, b]) => d3.descending(a, b))
1622
.map(([key]) => key),
1723
label: null
1824
},
19-
fy: {
20-
range: [20, -40]
25+
facet: {
26+
data,
27+
y: "division"
2128
},
2229
marks: [
23-
Plot.facetY(
24-
data,
25-
{y: "division"},
26-
[
27-
Plot.ruleY([0]),
28-
Plot.areaY(data, {x: "date", y: "unemployment", fill: "#eee"}),
29-
Plot.line(data, {x: "date", y: "unemployment"})
30-
]
31-
)
32-
],
33-
width: 960,
34-
height: 1080,
35-
marginLeft: 300
30+
Plot.areaY(data, {x: "date", y: "unemployment", fill: "#eee"}),
31+
Plot.line(data, {x: "date", y: "unemployment"}),
32+
Plot.ruleY([0])
33+
]
3634
}));
3735
});
3836

test/penguin-mass-sex.html

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,19 @@
77

88
d3.csv("data/penguins.csv", d3.autoType).then(data => {
99
document.body.appendChild(Plot.plot({
10+
marginLeft: 70,
1011
x: {
1112
round: true,
1213
label: "Body mass (g) →"
1314
},
15+
facet: {
16+
data,
17+
y: "sex"
18+
},
1419
marks: [
15-
Plot.facetY(
16-
data,
17-
{y: "sex"},
18-
[
19-
Plot.ruleY([0]),
20-
Plot.binX(data, {x: "body_mass_g"})
21-
]
22-
)
23-
],
24-
marginLeft: 70
20+
Plot.binX(data, {x: "body_mass_g"}),
21+
Plot.ruleY([0])
22+
]
2523
}));
2624
});
2725

0 commit comments

Comments
 (0)