Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI redo dec 24 #439

Open
wants to merge 56 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
5073c01
ESLint Footer, Header, and Root in App
Dec 14, 2024
ffc28cb
ESLint Body
Dec 15, 2024
0b4fd4c
Remove useless useEffect
Dec 16, 2024
729d1cb
Unify sidebar into 1 element with shadow; fix title appearance
Dec 16, 2024
7fc6918
Add footerHeight param to Upset component & prevent settings sidebar …
Dec 16, 2024
3b0d747
Create generic sidebar component
Dec 16, 2024
e731640
Apply generic sidebar to AltTextSidebar
Dec 16, 2024
0131e84
Pass tab index for close button through
Dec 16, 2024
f90aa97
Bugfix for empty div remaining after closing sidebar
Dec 16, 2024
c9f1ffc
Use generic sidebar for Element Sidebar
Dec 16, 2024
c056a53
Add multiselect in settings for visible sets
Dec 17, 2024
b869aee
Add settings control for visible atts
Dec 17, 2024
c646102
Bugfixes: Attributes multiselect label & order of attributes in plot
Dec 17, 2024
6dfd952
Remove attribute dropdown from header
Dec 17, 2024
3f58de4
Abstract toggle switches in the settings sidebar
Dec 17, 2024
0ec4ce8
UI (toggles) for general settings in sidebar
Dec 17, 2024
ec2d6ae
Add config fields for intersection size lables, set size labels, and …
Dec 17, 2024
35762ee
Ensure initializeProvenanceTracking setter converts config before set…
Dec 17, 2024
ecba999
Add config fields & selectors for general plot settings
Dec 17, 2024
50e16eb
Hide hidden sets & intersection size labels according to config settings
Dec 17, 2024
7cc775c
Matrix & set header JSDoc
Dec 19, 2024
8477d9d
Size labels for visible sets; responsive to global setting
Dec 19, 2024
cf1fe83
Bugfixes for set size bar: size black/white gap and label padding
Dec 19, 2024
07bbc86
Move "Load Data" into the meatball menu
Dec 21, 2024
6f62a63
ESLint App.tsx
Dec 21, 2024
935eb65
Strict type for data & move dispatchState to utils
Dec 21, 2024
a195a80
ESLint AccessibilityStatement.tsx
Dec 21, 2024
588e540
Abstract link to data table, add data table to accessibility statemen…
Dec 22, 2024
4ba509a
ESLint datatable.tsx
Dec 22, 2024
5c47334
Standardize heading styles for the upset package
Dec 22, 2024
6d1a592
Bugfix: correct header height
Dec 22, 2024
caa1ed1
Change footer height atom to number
Dec 22, 2024
5eebbef
Give UpsetHeading a full style prop
Dec 22, 2024
772d2c4
Make settings sidebar collapsible
Dec 22, 2024
2b9d380
Change hamburger menu icon in header to gear; re-rder elements
Dec 22, 2024
219e675
Change alttext font size to match rest of UI
Dec 22, 2024
252f1bd
Hide 2nd agg if 1st agg is none
Jan 7, 2025
42c7672
Increase font size & weight for settings sidebar section labels
Jan 7, 2025
63b9d11
Bump font size and weight a lil more for settings sidebar section labels
Jan 7, 2025
c165a69
Update title: emdash, flask logo, and min width at which to show
Jan 7, 2025
1dce955
Re-add 1px spacing between set size bars
Jan 7, 2025
46e54ea
Correct vertical centering of column titles
Jan 7, 2025
625448e
Right-align settings sidebar toggles
Jan 7, 2025
ad17b39
Reposition & resize the expand sidebar button
Jan 7, 2025
959a825
Absolute position of sidebar buttons so they don't displace other ele…
Jan 7, 2025
ffa2d45
Bugfix: Sidebar aria-label
Jan 8, 2025
a723346
Bugfix: z-index for sidebar and drawer
Jan 8, 2025
cf19169
Fix attribute dropdown tests for PR 439
Jan 8, 2025
064918b
Fix datatable test for PR 439
Jan 8, 2025
c93cf45
Bugfix: disinclude Degree and Deviation from plottable attributes
Jan 8, 2025
a5e2095
Fix element view test for PR 439
Jan 8, 2025
1d171a5
eslint provenance.spec.ts
Jan 8, 2025
f2c3ce2
Fix Settings header and user icon padding
Jan 10, 2025
0f7d2f2
Use Sidebar component for provenance vis
Jan 10, 2025
fa24ff9
Address Jake's review for PR 439
Jan 10, 2025
0f090aa
Rename dataTableLink.tsx to DataTableLink.tsx
NateLanza Jan 10, 2025
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<AltText>`): 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
Expand Down
42 changes: 22 additions & 20 deletions e2e-tests/attributeSelector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,53 @@ import { beforeTest } from './common';

test.beforeEach(beforeTest);

/**
* Selects or deselects an attribute from the attribute dropdown
* @param page the page to interact with
* @param attributeName the name of the attribute to toggle
* @param checked whether to select or deselect the attribute
*/
async function toggleAttribute(page, attributeName, checked) {
await page.getByLabel('Attributes').first().click();
await page.getByRole('option', { name: attributeName }).getByRole('checkbox').setChecked(checked);
await page.locator('#menu- > .MuiBackdrop-root').click();
}

test('Attribute Dropdown', async ({ page }) => {
await page.goto('http://localhost:3000/?workspace=Upset+Examples&table=simpsons&sessionId=193');

/// /////////////////
// Age
/// /////////////////
// Deseslect and assert that it's removed from the plot
await page.getByLabel('Attributes selection menu').click();
await page.getByRole('checkbox', { name: 'Age' }).uncheck();
await page.locator('.MuiPopover-root > .MuiBackdrop-root').click();
await toggleAttribute(page, 'Age', false);
await expect(page.getByLabel('Age').locator('rect')).toHaveCount(0);

// Reselect and assert that it's added back to the plot
await page.getByLabel('Attributes selection menu').click();
await page.getByLabel('Age').check();
await page.locator('.MuiPopover-root > .MuiBackdrop-root').click();
await expect(page.getByText('Age', { exact: true })).toBeVisible();
await toggleAttribute(page, 'Age', true);
// This doesn't make sense but it works to find the Age column header
await expect(page.locator('g').filter({ hasText: /^Age2020404060608080$/ }).locator('rect')).toBeVisible();

/// /////////////////
// Degree
/// /////////////////
// Deselect and assert that it's removed from the plot
await page.getByLabel('Attributes selection menu').click();
await page.getByRole('checkbox', { name: 'Degree' }).uncheck();
await page.locator('.MuiPopover-root > .MuiBackdrop-root').click();
await toggleAttribute(page, 'Degree', false);
await expect(page.locator('#upset-svg').getByLabel('Number of intersecting sets').locator('rect')).toHaveCount(0);

// Reselect and assert that it's added back to the plot
await page.getByLabel('Attributes selection menu').click();
await page.getByRole('checkbox', { name: 'Degree' }).check();
await page.locator('.MuiPopover-root > .MuiBackdrop-root').click();
await toggleAttribute(page, 'Degree', true);
await expect(page.locator('#upset-svg').getByLabel('Number of intersecting sets').locator('rect')).toBeVisible();

/// /////////////////
// Deviation
/// /////////////////
// Deselect and assert that it's removed from the plot
await page.getByLabel('Attributes selection menu').click();
await page.getByRole('checkbox', { name: 'Deviation' }).uncheck();
await page.locator('.MuiPopover-root > .MuiBackdrop-root').click();
await toggleAttribute(page, 'Deviation', false);
await expect(page.getByLabel('Deviation', { exact: true }).locator('rect')).toHaveCount(0);

// Reselect and assert that it's added back to the plot
await page.getByLabel('Attributes selection menu').click();
await page.getByRole('checkbox', { name: 'Deviation' }).check();
await page.locator('.MuiPopover-root > .MuiBackdrop-root').click();
await expect(page.getByText('Deviation', { exact: true })).toBeVisible();
await toggleAttribute(page, 'Deviation', true);
// This also doesn't make sense but uniquely selects the Deviation column header
await expect(page.locator('g').filter({ hasText: /^#Deviation-10%-10%-5%-5%0%0%5%5%10%10%Age2020404060608080$/ }).locator('rect').nth(1)).toBeVisible();
});
3 changes: 2 additions & 1 deletion e2e-tests/datatable.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ test('Datatable', async ({ page }) => {
// //////////////////
// Open the datatable
// //////////////////
await page.getByLabel('Additional options menu').click();
const page1Promise = page.waitForEvent('popup');
await page.getByRole('button', { name: 'Data Table' }).click();
await page.getByLabel('Data Tables (raw and computed)').click();

// //////////////////
// Test downloads
Expand Down
7 changes: 4 additions & 3 deletions e2e-tests/elementView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ test('Element View', async ({ page, browserName }) => {
await row.dispatchEvent('click');

// test expansion buttons
await page.getByLabel('Expand the sidebar in full').click();
await page.getByRole('button', { name: 'Expand the sidebar in full' }).click();
await page.getByLabel('Reduce the sidebar to normal').click();

// Ensure all headings are visible
Expand All @@ -78,8 +78,9 @@ test('Element View', async ({ page, browserName }) => {

// Check that the datatable is visible and populated
const dataTable = page.getByText(
'LabelAgeSchoolBlue HairDuff FanEvilMalePower PlantBart10yesnononoyesnoRalph8yesnononoyesnoMartin Prince10yesnononoyesnoRows per page:1001–3 of',
'LabelDegreeDeviationAgeSchoolBlue HairDuff FanEvilBart10yesnononoRalph8yesnononoMartin Prince10yesnononoRows per page:1001–3 of',
NateLanza marked this conversation as resolved.
Show resolved Hide resolved
);
dataTable.scrollIntoViewIfNeeded();
await expect(dataTable).toBeVisible();
const nameCell = await page.getByRole('cell', { name: 'Bart' });
await expect(nameCell).toBeVisible();
Expand Down Expand Up @@ -122,7 +123,7 @@ test('Element View', async ({ page, browserName }) => {
await downloadPromise;

// Check that the close button is visible and works
const elementViewClose = await page.getByLabel('Close the sidebar');
const elementViewClose = await page.getByRole('button', { name: 'Close the sidebar' });
await expect(elementViewClose).toBeVisible();
await elementViewClose.click();

Expand Down
6 changes: 4 additions & 2 deletions e2e-tests/provenance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ test('Selection History', async ({ page }) => {

// Testing history for an aggregate row selection & deselection
await page.getByRole('radio', { name: 'Degree' }).check();
await page.locator('g').filter({ hasText: /^Degree 3Degree 3$/ }).locator('rect').nth(0).click();
await page.locator('g').filter({ hasText: /^Degree 3Degree 3$/ }).locator('rect').nth(0)
.click();
await expect(page.locator('div').filter({ hasText: /^Select intersection "Degree 3"$/ }).nth(2)).toBeVisible();
await page.locator('g').filter({ hasText: /^Degree 3Degree 3$/ }).locator('rect').nth(0).click();
await page.locator('g').filter({ hasText: /^Degree 3Degree 3$/ }).locator('rect').nth(0)
.click();
await expect(page.getByText('Deselect intersection').nth(1)).toBeVisible();

// Check that selections are maintained after de-aggregation
Expand Down
72 changes: 72 additions & 0 deletions packages/app/.eslintrc.js
JakeWags marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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',
},
};
82 changes: 42 additions & 40 deletions packages/app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { createContext, useEffect, useMemo, useState } from 'react';
import {
createContext, useEffect, useMemo, useState,
} from 'react';

import { UpsetProvenance, UpsetActions, getActions, initializeProvenanceTracking } from '@visdesignlab/upset2-react';
import {
UpsetProvenance, UpsetActions, getActions, initializeProvenanceTracking,
} from '@visdesignlab/upset2-react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { convertConfig, DefaultConfig, UpsetConfig } from '@visdesignlab/upset2-core';
import { CircularProgress } from '@mui/material';
import { ProvenanceGraph } from '@trrack/core/graph/graph-slice';
import { dataSelector, encodedDataAtom } from './atoms/dataAtom';
import { Root } from './components/Root';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { DataTable } from './components/DataTable';
import { convertConfig, DefaultConfig, UpsetConfig } from '@visdesignlab/upset2-core';
import { configAtom } from './atoms/configAtoms';
import { queryParamAtom } from './atoms/queryParamAtom';
import { getMultinetSession } from './api/session';
import { CircularProgress } from '@mui/material';
import { ProvenanceGraph } from '@trrack/core/graph/graph-slice';

/** @jsxImportSource @emotion/react */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand All @@ -29,60 +33,60 @@ function App() {
const multinetData = useRecoilValue(dataSelector);
const encodedData = useRecoilValue(encodedDataAtom);
const setState = useSetRecoilState(configAtom);
const data = (encodedData === null) ? multinetData : encodedData
const data = (encodedData === null) ? multinetData : encodedData;
const { workspace, sessionId } = useRecoilValue(queryParamAtom);
const [sessionState, setSessionState] = useState<SessionState>(null); // null is not tried to load, undefined is tried and no state to load, and value is loaded value

const conf = useMemo(() => {
const config: UpsetConfig = { ...DefaultConfig }
const config: UpsetConfig = { ...DefaultConfig };
if (data !== null) {
const conf: UpsetConfig = JSON.parse(JSON.stringify(config))
const newConf: UpsetConfig = JSON.parse(JSON.stringify(config));
if (config.visibleSets.length === 0) {
const setList = Object.entries(data.sets);
conf.visibleSets = setList.slice(0, defaultVisibleSets).map((set) => set[0]) // get first 6 set names
conf.allSets = setList.map((set) => {return { name: set[0], size: set[1].size }})
newConf.visibleSets = setList.slice(0, defaultVisibleSets).map((set) => set[0]); // get first 6 set names
newConf.allSets = setList.map((set) => ({ name: set[0], size: set[1].size }));
}

// Add first 4 attribute columns (deviation + 3 attrs) to visibleAttributes
conf.visibleAttributes = [...DefaultConfig.visibleAttributes, ...data.attributeColumns.slice(0, 4)];
newConf.visibleAttributes = [...DefaultConfig.visibleAttributes, ...data.attributeColumns.slice(0, 4)];

// Default: a histogram for each attribute if no plots exist
if (conf.plots.histograms.length + conf.plots.scatterplots.length === 0) {
conf.plots.histograms = data.attributeColumns.map((attr) => {
return {
attribute: attr,
bins: 20, // 20 bins is the default used in upset/.../AddPlot.tsx
type: 'Histogram',
frequency: false,
id: Date.now().toString() // Same calculation as in upset/.../AddPlot.tsx
}
})
if (newConf.plots.histograms.length + newConf.plots.scatterplots.length === 0) {
newConf.plots.histograms = data.attributeColumns.map((attr) => ({
attribute: attr,
bins: 20, // 20 bins is the default used in upset/.../AddPlot.tsx
type: 'Histogram',
frequency: false,
id: Date.now().toString(), // Same calculation as in upset/.../AddPlot.tsx
}));
}

return conf;
return newConf;
}

return config;
}, [data]);

// Initialize Provenance and pass it setter to connect
const { provenance, actions } = useMemo(() => {
if (sessionState) {
const provenance: UpsetProvenance = initializeProvenanceTracking(conf);
const actions: UpsetActions = getActions(provenance);
const prov: UpsetProvenance = initializeProvenanceTracking(conf ?? undefined);
const act: UpsetActions = getActions(prov);

// Make sure the provenance state gets converted every time this is called
(provenance as UpsetProvenance & {_getState: typeof provenance.getState})._getState = provenance.getState;
provenance.getState = () => convertConfig(
(provenance as UpsetProvenance & {_getState: typeof provenance.getState})._getState()
(prov as UpsetProvenance & {_getState: typeof prov.getState})._getState = prov.getState;
prov.getState = () => convertConfig(
(prov as UpsetProvenance & {_getState: typeof prov.getState})._getState(),
);

if (sessionState && sessionState !== 'not found') {
provenance.importObject(structuredClone(sessionState));
prov.importObject(structuredClone(sessionState));
}

// Make sure the config atom stays up-to-date with the provenance
provenance.currentChange(() => setState(provenance.getState()));
prov.currentChange(() => setState(prov.getState()));

return { provenance: provenance, actions: actions };
return { provenance: prov, actions: act };
}
return { provenance: null, actions: null };
}, [conf, setState, sessionState]);
Expand All @@ -108,31 +112,29 @@ function App() {
update();
}, [sessionId, workspace]);

const provContext = useMemo(() => (provenance && actions ? { provenance, actions } : null), [provenance, actions]);

// Update the state on first render and if the provenance object changes
useEffect(() => {if (provenance?.getState()) setState(provenance?.getState())}, [provenance, setState]);
useEffect(() => { if (provenance?.getState()) setState(provenance?.getState()); }, [provenance, setState]);

return (
<BrowserRouter>
{provenance ?
{(provenance && provContext) ?
<ProvenanceContext.Provider
value={{
provenance,
actions,
}}
value={provContext}
>
<Routes>
<Route path="*" element={<Root provenance={provenance} actions={actions} data={null} config={conf} />} />
<Route path="/" element={<Root provenance={provenance} actions={actions} data={data} config={conf} />} />
<Route path="/datatable" element={<DataTable />} />
</Routes>
</ProvenanceContext.Provider>
:
:
<Routes>
<Route path="*" element={<CircularProgress />} />
<Route path="/" element={<CircularProgress />} />
<Route path="/datatable" element={<DataTable />} />
</Routes>
}
</Routes>}
</BrowserRouter>
);
}
Expand Down
Loading
Loading