diff --git a/.vscode/settings.json b/.vscode/settings.json index 0c024335b..d035223cc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,7 +18,7 @@ "typescript.preferences.importModuleSpecifier": "relative", "typescript.preferences.preferTypeOnlyAutoImports": true, "typescript.preferences.autoImportSpecifierExcludeRegexes": [ - "^(node:)?(console|util)$", + "^(node:)?(console|util|test)$", "^three/.*$" ], diff --git a/packages/app/src/providers/mock/mock-file.ts b/packages/app/src/providers/mock/mock-file.ts index 4954a0ebb..40626647e 100644 --- a/packages/app/src/providers/mock/mock-file.ts +++ b/packages/app/src/providers/mock/mock-file.ts @@ -152,6 +152,7 @@ export function makeMockFile(): GroupWithChildren { group('typed_arrays', [ array('uint8', { type: intType(false, 8) }), array('int16', { type: intType(true, 16) }), + array('int64', { type: intType(true, 64) }), array('float32', { type: floatType(32) }), array('float64', { type: floatType(64) }), withImageAttr(array('uint8_rgb', { type: intType(false, 8) })), @@ -267,6 +268,13 @@ export function makeMockFile(): GroupWithChildren { axes: { X: array('X'), Y_scatter: array('Y_scatter') }, axesAttr: ['X', 'Y_scatter'], }), + nxData('bigint', { + signal: array('twoD_bigint'), + auxiliary: { secondary_bigint: array('secondary_bigint') }, + auxAttr: ['secondary_bigint'], + axes: { X_bigint: array('X_bigint') }, + axesAttr: ['.', 'X_bigint'], + }), nxGroup('old-style', 'NXdata', { children: [ array('twoD', { diff --git a/packages/app/src/vis-packs/core/complex/MappedComplexLineVis.tsx b/packages/app/src/vis-packs/core/complex/MappedComplexLineVis.tsx index d5450db1d..e560f6b59 100644 --- a/packages/app/src/vis-packs/core/complex/MappedComplexLineVis.tsx +++ b/packages/app/src/vis-packs/core/complex/MappedComplexLineVis.tsx @@ -16,6 +16,7 @@ import { createPortal } from 'react-dom'; import { type DimensionMapping } from '../../../dimension-mapper/models'; import visualizerStyles from '../../../visualizer/Visualizer.module.css'; +import { useToNumArrays } from '../hooks'; import { type LineConfig } from '../line/config'; import { DEFAULT_DOMAIN } from '../utils'; import ComplexLineToolbar from './ComplexLineToolbar'; @@ -58,6 +59,7 @@ function MappedComplexLineVis(props: Props) { const { customDomain, yScaleType, xScaleType, curveType, showGrid } = lineConfig; + const numAxisArrays = useToNumArrays(axisValues); const [dataArray, ...auxArrays] = useMappedComplexArrays( [value, ...auxValues], dims, @@ -99,7 +101,7 @@ function MappedComplexLineVis(props: Props) { showGrid={showGrid} abscissaParams={{ label: axisLabels[xDimIndex], - value: axisValues[xDimIndex], + value: numAxisArrays[xDimIndex], scaleType: xScaleType, }} ordinateLabel={ordinateLabel} diff --git a/packages/app/src/vis-packs/core/complex/MappedComplexVis.tsx b/packages/app/src/vis-packs/core/complex/MappedComplexVis.tsx index 89c07426a..002a10aaa 100644 --- a/packages/app/src/vis-packs/core/complex/MappedComplexVis.tsx +++ b/packages/app/src/vis-packs/core/complex/MappedComplexVis.tsx @@ -21,6 +21,7 @@ import { useBaseArray, useMappedArray, useSlicedDimsAndMapping, + useToNumArrays, } from '../hooks'; import { DEFAULT_DOMAIN } from '../utils'; import ComplexToolbar from './ComplexToolbar'; @@ -62,8 +63,9 @@ function MappedComplexVis(props: Props) { invertColorMap, } = heatmapConfig; - const [slicedDims, slicedMapping] = useSlicedDimsAndMapping(dims, dimMapping); + const numAxisArrays = useToNumArrays(axisValues); + const [slicedDims, slicedMapping] = useSlicedDimsAndMapping(dims, dimMapping); const mappedArray = useMappedArray(value, slicedDims, slicedMapping); const { phaseValues, phaseBounds, amplitudeValues, amplitudeBounds } = @@ -112,11 +114,11 @@ function MappedComplexVis(props: Props) { invertColorMap={invertColorMap} abscissaParams={{ label: axisLabels[xDimIndex], - value: axisValues[xDimIndex], + value: numAxisArrays[xDimIndex], }} ordinateParams={{ label: axisLabels[yDimIndex], - value: axisValues[yDimIndex], + value: numAxisArrays[yDimIndex], }} alpha={ visType === ComplexVisType.PhaseAmplitude diff --git a/packages/app/src/vis-packs/core/heatmap/MappedHeatmapVis.tsx b/packages/app/src/vis-packs/core/heatmap/MappedHeatmapVis.tsx index 16d4f4357..9b96b7545 100644 --- a/packages/app/src/vis-packs/core/heatmap/MappedHeatmapVis.tsx +++ b/packages/app/src/vis-packs/core/heatmap/MappedHeatmapVis.tsx @@ -16,6 +16,7 @@ import { useMappedArray, useSlicedDimsAndMapping, useToNumArray, + useToNumArrays, } from '../hooks'; import { DEFAULT_DOMAIN, formatNumLikeType, getSliceSelection } from '../utils'; import { type HeatmapConfig } from './config'; @@ -58,6 +59,7 @@ function MappedHeatmapVis(props: Props) { } = config; const numArray = useToNumArray(value); + const numAxisArrays = useToNumArrays(axisValues); const { shape: dims } = dataset; const [slicedDims, slicedMapping] = useSlicedDimsAndMapping(dims, dimMapping); @@ -103,11 +105,11 @@ function MappedHeatmapVis(props: Props) { invertColorMap={invertColorMap} abscissaParams={{ label: axisLabels[xDimIndex], - value: axisValues[xDimIndex], + value: numAxisArrays[xDimIndex], }} ordinateParams={{ label: axisLabels[yDimIndex], - value: axisValues[yDimIndex], + value: numAxisArrays[yDimIndex], }} flipXAxis={flipXAxis} flipYAxis={flipYAxis} diff --git a/packages/app/src/vis-packs/core/hooks.ts b/packages/app/src/vis-packs/core/hooks.ts index b439d8cd5..e75e5a6dc 100644 --- a/packages/app/src/vis-packs/core/hooks.ts +++ b/packages/app/src/vis-packs/core/hooks.ts @@ -28,7 +28,15 @@ export const useToNumArray = createMemo(toNumArray); export function useToNumArrays( arrays: ArrayValue[], -): NumArray[] { +): NumArray[]; + +export function useToNumArrays( + arrays: (ArrayValue | undefined)[], +): (NumArray | undefined)[]; + +export function useToNumArrays( + arrays: (ArrayValue | undefined)[], +): (NumArray | undefined)[] { return useMemo(() => arrays.map(toNumArray), arrays); // eslint-disable-line react-hooks/exhaustive-deps } diff --git a/packages/app/src/vis-packs/core/line/MappedLineVis.tsx b/packages/app/src/vis-packs/core/line/MappedLineVis.tsx index 987a7e88b..3c0311930 100644 --- a/packages/app/src/vis-packs/core/line/MappedLineVis.tsx +++ b/packages/app/src/vis-packs/core/line/MappedLineVis.tsx @@ -80,11 +80,14 @@ function MappedLineVis(props: Props) { const numArray = useToNumArray(value); const numAuxArrays = useToNumArrays(auxValues); + const numErrorsArray = useToNumArray(errors); + const numAuxErrorsArrays = useToNumArrays(auxErrors); + const numAxisArrays = useToNumArrays(axisValues); const dataArray = useMappedArray(numArray, ...hookArgs); - const errorsArray = useMappedArray(errors, ...hookArgs); + const errorsArray = useMappedArray(numErrorsArray, ...hookArgs); const auxArrays = useMappedArrays(numAuxArrays, ...hookArgs); - const auxErrorsArrays = useMappedArrays(auxErrors, ...hookArgs); + const auxErrorsArrays = useMappedArrays(numAuxErrorsArrays, ...hookArgs); const dataDomain = useDomain( dataArray, @@ -131,7 +134,7 @@ function MappedLineVis(props: Props) { showGrid={showGrid} abscissaParams={{ label: axisLabels[xDimIndex], - value: axisValues[xDimIndex], + value: numAxisArrays[xDimIndex], scaleType: xScaleType, }} ordinateLabel={valueLabel} diff --git a/packages/app/src/vis-packs/core/matrix/utils.ts b/packages/app/src/vis-packs/core/matrix/utils.ts index 5174feea7..2b0c54b97 100644 --- a/packages/app/src/vis-packs/core/matrix/utils.ts +++ b/packages/app/src/vis-packs/core/matrix/utils.ts @@ -3,6 +3,7 @@ import { isBoolType, isComplexType, isEnumType, + isIntegerType, isNumericType, } from '@h5web/shared/guards'; import { @@ -23,7 +24,7 @@ import { format } from 'd3-format'; export function createNumericFormatter( notation: Notation, -): (val: ScalarValue) => string { +): (val: number) => string { switch (notation) { case Notation.FixedPoint: return format('.3f'); @@ -34,6 +35,19 @@ export function createNumericFormatter( } } +export function createBigIntFormatter( + notation: Notation, +): (val: ScalarValue) => string { + switch (notation) { + case Notation.Scientific: { + const formatter = createNumericFormatter(notation); + return (val) => formatter(Number(val)); + } + default: + return (val) => val.toString(); + } +} + export function createMatrixComplexFormatter( notation: Notation, ): (val: ScalarValue) => string { @@ -54,6 +68,10 @@ export function getFormatter( type: PrintableType, notation: Notation, ): ValueFormatter { + if (isIntegerType(type) && type.size === 64) { + return createBigIntFormatter(notation); + } + if (isNumericType(type)) { return createNumericFormatter(notation); } diff --git a/packages/app/src/vis-packs/core/rgb/MappedRgbVis.tsx b/packages/app/src/vis-packs/core/rgb/MappedRgbVis.tsx index 7017ce5af..cf1a8bf50 100644 --- a/packages/app/src/vis-packs/core/rgb/MappedRgbVis.tsx +++ b/packages/app/src/vis-packs/core/rgb/MappedRgbVis.tsx @@ -10,7 +10,12 @@ 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 { + useMappedArray, + useSlicedDimsAndMapping, + useToNumArray, + useToNumArrays, +} from '../hooks'; import { type RgbVisConfig } from './config'; import RgbToolbar from './RgbToolbar'; @@ -40,8 +45,12 @@ function MappedRgbVis(props: Props) { const { showGrid, keepRatio, imageType, flipXAxis, flipYAxis } = config; const { shape: dims } = dataset; + const numAxisArrays = useToNumArrays(axisValues); + const [slicedDims, slicedMapping] = useSlicedDimsAndMapping(dims, dimMapping); - const dataArray = useMappedArray(value, slicedDims, slicedMapping); + + const numArray = useToNumArray(value); + const dataArray = useMappedArray(numArray, slicedDims, slicedMapping); const xDimIndex = dimMapping.indexOf('x'); const yDimIndex = dimMapping.indexOf('y'); @@ -59,11 +68,11 @@ function MappedRgbVis(props: Props) { imageType={imageType} abscissaParams={{ label: axisLabels[xDimIndex], - value: axisValues[xDimIndex], + value: numAxisArrays[xDimIndex], }} ordinateParams={{ label: axisLabels[yDimIndex], - value: axisValues[yDimIndex], + value: numAxisArrays[yDimIndex], }} flipXAxis={flipXAxis} flipYAxis={flipYAxis} diff --git a/packages/app/src/vis-packs/core/scalar/utils.ts b/packages/app/src/vis-packs/core/scalar/utils.ts index ef94ea381..159049558 100644 --- a/packages/app/src/vis-packs/core/scalar/utils.ts +++ b/packages/app/src/vis-packs/core/scalar/utils.ts @@ -31,5 +31,5 @@ export function getFormatter( return createEnumFormatter(dataset.type.mapping); } - return (val: number | string) => val.toString(); + return (val: number | bigint | string) => val.toString(); } diff --git a/packages/app/src/vis-packs/core/scatter/MappedScatterVis.tsx b/packages/app/src/vis-packs/core/scatter/MappedScatterVis.tsx index b94a927e4..50d54d95e 100644 --- a/packages/app/src/vis-packs/core/scatter/MappedScatterVis.tsx +++ b/packages/app/src/vis-packs/core/scatter/MappedScatterVis.tsx @@ -5,7 +5,7 @@ import { type AxisMapping } from '@h5web/shared/nexus-models'; import { createPortal } from 'react-dom'; import visualizerStyles from '../../../visualizer/Visualizer.module.css'; -import { useBaseArray } from '../hooks'; +import { useBaseArray, useToNumArray, useToNumArrays } from '../hooks'; import { DEFAULT_DOMAIN } from '../utils'; import { type ScatterConfig } from './config'; import ScatterToolbar from './ScatterToolbar'; @@ -33,13 +33,16 @@ function MappedScatterVis(props: Props) { yScaleType, } = config; - const dataArray = useBaseArray(value, [value.length]); + const numArray = useToNumArray(value); + const numAxisArrays = useToNumArrays(axisValues); + + const dataArray = useBaseArray(numArray, [value.length]); const dataDomain = useDomain(dataArray, scaleType) || DEFAULT_DOMAIN; const visDomain = useVisDomain(customDomain, dataDomain); const [safeDomain] = useSafeDomain(visDomain, dataDomain, scaleType); const [xLabel, yLabel] = axisLabels; - const [xValue, yValue] = axisValues; + const [xValue, yValue] = numAxisArrays; assertDefined(xValue); assertDefined(yValue); diff --git a/packages/app/src/vis-packs/core/surface/MappedSurfaceVis.tsx b/packages/app/src/vis-packs/core/surface/MappedSurfaceVis.tsx index 527a6f37a..4d2cb19af 100644 --- a/packages/app/src/vis-packs/core/surface/MappedSurfaceVis.tsx +++ b/packages/app/src/vis-packs/core/surface/MappedSurfaceVis.tsx @@ -9,7 +9,11 @@ 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 { + useMappedArray, + useSlicedDimsAndMapping, + useToNumArray, +} from '../hooks'; import { DEFAULT_DOMAIN } from '../utils'; import { type SurfaceConfig } from './config'; import SurfaceToolbar from './SurfaceToolbar'; @@ -29,7 +33,9 @@ function MappedSurfaceVis(props: Props) { const { shape: dims } = dataset; const [slicedDims, slicedMapping] = useSlicedDimsAndMapping(dims, dimMapping); - const dataArray = useMappedArray(value, slicedDims, slicedMapping); + + const numArray = useToNumArray(value); + const dataArray = useMappedArray(numArray, slicedDims, slicedMapping); const dataDomain = useDomain(dataArray, scaleType) || DEFAULT_DOMAIN; const visDomain = useVisDomain(customDomain, dataDomain); diff --git a/packages/app/src/vis-packs/core/utils.ts b/packages/app/src/vis-packs/core/utils.ts index c270d1541..8c79fe778 100644 --- a/packages/app/src/vis-packs/core/utils.ts +++ b/packages/app/src/vis-packs/core/utils.ts @@ -1,5 +1,9 @@ import { type InteractionInfo } from '@h5web/lib'; -import { isIntegerType, isNumericType } from '@h5web/shared/guards'; +import { + isBigIntTypeArray, + isIntegerType, + isNumericType, +} from '@h5web/shared/guards'; import { type ArrayValue, DTypeClass, @@ -93,11 +97,33 @@ 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(arr: ArrayValue): NumArray { +export function toNumArray | undefined>( + arr: T, +): T extends ArrayValue ? NumArray : undefined; + +export function toNumArray( + arr: ArrayValue | undefined, +): NumArray | undefined { + if (!arr) { + return undefined; + } + + if (isBigIntTypeArray(arr)) { + return Float64Array.from(arr, Number); // cast to float 64 + } + + if (isBigIntArray(arr)) { + return arr.map(Number); // cast to float 64 + } + if (isBoolArray(arr)) { return arr.map((val) => (val ? 1 : 0)); } diff --git a/packages/app/src/vis-packs/core/visualizations.test.ts b/packages/app/src/vis-packs/core/visualizations.test.ts index f98b8cbf1..be73897ea 100644 --- a/packages/app/src/vis-packs/core/visualizations.test.ts +++ b/packages/app/src/vis-packs/core/visualizations.test.ts @@ -31,6 +31,7 @@ const mockStore = { const scalarInt = dataset('int', intType(), []); const scalarUint = dataset('uint', intType(false), []); +const scalarBigInt = dataset('bigint', intType(true, 64), []); const scalarFloat = dataset('float', floatType(), []); const scalarStr = dataset('float', strType(), []); const scalarBool = dataset('bool', boolType(intType(true, 8)), []); @@ -38,6 +39,7 @@ const scalarCplx = dataset('cplx', cplxType(floatType()), []); const scalarCompound = dataset('comp', compoundType({ int: intType() }), []); const oneDInt = dataset('int_1d', intType(), [5]); const oneDUint = dataset('uint_1d', intType(false), [5]); +const oneDBigUint = dataset('biguint_1d', intType(false, 64), [5]); const oneDBool = dataset('bool_1d', boolType(intType(true, 8)), [3]); const oneDCplx = dataset('cplx_1d', cplxType(floatType()), [10]); const oneDCompound = dataset('comp_1d', compoundType({ int: intType() }), [5]); @@ -82,6 +84,7 @@ describe('Scalar', () => { it('should support dataset with printable type and scalar shape', () => { expect(supportsDataset(scalarInt)).toBe(true); expect(supportsDataset(scalarUint)).toBe(true); + expect(supportsDataset(scalarBigInt)).toBe(true); expect(supportsDataset(scalarFloat)).toBe(true); expect(supportsDataset(scalarStr)).toBe(true); expect(supportsDataset(scalarBool)).toBe(true); @@ -103,6 +106,7 @@ describe('Matrix', () => { it('should support array dataset with printable type and at least one dimension', () => { expect(supportsDataset(oneDInt)).toBe(true); expect(supportsDataset(oneDUint)).toBe(true); + expect(supportsDataset(oneDBigUint)).toBe(true); expect(supportsDataset(twoDStr)).toBe(true); expect(supportsDataset(twoDCplx)).toBe(true); expect(supportsDataset(threeDFloat)).toBe(true); @@ -124,6 +128,7 @@ describe('Line', () => { it('should support array dataset with numeric-like type and at least one dimension', () => { expect(supportsDataset(oneDInt)).toBe(true); expect(supportsDataset(oneDUint)).toBe(true); + expect(supportsDataset(oneDBigUint)).toBe(true); expect(supportsDataset(oneDBool)).toBe(true); expect(supportsDataset(twoDBool)).toBe(true); expect(supportsDataset(threeDFloat)).toBe(true); diff --git a/packages/h5wasm/src/__snapshots__/h5wasm-api.test.ts.snap b/packages/h5wasm/src/__snapshots__/h5wasm-api.test.ts.snap index cd6283d03..c684584da 100644 --- a/packages/h5wasm/src/__snapshots__/h5wasm-api.test.ts.snap +++ b/packages/h5wasm/src/__snapshots__/h5wasm-api.test.ts.snap @@ -182,7 +182,7 @@ exports[`test file matches snapshot 1`] = ` "signed": true, "size": 64, }, - "value": -9223372036854776000, + "value": -9223372036854775808n, }, { "name": "int64_2D", @@ -204,14 +204,14 @@ exports[`test file matches snapshot 1`] = ` "signed": true, "size": 64, }, - "value": [ - 0, - 1, - 2, - 3, - 4, - 5, - ], + "value": BigInt64Array { + "0": 0n, + "1": 1n, + "2": 2n, + "3": 3n, + "4": 4n, + "5": 5n, + }, }, { "name": "uint8_scalar", @@ -374,7 +374,7 @@ exports[`test file matches snapshot 1`] = ` "signed": false, "size": 64, }, - "value": 18446744073709552000, + "value": 18446744073709551615n, }, { "name": "uint64_3D", @@ -397,16 +397,16 @@ exports[`test file matches snapshot 1`] = ` "signed": false, "size": 64, }, - "value": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 18446744073709552000, - ], + "value": BigUint64Array { + "0": 0n, + "1": 1n, + "2": 2n, + "3": 3n, + "4": 4n, + "5": 5n, + "6": 6n, + "7": 18446744073709551615n, + }, }, { "name": "float16_scalar", @@ -1434,7 +1434,7 @@ exports[`test file matches snapshot 1`] = ` }, }, "value": [ - 1, + 1n, 2, "foo", ], @@ -1512,17 +1512,17 @@ exports[`test file matches snapshot 1`] = ` }, "value": [ [ - 1, + 1n, NaN, "foo", ], [ - 2, + 2n, Infinity, "bar", ], [ - 3, + 3n, -0, "baz", ], @@ -1667,7 +1667,7 @@ exports[`test file matches snapshot 1`] = ` 1, 2, ], - 3, + 3n, ], ], }, @@ -1758,30 +1758,30 @@ exports[`test file matches snapshot 1`] = ` 0, 1, ], - [ - 0, - ], + BigUint64Array { + "0": 0n, + }, ], [ [ 2, 3, ], - [ - 0, - 1, - ], + BigUint64Array { + "0": 0n, + "1": 1n, + }, ], [ [ 4, 5, ], - [ - 0, - 1, - 2, - ], + BigUint64Array { + "0": 0n, + "1": 1n, + "2": 2n, + }, ], ], }, @@ -2225,18 +2225,18 @@ exports[`test file matches snapshot 1`] = ` "class": "Array (variable length)", }, "value": [ - [ - 0, - ], - [ - 0, - 1, - ], - [ - 0, - 1, - 2, - ], + BigInt64Array { + "0": 0n, + }, + BigInt64Array { + "0": 0n, + "1": 1n, + }, + BigInt64Array { + "0": 0n, + "1": 1n, + "2": 2n, + }, ], }, { diff --git a/packages/h5wasm/src/h5wasm-api.ts b/packages/h5wasm/src/h5wasm-api.ts index 94296355f..b1429080c 100644 --- a/packages/h5wasm/src/h5wasm-api.ts +++ b/packages/h5wasm/src/h5wasm-api.ts @@ -16,12 +16,7 @@ import { import { transfer } from 'comlink'; import { type Plugin } from './models'; -import { - getEnhancedError, - hasBigInts, - PLUGINS_BY_FILTER_ID, - sanitizeBigInts, -} from './utils'; +import { getEnhancedError, PLUGINS_BY_FILTER_ID } from './utils'; import { type H5WasmWorkerAPI } from './worker'; export class H5WasmApi extends DataProviderApi { @@ -48,8 +43,7 @@ export class H5WasmApi extends DataProviderApi { await this.processFilters(dataset); try { - const value = await this.remote.getValue(fileId, dataset.path, selection); - return hasBigInts(dataset.type) ? sanitizeBigInts(value) : value; + return await this.remote.getValue(fileId, dataset.path, selection); } catch (error) { throw getEnhancedError(error); } diff --git a/packages/h5wasm/src/utils.ts b/packages/h5wasm/src/utils.ts index 606aabc41..1bc73dba6 100644 --- a/packages/h5wasm/src/utils.ts +++ b/packages/h5wasm/src/utils.ts @@ -1,5 +1,3 @@ -import { type DType, DTypeClass } from '@h5web/shared/hdf5-models'; - import { type HDF5Diag, Plugin } from './models'; // https://support.hdfgroup.org/services/contributions.html @@ -15,38 +13,6 @@ export const PLUGINS_BY_FILTER_ID: Record = { 32_026: Plugin.Blosc2, }; -export function hasBigInts(type: DType): boolean { - if ( - type.class === DTypeClass.Enum || - type.class === DTypeClass.Array || - type.class === DTypeClass.VLen - ) { - return hasBigInts(type.base); - } - - if (type.class === DTypeClass.Compound) { - return Object.values(type.fields).some(hasBigInts); - } - - return type.class === DTypeClass.Integer && type.size === 64; -} - -export function sanitizeBigInts(value: unknown): unknown { - if (Array.isArray(value)) { - return value.map(sanitizeBigInts); - } - - if (value instanceof BigInt64Array || value instanceof BigUint64Array) { - return [...value].map(Number); - } - - if (typeof value === 'bigint') { - return Number(value); - } - - return value; -} - const DIAG_PREDICATES: ((diag: HDF5Diag) => boolean)[] = [ (diag: HDF5Diag) => { return ( diff --git a/packages/h5wasm/src/worker-utils.ts b/packages/h5wasm/src/worker-utils.ts index 647864e5b..903a66e63 100644 --- a/packages/h5wasm/src/worker-utils.ts +++ b/packages/h5wasm/src/worker-utils.ts @@ -222,6 +222,7 @@ function parseDType(metadata: Metadata): DType { littleEndian ? H5T_ORDER.LE : H5T_ORDER.BE, ); } + if (h5tClass === H5T_CLASS.FLOAT) { const { littleEndian } = metadata; return floatType(size * 8, littleEndian ? H5T_ORDER.LE : H5T_ORDER.BE); diff --git a/packages/shared/src/guards.test.ts b/packages/shared/src/guards.test.ts new file mode 100644 index 000000000..dc99a64fd --- /dev/null +++ b/packages/shared/src/guards.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, it } from 'vitest'; + +import { assertDatasetValue, assertScalarValue } from './guards'; +import { + boolType, + compoundType, + cplxType, + enumType, + floatType, + intType, + strType, +} from './hdf5-utils'; +import { dataset } from './mock-utils'; + +describe('assertScalarValue', () => { + it('should not throw when value satisfies dtype', () => { + expect(() => assertScalarValue(0, intType())).not.toThrow(); + expect(() => assertScalarValue(0, intType(false))).not.toThrow(); + expect(() => assertScalarValue(0, floatType())).not.toThrow(); + + expect(() => assertScalarValue(0, intType(true, 64))).not.toThrow(); + expect(() => assertScalarValue(0n, intType(true, 64))).not.toThrow(); + + expect(() => assertScalarValue(0, boolType(intType()))).not.toThrow(); + expect(() => assertScalarValue(false, boolType(intType()))).not.toThrow(); + + expect(() => + assertScalarValue(0, enumType(intType(), { FOO: 0 })), + ).not.toThrow(); + + expect(() => assertScalarValue('', strType())).not.toThrow(); + + expect(() => assertScalarValue([0, 0], cplxType(intType()))).not.toThrow(); + + expect(() => + assertScalarValue( + [0, ''], + compoundType({ + int: intType(), + str: strType(), + }), + ), + ).not.toThrow(); + }); + + it("should throw when value doesn't satisfies dtype", () => { + expect(() => assertScalarValue('', intType())).toThrow('Expected number'); + expect(() => assertScalarValue(0n, intType())).toThrow('Expected number'); + expect(() => assertScalarValue(true, intType())).toThrow('Expected number'); + expect(() => assertScalarValue([], intType())).toThrow('Expected number'); + expect(() => assertScalarValue(null, intType())).toThrow('Expected number'); + expect(() => assertScalarValue(undefined, intType())).toThrow( + 'Expected number', + ); + + expect(() => assertScalarValue(true, intType(true, 64))).toThrow( + 'Expected number or bigint', + ); + + expect(() => assertScalarValue('', boolType(intType()))).toThrow( + 'Expected number or boolean', + ); + + expect(() => + assertScalarValue('', enumType(intType(), { FOO: 0 })), + ).toThrow('Expected number'); + + expect(() => assertScalarValue(0, strType())).toThrow('Expected string'); + + expect(() => assertScalarValue(0, cplxType(floatType()))).toThrow( + 'Expected complex', + ); + expect(() => assertScalarValue([0], cplxType(floatType()))).toThrow( + 'Expected complex', + ); + expect(() => assertScalarValue([0, ''], cplxType(floatType()))).toThrow( + 'Expected complex', + ); + + expect(() => assertScalarValue(0, compoundType({}))).toThrow( + 'Expected array', + ); + expect(() => + assertScalarValue(0, compoundType({ foo: intType() })), + ).toThrow('Expected array'); + expect(() => + assertScalarValue([], compoundType({ foo: intType() })), + ).toThrow('Expected number'); + expect(() => + assertScalarValue([''], compoundType({ foo: intType() })), + ).toThrow('Expected number'); + }); +}); + +describe('assertDatasetValue', () => { + it('should not throw when value satisfies dataset type and shape', () => { + expect(() => + assertDatasetValue(0, dataset('foo', intType(), [])), + ).not.toThrow(); + + expect(() => + assertDatasetValue(0n, dataset('foo', intType(false, 64), [])), + ).not.toThrow(); + + expect(() => + assertDatasetValue('', dataset('foo', strType(), [])), + ).not.toThrow(); + + expect(() => + assertDatasetValue( + [true, false], + dataset('foo', boolType(intType()), [2]), + ), + ).not.toThrow(); + + expect(() => + assertDatasetValue( + Float32Array.from([0, 1]), + dataset('foo', floatType(), [2]), + ), + ).not.toThrow(); + + expect(() => + assertDatasetValue( + BigInt64Array.from([0n, 1n]), + dataset('foo', intType(true, 64), [2]), + ), + ).not.toThrow(); + + expect(() => + assertDatasetValue( + Float32Array.from([0, 1]), // big ints can be returned as any kind of numbers + dataset('foo', intType(true, 64), [2]), + ), + ).not.toThrow(); + }); + + describe('assertDatasetValue', () => { + it("should throw when value doesn't satisfy dataset type and shape", () => { + expect(() => + assertDatasetValue( + true, + dataset('foo', enumType(intType(), { FOO: 0 }), []), + ), + ).toThrow('Expected number'); + + expect(() => + assertDatasetValue(['foo', 'bar'], dataset('foo', intType(), [2])), + ).toThrow('Expected number'); + + expect(() => + assertDatasetValue( + BigInt64Array.from([0n, 1n]), + dataset('foo', intType(), [2]), + ), + ).toThrow('Expected number'); + }); + }); +}); diff --git a/packages/shared/src/guards.ts b/packages/shared/src/guards.ts index aaaf4aa97..1e6acea35 100644 --- a/packages/shared/src/guards.ts +++ b/packages/shared/src/guards.ts @@ -30,6 +30,7 @@ import { import { type AnyNumArray, type AxisScaleType, + type BigIntTypedArray, type ColorScaleType, type NumArray, } from './vis-models'; @@ -77,6 +78,12 @@ function assertNum(val: unknown): asserts val is number { } } +function assertNumOrBigInt(val: unknown): asserts val is number | bigint { + if (typeof val !== 'number' && typeof val !== 'bigint') { + throw new TypeError('Expected number or bigint'); + } +} + function assertNumOrBool(val: unknown): asserts val is number | boolean { if (typeof val !== 'number' && typeof val !== 'boolean') { throw new TypeError('Expected number or boolean'); @@ -133,6 +140,10 @@ export function isTypedArray(val: unknown): val is TypedArray { ); } +export function isBigIntTypeArray(val: unknown): val is BigIntTypedArray { + return val instanceof BigInt64Array || val instanceof BigUint64Array; +} + export function isGroup(entity: Entity): entity is Group { return entity.kind === EntityKind.Group; } @@ -412,11 +423,13 @@ export function isComplexValue( return type.class === DTypeClass.Complex; } -function assertScalarValue( - type: DType, +export function assertScalarValue( value: unknown, + type: DType, ): asserts value is ScalarValue { - if (isNumericType(type)) { + if (isIntegerType(type) && type.size === 64) { + assertNumOrBigInt(value); + } else if (isNumericType(type)) { assertNum(value); } else if (isBoolType(type)) { assertNumOrBool(value); @@ -429,7 +442,7 @@ function assertScalarValue( } else if (isCompoundType(type)) { assertArray(value); Object.values(type.fields).forEach((fieldType, index) => { - assertScalarValue(fieldType, value[index]); + assertScalarValue(value[index], fieldType); }); } } @@ -441,14 +454,18 @@ export function assertDatasetValue>( const { type } = dataset; if (hasScalarShape(dataset)) { - assertScalarValue(type, value); + assertScalarValue(value, type); } else { - if (!Array.isArray(value) && !isTypedArray(value)) { + if ( + !Array.isArray(value) && + !isTypedArray(value) && + !isBigIntTypeArray(value) + ) { throw new TypeError('Expected array or typed array'); } if (value.length > 0) { - assertScalarValue(type, value[0]); + assertScalarValue(value[0], type); } } } diff --git a/packages/shared/src/hdf5-models.ts b/packages/shared/src/hdf5-models.ts index 551c0b236..4e16a91b1 100644 --- a/packages/shared/src/hdf5-models.ts +++ b/packages/shared/src/hdf5-models.ts @@ -11,6 +11,7 @@ import { type H5T_TO_ENDIANNESS, type H5T_TO_STR_PAD, } from './h5t'; +import { type BigIntTypedArray } from './vis-models'; export enum EntityKind { Group = 'group', @@ -120,6 +121,7 @@ export type StrPad = (typeof H5T_TO_STR_PAD)[H5T_STR]; export type NumericType = IntegerType | FloatType; export type NumericLikeType = NumericType | BooleanType | EnumType; + export type PrintableType = StringType | NumericLikeType | ComplexType; export type DType = @@ -206,7 +208,10 @@ export interface UnknownType { /* ----- VALUE ----- */ export type ScalarValue = T extends NumericLikeType - ? number | (T extends BooleanType ? boolean : never) // let providers choose how to return booleans + ? + | number + | (T extends NumericType ? bigint : never) // let providers return bigints + | (T extends BooleanType ? boolean : never) // let providers return booleans : T extends StringType ? string : T extends ComplexType @@ -216,7 +221,11 @@ export type ScalarValue = T extends NumericLikeType : unknown; export type ArrayValue = T extends NumericLikeType - ? TypedArray | number[] | (T extends BooleanType ? boolean[] : never) // don't use `ScalarValue` to avoid `(number | boolean)[]` + ? + | TypedArray + | number[] + | (T extends NumericType ? BigIntTypedArray | bigint[] : never) + | (T extends BooleanType ? boolean[] : never) // don't use `ScalarValue` to avoid `(number | boolean)[]` : ScalarValue[]; export type Value = diff --git a/packages/shared/src/mock-utils.ts b/packages/shared/src/mock-utils.ts index 678486346..9e78cd965 100644 --- a/packages/shared/src/mock-utils.ts +++ b/packages/shared/src/mock-utils.ts @@ -297,6 +297,10 @@ function guessType(value: unknown): DType { return floatType(64); } + if (typeof value === 'bigint') { + return intType(true, 64); + } + if (typeof value === 'boolean') { return boolType(intType(true, 8)); } diff --git a/packages/shared/src/mock-values.ts b/packages/shared/src/mock-values.ts index 9a86732a9..50308b221 100644 --- a/packages/shared/src/mock-values.ts +++ b/packages/shared/src/mock-values.ts @@ -61,6 +61,16 @@ const twoD = () => { ); }; +const twoD_bigint = () => { + const { data: dataOneDBigInt } = oneD_bigint(); + return ndarray( + range5().flatMap((_, i) => + dataOneDBigInt.map((val) => val + 1n + BigInt(i) * 10n), + ), + [3, 10], + ); +}; + const twoD_bool = () => { const { data: dataOneDBool } = oneD_bool(); return ndarray( @@ -119,15 +129,7 @@ export const mockValues = { shapeTwoD, ); }, - twoD_bigint: () => { - const { data: dataOneDBigInt } = oneD_bigint(); - return ndarray( - range5().flatMap((_, i) => - dataOneDBigInt.map((val) => val + 1n + BigInt(i) * 10n), - ), - [3, 5], - ); - }, + twoD_bigint, twoD_cplx: () => ndarray( [ @@ -226,6 +228,7 @@ export const mockValues = { }, uint8: () => ndarray(Uint8Array.from(range7()), [2, 2]), int16: () => ndarray(Int16Array.from(range7()), [2, 2]), + int64: () => ndarray(BigInt64Array.from(range7(), BigInt), [2, 2]), float32: () => ndarray(Float32Array.from(range7()), [2, 2]), float64: () => ndarray(Float64Array.from(range7()), [2, 2]), int8_rgb: () => @@ -248,6 +251,7 @@ export const mockValues = { }, X_log: () => ndarray(range1().map((_, i) => (i + 1) * 0.1)), X_rgb: () => ndarray(range5()), + X_bigint: () => ndarray(range8().map(BigInt)), Y: () => ndarray(range2()), Y_desc: () => { const arr = range1(); @@ -268,6 +272,13 @@ export const mockValues = { ].flat(1), [2, 2], ), + secondary_bigint: () => { + const { data: dataTwoDBigInt, shape: shapeTwoDBigInt } = twoD_bigint(); + return ndarray( + dataTwoDBigInt.map((val) => val + 1n), + shapeTwoDBigInt, + ); + }, secondary_bool: () => { const { data: dataTwoDBool, shape: shapeTwoDBool } = twoD_bool(); return ndarray( diff --git a/packages/shared/src/vis-models.ts b/packages/shared/src/vis-models.ts index aa065a9de..597b80b8a 100644 --- a/packages/shared/src/vis-models.ts +++ b/packages/shared/src/vis-models.ts @@ -1,4 +1,9 @@ -import { type NdArray, type TypedArray } from 'ndarray'; +import { + type MaybeBigInt64Array, + type MaybeBigUint64Array, + type NdArray, + type TypedArray, +} from 'ndarray'; import { type DType, type ScalarValue } from './hdf5-models'; @@ -16,6 +21,8 @@ export type TypedArrayConstructor = | Float32ArrayConstructor | Float64ArrayConstructor; +export type BigIntTypedArray = MaybeBigInt64Array | MaybeBigUint64Array; + export type Domain = [min: number, max: number]; export type Axis = 'x' | 'y';