Skip to content

Commit 98dd563

Browse files
Filmbostock
authored andcommitted
clean
1 parent 868a6f5 commit 98dd563

File tree

2 files changed

+144
-135
lines changed

2 files changed

+144
-135
lines changed

src/facet.js

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import {group, intersection, sort} from "d3";
1+
import {group, intersection, sort, sum} from "d3";
22
import {arrayify, range} from "./options.js";
3+
import {Channel} from "./channel.js";
4+
import {warn} from "./warnings.js";
35

46
// Facet filter, by mark; for now only the "eq" filter is provided.
57
export function filterFacets(facets, {fx, fy}) {
@@ -50,3 +52,69 @@ export function facetTranslate(fx, fy) {
5052
? ({x}) => `translate(${fx(x)},0)`
5153
: ({y}) => `translate(0,${fy(y)})`;
5254
}
55+
56+
// Returns an index that for each facet lists all the elements present in other
57+
// facets in the original index
58+
export function excludeIndex(index) {
59+
const ex = [];
60+
const e = new Uint32Array(sum(index, (d) => d.length));
61+
for (const i of index) {
62+
let n = 0;
63+
for (const j of index) {
64+
if (i === j) continue;
65+
e.set(j, n);
66+
n += j.length;
67+
}
68+
ex.push(e.slice(0, n));
69+
}
70+
return ex;
71+
}
72+
73+
// Returns the facet groups, and possibly fx and fy channels, associated to the
74+
// top-level facet options {data, x, y}
75+
export function topFacetRead(facet) {
76+
if (facet == null) return;
77+
const {x, y} = facet;
78+
if (x != null || y != null) {
79+
const data = arrayify(facet.data);
80+
if (data == null) throw new Error(`missing facet data`); // TODO strict equality
81+
const fx = x != null ? Channel(data, {value: x, scale: "fx"}) : undefined;
82+
const fy = y != null ? Channel(data, {value: y, scale: "fy"}) : undefined;
83+
const groups = facetGroups(range(data), {fx, fy});
84+
// If the top-level faceting is non-trivial, track the corresponding data
85+
// length, in order to compare it for the warning above.
86+
const facetChannelLength =
87+
groups.size > 1 || (fx && fy && groups.size === 1 && [...groups][0][1].size > 1) ? data.length : undefined;
88+
return {groups, fx, fy, facetChannelLength};
89+
}
90+
}
91+
92+
// Returns the facet groups, and possibly fx and fy channels, associated to a
93+
// mark, either through top-level faceting or mark-level facet options {fx, fy}
94+
export function facetRead(mark, facetOptions, topFacetInfo) {
95+
if (mark.facet === null) return;
96+
97+
// This mark defines a mark-level facet.
98+
const {fx: x, fy: y} = mark;
99+
if (x != null || y != null) {
100+
const data = arrayify(mark.data);
101+
if (data == null) throw new Error(`missing facet data in ${mark.ariaLabel}`); // TODO strict equality
102+
const fx = x != null ? Channel(data, {value: x, scale: "fx"}) : undefined;
103+
const fy = y != null ? Channel(data, {value: y, scale: "fy"}) : undefined;
104+
return {groups: facetGroups(range(data), {fx, fy}), fx, fy};
105+
}
106+
107+
// This mark links to a top-level facet, if present.
108+
if (topFacetInfo === undefined) return;
109+
110+
const {groups, facetChannelLength} = topFacetInfo;
111+
if (mark.facet !== "auto" || mark.data === facetOptions.data) return {groups};
112+
113+
// Warn for the common pitfall of wanting to facet mapped data. See
114+
// above for the initialization of facetChannelLength.
115+
if (facetChannelLength !== undefined && arrayify(mark.data)?.length === facetChannelLength) {
116+
warn(
117+
`Warning: the ${mark.ariaLabel} mark appears to use faceted data, but isn’t faceted. The mark data has the same length as the facet data and the mark facet option is "auto", but the mark data and facet data are distinct. If this mark should be faceted, set the mark facet option to true; otherwise, suppress this warning by setting the mark facet option to false.`
118+
);
119+
}
120+
}

src/plot.js

Lines changed: 75 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {ascending, cross, group, select, sort, sum} from "d3";
1+
import {ascending, cross, group, select, sort} from "d3";
22
import {Axes, autoAxisTicks, autoScaleLabels} from "./axes.js";
3-
import {Channel, Channels, channelDomain, valueObject} from "./channel.js";
3+
import {Channels, channelDomain, valueObject} from "./channel.js";
44
import {Context, create} from "./context.js";
55
import {defined} from "./defined.js";
66
import {Dimensions} from "./dimensions.js";
@@ -11,8 +11,8 @@ import {position, registry as scaleRegistry} from "./scales/index.js";
1111
import {applyInlineStyles, maybeClassName, maybeClip, styles} from "./style.js";
1212
import {basic, initializer} from "./transforms/basic.js";
1313
import {maybeInterval} from "./transforms/interval.js";
14-
import {consumeWarnings, warn} from "./warnings.js";
15-
import {facetGroups, facetKeys, facetTranslate, filterFacets} from "./facet.js";
14+
import {consumeWarnings} from "./warnings.js";
15+
import {excludeIndex, facetKeys, facetTranslate, filterFacets, topFacetRead, facetRead} from "./facet.js";
1616

1717
/** @jsdoc plot */
1818
export function plot(options = {}) {
@@ -24,132 +24,83 @@ export function plot(options = {}) {
2424
// Flatten any nested marks.
2525
const marks = options.marks === undefined ? [] : options.marks.flat(Infinity).map(markify);
2626

27-
// A Map from Mark instance to its render state, including:
28-
// index - the data index e.g. [0, 1, 2, 3, …]
29-
// channels - an array of materialized channels e.g. [["x", {value}], …]
30-
// faceted - a boolean indicating whether this mark is faceted
31-
// values - an object of scaled values e.g. {x: [40, 32, …], …}
32-
const stateByMark = new Map();
33-
for (const mark of marks) {
34-
if (stateByMark.has(mark)) throw new Error("duplicate mark; each mark must be unique");
35-
36-
// TODO It’s undesirable to set this to an empty object here because it
37-
// makes it less obvious what the expected type of mark state is. And also
38-
// when we (eventually) migrate to TypeScript, this would be disallowed.
39-
// Previously mark state was a {data, facet, channels, values} object; now
40-
// it looks like we also use: fx, fy, groups, facetChannelLength,
41-
// facetsIndex. And these are set at various different points below, so
42-
// there are more intermediate representations where the state is partially
43-
// initialized. If possible we should try to reduce the number of
44-
// intermediate states and simplify the state representations to make the
45-
// logic easier to follow.
46-
stateByMark.set(mark, {});
47-
}
48-
4927
// A Map from scale name to an array of associated channels.
5028
const channelsByScale = new Map();
5129

5230
// Faceting!
5331
let facets;
5432

33+
// A map from top-level facet or mark to facet information, including:
34+
// * groups - a possibly nested map from facet values to indexes in the data
35+
// array
36+
// * fx - a channel to add to the fx scale
37+
// * fy - a channel to add to the fy scale
38+
// * facetChannelLength - the top-level facet indicates a facet channel length
39+
// to help warn the user if a different data of the same length is used in a
40+
// mark
41+
// * facetsIndex - In a second pass, a nested array of indices corresponding
42+
// to the valid facets
43+
const facetCollect = new Map();
44+
5545
// Collect all facet definitions (top-level facets then mark facets),
5646
// materialize the associated channels, and derive facet scales.
57-
if (facet || marks.some((mark) => mark.fx || mark.fy)) {
58-
// TODO non-null, not truthy
59-
60-
// TODO Remove/refactor this: here “top” is pretending to be a mark, but
61-
// it’s not actually a mark. Also there’s no “top” facet method, and the
62-
// ariaLabel isn’t used for anything. And eventually top is removed from
63-
// stateByMark. We can find a cleaner way to do this.
64-
const top =
65-
facet !== undefined
66-
? {data: facet.data, fx: facet.x, fy: facet.y, facet: "top", ariaLabel: "top-level facet option"}
67-
: {facet: null};
68-
69-
stateByMark.set(top, {});
70-
71-
for (const mark of [top, ...marks]) {
72-
const method = mark?.facet; // TODO rename to facet; remove check if mark is undefined?
73-
if (!method) continue; // TODO explicitly check for null
74-
const {fx: x, fy: y} = mark;
75-
const state = stateByMark.get(mark);
76-
if (x == null && y == null && facet != null) {
77-
// TODO strict equality
78-
if (method !== "auto" || mark.data === facet.data) {
79-
state.groups = stateByMark.get(top).groups;
80-
} else {
81-
// Warn for the common pitfall of wanting to facet mapped data. See
82-
// below for the initialization of facetChannelLength.
83-
const {facetChannelLength} = stateByMark.get(top);
84-
if (facetChannelLength !== undefined && arrayify(mark.data)?.length === facetChannelLength)
85-
warn(
86-
`Warning: the ${mark.ariaLabel} mark appears to use faceted data, but isn’t faceted. The mark data has the same length as the facet data and the mark facet option is "auto", but the mark data and facet data are distinct. If this mark should be faceted, set the mark facet option to true; otherwise, suppress this warning by setting the mark facet option to false.`
87-
);
88-
}
89-
} else {
90-
const data = arrayify(mark.data);
91-
if ((x != null || y != null) && data == null) throw new Error(`missing facet data in ${mark.ariaLabel}`); // TODO strict equality
92-
if (x != null) {
93-
// TODO strict equality
94-
state.fx = Channel(data, {value: x, scale: "fx"});
95-
if (!channelsByScale.has("fx")) channelsByScale.set("fx", []);
96-
channelsByScale.get("fx").push(state.fx);
97-
}
98-
if (y != null) {
99-
// TODO strict equality
100-
state.fy = Channel(data, {value: y, scale: "fy"});
101-
if (!channelsByScale.has("fy")) channelsByScale.set("fy", []);
102-
channelsByScale.get("fy").push(state.fy);
103-
}
104-
if (state.fx || state.fy) {
105-
// TODO strict equality
106-
const groups = facetGroups(range(data), state);
107-
state.groups = groups;
108-
// If the top-level faceting is non-trivial, store the corresponding
109-
// data length, in order to compare it for the warning above.
110-
if (
111-
mark === top &&
112-
(groups.size > 1 || (state.fx && state.fy && groups.size === 1 && [...groups][0][1].size > 1))
113-
)
114-
state.facetChannelLength = data.length; // TODO curly braces
115-
}
116-
}
47+
const topFacetInfo = topFacetRead(facet);
48+
if (topFacetInfo) facetCollect.set(null, topFacetInfo);
49+
50+
for (const mark of marks) {
51+
const f = facetRead(mark, facet, topFacetInfo);
52+
if (f) facetCollect.set(mark, f);
53+
}
54+
for (const f of facetCollect.values()) {
55+
const {fx, fy} = f;
56+
if (fx) {
57+
if (!channelsByScale.has("fx")) channelsByScale.set("fx", []);
58+
channelsByScale.get("fx").push(fx);
59+
}
60+
if (fy) {
61+
if (!channelsByScale.has("fy")) channelsByScale.set("fy", []);
62+
channelsByScale.get("fy").push(fy);
11763
}
64+
}
65+
66+
const facetScales = Scales(channelsByScale, options);
67+
68+
// All the possible facets are given by the domains of fx or fy, or the
69+
// cross-product of these domains if we facet by both x and y. We sort them in
70+
// order to apply the facet filters afterwards.
71+
const fxDomain = facetScales.fx?.scale.domain();
72+
const fyDomain = facetScales.fy?.scale.domain();
73+
facets =
74+
fxDomain && fyDomain
75+
? cross(sort(fxDomain, ascending), sort(fyDomain, ascending)).map(([x, y]) => ({x, y}))
76+
: fxDomain
77+
? sort(fxDomain, ascending).map((x) => ({x}))
78+
: fyDomain
79+
? sort(fyDomain, ascending).map((y) => ({y}))
80+
: undefined;
11881

119-
const facetScales = Scales(channelsByScale, options);
120-
121-
// All the possible facets are given by the domains of fx or fy, or the
122-
// cross-product of these domains if we facet by both x and y. We sort them in
123-
// order to apply the facet filters afterwards.
124-
const fxDomain = facetScales.fx?.scale.domain();
125-
const fyDomain = facetScales.fy?.scale.domain();
126-
facets =
127-
fxDomain && fyDomain
128-
? cross(sort(fxDomain, ascending), sort(fyDomain, ascending)).map(([x, y]) => ({x, y}))
129-
: fxDomain
130-
? sort(fxDomain, ascending).map((x) => ({x}))
131-
: fyDomain
132-
? sort(fyDomain, ascending).map((y) => ({y}))
133-
: null;
82+
if (facets !== undefined) {
83+
const facetsIndex = topFacetInfo ? filterFacets(facets, topFacetInfo) : undefined;
13484

13585
// Compute a facet index for each mark, parallel to the facets array.
136-
for (const mark of [top, ...marks]) {
137-
const method = mark.facet; // TODO rename to facet
138-
if (method === null) continue;
86+
for (const mark of marks) {
87+
const {facet} = mark;
88+
if (facet === null) continue;
13989
const {fx: x, fy: y} = mark;
140-
const state = stateByMark.get(mark);
90+
const facetInfo = facetCollect.get(mark);
91+
if (facetInfo === undefined) continue;
14192

14293
// For mark-level facets, compute an index for that mark’s data and options.
14394
if (x !== undefined || y !== undefined) {
144-
state.facetsIndex = filterFacets(facets, state);
95+
facetInfo.facetsIndex = filterFacets(facets, facetInfo);
14596
}
14697

14798
// Otherwise, link to the top-level facet information.
148-
else if (facet && (method !== "auto" || mark.data === facet.data)) {
149-
const {facetsIndex, fx, fy} = stateByMark.get(top);
150-
state.facetsIndex = facetsIndex;
151-
if (fx !== undefined) state.fx = fx;
152-
if (fy !== undefined) state.fy = fy;
99+
else if (topFacetInfo !== undefined) {
100+
facetInfo.facetsIndex = facetsIndex;
101+
const {fx, fy} = topFacetInfo;
102+
if (fx !== undefined) facetInfo.fx = fx;
103+
if (fy !== undefined) facetInfo.fy = fy;
153104
}
154105
}
155106

@@ -161,23 +112,22 @@ export function plot(options = {}) {
161112
// the domain. Expunge empty facets, and clear the corresponding elements
162113
// from the nested index in each mark.
163114
const nonEmpty = new Set();
164-
for (const {facetsIndex} of stateByMark.values()) {
115+
for (const {facetsIndex} of facetCollect.values()) {
165116
if (facetsIndex) {
166117
facetsIndex.forEach((index, i) => {
167118
if (index?.length > 0) nonEmpty.add(i);
168119
});
169120
}
170121
}
171-
if (nonEmpty.size < facets.length) {
122+
if (0 < nonEmpty.size && nonEmpty.size < facets.length) {
172123
facets = facets.filter((_, i) => nonEmpty.has(i));
173-
for (const state of stateByMark.values()) {
124+
for (const state of facetCollect.values()) {
174125
const {facetsIndex} = state;
126+
//console.warn(facetsIndex);
175127
if (!facetsIndex) continue;
176128
state.facetsIndex = facetsIndex.filter((_, i) => nonEmpty.has(i));
177129
}
178130
}
179-
180-
stateByMark.delete(top);
181131
}
182132

183133
// If a scale is explicitly declared in options, initialize its associated
@@ -190,9 +140,17 @@ export function plot(options = {}) {
190140
}
191141
}
192142

143+
// A Map from Mark instance to its render state, including:
144+
// index - the data index e.g. [0, 1, 2, 3, …]
145+
// channels - an array of materialized channels e.g. [["x", {value}], …]
146+
// faceted - a boolean indicating whether this mark is faceted
147+
// values - an object of scaled values e.g. {x: [40, 32, …], …}
148+
const stateByMark = new Map();
149+
193150
// Initialize the marks’ state.
194151
for (const mark of marks) {
195-
const state = stateByMark.get(mark);
152+
if (stateByMark.has(mark)) throw new Error("duplicate mark; each mark must be unique");
153+
const state = facetCollect.get(mark) || {};
196154
const facetsIndex = mark.facet === "exclude" ? excludeIndex(state.facetsIndex) : state.facetsIndex;
197155
const {data, facets, channels} = mark.initialize(facetsIndex, state);
198156
applyScaleTransforms(channels, options);
@@ -569,20 +527,3 @@ function nolabel(axis) {
569527
? axis // use the existing axis if unlabeled
570528
: Object.assign(Object.create(axis), {label: undefined});
571529
}
572-
573-
// Returns an index that for each facet lists all the elements present in other
574-
// facets in the original index
575-
function excludeIndex(index) {
576-
const ex = [];
577-
const e = new Uint32Array(sum(index, (d) => d.length));
578-
for (const i of index) {
579-
let n = 0;
580-
for (const j of index) {
581-
if (i === j) continue;
582-
e.set(j, n);
583-
n += j.length;
584-
}
585-
ex.push(e.slice(0, n));
586-
}
587-
return ex;
588-
}

0 commit comments

Comments
 (0)