1
- import { ascending , cross , group , select , sort , sum } from "d3" ;
1
+ import { ascending , cross , group , select , sort } from "d3" ;
2
2
import { Axes , autoAxisTicks , autoScaleLabels } from "./axes.js" ;
3
- import { Channel , Channels , channelDomain , valueObject } from "./channel.js" ;
3
+ import { Channels , channelDomain , valueObject } from "./channel.js" ;
4
4
import { Context , create } from "./context.js" ;
5
5
import { defined } from "./defined.js" ;
6
6
import { Dimensions } from "./dimensions.js" ;
@@ -11,8 +11,8 @@ import {position, registry as scaleRegistry} from "./scales/index.js";
11
11
import { applyInlineStyles , maybeClassName , maybeClip , styles } from "./style.js" ;
12
12
import { basic , initializer } from "./transforms/basic.js" ;
13
13
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" ;
16
16
17
17
/** @jsdoc plot */
18
18
export function plot ( options = { } ) {
@@ -24,132 +24,83 @@ export function plot(options = {}) {
24
24
// Flatten any nested marks.
25
25
const marks = options . marks === undefined ? [ ] : options . marks . flat ( Infinity ) . map ( markify ) ;
26
26
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
-
49
27
// A Map from scale name to an array of associated channels.
50
28
const channelsByScale = new Map ( ) ;
51
29
52
30
// Faceting!
53
31
let facets ;
54
32
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
+
55
45
// Collect all facet definitions (top-level facets then mark facets),
56
46
// 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 ) ;
117
63
}
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 ;
118
81
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 ;
134
84
135
85
// 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 ;
139
89
const { fx : x , fy : y } = mark ;
140
- const state = stateByMark . get ( mark ) ;
90
+ const facetInfo = facetCollect . get ( mark ) ;
91
+ if ( facetInfo === undefined ) continue ;
141
92
142
93
// For mark-level facets, compute an index for that mark’s data and options.
143
94
if ( x !== undefined || y !== undefined ) {
144
- state . facetsIndex = filterFacets ( facets , state ) ;
95
+ facetInfo . facetsIndex = filterFacets ( facets , facetInfo ) ;
145
96
}
146
97
147
98
// 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 ;
153
104
}
154
105
}
155
106
@@ -161,23 +112,22 @@ export function plot(options = {}) {
161
112
// the domain. Expunge empty facets, and clear the corresponding elements
162
113
// from the nested index in each mark.
163
114
const nonEmpty = new Set ( ) ;
164
- for ( const { facetsIndex} of stateByMark . values ( ) ) {
115
+ for ( const { facetsIndex} of facetCollect . values ( ) ) {
165
116
if ( facetsIndex ) {
166
117
facetsIndex . forEach ( ( index , i ) => {
167
118
if ( index ?. length > 0 ) nonEmpty . add ( i ) ;
168
119
} ) ;
169
120
}
170
121
}
171
- if ( nonEmpty . size < facets . length ) {
122
+ if ( 0 < nonEmpty . size && nonEmpty . size < facets . length ) {
172
123
facets = facets . filter ( ( _ , i ) => nonEmpty . has ( i ) ) ;
173
- for ( const state of stateByMark . values ( ) ) {
124
+ for ( const state of facetCollect . values ( ) ) {
174
125
const { facetsIndex} = state ;
126
+ //console.warn(facetsIndex);
175
127
if ( ! facetsIndex ) continue ;
176
128
state . facetsIndex = facetsIndex . filter ( ( _ , i ) => nonEmpty . has ( i ) ) ;
177
129
}
178
130
}
179
-
180
- stateByMark . delete ( top ) ;
181
131
}
182
132
183
133
// If a scale is explicitly declared in options, initialize its associated
@@ -190,9 +140,17 @@ export function plot(options = {}) {
190
140
}
191
141
}
192
142
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
+
193
150
// Initialize the marks’ state.
194
151
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 ) || { } ;
196
154
const facetsIndex = mark . facet === "exclude" ? excludeIndex ( state . facetsIndex ) : state . facetsIndex ;
197
155
const { data, facets, channels} = mark . initialize ( facetsIndex , state ) ;
198
156
applyScaleTransforms ( channels , options ) ;
@@ -569,20 +527,3 @@ function nolabel(axis) {
569
527
? axis // use the existing axis if unlabeled
570
528
: Object . assign ( Object . create ( axis ) , { label : undefined } ) ;
571
529
}
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