diff --git a/DashAI/front/package.json b/DashAI/front/package.json
index e0c8c7d5e..cc0f80d2b 100644
--- a/DashAI/front/package.json
+++ b/DashAI/front/package.json
@@ -15,6 +15,7 @@
"@tanstack/react-table": "^8.21.3",
"axios": "^1.3.4",
"formik": "^2.2.9",
+ "html2canvas": "^1.4.1",
"i18next": "^25.7.4",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-cli": "^1.34.1",
diff --git a/DashAI/front/src/components/DatasetVisualization.jsx b/DashAI/front/src/components/DatasetVisualization.jsx
index 440781765..7f84ae9e2 100644
--- a/DashAI/front/src/components/DatasetVisualization.jsx
+++ b/DashAI/front/src/components/DatasetVisualization.jsx
@@ -9,6 +9,7 @@ import {
Divider,
Tabs,
Tab,
+ Tooltip,
} from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { AddCircleOutline as AddIcon } from "@mui/icons-material";
@@ -20,7 +21,6 @@ import {
import { useTourContext } from "./tour/TourProvider";
import { formatDate } from "../pages/results/constants/formatDate";
import Header from "./notebooks/dataset/header/Header";
-import Tooltip from "@mui/material/Tooltip";
import OverviewTab from "./notebooks/dataset/tabs/OverviewTab";
import { NumericTab } from "./notebooks/dataset/tabs/NumericTab";
import { CategoricalTab } from "./notebooks/dataset/tabs/CategoricalTab";
@@ -185,7 +185,7 @@ export default function DatasetVisualization({
{dataset.name}
-
+
{
+ e.stopPropagation();
+ setAnchorEl(e.currentTarget);
+ };
+
+ const handleClose = () => setAnchorEl(null);
+
+ const captureElement = useCallback(
+ async (element) => {
+ if (!element) return;
+ setCapturing(true);
+ try {
+ // Wait a tick so the hidden button is removed from the DOM before capture
+ await new Promise((r) => requestAnimationFrame(r));
+ const canvas = await html2canvas(element, {
+ backgroundColor: null,
+ scale: 2,
+ });
+ const link = document.createElement("a");
+ link.download = `${filename}.png`;
+ link.href = canvas.toDataURL("image/png");
+ link.click();
+ } finally {
+ setCapturing(false);
+ }
+ },
+ [filename],
+ );
+
+ const handleExportCard = useCallback(async () => {
+ handleClose();
+ await captureElement(ref.current);
+ }, [captureElement]);
+
+ const handleExportChart = useCallback(async () => {
+ handleClose();
+ if (!ref.current) return;
+ const chart =
+ ref.current.querySelector(".recharts-wrapper") ||
+ ref.current.querySelector(".js-plotly-plot");
+ if (chart) {
+ setCapturing(true);
+ try {
+ await new Promise((r) => requestAnimationFrame(r));
+ const canvas = await html2canvas(chart, {
+ backgroundColor: null,
+ scale: 2,
+ });
+ const link = document.createElement("a");
+ link.download = `${filename}_chart.png`;
+ link.href = canvas.toDataURL("image/png");
+ link.click();
+ } finally {
+ setCapturing(false);
+ }
+ }
+ }, [filename]);
+
+ const handleExportCSV = useCallback(() => {
+ handleClose();
+ if (!exportData) return;
+ const data = Array.isArray(exportData) ? exportData : [exportData];
+ if (data.length === 0) return;
+ const headers = Object.keys(data[0]);
+ const csvRows = [
+ headers.join(","),
+ ...data.map((row) =>
+ headers
+ .map((h) => {
+ const val = row[h] ?? "";
+ const str = String(val);
+ return str.includes(",") || str.includes('"') || str.includes("\n")
+ ? `"${str.replace(/"/g, '""')}"`
+ : str;
+ })
+ .join(","),
+ ),
+ ];
+ const blob = new Blob([csvRows.join("\n")], { type: "text/csv" });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = `${filename}.csv`;
+ link.click();
+ URL.revokeObjectURL(url);
+ }, [exportData, filename]);
+
+ const handleExportJSON = useCallback(() => {
+ handleClose();
+ if (!exportData) return;
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = `${filename}.json`;
+ link.click();
+ URL.revokeObjectURL(url);
+ }, [exportData, filename]);
+
+ const hasChart = true; // Charts are detected at runtime
+ const hasData = Boolean(exportData);
+
+ return (
+
+ {!capturing && (
+
+
+
+
+
+ )}
+
+
+
+ {children}
+
+ );
+}
diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/CategoricalTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/CategoricalTab.jsx
index 42e8c1caf..699618909 100644
--- a/DashAI/front/src/components/notebooks/dataset/tabs/CategoricalTab.jsx
+++ b/DashAI/front/src/components/notebooks/dataset/tabs/CategoricalTab.jsx
@@ -1,5 +1,5 @@
import React from "react";
-import { Box, Typography, Card, CardContent } from "@mui/material";
+import { Box, Typography, CardContent } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import TitleIcon from "@mui/icons-material/Title";
import {
@@ -15,6 +15,7 @@ import {
Cell,
} from "recharts";
import { StatBox } from "../StatBox";
+import ExportableCard from "../ExportableCard";
import { useTranslation } from "react-i18next";
export const CategoricalTab = ({ categoricalStats }) => {
@@ -24,7 +25,12 @@ export const CategoricalTab = ({ categoricalStats }) => {
return (
{Object.entries(categoricalStats).map(([column, stats]) => (
-
+
{/* Header */}
@@ -148,7 +154,7 @@ export const CategoricalTab = ({ categoricalStats }) => {
-
+
))}
);
diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/CorrelationsTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/CorrelationsTab.jsx
index 18822e507..33381e97e 100644
--- a/DashAI/front/src/components/notebooks/dataset/tabs/CorrelationsTab.jsx
+++ b/DashAI/front/src/components/notebooks/dataset/tabs/CorrelationsTab.jsx
@@ -1,7 +1,8 @@
-import React, { useMemo } from "react";
-import { Box, Typography, CardContent, Card } from "@mui/material";
+import { useMemo } from "react";
+import { Box, Typography, CardContent } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import Plot from "react-plotly.js";
+import ExportableCard from "../ExportableCard";
import { useTranslation } from "react-i18next";
const CorrelationsTab = ({ correlations }) => {
@@ -45,7 +46,7 @@ const CorrelationsTab = ({ correlations }) => {
}, [correlations]);
return (
-
+
{t("datasets:label.correlationAnalysis")}
@@ -150,7 +151,7 @@ const CorrelationsTab = ({ correlations }) => {
-
+
);
};
diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/NumericTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/NumericTab.jsx
index 8dd2eae17..d40993e69 100644
--- a/DashAI/front/src/components/notebooks/dataset/tabs/NumericTab.jsx
+++ b/DashAI/front/src/components/notebooks/dataset/tabs/NumericTab.jsx
@@ -1,11 +1,12 @@
import React from "react";
-import { Box, Typography, Card, CardContent, Alert } from "@mui/material";
+import { Box, Typography, CardContent, Alert } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import TrendingUpIcon from "@mui/icons-material/TrendingUp";
import InfoIcon from "@mui/icons-material/Info";
import Plot from "react-plotly.js";
import { StatBox } from "../StatBox";
import { MetricRow } from "../MetricRow";
+import ExportableCard from "../ExportableCard";
import { Trans, useTranslation } from "react-i18next";
export const NumericTab = ({ numericStats }) => {
@@ -15,7 +16,13 @@ export const NumericTab = ({ numericStats }) => {
return (
{Object.entries(numericStats).map(([column, stats]) => (
-
+
{/* Title */}
@@ -188,7 +195,7 @@ export const NumericTab = ({ numericStats }) => {
)}
-
+
))}
);
diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/OverviewTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/OverviewTab.jsx
index a54acacb8..74fadfcc1 100644
--- a/DashAI/front/src/components/notebooks/dataset/tabs/OverviewTab.jsx
+++ b/DashAI/front/src/components/notebooks/dataset/tabs/OverviewTab.jsx
@@ -11,6 +11,7 @@ import {
Bar,
} from "recharts";
import MrtDatasetTable from "../MrtDatasetTable";
+import ExportableCard from "../ExportableCard";
import { useTranslation } from "react-i18next";
import { getColorByColumnType } from "../../../../utils";
@@ -88,7 +89,11 @@ const OverviewTab = ({
{/* Missing Values Overview — only shown when there are missing values */}
{missingData.some((data) => data.missing > 0) && (
-
+
-
+
)}
);
diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/QualityTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/QualityTab.jsx
index 4ec57ce9a..d68e414d4 100644
--- a/DashAI/front/src/components/notebooks/dataset/tabs/QualityTab.jsx
+++ b/DashAI/front/src/components/notebooks/dataset/tabs/QualityTab.jsx
@@ -1,14 +1,8 @@
import React from "react";
-import {
- Box,
- Typography,
- Paper,
- Card,
- CardContent,
- Alert,
-} from "@mui/material";
+import { Box, Typography, Paper, CardContent, Alert } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import IssueCard from "../IssueCard";
+import ExportableCard from "../ExportableCard";
import { useTranslation } from "react-i18next";
const QualityTab = ({ qualityInfo, totalRows }) => {
@@ -18,7 +12,7 @@ const QualityTab = ({ qualityInfo, totalRows }) => {
return (
{/* Data Quality Summary */}
-
+
{t("datasets:label.dataQualitySummary")}
@@ -127,10 +121,15 @@ const QualityTab = ({ qualityInfo, totalRows }) => {
-
+
{/* Missing Data by Column */}
-
+ ({ column: col, missing_ratio: ratio }),
+ )}
+ >
{t("datasets:label.missingDataByColumn")}
@@ -195,7 +194,7 @@ const QualityTab = ({ qualityInfo, totalRows }) => {
)}
-
+
);
};
diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/TextTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/TextTab.jsx
index 2021f42e2..ce6080540 100644
--- a/DashAI/front/src/components/notebooks/dataset/tabs/TextTab.jsx
+++ b/DashAI/front/src/components/notebooks/dataset/tabs/TextTab.jsx
@@ -2,7 +2,6 @@ import React from "react";
import {
Box,
Typography,
- Card,
CardContent,
Chip,
Alert,
@@ -22,6 +21,7 @@ import {
} from "recharts";
import { StatBox } from "../StatBox";
import { MetricRow } from "../MetricRow";
+import ExportableCard from "../ExportableCard";
import { useTranslation } from "react-i18next";
export const TextTab = ({ textStats }) => {
@@ -58,7 +58,13 @@ export const TextTab = ({ textStats }) => {
: null;
return (
-
+
{/* Title */}
@@ -208,7 +214,7 @@ export const TextTab = ({ textStats }) => {
-
+
);
})}
diff --git a/DashAI/front/src/utils/i18n/locales/en/datasets.json b/DashAI/front/src/utils/i18n/locales/en/datasets.json
index 2e79550f3..71494e2bb 100644
--- a/DashAI/front/src/utils/i18n/locales/en/datasets.json
+++ b/DashAI/front/src/utils/i18n/locales/en/datasets.json
@@ -140,6 +140,12 @@
"explorationType": "Exploration Type",
"explore": "Explore",
"explorer": "Explorer",
+ "exportCardImage": "Card as image",
+ "exportChartImage": "Chart as image",
+ "exportCSV": "Download as CSV",
+ "exportImage": "Download",
+ "exportJSON": "Download as JSON",
+ "exportMetrics": "Export metrics as JSON",
"fileSizeMB": "File Size (MB)",
"firstLastStopsAtExtremes": "Color stops must include positions 0 at the start and 1 at the end.",
"fontFamily": "Font Family",
diff --git a/DashAI/front/src/utils/i18n/locales/es/datasets.json b/DashAI/front/src/utils/i18n/locales/es/datasets.json
index bd974e05d..a514debb0 100644
--- a/DashAI/front/src/utils/i18n/locales/es/datasets.json
+++ b/DashAI/front/src/utils/i18n/locales/es/datasets.json
@@ -142,6 +142,12 @@
"explorationType": "Tipo de Exploración",
"explore": "Explorar",
"explorer": "Explorador",
+ "exportCardImage": "Tarjeta como imagen",
+ "exportChartImage": "Gráfico como imagen",
+ "exportCSV": "Descargar como CSV",
+ "exportImage": "Descargar",
+ "exportJSON": "Descargar como JSON",
+ "exportMetrics": "Exportar métricas como JSON",
"fileSizeMB": "Tamaño del Archivo (MB)",
"firstLastStopsAtExtremes": "Los puntos de color deben incluir posiciones 0 al inicio y 1 al final.",
"fontFamily": "Familia de Fuente",
diff --git a/DashAI/front/yarn.lock b/DashAI/front/yarn.lock
index 2a78a3e25..645b74363 100644
--- a/DashAI/front/yarn.lock
+++ b/DashAI/front/yarn.lock
@@ -7436,6 +7436,15 @@ __metadata:
languageName: node
linkType: hard
+"css-line-break@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "css-line-break@npm:2.1.0"
+ dependencies:
+ utrie: ^1.0.2
+ checksum: 37b1fe632b03be7a287cd394cef8b5285666343443125c510df9cfb6a4734a2c71e154ec8f7bbff72d7c339e1e5872989b1c52d86162aed27d6cc114725bb4d0
+ languageName: node
+ linkType: hard
+
"css-loader@npm:^6.5.1":
version: 6.11.0
resolution: "css-loader@npm:6.11.0"
@@ -8010,6 +8019,7 @@ __metadata:
eslint-plugin-promise: ^6.0.0
eslint-plugin-react: ^7.34.2
formik: ^2.2.9
+ html2canvas: ^1.4.1
i18next: ^25.7.4
i18next-browser-languagedetector: ^8.2.0
i18next-cli: ^1.34.1
@@ -11129,6 +11139,16 @@ __metadata:
languageName: node
linkType: hard
+"html2canvas@npm:^1.4.1":
+ version: 1.4.1
+ resolution: "html2canvas@npm:1.4.1"
+ dependencies:
+ css-line-break: ^2.1.0
+ text-segmentation: ^1.0.3
+ checksum: c134324af57f3262eecf982e436a4843fded3c6cf61954440ffd682527e4dd350e0c2fafd217c0b6f9a455fe345d0c67b4505689796ab160d4ca7c91c3766739
+ languageName: node
+ linkType: hard
+
"htmlparser2@npm:^6.1.0":
version: 6.1.0
resolution: "htmlparser2@npm:6.1.0"
@@ -18907,6 +18927,15 @@ __metadata:
languageName: node
linkType: hard
+"text-segmentation@npm:^1.0.3":
+ version: 1.0.3
+ resolution: "text-segmentation@npm:1.0.3"
+ dependencies:
+ utrie: ^1.0.2
+ checksum: 2e24632d59567c55ab49ac324815e2f7a8043e63e26b109636322ac3e30692cee8679a448fd5d0f0598a345f407afd0e34ba612e22524cf576d382d84058c013
+ languageName: node
+ linkType: hard
+
"text-table@npm:^0.2.0":
version: 0.2.0
resolution: "text-table@npm:0.2.0"
@@ -19643,6 +19672,15 @@ __metadata:
languageName: node
linkType: hard
+"utrie@npm:^1.0.2":
+ version: 1.0.2
+ resolution: "utrie@npm:1.0.2"
+ dependencies:
+ base64-arraybuffer: ^1.0.2
+ checksum: c96fbb7d4d8855a154327da0b18e39b7511cc70a7e4bcc3658e24f424bb884312d72b5ba777500b8858e34d365dc6b1a921dc5ca2f0d341182519c6b78e280a5
+ languageName: node
+ linkType: hard
+
"uuid@npm:^8.3.2":
version: 8.3.2
resolution: "uuid@npm:8.3.2"