@@ -10,6 +10,7 @@ import {
1010 useCallback ,
1111 useEffect ,
1212 useMemo ,
13+ useRef ,
1314 useState ,
1415} from "react" ;
1516import { PiSplitHorizontalBold } from "react-icons/pi" ;
@@ -21,6 +22,7 @@ import {
2122 LabelList ,
2223 Tooltip as RechartsTooltip ,
2324 ReferenceArea ,
25+ ReferenceLine ,
2426 ResponsiveContainer ,
2527 XAxis ,
2628 YAxis ,
@@ -73,6 +75,12 @@ export const TraceSpan = ({
7375 const [ dragStartX , setDragStartX ] = useState < number | null > ( null ) ;
7476 const [ initialDragMovement , setInitialDragMovement ] = useState ( false ) ;
7577
78+ // Crosshair state for showing precise timestamp on hover
79+ const [ crosshairX , setCrosshairX ] = useState < number | null > ( null ) ;
80+ const [ crosshairPixelX , setCrosshairPixelX ] = useState < number | null > ( null ) ;
81+ const [ isHoveringChart , setIsHoveringChart ] = useState ( false ) ;
82+ const chartContainerRef = useRef < HTMLDivElement > ( null ) ;
83+
7684 const spanData : BarChartTrace [ ] = useMemo ( ( ) => {
7785 if ( ! session || ! session . traces ) return [ ] ;
7886 const startTimeMs = session . start_time_unix_timestamp_ms ;
@@ -237,9 +245,33 @@ export const TraceSpan = ({
237245 [ lastChartDimensions ] ,
238246 ) ;
239247
240- // Handle mouse move for dragging the highlighter
248+ // Handle mouse move for dragging the highlighter and updating crosshair
241249 const handleMouseMove = useCallback (
242250 ( e : any ) => {
251+ // Update crosshair using native mouse position for accurate pixel placement
252+ const container = chartContainerRef . current ;
253+ if ( container && e . chartX !== undefined ) {
254+ const chartWidth = e . width || lastChartDimensions ?. width || 1000 ;
255+ const leftMargin = 20 ;
256+ const rightMargin = 30 ;
257+ const plotAreaWidth = chartWidth - leftMargin - rightMargin ;
258+
259+ // Store pixel position for CSS positioning
260+ setCrosshairPixelX ( e . chartX ) ;
261+
262+ // Calculate domain value for the label
263+ const adjustedX = e . chartX - leftMargin ;
264+ if ( adjustedX >= 0 && adjustedX <= plotAreaWidth ) {
265+ const ratio = adjustedX / plotAreaWidth ;
266+ const xDomain = domain [ 0 ] + ratio * ( domain [ 1 ] - domain [ 0 ] ) ;
267+ setCrosshairX ( xDomain ) ;
268+ // Set hovering true when we have valid coordinates (more reliable than onMouseEnter)
269+ setIsHoveringChart ( true ) ;
270+ } else {
271+ setCrosshairX ( null ) ;
272+ }
273+ }
274+
243275 if (
244276 ! isDragging ||
245277 ! highlighterActive ||
@@ -250,15 +282,15 @@ export const TraceSpan = ({
250282 )
251283 return ;
252284
253- // Ensure we have a valid chartX value
254- const chartX = e . chartX ;
255- if ( typeof chartX !== "number" ) return ;
285+ // Get chartX for drag handling
286+ const dragChartX = e . chartX ;
287+ if ( typeof dragChartX !== "number" ) return ;
256288
257289 // Consistently track chart dimensions
258- const chartWidth = e . width || lastChartDimensions ?. width || 1000 ;
259290 updateChartDimensions ( e ) ;
291+ const dragChartWidth = e . width || lastChartDimensions ?. width || 1000 ;
260292
261- const deltaX = chartX - dragStartX ;
293+ const deltaX = dragChartX - dragStartX ;
262294
263295 // Skip tiny movements to reduce jitter
264296 if ( Math . abs ( deltaX ) < 2 ) return ;
@@ -270,11 +302,11 @@ export const TraceSpan = ({
270302
271303 // Direct ratio calculation for movement - simpler and more accurate
272304 const domainWidth = domain [ 1 ] - domain [ 0 ] ;
273- const domainDeltaX = ( deltaX / chartWidth ) * domainWidth ;
305+ const domainDeltaX = ( deltaX / dragChartWidth ) * domainWidth ;
274306
275307 // Don't update highlighter during initial movement
276308 if ( ! initialDragMovement ) {
277- setDragStartX ( chartX ) ;
309+ setDragStartX ( dragChartX ) ;
278310 return ;
279311 }
280312
@@ -319,7 +351,7 @@ export const TraceSpan = ({
319351 setHighlighterEnd ( newEnd ) ;
320352
321353 // Always update dragStartX to prevent accumulation of small movements
322- setDragStartX ( chartX ) ;
354+ setDragStartX ( dragChartX ) ;
323355
324356 // Prevent default behavior and stop propagation
325357 e . preventDefault ?.( ) ;
@@ -336,9 +368,21 @@ export const TraceSpan = ({
336368 lastChartDimensions ,
337369 initialDragMovement ,
338370 updateChartDimensions ,
371+ pixelToDomain ,
339372 ] ,
340373 ) ;
341374
375+ // Handle mouse enter/leave for crosshair visibility
376+ const handleMouseEnter = useCallback ( ( ) => {
377+ setIsHoveringChart ( true ) ;
378+ } , [ ] ) ;
379+
380+ const handleMouseLeave = useCallback ( ( ) => {
381+ setIsHoveringChart ( false ) ;
382+ setCrosshairX ( null ) ;
383+ setCrosshairPixelX ( null ) ;
384+ } , [ ] ) ;
385+
342386 // Handle mouse down on the chart for dragging the highlighter
343387 const handleMouseDown = useCallback (
344388 ( e : any ) => {
@@ -500,11 +544,12 @@ export const TraceSpan = ({
500544 className = "relative flex h-full select-none flex-col"
501545 id = "sessions-trace-span"
502546 >
503- < ScrollArea >
504- < ResponsiveContainer
505- width = "100%"
506- height = { Math . max ( 300 , spanData . length * BAR_SIZE ) }
507- >
547+ < div ref = { chartContainerRef } className = "relative" >
548+ < ScrollArea >
549+ < ResponsiveContainer
550+ width = "100%"
551+ height = { Math . max ( 300 , spanData . length * BAR_SIZE ) }
552+ >
508553 < BarChart
509554 data = { spanData }
510555 layout = "vertical"
@@ -514,7 +559,11 @@ export const TraceSpan = ({
514559 onMouseDown = { handleMouseDown }
515560 onMouseMove = { handleMouseMove }
516561 onMouseUp = { handleMouseUp }
517- onMouseLeave = { handleMouseUp }
562+ onMouseEnter = { handleMouseEnter }
563+ onMouseLeave = { ( e ) => {
564+ handleMouseUp ( ) ;
565+ handleMouseLeave ( ) ;
566+ } }
518567 >
519568 < CartesianGrid
520569 strokeDasharray = "3 3"
@@ -784,9 +833,38 @@ export const TraceSpan = ({
784833 />
785834 </ >
786835 ) }
836+
787837 </ BarChart >
788- </ ResponsiveContainer >
789- </ ScrollArea >
838+ </ ResponsiveContainer >
839+ </ ScrollArea >
840+
841+ { /* CSS-positioned crosshair overlay */ }
842+ { isHoveringChart && crosshairPixelX !== null && crosshairX !== null && ! isDragging && (
843+ < div
844+ className = "pointer-events-none absolute top-0 z-10"
845+ style = { {
846+ left : crosshairPixelX ,
847+ height : "100%" ,
848+ } }
849+ >
850+ < div
851+ className = "h-full border-l border-dashed"
852+ style = { {
853+ borderColor : theme === "dark" ? "#94a3b8" : "#64748b" ,
854+ } }
855+ />
856+ < div
857+ className = "absolute -top-1 left-1/2 -translate-x-1/2 whitespace-nowrap rounded px-1.5 py-0.5 text-xs font-medium"
858+ style = { {
859+ backgroundColor : theme === "dark" ? "#334155" : "#f1f5f9" ,
860+ color : theme === "dark" ? "#e2e8f0" : "#334155" ,
861+ } }
862+ >
863+ { crosshairX . toFixed ( 3 ) } s
864+ </ div >
865+ </ div >
866+ ) }
867+ </ div >
790868 < ResponsiveContainer width = "100%" height = { 52 } >
791869 < BarChart
792870 data = { spanData }
0 commit comments