Skip to content

Commit f9ba101

Browse files
authored
Merge pull request #439 from visdesignlab/ui-redo-dec-24
UI redo dec '24
2 parents 52884c8 + 0f090aa commit f9ba101

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1945
-1401
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ const main = () => {
211211
- `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.
212212
- `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.
213213
- `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.
214+
- `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.
214215
- `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.
215216

216217
##### Configuration (Grammar) options

e2e-tests/attributeSelector.spec.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,53 @@ import { beforeTest } from './common';
33

44
test.beforeEach(beforeTest);
55

6+
/**
7+
* Selects or deselects an attribute from the attribute dropdown
8+
* @param page the page to interact with
9+
* @param attributeName the name of the attribute to toggle
10+
* @param checked whether to select or deselect the attribute
11+
*/
12+
async function toggleAttribute(page, attributeName, checked) {
13+
await page.getByLabel('Attributes').first().click();
14+
await page.getByRole('option', { name: attributeName }).getByRole('checkbox').setChecked(checked);
15+
await page.locator('#menu- > .MuiBackdrop-root').click();
16+
}
17+
618
test('Attribute Dropdown', async ({ page }) => {
719
await page.goto('http://localhost:3000/?workspace=Upset+Examples&table=simpsons&sessionId=193');
820

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

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

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

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

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

4851
// Reselect and assert that it's added back to the plot
49-
await page.getByLabel('Attributes selection menu').click();
50-
await page.getByRole('checkbox', { name: 'Deviation' }).check();
51-
await page.locator('.MuiPopover-root > .MuiBackdrop-root').click();
52-
await expect(page.getByText('Deviation', { exact: true })).toBeVisible();
52+
await toggleAttribute(page, 'Deviation', true);
53+
// This also doesn't make sense but uniquely selects the Deviation column header
54+
await expect(page.locator('g').filter({ hasText: /^#Deviation-10%-10%-5%-5%0%0%5%5%10%10%Age2020404060608080$/ }).locator('rect').nth(1)).toBeVisible();
5355
});

e2e-tests/datatable.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ test('Datatable', async ({ page }) => {
99
// //////////////////
1010
// Open the datatable
1111
// //////////////////
12+
await page.getByLabel('Additional options menu').click();
1213
const page1Promise = page.waitForEvent('popup');
13-
await page.getByRole('button', { name: 'Data Table' }).click();
14+
await page.getByLabel('Data Tables (raw and computed)').click();
1415

1516
// //////////////////
1617
// Test downloads

e2e-tests/elementView.spec.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ test('Element View', async ({ page, browserName }) => {
5959
await row.dispatchEvent('click');
6060

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

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

7979
// Check that the datatable is visible and populated
8080
const dataTable = page.getByText(
81-
'LabelAgeSchoolBlue HairDuff FanEvilMalePower PlantBart10yesnononoyesnoRalph8yesnononoyesnoMartin Prince10yesnononoyesnoRows per page:1001–3 of',
81+
'LabelDegreeDeviationAgeSchoolBlue HairDuff FanEvilBart10yesnononoRalph8yesnononoMartin Prince10yesnononoRows per page:1001–3 of',
8282
);
83+
dataTable.scrollIntoViewIfNeeded();
8384
await expect(dataTable).toBeVisible();
8485
const nameCell = await page.getByRole('cell', { name: 'Bart' });
8586
await expect(nameCell).toBeVisible();
@@ -122,7 +123,7 @@ test('Element View', async ({ page, browserName }) => {
122123
await downloadPromise;
123124

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

e2e-tests/provenance.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ test('Selection History', async ({ page }) => {
2424

2525
// Testing history for an aggregate row selection & deselection
2626
await page.getByRole('radio', { name: 'Degree' }).check();
27-
await page.locator('g').filter({ hasText: /^Degree 3Degree 3$/ }).locator('rect').nth(0).click();
27+
await page.locator('g').filter({ hasText: /^Degree 3Degree 3$/ }).locator('rect').nth(0)
28+
.click();
2829
await expect(page.locator('div').filter({ hasText: /^Select intersection "Degree 3"$/ }).nth(2)).toBeVisible();
29-
await page.locator('g').filter({ hasText: /^Degree 3Degree 3$/ }).locator('rect').nth(0).click();
30+
await page.locator('g').filter({ hasText: /^Degree 3Degree 3$/ }).locator('rect').nth(0)
31+
.click();
3032
await expect(page.getByText('Deselect intersection').nth(1)).toBeVisible();
3133

3234
// Check that selections are maintained after de-aggregation

packages/app/.eslintrc.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
module.exports = {
2+
env: {
3+
browser: true,
4+
es2021: true,
5+
},
6+
extends: [
7+
'plugin:react/recommended',
8+
'plugin:import/recommended',
9+
'plugin:@typescript-eslint/recommended',
10+
'airbnb',
11+
'plugin:import/typescript',
12+
],
13+
parser: '@typescript-eslint/parser',
14+
parserOptions: {
15+
ecmaFeatures: {
16+
jsx: true,
17+
},
18+
ecmaVersion: 13,
19+
sourceType: 'module',
20+
},
21+
plugins: ['react', '@typescript-eslint'],
22+
root: true,
23+
rules: {
24+
'react/jsx-filename-extension': [
25+
2,
26+
{ extensions: ['.js', '.jsx', '.ts', '.tsx'] },
27+
],
28+
'import/prefer-default-export': 'off',
29+
'import/no-extraneous-dependencies': [
30+
'error',
31+
{
32+
devDependencies: ['.storybook/**', '**/stories/**'],
33+
},
34+
],
35+
'react/function-component-definition': 'off',
36+
'no-plusplus': ['warn', { allowForLoopAfterthoughts: true }],
37+
'dot-notation': 'off',
38+
'import/extensions': [
39+
'error',
40+
'ignorePackages',
41+
{
42+
js: 'never',
43+
jsx: 'never',
44+
ts: 'never',
45+
tsx: 'never',
46+
},
47+
],
48+
'react/jsx-props-no-spreading': 'off',
49+
'react/require-default-props': 'off',
50+
'react/react-in-jsx-scope': 'off',
51+
'react/jsx-wrap-multilines': 'off',
52+
'react/no-unknown-property': 'off',
53+
'operator-linebreak': 'off',
54+
'@typescript-eslint/no-unused-vars': [
55+
'warn', // or "error"
56+
{
57+
argsIgnorePattern: '^_',
58+
varsIgnorePattern: '^_',
59+
caughtErrorsIgnorePattern: '^_',
60+
},
61+
],
62+
'@typescript-eslint/no-explicit-any': 'off',
63+
'max-len': 'off',
64+
'no-unused-vars': 'off',
65+
'no-param-reassign': 'off',
66+
'import/no-cycle': 'off',
67+
'no-underscore-dangle': 'off',
68+
'no-nested-ternary': 'off',
69+
'jsx-a11y/tabindex-no-positive': 'off',
70+
'no-bitwise': 'warn',
71+
},
72+
};

packages/app/src/App.tsx

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
import { createContext, useEffect, useMemo, useState } from 'react';
1+
import {
2+
createContext, useEffect, useMemo, useState,
3+
} from 'react';
24

3-
import { UpsetProvenance, UpsetActions, getActions, initializeProvenanceTracking } from '@visdesignlab/upset2-react';
5+
import {
6+
UpsetProvenance, UpsetActions, getActions, initializeProvenanceTracking,
7+
} from '@visdesignlab/upset2-react';
48
import { useRecoilValue, useSetRecoilState } from 'recoil';
9+
import { BrowserRouter, Route, Routes } from 'react-router-dom';
10+
import { convertConfig, DefaultConfig, UpsetConfig } from '@visdesignlab/upset2-core';
11+
import { CircularProgress } from '@mui/material';
12+
import { ProvenanceGraph } from '@trrack/core/graph/graph-slice';
513
import { dataSelector, encodedDataAtom } from './atoms/dataAtom';
614
import { Root } from './components/Root';
7-
import { BrowserRouter, Route, Routes } from 'react-router-dom';
815
import { DataTable } from './components/DataTable';
9-
import { convertConfig, DefaultConfig, UpsetConfig } from '@visdesignlab/upset2-core';
1016
import { configAtom } from './atoms/configAtoms';
1117
import { queryParamAtom } from './atoms/queryParamAtom';
1218
import { getMultinetSession } from './api/session';
13-
import { CircularProgress } from '@mui/material';
14-
import { ProvenanceGraph } from '@trrack/core/graph/graph-slice';
1519

1620
/** @jsxImportSource @emotion/react */
1721
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -29,60 +33,60 @@ function App() {
2933
const multinetData = useRecoilValue(dataSelector);
3034
const encodedData = useRecoilValue(encodedDataAtom);
3135
const setState = useSetRecoilState(configAtom);
32-
const data = (encodedData === null) ? multinetData : encodedData
36+
const data = (encodedData === null) ? multinetData : encodedData;
3337
const { workspace, sessionId } = useRecoilValue(queryParamAtom);
3438
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
3539

3640
const conf = useMemo(() => {
37-
const config: UpsetConfig = { ...DefaultConfig }
41+
const config: UpsetConfig = { ...DefaultConfig };
3842
if (data !== null) {
39-
const conf: UpsetConfig = JSON.parse(JSON.stringify(config))
43+
const newConf: UpsetConfig = JSON.parse(JSON.stringify(config));
4044
if (config.visibleSets.length === 0) {
4145
const setList = Object.entries(data.sets);
42-
conf.visibleSets = setList.slice(0, defaultVisibleSets).map((set) => set[0]) // get first 6 set names
43-
conf.allSets = setList.map((set) => {return { name: set[0], size: set[1].size }})
46+
newConf.visibleSets = setList.slice(0, defaultVisibleSets).map((set) => set[0]); // get first 6 set names
47+
newConf.allSets = setList.map((set) => ({ name: set[0], size: set[1].size }));
4448
}
4549

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

4953
// Default: a histogram for each attribute if no plots exist
50-
if (conf.plots.histograms.length + conf.plots.scatterplots.length === 0) {
51-
conf.plots.histograms = data.attributeColumns.map((attr) => {
52-
return {
53-
attribute: attr,
54-
bins: 20, // 20 bins is the default used in upset/.../AddPlot.tsx
55-
type: 'Histogram',
56-
frequency: false,
57-
id: Date.now().toString() // Same calculation as in upset/.../AddPlot.tsx
58-
}
59-
})
54+
if (newConf.plots.histograms.length + newConf.plots.scatterplots.length === 0) {
55+
newConf.plots.histograms = data.attributeColumns.map((attr) => ({
56+
attribute: attr,
57+
bins: 20, // 20 bins is the default used in upset/.../AddPlot.tsx
58+
type: 'Histogram',
59+
frequency: false,
60+
id: Date.now().toString(), // Same calculation as in upset/.../AddPlot.tsx
61+
}));
6062
}
6163

62-
return conf;
64+
return newConf;
6365
}
66+
67+
return config;
6468
}, [data]);
6569

6670
// Initialize Provenance and pass it setter to connect
6771
const { provenance, actions } = useMemo(() => {
6872
if (sessionState) {
69-
const provenance: UpsetProvenance = initializeProvenanceTracking(conf);
70-
const actions: UpsetActions = getActions(provenance);
73+
const prov: UpsetProvenance = initializeProvenanceTracking(conf ?? undefined);
74+
const act: UpsetActions = getActions(prov);
7175

7276
// Make sure the provenance state gets converted every time this is called
73-
(provenance as UpsetProvenance & {_getState: typeof provenance.getState})._getState = provenance.getState;
74-
provenance.getState = () => convertConfig(
75-
(provenance as UpsetProvenance & {_getState: typeof provenance.getState})._getState()
77+
(prov as UpsetProvenance & {_getState: typeof prov.getState})._getState = prov.getState;
78+
prov.getState = () => convertConfig(
79+
(prov as UpsetProvenance & {_getState: typeof prov.getState})._getState(),
7680
);
7781

7882
if (sessionState && sessionState !== 'not found') {
79-
provenance.importObject(structuredClone(sessionState));
83+
prov.importObject(structuredClone(sessionState));
8084
}
8185

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

85-
return { provenance: provenance, actions: actions };
89+
return { provenance: prov, actions: act };
8690
}
8791
return { provenance: null, actions: null };
8892
}, [conf, setState, sessionState]);
@@ -108,31 +112,29 @@ function App() {
108112
update();
109113
}, [sessionId, workspace]);
110114

115+
const provContext = useMemo(() => (provenance && actions ? { provenance, actions } : null), [provenance, actions]);
116+
111117
// Update the state on first render and if the provenance object changes
112-
useEffect(() => {if (provenance?.getState()) setState(provenance?.getState())}, [provenance, setState]);
118+
useEffect(() => { if (provenance?.getState()) setState(provenance?.getState()); }, [provenance, setState]);
113119

114120
return (
115121
<BrowserRouter>
116-
{provenance ?
122+
{(provenance && provContext) ?
117123
<ProvenanceContext.Provider
118-
value={{
119-
provenance,
120-
actions,
121-
}}
124+
value={provContext}
122125
>
123126
<Routes>
124127
<Route path="*" element={<Root provenance={provenance} actions={actions} data={null} config={conf} />} />
125128
<Route path="/" element={<Root provenance={provenance} actions={actions} data={data} config={conf} />} />
126129
<Route path="/datatable" element={<DataTable />} />
127130
</Routes>
128131
</ProvenanceContext.Provider>
129-
:
132+
:
130133
<Routes>
131134
<Route path="*" element={<CircularProgress />} />
132135
<Route path="/" element={<CircularProgress />} />
133136
<Route path="/datatable" element={<DataTable />} />
134-
</Routes>
135-
}
137+
</Routes>}
136138
</BrowserRouter>
137139
);
138140
}

0 commit comments

Comments
 (0)