@@ -4,7 +4,7 @@ import {Channel, channelSort} from "./channel.js";
4
4
import { defined } from "./defined.js" ;
5
5
import { Dimensions } from "./dimensions.js" ;
6
6
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" ;
8
8
import { Scales , ScaleFunctions , autoScaleRange , applyScales , exposeScales } from "./scales.js" ;
9
9
import { applyInlineStyles , maybeClassName , maybeClip , styles } from "./style.js" ;
10
10
import { basic } from "./transforms/basic.js" ;
@@ -16,40 +16,71 @@ export function plot(options = {}) {
16
16
// className for inline styles
17
17
const className = maybeClassName ( options . className ) ;
18
18
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
-
26
19
// Flatten any nested marks.
27
20
const marks = options . marks === undefined ? [ ] : options . marks . flat ( Infinity ) . map ( markify ) ;
28
21
29
22
// 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 ( ) ;
32
27
33
28
// A Map from scale name to an array of associated channels.
34
29
const scaleChannels = new Map ( ) ;
35
30
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
+
36
58
// Initialize the marks’ channels, indexing them by mark and scale as needed.
37
- // Also apply any scale transforms.
38
59
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 ) ;
41
67
for ( const [ , channel ] of channels ) {
42
68
const { scale} = channel ;
43
69
if ( scale !== undefined ) {
44
70
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 ) ;
48
72
else scaleChannels . set ( scale , [ channel ] ) ;
49
73
}
50
74
}
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 ) ;
53
84
}
54
85
55
86
const scaleDescriptors = Scales ( scaleChannels , options ) ;
@@ -61,6 +92,11 @@ export function plot(options = {}) {
61
92
autoScaleLabels ( scaleChannels , scaleDescriptors , axes , dimensions , options ) ;
62
93
autoAxisTicks ( scaleDescriptors , axes ) ;
63
94
95
+ // Compute channel values, applying scales as needed.
96
+ for ( const [ mark , channels ] of channelsByMark ) {
97
+ valuesByMark . set ( mark , applyScales ( channels , scales ) ) ;
98
+ }
99
+
64
100
const { width, height} = dimensions ;
65
101
66
102
const svg = create ( "svg" )
@@ -91,18 +127,77 @@ export function plot(options = {}) {
91
127
. node ( ) ;
92
128
93
129
// 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?
96
132
if ( axisY ) svg . appendChild ( axisY . render ( null , scales , dimensions ) ) ;
97
133
if ( axisX ) svg . appendChild ( axisX . render ( null , scales , dimensions ) ) ;
98
134
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
+ }
106
201
}
107
202
108
203
// Wrap the plot in a figure with a caption, if desired.
@@ -166,14 +261,14 @@ export class Mark {
166
261
this . dy = + dy || 0 ;
167
262
this . clip = maybeClip ( clip ) ;
168
263
}
169
- initialize ( facets , facetChannels ) {
264
+ initialize ( facetIndex , facetChannels ) {
170
265
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 ;
172
267
if ( data !== undefined && this . transform !== undefined ) {
173
- if ( facets === undefined ) index = index . length ? [ index ] : [ ] ;
268
+ if ( facetIndex === undefined ) index = index . length ? [ index ] : [ ] ;
174
269
( { facets : index , data} = this . transform ( data , index ) ) ;
175
270
data = arrayify ( data ) ;
176
- if ( facets === undefined && index . length ) ( [ index ] = index ) ;
271
+ if ( facetIndex === undefined && index . length ) ( [ index ] = index ) ;
177
272
}
178
273
const channels = this . channels . map ( channel => {
179
274
const { name} = channel ;
@@ -215,128 +310,6 @@ class Render extends Mark {
215
310
render ( ) { }
216
311
}
217
312
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
-
340
313
// Derives a copy of the specified axis with the label disabled.
341
314
function nolabel ( axis ) {
342
315
return axis === undefined || axis . label === undefined
0 commit comments