Skip to content

Commit eecae55

Browse files
committed
clean
1 parent f6c456e commit eecae55

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
/**
1818
* Renders a new plot given the specified *options* and returns the
@@ -368,132 +368,83 @@ export function plot(options = {}) {
368368
// Flatten any nested marks.
369369
const marks = options.marks === undefined ? [] : options.marks.flat(Infinity).map(markify);
370370

371-
// A Map from Mark instance to its render state, including:
372-
// index - the data index e.g. [0, 1, 2, 3, …]
373-
// channels - an array of materialized channels e.g. [["x", {value}], …]
374-
// faceted - a boolean indicating whether this mark is faceted
375-
// values - an object of scaled values e.g. {x: [40, 32, …], …}
376-
const stateByMark = new Map();
377-
for (const mark of marks) {
378-
if (stateByMark.has(mark)) throw new Error("duplicate mark; each mark must be unique");
379-
380-
// TODO It’s undesirable to set this to an empty object here because it
381-
// makes it less obvious what the expected type of mark state is. And also
382-
// when we (eventually) migrate to TypeScript, this would be disallowed.
383-
// Previously mark state was a {data, facet, channels, values} object; now
384-
// it looks like we also use: fx, fy, groups, facetChannelLength,
385-
// facetsIndex. And these are set at various different points below, so
386-
// there are more intermediate representations where the state is partially
387-
// initialized. If possible we should try to reduce the number of
388-
// intermediate states and simplify the state representations to make the
389-
// logic easier to follow.
390-
stateByMark.set(mark, {});
391-
}
392-
393371
// A Map from scale name to an array of associated channels.
394372
const channelsByScale = new Map();
395373

396374
// Faceting!
397375
let facets;
398376

377+
// A map from top-level facet or mark to facet information, including:
378+
// * groups - a possibly nested map from facet values to indexes in the data
379+
// array
380+
// * fx - a channel to add to the fx scale
381+
// * fy - a channel to add to the fy scale
382+
// * facetChannelLength - the top-level facet indicates a facet channel length
383+
// to help warn the user if a different data of the same length is used in a
384+
// mark
385+
// * facetsIndex - In a second pass, a nested array of indices corresponding
386+
// to the valid facets
387+
const facetCollect = new Map();
388+
399389
// Collect all facet definitions (top-level facets then mark facets),
400390
// materialize the associated channels, and derive facet scales.
401-
if (facet || marks.some((mark) => mark.fx || mark.fy)) {
402-
// TODO non-null, not truthy
403-
404-
// TODO Remove/refactor this: here “top” is pretending to be a mark, but
405-
// it’s not actually a mark. Also there’s no “top” facet method, and the
406-
// ariaLabel isn’t used for anything. And eventually top is removed from
407-
// stateByMark. We can find a cleaner way to do this.
408-
const top =
409-
facet !== undefined
410-
? {data: facet.data, fx: facet.x, fy: facet.y, facet: "top", ariaLabel: "top-level facet option"}
411-
: {facet: null};
412-
413-
stateByMark.set(top, {});
414-
415-
for (const mark of [top, ...marks]) {
416-
const method = mark?.facet; // TODO rename to facet; remove check if mark is undefined?
417-
if (!method) continue; // TODO explicitly check for null
418-
const {fx: x, fy: y} = mark;
419-
const state = stateByMark.get(mark);
420-
if (x == null && y == null && facet != null) {
421-
// TODO strict equality
422-
if (method !== "auto" || mark.data === facet.data) {
423-
state.groups = stateByMark.get(top).groups;
424-
} else {
425-
// Warn for the common pitfall of wanting to facet mapped data. See
426-
// below for the initialization of facetChannelLength.
427-
const {facetChannelLength} = stateByMark.get(top);
428-
if (facetChannelLength !== undefined && arrayify(mark.data)?.length === facetChannelLength)
429-
warn(
430-
`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.`
431-
);
432-
}
433-
} else {
434-
const data = arrayify(mark.data);
435-
if ((x != null || y != null) && data == null) throw new Error(`missing facet data in ${mark.ariaLabel}`); // TODO strict equality
436-
if (x != null) {
437-
// TODO strict equality
438-
state.fx = Channel(data, {value: x, scale: "fx"});
439-
if (!channelsByScale.has("fx")) channelsByScale.set("fx", []);
440-
channelsByScale.get("fx").push(state.fx);
441-
}
442-
if (y != null) {
443-
// TODO strict equality
444-
state.fy = Channel(data, {value: y, scale: "fy"});
445-
if (!channelsByScale.has("fy")) channelsByScale.set("fy", []);
446-
channelsByScale.get("fy").push(state.fy);
447-
}
448-
if (state.fx || state.fy) {
449-
// TODO strict equality
450-
const groups = facetGroups(range(data), state);
451-
state.groups = groups;
452-
// If the top-level faceting is non-trivial, store the corresponding
453-
// data length, in order to compare it for the warning above.
454-
if (
455-
mark === top &&
456-
(groups.size > 1 || (state.fx && state.fy && groups.size === 1 && [...groups][0][1].size > 1))
457-
)
458-
state.facetChannelLength = data.length; // TODO curly braces
459-
}
460-
}
391+
const topFacetInfo = topFacetRead(facet);
392+
if (topFacetInfo) facetCollect.set(null, topFacetInfo);
393+
394+
for (const mark of marks) {
395+
const f = facetRead(mark, facet, topFacetInfo);
396+
if (f) facetCollect.set(mark, f);
397+
}
398+
for (const f of facetCollect.values()) {
399+
const {fx, fy} = f;
400+
if (fx) {
401+
if (!channelsByScale.has("fx")) channelsByScale.set("fx", []);
402+
channelsByScale.get("fx").push(fx);
403+
}
404+
if (fy) {
405+
if (!channelsByScale.has("fy")) channelsByScale.set("fy", []);
406+
channelsByScale.get("fy").push(fy);
461407
}
408+
}
409+
410+
const facetScales = Scales(channelsByScale, options);
411+
412+
// All the possible facets are given by the domains of fx or fy, or the
413+
// cross-product of these domains if we facet by both x and y. We sort them in
414+
// order to apply the facet filters afterwards.
415+
const fxDomain = facetScales.fx?.scale.domain();
416+
const fyDomain = facetScales.fy?.scale.domain();
417+
facets =
418+
fxDomain && fyDomain
419+
? cross(sort(fxDomain, ascending), sort(fyDomain, ascending)).map(([x, y]) => ({x, y}))
420+
: fxDomain
421+
? sort(fxDomain, ascending).map((x) => ({x}))
422+
: fyDomain
423+
? sort(fyDomain, ascending).map((y) => ({y}))
424+
: undefined;
462425

463-
const facetScales = Scales(channelsByScale, options);
464-
465-
// All the possible facets are given by the domains of fx or fy, or the
466-
// cross-product of these domains if we facet by both x and y. We sort them in
467-
// order to apply the facet filters afterwards.
468-
const fxDomain = facetScales.fx?.scale.domain();
469-
const fyDomain = facetScales.fy?.scale.domain();
470-
facets =
471-
fxDomain && fyDomain
472-
? cross(sort(fxDomain, ascending), sort(fyDomain, ascending)).map(([x, y]) => ({x, y}))
473-
: fxDomain
474-
? sort(fxDomain, ascending).map((x) => ({x}))
475-
: fyDomain
476-
? sort(fyDomain, ascending).map((y) => ({y}))
477-
: null;
426+
if (facets !== undefined) {
427+
const facetsIndex = topFacetInfo ? filterFacets(facets, topFacetInfo) : undefined;
478428

479429
// Compute a facet index for each mark, parallel to the facets array.
480-
for (const mark of [top, ...marks]) {
481-
const method = mark.facet; // TODO rename to facet
482-
if (method === null) continue;
430+
for (const mark of marks) {
431+
const {facet} = mark;
432+
if (facet === null) continue;
483433
const {fx: x, fy: y} = mark;
484-
const state = stateByMark.get(mark);
434+
const facetInfo = facetCollect.get(mark);
435+
if (facetInfo === undefined) continue;
485436

486437
// For mark-level facets, compute an index for that mark’s data and options.
487438
if (x !== undefined || y !== undefined) {
488-
state.facetsIndex = filterFacets(facets, state);
439+
facetInfo.facetsIndex = filterFacets(facets, facetInfo);
489440
}
490441

491442
// Otherwise, link to the top-level facet information.
492-
else if (facet && (method !== "auto" || mark.data === facet.data)) {
493-
const {facetsIndex, fx, fy} = stateByMark.get(top);
494-
state.facetsIndex = facetsIndex;
495-
if (fx !== undefined) state.fx = fx;
496-
if (fy !== undefined) state.fy = fy;
443+
else if (topFacetInfo !== undefined) {
444+
facetInfo.facetsIndex = facetsIndex;
445+
const {fx, fy} = topFacetInfo;
446+
if (fx !== undefined) facetInfo.fx = fx;
447+
if (fy !== undefined) facetInfo.fy = fy;
497448
}
498449
}
499450

@@ -505,23 +456,22 @@ export function plot(options = {}) {
505456
// the domain. Expunge empty facets, and clear the corresponding elements
506457
// from the nested index in each mark.
507458
const nonEmpty = new Set();
508-
for (const {facetsIndex} of stateByMark.values()) {
459+
for (const {facetsIndex} of facetCollect.values()) {
509460
if (facetsIndex) {
510461
facetsIndex.forEach((index, i) => {
511462
if (index?.length > 0) nonEmpty.add(i);
512463
});
513464
}
514465
}
515-
if (nonEmpty.size < facets.length) {
466+
if (0 < nonEmpty.size && nonEmpty.size < facets.length) {
516467
facets = facets.filter((_, i) => nonEmpty.has(i));
517-
for (const state of stateByMark.values()) {
468+
for (const state of facetCollect.values()) {
518469
const {facetsIndex} = state;
470+
//console.warn(facetsIndex);
519471
if (!facetsIndex) continue;
520472
state.facetsIndex = facetsIndex.filter((_, i) => nonEmpty.has(i));
521473
}
522474
}
523-
524-
stateByMark.delete(top);
525475
}
526476

527477
// If a scale is explicitly declared in options, initialize its associated
@@ -534,9 +484,17 @@ export function plot(options = {}) {
534484
}
535485
}
536486

487+
// A Map from Mark instance to its render state, including:
488+
// index - the data index e.g. [0, 1, 2, 3, …]
489+
// channels - an array of materialized channels e.g. [["x", {value}], …]
490+
// faceted - a boolean indicating whether this mark is faceted
491+
// values - an object of scaled values e.g. {x: [40, 32, …], …}
492+
const stateByMark = new Map();
493+
537494
// Initialize the marks’ state.
538495
for (const mark of marks) {
539-
const state = stateByMark.get(mark);
496+
if (stateByMark.has(mark)) throw new Error("duplicate mark; each mark must be unique");
497+
const state = facetCollect.get(mark) || {};
540498
const facetsIndex = mark.facet === "exclude" ? excludeIndex(state.facetsIndex) : state.facetsIndex;
541499
const {data, facets, channels} = mark.initialize(facetsIndex, state);
542500
applyScaleTransforms(channels, options);
@@ -903,20 +861,3 @@ function nolabel(axis) {
903861
? axis // use the existing axis if unlabeled
904862
: Object.assign(Object.create(axis), {label: undefined});
905863
}
906-
907-
// Returns an index that for each facet lists all the elements present in other
908-
// facets in the original index
909-
function excludeIndex(index) {
910-
const ex = [];
911-
const e = new Uint32Array(sum(index, (d) => d.length));
912-
for (const i of index) {
913-
let n = 0;
914-
for (const j of index) {
915-
if (i === j) continue;
916-
e.set(j, n);
917-
n += j.length;
918-
}
919-
ex.push(e.slice(0, n));
920-
}
921-
return ex;
922-
}

0 commit comments

Comments
 (0)