facet bin! · Salebarn/plot@e4ec619 · GitHub
Skip to content

Commit e4ec619

Browse files
committed
facet bin!
1 parent 976457b commit e4ec619

5 files changed

Lines changed: 95 additions & 34 deletions

File tree

src/mark.js

Lines changed: 15 additions & 4 deletions

src/marks/bin.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@ import {bin} from "d3-array";
22
import {field, identity, zero} from "../mark.js";
33
import {Rect} from "./rect.js";
44

5-
// TODO configurable thresholds
6-
// TODO facet dimension
5+
// TODO Configurable thresholds
6+
// TODO What if value accessor is non-deterministic?
7+
// TODO What if data is not an array?
78
export class Bin extends Rect {
89
constructor(data, value, channels, style) {
910
super(data, channels, style);
10-
this.value = typeof value === "string" ? field(value) : value;
11+
// Because faceting may be in use, we don’t want d3.bin to choose thresholds
12+
// dynamically based on the input data; instead, we extract the set of
13+
// thresholds from an initial computation.
14+
this.bin = bin().value(typeof value === "string" ? field(value) : value);
15+
const bins = this.bin(data);
16+
this.bin.domain([bins[0].x0, bins[bins.length - 1].x1]);
17+
this.bin.thresholds(bins.slice(1).map(start));
1118
}
1219
initialize(data) {
13-
return super.initialize(bin().value(this.value)(data));
20+
return super.initialize(this.bin(data));
1421
}
1522
}
1623

src/marks/facet.js

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {group} from "d3-array";
22
import {create} from "d3-selection";
3-
import {Mark, indexOf} from "../mark.js";
3+
import {Mark} from "../mark.js";
44
import {autoScaleRange} from "../scales.js";
55

