Skip to content

Commit 4292468

Browse files
committed
inline faceting
1 parent 9428166 commit 4292468

File tree

1 file changed

+126
-153
lines changed

1 file changed

+126
-153
lines changed

src/plot.js

Lines changed: 126 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {Channel, channelSort} from "./channel.js";
44
import {defined} from "./defined.js";
55
import {Dimensions} from "./dimensions.js";
66
import {Legends, exposeLegends} from "./legends.js";
7-
import {arrayify, isOptions, keyword, range, first, second, where} from "./options.js";
7+
import {arrayify, isOptions, keyword, range, second, where} from "./options.js";
88
import {Scales, ScaleFunctions, autoScaleRange, applyScales, exposeScales} from "./scales.js";
99
import {applyInlineStyles, maybeClassName, maybeClip, styles} from "./style.js";
1010
import {basic} from "./transforms/basic.js";
@@ -16,40 +16,71 @@ export function plot(options = {}) {
1616
// className for inline styles
1717
const className = maybeClassName(options.className);
1818

19-
// When faceting, wrap all marks in a faceting mark.
20-
if (facet !== undefined) {
21-
const {marks} = options;
22-
const {data} = facet;
23-
options = {...options, marks: facets(data, facet, marks)};
24-
}
25-
2619
// Flatten any nested marks.
2720
const marks = options.marks === undefined ? [] : options.marks.flat(Infinity).map(markify);
2821

2922
// A Map from Mark instance to an object of named channel values.
30-
const markChannels = new Map();
31-
const markIndex = new Map();
23+
const indexByMark = new Map();
24+
const channelsByMark = new Map();
25+
const facetsByMark = new Set();
26+
const valuesByMark = new Map();
3227

3328
// A Map from scale name to an array of associated channels.
3429
const scaleChannels = new Map();
3530

31+
// Faceting!
32+
let facets; // map from facet key (e.g. "foo") to facet index ([0, 1, 2, …])
33+
let facetIndex; // index over the facet data, e.g. [0, 1, 2, 3, …]
34+
let facetChannels; // e.g. [["fx", {value}], ["fy", {value}]]
35+
let facetsIndex; // nested array of facet indexes [[0, 1, 3, …], [2, 5, …], …]
36+
let facetsExclude; // lazily-constructed opposite of facetsIndex
37+
if (facet !== undefined) {
38+
let {x, y, data} = facet;
39+
if (x != null || y != null) {
40+
data = arrayify(data);
41+
facetChannels = [];
42+
if (x != null) {
43+
const fx = Channel(data, {value: x, scale: "fx"});
44+
facetChannels.push(["fx", fx]);
45+
scaleChannels.set("fx", [fx]);
46+
}
47+
if (y != null) {
48+
const fy = Channel(data, {value: y, scale: "fy"});
49+
facetChannels.push(["fy", fy]);
50+
scaleChannels.set("fy", [fy]);
51+
}
52+
facetIndex = range(data);
53+
facets = facetGroups(facetIndex, facetChannels);
54+
facetsIndex = Array.from(facets, second);
55+
}
56+
}
57+
3658
// Initialize the marks’ channels, indexing them by mark and scale as needed.
37-
// Also apply any scale transforms.
3859
for (const mark of marks) {
39-
if (markChannels.has(mark)) throw new Error("duplicate mark");
40-
const {index, channels} = mark.initialize();
60+
if (channelsByMark.has(mark)) throw new Error("duplicate mark");
61+
const markFacets = facets === undefined ? undefined
62+
: mark.facet === "auto" ? mark.data === facet.data ? facetsIndex : undefined
63+
: mark.facet === "include" ? facetsIndex
64+
: mark.facet === "exclude" ? facetsExclude || (facetsExclude = facetsIndex.map(f => Uint32Array.from(difference(facetIndex, f))))
65+
: undefined;
66+
const {index, channels} = mark.initialize(markFacets, facetChannels);
4167
for (const [, channel] of channels) {
4268
const {scale} = channel;
4369
if (scale !== undefined) {
4470
const scaled = scaleChannels.get(scale);
45-
const {percent, transform = percent ? x => x * 100 : undefined} = options[scale] || {};
46-
if (transform != null) channel.value = Array.from(channel.value, transform);
47-
if (scaled) scaled.push(channel);
71+
if (scaled !== undefined) scaled.push(channel);
4872
else scaleChannels.set(scale, [channel]);
4973
}
5074
}
51-
markChannels.set(mark, channels);
52-
markIndex.set(mark, index);
75+
channelsByMark.set(mark, channels);
76+
indexByMark.set(mark, index);
77+
if (markFacets !== undefined) facetsByMark.add(mark);
78+
}
79+
80+
// Apply scale transforms.
81+
for (const [scale, channels] of scaleChannels) {
82+
const {percent, transform = percent ? x => x * 100 : undefined} = options[scale] || {};
83+
if (transform != null) for (const c of channels) c.value = Array.from(c.value, transform);
5384
}
5485

5586
const scaleDescriptors = Scales(scaleChannels, options);
@@ -61,6 +92,11 @@ export function plot(options = {}) {
6192
autoScaleLabels(scaleChannels, scaleDescriptors, axes, dimensions, options);
6293
autoAxisTicks(scaleDescriptors, axes);
6394

95+
// Compute channel values, applying scales as needed.
96+
for (const [mark, channels] of channelsByMark) {
97+
valuesByMark.set(mark, applyScales(channels, scales));
98+
}
99+
64100
const {width, height} = dimensions;
65101

66102
const svg = create("svg")
@@ -91,18 +127,77 @@ export function plot(options = {}) {
91127
.node();
92128

93129
// When faceting, render axes for fx and fy instead of x and y.
94-
const axisX = axes[facet !== undefined && scales.fx ? "fx" : "x"];
95-
const axisY = axes[facet !== undefined && scales.fy ? "fy" : "y"];
130+
const axisX = axes[facetIndex !== undefined && scales.fx ? "fx" : "x"]; // TODO drop facetIndex check?
131+
const axisY = axes[facetIndex !== undefined && scales.fy ? "fy" : "y"]; // TODO drop facetIndex check?
96132
if (axisY) svg.appendChild(axisY.render(null, scales, dimensions));
97133
if (axisX) svg.appendChild(axisX.render(null, scales, dimensions));
98134

99-
// Render marks.
100-
for (const mark of marks) {
101-
const channels = markChannels.get(mark);
102-
const values = applyScales(channels, scales);
103-
const index = mark.filter(markIndex.get(mark), channels, values);
104-
const node = mark.render(index, scales, values, dimensions, axes);
105-
if (node != null) svg.appendChild(node);
135+
// Render (possibly faceted) marks.
136+
if (facetIndex !== undefined) { // TODO fx || fy?
137+
const {fx, fy} = scales;
138+
const fyDomain = fy && fy.domain();
139+
const fxDomain = fx && fx.domain();
140+
const fyMargins = fy && {marginTop: 0, marginBottom: 0, height: fy.bandwidth()};
141+
const fxMargins = fx && {marginRight: 0, marginLeft: 0, width: fx.bandwidth()};
142+
const subdimensions = {...dimensions, ...fxMargins, ...fyMargins};
143+
const indexByFacet = facetMap(facetChannels);
144+
facets.forEach(([key], i) => indexByFacet.set(key, i));
145+
select(svg).append("g")
146+
.call(g => {
147+
if (fy && axes.y) {
148+
const axis1 = axes.y, axis2 = nolabel(axis1);
149+
const j = axis1.labelAnchor === "bottom" ? fyDomain.length - 1 : axis1.labelAnchor === "center" ? fyDomain.length >> 1 : 0;
150+
const fyDimensions = {...dimensions, ...fyMargins};
151+
g.selectAll()
152+
.data(fyDomain)
153+
.join("g")
154+
.attr("transform", ky => `translate(0,${fy(ky)})`)
155+
.append((ky, i) => (i === j ? axis1 : axis2).render(
156+
fx && where(fxDomain, kx => indexByFacet.has([kx, ky])),
157+
scales,
158+
fyDimensions
159+
));
160+
}
161+
if (fx && axes.x) {
162+
const axis1 = axes.x, axis2 = nolabel(axis1);
163+
const j = axis1.labelAnchor === "right" ? fxDomain.length - 1 : axis1.labelAnchor === "center" ? fxDomain.length >> 1 : 0;
164+
const {marginLeft, marginRight} = dimensions;
165+
const fxDimensions = {...dimensions, ...fxMargins, labelMarginLeft: marginLeft, labelMarginRight: marginRight};
166+
g.selectAll()
167+
.data(fxDomain)
168+
.join("g")
169+
.attr("transform", kx => `translate(${fx(kx)},0)`)
170+
.append((kx, i) => (i === j ? axis1 : axis2).render(
171+
fy && where(fyDomain, ky => indexByFacet.has([kx, ky])),
172+
scales,
173+
fxDimensions
174+
));
175+
}
176+
})
177+
.call(g => g.selectAll()
178+
.data(facetKeys(scales).filter(indexByFacet.has, indexByFacet))
179+
.join("g")
180+
.attr("transform", facetTranslate(fx, fy))
181+
.each(function(key) {
182+
const j = indexByFacet.get(key);
183+
for (const mark of marks) {
184+
const channels = channelsByMark.get(mark);
185+
const values = valuesByMark.get(mark);
186+
const markIndex = indexByMark.get(mark);
187+
const markFacetIndex = facetsByMark.has(mark) ? markIndex[j] : markIndex;
188+
const index = mark.filter(markFacetIndex, channels, values);
189+
const node = mark.render(index, scales, values, subdimensions);
190+
if (node != null) this.appendChild(node);
191+
}
192+
}));
193+
} else {
194+
for (const mark of marks) {
195+
const channels = channelsByMark.get(mark);
196+
const values = valuesByMark.get(mark);
197+
const index = mark.filter(indexByMark.get(mark), channels, values);
198+
const node = mark.render(index, scales, values, dimensions);
199+
if (node != null) svg.appendChild(node);
200+
}
106201
}
107202

108203
// Wrap the plot in a figure with a caption, if desired.
@@ -166,14 +261,14 @@ export class Mark {
166261
this.dy = +dy || 0;
167262
this.clip = maybeClip(clip);
168263
}
169-
initialize(facets, facetChannels) {
264+
initialize(facetIndex, facetChannels) {
170265
let data = arrayify(this.data);
171-
let index = facets === undefined && data != null ? range(data) : facets;
266+
let index = facetIndex === undefined && data != null ? range(data) : facetIndex;
172267
if (data !== undefined && this.transform !== undefined) {
173-
if (facets === undefined) index = index.length ? [index] : [];
268+
if (facetIndex === undefined) index = index.length ? [index] : [];
174269
({facets: index, data} = this.transform(data, index));
175270
data = arrayify(data);
176-
if (facets === undefined && index.length) ([index] = index);
271+
if (facetIndex === undefined && index.length) ([index] = index);
177272
}
178273
const channels = this.channels.map(channel => {
179274
const {name} = channel;
@@ -215,128 +310,6 @@ class Render extends Mark {
215310
render() {}
216311
}
217312

218-
function facets(data, {x, y, ...options}, marks) {
219-
return x === undefined && y === undefined
220-
? marks // if no facets are specified, ignore!
221-
: [new Facet(data, {x, y, ...options}, marks)];
222-
}
223-
224-
class Facet extends Mark {
225-
constructor(data, {x, y, ...options} = {}, marks = []) {
226-
if (data == null) throw new Error("missing facet data");
227-
super(
228-
data,
229-
[
230-
{name: "fx", value: x, scale: "fx", optional: true},
231-
{name: "fy", value: y, scale: "fy", optional: true}
232-
],
233-
options
234-
);
235-
this.marks = marks.flat(Infinity).map(markify);
236-
// The following fields are set by initialize:
237-
this.marksChannels = undefined; // array of mark channels
238-
this.marksIndexByFacet = undefined; // map from facet key to array of mark indexes
239-
}
240-
initialize() {
241-
const {index, channels: facetChannels} = super.initialize();
242-
const facets = index === undefined ? [] : facetGroups(index, facetChannels);
243-
const facetsKeys = Array.from(facets, first);
244-
const facetsIndex = Array.from(facets, second);
245-
const channels = facetChannels.slice();
246-
const marksChannels = this.marksChannels = [];
247-
const marksIndexByFacet = this.marksIndexByFacet = facetMap(facetChannels);
248-
for (const key of facetsKeys) marksIndexByFacet.set(key, new Array(this.marks.length));
249-
let facetsExclude;
250-
for (let i = 0; i < this.marks.length; ++i) {
251-
const mark = this.marks[i];
252-
const {facet} = mark;
253-
const markFacets = facet === "auto" ? mark.data === this.data ? facetsIndex : undefined
254-
: facet === "include" ? facetsIndex
255-
: facet === "exclude" ? facetsExclude || (facetsExclude = facetsIndex.map(f => Uint32Array.from(difference(index, f))))
256-
: undefined;
257-
const {index: markIndex, channels: markChannels} = mark.initialize(markFacets, facetChannels);
258-
// If an index is returned by mark.initialize, its structure depends on
259-
// whether or not faceting has been applied: it is a flat index ([0, 1, 2,
260-
// …]) when not faceted, and a nested index ([[0, 1, …], [2, 3, …], …])
261-
// when faceted.
262-
if (markIndex !== undefined) {
263-
if (markFacets) {
264-
for (let j = 0; j < facetsKeys.length; ++j) {
265-
marksIndexByFacet.get(facetsKeys[j])[i] = markIndex[j];
266-
}
267-
} else {
268-
for (let j = 0; j < facetsKeys.length; ++j) {
269-
marksIndexByFacet.get(facetsKeys[j])[i] = markIndex;
270-
}
271-
}
272-
}
273-
for (const [, channel] of markChannels) channels.push([, channel]); // anonymize channels
274-
marksChannels.push(markChannels);
275-
}
276-
return {index, channels};
277-
}
278-
filter(index) {
279-
return index;
280-
}
281-
render(index, scales, values, dimensions, axes) {
282-
const {marks, marksChannels, marksIndexByFacet} = this;
283-
const {fx, fy} = scales;
284-
const fyDomain = fy && fy.domain();
285-
const fxDomain = fx && fx.domain();
286-
const fyMargins = fy && {marginTop: 0, marginBottom: 0, height: fy.bandwidth()};
287-
const fxMargins = fx && {marginRight: 0, marginLeft: 0, width: fx.bandwidth()};
288-
const subdimensions = {...dimensions, ...fxMargins, ...fyMargins};
289-
const marksValues = marksChannels.map(channels => applyScales(channels, scales));
290-
return create("svg:g")
291-
.call(g => {
292-
if (fy && axes.y) {
293-
const axis1 = axes.y, axis2 = nolabel(axis1);
294-
const j = axis1.labelAnchor === "bottom" ? fyDomain.length - 1 : axis1.labelAnchor === "center" ? fyDomain.length >> 1 : 0;
295-
const fyDimensions = {...dimensions, ...fyMargins};
296-
g.selectAll()
297-
.data(fyDomain)
298-
.join("g")
299-
.attr("transform", ky => `translate(0,${fy(ky)})`)
300-
.append((ky, i) => (i === j ? axis1 : axis2).render(
301-
fx && where(fxDomain, kx => marksIndexByFacet.has([kx, ky])),
302-
scales,
303-
fyDimensions
304-
));
305-
}
306-
if (fx && axes.x) {
307-
const axis1 = axes.x, axis2 = nolabel(axis1);
308-
const j = axis1.labelAnchor === "right" ? fxDomain.length - 1 : axis1.labelAnchor === "center" ? fxDomain.length >> 1 : 0;
309-
const {marginLeft, marginRight} = dimensions;
310-
const fxDimensions = {...dimensions, ...fxMargins, labelMarginLeft: marginLeft, labelMarginRight: marginRight};
311-
g.selectAll()
312-
.data(fxDomain)
313-
.join("g")
314-
.attr("transform", kx => `translate(${fx(kx)},0)`)
315-
.append((kx, i) => (i === j ? axis1 : axis2).render(
316-
fy && where(fyDomain, ky => marksIndexByFacet.has([kx, ky])),
317-
scales,
318-
fxDimensions
319-
));
320-
}
321-
})
322-
.call(g => g.selectAll()
323-
.data(facetKeys(scales).filter(marksIndexByFacet.has, marksIndexByFacet))
324-
.join("g")
325-
.attr("transform", facetTranslate(fx, fy))
326-
.each(function(key) {
327-
const marksFacetIndex = marksIndexByFacet.get(key);
328-
for (let i = 0; i < marks.length; ++i) {
329-
const mark = marks[i];
330-
const values = marksValues[i];
331-
const index = mark.filter(marksFacetIndex[i], marksChannels[i], values);
332-
const node = mark.render(index, scales, values, subdimensions);
333-
if (node != null) this.appendChild(node);
334-
}
335-
}))
336-
.node();
337-
}
338-
}
339-
340313
// Derives a copy of the specified axis with the label disabled.
341314
function nolabel(axis) {
342315
return axis === undefined || axis.label === undefined

0 commit comments

Comments
 (0)