Skip to content

Commit

Permalink
Merge pull request #466 from visdesignlab/element-view-style-updates-…
Browse files Browse the repository at this point in the history
…jan-25

Element View UI Updates
  • Loading branch information
NateLanza authored Feb 6, 2025
2 parents b204a82 + 4714b92 commit f0ba9ab
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 87 deletions.
7 changes: 3 additions & 4 deletions e2e-tests/elementView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ test('Element View', async ({ page, browserName }) => {
await expect(elementViewHeading).toBeVisible();
const elementQueriesHeading = await page.getByRole('heading', { name: 'Element Queries' });
await expect(elementQueriesHeading).toBeVisible();
const elementVisualizationHeading = await page.getByRole('heading', { name: 'Element Visualization' });
const elementVisualizationHeading = await page.getByRole('heading', { name: 'Element View' });
await expect(elementVisualizationHeading).toBeVisible();
const queryResultHeading = await page.getByRole('heading', { name: 'Query Result' });
await expect(queryResultHeading).toBeVisible();
Expand All @@ -86,7 +86,7 @@ test('Element View', async ({ page, browserName }) => {
await expect(ageCell).toBeVisible();

// Check that the add plot button is visible
const addPlot = await page.getByRole('button', { name: 'Add Plot' });
const addPlot = await page.locator('.MuiPaper-root > div:nth-child(2) > button').first();
await expect(addPlot).toBeVisible();
await addPlot.click();

Expand Down Expand Up @@ -185,8 +185,7 @@ test('Element View', async ({ page, browserName }) => {
/*
* Plot removal
*/
await page.locator('div').filter({ hasText: /^Add Plot$/ }).getByRole('button').nth(1)
.click();
await page.locator('.MuiBox-root > .MuiBox-root > .MuiButtonBase-root').first().click();
await expect(page.locator('canvas')).not.toBeVisible();
});

Expand Down
22 changes: 21 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,17 @@ export type Rows = Subsets | Aggregates;
*/
export type Row = Subset | Aggregate;

/**
* Possible column types.
* @private Taken from the TableTypeAnnotation in multinet-api
*/
export type ColumnType = 'primary key' | 'edge source' | 'edge target' | 'label' | 'string' | 'boolean' | 'category' | 'number' | 'date' | 'ignored';

/**
* Maps column names to their string type
*/
export type ColumnTypes = {
[key: string]: string;
[key: string]: ColumnType;
}

export type CoreUpsetData = {
Expand Down Expand Up @@ -301,6 +310,17 @@ export enum ElementQueryType {
GREATER_THAN = 'greater than',
}

/**
* Possible string types for an element query on a numerical attribute
*/
// linter is saying this is already declared... on this line
// eslint-disable-next-line no-shadow
export enum NumericalQueryType {
EQUALS = 'equals',
LESS_THAN = 'less than',
GREATER_THAN = 'greater than',
}

/**
* Represents a selection of elements based on a comparison between an attribute and a query string.
*/
Expand Down
11 changes: 11 additions & 0 deletions packages/upset/src/atoms/attributeAtom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { atom, selector, selectorFamily } from 'recoil';

import { ColumnTypes } from '@visdesignlab/upset2-core';
import { itemsAtom } from './itemsAtoms';
import { dataAtom } from './dataAtom';

/**
* All attributes, including degree and deviation
Expand All @@ -10,6 +12,15 @@ export const attributeAtom = atom<string[]>({
default: [],
});

/**
* Types of all attributes
* @returns {ColumnTypes} Object with attribute names as keys and their types as values
*/
export const attTypesSelector = selector<ColumnTypes>({
key: 'attribute-types',
get: ({ get }) => get(dataAtom).columnTypes,
});

/**
* All attribute columns except Degree and Deviation
*/
Expand Down
11 changes: 9 additions & 2 deletions packages/upset/src/components/ElementView/BookmarkChips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import SquareIcon from '@mui/icons-material/Square';
import StarIcon from '@mui/icons-material/Star';
import StarBorderIcon from '@mui/icons-material/StarBorder';
import { Chip, Stack } from '@mui/material';
import { useContext } from 'react';
import { useContext, useMemo } from 'react';
import { useRecoilValue } from 'recoil';

import {
Expand Down Expand Up @@ -52,8 +52,15 @@ export const BookmarkChips = () => {
}
}

/** Whether there is at least 1 chip */
const hasChip = useMemo(
() => currentIntersection || currentSelection || bookmarked.length > 0,
[currentIntersection, currentSelection, bookmarked],
);

return (
<Stack direction="row" sx={{ flexFlow: 'row wrap' }}>
// Silly goofy stack treats minHeight as maxHeight when we have chips... why??? idk
<Stack direction="row" sx={{ flexFlow: 'row wrap', minHeight: hasChip ? undefined : '40px' }}>
{/* All chips from bookmarks */}
{bookmarked.map((bookmark) => (
<Chip
Expand Down
37 changes: 25 additions & 12 deletions packages/upset/src/components/ElementView/ElementSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
import { Item } from '@visdesignlab/upset2-core';
import { useRecoilValue } from 'recoil';

import { useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import AddchartIcon from '@mui/icons-material/Addchart';
import { columnsAtom } from '../../atoms/columnAtom';
import {
selectedElementSelector, selectedItemsCounter,
Expand All @@ -18,6 +19,7 @@ import { QueryInterface } from './QueryInterface';
import { bookmarkSelector, currentIntersectionSelector } from '../../atoms/config/currentIntersectionAtom';
import { Sidebar } from '../custom/Sidebar';
import { UpsetHeading } from '../custom/theme/heading';
import { AddPlotDialog } from './AddPlotDialog';

/**
* Props for the ElementSidebar component
Expand Down Expand Up @@ -71,6 +73,7 @@ function downloadElementsAsCSV(items: Item[], columns: string[], name: string) {
* @param close Function to close the sidebar
*/
export const ElementSidebar = ({ open, close }: Props) => {
const [openAddPlot, setOpenAddPlot] = useState(false);
const currentElementSelection = useRecoilValue(selectedElementSelector);
const selectedItems = useRecoilValue(selectedItemsSelector);
const itemCount = useRecoilValue(selectedItemsCounter);
Expand All @@ -84,25 +87,35 @@ export const ElementSidebar = ({ open, close }: Props) => {
[bookmarked.length, currentIntersection, currentElementSelection],
);

/**
* Closes the AddPlotDialog
*/
const onClose = useCallback(() => setOpenAddPlot(false), [setOpenAddPlot]);

return (
<Sidebar open={open} close={close} label="Element View Sidebar">
<Sidebar
open={open}
close={close}
label="Element View Sidebar"
buttons={
<Tooltip title="Add Plot">
<IconButton onClick={() => setOpenAddPlot(true)}>
<AddchartIcon />
</IconButton>
</Tooltip>
}
>
<div style={{ marginBottom: '1em' }}>
<UpsetHeading level="h1">
Element View
</UpsetHeading>
</div>
{showQueries && (
<>
<UpsetHeading level="h2">
Bookmarked Queries
</UpsetHeading>
<BookmarkChips />
</>
)}
<UpsetHeading level="h2" style={{ marginTop: showQueries ? '1em' : undefined }}>
Element Visualization
<UpsetHeading level="h3">
{showQueries ? 'Selections' : 'Selections Will Appear Here'}
</UpsetHeading>
<BookmarkChips />
<ElementVisualization />
<AddPlotDialog open={openAddPlot} onClose={onClose} />
<UpsetHeading level="h2" style={{ marginTop: '1em' }}>
Element Queries
</UpsetHeading>
Expand Down
34 changes: 5 additions & 29 deletions packages/upset/src/components/ElementView/ElementVisualization.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Box } from '@mui/system';
import {
useCallback,
useContext, useEffect, useMemo, useRef, useState,
useContext, useEffect, useMemo, useRef,
} from 'react';
import { VegaLite, View } from 'react-vega';
import { useRecoilValue } from 'recoil';
Expand All @@ -11,13 +11,12 @@ import {
NumericalQuery,
} from '@visdesignlab/upset2-core';
import {
Alert, Button, IconButton,
IconButton,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { bookmarkSelector, currentIntersectionSelector, elementColorSelector } from '../../atoms/config/currentIntersectionAtom';
import { elementColorSelector } from '../../atoms/config/currentIntersectionAtom';
import { histogramSelector, scatterplotsSelector } from '../../atoms/config/plotAtoms';
import { currentNumericalQuery, elementsInBookmarkSelector, selectedElementSelector } from '../../atoms/elementsSelectors';
import { AddPlotDialog } from './AddPlotDialog';
import { generateVegaSpec } from './generatePlotSpec';
import { ProvenanceContext } from '../Root';
import { UpsetActions } from '../../provenance';
Expand Down Expand Up @@ -58,16 +57,12 @@ export const ElementVisualization = () => {
/**
* External state
*/

const [openAddPlot, setOpenAddPlot] = useState(false);
const scatterplots = useRecoilValue(scatterplotsSelector);
const histograms = useRecoilValue(histogramSelector);
const bookmarked = useRecoilValue(bookmarkSelector);
const items = useRecoilValue(elementsInBookmarkSelector);
const numericalQuery = useRecoilValue(currentNumericalQuery);
const elementSelection = useRecoilValue(selectedElementSelector);
const selectColor = useRecoilValue(elementColorSelector);
const currentIntersection = useRecoilValue(currentIntersectionSelector);
const { actions }: {actions: UpsetActions} = useContext(ProvenanceContext);

/**
Expand All @@ -90,11 +85,6 @@ export const ElementVisualization = () => {
* Functions
*/

/**
* Closes the AddPlotDialog
*/
const onClose = () => setOpenAddPlot(false);

/**
* Saves brush bounds to state when the interactive brush is used.
* @param {Plot} signaled The plot that this signal fired on
Expand Down Expand Up @@ -135,20 +125,6 @@ export const ElementVisualization = () => {
draftSelection.current = undefined;
}}
>
<Button style={{ marginTop: '0.5em' }} onClick={() => setOpenAddPlot(true)}>Add Plot</Button>
{!currentIntersection && bookmarked.length === 0 && (
<Alert
severity="info"
variant="outlined"
role="generic"
sx={{
alignItems: 'center', marginBottom: '0.5em', border: 'none', color: '#777777',
}}
>
Currently visualizing all elements. Clicking on an intersection will visualize only its elements.
</Alert>
)}
<AddPlotDialog open={openAddPlot} onClose={onClose} />
<Box sx={{
overflowX: 'auto', display: 'flex', flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-around',
}}
Expand All @@ -158,14 +134,14 @@ export const ElementVisualization = () => {
<Box style={{ display: 'inline-block', position: 'relative' }}>
<IconButton
style={{
position: 'absolute', top: 0, left: 0, zIndex: 100, padding: 0,
position: 'absolute', top: 0, right: -15, zIndex: 100, padding: 0,
}}
onClick={() => {
actions.removePlot(plot);
views.current = views.current.filter(({ plot: p }) => p.id !== plot.id);
}}
>
<CloseIcon color="primary" />
<CloseIcon fontSize="small" />
</IconButton>
<VegaLite
spec={spec}
Expand Down
29 changes: 19 additions & 10 deletions packages/upset/src/components/ElementView/QueryInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ import {
} from '@mui/material';
import { Box } from '@mui/system';
import { useRecoilValue } from 'recoil';
import { ElementQueryToBookmark, ElementQueryType } from '@visdesignlab/upset2-core';
import { ElementQueryToBookmark, ElementQueryType, NumericalQueryType } from '@visdesignlab/upset2-core';
import {
useCallback, useContext, useEffect, useState,
} from 'react';
import { queryColumnsSelector } from '../../atoms/dataAtom';
import { currentElementQuery } from '../../atoms/elementsSelectors';
import { ProvenanceContext } from '../Root';
import { UpsetActions } from '../../provenance';
import { attTypesSelector } from '../../atoms/attributeAtom';

/**
* Default type for the element query
*/
const DEFAULT_TYPE = ElementQueryType.EQUALS;

/**
* Component showing a form allowing element queries to be created based on non-numeric fields
Expand All @@ -26,12 +32,13 @@ export const QueryInterface = () => {
const atts = useRecoilValue(queryColumnsSelector);
const currentSelection = useRecoilValue(currentElementQuery);
const { actions }: { actions: UpsetActions } = useContext(ProvenanceContext);
const attTypes = useRecoilValue(attTypesSelector);

const FIELD_MARGIN = '5px';
const FIELD_CSS = { marginTop: FIELD_MARGIN, width: '50%' };

const [attField, setAttField] = useState(currentSelection?.att);
const [typeField, setTypeField] = useState<string | undefined>(currentSelection?.type);
const [attField, setAttField] = useState(currentSelection?.att ?? atts.length > 0 ? atts[0] : undefined);
const [typeField, setTypeField] = useState<string | undefined>(currentSelection?.type ?? DEFAULT_TYPE);
const [queryField, setQueryField] = useState(currentSelection?.query);

// Resets input state every time the current selection changes
Expand All @@ -41,8 +48,8 @@ export const QueryInterface = () => {
setTypeField(currentSelection?.type);
setQueryField(currentSelection?.query);
} else {
setAttField(undefined);
setTypeField(undefined);
setAttField(atts.length > 0 ? atts[0] : undefined);
setTypeField(DEFAULT_TYPE);
setQueryField(undefined);
}
}, [currentSelection]);
Expand Down Expand Up @@ -81,7 +88,7 @@ export const QueryInterface = () => {
labelId="query-att-select-label"
label="Attribute Name"
value={attField ?? ''}
onChange={(e) => setAttField(e.target.value)}
onChange={(e) => { setAttField(e.target.value); setTypeField(DEFAULT_TYPE); }}
>
{atts.map((att) => (
<MenuItem key={att} value={att}>{att}</MenuItem>
Expand All @@ -91,15 +98,17 @@ export const QueryInterface = () => {
<FormControl css={FIELD_CSS}>
<InputLabel id="query-type-select-label">Query Type</InputLabel>
<Select
disabled={!!currentSelection}
disabled={!!currentSelection || !attField}
label="Query Type"
labelId="query-type-select-label"
value={typeField ?? ''}
onChange={(e) => setTypeField(e.target.value)}
>
{Object.values(ElementQueryType).map((type) => (
<MenuItem key={type} value={type}>{type}</MenuItem>
))}
{attField && Object.values(attTypes[attField] === 'number' ? NumericalQueryType : ElementQueryType).map(
(type) => (
<MenuItem key={type} value={type}>{type}</MenuItem>
),
)}
</Select>
</FormControl>
<Box css={{ height: '56px', marginTop: FIELD_MARGIN }}>
Expand Down
Loading

0 comments on commit f0ba9ab

Please sign in to comment.