Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DashAI/front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions DashAI/front/src/components/DatasetVisualization.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -185,7 +185,7 @@ export default function DatasetVisualization({
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.5 }}>
<Typography variant="h4">{dataset.name}</Typography>
</Box>
<Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Tooltip
title={t("datasets:label.dataQualityScoreTooltip")}
arrow
Expand Down
210 changes: 210 additions & 0 deletions DashAI/front/src/components/notebooks/dataset/ExportableCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { useRef, useCallback, useState } from "react";
import {
Card,
IconButton,
Tooltip,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
} from "@mui/material";
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
import ImageOutlinedIcon from "@mui/icons-material/ImageOutlined";
import BarChartOutlinedIcon from "@mui/icons-material/BarChartOutlined";
import TableChartOutlinedIcon from "@mui/icons-material/TableChartOutlined";
import DataObjectOutlinedIcon from "@mui/icons-material/DataObjectOutlined";
import html2canvas from "html2canvas";
import { useTranslation } from "react-i18next";

/**
* @param {Object} props
* @param {string} props.filename - Base filename for exports
* @param {Array|Object} props.exportData - Data for CSV/JSON export (optional)
*/
export default function ExportableCard({
children,
filename = "export",
exportData,
sx,
...props
}) {
const ref = useRef(null);
const { t } = useTranslation(["datasets"]);
const [anchorEl, setAnchorEl] = useState(null);
const [capturing, setCapturing] = useState(false);
const open = Boolean(anchorEl);

const handleClick = (e) => {
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 (
<Card ref={ref} sx={{ position: "relative", ...sx }} {...props}>
{!capturing && (
<Tooltip title={t("datasets:label.exportImage")} arrow>
<IconButton
size="small"
onClick={handleClick}
sx={{
position: "absolute",
top: 8,
right: 8,
zIndex: 1,
color: "primary.main",
opacity: 1,
}}
>
<FileDownloadOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
)}

<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
transformOrigin={{ vertical: "top", horizontal: "right" }}
slotProps={{
paper: {
sx: { minWidth: 200 },
},
}}
>
<MenuItem onClick={handleExportCard}>
<ListItemIcon>
<ImageOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText>{t("datasets:label.exportCardImage")}</ListItemText>
</MenuItem>

<MenuItem onClick={handleExportChart}>
<ListItemIcon>
<BarChartOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText>{t("datasets:label.exportChartImage")}</ListItemText>
</MenuItem>

{hasData && (
<MenuItem onClick={handleExportCSV}>
<ListItemIcon>
<TableChartOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText>{t("datasets:label.exportCSV")}</ListItemText>
</MenuItem>
)}

{hasData && (
<MenuItem onClick={handleExportJSON}>
<ListItemIcon>
<DataObjectOutlinedIcon fontSize="small" />
</ListItemIcon>
<ListItemText>{t("datasets:label.exportJSON")}</ListItemText>
</MenuItem>
)}
</Menu>

{children}
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 }) => {
Expand All @@ -24,7 +25,12 @@ export const CategoricalTab = ({ categoricalStats }) => {
return (
<Box display="flex" flexDirection="column" gap={4}>
{Object.entries(categoricalStats).map(([column, stats]) => (
<Card key={column} sx={{ borderRadius: 2 }}>
<ExportableCard
key={column}
filename={`categorical_${column}`}
exportData={{ column, ...stats }}
sx={{ borderRadius: 2 }}
>
<CardContent sx={{ bgcolor: theme.palette.ui.box }}>
{/* Header */}
<Box display="flex" alignItems="center" mb={2}>
Expand Down Expand Up @@ -148,7 +154,7 @@ export const CategoricalTab = ({ categoricalStats }) => {
</Box>
</Box>
</CardContent>
</Card>
</ExportableCard>
))}
</Box>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand Down Expand Up @@ -45,7 +46,7 @@ const CorrelationsTab = ({ correlations }) => {
}, [correlations]);

return (
<Card>
<ExportableCard filename="correlations" exportData={strongCorrelations}>
<CardContent sx={{ bgcolor: theme.palette.ui.panelDark }}>
<Typography variant="h6" fontWeight="bold" mb={3}>
{t("datasets:label.correlationAnalysis")}
Expand Down Expand Up @@ -150,7 +151,7 @@ const CorrelationsTab = ({ correlations }) => {
</Box>
</Box>
</CardContent>
</Card>
</ExportableCard>
);
};

Expand Down
13 changes: 10 additions & 3 deletions DashAI/front/src/components/notebooks/dataset/tabs/NumericTab.jsx
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand All @@ -15,7 +16,13 @@ export const NumericTab = ({ numericStats }) => {
return (
<Box display="flex" flexDirection="column" gap={4}>
{Object.entries(numericStats).map(([column, stats]) => (
<Card key={column} data-column-card={column} sx={{ borderRadius: 2 }}>
<ExportableCard
key={column}
filename={`numeric_${column}`}
exportData={{ column, ...stats }}
data-column-card={column}
sx={{ borderRadius: 2 }}
>
<CardContent sx={{ bgcolor: theme.palette.ui.box }}>
{/* Title */}
<Box display="flex" alignItems="center" mb={2}>
Expand Down Expand Up @@ -188,7 +195,7 @@ export const NumericTab = ({ numericStats }) => {
</Alert>
)}
</CardContent>
</Card>
</ExportableCard>
))}
</Box>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -88,7 +89,11 @@ const OverviewTab = ({
</Card>
{/* Missing Values Overview — only shown when there are missing values */}
{missingData.some((data) => data.missing > 0) && (
<Card data-section="missing-values-overview">
<ExportableCard
filename="missing_values_overview"
exportData={missingData}
data-section="missing-values-overview"
>
<CardContent sx={{ bgcolor: theme.palette.ui.box }}>
<Box
sx={{
Expand Down Expand Up @@ -141,7 +146,7 @@ const OverviewTab = ({
</ResponsiveContainer>
</Box>
</CardContent>
</Card>
</ExportableCard>
)}
</Box>
);
Expand Down
Loading
Loading