From f0cc5dafe85c51f2a82ab6f860cf37109219a5e1 Mon Sep 17 00:00:00 2001 From: Axel Bocciarelli Date: Thu, 3 Feb 2022 16:58:18 +0100 Subject: [PATCH] Let `MappedHeatmapVis` and related utilities accept typed arrays --- apps/storybook/src/Utilities.stories.mdx | 23 ++++++++++---- .../core/heatmap/MappedHeatmapVis.tsx | 3 +- packages/app/src/vis-packs/core/hooks.ts | 30 ++++++++++++------- packages/app/src/vis-packs/core/utils.ts | 19 +++++++----- packages/lib/src/vis/heatmap/HeatmapMesh.tsx | 6 ++-- packages/lib/src/vis/heatmap/HeatmapVis.tsx | 6 ++-- packages/lib/src/vis/heatmap/models.ts | 2 +- packages/lib/src/vis/heatmap/utils.ts | 6 ++-- packages/lib/src/vis/utils.ts | 11 ++++--- packages/shared/src/guards.ts | 21 ++++++++----- packages/shared/src/models-vis.ts | 5 ++++ packages/shared/src/utils.ts | 26 ++++++++-------- 12 files changed, 98 insertions(+), 60 deletions(-) diff --git a/apps/storybook/src/Utilities.stories.mdx b/apps/storybook/src/Utilities.stories.mdx index 815158641..2e91187ff 100644 --- a/apps/storybook/src/Utilities.stories.mdx +++ b/apps/storybook/src/Utilities.stories.mdx @@ -8,10 +8,18 @@ The library exposes a number of utility functions and custom React hooks, which #### getDomain -Find the min and max values contained in a array (or ndarray) of numbers. If `scaleType` is `ScaleType.Log` and the domain crosses zero, clamp the min to the first positive value or return `undefined` if the domain is not supported (i.e. `[-x, 0]`). +Find the min and max values contained in an array of numbers. The function supports ndarrays and typed arrays - e.g. `Uin16Array`, `NdArray`, `NdArray`, etc. + +- If `scaleType` is `ScaleType.Log` and the domain crosses zero, clamp the min to the first strictly positive value or return `undefined` if the domain is not supported (i.e. `[-x, 0]`). +- If `scaleType` is `ScaleType.Sqrt` and the domain crosses zero, clamp the min to the first positive value (including 0). +- If `errorArray` is provided, the returned domain accounts for errors. ```ts -getDomain(values: NdArray | number[], scaleType: ScaleType = ScaleType.Linear): Domain | undefined +getDomain( + valuesArray: AnyNumArray, // NdArray | TypedArray | number[] + scaleType: ScaleType = ScaleType.Linear, + errorArray?: AnyNumArray +): Domain | undefined const linearDomain = getDomain([10, 5, -1]); // [-1, 10] const logDomain = getDomain([-1, 2, 10], ScaleType.Log); // [2, 10] @@ -22,13 +30,16 @@ const myDomain = getDomain(myArray); // [0, 3] #### getDomains -Find the domains of multiple arrays (or ndarrays) of numbers. Useful when dealing with auxiliary curves. +Find the domains of multiple arrays of numbers. Useful when dealing with auxiliary curves. ```ts -getDomains(arrays: (NdArray | number[])[], scaleType: ScaleType = ScaleType.Linear): (Domain | undefined)[] +getDomains( + valuesArrays: AnyNumArray[], + scaleType: ScaleType = ScaleType.Linear +): (Domain | undefined)[] -const linearDomains = getDomains([[-1, 5, 10], myArray]); // [[-1, 10], [0, 3]] -const logDomains = getDomains([[-1, 5, 10], myArray], ScaleType.Log); // [[2, 10], [0, 3]] +const linearDomains = getDomains([[-1, 5, 10], ndarray([0, 1, 2, 3], [2, 2])]); // [[-1, 10], [0, 3]] +const logDomains = getDomains([[-1, 5, 10], [0, 1, 2, 3]], ScaleType.Log); // [[2, 10], [0, 3]] ``` #### getCombinedDomain diff --git a/packages/app/src/vis-packs/core/heatmap/MappedHeatmapVis.tsx b/packages/app/src/vis-packs/core/heatmap/MappedHeatmapVis.tsx index 25ccbec10..0b905990f 100644 --- a/packages/app/src/vis-packs/core/heatmap/MappedHeatmapVis.tsx +++ b/packages/app/src/vis-packs/core/heatmap/MappedHeatmapVis.tsx @@ -1,4 +1,5 @@ import { HeatmapVis } from '@h5web/lib'; +import type { TextureTypedArray } from '@h5web/lib/src/vis/heatmap/models'; import type { ArrayShape, Dataset, @@ -19,7 +20,7 @@ import { useSafeDomain, useVisDomain } from './hooks'; interface Props { dataset: Dataset; selection: string | undefined; - value: number[]; + value: number[] | TextureTypedArray; dims: number[]; dimMapping: DimensionMapping; axisMapping?: AxisMapping; diff --git a/packages/app/src/vis-packs/core/hooks.ts b/packages/app/src/vis-packs/core/hooks.ts index b8000f95c..2ae9989bd 100644 --- a/packages/app/src/vis-packs/core/hooks.ts +++ b/packages/app/src/vis-packs/core/hooks.ts @@ -1,5 +1,12 @@ import { getCombinedDomain } from '@h5web/lib'; -import type { ArrayShape, Dataset, ScalarShape, Value } from '@h5web/shared'; +import type { + AnyNumArray, + ArrayShape, + Dataset, + Domain, + ScalarShape, + Value, +} from '@h5web/shared'; import { isDefined, ScaleType, @@ -7,7 +14,7 @@ import { getValidDomainForScale, assertDatasetValue, } from '@h5web/shared'; -import type { NdArray } from 'ndarray'; +import type { NdArray, TypedArray } from 'ndarray'; import { useContext, useMemo } from 'react'; import { createMemo } from 'react-use'; @@ -73,18 +80,19 @@ const useBounds = createMemo(getBounds); const useValidDomainForScale = createMemo(getValidDomainForScale); export function useDomain( - valuesArray: NdArray | number[], + valuesArray: AnyNumArray, scaleType: ScaleType = ScaleType.Linear, - errorArray?: NdArray | number[] -) { + errorArray?: AnyNumArray +): Domain | undefined { + // Distinct memoized calls allows for bounds to not be recomputed when scale type changes const bounds = useBounds(valuesArray, errorArray); return useValidDomainForScale(bounds, scaleType); } export function useDomains( - valuesArrays: (NdArray | number[])[], + valuesArrays: AnyNumArray[], scaleType: ScaleType = ScaleType.Linear -) { +): (Domain | undefined)[] { const allBounds = useMemo(() => { return valuesArrays.map((arr) => getBounds(arr)); }, [valuesArrays]); @@ -100,15 +108,17 @@ export const useCombinedDomain = createMemo(getCombinedDomain); const useBaseArray = createMemo(getBaseArray); const useApplyMapping = createMemo(applyMapping); -export function useMappedArray( +export function useMappedArray( value: U, dims: number[], mapping: DimensionMapping, autoScale?: boolean -): U extends T[] ? [NdArray, NdArray] : [undefined, undefined]; +): U extends T[] | TypedArray + ? [NdArray, NdArray] + : [undefined, undefined]; export function useMappedArray( - value: T[] | undefined, + value: T[] | TypedArray | undefined, dims: number[], mapping: DimensionMapping, autoScale?: boolean diff --git a/packages/app/src/vis-packs/core/utils.ts b/packages/app/src/vis-packs/core/utils.ts index 97a652a2c..73fd7a089 100644 --- a/packages/app/src/vis-packs/core/utils.ts +++ b/packages/app/src/vis-packs/core/utils.ts @@ -1,7 +1,7 @@ import type { Domain } from '@h5web/shared'; import { createArrayFromView } from '@h5web/shared'; import { isNumber } from 'lodash'; -import type { NdArray } from 'ndarray'; +import type { NdArray, TypedArray } from 'ndarray'; import ndarray from 'ndarray'; import type { Axis, DimensionMapping } from '../../dimension-mapper/models'; @@ -9,27 +9,30 @@ import { isAxis } from '../../dimension-mapper/utils'; export const DEFAULT_DOMAIN: Domain = [0.1, 1]; -export function getBaseArray( +export function getBaseArray( value: U, rawDims: number[] -): U extends T[] ? NdArray : undefined; +): U extends T[] | TypedArray ? NdArray : undefined; export function getBaseArray( - value: T[] | undefined, + value: T[] | TypedArray | undefined, rawDims: number[] -): NdArray | undefined { +) { return value && ndarray(value, rawDims); } -export function applyMapping | undefined>( +export function applyMapping< + T, + U extends NdArray | undefined +>( baseArray: U, mapping: (number | Axis | ':')[] -): U extends NdArray ? U : undefined; +): U extends NdArray ? U : undefined; export function applyMapping( baseArray: NdArray | undefined, mapping: (number | Axis | ':')[] -): NdArray | undefined { +) { if (!baseArray) { return undefined; } diff --git a/packages/lib/src/vis/heatmap/HeatmapMesh.tsx b/packages/lib/src/vis/heatmap/HeatmapMesh.tsx index 53017606b..856f208a3 100644 --- a/packages/lib/src/vis/heatmap/HeatmapMesh.tsx +++ b/packages/lib/src/vis/heatmap/HeatmapMesh.tsx @@ -9,18 +9,18 @@ import { DataTexture, RGBFormat, UnsignedByteType } from 'three'; import { useAxisSystemContext } from '../..'; import type { VisScaleType } from '../models'; import VisMesh from '../shared/VisMesh'; -import type { ColorMap, CompatibleTypedArray, ScaleShader } from './models'; +import type { ColorMap, TextureTypedArray, ScaleShader } from './models'; import { getDataTexture, getInterpolator } from './utils'; interface Props { - values: NdArray; + values: NdArray; domain: Domain; scaleType: VisScaleType; colorMap: ColorMap; invertColorMap?: boolean; textureType?: TextureDataType; // override default texture type (determined from `values.dtype`) magFilter?: TextureFilter; - alphaValues?: NdArray; + alphaValues?: NdArray; alphaDomain?: Domain; } diff --git a/packages/lib/src/vis/heatmap/HeatmapVis.tsx b/packages/lib/src/vis/heatmap/HeatmapVis.tsx index 54d3d49ad..9dd75bc3e 100644 --- a/packages/lib/src/vis/heatmap/HeatmapVis.tsx +++ b/packages/lib/src/vis/heatmap/HeatmapVis.tsx @@ -20,14 +20,14 @@ import styles from './HeatmapVis.module.css'; import { useAxisValues } from './hooks'; import type { ColorMap, - CompatibleTypedArray, + TextureTypedArray, Layout, TooltipData, } from './models'; import { getDims } from './utils'; interface Props { - dataArray: NdArray; + dataArray: NdArray; domain: Domain | undefined; colorMap?: ColorMap; scaleType?: VisScaleType; @@ -38,7 +38,7 @@ interface Props { invertColorMap?: boolean; abscissaParams?: AxisParams; ordinateParams?: AxisParams; - alpha?: { array: NdArray; domain: Domain }; + alpha?: { array: NdArray; domain: Domain }; flipYAxis?: boolean; renderTooltip?: (data: TooltipData) => ReactElement; children?: ReactNode; diff --git a/packages/lib/src/vis/heatmap/models.ts b/packages/lib/src/vis/heatmap/models.ts index f75a10a6b..5131e3505 100644 --- a/packages/lib/src/vis/heatmap/models.ts +++ b/packages/lib/src/vis/heatmap/models.ts @@ -31,7 +31,7 @@ export interface ScaleShader { fragment: string; } -export type CompatibleTypedArray = Exclude< +export type TextureTypedArray = Exclude< TypedArray, Uint8ClampedArray | Float64Array >; diff --git a/packages/lib/src/vis/heatmap/utils.ts b/packages/lib/src/vis/heatmap/utils.ts index 2e15cd60a..65dbdb6a3 100644 --- a/packages/lib/src/vis/heatmap/utils.ts +++ b/packages/lib/src/vis/heatmap/utils.ts @@ -28,7 +28,7 @@ import { H5WEB_SCALES } from '../scales'; import { INTERPOLATORS } from './interpolators'; import type { ColorMap, - CompatibleTypedArray, + TextureTypedArray, D3Interpolator, Dims, } from './models'; @@ -41,7 +41,7 @@ export const GRADIENT_RANGE = range( ); export const TEXTURE_TYPE_BY_DTYPE: Record< - DataType, + DataType, TextureDataType > = { int8: ByteType, @@ -169,7 +169,7 @@ function getTextureFormatFromType(type: TextureDataType): PixelFormat { } export function getDataTexture( - values: NdArray, + values: NdArray, textureType = TEXTURE_TYPE_BY_DTYPE[values.dtype], magFilter = NearestFilter ): DataTexture { diff --git a/packages/lib/src/vis/utils.ts b/packages/lib/src/vis/utils.ts index d7a72a73c..bedbda1b3 100644 --- a/packages/lib/src/vis/utils.ts +++ b/packages/lib/src/vis/utils.ts @@ -1,4 +1,4 @@ -import type { Domain, NumericType } from '@h5web/shared'; +import type { AnyNumArray, Domain, NumericType } from '@h5web/shared'; import { getValidDomainForScale, ScaleType, @@ -11,7 +11,6 @@ import { import { scaleLinear, scaleThreshold } from '@visx/scale'; import { tickStep, range } from 'd3-array'; import type { ScaleLinear, ScaleThreshold } from 'd3-scale'; -import type { NdArray } from 'ndarray'; import { Vector3, Matrix4 } from 'three'; import { clamp } from 'three/src/math/MathUtils'; @@ -77,9 +76,9 @@ export function getSizeToFit( } export function getDomain( - valuesArray: NdArray | number[], + valuesArray: AnyNumArray, scaleType: ScaleType = ScaleType.Linear, - errorArray?: NdArray | number[] + errorArray?: AnyNumArray ): Domain | undefined { const bounds = getBounds(valuesArray, errorArray); @@ -87,10 +86,10 @@ export function getDomain( } export function getDomains( - arrays: (NdArray | number[])[], + valuesArrays: AnyNumArray[], scaleType: ScaleType = ScaleType.Linear ): (Domain | undefined)[] { - return arrays.map((arr) => getDomain(arr, scaleType)); + return valuesArrays.map((arr) => getDomain(arr, scaleType)); } function extendEmptyDomain( diff --git a/packages/shared/src/guards.ts b/packages/shared/src/guards.ts index d8572f20d..161603f49 100644 --- a/packages/shared/src/guards.ts +++ b/packages/shared/src/guards.ts @@ -1,5 +1,5 @@ import { isTypedArray } from 'lodash'; -import type { NdArray, TypedArray } from 'ndarray'; +import type { Data, NdArray } from 'ndarray'; import type { ReactChild, ReactElement } from 'react'; import { EntityKind, DTypeClass } from './models-hdf5'; @@ -23,8 +23,9 @@ import type { Primitive, Value, } from './models-hdf5'; +import type { AnyNumArray, NumArray } from './models-vis'; import { ScaleType } from './models-vis'; -import { toArray } from './utils'; +import { getValues } from './utils'; const PRINTABLE_DTYPES = new Set([ DTypeClass.Unsigned, @@ -329,16 +330,16 @@ export function assertDatasetValue>( } export function assertDataLength( - arr: NdArray | number[] | undefined, - dataArray: NdArray | number[], + arr: AnyNumArray | undefined, + dataArray: AnyNumArray, arrName: string ) { if (!arr) { return; } - const { length: arrLength } = toArray(arr); - const { length: dataLength } = toArray(dataArray); + const { length: arrLength } = getValues(arr); + const { length: dataLength } = getValues(dataArray); if (arrLength !== dataLength) { throw new Error( @@ -353,7 +354,13 @@ export function isScaleType(val: unknown): val is ScaleType { ); } -export function isTypedNdArray( +export function isNdArray( + arr: NdArray | T +): arr is NdArray { + return 'data' in arr; +} + +export function isTypedNdArray( arr: NdArray ): arr is NdArray> { return isTypedArray(arr.data); diff --git a/packages/shared/src/models-vis.ts b/packages/shared/src/models-vis.ts index ea387d0c4..53d613b48 100644 --- a/packages/shared/src/models-vis.ts +++ b/packages/shared/src/models-vis.ts @@ -1,3 +1,8 @@ +import type { NdArray, TypedArray } from 'ndarray'; + +export type NumArray = TypedArray | number[]; +export type AnyNumArray = NdArray | NumArray; + export type Domain = [number, number]; export enum ScaleType { diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index edeb5a48c..3966f20ea 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -1,12 +1,12 @@ import { format } from 'd3-format'; import ndarray from 'ndarray'; -import type { NdArray, TypedArray } from 'ndarray'; +import type { NdArray } from 'ndarray'; import { assign } from 'ndarray-ops'; -import { assertDataLength, isTypedNdArray } from './guards'; +import { assertDataLength, isNdArray, isTypedNdArray } from './guards'; import type { Entity, GroupWithChildren, H5WebComplex } from './models-hdf5'; import { ScaleType } from './models-vis'; -import type { Bounds, Domain } from './models-vis'; +import type { Bounds, Domain, AnyNumArray, NumArray } from './models-vis'; export const formatTick = format('.5~g'); export const formatBound = format('.3~e'); @@ -36,11 +36,11 @@ function createComplexFormatter(specifier: string, full = false) { }; } -export function toArray(arr: NdArray | number[]): number[] { - return 'data' in arr ? arr.data : arr; +export function getValues(arr: AnyNumArray): NumArray { + return isNdArray(arr) ? arr.data : arr; } -export function toTypedNdArray( +export function toTypedNdArray( arr: NdArray ): NdArray | Float32Array> { if (isTypedNdArray(arr)) { @@ -89,16 +89,18 @@ export function getNewBounds(oldBounds: Bounds, value: number): Bounds { } export function getBounds( - valuesArray: NdArray | number[], - errorArray?: NdArray | number[] + valuesArray: AnyNumArray, + errorArray?: AnyNumArray ): Bounds | undefined { assertDataLength(errorArray, valuesArray, 'error'); - const values = toArray(valuesArray); - const errors = errorArray && toArray(errorArray); + const values = getValues(valuesArray); + const errors = errorArray && getValues(errorArray); - const bounds = values.reduce( - (acc, val, i) => { + // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error + // @ts-ignore (https://github.com/microsoft/TypeScript/issues/44593) + const bounds = values.reduce( + (acc: Bounds, val: number, i: number) => { // Ignore NaN and Infinity from the bounds computation if (!Number.isFinite(val)) { return acc;