diff --git a/DashAI/front/src/components/DatasetVisualization.jsx b/DashAI/front/src/components/DatasetVisualization.jsx index be0f80a4d..cd43850db 100644 --- a/DashAI/front/src/components/DatasetVisualization.jsx +++ b/DashAI/front/src/components/DatasetVisualization.jsx @@ -60,6 +60,12 @@ export default function DatasetVisualization({ datasetsContext?.setDatasetTab ?? modelsContext?.setDatasetTab ?? (() => {}); + const scrollToColumn = + datasetsContext?.scrollToColumn ?? modelsContext?.scrollToColumn ?? null; + const setScrollToColumn = + datasetsContext?.setScrollToColumn ?? + modelsContext?.setScrollToColumn ?? + (() => {}); const [datasetInfo, setDatasetInfo] = useState(null); const [columnTypes, setColumnTypes] = useState({}); @@ -436,14 +442,24 @@ export default function DatasetVisualization({ /> )} {tab === 1 && ( - + )} {tab === 2 && ( )} - {tab === 3 && } + {tab === 3 && ( + + )} {tab === 4 && ( { const [uploadDataloader, setUploadDataloader] = useState(null); const [datasetInfo, setDatasetInfo] = useState(null); const [datasetTab, setDatasetTab] = useState(0); + const [scrollToColumn, setScrollToColumn] = useState(null); useEffect(() => { fetchNotebooks(); @@ -102,6 +103,8 @@ export const DatasetsAndNotebooksProvider = ({ children }) => { setDatasetInfo, datasetTab, setDatasetTab, + scrollToColumn, + setScrollToColumn, uploadDataloader, setUploadDataloader, }; diff --git a/DashAI/front/src/components/notebooks/dataset/ColumnInsights.jsx b/DashAI/front/src/components/notebooks/dataset/ColumnInsights.jsx index a0c32fb15..915d337e6 100644 --- a/DashAI/front/src/components/notebooks/dataset/ColumnInsights.jsx +++ b/DashAI/front/src/components/notebooks/dataset/ColumnInsights.jsx @@ -23,6 +23,7 @@ export default function ColumnInsights({ const { t } = useTranslation(["datasets"]); const context = useDatasetsAndNotebooks(); const setDatasetTab = onNavigateTab ?? context?.setDatasetTab ?? (() => {}); + const setScrollToColumn = context?.setScrollToColumn ?? (() => {}); const insights = useMemo(() => { const items = []; @@ -78,24 +79,23 @@ export default function ColumnInsights({ const handleClick = useCallback( (insight) => { - setDatasetTab(insight.tab); - // Wait for the tab content to render, then scroll to the card - setTimeout(() => { - const card = document.querySelector( - `[data-column-card="${insight.column}"]`, - ); - if (card) { - card.scrollIntoView({ behavior: "smooth", block: "center" }); - // Brief highlight effect - card.style.transition = "box-shadow 0.3s"; - card.style.boxShadow = `0 0 0 2px ${theme.palette.warning.main}`; - setTimeout(() => { - card.style.boxShadow = ""; - }, 2000); - } - }, 100); + const card = document.querySelector( + `[data-column-card="${insight.column}"]`, + ); + if (card) { + setDatasetTab(insight.tab); + card.scrollIntoView({ behavior: "smooth", block: "center" }); + card.style.transition = "box-shadow 0.3s"; + card.style.boxShadow = `0 0 0 2px ${theme.palette.warning.main}`; + setTimeout(() => { + card.style.boxShadow = ""; + }, 2000); + } else { + setDatasetTab(insight.tab); + setScrollToColumn(insight.column); + } }, - [setDatasetTab, theme], + [setDatasetTab, setScrollToColumn, theme], ); const colorMap = { diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/CategoricalTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/CategoricalTab.jsx index 86eb89c88..dd0036c3e 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, { useState } from "react"; -import { Box, Typography, CardContent } from "@mui/material"; +import { Box, Typography, CardContent, Button } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import TitleIcon from "@mui/icons-material/Title"; import { @@ -18,14 +18,20 @@ import { StatBox } from "../StatBox"; import ExportableCard from "../ExportableCard"; import { useTranslation } from "react-i18next"; +const BATCH_SIZE = 10; + export const CategoricalTab = ({ categoricalStats }) => { const { t } = useTranslation(["datasets", "common"]); const theme = useTheme(); const [activeIndices, setActiveIndices] = useState({}); + const entries = Object.entries(categoricalStats ?? {}); + const [visibleCount, setVisibleCount] = useState(BATCH_SIZE); + const visibleEntries = entries.slice(0, visibleCount); + const remaining = entries.length - visibleCount; return ( - {Object.entries(categoricalStats).map(([column, stats]) => ( + {visibleEntries.map(([column, stats]) => ( { ))} + {remaining > 0 && ( + + + + )} ); }; diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/CorrelationsTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/CorrelationsTab.jsx index 49ae36372..8dfb9461d 100644 --- a/DashAI/front/src/components/notebooks/dataset/tabs/CorrelationsTab.jsx +++ b/DashAI/front/src/components/notebooks/dataset/tabs/CorrelationsTab.jsx @@ -10,7 +10,7 @@ const CorrelationsTab = ({ correlations }) => { const theme = useTheme(); const { columns, zValues, strongCorrelations, leftMargin } = useMemo(() => { - const cols = Object.keys(correlations); + const cols = Object.keys(correlations ?? {}); // Build symmetric matrix const z = cols.map((col1) => @@ -67,12 +67,18 @@ const CorrelationsTab = ({ correlations }) => { ], zmin: -1, zmax: 1, - text: zValues.map((row) => row.map((val) => val.toFixed(3))), - texttemplate: "%{text}", - textfont: { - color: theme.palette.text.primary, - size: 11, - }, + ...(columns.length <= 30 + ? { + text: zValues.map((row) => + row.map((val) => val.toFixed(3)), + ), + texttemplate: "%{text}", + textfont: { + color: theme.palette.text.primary, + size: 11, + }, + } + : {}), hovertemplate: "%{x} — %{y}
r = %{z:.3f}", showscale: true, colorbar: { diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/NumericTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/NumericTab.jsx index da25b899c..1e6709a58 100644 --- a/DashAI/front/src/components/notebooks/dataset/tabs/NumericTab.jsx +++ b/DashAI/front/src/components/notebooks/dataset/tabs/NumericTab.jsx @@ -1,5 +1,5 @@ -import React from "react"; -import { Box, Typography, CardContent, Alert } from "@mui/material"; +import React, { useState, useRef, useLayoutEffect } from "react"; +import { Box, Typography, CardContent, Alert, Button } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import TrendingUpIcon from "@mui/icons-material/TrendingUp"; import InfoIcon from "@mui/icons-material/Info"; @@ -9,9 +9,56 @@ import { MetricRow } from "../MetricRow"; import ExportableCard from "../ExportableCard"; import { Trans, useTranslation } from "react-i18next"; -export const NumericTab = ({ numericStats }) => { +const BATCH_SIZE = 10; + +export const NumericTab = ({ + numericStats, + scrollToColumn, + setScrollToColumn, +}) => { const { t } = useTranslation(["datasets"]); const theme = useTheme(); + const entries = Object.entries(numericStats ?? {}); + const [visibleCount, setVisibleCount] = useState(BATCH_SIZE); + const visibleEntries = entries.slice(0, visibleCount); + const remaining = entries.length - visibleCount; + const pendingScrollRef = useRef(null); + + useLayoutEffect(() => { + if (!scrollToColumn) { + if (!pendingScrollRef.current) return; + const col = pendingScrollRef.current; + const card = document.querySelector(`[data-column-card="${col}"]`); + if (!card) return; + pendingScrollRef.current = null; + card.scrollIntoView({ behavior: "smooth", block: "center" }); + card.style.transition = "box-shadow 0.3s"; + card.style.boxShadow = `0 0 0 2px ${theme.palette.warning.main}`; + setTimeout(() => { + card.style.boxShadow = ""; + }, 2000); + return; + } + const idx = entries.findIndex(([col]) => col === scrollToColumn); + if (idx === -1) return; + if (idx >= visibleCount) { + pendingScrollRef.current = scrollToColumn; + setVisibleCount(idx + 1); + } else { + const card = document.querySelector( + `[data-column-card="${scrollToColumn}"]`, + ); + if (card) { + card.scrollIntoView({ behavior: "smooth", block: "center" }); + card.style.transition = "box-shadow 0.3s"; + card.style.boxShadow = `0 0 0 2px ${theme.palette.warning.main}`; + setTimeout(() => { + card.style.boxShadow = ""; + }, 2000); + } + } + setScrollToColumn(null); + }, [scrollToColumn, visibleCount]); const toNumberOrNull = (value) => { if ( @@ -33,7 +80,7 @@ export const NumericTab = ({ numericStats }) => { return ( - {Object.entries(numericStats ?? {}).map(([column, stats]) => ( + {visibleEntries.map(([column, stats]) => ( { ))} + {remaining > 0 && ( + + + + )} ); }; diff --git a/DashAI/front/src/components/notebooks/dataset/tabs/TextTab.jsx b/DashAI/front/src/components/notebooks/dataset/tabs/TextTab.jsx index f4e2ad541..7919ea577 100644 --- a/DashAI/front/src/components/notebooks/dataset/tabs/TextTab.jsx +++ b/DashAI/front/src/components/notebooks/dataset/tabs/TextTab.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useRef, useLayoutEffect } from "react"; import { Box, Typography, @@ -6,6 +6,7 @@ import { Chip, Alert, Tooltip, + Button, } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import TextFieldsIcon from "@mui/icons-material/TextFields"; @@ -24,13 +25,57 @@ import { MetricRow } from "../MetricRow"; import ExportableCard from "../ExportableCard"; import { useTranslation } from "react-i18next"; -export const TextTab = ({ textStats }) => { +const BATCH_SIZE = 10; + +export const TextTab = ({ textStats, scrollToColumn, setScrollToColumn }) => { const theme = useTheme(); const { t } = useTranslation(["datasets", "common"]); const [activeIndices, setActiveIndices] = useState({}); + const entries = Object.entries(textStats ?? {}); + const [visibleCount, setVisibleCount] = useState(BATCH_SIZE); + const visibleEntries = entries.slice(0, visibleCount); + const remaining = entries.length - visibleCount; + const pendingScrollRef = useRef(null); + + useLayoutEffect(() => { + if (!scrollToColumn) { + if (!pendingScrollRef.current) return; + const col = pendingScrollRef.current; + const card = document.querySelector(`[data-column-card="${col}"]`); + if (!card) return; + pendingScrollRef.current = null; + card.scrollIntoView({ behavior: "smooth", block: "center" }); + card.style.transition = "box-shadow 0.3s"; + card.style.boxShadow = `0 0 0 2px ${theme.palette.warning.main}`; + setTimeout(() => { + card.style.boxShadow = ""; + }, 2000); + return; + } + const idx = entries.findIndex(([col]) => col === scrollToColumn); + if (idx === -1) return; + if (idx >= visibleCount) { + pendingScrollRef.current = scrollToColumn; + setVisibleCount(idx + 1); + } else { + const card = document.querySelector( + `[data-column-card="${scrollToColumn}"]`, + ); + if (card) { + card.scrollIntoView({ behavior: "smooth", block: "center" }); + card.style.transition = "box-shadow 0.3s"; + card.style.boxShadow = `0 0 0 2px ${theme.palette.warning.main}`; + setTimeout(() => { + card.style.boxShadow = ""; + }, 2000); + } + } + setScrollToColumn(null); + }, [scrollToColumn, visibleCount]); + return ( - {Object.entries(textStats).map(([column, stats]) => { + {visibleEntries.map(([column, stats]) => { const lengthData = [ { label: t("datasets:label.min"), @@ -249,6 +294,16 @@ export const TextTab = ({ textStats }) => { ); })} + {remaining > 0 && ( + + + + )} ); };