Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Maintain dimension mapping when possible #1698

Merged
merged 1 commit into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex';
import styles from './App.module.css';
import BreadcrumbsBar from './breadcrumbs/BreadcrumbsBar';
import type { FeedbackContext } from './breadcrumbs/models';
import { DimMappingProvider } from './dimension-mapper/store';
import EntityLoader from './EntityLoader';
import ErrorFallback from './ErrorFallback';
import MetadataViewer from './metadata-viewer/MetadataViewer';
Expand Down Expand Up @@ -84,21 +85,25 @@ function App(props: Props) {
getFeedbackURL={getFeedbackURL}
/>
<VisConfigProvider>
<ErrorBoundary
resetKeys={[selectedPath, isInspecting]}
FallbackComponent={ErrorFallback}
>
<Suspense fallback={<EntityLoader isInspecting={isInspecting} />}>
{isInspecting ? (
<MetadataViewer
path={selectedPath}
onSelectPath={onSelectPath}
/>
) : (
<Visualizer path={selectedPath} />
)}
</Suspense>
</ErrorBoundary>
<DimMappingProvider>
loichuder marked this conversation as resolved.
Show resolved Hide resolved
<ErrorBoundary
resetKeys={[selectedPath, isInspecting]}
FallbackComponent={ErrorFallback}
>
<Suspense
fallback={<EntityLoader isInspecting={isInspecting} />}
>
{isInspecting ? (
<MetadataViewer
path={selectedPath}
onSelectPath={onSelectPath}
/>
) : (
<Visualizer path={selectedPath} />
)}
</Suspense>
</ErrorBoundary>
</DimMappingProvider>
</VisConfigProvider>
</ReflexElement>
</ReflexContainer>
Expand Down
86 changes: 82 additions & 4 deletions packages/app/src/__tests__/DimensionMapper.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { screen, within } from '@testing-library/react';
import { expect, test } from 'vitest';

import { renderApp, waitForAllLoaders } from '../test-utils';
import { getDimMappingBtn, renderApp } from '../test-utils';
import { Vis } from '../vis-packs/core/visualizations';

test('control mapping for X axis when visualizing 2D dataset as Line', async () => {
Expand Down Expand Up @@ -100,14 +100,92 @@ test('slice through 2D dataset', async () => {
const { user } = await renderApp({
initialPath: '/nD_datasets/twoD',
preferredVis: Vis.Line,
withFakeTimers: true, // required since React 18 upgrade (along with `waitForAllLoaders` below)
loichuder marked this conversation as resolved.
Show resolved Hide resolved
});

await waitForAllLoaders();

// Move to next slice with keyboard
const d0Slider = screen.getByRole('slider', { name: 'D0' });
await user.type(d0Slider, '{ArrowUp}');

expect(d0Slider).toHaveAttribute('aria-valuenow', '1');
});

test('maintain mapping when switching to inspect mode and back', async () => {
const { user } = await renderApp({
initialPath: '/nD_datasets/twoD',
preferredVis: Vis.Heatmap,
});

// Swap axes for D0 and D1
await user.click(getDimMappingBtn('x', 0));

// Toggle inspect mode
await user.click(screen.getByRole('tab', { name: 'Inspect' }));
await user.click(screen.getByRole('tab', { name: 'Display' }));

expect(getDimMappingBtn('x', 0)).toBeChecked();
expect(getDimMappingBtn('x', 1)).not.toBeChecked();
});

test('maintain mapping when switching to visualization with same axes count', async () => {
const { user, selectVisTab } = await renderApp({
initialPath: '/nD_datasets/twoD',
preferredVis: Vis.Heatmap,
});

// Swap axes for D0 and D1
await user.click(getDimMappingBtn('x', 0));

// Switch to Matrix visualization
await selectVisTab(Vis.Matrix);

expect(getDimMappingBtn('x', 0)).toBeChecked();
expect(getDimMappingBtn('x', 1)).not.toBeChecked();
});

test('maintain mapping when switching to dataset with same dimensions', async () => {
const { user, selectExplorerNode } = await renderApp({
initialPath: '/nD_datasets/twoD_bool',
preferredVis: Vis.Line,
});

// Swap axes for D0 and D1
await user.click(getDimMappingBtn('x', 0));

// Switch to dataset with same dimensions
await selectExplorerNode('twoD_enum');

expect(getDimMappingBtn('x', 0)).toBeChecked();
expect(getDimMappingBtn('x', 1)).not.toBeChecked();
});

test('reset mapping when switching to visualization with different axes count', async () => {
const { user, selectVisTab } = await renderApp({
initialPath: '/nD_datasets/twoD',
preferredVis: Vis.Heatmap,
});

// Swap axes for D0 and D1
await user.click(getDimMappingBtn('x', 0));

// Switch to Line visualization
await selectVisTab(Vis.Line);

expect(getDimMappingBtn('x', 0)).not.toBeChecked();
expect(getDimMappingBtn('x', 1)).toBeChecked();
});

test('reset mapping when switching to dataset with different dimensions', async () => {
const { user, selectExplorerNode } = await renderApp({
initialPath: '/nD_datasets/twoD',
preferredVis: Vis.Heatmap,
});

// Swap axes for D0 and D1
await user.click(getDimMappingBtn('x', 0));

// Switch to dataset with different dimensions
await selectExplorerNode('twoD_cplx');

expect(getDimMappingBtn('x', 0)).not.toBeChecked();
expect(getDimMappingBtn('x', 1)).toBeChecked();
});
axelboc marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 0 additions & 10 deletions packages/app/src/dimension-mapper/hooks.ts

This file was deleted.

72 changes: 72 additions & 0 deletions packages/app/src/dimension-mapper/store.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { ArrayShape } from '@h5web/shared/hdf5-models';
import type { PropsWithChildren } from 'react';
import { createContext, useContext, useEffect, useState } from 'react';
import type { StoreApi } from 'zustand';
import { createStore, useStore } from 'zustand';

import { areSameDims } from '../vis-packs/nexus/utils';
import type { DimensionMapping } from './models';

interface DimMappingState {
dims: ArrayShape;
axesCount: number;
mapping: DimensionMapping;
setMapping: (mapping: DimensionMapping) => void;
reset: (
dims: ArrayShape,
axesCount: number,
mapping: DimensionMapping,
) => void;
}

function createLineConfigStore() {
return createStore<DimMappingState>((set) => ({
dims: [],
axesCount: 0,
mapping: [],
setMapping: (mapping) => set({ mapping }),
reset: (dims, axesCount, mapping) => {
set({ dims, axesCount, mapping });
},
}));
}

const StoreContext = createContext({} as StoreApi<DimMappingState>);

interface Props {}
export function DimMappingProvider(props: PropsWithChildren<Props>) {
const { children } = props;

const [store] = useState(createLineConfigStore);

return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
}

export function useDimMappingState(
dims: number[],
axesCount: number,
): [DimensionMapping, (mapping: DimensionMapping) => void] {
const state = useStore(useContext(StoreContext));

/* If current mapping was initialised with different axes count and dimensions,
* need to compute new mapping and reset state. */
const isStale =
axesCount !== state.axesCount || !areSameDims(dims, state.dims);

const mapping = isStale
? [
...Array.from({ length: dims.length - axesCount }, () => 0),
...(dims.length > 0
? ['y' as const, 'x' as const].slice(-axesCount)
: []),
]
: state.mapping;

useEffect(() => {
state.reset(dims, axesCount, mapping);
}, [isStale]); // eslint-disable-line react-hooks/exhaustive-deps
loichuder marked this conversation as resolved.
Show resolved Hide resolved

return [mapping, state.setMapping];
}
5 changes: 5 additions & 0 deletions packages/app/src/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ export function getSelectedVisTab(): string {
return selectedTab.textContent;
}

export function getDimMappingBtn(axis: 'x' | 'y', dim: number): HTMLElement {
const radioGroup = screen.getByLabelText(`Dimension as ${axis} axis`);
return within(radioGroup).getByRole('radio', { name: `D${dim}` });
}

/**
* Mock a console method.
* Mocks are automatically cleared and restored after every test but you
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from '@h5web/shared/guards';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import { useDimMappingState } from '../../../dimension-mapper/store';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
import { useValuesInCache } from '../hooks';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@h5web/shared/guards';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import { useDimMappingState } from '../../../dimension-mapper/store';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
import { useHeatmapConfig } from '../heatmap/config';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@h5web/shared/guards';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import { useDimMappingState } from '../../../dimension-mapper/store';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
import { useValuesInCache } from '../hooks';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@h5web/shared/guards';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import { useDimMappingState } from '../../../dimension-mapper/store';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
import { useIgnoreFillValue, useValuesInCache } from '../hooks';
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/vis-packs/core/line/LineVisContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from '@h5web/shared/guards';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import { useDimMappingState } from '../../../dimension-mapper/store';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
import { useIgnoreFillValue, useValuesInCache } from '../hooks';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from '@h5web/shared/guards';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import { useDimMappingState } from '../../../dimension-mapper/store';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
import { useValuesInCache } from '../hooks';
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/vis-packs/core/rgb/RgbVisContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@h5web/shared/guards';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import { useDimMappingState } from '../../../dimension-mapper/store';
import { useDataContext } from '../../../providers/DataProvider';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@h5web/shared/guards';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import { useDimMappingState } from '../../../dimension-mapper/store';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
import { useValuesInCache } from '../hooks';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { assertGroup, assertMinDims } from '@h5web/shared/guards';
import { useState } from 'react';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import { useDimMappingState } from '../../../dimension-mapper/store';
import { useComplexConfig } from '../../core/complex/config';
import MappedComplexVis from '../../core/complex/MappedComplexVis';
import { useHeatmapConfig } from '../../core/heatmap/config';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ScaleType } from '@h5web/lib';
import { assertGroup, isAxisScaleType } from '@h5web/shared/guards';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import { useDimMappingState } from '../../../dimension-mapper/store';
import { useComplexLineConfig } from '../../core/complex/lineConfig';
import MappedComplexLineVis from '../../core/complex/MappedComplexLineVis';
import { useLineConfig } from '../../core/line/config';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { assertGroup, assertMinDims } from '@h5web/shared/guards';
import { useState } from 'react';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import { useDimMappingState } from '../../../dimension-mapper/store';
import { useHeatmapConfig } from '../../core/heatmap/config';
import MappedHeatmapVis from '../../core/heatmap/MappedHeatmapVis';
import { getSliceSelection } from '../../core/utils';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { assertGroup, assertMinDims } from '@h5web/shared/guards';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import { useDimMappingState } from '../../../dimension-mapper/store';
import { useRgbConfig } from '../../core/rgb/config';
import MappedRgbVis from '../../core/rgb/MappedRgbVis';
import { getSliceSelection } from '../../core/utils';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ScaleType } from '@h5web/lib';
import { assertGroup, isAxisScaleType } from '@h5web/shared/guards';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import { useDimMappingState } from '../../../dimension-mapper/store';
import { useLineConfig } from '../../core/line/config';
import MappedLineVis from '../../core/line/MappedLineVis';
import { getSliceSelection } from '../../core/utils';
Expand Down