diff --git a/packages/app/src/vis-packs/core/configs.ts b/packages/app/src/vis-packs/core/configs.ts index 16602389c..bf6b7c4f5 100644 --- a/packages/app/src/vis-packs/core/configs.ts +++ b/packages/app/src/vis-packs/core/configs.ts @@ -1,3 +1,4 @@ +export { MatrixConfigProvider } from './matrix/config'; export { LineConfigProvider } from './line/config'; export { HeatmapConfigProvider } from './heatmap/config'; export { ComplexConfigProvider } from './complex/config'; diff --git a/packages/app/src/vis-packs/core/matrix/MappedMatrixVis.tsx b/packages/app/src/vis-packs/core/matrix/MappedMatrixVis.tsx index e862762ec..e930cbf69 100644 --- a/packages/app/src/vis-packs/core/matrix/MappedMatrixVis.tsx +++ b/packages/app/src/vis-packs/core/matrix/MappedMatrixVis.tsx @@ -5,6 +5,7 @@ import { createPortal } from 'react-dom'; import type { DimensionMapping } from '../../../dimension-mapper/models'; import { useMappedArray, useSlicedDimsAndMapping } from '../hooks'; import MatrixToolbar from './MatrixToolbar'; +import { useMatrixVisConfig } from './config'; interface Props { value: Primitive[]; @@ -19,6 +20,8 @@ function MappedMatrixVis(props: Props) { const { value, dims, dimMapping, formatter, cellWidth, toolbarContainer } = props; + const sticky = useMatrixVisConfig((state) => state.sticky); + const [slicedDims, slicedMapping] = useSlicedDimsAndMapping(dims, dimMapping); const [mappedArray] = useMappedArray(value, slicedDims, slicedMapping); @@ -34,6 +37,7 @@ function MappedMatrixVis(props: Props) { dataArray={mappedArray} formatter={formatter} cellWidth={cellWidth} + sticky={sticky} /> ); diff --git a/packages/app/src/vis-packs/core/matrix/MatrixToolbar.tsx b/packages/app/src/vis-packs/core/matrix/MatrixToolbar.tsx index 595992b1f..8739c2f78 100644 --- a/packages/app/src/vis-packs/core/matrix/MatrixToolbar.tsx +++ b/packages/app/src/vis-packs/core/matrix/MatrixToolbar.tsx @@ -1,8 +1,9 @@ -import { DownloadBtn, Toolbar } from '@h5web/lib'; +import { DownloadBtn, Separator, ToggleBtn, Toolbar } from '@h5web/lib'; import type { Primitive, PrintableType } from '@h5web/shared'; import type { NdArray } from 'ndarray'; -import { FiDownload } from 'react-icons/fi'; +import { FiAnchor, FiDownload } from 'react-icons/fi'; +import { useMatrixVisConfig } from './config'; import { sliceToCsv } from './utils'; interface Props { @@ -11,6 +12,7 @@ interface Props { function MatrixToolbar(props: Props) { const { currentSlice } = props; + const { sticky, toggleSticky } = useMatrixVisConfig(); if (currentSlice && currentSlice.shape.length > 2) { throw new Error('Expected current slice to have at most two dimensions'); @@ -18,6 +20,15 @@ function MatrixToolbar(props: Props) { return ( + + + + {currentSlice && ( void; +} + +function createStore() { + return create( + persist( + (set) => ({ + sticky: false, + toggleSticky: () => set((state) => ({ sticky: !state.sticky })), + }), + { + name: 'h5web:matrix', + version: 1, + } + ) + ); +} + +const { Provider, useStore } = createContext(); +export const useMatrixVisConfig = useStore; + +export function MatrixConfigProvider(props: ConfigProviderProps) { + const { children } = props; + return {children}; +} diff --git a/packages/app/src/vis-packs/core/visualizations.ts b/packages/app/src/vis-packs/core/visualizations.ts index 3e9e69ca2..2e0abbba7 100644 --- a/packages/app/src/vis-packs/core/visualizations.ts +++ b/packages/app/src/vis-packs/core/visualizations.ts @@ -25,6 +25,7 @@ import { ComplexConfigProvider, ComplexLineConfigProvider, RgbConfigProvider, + MatrixConfigProvider, } from './configs'; import { RawVisContainer, @@ -76,6 +77,7 @@ export const CORE_VIS: Record = { name: Vis.Matrix, Icon: FiGrid, Container: MatrixVisContainer, + ConfigProvider: MatrixConfigProvider, supportsDataset: (dataset) => { return hasPrintableType(dataset) && hasArrayShape(dataset); }, diff --git a/packages/lib/src/vis/matrix/AnchorCell.tsx b/packages/lib/src/vis/matrix/AnchorCell.tsx deleted file mode 100644 index 6e75d8760..000000000 --- a/packages/lib/src/vis/matrix/AnchorCell.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useContext } from 'react'; -import { FiAnchor } from 'react-icons/fi'; - -import { GridSettingsContext } from './GridSettingsContext'; -import styles from './MatrixVis.module.css'; - -interface Props { - onToggle: () => void; - sticky: boolean; -} - -function AnchorCell(props: Props) { - const { onToggle, sticky } = props; - const { cellSize } = useContext(GridSettingsContext); - - return ( -
- -
- ); -} - -export default AnchorCell; diff --git a/packages/lib/src/vis/matrix/Cell.tsx b/packages/lib/src/vis/matrix/Cell.tsx index b0c32c893..426c8a92a 100644 --- a/packages/lib/src/vis/matrix/Cell.tsx +++ b/packages/lib/src/vis/matrix/Cell.tsx @@ -1,12 +1,12 @@ -import { useContext } from 'react'; +import { memo, useContext } from 'react'; import type { GridChildComponentProps } from 'react-window'; -import { GridSettingsContext } from './GridSettingsContext'; import styles from './MatrixVis.module.css'; +import { SettingsContext } from './context'; function Cell(props: GridChildComponentProps) { const { rowIndex, columnIndex, style } = props; - const { cellFormatter } = useContext(GridSettingsContext); + const { cellFormatter } = useContext(SettingsContext); // Disable index columns (rendering done by the innerElementType) if (rowIndex * columnIndex === 0) { @@ -20,7 +20,7 @@ function Cell(props: GridChildComponentProps) { role="cell" aria-rowindex={rowIndex} aria-colindex={columnIndex} - data-bg={(rowIndex + columnIndex) % 2 === 1 ? '' : undefined} + data-bg={(rowIndex + columnIndex) % 2 === 1 || undefined} > { // -1 to account for the index row and column @@ -30,4 +30,4 @@ function Cell(props: GridChildComponentProps) { ); } -export default Cell; +export default memo(Cell); diff --git a/packages/lib/src/vis/matrix/Grid.tsx b/packages/lib/src/vis/matrix/Grid.tsx new file mode 100644 index 000000000..744b84b3e --- /dev/null +++ b/packages/lib/src/vis/matrix/Grid.tsx @@ -0,0 +1,38 @@ +import { useMeasure } from '@react-hookz/web'; +import { useContext } from 'react'; +import { FixedSizeGrid as IndexedGrid } from 'react-window'; + +import Cell from './Cell'; +import styles from './MatrixVis.module.css'; +import StickyGrid from './StickyGrid'; +import { SettingsContext } from './context'; + +function Grid() { + const { rowCount, columnCount, cellSize, setRenderedItems } = + useContext(SettingsContext); + + const [wrapperSize, wrapperRef] = useMeasure(); + + return ( +
+ {wrapperSize && ( + + {Cell} + + )} +
+ ); +} + +export default Grid; diff --git a/packages/lib/src/vis/matrix/GridSettingsContext.tsx b/packages/lib/src/vis/matrix/GridSettingsContext.tsx deleted file mode 100644 index f536ac949..000000000 --- a/packages/lib/src/vis/matrix/GridSettingsContext.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { createContext } from 'react'; -import type { ReactNode } from 'react'; - -import type { Size } from '../models'; - -interface GridSettings { - cellSize: Size; - rowCount: number; - columnCount: number; - cellFormatter: (row: number, col: number) => string; -} - -export const GridSettingsContext = createContext({ - cellSize: { width: 0, height: 0 }, - rowCount: 0, - columnCount: 0, - cellFormatter: () => '0', -}); - -interface Props extends GridSettings { - children: ReactNode; -} - -function GridSettingsProvider(props: Props) { - const { children, ...settings } = props; - - return ( - - {children} - - ); -} - -export default GridSettingsProvider; diff --git a/packages/lib/src/vis/matrix/HeaderCells.tsx b/packages/lib/src/vis/matrix/HeaderCells.tsx new file mode 100644 index 000000000..7d6816b19 --- /dev/null +++ b/packages/lib/src/vis/matrix/HeaderCells.tsx @@ -0,0 +1,33 @@ +import { range } from 'lodash'; +import { useContext } from 'react'; + +import styles from './MatrixVis.module.css'; +import { SettingsContext } from './context'; + +interface Props { + indexMin: number; + indexMax: number; + transform: string /* to compensate for header cells not rendered in the range [0 indexMin] */; +} + +function HeaderCells(props: Props) { + const { indexMin, indexMax, transform } = props; + const { cellSize } = useContext(SettingsContext); + + return ( + <> + {range(indexMin, indexMax).map((index) => ( +
+ {index >= 0 && index} +
+ ))} + + ); +} + +export default HeaderCells; diff --git a/packages/lib/src/vis/matrix/IndexTrack.tsx b/packages/lib/src/vis/matrix/IndexTrack.tsx deleted file mode 100644 index e426c043c..000000000 --- a/packages/lib/src/vis/matrix/IndexTrack.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { range } from 'lodash'; -import { useContext } from 'react'; -import type { ReactNode } from 'react'; - -import { GridSettingsContext } from './GridSettingsContext'; -import styles from './MatrixVis.module.css'; - -interface Props { - cellCount: number; - className: string; - children?: ReactNode; -} - -function IndexTrack(props: Props) { - const { cellCount, className, children } = props; - const { cellSize } = useContext(GridSettingsContext); - - return ( -
- {children} - {range(cellCount).map((index) => ( -
- {index >= 0 && index} -
- ))} -
- ); -} - -export default IndexTrack; diff --git a/packages/lib/src/vis/matrix/MatrixVis.module.css b/packages/lib/src/vis/matrix/MatrixVis.module.css index 23296cb58..6772cedc9 100644 --- a/packages/lib/src/vis/matrix/MatrixVis.module.css +++ b/packages/lib/src/vis/matrix/MatrixVis.module.css @@ -5,21 +5,24 @@ .grid { position: absolute !important; /* prevent feedback loop with `useMeasure` on wrapper */ + z-index: 0; /* stacking context */ color: var(--h5w-matrix--color, inherit); font-family: var(--h5w-matrix--fontFamily, monospace); font-size: var(--h5w-matrix--fontSize, inherit); scrollbar-width: thin; } -.cell { +.innerContainer { display: flex; - align-items: center; - justify-content: center; - background-color: var(--h5w-matrix-cell--bgColor, transparent); } -.cell[data-bg] { - background-color: var(--h5w-matrix-cell--bgColorAlt, #eee); +.colHeaders { + display: flex; +} + +.rowHeaders { + display: flex; + flex-direction: column; } .indexCell { @@ -43,44 +46,36 @@ background-color: var(--h5w-matrix-indexCell--bgColorAlt, lightgray); } -.stickyGrid[data-sticky] .stickyElement { - position: sticky !important; - top: 0 !important; - left: 0 !important; +.topLeftCell { + composes: indexCell; } -.indexRow { - composes: stickyElement; - display: flex; - z-index: 2; -} - -.indexColumn { - composes: stickyElement; - display: flex; - flex-direction: column; - z-index: 1; +.stickyGrid[data-sticky] .colHeaders { + position: sticky; + top: 0; + z-index: 2; /* above row header cells and content cells */ } -.anchorCell { - composes: indexCell stickyElement; - align-items: stretch; - z-index: 3; +.stickyGrid[data-sticky] .rowHeaders { + position: sticky; + left: 0; + z-index: 1; /* above content cells */ } -.innerContainer { - display: flex; +.stickyGrid[data-sticky] .topLeftCell { + position: sticky; + left: 0; + top: 0; + z-index: 1; /* above column header cells */ } -.anchorBtn { - composes: btnClean from global; - flex: 1 1 0%; +.cell { display: flex; align-items: center; justify-content: center; + background-color: var(--h5w-matrix-cell--bgColor, transparent); } -.anchorBtn:hover, -.anchorBtn[aria-pressed='true'] { - background-color: var(--h5w-matrix-anchorCell--bgColor, silver); +.cell[data-bg] { + background-color: var(--h5w-matrix-cell--bgColorAlt, #eee); } diff --git a/packages/lib/src/vis/matrix/MatrixVis.tsx b/packages/lib/src/vis/matrix/MatrixVis.tsx index e0f56ffc9..bd37bb799 100644 --- a/packages/lib/src/vis/matrix/MatrixVis.tsx +++ b/packages/lib/src/vis/matrix/MatrixVis.tsx @@ -1,14 +1,9 @@ import type { Primitive } from '@h5web/shared'; -import { useMeasure } from '@react-hookz/web'; import type { NdArray } from 'ndarray'; -import { forwardRef } from 'react'; -import { FixedSizeGrid as IndexedGrid } from 'react-window'; import type { PrintableType } from '../models'; -import Cell from './Cell'; -import GridSettingsProvider from './GridSettingsContext'; -import styles from './MatrixVis.module.css'; -import StickyGrid from './StickyGrid'; +import Grid from './Grid'; +import GridProvider from './context'; const CELL_HEIGHT = 32; @@ -16,45 +11,31 @@ interface Props { dataArray: NdArray[]>; formatter: (value: Primitive) => string; cellWidth: number; + sticky: boolean; } function MatrixVis(props: Props) { - const { dataArray, formatter, cellWidth } = props; + const { dataArray, formatter, cellWidth, sticky } = props; const dims = dataArray.shape; - const [wrapperSize, wrapperRef] = useMeasure(); - const rowCount = dims[0] + 1; // includes IndexRow const columnCount = (dims.length === 2 ? dims[1] : 1) + 1; // includes IndexColumn + const cellFormatter = + dims.length === 1 + ? (row: number) => formatter(dataArray.get(row)) + : (row: number, col: number) => formatter(dataArray.get(row, col)); + return ( - formatter(dataArray.get(row)) - : (row, col) => formatter(dataArray.get(row, col)) - } + cellSize={{ width: cellWidth, height: CELL_HEIGHT }} + cellFormatter={cellFormatter} + sticky={sticky} > -
- {wrapperSize && ( - - {Cell} - - )} -
-
+ + ); } diff --git a/packages/lib/src/vis/matrix/StickyGrid.tsx b/packages/lib/src/vis/matrix/StickyGrid.tsx index 82ef14884..c12bf5978 100644 --- a/packages/lib/src/vis/matrix/StickyGrid.tsx +++ b/packages/lib/src/vis/matrix/StickyGrid.tsx @@ -1,39 +1,58 @@ -import { useContext, useState } from 'react'; -import type { ReactNode, Ref } from 'react'; +import type { ReactNode } from 'react'; +import { useContext, forwardRef } from 'react'; -import AnchorCell from './AnchorCell'; -import { GridSettingsContext } from './GridSettingsContext'; -import IndexTrack from './IndexTrack'; +import HeaderCells from './HeaderCells'; import styles from './MatrixVis.module.css'; +import { SettingsContext, RenderedItemsContext } from './context'; interface Props { children: ReactNode; style: React.CSSProperties; } -function StickyGrid(props: Props, ref: Ref) { +const StickyGrid = forwardRef((props, ref) => { const { children, style } = props; - const { rowCount, columnCount } = useContext(GridSettingsContext); + const { rowCount, columnCount, cellSize, sticky } = + useContext(SettingsContext); - const [sticky, setSticky] = useState(true); + const renderedItems = useContext(RenderedItemsContext); + const { + overscanColumnStartIndex: colStart = 0, + overscanColumnStopIndex: colStop = 0, + overscanRowStartIndex: rowStart = 0, + overscanRowStopIndex: rowStop = 0, + } = renderedItems || {}; return (
- - setSticky(!sticky)} /> - +
+
+ +
- +
+ +
{children}
); -} +}); export default StickyGrid; diff --git a/packages/lib/src/vis/matrix/context.tsx b/packages/lib/src/vis/matrix/context.tsx new file mode 100644 index 000000000..3f98dbf27 --- /dev/null +++ b/packages/lib/src/vis/matrix/context.tsx @@ -0,0 +1,53 @@ +import { useState, useMemo, createContext } from 'react'; +import type { ReactNode } from 'react'; +import type { GridOnItemsRenderedProps } from 'react-window'; + +import type { Size } from '../models'; + +interface GridSettings { + rowCount: number; + columnCount: number; + cellSize: Size; + sticky: boolean; + cellFormatter: (row: number, col: number) => string; + setRenderedItems: (renderedItems: GridOnItemsRenderedProps) => void; +} + +interface Props extends Omit { + children: ReactNode; +} + +export const SettingsContext = createContext({} as GridSettings); +export const RenderedItemsContext = createContext< + GridOnItemsRenderedProps | undefined +>(undefined); + +function GridProvider(props: Props) { + const { rowCount, columnCount, cellSize, sticky, cellFormatter, children } = + props; + + const [renderedItems, setRenderedItems] = + useState(); + + const settings = useMemo( + () => ({ + rowCount, + columnCount, + cellSize, + sticky, + cellFormatter, + setRenderedItems, + }), + [cellFormatter, cellSize, columnCount, sticky, rowCount] + ); + + return ( + + + {children} + + + ); +} + +export default GridProvider;