Skip to content

Commit

Permalink
Merge pull request #358 from visdesignlab/146-sort-by-set
Browse files Browse the repository at this point in the history
Sort By Set
  • Loading branch information
JakeWags committed May 13, 2024
2 parents ccecd32 + 0ccfdf9 commit fd02989
Show file tree
Hide file tree
Showing 16 changed files with 353 additions and 70 deletions.
13 changes: 9 additions & 4 deletions e2e-tests/alttext.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* 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';
Expand Down Expand Up @@ -57,16 +56,22 @@ test('Alt Text', async ({ page }) => {
/// /////////////////
/// 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.");
const aggErrMsg = 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.');
const attrSortErrMsg = 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');

/// Set Sort
await page.locator('p').filter({ hasText: /^Male$/ }).click();
const setSortErrMsg = page.getByText('Alt text generation is not yet supported for set sorting. To generate an alt text, sort by Size, Degree, or Deviation.');
await expect(setSortErrMsg).toBeVisible();

// reset sorting to size
await page.getByText('Size', { exact: true }).dispatchEvent('click');
/// /////////////////
// Plot Information
/// /////////////////
Expand Down
1 change: 0 additions & 1 deletion e2e-tests/attributeSelector.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* 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';
Expand Down
1 change: 0 additions & 1 deletion e2e-tests/datatable.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* 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';
Expand Down
1 change: 0 additions & 1 deletion e2e-tests/elementView.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* 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';
Expand Down
21 changes: 10 additions & 11 deletions e2e-tests/plot.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* 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';
Expand Down Expand Up @@ -48,8 +47,8 @@ async function toggleAdvancedScale(page) {
* @param setName name of set
*/
async function removeSetByName(page, setName: string) {
await page.locator('p').filter({ hasText: new RegExp('^' + setName + '$') }).click({ button: 'right' });
await page.getByRole('menuitem', { name: 'Remove Set: ' + setName }).click();
await page.locator('p').filter({ hasText: new RegExp(`^${setName}$`) }).click({ button: 'right' });
await page.getByRole('menuitem', { name: `Remove Set: ${setName}` }).click();
}

/**
Expand All @@ -59,9 +58,9 @@ async function removeSetByName(page, setName: string) {
*/
async function addSetByName(page, setName: string) {
await page.getByText(setName, { exact: true }).click({
button: 'right'
button: 'right',
});
await page.getByRole('menuitem', { name: 'Add Set: ' + setName }).click();
await page.getByRole('menuitem', { name: `Add Set: ${setName}` }).click();
}

/**
Expand All @@ -71,7 +70,7 @@ async function addSetByName(page, setName: string) {
*/
async function assertSizeScaleMax(page, max: number) {
await expect(page.locator('g.details-scale > g > g:last-child > text').nth(1))
.toHaveText(new RegExp('^' + max + '$'));
.toHaveText(new RegExp(`^${max}$`));
}

/**
Expand All @@ -88,22 +87,22 @@ test('Size header', async ({ page }) => {
// Ensure that the scale decreases when necessary upon set removal
await removeSetByName(page, 'Male');
await assertSizeScaleMax(page, 8);

// Ensure that the scale updates upon set addition
await addSetByName(page, 'Male');
await assertSizeScaleMax(page, 5);

// Ensure that dragging the advanced slider works
await toggleAdvancedScale(page);
await page.locator('g').filter({ hasText: /^0055101015152020Size001122334455$/ }).locator('rect').nth(1).
dragTo(page.getByText('15', { exact: true }).nth(1), {force: true});
await page.locator('g').filter({ hasText: /^0055101015152020Size001122334455$/ }).locator('rect').nth(1)
.dragTo(page.getByText('15', { exact: true }).nth(1), { force: true });
await assertSizeScaleMax(page, 15);

// Ensure that adding sets doesn't affect the advanced scale
await addSetByName(page, 'Blue Hair');
await assertSizeScaleMax(page, 15);

// Ensure that scale recalculates correctly when advanced is turned off
await toggleAdvancedScale(page);
await assertSizeScaleMax(page, 3);
});
});
1 change: 0 additions & 1 deletion e2e-tests/provenance.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* 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';
Expand Down
180 changes: 180 additions & 0 deletions e2e-tests/sort.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { test, expect, Page } 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();
}
});
});

const SIZE_ORDER = {
Ascending: [
'Subset_School',
'Subset_School~&~Evil~&~Male',
'Subset_School~&~Blue_Hair~&~Male',
'Subset_Duff_Fan~&~Evil~&~Male',
'Subset_Evil~&~Male',
],
Descending: [
'Subset_School~&~Male',
'Subset_Unincluded',
'Subset_Male',
'Subset_Duff_Fan~&~Male~&~Power Plant',
'Subset_Evil~&~Male',
],
};

const DEGREE_ORDER = {
Ascending: [
'Subset_Unincluded',
'Subset_Blue_Hair',
'Subset_Male',
'Subset_School',
'Subset_Duff_Fan~&~Male',
],
Descending: [
'Subset_Duff_Fan~&~Evil~&~Male',
'Subset_Duff_Fan~&~Male~&~Power Plant',
'Subset_Evil~&~Male~&~Power_Plant',
'Subset_School~&~Blue_Hair~&~Male',
'Subset_School~&~Evil~&~Male',
],
};

const DEVIATION_ORDER = {
Ascending: [
'Subset_Male',
'Subset_Evil~&~Male',
'Subset_Duff_Fan~&~Male',
'Subset_School',
'Subset_School~&~Evil~&~Male',
],
Descending: [
'Subset_Duff_Fan~&~Male~&~Power Plant',
'Subset_Blue_Hair',
'Subset_Evil~&~Male~&~Power_Plant',
'Subset_School~&~Male',
'Subset_Unincluded',
],
};

// Note: the keys in this object represent the VISIBLE SET sort order
const SET_MALE_ORDER = {
Alphabetical: [
'Subset_Male',
'Subset_Duff_Fan~&~Male',
'Subset_Evil~&~Male',
'Subset_School~&~Male',
'Subset_Duff_Fan~&~Evil~&~Male',
],
Ascending: [
'Subset_Male',
'Subset_School~&~Male',
'Subset_Evil~&~Male',
'Subset_Duff_Fan~&~Male',
'Subset_School~&~Blue_Hair~&~Male',
],
Descending: [
'Subset_Male',
'Subset_School~&~Male',
'Subset_Evil~&~Male',
'Subset_Duff_Fan~&~Male',
'Subset_School~&~Evil~&~Male',
],
};

/**
* Compares the sorted elements on a page with the given order.
*
* @param {Page} page - The playwright page object.
* @param {string[]} order - The expected order of elements.
* @returns {Promise<void>} - A promise that resolves when the comparison is complete.
*/
async function compareSortedElements(page: Page, order: string[]) {
const gElements = await page.locator('#matrixRows > g').all();

const res = (await Promise.all(gElements.map((gElement) => gElement.innerHTML()))).slice(0, 5);

for (let i = 0; i < order.length; i++) {
expect(res[i]).toContain(order[i]);
}
}

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

/// Ascending
await page.getByText('Size', { exact: true }).dispatchEvent('click');
await compareSortedElements(page, SIZE_ORDER.Ascending);

/// Descending
await page.getByText('Size', { exact: true }).dispatchEvent('click');
await compareSortedElements(page, SIZE_ORDER.Descending);
});

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

/// Ascending
await page.getByText('#').dispatchEvent('click');
await compareSortedElements(page, DEGREE_ORDER.Ascending);

/// Descending
await page.getByText('#').dispatchEvent('click');
await compareSortedElements(page, DEGREE_ORDER.Descending);
});

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

