diff --git a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx index 2a99721..a8d8962 100644 --- a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx +++ b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx @@ -14,6 +14,7 @@ import { Button, FormControlLabel, Switch, + Stack, } from "@mui/material"; import { fetchUserTrend, @@ -92,18 +93,37 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu const [clipOffscreen, setClipOffscreen] = useState(false); const chartRef = useRef(null); - const [zoomState, setZoomState] = useState>([]); + const [zoomState, setZoomState] = useState>([]); - // Capture zoom state when it changes + // Local state for axis input fields (not applied until button clicked) + const [xStartInput, setXStartInput] = useState(""); + const [xEndInput, setXEndInput] = useState(""); + const [yMinInput, setYMinInput] = useState(""); + const [yMaxInput, setYMaxInput] = useState(""); + + // Capture zoom state when it changes (using values, not percentages) const onDataZoom = useCallback(() => { const chartInstance = chartRef.current?.getEchartsInstance(); if (chartInstance) { - const opt = chartInstance.getOption() as { dataZoom?: Array<{ start?: number; end?: number }> }; + const opt = chartInstance.getOption() as { dataZoom?: Array<{ startValue?: number; endValue?: number }> }; if (opt.dataZoom) { setZoomState(opt.dataZoom.map((dz) => ({ - start: dz.start, - end: dz.end, + startValue: dz.startValue, + endValue: dz.endValue, }))); + // Sync input fields with current zoom + if (opt.dataZoom[0]?.startValue) { + setXStartInput(new Date(opt.dataZoom[0].startValue).toISOString().split("T")[0]); + } + if (opt.dataZoom[0]?.endValue) { + setXEndInput(new Date(opt.dataZoom[0].endValue).toISOString().split("T")[0]); + } + if (opt.dataZoom[1]?.startValue !== undefined) { + setYMinInput(formatMicrosecondsNum(opt.dataZoom[1].startValue).toString()); + } + if (opt.dataZoom[1]?.endValue !== undefined) { + setYMaxInput(formatMicrosecondsNum(opt.dataZoom[1].endValue).toString()); + } } } }, []); @@ -111,6 +131,10 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu // Clear saved zoom state when restore is triggered const onRestore = useCallback(() => { setZoomState([]); + setXStartInput(""); + setXEndInput(""); + setYMinInput(""); + setYMaxInput(""); }, []); const chartEvents = { @@ -305,6 +329,45 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu const gpuTypesFromCustom = customData?.time_series ? Object.keys(customData.time_series) : []; const gpuTypes = [...new Set([...gpuTypesFromUsers, ...gpuTypesFromCustom])]; + // Apply axis range from input fields + const handleApplyAxisRange = () => { + const newZoomState: Array<{ startValue?: number; endValue?: number }> = [{}, {}, {}, {}]; + + // Apply X-axis values + if (xStartInput) { + const date = new Date(xStartInput); + if (!isNaN(date.getTime())) { + newZoomState[0].startValue = date.getTime(); + newZoomState[2].startValue = date.getTime(); + } + } + if (xEndInput) { + const date = new Date(xEndInput); + if (!isNaN(date.getTime())) { + newZoomState[0].endValue = date.getTime(); + newZoomState[2].endValue = date.getTime(); + } + } + + // Apply Y-axis values + if (yMinInput) { + const value = parseFloat(yMinInput) / 1_000_000; + if (!isNaN(value)) { + newZoomState[1].startValue = value; + newZoomState[3].startValue = value; + } + } + if (yMaxInput) { + const value = parseFloat(yMaxInput) / 1_000_000; + if (!isNaN(value)) { + newZoomState[1].endValue = value; + newZoomState[3].endValue = value; + } + } + + setZoomState(newZoomState); + }; + const renderSearchInput = () => ( + + + X-Axis (Date): + + setXStartInput(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + sx={{ width: 150 }} + /> + setXEndInput(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + sx={{ width: 150 }} + /> + + Y-Axis (μs): + + setYMinInput(e.target.value)} + sx={{ width: 120 }} + /> + setYMaxInput(e.target.value)} + sx={{ width: 120 }} + /> + + ); }