From 8ff7a24b57ce6f0c4469f831d5cd68d182868f53 Mon Sep 17 00:00:00 2001 From: Loic Huder Date: Thu, 15 Jan 2026 14:03:24 +0100 Subject: [PATCH 1/3] Support scaling_factor and offset for axis --- .../src/vis-packs/nexus/NxValuesFetcher.tsx | 17 +++++- packages/app/src/vis-packs/nexus/hooks.ts | 9 ++- packages/app/src/vis-packs/nexus/models.ts | 7 ++- .../app/src/vis-packs/nexus/utils.test.ts | 24 +++++++- packages/app/src/vis-packs/nexus/utils.ts | 56 ++++++++++++++++++- 5 files changed, 105 insertions(+), 8 deletions(-) diff --git a/packages/app/src/vis-packs/nexus/NxValuesFetcher.tsx b/packages/app/src/vis-packs/nexus/NxValuesFetcher.tsx index 31715e9d4..dd8ec4ecf 100644 --- a/packages/app/src/vis-packs/nexus/NxValuesFetcher.tsx +++ b/packages/app/src/vis-packs/nexus/NxValuesFetcher.tsx @@ -43,7 +43,22 @@ function NxValuesFetcher( const errors = useDatasetValue(signalDef.errorDataset, selection); const auxValues = useDatasetsValues(auxDatasets, selection); const auxErrors = useDatasetsValues(auxErrorDatasets, selection); - const axisValues = useDatasetsValues(axisDatasets); + const rawAxisValues = useDatasetsValues(axisDatasets); + const axisValues = rawAxisValues.map((value, i) => { + const axisScalingFactor = axisDefs[i]?.scalingFactor; + const axisOffset = axisDefs[i]?.offset; + if (value && (axisScalingFactor || axisOffset)) { + const offset = axisOffset ?? 0; + const scalingFactor = axisScalingFactor ?? 1; + const scaledValue: number[] = []; + value.forEach((v) => { + scaledValue.push(scalingFactor * Number(v) + offset); + }); + return scaledValue; + } + + return value; + }); return render({ title, signal, errors, auxValues, auxErrors, axisValues }); } diff --git a/packages/app/src/vis-packs/nexus/hooks.ts b/packages/app/src/vis-packs/nexus/hooks.ts index a42dc873a..0e5c6ebcc 100644 --- a/packages/app/src/vis-packs/nexus/hooks.ts +++ b/packages/app/src/vis-packs/nexus/hooks.ts @@ -19,13 +19,14 @@ import { findTitleDataset, getDatasetInfo, getDefaultSlice, + getScalingInfo, getSilxStyle, } from './utils'; export const useDefaultSlice = createMemo(getDefaultSlice); export function useNxData(group: GroupWithChildren): NxData { - const { attrValuesStore } = useDataContext(); + const { attrValuesStore, valuesStore } = useDataContext(); assertNxDataGroup(group, attrValuesStore); const signalDataset = findSignalDataset(group, attrValuesStore); @@ -46,7 +47,11 @@ export function useNxData(group: GroupWithChildren): NxData { })), axisDefs: axisDatasets.map( (dataset) => - dataset && { dataset, ...getDatasetInfo(dataset, attrValuesStore) }, + dataset && { + dataset, + ...getDatasetInfo(dataset, attrValuesStore), + ...getScalingInfo(group, dataset.name, valuesStore), + }, ), defaultSlice: useDefaultSlice(group, signalDataset.shape, attrValuesStore), silxStyle: getSilxStyle(group, attrValuesStore), diff --git a/packages/app/src/vis-packs/nexus/models.ts b/packages/app/src/vis-packs/nexus/models.ts index a537de64b..65abc4321 100644 --- a/packages/app/src/vis-packs/nexus/models.ts +++ b/packages/app/src/vis-packs/nexus/models.ts @@ -28,6 +28,11 @@ export interface DatasetInfo { unit: string | undefined; } +export interface ScalingInfo { + scalingFactor: number | undefined; + offset: number | undefined; +} + export interface DatasetDef< T extends NumericLikeType | ComplexType = NumericLikeType | ComplexType, > extends DatasetInfo { @@ -38,7 +43,7 @@ type WithError = T & { errorDataset?: Dataset; }; -export type AxisDef = DatasetDef; +export type AxisDef = DatasetDef & ScalingInfo; export type DefaultSlice = (number | '.')[]; export interface SilxStyle { diff --git a/packages/app/src/vis-packs/nexus/utils.test.ts b/packages/app/src/vis-packs/nexus/utils.test.ts index 2a76eca1f..753857be5 100644 --- a/packages/app/src/vis-packs/nexus/utils.test.ts +++ b/packages/app/src/vis-packs/nexus/utils.test.ts @@ -5,9 +5,27 @@ import { describe, expect, it } from 'vitest'; import { applyDefaultSlice, guessKeepRatio } from './utils'; const axisDataset = dataset('foo', intType(), [5]); -const axisDefNoUnit = { label: 'foo', unit: undefined, dataset: axisDataset }; -const axisDefUnitX = { label: 'foo', unit: 'mm', dataset: axisDataset }; -const axisDefUnitY = { label: 'foo', unit: 'degrees', dataset: axisDataset }; +const axisDefNoUnit = { + label: 'foo', + unit: undefined, + dataset: axisDataset, + scalingFactor: undefined, + offset: undefined, +}; +const axisDefUnitX = { + label: 'foo', + unit: 'mm', + dataset: axisDataset, + scalingFactor: undefined, + offset: undefined, +}; +const axisDefUnitY = { + label: 'foo', + unit: 'degrees', + dataset: axisDataset, + scalingFactor: undefined, + offset: undefined, +}; describe('guessKeepRatio', () => { it('should return `cover` if units of both axes are provided and equal', () => { diff --git a/packages/app/src/vis-packs/nexus/utils.ts b/packages/app/src/vis-packs/nexus/utils.ts index 3390cf6e3..28a8c6f56 100644 --- a/packages/app/src/vis-packs/nexus/utils.ts +++ b/packages/app/src/vis-packs/nexus/utils.ts @@ -26,12 +26,13 @@ import { } from '@h5web/shared/hdf5-models'; import { getChildEntity } from '@h5web/shared/hdf5-utils'; -import { type AttrValuesStore } from '../../providers/models'; +import { type AttrValuesStore, type ValuesStore } from '../../providers/models'; import { hasAttribute } from '../../utils'; import { type AxisDef, type DatasetInfo, type DefaultSlice, + type ScalingInfo, type SilxStyle, } from './models'; @@ -130,6 +131,38 @@ export function findAuxErrorDataset( return dataset; } +export function findScalingFactor( + group: GroupWithChildren, + datasetName: string, +): Dataset | undefined { + const dataset = getChildEntity(group, `${datasetName}_scaling_factor`); + + if (!dataset) { + return undefined; + } + + assertDataset(dataset); + assertScalarShape(dataset); + assertNumericType(dataset); + return dataset; +} + +export function findOffset( + group: GroupWithChildren, + datasetName: string, +): Dataset | undefined { + const dataset = getChildEntity(group, `${datasetName}_offset`); + + if (!dataset) { + return undefined; + } + + assertDataset(dataset); + assertScalarShape(dataset); + assertNumericType(dataset); + return dataset; +} + export function findAssociatedDatasets( group: GroupWithChildren, type: 'axes' | 'auxiliary_signals', @@ -312,6 +345,27 @@ export function getSilxStyle( } } +export function getScalingInfo( + group: GroupWithChildren, + datasetName: string, + valuesStore: ValuesStore, +): ScalingInfo { + const scalingFactorDataset = findScalingFactor(group, datasetName); + const scalingFactor = scalingFactorDataset + ? Number(valuesStore.get({ dataset: scalingFactorDataset })) + : undefined; + + const offsetDataset = findOffset(group, datasetName); + const offset = offsetDataset + ? Number(valuesStore.get({ dataset: offsetDataset })) + : undefined; + + return { + scalingFactor, + offset, + }; +} + export function getDatasetInfo( dataset: Dataset, attrValuesStore: AttrValuesStore, From d5f048e1d07091b70f0c91358e579ab4ab996154 Mon Sep 17 00:00:00 2001 From: Axel Bocciarelli Date: Thu, 15 Jan 2026 15:10:17 +0100 Subject: [PATCH 2/3] Skip scaling axis values if bigint --- packages/app/src/vis-packs/core/utils.ts | 10 ++----- .../src/vis-packs/nexus/NxValuesFetcher.tsx | 26 +++++++++++-------- packages/shared/src/guards.ts | 8 ++++++ 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/app/src/vis-packs/core/utils.ts b/packages/app/src/vis-packs/core/utils.ts index 276ba12b5..a69d8babc 100644 --- a/packages/app/src/vis-packs/core/utils.ts +++ b/packages/app/src/vis-packs/core/utils.ts @@ -4,7 +4,9 @@ import { type InteractionInfo, } from '@h5web/lib'; import { + isBigIntArray, isBigIntTypedArray, + isBoolArray, isIntegerType, isNumericType, } from '@h5web/shared/guards'; @@ -88,14 +90,6 @@ export function getImageInteractions(keepRatio: boolean): InteractionInfo[] { return keepRatio ? BASE_INTERACTIONS : INTERACTIONS_WITH_AXIAL_ZOOM; } -function isBigIntArray(val: ArrayValue): val is bigint[] { - return Array.isArray(val) && typeof val[0] === 'bigint'; -} - -function isBoolArray(val: ArrayValue): val is boolean[] { - return Array.isArray(val) && typeof val[0] === 'boolean'; -} - export function toNumArray | undefined>( arr: T, ): T extends ArrayValue ? NumArray : undefined; diff --git a/packages/app/src/vis-packs/nexus/NxValuesFetcher.tsx b/packages/app/src/vis-packs/nexus/NxValuesFetcher.tsx index dd8ec4ecf..fba5955bf 100644 --- a/packages/app/src/vis-packs/nexus/NxValuesFetcher.tsx +++ b/packages/app/src/vis-packs/nexus/NxValuesFetcher.tsx @@ -1,3 +1,8 @@ +import { + assertDefined, + isBigIntArray, + isBigIntTypedArray, +} from '@h5web/shared/guards'; import { type ComplexType, type NumericLikeType, @@ -44,20 +49,19 @@ function NxValuesFetcher( const auxValues = useDatasetsValues(auxDatasets, selection); const auxErrors = useDatasetsValues(auxErrorDatasets, selection); const rawAxisValues = useDatasetsValues(axisDatasets); + const axisValues = rawAxisValues.map((value, i) => { - const axisScalingFactor = axisDefs[i]?.scalingFactor; - const axisOffset = axisDefs[i]?.offset; - if (value && (axisScalingFactor || axisOffset)) { - const offset = axisOffset ?? 0; - const scalingFactor = axisScalingFactor ?? 1; - const scaledValue: number[] = []; - value.forEach((v) => { - scaledValue.push(scalingFactor * Number(v) + offset); - }); - return scaledValue; + if (!value || isBigIntArray(value) || isBigIntTypedArray(value)) { + return value; + } + + assertDefined(axisDefs[i]); + const { scalingFactor, offset } = axisDefs[i]; + if (scalingFactor === undefined || offset === undefined) { + return value; } - return value; + return value.map((v) => scalingFactor * v + offset); }); return render({ title, signal, errors, auxValues, auxErrors, axisValues }); diff --git a/packages/shared/src/guards.ts b/packages/shared/src/guards.ts index 7b430890c..764b5db8f 100644 --- a/packages/shared/src/guards.ts +++ b/packages/shared/src/guards.ts @@ -148,6 +148,14 @@ export function isComplexArray(val: unknown): val is H5WebComplex[] { return Array.isArray(val) && isComplex(val[0]); } +export function isBigIntArray(val: unknown): val is bigint[] { + return Array.isArray(val) && typeof val[0] === 'bigint'; +} + +export function isBoolArray(val: unknown): val is boolean[] { + return Array.isArray(val) && typeof val[0] === 'boolean'; +} + export function isTypedArray(val: unknown): val is TypedArray { return ( val instanceof Int8Array || From e7e1b9683feafe6728220023f266ecb198e2f24e Mon Sep 17 00:00:00 2001 From: Loic Huder Date: Thu, 15 Jan 2026 16:57:55 +0100 Subject: [PATCH 3/3] Review --- packages/app/src/vis-packs/nexus/hooks.ts | 3 +- packages/app/src/vis-packs/nexus/utils.ts | 41 ++++++++++++++++------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/packages/app/src/vis-packs/nexus/hooks.ts b/packages/app/src/vis-packs/nexus/hooks.ts index 0e5c6ebcc..5fbc01cbd 100644 --- a/packages/app/src/vis-packs/nexus/hooks.ts +++ b/packages/app/src/vis-packs/nexus/hooks.ts @@ -49,8 +49,7 @@ export function useNxData(group: GroupWithChildren): NxData { (dataset) => dataset && { dataset, - ...getDatasetInfo(dataset, attrValuesStore), - ...getScalingInfo(group, dataset.name, valuesStore), + ...getAxisDef(group, dataset, attrValuesStore, valuesStore), }, ), defaultSlice: useDefaultSlice(group, signalDataset.shape, attrValuesStore), diff --git a/packages/app/src/vis-packs/nexus/utils.ts b/packages/app/src/vis-packs/nexus/utils.ts index 28a8c6f56..42daf1028 100644 --- a/packages/app/src/vis-packs/nexus/utils.ts +++ b/packages/app/src/vis-packs/nexus/utils.ts @@ -4,6 +4,7 @@ import { assertArrayShape, assertDataset, assertDefined, + assertNum, assertNumericLikeOrComplexType, assertNumericType, assertScalarShape, @@ -131,7 +132,7 @@ export function findAuxErrorDataset( return dataset; } -export function findScalingFactor( +export function findScalingFactorDataset( group: GroupWithChildren, datasetName: string, ): Dataset | undefined { @@ -147,7 +148,7 @@ export function findScalingFactor( return dataset; } -export function findOffset( +export function findOffsetDataset( group: GroupWithChildren, datasetName: string, ): Dataset | undefined { @@ -345,24 +346,38 @@ export function getSilxStyle( } } -export function getScalingInfo( +function getNumericValue( + dataset: Dataset | undefined, + valuesStore: ValuesStore, +): number | undefined { + if (dataset === undefined) { + return undefined; + } + + const value = valuesStore.get({ dataset }); + assertNum(value); + return value; +} + +export function getAxisDef( group: GroupWithChildren, - datasetName: string, + dataset: Dataset, + attrValuesStore: AttrValuesStore, valuesStore: ValuesStore, ): ScalingInfo { - const scalingFactorDataset = findScalingFactor(group, datasetName); - const scalingFactor = scalingFactorDataset - ? Number(valuesStore.get({ dataset: scalingFactorDataset })) - : undefined; - - const offsetDataset = findOffset(group, datasetName); - const offset = offsetDataset - ? Number(valuesStore.get({ dataset: offsetDataset })) - : undefined; + const scalingFactor = getNumericValue( + findScalingFactorDataset(group, dataset.name), + valuesStore, + ); + const offset = getNumericValue( + findOffsetDataset(group, dataset.name), + valuesStore, + ); return { scalingFactor, offset, + ...getDatasetInfo(dataset, attrValuesStore), }; }