/// Ascending
await page.getByLabel('Deviation', { exact: true }).locator('rect').dispatchEvent('click');
await compareSortedElements(page, DEVIATION_ORDER.Ascending);

/// Descending
await page.getByLabel('Deviation', { exact: true }).locator('rect').dispatchEvent('click');
await compareSortedElements(page, DEVIATION_ORDER.Descending);
});

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

/// Only one option for sortOrder for sets
await page.locator('p').filter({ hasText: /^Male$/ }).dispatchEvent('click');
await compareSortedElements(page, SET_MALE_ORDER.Alphabetical);

// Sort visible sets by size, ascending
// male is largest, so this should put it at the end.
// This will alter the order of the sets
await page.locator('p').filter({ hasText: /^Male$/ }).dispatchEvent('contextmenu');
await page.getByRole('menuitem', { name: 'Sort Sets by Size - Ascending' }).dispatchEvent('click');

await compareSortedElements(page, SET_MALE_ORDER.Ascending);

await page.locator('p').filter({ hasText: /^Male$/ }).dispatchEvent('contextmenu');
await page.getByRole('menuitem', { name: 'Sort Sets by Size - Descending' }).dispatchEvent('click');

await compareSortedElements(page, SET_MALE_ORDER.Descending);
});
2 changes: 1 addition & 1 deletion packages/app/src/components/AttributeDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const AttributeDropdown = (props: {anchorEl: HTMLElement, close: () => vo

const attributeItemCount: { [attr: string]: number | string } = {};

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


if (data) {
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/Body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const Body = ({ yOffset, data, config }: Props) => {
}

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.");
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.`);
}

let response;
Expand Down
48 changes: 47 additions & 1 deletion packages/core/src/sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Sets,
SortByOrder,
SixNumberSummary,
getBelongingSetsFromSetMembership,
} from './types';
import { deepCopy } from './utils';

Expand Down Expand Up @@ -109,6 +110,47 @@ function sortByDeviation(rows: Intersections, sortByOrder?: SortByOrder) {
return { values, order };
}

/**
* Sorts the rows based on a specified set. This should move the selected set to the top of the list, and sort the rest of the rows by degree ascending.
*
* @param rows - The rows to be sorted.
* @param sortBy - The set to sort by.
* @param vSetSortBy - The sort order for visible sets.
* @param visibleSets - The visible sets.
* @returns The sorted rows.
*/
function sortBySet(rows: Intersections, sortBy: string, vSetSortBy: SortVisibleBy, visibleSets: Sets) {
// create the subset of rows which contain sortBy in their set list
const setMembers: Intersections = { values: {}, order: [] };

// setMembers needs to be a filtered list of values that contain the set membership, as well as order which corresponds
const setMemberEntries = Object.entries(rows.values).filter(([, value]) => getBelongingSetsFromSetMembership(value.setMembership).includes(sortBy));
setMemberEntries.forEach(([key, value]) => {
setMembers.values[key] = value;
setMembers.order.push(key);
});

// create the subset of rows which do NOT contain sortBy in their set list
const nonSetMembers: Intersections = { values: {}, order: [] };

// filter the list for entries which do not have the selected set as members
const nonSetMemberEntries = Object.entries(rows.values).filter(([, value]) => !getBelongingSetsFromSetMembership(value.setMembership).includes(sortBy));
nonSetMemberEntries.forEach(([key, value]) => {
nonSetMembers.values[key] = value;
nonSetMembers.order.push(key);
});

// sort each of the two by degree (Ascending)
const sortedSetMembers = sortByDegree(setMembers, vSetSortBy, visibleSets, 'Ascending');
const sortedNonSetMembers = sortByDegree(nonSetMembers, vSetSortBy, visibleSets, 'Ascending');

// combine the two sorted row lists
const sortedOrder = [...sortedSetMembers.order, ...sortedNonSetMembers.order];
const sortedValues = { ...sortedSetMembers.values, ...sortedNonSetMembers.values };

return { values: sortedValues, order: sortedOrder };
}

/**
* @param {Intersections} rows - The intersections object containing values and order.
* @param {string} sortBy - The attribute to sort by
Expand Down Expand Up @@ -151,11 +193,15 @@ function sortByAttribute(rows: Intersections, sortBy: string, sortByOrder?: Sort
*/
function sortIntersections<T extends Intersections>(
intersections: T,
sortBy: SortBy,
sortBy: string,
vSetSortBy: SortVisibleBy,
visibleSets: Sets,
sortByOrder?: SortByOrder,
) {
if (sortBy.includes('Set_')) {
return sortBySet(intersections, sortBy, vSetSortBy, visibleSets);
}

switch (sortBy) {
case 'Size':
return sortBySize(intersections, sortByOrder);
Expand Down

0 comments on commit fd02989

Please sign in to comment.