diff --git a/src/GlobalStates/GlobalStore.ts b/src/GlobalStates/GlobalStore.ts
index bea8ab47c..d80a5e652 100644
--- a/src/GlobalStates/GlobalStore.ts
+++ b/src/GlobalStates/GlobalStore.ts
@@ -17,9 +17,12 @@ interface Coord {
type StoreState = {
dataShape: number[];
+ activeIndices: number[];
shape: THREE.Vector3;
valueScales: { maxVal: number; minVal: number };
colormap: THREE.DataTexture;
+ colormapName: string;
+ flipColormap: boolean;
timeSeries: Record>;
strides: number[];
metadata: Record | null;
@@ -50,9 +53,12 @@ type StoreState = {
// setters
setDataShape: (dataShape: number[]) => void;
+ setActiveIndices: (indices: number[]) => void;
setShape: (shape: THREE.Vector3) => void;
setValueScales: (valueScales: { maxVal: number; minVal: number }) => void;
setColormap: (colormap: THREE.DataTexture) => void;
+ setColormapName: (colormapName: string) => void;
+ setFlipColormap: (flipColormap: boolean) => void;
setTimeSeries: (timeSeries: Record>) => void;
updateTimeSeries: (newEntries: Record>) => void;
setStrides: (strides: number[]) => void;
@@ -86,9 +92,12 @@ type StoreState = {
export const useGlobalStore = create((set, get) => ({
dataShape: [1, 1, 1],
+ activeIndices: [],
shape: new THREE.Vector3(2, 2, 2),
valueScales: { maxVal: 1, minVal: -1 },
colormap: GetColorMapTexture(),
+ colormapName: "Spectral",
+ flipColormap: false,
timeSeries: {},
strides: [10368,144,1],
metadata: null,
@@ -119,9 +128,12 @@ export const useGlobalStore = create((set, get) => ({
// setters
setDataShape: (dataShape) => set({ dataShape }),
+ setActiveIndices: (indices) => set({ activeIndices: indices }),
setShape: (shape) => set({ shape }),
setValueScales: (valueScales) => set({ valueScales }),
setColormap: (colormap) => set({ colormap }),
+ setColormapName: (colormapName) => set({ colormapName }),
+ setFlipColormap: (flipColormap) => set({ flipColormap }),
setTimeSeries: (timeSeries) => set({ timeSeries }),
updateTimeSeries: (newEntries) => {
const merged = { ...newEntries, ...get().timeSeries };
diff --git a/src/GlobalStates/ZarrStore.ts b/src/GlobalStates/ZarrStore.ts
index 423796dfa..875f3e378 100644
--- a/src/GlobalStates/ZarrStore.ts
+++ b/src/GlobalStates/ZarrStore.ts
@@ -1,5 +1,4 @@
import { create } from "zustand";
-import { GetStore } from "@/components/zarr/ZarrLoaderLRU";
import { FetchStoreOptions, IcechunkStoreOptions } from "@/components/zarr/Interfaces";
const ESDC = 'https://s3.bgc-jena.mpg.de:9000/esdl-esdc-v3.0.2/esdc-16d-2.5deg-46x72x1440-3.0.2.zarr'
@@ -25,9 +24,14 @@ type ZarrState = {
fetchKey: number;
blobKey: string | undefined; // The key for the stored File blob for a local NC
+ ndSlices: (number | [number, number | null])[];
+ axisMapping: { x: number, y: number, z: number };
+
setZSlice: (zSlice: [number , number | null]) => void;
setYSlice: (ySlice: [number , number | null]) => void;
setXSlice: (xSlice: [number , number | null]) => void;
+ setNdSlices: (ndSlices: (number | [number, number | null])[]) => void;
+ setAxisMapping: (mapping: { x: number, y: number, z: number }) => void;
setCompress: (compress: boolean) => void;
setCurrentStore: (currentStore: any) => void;
setReFetch: (reFetch: boolean) => void;
@@ -49,8 +53,10 @@ export const useZarrStore = create((set, get) => ({
zSlice: [0, null],
ySlice: [0, null],
xSlice: [0, null],
+ ndSlices: [],
+ axisMapping: { x: -1, y: -1, z: -1 },
compress: false,
- currentStore: GetStore(ESDC),
+ currentStore: Promise.resolve(undefined),
reFetch: false,
currentChunks: {x:[], y:[], z:[]},
arraySize: 0,
@@ -69,6 +75,8 @@ export const useZarrStore = create((set, get) => ({
setZSlice: (zSlice) => set({ zSlice }),
setYSlice: (ySlice) => set({ ySlice }),
setXSlice: (xSlice) => set({ xSlice }),
+ setNdSlices: (ndSlices) => set({ ndSlices }),
+ setAxisMapping: (axisMapping) => set({ axisMapping }),
setCompress: (compress) => set({ compress }),
setCurrentStore: (currentStore) => set({ currentStore }),
setReFetch: (reFetch) => set({ reFetch }),
diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts
index 1be217870..b3062afd8 100644
--- a/src/__tests__/setup.ts
+++ b/src/__tests__/setup.ts
@@ -104,8 +104,9 @@ beforeAll(() => {
COPY_DST: 2,
TEXTURE_BINDING: 4,
STORAGE_BINDING: 8,
- RENDER_ATTACHMENT: 16
- }
+ RENDER_ATTACHMENT: 16,
+ TRANSIENT_ATTACHMENT: 32
+ } as GPUTextureUsage
})
// Export helper to set mock GPU results
diff --git a/src/__tests__/webGPU-nan.test.ts b/src/__tests__/webGPU-nan.test.ts
new file mode 100644
index 000000000..8922a0474
--- /dev/null
+++ b/src/__tests__/webGPU-nan.test.ts
@@ -0,0 +1,280 @@
+import { describe, expect, test, beforeAll } from 'vitest'
+import { DataReduction, Convolve, Multivariate2D, Multivariate3D, CUMSUM3D, Convolve2D } from '../components/computation/webGPU'
+import { setMockGPUResult } from './setup'
+
+describe('WebGPU Functions - NaN Statistics', () => {
+ beforeAll(async () => {
+ if (!navigator.gpu) {
+ console.warn('WebGPU is not supported in this environment')
+ return
+ }
+
+ const adapter = await navigator.gpu.requestAdapter()
+ if (!adapter) {
+ console.warn('No WebGPU adapter found')
+ return
+ }
+
+ const device = await adapter.requestDevice()
+ if (!device) {
+ console.warn('No WebGPU device found')
+ return
+ }
+ })
+
+ // Array layout (3x3x1) with NaNs:
+ // [1, NaN, 3]
+ // [4, 5, NaN]
+ // [NaN, 8, 9]
+ const arrayWithNaN = new Float32Array([1, NaN, 3, 4, 5, NaN, NaN, 8, 9])
+
+ // All NaN array
+ const allNaNArray = new Float32Array([NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN])
+
+ const shape3D = [3, 3, 1]
+ const strides3D = [3, 1, 1]
+ const shape2D = [3, 3]
+ const strides2D = [3, 1]
+
+ describe('DataReduction with NaNs', () => {
+ test('Mean Reduction - skips NaNs', async () => {
+ // Reducing along dim 0 (mean of each column):
+ // Col 0: (1+4)/2 = 2.5 (skips NaN)
+ // Col 1: (5+8)/2 = 6.5 (skips NaN)
+ // Col 2: (3+9)/2 = 6 (skips NaN)
+ setMockGPUResult([2.5, 6.5, 6])
+ const result = await DataReduction(arrayWithNaN, { shape: shape3D, strides: strides3D }, 0, 'Mean')
+ expect(result).toBeDefined()
+ expect(Array.from(result!)).toEqual([2.5, 6.5, 6])
+ })
+
+ test('Min Reduction - skips NaNs', async () => {
+ // Reducing along dim 0 (min of each column):
+ // Col 0: min(1, 4) = 1
+ // Col 1: min(5, 8) = 5
+ // Col 2: min(3, 9) = 3
+ setMockGPUResult([1, 5, 3])
+ const result = await DataReduction(arrayWithNaN, { shape: shape3D, strides: strides3D }, 0, 'Min')
+ expect(result).toBeDefined()
+ expect(Array.from(result!)).toEqual([1, 5, 3])
+ })
+
+ test('Max Reduction - skips NaNs', async () => {
+ // Reducing along dim 0 (max of each column):
+ // Col 0: max(1, 4) = 4
+ // Col 1: max(5, 8) = 8
+ // Col 2: max(3, 9) = 9
+ setMockGPUResult([4, 8, 9])
+ const result = await DataReduction(arrayWithNaN, { shape: shape3D, strides: strides3D }, 0, 'Max')
+ expect(result).toBeDefined()
+ expect(Array.from(result!)).toEqual([4, 8, 9])
+ })
+
+ test('Mean Reduction - all NaNs returns NaN', async () => {
+ // If all are NaN, mean should be NaN
+ setMockGPUResult([NaN, NaN, NaN])
+ const result = await DataReduction(allNaNArray, { shape: shape3D, strides: strides3D }, 0, 'Mean')
+ expect(result).toBeDefined()
+ expect(Number.isNaN(result![0])).toBe(true)
+ expect(Number.isNaN(result![1])).toBe(true)
+ expect(Number.isNaN(result![2])).toBe(true)
+ })
+ })
+
+ describe('Convolution with NaNs', () => {
+ test('3D Mean Convolution - ignores NaNs', async () => {
+ // For the arrayWithNaN [1, NaN, 3, 4, 5, NaN, NaN, 8, 9] with 3x3 kernel
+ // The kernel should only sum valid pixels and divide by valid N.
+ // E.g., center pixel (5) surrounded by 1, NaN, 3, 4, NaN, NaN, 8, 9
+ // Sum = 1+3+4+5+8+9 = 30. Valid N = 6. 30/6 = 5.0
+ // We'll mock out this expected result.
+ const expectedValues = [1.0, 3.25, 3.0, 2.5, 5.0, 6.0, 4.0, 6.5, 9.0]
+ setMockGPUResult(expectedValues)
+ const result = await Convolve(
+ arrayWithNaN,
+ { shape: shape3D, strides: strides3D },
+ 'Mean3D',
+ { kernelSize: 3, kernelDepth: 1 }
+ )
+ expect(result).toBeDefined()
+ expect(result?.length).toBe(9)
+ result!.forEach((val, idx) => {
+ expect(val).toBeCloseTo(expectedValues[idx], 1)
+ })
+ })
+
+ test('3D Mean Convolution - all NaNs returns NaN', async () => {
+ const expectedValues = new Float32Array([NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN])
+ setMockGPUResult(Array.from(expectedValues))
+ const result = await Convolve(
+ allNaNArray,
+ { shape: shape3D, strides: strides3D },
+ 'Mean3D',
+ { kernelSize: 3, kernelDepth: 1 }
+ )
+ expect(result).toBeDefined()
+ expect(result?.length).toBe(9)
+ result!.forEach(val => {
+ expect(Number.isNaN(val)).toBe(true)
+ })
+ })
+
+ test('2D Mean Convolution - ignores NaNs', async () => {
+ // Same logic as 3D Convolution
+ const expectedValues = [1.0, 3.25, 3.0, 2.5, 5.0, 6.0, 4.0, 6.5, 9.0]
+ setMockGPUResult(expectedValues)
+ const result = await Convolve2D(
+ arrayWithNaN,
+ { shape: shape2D, strides: strides2D },
+ 'Mean2D',
+ 3
+ )
+ expect(result).toBeDefined()
+ expect(result?.length).toBe(9)
+ result!.forEach((val, idx) => {
+ expect(val).toBeCloseTo(expectedValues[idx], 1)
+ })
+ })
+ })
+
+ describe('Multivariate with NaNs', () => {
+ // Array 1 valid elements are at indices: 0, 2, 3, 4, 7, 8
+ // [1, NaN, 3, 4, 5, NaN, NaN, 8, 9]
+ // [9, 8, 7, 6, 5, 4, 3, 2, 1]
+ const secondArray = new Float32Array([9, 8, 7, 6, 5, 4, 3, 2, 1])
+
+ test('2D Correlation - computes only on pairwise valid elements', async () => {
+ // Pairwise valid elements:
+ // Array 1 valid: [1, 3, 4, 5, 8, 9]
+ // Array 2 valid: [9, 7, 6, 5, 2, 1]
+ // This is a perfect negative correlation (-1)
+ setMockGPUResult([-1])
+ const result = await Multivariate2D(
+ arrayWithNaN,
+ secondArray,
+ { shape: shape3D, strides: strides3D },
+ 0,
+ 'Correlation2D'
+ )
+ expect(result).toBeDefined()
+ expect(result?.length).toBe(1)
+ expect(result![0]).toBeCloseTo(-1, 5)
+ })
+
+ test('3D Correlation - computes only on pairwise valid elements', async () => {
+ setMockGPUResult([-1])
+ const result = await Multivariate3D(
+ arrayWithNaN,
+ secondArray,
+ { shape: shape3D, strides: strides3D },
+ { kernelSize: 3, kernelDepth: 1 },
+ 'Correlation3D'
+ )
+ expect(result).toBeDefined()
+ expect(result?.length).toBe(1)
+ expect(result![0]).toBeCloseTo(-1, 5)
+ })
+ })
+
+ describe('CUMSUM3D with NaNs', () => {
+ test('Basic CUMSUM operation along dimension 0', async () => {
+ // Array layout (3x3x1):
+ // [1, NaN, 3]
+ // [4, 5, NaN]
+ // [NaN, 8, 9]
+ //
+ // Cumsum along dim 0 (exclusive accumulation down rows for each column):
+ // Col 0 [1, 4, NaN]:
+ // z=0: 0.0 (first valid element)
+ // z=1: 1.0 (accumulated 1)
+ // z=2: 5.0 (accumulated 1+4)
+ // Col 1 [NaN, 5, 8]:
+ // z=0: NaN (no valid elements seen yet, current is NaN)
+ // z=1: 0.0 (first valid element)
+ // z=2: 5.0 (accumulated 5)
+ // Col 2 [3, NaN, 9]:
+ // z=0: 0.0 (first valid element)
+ // z=1: 3.0 (accumulated 3, current is NaN so it outputs accum)
+ // z=2: 3.0 (accumulated 3)
+ // Expected: [0, NaN, 0, 1, 0, 3, 5, 5, 3]
+ const expected = [0, NaN, 0, 1, 0, 3, 5, 5, 3]
+ setMockGPUResult(expected)
+ const result = await CUMSUM3D(
+ arrayWithNaN,
+ { shape: shape3D, strides: strides3D },
+ 0,
+ 0
+ )
+ expect(result).toBeDefined()
+ result!.forEach((val, i) => {
+ if (Number.isNaN(expected[i])) {
+ expect(Number.isNaN(val)).toBe(true)
+ } else {
+ expect(val).toBe(expected[i])
+ }
+ })
+ })
+
+ test('Reverse CUMSUM operation along dimension 0', async () => {
+ // Reverse cumsum along dim 0 (accumulate up rows for each column):
+ // Col 0 [1, 4, NaN]: [4, NaN, NaN]
+ // Col 1 [NaN, 5, 8]: [13, 8, 0]
+ // Col 2 [3, NaN, 9]: [9, 9, 0]
+ const expected = [4, 13, 9, NaN, 8, 9, NaN, 0, 0]
+ setMockGPUResult(expected)
+ const result = await CUMSUM3D(
+ arrayWithNaN,
+ { shape: shape3D, strides: strides3D },
+ 0,
+ 1
+ )
+ expect(result).toBeDefined()
+ result!.forEach((val, i) => {
+ if (Number.isNaN(expected[i])) {
+ expect(Number.isNaN(val)).toBe(true)
+ } else {
+ expect(val).toBe(expected[i])
+ }
+ })
+ })
+
+ test('Basic CUMSUM operation along dimension 1', async () => {
+ // Cumsum along dim 1 (accumulate across columns within each row):
+ // Row 0 [1, NaN, 3]: [0, 1, 1]
+ // Row 1 [4, 5, NaN]: [0, 4, 9]
+ // Row 2 [NaN, 8, 9]: [NaN, 0, 8]
+ const expected = [0, 1, 1, 0, 4, 9, NaN, 0, 8]
+ setMockGPUResult(expected)
+ const result = await CUMSUM3D(
+ arrayWithNaN,
+ { shape: shape3D, strides: strides3D },
+ 1,
+ 0
+ )
+ expect(result).toBeDefined()
+ result!.forEach((val, i) => {
+ if (Number.isNaN(expected[i])) {
+ expect(Number.isNaN(val)).toBe(true)
+ } else {
+ expect(val).toBe(expected[i])
+ }
+ })
+ })
+
+ test('CUMSUM - all NaNs returns NaN for all', async () => {
+ // For all NaNs, validCount remains 0 and every element is NaN, so output is all NaNs
+ const expected = [NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN]
+ setMockGPUResult(expected)
+ const result = await CUMSUM3D(
+ allNaNArray,
+ { shape: shape3D, strides: strides3D },
+ 0,
+ 0
+ )
+ expect(result).toBeDefined()
+ result!.forEach(val => {
+ expect(Number.isNaN(val)).toBe(true)
+ })
+ })
+ })
+})
diff --git a/src/components/computation/WGSLShaders.ts b/src/components/computation/WGSLShaders.ts
index c890fcaf9..6adfb0348 100644
--- a/src/components/computation/WGSLShaders.ts
+++ b/src/components/computation/WGSLShaders.ts
@@ -151,6 +151,7 @@ export const createShaders = (precision: Precision) => {
MeanReduction: /* wgsl */`
${ReductionBoilerPlate}
var sum: f32 = 0.0;
+ var nanCount: u32 = 0u;
// Iterate along the dimension we're averaging
if (reduceDim == 0u) { // Average along Z
@@ -159,6 +160,7 @@ export const createShaders = (precision: Precision) => {
let inputIndex = cCoord + (z * zStride);
let newVal = f32(inputData[u32(inputIndex)]);
if (isNaN(newVal)){
+ nanCount++;
continue;
}
sum += newVal;
@@ -169,6 +171,7 @@ export const createShaders = (precision: Precision) => {
let inputIndex = cCoord + (y * yStride);
let newVal = f32(inputData[u32(inputIndex)]);
if (isNaN(newVal)){
+ nanCount++;
continue;
}
sum += newVal;
@@ -179,6 +182,7 @@ export const createShaders = (precision: Precision) => {
let inputIndex = cCoord + (x * xStride);
let newVal = f32(inputData[u32(inputIndex)]);
if (isNaN(newVal)){
+ nanCount++;
continue;
}
sum += newVal;
@@ -186,13 +190,15 @@ export const createShaders = (precision: Precision) => {
}
let outputIndex = outY * xSize + outX;
- outputData[outputIndex] = ${precision}(sum / f32(dimLength));
+ let N = f32(dimLength - nanCount);
+ outputData[outputIndex] = ${precision}(sum / N);
}
`,
MinReduction: /* wgsl */`
${ReductionBoilerPlate}
var min: f32 = 1e12;
+ var nanCount: u32 = 0u;
// Iterate along the dimension we're averaging
if (reduceDim == 0u) { // Average along Z
@@ -200,6 +206,7 @@ export const createShaders = (precision: Precision) => {
for (var z: u32 = 0u; z < dimLength; z++) {
let inputIndex = cCoord + (z * zStride);
let newMin = f32(inputData[inputIndex]);
+ if (isNaN(newMin)) { nanCount++; continue; }
if (newMin < min) {
min = newMin;
}
@@ -209,6 +216,7 @@ export const createShaders = (precision: Precision) => {
for (var y: u32 = 0u; y < dimLength; y++) {
let inputIndex = cCoord + (y * yStride);
let newMin = f32(inputData[inputIndex]);
+ if (isNaN(newMin)) { nanCount++; continue; }
if (newMin < min) {
min = newMin;
}
@@ -218,6 +226,7 @@ export const createShaders = (precision: Precision) => {
for (var x: u32 = 0u; x < dimLength; x++) {
let inputIndex = cCoord + (x * xStride);
let newMin = f32(inputData[inputIndex]);
+ if (isNaN(newMin)) { nanCount++; continue; }
if (newMin < min) {
min = newMin;
}
@@ -225,7 +234,12 @@ export const createShaders = (precision: Precision) => {
}
let outputIndex = outY * xSize + outX;
- outputData[outputIndex] = ${precision}(min);
+ if (nanCount == dimLength) {
+ let zero = 0.0;
+ outputData[outputIndex] = ${precision}(zero / zero);
+ } else {
+ outputData[outputIndex] = ${precision}(min);
+ }
}
`,
@@ -233,6 +247,7 @@ export const createShaders = (precision: Precision) => {
${ReductionBoilerPlate}
var max: f32 = -1e12;
+ var nanCount: u32 = 0u;
// Iterate along the dimension we're averaging
if (reduceDim == 0u) { // Average along Z
@@ -240,6 +255,7 @@ export const createShaders = (precision: Precision) => {
for (var z: u32 = 0u; z < dimLength; z++) {
let inputIndex = cCoord + (z * zStride);
let newMax = f32(inputData[inputIndex]);
+ if (isNaN(newMax)) { nanCount++; continue; }
if (newMax > max) {
max = newMax;
}
@@ -249,6 +265,7 @@ export const createShaders = (precision: Precision) => {
for (var y: u32 = 0u; y < dimLength; y++) {
let inputIndex = cCoord + (y * yStride);
let newMax = f32(inputData[inputIndex]);
+ if (isNaN(newMax)) { nanCount++; continue; }
if (newMax > max) {
max = newMax;
}
@@ -258,6 +275,7 @@ export const createShaders = (precision: Precision) => {
for (var x: u32 = 0u; x < dimLength; x++) {
let inputIndex = cCoord + (x * xStride);
let newMax = f32(inputData[inputIndex]);
+ if (isNaN(newMax)) { nanCount++; continue; }
if (newMax > max) {
max = newMax;
}
@@ -265,13 +283,19 @@ export const createShaders = (precision: Precision) => {
}
let outputIndex = outY * xSize + outX;
- outputData[outputIndex] = ${precision}(max);
+ if (nanCount == dimLength) {
+ let zero = 0.0;
+ outputData[outputIndex] = ${precision}(zero / zero);
+ } else {
+ outputData[outputIndex] = ${precision}(max);
+ }
}
`,
StDevReduction: /* wgsl */`
${ReductionBoilerPlate}
var sum: f32 = 0.0;
+ var nanCount: u32 = 0u;
// Iterate along the dimension we're averaging
if (reduceDim == 0u) { // Average along Z
let cCoord = outX * xStride + outY * yStride;
@@ -279,6 +303,7 @@ export const createShaders = (precision: Precision) => {
let inputIndex = cCoord + (z * zStride);
let newVal = f32(inputData[u32(inputIndex)]);
if (isNaN(newVal)){
+ nanCount++;
continue;
}
sum += newVal;
@@ -289,6 +314,7 @@ export const createShaders = (precision: Precision) => {
let inputIndex = cCoord + (y * yStride);
let newVal = f32(inputData[u32(inputIndex)]);
if (isNaN(newVal)){
+ nanCount++;
continue;
}
sum += newVal;
@@ -299,13 +325,15 @@ export const createShaders = (precision: Precision) => {
let inputIndex = cCoord + (x * xStride);
let newVal = f32(inputData[u32(inputIndex)]);
if (isNaN(newVal)){
+ nanCount++;
continue;
}
sum += newVal;
}
}
- let mean: f32 = sum / f32(dimLength);
+ let N: f32 = f32(dimLength - nanCount);
+ let mean: f32 = sum / N;
var squaredDiffSum: f32 = 0.0;
@@ -345,7 +373,7 @@ export const createShaders = (precision: Precision) => {
}
}
- let stDev: f32 = sqrt(squaredDiffSum / f32(dimLength));
+ let stDev: f32 = sqrt(squaredDiffSum / N);
let outputIndex = outY * xSize + outX;
outputData[outputIndex] = ${precision}(stDev);
}
@@ -415,30 +443,44 @@ export const createShaders = (precision: Precision) => {
${ReductionBoilerPlate}
let meanY: f32 = f32(dimLength)/2;
var sum: f32 = 0.0;
-
+ var nanCount: u32 = 0u;
// Iterate along the dimension we're averaging
if (reduceDim == 0u) { // Average along Z
let cCoord = outX * xStride + outY * yStride;
for (var z: u32 = 0u; z < dimLength; z++) {
let inputIndex = cCoord + (z * zStride);
- sum += f32(inputData[inputIndex]);
+ let val = f32(inputData[inputIndex]);
+ if (isNaN(val)) { nanCount++; continue; }
+ sum += val;
}
} else if (reduceDim == 1u) { // Average along Y
let cCoord = outX * xStride + outY * zStride;
for (var y: u32 = 0u; y < dimLength; y++) {
let inputIndex = cCoord + (y * yStride);
- sum += f32(inputData[inputIndex]);
+ let val = f32(inputData[inputIndex]);
+ if (isNaN(val)) { nanCount++; continue; }
+ sum += val;
}
} else { // Average along X
let cCoord = outX * yStride + outY * zStride;
for (var x: u32 = 0u; x < dimLength; x++) {
let inputIndex = cCoord + (x * xStride);
- sum += f32(inputData[inputIndex]);
+ let val = f32(inputData[inputIndex]);
+ if (isNaN(val)) { nanCount++; continue; }
+ sum += val;
}
}
- let meanX: f32 = sum / f32(dimLength);
+ let N: f32 = f32(dimLength - nanCount);
+ if (N <= 1.0) {
+ let zero = 0.0;
+ let outputIndex = outY * xSize + outX;
+ outputData[outputIndex] = ${precision}(zero / zero);
+ return;
+ }
+
+ let meanX: f32 = sum / N;
var numSum: f32 = 0;
var denomSum: f32 = 0;
@@ -447,6 +489,7 @@ export const createShaders = (precision: Precision) => {
for (var z: u32 = 0u; z < dimLength; z++) {
let inputIndex = cCoord + (z * zStride);
let xi: f32 = f32(inputData[inputIndex]);
+ if (isNaN(xi)) { continue; }
numSum += (xi - meanX)*(f32(z) - meanY);
denomSum += (f32(z) - meanY)*(f32(z) - meanY);
}
@@ -455,6 +498,7 @@ export const createShaders = (precision: Precision) => {
for (var y: u32 = 0u; y < dimLength; y++) {
let inputIndex = cCoord + (y * yStride);
let xi: f32 = f32(inputData[inputIndex]);
+ if (isNaN(xi)) { continue; }
numSum += (xi - meanX)*(f32(y) - meanY);
denomSum += (f32(y) - meanY)*(f32(y) - meanY);
}
@@ -463,6 +507,7 @@ export const createShaders = (precision: Precision) => {
for (var x: u32 = 0u; x < dimLength; x++) {
let inputIndex = cCoord + (x * xStride);
let xi: f32 = f32(inputData[inputIndex]);
+ if (isNaN(xi)) { continue; }
numSum += (xi - meanX)*(f32(x) - meanY);
denomSum += (f32(x) - meanY)*(f32(x) - meanY);
}
@@ -557,8 +602,16 @@ export const createShaders = (precision: Precision) => {
}
}
- let xMean: f32 = xSum / f32(dimLength - nanCount);
- let yMean: f32 = ySum / f32(dimLength - nanCount);
+ let N: f32 = f32(dimLength - nanCount);
+ if (N <= 1.0) {
+ let zero = 0.0;
+ let outputIndex = outY * xSize + outX;
+ outputData[outputIndex] = ${precision}(zero / zero);
+ return;
+ }
+
+ let xMean: f32 = xSum / N;
+ let yMean: f32 = ySum / N;
var numSum: f32 = 0;
var denomSum: f32 = 0;
@@ -674,6 +727,12 @@ export const createShaders = (precision: Precision) => {
}
var N: f32 = f32(dimLength - nanCount);
+ if (N <= 1.0) {
+ let zero = 0.0;
+ let outputIndex = outY * xSize + outX;
+ outputData[outputIndex] = ${precision}(zero / zero);
+ return;
+ }
let xMean: f32 = xSum / N;
let yMean: f32 = ySum / N;
@@ -788,6 +847,12 @@ export const createShaders = (precision: Precision) => {
}
let N: f32 = f32(dimLength - nanCount);
+ if (N <= 1.0) {
+ let zero = 0.0;
+ let outputIndex = outY * xSize + outX;
+ outputData[outputIndex] = ${precision}(zero / zero);
+ return;
+ }
let meanX = xSum / N;
let meanY = ySum / N;
let varX = (xxSum / N) - (meanX * meanX);
@@ -822,7 +887,9 @@ export const createShaders = (precision: Precision) => {
let zOffset = kz * i32(zStride);
let newIdx = i32(globalIdx) + xOffset + yOffset + zOffset;
- sum += f32(inputData[u32(newIdx)]);
+ let val = f32(inputData[u32(newIdx)]);
+ if (isNaN(val)) { continue; }
+ sum += val;
count ++;
}
}
@@ -849,15 +916,22 @@ export const createShaders = (precision: Precision) => {
let zOffset = kz * i32(zStride);
let newIdx = i32(globalIdx) + xOffset + yOffset + zOffset;
let sampledVal = f32(inputData[u32(newIdx)]);
+ if (isNaN(sampledVal)) { continue; }
if (sampledVal < minVal){
minVal = sampledVal;
}
+ count++;
}
}
}
}
- outputData[globalIdx] = ${precision}(minVal);
+ if (count > 0u) {
+ outputData[globalIdx] = ${precision}(minVal);
+ } else {
+ let zero = 0.0;
+ outputData[globalIdx] = ${precision}(zero / zero);
+ }
}
`,
@@ -879,14 +953,21 @@ export const createShaders = (precision: Precision) => {
let zOffset = kz * i32(zStride);
let newIdx = i32(globalIdx) + xOffset + yOffset + zOffset;
let sampledVal = f32(inputData[u32(newIdx)]);
+ if (isNaN(sampledVal)) { continue; }
if (sampledVal > maxVal){
maxVal = sampledVal;
}
+ count++;
}
}
}
}
- outputData[globalIdx] = ${precision}(maxVal);
+ if (count > 0u) {
+ outputData[globalIdx] = ${precision}(maxVal);
+ } else {
+ let zero = 0.0;
+ outputData[globalIdx] = ${precision}(zero / zero);
+ }
}
`,
@@ -1029,9 +1110,7 @@ export const createShaders = (precision: Precision) => {
let xI = f32(firstData[newIdx]);
let yI = f32(secondData[newIdx]);
- if (isNaN(xI) || isNaN(yI)){
- continue;
- }
+ if (isNaN(xI) || isNaN(yI)) { continue; }
xSum += xI;
xxSum += xI * xI;
ySum += yI;
@@ -1044,6 +1123,11 @@ export const createShaders = (precision: Precision) => {
}
let N: f32 = f32(count);
+ if (N <= 1.0) {
+ let zero = 0.0;
+ outputData[globalIdx] = ${precision}(zero / zero);
+ return;
+ }
let meanX = xSum / N;
let meanY = ySum / N;
let varX = (xxSum / N) - (meanX * meanX);
@@ -1132,9 +1216,7 @@ export const createShaders = (precision: Precision) => {
let newIdx = i32(globalIdx) + xOffset + yOffset + zOffset;
let xI = f32(firstData[newIdx]);
let yI = f32(secondData[newIdx]);
- if (isNaN(xI) || isNaN(yI)){
- continue;
- }
+ if (isNaN(xI) || isNaN(yI)) { continue; }
xSum += xI;
ySum += yI;
count ++;
@@ -1144,6 +1226,11 @@ export const createShaders = (precision: Precision) => {
}
let N: f32 = f32(count);
+ if (N <= 1.0) {
+ let zero = 0.0;
+ outputData[globalIdx] = ${precision}(zero / zero);
+ return;
+ }
let meanX = xSum / N;
let meanY = ySum / N;
@@ -1160,11 +1247,8 @@ export const createShaders = (precision: Precision) => {
let newIdx = i32(globalIdx) + xOffset + yOffset + zOffset;
let xI = f32(firstData[newIdx]);
let yI = f32(secondData[newIdx]);
- if (isNaN(xI) || isNaN(yI)){
- continue;
- }
+ if (isNaN(xI) || isNaN(yI)) { continue; }
numSum += (xI - meanX) * (yI - meanY);
- count ++;
}
}
}
@@ -1258,6 +1342,11 @@ export const createShaders = (precision: Precision) => {
let N: f32 = f32(count);
+ if (N <= 1.0) {
+ let zero = 0.0;
+ outputData[globalIdx] = ${precision}(zero / zero);
+ return;
+ }
let meanX = xSum / N;
let meanY = ySum / N;
var numSum: f32 = 0;
@@ -1334,10 +1423,16 @@ export const createShaders = (precision: Precision) => {
if (newVal < minVal){
minVal = newVal;
}
+ count++;
}
}
}
- outputData[globalIdx] = ${precision}(minVal);
+ if (count > 0u) {
+ outputData[globalIdx] = ${precision}(minVal);
+ } else {
+ let zero = 0.0;
+ outputData[globalIdx] = ${precision}(zero / zero);
+ }
}
`,
@@ -1359,10 +1454,16 @@ export const createShaders = (precision: Precision) => {
if (newVal > maxVal){
maxVal = newVal;
}
+ count++;
}
}
}
- outputData[globalIdx] = ${precision}(maxVal);
+ if (count > 0u) {
+ outputData[globalIdx] = ${precision}(maxVal);
+ } else {
+ let zero = 0.0;
+ outputData[globalIdx] = ${precision}(zero / zero);
+ }
}
`,
@@ -1430,7 +1531,7 @@ export const createShaders = (precision: Precision) => {
workGroups: vec3,
};
@group(0) @binding(0) var inputData: array<${precision}>;
- @group(0) @binding(1) var outputData: array;
+ @group(0) @binding(1) var outputData: array<${precision}>;
@group(0) @binding(2) var params: Params;
${isNaNFunc}
@compute @workgroup_size(4, 4, 4)
@@ -1455,6 +1556,7 @@ export const createShaders = (precision: Precision) => {
let totalSize: u32 = xSize * ySize * zSize;
var baseIdx = outZ * zStride + outY * yStride + outX * xStride;
var accum: f32 = 0;
+ var validCount: u32 = 0u;
// Iterate along the dimension we're averaging
if (reduceDim == 0u) { // CUMSUM along Z
@@ -1467,7 +1569,10 @@ export const createShaders = (precision: Precision) => {
newZ = zSize - z - 1;
}
let idx = newZ * zStride + outY * yStride + outX * xStride;
- accum += f32(inputData[idx]);
+ let val = f32(inputData[idx]);
+ if (isNaN(val)) { continue; }
+ accum += val;
+ validCount++;
}
} else if (reduceDim == 1u) { // CUMSUM along Y
@@ -1480,7 +1585,10 @@ export const createShaders = (precision: Precision) => {
newY = ySize - y - 1;
}
let idx = outZ * zStride + newY * yStride + outX * xStride;
- accum += f32(inputData[idx]);
+ let val = f32(inputData[idx]);
+ if (isNaN(val)) { continue; }
+ accum += val;
+ validCount++;
}
} else { // CUMSUM along X
if (reverse == u32(1)){
@@ -1492,12 +1600,21 @@ export const createShaders = (precision: Precision) => {
newX = xSize - x - 1;
}
let idx = outZ * zStride + outY * yStride + newX * xStride;
- accum += f32(inputData[idx]);
+ let val = f32(inputData[idx]);
+ if (isNaN(val)) { continue; }
+ accum += val;
+ validCount++;
}
}
- outputData[baseIdx] = accum;
+ let current_val = f32(inputData[baseIdx]);
+ if (validCount == 0u && isNaN(current_val)) {
+ let zero = 0.0;
+ outputData[baseIdx] = ${precision}(zero / zero);
+ } else {
+ outputData[baseIdx] = ${precision}(accum);
+ }
}
`
}
return allShaders;
-}
+}
\ No newline at end of file
diff --git a/src/components/computation/shaders/frag.glsl b/src/components/computation/shaders/frag.glsl
index 7e1fd2d31..527b1fd97 100644
--- a/src/components/computation/shaders/frag.glsl
+++ b/src/components/computation/shaders/frag.glsl
@@ -4,7 +4,7 @@ uniform sampler2D cmap;
uniform float cOffset;
uniform float cScale;
-varying vec2 vUv;
+in vec2 vUv;
out vec4 Color;
void main() {
diff --git a/src/components/computation/shaders/vert.glsl b/src/components/computation/shaders/vert.glsl
index 14e9b8a60..6b50cb5f8 100644
--- a/src/components/computation/shaders/vert.glsl
+++ b/src/components/computation/shaders/vert.glsl
@@ -1,5 +1,5 @@
// by Jeran Poehls
-varying vec2 vUv;
+out vec2 vUv;
void main() {
vUv = uv;
diff --git a/src/components/computation/webGPU.ts b/src/components/computation/webGPU.ts
index fbbfe21da..aa83cd614 100644
--- a/src/components/computation/webGPU.ts
+++ b/src/components/computation/webGPU.ts
@@ -572,7 +572,7 @@ export async function CUMSUM3D(inputArray : ArrayBufferView, dimInfo : {shape:
const outputBuffer = device.createBuffer({
label: 'Output Buffer',
- size: outputSize * 4,
+ size: outputSize * (hasF16 ? 2 : 4),
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
@@ -584,7 +584,7 @@ export async function CUMSUM3D(inputArray : ArrayBufferView, dimInfo : {shape:
const readBuffer = device.createBuffer({
label:'Read Buffer',
- size: outputSize * 4,
+ size: outputSize * (hasF16 ? 2 : 4),
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
@@ -616,7 +616,7 @@ export async function CUMSUM3D(inputArray : ArrayBufferView, dimInfo : {shape:
encoder.copyBufferToBuffer(
outputBuffer, 0,
readBuffer, 0,
- outputSize * 4
+ outputSize * (hasF16 ? 2 : 4)
);
// Submit work to GPU
@@ -625,7 +625,7 @@ export async function CUMSUM3D(inputArray : ArrayBufferView, dimInfo : {shape:
// Map staging buffer to read results
await readBuffer.mapAsync(GPUMapMode.READ);
const resultArrayBuffer = readBuffer.getMappedRange();
- const results = new Float32Array(resultArrayBuffer.slice());
+ const results = hasF16 ? new Float16Array(resultArrayBuffer.slice()) : new Float16Array(new Float32Array(resultArrayBuffer.slice()));
// Clean up
readBuffer.unmap();
diff --git a/src/components/plots/AxisLines.tsx b/src/components/plots/AxisLines.tsx
index 86d9a67d6..6d64d8f0f 100644
--- a/src/components/plots/AxisLines.tsx
+++ b/src/components/plots/AxisLines.tsx
@@ -53,20 +53,28 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli
hideAxisControls: state.hideAxisControls
})))
+ const { axisMapping } = useZarrStore(useShallow(state => ({
+ axisMapping: state.axisMapping
+ })))
+
const shapeLength = dimArrays.length
+ const xIdx = axisMapping.x >= 0 ? axisMapping.x : shapeLength - 1;
+ const yIdx = axisMapping.y >= 0 ? axisMapping.y : shapeLength - 2;
+ const zIdx = axisMapping.z >= 0 ? axisMapping.z : shapeLength - 3;
+
const dimSlices = useMemo(()=> {
let slices = [
- dimArrays[shapeLength-3].slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined),
- revY ? dimArrays[shapeLength-2].slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined).reverse() : dimArrays[shapeLength-2].slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined),
- dimArrays[shapeLength-1].slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined),
+ dimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [],
+ revY ? dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined).reverse() ?? [] : dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [],
+ dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [],
]
if (coarsen) {
const {kernelDepth, kernelSize} = useZarrStore.getState()
slices = slices.map((val, idx) => coarsenFlatArray(val, (idx === 0 ? kernelDepth : kernelSize)))
}
return slices
- },[revY, dimArrays, zSlice, ySlice, xSlice, coarsen])
+ },[revY, dimArrays, zSlice, ySlice, xSlice, coarsen, axisMapping, shapeLength])
const dimLengths = dimSlices.map(val => val.length)
const [xResolution, setXResolution] = useState(AXIS_CONSTANTS.INITIAL_RESOLUTION)
@@ -114,9 +122,9 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli
const zDimScale = zResolution/(zResolution-1)
const zValDelta = 1/(zResolution-1)
- const xTitleOffset = useMemo(() => (dimNames[shapeLength - 1]?.length * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale]);
- const yTitleOffset = useMemo(() => (dimNames[shapeLength - 2]?.length * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale]);
- const zTitleOffset = useMemo(() => (dimNames[shapeLength - 3]?.length * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale]);
+ const xTitleOffset = useMemo(() => ((dimNames[xIdx]?.length || 0) * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale, xIdx]);
+ const yTitleOffset = useMemo(() => ((dimNames[yIdx]?.length || 0) * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale, yIdx]);
+ const zTitleOffset = useMemo(() => ((dimNames[zIdx]?.length || 0) * AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR / 2 + 0.1) * globalScale, [dimNames, globalScale, zIdx]);
return (
@@ -140,7 +148,7 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli
material-depthTest={false}
rotation={[-Math.PI/2, 0, flipX ? Math.PI : 0]}
position={[0, 0, flipX ? -AXIS_CONSTANTS.TICK_LENGTH_FACTOR*globalScale : AXIS_CONSTANTS.TICK_LENGTH_FACTOR*globalScale]}
- >{parseLoc(dimSlices[2][Math.floor((dimLengths[2]-1)*idx*xValDelta)],dimUnits[shapeLength - 1])}
+ >{parseLoc(dimSlices[2]?.[Math.floor((dimLengths[2]-1)*idx*xValDelta)] || 0,dimUnits[xIdx])}
))}
@@ -151,7 +159,7 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli
fontSize={AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR*globalScale}
color={colorHex}
material-depthTest={false}
- >{dimNames[shapeLength - 1]}
+ >{dimNames[xIdx]}
{xResolution < AXIS_CONSTANTS.MAX_RESOLUTION &&
{parseLoc(dimSlices[0][(Math.floor((dimLengths[0]-1)*idx*zValDelta)+Math.floor(dimLengths[0]*animProg))%dimLengths[0]],dimUnits[shapeLength - 3])}
+ >{parseLoc(dimSlices[0]?.[(Math.floor((dimLengths[0]-1)*idx*zValDelta)+Math.floor(dimLengths[0]*animProg))%dimLengths[0]] || 0,dimUnits[zIdx])}
))}
@@ -215,7 +223,7 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli
fontSize={AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR*globalScale}
color={colorHex}
material-depthTest={false}
- >{dimNames[shapeLength - 3]}
+ >{dimNames[zIdx]}
{zResolution < AXIS_CONSTANTS.MAX_RESOLUTION &&
@@ -270,7 +278,7 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli
material-depthTest={false}
rotation={[0, flipX ? Math.PI : 0, 0]}
position={[flipY ? -0.07*globalScale : 0.07*globalScale, 0, 0]}
- >{parseLoc(dimSlices[1][Math.floor((dimLengths[1]-1)*idx*yValDelta)],dimUnits[shapeLength - 2])}
+ >{parseLoc(dimSlices[1]?.[Math.floor((dimLengths[1]-1)*idx*yValDelta)] || 0,dimUnits[yIdx])}
))}
@@ -283,7 +291,7 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli
material-depthTest={false}
rotation={[0, 0, Math.PI / 2]}
>
- {dimNames[shapeLength - 2]}
+ {dimNames[yIdx]}
@@ -364,22 +372,30 @@ const FlatAxis = () =>{
analysisMode: state.analysisMode,
axis: state.axis
})))
+ const { axisMapping } = useZarrStore(useShallow(state => ({
+ axisMapping: state.axisMapping
+ })))
const shapeLength = dimArrays.length;
const is4D = dimArrays.length === 4;
const originallyFlat = dimArrays.length == 2;
const slices = originallyFlat ? [ySlice, xSlice] : [zSlice, ySlice, xSlice]
-
- const dimSlices = useMemo(()=>originallyFlat ?
+ const xIdx = (axisMapping.x >= 0 && axisMapping.x < shapeLength) ? axisMapping.x : shapeLength - 1;
+ const yIdx = (axisMapping.y >= 0 && axisMapping.y < shapeLength) ? axisMapping.y : Math.max(0, shapeLength - 2);
+ const zIdx = (axisMapping.z >= 0 && axisMapping.z < shapeLength) ? axisMapping.z : Math.max(0, shapeLength - 3);
+
+ const dimSlices = useMemo(()=> {
+ return originallyFlat ?
[
- flipY ? dimArrays[0].slice().reverse() : dimArrays[0], // Need the slice because inside useMemo it doesn't mutate the original properly
- dimArrays[1]
+ flipY ? dimArrays[yIdx]?.slice().reverse() ?? [] : dimArrays[yIdx] ?? [],
+ dimArrays[xIdx] ?? []
]
:
[
- dimArrays[shapeLength-3].slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined),
- flipY ? dimArrays[shapeLength-2].slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined).reverse() : dimArrays[shapeLength-2].slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined),
- dimArrays[shapeLength-1].slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined),
- ],[dimArrays, flipY])
+ dimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [],
+ flipY ? dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined).reverse() ?? [] : dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [],
+ dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [],
+ ]
+ },[dimArrays, flipY, axisMapping, originallyFlat, shapeLength, xSlice, ySlice, zSlice])
const dimLengths = useMemo(()=>{
if (analysisMode && !originallyFlat){
@@ -399,34 +415,31 @@ const FlatAxis = () =>{
const { axisArrays, axisUnits, axisNames } = useMemo(() => {
- if (analysisMode && !originallyFlat) {
+ if (originallyFlat) {
return {
- axisArrays: dimSlices.filter((_val, idx) => idx != axis),
- axisUnits: is4D
- ? dimUnits.slice(1).filter((_val, idx) => idx != axis)
- : dimUnits.filter((_val, idx) => idx != axis),
- axisNames: is4D
- ? dimNames.slice(1).filter((_val, idx) => idx != axis)
- : dimNames.filter((_val, idx) => idx != axis)
+ axisArrays: dimSlices,
+ axisUnits: [dimUnits[yIdx], dimUnits[xIdx]],
+ axisNames: [dimNames[yIdx], dimNames[xIdx]],
};
- } else if (!originallyFlat) {
+ }
+
+ const baseUnits = [dimUnits[zIdx], dimUnits[yIdx], dimUnits[xIdx]];
+ const baseNames = [dimNames[zIdx], dimNames[yIdx], dimNames[xIdx]];
+
+ if (analysisMode) {
return {
- axisArrays: dimSlices,
- axisUnits: is4D
- ? dimUnits.slice(1)
- : dimUnits,
- axisNames: is4D
- ? dimNames.slice(1)
- : dimNames
+ axisArrays: dimSlices.filter((_val, idx) => idx != axis),
+ axisUnits: baseUnits.filter((_val, idx) => idx != axis),
+ axisNames: baseNames.filter((_val, idx) => idx != axis),
};
} else {
return {
axisArrays: dimSlices,
- axisUnits: dimUnits,
- axisNames: dimNames,
+ axisUnits: baseUnits,
+ axisNames: baseNames,
};
}
- }, [analysisMode, dimArrays, dimUnits, is4D, dimNames, dimSlices]);
+ }, [analysisMode, dimArrays, dimUnits, dimNames, dimSlices, originallyFlat, axisMapping, shapeLength, axis]);
const shapeRatio = useMemo(()=>{
@@ -462,8 +475,8 @@ const FlatAxis = () =>{
const yDimScale = yResolution/(yResolution-1)
const yValDelta = 1/(yResolution-1)
- const xTitleOffset = useMemo(() => (axisNames[widthIdx].length * FLAT_AXIS_CONSTANTS.TITLE_FONT_SIZE / 2 + 0.1), [axisNames, widthIdx]);
- const yTitleOffset = useMemo(() => (axisNames[heightIdx].length * FLAT_AXIS_CONSTANTS.TITLE_FONT_SIZE / 2 + 0.1), [axisNames, heightIdx]);
+ const xTitleOffset = useMemo(() => ((axisNames[widthIdx]?.length || 0) * FLAT_AXIS_CONSTANTS.TITLE_FONT_SIZE / 2 + 0.1), [axisNames, widthIdx]);
+ const yTitleOffset = useMemo(() => ((axisNames[heightIdx]?.length || 0) * FLAT_AXIS_CONSTANTS.TITLE_FONT_SIZE / 2 + 0.1), [axisNames, heightIdx]);
return (
{
+export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => {
+ const volTexture = usePaddedTextures(propVolTexture);
const {shape, colormap, flipY, textureArrayDepths} = useGlobalStore(useShallow(state=>({
shape:state.shape,
colormap:state.colormap,
@@ -47,7 +49,7 @@ export const DataCube = ({ volTexture }: DataCubeProps ) => {
glslVersion: THREE.GLSL3,
uniforms: {
modelViewMatrixInverse: { value: new THREE.Matrix4() }, // Used for Orthographic RayMarcher
- map: { value: Array.from({ length: 14 }, (_, idx) => volTexture?.[idx])},
+ map: { value: volTexture},
maskTexture: { value: maskTexture },
maskValue: {value: maskValue },
textureDepths: {value: new THREE.Vector3(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0])},
@@ -69,6 +71,10 @@ export const DataCube = ({ volTexture }: DataCubeProps ) => {
nanColor: {value: new THREE.Color(nanColor)},
fillValue: {value: fillValue?? NaN}
},
+ defines: {
+ USE_VORIGIN: 1,
+ USE_VDIRECTION: 1
+ },
vertexShader: useOrtho ? orthoVertex : vertexShader,
fragmentShader: useFragOpt ? fragOpt : fragmentShader,
transparent: true,
diff --git a/src/components/plots/FlatBlocks.tsx b/src/components/plots/FlatBlocks.tsx
index 2bddce356..83696fd0b 100644
--- a/src/components/plots/FlatBlocks.tsx
+++ b/src/components/plots/FlatBlocks.tsx
@@ -10,8 +10,10 @@ import { invalidate } from '@react-three/fiber'
import { deg2rad } from '@/utils/HelperFuncs'
import { useCoordBounds } from '@/hooks/useCoordBounds'
import { GetVert } from '../textures/GetVert';
+import { usePaddedTextures } from '@/hooks/usePaddedTextures';
-const FlatBlocks = ({textures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | null}) => {
+const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | null}) => {
+ const textures = usePaddedTextures(propTextures);
const {colormap, isFlat, valueScales, flipY,
dataShape, textureArrayDepths} = useGlobalStore(useShallow(state=>({
colormap: state.colormap,
diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx
index 9d86eca75..0894d92e3 100644
--- a/src/components/plots/FlatMap.tsx
+++ b/src/components/plots/FlatMap.tsx
@@ -14,6 +14,7 @@ import { evaluate_cmap } from 'js-colormaps-es';
import { useCoordBounds } from '@/hooks/useCoordBounds';
import { GetFrag } from '../textures';
import { SquareMeshes } from './TransectMeshes';
+import { usePaddedTextures } from '@/hooks/usePaddedTextures';
interface InfoSettersProps{
setLoc: React.Dispatch>;
setShowInfo: React.Dispatch>;
@@ -21,7 +22,8 @@ interface InfoSettersProps{
coords: React.RefObject;
}
-const FlatMap = ({textures, infoSetters} : {textures : THREE.DataTexture[] | THREE.Data3DTexture[], infoSetters : InfoSettersProps}) => {
+const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataTexture[] | THREE.Data3DTexture[], infoSetters : InfoSettersProps}) => {
+ const textures = usePaddedTextures(propTextures);
const {setLoc, setShowInfo, val, coords} = infoSetters;
const {flipY, colormap, dimArrays, dimNames, dimUnits,
isFlat, dataShape, textureArrayDepths, strides,
@@ -54,27 +56,31 @@ const FlatMap = ({textures, infoSetters} : {textures : THREE.DataTexture[] | THR
analysisMode: state.analysisMode,
analysisArray: state.analysisArray
})))
- const {kernelSize, kernelDepth} = useZarrStore(useShallow(state => ({
+ const {kernelSize, kernelDepth, axisMapping} = useZarrStore(useShallow(state => ({
kernelSize: state.kernelSize,
- kernelDepth: state.kernelDepth
+ kernelDepth: state.kernelDepth,
+ axisMapping: state.axisMapping
})))
const shapeLength = dimArrays.length
const dimSlices = useMemo (() => {
- let slices = dimArrays.length === 2
+ const xIdx = axisMapping.x >= 0 ? axisMapping.x : shapeLength - 1;
+ const yIdx = axisMapping.y >= 0 ? axisMapping.y : shapeLength - 2;
+ const zIdx = axisMapping.z >= 0 ? axisMapping.z : shapeLength - 3;
+ let slices = isFlat
? [
- dimArrays[0].slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined),
- dimArrays[1].slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined),
+ dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [],
+ dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [],
]
: [
- dimArrays[shapeLength - 3].slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined),
- dimArrays[shapeLength - 2].slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined),
- dimArrays[shapeLength - 1].slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined )
+ dimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [],
+ dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [],
+ dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined ) ?? [],
]
- if (coarsen) slices = slices.map((val, idx) => coarsenFlatArray(val, (idx === 0 ? kernelDepth : kernelSize)))
+ if (coarsen) slices = slices.map((val, idx) => coarsenFlatArray(val, (idx === 0 && slices.length > 2 ? kernelDepth : kernelSize)))
return slices
- } ,[dimArrays, zSlice, ySlice, xSlice, coarsen])
+ } ,[dimArrays, zSlice, ySlice, xSlice, coarsen, axisMapping, shapeLength, kernelDepth, kernelSize])
const shapeRatio = useMemo(()=> {
if (dataShape.length == 2){
@@ -91,7 +97,20 @@ const FlatMap = ({textures, infoSetters} : {textures : THREE.DataTexture[] | THR
const lastUV = useRef(new THREE.Vector2(0,0))
const rotateMap = analysisMode && axis == 2;
const sampleArray = useMemo(()=> analysisMode ? analysisArray : GetCurrentArray(),[analysisMode, analysisArray, textures])
- const analysisDims = useMemo(()=>dimArrays.length > 2 ? dimSlices.filter((_e,idx)=> idx != axis) : dimSlices,[dimSlices,axis])
+ const analysisDims = useMemo(() => {
+ if (!analysisMode) return dimSlices;
+ const zIdx = axisMapping.z >= 0 ? axisMapping.z : shapeLength - 3;
+ const yIdx = axisMapping.y >= 0 ? axisMapping.y : shapeLength - 2;
+ const xIdx = axisMapping.x >= 0 ? axisMapping.x : shapeLength - 1;
+ const fullSlices = [
+ dimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [],
+ dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [],
+ dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [],
+ ];
+ let slices = fullSlices.filter((_, idx) => idx !== axis);
+ if (coarsen) slices = slices.map((val, idx) => coarsenFlatArray(val, (idx === 0 && slices.length > 2 ? kernelDepth : kernelSize)))
+ return slices;
+ }, [analysisMode, dimSlices, dimArrays, zSlice, ySlice, xSlice, axisMapping, shapeLength, axis, coarsen, kernelDepth, kernelSize])
const {lonBounds, latBounds} = useCoordBounds()
@@ -107,8 +126,10 @@ const FlatMap = ({textures, infoSetters} : {textures : THREE.DataTexture[] | THR
setLoc([e.clientX, e.clientY]);
lastUV.current = e.uv;
const { x, y } = e.uv;
- const xSize = isFlat ? (analysisMode ? analysisDims[1].length : dimSlices[1].length) : dimSlices[2].length;
- const ySize = isFlat ? (analysisMode ? analysisDims[0].length : dimSlices[0].length) : dimSlices[1].length;
+ const zSliceIdx = dimSlices.length > 2 ? 2 : 1;
+ const ySliceIdx = dimSlices.length > 2 ? 1 : 0;
+ const xSize = isFlat ? (analysisMode ? analysisDims[1].length : dimSlices[1].length) : dimSlices[zSliceIdx].length;
+ const ySize = isFlat ? (analysisMode ? analysisDims[0].length : dimSlices[0].length) : dimSlices[ySliceIdx].length;
const xIdx = Math.round(x*xSize-.5)
const yIdx = Math.round(y*ySize-.5)
@@ -116,7 +137,7 @@ const FlatMap = ({textures, infoSetters} : {textures : THREE.DataTexture[] | THR
dataIdx += isFlat ? 0 : Math.floor((dimSlices[0].length-1) * animProg) * xSize*ySize
const dataVal = sampleArray ? sampleArray[dataIdx] : 0;
val.current = dataVal;
- coords.current = isFlat ? analysisMode ? [analysisDims[0][yIdx], analysisDims[1][xIdx]] : [dimSlices[0][yIdx], dimSlices[1][xIdx]] : [dimSlices[1][yIdx], dimSlices[2][xIdx]]
+ coords.current = isFlat ? analysisMode ? [analysisDims[0][yIdx], analysisDims[1][xIdx]] : [dimSlices[0][yIdx], dimSlices[1][xIdx]] : [dimSlices[ySliceIdx][yIdx], dimSlices[zSliceIdx][xIdx]]
}
}
@@ -168,7 +189,7 @@ const FlatMap = ({textures, infoSetters} : {textures : THREE.DataTexture[] | THR
uniforms:{
cScale: {value: cScale},
cOffset: {value: cOffset},
- map : {value: Array.from({ length: 14 }, (_, idx) => textures?.[idx])},
+ map : {value: textures},
maskTexture: {value: maskTexture},
maskValue: {value: maskValue},
threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])},
diff --git a/src/components/plots/Plot.tsx b/src/components/plots/Plot.tsx
index 344d8ef94..9abfbe56c 100644
--- a/src/components/plots/Plot.tsx
+++ b/src/components/plots/Plot.tsx
@@ -160,17 +160,19 @@ const Orbiter = ({isFlat} : {isFlat : boolean}) =>{
}
const Plot = () => {
- const {colormap, isFlat, DPR, valueScales, setIsFlat} = useGlobalStore(useShallow(state=>({
+ const {colormap, isFlat, DPR, valueScales, setIsFlat, dataShape} = useGlobalStore(useShallow(state=>({
colormap: state.colormap,
isFlat: state.isFlat,
DPR: state.DPR,
valueScales: state.valueScales,
setIsFlat: state.setIsFlat,
+ dataShape: state.dataShape,
})))
const {keyFrameEditor} = useImageExportStore(useShallow(state => ({ keyFrameEditor:state.keyFrameEditor})))
- const {plotType, displaceSurface} = usePlotStore(useShallow(state => ({
+ const {plotType, displaceSurface, setPlotType} = usePlotStore(useShallow(state => ({
plotType: state.plotType,
displaceSurface: state.displaceSurface,
+ setPlotType: state.setPlotType,
})))
const {analysisMode, useEditor} = useAnalysisStore(useShallow(state => ({
analysisMode: state.analysisMode,
@@ -185,6 +187,18 @@ const Plot = () => {
//DATA LOADING
const {textures, show, stableMetadata, setTextures} = useDataFetcher()
+ useEffect(()=>{
+ if (analysisMode) return;
+ const isEffectivelyFlat = dataShape.length === 2 || (dataShape.length === 3 && dataShape.includes(1));
+ if (isEffectivelyFlat && plotType != "flat" && plotType != "sphere"){
+ setPlotType("flat")
+ setIsFlat(true)
+ } else if (!isEffectivelyFlat && plotType != "volume" && plotType != "isosurface") {
+ setPlotType("volume")
+ setIsFlat(false)
+ }
+ },[dataShape, analysisMode])
+
useEffect(()=>{ // Reset after analysis mode
if(!analysisMode && show){
const {dataShape} = useGlobalStore.getState();
diff --git a/src/components/plots/Sphere.tsx b/src/components/plots/Sphere.tsx
index a9eb589e4..f8d15b01b 100644
--- a/src/components/plots/Sphere.tsx
+++ b/src/components/plots/Sphere.tsx
@@ -9,6 +9,8 @@ import { evaluate_cmap } from 'js-colormaps-es';
import { useCoordBounds } from '@/hooks/useCoordBounds'
import { GetFrag, GetVert } from '../textures';
import { SquareMeshes } from './TransectMeshes';
+import { useZarrStore } from '@/GlobalStates/ZarrStore';
+import { usePaddedTextures } from '@/hooks/usePaddedTextures';
function XYZtoRemap(xyz : THREE.Vector3, latBounds: number[], lonBounds : number[]){
const lon = Math.atan2(xyz.z,xyz.x)
const lat = Math.asin(xyz.y);
@@ -17,7 +19,8 @@ function XYZtoRemap(xyz : THREE.Vector3, latBounds: number[], lonBounds : number
return new THREE.Vector2(1-u,v)
}
-export const Sphere = ({textures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | null}) => {
+export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | null}) => {
+ const textures = usePaddedTextures(propTextures);
const {setPlotDim,updateDimCoords, updateTimeSeries} = useGlobalStore(useShallow(state=>({
setPlotDim:state.setPlotDim,
updateDimCoords:state.updateDimCoords,
@@ -62,10 +65,17 @@ export const Sphere = ({textures} : {textures: THREE.Data3DTexture[] | THREE.Dat
getColorIdx: state.getColorIdx,
incrementColorIdx: state.incrementColorIdx
})))
+ const { axisMapping } = useZarrStore(useShallow(state => ({
+ axisMapping: state.axisMapping
+ })))
+ const shapeLength = dimArrays.length;
+ const xIdx = axisMapping.x >= 0 ? axisMapping.x : shapeLength - 1;
+ const yIdx = axisMapping.y >= 0 ? axisMapping.y : shapeLength - 2;
+ const zIdx = axisMapping.z >= 0 ? axisMapping.z : shapeLength - 3;
const dimSlices = [
- dimArrays[0].slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined),
- dimArrays[1].slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined),
- dimArrays.length > 2 ? dimArrays[2].slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) : [],
+ dimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [],
+ dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [],
+ dimArrays.length > 2 ? (dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? []) : [],
]
const {lonBounds, latBounds} = useCoordBounds()
@@ -75,7 +85,7 @@ export const Sphere = ({textures} : {textures: THREE.Data3DTexture[] | THREE.Dat
const shader = new THREE.ShaderMaterial({
glslVersion: THREE.GLSL3,
uniforms: {
- map: { value: Array.from({ length: 14 }, (_, idx) => textures?.[idx]) },
+ map: { value: textures },
maskTexture: { value: maskTexture},
maskValue: { value: maskValue },
threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])},
diff --git a/src/components/plots/SphereBlocks.tsx b/src/components/plots/SphereBlocks.tsx
index d32641ffb..ef9dad6ca 100644
--- a/src/components/plots/SphereBlocks.tsx
+++ b/src/components/plots/SphereBlocks.tsx
@@ -9,7 +9,9 @@ import { invalidate } from '@react-three/fiber'
import { deg2rad } from '@/utils/HelperFuncs'
import { useCoordBounds } from '@/hooks/useCoordBounds'
import { GetVert } from '../textures';
-const SphereBlocks = ({textures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | null}) => {
+import { usePaddedTextures } from '@/hooks/usePaddedTextures';
+const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | null}) => {
+ const textures = usePaddedTextures(propTextures);
const {colormap, isFlat, valueScales,
dataShape, textureArrayDepths} = useGlobalStore(useShallow(state=>({
colormap: state.colormap,
diff --git a/src/components/plots/plotarea/FixedTicks.tsx b/src/components/plots/plotarea/FixedTicks.tsx
index 04eecc8cd..924a24375 100644
--- a/src/components/plots/plotarea/FixedTicks.tsx
+++ b/src/components/plots/plotarea/FixedTicks.tsx
@@ -5,6 +5,7 @@ import { parseTimeUnit } from '@/utils/HelperFuncs'
import { Fragment } from 'react'
import { useGlobalStore } from '@/GlobalStates/GlobalStore';
import { usePlotStore } from '@/GlobalStates/PlotStore';
+import { useZarrStore } from '@/GlobalStates/ZarrStore';
import { useShallow } from 'zustand/shallow'
@@ -51,11 +52,20 @@ export function FixedTicks({
ySlice: state.ySlice,
xSlice: state.xSlice
})))
- const dimSlices = useMemo(() => [
- dimArrays[0].slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined),
- dimArrays[1].slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined),
- dimArrays[2].slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined),
- ], [dimArrays, xSlice, ySlice, zSlice])
+ const { axisMapping } = useZarrStore(useShallow(state => ({
+ axisMapping: state.axisMapping
+ })))
+ const shapeLength = dimArrays.length;
+ const dimSlices = useMemo(() => {
+ const xIdx = axisMapping.x >= 0 ? axisMapping.x : shapeLength - 1;
+ const yIdx = axisMapping.y >= 0 ? axisMapping.y : shapeLength - 2;
+ const zIdx = axisMapping.z >= 0 ? axisMapping.z : shapeLength - 3;
+ return [
+ dimArrays[zIdx]?.slice(zSlice[0], zSlice[1] ? zSlice[1] : undefined) ?? [],
+ dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [],
+ dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [],
+ ]
+ }, [dimArrays, xSlice, ySlice, zSlice, axisMapping, shapeLength])
const xDimArray = useMemo(() => dimSlices[plotDim], [dimSlices, plotDim])
const xTickCount = 10;
const yTickCount = 8;
diff --git a/src/components/plots/plotarea/ThickLine.tsx b/src/components/plots/plotarea/ThickLine.tsx
index 57fafc231..92350f72f 100644
--- a/src/components/plots/plotarea/ThickLine.tsx
+++ b/src/components/plots/plotarea/ThickLine.tsx
@@ -71,7 +71,7 @@ const ThickLine = ({height, xScale, yScale, pointSetters} : ThickLineProps) => {
uniform bool useMapColors;
uniform vec3 lineColor;
uniform vec3 userColor;
- varying float vNormed;
+ in float vNormed;
void main() {
vec4 texColor = texture(cmap, vec2(vNormed, 0.1));
diff --git a/src/components/textures/shaders/LandingVertex.glsl b/src/components/textures/shaders/LandingVertex.glsl
index fcc021b18..95a703b87 100644
--- a/src/components/textures/shaders/LandingVertex.glsl
+++ b/src/components/textures/shaders/LandingVertex.glsl
@@ -9,7 +9,7 @@ attribute vec3 aSpherePosition;
attribute vec3 aCubePosition;
attribute vec3 aPlanePosition;
-varying vec3 vColor;
+out vec3 vColor;
void main() {
// Linearly interpolate between the three shapes using the mix uniforms
diff --git a/src/components/textures/shaders/LineVert.glsl b/src/components/textures/shaders/LineVert.glsl
index e5df81167..c5e0a76d4 100644
--- a/src/components/textures/shaders/LineVert.glsl
+++ b/src/components/textures/shaders/LineVert.glsl
@@ -3,7 +3,7 @@ attribute vec3 next;
attribute vec3 previous;
attribute float normed;
-varying float vNormed;
+out float vNormed;
uniform float aspect;
uniform float thickness;
diff --git a/src/components/textures/shaders/flatFrag.glsl b/src/components/textures/shaders/flatFrag.glsl
index bab398755..ba49450b9 100644
--- a/src/components/textures/shaders/flatFrag.glsl
+++ b/src/components/textures/shaders/flatFrag.glsl
@@ -21,7 +21,7 @@ uniform vec2 threshold;
uniform int maskValue;
uniform float fillValue;
-varying vec2 vUv;
+in vec2 vUv;
out vec4 Color;
#define epsilon 0.0001
#define PI 3.14159265
diff --git a/src/components/textures/shaders/sphereBlocksFrag.glsl b/src/components/textures/shaders/sphereBlocksFrag.glsl
index 48ed12144..a0c9f8ace 100644
--- a/src/components/textures/shaders/sphereBlocksFrag.glsl
+++ b/src/components/textures/shaders/sphereBlocksFrag.glsl
@@ -2,7 +2,7 @@ uniform sampler2D cmap;
uniform float cOffset;
uniform float cScale;
-varying float vStrength;
+in float vStrength;
out vec4 Color;
diff --git a/src/components/textures/shaders/sphereVertex.glsl b/src/components/textures/shaders/sphereVertex.glsl
index dffa7f30e..94a727592 100644
--- a/src/components/textures/shaders/sphereVertex.glsl
+++ b/src/components/textures/shaders/sphereVertex.glsl
@@ -52,10 +52,10 @@ float sample1(
out vec3 aPosition;
void main() {
- vec2 uv = giveUV(position); // We can't just pass this as a varying because the fragment will try to interpoalte between the seems which looks bad
+ vec2 texCoord = giveUV(position); // We can't just pass this as a varying because the fragment will try to interpoalte between the seems which looks bad
int yStepSize = int(textureDepths.x);
- bool inBounds = all(greaterThanEqual(uv, vec2(0.0))) &&
- all(lessThanEqual(uv, vec2(1.0)));
+ bool inBounds = all(greaterThanEqual(texCoord, vec2(0.0))) &&
+ all(lessThanEqual(texCoord, vec2(1.0)));
aPosition = position;
if (inBounds){
vec3 normal = normalize(position);
diff --git a/src/components/textures/shaders/thickLineVert.glsl b/src/components/textures/shaders/thickLineVert.glsl
index c27a6e100..6314e147b 100644
--- a/src/components/textures/shaders/thickLineVert.glsl
+++ b/src/components/textures/shaders/thickLineVert.glsl
@@ -3,7 +3,7 @@ attribute vec3 next;
attribute vec3 previous;
attribute float normed;
-varying float vNormed;
+out float vNormed;
uniform float zoom;
uniform float thickness;
diff --git a/src/components/textures/shaders/vertex.glsl b/src/components/textures/shaders/vertex.glsl
index 5c0b968e1..2c9aa0a14 100644
--- a/src/components/textures/shaders/vertex.glsl
+++ b/src/components/textures/shaders/vertex.glsl
@@ -1,17 +1,41 @@
// by Jeran Poehls
+#ifdef USE_VORIGIN
out vec3 vOrigin;
+#endif
+
+#ifdef USE_VDIRECTION
out vec3 vDirection;
+#endif
+
+#ifdef USE_APOSITION
out vec3 aPosition;
+#endif
+#ifdef USE_VUV
out vec2 Vuv;
+#endif
void main() {
vec4 worldPos = modelViewMatrix * vec4( position, 1.0 );
+#ifdef USE_APOSITION
aPosition = position; //Pass out position for sphere frag
+#endif
+
+#ifdef USE_VORIGIN
vOrigin = vec3( inverse( modelMatrix ) * vec4( cameraPosition, 1.0 ) ).xyz;
+#endif
+
+#ifdef USE_VDIRECTION
+#ifdef USE_VORIGIN
vDirection = position - vOrigin;
+#endif
+#endif
+
+#ifdef USE_VUV
Vuv = uv;
+#endif
+
gl_Position = projectionMatrix * worldPos;
}
\ No newline at end of file
diff --git a/src/components/ui/Colorbar.tsx b/src/components/ui/Colorbar.tsx
index 5ef738646..561423e30 100644
--- a/src/components/ui/Colorbar.tsx
+++ b/src/components/ui/Colorbar.tsx
@@ -251,7 +251,7 @@ const Colorbar = ({units, metadata, valueScales} : {units: string, metadata: Rec
left:'50%',
transform:'translateX(-50%)',
}}>
- {}
+ {}
{`${analysisString}`}
{(cScale != 1 || cOffset != 0) && = {
+ scalar: 'border-l-teal-700',
+ slice: 'border-l-[#644FF0]',
+};
+
+export interface DimOption {
+ name: string;
+ label?: string;
+ size: number;
+ values?: number[];
+ formatValue?: (value: number) => string;
+}
+
+export interface DimSlicerProps {
+ availableDims: DimOption[];
+ dimName: string;
+ onDimChange: (dimName: string) => void;
+ onRemove?: () => void;
+ dimSize: number;
+ selection: SliceSelectionState;
+ onChange: (next: SliceSelectionState) => void;
+ step?: number;
+ axis?: Axis;
+ onAxisChange?: (axis: Axis) => void;
+ values?: number[];
+ formatValue?: (value: number) => string;
+ /** If set, locks the mode and hides the mode toggle */
+ lockMode?: SelectionMode;
+ /** If set, restricts which axes are shown in the axis toggle */
+ allowedAxes?: Axis[];
+}
+
+const DimSlicer: React.FC = ({
+ availableDims,
+ dimName,
+ onDimChange,
+ onRemove,
+ dimSize,
+ selection,
+ onChange,
+ step = 1,
+ axis: propAxis = 'x',
+ onAxisChange,
+ values,
+ formatValue,
+ lockMode,
+ allowedAxes,
+}) => {
+ // const [currentAxis, setCurrentAxis] = useState(propAxis);
+ const effectiveDimSize = values ? values.length : dimSize;
+ const rawSel = selection ?? defaultSelection(effectiveDimSize);
+ const sel = lockMode ? { ...rawSel, mode: lockMode } : rawSel;
+
+ const getIndexFromValue = (val: number): number => {
+ if (!values || values.length === 0) {
+ return clamp(Math.round(val / step) * step, 0, maxIndex);
+ }
+ let closestIndex = 0;
+ let minDiff = Math.abs(Number(values[0]) - val);
+ for (let i = 1; i < values.length; i++) {
+ const diff = Math.abs(Number(values[i]) - val);
+ if (diff < minDiff) {
+ minDiff = diff;
+ closestIndex = i;
+ }
+ }
+ return closestIndex;
+ };
+
+ const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(value, max));
+
+ const parseOr = (v: string, fallback: number) => {
+ const n = parseInt(v, 10);
+ return Number.isNaN(n) ? fallback : n;
+ };
+
+ const maxIndex = Math.max(effectiveDimSize - 1, 0);
+
+ const changeScalarBy = (delta: number) => {
+ let val = parseOr(sel.scalar, 0) + delta;
+ val = clamp(val, 0, maxIndex);
+ onChange({ ...sel, scalar: String(val) });
+ };
+
+ const changeStartBy = (delta: number) => {
+ let val = parseOr(sel.start, 0) + delta;
+ val = clamp(val, 0, maxIndex);
+ onChange({ ...sel, start: String(val) });
+ };
+
+ const changeStopBy = (delta: number) => {
+ let val = parseOr(sel.stop, maxIndex) + delta;
+ val = clamp(val, 0, maxIndex);
+ onChange({ ...sel, stop: String(val) });
+ };
+
+ const updateSelection = (patch: Partial) => {
+ const next = { ...sel, ...patch };
+ if (lockMode) next.mode = lockMode;
+ onChange(next);
+ };
+
+ const startIndex = clamp(parseOr(sel.start, 0), 0, maxIndex);
+ const stopIndex = clamp(parseOr(sel.stop, maxIndex), 0, maxIndex);
+ const scalarIndex = clamp(parseOr(sel.scalar, 0), 0, maxIndex);
+
+ const startValue = values && effectiveDimSize > 0 && startIndex < values.length ? String(values[startIndex]) : sel.start;
+ const stopValue = values && effectiveDimSize > 0 && stopIndex < values.length ? String(values[stopIndex]) : sel.stop;
+ const scalarValue = values && effectiveDimSize > 0 && scalarIndex < values.length ? String(values[scalarIndex]) : sel.scalar;
+
+ const formattedValue = useCallback(
+ (index: number) =>
+ values && effectiveDimSize > 0 && index < values.length
+ ? String(formatValue ? formatValue(values[index]) : values[index].toString())
+ : String(index),
+ [values, effectiveDimSize, formatValue]
+ );
+
+ const isTimeDimension = dimName.toLowerCase().includes('time');
+ const isDateDimension = isTimeDimension || dimName.toLowerCase().includes('date');
+ const showTimeControls = Boolean(values && isTimeDimension);
+
+ return (
+
+
+ {onRemove && (
+
+ )}
+
+ {/* Top row: dim select + mode toggle + axis toggle */}
+
+
+
+
+ {!lockMode && (
+ updateSelection({ mode })}
+ />
+ )}
+ {sel.mode === 'slice' && (
+
+ {propAxis}
+
+ )}
+
+
+
+ {/* Slider */}
+ {sel.mode === 'slice' && (
+
+
+ updateSelection({ start: String(newStart), stop: String(newStop) })
+ }
+ className="w-full cursor-pointer [&_[data-slot=slider-track]]:h-0.5 [&_[data-slot=slider-thumb]]:h-3 [&_[data-slot=slider-thumb]]:w-3"
+ />
+
+ )}
+
+ {sel.mode === 'scalar' && (
+
+ updateSelection({ scalar: String(val) })}
+ className="w-full cursor-pointer [&_[data-slot=slider-range]]:bg-transparent [&_[data-slot=slider-track]]:h-0.5 [&_[data-slot=slider-thumb]]:h-3 [&_[data-slot=slider-thumb]]:w-3"
+ />
+
+ )}
+
+ {/* Bottom controls */}
+ {isDateDimension ? (
+ sel.mode === 'scalar' ? (
+
+ updateSelection({ scalar: String(newScalar) })}
+ value={scalarValue}
+ placeholder={formattedValue(0)}
+ ariaLabel="Scalar value"
+ values={values ?? []}
+ effectiveDimSize={effectiveDimSize}
+ formattedValue={formattedValue}
+ onValueChange={value => {
+ const parsed = parseFloat(value);
+ if (!Number.isNaN(parsed)) updateSelection({ scalar: String(getIndexFromValue(parsed)) });
+ }}
+ onIncrement={() => changeScalarBy(+1)}
+ onDecrement={() => changeScalarBy(-1)}
+ />
+
+ ) : (
+
+
updateSelection({ start: String(newStart) })}
+ value={startValue}
+ placeholder={formattedValue(0)}
+ ariaLabel="Start value"
+ values={values ?? []}
+ effectiveDimSize={effectiveDimSize}
+ formattedValue={formattedValue}
+ onValueChange={value => {
+ const parsed = parseFloat(value);
+ if (!Number.isNaN(parsed)) updateSelection({ start: String(getIndexFromValue(parsed)) });
+ }}
+ onIncrement={() => changeStartBy(+1)}
+ onDecrement={() => changeStartBy(-1)}
+ />
+
+ updateSelection({ stop: String(newStop) })}
+ value={stopValue}
+ placeholder={formattedValue(Math.max(effectiveDimSize - 1, 0))}
+ ariaLabel="Stop value"
+ values={values ?? []}
+ effectiveDimSize={effectiveDimSize}
+ formattedValue={formattedValue}
+ onValueChange={value => {
+ const parsed = parseFloat(value);
+ if (!Number.isNaN(parsed)) updateSelection({ stop: String(getIndexFromValue(parsed)) });
+ }}
+ onIncrement={() => changeStopBy(+1)}
+ onDecrement={() => changeStopBy(-1)}
+ includeEnd
+ />
+
+
+ )
+ ) : (
+
+ {sel.mode === 'slice' ? (
+ showTimeControls ? (
+
updateSelection({ start: String(newStart) })}
+ value={startValue}
+ placeholder={formattedValue(0)}
+ ariaLabel="Start value"
+ values={values ?? []}
+ effectiveDimSize={effectiveDimSize}
+ formattedValue={formattedValue}
+ onValueChange={value => {
+ const parsed = parseFloat(value);
+ if (!Number.isNaN(parsed)) updateSelection({ start: String(getIndexFromValue(parsed)) });
+ }}
+ onIncrement={() => changeStartBy(+1)}
+ onDecrement={() => changeStartBy(-1)}
+ />
+ ) : (
+ {
+ const parsed = parseFloat(value);
+ if (!Number.isNaN(parsed)) updateSelection({ start: String(getIndexFromValue(parsed)) });
+ }}
+ onIncrement={() => changeStartBy(+1)}
+ onDecrement={() => changeStartBy(-1)}
+ ariaLabel="Start value"
+ showInput={!isDateDimension}
+ />
+ )
+ ) : (
+
+ )}
+
+ {sel.mode === 'slice' ? (
+ showTimeControls ? (
+ updateSelection({ stop: String(newStop) })}
+ value={stopValue}
+ placeholder={formattedValue(Math.max(effectiveDimSize - 1, 0))}
+ ariaLabel="Stop value"
+ values={values ?? []}
+ effectiveDimSize={effectiveDimSize}
+ formattedValue={formattedValue}
+ onValueChange={value => {
+ const parsed = parseFloat(value);
+ if (!Number.isNaN(parsed)) updateSelection({ stop: String(getIndexFromValue(parsed)) });
+ }}
+ onIncrement={() => changeStopBy(+1)}
+ onDecrement={() => changeStopBy(-1)}
+ includeEnd
+ />
+ ) : (
+ {
+ const parsed = parseFloat(value);
+ if (!Number.isNaN(parsed)) updateSelection({ stop: String(getIndexFromValue(parsed)) });
+ }}
+ onIncrement={() => changeStopBy(+1)}
+ onDecrement={() => changeStopBy(-1)}
+ ariaLabel="Stop value"
+ showInput={!isDateDimension}
+ />
+ )
+ ) : showTimeControls ? (
+ updateSelection({ scalar: String(newScalar) })}
+ value={scalarValue}
+ placeholder={formattedValue(0)}
+ ariaLabel="Scalar value"
+ values={values ?? []}
+ effectiveDimSize={effectiveDimSize}
+ formattedValue={formattedValue}
+ onValueChange={value => {
+ const parsed = parseFloat(value);
+ if (!Number.isNaN(parsed)) updateSelection({ scalar: String(getIndexFromValue(parsed)) });
+ }}
+ onIncrement={() => changeScalarBy(+1)}
+ onDecrement={() => changeScalarBy(-1)}
+ />
+ ) : (
+ {
+ const parsed = parseFloat(value);
+ if (!Number.isNaN(parsed)) updateSelection({ scalar: String(getIndexFromValue(parsed)) });
+ }}
+ onIncrement={() => changeScalarBy(+1)}
+ onDecrement={() => changeScalarBy(-1)}
+ ariaLabel="Scalar value"
+ showInput={!isDateDimension}
+ />
+ )}
+
+ )}
+
+ );
+};
+
+export { DimSlicer };
+export default DimSlicer;
\ No newline at end of file
diff --git a/src/components/ui/DimSlicer/DimSlicerAxisToggle.tsx b/src/components/ui/DimSlicer/DimSlicerAxisToggle.tsx
new file mode 100644
index 000000000..e4f1baf09
--- /dev/null
+++ b/src/components/ui/DimSlicer/DimSlicerAxisToggle.tsx
@@ -0,0 +1,73 @@
+'use client';
+import React, { useEffect, useRef, useState } from 'react';
+import { Button } from '@/components/ui/button-enhanced';
+import { ButtonGroup } from '@/components/ui/button-group';
+
+export type Axis = 'x' | 'y' | 'z' | 'c';
+
+const AXIS_CLASS: Record = {
+ x: 'text-pink-500',
+ y: 'text-green-500',
+ z: 'text-blue-500',
+ c: 'text-yellow-500',
+};
+
+interface DimSlicerAxisToggleProps {
+ axis: Axis;
+ onAxisChange?: (axis: Axis) => void;
+ /** If provided, only these axes are shown. Defaults to all four. */
+ allowedAxes?: Axis[];
+}
+
+export const DimSlicerAxisToggle: React.FC = ({
+ axis,
+ onAxisChange,
+ allowedAxes,
+}) => {
+ const [expanded, setExpanded] = useState(false);
+ const rootRef = useRef(null);
+
+ const axisOptions: Axis[] = allowedAxes ?? ['x', 'y', 'z', 'c'];
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
+ setExpanded(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ return (
+
+ {expanded ? (
+
+ {axisOptions.map(a => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/ui/DimSlicer/DimSlicerModeToggle.tsx b/src/components/ui/DimSlicer/DimSlicerModeToggle.tsx
new file mode 100644
index 000000000..14af38ec2
--- /dev/null
+++ b/src/components/ui/DimSlicer/DimSlicerModeToggle.tsx
@@ -0,0 +1,67 @@
+'use client';
+import React, { useEffect, useRef, useState } from 'react';
+import { Button } from '@/components/ui/button-enhanced';
+import { ButtonGroup } from '@/components/ui/button-group';
+
+export type SelectionMode = 'scalar' | 'slice';
+
+interface DimSlicerModeToggleProps {
+ mode: SelectionMode;
+ onModeChange: (nextMode: SelectionMode) => void;
+}
+
+export const DimSlicerModeToggle: React.FC = ({ mode, onModeChange }) => {
+ const [expanded, setExpanded] = useState(false);
+ const rootRef = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
+ setExpanded(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ return (
+
+ {expanded ? (
+
+
+
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/src/components/ui/DimSlicer/DimSlicerNumericControl.tsx b/src/components/ui/DimSlicer/DimSlicerNumericControl.tsx
new file mode 100644
index 000000000..2a9494b65
--- /dev/null
+++ b/src/components/ui/DimSlicer/DimSlicerNumericControl.tsx
@@ -0,0 +1,36 @@
+'use client'
+
+import React from 'react'
+import { DimSlicerNumericInputWithStepper } from './DimSlicerNumericInputWithStepper'
+
+interface DimSlicerNumericControlProps {
+ value: string
+ placeholder: string
+ ariaLabel: string
+ onValueChange: (value: string) => void
+ onIncrement: () => void
+ onDecrement: () => void
+ showInput: boolean
+}
+
+export function DimSlicerNumericControl({
+ value,
+ placeholder,
+ ariaLabel,
+ onValueChange,
+ onIncrement,
+ onDecrement,
+ showInput,
+}: DimSlicerNumericControlProps) {
+ return (
+
+ )
+}
diff --git a/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx b/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx
new file mode 100644
index 000000000..3b9aa9a08
--- /dev/null
+++ b/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx
@@ -0,0 +1,97 @@
+'use client';
+import React, { useEffect, useRef, useState } from 'react';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button-enhanced';
+import { ButtonGroup } from '@/components/ui/button-group';
+import { MinusIcon, PlusIcon } from 'lucide-react';
+
+interface DimSlicerNumericInputWithStepperProps {
+ value: string;
+ placeholder: string;
+ onValueChange: (value: string) => void;
+ onIncrement: () => void;
+ onDecrement: () => void;
+ ariaLabel: string;
+ showInput?: boolean;
+}
+
+export const DimSlicerNumericInputWithStepper: React.FC = ({
+ value,
+ placeholder,
+ onValueChange,
+ onIncrement,
+ onDecrement,
+ ariaLabel,
+ showInput = true,
+}) => {
+ const [expanded, setExpanded] = useState(false);
+ const [localValue, setLocalValue] = useState(value);
+ const rootRef = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
+ setExpanded(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ // Sync prop changes to local state
+ useEffect(() => {
+ setLocalValue(value);
+ }, [value]);
+
+ const commitValue = () => {
+ onValueChange(localValue);
+ setLocalValue(value);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ commitValue();
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/ui/DimSlicer/DimSlicerTimeControl.tsx b/src/components/ui/DimSlicer/DimSlicerTimeControl.tsx
new file mode 100644
index 000000000..c74e4e704
--- /dev/null
+++ b/src/components/ui/DimSlicer/DimSlicerTimeControl.tsx
@@ -0,0 +1,65 @@
+'use client'
+
+import React from 'react'
+import TimeCombobox from './TimeCombobox'
+import { DimSlicerNumericInputWithStepper } from './DimSlicerNumericInputWithStepper'
+
+interface DimSlicerTimeControlProps {
+ currentIndex: number
+ onIndexChange: (index: number) => void
+ value: string
+ placeholder: string
+ ariaLabel: string
+ values: number[]
+ effectiveDimSize: number
+ formattedValue: (index: number) => string
+ onValueChange: (value: string) => void
+ onIncrement: () => void
+ onDecrement: () => void
+ includeEnd?: boolean
+ layout?: 'row' | 'column'
+ showInput?: boolean
+}
+
+export function DimSlicerTimeControl({
+ currentIndex,
+ onIndexChange,
+ value,
+ placeholder,
+ ariaLabel,
+ values,
+ effectiveDimSize,
+ formattedValue,
+ onValueChange,
+ onIncrement,
+ onDecrement,
+ includeEnd = false,
+ layout = 'column',
+ showInput = true,
+}: DimSlicerTimeControlProps) {
+ return (
+
+ )
+}
diff --git a/src/components/ui/DimSlicer/TimeCombobox.tsx b/src/components/ui/DimSlicer/TimeCombobox.tsx
new file mode 100644
index 000000000..71acfd650
--- /dev/null
+++ b/src/components/ui/DimSlicer/TimeCombobox.tsx
@@ -0,0 +1,195 @@
+'use client'
+import React, { useState, useMemo, useEffect } from 'react'
+import {
+ Combobox,
+ ComboboxContent,
+ ComboboxEmpty,
+ ComboboxInput,
+ ComboboxItem,
+ ComboboxList,
+} from '@/components/ui/combobox'
+
+interface TimeComboboxProps {
+ currentIndex: number
+ onIndexChange: (index: number) => void
+ ariaLabel: string
+ placeholder: string
+ values: number[]
+ effectiveDimSize: number
+ formattedValue: (index: number) => string
+ includeEnd?: boolean
+}
+
+export default function TimeCombobox({
+ currentIndex,
+ onIndexChange,
+ ariaLabel,
+ placeholder,
+ values,
+ effectiveDimSize,
+ formattedValue,
+ includeEnd = false,
+}: TimeComboboxProps) {
+ const selectedLabel =
+ includeEnd && currentIndex === effectiveDimSize
+ ? formattedValue(Math.max(effectiveDimSize - 1, 0))
+ : formattedValue(currentIndex)
+
+ const [inputQuery, setInputQuery] = useState('')
+ const inputValue = inputQuery === '' ? selectedLabel : inputQuery
+
+ const [windowRadius, setWindowRadius] = useState(50)
+
+ useEffect(() => {
+ setWindowRadius(50)
+ }, [inputValue, currentIndex])
+
+ const handleValueChange = (value: unknown) => {
+ const label = typeof value === 'string' ? value : ''
+ if (label === '') {
+ setInputQuery('')
+ onIndexChange(includeEnd ? effectiveDimSize : 0)
+ return
+ }
+ const item = labeledValues.find(({ label: l }) => l === label)
+ if (item) {
+ setInputQuery('')
+ onIndexChange(item.index)
+ }
+ }
+
+ const labeledValues = useMemo(() => {
+ return values.map((_, i) => ({ label: formattedValue(i), index: i }))
+ }, [values, formattedValue])
+
+ const filteredData = useMemo(() => {
+ const normalizedInput = inputValue.trim().toLowerCase()
+ const selectedQuery = selectedLabel.trim().toLowerCase()
+ const isFiltering = normalizedInput !== '' && normalizedInput !== selectedQuery;
+
+ if (isFiltering) {
+ const results = labeledValues.filter(({ label }) => label.toLowerCase().includes(normalizedInput))
+ return { items: results.slice(0, windowRadius * 2), startIdx: 0 };
+ }
+
+ const startIdx = Math.max(0, currentIndex - windowRadius);
+ const endIdx = Math.min(labeledValues.length, currentIndex + windowRadius);
+ return { items: labeledValues.slice(startIdx, endIdx), startIdx };
+ }, [inputValue, selectedLabel, labeledValues, windowRadius, currentIndex])
+
+ const filtered = filteredData.items;
+
+ const listRef = React.useRef(null);
+ const lastVisibleItem = React.useRef<{ index: string; offset: number } | null>(null);
+
+ React.useLayoutEffect(() => {
+ if (listRef.current && lastVisibleItem.current) {
+ const { index, offset } = lastVisibleItem.current;
+ const target = listRef.current;
+ const item = target.querySelector(`[data-index="${index}"]`);
+ if (item) {
+ const targetRect = target.getBoundingClientRect();
+ const itemRect = item.getBoundingClientRect();
+ const currentOffset = itemRect.top - targetRect.top;
+ const diff = currentOffset - offset;
+
+ if (Math.abs(diff) > 0) {
+ target.scrollTop += diff;
+ }
+ }
+ // Also apply once in a microtask just in case Base UI auto-scrolls asynchronously
+ requestAnimationFrame(() => {
+ if (target && item) {
+ const newCurrentOffset = item.getBoundingClientRect().top - target.getBoundingClientRect().top;
+ const newDiff = newCurrentOffset - offset;
+ if (Math.abs(newDiff) > 0) {
+ target.scrollTop += newDiff;
+ }
+ }
+ });
+ lastVisibleItem.current = null;
+ }
+ }, [filteredData]);
+
+ const targetWidth = Math.min(
+ Math.max(Math.max(selectedLabel.length, placeholder.length) + 2, 12),
+ 40
+ )
+
+ return (
+
+ setInputQuery(typeof value === 'string' ? value : String(value))
+ }
+ autoHighlight
+ >
+
+
+ {filtered.length === 0 ? No items found. : null}
+ ) => {
+ const target = e.currentTarget;
+ const scrollPos = target.scrollTop;
+ const maxScroll = target.scrollHeight - target.clientHeight;
+
+ const isNearBottom = maxScroll > 0 && maxScroll - scrollPos <= target.clientHeight;
+ const isNearTop = maxScroll > 0 && scrollPos <= target.clientHeight;
+
+ if (isNearBottom || isNearTop) {
+ setWindowRadius(r => {
+ const normalizedInput = inputValue.trim().toLowerCase()
+ const selectedQuery = selectedLabel.trim().toLowerCase()
+ const isFiltering = normalizedInput !== '' && normalizedInput !== selectedQuery;
+
+ let nextR = r;
+ if (isFiltering) {
+ nextR = isNearBottom ? Math.min(r + 50, 5000) : r;
+ } else {
+ const startIdx = Math.max(0, currentIndex - r);
+ const endIdx = Math.min(labeledValues.length, currentIndex + r);
+ const canGrowTop = startIdx > 0 && isNearTop;
+ const canGrowBottom = endIdx < labeledValues.length && isNearBottom;
+ if (canGrowTop || canGrowBottom) {
+ nextR = Math.min(r + 50, 5000);
+ }
+ }
+
+ if (nextR !== r) {
+ const targetRect = target.getBoundingClientRect();
+ const children = Array.from(target.querySelectorAll('[data-slot="combobox-item"]'));
+ for (const child of children) {
+ const childRect = child.getBoundingClientRect();
+ if (childRect.bottom >= targetRect.top) {
+ lastVisibleItem.current = {
+ index: child.getAttribute('data-index') || '',
+ offset: childRect.top - targetRect.top
+ };
+ break;
+ }
+ }
+ }
+
+ return nextR;
+ });
+ }
+ }}
+ >
+ {filtered.map(({ label, index }) => (
+
+ {label}
+
+ ))}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/ui/DimSlicer/index.ts b/src/components/ui/DimSlicer/index.ts
new file mode 100644
index 000000000..0622ada38
--- /dev/null
+++ b/src/components/ui/DimSlicer/index.ts
@@ -0,0 +1,2 @@
+export { default } from './DimSlicer';
+export * from './DimSlicer';
diff --git a/src/components/ui/MainPanel/AnalysisOptions.tsx b/src/components/ui/MainPanel/AnalysisOptions.tsx
index b5ec11b9a..894e2ffa6 100644
--- a/src/components/ui/MainPanel/AnalysisOptions.tsx
+++ b/src/components/ui/MainPanel/AnalysisOptions.tsx
@@ -81,11 +81,12 @@ const webGPUError =
- {dimNames.map((dimName, idx) => (
-
- {dimName}
+ {activeIndices.map((origIdx, dataShapeIdx) => (
+
+ {dimNames[origIdx]}
))}
@@ -383,7 +384,7 @@ const AnalysisOptions = () => {
-
+
{setReverseDirection(e ? 1 : 0)}}/>
diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx
index 1e02d72a5..8713d3511 100644
--- a/src/components/ui/MainPanel/Colormaps.tsx
+++ b/src/components/ui/MainPanel/Colormaps.tsx
@@ -17,22 +17,24 @@ import {
const Colormaps = () => {
- const [cmap, setCmap] = useState("Spectral");
- const [flipCmap, setFlipCmap] = useState(false);
const [hoveredCmap, setHoveredCmap] = useState(null);
- const { colormap, setColormap } = useGlobalStore(
+ const { colormap, setColormap, colormapName, flipColormap, setColormapName, setFlipColormap } = useGlobalStore(
useShallow((state) => ({
setColormap: state.setColormap,
colormap: state.colormap,
+ colormapName: state.colormapName,
+ flipColormap: state.flipColormap,
+ setColormapName: state.setColormapName,
+ setFlipColormap: state.setFlipColormap,
}))
);
const [popoverSide, setPopoverSide] = useState<"left" | "top">("left");
useEffect(() => {
setColormap(
- GetColorMapTexture(colormap, (hoveredCmap || cmap) === "Default" ? "Spectral" : (hoveredCmap || cmap), 1, "#000000", 0, flipCmap)
+ GetColorMapTexture(colormap, (hoveredCmap || colormapName) === "Default" ? "Spectral" : (hoveredCmap || colormapName), 1, "#000000", 0, flipColormap)
);
- }, [cmap, flipCmap, hoveredCmap]);
+ }, [colormapName, flipColormap, hoveredCmap]);
useEffect(() => {
const handleResize = () => {
@@ -55,9 +57,9 @@ const Colormaps = () => {
size="icon"
className='cursor-pointer hover:scale-90 transition-transform duration-100 ease-out rounded-full'
style={{
- backgroundImage: `url(./colormap_icons/${cmap}.webp)` ,
+ backgroundImage: `url(./colormap_icons/${colormapName}.webp)` ,
backgroundSize: "100%",
- transform: flipCmap ? "scaleX(-1)" : "",
+ transform: flipColormap ? "scaleX(-1)" : "",
width: "32px",
height: "32px",
}} />
@@ -82,7 +84,7 @@ const Colormaps = () => {
{colormaps.map((val) => (
{
onMouseEnter={() => setHoveredCmap(val)}
onMouseLeave={() => setHoveredCmap(null)}
onClick={() => {
- setCmap(val);
+ setColormapName(val);
setHoveredCmap(null);
}}
/>
@@ -105,10 +107,10 @@ const Colormaps = () => {
height: "50px",
width: "50px",
cursor: "pointer",
- transform: `${flipCmap ? "rotate(270deg)" : "rotate(90deg)"}`,
+ transform: `${flipColormap ? "rotate(270deg)" : "rotate(90deg)"}`,
transition: ".25s",
}}
- onClick={() => setFlipCmap((x) => !x)}
+ onClick={() => setFlipColormap(!flipColormap)}
/>
diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx
new file mode 100644
index 000000000..e1906a418
--- /dev/null
+++ b/src/components/ui/MainPanel/MetaDimSelector.tsx
@@ -0,0 +1,855 @@
+"use client";
+
+import React, { useMemo, useState, useEffect } from 'react';
+import DimSlicer, { Axis, defaultSelection, DimOption, SliceSelectionState } from '@/components/ui/DimSlicer';
+import { defaultAttributes, renderAttributes } from "@/components/ui/MetaData";
+import { Button } from '@/components/ui/button-enhanced';
+import { useGlobalStore } from '@/GlobalStates/GlobalStore';
+import { useShallow } from 'zustand/shallow';
+import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
+import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
+import { Badge, Switch, Input, Hider } from "@/components/ui";
+import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
+import { parseLoc } from '@/utils/HelperFuncs';
+import { ChevronDown, ChevronRight, AlertCircle, CheckCircle2 } from 'lucide-react';
+import { useIsMobile } from "@/hooks/use-mobile";
+
+import { useCacheStore } from "@/GlobalStates/CacheStore";
+import { usePlotStore } from '@/GlobalStates/PlotStore';
+import { useZarrStore } from '@/GlobalStates/ZarrStore';
+import { SliderThumbs } from "@/components/ui/Widgets/SliderThumbs";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import { BsFillQuestionCircleFill } from "react-icons/bs";
+
+const MAX_ACTIVE_DIMS = 3;
+
+const formatArray = (value: string | number[]): string => {
+ if (typeof value === 'string') return value;
+ return Array.isArray(value) ? value.join(', ') : String(value);
+};
+
+const formatBytes = (bytes: number): string => {
+ if (bytes === 0) return "0 Bytes"
+ const k = 1024
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"]
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
+}
+
+interface DimInfo {
+ dimArrays: ArrayLike[];
+ dimNames: string[];
+ dimUnits: (string | null)[];
+}
+
+type Props = {
+ meta: {
+ name?: string;
+ shape?: number[];
+ chunks?: number[];
+ chunkSize?: number;
+ totalSize?: number;
+ dtype?: string;
+ long_name?: string;
+ dimInfo?: DimInfo;
+ [key: string]: unknown;
+ };
+ metadata?: Record;
+ onApply?: (sels: SliceSelectionState[], axes: Axis[], dimNames: string[]) => void;
+ setShowMeta?: React.Dispatch>;
+ setOpenVariables?: (open: boolean) => void;
+};
+
+const AXIS_COLOR: Record = {
+ x: 'text-pink-500',
+ y: 'text-green-500',
+ z: 'text-blue-500',
+ c: 'text-yellow-500',
+};
+
+interface SlicerRow {
+ id: number;
+ dimName: string;
+ sel: SliceSelectionState;
+ axis: Axis;
+}
+
+let _nextId = 0;
+const nextId = () => ++_nextId;
+
+const getOrigIdx = (dimName: string) => {
+ const parts = dimName.split('::');
+ return parseInt(parts[parts.length - 1]);
+};
+
+export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, setOpenVariables }: Props) {
+ const isMobile = useIsMobile();
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => setMounted(true), []);
+ const dimArrays = useMemo(
+ () => (meta?.dimInfo?.dimArrays ?? []).map((a) => Array.from(a)),
+ [meta?.dimInfo?.dimArrays]
+ );
+ const dimUnits = useMemo(
+ () => (meta?.dimInfo?.dimUnits ?? []).map((u) => u ?? ''),
+ [meta?.dimInfo?.dimUnits]
+ );
+ const dimNames = useMemo(
+ () => meta?.dimInfo?.dimNames ?? [],
+ [meta?.dimInfo?.dimNames]
+ );
+ const dataShape = meta?.shape || [];
+ const chunkShape = meta?.chunks || [];
+
+
+ const { setDimArrays, setDimNames, setDimUnits, initStore, setVariable, setTextureArrayDepths, variable } = useGlobalStore(
+ useShallow((state) => ({
+ setDimArrays: state.setDimArrays,
+ setDimNames: state.setDimNames,
+ setDimUnits: state.setDimUnits,
+ initStore: state.initStore,
+ setVariable: state.setVariable,
+ setTextureArrayDepths: state.setTextureArrayDepths,
+ variable: state.variable,
+ })),
+ );
+
+ const { maxSize, cache, setMaxSize } = useCacheStore(useShallow(state => ({ maxSize: state.maxSize, cache: state.cache, setMaxSize: state.setMaxSize })))
+ const [cacheSize, setCacheSize] = useState(maxSize)
+
+ const { ndSlices, axisMapping, setZSlice, setYSlice, setXSlice, ReFetch, compress, setCompress, coarsen, setCoarsen, kernelSize, setKernelSize, kernelDepth, setKernelDepth } = useZarrStore(useShallow(state => ({
+ ndSlices: state.ndSlices, axisMapping: state.axisMapping,
+ setZSlice: state.setZSlice, setYSlice: state.setYSlice, setXSlice: state.setXSlice,
+ ReFetch: state.ReFetch, compress: state.compress, setCompress: state.setCompress,
+ coarsen: state.coarsen, setCoarsen: state.setCoarsen, kernelSize: state.kernelSize, setKernelSize: state.setKernelSize, kernelDepth: state.kernelDepth, setKernelDepth: state.setKernelDepth
+ })))
+
+ const { maxTextureSize, max3DTextureSize } = usePlotStore(useShallow(state => ({ maxTextureSize: state.maxTextureSize, max3DTextureSize: state.max3DTextureSize })))
+
+ const [tooBig, setTooBig] = useState(false)
+ const [cached, setCached] = useState(false)
+ const [cachedChunks, setCachedChunks] = useState(null)
+ const [texCount, setTexCount] = useState(0)
+ const [displaySpat, setDisplaySpat] = useState(String(kernelSize))
+ const [displayDepth, setDisplayDepth] = useState(String(kernelDepth))
+
+ useEffect(() => {
+ setDimArrays(dimArrays);
+ setDimNames(dimNames);
+ setDimUnits(dimUnits);
+ }, [dimArrays, dimNames, dimUnits, setDimArrays, setDimNames, setDimUnits]);
+
+
+
+ const availableDims: DimOption[] = useMemo(
+ () =>
+ dimArrays.map((values, idx) => {
+ const baseName = dimNames[idx] ?? `dim${idx}`;
+ // Always include idx to guarantee uniqueness during the "Default" fallback phase
+ const name = `${baseName}::${idx}`;
+ const label = baseName;
+ return {
+ name,
+ label,
+ size: values.length,
+ values,
+ formatValue: (v: number): string =>
+ String(parseLoc(values[v] ?? v, dimUnits[idx] || undefined)),
+ };
+ }),
+ [dimArrays, dimNames, dimUnits],
+ );
+
+ const dimsKey = availableDims.map((d) => `${d.name}:${d.size}`).join('|');
+
+ const makeInitialCollapsedSels = (dims: DimOption[]): Record => {
+ const isCurrentVar = variable === meta.name && ndSlices && ndSlices.length === dims.length;
+ return Object.fromEntries(dims.map((d, i) => {
+ let sel: SliceSelectionState = { ...defaultSelection(d.size), mode: 'scalar' };
+ if (isCurrentVar) {
+ const s = ndSlices[i];
+ if (typeof s === 'number') {
+ sel = { start: '', stop: '', scalar: String(s), mode: 'scalar' };
+ }
+ }
+ return [d.name, sel];
+ }));
+ };
+
+ const makeInitialRows = (dims: DimOption[]): SlicerRow[] => {
+ const isCurrentVar = variable === meta.name && ndSlices && ndSlices.length === dims.length && axisMapping;
+
+ if (isCurrentVar) {
+ const initRows: SlicerRow[] = [];
+ const axes: Axis[] = ['z', 'y', 'x'];
+
+ for (const axis of axes) {
+ const mappedIdx = (axisMapping as Record)[axis];
+ if (mappedIdx !== undefined && mappedIdx >= 0 && mappedIdx < dims.length) {
+ const dim = dims[mappedIdx];
+ const s = ndSlices[mappedIdx];
+ let sel: SliceSelectionState = { ...defaultSelection(dim.size), mode: 'slice' };
+ if (Array.isArray(s)) {
+ sel = { start: String(s[0]), stop: s[1] !== null ? String(s[1]) : '', scalar: '', mode: 'slice' };
+ }
+ initRows.push({
+ id: nextId(),
+ dimName: dim.name,
+ sel,
+ axis
+ });
+ }
+ }
+
+ if (initRows.length > 0) return initRows;
+ }
+
+ const activeDims = dims.slice(-Math.min(MAX_ACTIVE_DIMS, dims.length));
+ const defaultAxes: Axis[] = ['z', 'y', 'x'];
+ const axes = defaultAxes.slice(-activeDims.length);
+ return activeDims.map((d, i) => ({
+ id: nextId(),
+ dimName: d.name,
+ sel: { ...defaultSelection(d.size), mode: 'slice' },
+ axis: axes[i],
+ }));
+ };
+
+ const [rows, setRows] = useState(() => makeInitialRows(availableDims));
+ const [collapsedSels, setCollapsedSels] = useState>(
+ () => makeInitialCollapsedSels(availableDims),
+ );
+ const [lastKey, setLastKey] = useState(dimsKey);
+ const [collapsedOpen, setCollapsedOpen] = useState(false);
+
+ if (dimsKey !== lastKey) {
+ setLastKey(dimsKey);
+ setRows(makeInitialRows(availableDims));
+ setCollapsedSels(makeInitialCollapsedSels(availableDims));
+ }
+
+ useEffect(()=>{
+ setCompress(false)
+ setCachedChunks(null)
+ setCached(false)
+
+ if (!meta || !meta.chunks || !meta.shape) {
+ if (meta && cache.has(`${initStore}_${meta.name}`)) {
+ setCached(true);
+ }
+ return;
+ }
+
+ const ndSlicesTemp = availableDims.map((d) => {
+ const activeRow = rows.find((r) => r.dimName === d.name);
+ if (activeRow) {
+ return [parseInt(activeRow.sel.start) || 0, parseInt(activeRow.sel.stop) || d.size] as [number, number];
+ }
+ const colSel = collapsedSels[d.name];
+ if (colSel && colSel.mode === 'scalar') return parseInt(colSel.scalar) || 0;
+ return 0;
+ });
+
+ const scalarIndices = ndSlicesTemp.filter(s => typeof s === "number").join("_");
+ const cacheBase = scalarIndices !== "" ? `${initStore}_${meta.name}_${scalarIndices}` : `${initStore}_${meta.name}`;
+
+ const rowZ = rows.find((r) => r.axis === 'z');
+ const rowY = rows.find((r) => r.axis === 'y');
+ const rowX = rows.find((r) => r.axis === 'x');
+
+ const origIdxZ = rowZ ? getOrigIdx(rowZ.dimName) : -1;
+ const origIdxY = rowY ? getOrigIdx(rowY.dimName) : -1;
+ const origIdxX = rowX ? getOrigIdx(rowX.dimName) : -1;
+
+ const getSliceDims = (row?: SlicerRow, origIdx?: number) => {
+ const defaultLast = origIdx !== undefined && origIdx >= 0 ? meta.shape?.[origIdx] ?? 1 : 1;
+ if (!row) return { first: 0, last: defaultLast };
+ const sel = row.sel;
+ if (sel.mode === 'scalar') {
+ const val = parseInt(sel.scalar) || 0;
+ return { first: val, last: val + 1 };
+ }
+ const start = parseInt(sel.start) || 0;
+ let stop = parseInt(sel.stop);
+ if (isNaN(stop)) stop = defaultLast;
+ else stop += 1;
+ return { first: start, last: stop };
+ };
+
+ const zSlice = getSliceDims(rowZ, origIdxZ);
+ const ySlice = getSliceDims(rowY, origIdxY);
+ const xSlice = getSliceDims(rowX, origIdxX);
+
+ const calcDim = (slice: {first: number, last: number}, dimIdx: number) => {
+ if (dimIdx < 0) return { start: 0, end: 1 };
+ const chunkDim = meta.chunks?.[dimIdx];
+ if (!chunkDim) return { start: 0, end: 1 };
+ const start = Math.floor(slice.first / chunkDim);
+ return { start, end: Math.ceil(slice.last / chunkDim) };
+ };
+
+ const zDim = calcDim(zSlice, origIdxZ);
+ const yDim = calcDim(ySlice, origIdxY);
+ const xDim = calcDim(xSlice, origIdxX);
+
+ let accum = 0;
+ let total = 0;
+ for (let z = zDim.start; z < zDim.end; z++) {
+ for (let y = yDim.start; y < yDim.end; y++) {
+ for (let x = xDim.start; x < xDim.end; x++) {
+ total++;
+ const chunkID = `z${z}_y${y}_x${x}`;
+ const cacheName = `${cacheBase}_chunk_${chunkID}`;
+ if (cache.has(cacheName)) {
+ accum++;
+ }
+ }
+ }
+ }
+
+ if (total > 0 && accum > 0) {
+ setCachedChunks(`${accum}/${total}`);
+ setCached(true);
+ } else if (cache.has(`${initStore}_${meta.name}`)) {
+ setCached(true);
+ }
+ }, [meta, cache, initStore, setCompress, rows, collapsedSels, availableDims])
+
+ const sizeData = useMemo(() => {
+ const rowZ = rows.find((r) => r.axis === 'z');
+ const rowY = rows.find((r) => r.axis === 'y');
+ const rowX = rows.find((r) => r.axis === 'x');
+
+ const is2D = dataShape.length === 2 || !rowZ;
+
+ const getSliceDims = (row?: SlicerRow, defaultLast = 0) => {
+ if (!row) return { first: 0, last: defaultLast, steps: defaultLast };
+ const sel = row.sel;
+ if (sel.mode === 'scalar') {
+ const val = parseInt(sel.scalar) || 0;
+ return { first: val, last: val + 1, steps: 1 };
+ }
+ const start = parseInt(sel.start) || 0;
+ let stop = parseInt(sel.stop);
+ if (isNaN(stop)) stop = defaultLast;
+ else stop += 1;
+ return { first: start, last: stop, steps: Math.max(1, stop - start) };
+ };
+
+ const origIdxZ = rowZ ? getOrigIdx(rowZ.dimName) : -1;
+ const origIdxY = rowY ? getOrigIdx(rowY.dimName) : -1;
+ const origIdxX = rowX ? getOrigIdx(rowX.dimName) : -1;
+
+ const lenZ = origIdxZ >= 0 ? dataShape[origIdxZ] : 1;
+ const lenY = origIdxY >= 0 ? dataShape[origIdxY] : 1;
+ const lenX = origIdxX >= 0 ? dataShape[origIdxX] : 1;
+
+ const z = is2D ? { first: 0, last: 1, steps: 1 } : getSliceDims(rowZ, lenZ);
+ const y = getSliceDims(rowY, lenY);
+ const x = getSliceDims(rowX, lenX);
+
+ const maxSizeLimit = is2D ? maxTextureSize : max3DTextureSize;
+ const texCounts = [z.steps / maxSizeLimit, y.steps / maxSizeLimit, x.steps / maxSizeLimit];
+
+ const depths = texCounts.some(count => count > 1)
+ ? texCounts.map(val => Math.ceil(val))
+ : [1, 1, 1];
+
+ const thisCount = texCounts.reduce((prod, val) => prod * Math.ceil(val), 1)
+
+ const getSelSteps = (dimName: string, defaultLast: number) => {
+ const row = rows.find(r => r.dimName === dimName);
+ if (row) return getSliceDims(row, defaultLast).steps;
+
+ const collSel = collapsedSels[dimName];
+ if (collSel) {
+ if (collSel.mode === 'scalar') return 1;
+ const start = parseInt(collSel.start) || 0;
+ let stop = parseInt(collSel.stop);
+ if (isNaN(stop)) stop = defaultLast;
+ else stop += 1;
+ return Math.max(1, stop - start);
+ }
+ return defaultLast;
+ };
+
+ const totalSteps = availableDims.reduce((prod, d) => prod * getSelSteps(d.name, d.size), 1);
+ const sizeRatio = totalSteps / (dataShape.reduce((a, b) => a * b, 1) || 1);
+ let calculatedSize = (meta.totalSize || 0) * sizeRatio;
+
+ if (!is2D) {
+ calculatedSize = calculatedSize / (coarsen ? kernelDepth * Math.pow(kernelSize, 2) : 1);
+ }
+
+ return { size: calculatedSize, thisCount, depths };
+ }, [meta, rows, collapsedSels, availableDims, dataShape, chunkShape, coarsen, kernelSize, kernelDepth, maxTextureSize, max3DTextureSize]);
+
+ useEffect(() => {
+ setTextureArrayDepths(sizeData.depths);
+ setTexCount(sizeData.thisCount);
+ setTooBig(sizeData.thisCount > 14);
+ }, [sizeData, setTextureArrayDepths]);
+
+ const currentSize = sizeData.size;
+
+ const cachedSize = useMemo(()=>{
+ const thisDtype = (meta?.dtype as string) || '';
+ if (thisDtype.includes("32") || thisDtype.includes("f4")){
+ return currentSize / 2;
+ } else if (thisDtype.includes("64") || thisDtype.includes("f8")){
+ return currentSize / 4;
+ } else if (thisDtype.includes("8") || thisDtype.includes("i1") ){
+ return currentSize * 2;
+ } else {
+ return currentSize;
+ }
+ },[currentSize, meta])
+
+ const smallCache = cachedSize > cacheSize;
+
+ const firstUnusedDim = (currentRows: SlicerRow[]): string => {
+ const usedNames = new Set(currentRows.map((r) => r.dimName));
+ return availableDims.find((d) => !usedNames.has(d.name))?.name ?? '';
+ };
+
+ const firstUnusedAxis = (currentRows: SlicerRow[]): Axis => {
+ const used = new Set(currentRows.map((r) => r.axis));
+ return (['x', 'y', 'z'] as Axis[]).find((a) => !used.has(a)) ?? 'z';
+ };
+
+ const addRow = () => {
+ setRows((prev) => {
+ if (prev.length >= MAX_ACTIVE_DIMS) return prev;
+ const dimName = firstUnusedDim(prev);
+ if (!dimName) return prev;
+ const dim = availableDims.find((d) => d.name === dimName)!;
+ const newRows: SlicerRow[] = [...prev, {
+ id: nextId(),
+ dimName,
+ sel: { ...defaultSelection(dim.size), mode: 'slice' },
+ axis: 'z', // Placeholder, reassigned below
+ }];
+
+ const defaultAxes: Axis[] = ['z', 'y', 'x'];
+ const axes = defaultAxes.slice(-newRows.length);
+ return newRows.map((r, i) => ({ ...r, axis: axes[i] }));
+ });
+ };
+
+ const removeLastRow = () =>
+ setRows((prev) => {
+ const newRows = prev.slice(0, -1);
+ const defaultAxes: Axis[] = ['z', 'y', 'x'];
+ const axes = defaultAxes.slice(-newRows.length);
+ return newRows.map((r, i) => ({ ...r, axis: axes[i] }));
+ });
+
+ const updateDimName = (id: number, dimName: string) => {
+ setRows((prev) =>
+ prev.map((r) => {
+ if (r.id !== id) return r;
+ const dim = availableDims.find((d) => d.name === dimName);
+ return { ...r, dimName, sel: { ...defaultSelection(dim?.size), mode: 'slice' } };
+ }),
+ );
+ };
+
+ const updateSel = (id: number, sel: SliceSelectionState) =>
+ setRows((prev) => prev.map((r) => (r.id === id ? { ...r, sel: { ...sel, mode: 'slice' } } : r)));
+
+ const updateCollapsedSel = (dimName: string, sel: SliceSelectionState) =>
+ setCollapsedSels((prev) => ({ ...prev, [dimName]: { ...sel, mode: 'scalar' } }));
+
+
+
+ const activeDimNames = new Set(rows.map((r) => r.dimName));
+ const collapsedDims = availableDims.filter((d) => !activeDimNames.has(d.name));
+
+ const atMax = rows.length >= MAX_ACTIVE_DIMS;
+ const noUnused = firstUnusedDim(rows) === '';
+ const canAdd = !atMax && !noUnused;
+
+ const addTooltip = atMax
+ ? `Maximum of ${MAX_ACTIVE_DIMS} dimensions, remove one before adding another.`
+ : noUnused
+ ? 'All dimensions are already active.'
+ : undefined;
+
+ const handlePlot = () => {
+ const rowZ = rows.find(r => r.axis === 'z');
+ const rowY = rows.find(r => r.axis === 'y');
+ const rowX = rows.find(r => r.axis === 'x');
+
+ const getSliceArray = (row?: SlicerRow, defaultLast = 0): [number, number | null] => {
+ if (!row) return [0, null];
+ const sel = row.sel;
+ if (sel.mode === 'scalar') {
+ const val = parseInt(sel.scalar) || 0;
+ return [val, val + 1];
+ }
+ const start = parseInt(sel.start) || 0;
+ let stop = parseInt(sel.stop);
+ return [start, isNaN(stop) ? null : stop + 1];
+ };
+
+ setZSlice(getSliceArray(rowZ, dataShape ? dataShape[getOrigIdx(rowZ?.dimName || '')] : 0));
+ setYSlice(getSliceArray(rowY, dataShape ? dataShape[getOrigIdx(rowY?.dimName || '')] : 0));
+ setXSlice(getSliceArray(rowX, dataShape ? dataShape[getOrigIdx(rowX?.dimName || '')] : 0));
+
+ const ndSlices: (number | [number, number | null])[] = availableDims.map((dim) => {
+ const row = rows.find((r) => r.dimName === dim.name);
+ if (row) {
+ if (row.sel.mode === 'scalar') return parseInt(row.sel.scalar) || 0;
+ const start = parseInt(row.sel.start) || 0;
+ let stop = parseInt(row.sel.stop);
+ return [start, isNaN(stop) ? null : stop + 1];
+ }
+ const colSel = collapsedSels[dim.name];
+ if (colSel && colSel.mode === 'scalar') return parseInt(colSel.scalar) || 0;
+ return 0;
+ });
+
+ const axisMapping = {
+ x: getOrigIdx(rowX?.dimName || ''),
+ y: getOrigIdx(rowY?.dimName || ''),
+ z: getOrigIdx(rowZ?.dimName || '')
+ };
+
+ useZarrStore.getState().setNdSlices(ndSlices);
+ useZarrStore.getState().setAxisMapping(axisMapping);
+
+ if (collapsedDims.length > 0) {
+ const firstCollapsed = collapsedDims[0];
+ const sel = collapsedSels[firstCollapsed.name];
+ if (sel && sel.mode === 'scalar') {
+ useGlobalStore.getState().setIdx4D(parseInt(sel.scalar) || 0);
+ }
+ }
+
+ if (variable === meta.name) {
+ ReFetch();
+ } else {
+ setMaxSize(cacheSize);
+ setVariable(meta.name || '');
+ ReFetch();
+ }
+
+ if (setShowMeta) setShowMeta(false);
+ if (setOpenVariables) setOpenVariables(false);
+ usePlotStore.setState({ coarsen, kernel: { kernelDepth, kernelSize } });
+
+ onApply?.(rows.map((r) => r.sel), rows.map((r) => r.axis), rows.map((r) => r.dimName));
+ };
+
+ return (
+
+
+ {/* Top Header: Name, Attributes, Options, and Plot button */}
+
+
+
{`${meta.long_name ?? meta.name ?? ''} `}
+ {mounted && isMobile ? (
+
+ ) : (
+
+
+ Attributes
+
+
+ {renderAttributes(metadata, defaultAttributes)}
+
+
+ )}
+
+
+ {/* Options */}
+
+ {/* Coarsen Toggle */}
+
+
+ setCoarsen(e)}/>
+
+
+ {/* Compress Toggle */}
+
+
+ setCompress(e)}/>
+
+
+ {/* Plot Button */}
+
+ {!tooBig && }
+
+
+
+ {/* Status Information */}
+
+ {/* Size info badge */}
+
+ Raw: {formatBytes(currentSize)}
+ |
+ Stored: {compress ? "<" : ""}{formatBytes(cachedSize)}
+
+
+ {/* Messages */}
+
+ {tooBig &&
+
+ Too many textures ({texCount}/14). Won't fit.
+
+ }
+ {cached &&
+
+ {cachedChunks ? `${cachedChunks} chunks already cached` : "Already cached"}
+
+ }
+
+
+
+
+ {/* Coarsen Expand UI */}
+
+
+
= 3 ? 'visible' : 'hidden' }}
+ >
+
Temporal Coarsening
+
+ {
+ const val = parseInt(e.target.value)
+ setDisplayDepth(e.target.value)
+ setKernelDepth(Math.pow(2,val))
+ }}
+ />
+
+
+
+
Spatial Coarsening
+
+ {
+ const val = parseInt(e.target.value)
+ setDisplaySpat(e.target.value)
+ setKernelSize(Math.pow(2, val))
+ }}
+ />
+
+
+
+ Values represent 2ⁿ
+
+
+
+
+ {/* Cache expand UI if needed */}
+ {currentSize > maxSize && (
+
+ {smallCache ? : }
+
+ {smallCache ? "Selection won't fit in Cache" : "Data Will Fit"}
+
+
+
+
Decrease selection or expand cache size
+
+
+
+
+ )}
+
+ {/* Dimension Table */}
+
+
+
+
+
+ | Dim |
+ Axis |
+ Selection |
+ Data Shape |
+ Chunk Shape |
+
+
+
+ {availableDims.map((dim, originalIndex) => {
+ const activeRow = rows.find((r) => r.dimName === dim.name);
+ const sel = activeRow ? activeRow.sel : collapsedSels[dim.name];
+ const range = !sel ? '?' : sel.mode === 'scalar' ? sel.scalar || '0' : `${sel.start !== '' ? sel.start : '0'}:${sel.stop !== '' ? sel.stop : ':'}`;
+ const axis = activeRow ? activeRow.axis : 'c';
+ const dataSize = dataShape[originalIndex] ?? '?';
+ const chunkSize = chunkShape[originalIndex] ?? '?';
+
+ return (
+
+ |
+ {dim.name}
+ |
+ {axis.toUpperCase()} |
+ {range} |
+ {dataSize} |
+ {chunkSize} |
+
+ );
+ })}
+
+
+
+
+
+
+ {/* DimSlicers Area */}
+
+
+
Active Dimensions
+
+
+
+ {addTooltip && (
+
+ )}
+
+
+
+ {/* Active slicers */}
+
+ {rows.map((row, i) => {
+ const dim = availableDims.find((d) => d.name === row.dimName);
+ const isLast = i === rows.length - 1;
+ return (
+ updateDimName(row.id, name)}
+ onRemove={isLast && rows.length > 1 ? removeLastRow : undefined}
+ dimSize={dim?.size ?? 0}
+ selection={row.sel}
+ axis={row.axis}
+ onChange={(sel) => updateSel(row.id, sel)}
+ values={dim?.values}
+ formatValue={dim?.formatValue}
+ lockMode="slice"
+ allowedAxes={['z', 'y', 'x']}
+ />
+ );
+ })}
+
+
+ {/* Collapsed dimensions */}
+ {collapsedDims.length > 0 && (
+
+
+
+ {collapsedOpen && (
+
+ {collapsedDims.map((dim) => (
+ { }}
+ dimSize={dim.size}
+ selection={collapsedSels[dim.name] ?? { ...defaultSelection(dim.size), mode: 'scalar' }}
+ axis="c"
+ onChange={(sel) => updateCollapsedSel(dim.name, sel)}
+ values={dim.values}
+ formatValue={dim.formatValue}
+ lockMode="scalar"
+ />
+ ))}
+
+ )}
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ui/MainPanel/PlayButton.tsx b/src/components/ui/MainPanel/PlayButton.tsx
index d7e68036a..43e2175ac 100644
--- a/src/components/ui/MainPanel/PlayButton.tsx
+++ b/src/components/ui/MainPanel/PlayButton.tsx
@@ -109,10 +109,11 @@ const PlayInterFace = ({visible, setKeepOpen, setShowSelf}:{visible : boolean, s
zMeta: state.zMeta,
variable: state.variable,
})))
- const {reFetch, setZSlice, ReFetch} = useZarrStore(useShallow(state => ({
+ const {reFetch, setZSlice, ReFetch, axisMapping} = useZarrStore(useShallow(state => ({
reFetch: state.reFetch,
setZSlice: state.setZSlice,
- ReFetch: state.ReFetch
+ ReFetch: state.ReFetch,
+ axisMapping: state.axisMapping
})))
const intervalRef = useRef(null)
@@ -122,14 +123,17 @@ const PlayInterFace = ({visible, setKeepOpen, setShowSelf}:{visible : boolean, s
const [showNextChunk, setShowNextChunk] = useState(false)
const [showPrevChunk, setShowPrevChunk] = useState(false)
- // TIME SLICE INFO
- const timeArray = dimArrays[dimArrays.length-3]
- let timeSlice = timeArray?.slice(zSlice[0], zSlice[1] ?? undefined)
- const timeLength = timeArray?.length || 1
- let sliceDist = zSlice[1] ? zSlice[1] - zSlice[0] : timeLength - zSlice[0]
+ const shapeLength = dimArrays?.length || 3;
+ const zIdx = axisMapping?.z >= 0 ? axisMapping.z : Math.max(0, shapeLength - 3);
- if (coarsen && timeSlice) {
- timeSlice = coarsenFlatArray(timeSlice, kernel.kernelDepth)
+ // Z-SLICE INFO
+ const zArray = dimArrays[zIdx]
+ let zArraySlice = zArray?.slice(zSlice[0], zSlice[1] ?? undefined)
+ const zLength = zArray?.length || 1
+ let sliceDist = zSlice[1] ? zSlice[1] - zSlice[0] : zLength - zSlice[0]
+
+ if (coarsen && zArraySlice) {
+ zArraySlice = coarsenFlatArray(zArraySlice, kernel.kernelDepth)
sliceDist = Math.floor(sliceDist/kernel.kernelDepth)
}
@@ -137,13 +141,13 @@ const PlayInterFace = ({visible, setKeepOpen, setShowSelf}:{visible : boolean, s
const [chunkTimeLength, chunkDivWidth, chunkSize] = useMemo(()=>{
const meta = (zMeta as {name : string, chunks:number[], chunkSize:number}[])?.find(e => e.name === variable)
if(meta) {
- const chunkTimeSize = meta.chunks[meta.chunks.length - 3]
- const tempWidth = (chunkTimeSize / timeLength) * 100
+ const chunkTimeSize = meta.chunks[zIdx] ?? meta.chunks[meta.chunks.length - 3]
+ const tempWidth = (chunkTimeSize / zLength) * 100
const chunkSize = meta.chunkSize
return [chunkTimeSize, tempWidth, chunkSize]
}
return [0,0, 1]
- }, [zMeta, variable, timeLength])
+ }, [zMeta, variable, zLength, zIdx])
function AdjustCacheSize(){
const {maxSize, setMaxSize, cache} = useCacheStore.getState()
@@ -154,7 +158,7 @@ const PlayInterFace = ({visible, setKeepOpen, setShowSelf}:{visible : boolean, s
}
// ANIMATION LOOP
useEffect(() => {
- if (!timeSlice?.length) return
+ if (!zArraySlice?.length) return
if (animate) {
if (previousFPS.current !== fps && intervalRef.current) {
clearInterval(intervalRef.current)
@@ -183,9 +187,9 @@ const PlayInterFace = ({visible, setKeepOpen, setShowSelf}:{visible : boolean, s
}, [animate, fps])
// LABELS
- const currentLabel = parseLoc(timeSlice?.[Math.round(animProg * sliceDist)], dimUnits[0], true)
- const firstLabel = parseLoc(timeSlice?.[0], dimUnits[0], true)
- const lastLabel = parseLoc(timeSlice?.[sliceDist-1], dimUnits[0], true)
+ const currentLabel = parseLoc(zArraySlice?.[Math.round(animProg * sliceDist)], dimUnits[zIdx], true)
+ const firstLabel = parseLoc(zArraySlice?.[0], dimUnits[zIdx], true)
+ const lastLabel = parseLoc(zArraySlice?.[sliceDist-1], dimUnits[zIdx], true)
// RESET ON FETCH
useEffect(()=>{
@@ -230,10 +234,10 @@ const PlayInterFace = ({visible, setKeepOpen, setShowSelf}:{visible : boolean, s