From 5073c01d96ffe4d3cf876b4b61ba4219ae6da13f Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Sat, 14 Dec 2024 14:39:07 -0700 Subject: [PATCH 01/56] ESLint Footer, Header, and Root in App --- packages/app/.eslintrc.js | 72 +++++ packages/app/src/components/Footer/index.tsx | 148 +++++----- packages/app/src/components/Header/index.tsx | 277 ++++++++++--------- packages/app/src/components/Root.tsx | 86 +++--- 4 files changed, 341 insertions(+), 242 deletions(-) create mode 100644 packages/app/.eslintrc.js diff --git a/packages/app/.eslintrc.js b/packages/app/.eslintrc.js new file mode 100644 index 00000000..04cf3aa1 --- /dev/null +++ b/packages/app/.eslintrc.js @@ -0,0 +1,72 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: [ + 'plugin:react/recommended', + 'plugin:import/recommended', + 'plugin:@typescript-eslint/recommended', + 'airbnb', + 'plugin:import/typescript', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 13, + sourceType: 'module', + }, + plugins: ['react', '@typescript-eslint'], + root: true, + rules: { + 'react/jsx-filename-extension': [ + 2, + { extensions: ['.js', '.jsx', '.ts', '.tsx'] }, + ], + 'import/prefer-default-export': 'off', + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: ['.storybook/**', '**/stories/**'], + }, + ], + 'react/function-component-definition': 'off', + 'no-plusplus': ['warn', { allowForLoopAfterthoughts: true }], + 'dot-notation': 'off', + 'import/extensions': [ + 'error', + 'ignorePackages', + { + js: 'never', + jsx: 'never', + ts: 'never', + tsx: 'never', + }, + ], + 'react/jsx-props-no-spreading': 'off', + 'react/require-default-props': 'off', + 'react/react-in-jsx-scope': 'off', + 'react/jsx-wrap-multilines': 'off', + 'react/no-unknown-property': 'off', + 'operator-linebreak': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', // or "error" + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-explicit-any': 'off', + 'max-len': 'off', + 'no-unused-vars': 'off', + 'no-param-reassign': 'off', + 'import/no-cycle': 'off', + 'no-underscore-dangle': 'off', + 'no-nested-ternary': 'off', + 'jsx-a11y/tabindex-no-positive': 'off', + 'no-bitwise': 'warn', + }, +}; diff --git a/packages/app/src/components/Footer/index.tsx b/packages/app/src/components/Footer/index.tsx index 26b2876b..c18cf2da 100644 --- a/packages/app/src/components/Footer/index.tsx +++ b/packages/app/src/components/Footer/index.tsx @@ -1,82 +1,84 @@ -import { AccessibilityNew, BugReport } from "@mui/icons-material"; -import { Box, Button, Link } from "@mui/material"; -import vdl_logo from "../../assets/vdl_logo.svg"; -import { accessibilityStatementAtom } from "../../atoms/accessibilityStatementAtom"; -import { useRecoilState } from "recoil"; -import { AccessibilityStatement } from "../AccessiblityStatement"; -import { About } from "../About"; -import { aboutAtom } from "../../atoms/aboutAtom"; +import { AccessibilityNew, BugReport } from '@mui/icons-material'; +import { Box, Button, Link } from '@mui/material'; +import { useRecoilState } from 'recoil'; +import vdl_logo from '../../assets/vdl_logo.svg'; +import { accessibilityStatementAtom } from '../../atoms/accessibilityStatementAtom'; +import { AccessibilityStatement } from '../AccessiblityStatement'; +import { About } from '../About'; +import { aboutAtom } from '../../atoms/aboutAtom'; const Footer = () => { + const categoryCSS = { + display: 'flex', + justifyContent: 'space-around', + alignItems: 'center', + }; - const categoryCSS = { - display: "flex", - justifyContent: "space-around", - alignItems: "center", - } + const [accessibilityStatement, setAccessibilityStatement] = useRecoilState(accessibilityStatementAtom); + const [aboutModal, setAboutModal] = useRecoilState(aboutAtom); - const [ accessibilityStatement, setAccessibilityStatement ] = useRecoilState(accessibilityStatementAtom); - const [ aboutModal, setAboutModal ] = useRecoilState(aboutAtom); + return ( + +
+ + + + + - return ( - -
- - - - - + - - - {/* Accessibility Statement dialog */} - setAccessibilityStatement(false)} /> - {/* About dialog */} - setAboutModal(false)} /> - -
+ {/* Accessibility Statement dialog */} + setAccessibilityStatement(false)} /> + {/* About dialog */} + setAboutModal(false)} />
- ) -} +
+
+ ); +}; export default Footer; diff --git a/packages/app/src/components/Header/index.tsx b/packages/app/src/components/Header/index.tsx index 1067f70a..2bb05cbe 100644 --- a/packages/app/src/components/Header/index.tsx +++ b/packages/app/src/components/Header/index.tsx @@ -6,19 +6,24 @@ import UndoIcon from '@mui/icons-material/Undo'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import { AccountCircle, ErrorOutline } from '@mui/icons-material'; -import { AppBar, Avatar, Box, Button, ButtonGroup, IconButton, Menu, MenuItem, Toolbar, Tooltip, Typography } from '@mui/material'; +import { + AppBar, Avatar, Box, Button, ButtonGroup, IconButton, Menu, MenuItem, Toolbar, Tooltip, Typography, +} from '@mui/material'; import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil'; -import React, { useContext, useEffect, useState } from 'react'; +import React, { + useCallback, useContext, useEffect, useState, +} from 'react'; import localforage from 'localforage'; -import { getQueryParam, queryParamAtom, saveQueryParam } from '../../atoms/queryParamAtom'; +import { Link } from 'react-router-dom'; +import { + getQueryParam, queryParamAtom, saveQueryParam, restoreQueryParam, +} from '../../atoms/queryParamAtom'; import { provenanceVisAtom } from '../../atoms/provenanceVisAtom'; import { elementSidebarAtom } from '../../atoms/elementSidebarAtom'; import { ProvenanceContext } from '../../App'; import { ImportModal } from '../ImportModal'; import { AttributeDropdown } from '../AttributeDropdown'; import { importErrorAtom } from '../../atoms/importErrorAtom'; -import { Link } from 'react-router-dom'; -import { restoreQueryParam } from '../../atoms/queryParamAtom'; import { altTextSidebarAtom } from '../../atoms/altTextSidebarAtom'; import { loadingAtom } from '../../atoms/loadingAtom'; import { getMultinetDataUrl } from '../../api/getMultinetDataUrl'; @@ -28,25 +33,25 @@ import { rowsSelector } from '../../atoms/selectors'; const Header = ({ data }: { data: any }) => { const { workspace } = useRecoilValue(queryParamAtom); - const [ isProvVisOpen, setIsProvVisOpen ] = useRecoilState(provenanceVisAtom); - const [ isElementSidebarOpen, setIsElementSidebarOpen ] = useRecoilState(elementSidebarAtom); - const [ isAltTextSidebarOpen, setIsAltTextSidebarOpen ] = useRecoilState(altTextSidebarAtom); + const [isProvVisOpen, setIsProvVisOpen] = useRecoilState(provenanceVisAtom); + const [isElementSidebarOpen, setIsElementSidebarOpen] = useRecoilState(elementSidebarAtom); + const [isAltTextSidebarOpen, setIsAltTextSidebarOpen] = useRecoilState(altTextSidebarAtom); const importError = useRecoilValue(importErrorAtom); const setLoading = useSetRecoilState(loadingAtom); - + const { provenance } = useContext(ProvenanceContext); const rows = useRecoilValue(rowsSelector); - - const [ attributeDialog, setAttributeDialog ] = useState(false); - const [ showImportModal, setShowImportModal ] = useState(false); - const [ isMenuOpen, setIsMenuOpen ] = useState(false); - const [ loginMenuOpen, setLoginMenuOpen ] = useState(false); + + const [attributeDialog, setAttributeDialog] = useState(false); + const [showImportModal, setShowImportModal] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [loginMenuOpen, setLoginMenuOpen] = useState(false); const [anchorEl, setAnchorEl] = useState(); - - const visibleSets = provenance.getState().visibleSets; + + const { visibleSets } = provenance.getState(); const hiddenSets = provenance.getState().allSets.filter((set: Column) => !visibleSets.includes(set.name)); - /** + /** * Number of keyboard tab indices in the alttext sidebar; used to calculate the tab index of the other buttons * because alttext sidebar should get priority when open. This should be updated if more tab indices are added. * "Tab indicies" refers to the number of tabIndex properties on elements in the sidebar @@ -56,7 +61,12 @@ const Header = ({ data }: { data: any }) => { const handleImportModalClose = () => { setShowImportModal(false); - } + }; + + const handleMenuOpen = (target: EventTarget) => { + setAnchorEl(target as HTMLElement); + setIsMenuOpen(true); + }; const handleMenuClick = (target: EventTarget) => { handleMenuOpen(target); @@ -68,11 +78,6 @@ const Header = ({ data }: { data: any }) => { } }; - const handleMenuOpen = (target: EventTarget) => { - setAnchorEl(target as HTMLElement); - setIsMenuOpen(true); - }; - const handleMenuClose = () => { setAnchorEl(null); setIsMenuOpen(false); @@ -81,12 +86,12 @@ const Header = ({ data }: { data: any }) => { const handleLoginOpen = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); setLoginMenuOpen(true); - } + }; const handleLoginClose = () => { setAnchorEl(null); setLoginMenuOpen(false); - } + }; const handleAttributeClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -108,64 +113,77 @@ const Header = ({ data }: { data: any }) => { if (isAltTextSidebarOpen) { setIsAltTextSidebarOpen(false); } - } + }; /** * Dispatches the state by saving relevant data to the local storage. * This function saves the 'data', 'rows', 'visibleSets', 'hiddenSets', and query parameters to the local storage. */ - async function dispatchState() { + const dispatchState = useCallback(async () => { setLoading(true); await Promise.all([ localforage.clear(), localforage.setItem('data', data), localforage.setItem('rows', getAccessibleData(rows, true)), localforage.setItem('visibleSets', visibleSets), - localforage.setItem('hiddenSets', hiddenSets.map((set: Column) => set.name)) + localforage.setItem('hiddenSets', hiddenSets.map((set: Column) => set.name)), ]); saveQueryParam(); setLoading(false); - }; + }, [data, rows, visibleSets, hiddenSets]); + + const [userInfo, setUserInfo] = useState(null); - const [ userInfo, setUserInfo ] = useState(null); - useEffect(() => { const fetchInfo = async () => { - const userInfo = await getUserInfo(); - setUserInfo(userInfo); - } + const retrievedInfo = await getUserInfo(); + setUserInfo(retrievedInfo); + }; fetchInfo(); - }, []) + }, []); - const [ trrackPosition, setTrrackPosition ] = useState({ + const [trrackPosition, setTrrackPosition] = useState({ isAtLatest: true, - isAtRoot: true + isAtRoot: true, }); useEffect(() => { provenance.currentChange(() => { setTrrackPosition({ isAtLatest: provenance.current.children.length === 0, - isAtRoot: provenance.current.id === provenance.root.id - }) - }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - + isAtRoot: provenance.current.id === provenance.root.id, + }); + }); + }, []); + return ( - - - + + + {/* TEMPORARY REMOVAL UNTIL CHI SUBMISSION */} {/* */} - + Upset - Visualizing Intersecting Sets @@ -177,49 +195,54 @@ const Header = ({ data }: { data: any }) => { - + {data !== null && - <> - - - - - } + } {attributeDialog && - - } + } {importError && - - - - } + + + } - - setShowImportModal(true) } color="inherit" aria-label="Import UpSet JSON state file"> - Import State - - exportState(provenance)} color="inherit" aria-label="UpSet JSON state file download"> - Export State - - exportState(provenance, data, rows)} aria-label="Download UpSet JSON state file with table data included"> - Export State + Data - - downloadSVG()} aria-label="SVG Download of this upset plot"> - Download SVG - - { - closeAnySidebar(); + + setShowImportModal(true)} color="inherit" aria-label="Import UpSet JSON state file"> + Import State + + exportState(provenance)} color="inherit" aria-label="UpSet JSON state file download"> + Export State + + exportState(provenance, data, rows)} aria-label="Download UpSet JSON state file with table data included"> + Export State + Data + + downloadSVG()} aria-label="SVG Download of this upset plot"> + Download SVG + + { + closeAnySidebar(); - setIsProvVisOpen(true); - handleMenuClose(); - }} - aria-label="History tree sidebar" - > - Show History - - + setIsProvVisOpen(true); + handleMenuClose(); + }} + aria-label="History tree sidebar" + > + Show History + + { handleLoginOpen(e); }} aria-label="Login menu" aria-haspopup="menu" > - + {userInfo !== null ? `${userInfo.first_name.charAt(0)}${userInfo.last_name.charAt(0)}` - : - } + : } { onClose={handleLoginClose} anchorEl={anchorEl} > - {userInfo === null ? - { - restoreQueryParam(); - oAuth.redirectToLogin(); - }} - >Login - : { - oAuth.logout(); - window.location.reload(); + restoreQueryParam(); + oAuth.redirectToLogin(); }} - >Log out - } + > + Login + + : { + oAuth.logout(); + window.location.reload(); + }} + > + Log out + } diff --git a/packages/app/src/components/Root.tsx b/packages/app/src/components/Root.tsx index 15b9155e..03315594 100644 --- a/packages/app/src/components/Root.tsx +++ b/packages/app/src/components/Root.tsx @@ -1,11 +1,13 @@ -import { UpsetActions, UpsetProvenance } from "@visdesignlab/upset2-react" -import { UpsetConfig } from "@visdesignlab/upset2-core" -import { Box, css } from "@mui/material" -import { Body } from "./Body" -import Header from "./Header" -import { useRef, useState, useEffect, useMemo } from "react" -import Footer from "./Footer" -import { Home } from "./Home" +import { UpsetActions, UpsetProvenance } from '@visdesignlab/upset2-react'; +import { UpsetConfig } from '@visdesignlab/upset2-core'; +import { Box, css } from '@mui/material'; +import { + useRef, useState, useEffect, useMemo, +} from 'react'; +import { Body } from './Body'; +import Header from './Header'; +import Footer from './Footer'; +import { Home } from './Home'; type Props = { provenance: UpsetProvenance, @@ -14,42 +16,42 @@ type Props = { config?: UpsetConfig } -export const Root = ({provenance, actions, data, config}: Props) => { - const headerDiv = useRef(null); - const [headerHeight, setHeaderHeight] = useState(-1); +export const Root = ({ + provenance, actions, data, config, +}: Props) => { + const headerDiv = useRef(null); + const [headerHeight, setHeaderHeight] = useState(-1); - const AppCss = useMemo(() => { - return css` - overflow: ${data === null ? "auto" : "hidden"}; + const AppCss = useMemo(() => css` + overflow: ${data === null ? 'auto' : 'hidden'}; height: 100vh; display: grid; grid-template-rows: min-content auto; - `; - }, [data]); + `, [data]); - useEffect(() => { - const { current } = headerDiv; - if (!current) return; - - if (headerHeight > 0) return; - - setHeaderHeight(current.clientHeight); - }, [headerHeight, headerDiv]); - - return ( -
- theme.zIndex.drawer + 1, - position: 'relative', - }} - ref={headerDiv} - > -
- - {data === null && } - -
-
- ) -} \ No newline at end of file + useEffect(() => { + const { current } = headerDiv; + if (!current) return; + + if (headerHeight > 0) return; + + setHeaderHeight(current.clientHeight); + }, [headerHeight, headerDiv]); + + return ( +
+ theme.zIndex.drawer + 1, + position: 'relative', + }} + ref={headerDiv} + > +
+ + {data === null && } + +
+
+ ); +}; From ffc28cb7e3e1493acd83846b1c912f7c05b37e23 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Sat, 14 Dec 2024 17:42:58 -0700 Subject: [PATCH 02/56] ESLint Body --- packages/app/src/components/Body.tsx | 56 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/packages/app/src/components/Body.tsx b/packages/app/src/components/Body.tsx index 11972041..f5838c82 100644 --- a/packages/app/src/components/Body.tsx +++ b/packages/app/src/components/Body.tsx @@ -1,16 +1,16 @@ import { AltText, Upset, getAltTextConfig } from '@visdesignlab/upset2-react'; import { UpsetConfig } from '@visdesignlab/upset2-core'; import { useRecoilValue, useRecoilState } from 'recoil'; +import { useCallback, useContext, useEffect, useState } from 'react'; +import { Backdrop, CircularProgress } from '@mui/material'; import { encodedDataAtom } from '../atoms/dataAtom'; import { doesHaveSavedQueryParam, queryParamAtom, saveQueryParam } from '../atoms/queryParamAtom'; import { ErrorModal } from './ErrorModal'; import { ProvenanceContext } from '../App'; -import { useContext, useEffect, useState } from 'react'; import { provenanceVisAtom } from '../atoms/provenanceVisAtom'; import { elementSidebarAtom } from '../atoms/elementSidebarAtom'; import { altTextSidebarAtom } from '../atoms/altTextSidebarAtom'; import { loadingAtom } from '../atoms/loadingAtom'; -import { Backdrop, CircularProgress } from '@mui/material'; import { updateMultinetSession } from '../api/session'; import { generateAltText } from '../api/generateAltText'; import { api } from '../api/api'; @@ -25,31 +25,31 @@ export const Body = ({ data, config }: Props) => { const { workspace, table, sessionId } = useRecoilValue(queryParamAtom); const provObject = useContext(ProvenanceContext); const encodedData = useRecoilValue(encodedDataAtom); - const [ isProvVisOpen, setIsProvVisOpen ] = useRecoilState(provenanceVisAtom); - const [ isElementSidebarOpen, setIsElementSidebarOpen ] = useRecoilState(elementSidebarAtom); - const [ isAltTextSidebarOpen, setIsAltTextSidebarOpen ] = useRecoilState(altTextSidebarAtom); + const [isProvVisOpen, setIsProvVisOpen] = useRecoilState(provenanceVisAtom); + const [isElementSidebarOpen, setIsElementSidebarOpen] = useRecoilState(elementSidebarAtom); + const [isAltTextSidebarOpen, setIsAltTextSidebarOpen] = useRecoilState(altTextSidebarAtom); const loading = useRecoilValue(loadingAtom); const rows = useRecoilValue(rowsSelector); const provVis = { open: isProvVisOpen, - close: () => { setIsProvVisOpen(false) } - } + close: () => { setIsProvVisOpen(false); }, + }; const elementSidebar = { open: isElementSidebarOpen, - close: () => { setIsElementSidebarOpen(false) } - } + close: () => { setIsElementSidebarOpen(false); }, + }; const altTextSidebar = { open: isAltTextSidebarOpen, - close: () => { setIsAltTextSidebarOpen(false) } - } + close: () => { setIsAltTextSidebarOpen(false); }, + }; useEffect(() => { provObject.provenance.currentChange(() => { updateMultinetSession(workspace || '', sessionId || '', provObject.provenance.exportObject()); - }) + }); }, [provObject.provenance, sessionId, workspace]); // Check if the user has permissions to edit the plot @@ -62,8 +62,7 @@ export const Body = ({ data, config }: Props) => { // https://api.multinet.app/swagger/?format=openapi#/definitions/PermissionsReturn for possible permissions returns setPermissions(r.permission_label === 'owner' || r.permission_label === 'maintainer'); } catch (e) { - setPermissions(false) - return; + setPermissions(false); } }; @@ -78,33 +77,33 @@ export const Body = ({ data, config }: Props) => { * @throws Error with descriptive message if an error occurs while generating the alttxt * @returns A promise that resolves to the generated alt text. */ - async function getAltText(): Promise { + const getAltText: () => Promise = useCallback(async () => { const state = provObject.provenance.getState(); // Rows must be cloned to avoid a recoil error triggered far down in this call chain when a function writes rows - const config = getAltTextConfig(state, data, structuredClone(rows)); + const ATConfig = getAltTextConfig(state, data, structuredClone(rows)); - if (config.firstAggregateBy !== "None") { + if (ATConfig.firstAggregateBy !== 'None') { throw new Error("Alt text generation is not yet supported for aggregated plots. To generate an alt text, set aggregation to 'None' in the left sidebar."); } - if (!['Size', 'Degree', 'Deviation'].includes(config.sortBy)) { - throw new Error(`Alt text generation is not yet supported for ${config.sortBy.includes("Set_") ? 'set' : 'attribute'} sorting. To generate an alt text, sort by Size, Degree, or Deviation.`); + if (!['Size', 'Degree', 'Deviation'].includes(ATConfig.sortBy)) { + throw new Error(`Alt text generation is not yet supported for ${ATConfig.sortBy.includes('Set_') ? 'set' : 'attribute'} sorting. To generate an alt text, sort by Size, Degree, or Deviation.`); } let response; try { - response = await generateAltText(config); + response = await generateAltText(ATConfig); } catch (e: any) { if (e.response.status === 500) { - throw Error("Server error while generating alt text. Please try again later. If the issue persists, please contact an UpSet developer at vdl-faculty@sci.utah.edu."); + throw Error('Server error while generating alt text. Please try again later. If the issue persists, please contact an UpSet developer at vdl-faculty@sci.utah.edu.'); } else if (e.response.status === 400) { - throw Error("Error generating alt text. Contact an upset developer at vdl-faculty@sci.utah.edu."); + throw Error('Error generating alt text. Contact an upset developer at vdl-faculty@sci.utah.edu.'); } else { - throw Error("Unknown error while generating alt text: " + e.response.statusText + ". Please contact an UpSet developer at vdl-faculty@sci.utah.edu."); + throw Error(`Unknown error while generating alt text: ${e.response.statusText}. Please contact an UpSet developer at vdl-faculty@sci.utah.edu.`); } } return response.alttxt; - } + }, [provObject.provenance, data, rows]); if (data === null) return null; @@ -118,11 +117,11 @@ export const Body = ({ data, config }: Props) => { } return ( -
+
{ data.setColumns.length === 0 ? - : + :
- + { visualizeUpsetAttributes allowAttributeRemoval /> -
- } +
}
); }; From 0b4fd4c8f3a46ea883c077c1d76692b106cc755f Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Sun, 15 Dec 2024 19:33:40 -0700 Subject: [PATCH 03/56] Remove useless useEffect --- packages/app/src/components/Root.tsx | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/app/src/components/Root.tsx b/packages/app/src/components/Root.tsx index 03315594..db3426fb 100644 --- a/packages/app/src/components/Root.tsx +++ b/packages/app/src/components/Root.tsx @@ -2,7 +2,7 @@ import { UpsetActions, UpsetProvenance } from '@visdesignlab/upset2-react'; import { UpsetConfig } from '@visdesignlab/upset2-core'; import { Box, css } from '@mui/material'; import { - useRef, useState, useEffect, useMemo, + useMemo, } from 'react'; import { Body } from './Body'; import Header from './Header'; @@ -19,9 +19,6 @@ type Props = { export const Root = ({ provenance, actions, data, config, }: Props) => { - const headerDiv = useRef(null); - const [headerHeight, setHeaderHeight] = useState(-1); - const AppCss = useMemo(() => css` overflow: ${data === null ? 'auto' : 'hidden'}; height: 100vh; @@ -29,15 +26,6 @@ export const Root = ({ grid-template-rows: min-content auto; `, [data]); - useEffect(() => { - const { current } = headerDiv; - if (!current) return; - - if (headerHeight > 0) return; - - setHeaderHeight(current.clientHeight); - }, [headerHeight, headerDiv]); - return (
theme.zIndex.drawer + 1, position: 'relative', }} - ref={headerDiv} >
From 729d1cb36145c31e4928a28c2e88727366c1809c Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Sun, 15 Dec 2024 19:49:45 -0700 Subject: [PATCH 04/56] Unify sidebar into 1 element with shadow; fix title appearance --- packages/upset/src/components/Sidebar.tsx | 30 +++++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/upset/src/components/Sidebar.tsx b/packages/upset/src/components/Sidebar.tsx index dec7eecb..b9b8c300 100644 --- a/packages/upset/src/components/Sidebar.tsx +++ b/packages/upset/src/components/Sidebar.tsx @@ -20,6 +20,7 @@ import { AggregateBy, aggregateByList, } from '@visdesignlab/upset2-core'; import { + CSSProperties, Fragment, useContext, useEffect, useState, } from 'react'; import { useRecoilValue } from 'recoil'; @@ -80,16 +81,28 @@ export const Sidebar = () => { } }, [firstAggregateBy]); + /** Styles for the 3 accordions in the sidebar */ + const ACCORDION_CSS: CSSProperties = { + boxShadow: 'none', + }; + return ( -
- + Settings - + }> Aggregation @@ -152,6 +165,7 @@ export const Sidebar = () => { }} disableGutters disabled={firstAggregateBy === 'None'} + style={ACCORDION_CSS} > }> Second Aggregation @@ -209,7 +223,7 @@ export const Sidebar = () => { - + }> Filter Intersections @@ -306,6 +320,6 @@ export const Sidebar = () => { -
+ ); }; From 7fc691837ba01d881accbb892e850cf5eea8e443 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Sun, 15 Dec 2024 21:23:49 -0700 Subject: [PATCH 05/56] Add footerHeight param to Upset component & prevent settings sidebar overlap --- README.md | 1 + packages/app/src/components/Body.tsx | 6 +- packages/app/src/components/Footer/index.tsx | 6 +- packages/app/src/components/Root.tsx | 3 + packages/upset/src/atoms/dimensionsAtom.ts | 13 +- packages/upset/src/components/Root.tsx | 9 +- packages/upset/src/components/Sidebar.tsx | 400 ++++++++++--------- packages/upset/src/components/Upset.tsx | 3 + packages/upset/src/types.ts | 6 + 9 files changed, 245 insertions(+), 202 deletions(-) diff --git a/README.md b/README.md index 798025cd..7f6ee0c2 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,7 @@ const main = () => { - `provVis` (optional): [Sidebar options](#sidebar-options) for the provenance visualization sidebar. See [Trrack-Vis](https://github.com/Trrack/trrackvis) for more information about Trrack provenance visualization. - `elementSidebar` (optional): [Sidebar options](#sidebar-options) for the element visualization sidebar. This sidebar is used for element queries, element selection datatable, and supplimental plot generation. - `altTextSidebar` (optional): [Sidebar options](#sidebar-options) for the text description sidebar. This sidebar is used to display the generated text descriptions for an Upset 2.0 plot, given that the `generateAltText` function is provided. +- `footerHeight` (optional)(`number`): Height of the footer overlayed on the upset plot, in px, if one exists. Used to prevent the bottom of the sidebars from overlapping with the footer. - `generateAltText` (optional)(`() => Promise`): Async function which should return a generated AltText object. See [Alt Text Generation](#alt-text-generation) for more information about Alt Text generation. ##### Configuration (Grammar) options diff --git a/packages/app/src/components/Body.tsx b/packages/app/src/components/Body.tsx index f5838c82..326e6563 100644 --- a/packages/app/src/components/Body.tsx +++ b/packages/app/src/components/Body.tsx @@ -1,7 +1,9 @@ import { AltText, Upset, getAltTextConfig } from '@visdesignlab/upset2-react'; import { UpsetConfig } from '@visdesignlab/upset2-core'; import { useRecoilValue, useRecoilState } from 'recoil'; -import { useCallback, useContext, useEffect, useState } from 'react'; +import { + useCallback, useContext, useEffect, useState, +} from 'react'; import { Backdrop, CircularProgress } from '@mui/material'; import { encodedDataAtom } from '../atoms/dataAtom'; import { doesHaveSavedQueryParam, queryParamAtom, saveQueryParam } from '../atoms/queryParamAtom'; @@ -15,6 +17,7 @@ import { updateMultinetSession } from '../api/session'; import { generateAltText } from '../api/generateAltText'; import { api } from '../api/api'; import { rowsSelector } from '../atoms/selectors'; +import { FOOTER_HEIGHT } from './Root'; type Props = { data: any; @@ -132,6 +135,7 @@ export const Body = ({ data, config }: Props) => { provVis={provVis} elementSidebar={elementSidebar} altTextSidebar={altTextSidebar} + footerHeight={FOOTER_HEIGHT} generateAltText={getAltText} visualizeUpsetAttributes allowAttributeRemoval diff --git a/packages/app/src/components/Footer/index.tsx b/packages/app/src/components/Footer/index.tsx index c18cf2da..0be8f263 100644 --- a/packages/app/src/components/Footer/index.tsx +++ b/packages/app/src/components/Footer/index.tsx @@ -6,6 +6,7 @@ import { accessibilityStatementAtom } from '../../atoms/accessibilityStatementAt import { AccessibilityStatement } from '../AccessiblityStatement'; import { About } from '../About'; import { aboutAtom } from '../../atoms/aboutAtom'; +import { FOOTER_HEIGHT } from '../Root'; const Footer = () => { const categoryCSS = { @@ -18,7 +19,10 @@ const Footer = () => { const [aboutModal, setAboutModal] = useRecoilState(aboutAtom); return ( - +
{ diff --git a/packages/upset/src/atoms/dimensionsAtom.ts b/packages/upset/src/atoms/dimensionsAtom.ts index 92c537ba..0810ab1c 100644 --- a/packages/upset/src/atoms/dimensionsAtom.ts +++ b/packages/upset/src/atoms/dimensionsAtom.ts @@ -1,4 +1,4 @@ -import { selector } from 'recoil'; +import { atom, selector } from 'recoil'; import { calculateDimensions } from '../dimensions'; import { visibleAttributesSelector } from './config/visibleAttributes'; import { hiddenSetSelector, visibleSetSelector } from './config/visibleSetsAtoms'; @@ -22,3 +22,14 @@ ReturnType ); }, }); + +/** + * The spacing height necessary to prevent Upset sidebars from overlapping the footer. + * This is some multiple of the footer height provided to the Upset component; + * I don't know why it has to be multiplied but it does. + * @default 'auto' + */ +export const footerHeightAtom = atom({ + key: 'footerHeight', + default: 'auto', +}); diff --git a/packages/upset/src/components/Root.tsx b/packages/upset/src/components/Root.tsx index 23a5d719..79ff0e95 100644 --- a/packages/upset/src/components/Root.tsx +++ b/packages/upset/src/components/Root.tsx @@ -27,6 +27,7 @@ import { ContextMenu } from './ContextMenu'; import { ProvenanceVis } from './ProvenanceVis'; import { AltTextSidebar } from './AltTextSidebar'; import { AltText } from '../types'; +import { footerHeightAtom } from '../atoms/dimensionsAtom'; export const ProvenanceContext = createContext<{ provenance: UpsetProvenance; @@ -59,11 +60,12 @@ type Props = { open: boolean; close: () => void; }; + footerHeight?: number; generateAltText?: () => Promise; }; export const Root: FC = ({ - data, config, allowAttributeRemoval, hideSettings, canEditPlotInformation, extProvenance, provVis, elementSidebar, altTextSidebar, generateAltText, + data, config, allowAttributeRemoval, hideSettings, canEditPlotInformation, extProvenance, provVis, elementSidebar, altTextSidebar, footerHeight, generateAltText, }) => { // Get setter for recoil config atom const setState = useSetRecoilState(upsetConfigAtom); @@ -134,6 +136,11 @@ export const Root: FC = ({ }; }, []); + // Sets the footer height atom if provided as an argument + const setFooterHeight = useSetRecoilState(footerHeightAtom); + // Footer height needs to be doubled to work right... idk why that is! + useEffect(() => { if (footerHeight) setFooterHeight(`${2 * footerHeight}px`); }, [footerHeight]); + if (Object.keys(sets).length === 0 || Object.keys(items).length === 0) return null; return ( diff --git a/packages/upset/src/components/Sidebar.tsx b/packages/upset/src/components/Sidebar.tsx index b9b8c300..3b41192c 100644 --- a/packages/upset/src/components/Sidebar.tsx +++ b/packages/upset/src/components/Sidebar.tsx @@ -38,7 +38,7 @@ import { visibleSetSelector } from '../atoms/config/visibleSetsAtoms'; import { ProvenanceContext } from './Root'; import { HelpCircle, defaultMargin } from './custom/HelpCircle'; import { helpText } from '../utils/helpText'; -import { dimensionsSelector } from '../atoms/dimensionsAtom'; +import { dimensionsSelector, footerHeightAtom } from '../atoms/dimensionsAtom'; const itemDivCSS = css` display: flex; @@ -66,6 +66,7 @@ export const Sidebar = () => { const hideEmpty = useRecoilValue(hideEmptySelector); const hideNoSet = useRecoilValue(hideNoSetSelector); const dimensions = useRecoilValue(dimensionsSelector); + const footerHeight = useRecoilValue(footerHeightAtom); const [secondaryAccordionOpen, setSecondaryAccordionOpen] = useState( secondAggregateBy !== 'None', @@ -87,55 +88,56 @@ export const Sidebar = () => { }; return ( - - + - Settings - - - }> - Aggregation - - - - { - const newAggBy: AggregateBy = ev.target.value as AggregateBy; - actions.firstAggregateBy(newAggBy); - }} - > - {aggregateByList.map((agg) => ( - - { - if (e.key === 'Enter') { - actions.firstAggregateBy(agg); - } - }} - > - + Settings + + + }> + Aggregation + + + + { + const newAggBy: AggregateBy = ev.target.value as AggregateBy; + actions.firstAggregateBy(newAggBy); + }} + > + {aggregateByList.map((agg) => ( + + } - /> - {agg !== 'None' && } - - {agg === 'Overlaps' && firstAggregateBy === agg && ( + aria-label={agg !== 'None' ? helpText.aggregation[agg] : undefined} + onKeyDown={(e) => { + if (e.key === 'Enter') { + actions.firstAggregateBy(agg); + } + }} + > + } + /> + {agg !== 'None' && } + + {agg === 'Overlaps' && firstAggregateBy === agg && ( { actions.firstOverlapBy(val); }} /> - )} - - ))} - - - - - { - setSecondaryAccordionOpen(!secondaryAccordionOpen); - }} - disableGutters - disabled={firstAggregateBy === 'None'} - style={ACCORDION_CSS} - > - }> - Second Aggregation - - - - { - const newAggBy: AggregateBy = ev.target.value as AggregateBy; - actions.secondAggregateBy(newAggBy); - }} - > - {aggregateByList - .filter((agg) => agg !== firstAggregateBy) - .map((agg) => ( - - { - if (e.key === 'Enter') { - actions.secondAggregateBy(agg); - } - }} - > - } - /> - {agg !== 'None' && } - - {agg === 'Overlaps' && secondAggregateBy === agg && ( + )} + + ))} + + + + + { + setSecondaryAccordionOpen(!secondaryAccordionOpen); + }} + disableGutters + disabled={firstAggregateBy === 'None'} + style={ACCORDION_CSS} + > + }> + Second Aggregation + + + + { + const newAggBy: AggregateBy = ev.target.value as AggregateBy; + actions.secondAggregateBy(newAggBy); + }} + > + {aggregateByList + .filter((agg) => agg !== firstAggregateBy) + .map((agg) => ( + + { + if (e.key === 'Enter') { + actions.secondAggregateBy(agg); + } + }} + > + } + /> + {agg !== 'None' && } + + {agg === 'Overlaps' && secondAggregateBy === agg && ( { actions.secondOverlapBy(val); }} /> - )} - - ))} - - - - - - }> - Filter Intersections - - - - { - if (e.key === 'Enter') { - actions.setHideEmpty(!hideEmpty); - } - }} - > - { - actions.setHideEmpty(ev.target.checked); - }} - /> - } - labelPlacement="start" - /> - - - { - if (e.key === 'Enter') { - actions.setHideNoSet(!hideNoSet); - } - }} - > - { - actions.setHideNoSet(ev.target.checked); - }} - /> - } - labelPlacement="start" - /> - - - - - - - Filter by Degree - - - - - { - if (typeof newVal === 'number') { // if the sliders are set to the same value - setDegreeFilters([newVal, newVal]); - } else { - setDegreeFilters(newVal); + )} + + ))} + + + + + + }> + Filter Intersections + + + + { + if (e.key === 'Enter') { + actions.setHideEmpty(!hideEmpty); } }} - onChangeCommitted={() => { - // Prevents unncessary Trrack state changes - if (prevFilters[0] !== degreeFilters[0]) { - actions.setMinVisible(degreeFilters[0]); - } - if (prevFilters[1] !== degreeFilters[1]) { - actions.setMaxVisible(degreeFilters[1]); + > + { + actions.setHideEmpty(ev.target.checked); + }} + /> + } + labelPlacement="start" + /> + + + { + if (e.key === 'Enter') { + actions.setHideNoSet(!hideNoSet); } - setPrevFilters(degreeFilters); }} - /> - - - - + > + { + actions.setHideNoSet(ev.target.checked); + }} + /> + } + labelPlacement="start" + /> + + + + + + + Filter by Degree + + + + + { + if (typeof newVal === 'number') { // if the sliders are set to the same value + setDegreeFilters([newVal, newVal]); + } else { + setDegreeFilters(newVal); + } + }} + onChangeCommitted={() => { + // Prevents unncessary Trrack state changes + if (prevFilters[0] !== degreeFilters[0]) { + actions.setMinVisible(degreeFilters[0]); + } + if (prevFilters[1] !== degreeFilters[1]) { + actions.setMaxVisible(degreeFilters[1]); + } + setPrevFilters(degreeFilters); + }} + /> + + + + + + ); }; diff --git a/packages/upset/src/components/Upset.tsx b/packages/upset/src/components/Upset.tsx index e783ee78..6cbf8913 100644 --- a/packages/upset/src/components/Upset.tsx +++ b/packages/upset/src/components/Upset.tsx @@ -25,6 +25,7 @@ const defaultVisibleSets = 6; * @param {SidebarProps} [provVis] - The provenance visualization sidebar options. * @param {SidebarProps} [elementSidebar] - The element sidebar options. This sidebar is used for element queries, element selection datatable, and supplimental plot generation. * @param {SidebarProps} [altTextSidebar] - The alternative text sidebar options. This sidebar is used to display the generated text descriptions for an Upset 2.0 plot, given that the `generateAltText` function is provided. + * @param {number} [footerHeight] - Height of the footer overlayed on the upset plot, in px, if one exists. Used to prevent the bottom of the sidebars from overlapping with the footer. * @param {() => Promise} [generateAltText] - The function to generate alternative text. * @returns {JSX.Element} The rendered Upset component. */ @@ -41,6 +42,7 @@ export const Upset: FC = ({ provVis, elementSidebar, altTextSidebar, + footerHeight, generateAltText, }) => { // If the provided data is not already processed by UpSet core, process it @@ -115,6 +117,7 @@ export const Upset: FC = ({ provVis={provVis} elementSidebar={elementSidebar} altTextSidebar={altTextSidebar} + footerHeight={footerHeight} generateAltText={generateAltText} /> diff --git a/packages/upset/src/types.ts b/packages/upset/src/types.ts index 0424ffa3..30f17c92 100644 --- a/packages/upset/src/types.ts +++ b/packages/upset/src/types.ts @@ -173,6 +173,12 @@ export interface UpsetProps { */ altTextSidebar?: SidebarProps; + /** + * Height of the footer overlayed on the upset plot, in px, if one exists. + * Used to prevent the bottom of the sidebars from overlapping with the footer. + */ + footerHeight?: number; + /** * Async function which should return a generated AltText object. */ From 3b0d7471ccd7dbaa40f70265efb15f77d4efbf8c Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Mon, 16 Dec 2024 11:53:28 -0700 Subject: [PATCH 06/56] Create generic sidebar component --- .../{Sidebar.tsx => SettingsSidebar.tsx} | 4 +- .../upset/src/components/custom/Sidebar.tsx | 136 ++++++++++++++++++ 2 files changed, 138 insertions(+), 2 deletions(-) rename packages/upset/src/components/{Sidebar.tsx => SettingsSidebar.tsx} (99%) create mode 100644 packages/upset/src/components/custom/Sidebar.tsx diff --git a/packages/upset/src/components/Sidebar.tsx b/packages/upset/src/components/SettingsSidebar.tsx similarity index 99% rename from packages/upset/src/components/Sidebar.tsx rename to packages/upset/src/components/SettingsSidebar.tsx index 3b41192c..2ee9bb4c 100644 --- a/packages/upset/src/components/Sidebar.tsx +++ b/packages/upset/src/components/SettingsSidebar.tsx @@ -51,7 +51,7 @@ const sidebarHeaderCSS = css` `; /** @jsxImportSource @emotion/react */ -export const Sidebar = () => { +export const SettingsSidebar = () => { const { actions } = useContext( ProvenanceContext, ); @@ -323,7 +323,7 @@ export const Sidebar = () => { - + ); }; diff --git a/packages/upset/src/components/custom/Sidebar.tsx b/packages/upset/src/components/custom/Sidebar.tsx new file mode 100644 index 00000000..e08cc9a2 --- /dev/null +++ b/packages/upset/src/components/custom/Sidebar.tsx @@ -0,0 +1,136 @@ +import OpenInFullIcon from '@mui/icons-material/OpenInFull'; +import CloseFullscreen from '@mui/icons-material/CloseFullscreen'; +import CloseIcon from '@mui/icons-material/Close'; +import { Box, Drawer, IconButton } from '@mui/material'; +import React, { + FC, PropsWithChildren, useCallback, useState, +} from 'react'; +import { useRecoilValue } from 'recoil'; +import { footerHeightAtom } from '../../atoms/dimensionsAtom'; + +type Props= { + open: boolean; + close: () => void; +} + +/** + * A collapsible, right-sidebar for the plot + */ +export const Sidebar: FC> = ({ open, close, children }) => { + /** Chosen so we don't get a horizontal scrollbar in the element view table */ + const INITIAL_DRAWER_WIDTH = 462; + /** Chosen so we don't get a new line for the "Apply" button in the element query controls */ + const MIN_DRAWER_WIDTH = 368; + + const [fullWidth, setFullWidth] = useState(false); + const [drawerWidth, setDrawerWidth] = useState(INITIAL_DRAWER_WIDTH); + const footerHeight = useRecoilValue(footerHeightAtom); + + /** + * Callbacks + */ + + /** + * Only fires when the user drags the side of the sidebar; resizes the sidebar + */ + const handleMouseMove = useCallback((e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + const newWidth = document.body.clientWidth - e.clientX; + + if (newWidth > MIN_DRAWER_WIDTH) { + setDrawerWidth(newWidth); + } + }, [document.body.clientWidth]); + + /** + * Unattaches itself and handleMouseMove from document when the user stops dragging the sidebar + */ + const handleMouseUp = useCallback(() => { + document.removeEventListener('mouseup', handleMouseUp, true); + document.removeEventListener('mousemove', handleMouseMove, true); + }, [handleMouseMove, document]); + + /** + * Enables dragging when the user clicks the side of the drawer + */ + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + document.addEventListener('mouseup', handleMouseUp, true); + document.addEventListener('mousemove', handleMouseMove, true); + }, + [handleMouseUp, handleMouseMove, document], + ); + + return ( + + handleMouseDown(e)} + /> +
+ { !fullWidth ? + { + setFullWidth(true); + }} + aria-label="Expand the sidebar in full screen" + > + + + : + { + if (fullWidth) { + setFullWidth(false); + } + }} + aria-label="Reduce the sidebar to normal size" + > + + } + { + close(); + }} + aria-label="Close the sidebar" + > + + +
+ {children} + +
+ ); +}; From e73164043e967bb8b5cfe568ec6b7a070036eec3 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Mon, 16 Dec 2024 12:26:31 -0700 Subject: [PATCH 07/56] Apply generic sidebar to AltTextSidebar --- .../upset/src/components/AltTextSidebar.tsx | 258 ++++++++---------- packages/upset/src/components/Root.tsx | 4 +- 2 files changed, 111 insertions(+), 151 deletions(-) diff --git a/packages/upset/src/components/AltTextSidebar.tsx b/packages/upset/src/components/AltTextSidebar.tsx index 83dca27c..a64436d8 100644 --- a/packages/upset/src/components/AltTextSidebar.tsx +++ b/packages/upset/src/components/AltTextSidebar.tsx @@ -2,14 +2,12 @@ import { Box, Button, Divider, - Drawer, Icon, TextField, Typography, css, } from '@mui/material'; import EditIcon from '@mui/icons-material/Edit'; -import CloseIcon from '@mui/icons-material/Close'; import { useState, useEffect, FC, useContext, useMemo, @@ -25,6 +23,7 @@ import { PlotInformation } from './custom/PlotInformation'; import { UpsetActions } from '../provenance'; import { plotInformationSelector } from '../atoms/config/plotInformationAtom'; import { canEditPlotInformationAtom } from '../atoms/config/canEditPlotInformationAtom'; +import { Sidebar } from './custom/Sidebar'; /** * Props for the AltTextSidebar component. @@ -47,8 +46,6 @@ type Props = { generateAltText: () => Promise; } -const initialDrawerWidth = 450; - /** * Displays a sidebar for generating alternative text * and editing the alt text, caption, title, and plot information. @@ -149,11 +146,12 @@ export const AltTextSidebar: FC = ({ open, close, generateAltText }) => { }, [useLong, userLongText, userShortText, altText?.shortDescription, altText?.longDescription]); const divider = ; @@ -166,48 +164,12 @@ export const AltTextSidebar: FC = ({ open, close, generateAltText }) => { ); return ( - -
-
- - - {displayPlotInfo ? plotInfo.title ?? 'Editing Plot Information' : 'Text Description'} - - - - {divider} - {displayPlotInfo && + + + {displayPlotInfo ? plotInfo.title ?? 'Editing Plot Information' : 'Text Description'} + + {divider} + {displayPlotInfo && // We only want to display plotInfo if the user is editing OR if they've entered some field other than title (plotInfoEditing || Object.entries(plotInfo).filter(([k, _]) => k !== 'title').some(([_, v]) => !!v)) ? ( = ({ open, close, generateAltText }) => { editing={plotInfoEditing} setEditing={setPlotInfoEditing} /> - ) : ( - // only show "Add Plot Information" if the user has edit permissions - canEditPlotInformation ? ( + ) : ( + // only show "Add Plot Information" if the user has edit permissions + canEditPlotInformation ? ( + + ) : null + )} + {displayPlotInfo && ( + <> + + Text Description + + {divider} + + )} + + {textGenErr && !userLongText && !userShortText ? ( + {textGenErr} + ) : ( + textEditing ? ( + <> - ) : null - )} - {displayPlotInfo && ( - <> - - Text Description - - {divider} - - )} - - {textGenErr && !userLongText && !userShortText ? ( - {textGenErr} + +
+ (useLong ? setUserLongText(e.target.value) : setUserShortText(e.target.value))} + value={(displayAltText)} + tabIndex={6} + aria-flowto="saveAltTextButton" + /> +
+ ) : ( - textEditing ? ( - <> - - -
- (useLong ? setUserLongText(e.target.value) : setUserShortText(e.target.value))} - value={(displayAltText)} - tabIndex={6} - aria-flowto="saveAltTextButton" - /> -
- - ) : ( - + {canEditPlotInformation && ( + // Only show the edit button if the user has edit permissions + - )} - - - ) - )} - -
-
-
+ + + + + )} + +
+ ) + )} + + + ); }; diff --git a/packages/upset/src/components/Root.tsx b/packages/upset/src/components/Root.tsx index 79ff0e95..91aa2a67 100644 --- a/packages/upset/src/components/Root.tsx +++ b/packages/upset/src/components/Root.tsx @@ -21,7 +21,7 @@ import { import { Body } from './Body'; import { ElementSidebar } from './ElementView/ElementSidebar'; import { Header } from './Header/Header'; -import { Sidebar } from './Sidebar'; +import { SettingsSidebar } from './SettingsSidebar'; import { SvgBase } from './SvgBase'; import { ContextMenu } from './ContextMenu'; import { ProvenanceVis } from './ProvenanceVis'; @@ -154,7 +154,7 @@ export const Root: FC = ({ ${baseStyle}; `} > - +
}

Date: Mon, 16 Dec 2024 12:33:00 -0700 Subject: [PATCH 08/56] Pass tab index for close button through --- packages/upset/src/components/AltTextSidebar.tsx | 2 +- packages/upset/src/components/custom/Sidebar.tsx | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/upset/src/components/AltTextSidebar.tsx b/packages/upset/src/components/AltTextSidebar.tsx index a64436d8..769bb4fe 100644 --- a/packages/upset/src/components/AltTextSidebar.tsx +++ b/packages/upset/src/components/AltTextSidebar.tsx @@ -164,7 +164,7 @@ export const AltTextSidebar: FC = ({ open, close, generateAltText }) => { ); return ( - + {displayPlotInfo ? plotInfo.title ?? 'Editing Plot Information' : 'Text Description'} diff --git a/packages/upset/src/components/custom/Sidebar.tsx b/packages/upset/src/components/custom/Sidebar.tsx index e08cc9a2..ba2d0c28 100644 --- a/packages/upset/src/components/custom/Sidebar.tsx +++ b/packages/upset/src/components/custom/Sidebar.tsx @@ -9,14 +9,20 @@ import { useRecoilValue } from 'recoil'; import { footerHeightAtom } from '../../atoms/dimensionsAtom'; type Props= { + /** Whether the sidebar is open */ open: boolean; + /** Function to close the sidebar */ close: () => void; + /** Tab index for the close button */ + closeButtonTabIndex?: number; } /** * A collapsible, right-sidebar for the plot */ -export const Sidebar: FC> = ({ open, close, children }) => { +export const Sidebar: FC> = ({ + open, close, closeButtonTabIndex, children, +}) => { /** Chosen so we don't get a horizontal scrollbar in the element view table */ const INITIAL_DRAWER_WIDTH = 462; /** Chosen so we don't get a new line for the "Apply" button in the element query controls */ @@ -124,6 +130,7 @@ export const Sidebar: FC> = ({ open, close, children }) onClick={() => { close(); }} + tabIndex={closeButtonTabIndex} aria-label="Close the sidebar" > From f90aa974285bc356c4a6653391416f31d70c1677 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Mon, 16 Dec 2024 12:40:11 -0700 Subject: [PATCH 09/56] Bugfix for empty div remaining after closing sidebar --- packages/upset/src/components/custom/Sidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/upset/src/components/custom/Sidebar.tsx b/packages/upset/src/components/custom/Sidebar.tsx index ba2d0c28..80ba92aa 100644 --- a/packages/upset/src/components/custom/Sidebar.tsx +++ b/packages/upset/src/components/custom/Sidebar.tsx @@ -74,12 +74,12 @@ export const Sidebar: FC> = ({ Date: Mon, 16 Dec 2024 12:40:49 -0700 Subject: [PATCH 10/56] Use generic sidebar for Element Sidebar --- .../components/ElementView/ElementSidebar.tsx | 139 +----------------- 1 file changed, 4 insertions(+), 135 deletions(-) diff --git a/packages/upset/src/components/ElementView/ElementSidebar.tsx b/packages/upset/src/components/ElementView/ElementSidebar.tsx index 4df141a9..ee698012 100644 --- a/packages/upset/src/components/ElementView/ElementSidebar.tsx +++ b/packages/upset/src/components/ElementView/ElementSidebar.tsx @@ -1,14 +1,8 @@ -import OpenInFullIcon from '@mui/icons-material/OpenInFull'; -import CloseFullscreen from '@mui/icons-material/CloseFullscreen'; import DownloadIcon from '@mui/icons-material/Download'; -import CloseIcon from '@mui/icons-material/Close'; import { - Box, Divider, Drawer, IconButton, Tooltip, Typography, css, + Divider, IconButton, Tooltip, Typography, } from '@mui/material'; import { Item } from '@visdesignlab/upset2-core'; -import React, { - useCallback, useContext, useEffect, useState, -} from 'react'; import { useRecoilValue } from 'recoil'; import { columnsAtom } from '../../atoms/columnAtom'; @@ -19,10 +13,9 @@ import { import { BookmarkChips } from './BookmarkChips'; import { ElementTable } from './ElementTable'; import { ElementVisualization } from './ElementVisualization'; -import { UpsetActions } from '../../provenance'; -import { ProvenanceContext } from '../Root'; import { QueryInterface } from './QueryInterface'; import { bookmarkSelector, currentIntersectionSelector } from '../../atoms/config/currentIntersectionAtom'; +import { Sidebar } from '../custom/Sidebar'; /** * Props for the ElementSidebar component @@ -34,15 +27,6 @@ type Props = { close: () => void } -/** - * The *exact* width at which we don't get a horizontal scrollbar in the table controls - */ -const initialDrawerWidth = 462; -/** - * The *exact* width at which the 'apply' button in the element query controls is forced onto a new line - */ -const minDrawerWidth = 368; - /** * Immediately downloads a csv containing items with the given columns * @param items Rows to download @@ -85,130 +69,15 @@ function downloadElementsAsCSV(items: Item[], columns: string[], name: string) { * @param close Function to close the sidebar */ export const ElementSidebar = ({ open, close }: Props) => { - const [fullWidth, setFullWidth] = useState(false); - const [drawerWidth, setDrawerWidth] = useState(initialDrawerWidth); const currentElementSelection = useRecoilValue(selectedElementSelector); const selectedItems = useRecoilValue(selectedItemsSelector); const itemCount = useRecoilValue(selectedItemsCounter); const columns = useRecoilValue(columnsAtom); - const [hideElementSidebar, setHideElementSidebar] = useState(!open); - const { actions }: {actions: UpsetActions} = useContext(ProvenanceContext); const bookmarked = useRecoilValue(bookmarkSelector); const currentIntersection = useRecoilValue(currentIntersectionSelector); - /** - * Effects - */ - - useEffect(() => { - setHideElementSidebar(!open); - }, [open]); - - /** - * Callbacks - */ - - const handleMouseMove = useCallback((e: MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - const newWidth = document.body.clientWidth - e.clientX; - - if (newWidth > minDrawerWidth) { - setDrawerWidth(newWidth); - } - }, []); - - const handleMouseUp = useCallback(() => { - document.removeEventListener('mouseup', handleMouseUp, true); - document.removeEventListener('mousemove', handleMouseMove, true); - }, [handleMouseMove]); - - const handleMouseDown = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - document.addEventListener('mouseup', handleMouseUp, true); - document.addEventListener('mousemove', handleMouseMove, true); - }, - [handleMouseUp, handleMouseMove], - ); - return ( - - handleMouseDown(e)} - /> -
- { !fullWidth ? - { - setFullWidth(true); - }} - aria-label="Expand the sidebar in full screen" - > - - - : - { - if (fullWidth) { - setFullWidth(false); - } else { - setHideElementSidebar(true); - } - }} - aria-label="Reduce the sidebar to normal size" - > - - } - { - setHideElementSidebar(true); - actions.setElementSelection(currentElementSelection); - close(); - }} - aria-label="Close the sidebar" - > - - -
+
Element View @@ -252,6 +121,6 @@ export const ElementSidebar = ({ open, close }: Props) => { - + ); }; From c056a534ca9a5f3f6f3e08a3302851c59b050f87 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Mon, 16 Dec 2024 21:53:55 -0700 Subject: [PATCH 11/56] Add multiselect in settings for visible sets --- .../upset/src/components/SettingsSidebar.tsx | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/upset/src/components/SettingsSidebar.tsx b/packages/upset/src/components/SettingsSidebar.tsx index 2ee9bb4c..432b856d 100644 --- a/packages/upset/src/components/SettingsSidebar.tsx +++ b/packages/upset/src/components/SettingsSidebar.tsx @@ -5,12 +5,19 @@ import { AccordionDetails, AccordionSummary, Box, + Checkbox, FormControl, FormControlLabel, FormGroup, FormLabel, + InputLabel, + ListItemText, + MenuItem, + OutlinedInput, Radio, RadioGroup, + Select, + SelectChangeEvent, Slider, Switch, TextField, @@ -18,6 +25,7 @@ import { } from '@mui/material'; import { AggregateBy, aggregateByList, + BaseElement, } from '@visdesignlab/upset2-core'; import { CSSProperties, @@ -39,6 +47,8 @@ import { ProvenanceContext } from './Root'; import { HelpCircle, defaultMargin } from './custom/HelpCircle'; import { helpText } from '../utils/helpText'; import { dimensionsSelector, footerHeightAtom } from '../atoms/dimensionsAtom'; +import { setsAtom } from '../atoms/setsAtoms'; +import { UpsetActions } from '../provenance'; const itemDivCSS = css` display: flex; @@ -50,13 +60,26 @@ const sidebarHeaderCSS = css` font-size: 0.95rem; `; +/** + * Finds the added and removed elements between two arrays + * @param old The old array + * @param current The new array + * @returns An object with the added and removed elements + */ +function findChange(old: string[], current: string[]): {added: string[], removed: string[]} { + const added = current.filter((s) => !old.includes(s)); + const removed = old.filter((s) => !current.includes(s)); + return { added, removed }; +} + /** @jsxImportSource @emotion/react */ export const SettingsSidebar = () => { - const { actions } = useContext( + const { actions }: {actions: UpsetActions} = useContext( ProvenanceContext, ); const visibleSets = useRecoilValue(visibleSetSelector); + const allSets = useRecoilValue(setsAtom); const firstAggregateBy = useRecoilValue(firstAggregateSelector); const firstOverlapDegree = useRecoilValue(firstOvelapDegreeSelector); const secondAggregateBy = useRecoilValue(secondAggregateSelector); @@ -87,6 +110,13 @@ export const SettingsSidebar = () => { boxShadow: 'none', }; + const handleSetChange = (event: SelectChangeEvent) => { + const newSets = typeof event.target.value === 'string' ? [event.target.value] : event.target.value; + const { added, removed } = findChange(visibleSets, newSets); + added.forEach((s) => actions.addVisibleSet(s)); + removed.forEach((s) => actions.removeVisibleSet(s)); + }; + return ( { > Settings + + }> + Sets and Attributes + + + + Sets + + + + }> Aggregation From b869aee7935ff846356e563c5ca2101f5838ab0c Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Mon, 16 Dec 2024 22:20:37 -0700 Subject: [PATCH 12/56] Add settings control for visible atts --- packages/upset/src/atoms/attributeAtom.ts | 3 ++ packages/upset/src/components/Root.tsx | 2 +- .../upset/src/components/SettingsSidebar.tsx | 45 +++++++++++++++++-- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/packages/upset/src/atoms/attributeAtom.ts b/packages/upset/src/atoms/attributeAtom.ts index 4fc9b04e..bac26d2d 100644 --- a/packages/upset/src/atoms/attributeAtom.ts +++ b/packages/upset/src/atoms/attributeAtom.ts @@ -2,6 +2,9 @@ import { atom, selectorFamily } from 'recoil'; import { itemsAtom } from './itemsAtoms'; +/** + * All attributes, including degree and deviation + */ export const attributeAtom = atom({ key: 'attribute-columns', default: [], diff --git a/packages/upset/src/components/Root.tsx b/packages/upset/src/components/Root.tsx index 91aa2a67..47f8ff07 100644 --- a/packages/upset/src/components/Root.tsx +++ b/packages/upset/src/components/Root.tsx @@ -116,7 +116,7 @@ export const Root: FC = ({ useEffect(() => { setSets(data.sets); setItems(data.items); - setAttributeColumns(data.attributeColumns); + setAttributeColumns(['Degree', 'Deviation', ...data.attributeColumns]); setAllColumns(data.columns); setData(data); // if it is defined, pass through the provided value, else, default to true diff --git a/packages/upset/src/components/SettingsSidebar.tsx b/packages/upset/src/components/SettingsSidebar.tsx index 432b856d..ef393963 100644 --- a/packages/upset/src/components/SettingsSidebar.tsx +++ b/packages/upset/src/components/SettingsSidebar.tsx @@ -29,7 +29,7 @@ import { } from '@visdesignlab/upset2-core'; import { CSSProperties, - Fragment, useContext, useEffect, useState, + Fragment, useCallback, useContext, useEffect, useState, } from 'react'; import { useRecoilValue } from 'recoil'; @@ -49,6 +49,8 @@ import { helpText } from '../utils/helpText'; import { dimensionsSelector, footerHeightAtom } from '../atoms/dimensionsAtom'; import { setsAtom } from '../atoms/setsAtoms'; import { UpsetActions } from '../provenance'; +import { attributeAtom } from '../atoms/attributeAtom'; +import { visibleAttributesSelector } from '../atoms/config/visibleAttributes'; const itemDivCSS = css` display: flex; @@ -80,10 +82,14 @@ export const SettingsSidebar = () => { const visibleSets = useRecoilValue(visibleSetSelector); const allSets = useRecoilValue(setsAtom); + const allAtts = useRecoilValue(attributeAtom); + const visibleAtts = useRecoilValue(visibleAttributesSelector); + const firstAggregateBy = useRecoilValue(firstAggregateSelector); const firstOverlapDegree = useRecoilValue(firstOvelapDegreeSelector); const secondAggregateBy = useRecoilValue(secondAggregateSelector); const secondOverlapDegree = useRecoilValue(secondOverlapDegreeSelector); + const maxVisible = useRecoilValue(maxVisibleSelector); const minVisible = useRecoilValue(minVisibleSelector); const hideEmpty = useRecoilValue(hideEmptySelector); @@ -110,12 +116,26 @@ export const SettingsSidebar = () => { boxShadow: 'none', }; - const handleSetChange = (event: SelectChangeEvent) => { + /** + * Handles a change in the visible sets multiselect by adding or removing the sets that changed + */ + const handleSetChange = useCallback((event: SelectChangeEvent) => { const newSets = typeof event.target.value === 'string' ? [event.target.value] : event.target.value; const { added, removed } = findChange(visibleSets, newSets); added.forEach((s) => actions.addVisibleSet(s)); removed.forEach((s) => actions.removeVisibleSet(s)); - }; + }, [visibleSets, actions]); + + /** + * Handles a change in the visible attributes multiselect + * by adding or removing the attributes that changed + */ + const handleAttChange = useCallback((event: SelectChangeEvent) => { + const newAtts = typeof event.target.value === 'string' ? [event.target.value] : event.target.value; + const { added, removed } = findChange(visibleAtts, newAtts); + added.forEach((a) => actions.addAttribute(a)); + removed.forEach((a) => actions.removeAttribute(a)); + }, [visibleAtts, actions]); return ( @@ -158,6 +178,25 @@ export const SettingsSidebar = () => { ))} + + Attributes + + From c6461021dcdda6d946e789efc5b35c883ab31499 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Mon, 16 Dec 2024 22:41:39 -0700 Subject: [PATCH 13/56] Bugfixes: Attributes multiselect label & order of attributes in plot --- .../upset/src/components/SettingsSidebar.tsx | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/upset/src/components/SettingsSidebar.tsx b/packages/upset/src/components/SettingsSidebar.tsx index ef393963..b5f00590 100644 --- a/packages/upset/src/components/SettingsSidebar.tsx +++ b/packages/upset/src/components/SettingsSidebar.tsx @@ -132,9 +132,17 @@ export const SettingsSidebar = () => { */ const handleAttChange = useCallback((event: SelectChangeEvent) => { const newAtts = typeof event.target.value === 'string' ? [event.target.value] : event.target.value; - const { added, removed } = findChange(visibleAtts, newAtts); - added.forEach((a) => actions.addAttribute(a)); - removed.forEach((a) => actions.removeAttribute(a)); + // Ensures that the order is always Degree, Deviation, then the rest; + // this keeps the plot consistent & prevents graphical bugs + newAtts.sort((a, b) => { + if (a === 'Degree') return -1; + if (b === 'Degree') return 1; + if (a === 'Deviation') return -1; + if (b === 'Deviation') return 1; + return 0; + }); + // This simply sets the config visibleAtts to all newAtts, so it removes atts as well + actions.addMultipleAttributes(newAtts); }, [visibleAtts, actions]); return ( @@ -179,7 +187,13 @@ export const SettingsSidebar = () => { - Attributes + + Attributes +