From 25210343b9c67a4e3f6a51e531cbbbbaec542fa6 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Tue, 24 Sep 2024 15:57:08 -0600 Subject: [PATCH 01/37] Add selector for attribute values in a row --- packages/upset/src/atoms/elementsSelectors.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/upset/src/atoms/elementsSelectors.ts b/packages/upset/src/atoms/elementsSelectors.ts index 2a8be879..39ac689d 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, ElementSelection, Item, Row, flattenedOnlyRows, getItems, } from '@visdesignlab/upset2-core'; import { selector, selectorFamily } from 'recoil'; import { @@ -48,6 +48,26 @@ export const elementSelector = selectorFamily< }, }); +/** + * Gets all values for a given attribute for all items in a given row. + * If the provided attribute does not exist or is not numeric, + * outputs a console warning & returns an empty list. + */ +export const attValuesSelector = selectorFamily({ + key: 'att-values', + get: ({ row, att }) => ({ get }) => { + const items = get(elementSelector(row.id)); + + // We could filter the whole array before we map, but attributes should all be the same type, + // so its sufficient and more performant to only check the first attribute + if (!items[0][att] || typeof items[0][att] !== 'number') { + console.warn('Attempted to get values for nonexistent or non-numeric attribute ', att); + return []; + } + return items.map((item) => item[att] as number); + }, +}); + /** * Gets the number of elements in the intersection represented by the provided ID * @param id - The ID of the intersection to get elements for. From f08aed32d42fb416ed80d508f8e7a12ff28f9e87 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Tue, 24 Sep 2024 15:57:25 -0600 Subject: [PATCH 02/37] Use row attribute value selector to improve AttributeBar performance --- .../Columns/Attribute/AttributeBar.tsx | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/packages/upset/src/components/Columns/Attribute/AttributeBar.tsx b/packages/upset/src/components/Columns/Attribute/AttributeBar.tsx index 87ab3cea..bf890493 100644 --- a/packages/upset/src/components/Columns/Attribute/AttributeBar.tsx +++ b/packages/upset/src/components/Columns/Attribute/AttributeBar.tsx @@ -1,7 +1,7 @@ import { - Aggregate, SixNumberSummary, Items, Subset, isRowAggregate, + Aggregate, SixNumberSummary, Subset, isRowAggregate, } from '@visdesignlab/upset2-core'; -import React, { FC, useMemo } from 'react'; +import React, { FC } from 'react'; import { useRecoilValue } from 'recoil'; import { attributeMinMaxSelector } from '../../../atoms/attributeAtom'; import { dimensionsSelector } from '../../../atoms/dimensionsAtom'; @@ -11,9 +11,9 @@ import { BoxPlot } from './AttributePlots/BoxPlot'; import { DotPlot } from './AttributePlots/DotPlot'; import { StripPlot } from './AttributePlots/StripPlot'; import { DensityPlot } from './AttributePlots/DensityPlot'; -import { itemsAtom } from '../../../atoms/itemsAtoms'; import { DeviationBar } from '../DeviationBar'; import { attributePlotsSelector } from '../../../atoms/config/plotAtoms'; +import { attValuesSelector } from '../../../atoms/elementsSelectors'; /** * Attribute bar props @@ -37,30 +37,12 @@ type Props = { // Threshold for when to render a dot plot regardless of selected plot type const DOT_PLOT_THRESHOLD = 5; -/** - * Get the values from a row based on the attribute - * @param row The row to get the values from - * @param attribute The attribute to get the values for - * @param items The items to get the values from - * @returns The values for the attribute - */ -const getValuesFromRow = (row: Subset | Aggregate, attribute: string, items: Items): number[] => { - if (isRowAggregate(row)) { - return Object.values(row.items.values).map((item) => getValuesFromRow(item, attribute, items)).flat(); - } - - return Object.entries(items).filter( - ([key, _]) => row.items.includes(key), - ).map(([_, value]) => Number(value[attribute])); -}; - // this is recomputing every hover event? export const AttributeBar: FC = ({ attribute, summary, row }) => { const dimensions = useRecoilValue(dimensionsSelector); const { min, max } = useRecoilValue(attributeMinMaxSelector(attribute)); const scale = useScale([min, max], [0, dimensions.attribute.width]); - const items = useRecoilValue(itemsAtom); - const values = useMemo(() => getValuesFromRow(row, attribute, items), [row, attribute, items]); + const values = useRecoilValue(attValuesSelector({ row, att: attribute })); const attributePlots = useRecoilValue(attributePlotsSelector); From 9fc352b8252de6d73b36a06aaf7cfa551dec62e0 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Tue, 24 Sep 2024 16:59:05 -0600 Subject: [PATCH 03/37] Updates to AttributeBar comments & style --- .../components/Columns/Attribute/AttributeBar.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/upset/src/components/Columns/Attribute/AttributeBar.tsx b/packages/upset/src/components/Columns/Attribute/AttributeBar.tsx index bf890493..12309348 100644 --- a/packages/upset/src/components/Columns/Attribute/AttributeBar.tsx +++ b/packages/upset/src/components/Columns/Attribute/AttributeBar.tsx @@ -37,7 +37,9 @@ type Props = { // Threshold for when to render a dot plot regardless of selected plot type const DOT_PLOT_THRESHOLD = 5; -// this is recomputing every hover event? +/** + * A single attribute chart, visualizing one attribute's values for one intersection. + */ export const AttributeBar: FC = ({ attribute, summary, row }) => { const dimensions = useRecoilValue(dimensionsSelector); const { min, max } = useRecoilValue(attributeMinMaxSelector(attribute)); @@ -46,7 +48,14 @@ export const AttributeBar: FC = ({ attribute, summary, row }) => { const attributePlots = useRecoilValue(attributePlotsSelector); - if (typeof summary !== 'number' && (summary.max === undefined || summary.min === undefined || summary.first === undefined || summary.third === undefined || summary.median === undefined)) { + if ( + typeof summary !== 'number' + && ( + summary.max === undefined + || summary.min === undefined + || summary.first === undefined + || summary.third === undefined + || summary.median === undefined)) { return null; } From 8cedadbdb887d7932af2c5da283d55fd8147cf98 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Tue, 24 Sep 2024 17:35:26 -0600 Subject: [PATCH 04/37] Exclude element sidebar from DOM unless open --- packages/upset/src/components/Root.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/upset/src/components/Root.tsx b/packages/upset/src/components/Root.tsx index ba57552a..9b0aa6ea 100644 --- a/packages/upset/src/components/Root.tsx +++ b/packages/upset/src/components/Root.tsx @@ -169,7 +169,7 @@ export const Root: FC = ({ - {elementSidebar && } + {elementSidebar && elementSidebar.open && } {provVis && } {(altTextSidebar && generateAltText) && } From 2963aa8c4c0946b435eb8e2bcf4410f1d7375236 Mon Sep 17 00:00:00 2001 From: Nate Lanza Date: Thu, 26 Sep 2024 18:03:02 -0600 Subject: [PATCH 05/37] Use callbacks for CollapseAllButton's functions --- .../components/Header/CollapseAllButton.tsx | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/upset/src/components/Header/CollapseAllButton.tsx b/packages/upset/src/components/Header/CollapseAllButton.tsx index f98940e0..775de2bc 100644 --- a/packages/upset/src/components/Header/CollapseAllButton.tsx +++ b/packages/upset/src/components/Header/CollapseAllButton.tsx @@ -2,7 +2,7 @@ import { css, SvgIcon, Tooltip } from '@mui/material'; import { DoubleArrow } from '@mui/icons-material'; import { getRows, isRowAggregate } from '@visdesignlab/upset2-core'; import { useRecoilValue } from 'recoil'; -import { useContext, useState } from 'react'; +import { useCallback, useContext, useState } from 'react'; import Group from '../custom/Group'; import { mousePointer } from '../../utils/styles'; import { ProvenanceContext } from '../Root'; @@ -20,6 +20,10 @@ const collapseAllStyle = css` `; export const CollapseAllButton = () => { + /* + * State + */ + const firstAggregateBy = useRecoilValue(firstAggregateSelector); const { provenance, actions } = useContext(ProvenanceContext); const data = useRecoilValue(dataAtom); @@ -27,7 +31,14 @@ export const CollapseAllButton = () => { const rows = getRows(data, provenance.getState()); const [allCollapsed, setAllCollapsed] = useState(false); - const toggleCollapseAll = () => { + /* + * Callbacks + */ + + /** + * Toggles the collapse state of all rows. + */ + const toggleCollapseAll = useCallback(() => { const ids: string[] = []; if (allCollapsed === true) { @@ -44,14 +55,17 @@ export const CollapseAllButton = () => { setAllCollapsed(true); actions.collapseAll(ids); } - }; + }, [allCollapsed, actions, rows]); - const getTransform = () => { + /** + * Get the transform for the icon. + */ + const getTransform = useCallback(() => { if (!allCollapsed) { return `rotate(-90) translate(-${iconSize}, -${iconSize})`; } return 'rotate(90)'; - }; + }, [allCollapsed, iconSize]); return (