@@ -186,6 +186,8 @@ export class TestDataProvider {
186186 timeGraphEntries . model . entries . forEach ( entry => {
187187 rowIds . push ( entry . id ) ;
188188 } ) ;
189+ // Add XY plot row IDs
190+ rowIds . push ( - 100 , - 101 ) ;
189191 return rowIds ;
190192 }
191193
@@ -292,6 +294,13 @@ export class TestDataProvider {
292294 } ) ;
293295 } ) ;
294296
297+ // Generate XY plot rows with fixed-time EKG-style data
298+ const xyRows : TimelineChart . TimeGraphRowModel [ ] = [
299+ this . createXYRow ( - 100 , 'Heart Rate (EKG)' , range , ( t ) => this . ekgWaveform ( t ) ) ,
300+ this . createXYRow ( - 101 , 'Respiration (EKG)' , range , ( t ) => this . ekgWaveform ( t * 0.4 + 0.1 ) )
301+ ] ;
302+ rows . push ( ...xyRows ) ;
303+
295304 return {
296305 id : "" ,
297306 arrows,
@@ -300,4 +309,63 @@ export class TestDataProvider {
300309 totalLength : this . totalLength
301310 } ;
302311 }
312+
313+ private createXYRow ( id : number , name : string , range : TimelineChart . TimeGraphRange , fn : ( t : number ) => number ) : TimelineChart . TimeGraphRowModel {
314+ // Generate points with fixed time positions spanning the full trace.
315+ // Only emit points that fall within the requested range (with small buffer).
316+ const totalLen = Number ( this . totalLength ) ;
317+ const rangeStart = Number ( range . start ) ;
318+ const rangeEnd = Number ( range . end ) ;
319+ const rangeLen = rangeEnd - rangeStart ;
320+
321+ // Use enough points to get ~1 point per 2 pixels at current resolution
322+ const numPoints = Math . max ( 200 , Math . min ( 2000 , Math . round ( rangeLen / ( totalLen / 2000 ) ) ) ) ;
323+ const step = rangeLen / ( numPoints - 1 ) ;
324+
325+ const points : TimelineChart . TimeGraphXYPoint [ ] = [ ] ;
326+ for ( let i = 0 ; i < numPoints ; i ++ ) {
327+ const timeNum = rangeStart + i * step ;
328+ const t = timeNum / totalLen ; // normalized position in full trace
329+ const time = BigInt ( Math . round ( timeNum ) ) ;
330+ points . push ( { time, value : Math . max ( 0 , Math . min ( 1 , fn ( t ) ) ) } ) ;
331+ }
332+ return {
333+ id,
334+ name,
335+ range : { start : range . start , end : range . end } ,
336+ states : [ ] ,
337+ annotations : [ ] ,
338+ xySeries : [ { id : `xy_${ id } ` , label : name , color : id === - 100 ? 0x22cc44 : 0x44aaff , points } ] ,
339+ prevPossibleState : range . start ,
340+ nextPossibleState : range . end
341+ } ;
342+ }
343+
344+ /** EKG-style waveform: flat baseline with periodic sharp QRS-like spikes */
345+ private ekgWaveform ( t : number ) : number {
346+ // Repeat every cycle; ~20 beats across the full trace
347+ const cycles = 20 ;
348+ const phase = ( t * cycles ) % 1 ;
349+
350+ // P wave (small bump)
351+ if ( phase > 0.1 && phase < 0.2 ) {
352+ const p = ( phase - 0.1 ) / 0.1 ;
353+ return 0.5 + 0.08 * Math . sin ( p * Math . PI ) ;
354+ }
355+ // QRS complex (sharp spike)
356+ if ( phase > 0.25 && phase < 0.45 ) {
357+ const q = ( phase - 0.25 ) / 0.2 ;
358+ if ( q < 0.2 ) return 0.5 - 0.1 * ( q / 0.2 ) ; // Q dip
359+ if ( q < 0.5 ) return 0.5 - 0.1 + 0.9 * ( ( q - 0.2 ) / 0.3 ) ; // R spike up
360+ if ( q < 0.7 ) return 0.5 + 0.8 - 1.0 * ( ( q - 0.5 ) / 0.2 ) ; // R spike down
361+ return 0.5 - 0.2 + 0.2 * ( ( q - 0.7 ) / 0.3 ) ; // S recovery
362+ }
363+ // T wave (broad bump)
364+ if ( phase > 0.55 && phase < 0.75 ) {
365+ const tw = ( phase - 0.55 ) / 0.2 ;
366+ return 0.5 + 0.12 * Math . sin ( tw * Math . PI ) ;
367+ }
368+ // Baseline
369+ return 0.5 ;
370+ }
303371}
0 commit comments