Skip to content

Commit 32ab30a

Browse files
authored
Update ZarrPixelSource compatability (#258)
1 parent d1d36ec commit 32ab30a

File tree

6 files changed

+51
-35
lines changed

6 files changed

+51
-35
lines changed

src/ZarrPixelSource.ts

+42-23
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,59 @@ type Slice = ReturnType<typeof zarr.slice>;
99
const X_AXIS_NAME = "x";
1010
const Y_AXIS_NAME = "y";
1111
const RGBA_CHANNEL_AXIS_NAME = "_c";
12-
const SUPPORTED_DTYPES = ["Uint8", "Uint16", "Uint32", "Float32", "Int8", "Int16", "Int32", "Float64"] as const;
1312

14-
export class ZarrPixelSource<S extends Array<string> = Array<string>> implements viv.PixelSource<S> {
15-
#arr: zarr.Array<zarr.NumberDataType, zarr.Readable>;
16-
readonly labels: viv.Labels<S>;
13+
type VivPixelData = {
14+
data: zarr.TypedArray<Lowercase<viv.SupportedDtype>>;
15+
width: number;
16+
height: number;
17+
};
18+
19+
export class ZarrPixelSource implements viv.PixelSource<Array<string>> {
20+
readonly labels: viv.Labels<Array<string>>;
1721
readonly tileSize: number;
1822
readonly dtype: viv.SupportedDtype;
23+
readonly #arr: zarr.Array<zarr.NumberDataType | zarr.BigintDataType, zarr.Readable>;
24+
readonly #transform: (
25+
arr: zarr.TypedArray<zarr.NumberDataType | zarr.BigintDataType>,
26+
) => zarr.TypedArray<Lowercase<viv.SupportedDtype>>;
1927

2028
#pendingId: undefined | number = undefined;
2129
#pending: Array<{
22-
resolve: (data: viv.PixelData) => void;
30+
resolve: (data: VivPixelData) => void;
2331
reject: (err: unknown) => void;
2432
request: {
2533
selection: Array<number | zarr.Slice>;
2634
signal?: AbortSignal;
2735
};
2836
}> = [];
2937

30-
constructor(arr: zarr.Array<zarr.DataType, zarr.Readable>, options: { labels: viv.Labels<S>; tileSize: number }) {
31-
const dtype = capitalize(arr.dtype);
32-
assert(arr.is("number") && isSupportedDtype(dtype), `Unsupported viv dtype: ${dtype}`);
33-
this.dtype = dtype;
38+
constructor(
39+
arr: zarr.Array<zarr.DataType, zarr.Readable>,
40+
options: { labels: viv.Labels<Array<string>>; tileSize: number },
41+
) {
42+
assert(arr.is("number") || arr.is("bigint"), `Unsupported viv dtype: ${arr.dtype}`);
3443
this.#arr = arr;
3544
this.labels = options.labels;
3645
this.tileSize = options.tileSize;
46+
/**
47+
* Some `zarrita` data types are not supported by Viv and require casting.
48+
*
49+
* Note how the casted type in the transform function is type-cast to `zarr.TypedArray<typeof arr.dtype>`.
50+
* This ensures that the function body is correct based on whatever type narrowing we do in the if/else
51+
* blocks based on dtype.
52+
*
53+
* TODO: Maybe we should add a console warning?
54+
*/
55+
if (arr.dtype === "uint64" || arr.dtype === "int64") {
56+
this.dtype = "Uint32";
57+
this.#transform = (x) => Uint32Array.from(x as zarr.TypedArray<typeof arr.dtype>, (bint) => Number(bint));
58+
} else if (arr.dtype === "float16") {
59+
this.dtype = "Float32";
60+
this.#transform = (x) => new Float32Array(x as zarr.TypedArray<typeof arr.dtype>);
61+
} else {
62+
this.dtype = capitalize(arr.dtype);
63+
this.#transform = (x) => x as zarr.TypedArray<typeof arr.dtype>;
64+
}
3765
}
3866

3967
get #width() {
@@ -51,7 +79,7 @@ export class ZarrPixelSource<S extends Array<string> = Array<string>> implements
5179
}
5280

5381
async getRaster(options: {
54-
selection: viv.PixelSourceSelection<S> | Array<number>;
82+
selection: viv.PixelSourceSelection<Array<string>> | Array<number>;
5583
signal?: AbortSignal;
5684
}): Promise<viv.PixelData> {
5785
const { selection, signal } = options;
@@ -71,7 +99,7 @@ export class ZarrPixelSource<S extends Array<string> = Array<string>> implements
7199
async getTile(options: {
72100
x: number;
73101
y: number;
74-
selection: viv.PixelSourceSelection<S> | Array<number>;
102+
selection: viv.PixelSourceSelection<Array<string>> | Array<number>;
75103
signal?: AbortSignal;
76104
}): Promise<viv.PixelData> {
77105
const { x, y, selection, signal } = options;
@@ -88,9 +116,10 @@ export class ZarrPixelSource<S extends Array<string> = Array<string>> implements
88116
}
89117

90118
async #fetchData(request: { selection: Array<number | Slice>; signal?: AbortSignal }): Promise<viv.PixelData> {
91-
const { promise, resolve, reject } = Promise.withResolvers<viv.PixelData>();
119+
const { promise, resolve, reject } = Promise.withResolvers<VivPixelData>();
92120
this.#pending.push({ request, resolve, reject });
93121
this.#pendingId = this.#pendingId ?? requestAnimationFrame(() => this.#fetchPending());
122+
// @ts-expect-error - The missing generic ArrayBuffer type from Viv makes VivPixelData and viv.PixelData incompatible, even though they are.
94123
return promise;
95124
}
96125

@@ -104,13 +133,8 @@ export class ZarrPixelSource<S extends Array<string> = Array<string>> implements
104133
zarr
105134
.get(this.#arr, request.selection, { opts: { signal: request.signal } })
106135
.then(({ data, shape }) => {
107-
if (data instanceof BigInt64Array || data instanceof BigUint64Array) {
108-
// We need to cast data these typed arrays to something that is viv compatible.
109-
// See the comment in the constructor for more information.
110-
data = Uint32Array.from(data, (bint) => Number(bint));
111-
}
112136
resolve({
113-
data: data as viv.SupportedTypedArray,
137+
data: this.#transform(data),
114138
width: shape[1],
115139
height: shape[0],
116140
});
@@ -154,8 +178,3 @@ function capitalize<T extends string>(s: T): Capitalize<T> {
154178
// @ts-expect-error - TypeScript can't verify that the return type is correct
155179
return s[0].toUpperCase() + s.slice(1);
156180
}
157-
158-
function isSupportedDtype(dtype: string): dtype is viv.SupportedDtype {
159-
// @ts-expect-error - TypeScript can't verify that the return type is correct
160-
return SUPPORTED_DTYPES.includes(dtype);
161-
}

src/gridLayer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { BaseLayerProps } from "./state";
99
import { assert } from "./utils";
1010

1111
export interface GridLoader {
12-
loader: ZarrPixelSource<string[]>;
12+
loader: ZarrPixelSource;
1313
row: number;
1414
col: number;
1515
name: string;

src/io.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ import { loadOmeroMultiscales, loadPlate, loadWell } from "./ome";
77
import type { ImageLayerConfig, LayerState, MultichannelConfig, SingleChannelConfig, SourceData } from "./state";
88
import * as utils from "./utils";
99

10-
async function loadSingleChannel(
11-
config: SingleChannelConfig,
12-
data: Array<ZarrPixelSource<string[]>>,
13-
): Promise<SourceData> {
10+
async function loadSingleChannel(config: SingleChannelConfig, data: Array<ZarrPixelSource>): Promise<SourceData> {
1411
const { color, contrast_limits, visibility, name, colormap = "", opacity = 1 } = config;
1512
const lowres = data[data.length - 1];
1613
const selection = Array(data[0].shape.length).fill(0);
@@ -35,7 +32,7 @@ async function loadSingleChannel(
3532

3633
async function loadMultiChannel(
3734
config: MultichannelConfig,
38-
data: ZarrPixelSource<string[]>[],
35+
data: Array<ZarrPixelSource>,
3936
channelAxis: number,
4037
): Promise<SourceData> {
4138
const { names, contrast_limits, name, model_matrix, opacity = 1, colormap = "" } = config;

src/ome.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ type Meta = {
279279
defaultSelection: Array<number>;
280280
};
281281

282-
async function defaultMeta(loader: ZarrPixelSource<string[]>, axis_labels: string[]): Promise<Meta> {
282+
async function defaultMeta(loader: ZarrPixelSource, axis_labels: string[]): Promise<Meta> {
283283
const channel_axis = axis_labels.indexOf("c");
284284
const channel_count = channel_axis === -1 ? 1 : loader.shape[channel_axis];
285285
const visibilities = utils.getDefaultVisibilities(channel_count);

src/state.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,17 @@ export interface BaseLayerProps {
8484
}
8585

8686
interface MultiscaleImageLayerProps extends BaseLayerProps {
87-
loader: ZarrPixelSource<string[]>[];
87+
loader: Array<ZarrPixelSource>;
8888
}
8989

9090
interface ImageLayerProps extends BaseLayerProps {
91-
loader: ZarrPixelSource<string[]>;
91+
loader: ZarrPixelSource;
9292
}
9393

9494
type LayerMap = {
9595
image: [typeof ImageLayer, ImageLayerProps];
9696
multiscale: [typeof MultiscaleImageLayer, MultiscaleImageLayerProps];
97-
grid: [GridLayer, { loader: ZarrPixelSource<string[]> | ZarrPixelSource<string[]>[] } & GridLayerProps];
97+
grid: [GridLayer, { loader: ZarrPixelSource | Array<ZarrPixelSource> } & GridLayerProps];
9898
};
9999

100100
// biome-ignore lint/suspicious/noExplicitAny: Need a catch all for layer types

src/utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,8 @@ export async function calcDataRange(
290290
return [minVal, maxVal];
291291
}
292292

293-
export async function calcConstrastLimits<S extends string[]>(
294-
source: ZarrPixelSource<S>,
293+
export async function calcConstrastLimits(
294+
source: ZarrPixelSource,
295295
channelAxis: number,
296296
visibilities: boolean[],
297297
defaultSelection?: number[],

0 commit comments

Comments
 (0)