@biongff/vizarr 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/dist/biongff-vizarr.cjs.js +1 -0
- package/dist/biongff-vizarr.es.js +9 -0
- package/dist/blosc-CeItQ6qj.cjs +17 -0
- package/dist/blosc-DaK8KnI4.js +719 -0
- package/dist/chunk-INHXZS53-DiyuLb3Z.js +14 -0
- package/dist/chunk-INHXZS53-z3BpFH8p.cjs +1 -0
- package/dist/deflate-DzrO3E-1.cjs +1 -0
- package/dist/deflate-Q4iCMMBa.js +10 -0
- package/dist/index-3CiumG5V.js +53876 -0
- package/dist/index-Ckhcz4QQ.cjs +3815 -0
- package/dist/index.d.ts +119 -0
- package/dist/jpeg-BdEIe3QG.js +533 -0
- package/dist/jpeg-DFzBPcur.cjs +1 -0
- package/dist/jpeg2k-1tnzEScI.js +24 -0
- package/dist/jpeg2k-CcF6vCxI.cjs +1 -0
- package/dist/lerc-35FtXNGP.cjs +1 -0
- package/dist/lerc-B6qlOBE2.js +1039 -0
- package/dist/lz4-CssV0LoA.js +643 -0
- package/dist/lz4-PFaIsPAh.cjs +15 -0
- package/dist/lzw-CeXp5T9s.js +84 -0
- package/dist/lzw-DnHvgKbO.cjs +1 -0
- package/dist/packbits-CAZoMCQq.js +24 -0
- package/dist/packbits-DwdUAwAt.cjs +1 -0
- package/dist/pako.esm-Bx5X36Wo.js +1074 -0
- package/dist/pako.esm-DZC2QrbJ.cjs +1 -0
- package/dist/raw-BB450wdT.cjs +1 -0
- package/dist/raw-oTi55tWo.js +9 -0
- package/dist/ref-Bd0nPTvp.js +196 -0
- package/dist/ref-CIxavIn6.cjs +1 -0
- package/dist/webimage-66a9vFf5.cjs +1 -0
- package/dist/webimage-CKRNzqyG.js +19 -0
- package/dist/zstd-Cqadn9HA.js +610 -0
- package/dist/zstd-_xUhkGOV.cjs +15 -0
- package/package.json +35 -21
- package/.gitattributes +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -32
- package/.github/ISSUE_TEMPLATE/documentation_update.md +0 -16
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/.github/ISSUE_TEMPLATE/maintenance.md +0 -19
- package/.github/ISSUE_TEMPLATE/optimisation_request.md +0 -23
- package/.github/ISSUE_TEMPLATE/support_request.md +0 -25
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -21
- package/.github/dependabot.yml +0 -13
- package/.github/workflows/deploy.yml +0 -56
- package/.github/workflows/npm-publish-dev.yml +0 -49
- package/.github/workflows/npm-publish.yml +0 -42
- package/.github/workflows/release.yml +0 -37
- package/.github/workflows/test.yml +0 -21
- package/.releaserc.json +0 -29
- package/CHANGELOG.md +0 -70
- package/CITATION.cff +0 -56
- package/CONTRIBUTING.md +0 -82
- package/README.md +0 -85
- package/assets/favicon.ico +0 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-wide.png +0 -0
- package/assets/logo-wide.svg +0 -60
- package/assets/logo.png +0 -0
- package/assets/logo.svg +0 -36
- package/assets/robots.txt +0 -3
- package/assets/screenshot.png +0 -0
- package/biome.json +0 -34
- package/index.html +0 -24
- package/main.ts +0 -52
- package/patches/@vivjs__loaders@0.17.2.patch +0 -13
- package/python/.python-version +0 -1
- package/python/README.md +0 -14
- package/python/Untitled.ipynb +0 -40
- package/python/deno.json +0 -22
- package/python/notebooks/IDR_example.ipynb +0 -86
- package/python/notebooks/README.md +0 -35
- package/python/notebooks/create_fixture.py +0 -72
- package/python/notebooks/getting_started.ipynb +0 -217
- package/python/notebooks/mandelbrot.ipynb +0 -211
- package/python/notebooks/spatial_transformations.ipynb +0 -139
- package/python/pyproject.toml +0 -18
- package/python/src/vizarr/__init__.py +0 -10
- package/python/src/vizarr/_widget.js +0 -113
- package/python/src/vizarr/_widget.py +0 -67
- package/python/uv.lock +0 -2495
- package/src/ZarrPixelSource.ts +0 -182
- package/src/codecs/jpeg2k.ts +0 -22
- package/src/codecs/register.ts +0 -2
- package/src/components/LayerController/AcquisitionController.tsx +0 -46
- package/src/components/LayerController/AddChannelButton.tsx +0 -122
- package/src/components/LayerController/AxisOptions.tsx +0 -93
- package/src/components/LayerController/AxisSlider.tsx +0 -86
- package/src/components/LayerController/AxisSliders.tsx +0 -29
- package/src/components/LayerController/ChannelController.tsx +0 -84
- package/src/components/LayerController/ChannelOptions.tsx +0 -183
- package/src/components/LayerController/ColorPalette.tsx +0 -40
- package/src/components/LayerController/Content.tsx +0 -71
- package/src/components/LayerController/Header.tsx +0 -48
- package/src/components/LayerController/Labels.tsx +0 -73
- package/src/components/LayerController/LayerFitToViewportButton.tsx +0 -43
- package/src/components/LayerController/LayerVisibilityButton.tsx +0 -32
- package/src/components/LayerController/OpacitySlider.tsx +0 -25
- package/src/components/LayerController/index.tsx +0 -42
- package/src/components/Menu.tsx +0 -54
- package/src/components/Viewer.tsx +0 -136
- package/src/hooks.ts +0 -29
- package/src/index.tsx +0 -138
- package/src/io.ts +0 -276
- package/src/layers/grid-layer.ts +0 -214
- package/src/layers/label-layer.ts +0 -446
- package/src/layers/viv-layers.ts +0 -55
- package/src/lru-store.ts +0 -57
- package/src/ome.ts +0 -388
- package/src/state.ts +0 -202
- package/src/theme.ts +0 -80
- package/src/types.ts +0 -112
- package/src/utils.ts +0 -663
- package/tsconfig.json +0 -14
- package/vite.config.js +0 -47
package/src/ZarrPixelSource.ts
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import * as zarr from "zarrita";
|
|
2
|
-
import { assert } from "./utils";
|
|
3
|
-
|
|
4
|
-
import type * as viv from "@vivjs/types";
|
|
5
|
-
|
|
6
|
-
// TODO: Export from top-level zarrita
|
|
7
|
-
type Slice = ReturnType<typeof zarr.slice>;
|
|
8
|
-
|
|
9
|
-
const X_AXIS_NAME = "x";
|
|
10
|
-
const Y_AXIS_NAME = "y";
|
|
11
|
-
const RGBA_CHANNEL_AXIS_NAME = "_c";
|
|
12
|
-
|
|
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>>;
|
|
21
|
-
readonly tileSize: number;
|
|
22
|
-
readonly dtype: viv.SupportedDtype;
|
|
23
|
-
readonly meta?: viv.PixelSourceMeta;
|
|
24
|
-
readonly #arr: zarr.Array<zarr.NumberDataType | zarr.BigintDataType, zarr.Readable>;
|
|
25
|
-
readonly #transform: (
|
|
26
|
-
arr: zarr.TypedArray<zarr.NumberDataType | zarr.BigintDataType>,
|
|
27
|
-
) => zarr.TypedArray<Lowercase<viv.SupportedDtype>>;
|
|
28
|
-
|
|
29
|
-
#pendingId: undefined | number = undefined;
|
|
30
|
-
#pending: Array<{
|
|
31
|
-
resolve: (data: VivPixelData) => void;
|
|
32
|
-
reject: (err: unknown) => void;
|
|
33
|
-
request: {
|
|
34
|
-
selection: Array<number | zarr.Slice>;
|
|
35
|
-
signal?: AbortSignal;
|
|
36
|
-
};
|
|
37
|
-
}> = [];
|
|
38
|
-
|
|
39
|
-
constructor(
|
|
40
|
-
arr: zarr.Array<zarr.DataType, zarr.Readable>,
|
|
41
|
-
options: { labels: viv.Labels<Array<string>>; tileSize: number; meta?: viv.PixelSourceMeta },
|
|
42
|
-
) {
|
|
43
|
-
assert(arr.is("number") || arr.is("bigint"), `Unsupported viv dtype: ${arr.dtype}`);
|
|
44
|
-
this.#arr = arr;
|
|
45
|
-
this.labels = options.labels;
|
|
46
|
-
this.tileSize = options.tileSize;
|
|
47
|
-
this.meta = options.meta;
|
|
48
|
-
/**
|
|
49
|
-
* Some `zarrita` data types are not supported by Viv and require casting.
|
|
50
|
-
*
|
|
51
|
-
* Note how the casted type in the transform function is type-cast to `zarr.TypedArray<typeof arr.dtype>`.
|
|
52
|
-
* This ensures that the function body is correct based on whatever type narrowing we do in the if/else
|
|
53
|
-
* blocks based on dtype.
|
|
54
|
-
*
|
|
55
|
-
* TODO: Maybe we should add a console warning?
|
|
56
|
-
*/
|
|
57
|
-
if (arr.dtype === "uint64" || arr.dtype === "int64") {
|
|
58
|
-
this.dtype = "Uint32";
|
|
59
|
-
this.#transform = (x) => Uint32Array.from(x as zarr.TypedArray<typeof arr.dtype>, (bint) => Number(bint));
|
|
60
|
-
} else if (arr.dtype === "float16") {
|
|
61
|
-
this.dtype = "Float32";
|
|
62
|
-
this.#transform = (x) => new Float32Array(x as zarr.TypedArray<typeof arr.dtype>);
|
|
63
|
-
} else {
|
|
64
|
-
this.dtype = capitalize(arr.dtype);
|
|
65
|
-
this.#transform = (x) => x as zarr.TypedArray<typeof arr.dtype>;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
get #width() {
|
|
70
|
-
const lastIndex = this.shape.length - 1;
|
|
71
|
-
return this.shape[this.labels.indexOf("c") === lastIndex ? lastIndex - 1 : lastIndex];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
get #height() {
|
|
75
|
-
const lastIndex = this.shape.length - 1;
|
|
76
|
-
return this.shape[this.labels.indexOf("c") === lastIndex ? lastIndex - 2 : lastIndex - 1];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
get shape() {
|
|
80
|
-
return this.#arr.shape;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async getRaster(options: {
|
|
84
|
-
selection: viv.PixelSourceSelection<Array<string>> | Array<number>;
|
|
85
|
-
signal?: AbortSignal;
|
|
86
|
-
}): Promise<viv.PixelData> {
|
|
87
|
-
const { selection, signal } = options;
|
|
88
|
-
return this.#fetchData({
|
|
89
|
-
selection: buildZarrSelection(selection, {
|
|
90
|
-
labels: this.labels,
|
|
91
|
-
slices: { x: zarr.slice(null), y: zarr.slice(null) },
|
|
92
|
-
}),
|
|
93
|
-
signal,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
onTileError(_err: unknown): void {
|
|
98
|
-
// no-op
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async getTile(options: {
|
|
102
|
-
x: number;
|
|
103
|
-
y: number;
|
|
104
|
-
selection: viv.PixelSourceSelection<Array<string>> | Array<number>;
|
|
105
|
-
signal?: AbortSignal;
|
|
106
|
-
}): Promise<viv.PixelData> {
|
|
107
|
-
const { x, y, selection, signal } = options;
|
|
108
|
-
return this.#fetchData({
|
|
109
|
-
selection: buildZarrSelection(selection, {
|
|
110
|
-
labels: this.labels,
|
|
111
|
-
slices: {
|
|
112
|
-
x: zarr.slice(x * this.tileSize, Math.min((x + 1) * this.tileSize, this.#width)),
|
|
113
|
-
y: zarr.slice(y * this.tileSize, Math.min((y + 1) * this.tileSize, this.#height)),
|
|
114
|
-
},
|
|
115
|
-
}),
|
|
116
|
-
signal,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async #fetchData(request: { selection: Array<number | Slice>; signal?: AbortSignal }): Promise<viv.PixelData> {
|
|
121
|
-
const { promise, resolve, reject } = Promise.withResolvers<VivPixelData>();
|
|
122
|
-
this.#pending.push({ request, resolve, reject });
|
|
123
|
-
this.#pendingId = this.#pendingId ?? requestAnimationFrame(() => this.#fetchPending());
|
|
124
|
-
// @ts-expect-error - The missing generic ArrayBuffer type from Viv makes VivPixelData and viv.PixelData incompatible, even though they are.
|
|
125
|
-
return promise;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Fetch a pending batch of requests together and resolve independently.
|
|
130
|
-
*
|
|
131
|
-
* TODO: There could be more optimizations (e.g., multi-get)
|
|
132
|
-
*/
|
|
133
|
-
async #fetchPending() {
|
|
134
|
-
for (const { request, resolve, reject } of this.#pending) {
|
|
135
|
-
zarr
|
|
136
|
-
.get(this.#arr, request.selection, { opts: { signal: request.signal } })
|
|
137
|
-
.then(({ data, shape }) => {
|
|
138
|
-
resolve({
|
|
139
|
-
data: this.#transform(data),
|
|
140
|
-
width: shape[1],
|
|
141
|
-
height: shape[0],
|
|
142
|
-
});
|
|
143
|
-
})
|
|
144
|
-
.catch((error) => reject(error));
|
|
145
|
-
}
|
|
146
|
-
this.#pendingId = undefined;
|
|
147
|
-
this.#pending = [];
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function buildZarrSelection(
|
|
152
|
-
baseSelection: Record<string, number> | Array<number>,
|
|
153
|
-
options: {
|
|
154
|
-
labels: string[];
|
|
155
|
-
slices: { x: zarr.Slice; y: zarr.Slice };
|
|
156
|
-
},
|
|
157
|
-
): Array<Slice | number> {
|
|
158
|
-
const { labels, slices } = options;
|
|
159
|
-
let selection: Array<Slice | number>;
|
|
160
|
-
if (Array.isArray(baseSelection)) {
|
|
161
|
-
// shallow copy
|
|
162
|
-
selection = [...baseSelection];
|
|
163
|
-
} else {
|
|
164
|
-
// initialize with zeros
|
|
165
|
-
selection = Array.from({ length: labels.length }, () => 0);
|
|
166
|
-
// fill in the selection
|
|
167
|
-
for (const [key, idx] of Object.entries(baseSelection)) {
|
|
168
|
-
selection[labels.indexOf(key)] = idx;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
selection[labels.indexOf(X_AXIS_NAME)] = slices.x;
|
|
172
|
-
selection[labels.indexOf(Y_AXIS_NAME)] = slices.y;
|
|
173
|
-
if (RGBA_CHANNEL_AXIS_NAME in labels) {
|
|
174
|
-
selection[labels.indexOf(RGBA_CHANNEL_AXIS_NAME)] = zarr.slice(null);
|
|
175
|
-
}
|
|
176
|
-
return selection;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function capitalize<T extends string>(s: T): Capitalize<T> {
|
|
180
|
-
// @ts-expect-error - TypeScript can't verify that the return type is correct
|
|
181
|
-
return s[0].toUpperCase() + s.slice(1);
|
|
182
|
-
}
|
package/src/codecs/jpeg2k.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// TODO: Remove dependency on CDN for pdf.js source. Could use git submodule or
|
|
2
|
-
// `deno bundle` since the pdf.js dist does not include ESM source code (only UMD or CJS).
|
|
3
|
-
|
|
4
|
-
// @ts-ignore
|
|
5
|
-
import { JpxImage } from "https://cdn.jsdelivr.net/gh/mozilla/pdf.js@30bd5f0/src/core/jpx.js";
|
|
6
|
-
|
|
7
|
-
export default class Jpeg2k {
|
|
8
|
-
kind = "bytes_to_bytes" as const;
|
|
9
|
-
static codecId = "jpeg2k";
|
|
10
|
-
static fromConfig(): Jpeg2k {
|
|
11
|
-
return new Jpeg2k();
|
|
12
|
-
}
|
|
13
|
-
encode(_: Uint8Array): never {
|
|
14
|
-
throw new Error("encode not implemented");
|
|
15
|
-
}
|
|
16
|
-
async decode(data: Uint8Array): Promise<Uint8Array> {
|
|
17
|
-
const img = new JpxImage();
|
|
18
|
-
img.failOnCorruptedImage = true;
|
|
19
|
-
img.parse(data);
|
|
20
|
-
return img.tiles[0].items;
|
|
21
|
-
}
|
|
22
|
-
}
|
package/src/codecs/register.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { Grid, NativeSelect } from "@mui/material";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import type { ChangeEvent } from "react";
|
|
4
|
-
import { useSourceData } from "../../hooks";
|
|
5
|
-
|
|
6
|
-
function AcquisitionController() {
|
|
7
|
-
const [sourceData] = useSourceData();
|
|
8
|
-
const { acquisitionId, acquisitions } = sourceData;
|
|
9
|
-
|
|
10
|
-
if (!acquisitions) {
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const handleSelectionChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
|
15
|
-
let value = event.target.value;
|
|
16
|
-
const url = new URL(window.location.href);
|
|
17
|
-
if (value === "-1") {
|
|
18
|
-
url.searchParams.delete("acquisition");
|
|
19
|
-
} else {
|
|
20
|
-
url.searchParams.set("acquisition", value);
|
|
21
|
-
}
|
|
22
|
-
window.location.href = decodeURIComponent(url.href);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<>
|
|
27
|
-
<Grid>
|
|
28
|
-
<NativeSelect fullWidth style={{ fontSize: "0.7em" }} onChange={handleSelectionChange} value={acquisitionId}>
|
|
29
|
-
<option value="-1" key="-1">
|
|
30
|
-
Filter by Acquisition
|
|
31
|
-
</option>
|
|
32
|
-
{acquisitions.map((acq) => {
|
|
33
|
-
acq = acq as Ome.Acquisition;
|
|
34
|
-
return (
|
|
35
|
-
<option value={acq.id} key={acq.id}>
|
|
36
|
-
Acquisition: {acq.name}
|
|
37
|
-
</option>
|
|
38
|
-
);
|
|
39
|
-
})}
|
|
40
|
-
</NativeSelect>
|
|
41
|
-
</Grid>
|
|
42
|
-
</>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export default AcquisitionController;
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { Add } from "@mui/icons-material";
|
|
2
|
-
import { Divider, IconButton, NativeSelect, Paper, Popover, Typography } from "@mui/material";
|
|
3
|
-
import React, { useState } from "react";
|
|
4
|
-
import type { ChangeEvent, MouseEvent } from "react";
|
|
5
|
-
|
|
6
|
-
import { useLayerState, useSourceData } from "../../hooks";
|
|
7
|
-
import { MAX_CHANNELS, calcDataRange, hexToRGB, resolveLoaderFromLayerProps } from "../../utils";
|
|
8
|
-
|
|
9
|
-
function AddChannelButton() {
|
|
10
|
-
const [source, setSource] = useSourceData();
|
|
11
|
-
const [layer, setLayer] = useLayerState();
|
|
12
|
-
const [anchorEl, setAnchorEl] = useState<null | Element>(null);
|
|
13
|
-
|
|
14
|
-
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
|
|
15
|
-
setAnchorEl(event.currentTarget);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const handleClose = () => {
|
|
19
|
-
setAnchorEl(null);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const handleChange = async (event: ChangeEvent<HTMLSelectElement>) => {
|
|
23
|
-
handleClose();
|
|
24
|
-
const channelIndex = +event.target.value;
|
|
25
|
-
const channelSelection = [...source.defaults.selection];
|
|
26
|
-
if (source.channel_axis) {
|
|
27
|
-
channelSelection[source.channel_axis] = channelIndex;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// cacluate contrast limits if missing from source;
|
|
31
|
-
let lim: [min: number, max: number];
|
|
32
|
-
if (source.contrast_limits[channelIndex]) {
|
|
33
|
-
lim = source.contrast_limits[channelIndex] as [number, number];
|
|
34
|
-
} else {
|
|
35
|
-
const loader = resolveLoaderFromLayerProps(layer.layerProps);
|
|
36
|
-
const lowres = Array.isArray(loader) ? loader[loader.length - 1] : loader;
|
|
37
|
-
lim = await calcDataRange(lowres, channelSelection);
|
|
38
|
-
// Update source data with newly calculated limit
|
|
39
|
-
setSource((prev) => {
|
|
40
|
-
const clims = [...prev.contrast_limits];
|
|
41
|
-
clims[channelIndex] = lim;
|
|
42
|
-
return { ...prev, contrast_limits: clims };
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
setLayer((prev) => {
|
|
47
|
-
const { layerProps } = prev;
|
|
48
|
-
const selections = [...layerProps.selections, channelSelection];
|
|
49
|
-
const colors = [...layerProps.colors, hexToRGB(source.colors[channelIndex])];
|
|
50
|
-
const contrastLimits = [...layerProps.contrastLimitsRange, lim];
|
|
51
|
-
const channelsVisible = [...layerProps.channelsVisible, true];
|
|
52
|
-
return {
|
|
53
|
-
...prev,
|
|
54
|
-
layerProps: {
|
|
55
|
-
...layerProps,
|
|
56
|
-
selections,
|
|
57
|
-
colors,
|
|
58
|
-
contrastLimits,
|
|
59
|
-
contrastLimitsRange: [...contrastLimits],
|
|
60
|
-
channelsVisible,
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
});
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const { names } = source;
|
|
67
|
-
const open = Boolean(anchorEl);
|
|
68
|
-
const id = open ? `layer-${source.id}-add-channel` : undefined;
|
|
69
|
-
return (
|
|
70
|
-
<>
|
|
71
|
-
<IconButton
|
|
72
|
-
onClick={handleClick}
|
|
73
|
-
aria-describedby={id}
|
|
74
|
-
style={{
|
|
75
|
-
backgroundColor: "transparent",
|
|
76
|
-
padding: 0,
|
|
77
|
-
zIndex: 2,
|
|
78
|
-
cursor: "pointer",
|
|
79
|
-
}}
|
|
80
|
-
disabled={layer.layerProps.selections.length === MAX_CHANNELS}
|
|
81
|
-
>
|
|
82
|
-
<Add />
|
|
83
|
-
</IconButton>
|
|
84
|
-
<Popover
|
|
85
|
-
id={id}
|
|
86
|
-
open={open}
|
|
87
|
-
anchorEl={anchorEl}
|
|
88
|
-
onClose={handleClose}
|
|
89
|
-
anchorOrigin={{
|
|
90
|
-
vertical: "bottom",
|
|
91
|
-
horizontal: "left",
|
|
92
|
-
}}
|
|
93
|
-
transformOrigin={{
|
|
94
|
-
vertical: "top",
|
|
95
|
-
horizontal: "left",
|
|
96
|
-
}}
|
|
97
|
-
>
|
|
98
|
-
<Paper style={{ padding: "0px 4px", marginBottom: 4, width: "8em" }}>
|
|
99
|
-
<Typography variant="caption">selection: </Typography>
|
|
100
|
-
<Divider />
|
|
101
|
-
<NativeSelect
|
|
102
|
-
fullWidth
|
|
103
|
-
style={{ fontSize: "0.7em" }}
|
|
104
|
-
id={`layer-${source.id}-channel-select`}
|
|
105
|
-
onChange={handleChange}
|
|
106
|
-
>
|
|
107
|
-
<option aria-label="None" value="">
|
|
108
|
-
None
|
|
109
|
-
</option>
|
|
110
|
-
{names.map((name, i) => (
|
|
111
|
-
<option value={i} key={name}>
|
|
112
|
-
{name}
|
|
113
|
-
</option>
|
|
114
|
-
))}
|
|
115
|
-
</NativeSelect>
|
|
116
|
-
</Paper>
|
|
117
|
-
</Popover>
|
|
118
|
-
</>
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export default AddChannelButton;
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { MoreHoriz } from "@mui/icons-material";
|
|
2
|
-
import { Divider, IconButton, Input, Paper, Popover, Typography } from "@mui/material";
|
|
3
|
-
import { styled } from "@mui/material/styles";
|
|
4
|
-
import React, { useState } from "react";
|
|
5
|
-
import type { ChangeEvent, MouseEvent } from "react";
|
|
6
|
-
import { useLayerState, useSourceData } from "../../hooks";
|
|
7
|
-
|
|
8
|
-
const DenseInput = styled(Input)`
|
|
9
|
-
width: 5.5em;
|
|
10
|
-
font-size: 0.7em;
|
|
11
|
-
`;
|
|
12
|
-
|
|
13
|
-
interface Props {
|
|
14
|
-
axisIndex: number;
|
|
15
|
-
max: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function AxisOptions({ axisIndex, max }: Props) {
|
|
19
|
-
const [sourceData] = useSourceData();
|
|
20
|
-
const [layer, setLayer] = useLayerState();
|
|
21
|
-
const [anchorEl, setAnchorEl] = useState<null | Element>(null);
|
|
22
|
-
|
|
23
|
-
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
|
|
24
|
-
setAnchorEl(event.currentTarget);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const handleClose = () => {
|
|
28
|
-
setAnchorEl(null);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const handleIndexChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
32
|
-
let value = +event.target.value;
|
|
33
|
-
// Restrict value to valid range
|
|
34
|
-
if (value < 0) value = 0;
|
|
35
|
-
if (value > max) value = max;
|
|
36
|
-
|
|
37
|
-
setLayer((prev) => {
|
|
38
|
-
let layerProps = { ...prev.layerProps };
|
|
39
|
-
// for each channel, update index
|
|
40
|
-
layerProps.selections = layerProps.selections.map((ch) => {
|
|
41
|
-
let new_ch = [...ch];
|
|
42
|
-
new_ch[axisIndex] = value;
|
|
43
|
-
return new_ch;
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
return { ...prev, layerProps };
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const open = Boolean(anchorEl);
|
|
51
|
-
const id = open ? `${axisIndex}-index-${sourceData.id}-options` : undefined;
|
|
52
|
-
const value = layer.layerProps.selections[0] ? layer.layerProps.selections[0][axisIndex] : 1;
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<>
|
|
56
|
-
<IconButton
|
|
57
|
-
onClick={handleClick}
|
|
58
|
-
aria-describedby={id}
|
|
59
|
-
style={{
|
|
60
|
-
backgroundColor: "transparent",
|
|
61
|
-
padding: 0,
|
|
62
|
-
zIndex: 2,
|
|
63
|
-
cursor: "pointer",
|
|
64
|
-
}}
|
|
65
|
-
>
|
|
66
|
-
<MoreHoriz />
|
|
67
|
-
</IconButton>
|
|
68
|
-
<Popover
|
|
69
|
-
id={id}
|
|
70
|
-
open={open}
|
|
71
|
-
anchorEl={anchorEl}
|
|
72
|
-
onClose={handleClose}
|
|
73
|
-
anchorOrigin={{
|
|
74
|
-
vertical: "bottom",
|
|
75
|
-
horizontal: "left",
|
|
76
|
-
}}
|
|
77
|
-
transformOrigin={{
|
|
78
|
-
vertical: "top",
|
|
79
|
-
horizontal: "left",
|
|
80
|
-
}}
|
|
81
|
-
>
|
|
82
|
-
<Paper style={{ padding: "0px 4px", marginBottom: 4 }}>
|
|
83
|
-
<Typography variant="caption">Index:</Typography>
|
|
84
|
-
<Divider />
|
|
85
|
-
<DenseInput value={value} onChange={handleIndexChange} type="number" id="max" fullWidth={false} />
|
|
86
|
-
<Divider />
|
|
87
|
-
</Paper>
|
|
88
|
-
</Popover>
|
|
89
|
-
</>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export default AxisOptions;
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { Divider, Grid, Slider, Typography } from "@mui/material";
|
|
2
|
-
import { styled } from "@mui/material/styles";
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { useLayerState, useSourceData } from "../../hooks";
|
|
5
|
-
import DimensionOptions from "./AxisOptions";
|
|
6
|
-
|
|
7
|
-
const DenseSlider = styled(Slider)`
|
|
8
|
-
color: white;
|
|
9
|
-
padding: 10px 0px 5px 0px;
|
|
10
|
-
margin-right: 5px;
|
|
11
|
-
:active { box-shadow: 0px 0px 0px 8px rgba(158, 158, 158, 0.16); }
|
|
12
|
-
`;
|
|
13
|
-
|
|
14
|
-
interface Props {
|
|
15
|
-
axisIndex: number;
|
|
16
|
-
max: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function AxisSlider({ axisIndex, max }: Props) {
|
|
20
|
-
const [layer, setLayer] = useLayerState();
|
|
21
|
-
const [sourceData] = useSourceData();
|
|
22
|
-
const { axis_labels } = sourceData;
|
|
23
|
-
let axisLabel = axis_labels[axisIndex];
|
|
24
|
-
if (axisLabel === "t" || axisLabel === "z") {
|
|
25
|
-
axisLabel = axisLabel.toUpperCase();
|
|
26
|
-
}
|
|
27
|
-
// state of the slider to update UI while dragging
|
|
28
|
-
const [value, setValue] = React.useState(0);
|
|
29
|
-
|
|
30
|
-
// If axis index change externally, need to update state
|
|
31
|
-
React.useEffect(() => {
|
|
32
|
-
// Use first channel to get initial value of slider - can be undefined on first render
|
|
33
|
-
setValue(layer.layerProps.selections[0] ? layer.layerProps.selections[0][axisIndex] : 1);
|
|
34
|
-
}, [layer.layerProps.selections, axisIndex]);
|
|
35
|
-
|
|
36
|
-
const handleRelease = () => {
|
|
37
|
-
setLayer((prev) => {
|
|
38
|
-
let layerProps = { ...prev.layerProps };
|
|
39
|
-
// for each channel, update index of this axis
|
|
40
|
-
layerProps.selections = layerProps.selections.map((ch) => {
|
|
41
|
-
let new_ch = [...ch];
|
|
42
|
-
new_ch[axisIndex] = value;
|
|
43
|
-
return new_ch;
|
|
44
|
-
});
|
|
45
|
-
return { ...prev, layerProps };
|
|
46
|
-
});
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const handleDrag = (_: Event, value: number | number[]) => {
|
|
50
|
-
setValue(value as number);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<>
|
|
55
|
-
<Grid>
|
|
56
|
-
<Grid container justifyContent="space-between">
|
|
57
|
-
<Grid size={{ xs: 10 }}>
|
|
58
|
-
<div style={{ width: 165, overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
59
|
-
<Typography variant="caption" noWrap>
|
|
60
|
-
{axisLabel}: {value}/{max}
|
|
61
|
-
</Typography>
|
|
62
|
-
</div>
|
|
63
|
-
</Grid>
|
|
64
|
-
<Grid size={{ xs: 1 }}>
|
|
65
|
-
<DimensionOptions axisIndex={axisIndex} max={max} />
|
|
66
|
-
</Grid>
|
|
67
|
-
</Grid>
|
|
68
|
-
<Grid container justifyContent="space-between">
|
|
69
|
-
<Grid size={{ xs: 12 }}>
|
|
70
|
-
<DenseSlider
|
|
71
|
-
value={value}
|
|
72
|
-
onChange={handleDrag}
|
|
73
|
-
onChangeCommitted={handleRelease}
|
|
74
|
-
min={0}
|
|
75
|
-
max={max}
|
|
76
|
-
step={1}
|
|
77
|
-
/>
|
|
78
|
-
</Grid>
|
|
79
|
-
</Grid>
|
|
80
|
-
</Grid>
|
|
81
|
-
<Divider />
|
|
82
|
-
</>
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export default AxisSlider;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Divider, Grid } from "@mui/material";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { useSourceData } from "../../hooks";
|
|
4
|
-
import AxisSlider from "./AxisSlider";
|
|
5
|
-
|
|
6
|
-
function AxisSliders() {
|
|
7
|
-
const [sourceData] = useSourceData();
|
|
8
|
-
const { axis_labels, channel_axis, loader } = sourceData;
|
|
9
|
-
|
|
10
|
-
const sliders = axis_labels
|
|
11
|
-
.slice(0, -2) // ignore last two axes, [y,x]
|
|
12
|
-
.map((name, i): [string, number, number] => [name, i, loader[0].shape[i]]) // capture the name, index, and size of non-yx dims
|
|
13
|
-
.filter((d) => {
|
|
14
|
-
if (d[1] === channel_axis) return false; // ignore channel_axis (for OME-Zarr channel_axis === 1)
|
|
15
|
-
if (d[2] > 1) return true; // keep if size > 1
|
|
16
|
-
return false; // otherwise ignore as well
|
|
17
|
-
})
|
|
18
|
-
.map(([name, i, size]) => <AxisSlider key={name} axisIndex={i} max={size - 1} />);
|
|
19
|
-
|
|
20
|
-
if (sliders.length === 0) return null;
|
|
21
|
-
return (
|
|
22
|
-
<>
|
|
23
|
-
<Grid>{sliders}</Grid>
|
|
24
|
-
<Divider />
|
|
25
|
-
</>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export default AxisSliders;
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { RadioButtonChecked, RadioButtonUnchecked } from "@mui/icons-material";
|
|
2
|
-
import { Grid, IconButton, Slider, Typography } from "@mui/material";
|
|
3
|
-
import React from "react";
|
|
4
|
-
import { useLayerState, useSourceData } from "../../hooks";
|
|
5
|
-
import ChannelOptions from "./ChannelOptions";
|
|
6
|
-
|
|
7
|
-
function ChannelController({ channelIndex }: { channelIndex: number }) {
|
|
8
|
-
const [sourceData] = useSourceData();
|
|
9
|
-
const [layer, setLayer] = useLayerState();
|
|
10
|
-
|
|
11
|
-
const handleContrastChange = (_: Event, v: number | number[]) => {
|
|
12
|
-
setLayer((prev) => {
|
|
13
|
-
const contrastLimits = [...prev.layerProps.contrastLimits];
|
|
14
|
-
contrastLimits[channelIndex] = v as [number, number];
|
|
15
|
-
return { ...prev, layerProps: { ...prev.layerProps, contrastLimits } };
|
|
16
|
-
});
|
|
17
|
-
};
|
|
18
|
-
const handleVisibilityChange = () => {
|
|
19
|
-
setLayer((prev) => {
|
|
20
|
-
const channelsVisible = [...prev.layerProps.channelsVisible];
|
|
21
|
-
channelsVisible[channelIndex] = !channelsVisible[channelIndex];
|
|
22
|
-
return { ...prev, layerProps: { ...prev.layerProps, channelsVisible } };
|
|
23
|
-
});
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const lp = layer.layerProps;
|
|
27
|
-
|
|
28
|
-
// Material slider tries to sort in place. Need to copy.
|
|
29
|
-
const value = [...lp.contrastLimits[channelIndex]];
|
|
30
|
-
const color = `rgb(${lp.colormap ? [255, 255, 255] : lp.colors[channelIndex]})`;
|
|
31
|
-
const on = lp.channelsVisible[channelIndex];
|
|
32
|
-
const [min, max] = lp.contrastLimitsRange[channelIndex];
|
|
33
|
-
|
|
34
|
-
const { channel_axis, names } = sourceData;
|
|
35
|
-
const selection = lp.selections[channelIndex];
|
|
36
|
-
const nameIndex = Number.isInteger(channel_axis) ? selection[channel_axis as number] : 0;
|
|
37
|
-
const label = names[nameIndex];
|
|
38
|
-
return (
|
|
39
|
-
<>
|
|
40
|
-
<Grid container justifyContent="space-between" wrap="nowrap">
|
|
41
|
-
<Grid size={{ xs: 10 }}>
|
|
42
|
-
<div style={{ width: 165, overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
43
|
-
<Typography variant="caption" noWrap>
|
|
44
|
-
{label}
|
|
45
|
-
</Typography>
|
|
46
|
-
</div>
|
|
47
|
-
</Grid>
|
|
48
|
-
<Grid size={{ xs: 1 }}>
|
|
49
|
-
<ChannelOptions channelIndex={channelIndex} />
|
|
50
|
-
</Grid>
|
|
51
|
-
</Grid>
|
|
52
|
-
<Grid container justifyContent="space-between">
|
|
53
|
-
<Grid size={{ xs: 2 }}>
|
|
54
|
-
<IconButton
|
|
55
|
-
style={{
|
|
56
|
-
color,
|
|
57
|
-
backgroundColor: "transparent",
|
|
58
|
-
padding: 0,
|
|
59
|
-
zIndex: 2,
|
|
60
|
-
}}
|
|
61
|
-
onClick={handleVisibilityChange}
|
|
62
|
-
>
|
|
63
|
-
{on ? <RadioButtonChecked /> : <RadioButtonUnchecked />}
|
|
64
|
-
</IconButton>
|
|
65
|
-
</Grid>
|
|
66
|
-
<Grid size={{ xs: 10 }}>
|
|
67
|
-
<Slider
|
|
68
|
-
value={value}
|
|
69
|
-
onChange={handleContrastChange}
|
|
70
|
-
min={min}
|
|
71
|
-
max={max}
|
|
72
|
-
step={0.01}
|
|
73
|
-
style={{
|
|
74
|
-
padding: "10px 0px 5px 0px",
|
|
75
|
-
color,
|
|
76
|
-
}}
|
|
77
|
-
/>
|
|
78
|
-
</Grid>
|
|
79
|
-
</Grid>
|
|
80
|
-
</>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export default ChannelController;
|