@@ -71,9 +71,11 @@ interface GenericXYState extends AbstractTreeOutputState {
7171 cursor : string ;
7272 timelineUnit : string ;
7373 timelineUnitType : TimelineUnitType ;
74+ xAxisTitle ?: string ;
75+ range : TimeRange ;
7476}
7577
76- type TimelineUnitType = 'time' | 'cycles' | 'bytes' | 'calls' | 'bytes/sec' | 'iterations/sec' ;
78+ type TimelineUnitType = 'time' | 'DURATION' | ' cycles' | 'bytes' | 'calls' | 'bytes/sec' | 'iterations/sec' ;
7779
7880interface GenericXYProps extends AbstractOutputProps {
7981 formatX ?: ( x : number | bigint | string ) => string ;
@@ -143,9 +145,10 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
143145 allMax : 0 ,
144146 allMin : 0 ,
145147 cursor : 'default' ,
146- showTree : true ,
147148 timelineUnit : this . props . timelineUnit || 'ms' ,
148- timelineUnitType : this . props . timelineUnitType || 'time'
149+ timelineUnitType : this . props . timelineUnitType || 'DURATION' ,
150+ showTree : true ,
151+ range : new TimeRange ( )
149152 } ;
150153
151154 this . addPinViewOptions ( ( ) => ( {
@@ -202,7 +205,7 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
202205 }
203206
204207 renderYAxis ( ) : React . ReactNode {
205- const chartHeight = parseInt ( String ( this . props . style . height ) , 10 ) ;
208+ const chartHeight = parseInt ( String ( this . props . style . height ) , 10 ) - this . timelineHeight ;
206209 let yMin = this . state . allMin ;
207210 let yMax = this . state . allMax ;
208211 if ( ! Number . isFinite ( yMin ) || ! Number . isFinite ( yMax ) ) {
@@ -326,6 +329,7 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
326329 } )
327330 ) ;
328331 this . calculateYRange ( ) ;
332+
329333 this . updateTimeline ( ) ;
330334 return ;
331335 }
@@ -338,6 +342,61 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
338342 const xy = this . buildXYData ( series , this . mode ) ;
339343 flushSync ( ( ) => this . setState ( { xyData : xy , outputStatus : model . status ?? ResponseStatus . COMPLETED } ) ) ;
340344 this . calculateYRange ( ) ;
345+
346+ // Extract model types from all series
347+ let minStart = Number . MAX_SAFE_INTEGER ;
348+ let maxEnd = Number . MIN_SAFE_INTEGER ;
349+ let commonLabel : string | undefined ;
350+ let commonUnit : string | undefined ;
351+ let commonDataType : string | undefined ;
352+
353+ series . forEach ( ( s , index ) => {
354+ if ( s . xValuesDescription ) {
355+ const { axisDomain, label, unit, dataType } = s . xValuesDescription ;
356+
357+ // Track min start and max end from axisDomain
358+ if ( axisDomain && axisDomain . type === 'range' ) {
359+ const start = Number ( axisDomain . start ) ;
360+ const end = Number ( axisDomain . end ) ;
361+ if ( start < minStart ) minStart = start ;
362+ if ( end > maxEnd ) maxEnd = end ;
363+ }
364+
365+ // Check if all series have same label/unit/dataType
366+ if ( index === 0 ) {
367+ commonLabel = label ;
368+ commonUnit = unit ;
369+ commonDataType = dataType ;
370+ } else {
371+ if ( commonLabel !== label ) commonLabel = undefined ;
372+ if ( commonUnit !== unit ) commonUnit = undefined ;
373+ if ( commonDataType !== dataType ) commonDataType = undefined ;
374+ }
375+ }
376+ } ) ;
377+
378+ // Calculate union range
379+ const unionRange = maxEnd - minStart ;
380+
381+ // Default to 'time' if datatype differs across series
382+ const finalDataType = commonDataType || 'time' ;
383+
384+ // Update state with extracted info
385+ const updates : Partial < GenericXYState > = { } ;
386+ if ( commonUnit && commonUnit !== this . state . timelineUnit ) {
387+ updates . timelineUnit = commonUnit ;
388+ }
389+ if ( finalDataType !== this . state . timelineUnitType ) {
390+ updates . timelineUnitType = finalDataType as TimelineUnitType ;
391+ }
392+ if ( commonLabel !== this . state . xAxisTitle ) {
393+ updates . xAxisTitle = commonLabel ;
394+ }
395+ updates . range = new TimeRange ( BigInt ( minStart - 1 ) , BigInt ( maxEnd - 1 ) , BigInt ( 1 ) ) ;
396+ if ( Object . keys ( updates ) . length > 0 ) {
397+ this . setState ( updates as any ) ;
398+ }
399+
341400 this . updateTimeline ( ) ;
342401 }
343402
@@ -456,6 +515,14 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
456515 this . _debouncedUpdateXY ( ) ;
457516 }
458517 }
518+
519+ // Update timeline after render if data changed
520+ if ( prevState . xyData !== this . state . xyData ) {
521+ setTimeout ( ( ) => {
522+ this . updateTimeline ( ) ;
523+ } , 0 ) ;
524+ }
525+
459526 }
460527
461528 componentWillUnmount ( ) : void {
@@ -529,7 +596,7 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
529596
530597 const chartProps = {
531598 data : data ,
532- height : parseInt ( String ( this . props . style . height ) ) ,
599+ height : parseInt ( String ( this . props . style . height ) ) - this . timelineHeight ,
533600 options : options ,
534601 ref : this . chartRef
535602 } ;
@@ -726,57 +793,134 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
726793 }
727794
728795 private renderTimeline ( ) : React . ReactNode {
729- if ( this . state . timelineUnitType === 'time' && ! this . isTimeAxis ) return < svg /> ;
730-
731796 const chartWidth = this . getChartWidth ( ) ;
732- if ( chartWidth <= 0 ) return < svg /> ;
733797
734- return (
735- < svg
798+ const svgElement = (
799+ < svg
736800 ref = { this . timelineRef }
737- width = { chartWidth }
801+ width = { Math . max ( chartWidth , 1 ) }
738802 height = { this . timelineHeight }
739- style = { { marginLeft : this . margin . left } }
803+ style = { {
804+ position : 'relative'
805+ } }
740806 />
741807 ) ;
808+
809+ return svgElement ;
742810 }
743811
744812 private updateTimeline ( ) : void {
745813 if ( ! this . timelineRef . current ) return ;
746- if ( this . state . timelineUnitType === 'time' && ! this . isTimeAxis ) return ;
747814
748815 const svg = d3 . select ( this . timelineRef . current ) ;
749816 svg . selectAll ( '*' ) . remove ( ) ;
750817
751818 const chartWidth = this . getChartWidth ( ) ;
819+ if ( chartWidth <= 0 ) return ;
820+
752821 const { start, end } = this . getTimelineRange ( ) ;
753-
822+
754823 const scale = d3 . scaleLinear ( )
755824 . domain ( [ start , end ] )
756825 . range ( [ 0 , chartWidth ] ) ;
757826
758827 const axis = d3 . axisBottom ( scale )
759828 . tickFormat ( d => this . formatTimelineValue ( Number ( d ) ) ) ;
760829
761- svg . append ( 'g' )
830+ const axisGroup = svg . append ( 'g' )
762831 . attr ( 'transform' , `translate(0, 0)` )
763832 . call ( axis ) ;
833+
834+ // Add minor ticks for range items
835+ this . addMinorTicks ( svg , scale , chartWidth ) ;
836+ }
837+
838+ private addMinorTicks ( svg : d3 . Selection < SVGSVGElement , unknown , null , undefined > , scale : d3 . ScaleLinear < number , number > , chartWidth : number ) : void {
839+ const labels = this . state . xyData . labels ;
840+ if ( ! labels . length ) return ;
841+
842+ // For each range item, add minor ticks at start and end
843+ labels . forEach ( ( label ) => {
844+ if ( label . includes ( '[' ) && label . includes ( ',' ) ) {
845+ // Parse range format: "[start unit, end unit]"
846+ const match = label . match ( / \[ ( .* ?) , \s * ( .* ?) \] / ) ;
847+ if ( match ) {
848+ const startVal = parseFloat ( match [ 1 ] ) ;
849+ const endVal = parseFloat ( match [ 2 ] ) ;
850+
851+ if ( ! isNaN ( startVal ) && ! isNaN ( endVal ) ) {
852+ const startX = scale ( startVal ) ;
853+ const endX = scale ( endVal ) ;
854+
855+ // Add minor ticks
856+ if ( startX >= 0 && startX <= chartWidth ) {
857+ svg . append ( 'line' )
858+ . attr ( 'x1' , startX )
859+ . attr ( 'x2' , startX )
860+ . attr ( 'y1' , 0 )
861+ . attr ( 'y2' , 3 )
862+ . attr ( 'stroke' , '#666' )
863+ . attr ( 'stroke-width' , 0.5 ) ;
864+ }
865+
866+ if ( endX >= 0 && endX <= chartWidth ) {
867+ svg . append ( 'line' )
868+ . attr ( 'x1' , endX )
869+ . attr ( 'x2' , endX )
870+ . attr ( 'y1' , 0 )
871+ . attr ( 'y2' , 3 )
872+ . attr ( 'stroke' , '#666' )
873+ . attr ( 'stroke-width' , 0.5 ) ;
874+ }
875+ }
876+ }
877+ }
878+ } ) ;
764879 }
765880
766881 private getTimelineRange ( ) : { start : number ; end : number } {
882+ const labels = this . state . xyData . labels ;
883+
884+ // If we have range labels, extract the actual range from the data
885+ if ( labels . length > 0 && labels [ 0 ] . includes ( '[' ) && labels [ 0 ] . includes ( ',' ) ) {
886+ let minStart = Number . MAX_SAFE_INTEGER ;
887+ let maxEnd = Number . MIN_SAFE_INTEGER ;
888+
889+ labels . forEach ( label => {
890+ const match = label . match ( / \[ ( .* ?) , \s * ( .* ?) \] / ) ;
891+ if ( match ) {
892+ const start = parseFloat ( match [ 1 ] ) ;
893+ const end = parseFloat ( match [ 2 ] ) ;
894+ if ( ! isNaN ( start ) && ! isNaN ( end ) ) {
895+ minStart = Math . min ( minStart , start ) ;
896+ maxEnd = Math . max ( maxEnd , end ) ;
897+ }
898+ }
899+ } ) ;
900+
901+ if ( minStart !== Number . MAX_SAFE_INTEGER && maxEnd !== Number . MIN_SAFE_INTEGER ) {
902+ return { start : minStart , end : maxEnd } ;
903+ }
904+ }
905+
906+ // Fallback to view/range based on timeline unit type
767907 switch ( this . state . timelineUnitType ) {
768908 case 'time' :
769909 return {
770910 start : Number ( this . props . viewRange . getStart ( ) ) ,
771911 end : Number ( this . props . viewRange . getEnd ( ) )
772912 } ;
913+ case 'DURATION' :
914+ return {
915+ start : Number ( this . state . range . getStart ( ) ) ,
916+ end : Number ( this . state . range . getEnd ( ) )
917+ } ;
773918 case 'cycles' :
774919 case 'bytes' :
775920 case 'calls' :
776921 case 'bytes/sec' :
777922 case 'iterations/sec' :
778923 // For non-time units, use the data range
779- const labels = this . state . xyData . labels ;
780924 if ( labels . length === 0 ) return { start : 0 , end : 1 } ;
781925 return { start : 0 , end : labels . length - 1 } ;
782926 default :
@@ -787,7 +931,8 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
787931 private formatTimelineValue ( value : number ) : string {
788932 switch ( this . state . timelineUnitType ) {
789933 case 'time' :
790- return `${ d3 . format ( '.2f' ) ( value ) } ${ this . state . timelineUnit } ` ;
934+ case 'DURATION' :
935+ return this . formatTime ( value ) ;
791936 case 'cycles' :
792937 case 'calls' :
793938 return `${ d3 . format ( '.0f' ) ( value ) } ${ this . state . timelineUnit } ` ;
@@ -802,29 +947,51 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
802947 }
803948 }
804949
950+ private formatTime ( value : number ) : string {
951+ const units = [
952+ { name : 'ns' , factor : 1 } ,
953+ { name : 'μs' , factor : 1000 } ,
954+ { name : 'ms' , factor : 1000000 } ,
955+ { name : 's' , factor : 1000000000 }
956+ ] ;
957+
958+ let unitIndex = 0 ;
959+ let scaledValue = value ;
960+
961+ for ( let i = units . length - 1 ; i >= 0 ; i -- ) {
962+ if ( Math . abs ( value ) >= units [ i ] . factor ) {
963+ scaledValue = value / units [ i ] . factor ;
964+ unitIndex = i ;
965+ break ;
966+ }
967+ }
968+
969+ return `${ d3 . format ( '.1f' ) ( scaledValue ) } ${ units [ unitIndex ] . name } ` ;
970+ }
971+
805972 private formatDataRate ( rate : number ) : string {
806973 const units = [ 'B/s' , 'KB/s' , 'MB/s' , 'GB/s' ] ;
807974 let value = rate ;
808975 let unitIndex = 0 ;
809-
976+
810977 while ( value >= 1024 && unitIndex < units . length - 1 ) {
811978 value /= 1024 ;
812979 unitIndex ++ ;
813980 }
814-
981+
815982 return `${ d3 . format ( '.1f' ) ( value ) } ${ units [ unitIndex ] } ` ;
816983 }
817984
818985 private formatBytes ( bytes : number ) : string {
819986 const units = [ 'B' , 'KB' , 'MB' , 'GB' ] ;
820987 let value = bytes ;
821988 let unitIndex = 0 ;
822-
989+
823990 while ( value >= 1024 && unitIndex < units . length - 1 ) {
824991 value /= 1024 ;
825992 unitIndex ++ ;
826993 }
827-
994+
828995 return `${ d3 . format ( '.1f' ) ( value ) } ${ units [ unitIndex ] } ` ;
829996 }
830997
@@ -849,7 +1016,7 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
8491016 onContextMenu = { e => e . preventDefault ( ) }
8501017 onMouseLeave = { e => this . onMouseLeave ( e ) }
8511018 onMouseDown = { e => this . onMouseDown ( e ) }
852- style = { { height : this . props . style . height , position : 'relative' , cursor : this . state . cursor } }
1019+ style = { { height : parseInt ( String ( this . props . style . height ) ) - this . timelineHeight , position : 'relative' , cursor : this . state . cursor } }
8531020 ref = { this . divRef }
8541021 >
8551022 { this . chooseReactChart ( ) }
@@ -861,7 +1028,24 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
8611028 </ div >
8621029 ) }
8631030 </ div >
864- { this . renderTimeline ( ) }
1031+ < div style = { { position : 'relative' } } >
1032+ { this . renderTimeline ( ) }
1033+ { this . state . xAxisTitle && (
1034+ < div style = { {
1035+ position : 'absolute' ,
1036+ top : '50%' ,
1037+ left : '50%' ,
1038+ transform : 'translate(-50%, -50%)' ,
1039+ textAlign : 'center' ,
1040+ fontSize : '12px' ,
1041+ color : 'rgba(102, 102, 102, 0.5)' ,
1042+ pointerEvents : 'none' ,
1043+ zIndex : 10
1044+ } } >
1045+ { this . state . xAxisTitle }
1046+ </ div >
1047+ ) }
1048+ </ div >
8651049 </ div >
8661050 ) ;
8671051 }
0 commit comments