From 9a315cec8c78b5447116c8d91fce9b5fd8b46b0f Mon Sep 17 00:00:00 2001 From: Felipe Date: Tue, 24 Feb 2026 21:46:45 -0300 Subject: [PATCH 1/2] feat: enhance metrics visualization with new chart types and improved layout --- .../results/components/ResultsGraphs.jsx | 285 +++++++----------- .../components/ResultsGraphsLayout.jsx | 67 ++-- .../components/ResultsGraphsParameters.jsx | 203 +++++++++---- .../results/components/ResultsGraphsPlot.jsx | 51 +++- .../components/ResultsGraphsSelection.jsx | 74 ++--- .../pages/results/constants/graphsMaking.jsx | 76 +++-- .../pages/results/constants/layoutMaking.jsx | 87 ++++-- 7 files changed, 454 insertions(+), 389 deletions(-) diff --git a/DashAI/front/src/pages/results/components/ResultsGraphs.jsx b/DashAI/front/src/pages/results/components/ResultsGraphs.jsx index 5407cac0f..daa658428 100644 --- a/DashAI/front/src/pages/results/components/ResultsGraphs.jsx +++ b/DashAI/front/src/pages/results/components/ResultsGraphs.jsx @@ -1,195 +1,140 @@ import PropTypes from "prop-types"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { Alert, AlertTitle } from "@mui/material"; import { useSnackbar } from "notistack"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; import graphsMaking from "../constants/graphsMaking"; import layoutMaking from "../constants/layoutMaking"; import ResultsGraphsLayout from "./ResultsGraphsLayout"; -import { useTranslation } from "react-i18next"; function ResultsGraphs({ runs }) { const { enqueueSnackbar } = useSnackbar(); - const [selectedChart, setSelectedChart] = useState("radar"); - const [selectedParameters, setSelectedParameters] = useState([]); - const [showCustomMetrics, setShowCustomMetrics] = useState(false); - const [selectedGeneralMetric, setSelectedGeneralMetric] = useState("test"); + const theme = useTheme(); + const { t } = useTranslation(["models"]); - const [concatenatedMetrics, setConcatenatedMetrics] = useState([]); - const [tabularMetrics, setTabularMetrics] = useState([]); + const [selectedChart, setSelectedChart] = useState("bar"); + const [selectedSplit, setSelectedSplit] = useState("test"); + const [selectedMetrics, setSelectedMetrics] = useState([]); const [chartData, setChartData] = useState({}); - const [filteredDataProcess, setFilteredDataProcess] = useState([]); - const { t } = useTranslation(["models"]); - const handleChangeChart = (chartType) => { - setSelectedChart(chartType); - }; + const finishedRuns = useMemo( + () => runs.filter((r) => r.status === 3), + [runs], + ); - const handleToggleParameter = (parameter) => { - setSelectedParameters((prev) => - prev.includes(parameter) - ? prev.filter((p) => p !== parameter) - : [...prev, parameter], - ); - }; + const availableMetrics = useMemo(() => { + const sets = { train: new Set(), validation: new Set(), test: new Set() }; + finishedRuns.forEach((run) => { + if (run.train_metrics) + Object.keys(run.train_metrics).forEach((m) => sets.train.add(m)); + if (run.validation_metrics) + Object.keys(run.validation_metrics).forEach((m) => + sets.validation.add(m), + ); + if (run.test_metrics) + Object.keys(run.test_metrics).forEach((m) => sets.test.add(m)); + }); + return { + train: Array.from(sets.train), + validation: Array.from(sets.validation), + test: Array.from(sets.test), + }; + }, [finishedRuns]); - const handleToggleMetrics = () => { - setShowCustomMetrics((prev) => !prev); - setSelectedParameters([]); - }; + useEffect(() => { + if (availableMetrics.test.length > 0) setSelectedSplit("test"); + else if (availableMetrics.validation.length > 0) + setSelectedSplit("validation"); + else if (availableMetrics.train.length > 0) setSelectedSplit("train"); + }, [availableMetrics]); useEffect(() => { - if (!runs) return; - - const processData = async () => { - try { - // Only take finished runs - const finished = runs.filter((item) => item.status === 3); // Finished - setFilteredDataProcess(finished); - - if (finished.length === 0) return; - - const graphsToView = {}; - let parameterIndex = []; - const generalParameters = []; - let pieCounter = 0; - - // Extract metrics - const extractedMetrics = finished.map((item) => { - const metrics = {}; - Object.keys(item).forEach((key) => { - if (key.includes("metrics")) { - metrics[key] = item[key]; - } - }); - return metrics; - }); + setSelectedMetrics(availableMetrics[selectedSplit] ?? []); + }, [selectedSplit, availableMetrics]); - if (extractedMetrics.length > 0) { - const metricsOrder = Object.keys(extractedMetrics[0]); - const metricsValuesOrder = Object.keys( - extractedMetrics[0][metricsOrder[0]], - ); - - const concatenated = metricsOrder - .map((m) => m.split("_")[0]) - .concat(metricsValuesOrder); - - setConcatenatedMetrics(concatenated); - - // Build table metrics - const tableMetrics = []; - metricsOrder.forEach((metricType) => { - metricsValuesOrder.forEach((metric) => { - tableMetrics.push(`${metricType.split("_")[0]} ${metric}`); - }); - }); - setTabularMetrics(tableMetrics); - - // Pick indices of selected parameters - if (showCustomMetrics) { - parameterIndex = selectedParameters.map((p) => - tableMetrics.indexOf(p), - ); - } else if (selectedGeneralMetric.length > 0) { - const criteria = {}; - concatenated.forEach((item) => (criteria[item] = item)); - - tableMetrics.forEach((metric, index) => { - Object.entries(criteria).forEach(([metName, substring]) => { - if ( - selectedGeneralMetric === metName && - metric.includes(substring) - ) { - parameterIndex.push(index); - generalParameters.push(metric); - } - }); - }); - } - - // Build values for each run - finished.forEach((item) => { - const numericValues = []; - - metricsOrder.forEach((metricType) => { - const values = item[metricType]; - metricsValuesOrder.forEach((metric) => { - numericValues.push(values[metric]); - }); - }); - - const relevantValues = parameterIndex.map( - (index) => numericValues[index], - ); - - graphsMaking( - graphsToView, - item, - relevantValues, - showCustomMetrics, - selectedParameters, - generalParameters, - pieCounter, - ); - - pieCounter += 1; - }); - - // Generate layouts - const { generalLayout, pieLayout } = layoutMaking( - selectedChart, - graphsToView, - ); - - const keys = Object.keys(graphsToView); - const radarValues = graphsToView[keys[0]]; - const barValues = graphsToView[keys[1]]; - const pieValues = graphsToView[keys[2]]; - - setChartData({ - generalLayout, - pieLayout, - radarValues, - barValues, - pieValues, - }); - } - } catch (error) { - enqueueSnackbar(t("models:error.errorProcesingExperimentResults"), { - variant: "error", + useEffect(() => { + if (finishedRuns.length === 0 || selectedMetrics.length === 0) { + setChartData({}); + return; + } + + try { + const metricsKey = `${selectedSplit}_metrics`; + const graphsToView = {}; + + finishedRuns.forEach((run, idx) => { + const metricsObj = run[metricsKey] ?? {}; + const values = selectedMetrics.map((m) => { + const v = metricsObj[m]; + if (v === undefined || v === null) return null; + if (Array.isArray(v)) return v[v.length - 1]?.value ?? null; + return typeof v === "number" ? v : null; }); - console.error(error); + graphsMaking(graphsToView, run, selectedMetrics, values, idx, theme); + }); + + const { generalLayout } = layoutMaking( + selectedChart, + graphsToView, + theme, + ); + setChartData({ generalLayout, ...graphsToView }); + } catch (error) { + enqueueSnackbar(t("models:error.errorProcesingExperimentResults"), { + variant: "error", + }); + console.error(error); + } + }, [ + finishedRuns, + selectedSplit, + selectedMetrics, + selectedChart, + theme, + enqueueSnackbar, + t, + ]); + + const handleChangeChart = (chartType) => setSelectedChart(chartType); + const handleChangeSplit = (split) => setSelectedSplit(split); + const handleToggleMetric = (metric) => { + const canonicalOrder = availableMetrics[selectedSplit] ?? []; + setSelectedMetrics((prev) => { + if (prev.includes(metric)) { + return prev.filter((m) => m !== metric); } - }; - - processData(); - }, [runs, selectedParameters, selectedChart, showCustomMetrics]); + const next = new Set([...prev, metric]); + return canonicalOrder.filter((m) => next.has(m)); + }); + }; + const handleSelectAll = () => + setSelectedMetrics(availableMetrics[selectedSplit] ?? []); + const handleClearAll = () => setSelectedMetrics([]); + + if (finishedRuns.length === 0) { + return ( + + No information from the experiments + There are no completed experiments or all have an error status. + + ); + } return ( - <> - {filteredDataProcess.length === 0 ? ( - - No information from the experiments - There are no completed experiments or all have an error status. - - ) : ( - - )} - + ); } diff --git a/DashAI/front/src/pages/results/components/ResultsGraphsLayout.jsx b/DashAI/front/src/pages/results/components/ResultsGraphsLayout.jsx index 704150eb6..46185b818 100644 --- a/DashAI/front/src/pages/results/components/ResultsGraphsLayout.jsx +++ b/DashAI/front/src/pages/results/components/ResultsGraphsLayout.jsx @@ -3,22 +3,19 @@ import PropTypes from "prop-types"; import { Box } from "@mui/material"; import ResultsGraphsSelection from "./ResultsGraphsSelection"; -import ResultsGraphsSwitch from "./ResultsGraphsSwitch"; import ResultsGraphsParameters from "./ResultsGraphsParameters"; import ResultsGraphsPlot from "./ResultsGraphsPlot"; function ResultsGraphsLayout({ selectedChart, handleChangeChart, - showCustomMetrics, - handleToggleMetrics, - tabularMetrics, - selectedParameters, - handleToggleParameter, - selectedGeneralMetric, - setSelectedGeneralMetric, - setSelectedParameters, - concatenatedMetrics, + selectedSplit, + handleChangeSplit, + availableMetrics, + selectedMetrics, + handleToggleMetric, + handleSelectAll, + handleClearAll, chartData, }) { return ( @@ -26,36 +23,28 @@ function ResultsGraphsLayout({ display="flex" flexDirection="column" alignItems="stretch" - textAlign="center" width="100%" height="100%" > - {/* Chart selection buttons */} + {/* Chart type selector */} - {/* Switch Container */} - - - - {/* Parameter container */} + + {/* Split + metric filter sidebar */} - {/* Plotly Chart */} + {/* Plotly chart area */} - {showCustomMetrics ? ( - tabularMetrics.map((param) => ( - handleToggleParameter(param)} - /> - } - label={param} - /> - )) - ) : ( - { - const selectedMetric = event.target.value; - setSelectedGeneralMetric(selectedMetric); - setSelectedParameters([selectedMetric]); + {/* ── Split selector ── */} + + + {t("common:split", "Split")} + + { + if (v) handleChangeSplit(v); }} + size="small" + fullWidth > - {concatenatedMetrics.map((param) => ( + {splits.map(({ key, label }) => ( + + {label} + + ))} + + + + {/* ── Metric checkboxes ── */} + + + + {t("common:metrics", "Metrics")} + + + + + + + + + {currentMetrics.length === 0 ? ( + + {t("models:label.noMetricsAvailableForThisView")} + + ) : ( + currentMetrics.map((metric) => ( } - label={param} + key={metric} + control={ + handleToggleMetric(metric)} + /> + } + label={{metric}} + sx={{ display: "flex", m: 0, py: 0.25 }} /> - ))} - - )} + )) + )} + ); } ResultsGraphsParameters.propTypes = { - showCustomMetrics: PropTypes.bool.isRequired, - tabularMetrics: PropTypes.array.isRequired, - selectedParameters: PropTypes.array.isRequired, - handleToggleParameter: PropTypes.func.isRequired, - selectedGeneralMetric: PropTypes.string.isRequired, - setSelectedGeneralMetric: PropTypes.func.isRequired, - setSelectedParameters: PropTypes.func.isRequired, - concatenatedMetrics: PropTypes.array.isRequired, + selectedSplit: PropTypes.string.isRequired, + handleChangeSplit: PropTypes.func.isRequired, + availableMetrics: PropTypes.shape({ + train: PropTypes.array, + validation: PropTypes.array, + test: PropTypes.array, + }).isRequired, + selectedMetrics: PropTypes.array.isRequired, + handleToggleMetric: PropTypes.func.isRequired, + handleSelectAll: PropTypes.func.isRequired, + handleClearAll: PropTypes.func.isRequired, }; export default ResultsGraphsParameters; diff --git a/DashAI/front/src/pages/results/components/ResultsGraphsPlot.jsx b/DashAI/front/src/pages/results/components/ResultsGraphsPlot.jsx index a16d7cde1..5f2ca70eb 100644 --- a/DashAI/front/src/pages/results/components/ResultsGraphsPlot.jsx +++ b/DashAI/front/src/pages/results/components/ResultsGraphsPlot.jsx @@ -1,30 +1,51 @@ import React from "react"; import PropTypes from "prop-types"; -import { Box } from "@mui/material"; +import { Box, Typography } from "@mui/material"; import Plot from "react-plotly.js"; +import { useTranslation } from "react-i18next"; function ResultsGraphsPlot({ selectedChart, chartData }) { + const { t } = useTranslation(["models"]); + + const traceData = + selectedChart === "radar" ? (chartData.radar ?? []) : (chartData.bar ?? []); + + const hasData = traceData.length > 0; + + if (!hasData) { + return ( + + + {t("models:label.noMetricsAvailableForThisView")} + + + ); + } + return ( - + ); diff --git a/DashAI/front/src/pages/results/components/ResultsGraphsSelection.jsx b/DashAI/front/src/pages/results/components/ResultsGraphsSelection.jsx index 0c9cb2fe1..340df48fb 100644 --- a/DashAI/front/src/pages/results/components/ResultsGraphsSelection.jsx +++ b/DashAI/front/src/pages/results/components/ResultsGraphsSelection.jsx @@ -1,56 +1,46 @@ import React from "react"; import PropTypes from "prop-types"; -import { Box, Button } from "@mui/material"; -import { useTranslation } from "react-i18next"; +import { + Box, + ToggleButton, + ToggleButtonGroup, + Typography, +} from "@mui/material"; import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; function ResultsGraphsSelection({ selectedChart, handleChangeChart }) { const { t } = useTranslation(["models"]); const theme = useTheme(); + return ( - - - - {/* */} + {t("models:label.bar")} + {t("models:label.radar")} + ); } diff --git a/DashAI/front/src/pages/results/constants/graphsMaking.jsx b/DashAI/front/src/pages/results/constants/graphsMaking.jsx index cd4714ef8..27438011d 100644 --- a/DashAI/front/src/pages/results/constants/graphsMaking.jsx +++ b/DashAI/front/src/pages/results/constants/graphsMaking.jsx @@ -1,54 +1,50 @@ -// Add news graphs and how to generate if applies -function graphsMaking( - graphsToView, - item, - relevantNumericValues, - showCustomMetrics, - selectedParameters, - generalParameters, - pieCounter, -) { +const getTraceColors = (theme) => [ + theme.palette.primary.main, + theme.palette.secondary.main, + theme.palette.chart?.train || "#4caf50", + theme.palette.chart?.test || "#2196f3", + theme.palette.chart?.validation || "#ff9800", + theme.palette.success?.main || "#43A047", + theme.palette.info?.main || "#2196f3", + theme.palette.warning?.main || "#ed6c02", + theme.palette.error?.main || "#d32f2f", +]; + +/** + * Append one run's traces to graphsToView for radar and bar chart types. + * + * @param {object} graphsToView Accumulator object { radar: [], bar: [] } + * @param {object} run Run data object + * @param {string[]} metrics Metric names to plot + * @param {number[]} values Corresponding metric values (null if missing) + * @param {number} runIndex Zero-based index used to pick a trace color + * @param {object} theme MUI theme object + */ +function graphsMaking(graphsToView, run, metrics, values, runIndex, theme) { graphsToView.radar = graphsToView.radar || []; graphsToView.bar = graphsToView.bar || []; - graphsToView.pie = graphsToView.pie || []; - const radarTheta = showCustomMetrics ? selectedParameters : generalParameters; - const radarR = relevantNumericValues; + const colors = getTraceColors(theme); + const color = colors[runIndex % colors.length]; + const runLabel = run.run_name || run.name || `Run ${runIndex + 1}`; - // Radar Graph + const radarValues = values.map((v) => v ?? 0); graphsToView.radar.push({ type: "scatterpolar", - name: item.name, - automargin: true, - r: [...radarR, radarR[0]], // Add first value at the end to close the shape - theta: [...radarTheta, radarTheta[0]], // Add first label at the end + name: runLabel, + r: [...radarValues, radarValues[0]], + theta: [...metrics, metrics[0]], fill: "toself", + line: { color, width: 2 }, + opacity: 0.85, }); - // Bar Graph graphsToView.bar.push({ type: "bar", - automargin: true, - name: item.name, - x: showCustomMetrics ? selectedParameters : generalParameters, - y: relevantNumericValues, - }); - - // Pie Graph - graphsToView.pie.push({ - type: "pie", - name: item.name, - automargin: true, - title: item.name, - labels: showCustomMetrics ? selectedParameters : generalParameters, - values: relevantNumericValues, - domain: { - row: Math.floor(pieCounter / 2), - column: pieCounter % 2, - }, - hoverinfo: "label+percent+name", - textinfo: "percent", - textposition: "inside", + name: runLabel, + x: metrics, + y: values, + marker: { color, opacity: 0.85 }, }); return graphsToView; diff --git a/DashAI/front/src/pages/results/constants/layoutMaking.jsx b/DashAI/front/src/pages/results/constants/layoutMaking.jsx index f5516a2d9..902f240a7 100644 --- a/DashAI/front/src/pages/results/constants/layoutMaking.jsx +++ b/DashAI/front/src/pages/results/constants/layoutMaking.jsx @@ -1,35 +1,70 @@ -// Add news graphs and how to generate if applies -function layoutMaking(selectedChart, graphsToView) { - // General Layout - const generalLayout = { - polar: { - radialaxis: { visible: selectedChart === "radar", range: [0, 1] }, - }, - showlegend: true, - height: 480, - width: 800, - }; +/** + * Build a Plotly layout object that respects the current MUI theme. + * Colors update automatically whenever the user switches between light / dark mode. + * + * @param {string} selectedChart "radar" | "bar" + * @param {object} _graphsToView Unused – kept for API compatibility + * @param {object} theme MUI theme object + * @returns {{ generalLayout: object }} + */ +function layoutMaking(selectedChart, _graphsToView, theme) { + const bgColor = theme.palette.background.paper; + const textColor = theme.palette.text.primary; + const gridColor = theme.palette.divider; - // Layout only for Pie Charts - let numRows, numColumns; - if (graphsToView.pie.length <= 2) { - numRows = 1; - numColumns = graphsToView.pie.length; - } else { - numRows = Math.ceil(graphsToView.pie.length / 2); - numColumns = Math.min(2, graphsToView.pie.length); - } + const axisConfig = + selectedChart === "radar" + ? { + polar: { + bgcolor: bgColor, + radialaxis: { + visible: true, + gridcolor: gridColor, + linecolor: gridColor, + tickfont: { color: textColor }, + }, + angularaxis: { + color: textColor, + gridcolor: gridColor, + linecolor: gridColor, + }, + }, + } + : { + barmode: "group", + xaxis: { + gridcolor: gridColor, + zerolinecolor: gridColor, + tickfont: { color: textColor }, + }, + yaxis: { + gridcolor: gridColor, + zerolinecolor: gridColor, + tickfont: { color: textColor }, + }, + }; - const pieLayout = { - height: 480, - width: 800, - grid: { rows: numRows, columns: numColumns }, + const generalLayout = { + ...axisConfig, + showlegend: true, + height: 460, + autosize: true, + paper_bgcolor: bgColor, + plot_bgcolor: bgColor, + font: { + color: textColor, + family: "Quicksand-Bold, sans-serif", + size: 12, + }, legend: { - itemclick: false, + bgcolor: bgColor, + bordercolor: gridColor, + borderwidth: 1, }, + margin: { l: 60, r: 30, t: 40, b: 80 }, }; - return { generalLayout, pieLayout }; + return { generalLayout }; } export default layoutMaking; From 31b6523fe7bcec546fe454e18a5819fce00be6af Mon Sep 17 00:00:00 2001 From: Felipe Date: Tue, 24 Feb 2026 21:59:20 -0300 Subject: [PATCH 2/2] feat: refactor ResultsGraphs components to support controlled split state from SessionVisualization --- .../models/SessionVisualization.jsx | 59 ++++++++-------- .../results/components/ResultsGraphs.jsx | 33 ++++++--- .../components/ResultsGraphsLayout.jsx | 18 ++--- .../components/ResultsGraphsParameters.jsx | 67 ++----------------- 4 files changed, 61 insertions(+), 116 deletions(-) diff --git a/DashAI/front/src/components/models/SessionVisualization.jsx b/DashAI/front/src/components/models/SessionVisualization.jsx index 90bab59da..a7c098573 100644 --- a/DashAI/front/src/components/models/SessionVisualization.jsx +++ b/DashAI/front/src/components/models/SessionVisualization.jsx @@ -222,34 +222,31 @@ export default function SessionVisualization({ {t("models:label.modelComparison")} - {/* Metric Split Selector */} - {showTable && - (hasTrainMetrics || hasValidationMetrics || hasTestMetrics) && ( - { - if (newValue !== null) setMetricSplit(newValue); - }} - size="small" - > - {hasTrainMetrics && ( - - {t("common:train")} - - )} - {hasValidationMetrics && ( - - {t("common:validation")} - - )} - {hasTestMetrics && ( - - {t("common:test")} - - )} - - )} + {/* Metric Split Selector — controls both table and graph views */} + {(hasTrainMetrics || hasValidationMetrics || hasTestMetrics) && ( + { + if (newValue !== null) setMetricSplit(newValue); + }} + size="small" + > + {hasTrainMetrics && ( + + {t("common:train")} + + )} + {hasValidationMetrics && ( + + {t("common:validation")} + + )} + {hasTestMetrics && ( + {t("common:test")} + )} + + )} {/* Toggle between Table and Graphs */} @@ -313,7 +310,11 @@ export default function SessionVisualization({ metricSplit={metricSplit} /> ) : ( - + )} )} diff --git a/DashAI/front/src/pages/results/components/ResultsGraphs.jsx b/DashAI/front/src/pages/results/components/ResultsGraphs.jsx index daa658428..dbb34c9bb 100644 --- a/DashAI/front/src/pages/results/components/ResultsGraphs.jsx +++ b/DashAI/front/src/pages/results/components/ResultsGraphs.jsx @@ -9,16 +9,21 @@ import graphsMaking from "../constants/graphsMaking"; import layoutMaking from "../constants/layoutMaking"; import ResultsGraphsLayout from "./ResultsGraphsLayout"; -function ResultsGraphs({ runs }) { +function ResultsGraphs({ runs, selectedSplit: splitProp, onSplitChange }) { const { enqueueSnackbar } = useSnackbar(); const theme = useTheme(); const { t } = useTranslation(["models"]); const [selectedChart, setSelectedChart] = useState("bar"); - const [selectedSplit, setSelectedSplit] = useState("test"); + // Internal split state — used only when no controlled prop is provided + const [internalSplit, setInternalSplit] = useState("test"); const [selectedMetrics, setSelectedMetrics] = useState([]); const [chartData, setChartData] = useState({}); + // Controlled or uncontrolled split + const selectedSplit = splitProp ?? internalSplit; + const handleChangeSplit = onSplitChange ?? setInternalSplit; + const finishedRuns = useMemo( () => runs.filter((r) => r.status === 3), [runs], @@ -43,12 +48,14 @@ function ResultsGraphs({ runs }) { }; }, [finishedRuns]); + // Auto-select a split only when running in uncontrolled mode useEffect(() => { - if (availableMetrics.test.length > 0) setSelectedSplit("test"); + if (splitProp !== undefined) return; + if (availableMetrics.test.length > 0) setInternalSplit("test"); else if (availableMetrics.validation.length > 0) - setSelectedSplit("validation"); - else if (availableMetrics.train.length > 0) setSelectedSplit("train"); - }, [availableMetrics]); + setInternalSplit("validation"); + else if (availableMetrics.train.length > 0) setInternalSplit("train"); + }, [availableMetrics, splitProp]); useEffect(() => { setSelectedMetrics(availableMetrics[selectedSplit] ?? []); @@ -98,7 +105,6 @@ function ResultsGraphs({ runs }) { ]); const handleChangeChart = (chartType) => setSelectedChart(chartType); - const handleChangeSplit = (split) => setSelectedSplit(split); const handleToggleMetric = (metric) => { const canonicalOrder = availableMetrics[selectedSplit] ?? []; setSelectedMetrics((prev) => { @@ -122,13 +128,13 @@ function ResultsGraphs({ runs }) { ); } + const currentMetrics = availableMetrics[selectedSplit] ?? []; + return ( - {/* Split + metric filter sidebar */} + {/* Metric filter sidebar */} - {/* ── Split selector ── */} - - - {t("common:split", "Split")} - - { - if (v) handleChangeSplit(v); - }} - size="small" - fullWidth - > - {splits.map(({ key, label }) => ( - - {label} - - ))} - - - {/* ── Metric checkboxes ── */}