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" ;
22import { Axes , autoAxisTicks , autoScaleLabels } from "./axes.js" ;
33import { Channel , Channels , channelDomain , valueObject } from "./channel.js" ;
44import { Context , create } from "./context.js" ;
55import { defined } from "./defined.js" ;
66import { Dimensions } from "./dimensions.js" ;
77import { 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" ;
1010import { position , registry as scaleRegistry } from "./scales/index.js" ;
1111import { applyInlineStyles , maybeClassName , maybeClip , styles } from "./style.js" ;
12- import { maybeTimeFilter , maybeTween , defaultKeys } from "./time.js" ;
12+ import { animate , maybeTimeFilter , defaultKey , prepareTimeScale } from "./time.js" ;
1313import { basic , initializer } from "./transforms/basic.js" ;
1414import { maybeInterval } from "./transforms/interval.js" ;
1515import { consumeWarnings } from "./warnings.js" ;
1616
1717export function plot ( options = { } ) {
18- const { facet, time , style, caption, ariaLabel, ariaDescription} = options ;
18+ const { facet, style, caption, ariaLabel, ariaDescription} = options ;
1919
2020 // className for inline styles
2121 const className = maybeClassName ( options . className ) ;
@@ -72,7 +72,7 @@ export function plot(options = {}) {
7272 }
7373
7474 // Initialize the marks’ state.
75- const markTimes = new Map ( ) ;
75+ let hasTime = ! ! options . time ;
7676 for ( const mark of marks ) {
7777 if ( stateByMark . has ( mark ) ) throw new Error ( "duplicate mark; each mark must be unique" ) ;
7878
@@ -84,8 +84,12 @@ export function plot(options = {}) {
8484
8585 const { data, facets, channels, time, timeFacets} = mark . initialize ( markFacets , facetChannels ) ;
8686 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+ }
8993 }
9094
9195 // Initalize the scales and axes.
@@ -135,79 +139,18 @@ export function plot(options = {}) {
135139 }
136140
137141 // 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 ;
203146 }
204147
205148 for ( const [ mark , state ] of stateByMark ) {
206149 const { facets, values} = state ;
207150
208151 // Reassemble time facets
209152 if ( mark . time ) {
210- const m = markTimes . get ( mark ) ;
153+ const m = stateByMark . get ( mark ) ;
211154 const { domain, time, timeFacets} = m ;
212155 if ( domain . length <= 1 ) continue ;
213156
@@ -226,8 +169,6 @@ export function plot(options = {}) {
226169 }
227170 }
228171
229- const animateMarks = [ ] ;
230-
231172 const { width, height} = dimensions ;
232173
233174 const svg = create ( "svg" , context )
@@ -305,14 +246,14 @@ export function plot(options = {}) {
305246 . attr ( "transform" , facetTranslate ( fx , fy ) )
306247 . each ( function ( key ) {
307248 const j = indexByFacet . get ( key ) ;
308- for ( const [ mark , { channels, values, facets} ] of stateByMark ) {
249+ for ( const [ mark , { channels, values, facets, layouts } ] of stateByMark ) {
309250 const facet = facets ? mark . filter ( facets [ j ] ?? facets [ 0 ] , channels , values ) : null ;
310- const index = mark . time ? [ ] : facet ;
251+ const index = layouts ? [ ] : facet ;
311252 const node = mark . render ( index , scales , values , subdimensions , context ) ;
312253 if ( node != null ) {
313254 this . appendChild ( node ) ;
314- if ( mark . time ) {
315- animateMarks . push ( {
255+ if ( layouts ) {
256+ layouts . push ( {
316257 mark,
317258 node,
318259 facet,
@@ -323,14 +264,14 @@ export function plot(options = {}) {
323264 }
324265 } ) ;
325266 } else {
326- for ( const [ mark , { channels, values, facets} ] of stateByMark ) {
267+ for ( const [ mark , { channels, values, facets, layouts } ] of stateByMark ) {
327268 const facet = facets ? mark . filter ( facets [ 0 ] , channels , values ) : null ;
328- const index = mark . time ? [ ] : facet ;
269+ const index = layouts ? [ ] : facet ;
329270 const node = mark . render ( index , scales , values , dimensions , context ) ;
330271 if ( node != null ) {
331272 svg . appendChild ( node ) ;
332273 if ( mark . time ) {
333- animateMarks . push ( {
274+ layouts . push ( {
334275 mark,
335276 node,
336277 facet,
@@ -373,191 +314,14 @@ export function plot(options = {}) {
373314 . text ( `${ w . toLocaleString ( "en-US" ) } warning${ w === 1 ? "" : "s" } . Please check the console.` ) ;
374315 }
375316
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+ ) ;
561325 }
562326
563327 return figure ;
@@ -596,7 +360,7 @@ export class Mark {
596360 group ( facet , i => T [ i ] ) ,
597361 ( [ t , I ] ) => ( timeFacets . push ( j ) , time . push ( t ) , I )
598362 ) ) ;
599- this . channels . key = { value : this . key ?? defaultKeys ( T ) , filter : null } ;
363+ this . channels . key = { value : this . key ?? defaultKey ( T ) , filter : null } ;
600364 }
601365 if ( this . transform != null ) {
602366 ( { data, facets} = this . transform ( data , facets ) ) , data = arrayify ( data ) ;
@@ -766,7 +530,3 @@ class FacetMap2 extends FacetMap {
766530 return this ;
767531 }
768532}
769-
770- function frac ( x ) {
771- return x - Math . floor ( x ) ;
772- }
0 commit comments