@@ -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,6 +69,7 @@ interface GenericXYState extends AbstractTreeOutputState {
6869 allMax : number ;
6970 allMin : number ;
7071 cursor : string ;
72+ currentSampling ?: Sampling ;
7173}
7274
7375interface GenericXYProps extends AbstractOutputProps {
@@ -91,8 +93,9 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
9193 private readonly chartRef = React . createRef < any > ( ) ;
9294 private readonly yAxisRef : any ;
9395 private readonly divRef = React . createRef < HTMLDivElement > ( ) ;
96+ private readonly xAxisRef = React . createRef < SVGGElement > ( ) ;
9497
95- private readonly margin = { top : 15 , right : 0 , bottom : 6 , left : this . getYAxisWidth ( ) } ;
98+ private readonly margin = { top : 15 , right : 0 , bottom : 30 , left : this . getYAxisWidth ( ) } ;
9699
97100 private mouseIsDown = false ;
98101 private isPanning = false ;
@@ -134,7 +137,8 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
134137 allMax : 0 ,
135138 allMin : 0 ,
136139 cursor : 'default' ,
137- showTree : true
140+ showTree : true ,
141+ currentSampling : undefined
138142 } ;
139143
140144 this . addPinViewOptions ( ( ) => ( {
@@ -311,7 +315,8 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
311315 flushSync ( ( ) =>
312316 this . setState ( {
313317 xyData : { labels : [ ] , datasets : [ ] , labelIndices : [ ] } ,
314- outputStatus : model . status ?? ResponseStatus . COMPLETED
318+ outputStatus : model . status ?? ResponseStatus . COMPLETED ,
319+ currentSampling : undefined
315320 } )
316321 ) ;
317322 this . calculateYRange ( ) ;
@@ -323,9 +328,15 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
323328 this . isTimeAxis = ! ! series [ 0 ] . xValues ;
324329 this . mode = st === 'scatter' ? ChartMode . SCATTER : st === 'line' ? ChartMode . LINE : ChartMode . BAR ;
325330
331+ const xValues = series [ 0 ] . xValues ?? series [ 0 ] . xRanges ?? series [ 0 ] . xCategories ;
326332 const xy = this . buildXYData ( series , this . mode ) ;
327- flushSync ( ( ) => this . setState ( { xyData : xy , outputStatus : model . status ?? ResponseStatus . COMPLETED } ) ) ;
333+ flushSync ( ( ) => this . setState ( {
334+ xyData : xy ,
335+ outputStatus : model . status ?? ResponseStatus . COMPLETED ,
336+ currentSampling : xValues as Sampling
337+ } ) ) ;
328338 this . calculateYRange ( ) ;
339+ this . updateXAxis ( ) ;
329340 }
330341
331342 private buildXYData ( seriesObj : XYSeries [ ] , mode : ChartMode ) : GenericXYData {
@@ -408,6 +419,68 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
408419 signalManager ( ) . emit ( 'SAVE_AS_CSV' , this . props . traceId , csv ) ;
409420 }
410421
422+ private updateXAxis ( ) : void {
423+ if ( ! this . xAxisRef . current || ! this . state . currentSampling ) {
424+ return ;
425+ }
426+
427+ const chartWidth = this . getChartWidth ( ) ;
428+ if ( chartWidth <= 0 ) return ;
429+
430+ const sampling = this . state . currentSampling ;
431+ let scale : d3 . ScaleTime < number , number > | d3 . ScaleLinear < number , number > | d3 . ScaleBand < string > ;
432+ let axis : d3 . Axis < any > ;
433+
434+ if ( isTimestampSampling ( sampling ) ) {
435+ const domain = d3 . extent ( sampling . map ( ts => new Date ( Number ( ts ) / 1000000 ) ) ) as [ Date , Date ] ;
436+ scale = d3 . scaleTime ( ) . domain ( domain ) . range ( [ 0 , chartWidth ] ) ;
437+ axis = d3 . axisBottom ( scale ) . ticks ( 5 ) . tickSizeInner ( - parseInt ( String ( this . props . style . height ) ) + this . margin . top + this . margin . bottom ) ;
438+ } else if ( isRangeSampling ( sampling ) ) {
439+ const timestamps = sampling . map ( range => Number ( range . start ) / 1000000 ) ;
440+ console . log ( 'Raw timestamps (ms):' , timestamps . slice ( 0 , 3 ) ) ;
441+ const dates = timestamps . map ( ts => new Date ( ts ) ) ;
442+ console . log ( 'Converted dates:' , dates . slice ( 0 , 3 ) ) ;
443+ const domain = d3 . extent ( dates ) as [ Date , Date ] ;
444+ console . log ( 'Domain:' , domain ) ;
445+ scale = d3 . scaleTime ( ) . domain ( domain ) . range ( [ 0 , chartWidth ] ) ;
446+ axis = d3 . axisBottom ( scale ) . ticks ( 5 ) . tickSizeInner ( - parseInt ( String ( this . props . style . height ) ) + this . margin . top + this . margin . bottom )
447+ . tickFormat ( ( d ) => {
448+ const date = d instanceof Date ? d : new Date ( d as number ) ;
449+ const rangeIndex = Math . floor ( ( date . getTime ( ) - domain [ 0 ] . getTime ( ) ) / ( domain [ 1 ] . getTime ( ) - domain [ 0 ] . getTime ( ) ) * ( sampling . length - 1 ) ) ;
450+ const range = sampling [ Math . min ( rangeIndex , sampling . length - 1 ) ] ;
451+ const endNs = Number ( range . end ) ;
452+
453+ const formatDuration = ( ns : number ) : string => {
454+ if ( ns >= 1e9 ) return `${ ( ns / 1e9 ) . toFixed ( 2 ) } s` ;
455+ if ( ns >= 1e6 ) return `${ ( ns / 1e6 ) . toFixed ( 2 ) } ms` ;
456+ if ( ns >= 1e3 ) return `${ ( ns / 1e3 ) . toFixed ( 2 ) } μs` ;
457+ return `${ ns } ns` ;
458+ } ;
459+
460+ return formatDuration ( endNs ) ;
461+ } ) ;
462+ } else if ( isCategorySampling ( sampling ) ) {
463+ scale = d3 . scaleBand ( ) . domain ( sampling ) . range ( [ 0 , chartWidth ] ) . padding ( 0.1 ) ;
464+ axis = d3 . axisBottom ( scale ) . tickSizeInner ( - parseInt ( String ( this . props . style . height ) ) + this . margin . top + this . margin . bottom ) ;
465+ } else {
466+ return ;
467+ }
468+
469+ d3 . select ( this . xAxisRef . current ) . call ( axis ) ;
470+ // Ensure ticks and text are visible
471+ d3 . select ( this . xAxisRef . current ) . selectAll ( 'text' )
472+ . style ( 'fill' , 'var(--vscode-foreground)' )
473+ . style ( 'font-size' , '12px' )
474+ . attr ( 'y' , 9 )
475+ . attr ( 'dy' , '0.71em' ) ;
476+ d3 . select ( this . xAxisRef . current ) . selectAll ( '.tick line' )
477+ . style ( 'stroke' , 'var(--vscode-foreground)' )
478+ . attr ( 'y2' , 6 ) ;
479+ d3 . select ( this . xAxisRef . current ) . select ( '.domain' )
480+ . style ( 'stroke' , 'var(--vscode-foreground)' )
481+ . style ( 'stroke-width' , '1px' ) ;
482+ }
483+
411484 private calculateYRange ( ) {
412485 const ds = this . state . xyData ?. datasets ?? [ ] ;
413486 if ( ! ds . length ) {
@@ -420,6 +493,30 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
420493 this . setState ( { allMax : max , allMin : min } ) ;
421494 }
422495
496+ renderXAxis ( ) : React . ReactNode {
497+ const chartHeight = parseInt ( String ( this . props . style . height ) , 10 ) ;
498+ const xTransform = `translate(${ this . margin . left } , 5)` ;
499+ const chartWidth = this . getChartWidth ( ) ;
500+ return (
501+ < svg
502+ height = { this . margin . bottom }
503+ width = { chartWidth + this . margin . left }
504+ style = { { position : 'absolute' , bottom : 0 } }
505+ >
506+ < line
507+ x1 = { this . margin . left }
508+ y1 = { 0 }
509+ x2 = { chartWidth + this . margin . left }
510+ y2 = { 0 }
511+ stroke = "var(--vscode-foreground)"
512+ strokeWidth = { 1 }
513+ opacity = { 0.3 }
514+ />
515+ < g className = "x-axis" ref = { this . xAxisRef } transform = { xTransform } />
516+ </ svg >
517+ ) ;
518+ }
519+
423520 componentDidMount ( ) : void {
424521 super . componentDidMount ?.( ) ;
425522 this . waitAnalysisCompletion ( ) ;
@@ -437,10 +534,15 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
437534 prevProps . viewRange . getEnd ( ) !== this . props . viewRange . getEnd ( ) ;
438535
439536 const checksChanged = prevState . checkedSeries !== this . state . checkedSeries ;
537+ const samplingChanged = prevState . currentSampling !== this . state . currentSampling ;
440538
441539 if ( sizeChanged || viewChanged || checksChanged ) {
442540 if ( this . getChartWidth ( ) > 0 ) this . _debouncedUpdateXY ( ) ;
443541 }
542+
543+ if ( sizeChanged || samplingChanged ) {
544+ this . updateXAxis ( ) ;
545+ }
444546 }
445547
446548 componentWillUnmount ( ) : void {
@@ -734,6 +836,7 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
734836 ref = { this . divRef }
735837 >
736838 { this . chooseReactChart ( ) }
839+ { this . renderXAxis ( ) }
737840 { this . state . outputStatus === ResponseStatus . RUNNING && (
738841 < div className = "analysis-running-overflow" style = { { width : this . getChartWidth ( ) } } >
739842 < div >
0 commit comments