From 410ae38d4a3725c39094916b1638caef0793623f Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Thu, 22 Aug 2024 14:38:24 -0600 Subject: [PATCH 01/24] Add QueryType --- packages/core/src/types.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 81743c3f..ffc227c1 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -34,6 +34,18 @@ export type PlotInformation = { caption?: string; }; +/** + * Possible string types for an element query + */ +// linter is saying this is already declared... on this line +// eslint-disable-next-line no-shadow +export enum QueryType { + EQUALS = 'equals', + CONTAINS = 'contains', + LENGTH = 'length', + REGEX = 'regex' +} + /** * Represents a row in the UpSet plot. * @privateRemarks typechecked by isRowType in typecheck.ts; changes here must be reflected there From 09fe5c5da29e76f96fbc129c152c4b48d666b3d4 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Thu, 22 Aug 2024 14:38:35 -0600 Subject: [PATCH 02/24] Add queryColumnsSelector --- packages/upset/src/atoms/dataAtom.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/upset/src/atoms/dataAtom.ts b/packages/upset/src/atoms/dataAtom.ts index 4006b552..65f09c27 100644 --- a/packages/upset/src/atoms/dataAtom.ts +++ b/packages/upset/src/atoms/dataAtom.ts @@ -1,5 +1,5 @@ -import { CoreUpsetData } from '@visdesignlab/upset2-core'; -import { atom } from 'recoil'; +import { ColumnName, CoreUpsetData } from '@visdesignlab/upset2-core'; +import { atom, selector } from 'recoil'; /** * Atom to store the data for the Upset plot @@ -8,3 +8,17 @@ export const dataAtom = atom>({ key: 'data', default: {}, }); + +/** + * Returns all columns that can be used in a string query, ie are not numeric, not set columns, + * and not private columns that start with _ + */ +export const queryColumnsSelector = selector({ + key: 'data-columns', + get: ({ get }) => { + const data = get(dataAtom); + return data.columns.filter((col) => data.columnTypes[col] !== 'number' + && !data.setColumns.includes(col) + && !col.startsWith('_')); + }, +}); From 1cd47b7fe912af462ffd7bf95ae5d7eb6e71fc4f Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Thu, 22 Aug 2024 14:39:31 -0600 Subject: [PATCH 03/24] Add basic UI for element queries with no functionality --- .../{ElementQueries.tsx => BookmarkChips.tsx} | 2 +- .../components/ElementView/ElementSidebar.tsx | 12 +++- .../components/ElementView/QueryInterface.tsx | 56 +++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) rename packages/upset/src/components/ElementView/{ElementQueries.tsx => BookmarkChips.tsx} (99%) create mode 100644 packages/upset/src/components/ElementView/QueryInterface.tsx diff --git a/packages/upset/src/components/ElementView/ElementQueries.tsx b/packages/upset/src/components/ElementView/BookmarkChips.tsx similarity index 99% rename from packages/upset/src/components/ElementView/ElementQueries.tsx rename to packages/upset/src/components/ElementView/BookmarkChips.tsx index d63f143b..8d37f6c6 100644 --- a/packages/upset/src/components/ElementView/ElementQueries.tsx +++ b/packages/upset/src/components/ElementView/BookmarkChips.tsx @@ -24,7 +24,7 @@ import { elementSelectionParameters, selectedElementSelector } from '../../atoms * Shows a stack of chips representing bookmarks and the current intersection/element selection, * with options to add and remove bookmarks */ -export const ElementQueries = () => { +export const BookmarkChips = () => { const { provenance, actions }: {provenance: UpsetProvenance, actions: UpsetActions} = useContext(ProvenanceContext); const currentIntersection = useRecoilValue(currentIntersectionSelector); const colorPallete = useRecoilValue(bookmarkedColorPalette); diff --git a/packages/upset/src/components/ElementView/ElementSidebar.tsx b/packages/upset/src/components/ElementView/ElementSidebar.tsx index 6d02bc6e..10e0ddd0 100644 --- a/packages/upset/src/components/ElementView/ElementSidebar.tsx +++ b/packages/upset/src/components/ElementView/ElementSidebar.tsx @@ -17,11 +17,12 @@ import { elementSelector, intersectionCountSelector, selectedElementSelector, selectedItemsCounter, selectedItemsSelector, } from '../../atoms/elementsSelectors'; -import { ElementQueries } from './ElementQueries'; +import { BookmarkChips } from './BookmarkChips'; import { ElementTable } from './ElementTable'; import { ElementVisualization } from './ElementVisualization'; import { UpsetActions } from '../../provenance'; import { ProvenanceContext } from '../Root'; +import { QueryInterface } from './QueryInterface'; /** * Props for the ElementSidebar component @@ -220,15 +221,20 @@ export const ElementSidebar = ({ open, close }: Props) => { - Element Queries + Bookmarked Queries - + Element Visualization + + Element Queries + + + Query Result { + /** + * State + */ + + const atts = useRecoilValue(queryColumnsSelector); + + const FIELD_MARGIN = '5px'; + const FIELD_CSS = { marginTop: FIELD_MARGIN, width: '50%' }; + + return ( + + + Attribute Name + + + + Query Type + + + + + + + + ); +}; From e07b1c8ee5066f9ce423c4faa237a57ef72009c5 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Fri, 23 Aug 2024 12:23:49 -0600 Subject: [PATCH 04/24] Rename ElementSelection type to NumericalQuery --- packages/core/src/typecheck.ts | 10 +++++----- packages/core/src/types.ts | 7 ++++--- packages/core/src/typeutils.ts | 6 +++--- packages/upset/src/atoms/elementsSelectors.ts | 4 ++-- .../ElementView/ElementVisualization.tsx | 8 ++++---- .../src/components/ElementView/generatePlotSpec.ts | 14 +++++++------- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/packages/core/src/typecheck.ts b/packages/core/src/typecheck.ts index a21cec2c..f93d91c7 100644 --- a/packages/core/src/typecheck.ts +++ b/packages/core/src/typecheck.ts @@ -1,5 +1,5 @@ import { - Aggregate, AggregateBy, aggregateByList, AltText, AttributePlots, AttributePlotType, BaseElement, BaseIntersection, Bookmark, BookmarkedSelection, Column, ElementSelection, Histogram, PlotInformation, Row, RowType, Scatterplot, Subset, Subsets, UpsetConfig, + Aggregate, AggregateBy, aggregateByList, AltText, AttributePlots, AttributePlotType, BaseElement, BaseIntersection, Bookmark, BookmarkedSelection, Column, NumericalQuery, Histogram, PlotInformation, Row, RowType, Scatterplot, Subset, Subsets, UpsetConfig, } from './types'; import { deepCopy } from './utils'; @@ -98,11 +98,11 @@ export function isAltText(val: unknown): val is AltText { } /** - * Validates that the given value is an ElementSelection. + * Validates that the given value is a NumericalQuery. * @param value The value to check. - * @returns whether the value is an ElementSelection. + * @returns whether the value is a NumericalQuery. */ -export function isElementSelection(value: unknown): value is ElementSelection { +export function isNumericalQuery(value: unknown): value is NumericalQuery { return ( !!value && typeof value === 'object' @@ -255,7 +255,7 @@ export function isBookmarkedSelection(b: unknown): b is BookmarkedSelection { return isBookmark(b) && b.type === 'elements' && Object.hasOwn(b, 'selection') - && isElementSelection((b as BookmarkedSelection).selection); + && isNumericalQuery((b as BookmarkedSelection).selection); } /** diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ffc227c1..21545db3 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -333,7 +333,8 @@ export type BookmarkedIntersection = Bookmark & { } /** - * Represents a selection of elements in the Element View. + * Represents a selection of elements based on their numerical attributes, + * currently only from brushes in the element view. * Maps attribute names to an array with the minimum and maximum * values of the selection over each attribute. * @@ -342,7 +343,7 @@ export type BookmarkedIntersection = Bookmark & { * upset/src/components/ElementView/ElementVisualization.tsx. * This is typechecked by isElementSelection in typecheck.ts; changes here must be reflected there. */ -export type ElementSelection = {[attName: string] : [number, number]}; +export type NumericalQuery = {[attName: string] : [number, number]}; /** * Represents a bookmarked element selection, created in the Element View. @@ -352,7 +353,7 @@ export type BookmarkedSelection = Bookmark & { /** * The selection parameters */ - selection: ElementSelection; + selection: NumericalQuery; /** * Indicates type at runtime */ diff --git a/packages/core/src/typeutils.ts b/packages/core/src/typeutils.ts index 7eb5b750..29133f63 100644 --- a/packages/core/src/typeutils.ts +++ b/packages/core/src/typeutils.ts @@ -1,6 +1,6 @@ import { Aggregate, - Aggregates, Bookmark, BookmarkedIntersection, BookmarkedSelection, ElementSelection, Row, Rows, SetMembershipStatus, Subset, Subsets, + Aggregates, Bookmark, BookmarkedIntersection, BookmarkedSelection, NumericalQuery, Row, Rows, SetMembershipStatus, Subset, Subsets, } from './types'; import { hashString } from './utils'; @@ -69,7 +69,7 @@ export function isBookmarkedIntersection(b: Bookmark): b is BookmarkedIntersecti * @param {number} decimalPlaces The number of decimal places to use when comparing equality of numbers, default 4 * @returns Whether a and b are equal */ -export function elementSelectionsEqual(a: ElementSelection | undefined, b: ElementSelection | undefined, decimalPlaces = 4): boolean { +export function numericalQueriesEqual(a: NumericalQuery | undefined, b: NumericalQuery | undefined, decimalPlaces = 4): boolean { // We want undefined == {} if (!a || Object.keys(a).length === 0) { return (!b || Object.keys(b).length === 0); @@ -98,7 +98,7 @@ export function elementSelectionsEqual(a: ElementSelection | undefined, b: Eleme * @param selection The numerical attribute query. * @returns The element selection. */ -export function elementSelectionToBookmark(selection: ElementSelection): BookmarkedSelection { +export function numericalQueryToBookmark(selection: NumericalQuery): BookmarkedSelection { // Normalizing prevents floating point error from causing different hashes const norm = (i : number) => Math.abs(Math.round(i * 10000)); diff --git a/packages/upset/src/atoms/elementsSelectors.ts b/packages/upset/src/atoms/elementsSelectors.ts index 2a8be879..b50362b6 100644 --- a/packages/upset/src/atoms/elementsSelectors.ts +++ b/packages/upset/src/atoms/elementsSelectors.ts @@ -1,7 +1,7 @@ import { Aggregate, BaseIntersection, - BookmarkedSelection, ElementSelection, Item, flattenedOnlyRows, getItems, + BookmarkedSelection, NumericalQuery, Item, flattenedOnlyRows, getItems, } from '@visdesignlab/upset2-core'; import { selector, selectorFamily } from 'recoil'; import { @@ -109,7 +109,7 @@ export const selectedElementSelector = selector({ * Gets the parameters for the current selection of elements, * ie the 'selected' property of the selectedElementsSelector */ -export const elementSelectionParameters = selector({ +export const elementSelectionParameters = selector({ key: 'config-current-element-selection', get: ({ get }) => get(selectedElementSelector)?.selection, }); diff --git a/packages/upset/src/components/ElementView/ElementVisualization.tsx b/packages/upset/src/components/ElementView/ElementVisualization.tsx index 969a5e05..3d9ec201 100644 --- a/packages/upset/src/components/ElementView/ElementVisualization.tsx +++ b/packages/upset/src/components/ElementView/ElementVisualization.tsx @@ -5,7 +5,7 @@ import { import { SignalListener, VegaLite } from 'react-vega'; import { useRecoilValue } from 'recoil'; -import { elementSelectionToBookmark, elementSelectionsEqual, isElementSelection } from '@visdesignlab/upset2-core'; +import { numericalQueryToBookmark, numericalQueriesEqual, isNumericalQuery } from '@visdesignlab/upset2-core'; import { Button } from '@mui/material'; import { bookmarkSelector, elementColorSelector } from '../../atoms/config/currentIntersectionAtom'; import { histogramSelector, scatterplotsSelector } from '../../atoms/config/plotAtoms'; @@ -59,7 +59,7 @@ export const ElementVisualization = () => { * to an array of the bounds of the brush, but Vega's output format can change if the spec changes. */ const brushHandler: SignalListener = (_: string, value: unknown) => { - if (!isElementSelection(value)) return; + if (!isNumericalQuery(value)) return; draftSelection.current = value; }; @@ -70,9 +70,9 @@ export const ElementVisualization = () => { if ( draftSelection.current && Object.keys(draftSelection.current).length > 0 - && !elementSelectionsEqual(draftSelection.current, elementSelection) + && !numericalQueriesEqual(draftSelection.current, elementSelection) ) { - actions.setElementSelection(elementSelectionToBookmark(draftSelection.current)); + actions.setElementSelection(numericalQueryToBookmark(draftSelection.current)); } else { actions.setElementSelection(null); } diff --git a/packages/upset/src/components/ElementView/generatePlotSpec.ts b/packages/upset/src/components/ElementView/generatePlotSpec.ts index 36d3028d..9c5f0a4d 100644 --- a/packages/upset/src/components/ElementView/generatePlotSpec.ts +++ b/packages/upset/src/components/ElementView/generatePlotSpec.ts @@ -1,10 +1,10 @@ import { - ElementSelection, Histogram, isHistogram, isScatterplot, Plot, Scatterplot, + NumericalQuery, Histogram, isHistogram, isScatterplot, Plot, Scatterplot, } from '@visdesignlab/upset2-core'; import { VisualizationSpec } from 'react-vega'; /** - * Converts an elementselection to a value for a vega param. + * Converts an NumericalQuery to a value for a vega param. * Plots want x and y ranges instead of attribute ranges, so we need to convert the selection to match. * If this plot's axis don't match the selection attributes, we return undefined to avoid conflicting selections. * @param plot The plot that we need a selection value for @@ -12,8 +12,8 @@ import { VisualizationSpec } from 'react-vega'; * @returns An object which can be assigned to the 'value' field of a vega param in the plot * to display the selection in the plot. */ -function convertSelection(plot: Plot, select: ElementSelection): ElementSelection | undefined { - let val: ElementSelection | undefined; +function convertSelection(plot: Plot, select: NumericalQuery): NumericalQuery | undefined { + let val: NumericalQuery | undefined; if (isScatterplot(plot) && select[plot.x] && select[plot.y]) { val = { x: select[plot.x], @@ -81,7 +81,7 @@ export function createScatterplotSpec( */ export function createScatterplotRow( specs: Scatterplot[], - selection: ElementSelection | undefined, + selection: NumericalQuery | undefined, selectColor: string, ): VisualizationSpec[] { return specs.map((s) => ({ @@ -218,7 +218,7 @@ export function createHistogramSpec( */ export function createHistogramRow( histograms: Histogram[], - selection: ElementSelection | undefined, + selection: NumericalQuery | undefined, ) : VisualizationSpec[] { function makeParams(plot: Histogram) { @@ -346,7 +346,7 @@ export function generateVega( scatterplots: Scatterplot[], histograms: Histogram[], selectColor: string, - selection? : ElementSelection, + selection? : NumericalQuery, ): VisualizationSpec { // If we have an empty selection {}, we need to feed undefined to the specs, but !!{} is true const newSelection = selection && Object.keys(selection).length > 0 ? selection : undefined; From d94ea6e33eaf8731428a25bc8580ad4950a083ae Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Fri, 23 Aug 2024 13:05:15 -0600 Subject: [PATCH 05/24] Add type for string queries --- packages/core/src/types.ts | 70 ++++++++++++------- .../components/ElementView/QueryInterface.tsx | 4 +- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 21545db3..507ec486 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -34,18 +34,6 @@ export type PlotInformation = { caption?: string; }; -/** - * Possible string types for an element query - */ -// linter is saying this is already declared... on this line -// eslint-disable-next-line no-shadow -export enum QueryType { - EQUALS = 'equals', - CONTAINS = 'contains', - LENGTH = 'length', - REGEX = 'regex' -} - /** * Represents a row in the UpSet plot. * @privateRemarks typechecked by isRowType in typecheck.ts; changes here must be reflected there @@ -299,6 +287,51 @@ export enum AttributePlotType { */ export type AttributePlots = Record; +/** + * Possible string types for an element query + */ +// linter is saying this is already declared... on this line +// eslint-disable-next-line no-shadow +export enum StringQueryType { + EQUALS = 'equals', + CONTAINS = 'contains', + LENGTH = 'length', + REGEX = 'regex' +} + +/** + * Represents a selection of elements based on a string attribute. + */ +export type StringQuery = { + /** + * Name of the attribute being queried upon + */ + att: string, + /** + * Type of the query; determines the mechanism used to evaluate whether the value of att + * on a given element matches this query + */ + type: StringQueryType, + /** + * The query string. To be included in this query, the value of att on a given + * element must match this query string according to the rules set by the type. + */ + query: string, +} + +/** + * Represents a selection of elements based on their numerical attributes, + * currently only from brushes in the element view. + * Maps attribute names to an array with the minimum and maximum + * values of the selection over each attribute. + * + * @privateRemarks + * This *needs* to match the data format outputted by Vega-Lite to the 'brush' signal in + * upset/src/components/ElementView/ElementVisualization.tsx. + * This is typechecked by isElementSelection in typecheck.ts; changes here must be reflected there. + */ +export type NumericalQuery = {[attName: string] : [number, number]}; + /** * Base representation of a bookmarkable type * @privateRemarks typechecked by isBookmark in typecheck.ts; changes here must be reflected there @@ -332,19 +365,6 @@ export type BookmarkedIntersection = Bookmark & { type: 'intersection'; } -/** - * Represents a selection of elements based on their numerical attributes, - * currently only from brushes in the element view. - * Maps attribute names to an array with the minimum and maximum - * values of the selection over each attribute. - * - * @privateRemarks - * This *needs* to match the data format outputted by Vega-Lite to the 'brush' signal in - * upset/src/components/ElementView/ElementVisualization.tsx. - * This is typechecked by isElementSelection in typecheck.ts; changes here must be reflected there. - */ -export type NumericalQuery = {[attName: string] : [number, number]}; - /** * Represents a bookmarked element selection, created in the Element View. * @privateRemarks typechecked by isBookmarkedSelection in typecheck.ts; changes here must be reflected there diff --git a/packages/upset/src/components/ElementView/QueryInterface.tsx b/packages/upset/src/components/ElementView/QueryInterface.tsx index 9072a777..e582c89a 100644 --- a/packages/upset/src/components/ElementView/QueryInterface.tsx +++ b/packages/upset/src/components/ElementView/QueryInterface.tsx @@ -5,7 +5,7 @@ import { } from '@mui/material'; import { Box } from '@mui/system'; import { useRecoilValue } from 'recoil'; -import { QueryType } from '@visdesignlab/upset2-core'; +import { StringQueryType } from '@visdesignlab/upset2-core'; import { queryColumnsSelector } from '../../atoms/dataAtom'; /** @@ -35,7 +35,7 @@ export const QueryInterface = () => { Query Type From 63b8da6c17141f337ebaaedc0ea5304555ca189c Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Fri, 23 Aug 2024 13:09:04 -0600 Subject: [PATCH 06/24] Rename BookmarkedSelection to NumericalBookmark --- packages/core/src/convertConfig.ts | 4 ++-- packages/core/src/typecheck.ts | 10 +++++----- packages/core/src/types.ts | 10 +++++----- packages/core/src/typeutils.ts | 4 ++-- packages/upset/src/atoms/elementsSelectors.ts | 4 ++-- .../upset/src/components/ElementView/BookmarkChips.tsx | 8 ++++---- packages/upset/src/provenance/index.ts | 10 +++++----- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/core/src/convertConfig.ts b/packages/core/src/convertConfig.ts index 6d09fa58..9098aec7 100644 --- a/packages/core/src/convertConfig.ts +++ b/packages/core/src/convertConfig.ts @@ -1,5 +1,5 @@ import { - AggregateBy, Bookmark, BookmarkedSelection, Column, ColumnName, Histogram, PlotInformation, Row, Scatterplot, + AggregateBy, Bookmark, NumericalBookmark, Column, ColumnName, Histogram, PlotInformation, Row, Scatterplot, SortByOrder, SortVisibleBy, UpsetConfig, } from './types'; import { isUpsetConfig } from './typecheck'; @@ -52,7 +52,7 @@ type PreVersionConfig = { }; allSets: Column[]; selected: Row | null; - elementSelection: BookmarkedSelection | null; + elementSelection: NumericalBookmark | null; }; /** diff --git a/packages/core/src/typecheck.ts b/packages/core/src/typecheck.ts index f93d91c7..5642251a 100644 --- a/packages/core/src/typecheck.ts +++ b/packages/core/src/typecheck.ts @@ -1,5 +1,5 @@ import { - Aggregate, AggregateBy, aggregateByList, AltText, AttributePlots, AttributePlotType, BaseElement, BaseIntersection, Bookmark, BookmarkedSelection, Column, NumericalQuery, Histogram, PlotInformation, Row, RowType, Scatterplot, Subset, Subsets, UpsetConfig, + Aggregate, AggregateBy, aggregateByList, AltText, AttributePlots, AttributePlotType, BaseElement, BaseIntersection, Bookmark, NumericalBookmark, Column, NumericalQuery, Histogram, PlotInformation, Row, RowType, Scatterplot, Subset, Subsets, UpsetConfig, } from './types'; import { deepCopy } from './utils'; @@ -247,15 +247,15 @@ export function isRow(r: unknown): r is Row { } /** - * Type guard for BookmarkedSelection + * Type guard for NumericalBookmark * @param b variable to check * @returns {boolean} */ -export function isBookmarkedSelection(b: unknown): b is BookmarkedSelection { +export function isNumericalBookmark(b: unknown): b is NumericalBookmark { return isBookmark(b) && b.type === 'elements' && Object.hasOwn(b, 'selection') - && isNumericalQuery((b as BookmarkedSelection).selection); + && isNumericalQuery((b as NumericalBookmark).selection); } /** @@ -505,7 +505,7 @@ export function isUpsetConfig(config: unknown): config is UpsetConfig { } // elementSelection - if (!(elementSelection === null || isBookmarkedSelection(elementSelection))) { + if (!(elementSelection === null || isNumericalBookmark(elementSelection))) { console.warn('Upset config error: Element selection is not a bookmarked selection'); return false; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 507ec486..58caae00 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -328,7 +328,7 @@ export type StringQuery = { * @privateRemarks * This *needs* to match the data format outputted by Vega-Lite to the 'brush' signal in * upset/src/components/ElementView/ElementVisualization.tsx. - * This is typechecked by isElementSelection in typecheck.ts; changes here must be reflected there. + * This is typechecked by isNumericalQuery in typecheck.ts; changes here must be reflected there. */ export type NumericalQuery = {[attName: string] : [number, number]}; @@ -366,10 +366,10 @@ export type BookmarkedIntersection = Bookmark & { } /** - * Represents a bookmarked element selection, created in the Element View. - * @privateRemarks typechecked by isBookmarkedSelection in typecheck.ts; changes here must be reflected there + * Represents a bookmarked element selection based on numerical attributes, created in the Element View. + * @privateRemarks typechecked by isNumericalBookmark in typecheck.ts; changes here must be reflected there */ -export type BookmarkedSelection = Bookmark & { +export type NumericalBookmark = Bookmark & { /** * The selection parameters */ @@ -448,7 +448,7 @@ export type UpsetConfig = { /** * Selected elements (data points) in the Element View. */ - elementSelection: BookmarkedSelection | null; + elementSelection: NumericalBookmark | null; version: '0.1.0'; useUserAlt: boolean; userAltText: AltText | null; diff --git a/packages/core/src/typeutils.ts b/packages/core/src/typeutils.ts index 29133f63..e387b2b8 100644 --- a/packages/core/src/typeutils.ts +++ b/packages/core/src/typeutils.ts @@ -1,6 +1,6 @@ import { Aggregate, - Aggregates, Bookmark, BookmarkedIntersection, BookmarkedSelection, NumericalQuery, Row, Rows, SetMembershipStatus, Subset, Subsets, + Aggregates, Bookmark, BookmarkedIntersection, NumericalBookmark, NumericalQuery, Row, Rows, SetMembershipStatus, Subset, Subsets, } from './types'; import { hashString } from './utils'; @@ -98,7 +98,7 @@ export function numericalQueriesEqual(a: NumericalQuery | undefined, b: Numerica * @param selection The numerical attribute query. * @returns The element selection. */ -export function numericalQueryToBookmark(selection: NumericalQuery): BookmarkedSelection { +export function numericalQueryToBookmark(selection: NumericalQuery): NumericalBookmark { // Normalizing prevents floating point error from causing different hashes const norm = (i : number) => Math.abs(Math.round(i * 10000)); diff --git a/packages/upset/src/atoms/elementsSelectors.ts b/packages/upset/src/atoms/elementsSelectors.ts index b50362b6..c7c92aab 100644 --- a/packages/upset/src/atoms/elementsSelectors.ts +++ b/packages/upset/src/atoms/elementsSelectors.ts @@ -1,7 +1,7 @@ import { Aggregate, BaseIntersection, - BookmarkedSelection, NumericalQuery, Item, flattenedOnlyRows, getItems, + NumericalBookmark, NumericalQuery, Item, flattenedOnlyRows, getItems, } from '@visdesignlab/upset2-core'; import { selector, selectorFamily } from 'recoil'; import { @@ -100,7 +100,7 @@ export const elementItemMapSelector = selectorFamily({ * Gets the current selection of elements * @returns The current selection of elements */ -export const selectedElementSelector = selector({ +export const selectedElementSelector = selector({ key: 'config-element-selection', get: ({ get }) => get(upsetConfigAtom).elementSelection, }); diff --git a/packages/upset/src/components/ElementView/BookmarkChips.tsx b/packages/upset/src/components/ElementView/BookmarkChips.tsx index 8d37f6c6..ffcff6ef 100644 --- a/packages/upset/src/components/ElementView/BookmarkChips.tsx +++ b/packages/upset/src/components/ElementView/BookmarkChips.tsx @@ -6,7 +6,7 @@ import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; import { - Bookmark, BookmarkedIntersection, BookmarkedSelection, flattenedOnlyRows, isBookmarkedIntersection, isBookmarkedSelection, + Bookmark, BookmarkedIntersection, NumericalBookmark, flattenedOnlyRows, isBookmarkedIntersection, isNumericalBookmark, } from '@visdesignlab/upset2-core'; import { bookmarkedColorPalette, @@ -46,7 +46,7 @@ export const BookmarkChips = () => { if (isBookmarkedIntersection(bookmark)) { if (currentIntersection?.id === bookmark.id) actions.setSelected(null); else actions.setSelected(rows[bookmark.id]); - } else if (isBookmarkedSelection(bookmark)) { + } else if (isNumericalBookmark(bookmark)) { // Need to update both the saved trrack state & the selection atom when a chip is clicked if (currentSelection?.id === bookmark.id && savedSelection !== null) actions.setElementSelection(null); else if (savedSelection?.id !== bookmark.id) actions.setElementSelection(bookmark); @@ -153,7 +153,7 @@ export const BookmarkChips = () => { aria-label={`Selected elements ${currentSelection.label}`} onKeyDown={(e) => { if (e.key === 'Enter') { - actions.addBookmark({ + actions.addBookmark({ id: currentSelection.id, label: currentSelection.label, type: 'elements', @@ -163,7 +163,7 @@ export const BookmarkChips = () => { }} label={`${currentSelection.label}`} onDelete={() => { - actions.addBookmark({ + actions.addBookmark({ id: currentSelection.id, label: currentSelection.label, type: 'elements', diff --git a/packages/upset/src/provenance/index.ts b/packages/upset/src/provenance/index.ts index 70682dcb..9b81e8b4 100644 --- a/packages/upset/src/provenance/index.ts +++ b/packages/upset/src/provenance/index.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { AggregateBy, Plot, PlotInformation, SortByOrder, SortVisibleBy, UpsetConfig, DefaultConfig, Row, - Bookmark, BookmarkedSelection, + Bookmark, NumericalBookmark, convertConfig, ColumnName, AltText, @@ -331,10 +331,10 @@ const setSelectedAction = register( }, ); -const setElementSelectionAction = register( +const setElementSelectionAction = register( 'select-elements', - (state: UpsetConfig, bookmarkedSelection) => { - state.elementSelection = bookmarkedSelection; + (state: UpsetConfig, NumericalBookmark) => { + state.elementSelection = NumericalBookmark; return state; }, ); @@ -440,7 +440,7 @@ export function getActions(provenance: UpsetProvenance) { * which is a filter on items based on their attributes. * @param selection The selection to set */ - setElementSelection: (selection: BookmarkedSelection | null) => provenance.apply( + setElementSelection: (selection: NumericalBookmark | null) => provenance.apply( selection && Object.keys(selection.selection).length > 0 ? `Selected elements based on the following keys: ${Object.keys(selection.selection).join(' ')}` : 'Deselected elements', From b524e78fa879645a7f403364e070a5baccc71543 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Fri, 23 Aug 2024 13:21:26 -0600 Subject: [PATCH 07/24] Add TextualQuery and TextualBookmark types --- packages/core/src/typecheck.ts | 40 ++++++++++++++++++- packages/core/src/types.ts | 25 +++++++++--- packages/core/src/typeutils.ts | 2 +- .../components/ElementView/BookmarkChips.tsx | 4 +- .../components/ElementView/QueryInterface.tsx | 4 +- 5 files changed, 63 insertions(+), 12 deletions(-) diff --git a/packages/core/src/typecheck.ts b/packages/core/src/typecheck.ts index 5642251a..f3096407 100644 --- a/packages/core/src/typecheck.ts +++ b/packages/core/src/typecheck.ts @@ -1,5 +1,8 @@ import { Aggregate, AggregateBy, aggregateByList, AltText, AttributePlots, AttributePlotType, BaseElement, BaseIntersection, Bookmark, NumericalBookmark, Column, NumericalQuery, Histogram, PlotInformation, Row, RowType, Scatterplot, Subset, Subsets, UpsetConfig, + TextualQuery, + TextualQueryType, + TextualBookmark, } from './types'; import { deepCopy } from './utils'; @@ -113,6 +116,24 @@ export function isNumericalQuery(value: unknown): value is NumericalQuery { ); } +/** + * Type guard for TextualQuery + * @param val The value to check. + * @returns whether the value is a TextualQuery + */ +export function isTextualQuery(val: unknown): val is TextualQuery { + return ( + !!val + && typeof val === 'object' + && Object.hasOwn(val, 'att') + && Object.hasOwn(val, 'type') + && Object.hasOwn(val, 'query') + && typeof (val as TextualQuery).att === 'string' + && Object.values(TextualQueryType).includes((val as TextualQuery).type) + && typeof (val as TextualQuery).query === 'string' + ); +} + /** * Type guard for Scatterplot * @param s variable to check @@ -162,7 +183,10 @@ export function isBookmark(b: unknown): b is Bookmark { && Object.hasOwn(b, 'type') && typeof (b as Bookmark).id === 'string' && typeof (b as Bookmark).label === 'string' - && ((b as Bookmark).type === 'intersection' || (b as Bookmark).type === 'elements'); + && ((b as Bookmark).type === 'intersection' + || (b as Bookmark).type === 'numerical' + || (b as Bookmark).type === 'textual' + ); } /** @@ -253,11 +277,23 @@ export function isRow(r: unknown): r is Row { */ export function isNumericalBookmark(b: unknown): b is NumericalBookmark { return isBookmark(b) - && b.type === 'elements' + && b.type === 'numerical' && Object.hasOwn(b, 'selection') && isNumericalQuery((b as NumericalBookmark).selection); } +/** + * Type guard for TextualBookmark + * @param b variable to check + * @returns {boolean} + */ +export function isTextualBookmark(b: unknown): b is TextualBookmark { + return isBookmark(b) + && b.type === 'textual' + && Object.hasOwn(b, 'selection') + && isTextualQuery((b as TextualBookmark).selection); +} + /** * Determines if the given object is a valid UpsetConfig using the CURRENT version. * @privateRemarks diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 58caae00..79f42cd8 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -292,7 +292,7 @@ export type AttributePlots = Record; */ // linter is saying this is already declared... on this line // eslint-disable-next-line no-shadow -export enum StringQueryType { +export enum TextualQueryType { EQUALS = 'equals', CONTAINS = 'contains', LENGTH = 'length', @@ -302,7 +302,7 @@ export enum StringQueryType { /** * Represents a selection of elements based on a string attribute. */ -export type StringQuery = { +export type TextualQuery = { /** * Name of the attribute being queried upon */ @@ -311,7 +311,7 @@ export type StringQuery = { * Type of the query; determines the mechanism used to evaluate whether the value of att * on a given element matches this query */ - type: StringQueryType, + type: TextualQueryType, /** * The query string. To be included in this query, the value of att on a given * element must match this query string according to the rules set by the type. @@ -348,7 +348,7 @@ export type Bookmark = { /** * Subtype of the bookmark; used to determine what fields are available at runtime */ - type: 'intersection' | 'elements'; + type: 'intersection' | 'numerical' | 'textual'; }; /** @@ -377,7 +377,22 @@ export type NumericalBookmark = Bookmark & { /** * Indicates type at runtime */ - type: 'elements'; + type: 'numerical'; +} + +/** + * Represents a bookmarked element selection based on textual attributes, created in the element view + * @privateRemarks typechecked by isTextualBookmark in typecheck.ts; changes here must be reflected there + */ +export type TextualBookmark = Bookmark & { + /** + * Selection parameters + */ + selection: TextualQuery; + /** + * Indicates type at runtim + */ + type: 'textual'; } /** diff --git a/packages/core/src/typeutils.ts b/packages/core/src/typeutils.ts index e387b2b8..e0f50748 100644 --- a/packages/core/src/typeutils.ts +++ b/packages/core/src/typeutils.ts @@ -125,7 +125,7 @@ export function numericalQueryToBookmark(selection: NumericalQuery): NumericalBo return { id: i.toString(), label, - type: 'elements', + type: 'numerical', selection, }; } diff --git a/packages/upset/src/components/ElementView/BookmarkChips.tsx b/packages/upset/src/components/ElementView/BookmarkChips.tsx index ffcff6ef..c9e120b9 100644 --- a/packages/upset/src/components/ElementView/BookmarkChips.tsx +++ b/packages/upset/src/components/ElementView/BookmarkChips.tsx @@ -156,7 +156,7 @@ export const BookmarkChips = () => { actions.addBookmark({ id: currentSelection.id, label: currentSelection.label, - type: 'elements', + type: 'numerical', selection: elementSelection, }); } @@ -166,7 +166,7 @@ export const BookmarkChips = () => { actions.addBookmark({ id: currentSelection.id, label: currentSelection.label, - type: 'elements', + type: 'numerical', selection: elementSelection, }); }} diff --git a/packages/upset/src/components/ElementView/QueryInterface.tsx b/packages/upset/src/components/ElementView/QueryInterface.tsx index e582c89a..df3974b6 100644 --- a/packages/upset/src/components/ElementView/QueryInterface.tsx +++ b/packages/upset/src/components/ElementView/QueryInterface.tsx @@ -5,7 +5,7 @@ import { } from '@mui/material'; import { Box } from '@mui/system'; import { useRecoilValue } from 'recoil'; -import { StringQueryType } from '@visdesignlab/upset2-core'; +import { TextualQueryType } from '@visdesignlab/upset2-core'; import { queryColumnsSelector } from '../../atoms/dataAtom'; /** @@ -35,7 +35,7 @@ export const QueryInterface = () => { Query Type From a178abfccf21e9ae6a0ea463505a7a6e775bf362 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Fri, 23 Aug 2024 16:06:40 -0600 Subject: [PATCH 08/24] Update isTextualQuery to use isObject --- packages/core/src/typecheck.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/typecheck.ts b/packages/core/src/typecheck.ts index f6ebd046..4b234c45 100644 --- a/packages/core/src/typecheck.ts +++ b/packages/core/src/typecheck.ts @@ -127,8 +127,7 @@ export function isNumericalQuery(value: unknown): value is NumericalQuery { */ export function isTextualQuery(val: unknown): val is TextualQuery { return ( - !!val - && typeof val === 'object' + isObject(val) && Object.hasOwn(val, 'att') && Object.hasOwn(val, 'type') && Object.hasOwn(val, 'query') From f767d66564cd669f54029131acf0ea7f7d4ce340 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Fri, 23 Aug 2024 16:19:24 -0600 Subject: [PATCH 09/24] Add TextualBookmark as an option for the config's ElementSelection --- packages/core/src/typecheck.ts | 12 +++++++++++- packages/core/src/types.ts | 8 +++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/core/src/typecheck.ts b/packages/core/src/typecheck.ts index 4b234c45..b57eaa9f 100644 --- a/packages/core/src/typecheck.ts +++ b/packages/core/src/typecheck.ts @@ -3,6 +3,7 @@ import { TextualQuery, TextualQueryType, TextualBookmark, + ElementSelection, } from './types'; import { deepCopy } from './utils'; @@ -292,6 +293,15 @@ export function isTextualBookmark(b: unknown): b is TextualBookmark { && isTextualQuery((b as TextualBookmark).selection); } +/** + * Determines if the given object is an ElementSelection. + * @param e The object to check. + * @returns {boolean} Whether the object is an ElementSelection. + */ +export function isElementSelection(e: unknown): e is ElementSelection { + return isNumericalBookmark(e) || isTextualBookmark(e); +} + /** * Determines if the given object is a valid UpsetConfig using the CURRENT version. * @privateRemarks @@ -538,7 +548,7 @@ export function isUpsetConfig(config: unknown): config is UpsetConfig { } // elementSelection - if (!(elementSelection === null || isNumericalBookmark(elementSelection))) { + if (!(elementSelection === null || isElementSelection(elementSelection))) { console.warn('Upset config error: Element selection is not a bookmarked selection'); return false; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 79f42cd8..caeb17f2 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -395,6 +395,12 @@ export type TextualBookmark = Bookmark & { type: 'textual'; } +/** + * A bookmark which represents a selection of elements + * @privateRemarks typechecked by isElementSelection in typecheck.ts; changes here must be reflected there + */ +export type ElementSelection = NumericalBookmark | TextualBookmark; + /** * Represents the alternative text for an Upset plot. * @privateRemarks typechecked by isAltText in typecheck.ts; changes here must be reflected there @@ -463,7 +469,7 @@ export type UpsetConfig = { /** * Selected elements (data points) in the Element View. */ - elementSelection: NumericalBookmark | null; + elementSelection: ElementSelection | null; version: '0.1.0'; useUserAlt: boolean; userAltText: AltText | null; From ce205d5f68d5f2802ee35b046322fac05036003a Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Tue, 27 Aug 2024 11:35:43 -0600 Subject: [PATCH 10/24] Allow querying numeric columns --- packages/upset/src/atoms/dataAtom.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/upset/src/atoms/dataAtom.ts b/packages/upset/src/atoms/dataAtom.ts index 65f09c27..22b6ca92 100644 --- a/packages/upset/src/atoms/dataAtom.ts +++ b/packages/upset/src/atoms/dataAtom.ts @@ -16,9 +16,9 @@ export const dataAtom = atom>({ export const queryColumnsSelector = selector({ key: 'data-columns', get: ({ get }) => { + const BUILTIN_COLS = ['_id', '_from', '_to', '_key', '_rev']; const data = get(dataAtom); - return data.columns.filter((col) => data.columnTypes[col] !== 'number' - && !data.setColumns.includes(col) - && !col.startsWith('_')); + return data.columns.filter((col) => !data.setColumns.includes(col) + && !BUILTIN_COLS.includes(col)); }, }); From b998cd72709ee1e4cc01ff17dee6049e1645b998 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Tue, 27 Aug 2024 14:16:28 -0600 Subject: [PATCH 11/24] Rename TextualQuery to ElementQuery and add <, > operations to element queries --- packages/core/src/typecheck.ts | 30 ++++++++--------- packages/core/src/types.ts | 32 ++++++++++--------- .../components/ElementView/QueryInterface.tsx | 4 +-- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/packages/core/src/typecheck.ts b/packages/core/src/typecheck.ts index b57eaa9f..c85bad85 100644 --- a/packages/core/src/typecheck.ts +++ b/packages/core/src/typecheck.ts @@ -1,8 +1,8 @@ import { Aggregate, AggregateBy, aggregateByList, AltText, AttributePlots, AttributePlotType, BaseElement, BaseIntersection, Bookmark, NumericalBookmark, Column, NumericalQuery, Histogram, PlotInformation, Row, RowType, Scatterplot, Subset, Subsets, UpsetConfig, - TextualQuery, - TextualQueryType, - TextualBookmark, + ElementQuery, + ElementQueryType, + ElementBookmark, ElementSelection, } from './types'; import { deepCopy } from './utils'; @@ -122,19 +122,19 @@ export function isNumericalQuery(value: unknown): value is NumericalQuery { } /** - * Type guard for TextualQuery + * Type guard for ElementQuery * @param val The value to check. - * @returns whether the value is a TextualQuery + * @returns whether the value is a ElementQuery */ -export function isTextualQuery(val: unknown): val is TextualQuery { +export function isElementQuery(val: unknown): val is ElementQuery { return ( isObject(val) && Object.hasOwn(val, 'att') && Object.hasOwn(val, 'type') && Object.hasOwn(val, 'query') - && typeof (val as TextualQuery).att === 'string' - && Object.values(TextualQueryType).includes((val as TextualQuery).type) - && typeof (val as TextualQuery).query === 'string' + && typeof (val as ElementQuery).att === 'string' + && Object.values(ElementQueryType).includes((val as ElementQuery).type) + && typeof (val as ElementQuery).query === 'string' ); } @@ -186,7 +186,7 @@ export function isBookmark(b: unknown): b is Bookmark { && typeof (b as Bookmark).label === 'string' && ((b as Bookmark).type === 'intersection' || (b as Bookmark).type === 'numerical' - || (b as Bookmark).type === 'textual' + || (b as Bookmark).type === 'element' ); } @@ -282,15 +282,15 @@ export function isNumericalBookmark(b: unknown): b is NumericalBookmark { } /** - * Type guard for TextualBookmark + * Type guard for ElementBookmark * @param b variable to check * @returns {boolean} */ -export function isTextualBookmark(b: unknown): b is TextualBookmark { +export function isElementBookmark(b: unknown): b is ElementBookmark { return isBookmark(b) - && b.type === 'textual' + && b.type === 'element' && Object.hasOwn(b, 'selection') - && isTextualQuery((b as TextualBookmark).selection); + && isElementQuery((b as ElementBookmark).selection); } /** @@ -299,7 +299,7 @@ export function isTextualBookmark(b: unknown): b is TextualBookmark { * @returns {boolean} Whether the object is an ElementSelection. */ export function isElementSelection(e: unknown): e is ElementSelection { - return isNumericalBookmark(e) || isTextualBookmark(e); + return isNumericalBookmark(e) || isElementBookmark(e); } /** diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index caeb17f2..ee9c6c07 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -292,17 +292,19 @@ export type AttributePlots = Record; */ // linter is saying this is already declared... on this line // eslint-disable-next-line no-shadow -export enum TextualQueryType { - EQUALS = 'equals', - CONTAINS = 'contains', - LENGTH = 'length', - REGEX = 'regex' +export enum ElementQueryType { + EQUALS = 'eq', + CONTAINS = 'ctns', + LENGTH = 'len', + REGEX = 'regex', + LESS_THAN = 'lt', + GREATER_THAN = 'gt', } /** - * Represents a selection of elements based on a string attribute. + * Represents a selection of elements based on a comparison between an attribute and a query string. */ -export type TextualQuery = { +export type ElementQuery = { /** * Name of the attribute being queried upon */ @@ -311,7 +313,7 @@ export type TextualQuery = { * Type of the query; determines the mechanism used to evaluate whether the value of att * on a given element matches this query */ - type: TextualQueryType, + type: ElementQueryType, /** * The query string. To be included in this query, the value of att on a given * element must match this query string according to the rules set by the type. @@ -348,7 +350,7 @@ export type Bookmark = { /** * Subtype of the bookmark; used to determine what fields are available at runtime */ - type: 'intersection' | 'numerical' | 'textual'; + type: 'intersection' | 'numerical' | 'element'; }; /** @@ -381,25 +383,25 @@ export type NumericalBookmark = Bookmark & { } /** - * Represents a bookmarked element selection based on textual attributes, created in the element view - * @privateRemarks typechecked by isTextualBookmark in typecheck.ts; changes here must be reflected there + * Represents a bookmarked element selection based on attribute comparisons, created in the element view + * @privateRemarks typechecked by isElementBookmark in typecheck.ts; changes here must be reflected there */ -export type TextualBookmark = Bookmark & { +export type ElementBookmark = Bookmark & { /** * Selection parameters */ - selection: TextualQuery; + selection: ElementQuery; /** * Indicates type at runtim */ - type: 'textual'; + type: 'element'; } /** * A bookmark which represents a selection of elements * @privateRemarks typechecked by isElementSelection in typecheck.ts; changes here must be reflected there */ -export type ElementSelection = NumericalBookmark | TextualBookmark; +export type ElementSelection = NumericalBookmark | ElementBookmark; /** * Represents the alternative text for an Upset plot. diff --git a/packages/upset/src/components/ElementView/QueryInterface.tsx b/packages/upset/src/components/ElementView/QueryInterface.tsx index df3974b6..dbdb12d6 100644 --- a/packages/upset/src/components/ElementView/QueryInterface.tsx +++ b/packages/upset/src/components/ElementView/QueryInterface.tsx @@ -5,7 +5,7 @@ import { } from '@mui/material'; import { Box } from '@mui/system'; import { useRecoilValue } from 'recoil'; -import { TextualQueryType } from '@visdesignlab/upset2-core'; +import { ElementQueryType } from '@visdesignlab/upset2-core'; import { queryColumnsSelector } from '../../atoms/dataAtom'; /** @@ -35,7 +35,7 @@ export const QueryInterface = () => { Query Type From d372304ed3934b6e69888667a01e45503b646cff Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Tue, 17 Sep 2024 16:53:11 -0600 Subject: [PATCH 12/24] Update currentSelection selector to be type ElementSelection & update its references --- packages/core/src/index.ts | 1 + packages/core/src/utils.ts | 47 +++++++++++++++++++ packages/upset/src/atoms/elementsSelectors.ts | 42 +++++++---------- .../components/ElementView/BookmarkChips.tsx | 35 ++++++-------- .../components/ElementView/ElementTable.tsx | 4 +- .../ElementView/ElementVisualization.tsx | 14 +++--- packages/upset/src/provenance/index.ts | 12 +++-- 7 files changed, 94 insertions(+), 61 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e044add5..785207ca 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,3 +8,4 @@ export * from './defaultConfig'; export * from './convertConfig'; export * from './typecheck'; export * from './typeutils'; +export * from './utils'; diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index d69cd7f7..cf401d49 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -1,3 +1,6 @@ +import { isNumericalBookmark, isElementBookmark } from './typecheck'; +import { ElementSelection, Item, ElementQueryType } from './types'; + /** * DEPRECATED: Currently serves only as an alias for structuredClone; use that instead. * Create a deep copy (with all fields recursively copied) of an object using structured cloning; @@ -27,3 +30,47 @@ export function hashString(str: string): number { } return hash; } + +/** + * Filter items based on a selection + * @param items Items to filter + * @param filter Selection to filter by + * @returns Filtered items + */ +export function filterItems(items: Item[], filter: ElementSelection): Item[] { + const result: Item[] = []; + if (isNumericalBookmark(filter)) { + return items.filter( + (item) => Object.entries(filter.selection).every( + ([key, value]) => typeof item[key] === 'number' + && item[key] as number >= value[0] + && item[key] as number <= value[1], + ), + ); + } if (isElementBookmark(filter)) { + const { att } = filter.selection; + const { query } = filter.selection; + + return items.filter((item) => { + if (!Object.hasOwn(item, att)) return false; + + switch (filter.selection.type) { + case ElementQueryType.CONTAINS: + return (`${item[att]}`).includes(query); + case ElementQueryType.EQUALS: + return (`${item[att]}` === query); + case ElementQueryType.LENGTH: + return ((`${item[att]}`).length === Number(query)); + case ElementQueryType.REGEX: + return (new RegExp(query).test(`${item[att]}`)); + case ElementQueryType.GREATER_THAN: + return item[att] > (typeof item[att] === 'number' ? Number(query) : query); + case ElementQueryType.LESS_THAN: + return item[att] < (typeof item[att] === 'number' ? Number(query) : query); + default: + } + return false; + }); + } + return result; +} diff --git a/packages/upset/src/atoms/elementsSelectors.ts b/packages/upset/src/atoms/elementsSelectors.ts index c7c92aab..d167c875 100644 --- a/packages/upset/src/atoms/elementsSelectors.ts +++ b/packages/upset/src/atoms/elementsSelectors.ts @@ -1,7 +1,9 @@ import { Aggregate, BaseIntersection, - NumericalBookmark, NumericalQuery, Item, flattenedOnlyRows, getItems, + NumericalQuery, Item, flattenedOnlyRows, getItems, + ElementSelection, + filterItems, } from '@visdesignlab/upset2-core'; import { selector, selectorFamily } from 'recoil'; import { @@ -100,7 +102,7 @@ export const elementItemMapSelector = selectorFamily({ * Gets the current selection of elements * @returns The current selection of elements */ -export const selectedElementSelector = selector({ +export const selectedElementSelector = selector({ key: 'config-element-selection', get: ({ get }) => get(upsetConfigAtom).elementSelection, }); @@ -109,9 +111,12 @@ export const selectedElementSelector = selector({ * Gets the parameters for the current selection of elements, * ie the 'selected' property of the selectedElementsSelector */ -export const elementSelectionParameters = selector({ +export const currentNumericalQuery = selector({ key: 'config-current-element-selection', - get: ({ get }) => get(selectedElementSelector)?.selection, + get: ({ get }) => { + const elementSelection = get(selectedElementSelector); + return elementSelection?.type === 'numerical' ? elementSelection.selection : undefined; + }, }); /** @@ -123,15 +128,10 @@ export const selectedItemsSelector = selector({ get: ({ get }) => { const bookmarks = get(bookmarkSelector); const items: Item[] = get(elementItemMapSelector(bookmarks.map((b) => b.id))); - const selection = get(elementSelectionParameters); + const selection = get(selectedElementSelector); if (!selection) return []; - const result: Item[] = []; - items.forEach((item) => { - if (Object.entries(selection).every(([key, value]) => typeof item[key] === 'number' && - item[key] as number >= value[0] && item[key] as number <= value[1])) { result.push(item); } - }); - return result; + return filterItems(items, selection); }, }); @@ -150,21 +150,11 @@ export const subsetSelectedCount = selectorFamily({ key: 'subset-selected', get: (id: string) => ({ get }) => { const items = get(elementSelector(id)); - const selection = get(elementSelectionParameters); - - if (!selection || Object.keys(selection).length === 0) return 0; - - let count = 0; - items.forEach((item) => { - if (Object.entries(selection).every( - ([key, value]) => typeof item[key] === 'number' - && item[key] as number >= value[0] - && item[key] as number <= value[1], - )) { - count++; - } - }); - return count; + const selection = get(selectedElementSelector); + + if (!selection) return 0; + + return filterItems(items, selection).length; }, }); diff --git a/packages/upset/src/components/ElementView/BookmarkChips.tsx b/packages/upset/src/components/ElementView/BookmarkChips.tsx index c9e120b9..1850ba3d 100644 --- a/packages/upset/src/components/ElementView/BookmarkChips.tsx +++ b/packages/upset/src/components/ElementView/BookmarkChips.tsx @@ -6,7 +6,8 @@ import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; import { - Bookmark, BookmarkedIntersection, NumericalBookmark, flattenedOnlyRows, isBookmarkedIntersection, isNumericalBookmark, + Bookmark, BookmarkedIntersection, flattenedOnlyRows, isBookmarkedIntersection, + isElementSelection, } from '@visdesignlab/upset2-core'; import { bookmarkedColorPalette, @@ -18,7 +19,7 @@ import { import { ProvenanceContext } from '../Root'; import { dataAtom } from '../../atoms/dataAtom'; import { UpsetActions, UpsetProvenance } from '../../provenance'; -import { elementSelectionParameters, selectedElementSelector } from '../../atoms/elementsSelectors'; +import { selectedElementSelector } from '../../atoms/elementsSelectors'; /** * Shows a stack of chips representing bookmarks and the current intersection/element selection, @@ -34,22 +35,22 @@ export const BookmarkChips = () => { const bookmarked = useRecoilValue(bookmarkSelector); const currentIntersectionDisplayName = currentIntersection?.elementName.replaceAll('~&~', ' & ') || ''; const currentSelection = useRecoilValue(selectedElementSelector); - const savedSelection = useRecoilValue(selectedElementSelector); const elementSelectionColor = useRecoilValue(elementColorSelector); - const elementSelection = useRecoilValue(elementSelectionParameters); /** * Handles when a chip in the bookmark stack is clicked * @param bookmark Clicked bookmark */ function chipClicked(bookmark: Bookmark) { + console.log('chipclicked', bookmark); if (isBookmarkedIntersection(bookmark)) { if (currentIntersection?.id === bookmark.id) actions.setSelected(null); else actions.setSelected(rows[bookmark.id]); - } else if (isNumericalBookmark(bookmark)) { + } else if (isElementSelection(bookmark)) { // Need to update both the saved trrack state & the selection atom when a chip is clicked - if (currentSelection?.id === bookmark.id && savedSelection !== null) actions.setElementSelection(null); - else if (savedSelection?.id !== bookmark.id) actions.setElementSelection(bookmark); + console.log("bookmark", bookmark, currentSelection); + if (currentSelection?.id === bookmark.id) actions.setElementSelection(null); + else actions.setElementSelection(bookmark); } } @@ -100,8 +101,10 @@ export const BookmarkChips = () => { onDelete={() => { if (currentIntersection?.id === bookmark.id) { actions.setSelected(null); + } else if (currentSelection?.id === bookmark.id) { + actions.setElementSelection(null); } - actions.removeBookmark({ id: bookmark.id, label: bookmark.label, type: bookmark.type }); + actions.removeBookmark(bookmark); }} /> ))} @@ -140,7 +143,7 @@ export const BookmarkChips = () => { /> )} {/* Chip for the current element selection */} - {elementSelection && currentSelection && !bookmarked.find((b) => b.id === currentSelection.id) && ( + {currentSelection && !bookmarked.find((b) => b.id === currentSelection.id) && ( ({ margin: theme.spacing(0.5), @@ -153,22 +156,12 @@ export const BookmarkChips = () => { aria-label={`Selected elements ${currentSelection.label}`} onKeyDown={(e) => { if (e.key === 'Enter') { - actions.addBookmark({ - id: currentSelection.id, - label: currentSelection.label, - type: 'numerical', - selection: elementSelection, - }); + actions.addBookmark(structuredClone(currentSelection)); } }} label={`${currentSelection.label}`} onDelete={() => { - actions.addBookmark({ - id: currentSelection.id, - label: currentSelection.label, - type: 'numerical', - selection: elementSelection, - }); + actions.addBookmark(structuredClone(currentSelection)); }} deleteIcon={} /> diff --git a/packages/upset/src/components/ElementView/ElementTable.tsx b/packages/upset/src/components/ElementView/ElementTable.tsx index e4b82df4..062d2e38 100644 --- a/packages/upset/src/components/ElementView/ElementTable.tsx +++ b/packages/upset/src/components/ElementView/ElementTable.tsx @@ -5,7 +5,7 @@ import { FC, useMemo } from 'react'; import { useRecoilValue } from 'recoil'; import { attributeAtom } from '../../atoms/attributeAtom'; -import { elementSelectionParameters, elementSelector, selectedItemsSelector } from '../../atoms/elementsSelectors'; +import { elementSelector, selectedElementSelector, selectedItemsSelector } from '../../atoms/elementsSelectors'; import { currentIntersectionSelector } from '../../atoms/config/currentIntersectionAtom'; /** @@ -43,7 +43,7 @@ function useColumns(columns: string[]) { export const ElementTable: FC = () => { const currentIntersection = useRecoilValue(currentIntersectionSelector); const attributeColumns = useRecoilValue(attributeAtom); - const elementSelection = useRecoilValue(elementSelectionParameters); + const elementSelection = useRecoilValue(selectedElementSelector); const elements = elementSelection ? useRecoilValue(selectedItemsSelector) : useRecoilValue(elementSelector(currentIntersection?.id)); diff --git a/packages/upset/src/components/ElementView/ElementVisualization.tsx b/packages/upset/src/components/ElementView/ElementVisualization.tsx index 3d9ec201..7e7525cb 100644 --- a/packages/upset/src/components/ElementView/ElementVisualization.tsx +++ b/packages/upset/src/components/ElementView/ElementVisualization.tsx @@ -9,7 +9,7 @@ import { numericalQueryToBookmark, numericalQueriesEqual, isNumericalQuery } fro import { Button } from '@mui/material'; import { bookmarkSelector, elementColorSelector } from '../../atoms/config/currentIntersectionAtom'; import { histogramSelector, scatterplotsSelector } from '../../atoms/config/plotAtoms'; -import { elementItemMapSelector, elementSelectionParameters } from '../../atoms/elementsSelectors'; +import { elementItemMapSelector, currentNumericalQuery } from '../../atoms/elementsSelectors'; import { AddPlotDialog } from './AddPlotDialog'; import { generateVega } from './generatePlotSpec'; import { ProvenanceContext } from '../Root'; @@ -29,7 +29,7 @@ export const ElementVisualization = () => { const histograms = useRecoilValue(histogramSelector); const bookmarked = useRecoilValue(bookmarkSelector); const items = useRecoilValue(elementItemMapSelector(bookmarked.map((b) => b.id))); - const elementSelection = useRecoilValue(elementSelectionParameters); + const numericalQuery = useRecoilValue(currentNumericalQuery); const selectColor = useRecoilValue(elementColorSelector); const { actions }: {actions: UpsetActions} = useContext(ProvenanceContext); @@ -37,10 +37,10 @@ export const ElementVisualization = () => { * Hooks */ - const draftSelection = useRef(elementSelection); + const draftSelection = useRef(numericalQuery); const vegaSpec = useMemo( - () => generateVega(scatterplots, histograms, selectColor, elementSelection), - [scatterplots, histograms, selectColor, elementSelection], + () => generateVega(scatterplots, histograms, selectColor, numericalQuery), + [scatterplots, histograms, selectColor, numericalQuery], ); /** @@ -70,7 +70,7 @@ export const ElementVisualization = () => { if ( draftSelection.current && Object.keys(draftSelection.current).length > 0 - && !numericalQueriesEqual(draftSelection.current, elementSelection) + && !numericalQueriesEqual(draftSelection.current, numericalQuery) ) { actions.setElementSelection(numericalQueryToBookmark(draftSelection.current)); } else { @@ -86,7 +86,7 @@ export const ElementVisualization = () => { ( }, ); -const setElementSelectionAction = register( +const setElementSelectionAction = register( 'select-elements', - (state: UpsetConfig, NumericalBookmark) => { - state.elementSelection = NumericalBookmark; + (state: UpsetConfig, elementSelection) => { + state.elementSelection = elementSelection; return state; }, ); @@ -440,7 +441,8 @@ export function getActions(provenance: UpsetProvenance) { * which is a filter on items based on their attributes. * @param selection The selection to set */ - setElementSelection: (selection: NumericalBookmark | null) => provenance.apply( + setElementSelection: (selection: ElementSelection | null) => provenance.apply( + // Object.keys check is for numerical queries, which can come out of vega as {} selection && Object.keys(selection.selection).length > 0 ? `Selected elements based on the following keys: ${Object.keys(selection.selection).join(' ')}` : 'Deselected elements', From c877b918c9726af6fad3719c561c82de93acb6c1 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Tue, 17 Sep 2024 16:53:54 -0600 Subject: [PATCH 13/24] Remove console.logs --- packages/upset/src/components/ElementView/BookmarkChips.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/upset/src/components/ElementView/BookmarkChips.tsx b/packages/upset/src/components/ElementView/BookmarkChips.tsx index 1850ba3d..d4f3eca6 100644 --- a/packages/upset/src/components/ElementView/BookmarkChips.tsx +++ b/packages/upset/src/components/ElementView/BookmarkChips.tsx @@ -42,13 +42,11 @@ export const BookmarkChips = () => { * @param bookmark Clicked bookmark */ function chipClicked(bookmark: Bookmark) { - console.log('chipclicked', bookmark); if (isBookmarkedIntersection(bookmark)) { if (currentIntersection?.id === bookmark.id) actions.setSelected(null); else actions.setSelected(rows[bookmark.id]); } else if (isElementSelection(bookmark)) { // Need to update both the saved trrack state & the selection atom when a chip is clicked - console.log("bookmark", bookmark, currentSelection); if (currentSelection?.id === bookmark.id) actions.setElementSelection(null); else actions.setElementSelection(bookmark); } From 4abff543de5ba2ba4e459a10566b2a2ccd3b4487 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Wed, 18 Sep 2024 15:35:31 -0600 Subject: [PATCH 14/24] Add ElementQueryToBookmark helper --- packages/core/src/types.ts | 10 +++++----- packages/core/src/typeutils.ts | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ee9c6c07..c7d701aa 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -293,12 +293,12 @@ export type AttributePlots = Record; // linter is saying this is already declared... on this line // eslint-disable-next-line no-shadow export enum ElementQueryType { - EQUALS = 'eq', - CONTAINS = 'ctns', - LENGTH = 'len', + EQUALS = 'equals', + CONTAINS = 'contains', + LENGTH = 'length equals', REGEX = 'regex', - LESS_THAN = 'lt', - GREATER_THAN = 'gt', + LESS_THAN = 'less than', + GREATER_THAN = 'greater than', } /** diff --git a/packages/core/src/typeutils.ts b/packages/core/src/typeutils.ts index e0f50748..9449a3e6 100644 --- a/packages/core/src/typeutils.ts +++ b/packages/core/src/typeutils.ts @@ -1,6 +1,6 @@ import { Aggregate, - Aggregates, Bookmark, BookmarkedIntersection, NumericalBookmark, NumericalQuery, Row, Rows, SetMembershipStatus, Subset, Subsets, + Aggregates, Bookmark, BookmarkedIntersection, ElementBookmark, ElementQuery, NumericalBookmark, NumericalQuery, Row, Rows, SetMembershipStatus, Subset, Subsets, } from './types'; import { hashString } from './utils'; @@ -130,6 +130,23 @@ export function numericalQueryToBookmark(selection: NumericalQuery): NumericalBo }; } +/** + * Converts an element selection to a bookmark. + * Generates the ID by hashing the selection and labels the bookmark with the selection parameters. + * @param selection The element query. + * @returns The element selection. + */ +export function ElementQueryToBookmark(selection: ElementQuery): ElementBookmark { + const i = hashString(JSON.stringify(selection)); + const label = `${selection.att} ${selection.type} ${selection.query}`; + return { + id: i.toString(), + label, + type: 'element', + selection, + }; +} + /** * Calculates the degree of set membership based on the provided membership object. * The degree of set membership is the number of sets in which the subset is comprised of. From acdc8edbad57250164475d4e92dc3b3a90fda3eb Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Wed, 18 Sep 2024 15:35:50 -0600 Subject: [PATCH 15/24] Add functionality to QueryInterface UI --- packages/upset/src/atoms/elementsSelectors.ts | 19 +++++++- .../components/ElementView/QueryInterface.tsx | 46 ++++++++++++++++--- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/packages/upset/src/atoms/elementsSelectors.ts b/packages/upset/src/atoms/elementsSelectors.ts index d167c875..746af9c0 100644 --- a/packages/upset/src/atoms/elementsSelectors.ts +++ b/packages/upset/src/atoms/elementsSelectors.ts @@ -4,6 +4,7 @@ import { NumericalQuery, Item, flattenedOnlyRows, getItems, ElementSelection, filterItems, + ElementQuery, } from '@visdesignlab/upset2-core'; import { selector, selectorFamily } from 'recoil'; import { @@ -108,8 +109,9 @@ export const selectedElementSelector = selector({ }); /** - * Gets the parameters for the current selection of elements, - * ie the 'selected' property of the selectedElementsSelector + * Gets the parameters for the current numerical selection of elements, + * ie the 'selected' property of the selectedElementsSelector. + * If the current selection is not numerical, returns undefined. */ export const currentNumericalQuery = selector({ key: 'config-current-element-selection', @@ -119,6 +121,19 @@ export const currentNumericalQuery = selector({ }, }); +/** + * Gets the parameters for the current selection of elements, + * ie the 'selected' property of the selectedElementsSelector. + * If the current selection is not an element query, returns undefined. + */ +export const currentElementQuery = selector({ + key: 'config-current-element-query', + get: ({ get }) => { + const elementSelection = get(selectedElementSelector); + return elementSelection?.type === 'element' ? elementSelection.selection : undefined; + }, +}); + /** * Returns all items that are in a bookmarked intersection OR the currently selected intersection * AND are within the bounds of the current element selection. diff --git a/packages/upset/src/components/ElementView/QueryInterface.tsx b/packages/upset/src/components/ElementView/QueryInterface.tsx index dbdb12d6..a1c916bd 100644 --- a/packages/upset/src/components/ElementView/QueryInterface.tsx +++ b/packages/upset/src/components/ElementView/QueryInterface.tsx @@ -5,28 +5,58 @@ import { } from '@mui/material'; import { Box } from '@mui/system'; import { useRecoilValue } from 'recoil'; -import { ElementQueryType } from '@visdesignlab/upset2-core'; +import { ElementQueryToBookmark, ElementQueryType } from '@visdesignlab/upset2-core'; +import { useCallback, useContext, useState } from 'react'; import { queryColumnsSelector } from '../../atoms/dataAtom'; +import { currentElementQuery } from '../../atoms/elementsSelectors'; +import { ProvenanceContext } from '../Root'; +import { UpsetActions } from '../../provenance'; /** * Component showing a form allowing element queries to be created based on non-numeric fields * @returns {EmotionJSX.Element} */ export const QueryInterface = () => { - /** + /* * State */ const atts = useRecoilValue(queryColumnsSelector); + const currentSelection = useRecoilValue(currentElementQuery); + const { actions }: { actions: UpsetActions } = useContext(ProvenanceContext); const FIELD_MARGIN = '5px'; const FIELD_CSS = { marginTop: FIELD_MARGIN, width: '50%' }; - return ( + const [attField, setAttField] = useState(currentSelection?.att); + const [typeField, setTypeField] = useState(currentSelection?.type); + const [queryField, setQueryField] = useState(currentSelection?.query); + + /* + * Functions + */ + + /** + * Save the current query + */ + const save = useCallback(() => { + if (attField && typeField && queryField + && Object.values(ElementQueryType).includes(typeField as ElementQueryType) + && atts.includes(attField) + ) { + actions.setElementSelection(ElementQueryToBookmark({ + att: attField, + type: typeField as ElementQueryType || ElementQueryType.EQUALS, + query: queryField, + })); + } + }, [attField, typeField, queryField, atts, actions]); + + return atts.length > 0 ? ( Attribute Name - setAttField(e.target.value)}> {atts.map((att) => ( {att} ))} @@ -34,7 +64,7 @@ export const QueryInterface = () => { Query Type - setTypeField(e.target.value)}> {Object.values(ElementQueryType).map((type) => ( {type} ))} @@ -48,9 +78,11 @@ export const QueryInterface = () => { width: '80%', display: 'inline-block', }} + value={queryField ?? ''} + onChange={(e) => setQueryField(e.target.value)} /> - + - ); + ) : null; }; From 93f883ecab49cb8f1cfdf7f04dd311fc8cd3c5a3 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Mon, 21 Oct 2024 12:45:12 -0600 Subject: [PATCH 16/24] Change query save button to "clear" button when an element query is active; allows clearing of element query --- .../components/ElementView/QueryInterface.tsx | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/packages/upset/src/components/ElementView/QueryInterface.tsx b/packages/upset/src/components/ElementView/QueryInterface.tsx index a1c916bd..7e0ec694 100644 --- a/packages/upset/src/components/ElementView/QueryInterface.tsx +++ b/packages/upset/src/components/ElementView/QueryInterface.tsx @@ -37,10 +37,15 @@ export const QueryInterface = () => { */ /** - * Save the current query + * Save the current query if none is defined, or clear the existing selection */ - const save = useCallback(() => { - if (attField && typeField && queryField + const saveOrClear = useCallback(() => { + if (currentSelection) { + actions.setElementSelection(null); + setAttField(undefined); + setTypeField(undefined); + setQueryField(undefined); + } else if (attField && typeField && queryField && Object.values(ElementQueryType).includes(typeField as ElementQueryType) && atts.includes(attField) ) { @@ -50,13 +55,18 @@ export const QueryInterface = () => { query: queryField, })); } - }, [attField, typeField, queryField, atts, actions]); + }, [attField, typeField, queryField, atts, actions, currentSelection]); return atts.length > 0 ? ( Attribute Name - setAttField(e.target.value)} + > {atts.map((att) => ( {att} ))} @@ -64,7 +74,12 @@ export const QueryInterface = () => { Query Type - setTypeField(e.target.value)} + > {Object.values(ElementQueryType).map((type) => ( {type} ))} @@ -78,10 +93,18 @@ export const QueryInterface = () => { width: '80%', display: 'inline-block', }} + disabled={!!currentSelection} value={queryField ?? ''} onChange={(e) => setQueryField(e.target.value)} /> - + ) : null; From c96d041e35986cb2a1cca2d476955e2598f1ed44 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Mon, 21 Oct 2024 13:25:13 -0600 Subject: [PATCH 17/24] Add testing for Element Query Interface --- e2e-tests/elementView.spec.ts | 142 +++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/e2e-tests/elementView.spec.ts b/e2e-tests/elementView.spec.ts index cc1e2a89..6977be9b 100644 --- a/e2e-tests/elementView.spec.ts +++ b/e2e-tests/elementView.spec.ts @@ -20,7 +20,7 @@ test.beforeEach(async ({ page }) => { } else if (url.includes('alttxt')) { json = mockAltText; await route.fulfill({ json }); - } else if (url.includes('workspaces/Upset%20Examples/sessions/table/193/state/')) { + } else if (url.includes('workspaces/Upset%20Examples/sessions/table/193/')) { await route.fulfill({ status: 200 }); } else { await route.continue(); @@ -181,3 +181,143 @@ test('Element View', async ({ page }) => { await expect(schoolBlueHairMaleSelectionRect).toBeVisible(); await expect(schoolMale3rdPoly).toBeVisible(); }); + +/** + * Clears the selection in the element view; element view must be open + * @param page The page to perform the clear selection on + */ +async function clearSelection(page: Page): Promise { + await page.getByRole('button', { name: 'Clear' }).click(); +} + +/** + * Sets a query in the element view; element view must be open + * @param page The current testing page + * @param att The attribute to query + * @param type The type of query + * @param query The query string + */ +async function setQuery(page: Page, att: string, type: string, query: string): Promise { + await page.getByLabel('Attribute Name').click(); + await page.getByRole('option', { name: att }).click(); + await page.getByLabel('Query Type').click(); + await page.getByRole('option', { name: type, exact: true }).click(); + await page.getByPlaceholder('Query').click(); + await page.getByPlaceholder('Query').fill(query); + await page.getByRole('button', { name: 'Save' }).click(); +} + +test('Query Selection', async ({ page }) => { + await page.goto('http://localhost:3000/?workspace=Upset+Examples&table=simpsons&sessionId=193'); + await page.getByLabel('Element View Sidebar Toggle').click(); + await page.locator('[id="Subset_School\\~\\&\\~Male"] g').filter({ hasText: /^Blue Hair$/ }).locator('circle').click(); + await page.getByLabel('Selected intersection School').click(); + + // Selected elements for testing + const ralphCell = page.getByRole('cell', { name: 'Ralph' }); + const age8Cell = page.getByRole('cell', { name: '8' }); + const bartCell = page.getByRole('cell', { name: 'Bart' }); + const age10Cell1 = page.getByRole('cell', { name: '10' }).first(); + const age10Cell2 = page.getByRole('cell', { name: '10' }).nth(1); + const martinCell = page.getByRole('cell', { name: 'Martin Prince' }); + const schoolSelectPoly = page.locator('#Subset_School polygon').nth(1); + const maleSelectPoly = page.locator('#Subset_Male polygon').nth(1); + const schoolMaleSelectPoly = page.locator('[id="Subset_School\\~\\&\\~Male"] polygon').nth(1); + + // Test less than query + await setQuery(page, 'Age', 'less than', '10'); + + await expect(schoolMaleSelectPoly).toBeVisible(); + await expect(ralphCell).toBeVisible(); + await expect(age8Cell).toBeVisible(); + await expect(bartCell).not.toBeVisible(); + await expect(age10Cell1).not.toBeVisible(); + await expect(age10Cell2).not.toBeVisible(); + await expect(martinCell).not.toBeVisible(); + await expect(schoolSelectPoly).toBeVisible(); + await expect(maleSelectPoly).not.toBeVisible(); + + // Test greater than query + await clearSelection(page); + await setQuery(page, 'Age', 'greater than', '8'); + + await expect(bartCell).toBeVisible(); + await expect(age10Cell1).toBeVisible(); + await expect(age10Cell2).toBeVisible(); + await expect(martinCell).toBeVisible(); + await expect(maleSelectPoly).toBeVisible(); + await expect(schoolMaleSelectPoly).toBeVisible(); + await expect(ralphCell).not.toBeVisible(); + await expect(age8Cell).not.toBeVisible(); + await expect(schoolSelectPoly).not.toBeVisible(); + + // Test contains query + await clearSelection(page); + await setQuery(page, 'Name', 'contains', 't'); + + await expect(bartCell).toBeVisible(); + await expect(age10Cell1).toBeVisible(); + await expect(age10Cell2).toBeVisible(); + await expect(martinCell).toBeVisible(); + await expect(maleSelectPoly).toBeVisible(); + await expect(schoolMaleSelectPoly).toBeVisible(); + await expect(ralphCell).not.toBeVisible(); + await expect(age8Cell).not.toBeVisible(); + await expect(schoolSelectPoly).not.toBeVisible(); + + // Test equals query + await clearSelection(page); + await setQuery(page, 'Name', 'equals', 'Bart'); + + await expect(bartCell).toBeVisible(); + await expect(age10Cell1).toBeVisible(); + await expect(schoolMaleSelectPoly).toBeVisible(); + await expect(age10Cell2).not.toBeVisible(); + await expect(martinCell).not.toBeVisible(); + await expect(maleSelectPoly).not.toBeVisible(); + await expect(ralphCell).not.toBeVisible(); + await expect(age8Cell).not.toBeVisible(); + await expect(schoolSelectPoly).not.toBeVisible(); + + // Test length equals query + await clearSelection(page); + await setQuery(page, 'Name', 'length equals', '5'); + + await expect(ralphCell).toBeVisible(); + await expect(age8Cell).toBeVisible(); + await expect(schoolMaleSelectPoly).toBeVisible(); + await expect(bartCell).not.toBeVisible(); + await expect(age10Cell1).not.toBeVisible(); + await expect(age10Cell2).not.toBeVisible(); + await expect(martinCell).not.toBeVisible(); + await expect(schoolSelectPoly).not.toBeVisible(); + await expect(maleSelectPoly).not.toBeVisible(); + + // Test regex query + await clearSelection(page); + await setQuery(page, 'Name', 'regex', '^([A-z]+)$'); + + await expect(page.locator('[id="Subset_Evil\\~\\&\\~Male"] polygon').nth(1)).not.toBeVisible(); + await expect(bartCell).toBeVisible(); + await expect(age10Cell1).toBeVisible(); + await expect(ralphCell).toBeVisible(); + await expect(age8Cell).toBeVisible(); + await expect(martinCell).not.toBeVisible(); + await expect(age10Cell2).not.toBeVisible(); + await expect(schoolMaleSelectPoly).toBeVisible(); + await expect(schoolSelectPoly).toBeVisible(); + await expect(maleSelectPoly).toBeVisible(); + + // Test clear selection + await clearSelection(page); + await expect(bartCell).toBeVisible(); + await expect(age10Cell1).toBeVisible(); + await expect(ralphCell).toBeVisible(); + await expect(age8Cell).toBeVisible(); + await expect(martinCell).toBeVisible(); + await expect(age10Cell2).toBeVisible(); + // Only visible because the intersection is selected + await expect(schoolMaleSelectPoly).toBeVisible(); + await expect(schoolSelectPoly).not.toBeVisible(); + await expect(maleSelectPoly).not.toBeVisible(); +}); From bd2cdbdcdc699070b2b0b21349a53341ddc06187 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Tue, 22 Oct 2024 13:31:28 -0600 Subject: [PATCH 18/24] Fix ElementView test in Firefox --- e2e-tests/elementView.spec.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/e2e-tests/elementView.spec.ts b/e2e-tests/elementView.spec.ts index 7880adff..d7d04a7d 100644 --- a/e2e-tests/elementView.spec.ts +++ b/e2e-tests/elementView.spec.ts @@ -6,7 +6,8 @@ import { beforeTest } from './common'; test.beforeEach(beforeTest); /** - * Drags the mouse from the center of the element to the specified offset + * Drags the mouse from the center of the element to the specified offset. + * Doesn't quite work in firefox; use caution * @see https://stackoverflow.com/a/71147367 * @param element The element to drag over * @param xOffset The x offset to drag to @@ -28,7 +29,7 @@ async function dragElement(element: Locator, xOffset: number, yOffset: number, p await page.mouse.up(); } -test('Element View', async ({ page }) => { +test('Element View', async ({ page, browserName }) => { await page.goto('http://localhost:3000/?workspace=Upset+Examples&table=simpsons&sessionId=193'); // Make selection @@ -112,6 +113,10 @@ test('Element View', async ({ page }) => { // Check that the selection chip is visible after selecting await elementViewToggle.click(); await dragElement(page.locator('canvas'), 150, 0, page); + // For some reason, in firefox, the dragElement() method doesn't quite work right and the end of the drag + // is off the element, which doesn't fire the select handler. Clicking the canvas in firefox fires the event, + // but clicking in chromium/webkit resets the selection & breaks the test + if (browserName === 'firefox') await page.locator('canvas').first().click(); const elementSelectionChip = await page.getByLabel('Selected elements Atts: Age'); await expect(elementSelectionChip).toBeVisible(); From 76181f58853269507fe1b10f1066f87fb29c8960 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Tue, 22 Oct 2024 13:53:22 -0600 Subject: [PATCH 19/24] Move intersection notification to element vis --- .../components/ElementView/BookmarkChips.tsx | 206 ++++++++---------- .../ElementView/ElementVisualization.tsx | 17 +- 2 files changed, 111 insertions(+), 112 deletions(-) diff --git a/packages/upset/src/components/ElementView/BookmarkChips.tsx b/packages/upset/src/components/ElementView/BookmarkChips.tsx index d4f3eca6..6eb62a72 100644 --- a/packages/upset/src/components/ElementView/BookmarkChips.tsx +++ b/packages/upset/src/components/ElementView/BookmarkChips.tsx @@ -1,7 +1,7 @@ import SquareIcon from '@mui/icons-material/Square'; import StarIcon from '@mui/icons-material/Star'; import StarBorderIcon from '@mui/icons-material/StarBorder'; -import { Alert, Chip, Stack } from '@mui/material'; +import { Chip, Stack } from '@mui/material'; import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; @@ -53,118 +53,104 @@ export const BookmarkChips = () => { } return ( - <> - {!currentIntersection && bookmarked.length === 0 && ( - - Please click on intersections to select an intersection. - - )} - - {/* All chips from bookmarks */} - {bookmarked.map((bookmark) => ( - ({ - margin: theme.spacing(0.5), - '.MuiChip-icon': { - color: colorPallete[bookmark.id], - }, - backgroundColor: + + {/* All chips from bookmarks */} + {bookmarked.map((bookmark) => ( + ({ + margin: theme.spacing(0.5), + '.MuiChip-icon': { + color: colorPallete[bookmark.id], + }, + backgroundColor: bookmark.id === currentIntersection?.id || bookmark.id === currentSelection?.id ? 'rgba(0,0,0,0.2)' : 'default', - })} - key={bookmark.id} - aria-label={`Bookmarked intersection ${bookmark.label}${ - isBookmarkedIntersection(bookmark) ? `, size ${bookmark.size}` : ''}`} - onKeyDown={(e) => { - if (e.key === 'Enter') { - chipClicked(bookmark); - } - }} - label={isBookmarkedIntersection(bookmark) - ? `${bookmark.label} - ${bookmark.size}` : `${bookmark.label}`} - icon={} - deleteIcon={} - onClick={() => { + })} + key={bookmark.id} + aria-label={`Bookmarked intersection ${bookmark.label}${ + isBookmarkedIntersection(bookmark) ? `, size ${bookmark.size}` : ''}`} + onKeyDown={(e) => { + if (e.key === 'Enter') { chipClicked(bookmark); - }} - onDelete={() => { - if (currentIntersection?.id === bookmark.id) { - actions.setSelected(null); - } else if (currentSelection?.id === bookmark.id) { - actions.setElementSelection(null); - } - actions.removeBookmark(bookmark); - }} - /> - ))} - {/* Chip for the currently selected intersection */} - {currentIntersection && !bookmarked.find((b) => b.id === currentIntersection.id) && ( - ({ - margin: theme.spacing(0.5), - '.MuiChip-icon': { - color: nextColor, - }, - backgroundColor: 'rgba(0,0,0,0.2)', - })} - icon={} - aria-label={`Selected intersection ${currentIntersectionDisplayName}, size ${currentIntersection.size}`} - onKeyDown={(e) => { - if (e.key === 'Enter') { - actions.addBookmark({ - id: currentIntersection.id, - label: currentIntersectionDisplayName, - size: currentIntersection.size, - type: 'intersection', - }); - } - }} - label={`${currentIntersectionDisplayName} - ${currentIntersection.size}`} - onDelete={() => { - actions.addBookmark({ - id: currentIntersection.id, - label: currentIntersectionDisplayName, - size: currentIntersection.size, - type: 'intersection', - }); - }} - deleteIcon={} - /> - )} - {/* Chip for the current element selection */} - {currentSelection && !bookmarked.find((b) => b.id === currentSelection.id) && ( - ({ - margin: theme.spacing(0.5), - '.MuiChip-icon': { - color: elementSelectionColor, - }, - backgroundColor: 'rgba(0,0,0,0.2)', - })} - icon={} - aria-label={`Selected elements ${currentSelection.label}`} - onKeyDown={(e) => { - if (e.key === 'Enter') { - actions.addBookmark(structuredClone(currentSelection)); - } - }} - label={`${currentSelection.label}`} - onDelete={() => { - actions.addBookmark(structuredClone(currentSelection)); - }} - deleteIcon={} - /> - )} - - + } + }} + label={isBookmarkedIntersection(bookmark) + ? `${bookmark.label} - ${bookmark.size}` : `${bookmark.label}`} + icon={} + deleteIcon={} + onClick={() => { + chipClicked(bookmark); + }} + onDelete={() => { + if (currentIntersection?.id === bookmark.id) { + actions.setSelected(null); + } else if (currentSelection?.id === bookmark.id) { + actions.setElementSelection(null); + } + actions.removeBookmark(bookmark); + }} + /> + ))} + {/* Chip for the currently selected intersection */} + {currentIntersection && !bookmarked.find((b) => b.id === currentIntersection.id) && ( + ({ + margin: theme.spacing(0.5), + '.MuiChip-icon': { + color: nextColor, + }, + backgroundColor: 'rgba(0,0,0,0.2)', + })} + icon={} + aria-label={`Selected intersection ${currentIntersectionDisplayName}, size ${currentIntersection.size}`} + onKeyDown={(e) => { + if (e.key === 'Enter') { + actions.addBookmark({ + id: currentIntersection.id, + label: currentIntersectionDisplayName, + size: currentIntersection.size, + type: 'intersection', + }); + } + }} + label={`${currentIntersectionDisplayName} - ${currentIntersection.size}`} + onDelete={() => { + actions.addBookmark({ + id: currentIntersection.id, + label: currentIntersectionDisplayName, + size: currentIntersection.size, + type: 'intersection', + }); + }} + deleteIcon={} + /> + )} + {/* Chip for the current element selection */} + {currentSelection && !bookmarked.find((b) => b.id === currentSelection.id) && ( + ({ + margin: theme.spacing(0.5), + '.MuiChip-icon': { + color: elementSelectionColor, + }, + backgroundColor: 'rgba(0,0,0,0.2)', + })} + icon={} + aria-label={`Selected elements ${currentSelection.label}`} + onKeyDown={(e) => { + if (e.key === 'Enter') { + actions.addBookmark(structuredClone(currentSelection)); + } + }} + label={`${currentSelection.label}`} + onDelete={() => { + actions.addBookmark(structuredClone(currentSelection)); + }} + deleteIcon={} + /> + )} + ); }; diff --git a/packages/upset/src/components/ElementView/ElementVisualization.tsx b/packages/upset/src/components/ElementView/ElementVisualization.tsx index ec4db67a..10a38b37 100644 --- a/packages/upset/src/components/ElementView/ElementVisualization.tsx +++ b/packages/upset/src/components/ElementView/ElementVisualization.tsx @@ -7,8 +7,8 @@ import { SignalListener, VegaLite } from 'react-vega'; import { useRecoilValue } from 'recoil'; import { numericalQueryToBookmark, numericalQueriesEqual, isNumericalQuery } from '@visdesignlab/upset2-core'; -import { Button } from '@mui/material'; -import { bookmarkSelector, elementColorSelector } from '../../atoms/config/currentIntersectionAtom'; +import { Alert, Button } from '@mui/material'; +import { bookmarkSelector, currentIntersectionSelector, elementColorSelector } from '../../atoms/config/currentIntersectionAtom'; import { histogramSelector, scatterplotsSelector } from '../../atoms/config/plotAtoms'; import { elementItemMapSelector, currentNumericalQuery } from '../../atoms/elementsSelectors'; import { AddPlotDialog } from './AddPlotDialog'; @@ -32,6 +32,7 @@ export const ElementVisualization = () => { const items = useRecoilValue(elementItemMapSelector(bookmarked.map((b) => b.id))); const numericalQuery = useRecoilValue(currentNumericalQuery); const selectColor = useRecoilValue(elementColorSelector); + const currentIntersection = useRecoilValue(currentIntersectionSelector); const { actions }: {actions: UpsetActions} = useContext(ProvenanceContext); /** @@ -87,6 +88,18 @@ export const ElementVisualization = () => { draftSelection.current = undefined; }} > + {!currentIntersection && bookmarked.length === 0 && ( + + Please click on intersections to select an intersection. + + )} From a18a76aab35c447bc9293666f177bdf074f239b4 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Tue, 22 Oct 2024 14:08:16 -0600 Subject: [PATCH 20/24] Bugfixes: Update query selection fields when current selection changes; UI fixes for fields --- .../components/ElementView/QueryInterface.tsx | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/upset/src/components/ElementView/QueryInterface.tsx b/packages/upset/src/components/ElementView/QueryInterface.tsx index 7e0ec694..cda5b8ab 100644 --- a/packages/upset/src/components/ElementView/QueryInterface.tsx +++ b/packages/upset/src/components/ElementView/QueryInterface.tsx @@ -6,7 +6,9 @@ import { import { Box } from '@mui/system'; import { useRecoilValue } from 'recoil'; import { ElementQueryToBookmark, ElementQueryType } from '@visdesignlab/upset2-core'; -import { useCallback, useContext, useState } from 'react'; +import { + useCallback, useContext, useEffect, useState, +} from 'react'; import { queryColumnsSelector } from '../../atoms/dataAtom'; import { currentElementQuery } from '../../atoms/elementsSelectors'; import { ProvenanceContext } from '../Root'; @@ -32,6 +34,19 @@ export const QueryInterface = () => { const [typeField, setTypeField] = useState(currentSelection?.type); const [queryField, setQueryField] = useState(currentSelection?.query); + // Resets input state every time the current selection changes + useEffect(() => { + if (currentSelection) { + setAttField(currentSelection?.att); + setTypeField(currentSelection?.type); + setQueryField(currentSelection?.query); + } else { + setAttField(undefined); + setTypeField(undefined); + setQueryField(undefined); + } + }, [currentSelection]); + /* * Functions */ @@ -58,12 +73,13 @@ export const QueryInterface = () => { }, [attField, typeField, queryField, atts, actions, currentSelection]); return atts.length > 0 ? ( - + Attribute Name setTypeField(e.target.value)} @@ -102,8 +119,9 @@ export const QueryInterface = () => { css={{ width: '20%', height: '100%' }} onClick={saveOrClear} color={currentSelection ? 'error' : 'success'} + variant="outlined" > - {currentSelection ? 'Clear' : 'Save'} + {currentSelection ? 'Clear' : 'Apply'} From 069d0a070632e0eedadb6686ca11c3a8c305c51a Mon Sep 17 00:00:00 2001 From: NateLanza <58234814+NateLanza@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:15:31 -0600 Subject: [PATCH 21/24] Change copy on no selection message in Element Vis Co-authored-by: Jack Wilburn --- .../upset/src/components/ElementView/ElementVisualization.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/upset/src/components/ElementView/ElementVisualization.tsx b/packages/upset/src/components/ElementView/ElementVisualization.tsx index 10a38b37..0ec47357 100644 --- a/packages/upset/src/components/ElementView/ElementVisualization.tsx +++ b/packages/upset/src/components/ElementView/ElementVisualization.tsx @@ -97,7 +97,7 @@ export const ElementVisualization = () => { alignItems: 'center', margin: '0.5em 0', border: 'none', color: '#777777', }} > - Please click on intersections to select an intersection. + Please click on an intersection to visualize its attributes. )} From ab6dc6db8bf6794b34e67dca29171db9c94f5844 Mon Sep 17 00:00:00 2001 From: NateLanza <58234814+NateLanza@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:15:59 -0600 Subject: [PATCH 22/24] Typecheck formatting update Co-authored-by: Jack Wilburn --- packages/core/src/typecheck.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/typecheck.ts b/packages/core/src/typecheck.ts index 5003252a..e2f41169 100644 --- a/packages/core/src/typecheck.ts +++ b/packages/core/src/typecheck.ts @@ -184,7 +184,8 @@ export function isBookmark(b: unknown): b is Bookmark { && Object.hasOwn(b, 'type') && typeof (b as Bookmark).id === 'string' && typeof (b as Bookmark).label === 'string' - && ((b as Bookmark).type === 'intersection' + && ( + (b as Bookmark).type === 'intersection' || (b as Bookmark).type === 'numerical' || (b as Bookmark).type === 'element' ); From 166a000a14a78a64797791694d6cab42dc9fdf76 Mon Sep 17 00:00:00 2001 From: NateLanza <58234814+NateLanza@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:16:40 -0600 Subject: [PATCH 23/24] Use localCompare for att-based element filtering Co-authored-by: Jack Wilburn --- packages/core/src/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index cf401d49..bb30fbe8 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -64,9 +64,9 @@ export function filterItems(items: Item[], filter: ElementSelection): Item[] { case ElementQueryType.REGEX: return (new RegExp(query).test(`${item[att]}`)); case ElementQueryType.GREATER_THAN: - return item[att] > (typeof item[att] === 'number' ? Number(query) : query); + return `${item[att]}`.localeCompare(query, undefined, { numeric: typeof item[att] === 'number' }) > 0; case ElementQueryType.LESS_THAN: - return item[att] < (typeof item[att] === 'number' ? Number(query) : query); + return `${item[att]}`.localeCompare(query, undefined, { numeric: typeof item[att] === 'number' }) < 0; default: } return false; From d27a871818b2e296f4b249ca0522cea57f334544 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Wed, 23 Oct 2024 15:19:24 -0600 Subject: [PATCH 24/24] Correct button name in Query Selection test --- e2e-tests/elementView.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-tests/elementView.spec.ts b/e2e-tests/elementView.spec.ts index d7d04a7d..3636d2cc 100644 --- a/e2e-tests/elementView.spec.ts +++ b/e2e-tests/elementView.spec.ts @@ -183,7 +183,7 @@ async function setQuery(page: Page, att: string, type: string, query: string): P await page.getByRole('option', { name: type, exact: true }).click(); await page.getByPlaceholder('Query').click(); await page.getByPlaceholder('Query').fill(query); - await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('button', { name: 'Apply' }).click(); } test('Query Selection', async ({ page }) => {