diff --git a/DashAI/front/src/components/models/SessionVisualization.jsx b/DashAI/front/src/components/models/SessionVisualization.jsx
index 08d5881ba..2d216f07d 100644
--- a/DashAI/front/src/components/models/SessionVisualization.jsx
+++ b/DashAI/front/src/components/models/SessionVisualization.jsx
@@ -268,34 +268,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 */}
@@ -359,7 +356,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 5407cac0f..dbb34c9bb 100644
--- a/DashAI/front/src/pages/results/components/ResultsGraphs.jsx
+++ b/DashAI/front/src/pages/results/components/ResultsGraphs.jsx
@@ -1,200 +1,158 @@
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 }) {
+function ResultsGraphs({ runs, selectedSplit: splitProp, onSplitChange }) {
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");
+ // Internal split state — used only when no controlled prop is provided
+ const [internalSplit, setInternalSplit] = useState("test");
+ const [selectedMetrics, setSelectedMetrics] = useState([]);
const [chartData, setChartData] = useState({});
- const [filteredDataProcess, setFilteredDataProcess] = useState([]);
- const { t } = useTranslation(["models"]);
- const handleChangeChart = (chartType) => {
- setSelectedChart(chartType);
- };
+ // Controlled or uncontrolled split
+ const selectedSplit = splitProp ?? internalSplit;
+ const handleChangeSplit = onSplitChange ?? setInternalSplit;
- const handleToggleParameter = (parameter) => {
- setSelectedParameters((prev) =>
- prev.includes(parameter)
- ? prev.filter((p) => p !== parameter)
- : [...prev, parameter],
- );
- };
+ const finishedRuns = useMemo(
+ () => runs.filter((r) => r.status === 3),
+ [runs],
+ );
- const handleToggleMetrics = () => {
- setShowCustomMetrics((prev) => !prev);
- setSelectedParameters([]);
- };
+ 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]);
+ // Auto-select a split only when running in uncontrolled mode
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;
- });
+ if (splitProp !== undefined) return;
+ if (availableMetrics.test.length > 0) setInternalSplit("test");
+ else if (availableMetrics.validation.length > 0)
+ setInternalSplit("validation");
+ else if (availableMetrics.train.length > 0) setInternalSplit("train");
+ }, [availableMetrics, splitProp]);
+
+ useEffect(() => {
+ 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 handleToggleMetric = (metric) => {
+ const canonicalOrder = availableMetrics[selectedSplit] ?? [];
+ setSelectedMetrics((prev) => {
+ if (prev.includes(metric)) {
+ return prev.filter((m) => m !== metric);
}
- };
+ 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.
+
+ );
+ }
- processData();
- }, [runs, selectedParameters, selectedChart, showCustomMetrics]);
+ const currentMetrics = availableMetrics[selectedSplit] ?? [];
return (
- <>
- {filteredDataProcess.length === 0 ? (
-
- No information from the experiments
- There are no completed experiments or all have an error status.
-
- ) : (
-
- )}
- >
+
);
}
ResultsGraphs.propTypes = {
runs: PropTypes.array.isRequired,
+ selectedSplit: PropTypes.string,
+ onSplitChange: PropTypes.func,
+};
+
+ResultsGraphs.defaultProps = {
+ selectedSplit: undefined,
+ onSplitChange: undefined,
};
export default ResultsGraphs;
diff --git a/DashAI/front/src/pages/results/components/ResultsGraphsLayout.jsx b/DashAI/front/src/pages/results/components/ResultsGraphsLayout.jsx
index 704150eb6..426a8e381 100644
--- a/DashAI/front/src/pages/results/components/ResultsGraphsLayout.jsx
+++ b/DashAI/front/src/pages/results/components/ResultsGraphsLayout.jsx
@@ -3,22 +3,17 @@ 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,
+ currentMetrics,
+ selectedMetrics,
+ handleToggleMetric,
+ handleSelectAll,
+ handleClearAll,
chartData,
}) {
return (
@@ -26,36 +21,26 @@ function ResultsGraphsLayout({
display="flex"
flexDirection="column"
alignItems="stretch"
- textAlign="center"
width="100%"
height="100%"
>
- {/* Chart selection buttons */}
+ {/* Chart type selector */}
- {/* Switch Container */}
-
-
-
- {/* Parameter container */}
+
+ {/* 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]);
- }}
+ {/* ── Metric checkboxes ── */}
+
+
- {concatenatedMetrics.map((param) => (
+
+ {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,
+ currentMetrics: 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;