diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 0265f1431..a1bc1a622 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -10,6 +10,7 @@ module.exports = createConfig(__dirname, getDependencies(), [ 'testing-library/await-async-utils': 'off', // Cypress has its own way of dealing with asynchronicity 'testing-library/prefer-screen-queries': 'off', // Cypress provides `cy` object instead of `screen` 'sonarjs/no-duplicate-string': 'off', // incompatible with Cypress testing syntax + 'sonarjs/cognitive-complexity': 'off', // allow long `describe` and `context` functions 'unicorn/numeric-separators-style': 'off', // not supported }, }, diff --git a/cypress/e2e/app.cy.ts b/cypress/e2e/app.cy.ts index 35d3ba8bc..87f3bcdf3 100644 --- a/cypress/e2e/app.cy.ts +++ b/cypress/e2e/app.cy.ts @@ -289,7 +289,7 @@ describe('/mock', () => { cy.findByRole('figure', { name: 'NeXus 2D' }).should('be.visible'); }); - it('visualize dataset with "spectrum" interpretation as NxSpectrum', () => { + it('visualize dataset with "spectrum" interpretation as NxLine', () => { cy.selectExplorerNode('nexus_entry'); cy.selectExplorerNode('spectrum'); @@ -304,7 +304,7 @@ describe('/mock', () => { cy.get('svg[data-type="abscissa"] svg').should('have.text', 'X (nm)'); if (Cypress.env('TAKE_SNAPSHOTS')) { - cy.matchImageSnapshot('nxspectrum'); + cy.matchImageSnapshot('nxline'); } }); @@ -340,7 +340,7 @@ describe('/mock', () => { ); }); - it('visualize dataset with log scales on both axes on NxSpectrum with SILX_style', () => { + it('visualize dataset with log scales on both axes on NxLine with SILX_style', () => { cy.selectExplorerNode('nexus_entry'); cy.selectExplorerNode('log_spectrum'); @@ -357,7 +357,7 @@ describe('/mock', () => { } }); - it('visualize signal and auxiliary signals datasets as NxSpectrum', () => { + it('visualize signal and auxiliary signals datasets as NxLine', () => { cy.selectExplorerNode('nexus_entry'); cy.selectExplorerNode('spectrum_with_aux'); @@ -391,6 +391,32 @@ describe('/mock', () => { } }); + it('visualize 2D complex signal as NxImage', () => { + cy.selectExplorerNode('nexus_entry'); + cy.selectExplorerNode('complex'); + + cy.findByRole('heading', { + name: 'nexus_entry / complex', + }).should('be.visible'); + + if (Cypress.env('TAKE_SNAPSHOTS')) { + cy.matchImageSnapshot('nximage_complex_2d'); + } + }); + + it('visualize 2D complex signal with "spectrum" interpretation and auxiliaries as NxLine', () => { + cy.selectExplorerNode('nexus_entry'); + cy.selectExplorerNode('complex_spectrum'); + + cy.findByRole('heading', { + name: 'nexus_entry / complex_spectrum', + }).should('be.visible'); + + if (Cypress.env('TAKE_SNAPSHOTS')) { + cy.matchImageSnapshot('nxline_complex_2d_aux'); + } + }); + it('visualize dataset with "rgb-image" interpretation as NxRGB', () => { cy.selectExplorerNode('nexus_entry'); cy.selectExplorerNode('rgb-image'); diff --git a/cypress/snapshots/app.cy.ts/nxspectrum.snap.png b/cypress/snapshots/app.cy.ts/nxline.snap.png similarity index 100% rename from cypress/snapshots/app.cy.ts/nxspectrum.snap.png rename to cypress/snapshots/app.cy.ts/nxline.snap.png diff --git a/packages/app/src/providers/mock/mock-file.ts b/packages/app/src/providers/mock/mock-file.ts index 97e79b6ce..7d55ef45d 100644 --- a/packages/app/src/providers/mock/mock-file.ts +++ b/packages/app/src/providers/mock/mock-file.ts @@ -228,6 +228,8 @@ export function makeMockFile(): GroupWithChildren { signal: withNxAttr(array('twoD_cplx'), { interpretation: 'spectrum', }), + auxiliary: { secondary_cplx: array('secondary_cplx') }, + auxAttr: ['secondary_cplx'], }), nxData('rgb-image', { signal: withImageAttr( diff --git a/packages/app/src/vis-packs/core/complex/MappedComplexLineVis.tsx b/packages/app/src/vis-packs/core/complex/MappedComplexLineVis.tsx index b03114610..30a2842c5 100644 --- a/packages/app/src/vis-packs/core/complex/MappedComplexLineVis.tsx +++ b/packages/app/src/vis-packs/core/complex/MappedComplexLineVis.tsx @@ -1,23 +1,30 @@ -import { LineVis, useDomain, useSafeDomain, useVisDomain } from '@h5web/lib'; -import type { H5WebComplex } from '@h5web/shared/hdf5-models'; +import { + LineVis, + useCombinedDomain, + useDomain, + useDomains, + useSafeDomain, + useVisDomain, +} from '@h5web/lib'; +import type { ArrayValue, ComplexType } from '@h5web/shared/hdf5-models'; import type { AxisMapping } from '@h5web/shared/nexus-models'; import type { NumArray } from '@h5web/shared/vis-models'; -import { ComplexVisType } from '@h5web/shared/vis-models'; -import { useMemo } from 'react'; import { createPortal } from 'react-dom'; import type { DimensionMapping } from '../../../dimension-mapper/models'; import visualizerStyles from '../../../visualizer/Visualizer.module.css'; -import { useMappedArray, useSlicedDimsAndMapping } from '../hooks'; import type { LineConfig } from '../line/config'; import { DEFAULT_DOMAIN } from '../utils'; import ComplexLineToolbar from './ComplexLineToolbar'; +import { useMappedComplexArrays } from './hooks'; import type { ComplexLineConfig } from './lineConfig'; -import { COMPLEX_VIS_TYPE_LABELS, getPhaseAmplitudeValues } from './utils'; +import { COMPLEX_VIS_TYPE_LABELS } from './utils'; interface Props { - value: H5WebComplex[]; + value: ArrayValue; valueLabel?: string; + auxLabels?: string[]; + auxValues?: ArrayValue[]; dims: number[]; dimMapping: DimensionMapping; axisLabels?: AxisMapping; @@ -32,6 +39,8 @@ function MappedComplexLineVis(props: Props) { const { value, valueLabel, + auxLabels = [], + auxValues = [], dims, dimMapping, axisLabels, @@ -46,21 +55,20 @@ function MappedComplexLineVis(props: Props) { const { customDomain, yScaleType, xScaleType, curveType, showGrid } = lineConfig; - const [slicedDims, slicedMapping] = useSlicedDimsAndMapping(dims, dimMapping); - const { phaseValues, amplitudeValues } = useMemo( - () => getPhaseAmplitudeValues(value), - [value], + const [dataArray, ...auxArrays] = useMappedComplexArrays( + [value, ...auxValues], + dims, + dimMapping, + visType, ); - const dataArray = useMappedArray( - visType === ComplexVisType.Amplitude ? amplitudeValues : phaseValues, - slicedDims, - slicedMapping, - ); + const dataDomain = useDomain(dataArray, yScaleType); + const auxDomains = useDomains(auxArrays, yScaleType); + const combinedDomain = + useCombinedDomain([dataDomain, ...auxDomains]) || DEFAULT_DOMAIN; - const dataDomain = useDomain(dataArray, yScaleType) || DEFAULT_DOMAIN; - const visDomain = useVisDomain(customDomain, dataDomain); - const [safeDomain] = useSafeDomain(visDomain, dataDomain, yScaleType); + const visDomain = useVisDomain(customDomain, combinedDomain); + const [safeDomain] = useSafeDomain(visDomain, combinedDomain, yScaleType); const xDimIndex = dimMapping.indexOf('x'); const ordinateLabel = valueLabel @@ -72,7 +80,7 @@ function MappedComplexLineVis(props: Props) { {toolbarContainer && createPortal( , @@ -93,6 +101,10 @@ function MappedComplexLineVis(props: Props) { }} ordinateLabel={ordinateLabel} title={title} + auxiliaries={auxArrays.map((array, i) => ({ + label: auxLabels[i], + array, + }))} testid={dimMapping.toString()} /> diff --git a/packages/app/src/vis-packs/core/complex/hooks.ts b/packages/app/src/vis-packs/core/complex/hooks.ts new file mode 100644 index 000000000..eb430f672 --- /dev/null +++ b/packages/app/src/vis-packs/core/complex/hooks.ts @@ -0,0 +1,40 @@ +import { ComplexVisType } from '@h5web/lib'; +import type { H5WebComplex } from '@h5web/shared/hdf5-models'; +import type { ComplexLineVisType } from '@h5web/shared/vis-models'; +import type { NdArray } from 'ndarray'; +import { useMemo } from 'react'; + +import type { DimensionMapping } from '../../../dimension-mapper/models'; +import { useSlicedDimsAndMapping } from '../hooks'; +import { applyMapping, getBaseArray } from '../utils'; +import { getPhaseAmplitudeValues } from './utils'; + +export function useMappedComplexArrays( + values: H5WebComplex[][], + dims: number[], + mapping: DimensionMapping, + complexVisType: ComplexLineVisType, +): NdArray[] { + const [slicedDims, slicedMapping] = useSlicedDimsAndMapping(dims, mapping); + + const phaseAmplitudeValues = useMemo( + () => values.map(getPhaseAmplitudeValues), + [...values], // eslint-disable-line react-hooks/exhaustive-deps + ); + + const baseArrays = useMemo(() => { + return phaseAmplitudeValues.map((paValues) => + getBaseArray( + complexVisType === ComplexVisType.Phase + ? paValues.phaseValues + : paValues.amplitudeValues, + slicedDims, + ), + ); + }, [complexVisType, slicedDims, phaseAmplitudeValues]); + + return useMemo( + () => baseArrays.map((ndArr) => applyMapping(ndArr, slicedMapping)), + [baseArrays, slicedMapping], + ); +} diff --git a/packages/app/src/vis-packs/core/complex/lineConfig.tsx b/packages/app/src/vis-packs/core/complex/lineConfig.tsx index ea4191a04..4d413b5ca 100644 --- a/packages/app/src/vis-packs/core/complex/lineConfig.tsx +++ b/packages/app/src/vis-packs/core/complex/lineConfig.tsx @@ -1,3 +1,4 @@ +import type { ComplexLineVisType } from '@h5web/shared/vis-models'; import { ComplexVisType } from '@h5web/shared/vis-models'; import { createContext, useContext, useState } from 'react'; import type { StoreApi } from 'zustand'; @@ -6,8 +7,6 @@ import { persist } from 'zustand/middleware'; import type { ConfigProviderProps } from '../../models'; -type ComplexLineVisType = ComplexVisType.Phase | ComplexVisType.Amplitude; - export interface ComplexLineConfig { visType: ComplexLineVisType; setVisType: (visType: ComplexLineVisType) => void; diff --git a/packages/app/src/vis-packs/nexus/containers/NxComplexSpectrumContainer.tsx b/packages/app/src/vis-packs/nexus/containers/NxComplexSpectrumContainer.tsx index 5098a67d9..9199cd1ea 100644 --- a/packages/app/src/vis-packs/nexus/containers/NxComplexSpectrumContainer.tsx +++ b/packages/app/src/vis-packs/nexus/containers/NxComplexSpectrumContainer.tsx @@ -20,7 +20,7 @@ function NxComplexSpectrumContainer(props: VisContainerProps) { const nxData = useNxData(entity); assertComplexNxData(nxData); - const { signalDef, axisDefs, silxStyle } = nxData; + const { signalDef, axisDefs, auxDefs, silxStyle } = nxData; const signalDims = signalDef.dataset.shape; const [dimMapping, setDimMapping] = useDimMappingState(signalDims, 1); @@ -50,12 +50,14 @@ function NxComplexSpectrumContainer(props: VisContainerProps) { nxData={nxData} selection={getSliceSelection(dimMapping)} render={(nxValues) => { - const { signal, axisValues, title } = nxValues; + const { signal, axisValues, auxValues, title } = nxValues; return ( def?.label)} + auxValues={auxValues} dims={signalDims} dimMapping={dimMapping} axisLabels={axisLabels} diff --git a/packages/app/src/vis-packs/nexus/containers/NxSpectrumContainer.tsx b/packages/app/src/vis-packs/nexus/containers/NxSpectrumContainer.tsx index 54ad263f9..d0506f051 100644 --- a/packages/app/src/vis-packs/nexus/containers/NxSpectrumContainer.tsx +++ b/packages/app/src/vis-packs/nexus/containers/NxSpectrumContainer.tsx @@ -19,8 +19,8 @@ function NxSpectrumContainer(props: VisContainerProps) { const nxData = useNxData(entity); assertNumericNxData(nxData); - const { signalDef, axisDefs, auxDefs, silxStyle } = nxData; + const { signalDef, axisDefs, auxDefs, silxStyle } = nxData; const signalDims = signalDef.dataset.shape; const errorDims = signalDef.errorDataset?.shape; diff --git a/packages/app/src/vis-packs/nexus/guards.ts b/packages/app/src/vis-packs/nexus/guards.ts index 8ce86933d..84d0d1281 100644 --- a/packages/app/src/vis-packs/nexus/guards.ts +++ b/packages/app/src/vis-packs/nexus/guards.ts @@ -6,13 +6,15 @@ import type { NxData } from './models'; export function assertNumericNxData( nxData: NxData, ): asserts nxData is NxData { - const { signalDef } = nxData; + const { signalDef, auxDefs } = nxData; assertNumericType(signalDef.dataset); + auxDefs.forEach((def) => assertNumericType(def.dataset)); } export function assertComplexNxData( nxData: NxData, ): asserts nxData is NxData { - const { signalDef } = nxData; + const { signalDef, auxDefs } = nxData; assertComplexType(signalDef.dataset); + auxDefs.forEach((def) => assertComplexType(def.dataset)); } diff --git a/packages/app/src/vis-packs/nexus/hooks.ts b/packages/app/src/vis-packs/nexus/hooks.ts index f25b19f26..e97353766 100644 --- a/packages/app/src/vis-packs/nexus/hooks.ts +++ b/packages/app/src/vis-packs/nexus/hooks.ts @@ -1,4 +1,3 @@ -import { isDefined } from '@h5web/shared/guards'; import type { GroupWithChildren } from '@h5web/shared/hdf5-models'; import type { DimensionMapping } from '../../dimension-mapper/models'; @@ -7,8 +6,8 @@ import { useValuesInCache } from '../core/hooks'; import type { NxData } from './models'; import { assertNxDataGroup, - findAssociatedDatasets, findAuxErrorDataset, + findAuxiliaryDatasets, findAxesDatasets, findErrorDataset, findSignalDataset, @@ -23,11 +22,7 @@ export function useNxData(group: GroupWithChildren): NxData { assertNxDataGroup(group, attrValuesStore); const signalDataset = findSignalDataset(group, attrValuesStore); const axisDatasets = findAxesDatasets(group, signalDataset, attrValuesStore); - const auxSignals = findAssociatedDatasets( - group, - 'auxiliary_signals', - attrValuesStore, - ).filter(isDefined); + const auxSignals = findAuxiliaryDatasets(group, attrValuesStore); return { titleDataset: findTitleDataset(group), diff --git a/packages/app/src/vis-packs/nexus/models.ts b/packages/app/src/vis-packs/nexus/models.ts index 554a3bbad..42b7c5b12 100644 --- a/packages/app/src/vis-packs/nexus/models.ts +++ b/packages/app/src/vis-packs/nexus/models.ts @@ -38,7 +38,6 @@ type WithError = T & { errorDataset?: NumArrayDataset; }; -export type AuxDef = WithError>; export type AxisDef = DatasetDef; export interface SilxStyle { @@ -51,7 +50,7 @@ export interface NxData< > { titleDataset?: Dataset; signalDef: WithError>; - auxDefs: AuxDef[]; + auxDefs: WithError>[]; axisDefs: AxisMapping; silxStyle: SilxStyle; } @@ -60,7 +59,7 @@ export interface NxValues { title: string; signal: ArrayValue; errors?: NumArray; - auxValues: NumArray[]; + auxValues: ArrayValue[]; auxErrors: (NumArray | undefined)[]; axisValues: AxisMapping; } diff --git a/packages/app/src/vis-packs/nexus/utils.ts b/packages/app/src/vis-packs/nexus/utils.ts index 669a39bbc..b12bea28c 100644 --- a/packages/app/src/vis-packs/nexus/utils.ts +++ b/packages/app/src/vis-packs/nexus/utils.ts @@ -10,6 +10,7 @@ import { assertStringType, isAxisScaleType, isColorScaleType, + isDefined, } from '@h5web/shared/guards'; import type { ArrayShape, @@ -127,7 +128,7 @@ export function findAssociatedDatasets( group: GroupWithChildren, type: 'axes' | 'auxiliary_signals', attrValuesStore: AttrValuesStore, -): (NumArrayDataset | undefined)[] { +): (Dataset | undefined)[] { const dsetList = attrValuesStore.getSingle(group, type) || []; const dsetNames = typeof dsetList === 'string' ? [dsetList] : dsetList; assertArray(dsetNames); @@ -147,7 +148,6 @@ export function findAssociatedDatasets( assertDefined(dataset, `Expected child entity "${name}" to exist`); assertDataset(dataset, `Expected child "${name}" to be a dataset`); assertArrayShape(dataset); - assertNumericType(dataset); return dataset; }); } @@ -190,12 +190,31 @@ export function findAxesDatasets( group: GroupWithChildren, signal: Dataset, attrValuesStore: AttrValuesStore, -) { +): (NumArrayDataset | undefined)[] { if (!hasAttribute(group, 'axes')) { return findOldStyleAxesDatasets(group, signal, attrValuesStore); } - return findAssociatedDatasets(group, 'axes', attrValuesStore); + return findAssociatedDatasets(group, 'axes', attrValuesStore).map( + (dataset) => { + if (dataset) { + assertNumericType(dataset); + } + return dataset; + }, + ); +} + +export function findAuxiliaryDatasets( + group: GroupWithChildren, + attrValuesStore: AttrValuesStore, +): Dataset[] { + return findAssociatedDatasets(group, 'auxiliary_signals', attrValuesStore) + .filter(isDefined) + .map((dataset) => { + assertNumericOrComplexType(dataset); + return dataset; + }); } export function findTitleDataset( diff --git a/packages/shared/src/mock-values.ts b/packages/shared/src/mock-values.ts index 30c1cd979..193d9042a 100644 --- a/packages/shared/src/mock-values.ts +++ b/packages/shared/src/mock-values.ts @@ -231,6 +231,14 @@ export const mockValues = { twoD().data.map((v) => v * 2), [20, 41], ), + secondary_cplx: () => + ndarray( + [ + [cplx(1, -6), cplx(-3.1, -1)], + [cplx(6, -1), cplx(-4, 1.1)], + ].flat(1), + [2, 2], + ), tertiary: () => ndarray( twoD().data.map((v) => v / 2), diff --git a/packages/shared/src/vis-models.ts b/packages/shared/src/vis-models.ts index f02df262d..91510553b 100644 --- a/packages/shared/src/vis-models.ts +++ b/packages/shared/src/vis-models.ts @@ -38,6 +38,7 @@ export enum ComplexVisType { Amplitude = 'amplitude', PhaseAmplitude = 'phase-amplitude', } +export type ComplexLineVisType = Exclude; export interface Bounds { min: number;