Skip to content

Commit

Permalink
Let MappedHeatmapVis and related utilities accept typed arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboc committed Feb 4, 2022
1 parent a37eb7b commit f0cc5da
Show file tree
Hide file tree
Showing 12 changed files with 98 additions and 60 deletions.
23 changes: 17 additions & 6 deletions apps/storybook/src/Utilities.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Float32Array>`, `NdArray<number>`, 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[]> | 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]
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/vis-packs/core/heatmap/MappedHeatmapVis.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HeatmapVis } from '@h5web/lib';
import type { TextureTypedArray } from '@h5web/lib/src/vis/heatmap/models';
import type {
ArrayShape,
Dataset,
Expand All @@ -19,7 +20,7 @@ import { useSafeDomain, useVisDomain } from './hooks';
interface Props {
dataset: Dataset<ArrayShape, NumericType>;
selection: string | undefined;
value: number[];
value: number[] | TextureTypedArray;
dims: number[];
dimMapping: DimensionMapping;
axisMapping?: AxisMapping;
Expand Down
30 changes: 20 additions & 10 deletions packages/app/src/vis-packs/core/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
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,
getBounds,
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';

Expand Down Expand Up @@ -73,18 +80,19 @@ const useBounds = createMemo(getBounds);
const useValidDomainForScale = createMemo(getValidDomainForScale);

export function useDomain(
valuesArray: NdArray<number[]> | number[],
valuesArray: AnyNumArray,
scaleType: ScaleType = ScaleType.Linear,
errorArray?: NdArray<number[]> | 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[]> | number[])[],
valuesArrays: AnyNumArray[],
scaleType: ScaleType = ScaleType.Linear
) {
): (Domain | undefined)[] {
const allBounds = useMemo(() => {
return valuesArrays.map((arr) => getBounds(arr));
}, [valuesArrays]);
Expand All @@ -100,15 +108,17 @@ export const useCombinedDomain = createMemo(getCombinedDomain);
const useBaseArray = createMemo(getBaseArray);
const useApplyMapping = createMemo(applyMapping);

export function useMappedArray<T, U extends T[] | undefined>(
export function useMappedArray<T, U extends T[] | TypedArray | undefined>(
value: U,
dims: number[],
mapping: DimensionMapping,
autoScale?: boolean
): U extends T[] ? [NdArray<U>, NdArray<U>] : [undefined, undefined];
): U extends T[] | TypedArray
? [NdArray<U>, NdArray<U>]
: [undefined, undefined];

export function useMappedArray<T>(
value: T[] | undefined,
value: T[] | TypedArray | undefined,
dims: number[],
mapping: DimensionMapping,
autoScale?: boolean
Expand Down
19 changes: 11 additions & 8 deletions packages/app/src/vis-packs/core/utils.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
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';
import { isAxis } from '../../dimension-mapper/utils';

export const DEFAULT_DOMAIN: Domain = [0.1, 1];

export function getBaseArray<T, U extends T[] | undefined>(
export function getBaseArray<T, U extends T[] | TypedArray | undefined>(
value: U,
rawDims: number[]
): U extends T[] ? NdArray<U> : undefined;
): U extends T[] | TypedArray ? NdArray<U> : undefined;

export function getBaseArray<T>(
value: T[] | undefined,
value: T[] | TypedArray | undefined,
rawDims: number[]
): NdArray<T[]> | undefined {
) {
return value && ndarray(value, rawDims);
}

export function applyMapping<T, U extends NdArray<T[]> | undefined>(
export function applyMapping<
T,
U extends NdArray<T[] | TypedArray> | undefined
>(
baseArray: U,
mapping: (number | Axis | ':')[]
): U extends NdArray<T[]> ? U : undefined;
): U extends NdArray<T[] | TypedArray> ? U : undefined;

export function applyMapping<T>(
baseArray: NdArray<T[]> | undefined,
mapping: (number | Axis | ':')[]
): NdArray<T[]> | undefined {
) {
if (!baseArray) {
return undefined;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/lib/src/vis/heatmap/HeatmapMesh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<CompatibleTypedArray>;
values: NdArray<TextureTypedArray>;
domain: Domain;
scaleType: VisScaleType;
colorMap: ColorMap;
invertColorMap?: boolean;
textureType?: TextureDataType; // override default texture type (determined from `values.dtype`)
magFilter?: TextureFilter;
alphaValues?: NdArray<CompatibleTypedArray>;
alphaValues?: NdArray<TextureTypedArray>;
alphaDomain?: Domain;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/lib/src/vis/heatmap/HeatmapVis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<number[] | CompatibleTypedArray>;
dataArray: NdArray<number[] | TextureTypedArray>;
domain: Domain | undefined;
colorMap?: ColorMap;
scaleType?: VisScaleType;
Expand All @@ -38,7 +38,7 @@ interface Props {
invertColorMap?: boolean;
abscissaParams?: AxisParams;
ordinateParams?: AxisParams;
alpha?: { array: NdArray<number[] | CompatibleTypedArray>; domain: Domain };
alpha?: { array: NdArray<number[] | TextureTypedArray>; domain: Domain };
flipYAxis?: boolean;
renderTooltip?: (data: TooltipData) => ReactElement;
children?: ReactNode;
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/src/vis/heatmap/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface ScaleShader {
fragment: string;
}

export type CompatibleTypedArray = Exclude<
export type TextureTypedArray = Exclude<
TypedArray,
Uint8ClampedArray | Float64Array
>;
6 changes: 3 additions & 3 deletions packages/lib/src/vis/heatmap/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { H5WEB_SCALES } from '../scales';
import { INTERPOLATORS } from './interpolators';
import type {
ColorMap,
CompatibleTypedArray,
TextureTypedArray,
D3Interpolator,
Dims,
} from './models';
Expand All @@ -41,7 +41,7 @@ export const GRADIENT_RANGE = range(
);

export const TEXTURE_TYPE_BY_DTYPE: Record<
DataType<CompatibleTypedArray>,
DataType<TextureTypedArray>,
TextureDataType
> = {
int8: ByteType,
Expand Down Expand Up @@ -169,7 +169,7 @@ function getTextureFormatFromType(type: TextureDataType): PixelFormat {
}

export function getDataTexture(
values: NdArray<CompatibleTypedArray>,
values: NdArray<TextureTypedArray>,
textureType = TEXTURE_TYPE_BY_DTYPE[values.dtype],
magFilter = NearestFilter
): DataTexture {
Expand Down
11 changes: 5 additions & 6 deletions packages/lib/src/vis/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Domain, NumericType } from '@h5web/shared';
import type { AnyNumArray, Domain, NumericType } from '@h5web/shared';
import {
getValidDomainForScale,
ScaleType,
Expand All @@ -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';

Expand Down Expand Up @@ -77,20 +76,20 @@ export function getSizeToFit(
}

export function getDomain(
valuesArray: NdArray<number[]> | number[],
valuesArray: AnyNumArray,
scaleType: ScaleType = ScaleType.Linear,
errorArray?: NdArray<number[]> | number[]
errorArray?: AnyNumArray
): Domain | undefined {
const bounds = getBounds(valuesArray, errorArray);

return getValidDomainForScale(bounds, scaleType);
}

export function getDomains(
arrays: (NdArray<number[]> | 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(
Expand Down
21 changes: 14 additions & 7 deletions packages/shared/src/guards.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -329,16 +330,16 @@ export function assertDatasetValue<D extends Dataset<ScalarShape | ArrayShape>>(
}

export function assertDataLength(
arr: NdArray<number[]> | number[] | undefined,
dataArray: NdArray<number[]> | 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(
Expand All @@ -353,7 +354,13 @@ export function isScaleType(val: unknown): val is ScaleType {
);
}

export function isTypedNdArray<T extends number[] | TypedArray>(
export function isNdArray<T extends Data>(
arr: NdArray<T> | T
): arr is NdArray<T> {
return 'data' in arr;
}

export function isTypedNdArray<T extends NumArray>(
arr: NdArray<T>
): arr is NdArray<Exclude<T, number[]>> {
return isTypedArray(arr.data);
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/models-vis.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import type { NdArray, TypedArray } from 'ndarray';

export type NumArray = TypedArray | number[];
export type AnyNumArray = NdArray<NumArray> | NumArray;

export type Domain = [number, number];

export enum ScaleType {
Expand Down
Loading

0 comments on commit f0cc5da

Please sign in to comment.