@@ -19,6 +19,7 @@ import { EntryTree } from './utils/filter-tree/entry-tree';
1919import { TimeRange } from 'traceviewer-base/src/utils/time-range' ;
2020import { BIMath } from 'timeline-chart/lib/bigint-utils' ;
2121import { debounce } from 'lodash' ;
22+ import * as d3 from 'd3' ;
2223
2324import {
2425 applyYAxis ,
@@ -68,12 +69,18 @@ interface GenericXYState extends AbstractTreeOutputState {
6869 allMax : number ;
6970 allMin : number ;
7071 cursor : string ;
72+ timelineUnit : string ;
73+ timelineUnitType : TimelineUnitType ;
7174}
7275
76+ type TimelineUnitType = 'time' | 'cycles' | 'bytes' | 'calls' | 'bytes/sec' | 'iterations/sec' ;
77+
7378interface GenericXYProps extends AbstractOutputProps {
7479 formatX ?: ( x : number | bigint | string ) => string ;
7580 formatY ?: ( y : number ) => string ;
7681 stacked ?: boolean ;
82+ timelineUnit ?: string ;
83+ timelineUnitType ?: TimelineUnitType ;
7784}
7885
7986enum ChartMode {
@@ -91,8 +98,10 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
9198 private readonly chartRef = React . createRef < any > ( ) ;
9299 private readonly yAxisRef : any ;
93100 private readonly divRef = React . createRef < HTMLDivElement > ( ) ;
101+ private readonly timelineRef = React . createRef < SVGSVGElement > ( ) ;
94102
95103 private readonly margin = { top : 15 , right : 0 , bottom : 6 , left : this . getYAxisWidth ( ) } ;
104+ private readonly timelineHeight = 30 ;
96105
97106 private mouseIsDown = false ;
98107 private isPanning = false ;
@@ -134,7 +143,9 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
134143 allMax : 0 ,
135144 allMin : 0 ,
136145 cursor : 'default' ,
137- showTree : true
146+ showTree : true ,
147+ timelineUnit : this . props . timelineUnit || 'ms' ,
148+ timelineUnitType : this . props . timelineUnitType || 'time'
138149 } ;
139150
140151 this . addPinViewOptions ( ( ) => ( {
@@ -315,6 +326,7 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
315326 } )
316327 ) ;
317328 this . calculateYRange ( ) ;
329+ this . updateTimeline ( ) ;
318330 return ;
319331 }
320332
@@ -326,6 +338,7 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
326338 const xy = this . buildXYData ( series , this . mode ) ;
327339 flushSync ( ( ) => this . setState ( { xyData : xy , outputStatus : model . status ?? ResponseStatus . COMPLETED } ) ) ;
328340 this . calculateYRange ( ) ;
341+ this . updateTimeline ( ) ;
329342 }
330343
331344 private buildXYData ( seriesObj : XYSeries [ ] , mode : ChartMode ) : GenericXYData {
@@ -439,7 +452,10 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
439452 const checksChanged = prevState . checkedSeries !== this . state . checkedSeries ;
440453
441454 if ( sizeChanged || viewChanged || checksChanged ) {
442- if ( this . getChartWidth ( ) > 0 ) this . _debouncedUpdateXY ( ) ;
455+ if ( this . getChartWidth ( ) > 0 ) {
456+ this . _debouncedUpdateXY ( ) ;
457+ this . updateTimeline ( ) ;
458+ }
443459 }
444460 }
445461
@@ -710,6 +726,109 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
710726 }
711727 }
712728
729+ private renderTimeline ( ) : React . ReactNode {
730+ if ( this . state . timelineUnitType === 'time' && ! this . isTimeAxis ) return < svg /> ;
731+
732+ const chartWidth = this . getChartWidth ( ) ;
733+ if ( chartWidth <= 0 ) return < svg /> ;
734+
735+ return (
736+ < svg
737+ ref = { this . timelineRef }
738+ width = { chartWidth }
739+ height = { this . timelineHeight }
740+ style = { { marginLeft : this . margin . left } }
741+ />
742+ ) ;
743+ }
744+
745+ private updateTimeline ( ) : void {
746+ if ( ! this . timelineRef . current ) return ;
747+ if ( this . state . timelineUnitType === 'time' && ! this . isTimeAxis ) return ;
748+
749+ const svg = d3 . select ( this . timelineRef . current ) ;
750+ svg . selectAll ( '*' ) . remove ( ) ;
751+
752+ const chartWidth = this . getChartWidth ( ) ;
753+ const { start, end } = this . getTimelineRange ( ) ;
754+
755+ const scale = d3 . scaleLinear ( )
756+ . domain ( [ start , end ] )
757+ . range ( [ 0 , chartWidth ] ) ;
758+
759+ const axis = d3 . axisBottom ( scale )
760+ . tickFormat ( d => this . formatTimelineValue ( Number ( d ) ) ) ;
761+
762+ svg . append ( 'g' )
763+ . attr ( 'transform' , `translate(0, 0)` )
764+ . call ( axis ) ;
765+ }
766+
767+ private getTimelineRange ( ) : { start : number ; end : number } {
768+ switch ( this . state . timelineUnitType ) {
769+ case 'time' :
770+ return {
771+ start : Number ( this . props . viewRange . getStart ( ) ) ,
772+ end : Number ( this . props . viewRange . getEnd ( ) )
773+ } ;
774+ case 'cycles' :
775+ case 'bytes' :
776+ case 'calls' :
777+ case 'bytes/sec' :
778+ case 'iterations/sec' :
779+ // For non-time units, use the data range
780+ const labels = this . state . xyData . labels ;
781+ if ( labels . length === 0 ) return { start : 0 , end : 1 } ;
782+ return { start : 0 , end : labels . length - 1 } ;
783+ default :
784+ return { start : 0 , end : 1 } ;
785+ }
786+ }
787+
788+ private formatTimelineValue ( value : number ) : string {
789+ switch ( this . state . timelineUnitType ) {
790+ case 'time' :
791+ return `${ d3 . format ( '.2f' ) ( value ) } ${ this . state . timelineUnit } ` ;
792+ case 'cycles' :
793+ case 'calls' :
794+ return `${ d3 . format ( '.0f' ) ( value ) } ${ this . state . timelineUnit } ` ;
795+ case 'bytes' :
796+ return this . formatBytes ( value ) ;
797+ case 'bytes/sec' :
798+ return this . formatDataRate ( value ) ;
799+ case 'iterations/sec' :
800+ return `${ d3 . format ( '.1f' ) ( value ) } iter/s` ;
801+ default :
802+ return `${ value } ${ this . state . timelineUnit } ` ;
803+ }
804+ }
805+
806+ private formatDataRate ( rate : number ) : string {
807+ const units = [ 'B/s' , 'KB/s' , 'MB/s' , 'GB/s' ] ;
808+ let value = rate ;
809+ let unitIndex = 0 ;
810+
811+ while ( value >= 1024 && unitIndex < units . length - 1 ) {
812+ value /= 1024 ;
813+ unitIndex ++ ;
814+ }
815+
816+ return `${ d3 . format ( '.1f' ) ( value ) } ${ units [ unitIndex ] } ` ;
817+ }
818+
819+ private formatBytes ( bytes : number ) : string {
820+ const units = [ 'B' , 'KB' , 'MB' , 'GB' ] ;
821+ let value = bytes ;
822+ let unitIndex = 0 ;
823+
824+ while ( value >= 1024 && unitIndex < units . length - 1 ) {
825+ value /= 1024 ;
826+ unitIndex ++ ;
827+ }
828+
829+ return `${ d3 . format ( '.1f' ) ( value ) } ${ units [ unitIndex ] } ` ;
830+ }
831+
713832 renderChart ( ) : React . ReactNode {
714833 const isEmpty =
715834 this . state . outputStatus === ResponseStatus . COMPLETED && ( this . state . xyData ?. datasets ?. length ?? 0 ) === 0 ;
@@ -719,28 +838,31 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
719838 }
720839
721840 return (
722- < div
723- id = { this . getOutputComponentDomId ( ) + 'focusContainer' }
724- className = "xy-main"
725- tabIndex = { 0 }
726- onKeyDown = { e => this . onKeyDown ( e ) }
727- onKeyUp = { e => this . onKeyUp ( e ) }
728- onWheel = { e => this . onWheel ( e ) }
729- onMouseMove = { e => this . onMouseMove ( e ) }
730- onContextMenu = { e => e . preventDefault ( ) }
731- onMouseLeave = { e => this . onMouseLeave ( e ) }
732- onMouseDown = { e => this . onMouseDown ( e ) }
733- style = { { height : this . props . style . height , position : 'relative' , cursor : this . state . cursor } }
734- ref = { this . divRef }
735- >
736- { this . chooseReactChart ( ) }
737- { this . state . outputStatus === ResponseStatus . RUNNING && (
738- < div className = "analysis-running-overflow" style = { { width : this . getChartWidth ( ) } } >
739- < div >
740- < span > Analysis running</ span >
841+ < div >
842+ < div
843+ id = { this . getOutputComponentDomId ( ) + 'focusContainer' }
844+ className = "xy-main"
845+ tabIndex = { 0 }
846+ onKeyDown = { e => this . onKeyDown ( e ) }
847+ onKeyUp = { e => this . onKeyUp ( e ) }
848+ onWheel = { e => this . onWheel ( e ) }
849+ onMouseMove = { e => this . onMouseMove ( e ) }
850+ onContextMenu = { e => e . preventDefault ( ) }
851+ onMouseLeave = { e => this . onMouseLeave ( e ) }
852+ onMouseDown = { e => this . onMouseDown ( e ) }
853+ style = { { height : this . props . style . height , position : 'relative' , cursor : this . state . cursor } }
854+ ref = { this . divRef }
855+ >
856+ { this . chooseReactChart ( ) }
857+ { this . state . outputStatus === ResponseStatus . RUNNING && (
858+ < div className = "analysis-running-overflow" style = { { width : this . getChartWidth ( ) } } >
859+ < div >
860+ < span > Analysis running</ span >
861+ </ div >
741862 </ div >
742- </ div >
743- ) }
863+ ) }
864+ </ div >
865+ { this . renderTimeline ( ) }
744866 </ div >
745867 ) ;
746868 }
0 commit comments