diff --git a/cypress/snapshots/app.cy.ts/auximage.snap.png b/cypress/snapshots/app.cy.ts/auximage.snap.png index fdd01432d..c68917c9b 100644 Binary files a/cypress/snapshots/app.cy.ts/auximage.snap.png and b/cypress/snapshots/app.cy.ts/auximage.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/auxspectrum.snap.png b/cypress/snapshots/app.cy.ts/auxspectrum.snap.png index ff1a62632..3e10aacda 100644 Binary files a/cypress/snapshots/app.cy.ts/auxspectrum.snap.png and b/cypress/snapshots/app.cy.ts/auxspectrum.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/bgr_image.snap.png b/cypress/snapshots/app.cy.ts/bgr_image.snap.png index b572c4676..8a17a115f 100644 Binary files a/cypress/snapshots/app.cy.ts/bgr_image.snap.png and b/cypress/snapshots/app.cy.ts/bgr_image.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/compound_1D.snap.png b/cypress/snapshots/app.cy.ts/compound_1D.snap.png index 158c659f4..ca3cdda42 100644 Binary files a/cypress/snapshots/app.cy.ts/compound_1D.snap.png and b/cypress/snapshots/app.cy.ts/compound_1D.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/heatmap_2D.snap.png b/cypress/snapshots/app.cy.ts/heatmap_2D.snap.png index d7b5626f5..2d45f5029 100644 Binary files a/cypress/snapshots/app.cy.ts/heatmap_2D.snap.png and b/cypress/snapshots/app.cy.ts/heatmap_2D.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/heatmap_2D_complex.snap.png b/cypress/snapshots/app.cy.ts/heatmap_2D_complex.snap.png index dea31404c..cfcaafd26 100644 Binary files a/cypress/snapshots/app.cy.ts/heatmap_2D_complex.snap.png and b/cypress/snapshots/app.cy.ts/heatmap_2D_complex.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/heatmap_2D_inverted_cmap.snap.png b/cypress/snapshots/app.cy.ts/heatmap_2D_inverted_cmap.snap.png index 7b7626318..bbd2513af 100644 Binary files a/cypress/snapshots/app.cy.ts/heatmap_2D_inverted_cmap.snap.png and b/cypress/snapshots/app.cy.ts/heatmap_2D_inverted_cmap.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/heatmap_4d_default.snap.png b/cypress/snapshots/app.cy.ts/heatmap_4d_default.snap.png index 81ea11d37..c9b65f119 100644 Binary files a/cypress/snapshots/app.cy.ts/heatmap_4d_default.snap.png and b/cypress/snapshots/app.cy.ts/heatmap_4d_default.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/heatmap_4d_remapped.snap.png b/cypress/snapshots/app.cy.ts/heatmap_4d_remapped.snap.png index 03b609dc9..1ce19b668 100644 Binary files a/cypress/snapshots/app.cy.ts/heatmap_4d_remapped.snap.png and b/cypress/snapshots/app.cy.ts/heatmap_4d_remapped.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/heatmap_4d_sliced.snap.png b/cypress/snapshots/app.cy.ts/heatmap_4d_sliced.snap.png index cd9872e8e..96bc89c2e 100644 Binary files a/cypress/snapshots/app.cy.ts/heatmap_4d_sliced.snap.png and b/cypress/snapshots/app.cy.ts/heatmap_4d_sliced.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/heatmap_4d_zeros.snap.png b/cypress/snapshots/app.cy.ts/heatmap_4d_zeros.snap.png index f7f5941f2..fd6cee207 100644 Binary files a/cypress/snapshots/app.cy.ts/heatmap_4d_zeros.snap.png and b/cypress/snapshots/app.cy.ts/heatmap_4d_zeros.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/heatmap_domain.snap.png b/cypress/snapshots/app.cy.ts/heatmap_domain.snap.png index 11fe2a081..f7d944e77 100644 Binary files a/cypress/snapshots/app.cy.ts/heatmap_domain.snap.png and b/cypress/snapshots/app.cy.ts/heatmap_domain.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/line_1D.snap.png b/cypress/snapshots/app.cy.ts/line_1D.snap.png index 84d542f73..fe8980f68 100644 Binary files a/cypress/snapshots/app.cy.ts/line_1D.snap.png and b/cypress/snapshots/app.cy.ts/line_1D.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/line_complex_1D.snap.png b/cypress/snapshots/app.cy.ts/line_complex_1D.snap.png index f519f6d10..23b316761 100644 Binary files a/cypress/snapshots/app.cy.ts/line_complex_1D.snap.png and b/cypress/snapshots/app.cy.ts/line_complex_1D.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/logspectrum.snap.png b/cypress/snapshots/app.cy.ts/logspectrum.snap.png index 3eb982627..1e13c9414 100644 Binary files a/cypress/snapshots/app.cy.ts/logspectrum.snap.png and b/cypress/snapshots/app.cy.ts/logspectrum.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/matrix_1D.snap.png b/cypress/snapshots/app.cy.ts/matrix_1D.snap.png index 5fc732f42..731e6168f 100644 Binary files a/cypress/snapshots/app.cy.ts/matrix_1D.snap.png and b/cypress/snapshots/app.cy.ts/matrix_1D.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/nximage.snap.png b/cypress/snapshots/app.cy.ts/nximage.snap.png index 0a6f985b7..ece716b98 100644 Binary files a/cypress/snapshots/app.cy.ts/nximage.snap.png and b/cypress/snapshots/app.cy.ts/nximage.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/nximage_complex_2d.snap.png b/cypress/snapshots/app.cy.ts/nximage_complex_2d.snap.png index 4de43f04a..ae4864793 100644 Binary files a/cypress/snapshots/app.cy.ts/nximage_complex_2d.snap.png and b/cypress/snapshots/app.cy.ts/nximage_complex_2d.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/nxline.snap.png b/cypress/snapshots/app.cy.ts/nxline.snap.png index 097211a08..fcf51ccc3 100644 Binary files a/cypress/snapshots/app.cy.ts/nxline.snap.png and b/cypress/snapshots/app.cy.ts/nxline.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/nxline_complex_2d_aux.snap.png b/cypress/snapshots/app.cy.ts/nxline_complex_2d_aux.snap.png index b1906c3c0..74976367f 100644 Binary files a/cypress/snapshots/app.cy.ts/nxline_complex_2d_aux.snap.png and b/cypress/snapshots/app.cy.ts/nxline_complex_2d_aux.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/nxrgb.snap.png b/cypress/snapshots/app.cy.ts/nxrgb.snap.png index 2cd26c975..bc208b9a4 100644 Binary files a/cypress/snapshots/app.cy.ts/nxrgb.snap.png and b/cypress/snapshots/app.cy.ts/nxrgb.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/nxscatter.snap.png b/cypress/snapshots/app.cy.ts/nxscatter.snap.png index 2cc53c897..e095460f7 100644 Binary files a/cypress/snapshots/app.cy.ts/nxscatter.snap.png and b/cypress/snapshots/app.cy.ts/nxscatter.snap.png differ diff --git a/cypress/snapshots/app.cy.ts/rgb_image.snap.png b/cypress/snapshots/app.cy.ts/rgb_image.snap.png index cac1028a6..f3e9d8adb 100644 Binary files a/cypress/snapshots/app.cy.ts/rgb_image.snap.png and b/cypress/snapshots/app.cy.ts/rgb_image.snap.png differ diff --git a/packages/app/src/__tests__/CorePack.test.tsx b/packages/app/src/__tests__/CorePack.test.tsx index 5349168d5..d4fca6350 100644 --- a/packages/app/src/__tests__/CorePack.test.tsx +++ b/packages/app/src/__tests__/CorePack.test.tsx @@ -64,6 +64,22 @@ test('visualize 1D dataset as matrix', async () => { expect(screen.getByText('3.610e+2')).toBeVisible(); }); +test('visualize 1D boolean dataset', async () => { + await renderApp('/nD_datasets/oneD_bool'); + + expect(getVisTabs()).toEqual([Vis.Matrix, Vis.Line]); + expect(getSelectedVisTab()).toBe(Vis.Line); + expect(screen.getByRole('figure', { name: 'oneD_bool' })).toBeVisible(); +}); + +test('visualize 1D enum dataset', async () => { + await renderApp('/nD_datasets/oneD_enum'); + + expect(getVisTabs()).toEqual([Vis.Matrix, Vis.Line]); + expect(getSelectedVisTab()).toBe(Vis.Line); + expect(screen.getByRole('figure', { name: 'oneD_enum' })).toBeVisible(); +}); + test('visualize 1D complex dataset', async () => { await renderApp('/nD_datasets/oneD_cplx'); @@ -119,6 +135,17 @@ test('visualize 2D boolean dataset', async () => { expect(within(figure).getByText('1e+0')).toBeVisible(); // color bar limit }); +test('visualize 2D enum dataset', async () => { + await renderApp('/nD_datasets/twoD_enum'); + + expect(getVisTabs()).toEqual([Vis.Matrix, Vis.Line, Vis.Heatmap]); + expect(getSelectedVisTab()).toBe(Vis.Heatmap); + + const figure = screen.getByRole('figure', { name: 'twoD_enum' }); + expect(figure).toBeVisible(); + expect(within(figure).getByText('2e+0')).toBeVisible(); // color bar limit +}); + test('visualize 2D complex dataset', async () => { const { user } = await renderApp('/nD_datasets/twoD_cplx'); diff --git a/packages/app/src/__tests__/NexusPack.test.tsx b/packages/app/src/__tests__/NexusPack.test.tsx index 1c572f6f9..b169fd788 100644 --- a/packages/app/src/__tests__/NexusPack.test.tsx +++ b/packages/app/src/__tests__/NexusPack.test.tsx @@ -76,6 +76,22 @@ test('visualize NXdata group with old-style signal', async () => { ).toBeVisible(); }); +test('visualize NXdata group with boolean signal', async () => { + await renderApp('/nexus_entry/numeric-like/bool'); + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum, NexusVis.NxImage]); + expect( + screen.getByRole('figure', { name: 'twoD_bool' }), // name of dataset with `signal` attribute + ).toBeVisible(); +}); + +test('visualize NXdata group with enum signal', async () => { + await renderApp('/nexus_entry/numeric-like/enum'); + expect(getVisTabs()).toEqual([NexusVis.NxSpectrum, NexusVis.NxImage]); + expect( + screen.getByRole('figure', { name: 'twoD_enum' }), // name of dataset with `signal` attribute + ).toBeVisible(); +}); + test('visualize group with `default` attribute', async () => { // NXroot with relative path to NXentry group with relative path to NXdata group with 2D signal const { selectExplorerNode } = await renderApp(); @@ -177,7 +193,9 @@ test('show error/fallback for malformed NeXus entity', async () => { // Type of signal dataset is not numeric await selectExplorerNode('signal_not_numeric'); expect( - screen.getByText('Expected dataset to have numeric or complex type'), + screen.getByText( + 'Expected dataset to have numeric, boolean, enum or complex type', + ), ).toBeVisible(); errorSpy.mockClear(); diff --git a/packages/app/src/providers/h5grove/__snapshots__/h5grove-api.test.ts.snap b/packages/app/src/providers/h5grove/__snapshots__/h5grove-api.test.ts.snap index 91311504d..8c7ec5987 100644 --- a/packages/app/src/providers/h5grove/__snapshots__/h5grove-api.test.ts.snap +++ b/packages/app/src/providers/h5grove/__snapshots__/h5grove-api.test.ts.snap @@ -1720,8 +1720,8 @@ exports[`test file matches snapshot 1`] = ` }, "class": "Enumeration", "mapping": { - "A": 0, - "B": 1, + "0": "A", + "1": "B", }, }, "value": 1, @@ -1753,12 +1753,119 @@ exports[`test file matches snapshot 1`] = ` }, "class": "Enumeration", "mapping": { - "A": 256, - "B": 257, + "256": "A", + "257": "B", }, }, "value": 256, }, + { + "name": "enum_1D", + "rawType": { + "base": { + "class": 0, + "dtype": "|u1", + "order": 0, + "sign": 0, + "size": 1, + }, + "class": 8, + "dtype": "|u1", + "members": { + "A": 0, + "B": 1, + "C": 2, + }, + "size": 1, + }, + "shape": [ + 10, + ], + "type": { + "base": { + "class": "Integer (unsigned)", + "endianness": "little-endian", + "size": 8, + }, + "class": "Enumeration", + "mapping": { + "0": "A", + "1": "B", + "2": "C", + }, + }, + "value": Uint8Array [ + 0, + 2, + 2, + 1, + 1, + 0, + 2, + 2, + 1, + 1, + ], + }, + { + "name": "enum_2D", + "rawType": { + "base": { + "class": 0, + "dtype": "|u1", + "order": 0, + "sign": 0, + "size": 1, + }, + "class": 8, + "dtype": "|u1", + "members": { + "A": 0, + "B": 1, + "C": 2, + }, + "size": 1, + }, + "shape": [ + 2, + 10, + ], + "type": { + "base": { + "class": "Integer (unsigned)", + "endianness": "little-endian", + "size": 8, + }, + "class": "Enumeration", + "mapping": { + "0": "A", + "1": "B", + "2": "C", + }, + }, + "value": Uint8Array [ + 0, + 2, + 2, + 1, + 1, + 0, + 2, + 2, + 1, + 1, + 2, + 0, + 1, + 1, + 0, + 2, + 2, + 2, + 2, + 1, + ], + }, { "name": "vlen_int8_scalar", "rawType": { diff --git a/packages/app/src/providers/hsds/utils.ts b/packages/app/src/providers/hsds/utils.ts index 6379994df..cc1fce295 100644 --- a/packages/app/src/providers/hsds/utils.ts +++ b/packages/app/src/providers/hsds/utils.ts @@ -17,13 +17,11 @@ import type { } from '@h5web/shared/hdf5-models'; import { DTypeClass } from '@h5web/shared/hdf5-models'; import { - boolType, compoundType, cplxType, - enumType, + enumOrBoolType, floatType, intType, - isBoolEnumType, strType, uintType, unknownType, @@ -123,9 +121,7 @@ function convertHsdsCompoundType( function convertHsdsEnumType(hsdsType: HsdsEnumType): EnumType | BooleanType { const { base, mapping } = hsdsType; assertHsdsNumericType(base); - - const type = enumType(convertHsdsNumericType(base), mapping); - return isBoolEnumType(type) ? boolType() : type; // booleans stored as enums by h5py + return enumOrBoolType(convertHsdsNumericType(base), mapping); } export function convertHsdsType(hsdsType: HsdsType): DType { diff --git a/packages/app/src/providers/mock/mock-file.ts b/packages/app/src/providers/mock/mock-file.ts index a6bc68158..c6862940d 100644 --- a/packages/app/src/providers/mock/mock-file.ts +++ b/packages/app/src/providers/mock/mock-file.ts @@ -40,6 +40,8 @@ const PNG_RED_DOT = new Uint8Array([ 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, ]); +const ENUM_MAPPING = { FOO: 0, BAR: 1, BAZ: 2 }; + export function makeMockFile(): GroupWithChildren { return nxGroup('source.h5', 'NXroot', { isRoot: true, @@ -87,10 +89,10 @@ export function makeMockFile(): GroupWithChildren { ], }), scalar('scalar_enum', 2, { - type: enumType(uintType(8), { FOO: 0, BAR: 1, BAZ: 2 }), + type: enumType(uintType(8), ENUM_MAPPING), attributes: [ scalarAttr('attr', 2, { - type: enumType(uintType(8), { FOO: 0, BAR: 1, BAZ: 2 }), + type: enumType(uintType(8), ENUM_MAPPING), }), ], }), @@ -117,6 +119,7 @@ export function makeMockFile(): GroupWithChildren { }), }), array('oneD_bool'), + array('oneD_enum', { type: enumType(uintType(8), ENUM_MAPPING) }), array('twoD'), array('twoD_cplx'), array('twoD_compound', { @@ -129,6 +132,7 @@ export function makeMockFile(): GroupWithChildren { }), }), array('twoD_bool'), + array('twoD_enum', { type: enumType(uintType(8), ENUM_MAPPING) }), array('threeD'), array('threeD_bool'), array('threeD_cplx'), @@ -268,6 +272,16 @@ export function makeMockFile(): GroupWithChildren { }), ], }), + nxGroup('numeric-like', 'NXprocess', { + children: [ + nxData('bool', { signal: array('twoD_bool') }), + nxData('enum', { + signal: array('twoD_enum', { + type: enumType(uintType(8), ENUM_MAPPING), + }), + }), + ], + }), ], }), nxGroup('nexus_note', 'NXnote', { diff --git a/packages/app/src/providers/models.ts b/packages/app/src/providers/models.ts index f1727fb47..69ad77dfc 100644 --- a/packages/app/src/providers/models.ts +++ b/packages/app/src/providers/models.ts @@ -8,7 +8,6 @@ import type { } from '@h5web/shared/hdf5-models'; import type { FetchStore } from '@h5web/shared/react-suspense-fetch'; -import type { ImageAttribute } from '../vis-packs/core/models'; import type { NxAttribute } from '../vis-packs/nexus/models'; export type EntitiesStore = FetchStore; @@ -23,6 +22,7 @@ export interface AttrValuesStore extends FetchStore { getSingle: (entity: Entity, attrName: AttrName) => unknown; } +export type ImageAttribute = 'CLASS' | 'IMAGE_SUBCLASS'; export type AttrName = NxAttribute | ImageAttribute | '_FillValue'; export type ExportFormat = 'json' | 'csv' | 'npy' | 'tiff'; diff --git a/packages/app/src/providers/utils.ts b/packages/app/src/providers/utils.ts index ccd05ae76..f7c15a487 100644 --- a/packages/app/src/providers/utils.ts +++ b/packages/app/src/providers/utils.ts @@ -1,4 +1,4 @@ -import { isNumericType } from '@h5web/shared/guards'; +import { isEnumType, isNumericType } from '@h5web/shared/guards'; import type { ArrayShape, Dataset, @@ -33,11 +33,15 @@ export async function handleAxiosError( } export function typedArrayFromDType(dtype: DType) { - /* Adapted from https://github.com/ludwigschubert/js-numpy-parser/blob/v1.2.3/src/main.js#L116 */ + if (isEnumType(dtype)) { + return typedArrayFromDType(dtype.base); + } + if (!isNumericType(dtype)) { return undefined; } + /* Adapted from https://github.com/ludwigschubert/js-numpy-parser/blob/v1.2.3/src/main.js#L116 */ const { class: dtypeClass, size } = dtype; if (dtypeClass === DTypeClass.Integer) { diff --git a/packages/app/src/vis-packs/core/matrix/utils.ts b/packages/app/src/vis-packs/core/matrix/utils.ts index 8a264d570..d5efba77d 100644 --- a/packages/app/src/vis-packs/core/matrix/utils.ts +++ b/packages/app/src/vis-packs/core/matrix/utils.ts @@ -1,5 +1,5 @@ import { Notation } from '@h5web/lib'; -import { isComplexType, isNumericType } from '@h5web/shared/guards'; +import { isComplexType, isEnumType, isNumericType } from '@h5web/shared/guards'; import type { ComplexType, NumericType, @@ -7,11 +7,13 @@ import type { PrintableType, } from '@h5web/shared/hdf5-models'; import { DTypeClass } from '@h5web/shared/hdf5-models'; -import { createComplexFormatter } from '@h5web/shared/vis-utils'; +import type { ValueFormatter } from '@h5web/shared/vis-models'; +import { + createComplexFormatter, + createEnumFormatter, +} from '@h5web/shared/vis-utils'; import { format } from 'd3-format'; -import type { ValueFormatter } from '../models'; - export function createNumericFormatter( notation: Notation, ): ValueFormatter { @@ -48,6 +50,10 @@ export function getFormatter( return createNumericFormatter(notation); } + if (isEnumType(type)) { + return createEnumFormatter(type.mapping); + } + return (val) => (val as string | boolean).toString(); } diff --git a/packages/app/src/vis-packs/core/models.ts b/packages/app/src/vis-packs/core/models.ts deleted file mode 100644 index c0203ae15..000000000 --- a/packages/app/src/vis-packs/core/models.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { DType, Primitive } from '@h5web/shared/hdf5-models'; - -export type ImageAttribute = 'CLASS' | 'IMAGE_SUBCLASS'; - -export type ValueFormatter = (val: Primitive) => string; diff --git a/packages/app/src/vis-packs/core/scalar/utils.ts b/packages/app/src/vis-packs/core/scalar/utils.ts index 2eda351b1..54c7471b5 100644 --- a/packages/app/src/vis-packs/core/scalar/utils.ts +++ b/packages/app/src/vis-packs/core/scalar/utils.ts @@ -1,19 +1,24 @@ -import { hasComplexType } from '@h5web/shared/guards'; +import { hasComplexType, hasEnumType } from '@h5web/shared/guards'; import type { ArrayShape, Dataset, - H5WebComplex, PrintableType, } from '@h5web/shared/hdf5-models'; -import { formatScalarComplex } from '@h5web/shared/vis-utils'; - -import type { ValueFormatter } from '../models'; +import type { ValueFormatter } from '@h5web/shared/vis-models'; +import { + createEnumFormatter, + formatScalarComplex, +} from '@h5web/shared/vis-utils'; export function getFormatter( dataset: Dataset, ): ValueFormatter { if (hasComplexType(dataset)) { - return (val) => formatScalarComplex(val as H5WebComplex); + return formatScalarComplex; + } + + if (hasEnumType(dataset)) { + return createEnumFormatter(dataset.type.mapping); } return (val) => (val as number | string | boolean).toString(); diff --git a/packages/app/src/vis-packs/core/utils.ts b/packages/app/src/vis-packs/core/utils.ts index 50df75cf1..caf4d6ba3 100644 --- a/packages/app/src/vis-packs/core/utils.ts +++ b/packages/app/src/vis-packs/core/utils.ts @@ -98,6 +98,7 @@ export function toNumArray(arr: ArrayValue): NumArray { const TYPE_STRINGS: Record = { [DTypeClass.Bool]: 'bool', + [DTypeClass.Enum]: 'enum', [DTypeClass.Integer]: 'int', [DTypeClass.Unsigned]: 'uint', [DTypeClass.Float]: 'float', diff --git a/packages/app/src/vis-packs/nexus/containers/NxImageContainer.tsx b/packages/app/src/vis-packs/nexus/containers/NxImageContainer.tsx index 008bd9c5e..e85f52095 100644 --- a/packages/app/src/vis-packs/nexus/containers/NxImageContainer.tsx +++ b/packages/app/src/vis-packs/nexus/containers/NxImageContainer.tsx @@ -8,7 +8,7 @@ import MappedHeatmapVis from '../../core/heatmap/MappedHeatmapVis'; import { getSliceSelection } from '../../core/utils'; import type { VisContainerProps } from '../../models'; import VisBoundary from '../../VisBoundary'; -import { assertNumericNxData } from '../guards'; +import { assertNumericLikeNxData } from '../guards'; import { useNxData, useNxImageDataToFetch, useNxValuesCached } from '../hooks'; import NxSignalPicker from '../NxSignalPicker'; import NxValuesFetcher from '../NxValuesFetcher'; @@ -19,7 +19,7 @@ function NxImageContainer(props: VisContainerProps) { assertGroup(entity); const nxData = useNxData(entity); - assertNumericNxData(nxData); + assertNumericLikeNxData(nxData); const { signalDef, axisDefs, auxDefs, silxStyle } = nxData; const [selectedDef, setSelectedDef] = useState(signalDef); diff --git a/packages/app/src/vis-packs/nexus/containers/NxSpectrumContainer.tsx b/packages/app/src/vis-packs/nexus/containers/NxSpectrumContainer.tsx index d0506f051..5366abee9 100644 --- a/packages/app/src/vis-packs/nexus/containers/NxSpectrumContainer.tsx +++ b/packages/app/src/vis-packs/nexus/containers/NxSpectrumContainer.tsx @@ -8,7 +8,7 @@ import MappedLineVis from '../../core/line/MappedLineVis'; import { getSliceSelection } from '../../core/utils'; import type { VisContainerProps } from '../../models'; import VisBoundary from '../../VisBoundary'; -import { assertNumericNxData } from '../guards'; +import { assertNumericLikeNxData } from '../guards'; import { useNxData, useNxValuesCached } from '../hooks'; import NxValuesFetcher from '../NxValuesFetcher'; import { areSameDims } from '../utils'; @@ -18,7 +18,7 @@ function NxSpectrumContainer(props: VisContainerProps) { assertGroup(entity); const nxData = useNxData(entity); - assertNumericNxData(nxData); + assertNumericLikeNxData(nxData); const { signalDef, axisDefs, auxDefs, silxStyle } = nxData; const signalDims = signalDef.dataset.shape; diff --git a/packages/app/src/vis-packs/nexus/guards.ts b/packages/app/src/vis-packs/nexus/guards.ts index 84d0d1281..409cbec92 100644 --- a/packages/app/src/vis-packs/nexus/guards.ts +++ b/packages/app/src/vis-packs/nexus/guards.ts @@ -1,8 +1,20 @@ -import { assertComplexType, assertNumericType } from '@h5web/shared/guards'; +import { + assertComplexType, + assertNumericLikeType, + assertNumericType, +} from '@h5web/shared/guards'; import type { ComplexType, NumericType } from '@h5web/shared/hdf5-models'; import type { NxData } from './models'; +export function assertNumericLikeNxData( + nxData: NxData, +): asserts nxData is NxData { + const { signalDef, auxDefs } = nxData; + assertNumericLikeType(signalDef.dataset); + auxDefs.forEach((def) => assertNumericLikeType(def.dataset)); +} + export function assertNumericNxData( nxData: NxData, ): asserts nxData is NxData { diff --git a/packages/app/src/vis-packs/nexus/models.ts b/packages/app/src/vis-packs/nexus/models.ts index 42b7c5b12..7a5027a38 100644 --- a/packages/app/src/vis-packs/nexus/models.ts +++ b/packages/app/src/vis-packs/nexus/models.ts @@ -5,6 +5,7 @@ import type { ComplexType, Dataset, NumArrayDataset, + NumericLikeType, NumericType, ScalarShape, StringType, @@ -29,7 +30,7 @@ export interface DatasetInfo { } export interface DatasetDef< - T extends NumericType | ComplexType = NumericType | ComplexType, + T extends NumericLikeType | ComplexType = NumericLikeType | ComplexType, > extends DatasetInfo { dataset: Dataset; } @@ -46,7 +47,7 @@ export interface SilxStyle { } export interface NxData< - T extends NumericType | ComplexType = NumericType | ComplexType, + T extends NumericLikeType | ComplexType = NumericLikeType | ComplexType, > { titleDataset?: Dataset; signalDef: WithError>; diff --git a/packages/app/src/vis-packs/nexus/utils.ts b/packages/app/src/vis-packs/nexus/utils.ts index b12bea28c..864e7c312 100644 --- a/packages/app/src/vis-packs/nexus/utils.ts +++ b/packages/app/src/vis-packs/nexus/utils.ts @@ -3,7 +3,7 @@ import { assertArrayShape, assertDataset, assertDefined, - assertNumericOrComplexType, + assertNumericLikeOrComplexType, assertNumericType, assertScalarShape, assertStr, @@ -19,7 +19,7 @@ import type { Group, GroupWithChildren, NumArrayDataset, - NumericType, + NumericLikeType, ScalarShape, StringType, } from '@h5web/shared/hdf5-models'; @@ -58,7 +58,7 @@ export function isNxNoteGroup( function findOldStyleSignalDataset( group: GroupWithChildren, -): Dataset { +): Dataset { const dataset = group.children.find((child) => hasAttribute(child, 'signal')); assertDefined(dataset); assertDataset( @@ -66,14 +66,14 @@ function findOldStyleSignalDataset( `Expected old-style "${dataset.name}" signal to be a dataset`, ); assertArrayShape(dataset); - assertNumericOrComplexType(dataset); + assertNumericLikeOrComplexType(dataset); return dataset; } export function findSignalDataset( group: GroupWithChildren, attrValuesStore: AttrValuesStore, -): Dataset { +): Dataset { if (!hasAttribute(group, 'signal')) { return findOldStyleSignalDataset(group); } @@ -86,7 +86,7 @@ export function findSignalDataset( assertDefined(dataset, `Expected "${signal}" signal entity to exist`); assertDataset(dataset, `Expected "${signal}" signal to be a dataset`); assertArrayShape(dataset); - assertNumericOrComplexType(dataset); + assertNumericLikeOrComplexType(dataset); return dataset; } @@ -208,11 +208,11 @@ export function findAxesDatasets( export function findAuxiliaryDatasets( group: GroupWithChildren, attrValuesStore: AttrValuesStore, -): Dataset[] { +): Dataset[] { return findAssociatedDatasets(group, 'auxiliary_signals', attrValuesStore) .filter(isDefined) .map((dataset) => { - assertNumericOrComplexType(dataset); + assertNumericLikeOrComplexType(dataset); return dataset; }); } diff --git a/packages/h5wasm/src/__snapshots__/h5wasm-api.test.ts.snap b/packages/h5wasm/src/__snapshots__/h5wasm-api.test.ts.snap index a182ec99a..9939b822f 100644 --- a/packages/h5wasm/src/__snapshots__/h5wasm-api.test.ts.snap +++ b/packages/h5wasm/src/__snapshots__/h5wasm-api.test.ts.snap @@ -1727,8 +1727,8 @@ exports[`test file matches snapshot 1`] = ` 0, 0, 0, - 136, - 17, + 40, + 48, 19, 0, ], @@ -1743,8 +1743,8 @@ exports[`test file matches snapshot 1`] = ` 0, 0, 0, - 168, - 39, + 152, + 70, 19, 0, ], @@ -1759,8 +1759,8 @@ exports[`test file matches snapshot 1`] = ` 0, 0, 0, - 192, - 39, + 176, + 70, 19, 0, ], @@ -1966,8 +1966,8 @@ exports[`test file matches snapshot 1`] = ` }, "class": "Enumeration", "mapping": { - "A": 0, - "B": 1, + "0": "A", + "1": "B", }, }, "value": 1, @@ -1999,12 +1999,119 @@ exports[`test file matches snapshot 1`] = ` }, "class": "Enumeration", "mapping": { - "A": 256, - "B": 257, + "256": "A", + "257": "B", }, }, "value": 256, }, + { + "name": "enum_1D", + "rawType": { + "enum_type": { + "members": { + "A": 0, + "B": 1, + "C": 2, + }, + "nmembers": 3, + "type": 0, + }, + "littleEndian": true, + "signed": false, + "size": 1, + "total_size": 10, + "type": 8, + "vlen": false, + }, + "shape": [ + 10, + ], + "type": { + "base": { + "class": "Integer (unsigned)", + "endianness": "little-endian", + "size": 8, + }, + "class": "Enumeration", + "mapping": { + "0": "A", + "1": "B", + "2": "C", + }, + }, + "value": Uint8Array [ + 0, + 2, + 2, + 1, + 1, + 0, + 2, + 2, + 1, + 1, + ], + }, + { + "name": "enum_2D", + "rawType": { + "enum_type": { + "members": { + "A": 0, + "B": 1, + "C": 2, + }, + "nmembers": 3, + "type": 0, + }, + "littleEndian": true, + "signed": false, + "size": 1, + "total_size": 20, + "type": 8, + "vlen": false, + }, + "shape": [ + 2, + 10, + ], + "type": { + "base": { + "class": "Integer (unsigned)", + "endianness": "little-endian", + "size": 8, + }, + "class": "Enumeration", + "mapping": { + "0": "A", + "1": "B", + "2": "C", + }, + }, + "value": Uint8Array [ + 0, + 2, + 2, + 1, + 1, + 0, + 2, + 2, + 1, + 1, + 2, + 0, + 1, + 1, + 0, + 2, + 2, + 2, + 2, + 1, + ], + }, { "name": "vlen_int8_scalar", "rawType": { @@ -2027,8 +2134,8 @@ exports[`test file matches snapshot 1`] = ` 0, 0, 0, - 88, - 63, + 112, + 250, 18, 0, ], @@ -2057,24 +2164,24 @@ exports[`test file matches snapshot 1`] = ` 0, 0, 0, - 24, - 74, - 15, + 144, + 42, + 19, 0, 2, 0, 0, 0, - 128, - 250, - 17, + 120, + 76, + 19, 0, 3, 0, 0, 0, - 48, - 41, + 120, + 77, 19, 0, ], diff --git a/packages/shared/src/guards.ts b/packages/shared/src/guards.ts index a51a13619..d5c7de966 100644 --- a/packages/shared/src/guards.ts +++ b/packages/shared/src/guards.ts @@ -10,6 +10,7 @@ import type { Datatype, DType, Entity, + EnumType, Group, GroupWithChildren, H5WebComplex, @@ -38,6 +39,7 @@ const PRINTABLE_DTYPES = new Set([ DTypeClass.Float, DTypeClass.String, DTypeClass.Bool, + DTypeClass.Enum, DTypeClass.Complex, ]); @@ -268,6 +270,16 @@ export function hasBoolType( return isBoolType(dataset.type); } +export function isEnumType(type: DType): type is EnumType { + return type.class === DTypeClass.Enum; +} + +export function hasEnumType( + dataset: Dataset, +): dataset is Dataset { + return isEnumType(dataset.type); +} + function hasStringType( dataset: Dataset, ): dataset is Dataset { @@ -305,14 +317,15 @@ export function assertNumericType( export function hasNumericLikeType( dataset: Dataset, ): dataset is Dataset { - return isNumericType(dataset.type) || isBoolType(dataset.type); + const { type } = dataset; + return isNumericType(type) || isBoolType(type) || isEnumType(type); } export function assertNumericLikeType( dataset: Dataset, ): asserts dataset is Dataset { if (!hasNumericLikeType(dataset)) { - throw new Error('Expected dataset to have numeric or boolean type'); + throw new Error('Expected dataset to have numeric, boolean or enum type'); } } @@ -338,11 +351,13 @@ export function assertComplexType( } } -export function assertNumericOrComplexType( +export function assertNumericLikeOrComplexType( dataset: Dataset, -): asserts dataset is Dataset { - if (!hasNumericType(dataset) && !hasComplexType(dataset)) { - throw new Error('Expected dataset to have numeric or complex type'); +): asserts dataset is Dataset { + if (!hasNumericLikeType(dataset) && !hasComplexType(dataset)) { + throw new Error( + 'Expected dataset to have numeric, boolean, enum or complex type', + ); } } @@ -359,6 +374,7 @@ export function assertPrintableType( !hasStringType(dataset) && !hasNumericType(dataset) && !hasBoolType(dataset) && + !hasEnumType(dataset) && !hasComplexType(dataset) ) { throw new Error('Expected dataset to have displayable type'); diff --git a/packages/shared/src/hdf5-models.ts b/packages/shared/src/hdf5-models.ts index a1993e267..009165462 100644 --- a/packages/shared/src/hdf5-models.ts +++ b/packages/shared/src/hdf5-models.ts @@ -121,23 +121,19 @@ export type Endianness = (typeof H5T_TO_ENDIANNESS)[H5T_ORDER]; export type CharSet = (typeof H5T_TO_CHAR_SET)[H5T_CSET]; export type StrPad = (typeof H5T_TO_STR_PAD)[H5T_STR]; +export type NumericLikeType = NumericType | BooleanType | EnumType; +export type PrintableType = StringType | NumericLikeType | ComplexType; + export type DType = | PrintableType | CompoundType | ArrayType - | EnumType | TimeType | BitfieldType | OpaqueType | ReferenceType | UnknownType; -export type PrintableType = - | BooleanType - | NumericType - | ComplexType - | StringType; - export interface BooleanType { class: DTypeClass.Bool; } @@ -148,8 +144,6 @@ export interface NumericType { endianness: Endianness | undefined; } -export type NumericLikeType = NumericType | BooleanType; - export interface ComplexType { class: DTypeClass.Complex; realType: NumericType; @@ -181,7 +175,7 @@ export interface ArrayType { export interface EnumType { class: DTypeClass.Enum; base: NumericType; // technically, only int/uint - mapping: Record; + mapping: Record; } export interface TimeType { @@ -209,7 +203,7 @@ export interface UnknownType { /* ----------------- */ /* ----- VALUE ----- */ -export type Primitive = T extends NumericType +export type Primitive = T extends NumericType | EnumType ? number : T extends StringType ? string @@ -223,7 +217,7 @@ export type Primitive = T extends NumericType export type ArrayValue = | Primitive[] - | (T extends NumericType ? TypedArray : never); + | (T extends NumericType | EnumType ? TypedArray : never); export type Value = D extends Dataset diff --git a/packages/shared/src/hdf5-utils.ts b/packages/shared/src/hdf5-utils.ts index b62638485..d4dae41ae 100644 --- a/packages/shared/src/hdf5-utils.ts +++ b/packages/shared/src/hdf5-utils.ts @@ -143,29 +143,31 @@ export function arrayType( export function enumType( baseType: NumericType, - mapping: Record, + hdf5Mapping: Record, ): EnumType { - return { class: DTypeClass.Enum, base: baseType, mapping }; -} - -export function isBoolEnumType(type: EnumType): boolean { - const { mapping } = type; - return ( - Object.keys(mapping).length === 2 && - mapping.FALSE === 0 && - mapping.TRUE === 1 - ); + return { + class: DTypeClass.Enum, + base: baseType, + // Swap mapping to optimise retrieval of enum keys from numeric values + mapping: Object.fromEntries( + Object.entries(hdf5Mapping).map(([k, v]) => [v, k]), + ), + }; } export function enumOrBoolType( baseType: NumericType, - mapping: Record, + hdf5Mapping: Record, ): EnumType | BooleanType { - if (mapping.FALSE === 0 && mapping.TRUE === 1) { + if ( + Object.keys(hdf5Mapping).length === 2 && + hdf5Mapping.FALSE === 0 && + hdf5Mapping.TRUE === 1 + ) { return boolType(); } - return enumType(baseType, mapping); + return enumType(baseType, hdf5Mapping); } export function timeType(): TimeType { diff --git a/packages/shared/src/mock-values.ts b/packages/shared/src/mock-values.ts index 193d9042a..2043b1ea1 100644 --- a/packages/shared/src/mock-values.ts +++ b/packages/shared/src/mock-values.ts @@ -20,6 +20,8 @@ const oneD = () => ndarray(range1().map((val) => val ** 2)); const oneD_bool = () => ndarray([true, false, false, true, true, true, false, true, false, false]); +const oneD_enum = () => ndarray([0, 2, 2, 1, 1, 0, 2, 2, 1, 1]); + const oneD_cplx = () => ndarray( range9().map((val) => @@ -90,6 +92,7 @@ export const mockValues = { oneD_cplx, oneD_compound, oneD_bool, + oneD_enum, oneD_errors: () => ndarray(oneD().data.map((val) => Math.abs(val) / 10)), oneD_str: () => ndarray(['foo', 'bar']), twoD, @@ -136,6 +139,15 @@ export const mockValues = { [10, 10], ); }, + twoD_enum: () => { + const { data: dataOneDEnum } = oneD_enum(); + return ndarray( + dataOneDEnum.flatMap((rowEnum, i) => + dataOneDEnum.map((colEnum) => (i % 2 === 0 ? colEnum : rowEnum)), + ), + [10, 10], + ); + }, twoD_errors: () => { const arr = range1(); return ndarray( diff --git a/packages/shared/src/vis-models.ts b/packages/shared/src/vis-models.ts index 91510553b..abcc1522a 100644 --- a/packages/shared/src/vis-models.ts +++ b/packages/shared/src/vis-models.ts @@ -1,5 +1,7 @@ import type { NdArray, TypedArray } from 'ndarray'; +import type { DType, Primitive } from './hdf5-models'; + export type NumArray = TypedArray | number[]; export type AnyNumArray = NdArray | NumArray; @@ -60,3 +62,5 @@ export interface Dims { export type MappedTuple = { [index in keyof T]: U; }; + +export type ValueFormatter = (val: Primitive) => string; diff --git a/packages/shared/src/vis-utils.ts b/packages/shared/src/vis-utils.ts index a02db53f1..574be11b4 100644 --- a/packages/shared/src/vis-utils.ts +++ b/packages/shared/src/vis-utils.ts @@ -4,7 +4,7 @@ import ndarray from 'ndarray'; import { assign } from 'ndarray-ops'; import { assertLength, isNdArray } from './guards'; -import type { H5WebComplex } from './hdf5-models'; +import type { ComplexType, EnumType } from './hdf5-models'; import type { AnyNumArray, AxisScaleType, @@ -14,6 +14,7 @@ import type { Domain, NumArray, TypedArrayConstructor, + ValueFormatter, } from './vis-models'; import { ScaleType } from './vis-models'; @@ -53,10 +54,13 @@ export function formatTick(val: number | { valueOf(): number }): string { return str; } -export function createComplexFormatter(specifier: string, full = false) { +export function createComplexFormatter( + specifier: string, + full = false, +): ValueFormatter { const formatVal = format(specifier); - return (value: H5WebComplex) => { + return (value) => { const [real, imag] = value; if (imag === 0 && !full) { @@ -72,6 +76,12 @@ export function createComplexFormatter(specifier: string, full = false) { }; } +export function createEnumFormatter( + mapping: Record, +): ValueFormatter { + return (value) => (value in mapping ? mapping[value] : value.toString()); +} + export function getValues(arr: AnyNumArray): NumArray { return isNdArray(arr) ? arr.data : arr; } diff --git a/support/sample/create_h5_sample.py b/support/sample/create_h5_sample.py index 6a7093264..d917b409a 100644 --- a/support/sample/create_h5_sample.py +++ b/support/sample/create_h5_sample.py @@ -246,6 +246,18 @@ def print_h5t_class(dataset): 256, h5py.enum_dtype({"A": 256, "B": 257}, np.int32), ) + add_array( + h5, + "enum", + np.array([0, 2, 2, 1, 1, 0, 2, 2, 1, 1]), + h5py.enum_dtype({"A": 0, "B": 1, "C": 2}), + ) + add_array( + h5, + "enum", + np.array([[0, 2, 2, 1, 1, 0, 2, 2, 1, 1], [2, 0, 1, 1, 0, 2, 2, 2, 2, 1]]), + h5py.enum_dtype({"A": 0, "B": 1, "C": 2}), + ) # === H5T_VLEN ===