Skip to content

Commit

Permalink
Merge pull request #352 from visdesignlab/297-attribute-consolidation
Browse files Browse the repository at this point in the history
Attribute consolidation
  • Loading branch information
JakeWags committed May 2, 2024
2 parents f288640 + 6579548 commit ccecd32
Show file tree
Hide file tree
Showing 37 changed files with 702 additions and 399 deletions.
23 changes: 16 additions & 7 deletions e2e-tests/alttext.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,27 @@ test('Alt Text', async ({ page }) => {
await expect(altTextHeading).toBeVisible();

/// /////////////////
// Plot Information
// Error Handling
/// /////////////////
const plotInformation = await page.getByRole('button', { name: 'Plot Information' });
await expect(plotInformation).toBeVisible();
await plotInformation.click();

// Test error message for aggregated plots
/// Aggregates
await page.getByRole('radio', { name: 'Degree' }).check();
const aggErrMsg = await page.getByText("Alt text generation is not yet supported for aggregated plots. To generate an alt text, set aggregation to 'None' in the left sidebar.");
await expect(aggErrMsg).toBeVisible();
await page.getByRole('radio', { name: 'None' }).check();

/// Attribute Sort
await page.getByLabel('Age').locator('rect').dispatchEvent('click');
const attrSortErrMsg = await page.getByText('Alt text generation is not yet supported for attribute sorting. To generate an alt text, sort by Size, Degree, or Deviation.');
await expect(attrSortErrMsg).toBeVisible();
await page.getByText('Size', { exact: true }).dispatchEvent('click');

/// /////////////////
// Plot Information
/// /////////////////
const plotInformation = await page.getByRole('button', { name: 'Plot Information' });
await expect(plotInformation).toBeVisible();
await plotInformation.click();

const editPlotInformationButton = await page.getByLabel('Toggle editable descriptions');
await expect(editPlotInformationButton).toBeVisible();
await editPlotInformationButton.click();
Expand Down Expand Up @@ -97,7 +106,7 @@ test('Alt Text', async ({ page }) => {
/// /////////////////
// Short Description
/// /////////////////
await expect(page.getByText("This is an UpSet plot"))
await expect(page.getByText('This is an UpSet plot'))
.toContainText('This is an UpSet plot which shows set intersection of 6 sets out of 6 sets and the largest intersection is School, and Male (3). The plot is sorted by size and 12 non-empty intersections are shown.');
await page.getByRole('button', { name: 'Show More' }).click();
await page.getByRole('button', { name: 'Show Less' }).click();
Expand Down
47 changes: 0 additions & 47 deletions e2e-tests/app.spec.ts

This file was deleted.

80 changes: 80 additions & 0 deletions e2e-tests/attributeSelector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint-disable testing-library/prefer-screen-queries */
import { test, expect } from '@playwright/test';
import mockData from '../playwright/mock-data/simpsons/simpsons_data.json';
import mockAnnotations from '../playwright/mock-data/simpsons/simpsons_annotations.json';
import mockAltText from '../playwright/mock-data/simpsons/simpsons_alttxt.json';

test.beforeEach(async ({ page }) => {
await page.route('*/**/api/**', async (route) => {
const url = route.request().url();
let json;

if (url) {
if (url.includes('workspaces/Upset%20Examples/tables/simpsons/rows/?limit=9007199254740991')) {
json = mockData;
await route.fulfill({ json });
} else if (url.includes('workspaces/Upset%20Examples/tables/simpsons/annotations/')) {
json = mockAnnotations;
await route.fulfill({ json });
} else if (url.includes('alttxt')) {
json = mockAltText;
await route.fulfill({ json });
} else if (url.includes('workspaces/Upset%20Examples/sessions/table/193/state/')) {
await route.fulfill({ status: 200 });
} else {
await route.continue();
}
} else {
await route.abort();
}
});
});

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('Open attributes selection menu').click();
await page.getByRole('checkbox', { name: 'Age' }).uncheck();
await page.locator('.MuiPopover-root > .MuiBackdrop-root').click();
await expect(page.getByLabel('Age').locator('rect')).toHaveCount(0);

// Reselect and assert that it's added back to the plot
await page.getByLabel('Open attributes selection menu').click();
await page.getByLabel('Age').check();
await page.locator('.MuiPopover-root > .MuiBackdrop-root').click();
await expect(page.getByLabel('Age').locator('rect')).toBeVisible();

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

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

/// /////////////////
// Deviation
/// /////////////////
// Deselect and assert that it's removed from the plot
await page.getByLabel('Open attributes selection menu').click();
await page.getByRole('checkbox', { name: 'Deviation' }).uncheck();
await page.locator('.MuiPopover-root > .MuiBackdrop-root').click();
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('Open attributes selection menu').click();
await page.getByRole('checkbox', { name: 'Deviation' }).check();
await page.locator('.MuiPopover-root > .MuiBackdrop-root').click();
await expect(page.getByLabel('Deviation', { exact: true }).locator('rect')).toBeVisible();
});
25 changes: 14 additions & 11 deletions e2e-tests/provenance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ test.beforeEach(async ({ page }) => {
});

/**
* Asserts that trrack history works for selecting and deselecting rows, provenance tree is displayed correctly,
* Asserts that trrack history works for selecting and deselecting rows, provenance tree is displayed correctly,
* reverting to an earlier state works, elementView row deselection is trracked,
* and aggregate rows can be selected and deselected.
*/
Expand All @@ -40,6 +40,9 @@ test('Selection History', async ({ page }) => {
await page.getByLabel('Open additional options menu').click();
await page.getByLabel('Open history tree sidebar').click();

const schoolIntersection = page.locator('.css-1kek4un-Y > g:nth-child(3) > rect').first();
const duffFanIntersection = page.locator('g:nth-child(7) > .css-1kek4un-Y > g:nth-child(3) > rect').first();

// Testing history for a subset selection & deselection
await page.locator('g > circle').first().click();
await page.locator('g > circle').first().click();
Expand All @@ -54,17 +57,17 @@ test('Selection History', async ({ page }) => {
await expect(page.getByText('Deselect intersection').nth(1)).toBeVisible();

// Check that selections are maintained after de-aggregation
await page.locator('g:nth-child(4) > .css-1kek4un-Y > g:nth-child(4) > rect').click();
await schoolIntersection.click();
await page.getByRole('radio', { name: 'None' }).check();
await page.locator('.css-zf6412').click();
await expect(page.getByText('Deselect intersection').nth(2)).toBeVisible();

// Check that selections can be reverted & start a new history tree branch
await page.locator('g:nth-child(10) > circle').click();
await page.locator('.css-zf6412').click();
await page.locator('g:nth-child(7) > .css-1kek4un-Y > g:nth-child(4) > rect').click();
await await duffFanIntersection.click();
await expect(page.getByText('Deselect intersection')).toBeVisible();
await expect(page.getByText('Select intersection "Duff Fan')).toBeVisible();
await expect(page.getByText('Select intersection "School')).toBeVisible();

// Check that deselection triggered by element view unbookmarking is reflected in history tree.
// Also tests that the bookmarking & unbookmarking is trracked
Expand All @@ -73,22 +76,22 @@ test('Selection History', async ({ page }) => {
await page.locator('span.MuiChip-label+svg[data-testid="StarIcon"]').click();
await page.getByLabel('Open additional options menu').click();
await page.getByLabel('Open history tree sidebar').click();

await expect(page.getByText('Unbookmark Duff Fan & Male')).toBeVisible();
await expect(page.getByText('Deselect intersection').nth(1)).toBeVisible();
await expect(page.getByText('Bookmark Duff Fan & Male', { exact: true })).toBeVisible();
await expect(page.getByText('Select intersection "Duff Fan')).toBeVisible();
await expect(page.getByText('Select intersection "School')).toBeVisible();
});

/**
* Tests that overlap history works
* Tests that overlap history works
* & doesn't produce duplicate actions when the overlap degree is changed to the same value
*/
test('Overlap History', async ({ page }) => {
await page.goto('http://localhost:3000/?workspace=Upset+Examples&table=simpsons&sessionId=193');
await page.getByLabel('Open additional options menu').click();
await page.getByLabel('Open history tree sidebar').click();

// Ensure that duplicate actions aren't recorded for decreasing first degree
await page.getByRole('radio', { name: 'Overlaps' }).check();
await page.getByRole('spinbutton', { name: 'Degree' }).click();
Expand All @@ -112,11 +115,11 @@ test('Overlap History', async ({ page }) => {
await page.locator('div').filter({ hasText: /^Second AggregationDegreeSetsOverlapsNone$/ })
.getByLabel('Overlaps', { exact: true }).check();
await page.getByRole('spinbutton', { name: 'Degree' }).click();

// Ensure no duplicate action for decreasing 2nd degree
await page.getByRole('spinbutton', { name: 'Degree' }).press('ArrowDown');
await expect(page.getByText('Second overlap by 2')).toHaveCount(0);

// Try to increment degree to 7; confirm no duplicates
await page.getByRole('spinbutton', { name: 'Degree' }).press('ArrowUp');
await page.getByRole('spinbutton', { name: 'Degree' }).press('ArrowUp');
Expand All @@ -127,4 +130,4 @@ test('Overlap History', async ({ page }) => {
await expect(page.getByText('Second overlap by 4')).toHaveCount(1);
await expect(page.getByText('Second overlap by 5')).toHaveCount(1);
await expect(page.getByText('Second overlap by 6')).toHaveCount(1);
})
});
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"doc": "typedoc --options typedoc.json"
},
"devDependencies": {
"@playwright/test": "^1.15.0",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"eslint": "^8.6.0",
Expand Down Expand Up @@ -45,6 +44,7 @@
"eslint-config-react-app": "^7.0.1",
"vite": "^4.4.9",
"vite-plugin-dts": "^3.5.1",
"vite-tsconfig-paths": "^4.2.0"
"vite-tsconfig-paths": "^4.2.0",
"@playwright/test": "^1.15.0"
}
}
4 changes: 3 additions & 1 deletion packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { upsetConfigAtom } from './atoms/config/upsetConfigAtoms';
import { Root } from './components/Root';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { DataTable } from './components/DataTable';
import { DefaultConfig } from '@visdesignlab/upset2-core';

/** @jsxImportSource @emotion/react */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down Expand Up @@ -34,7 +35,8 @@ function App() {
conf.allSets = setList.map((set) => {return { name: set[0], size: set[1].size }})
}

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

return conf;
}
Expand Down
1 change: 0 additions & 1 deletion packages/app/src/atoms/dataAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export const dataSelector = selector<CoreUpsetData | null>({
limit: Number.MAX_SAFE_INTEGER,
})
).results;
console.log("await completed");
} catch (e) {
return null;
}
Expand Down
30 changes: 23 additions & 7 deletions packages/app/src/components/AttributeDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import {
Container,
TextField,
} from "@mui/material"
import { useContext } from "react"
import { useContext, useMemo } from "react"
import { ProvenanceContext } from "./Root"
import { dataSelector } from "../atoms/dataAtom";
import { useRecoilValue } from "recoil";
import { useState } from "react";
import { CoreUpsetData } from "@visdesignlab/upset2-core";
import { CoreUpsetData, DefaultConfig } from "@visdesignlab/upset2-core";

/**
* Get the count of items that have a specific attribute.
Expand All @@ -29,6 +29,9 @@ import { CoreUpsetData } from "@visdesignlab/upset2-core";
* @returns The count of items with the specified attribute value.
*/
const getAttributeItemCount = (attribute: string, data: CoreUpsetData) => {
if (DefaultConfig.visibleAttributes.includes(attribute)) {
return '';
}
let count = 0;

Object.values(data.items).forEach((item) => {
Expand Down Expand Up @@ -63,11 +66,17 @@ export const AttributeDropdown = (props: {anchorEl: HTMLElement, close: () => vo
);

const [ searchTerm, setSearchTerm ] = useState<string>("");
const attributeItemCount: { [attr: string]: number } = {};

data?.attributeColumns.forEach((attr) => {
attributeItemCount[attr] = getAttributeItemCount(attr,data);
})
const attributeItemCount: { [attr: string]: number | string } = {};

const attributes = data ? [...data.attributeColumns, ...DefaultConfig.visibleAttributes]: [...DefaultConfig.visibleAttributes];


if (data) {
attributes.forEach((attr) => {
attributeItemCount[attr] = getAttributeItemCount(attr,data);
})
}

/**
* Handle checkbox toggle: add or remove the attribute from the visible attributes
Expand All @@ -84,6 +93,13 @@ export const AttributeDropdown = (props: {anchorEl: HTMLElement, close: () => vo
newChecked.push(attr);
}

// move 'Degree' to the first element if it is selected
const degreeIndex = newChecked.indexOf('Degree');
if (degreeIndex !== -1) {
newChecked.splice(degreeIndex, 1); // Remove 'Degree' from its current position
newChecked.unshift('Degree'); // Move 'Degree' to the beginning
}

setChecked(newChecked);
actions.addMultipleAttributes(newChecked);
}
Expand All @@ -104,7 +120,7 @@ export const AttributeDropdown = (props: {anchorEl: HTMLElement, close: () => vo
if (data === undefined || data === null) {
return []
}
return data.attributeColumns.map((attr, index) => {
return attributes.map((attr, index) => {
return {
id: index,
attribute: attr,
Expand Down
8 changes: 6 additions & 2 deletions packages/app/src/components/Body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ export const Body = ({ yOffset, data, config }: Props) => {
/**
* Generates alt text for a plot based on the current state and configuration.
* If an error occurs during the generation, an error message is returned.
* Alt text generation currently is not supported for aggregated plots
* and an error message is returned if the plot is aggregated.
* Alt text generation currently is not supported for aggregated or attribute sorted plots
* and an error message is returned.
* @throws Error with descriptive message if an error occurs while generating the alttxt
* @returns A promise that resolves to the generated alt text.
*/
Expand All @@ -66,6 +66,10 @@ export const Body = ({ yOffset, data, config }: Props) => {
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 attribute sorting. To generate an alt text, sort by Size, Degree, or Deviation.");
}

let response;
try {
response = await api.generateAltText(true, config);
Expand Down

0 comments on commit ccecd32

Please sign in to comment.