66
export class FacetY extends Mark {
@@ -12,32 +12,66 @@ export class FacetY extends Mark {
1212
]
1313
);
1414
this.marks = marks;
15+
this.facets = undefined; // set by initialize
1516
}
16-
initialize() {
17-
return [
18-
...super.initialize(),
19-
...this.marks.flatMap(m => m.initialize().map(facetYChannel))
20-
];
17+
initialize(data) {
18+
const {index, channels: [y]} = super.initialize(data);
19+
const [, {value: Y}] = y;
20+
const subchannels = [];
21+
const facets = this.facets = new Map();
22+
23+
//
24+
for (const [facetKey, facetIndex] of group(index, (d, i) => Y[i])) {
25+
const facetData = Array.from(facetIndex, i => data[i]);
26+
const markIndex = new Map();
27+
const markChannels = new Map();
28+
for (const mark of this.marks) {
29+
if (markIndex.has(mark)) throw new Error("duplicate mark");
30+
const markData = mark.data === data ? facetData : mark.data;
31+
const named = Object.create(null);
32+
const {index, channels} = mark.initialize(markData);
33+
for (const [name, channel] of channels) {
34+
if (name !== undefined) named[name] = channel.value;
35+
subchannels.push([undefined, facetYChannel(channel)]);
36+
}
37+
markIndex.set(mark, index);
38+
markChannels.set(mark, named);
39+
}
40+
facets.set(facetKey, {markIndex, markChannels});
41+
}
42+
43+
return {index, channels: [y, ...subchannels]};
2144
}
22-
render(I, {y, fy, ...scales}, dimensions) {
23-
const {data, marks, channels: {y: {value: Y}}} = this;
45+
render(index, {y, fy, ...scales}, channels, options) {
46+
const {marks, facets} = this;
47+
const {marginRight, marginLeft, width} = options;
2448
const subscales = {y: fy, ...scales};
25-
const subdimensions = {...dimensions, marginTop: 0, marginBottom: 0, height: y.bandwidth()};
26-
const G = group(I, i => Y[i]);
2749

28-
autoScaleRange({y: fy}, subdimensions);
50+
const subdimensions = {
51+
marginTop: 0,
52+
marginRight,
53+
marginBottom: 0,
54+
marginLeft,
55+
width,
56+
height: y.bandwidth()
57+
};
58+
59+
autoScaleRange({y: options.scales.fy}, subdimensions);
2960

3061
return create("svg:g")
3162
.call(g => g.selectAll()
3263
.data(y.domain())
3364
.join("g")
3465
.attr("transform", (key) => `translate(0,${y(key)})`)
3566
.each(function(key) {
67+
const {markIndex, markChannels} = facets.get(key);
3668
for (const mark of marks) {
37-
const index = mark.data === data ? G.get(key)
38-
: mark.data === undefined ? undefined
39-
: Array.from(mark.data, indexOf);
40-
const node = mark.render(index, subscales, subdimensions);
69+
const node = mark.render(
70+
markIndex.get(mark),
71+
subscales,
72+
markChannels.get(mark),
73+
subdimensions
74+
);
4175
if (node != null) this.appendChild(node);
4276
}
4377
}))
@@ -49,7 +83,6 @@ export function facetY(data, channels, marks) {
4983
return new FacetY(data, channels, marks);
5084
}
5185

52-
function facetYChannel([name, {scale, ...channel}]) {
53-
name; // TODO Retrieve each mark’s channels.
54-
return [undefined, {...channel, scale: scale === "y" ? "fy" : scale}];
86+
function facetYChannel({scale, ...channel}) {
87+
return {...channel, scale: scale === "y" ? "fy" : scale};
5588
}

src/plot.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import {create} from "d3-selection";
22
import {Axes, autoAxisTicks, autoAxisLabels} from "./axes.js";
3-
import {indexOf} from "./mark.js";
43
import {Scales, autoScaleRange} from "./scales.js";
54

65
export function plot(options = {}) {
76
const {marks = []} = options;
87

98
// A Map from Mark instance to an object of named channel values.
109
const markChannels = new Map();
10+
const markIndex = new Map();
1111

1212
// A Map from scale name to an array of associated channels.
1313
const scaleChannels = new Map();
1414

1515
// Initialize the marks’ channels, indexing them by mark and scale as needed.
1616
for (const mark of marks) {
17+
if (markChannels.has(mark)) throw new Error("duplicate mark");
1718
const named = Object.create(null);
18-
for (const [name, channel] of mark.initialize(mark.data)) {
19+
const {index, channels} = mark.initialize(mark.data);
20+
for (const [name, channel] of channels) {
1921
if (name !== undefined) {
20-
if (name in named) throw new Error(`duplicate channel: ${name}`);
2122
named[name] = channel.value;
2223
}
2324
if (channel.scale !== undefined) {
@@ -26,8 +27,8 @@ export function plot(options = {}) {
2627
else scaleChannels.set(channel.scale, [channel]);
2728
}
2829
}
29-
if (markChannels.has(mark)) throw new Error("duplicate mark");
3030
markChannels.set(mark, named);
31+
markIndex.set(mark, index);
3132
}
3233

3334
const scaleDescriptors = Scales(scaleChannels, options.scales);
@@ -42,6 +43,7 @@ export function plot(options = {}) {
4243
if (axes.y) marks.unshift(axes.y);
4344
if (axes.x) marks.unshift(axes.x);
4445

46+
const plotOptions = {scales: scaleDescriptors, axes, ...dimensions};
4547
const {width, height} = dimensions;
4648

4749
const svg = create("svg")
@@ -51,9 +53,9 @@ export function plot(options = {}) {
5153
.style("background", "white");
5254

5355
for (const mark of marks) {
54-
const {data} = mark;
55-
const index = data === undefined ? undefined : Array.from(data, indexOf);
56-
const node = mark.render(index, scales, markChannels.get(mark), dimensions);
56+
const channels = markChannels.get(mark);
57+
const index = markIndex.get(mark);
58+
const node = mark.render(index, scales, channels, plotOptions);
5759
if (node != null) svg.append(() => node);
5860
}
5961

test/facet.html

Lines changed: 9 additions & 1 deletion

0 commit comments

Comments
 (0)