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
/**
18
18
* Renders a new plot given the specified *options* and returns the
@@ -368,132 +368,83 @@ export function plot(options = {}) {
368
368
// Flatten any nested marks.
369
369
const marks = options . marks === undefined ? [ ] : options . marks . flat ( Infinity ) . map ( markify ) ;
370
370
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
-
393
371
// A Map from scale name to an array of associated channels.
394
372
const channelsByScale = new Map ( ) ;
395
373
396
374
// Faceting!
397
375
let facets ;
398
376
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
+
399
389
// Collect all facet definitions (top-level facets then mark facets),
400
390
// 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 ) ;
461
407
}
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 ;
462
425
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 ;
478
428
479
429
// 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 ;
483
433
const { fx : x , fy : y } = mark ;
484
- const state = stateByMark . get ( mark ) ;
434
+ const facetInfo = facetCollect . get ( mark ) ;
435
+ if ( facetInfo === undefined ) continue ;
485
436
486
437
// For mark-level facets, compute an index for that mark’s data and options.
487
438
if ( x !== undefined || y !== undefined ) {
488
- state . facetsIndex = filterFacets ( facets , state ) ;
439
+ facetInfo . facetsIndex = filterFacets ( facets , facetInfo ) ;
489
440
}
490
441
491
442
// 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 ;
497
448
}
498
449
}
499
450
@@ -505,23 +456,22 @@ export function plot(options = {}) {
505
456
// the domain. Expunge empty facets, and clear the corresponding elements
506
457
// from the nested index in each mark.
507
458
const nonEmpty = new Set ( ) ;
508
- for ( const { facetsIndex} of stateByMark . values ( ) ) {
459
+ for ( const { facetsIndex} of facetCollect . values ( ) ) {
509
460
if ( facetsIndex ) {
510
461
facetsIndex . forEach ( ( index , i ) => {
511
462
if ( index ?. length > 0 ) nonEmpty . add ( i ) ;
512
463
} ) ;
513
464
}
514
465
}
515
- if ( nonEmpty . size < facets . length ) {
466
+ if ( 0 < nonEmpty . size && nonEmpty . size < facets . length ) {
516
467
facets = facets . filter ( ( _ , i ) => nonEmpty . has ( i ) ) ;
517
- for ( const state of stateByMark . values ( ) ) {
468
+ for ( const state of facetCollect . values ( ) ) {
518
469
const { facetsIndex} = state ;
470
+ //console.warn(facetsIndex);
519
471
if ( ! facetsIndex ) continue ;
520
472
state . facetsIndex = facetsIndex . filter ( ( _ , i ) => nonEmpty . has ( i ) ) ;
521
473
}
522
474
}
523
-
524
- stateByMark . delete ( top ) ;
525
475
}
526
476
527
477
// If a scale is explicitly declared in options, initialize its associated
@@ -534,9 +484,17 @@ export function plot(options = {}) {
534
484
}
535
485
}
536
486
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
+
537
494
// Initialize the marks’ state.
538
495
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 ) || { } ;
540
498
const facetsIndex = mark . facet === "exclude" ? excludeIndex ( state . facetsIndex ) : state . facetsIndex ;
541
499
const { data, facets, channels} = mark . initialize ( facetsIndex , state ) ;
542
500
applyScaleTransforms ( channels , options ) ;
@@ -903,20 +861,3 @@ function nolabel(axis) {
903
861
? axis // use the existing axis if unlabeled
904
862
: Object . assign ( Object . create ( axis ) , { label : undefined } ) ;
905
863
}
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