1
- import { bisectLeft , cross , difference , easeQuadInOut , extent , group , groups , InternMap , interpolate , interpolateNumber , interpolateRound , interpolateHsl , intersection , scaleLinear , select } from "d3" ;
1
+ import { cross , difference , group , groups , InternMap , select } from "d3" ;
2
2
import { Axes , autoAxisTicks , autoScaleLabels } from "./axes.js" ;
3
3
import { Channel , 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" ;
7
7
import { Legends , exposeLegends } from "./legends.js" ;
8
- import { arrayify , constant , isDomainSort , isScaleOptions , keyword , map , maybeNamed , range , second , valueof , where , yes } from "./options.js" ;
9
- import { Scales , ScaleFunctions , autoScaleRange , coerceNumbers , exposeScales , isOrdinalScale } from "./scales.js" ;
8
+ import { arrayify , isDomainSort , isScaleOptions , keyword , map , maybeNamed , range , second , valueof , where , yes } from "./options.js" ;
9
+ import { Scales , ScaleFunctions , autoScaleRange , exposeScales } from "./scales.js" ;
10
10
import { position , registry as scaleRegistry } from "./scales/index.js" ;
11
11
import { applyInlineStyles , maybeClassName , maybeClip , styles } from "./style.js" ;
12
- import { maybeTimeFilter , maybeTween , defaultKeys } from "./time.js" ;
12
+ import { animate , maybeTimeFilter , defaultKey , prepareTimeScale } from "./time.js" ;
13
13
import { basic , initializer } from "./transforms/basic.js" ;
14
14
import { maybeInterval } from "./transforms/interval.js" ;
15
15
import { consumeWarnings } from "./warnings.js" ;
16
16
17
17
export function plot ( options = { } ) {
18
- const { facet, time , style, caption, ariaLabel, ariaDescription} = options ;
18
+ const { facet, style, caption, ariaLabel, ariaDescription} = options ;
19
19
20
20
// className for inline styles
21
21
const className = maybeClassName ( options . className ) ;
@@ -72,7 +72,7 @@ export function plot(options = {}) {
72
72
}
73
73
74
74
// Initialize the marks’ state.
75
- const markTimes = new Map ( ) ;
75
+ let hasTime = ! ! options . time ;
76
76
for ( const mark of marks ) {
77
77
if ( stateByMark . has ( mark ) ) throw new Error ( "duplicate mark; each mark must be unique" ) ;
78
78
@@ -84,8 +84,12 @@ export function plot(options = {}) {
84
84
85
85
const { data, facets, channels, time, timeFacets} = mark . initialize ( markFacets , facetChannels ) ;
86
86
applyScaleTransforms ( channels , options ) ;
87
- stateByMark . set ( mark , { data, facets, channels} ) ;
88
- if ( timeFacets . length ) markTimes . set ( mark , { time, timeFacets} ) ;
87
+ if ( timeFacets . length ) {
88
+ stateByMark . set ( mark , { data, facets, channels, time, timeFacets, layouts : [ ] } ) ;
89
+ hasTime = true ;
90
+ } else {
91
+ stateByMark . set ( mark , { data, facets, channels} ) ;
92
+ }
89
93
}
90
94
91
95
// Initalize the scales and axes.
@@ -135,79 +139,18 @@ export function plot(options = {}) {
135
139
}
136
140
137
141
// Infer the time scale
138
- let interpolateTime ;
139
- let timeScale ;
140
- if ( markTimes . size ) {
141
- ( { time : timeScale } = Scales ( new Map ( [ [ "time" , Array . from ( markTimes , ( [ , { time} ] ) => ( { value : time } ) ) ] ] ) , options ) ) ;
142
- if ( isOrdinalScale ( timeScale ) ) {
143
- const index = new InternMap ( timeScale . domain . map ( ( d , i ) => [ d , i ] ) ) ;
144
- // ordinal times are mapped to their rank in the ordinal domain
145
- for ( const [ , m ] of markTimes ) {
146
- const domain = [ ...intersection ( timeScale . domain , m . time ) ] ;
147
- m . time = m . time . map ( d => index . get ( d ) ) ;
148
- m . domain = domain . map ( d => index . get ( d ) ) ;
149
- }
150
- } else {
151
- for ( const [ , m ] of markTimes ) {
152
- m . time = coerceNumbers ( m . time ) ;
153
- m . domain = [ ] ;
154
- for ( const t of m . time ) {
155
- if ( isNaN ( t ) || ! isFinite ( t ) ) continue ;
156
- const i = bisectLeft ( m . domain , t ) ;
157
- if ( m . domain [ i ] === t ) continue ;
158
- m . domain . splice ( i , 0 , t ) ;
159
- }
160
- }
161
- }
162
-
163
- const {
164
- delay = 0 ,
165
- duration = 5000 ,
166
- direction = 1 ,
167
- playbackRate = 1 ,
168
- initial,
169
- autoplay = true ,
170
- iterations = 0 ,
171
- loop = ! ! iterations ,
172
- alternate = false ,
173
- loopDelay = 1000
174
- } = time == null ? { } : time ;
175
- interpolateTime = scaleLinear ( )
176
- . domain ( isOrdinalScale ( timeScale ) ? [ 0 , timeScale . domain . length - 1 ] : extent ( timeScale . domain ) )
177
- . range ( [ 0 , 1 ] ) ;
178
-
179
- if ( typeof delay !== "number" || delay < 0 || ! isFinite ( delay ) ) throw new Error ( `Unsupported delay ${ delay } .` ) ;
180
- if ( typeof duration !== "number" || duration < 0 || ! isFinite ( duration ) ) throw new Error ( `Unsupported duration ${ duration } .` ) ;
181
- if ( ! [ - 1 , 1 , null ] . includes ( direction ) ) throw new Error ( `Unsupported direction ${ direction } .` ) ;
182
- if ( initial != null && Number . isNaN ( interpolateTime ( initial ) ) ) throw new Error ( `Unsupported initial time ${ initial } .` ) ;
183
- if ( typeof autoplay !== "boolean" ) throw new Error ( `Unsupported autoplay option ${ autoplay } .` ) ;
184
- if ( typeof loop !== "boolean" ) throw new Error ( `Unsupported loop option ${ loop } .` ) ;
185
- if ( typeof playbackRate !== "number" ) throw new Error ( `Unsupported playback rate ${ playbackRate } .` ) ;
186
- if ( typeof alternate !== "boolean" ) throw new Error ( `Unsupported alternate option ${ alternate } .` ) ;
187
- if ( typeof loopDelay !== "number" || loopDelay < 0 ) throw new Error ( `Unsupported loop delay ${ loopDelay } .` ) ;
188
- scaleDescriptors . time = {
189
- type : timeScale . type ,
190
- domain : timeScale . domain ,
191
- delay,
192
- duration,
193
- direction,
194
- playbackRate,
195
- initial,
196
- autoplay,
197
- iterations,
198
- loop,
199
- alternate,
200
- loopDelay,
201
- scale : timeScale . scale
202
- } ;
142
+ const time = hasTime && prepareTimeScale ( options , stateByMark ) ;
143
+ if ( time ) {
144
+ scaleDescriptors . time = time ;
145
+ scales . time = time . scale ;
203
146
}
204
147
205
148
for ( const [ mark , state ] of stateByMark ) {
206
149
const { facets, values} = state ;
207
150
208
151
// Reassemble time facets
209
152
if ( mark . time ) {
210
- const m = markTimes . get ( mark ) ;
153
+ const m = stateByMark . get ( mark ) ;
211
154
const { domain, time, timeFacets} = m ;
212
155
if ( domain . length <= 1 ) continue ;
213
156
@@ -226,8 +169,6 @@ export function plot(options = {}) {
226
169
}
227
170
}
228
171
229
- const animateMarks = [ ] ;
230
-
231
172
const { width, height} = dimensions ;
232
173
233
174
const svg = create ( "svg" , context )
@@ -305,14 +246,14 @@ export function plot(options = {}) {
305
246
. attr ( "transform" , facetTranslate ( fx , fy ) )
306
247
. each ( function ( key ) {
307
248
const j = indexByFacet . get ( key ) ;
308
- for ( const [ mark , { channels, values, facets} ] of stateByMark ) {
249
+ for ( const [ mark , { channels, values, facets, layouts } ] of stateByMark ) {
309
250
const facet = facets ? mark . filter ( facets [ j ] ?? facets [ 0 ] , channels , values ) : null ;
310
- const index = mark . time ? [ ] : facet ;
251
+ const index = layouts ? [ ] : facet ;
311
252
const node = mark . render ( index , scales , values , subdimensions , context ) ;
312
253
if ( node != null ) {
313
254
this . appendChild ( node ) ;
314
- if ( mark . time ) {
315
- animateMarks . push ( {
255
+ if ( layouts ) {
256
+ layouts . push ( {
316
257
mark,
317
258
node,
318
259
facet,
@@ -323,14 +264,14 @@ export function plot(options = {}) {
323
264
}
324
265
} ) ;
325
266
} else {
326
- for ( const [ mark , { channels, values, facets} ] of stateByMark ) {
267
+ for ( const [ mark , { channels, values, facets, layouts } ] of stateByMark ) {
327
268
const facet = facets ? mark . filter ( facets [ 0 ] , channels , values ) : null ;
328
- const index = mark . time ? [ ] : facet ;
269
+ const index = layouts ? [ ] : facet ;
329
270
const node = mark . render ( index , scales , values , dimensions , context ) ;
330
271
if ( node != null ) {
331
272
svg . appendChild ( node ) ;
332
273
if ( mark . time ) {
333
- animateMarks . push ( {
274
+ layouts . push ( {
334
275
mark,
335
276
node,
336
277
facet,
@@ -373,191 +314,14 @@ export function plot(options = {}) {
373
314
. text ( `${ w . toLocaleString ( "en-US" ) } warning${ w === 1 ? "" : "s" } . Please check the console.` ) ;
374
315
}
375
316
376
- if ( animateMarks . length > 0 ) {
377
- const { alternate, autoplay, delay, direction, duration, initial, iterations, loopDelay} = scaleDescriptors . time ;
378
- let { loop, playbackRate} = scaleDescriptors . time ;
379
- let lastTick ;
380
- let t1 , currentTime , ended = false , paused = ! autoplay ;
381
-
382
- const timeupdate = ( t ) => {
383
- if ( t1 === ( t = Math . max ( 0 , Math . min ( 1 , t ) ) ) ) return ;
384
- currentTime = interpolateTime . invert ( t1 = t ) ;
385
- for ( const timeMark of animateMarks ) {
386
- const { mark, facet, dimensions} = timeMark ;
387
- const { channels : { key} } = stateByMark . get ( mark ) ;
388
- const { domain} = markTimes . get ( mark ) ;
389
- const K = key ? key . value : null ;
390
- const i0 = bisectLeft ( domain , currentTime ) ;
391
- const time0 = domain [ i0 - 1 ] ;
392
- const time1 = domain [ i0 ] !== undefined ? domain [ i0 ] : time0 ;
393
- const timet = time1 === time0 ? 0 : ( t - interpolateTime ( time0 ) ) / ( interpolateTime ( time1 ) - interpolateTime ( time0 ) ) ;
394
- const { interp, opacity} = stateByMark . get ( mark ) ;
395
- const T = interp . time ;
396
- let timeNode ;
397
- const I0 = facet . filter ( i => T [ i ] === time0 ) ; // preceding keyframe
398
- const I1 = facet . filter ( i => T [ i ] === time1 ) ; // following keyframe
399
- let enter = [ ] , update = [ ] , target = [ ] , exit = [ ] ;
400
- if ( K ) {
401
- const K0 = new Set ( I0 . map ( i => K [ i ] ) ) ;
402
- const K1 = new Set ( I1 . map ( i => K [ i ] ) ) ;
403
- const Kenter = difference ( K1 , K0 ) ;
404
- const Kupdate = intersection ( K0 , K1 ) ;
405
- const Kexit = difference ( K0 , K1 ) ;
406
- enter = I1 . filter ( i => Kenter . has ( K [ i ] ) ) ;
407
- update = I0 . filter ( i => Kupdate . has ( K [ i ] ) ) ;
408
- target = update . map ( i => I1 . find ( j => K [ i ] === K [ j ] ) ) ; // TODO: use an index
409
- exit = I0 . filter ( i => Kexit . has ( K [ i ] ) ) ;
410
- } else {
411
- enter = I1 ;
412
- exit = I0 ;
413
- }
414
- const n = update . length ;
415
- const nt = n + enter . length + exit . length ;
416
- const Ii = Uint32Array . from ( { length : nt } ) . map ( ( _ , i ) => i + T . length ) ;
417
- if ( exit . length || enter . length ) interp . opacity = opacity ;
418
-
419
- // TODO This is interpolating the already-scaled values, but we
420
- // probably want to interpolate in data space instead and then
421
- // re-apply the scales. I’m not sure what to do for ordinal data,
422
- // but interpolating in data space will ensure that the resulting
423
- // instantaneous visualization is meaningful and valid. TODO If the
424
- // data is sparse (not all series have values for all times), then
425
- // we will need a separate key channel to align the start and end
426
- // values for interpolation; this code currently assumes that the
427
- // data is complete.
428
- for ( const k in interp ) {
429
- if ( k === "time" ) {
430
- for ( let i = 0 ; i < nt ; ++ i ) interp [ k ] [ Ii [ i ] ] = currentTime ;
431
- } else if ( k === "opacity" ) {
432
- const _exit = easeQuadInOut ( 1 - timet ) ;
433
- const _enter = easeQuadInOut ( timet ) ;
434
- for ( let i = 0 ; i < exit . length ; ++ i ) interp [ k ] [ Ii [ i ] ] = _exit ;
435
- for ( let i = 0 ; i < n ; ++ i ) interp [ k ] [ Ii [ exit . length + i ] ] = 1 ;
436
- for ( let i = 0 ; i < enter . length ; ++ i ) interp [ k ] [ Ii [ exit . length + n + i ] ] = _enter ;
437
- } else {
438
- const tween = maybeTween ( mark . tween , k ) ;
439
- const interpolator = tween ? tween :
440
- [ "time" ] . includes ( k ) ? ( ) => constant ( currentTime ) :
441
- [ "x" , "x1" , "x2" , "y" , "y1" , "y2" , "r" ] . includes ( k ) ? interpolateNumber :
442
- [ "fill" , "stroke" ] . includes ( k ) ? interpolateHsl :
443
- [ "text" ] . includes ( k ) ? ( a , b ) => typeof a === "number" ? ( frac ( a ) || frac ( b ) || Math . abs ( a - b ) < 3 ) ? interpolateNumber ( a , b ) : interpolateRound ( a , b ) : constant ( a ) :
444
- interpolate ;
445
- for ( let i = 0 ; i < exit . length ; ++ i ) interp [ k ] [ Ii [ i ] ] = interp [ k ] [ exit [ i ] ] ;
446
- for ( let i = 0 ; i < n ; ++ i ) {
447
- const prev = interp [ k ] [ update [ i ] ] , next = interp [ k ] [ target [ i ] ] ;
448
- interp [ k ] [ Ii [ i + exit . length ] ] = prev == next ? prev : interpolator ( prev , next ) ( timet ) ;
449
- }
450
- for ( let i = 0 ; i < enter . length ; ++ i ) interp [ k ] [ Ii [ i + n + exit . length ] ] = interp [ k ] [ enter [ i ] ] ;
451
- }
452
- }
453
- const ifacet = [ ...facet . filter ( i => T [ i ] < time1 ) , ...( currentTime < time1 ) ? Ii : [ ] , ...facet . filter ( i => T [ i ] >= time1 ) ] ;
454
- const index = mark . timeFilter ( ifacet , T , currentTime ) ;
455
- timeNode = mark . render ( index , scales , interp , dimensions , context ) ;
456
- timeMark . node . replaceWith ( timeMark . node = timeNode ) ;
457
- }
458
-
459
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/timeupdate_event
460
- if ( window . CustomEvent ) figure . dispatchEvent ( new window . CustomEvent ( "timeupdate" ) ) ;
461
- } ;
462
-
463
- let ticker = direction * playbackRate < 0 ? 1 : 0 ;
464
- const tick = function ( ) {
465
- if ( paused ) {
466
- lastTick = undefined ;
467
- } else {
468
- // advance (or rewind) the clock by dt
469
- const dt = lastTick === undefined
470
- ? ( lastTick = performance . now ( ) , 0 )
471
- : performance . now ( ) - lastTick ;
472
- lastTick += dt ;
473
- ticker += dt * direction * playbackRate / duration ;
474
- }
475
-
476
- // t is the projection of the clock to the looping interval
477
- let t = ticker ;
478
-
479
- if ( loop ) {
480
- const s = 1 + loopDelay / duration ;
481
- const t0 = Math . floor ( ( 0.5 + Math . abs ( t - 0.5 ) ) / s ) ;
482
- if ( iterations && t0 >= iterations ) {
483
- t = 2 ; // ends
484
- } else {
485
- const f = t - s * t0 * Math . sign ( t - 0.5 ) ;
486
- if ( alternate ) {
487
- t = t0 % 2 ? 1 - f : f ;
488
- } else {
489
- t = f ;
490
- }
491
- t = Math . max ( 0 , Math . min ( 1 , t ) ) ;
492
- }
493
- }
494
- ended = t < 0 || t > 1 ;
495
- if ( ended ) paused = true ;
496
-
497
- timeupdate ( t ) ;
498
- if ( figure . parentElement ) requestAnimationFrame ( tick ) ;
499
- } ;
500
-
501
- // When using setTime, the argument is in the original time domain
502
- const setTime = function ( time ) {
503
- if ( isOrdinalScale ( timeScale ) ) {
504
- const i = timeScale . domain . indexOf ( time ) ;
505
- if ( i === - 1 ) throw new Error ( `unknown time ${ time } ` ) ;
506
- time = i ;
507
- }
508
- ticker = interpolateTime ( time ) ;
509
- currentTime = interpolateTime . invert ( ticker ) ;
510
- ended = ticker < 0 || ticker > 1 ;
511
- lastTick = t1 = undefined ;
512
- timeupdate ( Math . max ( 0 , Math . min ( 1 , ticker ) ) ) ;
513
- } ;
514
-
515
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play
516
- figure . play = ( ) => {
517
- if ( ended ) {
518
- setTime ( initial == null
519
- ? timeScale . domain [ direction * playbackRate < 0 ? timeScale . domain . length - 1 : 0 ]
520
- : initial
521
- ) ;
522
- }
523
- paused = false ;
524
- return new Promise ( r => r ( ) ) ;
525
- } ;
526
-
527
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/pause
528
- figure . pause = ( ) => {
529
- paused = true ;
530
- t1 = undefined ;
531
- } ;
532
-
533
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/duration
534
- Object . defineProperty ( figure , 'duration' , { get : ( ) => duration / 1000 } ) ;
535
-
536
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/paused
537
- Object . defineProperty ( figure , 'paused' , { get : ( ) => paused } ) ;
538
-
539
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended
540
- Object . defineProperty ( figure , 'ended' , { get : ( ) => ended } ) ;
541
-
542
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/currentTime
543
- Object . defineProperty ( figure , 'currentTime' , {
544
- get : ( ) => isOrdinalScale ( timeScale ) ? timeScale . domain [ Math . floor ( currentTime ) ]
545
- : timeScale . type === "utc" || timeScale . type === "time" ? new Date ( currentTime )
546
- : currentTime ,
547
- set : setTime
548
- } ) ;
549
-
550
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/playbackRate
551
- // https://github.com/whatwg/html/issues/3754
552
- Object . defineProperty ( figure , 'playbackRate' , { get : ( ) => playbackRate , set : ( l ) => { ! isNaN ( l = + l ) && ( playbackRate = l ) ; } } ) ;
553
-
554
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loop
555
- Object . defineProperty ( figure , 'loop' , { get : ( ) => loop , set : ( l ) => { loop = ! ! l ; } } ) ;
556
-
557
- if ( initial != null ) setTime ( initial ) ;
558
-
559
- timeupdate ( ticker ) ;
560
- setTimeout ( tick , delay ) ;
317
+ if ( hasTime ) {
318
+ animate (
319
+ stateByMark ,
320
+ time ,
321
+ scales ,
322
+ figure ,
323
+ context
324
+ ) ;
561
325
}
562
326
563
327
return figure ;
@@ -596,7 +360,7 @@ export class Mark {
596
360
group ( facet , i => T [ i ] ) ,
597
361
( [ t , I ] ) => ( timeFacets . push ( j ) , time . push ( t ) , I )
598
362
) ) ;
599
- this . channels . key = { value : this . key ?? defaultKeys ( T ) , filter : null } ;
363
+ this . channels . key = { value : this . key ?? defaultKey ( T ) , filter : null } ;
600
364
}
601
365
if ( this . transform != null ) {
602
366
( { data, facets} = this . transform ( data , facets ) ) , data = arrayify ( data ) ;
@@ -766,7 +530,3 @@ class FacetMap2 extends FacetMap {
766
530
return this ;
767
531
}
768
532
}
769
-
770
- function frac ( x ) {
771
- return x - Math . floor ( x ) ;
772
- }
0 commit comments