@@ -14,6 +14,7 @@ import {
1414 Button ,
1515 FormControlLabel ,
1616 Switch ,
17+ Stack ,
1718} from "@mui/material" ;
1819import {
1920 fetchUserTrend ,
@@ -92,25 +93,48 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu
9293 const [ clipOffscreen , setClipOffscreen ] = useState ( false ) ;
9394
9495 const chartRef = useRef < ReactECharts > ( null ) ;
95- const [ zoomState , setZoomState ] = useState < Array < { start ?: number ; end ?: number } > > ( [ ] ) ;
96+ const [ zoomState , setZoomState ] = useState < Array < { startValue ?: number ; endValue ?: number } > > ( [ ] ) ;
9697
97- // Capture zoom state when it changes
98+ // Local state for axis input fields (not applied until button clicked)
99+ const [ xStartInput , setXStartInput ] = useState ( "" ) ;
100+ const [ xEndInput , setXEndInput ] = useState ( "" ) ;
101+ const [ yMinInput , setYMinInput ] = useState ( "" ) ;
102+ const [ yMaxInput , setYMaxInput ] = useState ( "" ) ;
103+
104+ // Capture zoom state when it changes (using values, not percentages)
98105 const onDataZoom = useCallback ( ( ) => {
99106 const chartInstance = chartRef . current ?. getEchartsInstance ( ) ;
100107 if ( chartInstance ) {
101- const opt = chartInstance . getOption ( ) as { dataZoom ?: Array < { start ?: number ; end ?: number } > } ;
108+ const opt = chartInstance . getOption ( ) as { dataZoom ?: Array < { startValue ?: number ; endValue ?: number } > } ;
102109 if ( opt . dataZoom ) {
103110 setZoomState ( opt . dataZoom . map ( ( dz ) => ( {
104- start : dz . start ,
105- end : dz . end ,
111+ startValue : dz . startValue ,
112+ endValue : dz . endValue ,
106113 } ) ) ) ;
114+ // Sync input fields with current zoom
115+ if ( opt . dataZoom [ 0 ] ?. startValue ) {
116+ setXStartInput ( new Date ( opt . dataZoom [ 0 ] . startValue ) . toISOString ( ) . split ( "T" ) [ 0 ] ) ;
117+ }
118+ if ( opt . dataZoom [ 0 ] ?. endValue ) {
119+ setXEndInput ( new Date ( opt . dataZoom [ 0 ] . endValue ) . toISOString ( ) . split ( "T" ) [ 0 ] ) ;
120+ }
121+ if ( opt . dataZoom [ 1 ] ?. startValue !== undefined ) {
122+ setYMinInput ( formatMicrosecondsNum ( opt . dataZoom [ 1 ] . startValue ) . toString ( ) ) ;
123+ }
124+ if ( opt . dataZoom [ 1 ] ?. endValue !== undefined ) {
125+ setYMaxInput ( formatMicrosecondsNum ( opt . dataZoom [ 1 ] . endValue ) . toString ( ) ) ;
126+ }
107127 }
108128 }
109129 } , [ ] ) ;
110130
111131 // Clear saved zoom state when restore is triggered
112132 const onRestore = useCallback ( ( ) => {
113133 setZoomState ( [ ] ) ;
134+ setXStartInput ( "" ) ;
135+ setXEndInput ( "" ) ;
136+ setYMinInput ( "" ) ;
137+ setYMaxInput ( "" ) ;
114138 } , [ ] ) ;
115139
116140 const chartEvents = {
@@ -305,6 +329,45 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu
305329 const gpuTypesFromCustom = customData ?. time_series ? Object . keys ( customData . time_series ) : [ ] ;
306330 const gpuTypes = [ ...new Set ( [ ...gpuTypesFromUsers , ...gpuTypesFromCustom ] ) ] ;
307331
332+ // Apply axis range from input fields
333+ const handleApplyAxisRange = ( ) => {
334+ const newZoomState : Array < { startValue ?: number ; endValue ?: number } > = [ { } , { } , { } , { } ] ;
335+
336+ // Apply X-axis values
337+ if ( xStartInput ) {
338+ const date = new Date ( xStartInput ) ;
339+ if ( ! isNaN ( date . getTime ( ) ) ) {
340+ newZoomState [ 0 ] . startValue = date . getTime ( ) ;
341+ newZoomState [ 2 ] . startValue = date . getTime ( ) ;
342+ }
343+ }
344+ if ( xEndInput ) {
345+ const date = new Date ( xEndInput ) ;
346+ if ( ! isNaN ( date . getTime ( ) ) ) {
347+ newZoomState [ 0 ] . endValue = date . getTime ( ) ;
348+ newZoomState [ 2 ] . endValue = date . getTime ( ) ;
349+ }
350+ }
351+
352+ // Apply Y-axis values
353+ if ( yMinInput ) {
354+ const value = parseFloat ( yMinInput ) / 1_000_000 ;
355+ if ( ! isNaN ( value ) ) {
356+ newZoomState [ 1 ] . startValue = value ;
357+ newZoomState [ 3 ] . startValue = value ;
358+ }
359+ }
360+ if ( yMaxInput ) {
361+ const value = parseFloat ( yMaxInput ) / 1_000_000 ;
362+ if ( ! isNaN ( value ) ) {
363+ newZoomState [ 1 ] . endValue = value ;
364+ newZoomState [ 3 ] . endValue = value ;
365+ }
366+ }
367+
368+ setZoomState ( newZoomState ) ;
369+ } ;
370+
308371 const renderSearchInput = ( ) => (
309372 < Box sx = { { mb : 2 , display : "flex" , gap : 2 , alignItems : "flex-start" } } >
310373 < Autocomplete
@@ -591,13 +654,15 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu
591654 type : "inside" as const ,
592655 xAxisIndex : 0 ,
593656 filterMode,
594- ...( zoomState [ 0 ] && { start : zoomState [ 0 ] . start , end : zoomState [ 0 ] . end } ) ,
657+ ...( zoomState [ 0 ] ?. startValue !== undefined && { startValue : zoomState [ 0 ] . startValue } ) ,
658+ ...( zoomState [ 0 ] ?. endValue !== undefined && { endValue : zoomState [ 0 ] . endValue } ) ,
595659 } ,
596660 {
597661 type : "inside" as const ,
598662 yAxisIndex : 0 ,
599663 filterMode,
600- ...( zoomState [ 1 ] && { start : zoomState [ 1 ] . start , end : zoomState [ 1 ] . end } ) ,
664+ ...( zoomState [ 1 ] ?. startValue !== undefined && { startValue : zoomState [ 1 ] . startValue } ) ,
665+ ...( zoomState [ 1 ] ?. endValue !== undefined && { endValue : zoomState [ 1 ] . endValue } ) ,
601666 } ,
602667 {
603668 type : "slider" as const ,
@@ -614,7 +679,8 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu
614679 textStyle : {
615680 color : textColor ,
616681 } ,
617- ...( zoomState [ 2 ] && { start : zoomState [ 2 ] . start , end : zoomState [ 2 ] . end } ) ,
682+ ...( zoomState [ 2 ] ?. startValue !== undefined && { startValue : zoomState [ 2 ] . startValue } ) ,
683+ ...( zoomState [ 2 ] ?. endValue !== undefined && { endValue : zoomState [ 2 ] . endValue } ) ,
618684 } ,
619685 {
620686 type : "slider" as const ,
@@ -631,7 +697,8 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu
631697 textStyle : {
632698 color : textColor ,
633699 } ,
634- ...( zoomState [ 3 ] && { start : zoomState [ 3 ] . start , end : zoomState [ 3 ] . end } ) ,
700+ ...( zoomState [ 3 ] ?. startValue !== undefined && { startValue : zoomState [ 3 ] . startValue } ) ,
701+ ...( zoomState [ 3 ] ?. endValue !== undefined && { endValue : zoomState [ 3 ] . endValue } ) ,
635702 } ,
636703 ] ;
637704
@@ -752,6 +819,56 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu
752819 notMerge = { true }
753820 onEvents = { chartEvents }
754821 />
822+ < Stack direction = "row" spacing = { 2 } sx = { { mt : 2 , alignItems : "center" , flexWrap : "wrap" } } >
823+ < Typography variant = "body2" color = "text.secondary" >
824+ X-Axis (Date):
825+ </ Typography >
826+ < TextField
827+ label = "Start"
828+ type = "date"
829+ size = "small"
830+ value = { xStartInput }
831+ onChange = { ( e ) => setXStartInput ( e . target . value ) }
832+ slotProps = { { inputLabel : { shrink : true } } }
833+ sx = { { width : 150 } }
834+ />
835+ < TextField
836+ label = "End"
837+ type = "date"
838+ size = "small"
839+ value = { xEndInput }
840+ onChange = { ( e ) => setXEndInput ( e . target . value ) }
841+ slotProps = { { inputLabel : { shrink : true } } }
842+ sx = { { width : 150 } }
843+ />
844+ < Typography variant = "body2" color = "text.secondary" sx = { { ml : 2 } } >
845+ Y-Axis (μs):
846+ </ Typography >
847+ < TextField
848+ label = "Min"
849+ type = "number"
850+ size = "small"
851+ value = { yMinInput }
852+ onChange = { ( e ) => setYMinInput ( e . target . value ) }
853+ sx = { { width : 120 } }
854+ />
855+ < TextField
856+ label = "Max"
857+ type = "number"
858+ size = "small"
859+ value = { yMaxInput }
860+ onChange = { ( e ) => setYMaxInput ( e . target . value ) }
861+ sx = { { width : 120 } }
862+ />
863+ < Button
864+ variant = "contained"
865+ size = "small"
866+ onClick = { handleApplyAxisRange }
867+ sx = { { height : 40 } }
868+ >
869+ Apply
870+ </ Button >
871+ </ Stack >
755872 </ Box >
756873 ) ;
757874}
0 commit comments