From cca0ec7686c3e336173dcfa73a7a7bc0e0c2dfdb Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 26 Jun 2026 11:00:59 +0200 Subject: [PATCH 01/58] dim select components --- src/components/ui/DimSlicer/DimSlicer.tsx | 406 ++++++++++++++++++ .../ui/DimSlicer/DimSlicerAxisToggle.tsx | 73 ++++ .../ui/DimSlicer/DimSlicerModeToggle.tsx | 67 +++ .../ui/DimSlicer/DimSlicerNumericControl.tsx | 36 ++ .../DimSlicerNumericInputWithStepper.tsx | 105 +++++ .../ui/DimSlicer/DimSlicerTimeControl.tsx | 65 +++ src/components/ui/DimSlicer/TimeCombobox.tsx | 100 +++++ src/components/ui/DimSlicer/index.ts | 2 + .../ui/MainPanel/MetaDimSelector.tsx | 355 +++++++++++++++ src/components/ui/MainPanel/Variables.tsx | 73 ++-- 10 files changed, 1249 insertions(+), 33 deletions(-) create mode 100644 src/components/ui/DimSlicer/DimSlicer.tsx create mode 100644 src/components/ui/DimSlicer/DimSlicerAxisToggle.tsx create mode 100644 src/components/ui/DimSlicer/DimSlicerModeToggle.tsx create mode 100644 src/components/ui/DimSlicer/DimSlicerNumericControl.tsx create mode 100644 src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx create mode 100644 src/components/ui/DimSlicer/DimSlicerTimeControl.tsx create mode 100644 src/components/ui/DimSlicer/TimeCombobox.tsx create mode 100644 src/components/ui/DimSlicer/index.ts create mode 100644 src/components/ui/MainPanel/MetaDimSelector.tsx diff --git a/src/components/ui/DimSlicer/DimSlicer.tsx b/src/components/ui/DimSlicer/DimSlicer.tsx new file mode 100644 index 00000000..21965e61 --- /dev/null +++ b/src/components/ui/DimSlicer/DimSlicer.tsx @@ -0,0 +1,406 @@ +'use client'; +import React, { useState, useCallback } from 'react'; +import { Slider } from '@/components/ui/slider'; +import { Trash2 } from 'lucide-react'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +import { DimSlicerAxisToggle } from './DimSlicerAxisToggle'; +import { DimSlicerModeToggle } from './DimSlicerModeToggle'; +import { DimSlicerNumericControl } from './DimSlicerNumericControl'; +import { DimSlicerTimeControl } from './DimSlicerTimeControl'; + +export type SelectionMode = 'scalar' | 'slice'; +export type Axis = 'x' | 'y' | 'z' | 'c'; + +export interface SliceSelectionState { + mode: SelectionMode; + scalar: string; + start: string; + stop: string; +} + +export function defaultSelection(dimSize?: number): SliceSelectionState { + const maxIndex = dimSize ? Math.max(dimSize - 1, 0) : 0; + return { mode: 'slice', scalar: '0', start: '0', stop: String(maxIndex) }; +} + +const MODE_ACCENT: Record = { + scalar: 'border-l-teal-700', + slice: 'border-l-[#644FF0]', +}; + +export interface DimOption { + name: 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) return val; + let closestIndex = 0; + let minDiff = Math.abs(values[0] - val); + for (let i = 1; i < values.length; i++) { + const diff = Math.abs(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 00000000..a58adac8 --- /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'; +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 00000000..30a15f4a --- /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'; +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 00000000..2a9494b6 --- /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 00000000..4cd1e36b --- /dev/null +++ b/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx @@ -0,0 +1,105 @@ +'use client'; +import React, { useEffect, useRef, useState } from 'react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +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 isFocused = useRef(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); + }, []); + + // Sync prop changes to local state when not focused + useEffect(() => { + if (!isFocused.current) { + setLocalValue(value); + } + }, [value]); + + const handleFocus = () => { + isFocused.current = true; + }; + + const handleBlur = () => { + isFocused.current = false; + onValueChange(localValue); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + onValueChange(localValue); + } + }; + + return ( +
+ + {showInput && ( + setLocalValue(e.target.value)} + onFocus={handleFocus} + onBlur={handleBlur} + onKeyDown={handleKeyDown} + onClick={() => setExpanded(false)} + className="no-spinner h-7 text-xs w-16 text-center appearance-none" + placeholder={placeholder} + aria-label={ariaLabel} + /> + )} + {expanded ? ( + + + + + ) : ( + + )} + +
+ ); +}; diff --git a/src/components/ui/DimSlicer/DimSlicerTimeControl.tsx b/src/components/ui/DimSlicer/DimSlicerTimeControl.tsx new file mode 100644 index 00000000..af9e6285 --- /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 00000000..b15c8f5c --- /dev/null +++ b/src/components/ui/DimSlicer/TimeCombobox.tsx @@ -0,0 +1,100 @@ +'use client' +import React, { useState, useMemo } 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 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 filtered = useMemo(() => { + const normalizedInput = inputValue.trim().toLowerCase() + const selectedQuery = selectedLabel.trim().toLowerCase() + return normalizedInput === '' || normalizedInput === selectedQuery + ? labeledValues + : labeledValues.filter(({ label }) => label.toLowerCase().includes(normalizedInput)) + }, [inputValue, selectedLabel, labeledValues]) + + const targetWidth = Math.min( + Math.max(Math.max(selectedLabel.length, placeholder.length) + 2, 12), + 20 + ) + + return ( + + setInputQuery(typeof value === 'string' ? value : String(value)) + } + autoHighlight + > + + + {filtered.length === 0 ? No items found. : null} + + {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 00000000..0622ada3 --- /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/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx new file mode 100644 index 00000000..3f74a99c --- /dev/null +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -0,0 +1,355 @@ +"use client"; + +import React, { useMemo, useState } 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'; +import { useGlobalStore } from '@/GlobalStates/GlobalStore'; +import { useShallow } from 'zustand/shallow'; +import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; +import { Badge } from "@/components/ui"; +import { parseLoc } from '@/utils/HelperFuncs'; +import { ChevronDown, ChevronRight } from 'lucide-react'; + +const MAX_ACTIVE_DIMS = 3; +const DEFAULT_AXES: Axis[] = ['z', 'y', 'x']; + +const formatArray = (value: string | number[]): string => { + if (typeof value === 'string') return value; + return Array.isArray(value) ? value.join(', ') : String(value); +}; + +interface DimInfo { + dimArrays: ArrayLike[]; + dimNames: string[]; + dimUnits: (string | null)[]; +} + +type Props = { + meta: { + name?: string; + shape?: number[]; + chunks?: number[]; + long_name?: string; + dimInfo?: DimInfo; + [key: string]: unknown; + }; + metadata?: Record; + onApply?: (sels: SliceSelectionState[], axes: Axis[], dimNames: string[]) => void; +}; + +const AXIS_COLOR: Record = { + x: 'text-pink-500', + y: 'text-green-500', + z: 'text-blue-500', + c: 'text-yellow-500', +}; + +function axisForIndex(idx: number): Axis { + return DEFAULT_AXES[idx] ?? DEFAULT_AXES[DEFAULT_AXES.length - 1]; +} + +function selectionSummary( + availableDims: DimOption[], + activeRows: SlicerRow[], + collapsedSels: Record, +): string { + const parts = availableDims.map((dim) => { + const activeRow = activeRows.find((r) => r.dimName === dim.name); + const sel = activeRow ? activeRow.sel : collapsedSels[dim.name]; + if (!sel) return `${dim.name}=?`; + const range = + sel.mode === 'scalar' + ? sel.scalar || '0' + : `${sel.start !== '' ? sel.start : '0'}:${sel.stop !== '' ? sel.stop : ':'}`; + return `${dim.name}=${range}`; + }); + return `[ ${parts.join(', ')} ]`; +} + +interface SlicerRow { + id: number; + dimName: string; + sel: SliceSelectionState; + axis: Axis; +} + +let _nextId = 0; +const nextId = () => ++_nextId; + +export default function MetaDimSelector({ meta, metadata, onApply }: Props) { + const rawDimArrays = meta?.dimInfo?.dimArrays ?? []; + const rawDimNames = meta?.dimInfo?.dimNames ?? []; + const rawDimUnits = meta?.dimInfo?.dimUnits ?? []; + const dataShape = meta?.shape; + const chunkShape = meta?.chunks; + + const dimArrays: number[][] = useMemo( + () => rawDimArrays.map((a) => Array.from(a)), + [rawDimArrays], + ); + const dimUnits: string[] = useMemo( + () => rawDimUnits.map((u) => u ?? ''), + [rawDimUnits], + ); + const dimNames: string[] = rawDimNames; + + const { setDimArrays, setDimNames, setDimUnits } = useGlobalStore( + useShallow((state) => ({ + setDimArrays: state.setDimArrays, + setDimNames: state.setDimNames, + setDimUnits: state.setDimUnits, + })), + ); + + React.useEffect(() => { + setDimArrays(dimArrays); + setDimNames(dimNames); + setDimUnits(dimUnits); + }, [dimArrays, dimNames, dimUnits, setDimArrays, setDimNames, setDimUnits]); + + const availableDims: DimOption[] = useMemo( + () => + dimArrays.map((values, idx) => ({ + name: dimNames[idx] ?? `dim${idx}`, + 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 => + Object.fromEntries(dims.map((d) => [d.name, { ...defaultSelection(d.size), mode: 'scalar' as const }])); + + /** Default to first MIN(3, availableDims.length) dims as active rows */ + const makeInitialRows = (dims: DimOption[]): SlicerRow[] => + dims.slice(0, MAX_ACTIVE_DIMS).map((d, i) => ({ + id: nextId(), + dimName: d.name, + sel: { ...defaultSelection(d.size), mode: 'slice' }, + axis: axisForIndex(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)); + } + + const firstUnusedDim = (currentRows: SlicerRow[]): string => { + const usedNames = new Set(currentRows.map((r) => r.dimName)); + return availableDims.find((d) => !usedNames.has(d.name))?.name ?? ''; + }; + + 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 newRow: SlicerRow = { + id: nextId(), + dimName, + sel: { ...defaultSelection(dim.size), mode: 'slice' }, + axis: axisForIndex(prev.length), + }; + return [...prev, newRow]; + }); + }; + + const removeLastRow = () => + setRows((prev) => prev.slice(0, -1)); + + 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 updateAxis = (id: number, axis: Axis) => + // setRows((prev) => prev.map((r) => (r.id === id ? { ...r, axis } : r))); + + const updateCollapsedSel = (dimName: string, sel: SliceSelectionState) => + setCollapsedSels((prev) => ({ ...prev, [dimName]: { ...sel, mode: 'scalar' } })); + + const summary = useMemo( + () => selectionSummary(availableDims, rows, collapsedSels), + [availableDims, rows, collapsedSels], + ); + + 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; + + return ( + <> + {`${meta.long_name} `} + + + Attributes + + + {renderAttributes(metadata, defaultAttributes)} + + +
+ +
+ {'selection'} {summary} +
+ +
+ {rows.map((row) => { + const originalIndex = availableDims.findIndex( + (d) => d.name === row.dimName + ); + + return ( + + ( + {row.dimName} + ,{' '} + {row.axis} + ,{' '} + + {originalIndex} + + ) + + ); + })} +
+ +
+
+ Data Shape + {`[${formatArray(dataShape ?? [])}]`} +
+
+ Chunk Shape + {`[${formatArray(chunkShape ?? [])}]`} +
+
+ +
+ + + {addTooltip && ( +
+
+ {addTooltip} +
+
+ )} +
+ + {/* Active slicers — locked to slice, z/y/x axes only, trash only on last */} +
+ {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)} + // onAxisChange={(axis) => updateAxis(row.id, axis)} + 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/Variables.tsx b/src/components/ui/MainPanel/Variables.tsx index c3564fb8..694ec24d 100644 --- a/src/components/ui/MainPanel/Variables.tsx +++ b/src/components/ui/MainPanel/Variables.tsx @@ -5,7 +5,7 @@ import { TbVariable } from "react-icons/tb"; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { useShallow } from "zustand/shallow"; import { Separator } from "@/components/ui/separator"; -import MetaDataInfo from "./MetaDataInfo"; +import MetaDimSelector from "./MetaDimSelector"; import { GetDimInfo } from "@/utils/HelperFuncs"; import { GetAttributes } from "@/components/zarr/ZarrLoaderLRU"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; @@ -34,7 +34,7 @@ const Variables = () => { const [openMetaPopover, setOpenMetaPopover] = useState(false); const [showMeta, setShowMeta] = useState(false); - const { variables, zMeta, metadata, initStore, openVariables, setMetadata, setOpenVariables } = useGlobalStore( + const { variables, zMeta, metadata, initStore, openVariables, setMetadata, setOpenVariables } = useGlobalStore( useShallow((state) => ({ variables: state.variables, zMeta: state.zMeta, @@ -46,9 +46,7 @@ const Variables = () => { })) ); - const [dimArrays, setDimArrays] = useState([[0],[0],[0]]); - const [dimUnits, setDimUnits] = useState([null,null,null]); - const [dimNames, setDimNames] = useState(["Default"]); + const [selectedVar, setSelectedVar] = useState(null); const [meta, setMeta] = useState(null); @@ -110,10 +108,24 @@ const Variables = () => { // Handle variable selection const handleVariableSelect = (val: string, idx: number) => { setSelectedVar(val); - GetDimInfo(val).then(e => { - setDimNames(e.dimNames); - setDimArrays(e.dimArrays); - setDimUnits(e.dimUnits); + setMeta(null); + setMetadata(null); + + Promise.all([GetDimInfo(val), GetAttributes(val)]).then(([dimInfo, attr]) => { + const relevant = zMeta?.find((e: any) => e.name === val); + if (relevant) { + setMeta({ + ...relevant, + dimInfo: { + dimArrays: dimInfo.dimArrays, + dimNames: dimInfo.dimNames, + dimUnits: dimInfo.dimUnits, + }, + }); + } + setMetadata(attr); + }).catch((err) => { + console.error("Failed to fetch dimension info or attributes:", err); }); if (popoverSide === "left") { @@ -124,20 +136,10 @@ const Variables = () => { }; useEffect(() => { - if (variables && zMeta && selectedVar) { - const relevant = zMeta.find((e: any) => e.name === selectedVar); - if (relevant){ - setMeta({...relevant, dimInfo : {dimArrays, dimNames, dimUnits}}); - GetAttributes(selectedVar).then(e=>setMetadata(e)); - } - } - }, [selectedVar, variables, zMeta, dimArrays, dimNames, dimUnits]); - - useEffect(()=>{ setSelectedVar(null); setMeta(null); setMetadata(null); - },[initStore, setMetadata]); + }, [initStore, setMetadata]); useEffect(() => { const handleResize = () => { @@ -315,15 +317,19 @@ const Variables = () => { data-meta-popover side="left" align="start" - className="max-h-[80vh] overflow-y-auto w-[300px]" + className="max-h-[80vh] overflow-y-auto w-[400px]" > - {meta && ( - { + // close UI after applying selections + setOpenMetaPopover(false); + setOpenVariables(false); + // future: persist sels/axes to store + console.log('Applied slices', sels, axes); + }} /> )} @@ -332,15 +338,16 @@ const Variables = () => { {popoverSide === "top" && ( - {} + { }
{meta && ( - { + setShowMeta(false); + setOpenVariables(false); + console.log('Applied slices', sels, axes); + }} /> )}
From 63efcb3fce5912c40900bbcfd13e95eda560f724 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 26 Jun 2026 11:09:36 +0200 Subject: [PATCH 02/58] fix value shown --- src/components/ui/DimSlicer/DimSlicer.tsx | 4 +++- .../DimSlicerNumericInputWithStepper.tsx | 20 ++++++------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/components/ui/DimSlicer/DimSlicer.tsx b/src/components/ui/DimSlicer/DimSlicer.tsx index 21965e61..c12265f4 100644 --- a/src/components/ui/DimSlicer/DimSlicer.tsx +++ b/src/components/ui/DimSlicer/DimSlicer.tsx @@ -83,7 +83,9 @@ const DimSlicer: React.FC = ({ const sel = lockMode ? { ...rawSel, mode: lockMode } : rawSel; const getIndexFromValue = (val: number): number => { - if (!values) return val; + if (!values) { + return clamp(Math.round(val / step) * step, 0, maxIndex); + } let closestIndex = 0; let minDiff = Math.abs(values[0] - val); for (let i = 1; i < values.length; i++) { diff --git a/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx b/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx index 4cd1e36b..c99f0915 100644 --- a/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx +++ b/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx @@ -26,7 +26,6 @@ export const DimSlicerNumericInputWithStepper: React.FC { const [expanded, setExpanded] = useState(false); const [localValue, setLocalValue] = useState(value); - const isFocused = useRef(false); const rootRef = useRef(null); useEffect(() => { @@ -40,25 +39,19 @@ export const DimSlicerNumericInputWithStepper: React.FC document.removeEventListener('mousedown', handleClickOutside); }, []); - // Sync prop changes to local state when not focused + // Sync prop changes to local state useEffect(() => { - if (!isFocused.current) { - setLocalValue(value); - } + setLocalValue(value); }, [value]); - const handleFocus = () => { - isFocused.current = true; - }; - - const handleBlur = () => { - isFocused.current = false; + const commitValue = () => { onValueChange(localValue); + setLocalValue(value); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { - onValueChange(localValue); + commitValue(); } }; @@ -70,8 +63,7 @@ export const DimSlicerNumericInputWithStepper: React.FC setLocalValue(e.target.value)} - onFocus={handleFocus} - onBlur={handleBlur} + onBlur={commitValue} onKeyDown={handleKeyDown} onClick={() => setExpanded(false)} className="no-spinner h-7 text-xs w-16 text-center appearance-none" From 9e6469678eb39528d631d12655be89be5eefe09f Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 26 Jun 2026 11:29:05 +0200 Subject: [PATCH 03/58] fixes defaults keys --- src/components/ui/DimSlicer/DimSlicer.tsx | 16 ++++---- .../ui/MainPanel/MetaDimSelector.tsx | 39 +++++++++++-------- src/components/ui/MainPanel/Variables.tsx | 2 + 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/components/ui/DimSlicer/DimSlicer.tsx b/src/components/ui/DimSlicer/DimSlicer.tsx index c12265f4..8f4cb34b 100644 --- a/src/components/ui/DimSlicer/DimSlicer.tsx +++ b/src/components/ui/DimSlicer/DimSlicer.tsx @@ -37,6 +37,7 @@ const MODE_ACCENT: Record = { export interface DimOption { name: string; + label?: string; size: number; values?: number[]; formatValue?: (value: number) => string; @@ -172,8 +173,8 @@ const DimSlicer: React.FC = ({ {availableDims.map((d) => ( - - {d.name} + + {d.label ?? d.name} ))} @@ -187,12 +188,11 @@ const DimSlicer: React.FC = ({ /> )} {sel.mode === 'slice' && ( - + {propAxis} )} diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index 3f74a99c..1ea171aa 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -79,9 +79,9 @@ const nextId = () => ++_nextId; export default function MetaDimSelector({ meta, metadata, onApply }: Props) { const rawDimArrays = meta?.dimInfo?.dimArrays ?? []; - const rawDimNames = meta?.dimInfo?.dimNames ?? []; - const rawDimUnits = meta?.dimInfo?.dimUnits ?? []; - const dataShape = meta?.shape; + const rawDimNames = meta?.dimInfo?.dimNames ?? []; + const rawDimUnits = meta?.dimInfo?.dimUnits ?? []; + const dataShape = meta?.shape; const chunkShape = meta?.chunks; const dimArrays: number[][] = useMemo( @@ -97,8 +97,8 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { const { setDimArrays, setDimNames, setDimUnits } = useGlobalStore( useShallow((state) => ({ setDimArrays: state.setDimArrays, - setDimNames: state.setDimNames, - setDimUnits: state.setDimUnits, + setDimNames: state.setDimNames, + setDimUnits: state.setDimUnits, })), ); @@ -110,13 +110,20 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { const availableDims: DimOption[] = useMemo( () => - dimArrays.map((values, idx) => ({ - name: dimNames[idx] ?? `dim${idx}`, - size: values.length, - values, - formatValue: (v: number): string => - String(parseLoc(values[v] ?? v, dimUnits[idx] || undefined)), - })), + 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], ); @@ -205,8 +212,8 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { const addTooltip = atMax ? `Maximum of ${MAX_ACTIVE_DIMS} dimensions, remove one before adding another.` : noUnused - ? 'All dimensions are already active.' - : undefined; + ? 'All dimensions are already active.' + : undefined; return ( <> @@ -309,7 +316,7 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { /> ); })} -
+ {/* Collapsed dimensions */} {collapsedDims.length > 0 && ( @@ -330,7 +337,7 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { key={dim.name} availableDims={availableDims} dimName={dim.name} - onDimChange={() => {}} + onDimChange={() => { }} dimSize={dim.size} selection={collapsedSels[dim.name] ?? { ...defaultSelection(dim.size), mode: 'scalar' }} axis="c" diff --git a/src/components/ui/MainPanel/Variables.tsx b/src/components/ui/MainPanel/Variables.tsx index 694ec24d..391605b1 100644 --- a/src/components/ui/MainPanel/Variables.tsx +++ b/src/components/ui/MainPanel/Variables.tsx @@ -321,6 +321,7 @@ const Variables = () => { > {metadata && ( { @@ -342,6 +343,7 @@ const Variables = () => {
{meta && ( { setShowMeta(false); From a7493a100e8181181b04d30a1b8875aabad8b22c Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 26 Jun 2026 11:35:49 +0200 Subject: [PATCH 04/58] enhanced button --- src/components/ui/DimSlicer/DimSlicerAxisToggle.tsx | 2 +- src/components/ui/DimSlicer/DimSlicerModeToggle.tsx | 2 +- .../ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx | 2 +- src/components/ui/MainPanel/MetaDimSelector.tsx | 7 +++++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/ui/DimSlicer/DimSlicerAxisToggle.tsx b/src/components/ui/DimSlicer/DimSlicerAxisToggle.tsx index a58adac8..e4f1baf0 100644 --- a/src/components/ui/DimSlicer/DimSlicerAxisToggle.tsx +++ b/src/components/ui/DimSlicer/DimSlicerAxisToggle.tsx @@ -1,6 +1,6 @@ 'use client'; import React, { useEffect, useRef, useState } from 'react'; -import { Button } from '@/components/ui/button'; +import { Button } from '@/components/ui/button-enhanced'; import { ButtonGroup } from '@/components/ui/button-group'; export type Axis = 'x' | 'y' | 'z' | 'c'; diff --git a/src/components/ui/DimSlicer/DimSlicerModeToggle.tsx b/src/components/ui/DimSlicer/DimSlicerModeToggle.tsx index 30a15f4a..14af38ec 100644 --- a/src/components/ui/DimSlicer/DimSlicerModeToggle.tsx +++ b/src/components/ui/DimSlicer/DimSlicerModeToggle.tsx @@ -1,6 +1,6 @@ 'use client'; import React, { useEffect, useRef, useState } from 'react'; -import { Button } from '@/components/ui/button'; +import { Button } from '@/components/ui/button-enhanced'; import { ButtonGroup } from '@/components/ui/button-group'; export type SelectionMode = 'scalar' | 'slice'; diff --git a/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx b/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx index c99f0915..3b9aa9a0 100644 --- a/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx +++ b/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useEffect, useRef, useState } from 'react'; import { Input } from '@/components/ui/input'; -import { Button } from '@/components/ui/button'; +import { Button } from '@/components/ui/button-enhanced'; import { ButtonGroup } from '@/components/ui/button-group'; import { MinusIcon, PlusIcon } from 'lucide-react'; diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index 1ea171aa..d5a3454b 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -3,7 +3,7 @@ import React, { useMemo, useState } 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'; +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"; @@ -353,7 +353,10 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { )}
-
From f13f165808c254d05a3362fbdf99c40453151b86 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 26 Jun 2026 11:38:33 +0200 Subject: [PATCH 05/58] more keys --- src/components/ui/DimSlicer/DimSlicer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/DimSlicer/DimSlicer.tsx b/src/components/ui/DimSlicer/DimSlicer.tsx index 8f4cb34b..119e9925 100644 --- a/src/components/ui/DimSlicer/DimSlicer.tsx +++ b/src/components/ui/DimSlicer/DimSlicer.tsx @@ -173,7 +173,7 @@ const DimSlicer: React.FC = ({ {availableDims.map((d) => ( - + {d.label ?? d.name} ))} From 7524860c878b18c055e6c0a3dcc3e57a6209e870 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 18:29:47 +0200 Subject: [PATCH 06/58] sugg --- src/components/ui/DimSlicer/DimSlicer.tsx | 8 +++--- .../ui/MainPanel/MetaDimSelector.tsx | 26 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/components/ui/DimSlicer/DimSlicer.tsx b/src/components/ui/DimSlicer/DimSlicer.tsx index 119e9925..bcdb23d4 100644 --- a/src/components/ui/DimSlicer/DimSlicer.tsx +++ b/src/components/ui/DimSlicer/DimSlicer.tsx @@ -84,7 +84,7 @@ const DimSlicer: React.FC = ({ const sel = lockMode ? { ...rawSel, mode: lockMode } : rawSel; const getIndexFromValue = (val: number): number => { - if (!values) { + if (!values || values.length === 0) { return clamp(Math.round(val / step) * step, 0, maxIndex); } let closestIndex = 0; @@ -189,9 +189,9 @@ const DimSlicer: React.FC = ({ )} {sel.mode === 'slice' && ( {propAxis} diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index d5a3454b..e24333a7 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -78,21 +78,21 @@ let _nextId = 0; const nextId = () => ++_nextId; export default function MetaDimSelector({ meta, metadata, onApply }: Props) { - const rawDimArrays = meta?.dimInfo?.dimArrays ?? []; - const rawDimNames = meta?.dimInfo?.dimNames ?? []; - const rawDimUnits = meta?.dimInfo?.dimUnits ?? []; + 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 dimArrays: number[][] = useMemo( - () => rawDimArrays.map((a) => Array.from(a)), - [rawDimArrays], - ); - const dimUnits: string[] = useMemo( - () => rawDimUnits.map((u) => u ?? ''), - [rawDimUnits], - ); - const dimNames: string[] = rawDimNames; const { setDimArrays, setDimNames, setDimUnits } = useGlobalStore( useShallow((state) => ({ @@ -217,7 +217,7 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { return ( <> - {`${meta.long_name} `} + {`${meta.long_name ?? ''} `} Attributes From dc2b43fd3251efb6e9fdfe98aec384c57811bc2f Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 19:02:21 +0200 Subject: [PATCH 07/58] mv over --- .../ui/MainPanel/MetaDimSelector.tsx | 334 +++++++++++++++++- 1 file changed, 317 insertions(+), 17 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index e24333a7..f6376032 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -1,16 +1,23 @@ "use client"; -import React, { useMemo, useState } from 'react'; +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 { Badge } from "@/components/ui"; +import { Badge, Switch, Input, Hider } from "@/components/ui"; import { parseLoc } from '@/utils/HelperFuncs'; import { ChevronDown, ChevronRight } from 'lucide-react'; +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 DEFAULT_AXES: Axis[] = ['z', 'y', 'x']; @@ -19,6 +26,14 @@ const formatArray = (value: string | number[]): string => { 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[]; @@ -30,12 +45,17 @@ type Props = { 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 = { @@ -77,7 +97,12 @@ interface SlicerRow { let _nextId = 0; const nextId = () => ++_nextId; -export default function MetaDimSelector({ meta, metadata, onApply }: Props) { +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 dimArrays = useMemo( () => (meta?.dimInfo?.dimArrays ?? []).map((a) => Array.from(a)), [meta?.dimInfo?.dimArrays] @@ -90,24 +115,54 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { () => meta?.dimInfo?.dimNames ?? [], [meta?.dimInfo?.dimNames] ); - const dataShape = meta?.shape; - const chunkShape = meta?.chunks; + const dataShape = meta?.shape || []; + const chunkShape = meta?.chunks || []; - const { setDimArrays, setDimNames, setDimUnits } = useGlobalStore( + 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 { setZSlice, setYSlice, setXSlice, ReFetch, compress, setCompress, coarsen, setCoarsen, kernelSize, setKernelSize, kernelDepth, setKernelDepth } = useZarrStore(useShallow(state => ({ + 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 [texCount, setTexCount] = useState(0) + const [displaySpat, setDisplaySpat] = useState(String(kernelSize)) + const [displayDepth, setDisplayDepth] = useState(String(kernelDepth)) + React.useEffect(() => { setDimArrays(dimArrays); setDimNames(dimNames); setDimUnits(dimUnits); }, [dimArrays, dimNames, dimUnits, setDimArrays, setDimNames, setDimUnits]); + useEffect(()=>{ + setCompress(false) + if (cache.has(`${initStore}_${meta.name}`)){ + setCached(true); + } else { + setCached(false); + } + },[meta, cache, initStore, setCompress]) + const availableDims: DimOption[] = useMemo( () => dimArrays.map((values, idx) => { @@ -154,6 +209,88 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { setCollapsedSels(makeInitialCollapsedSels(availableDims)); } + const currentSize = useMemo(() => { + const is2D = dataShape.length === 2; + + 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; + return { first: start, last: stop, steps: Math.max(1, stop - start) }; + }; + + 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 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]; + + setTextureArrayDepths( + 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) + setTexCount(thisCount) + + if (thisCount > 14){ + setTooBig(true) + } else { + setTooBig(false) + } + + if (is2D) { + const totalSteps = x.steps * y.steps; + const sizeRatio = totalSteps / (dataShape[0] * dataShape[1] || 1); + return (meta.totalSize || 0) * sizeRatio; + } else { + const chunkZ = origIdxZ >= 0 ? chunkShape[origIdxZ] : 1; + const chunkY = origIdxY >= 0 ? chunkShape[origIdxY] : 1; + const chunkX = origIdxX >= 0 ? chunkShape[origIdxX] : 1; + + const xChunksNeeded = Math.ceil(x.steps / chunkX); + const yChunksNeeded = Math.ceil(y.steps / chunkY); + const zChunksNeeded = Math.ceil(z.steps / chunkZ); + + const size = xChunksNeeded * yChunksNeeded * zChunksNeeded * (meta.chunkSize || 1); + return size / (coarsen ? kernelDepth * Math.pow(kernelSize, 2) : 1) + } + }, [meta, rows, dataShape, chunkShape, coarsen, kernelSize, kernelDepth, maxTextureSize, max3DTextureSize, setTextureArrayDepths]); + + 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 ?? ''; @@ -191,9 +328,6 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { const updateSel = (id: number, sel: SliceSelectionState) => setRows((prev) => prev.map((r) => (r.id === id ? { ...r, sel: { ...sel, mode: 'slice' } } : r))); - // const updateAxis = (id: number, axis: Axis) => - // setRows((prev) => prev.map((r) => (r.id === id ? { ...r, axis } : r))); - const updateCollapsedSel = (dimName: string, sel: SliceSelectionState) => setCollapsedSels((prev) => ({ ...prev, [dimName]: { ...sel, mode: 'scalar' } })); @@ -215,6 +349,48 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { ? '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]; + } + return [parseInt(sel.start) || 0, parseInt(sel.stop) || defaultLast]; + }; + + 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)); + + 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 ( <> {`${meta.long_name ?? ''} `} @@ -308,7 +484,6 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { selection={row.sel} axis={row.axis} onChange={(sel) => updateSel(row.id, sel)} - // onAxisChange={(axis) => updateAxis(row.id, axis)} values={dim?.values} formatValue={dim?.formatValue} lockMode="slice" @@ -352,13 +527,138 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) {
)} -
- + {/* Coarsen Section */} +
+
+ + setCoarsen(e)}/> +
+
+ +
+
= 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 2n +
+
+
+ + {/* Size and Cache UI */} +
+
+
+ Raw Size: {formatBytes(currentSize)} +
+
+ Stored size: {compress ? "<" : null} {formatBytes(cachedSize)} +
+
+ + {currentSize > maxSize && ( + <> +
+
+ + {smallCache ? "Selection won't fit in Cache" : "Data Will Fit"} + +
+
+ Decrease selection or Increase cache size
+
+ Expand Cache: +
+ setCacheSize(parseInt(e.target.value)*(1024*1024))} + /> + MB +
+ + + + + + + Increasing this too far can cause crashes. Mobile users beware + + +
+ setCacheSize(maxSize+e[0]*(1024*1024))} + /> +
+ + )} + +
+
+ +
+ setCompress(e)}/> +
+ +
+ {cached && +
+ This data is already cached. +
+ } + {tooBig && +
+ + Not only will this certainly not fit in memory, but it also won't fit in a single shader call. You are wild for this one. Textures: {texCount}/14 + +
+ } + {!tooBig && }
); From 104de3cbeb1a8f99a92e76a03c148df1ef995079 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 19:15:57 +0200 Subject: [PATCH 08/58] r rules --- .../ui/MainPanel/MetaDimSelector.tsx | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index f6376032..e6a70d3a 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -209,7 +209,7 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, setCollapsedSels(makeInitialCollapsedSels(availableDims)); } - const currentSize = useMemo(() => { + const sizeData = useMemo(() => { const is2D = dataShape.length === 2; const getSliceDims = (row?: SlicerRow, defaultLast = 0) => { @@ -244,24 +244,17 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, const maxSizeLimit = is2D ? maxTextureSize : max3DTextureSize; const texCounts = [z.steps / maxSizeLimit, y.steps / maxSizeLimit, x.steps / maxSizeLimit]; - setTextureArrayDepths( - texCounts.some(count => count > 1) + 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) - setTexCount(thisCount) + : [1, 1, 1]; - if (thisCount > 14){ - setTooBig(true) - } else { - setTooBig(false) - } + const thisCount = texCounts.reduce((prod, val) => prod * Math.ceil(val), 1) + let calculatedSize = 0; if (is2D) { const totalSteps = x.steps * y.steps; const sizeRatio = totalSteps / (dataShape[0] * dataShape[1] || 1); - return (meta.totalSize || 0) * sizeRatio; + calculatedSize = (meta.totalSize || 0) * sizeRatio; } else { const chunkZ = origIdxZ >= 0 ? chunkShape[origIdxZ] : 1; const chunkY = origIdxY >= 0 ? chunkShape[origIdxY] : 1; @@ -272,9 +265,19 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, const zChunksNeeded = Math.ceil(z.steps / chunkZ); const size = xChunksNeeded * yChunksNeeded * zChunksNeeded * (meta.chunkSize || 1); - return size / (coarsen ? kernelDepth * Math.pow(kernelSize, 2) : 1) + calculatedSize = size / (coarsen ? kernelDepth * Math.pow(kernelSize, 2) : 1); } - }, [meta, rows, dataShape, chunkShape, coarsen, kernelSize, kernelDepth, maxTextureSize, max3DTextureSize, setTextureArrayDepths]); + + return { size: calculatedSize, thisCount, depths }; + }, [meta, rows, 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) || ''; From 48d936d7ecde277bc869d25248dd7b5cf5ce1ab1 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 19:25:19 +0200 Subject: [PATCH 09/58] fix vertex, specialize on use case --- src/components/plots/CountryBorders.tsx | 5 ++++- src/components/plots/DataCube.tsx | 4 ++++ src/components/textures/shaders/vertex.glsl | 24 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/components/plots/CountryBorders.tsx b/src/components/plots/CountryBorders.tsx index 94d45696..42bd8cd2 100644 --- a/src/components/plots/CountryBorders.tsx +++ b/src/components/plots/CountryBorders.tsx @@ -69,10 +69,13 @@ function Borders({features}:{features: any}){ vertexShader, fragmentShader: bordersFrag, uniforms:{ - xBounds: {value: new THREE.Vector2(xRange[0], xRange[1])}, + xBounds: {value: new THREE.Vector2(-xRange[1],-xRange[0])}, yBounds: {value: new THREE.Vector2(yRange[0]/shape.x, yRange[1]/shape.x)}, borderColor: {value: new THREE.Color(borderColor)}, trim: {value: !spherize}, + }, + defines: { + USE_APOSITION: 1 } } ),[]) diff --git a/src/components/plots/DataCube.tsx b/src/components/plots/DataCube.tsx index bc97a87a..98b4aba4 100644 --- a/src/components/plots/DataCube.tsx +++ b/src/components/plots/DataCube.tsx @@ -69,6 +69,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/textures/shaders/vertex.glsl b/src/components/textures/shaders/vertex.glsl index 5c0b968e..2c9aa0a1 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 From 62372c9392d8f4185fdf4c456f47cf292fcddca3 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 19:55:47 +0200 Subject: [PATCH 10/58] ndSlices mappings --- src/GlobalStates/ZarrStore.ts | 9 ++ .../ui/MainPanel/MetaDimSelector.tsx | 32 +++++-- src/components/zarr/GetArray.ts | 50 ++++++++-- src/components/zarr/dataFetchers.ts | 92 +++++++++++++++---- 4 files changed, 145 insertions(+), 38 deletions(-) diff --git a/src/GlobalStates/ZarrStore.ts b/src/GlobalStates/ZarrStore.ts index 423796df..8fb2c104 100644 --- a/src/GlobalStates/ZarrStore.ts +++ b/src/GlobalStates/ZarrStore.ts @@ -25,9 +25,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,6 +54,8 @@ 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), reFetch: false, @@ -69,6 +76,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/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index e6a70d3a..edf2b6a3 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -256,15 +256,9 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, const sizeRatio = totalSteps / (dataShape[0] * dataShape[1] || 1); calculatedSize = (meta.totalSize || 0) * sizeRatio; } else { - const chunkZ = origIdxZ >= 0 ? chunkShape[origIdxZ] : 1; - const chunkY = origIdxY >= 0 ? chunkShape[origIdxY] : 1; - const chunkX = origIdxX >= 0 ? chunkShape[origIdxX] : 1; - - const xChunksNeeded = Math.ceil(x.steps / chunkX); - const yChunksNeeded = Math.ceil(y.steps / chunkY); - const zChunksNeeded = Math.ceil(z.steps / chunkZ); - - const size = xChunksNeeded * yChunksNeeded * zChunksNeeded * (meta.chunkSize || 1); + const totalSteps = x.steps * y.steps * z.steps; + const sizeRatio = totalSteps / (dataShape.reduce((a, b) => a * b, 1) || 1); + const size = (meta.totalSize || 0) * sizeRatio; calculatedSize = size / (coarsen ? kernelDepth * Math.pow(kernelSize, 2) : 1); } @@ -371,6 +365,26 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, 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; + return [parseInt(row.sel.start) || 0, parseInt(row.sel.stop) || dim.size]; + } + 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]; diff --git a/src/components/zarr/GetArray.ts b/src/components/zarr/GetArray.ts index c1ded8fa..7ce89f04 100644 --- a/src/components/zarr/GetArray.ts +++ b/src/components/zarr/GetArray.ts @@ -5,12 +5,12 @@ import { useErrorStore } from "@/GlobalStates/ErrorStore"; import { calculateStrides } from "@/utils/HelperFuncs"; import { ToFloat16, CompressArray, DecompressArray, copyChunkToArray, RescaleArray, copyChunkToArray2D } from "./utils"; import { NCFetcher, zarrFetcher } from "./dataFetchers"; -import { Convolve } from "../computation/webGPU"; +import { Convolve, Convolve2D } from "../computation/webGPU"; import { coarsen3DArray } from "@/utils/HelperFuncs"; export async function GetArray(varOveride?: string) { const { idx4D, initStore, variable, setProgress, setStrides, setStatus } = useGlobalStore.getState(); - const { compress, xSlice, ySlice, zSlice, coarsen, kernelSize, kernelDepth, fetchNC, setCurrentChunks, setArraySize } = useZarrStore.getState(); + const { compress, xSlice, ySlice, zSlice, ndSlices, axisMapping, coarsen, kernelSize, kernelDepth, fetchNC, setCurrentChunks, setArraySize } = useZarrStore.getState(); const { cache } = useCacheStore.getState(); const useNC = initStore.startsWith("local") && fetchNC // In case a user has NetCDF switched but then goes to a remote const fetcher = useNC ? NCFetcher() : zarrFetcher() @@ -19,7 +19,9 @@ export async function GetArray(varOveride?: string) { const { shape, chunkShape, fillValue, dtype } = meta; const rank = shape.length; const hasZ = rank >= 3; - const xDimIndex = rank - 1, yDimIndex = rank - 2, zDimIndex = rank - 3; + const xDimIndex = axisMapping.x >= 0 ? axisMapping.x : rank - 1; + const yDimIndex = axisMapping.y >= 0 ? axisMapping.y : rank - 2; + const zDimIndex = axisMapping.z >= 0 ? axisMapping.z : rank - 3; const calcDim = (slice: [number, number | null], dimIdx: number) => { // This function provides information for extraction from each dimension of datarray if (dimIdx < 0) return { start: 0, end: 1, size: 0, chunkDim: 1 }; @@ -91,18 +93,46 @@ export async function GetArray(varOveride?: string) { [yDim.start, xDim.start]) } } else { - const raw = await fetcher.fetchChunk({ variable:targetVariable, rank, shape, chunkShape, x, y, z, xDimIndex, yDimIndex, zDimIndex, idx4D }); + const raw = await fetcher.fetchChunk({ variable:targetVariable, rank, shape, chunkShape, x, y, z, xDimIndex, yDimIndex, zDimIndex, idx4D, ndSlices, axisMapping }); const rawData = Number.isFinite(fillValue) ? raw.data.map((v: number) => v === fillValue ? NaN : v) : raw.data; // Don't map if no fillvalue let [chunkF16, newScalingFactor] = ToFloat16(rawData, scalingFactor); - let thisShape = raw.shape; - let chunkStride = raw.stride; + + // Determine which indices in raw.shape map to Z, Y, X + const activeDims: number[] = []; + for (let i = 0; i < rank; i++) { + if (ndSlices && ndSlices.length === rank) { + if (i === xDimIndex || i === yDimIndex || i === zDimIndex || Array.isArray(ndSlices[i])) { + activeDims.push(i); + } + } else { + if (i === xDimIndex || i === yDimIndex || i === zDimIndex) { + activeDims.push(i); + } + } + } + + const zIndexInRaw = activeDims.indexOf(zDimIndex); + const yIndexInRaw = activeDims.indexOf(yDimIndex); + const xIndexInRaw = activeDims.indexOf(xDimIndex); + + let thisShape = hasZ ? [raw.shape[zIndexInRaw], raw.shape[yIndexInRaw], raw.shape[xIndexInRaw]] : [raw.shape[yIndexInRaw], raw.shape[xIndexInRaw]]; + let chunkStride = hasZ ? [raw.stride[zIndexInRaw], raw.stride[yIndexInRaw], raw.stride[xIndexInRaw]] : [raw.stride[yIndexInRaw], raw.stride[xIndexInRaw]]; if (coarsen) { - chunkF16 = await Convolve(chunkF16, { shape: chunkShape, strides: chunkStride }, "Mean3D", { kernelSize, kernelDepth }) as Float16Array; - thisShape = thisShape.map((dim, idx) => Math.floor(dim / (idx === 0 ? kernelDepth : kernelSize))); - chunkF16 = coarsen3DArray(chunkF16, chunkShape, chunkStride as any, kernelSize, kernelDepth, thisShape.reduce((a, b) => a * b, 1)); + const origShape = [...thisShape]; + if (hasZ) { + chunkF16 = await Convolve(chunkF16, { shape: origShape, strides: chunkStride }, "Mean3D", { kernelSize, kernelDepth }) as Float16Array; + thisShape = origShape.map((dim, idx) => Math.floor(dim / (idx === 0 ? kernelDepth : kernelSize))); + chunkF16 = coarsen3DArray(chunkF16, origShape as [number, number, number], chunkStride as [number, number, number], kernelSize, kernelDepth, thisShape.reduce((a, b) => a * b, 1)); + } else { + chunkF16 = await Convolve2D(chunkF16, { shape: origShape, strides: chunkStride }, "Mean2D", kernelSize) as Float16Array; + thisShape = origShape.map((dim, idx) => Math.floor(dim / kernelSize)); + const paddedShape = [1, origShape[0], origShape[1]] as [number, number, number]; + const paddedStride = [1, chunkStride[0], chunkStride[1]] as [number, number, number]; + chunkF16 = coarsen3DArray(chunkF16, paddedShape, paddedStride, kernelSize, 1, thisShape.reduce((a, b) => a * b, 1)); + } chunkStride = calculateStrides(thisShape); } @@ -126,7 +156,7 @@ export async function GetArray(varOveride?: string) { cache.set(cacheName, { data: compress ? CompressArray(chunkF16, 7) : chunkF16, - shape: chunkShape, stride: chunkStride, + shape: thisShape, stride: chunkStride, scaling: scalingFactor, compressed: compress, coarsened: coarsen, kernel: { kernelDepth: coarsen ? kernelDepth : undefined, kernelSize: coarsen ? kernelSize : undefined } }); diff --git a/src/components/zarr/dataFetchers.ts b/src/components/zarr/dataFetchers.ts index 0b5186a6..a9fa0fe6 100644 --- a/src/components/zarr/dataFetchers.ts +++ b/src/components/zarr/dataFetchers.ts @@ -98,23 +98,50 @@ export function NCFetcher() { return { shape, chunkShape, fillValue, validRange, preScaling, dtype: varInfo.dtype }; }, - async fetchChunk({ rank, shape, chunkShape, x, y, z, xDimIndex, yDimIndex, zDimIndex, idx4D, variable }: any): Promise { + async fetchChunk({ rank, shape, chunkShape, x, y, z, xDimIndex, yDimIndex, zDimIndex, idx4D, variable, ndSlices }: any): Promise { const starts = new Array(rank).fill(0); const counts = new Array(rank).fill(1); - if (rank > 3) { starts[0] = idx4D; counts[0] = 1; } - starts[xDimIndex] = x * chunkShape[xDimIndex]; - counts[xDimIndex] = Math.min(chunkShape[xDimIndex], shape[xDimIndex] - starts[xDimIndex]); - starts[yDimIndex] = y * chunkShape[yDimIndex]; - counts[yDimIndex] = Math.min(chunkShape[yDimIndex], shape[yDimIndex] - starts[yDimIndex]); - if (zDimIndex >= 0) { - starts[zDimIndex] = z * chunkShape[zDimIndex]; - counts[zDimIndex] = Math.min(chunkShape[zDimIndex], shape[zDimIndex] - starts[zDimIndex]); + if (ndSlices && ndSlices.length === rank) { + for (let i = 0; i < rank; i++) { + if (i === xDimIndex) { + starts[i] = x * chunkShape[i]; + counts[i] = Math.min(chunkShape[i], shape[i] - starts[i]); + } else if (i === yDimIndex) { + starts[i] = y * chunkShape[i]; + counts[i] = Math.min(chunkShape[i], shape[i] - starts[i]); + } else if (i === zDimIndex) { + starts[i] = z * chunkShape[i]; + counts[i] = Math.min(chunkShape[i], shape[i] - starts[i]); + } else { + const sel = ndSlices[i]; + if (Array.isArray(sel)) { + starts[i] = sel[0]; + counts[i] = sel[1] - sel[0]; + } else { + starts[i] = sel; + counts[i] = 1; + } + } + } + } else { + if (rank > 3) { starts[0] = idx4D; counts[0] = 1; } + starts[xDimIndex] = x * chunkShape[xDimIndex]; + counts[xDimIndex] = Math.min(chunkShape[xDimIndex], shape[xDimIndex] - starts[xDimIndex]); + starts[yDimIndex] = y * chunkShape[yDimIndex]; + counts[yDimIndex] = Math.min(chunkShape[yDimIndex], shape[yDimIndex] - starts[yDimIndex]); + if (zDimIndex >= 0) { + starts[zDimIndex] = z * chunkShape[zDimIndex]; + counts[zDimIndex] = Math.min(chunkShape[zDimIndex], shape[zDimIndex] - starts[zDimIndex]); + } } let data = await ncModule.getSlicedVariableArray(variable, starts, counts); - return { data, shape: counts as number[], stride: calculateStrides(counts) }; + // Filter out collapsed dims so shape matches Zarrita behavior + let collapsedShape = counts.filter((c, i) => ndSlices ? (!Array.isArray(ndSlices[i]) && i !== xDimIndex && i !== yDimIndex && i !== zDimIndex ? false : true) : c !== 1); + if (collapsedShape.length === 0) collapsedShape = [1]; + return { data, shape: collapsedShape, stride: calculateStrides(collapsedShape) }; }, }; } @@ -163,6 +190,7 @@ export class ZarrFetcher { async fetchChunk({ rank, + shape, chunkShape, x, y, @@ -171,6 +199,7 @@ export class ZarrFetcher { yDimIndex, zDimIndex, idx4D, + ndSlices }: any): Promise { if (!this.outVar) { await this.init(); @@ -180,13 +209,32 @@ export class ZarrFetcher { } const chunkSlice = new Array(rank).fill(0); - chunkSlice[xDimIndex] = zarr.slice(x * chunkShape[xDimIndex], (x + 1) * chunkShape[xDimIndex]); - chunkSlice[yDimIndex] = zarr.slice(y * chunkShape[yDimIndex], (y + 1) * chunkShape[yDimIndex]); - if (zDimIndex >= 0) { - chunkSlice[zDimIndex] = zarr.slice(z * chunkShape[zDimIndex], (z + 1) * chunkShape[zDimIndex]); - } - if (rank >= 4) { - chunkSlice[0] = idx4D; + if (ndSlices && ndSlices.length === rank) { + for (let i = 0; i < rank; i++) { + if (i === xDimIndex) { + chunkSlice[i] = zarr.slice(x * chunkShape[i], Math.min((x + 1) * chunkShape[i], shape[i])); + } else if (i === yDimIndex) { + chunkSlice[i] = zarr.slice(y * chunkShape[i], Math.min((y + 1) * chunkShape[i], shape[i])); + } else if (i === zDimIndex) { + chunkSlice[i] = zarr.slice(z * chunkShape[i], Math.min((z + 1) * chunkShape[i], shape[i])); + } else { + const sel = ndSlices[i]; + if (Array.isArray(sel)) { + chunkSlice[i] = zarr.slice(sel[0], sel[1]); + } else { + chunkSlice[i] = sel; + } + } + } + } else { + chunkSlice[xDimIndex] = zarr.slice(x * chunkShape[xDimIndex], (x + 1) * chunkShape[xDimIndex]); + chunkSlice[yDimIndex] = zarr.slice(y * chunkShape[yDimIndex], (y + 1) * chunkShape[yDimIndex]); + if (zDimIndex >= 0) { + chunkSlice[zDimIndex] = zarr.slice(z * chunkShape[zDimIndex], (z + 1) * chunkShape[zDimIndex]); + } + if (rank >= 4) { + chunkSlice[0] = idx4D; + } } const chunk = await fetchWithRetry( @@ -198,10 +246,16 @@ export class ZarrFetcher { if (!chunk || chunk.data instanceof BigInt64Array || chunk.data instanceof BigUint64Array) { throw new Error("BigInt arrays not supported."); } + + let outShape = chunk.shape; + if (!outShape || outShape.length === 0) { + outShape = chunkShape; // fallback if Zarrita doesn't collapse + } + return { data: chunk.data as Float32Array, - shape: chunkShape, - stride: chunk.stride, + shape: outShape as number[], + stride: chunk.stride as number[], }; } } \ No newline at end of file From 86cf48f48bb40df294f2a05c3a779cb2a2e9054a Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 21:14:11 +0200 Subject: [PATCH 11/58] too many --- src/GlobalStates/ZarrStore.ts | 3 +- src/components/computation/shaders/frag.glsl | 2 +- src/components/computation/shaders/vert.glsl | 2 +- src/components/plots/AxisLines.tsx | 38 +++++++++++++------ src/components/plots/FlatMap.tsx | 22 ++++++----- src/components/plots/Sphere.tsx | 14 +++++-- src/components/plots/plotarea/FixedTicks.tsx | 19 +++++++--- src/components/textures/shaders/flatFrag.glsl | 2 +- .../textures/shaders/sphereVertex.glsl | 6 +-- src/components/zarr/GetArray.ts | 4 +- src/components/zarr/ZarrLoaderLRU.ts | 2 + src/components/zarr/dataFetchers.ts | 14 ++++--- src/utils/HelperFuncs.ts | 14 ++++--- 13 files changed, 94 insertions(+), 48 deletions(-) diff --git a/src/GlobalStates/ZarrStore.ts b/src/GlobalStates/ZarrStore.ts index 8fb2c104..875f3e37 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' @@ -57,7 +56,7 @@ export const useZarrStore = create((set, get) => ({ 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, diff --git a/src/components/computation/shaders/frag.glsl b/src/components/computation/shaders/frag.glsl index 7e1fd2d3..527b1fd9 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 14e9b8a6..6b50cb5f 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/plots/AxisLines.tsx b/src/components/plots/AxisLines.tsx index 86d9a67d..a97873e9 100644 --- a/src/components/plots/AxisLines.tsx +++ b/src/components/plots/AxisLines.tsx @@ -53,20 +53,27 @@ 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 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; 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) @@ -364,22 +371,31 @@ 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 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 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){ diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index 9d86eca7..1e3e7edd 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -54,27 +54,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 (() => { + 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 = dimArrays.length === 2 ? [ - 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){ diff --git a/src/components/plots/Sphere.tsx b/src/components/plots/Sphere.tsx index a9eb589e..ac1519e6 100644 --- a/src/components/plots/Sphere.tsx +++ b/src/components/plots/Sphere.tsx @@ -9,6 +9,7 @@ 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'; function XYZtoRemap(xyz : THREE.Vector3, latBounds: number[], lonBounds : number[]){ const lon = Math.atan2(xyz.z,xyz.x) const lat = Math.asin(xyz.y); @@ -62,10 +63,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() diff --git a/src/components/plots/plotarea/FixedTicks.tsx b/src/components/plots/plotarea/FixedTicks.tsx index 04eecc8c..d5b7be84 100644 --- a/src/components/plots/plotarea/FixedTicks.tsx +++ b/src/components/plots/plotarea/FixedTicks.tsx @@ -51,11 +51,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/textures/shaders/flatFrag.glsl b/src/components/textures/shaders/flatFrag.glsl index bab39875..ba49450b 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/sphereVertex.glsl b/src/components/textures/shaders/sphereVertex.glsl index dffa7f30..94a72759 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/zarr/GetArray.ts b/src/components/zarr/GetArray.ts index 7ce89f04..68f782d9 100644 --- a/src/components/zarr/GetArray.ts +++ b/src/components/zarr/GetArray.ts @@ -56,6 +56,9 @@ export async function GetArray(varOveride?: string) { let iter = 1; const rescaleIDs: string[] = []; + const scalarIndices = (ndSlices && ndSlices.length > 0) ? ndSlices.filter(s => typeof s === "number").join("_") : (idx4D ?? ""); + const cacheBase = scalarIndices !== "" ? `${initStore}_${targetVariable}_${scalarIndices}` : `${initStore}_${targetVariable}`; + setStatus("Downloading..."); setProgress(0); @@ -63,7 +66,6 @@ export async function GetArray(varOveride?: string) { for (let y = yDim.start; y < yDim.end; y++) { for (let x = xDim.start; x < xDim.end; x++) { const chunkID = `z${z}_y${y}_x${x}`; - const cacheBase = rank > 3 ? `${initStore}_${targetVariable}_${idx4D}` : `${initStore}_${targetVariable}`; const cacheName = `${cacheBase}_chunk_${chunkID}`; const cachedChunk = cache.get(cacheName); const isCacheValid = cachedChunk && diff --git a/src/components/zarr/ZarrLoaderLRU.ts b/src/components/zarr/ZarrLoaderLRU.ts index c0e6eb12..28ca70e3 100644 --- a/src/components/zarr/ZarrLoaderLRU.ts +++ b/src/components/zarr/ZarrLoaderLRU.ts @@ -44,6 +44,7 @@ export async function GetZarrMetadata( groupStore: Promise, ): Promise { const group = await groupStore; + if (!group) return []; if (group.store instanceof IcechunkStore) { return getIcechunkMetadata(group as zarr.Group); } @@ -56,6 +57,7 @@ export async function GetTitleDescription( groupStore: Promise, ): Promise { const group = await groupStore; + if (!group) return { title: null, description: null }; if (group.store instanceof IcechunkStore) { return getIcechunkTitleDescription(group as zarr.Group); } diff --git a/src/components/zarr/dataFetchers.ts b/src/components/zarr/dataFetchers.ts index a9fa0fe6..d9ed8ba3 100644 --- a/src/components/zarr/dataFetchers.ts +++ b/src/components/zarr/dataFetchers.ts @@ -42,6 +42,7 @@ export function zarrFetcher() { return { async getMetadata(variable: string) { const group = await currentStore; + if (!group) throw new Error("Zarr store not initialized"); const tempOutVar = await zarr.open(group.resolve(variable), { kind: "array" }); outVar = tempOutVar if (!outVar.is("number") && !outVar.is("bigint")) { @@ -60,12 +61,13 @@ export function zarrFetcher() { _outVar: outVar, // carry through for fetchChunk } as any; }, - async fetchChunk({ variable, rank, chunkShape, x, y, z, xDimIndex, yDimIndex, zDimIndex, idx4D }: any): Promise { - const chunkSlice = new Array(rank).fill(0); - chunkSlice[xDimIndex] = zarr.slice(x * chunkShape[xDimIndex], (x + 1) * chunkShape[xDimIndex]); - chunkSlice[yDimIndex] = zarr.slice(y * chunkShape[yDimIndex], (y + 1) * chunkShape[yDimIndex]); - if (zDimIndex >= 0) chunkSlice[zDimIndex] = zarr.slice(z * chunkShape[zDimIndex], (z + 1) * chunkShape[zDimIndex]); - if (rank >= 4) chunkSlice[0] = idx4D; + async fetchChunk({ variable, chunkShape, ndSlices }: any): Promise { + const chunkSlice = ndSlices.map((s: any, i: number) => { + if (typeof s === "number") return s; + const start = s[0] * chunkShape[i]; + const end = (s[0] + 1) * chunkShape[i]; + return zarr.slice(start, end); + }); const chunk = await fetchWithRetry(() => zarr.get(outVar, chunkSlice), `variable ${variable}`, useGlobalStore.getState().setStatus); if (!chunk || chunk.data instanceof BigInt64Array || chunk.data instanceof BigUint64Array) { diff --git a/src/utils/HelperFuncs.ts b/src/utils/HelperFuncs.ts index e3df047a..3fa8d229 100644 --- a/src/utils/HelperFuncs.ts +++ b/src/utils/HelperFuncs.ts @@ -259,13 +259,16 @@ function DecompressArray(compressed : Uint8Array){ export function GetCurrentArray(overrideStore?:string){ const { variable, is4D, idx4D, initStore, strides, dataShape, setStatus }= useGlobalStore.getState() - const { arraySize, currentChunks } = useZarrStore.getState() + const { arraySize, currentChunks, ndSlices } = useZarrStore.getState() const {cache} = useCacheStore.getState(); const store = overrideStore ? overrideStore : initStore - if (cache.has(is4D ? `${store}_${idx4D}_${variable}` : `${store}_${variable}`)){ - const chunk = cache.get(is4D ? `${store}_${idx4D}_${variable}` : `${store}_${variable}`) - const compressed = chunk.compressed + const scalarIndices = (ndSlices && ndSlices.length > 0) ? ndSlices.filter(s => typeof s === "number").join("_") : (idx4D ?? ""); + const cacheBase = scalarIndices !== "" ? `${store}_${variable}_${scalarIndices}` : `${store}_${variable}`; + + if (cache.has(cacheBase)){ + const chunk = cache.get(cacheBase) + const compressed = chunk?.compressed setStatus(compressed ? "Decompressing data..." : null) const thisData = compressed ? DecompressArray(chunk.data) : chunk.data setStatus(null) @@ -281,8 +284,9 @@ export function GetCurrentArray(overrideStore?:string){ for (let y = yStartIdx; y < yEndIdx; y++) { for (let x = xStartIdx; x < xEndIdx; x++) { const chunkID = `z${z}_y${y}_x${x}` - const cacheName = is4D ? `${store}_${variable}_${idx4D}_chunk_${chunkID}` : `${store}_${variable}_chunk_${chunkID}` + const cacheName = `${cacheBase}_chunk_${chunkID}` const chunk = cache.get(cacheName) + if (!chunk) continue; const compressed = chunk.compressed const thisData = compressed ? DecompressArray(chunk.data) : chunk.data copyChunkToArray( From 9057e776999cf5cd689e6dc065d6411454ac0ec1 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 21:26:31 +0200 Subject: [PATCH 12/58] fixes axislines --- src/components/plots/AxisLines.tsx | 68 +++++++++++++++--------------- src/utils/HelperFuncs.ts | 47 ++++++++++++--------- 2 files changed, 60 insertions(+), 55 deletions(-) diff --git a/src/components/plots/AxisLines.tsx b/src/components/plots/AxisLines.tsx index a97873e9..8615af0c 100644 --- a/src/components/plots/AxisLines.tsx +++ b/src/components/plots/AxisLines.tsx @@ -59,10 +59,11 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli 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(()=> { - 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 = [ 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) ?? [], @@ -121,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 ( @@ -147,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])} ))} @@ -158,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])} ))} @@ -222,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 && @@ -277,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])} ))} @@ -290,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]} @@ -379,11 +380,11 @@ const FlatAxis = () =>{ const originallyFlat = dimArrays.length == 2; const slices = originallyFlat ? [ySlice, xSlice] : [zSlice, ySlice, xSlice] + 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(()=> { - 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 originallyFlat ? [ flipY ? dimArrays[yIdx]?.slice().reverse() ?? [] : dimArrays[yIdx] ?? [], @@ -415,34 +416,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(()=>{ diff --git a/src/utils/HelperFuncs.ts b/src/utils/HelperFuncs.ts index 3fa8d229..0b159378 100644 --- a/src/utils/HelperFuncs.ts +++ b/src/utils/HelperFuncs.ts @@ -73,7 +73,7 @@ const months = [ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; -export function parseLoc(input:number, units: string | undefined, verbose: boolean = false) { +export function parseLoc(input: any, units: string | undefined, verbose: boolean = false) { if (!units){ if (typeof(input) == 'bigint'){ return input; @@ -105,15 +105,15 @@ export function parseLoc(input:number, units: string | undefined, verbose: boole return input; } } - if ( units.match(/(degree|degrees|deg|°)/i) ){ - if (input){ - return `${input.toFixed(2)}°` + if ( units && units.match(/(degree|degrees|deg|°)/i) ){ + if (input !== undefined && input !== null){ + return `${Number(input).toFixed(2)}°` } else{ return input } } else { - return input ? input.toFixed(2) : input; + return (input !== undefined && input !== null && typeof input !== 'string') ? Number(input).toFixed(2) : input; } } @@ -179,27 +179,34 @@ export function linspace(start: number, stop: number, num: number): number[] { return Array.from({ length: num }, (_, i) => start + step * i); } -export function ParseExtent(dimUnits: string[], dimArrays: number[][]){ +export function ParseExtent(dimUnits: string[], dimArrays: any[][]){ const {setLonExtent, setLatExtent, setLonResolution, setLatResolution, setOriginalExtent } = usePlotStore.getState(); const {xSlice, ySlice} = usePlotStore.getState(); - const tempUnits = dimUnits.length > 2 ? dimUnits.slice(1) : dimUnits; + const {axisMapping} = useZarrStore.getState(); + const shapeLength = dimArrays.length; + const xIdx = axisMapping.x >= 0 ? axisMapping.x : shapeLength - 1; + const yIdx = axisMapping.y >= 0 ? axisMapping.y : shapeLength - 2; + let tryParse = false; - for (const unit of tempUnits){ - if (!unit) continue; - if (unit.match(/(degree|degrees|deg|°)/i)){ + const xUnit = dimUnits[xIdx]; + const yUnit = dimUnits[yIdx]; + + if ((xUnit && xUnit.match(/(degree|degrees|deg|°)/i)) || (yUnit && yUnit.match(/(degree|degrees|deg|°)/i))) { tryParse = true; - break; - } } + if (tryParse){ - const tempArrs = dimArrays.length > 2 ? dimArrays.slice(1) : dimArrays - const minLat = tempArrs[0][ySlice[0]] - const maxLat = tempArrs[0][ySlice[1]??tempArrs[0].length-1] - let minLon = tempArrs[1][xSlice[0]] - let maxLon = tempArrs[1][xSlice[1]?? tempArrs[1].length-1] + const xArray = dimArrays[xIdx] || []; + const yArray = dimArrays[yIdx] || []; + + const minLat = Number(yArray[ySlice[0]]); + const maxLat = Number(yArray[ySlice[1] ?? yArray.length-1]); + let minLon = Number(xArray[xSlice[0]]); + let maxLon = Number(xArray[xSlice[1] ?? xArray.length-1]); + if (maxLon > 180){ maxLon -= 180 - minLon -=180 + minLon -= 180 usePlotStore.setState({is360Deg:true}) } else{ usePlotStore.setState({is360Deg:false}) @@ -207,8 +214,8 @@ export function ParseExtent(dimUnits: string[], dimArrays: number[][]){ setLonExtent([minLon, maxLon]) setLatExtent([minLat, maxLat]) - const latRes = Math.abs(tempArrs[0][1] - tempArrs[0][0]) - const lonRes = Math.abs(tempArrs[1][1] - tempArrs[1][0]) + const latRes = Math.abs(Number(yArray[1] ?? 0) - Number(yArray[0] ?? 0)) || 1; + const lonRes = Math.abs(Number(xArray[1] ?? 0) - Number(xArray[0] ?? 0)) || 1; setLonResolution(lonRes) setLatResolution(latRes) setOriginalExtent(new THREE.Vector4(minLon, maxLon, minLat, maxLat)) From 9d841db31b6c9f3d529b1b5ac2f1500eff66f905 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 21:38:05 +0200 Subject: [PATCH 13/58] more glsl3 updates --- src/components/plots/plotarea/ThickLine.tsx | 2 +- src/components/textures/shaders/LandingVertex.glsl | 2 +- src/components/textures/shaders/LineVert.glsl | 2 +- src/components/textures/shaders/sphereBlocksFrag.glsl | 2 +- src/components/textures/shaders/thickLineVert.glsl | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/plots/plotarea/ThickLine.tsx b/src/components/plots/plotarea/ThickLine.tsx index 57fafc23..92350f72 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 fcc021b1..95a703b8 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 e5df8116..c5e0a76d 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/sphereBlocksFrag.glsl b/src/components/textures/shaders/sphereBlocksFrag.glsl index 48ed1214..a0c9f8ac 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/thickLineVert.glsl b/src/components/textures/shaders/thickLineVert.glsl index c27a6e10..6314e147 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; From 178217999b4320e236c488aa2d120725bbb953ca Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 21:46:56 +0200 Subject: [PATCH 14/58] fixes webgl issues in firefox --- src/components/plots/DataCube.tsx | 2 +- src/components/plots/FlatBlocks.tsx | 4 ++-- src/components/plots/FlatMap.tsx | 2 +- src/components/plots/Sphere.tsx | 2 +- src/components/plots/SphereBlocks.tsx | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/plots/DataCube.tsx b/src/components/plots/DataCube.tsx index 98b4aba4..88983690 100644 --- a/src/components/plots/DataCube.tsx +++ b/src/components/plots/DataCube.tsx @@ -47,7 +47,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: Array.from({ length: 14 }, (_, idx) => volTexture?.[idx] ?? volTexture?.[0])}, maskTexture: { value: maskTexture }, maskValue: {value: maskValue }, textureDepths: {value: new THREE.Vector3(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0])}, diff --git a/src/components/plots/FlatBlocks.tsx b/src/components/plots/FlatBlocks.tsx index 2bddce35..0a29834d 100644 --- a/src/components/plots/FlatBlocks.tsx +++ b/src/components/plots/FlatBlocks.tsx @@ -81,7 +81,7 @@ const FlatBlocks = ({textures} : {textures: THREE.Data3DTexture[] | THREE.DataTe const shader = new THREE.ShaderMaterial({ glslVersion: THREE.GLSL3, uniforms: { - map: { value: textures }, + map: { value: Array.from({ length: 14 }, (_, idx) => textures?.[idx] ?? textures?.[0]) }, maskTexture: {value: maskTexture}, maskValue: {value: maskValue}, threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, @@ -111,7 +111,7 @@ const FlatBlocks = ({textures} : {textures: THREE.Data3DTexture[] | THREE.DataTe useEffect(()=>{ if (shaderMaterial){ const uniforms = shaderMaterial.uniforms; - uniforms.map.value = textures; + uniforms.map.value = Array.from({ length: 14 }, (_, idx) => textures?.[idx] ?? textures?.[0]); uniforms.animateProg.value = animProg uniforms.displaceZero.value = -valueScales.minVal/(valueScales.maxVal-valueScales.minVal) uniforms.displacement.value = displacement diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index 1e3e7edd..4f61e960 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -172,7 +172,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: Array.from({ length: 14 }, (_, idx) => textures?.[idx] ?? textures?.[0])}, maskTexture: {value: maskTexture}, maskValue: {value: maskValue}, threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, diff --git a/src/components/plots/Sphere.tsx b/src/components/plots/Sphere.tsx index ac1519e6..a24595f4 100644 --- a/src/components/plots/Sphere.tsx +++ b/src/components/plots/Sphere.tsx @@ -83,7 +83,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: Array.from({ length: 14 }, (_, idx) => textures?.[idx] ?? textures?.[0]) }, 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 d32641ff..67d453c9 100644 --- a/src/components/plots/SphereBlocks.tsx +++ b/src/components/plots/SphereBlocks.tsx @@ -79,7 +79,7 @@ const SphereBlocks = ({textures} : {textures: THREE.Data3DTexture[] | THREE.Data const shader = new THREE.ShaderMaterial({ glslVersion: THREE.GLSL3, uniforms: { - map: { value: textures }, + map: { value: Array.from({ length: 14 }, (_, idx) => textures?.[idx] ?? textures?.[0]) }, maskTexture: {value: maskTexture}, maskValue: {value: maskValue}, threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, @@ -108,7 +108,7 @@ const SphereBlocks = ({textures} : {textures: THREE.Data3DTexture[] | THREE.Data useEffect(()=>{ if (shaderMaterial){ const uniforms = shaderMaterial.uniforms; - uniforms.map.value = textures; + uniforms.map.value = Array.from({ length: 14 }, (_, idx) => textures?.[idx] ?? textures?.[0]); uniforms.animateProg.value = animProg uniforms.displaceZero.value = -valueScales.minVal/(valueScales.maxVal-valueScales.minVal) uniforms.displacement.value = sphereDisplacement From 6536059abb4c6d3ba1246c3dadd65efd01224017 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 22:07:57 +0200 Subject: [PATCH 15/58] all dims --- .../ui/MainPanel/MetaDimSelector.tsx | 33 ++++++++++------ src/components/zarr/utils.ts | 38 +++++++++++-------- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index edf2b6a3..d074bfa3 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -250,20 +250,31 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, const thisCount = texCounts.reduce((prod, val) => prod * Math.ceil(val), 1) - let calculatedSize = 0; - if (is2D) { - const totalSteps = x.steps * y.steps; - const sizeRatio = totalSteps / (dataShape[0] * dataShape[1] || 1); - calculatedSize = (meta.totalSize || 0) * sizeRatio; - } else { - const totalSteps = x.steps * y.steps * z.steps; - const sizeRatio = totalSteps / (dataShape.reduce((a, b) => a * b, 1) || 1); - const size = (meta.totalSize || 0) * sizeRatio; - calculatedSize = size / (coarsen ? kernelDepth * Math.pow(kernelSize, 2) : 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; + 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, dataShape, chunkShape, coarsen, kernelSize, kernelDepth, maxTextureSize, max3DTextureSize]); + }, [meta, rows, collapsedSels, availableDims, dataShape, chunkShape, coarsen, kernelSize, kernelDepth, maxTextureSize, max3DTextureSize]); useEffect(() => { setTextureArrayDepths(sizeData.depths); diff --git a/src/components/zarr/utils.ts b/src/components/zarr/utils.ts index 820b2b21..86cf9eca 100644 --- a/src/components/zarr/utils.ts +++ b/src/components/zarr/utils.ts @@ -230,14 +230,17 @@ export function copyChunkToArray( (yStart + cy) * destStride[1] + xStart; - // Get the row of data from the source chunk, using the new xLimit - const rowData = chunkData.subarray( - sourceRowOffset, - sourceRowOffset + xLimit, - ); - - // Place the row in the correct position in the final array - destArray.set(rowData, destRowOffset); + if (chunkStride[2] === 1) { + const rowData = chunkData.subarray( + sourceRowOffset, + sourceRowOffset + xLimit, + ); + destArray.set(rowData, destRowOffset); + } else { + for (let cx = 0; cx < xLimit; cx++) { + destArray[destRowOffset + cx] = chunkData[sourceRowOffset + cx * chunkStride[2]]; + } + } } } } @@ -281,14 +284,17 @@ export function copyChunkToArray2D( // destStride[0] is the stride for the Y-dimension const destRowOffset = (yStart + cy) * destStride[0] + xStart; - // Get the row of data from the source chunk, using the calculated xLimit - const rowData = chunkData.subarray( - sourceRowOffset, - sourceRowOffset + xLimit, - ); - - // Place the row in the correct position in the final destination array - destArray.set(rowData, destRowOffset); + if (chunkStride[1] === 1) { + const rowData = chunkData.subarray( + sourceRowOffset, + sourceRowOffset + xLimit, + ); + destArray.set(rowData, destRowOffset); + } else { + for (let cx = 0; cx < xLimit; cx++) { + destArray[destRowOffset + cx] = chunkData[sourceRowOffset + cx * chunkStride[1]]; + } + } } } From 0194816fdf83fdd161e671abf0cc881f245178f5 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 22:20:50 +0200 Subject: [PATCH 16/58] 2d vars --- src/components/plots/AxisLines.tsx | 11 +++++----- .../ui/MainPanel/MetaDimSelector.tsx | 22 ++++++++++++------- src/hooks/useDataFetcher.tsx | 2 +- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/components/plots/AxisLines.tsx b/src/components/plots/AxisLines.tsx index 8615af0c..6d64d8f0 100644 --- a/src/components/plots/AxisLines.tsx +++ b/src/components/plots/AxisLines.tsx @@ -379,10 +379,9 @@ const FlatAxis = () =>{ const is4D = dimArrays.length === 4; const originallyFlat = dimArrays.length == 2; const slices = originallyFlat ? [ySlice, xSlice] : [zSlice, ySlice, xSlice] - - 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 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 ? @@ -476,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 ( = { c: 'text-yellow-500', }; -function axisForIndex(idx: number): Axis { - return DEFAULT_AXES[idx] ?? DEFAULT_AXES[DEFAULT_AXES.length - 1]; -} + function selectionSummary( availableDims: DimOption[], @@ -187,14 +185,17 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, const makeInitialCollapsedSels = (dims: DimOption[]): Record => Object.fromEntries(dims.map((d) => [d.name, { ...defaultSelection(d.size), mode: 'scalar' as const }])); - /** Default to first MIN(3, availableDims.length) dims as active rows */ - const makeInitialRows = (dims: DimOption[]): SlicerRow[] => - dims.slice(0, MAX_ACTIVE_DIMS).map((d, i) => ({ + const makeInitialRows = (dims: DimOption[]): SlicerRow[] => { + 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: axisForIndex(i), + axis: axes[i], })); + }; const [rows, setRows] = useState(() => makeInitialRows(availableDims)); const [collapsedSels, setCollapsedSels] = useState>( @@ -304,6 +305,11 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, 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; @@ -314,7 +320,7 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, id: nextId(), dimName, sel: { ...defaultSelection(dim.size), mode: 'slice' }, - axis: axisForIndex(prev.length), + axis: firstUnusedAxis(prev), }; return [...prev, newRow]; }); diff --git a/src/hooks/useDataFetcher.tsx b/src/hooks/useDataFetcher.tsx index 850a6110..f6e461c1 100644 --- a/src/hooks/useDataFetcher.tsx +++ b/src/hooks/useDataFetcher.tsx @@ -89,7 +89,7 @@ export const useDataFetcher = () => { if (shapeLength === 2) { setIsFlat(true); if (!["flat", "sphere"].includes(plotType)) { - setPlotType("sphere"); + setPlotType("flat"); } } else { setIsFlat(false); From 33a7a83c5ad1dc740cf392481d9bdbd78b096009 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 22:33:00 +0200 Subject: [PATCH 17/58] via axismapping --- src/components/ui/MainPanel/MetaDimSelector.tsx | 10 +++++----- src/components/zarr/GetArray.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index 4d46d4c3..3c70627f 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -211,7 +211,11 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, } const sizeData = useMemo(() => { - const is2D = dataShape.length === 2; + 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 }; @@ -226,10 +230,6 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, return { first: start, last: stop, steps: Math.max(1, stop - start) }; }; - 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; diff --git a/src/components/zarr/GetArray.ts b/src/components/zarr/GetArray.ts index 68f782d9..b749cf06 100644 --- a/src/components/zarr/GetArray.ts +++ b/src/components/zarr/GetArray.ts @@ -18,10 +18,10 @@ export async function GetArray(varOveride?: string) { const meta = await fetcher.getMetadata(targetVariable); const { shape, chunkShape, fillValue, dtype } = meta; const rank = shape.length; - const hasZ = rank >= 3; + const hasZ = axisMapping.z >= 0; const xDimIndex = axisMapping.x >= 0 ? axisMapping.x : rank - 1; const yDimIndex = axisMapping.y >= 0 ? axisMapping.y : rank - 2; - const zDimIndex = axisMapping.z >= 0 ? axisMapping.z : rank - 3; + const zDimIndex = axisMapping.z >= 0 ? axisMapping.z : -1; const calcDim = (slice: [number, number | null], dimIdx: number) => { // This function provides information for extraction from each dimension of datarray if (dimIdx < 0) return { start: 0, end: 1, size: 0, chunkDim: 1 }; From a3cef10b372a1e32717f57c2543360d8cd0c3e75 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 22:51:24 +0200 Subject: [PATCH 18/58] 2d --- src/components/zarr/GetArray.ts | 25 ++++++-- src/components/zarr/NCGetters.ts | 13 ++-- src/components/zarr/utils.ts | 105 +++++++++++++------------------ src/utils/HelperFuncs.ts | 3 +- 4 files changed, 76 insertions(+), 70 deletions(-) diff --git a/src/components/zarr/GetArray.ts b/src/components/zarr/GetArray.ts index b749cf06..371a04f1 100644 --- a/src/components/zarr/GetArray.ts +++ b/src/components/zarr/GetArray.ts @@ -82,7 +82,8 @@ export async function GetArray(varOveride?: string) { typedArray, outputShape, destStride as any, [z, y, x], - [zDim.start, yDim.start, xDim.start] + [zDim.chunkDim, yDim.chunkDim, xDim.chunkDim], + [zSlice[0], ySlice[0], xSlice[0]] ) } else { copyChunkToArray2D( @@ -92,7 +93,9 @@ export async function GetArray(varOveride?: string) { typedArray, outputShape, destStride as any, [y, x], - [yDim.start, xDim.start]) + [yDim.chunkDim, xDim.chunkDim], + [ySlice[0], xSlice[0]] + ) } } else { const raw = await fetcher.fetchChunk({ variable:targetVariable, rank, shape, chunkShape, x, y, z, xDimIndex, yDimIndex, zDimIndex, idx4D, ndSlices, axisMapping }); @@ -151,16 +154,28 @@ export async function GetArray(varOveride?: string) { } if (hasZ) { - copyChunkToArray(chunkF16, thisShape.slice(-3), chunkStride.slice(-3) as any, typedArray, outputShape, destStride as any, [z, y, x], [zDim.start, yDim.start, xDim.start]); + copyChunkToArray( + chunkF16, thisShape.slice(-3), chunkStride.slice(-3) as any, + typedArray, outputShape, destStride as any, [z, y, x], + [zDim.chunkDim, yDim.chunkDim, xDim.chunkDim], + [zSlice[0], ySlice[0], xSlice[0]] + ); } else { - copyChunkToArray2D(chunkF16, thisShape, chunkStride as any, typedArray, outputShape, destStride as any, [y, x], [yDim.start, xDim.start]); + copyChunkToArray2D( + chunkF16, thisShape, chunkStride as any, + typedArray, outputShape, destStride as any, [y, x], + [yDim.chunkDim, xDim.chunkDim], + [ySlice[0], xSlice[0]] + ); } cache.set(cacheName, { data: compress ? CompressArray(chunkF16, 7) : chunkF16, shape: thisShape, stride: chunkStride, scaling: scalingFactor, compressed: compress, coarsened: coarsen, - kernel: { kernelDepth: coarsen ? kernelDepth : undefined, kernelSize: coarsen ? kernelSize : undefined } + kernel: { kernelDepth: coarsen ? kernelDepth : undefined, kernelSize: coarsen ? kernelSize : undefined }, + fullChunkDim: [zDim.chunkDim, yDim.chunkDim, xDim.chunkDim], + sliceStart: [zSlice[0] ?? 0, ySlice[0] ?? 0, xSlice[0] ?? 0] }); rescaleIDs.push(chunkID); } diff --git a/src/components/zarr/NCGetters.ts b/src/components/zarr/NCGetters.ts index 1e0f9e2b..2e027710 100644 --- a/src/components/zarr/NCGetters.ts +++ b/src/components/zarr/NCGetters.ts @@ -158,7 +158,8 @@ export async function GetNCArray(variable: string){ outputShape, destStride as [number, number, number], [z,y,x], - [zDim.start,yDim.start,xDim.start], + [zDim.chunkDim,yDim.chunkDim,xDim.chunkDim], + [zSlice[0] ?? 0, ySlice[0] ?? 0, xSlice[0] ?? 0], ) setProgress(Math.round(iter/totalChunksToLoad*100)) // Progress Bar iter ++; @@ -241,7 +242,8 @@ export async function GetNCArray(variable: string){ outputShape, destStride as [number, number, number], [z,y,x], - [zDim.start,yDim.start,xDim.start], + [zDim.chunkDim,yDim.chunkDim,xDim.chunkDim], + [zSlice[0] ?? 0, ySlice[0] ?? 0, xSlice[0] ?? 0], ) else copyChunkToArray2D( chunkF16, @@ -251,7 +253,8 @@ export async function GetNCArray(variable: string){ outputShape, destStride as [number, number], [y,x], - [yDim.start,xDim.start], + [yDim.chunkDim,xDim.chunkDim], + [ySlice[0] ?? 0, xSlice[0] ?? 0], ) const cacheChunk = { data: compress ? CompressArray(chunkF16, 7) : chunkF16, @@ -263,7 +266,9 @@ export async function GetNCArray(variable: string){ kernel: { kernelDepth: coarsen ? kernelDepth : undefined, kernelSize: coarsen ? kernelSize : undefined - } + }, + fullChunkDim: [zDim.chunkDim, yDim.chunkDim, xDim.chunkDim], + sliceStart: [zSlice[0] ?? 0, ySlice[0] ?? 0, xSlice[0] ?? 0] } cache.set(cacheName,cacheChunk) setProgress(Math.round(iter/totalChunksToLoad*100)) // Progress Bar diff --git a/src/components/zarr/utils.ts b/src/components/zarr/utils.ts index 86cf9eca..72d2f158 100644 --- a/src/components/zarr/utils.ts +++ b/src/components/zarr/utils.ts @@ -195,50 +195,43 @@ export function copyChunkToArray( destShape: number[], destStride: number[], chunkGridPos: number[], - chunkGridStart: number[], + fullChunkDim: number[], + sliceStart: number[], ): void { const [z, y, x] = chunkGridPos; - const [zStartIdx, yStartIdx, xStartIdx] = chunkGridStart; - const [chunkShapeZ, chunkShapeY, chunkShapeX] = chunkShape; + const [chunkDimZ, chunkDimY, chunkDimX] = fullChunkDim; + const [sliceStartZ, sliceStartY, sliceStartX] = sliceStart; const [destShapeZ, destShapeY, destShapeX] = destShape; - // 1. Calculate the local coordinates of the chunk within the destination grid - const localZ = z - zStartIdx; - const localY = y - yStartIdx; - const localX = x - xStartIdx; - - // 2. Determine the starting element position for this chunk in the destination array - const zStart = localZ * chunkShapeZ; - const yStart = localY * chunkShapeY; - const xStart = localX * chunkShapeX; - - // 3. Calculate the actual number of elements to copy for this chunk - // This prevents writing past the end of the destination array for partial chunks. - const zLimit = Math.min(chunkShapeZ, destShapeZ - zStart); - const yLimit = Math.min(chunkShapeY, destShapeY - yStart); - const xLimit = Math.min(chunkShapeX, destShapeX - xStart); - - // 4. Loop using the calculated limits and copy row by row - for (let cz = 0; cz < zLimit; cz++) { - for (let cy = 0; cy < yLimit; cy++) { - // Offset to the start of the row in the SOURCE chunk data - const sourceRowOffset = cz * chunkStride[0] + cy * chunkStride[1]; + const absZ = z * chunkDimZ; + const absY = y * chunkDimY; + const absX = x * chunkDimX; + + const czStart = Math.max(0, sliceStartZ - absZ); + const czEnd = Math.min(chunkShape[0], sliceStartZ + destShapeZ - absZ); + const cyStart = Math.max(0, sliceStartY - absY); + const cyEnd = Math.min(chunkShape[1], sliceStartY + destShapeY - absY); + const cxStart = Math.max(0, sliceStartX - absX); + const cxEnd = Math.min(chunkShape[2], sliceStartX + destShapeX - absX); - // Offset to the start of the row in the DESTINATION typedArray - const destRowOffset = - (zStart + cz) * destStride[0] + - (yStart + cy) * destStride[1] + - xStart; + for (let cz = czStart; cz < czEnd; cz++) { + for (let cy = cyStart; cy < cyEnd; cy++) { + const sourceRowOffset = cz * chunkStride[0] + cy * chunkStride[1]; + const destZ = (absZ + cz) - sliceStartZ; + const destY = (absY + cy) - sliceStartY; + const destXStart = (absX + cxStart) - sliceStartX; + const destRowOffset = destZ * destStride[0] + destY * destStride[1] + destXStart; if (chunkStride[2] === 1) { const rowData = chunkData.subarray( - sourceRowOffset, - sourceRowOffset + xLimit, + sourceRowOffset + cxStart, + sourceRowOffset + cxEnd, ); destArray.set(rowData, destRowOffset); } else { - for (let cx = 0; cx < xLimit; cx++) { - destArray[destRowOffset + cx] = chunkData[sourceRowOffset + cx * chunkStride[2]]; + for (let cx = cxStart; cx < cxEnd; cx++) { + const destX = (absX + cx) - sliceStartX; + destArray[destZ * destStride[0] + destY * destStride[1] + destX] = chunkData[sourceRowOffset + cx * chunkStride[2]]; } } } @@ -253,46 +246,38 @@ export function copyChunkToArray2D( destShape: number[], destStride: number[], chunkGridPos: number[], - chunkGridStart: number[], + fullChunkDim: number[], + sliceStart: number[], ): void { - // Destructure the 2D properties const [y, x] = chunkGridPos; - const [yStartIdx, xStartIdx] = chunkGridStart; - const [chunkShapeY, chunkShapeX] = chunkShape; + const [chunkDimY, chunkDimX] = fullChunkDim; + const [sliceStartY, sliceStartX] = sliceStart; const [destShapeY, destShapeX] = destShape; - // 1. Calculate the local coordinates of the chunk within the destination grid - const localY = y - yStartIdx; - const localX = x - xStartIdx; + const absY = y * chunkDimY; + const absX = x * chunkDimX; - // 2. Determine the starting element position for this chunk in the destination array - const yStart = localY * chunkShapeY; - const xStart = localX * chunkShapeX; + const cyStart = Math.max(0, sliceStartY - absY); + const cyEnd = Math.min(chunkShape[0], sliceStartY + destShapeY - absY); + const cxStart = Math.max(0, sliceStartX - absX); + const cxEnd = Math.min(chunkShape[1], sliceStartX + destShapeX - absX); - // 3. Calculate the actual number of elements to copy for this chunk. - // This prevents writing past the end of the destination array for partial chunks. - const yLimit = Math.min(chunkShapeY, destShapeY - yStart); - const xLimit = Math.min(chunkShapeX, destShapeX - xStart); - - // 4. Loop through the rows (Y-axis) and copy each one - for (let cy = 0; cy < yLimit; cy++) { - // Offset to the start of the row in the SOURCE chunk data - // chunkStride[0] is the stride for the Y-dimension + for (let cy = cyStart; cy < cyEnd; cy++) { const sourceRowOffset = cy * chunkStride[0]; - - // Offset to the start of the row in the DESTINATION typedArray - // destStride[0] is the stride for the Y-dimension - const destRowOffset = (yStart + cy) * destStride[0] + xStart; + const destY = (absY + cy) - sliceStartY; + const destXStart = (absX + cxStart) - sliceStartX; + const destRowOffset = destY * destStride[0] + destXStart; if (chunkStride[1] === 1) { const rowData = chunkData.subarray( - sourceRowOffset, - sourceRowOffset + xLimit, + sourceRowOffset + cxStart, + sourceRowOffset + cxEnd, ); destArray.set(rowData, destRowOffset); } else { - for (let cx = 0; cx < xLimit; cx++) { - destArray[destRowOffset + cx] = chunkData[sourceRowOffset + cx * chunkStride[1]]; + for (let cx = cxStart; cx < cxEnd; cx++) { + const destX = (absX + cx) - sliceStartX; + destArray[destY * destStride[0] + destX] = chunkData[sourceRowOffset + cx * chunkStride[1]]; } } } diff --git a/src/utils/HelperFuncs.ts b/src/utils/HelperFuncs.ts index 0b159378..71a7c9df 100644 --- a/src/utils/HelperFuncs.ts +++ b/src/utils/HelperFuncs.ts @@ -304,7 +304,8 @@ export function GetCurrentArray(overrideStore?:string){ dataShape, strides as [number, number, number], [z, y, x], - [zStartIdx, yStartIdx, xStartIdx] + chunk.fullChunkDim || [1, 1, 1], + chunk.sliceStart || [0, 0, 0] ) } } From b4cf2f30b9b61c04aff71a8a120438aba921bb77 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 23:24:07 +0200 Subject: [PATCH 19/58] get --- .../ui/MainPanel/MetaDimSelector.tsx | 18 +++++-- src/components/zarr/GetArray.ts | 50 +++++++++++++++---- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index 3c70627f..dbabecc2 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -316,18 +316,26 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, const dimName = firstUnusedDim(prev); if (!dimName) return prev; const dim = availableDims.find((d) => d.name === dimName)!; - const newRow: SlicerRow = { + const newRows: SlicerRow[] = [...prev, { id: nextId(), dimName, sel: { ...defaultSelection(dim.size), mode: 'slice' }, - axis: firstUnusedAxis(prev), - }; - return [...prev, newRow]; + 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) => prev.slice(0, -1)); + 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) => diff --git a/src/components/zarr/GetArray.ts b/src/components/zarr/GetArray.ts index 371a04f1..4437e10a 100644 --- a/src/components/zarr/GetArray.ts +++ b/src/components/zarr/GetArray.ts @@ -18,23 +18,50 @@ export async function GetArray(varOveride?: string) { const meta = await fetcher.getMetadata(targetVariable); const { shape, chunkShape, fillValue, dtype } = meta; const rank = shape.length; - const hasZ = axisMapping.z >= 0; - const xDimIndex = axisMapping.x >= 0 ? axisMapping.x : rank - 1; - const yDimIndex = axisMapping.y >= 0 ? axisMapping.y : rank - 2; - const zDimIndex = axisMapping.z >= 0 ? axisMapping.z : -1; + // Identify which dimensions are already explicitly mapped + const parseMapped = (val: any) => typeof val === 'number' && !isNaN(val) && val >= 0 ? val : -1; + const mappedX = parseMapped(axisMapping.x); + const mappedY = parseMapped(axisMapping.y); + const mappedZ = parseMapped(axisMapping.z); + + const mappedDims = new Set([mappedX, mappedY, mappedZ].filter(v => v >= 0)); + const unmappedDims: number[] = []; + for (let i = rank - 1; i >= 0; i--) { + if (!mappedDims.has(i)) unmappedDims.push(i); + } + + const xDimIndex = mappedX >= 0 ? mappedX : (unmappedDims.length > 0 ? unmappedDims.shift()! : rank - 1); + const yDimIndex = mappedY >= 0 ? mappedY : (unmappedDims.length > 0 ? unmappedDims.shift()! : rank - 2); + const zDimIndex = mappedZ >= 0 ? mappedZ : -1; + + const hasZ = zDimIndex >= 0; - const calcDim = (slice: [number, number | null], dimIdx: number) => { // This function provides information for extraction from each dimension of datarray + const calcDim = (slice: [number, number | null], dimIdx: number) => { if (dimIdx < 0) return { start: 0, end: 1, size: 0, chunkDim: 1 }; const dimSize = shape[dimIdx]; const chunkDim = chunkShape[dimIdx]; const start = Math.floor(slice[0] / chunkDim); const sliceEnd = slice[1] ?? dimSize; - return { start, end: Math.ceil(sliceEnd / chunkDim), size: sliceEnd - slice[0], chunkDim }; + return { start, end: Math.ceil(sliceEnd / chunkDim), size: sliceEnd - slice[0], chunkDim, offset: slice[0] % chunkDim }; }; - const xDim = calcDim(xSlice, xDimIndex); - const yDim = calcDim(ySlice, yDimIndex); - const zDim = calcDim(zSlice, zDimIndex); + // If an axis is unmapped in the UI (NaN), we MUST fetch it as a scalar (size 1) + // using the collapsed value from ndSlices. Otherwise, it defaults to [0, null] + // and fetches the entire dimension, leading to massive memory requests (OOM). + const getEffectiveSlice = (mappingIdx: number, dimIdx: number, uiSlice: [number, number | null]): [number, number | null] => { + if (mappingIdx >= 0) return uiSlice; // Axis is mapped, use the UI slice + + if (dimIdx >= 0 && ndSlices && ndSlices[dimIdx] !== undefined) { + const sel = ndSlices[dimIdx]; + if (typeof sel === 'number') return [sel, sel + 1]; + if (Array.isArray(sel)) return sel as [number, number | null]; + } + return [0, 1]; // Safe fallback to scalar + }; + + const xDim = calcDim(getEffectiveSlice(axisMapping.x, xDimIndex, xSlice), xDimIndex); + const yDim = calcDim(getEffectiveSlice(axisMapping.y, yDimIndex, ySlice), yDimIndex); + const zDim = calcDim(getEffectiveSlice(axisMapping.z, zDimIndex, zSlice), zDimIndex); let outputShape = hasZ ? [zDim.size, yDim.size, xDim.size] : [yDim.size, xDim.size]; if (coarsen) { @@ -42,7 +69,10 @@ export async function GetArray(varOveride?: string) { } const totalElements = outputShape.reduce((a, b) => a * b, 1); - if (totalElements > 1e9) { useErrorStore.getState().setError("largeArray"); throw new Error("Cannot allocate array."); } + if (totalElements > 1e9) { + useErrorStore.getState().setError("largeArray"); + throw new Error("Cannot allocate array. Details printed to console."); + } const destStride = calculateStrides(outputShape); setStrides(destStride); From 2f12f5bbde736a2372c03c07e6c0efc2140523cc Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sat, 4 Jul 2026 23:36:06 +0200 Subject: [PATCH 20/58] isFlat --- src/components/plots/FlatMap.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index 4f61e960..f9348bc7 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -66,7 +66,7 @@ const FlatMap = ({textures, infoSetters} : {textures : THREE.DataTexture[] | THR 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 = dimArrays.length === 2 + let slices = isFlat ? [ dimArrays[yIdx]?.slice(ySlice[0], ySlice[1] ? ySlice[1] : undefined) ?? [], dimArrays[xIdx]?.slice(xSlice[0], xSlice[1] ? xSlice[1] : undefined) ?? [], From 0167373692bef121f64782a08735a8f35e3afaf2 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 09:01:14 +0200 Subject: [PATCH 21/58] fix import --- src/components/plots/plotarea/FixedTicks.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/plots/plotarea/FixedTicks.tsx b/src/components/plots/plotarea/FixedTicks.tsx index d5b7be84..924a2437 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' From 7cb9addf1fa6c1e9385a46667e09a06348b5b827 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 09:46:27 +0200 Subject: [PATCH 22/58] packed chunks --- src/components/zarr/dataFetchers.ts | 44 +++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/components/zarr/dataFetchers.ts b/src/components/zarr/dataFetchers.ts index d9ed8ba3..5e3f755c 100644 --- a/src/components/zarr/dataFetchers.ts +++ b/src/components/zarr/dataFetchers.ts @@ -61,19 +61,47 @@ export function zarrFetcher() { _outVar: outVar, // carry through for fetchChunk } as any; }, - async fetchChunk({ variable, chunkShape, ndSlices }: any): Promise { - const chunkSlice = ndSlices.map((s: any, i: number) => { - if (typeof s === "number") return s; - const start = s[0] * chunkShape[i]; - const end = (s[0] + 1) * chunkShape[i]; - return zarr.slice(start, end); - }); + async fetchChunk({ rank, shape, chunkShape, x, y, z, xDimIndex, yDimIndex, zDimIndex, idx4D, variable, ndSlices }: any): Promise { + const chunkSlice = new Array(rank).fill(0); + if (ndSlices && ndSlices.length === rank) { + for (let i = 0; i < rank; i++) { + if (i === xDimIndex) { + chunkSlice[i] = zarr.slice(x * chunkShape[i], Math.min((x + 1) * chunkShape[i], shape[i])); + } else if (i === yDimIndex) { + chunkSlice[i] = zarr.slice(y * chunkShape[i], Math.min((y + 1) * chunkShape[i], shape[i])); + } else if (i === zDimIndex) { + chunkSlice[i] = zarr.slice(z * chunkShape[i], Math.min((z + 1) * chunkShape[i], shape[i])); + } else { + const sel = ndSlices[i]; + if (Array.isArray(sel)) { + chunkSlice[i] = zarr.slice(sel[0], sel[1]); + } else { + chunkSlice[i] = sel; + } + } + } + } else { + chunkSlice[xDimIndex] = zarr.slice(x * chunkShape[xDimIndex], (x + 1) * chunkShape[xDimIndex]); + chunkSlice[yDimIndex] = zarr.slice(y * chunkShape[yDimIndex], (y + 1) * chunkShape[yDimIndex]); + if (zDimIndex >= 0) { + chunkSlice[zDimIndex] = zarr.slice(z * chunkShape[zDimIndex], (z + 1) * chunkShape[zDimIndex]); + } + if (rank >= 4) { + chunkSlice[0] = idx4D; + } + } const chunk = await fetchWithRetry(() => zarr.get(outVar, chunkSlice), `variable ${variable}`, useGlobalStore.getState().setStatus); if (!chunk || chunk.data instanceof BigInt64Array || chunk.data instanceof BigUint64Array) { throw new Error("BigInt arrays not supported."); } - return { data: chunk.data as Float32Array, shape: chunkShape, stride: chunk.stride }; + + let outShape = chunk.shape; + if (!outShape || outShape.length === 0) { + outShape = chunkShape; // fallback if Zarrita doesn't collapse + } + + return { data: chunk.data as Float32Array, shape: outShape as number[], stride: chunk.stride as number[] }; }, }; } From b2fa6e05a0ed7f533a7bcb5ed12e20ed4869ad0d Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 10:08:19 +0200 Subject: [PATCH 23/58] fix nan propagation --- src/components/zarr/dataFetchers.ts | 6 ++++++ src/components/zarr/utils.ts | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/src/components/zarr/dataFetchers.ts b/src/components/zarr/dataFetchers.ts index 5e3f755c..957f40a8 100644 --- a/src/components/zarr/dataFetchers.ts +++ b/src/components/zarr/dataFetchers.ts @@ -171,6 +171,12 @@ export function NCFetcher() { // Filter out collapsed dims so shape matches Zarrita behavior let collapsedShape = counts.filter((c, i) => ndSlices ? (!Array.isArray(ndSlices[i]) && i !== xDimIndex && i !== yDimIndex && i !== zDimIndex ? false : true) : c !== 1); if (collapsedShape.length === 0) collapsedShape = [1]; + + console.log("NCFetcher fetchChunk:", { + variable, rank, x, y, z, xDimIndex, yDimIndex, zDimIndex, ndSlices, + starts, counts, collapsedShape, rawShape: shape, chunkShape + }); + return { data, shape: collapsedShape, stride: calculateStrides(collapsedShape) }; }, }; diff --git a/src/components/zarr/utils.ts b/src/components/zarr/utils.ts index 72d2f158..d02821eb 100644 --- a/src/components/zarr/utils.ts +++ b/src/components/zarr/utils.ts @@ -52,6 +52,14 @@ export function ToFloat16( maxVal = val; } } + if (maxVal === 0) { + const newArray = new Float16Array(array.length); + for (let i = 0; i < array.length; i++) { + newArray[i] = array[i] === 0 ? 0 : NaN; + } + return [newArray, initialScale !== 0 ? initialScale : null]; + } + const additionalScaling = Math.ceil(Math.log10(maxVal / 65504)); const needsRescale = additionalScaling > 0 || additionalScaling <= -6; //I think this is complicating things. Because if it was already scaled then there should already by enough variance in the data it doesn't need to go further From e874634ac3902b451183beeec51272cd48fc598f Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 10:11:07 +0200 Subject: [PATCH 24/58] rm console --- src/components/zarr/dataFetchers.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/zarr/dataFetchers.ts b/src/components/zarr/dataFetchers.ts index 957f40a8..5e3f755c 100644 --- a/src/components/zarr/dataFetchers.ts +++ b/src/components/zarr/dataFetchers.ts @@ -171,12 +171,6 @@ export function NCFetcher() { // Filter out collapsed dims so shape matches Zarrita behavior let collapsedShape = counts.filter((c, i) => ndSlices ? (!Array.isArray(ndSlices[i]) && i !== xDimIndex && i !== yDimIndex && i !== zDimIndex ? false : true) : c !== 1); if (collapsedShape.length === 0) collapsedShape = [1]; - - console.log("NCFetcher fetchChunk:", { - variable, rank, x, y, z, xDimIndex, yDimIndex, zDimIndex, ndSlices, - starts, counts, collapsedShape, rawShape: shape, chunkShape - }); - return { data, shape: collapsedShape, stride: calculateStrides(collapsedShape) }; }, }; From aaa7df322cd78371da9fd8857876747bbb374551 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 10:44:23 +0200 Subject: [PATCH 25/58] play any direction --- src/components/ui/MainPanel/PlayButton.tsx | 50 ++++++++++--------- .../ui/NavBar/ExportImageSettings.tsx | 17 +++++-- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/components/ui/MainPanel/PlayButton.tsx b/src/components/ui/MainPanel/PlayButton.tsx index d7e68036..43e2175a 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
{/* VISUALIZER */} - {(sliceDist < Math.floor(timeLength / (coarsen ? kernel.kernelDepth : 1))) && {firstLabel} { const v = Array.isArray(vals) ? vals[0] : 0 - setAnimProg(v / timeLength) + setAnimProg(v / zLength) previousVal.current = v }} /> diff --git a/src/components/ui/NavBar/ExportImageSettings.tsx b/src/components/ui/NavBar/ExportImageSettings.tsx index 08e13b3f..69366357 100644 --- a/src/components/ui/NavBar/ExportImageSettings.tsx +++ b/src/components/ui/NavBar/ExportImageSettings.tsx @@ -11,6 +11,7 @@ import { import { useImageExportStore } from '@/GlobalStates/ImageExportStore'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; +import { useZarrStore } from '@/GlobalStates/ZarrStore'; import { useShallow } from 'zustand/shallow'; import { ChevronDown } from 'lucide-react'; import { @@ -61,12 +62,18 @@ const ExportImageSettings = () => { const [showSettings, setShowSettings] = useState(true) const [previewState, setPreviewState] = useState(false) + const { axisMapping } = useZarrStore(useShallow(state => ({ + axisMapping: state.axisMapping + }))); + useEffect(()=>{ - const timeArray = dimArrays[dimArrays.length-3] - const timeLength = timeArray?.length || 1 - const sliceDist = zSlice[1] ? zSlice[1] - zSlice[0] : timeLength - zSlice[0] - setFrames(sliceDist) - },[zSlice]) + const shapeLength = dimArrays?.length || 3; + const zIdx = axisMapping?.z >= 0 ? axisMapping.z : Math.max(0, shapeLength - 3); + const zArray = dimArrays[zIdx]; + const zLength = zArray?.length || 1; + const sliceDist = zSlice[1] ? zSlice[1] - zSlice[0] : zLength - zSlice[0]; + setFrames(sliceDist); + },[zSlice, dimArrays, axisMapping]) return ( From a6b8b8011a7067d537ce1a635e5d5f96a832fddb Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 13:49:01 +0200 Subject: [PATCH 26/58] pass right indices --- src/GlobalStates/GlobalStore.ts | 4 ++++ src/components/ui/MainPanel/AnalysisOptions.tsx | 11 ++++++----- src/components/zarr/GetArray.ts | 2 +- src/hooks/useDataFetcher.tsx | 3 +++ 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/GlobalStates/GlobalStore.ts b/src/GlobalStates/GlobalStore.ts index bea8ab47..bdeca52a 100644 --- a/src/GlobalStates/GlobalStore.ts +++ b/src/GlobalStates/GlobalStore.ts @@ -17,6 +17,7 @@ interface Coord { type StoreState = { dataShape: number[]; + activeIndices: number[]; shape: THREE.Vector3; valueScales: { maxVal: number; minVal: number }; colormap: THREE.DataTexture; @@ -50,6 +51,7 @@ 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; @@ -86,6 +88,7 @@ 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(), @@ -119,6 +122,7 @@ 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 }), diff --git a/src/components/ui/MainPanel/AnalysisOptions.tsx b/src/components/ui/MainPanel/AnalysisOptions.tsx index b5ec11b9..fd6f1347 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]} ))} diff --git a/src/components/zarr/GetArray.ts b/src/components/zarr/GetArray.ts index 4437e10a..5d4cfe12 100644 --- a/src/components/zarr/GetArray.ts +++ b/src/components/zarr/GetArray.ts @@ -214,5 +214,5 @@ export async function GetArray(varOveride?: string) { } } setProgress(0); - return { data: typedArray, shape: outputShape, dtype, scalingFactor }; + return { data: typedArray, shape: outputShape, indices: hasZ ? [zDimIndex, yDimIndex, xDimIndex] : [yDimIndex, xDimIndex], dtype, scalingFactor }; } \ No newline at end of file diff --git a/src/hooks/useDataFetcher.tsx b/src/hooks/useDataFetcher.tsx index f6e461c1..5bb39023 100644 --- a/src/hooks/useDataFetcher.tsx +++ b/src/hooks/useDataFetcher.tsx @@ -75,6 +75,9 @@ export const useDataFetcher = () => { //---- Main Fetch ----// GetArray().then((result) => { const shape = result.shape.filter((val) => val != 1); + const activeIndices = result.indices.filter((_, idx) => result.shape[idx] != 1); + useGlobalStore.getState().setActiveIndices(activeIndices); + const [tempTexture, scaling] = ArrayToTexture({ data: result.data, shape From a61e2c3424000c2122db4d76eb1a4889bf6bc492 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 14:05:14 +0200 Subject: [PATCH 27/58] conv --- src/components/computation/WGSLShaders.ts | 7 +++---- src/components/plots/FlatMap.tsx | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/components/computation/WGSLShaders.ts b/src/components/computation/WGSLShaders.ts index c890fcaf..688fb7f2 100644 --- a/src/components/computation/WGSLShaders.ts +++ b/src/components/computation/WGSLShaders.ts @@ -91,10 +91,9 @@ export const createShaders = (precision: Precision) => { return; } - let total_threads_per_slice = workGroups.x * workGroups.y * 16; - let globalIdx = global_id.z * total_threads_per_slice + - global_id.y * (workGroups.x * 4) + - global_id.x; + let globalIdx = outZ * ySize * xSize + + outY * xSize + + outX; let xy_radius: i32 = i32(kernelSize/2u); let z_radius: i32 = i32(kernelDepth/2u); diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index f9348bc7..df42111e 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -95,7 +95,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() From d21079639419ee3eee4bd17ef85de64db5d83b0b Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 15:17:20 +0200 Subject: [PATCH 28/58] fixes 1 var stats --- src/components/computation/WGSLShaders.ts | 81 +++++++++++++++++------ src/components/computation/webGPU.ts | 69 ++++++++++++------- src/components/plots/FlatMap.tsx | 8 ++- src/components/plots/Plot.tsx | 18 ++++- 4 files changed, 127 insertions(+), 49 deletions(-) diff --git a/src/components/computation/WGSLShaders.ts b/src/components/computation/WGSLShaders.ts index 688fb7f2..d521c175 100644 --- a/src/components/computation/WGSLShaders.ts +++ b/src/components/computation/WGSLShaders.ts @@ -729,13 +729,10 @@ export const createShaders = (precision: Precision) => { } var xSum: f32 = 0.0; - var xxSum: f32 = 0.0; var ySum: f32 = 0.0; - var yySum: f32 = 0.0; - var xySum: f32 = 0.0; - - var nanCount: u32 = 0; - // Iterate along the dimension we're averaging + var nanCount: u32 = 0u; + + // --- First Pass: Calculate Means --- if (reduceDim == 0u) { // Average along Z let cCoord = outX * xStride + outY * yStride; for (var z: u32 = 0u; z < dimLength; z++) { @@ -747,10 +744,7 @@ export const createShaders = (precision: Precision) => { continue; } xSum += xI; - xxSum += xI * xI; ySum += yI; - yySum += yI * yI; - xySum += xI * yI; } } else if (reduceDim == 1u) { // Average along Y let cCoord = outX * xStride + outY * zStride; @@ -763,10 +757,7 @@ export const createShaders = (precision: Precision) => { continue; } xSum += xI; - xxSum += xI * xI; ySum += yI; - yySum += yI * yI; - xySum += xI * yI; } } else { // Average along X let cCoord = outX * yStride + outY * zStride; @@ -779,22 +770,74 @@ export const createShaders = (precision: Precision) => { continue; } xSum += xI; - xxSum += xI * xI; ySum += yI; - yySum += yI * yI; - xySum += xI * yI; } } let N: f32 = f32(dimLength - nanCount); + if (N <= 1.0) { + let outputIndex = outY * xSize + outX; + outputData[outputIndex] = ${precision}(0.0 / 0.0); // NaN + return; + } + let meanX = xSum / N; let meanY = ySum / N; - let varX = (xxSum / N) - (meanX * meanX); - let varY = (yySum / N) - (meanY * meanY); - let covXY = (xySum / N) - (meanX * meanY); + + // --- Second Pass: Calculate Variance and Covariance --- + var varXSum: f32 = 0.0; + var varYSum: f32 = 0.0; + var covXYSum: f32 = 0.0; + + 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); + let xI = f32(firstData[inputIndex]); + let yI = f32(secondData[inputIndex]); + if (isNaN(xI) || isNaN(yI)){ continue; } + let dx = xI - meanX; + let dy = yI - meanY; + varXSum += dx * dx; + varYSum += dy * dy; + covXYSum += dx * dy; + } + } 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); + let xI = f32(firstData[inputIndex]); + let yI = f32(secondData[inputIndex]); + if (isNaN(xI) || isNaN(yI)){ continue; } + let dx = xI - meanX; + let dy = yI - meanY; + varXSum += dx * dx; + varYSum += dy * dy; + covXYSum += dx * dy; + } + } else { // Average along X + let cCoord = outX * yStride + outY * zStride; + for (var x: u32 = 0u; x < dimLength; x++) { + let inputIndex = cCoord + (x * xStride); + let xI = f32(firstData[inputIndex]); + let yI = f32(secondData[inputIndex]); + if (isNaN(xI) || isNaN(yI)){ continue; } + let dx = xI - meanX; + let dy = yI - meanY; + varXSum += dx * dx; + varYSum += dy * dy; + covXYSum += dx * dy; + } + } + + let varX = varXSum / N; + let varY = varYSum / N; + let covXY = covXYSum / N; let sigmaX = sqrt(max(0.0, varX)); let sigmaY = sqrt(max(0.0, varY)); - let epsilon = 1e-6; + + // Use 1e-4 because f16 can flush 1e-6 to zero + let epsilon = 1e-4; let denominator = sigmaX * sigmaY + epsilon; let correlation = covXY / denominator; diff --git a/src/components/computation/webGPU.ts b/src/components/computation/webGPU.ts index fbbfe21d..8d24d363 100644 --- a/src/components/computation/webGPU.ts +++ b/src/components/computation/webGPU.ts @@ -50,6 +50,25 @@ const InitializeDevice = async () => { Error('need a browser that supports WebGPU'); return {device, hasF16}; } else{ + const originalCreateBuffer = device.createBuffer.bind(device); + device.createBuffer = (descriptor: GPUBufferDescriptor) => { + return originalCreateBuffer({ + ...descriptor, + size: Math.ceil(descriptor.size / 4) * 4 + }); + }; + + const originalWriteBuffer = device.queue.writeBuffer.bind(device.queue); + device.queue.writeBuffer = (buffer: GPUBuffer, bufferOffset: GPUSize64, data: BufferSource | SharedArrayBuffer, dataOffset?: GPUSize64, size?: GPUSize64) => { + let dataToWrite = data as ArrayBufferView; + if (dataToWrite.byteLength % 4 !== 0) { + const paddedLength = Math.ceil(dataToWrite.byteLength / 4) * 4; + const paddedBuffer = new Uint8Array(paddedLength); + paddedBuffer.set(new Uint8Array(dataToWrite.buffer, dataToWrite.byteOffset, dataToWrite.byteLength)); + dataToWrite = paddedBuffer; + } + originalWriteBuffer(buffer, bufferOffset, dataToWrite as any, dataOffset, size); + }; return {device, hasF16} } } @@ -108,7 +127,7 @@ export async function DataReduction(inputArray : ArrayBufferView, dimInfo : {sha const outputBuffer = device.createBuffer({ label: 'Output Buffer', - size: outputSize * (hasF16 ? 2 : 4), + size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); @@ -119,7 +138,7 @@ export async function DataReduction(inputArray : ArrayBufferView, dimInfo : {sha const readBuffer = device.createBuffer({ label:'Output Buffer', - size: outputSize * (hasF16 ? 2 : 4), + size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); // Write Buffers to GPU @@ -149,7 +168,7 @@ export async function DataReduction(inputArray : ArrayBufferView, dimInfo : {sha encoder.copyBufferToBuffer( outputBuffer, 0, readBuffer, 0, - outputSize * (hasF16 ? 2 : 4) + Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4 ); // Submit work to GPU @@ -162,7 +181,7 @@ export async function DataReduction(inputArray : ArrayBufferView, dimInfo : {sha // Clean up readBuffer.unmap(); - return results; + return results.slice(0, outputSize); } @@ -219,7 +238,7 @@ export async function Convolve(inputArray : ArrayBufferView, dimInfo : {shape: const outputBuffer = device.createBuffer({ label: 'Output Buffer', - size: outputSize * (hasF16 ? 2 : 4), + size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); @@ -231,7 +250,7 @@ export async function Convolve(inputArray : ArrayBufferView, dimInfo : {shape: const readBuffer = device.createBuffer({ label:'Read Buffer', - size: outputSize * (hasF16 ? 2 : 4), + size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); @@ -262,7 +281,7 @@ export async function Convolve(inputArray : ArrayBufferView, dimInfo : {shape: encoder.copyBufferToBuffer( outputBuffer, 0, readBuffer, 0, - outputSize * (hasF16 ? 2 : 4) + Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4 ); // Submit work to GPU @@ -275,7 +294,7 @@ export async function Convolve(inputArray : ArrayBufferView, dimInfo : {shape: // Clean up readBuffer.unmap(); - return results; + return results.slice(0, outputSize); } export async function Multivariate2D(firstArray: ArrayBufferView, secondArray: ArrayBufferView, dimInfo : {shape: number[], strides: number[]}, reduceDim: number, operation:string){ @@ -338,7 +357,7 @@ export async function Multivariate2D(firstArray: ArrayBufferView, secondArray: A const outputBuffer = device.createBuffer({ label: 'Output Buffer', - size: outputSize * (hasF16 ? 2 : 4), + size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); @@ -349,7 +368,7 @@ export async function Multivariate2D(firstArray: ArrayBufferView, secondArray: A const readBuffer = device.createBuffer({ label:'Output Buffer', - size: outputSize * (hasF16 ? 2 : 4), + size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); @@ -383,7 +402,7 @@ export async function Multivariate2D(firstArray: ArrayBufferView, secondArray: A encoder.copyBufferToBuffer( outputBuffer, 0, readBuffer, 0, - outputSize * (hasF16 ? 2 : 4) + Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4 ); // Submit work to GPU @@ -396,7 +415,7 @@ export async function Multivariate2D(firstArray: ArrayBufferView, secondArray: A // Clean up readBuffer.unmap(); - return results; + return results.slice(0, outputSize); } export async function Multivariate3D(firstArray: ArrayBufferView, secondArray: ArrayBufferView, dimInfo : {shape: number[], strides: number[]}, kernel: {kernelSize: number, kernelDepth: number}, operation: string){ @@ -460,7 +479,7 @@ export async function Multivariate3D(firstArray: ArrayBufferView, secondArray: A const outputBuffer = device.createBuffer({ label: 'Output Buffer', - size: outputSize * (hasF16 ? 2 : 4), + size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); @@ -471,7 +490,7 @@ export async function Multivariate3D(firstArray: ArrayBufferView, secondArray: A const readBuffer = device.createBuffer({ label:'Read Buffer', - size: outputSize * (hasF16 ? 2 : 4), + size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); @@ -505,7 +524,7 @@ export async function Multivariate3D(firstArray: ArrayBufferView, secondArray: A encoder.copyBufferToBuffer( outputBuffer, 0, readBuffer, 0, - outputSize * (hasF16 ? 2 : 4) + Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4 ); // Submit work to GPU @@ -518,7 +537,7 @@ export async function Multivariate3D(firstArray: ArrayBufferView, secondArray: A // Clean up readBuffer.unmap(); - return results; + return results.slice(0, outputSize); } export async function CUMSUM3D(inputArray : ArrayBufferView, dimInfo : {shape: number[], strides: number[]}, reduceDim: number, reverse: number){ @@ -629,7 +648,7 @@ export async function CUMSUM3D(inputArray : ArrayBufferView, dimInfo : {shape: // Clean up readBuffer.unmap(); - return results; + return results.slice(0, outputSize); } export async function Convolve2D(inputArray : ArrayBufferView, dimInfo : {shape: number[], strides: number[]}, operation: string, kernelSize: number){ @@ -679,7 +698,7 @@ export async function Convolve2D(inputArray : ArrayBufferView, dimInfo : {shape const outputBuffer = device.createBuffer({ label: 'Output Buffer', - size: outputSize * (hasF16 ? 2 : 4), + size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); @@ -691,7 +710,7 @@ export async function Convolve2D(inputArray : ArrayBufferView, dimInfo : {shape const readBuffer = device.createBuffer({ label:'Read Buffer', - size: outputSize * (hasF16 ? 2 : 4), + size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); @@ -723,7 +742,7 @@ export async function Convolve2D(inputArray : ArrayBufferView, dimInfo : {shape encoder.copyBufferToBuffer( outputBuffer, 0, readBuffer, 0, - outputSize * (hasF16 ? 2 : 4) + Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4 ); // Submit work to GPU @@ -736,7 +755,7 @@ export async function Convolve2D(inputArray : ArrayBufferView, dimInfo : {shape // Clean up readBuffer.unmap(); - return results; + return results.slice(0, outputSize); } export async function CustomShader(inputArray : ArrayBufferView, dimInfo : {dataShape: number[], outputShape: number[], strides: number[]}, kernel: {kernelSize: number, kernelDepth: number}, reduceDim: number, shaderCode: string){ @@ -797,7 +816,7 @@ export async function CustomShader(inputArray : ArrayBufferView, dimInfo : {dat const outputBuffer = device.createBuffer({ label: 'Output Buffer', - size: outputSize * (hasF16 ? 2 : 4), + size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); @@ -809,7 +828,7 @@ export async function CustomShader(inputArray : ArrayBufferView, dimInfo : {dat const readBuffer = device.createBuffer({ label:'Read Buffer', - size: outputSize * (hasF16 ? 2 : 4), + size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); @@ -843,7 +862,7 @@ export async function CustomShader(inputArray : ArrayBufferView, dimInfo : {dat encoder.copyBufferToBuffer( outputBuffer, 0, readBuffer, 0, - outputSize * (hasF16 ? 2 : 4) + Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4 ); // Submit work to GPU @@ -856,5 +875,5 @@ export async function CustomShader(inputArray : ArrayBufferView, dimInfo : {dat // Clean up readBuffer.unmap(); - return results; + return results.slice(0, outputSize); } \ No newline at end of file diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index df42111e..1c76f8b3 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -124,8 +124,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) @@ -133,7 +135,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]] } } diff --git a/src/components/plots/Plot.tsx b/src/components/plots/Plot.tsx index 344d8ef9..9abfbe56 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(); From 4534047d609cc2b14301d76bd4802f7bf0d1bf23 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 16:09:44 +0200 Subject: [PATCH 29/58] fixes nans 2 vars --- src/components/computation/WGSLShaders.ts | 45 ++++++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/components/computation/WGSLShaders.ts b/src/components/computation/WGSLShaders.ts index d521c175..fa03facb 100644 --- a/src/components/computation/WGSLShaders.ts +++ b/src/components/computation/WGSLShaders.ts @@ -150,6 +150,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 @@ -158,6 +159,7 @@ export const createShaders = (precision: Precision) => { let inputIndex = cCoord + (z * zStride); let newVal = f32(inputData[u32(inputIndex)]); if (isNaN(newVal)){ + nanCount++; continue; } sum += newVal; @@ -168,6 +170,7 @@ export const createShaders = (precision: Precision) => { let inputIndex = cCoord + (y * yStride); let newVal = f32(inputData[u32(inputIndex)]); if (isNaN(newVal)){ + nanCount++; continue; } sum += newVal; @@ -178,14 +181,20 @@ export const createShaders = (precision: Precision) => { let inputIndex = cCoord + (x * xStride); let newVal = f32(inputData[u32(inputIndex)]); if (isNaN(newVal)){ + nanCount++; continue; } sum += newVal; } } + let N = f32(dimLength - nanCount); let outputIndex = outY * xSize + outX; - outputData[outputIndex] = ${precision}(sum / f32(dimLength)); + if (N > 0.0) { + outputData[outputIndex] = ${precision}(sum / N); + } else { + outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); // NaN + } } `, @@ -556,8 +565,14 @@ 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 outputIndex = outY * xSize + outX; + outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); // NaN + return; + } + let xMean: f32 = xSum / N; + let yMean: f32 = ySum / N; var numSum: f32 = 0; var denomSum: f32 = 0; @@ -601,7 +616,11 @@ export const createShaders = (precision: Precision) => { } let outputIndex = outY * xSize + outX; - outputData[outputIndex] = ${precision}(numSum/(denomSum+1e-4)); + if (denomSum > 1e-7) { + outputData[outputIndex] = ${precision}(numSum / denomSum); + } else { + outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); // NaN + } } `, @@ -673,6 +692,11 @@ export const createShaders = (precision: Precision) => { } var N: f32 = f32(dimLength - nanCount); + if (N <= 1.0) { + let outputIndex = outY * xSize + outX; + outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); // NaN + return; + } let xMean: f32 = xSum / N; let yMean: f32 = ySum / N; @@ -689,7 +713,7 @@ export const createShaders = (precision: Precision) => { } let outputIndex = outY * xSize + outX; - outputData[outputIndex] = ${precision}(numSum / (N - 1)); + outputData[outputIndex] = ${precision}(numSum / (N - 1.0)); } `, @@ -777,7 +801,7 @@ export const createShaders = (precision: Precision) => { let N: f32 = f32(dimLength - nanCount); if (N <= 1.0) { let outputIndex = outY * xSize + outX; - outputData[outputIndex] = ${precision}(0.0 / 0.0); // NaN + outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); // NaN return; } @@ -836,10 +860,11 @@ export const createShaders = (precision: Precision) => { let sigmaX = sqrt(max(0.0, varX)); let sigmaY = sqrt(max(0.0, varY)); - // Use 1e-4 because f16 can flush 1e-6 to zero - let epsilon = 1e-4; - let denominator = sigmaX * sigmaY + epsilon; - let correlation = covXY / denominator; + let denominator = sigmaX * sigmaY; + var correlation: f32 = 0.0; + if (denominator > 1e-7) { + correlation = covXY / denominator; + } let outputIndex = outY * xSize + outX; outputData[outputIndex] = ${precision}(correlation); From 3a133ccbadacf0d3b266062b80fd43f90f9967ef Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 16:24:36 +0200 Subject: [PATCH 30/58] fixes idx memory strides --- src/components/computation/WGSLShaders.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/components/computation/WGSLShaders.ts b/src/components/computation/WGSLShaders.ts index fa03facb..25f92308 100644 --- a/src/components/computation/WGSLShaders.ts +++ b/src/components/computation/WGSLShaders.ts @@ -1058,10 +1058,9 @@ export const createShaders = (precision: Precision) => { return; } - let total_threads_per_slice = workGroups.x * workGroups.y * 16; - let globalIdx = global_id.z * total_threads_per_slice + - global_id.y * (workGroups.x * 4) + - global_id.x; + let globalIdx = outZ * zStride + + outY * yStride + + outX * xStride; let xy_radius: i32 = i32(kernelSize/2u); let z_radius: i32 = i32(kernelDepth/2u); @@ -1164,10 +1163,9 @@ export const createShaders = (precision: Precision) => { return; } - let total_threads_per_slice = workGroups.x * workGroups.y * 16; - let globalIdx = global_id.z * total_threads_per_slice + - global_id.y * (workGroups.x * 4) + - global_id.x; + let globalIdx = outZ * zStride + + outY * yStride + + outX * xStride; let xy_radius: i32 = i32(kernelSize/2u); let z_radius: i32 = i32(kernelDepth/2u); @@ -1278,10 +1276,9 @@ export const createShaders = (precision: Precision) => { return; } - let total_threads_per_slice = workGroups.x * workGroups.y * 16; - let globalIdx = global_id.z * total_threads_per_slice + - global_id.y * (workGroups.x * 4) + - global_id.x; + let globalIdx = outZ * zStride + + outY * yStride + + outX * xStride; let xy_radius: i32 = i32(kernelSize/2u); let z_radius: i32 = i32(kernelDepth/2u); From 0fefd3b82d41dc3eac11ab11ddbc4d89b7b95f1f Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 16:50:55 +0200 Subject: [PATCH 31/58] Infity is NaN --- src/components/computation/WGSLShaders.ts | 95 +++++++++++++++++++---- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/src/components/computation/WGSLShaders.ts b/src/components/computation/WGSLShaders.ts index 25f92308..59fb48d1 100644 --- a/src/components/computation/WGSLShaders.ts +++ b/src/components/computation/WGSLShaders.ts @@ -201,6 +201,7 @@ export const createShaders = (precision: Precision) => { MinReduction: /* wgsl */` ${ReductionBoilerPlate} var min: f32 = 1e12; + var nanCount: u32 = 0u; // Iterate along the dimension we're averaging if (reduceDim == 0u) { // Average along Z @@ -208,6 +209,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; } @@ -217,6 +219,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; } @@ -226,6 +229,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; } @@ -233,7 +237,11 @@ export const createShaders = (precision: Precision) => { } let outputIndex = outY * xSize + outX; - outputData[outputIndex] = ${precision}(min); + if (nanCount == dimLength) { + outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); + } else { + outputData[outputIndex] = ${precision}(min); + } } `, @@ -241,6 +249,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 @@ -248,6 +257,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; } @@ -257,6 +267,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; } @@ -266,6 +277,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; } @@ -273,13 +285,18 @@ export const createShaders = (precision: Precision) => { } let outputIndex = outY * xSize + outX; - outputData[outputIndex] = ${precision}(max); + if (nanCount == dimLength) { + outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); + } 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; @@ -287,6 +304,7 @@ export const createShaders = (precision: Precision) => { let inputIndex = cCoord + (z * zStride); let newVal = f32(inputData[u32(inputIndex)]); if (isNaN(newVal)){ + nanCount++; continue; } sum += newVal; @@ -297,6 +315,7 @@ export const createShaders = (precision: Precision) => { let inputIndex = cCoord + (y * yStride); let newVal = f32(inputData[u32(inputIndex)]); if (isNaN(newVal)){ + nanCount++; continue; } sum += newVal; @@ -307,13 +326,20 @@ 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(dimLength - nanCount); + if (N <= 1.0) { + let outputIndex = outY * xSize + outX; + outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); + return; + } + let mean: f32 = sum / N; var squaredDiffSum: f32 = 0.0; @@ -353,7 +379,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); } @@ -888,14 +914,19 @@ export const createShaders = (precision: Precision) => { let yOffset = ky * i32(yStride); 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 ++; } } } } - outputData[globalIdx] = ${precision}(sum / f32(count)); + if (count > 0u) { + outputData[globalIdx] = ${precision}(sum / f32(count)); + } else { + outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); + } } `, @@ -916,15 +947,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 < minVal){ minVal = sampledVal; } + count++; } } } } - outputData[globalIdx] = ${precision}(minVal); + if (count > 0u) { + outputData[globalIdx] = ${precision}(minVal); + } else { + outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); + } } `, @@ -946,14 +983,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 { + outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); + } } `, @@ -984,6 +1028,11 @@ export const createShaders = (precision: Precision) => { } } + if (count <= 1u) { + outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); + return; + } + let mean: f32 = sum / f32(count); var squaredDiffSum: f32 = 0.0; @@ -1109,6 +1158,11 @@ export const createShaders = (precision: Precision) => { } } + if (count <= 1u) { + outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); + return; + } + let N: f32 = f32(count); let meanX = xSum / N; let meanY = ySum / N; @@ -1117,9 +1171,11 @@ export const createShaders = (precision: Precision) => { let covXY = (xySum / N) - (meanX * meanY); let sigmaX = sqrt(max(0.0, varX)); let sigmaY = sqrt(max(0.0, varY)); - let epsilon = 1e-6; - let denominator = sigmaX * sigmaY + epsilon; - let correlation = covXY / denominator; + let denominator = sigmaX * sigmaY; + var correlation: f32 = bitcast(0x7fc00000u); + if (denominator > 1e-7) { + correlation = covXY / denominator; + } outputData[globalIdx] = ${precision}(correlation); } @@ -1208,6 +1264,10 @@ export const createShaders = (precision: Precision) => { } } + if (count <= 1u) { + outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); + return; + } let N: f32 = f32(count); let meanX = xSum / N; let meanY = ySum / N; @@ -1321,6 +1381,11 @@ export const createShaders = (precision: Precision) => { } + if (count <= 1u) { + outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); + return; + } + let N: f32 = f32(count); let meanX = xSum / N; let meanY = ySum / N; @@ -1349,7 +1414,11 @@ export const createShaders = (precision: Precision) => { } } } - outputData[globalIdx] = ${precision}(numSum/denomSum); + if (denomSum > 1e-7) { + outputData[globalIdx] = ${precision}(numSum/denomSum); + } else { + outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); + } } `, // #endregion From f7b0e019f76baaba1d9ea6033e759f87bae907a5 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 18:37:30 +0200 Subject: [PATCH 32/58] rm DOM lagging --- src/components/ui/DimSlicer/TimeCombobox.tsx | 3 ++- src/components/ui/MainPanel/Variables.tsx | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/ui/DimSlicer/TimeCombobox.tsx b/src/components/ui/DimSlicer/TimeCombobox.tsx index b15c8f5c..210698d6 100644 --- a/src/components/ui/DimSlicer/TimeCombobox.tsx +++ b/src/components/ui/DimSlicer/TimeCombobox.tsx @@ -59,9 +59,10 @@ export default function TimeCombobox({ const filtered = useMemo(() => { const normalizedInput = inputValue.trim().toLowerCase() const selectedQuery = selectedLabel.trim().toLowerCase() - return normalizedInput === '' || normalizedInput === selectedQuery + const results = normalizedInput === '' || normalizedInput === selectedQuery ? labeledValues : labeledValues.filter(({ label }) => label.toLowerCase().includes(normalizedInput)) + return results.slice(0, 100) }, [inputValue, selectedLabel, labeledValues]) const targetWidth = Math.min( diff --git a/src/components/ui/MainPanel/Variables.tsx b/src/components/ui/MainPanel/Variables.tsx index 391605b1..2a25f5c1 100644 --- a/src/components/ui/MainPanel/Variables.tsx +++ b/src/components/ui/MainPanel/Variables.tsx @@ -107,6 +107,15 @@ const Variables = () => { // Handle variable selection const handleVariableSelect = (val: string, idx: number) => { + if (selectedVar === val && meta?.name === val && metadata) { + if (popoverSide === "left") { + setOpenMetaPopover(true); + } else { + setShowMeta(true); + } + return; + } + setSelectedVar(val); setMeta(null); setMetadata(null); @@ -319,7 +328,7 @@ const Variables = () => { align="start" className="max-h-[80vh] overflow-y-auto w-[400px]" > - {metadata && ( + {metadata && meta && ( { { }
- {meta && ( + {meta && metadata && ( { setShowMeta(false); setOpenVariables(false); From c3ce153cd8b65c6ed91afe45e1f86c4f30abf6c3 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 18:51:59 +0200 Subject: [PATCH 33/58] windowing --- src/components/ui/DimSlicer/TimeCombobox.tsx | 56 +++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/src/components/ui/DimSlicer/TimeCombobox.tsx b/src/components/ui/DimSlicer/TimeCombobox.tsx index 210698d6..78071f04 100644 --- a/src/components/ui/DimSlicer/TimeCombobox.tsx +++ b/src/components/ui/DimSlicer/TimeCombobox.tsx @@ -1,5 +1,5 @@ 'use client' -import React, { useState, useMemo } from 'react' +import React, { useState, useMemo, useEffect } from 'react' import { Combobox, ComboboxContent, @@ -38,6 +38,12 @@ export default function TimeCombobox({ 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 === '') { @@ -59,11 +65,17 @@ export default function TimeCombobox({ const filtered = useMemo(() => { const normalizedInput = inputValue.trim().toLowerCase() const selectedQuery = selectedLabel.trim().toLowerCase() - const results = normalizedInput === '' || normalizedInput === selectedQuery - ? labeledValues - : labeledValues.filter(({ label }) => label.toLowerCase().includes(normalizedInput)) - return results.slice(0, 100) - }, [inputValue, selectedLabel, labeledValues]) + const isFiltering = normalizedInput !== '' && normalizedInput !== selectedQuery; + + if (isFiltering) { + const results = labeledValues.filter(({ label }) => label.toLowerCase().includes(normalizedInput)) + return results.slice(0, windowRadius * 2); + } + + const startIdx = Math.max(0, currentIndex - windowRadius); + const endIdx = Math.min(labeledValues.length, currentIndex + windowRadius); + return labeledValues.slice(startIdx, endIdx); + }, [inputValue, selectedLabel, labeledValues, windowRadius, currentIndex]) const targetWidth = Math.min( Math.max(Math.max(selectedLabel.length, placeholder.length) + 2, 12), @@ -88,7 +100,37 @@ export default function TimeCombobox({ /> {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; + + if (isFiltering) { + return 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) { + return Math.min(r + 50, 5000); + } + } + return r; + }); + } + }} + > {filtered.map(({ label, index }) => ( {label} From 96d5ac2c3dd1fe0b2209ea28f56332cd63fcf635 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 19:00:07 +0200 Subject: [PATCH 34/58] good scroll up --- src/components/ui/DimSlicer/TimeCombobox.tsx | 66 +++++++++++++++++--- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/src/components/ui/DimSlicer/TimeCombobox.tsx b/src/components/ui/DimSlicer/TimeCombobox.tsx index 78071f04..6ad4ba5f 100644 --- a/src/components/ui/DimSlicer/TimeCombobox.tsx +++ b/src/components/ui/DimSlicer/TimeCombobox.tsx @@ -62,21 +62,55 @@ export default function TimeCombobox({ return values.map((_, i) => ({ label: formattedValue(i), index: i })) }, [values, formattedValue]) - const filtered = useMemo(() => { + 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 results.slice(0, windowRadius * 2); + 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 labeledValues.slice(startIdx, endIdx); + 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), 20 @@ -101,6 +135,7 @@ export default function TimeCombobox({ {filtered.length === 0 ? No items found. : null} ) => { const target = e.currentTarget; const scrollPos = target.scrollTop; @@ -115,24 +150,41 @@ export default function TimeCombobox({ const selectedQuery = selectedLabel.trim().toLowerCase() const isFiltering = normalizedInput !== '' && normalizedInput !== selectedQuery; + let nextR = r; if (isFiltering) { - return isNearBottom ? Math.min(r + 50, 5000) : r; + 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) { - return Math.min(r + 50, 5000); + nextR = Math.min(r + 50, 5000); } } - return r; + + 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} ))} From e33de18beb2bd33df284f33645b6d89589081ef0 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 19:15:03 +0200 Subject: [PATCH 35/58] back hours --- src/components/ui/DimSlicer/TimeCombobox.tsx | 2 +- src/utils/HelperFuncs.ts | 28 ++++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/components/ui/DimSlicer/TimeCombobox.tsx b/src/components/ui/DimSlicer/TimeCombobox.tsx index 6ad4ba5f..020d4ece 100644 --- a/src/components/ui/DimSlicer/TimeCombobox.tsx +++ b/src/components/ui/DimSlicer/TimeCombobox.tsx @@ -113,7 +113,7 @@ export default function TimeCombobox({ const targetWidth = Math.min( Math.max(Math.max(selectedLabel.length, placeholder.length) + 2, 12), - 20 + 40 ) return ( diff --git a/src/utils/HelperFuncs.ts b/src/utils/HelperFuncs.ts index 71a7c9df..c2fe085d 100644 --- a/src/utils/HelperFuncs.ts +++ b/src/utils/HelperFuncs.ts @@ -92,13 +92,31 @@ export function parseLoc(input: any, units: string | undefined, verbose: boolean const [scale, offset] = parseTimeUnit(units) const timeStamp = Number(input) * scale; const date = new Date(timeStamp + offset); + + const day = date.getUTCDate(); + const month = date.getUTCMonth() + 1; // Months are 0-indexed + const year = date.getUTCFullYear(); + const hours = date.getUTCHours(); + const mins = date.getUTCMinutes(); + const secs = date.getUTCSeconds(); + + const lowerUnits = units.toLowerCase(); + const showTime = lowerUnits.includes('hour') || lowerUnits.includes('min') || lowerUnits.includes('sec') || hours !== 0 || mins !== 0 || secs !== 0; + if (verbose) { - return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`; // e.g., "18 Aug 2025" + let dateStr = `${day} ${months[month - 1]} ${year}`; + if (showTime) { + dateStr += ` ${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`; + if (secs !== 0 || lowerUnits.includes('sec')) dateStr += `:${String(secs).padStart(2, '0')}`; + } + return dateStr; } else { - const day = date.getDate(); - const month = date.getMonth() + 1; // Months are 0-indexed - const year = date.getFullYear(); - return `${String(day).padStart(2, '0')}-${String(month).padStart(2, '0')}-${year}`; // e.g., "18-8-2025" + let dateStr = `${String(day).padStart(2, '0')}-${String(month).padStart(2, '0')}-${year}`; + if (showTime) { + dateStr += ` ${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`; + if (secs !== 0 || lowerUnits.includes('sec')) dateStr += `:${String(secs).padStart(2, '0')}`; + } + return dateStr; } } catch{ From 8cb110b19f52622aa8b52dd39468171e000b4d0c Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 19:32:33 +0200 Subject: [PATCH 36/58] flex wrap --- src/components/ui/DimSlicer/DimSlicerTimeControl.tsx | 2 +- src/components/ui/DimSlicer/TimeCombobox.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ui/DimSlicer/DimSlicerTimeControl.tsx b/src/components/ui/DimSlicer/DimSlicerTimeControl.tsx index af9e6285..c74e4e70 100644 --- a/src/components/ui/DimSlicer/DimSlicerTimeControl.tsx +++ b/src/components/ui/DimSlicer/DimSlicerTimeControl.tsx @@ -38,7 +38,7 @@ export function DimSlicerTimeControl({ showInput = true, }: DimSlicerTimeControlProps) { return ( -
+
Date: Sun, 5 Jul 2026 19:45:27 +0200 Subject: [PATCH 37/58] spinner, global styles --- src/components/ui/MainPanel/Variables.tsx | 37 +++++++++++++++-------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/components/ui/MainPanel/Variables.tsx b/src/components/ui/MainPanel/Variables.tsx index 2a25f5c1..32b85ca7 100644 --- a/src/components/ui/MainPanel/Variables.tsx +++ b/src/components/ui/MainPanel/Variables.tsx @@ -1,7 +1,8 @@ "use client"; -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect, useMemo, useRef } from "react"; import { TbVariable } from "react-icons/tb"; +import { Loader2 } from "lucide-react"; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { useShallow } from "zustand/shallow"; import { Separator } from "@/components/ui/separator"; @@ -49,6 +50,8 @@ const Variables = () => { const [selectedVar, setSelectedVar] = useState(null); + const [isLoadingVar, setIsLoadingVar] = useState(null); + const activeRequest = useRef(null); const [meta, setMeta] = useState(null); const [query, setQuery] = useState(""); // root *open by default* (collapsible but starts open) @@ -116,11 +119,15 @@ const Variables = () => { return; } + setIsLoadingVar(val); setSelectedVar(val); setMeta(null); setMetadata(null); + activeRequest.current = val; Promise.all([GetDimInfo(val), GetAttributes(val)]).then(([dimInfo, attr]) => { + if (activeRequest.current !== val) return; + const relevant = zMeta?.find((e: any) => e.name === val); if (relevant) { setMeta({ @@ -133,15 +140,19 @@ const Variables = () => { }); } setMetadata(attr); + setIsLoadingVar(null); + + if (popoverSide === "left") { + setOpenMetaPopover(true); + } else { + setShowMeta(true); + } }).catch((err) => { + if (activeRequest.current === val) { + setIsLoadingVar(null); + } console.error("Failed to fetch dimension info or attributes:", err); }); - - if (popoverSide === "left") { - setOpenMetaPopover(true); - } else { - setShowMeta(true); - } }; useEffect(() => { @@ -167,13 +178,15 @@ const Variables = () => { return (
handleVariableSelect(val, idx)} > - {variableName} + {variableName} + {isLoadingVar === val && }
{!isLastItem && }
From 86d8754a76235ea10b6015b09f4493f8468eb254 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 20:13:51 +0200 Subject: [PATCH 38/58] descriptions --- src/components/ui/DimSlicer/DimSlicer.tsx | 4 ++-- src/components/ui/MainPanel/Variables.tsx | 11 +++-------- src/components/ui/MetaData.tsx | 6 ++++-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/components/ui/DimSlicer/DimSlicer.tsx b/src/components/ui/DimSlicer/DimSlicer.tsx index bcdb23d4..a92150bd 100644 --- a/src/components/ui/DimSlicer/DimSlicer.tsx +++ b/src/components/ui/DimSlicer/DimSlicer.tsx @@ -88,9 +88,9 @@ const DimSlicer: React.FC = ({ return clamp(Math.round(val / step) * step, 0, maxIndex); } let closestIndex = 0; - let minDiff = Math.abs(values[0] - val); + let minDiff = Math.abs(Number(values[0]) - val); for (let i = 1; i < values.length; i++) { - const diff = Math.abs(values[i] - val); + const diff = Math.abs(Number(values[i]) - val); if (diff < minDiff) { minDiff = diff; closestIndex = i; diff --git a/src/components/ui/MainPanel/Variables.tsx b/src/components/ui/MainPanel/Variables.tsx index 32b85ca7..53dcce2e 100644 --- a/src/components/ui/MainPanel/Variables.tsx +++ b/src/components/ui/MainPanel/Variables.tsx @@ -21,6 +21,7 @@ import { Dialog, DialogContent, DialogTitle, + DialogDescription, } from "@/components/ui/dialog"; import { Accordion, @@ -302,13 +303,6 @@ const Variables = () => { { - const target = e.target as HTMLElement; - // If the click is inside the MetaData popover, do not close - if (target.closest('[data-meta-popover]')) { - e.preventDefault(); - } - }} >
{ )} {popoverSide === "top" && ( - + { } + Variables configuration dialog
{meta && metadata && ( , variable: string }) => { return ( - + @@ -84,9 +85,10 @@ const Metadata = ({ data, variable }: { data: Record, variable: str Show Variable Attributes - + Attributes + Metadata Information for variable
From 3a7b5dd7e954815729607465485f32cb3ae368af Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 20:20:09 +0200 Subject: [PATCH 39/58] keep open --- src/components/ui/MainPanel/Variables.tsx | 28 ++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/components/ui/MainPanel/Variables.tsx b/src/components/ui/MainPanel/Variables.tsx index 53dcce2e..411a54d1 100644 --- a/src/components/ui/MainPanel/Variables.tsx +++ b/src/components/ui/MainPanel/Variables.tsx @@ -53,6 +53,7 @@ const Variables = () => { const [selectedVar, setSelectedVar] = useState(null); const [isLoadingVar, setIsLoadingVar] = useState(null); const activeRequest = useRef(null); + const isInteractingWithMeta = useRef(false); const [meta, setMeta] = useState(null); const [query, setQuery] = useState(""); // root *open by default* (collapsible but starts open) @@ -109,6 +110,13 @@ const Variables = () => { } }, [query, tree]); + const handleOpenChange = (open: boolean) => { + if (!open && isInteractingWithMeta.current) { + return; // Do not close if interacting with Meta popovers/portals + } + setOpenVariables(open); + }; + // Handle variable selection const handleVariableSelect = (val: string, idx: number) => { if (selectedVar === val && meta?.name === val && metadata) { @@ -273,7 +281,7 @@ const Variables = () => { return ( <> - +
@@ -303,6 +311,24 @@ const Variables = () => { { + const target = e.target as HTMLElement; + // Prevent the main variable list from closing when interacting with Meta popups/portals. + // We set a flag instead of calling e.preventDefault() so that native text selection still works! + if ( + target.closest('[data-meta-popover]') || + target.closest('.metadata-dialog') || + target.closest('[data-slot="combobox-content"]') || + target.closest('[role="dialog"]') || + target.closest('[role="listbox"]') || + target.closest('[data-radix-popper-content-wrapper]') + ) { + isInteractingWithMeta.current = true; + setTimeout(() => { + isInteractingWithMeta.current = false; + }, 0); + } + }} >
Date: Sun, 5 Jul 2026 20:42:55 +0200 Subject: [PATCH 40/58] mv to Dialog --- .../ui/MainPanel/MetaDimSelector.tsx | 27 +++++--- src/components/ui/MainPanel/Variables.tsx | 13 +--- src/components/ui/MetaData.tsx | 69 +++++++++++-------- 3 files changed, 58 insertions(+), 51 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index dbabecc2..bcaade1c 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -7,6 +7,7 @@ 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 { parseLoc } from '@/utils/HelperFuncs'; import { ChevronDown, ChevronRight } from 'lucide-react'; @@ -436,18 +437,22 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, return ( <> {`${meta.long_name ?? ''} `} - - + + Attributes - - - {renderAttributes(metadata, defaultAttributes)} - - + + + + Attributes + Metadata Information for variable + +
+
+ {renderAttributes(metadata, defaultAttributes)} +
+
+
+

diff --git a/src/components/ui/MainPanel/Variables.tsx b/src/components/ui/MainPanel/Variables.tsx index 411a54d1..f86a41f4 100644 --- a/src/components/ui/MainPanel/Variables.tsx +++ b/src/components/ui/MainPanel/Variables.tsx @@ -12,6 +12,7 @@ import { GetAttributes } from "@/components/zarr/ZarrLoaderLRU"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; import { Button } from "@/components/ui/button-enhanced"; import { Input } from "../input"; +import { useIsMobile } from "@/hooks/use-mobile"; import { Tooltip, TooltipContent, @@ -32,7 +33,8 @@ import { const Variables = () => { - const [popoverSide, setPopoverSide] = useState<"left" | "top">("left"); + const isMobile = useIsMobile(); + const popoverSide = isMobile ? "top" : "left"; const [openMetaPopover, setOpenMetaPopover] = useState(false); const [showMeta, setShowMeta] = useState(false); @@ -170,15 +172,6 @@ const Variables = () => { setMetadata(null); }, [initStore, setMetadata]); - useEffect(() => { - const handleResize = () => { - setPopoverSide(window.innerWidth < 768 ? "top" : "left"); - }; - handleResize(); - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, []); - // Variable item renderer (keeps separator between variables in same group) const VariableItem = ({ val, idx, arrayLength }: { val: string; idx: number; arrayLength: number }) => { const variableName = val.split('/').pop() || val; diff --git a/src/components/ui/MetaData.tsx b/src/components/ui/MetaData.tsx index 8a68d470..826b6809 100644 --- a/src/components/ui/MetaData.tsx +++ b/src/components/ui/MetaData.tsx @@ -20,6 +20,7 @@ import { import { Button } from "@/components/ui/button-enhanced" +import { useIsMobile } from "@/hooks/use-mobile" export const defaultAttributes = [ "long_name", @@ -66,37 +67,45 @@ export function renderAttributes( }); } const Metadata = ({ data, variable }: { data: Record, variable: string }) => { + const trigger = ( + + ); + + const content = ( +
+
+ {renderAttributes(data, defaultAttributes)} +
+
+ ); + return ( - - - - - - - - - Show Variable Attributes - - - - - Attributes - Metadata Information for variable - -
-
- {renderAttributes(data, defaultAttributes)} -
-
-
-
+ + + + + {trigger} + + + + Show Variable Attributes + + + + + Attributes + Metadata Information for variable + + {content} + + ); }; From 98ee7ce890d47652271cba66c981dcf2a7b6f29c Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 20:51:29 +0200 Subject: [PATCH 41/58] popover attributes --- .../ui/MainPanel/MetaDimSelector.tsx | 47 ++++++++++------ src/components/ui/MetaData.tsx | 53 +++++++++++++++---- 2 files changed, 75 insertions(+), 25 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index bcaade1c..f7be1ee2 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -11,6 +11,7 @@ import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, Dialog import { Badge, Switch, Input, Hider } from "@/components/ui"; import { parseLoc } from '@/utils/HelperFuncs'; import { ChevronDown, ChevronRight } from 'lucide-react'; +import { useIsMobile } from "@/hooks/use-mobile"; import { useCacheStore } from "@/GlobalStates/CacheStore"; import { usePlotStore } from '@/GlobalStates/PlotStore'; @@ -102,6 +103,7 @@ const getOrigIdx = (dimName: string) => { }; export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, setOpenVariables }: Props) { + const isMobile = useIsMobile(); const dimArrays = useMemo( () => (meta?.dimInfo?.dimArrays ?? []).map((a) => Array.from(a)), [meta?.dimInfo?.dimArrays] @@ -437,22 +439,37 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, return ( <> {`${meta.long_name ?? ''} `} - - - Attributes - - - - Attributes - Metadata Information for variable - -
-
- {renderAttributes(metadata, defaultAttributes)} + {isMobile ? ( + + + Attributes + + + + Attributes + Metadata Information for variable + +
+
+ {renderAttributes(metadata, defaultAttributes)} +
-
- -
+ +
+ ) : ( + + + Attributes + + + {renderAttributes(metadata, defaultAttributes)} + + + )}
diff --git a/src/components/ui/MetaData.tsx b/src/components/ui/MetaData.tsx index 826b6809..812253dd 100644 --- a/src/components/ui/MetaData.tsx +++ b/src/components/ui/MetaData.tsx @@ -12,6 +12,12 @@ import { DialogTrigger, } from "@/components/ui/dialog" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" + import { Tooltip, TooltipContent, @@ -67,6 +73,8 @@ export function renderAttributes( }); } const Metadata = ({ data, variable }: { data: Record, variable: string }) => { + const isMobile = useIsMobile(); + const trigger = (
); + if (isMobile) { + return ( + + + + + {trigger} + + + + Show Variable Attributes + + + + + Attributes + Metadata Information for variable + + {content} + + + ); + } + return ( - + - + {trigger} - + Show Variable Attributes - - - Attributes - Metadata Information for variable - + +

Attributes

{content} -
-
+ + ); }; From 159b29173884c86480abeecf524402a49515b0ad Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 22:37:29 +0200 Subject: [PATCH 42/58] fixes width --- .../ui/MainPanel/MetaDimSelector.tsx | 566 +++++++++--------- src/components/ui/MainPanel/Variables.tsx | 2 +- 2 files changed, 281 insertions(+), 287 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index f7be1ee2..cf84eefc 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -9,8 +9,9 @@ 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 } from 'lucide-react'; +import { ChevronDown, ChevronRight, AlertCircle, CheckCircle2 } from 'lucide-react'; import { useIsMobile } from "@/hooks/use-mobile"; import { useCacheStore } from "@/GlobalStates/CacheStore"; @@ -69,23 +70,7 @@ const AXIS_COLOR: Record = { -function selectionSummary( - availableDims: DimOption[], - activeRows: SlicerRow[], - collapsedSels: Record, -): string { - const parts = availableDims.map((dim) => { - const activeRow = activeRows.find((r) => r.dimName === dim.name); - const sel = activeRow ? activeRow.sel : collapsedSels[dim.name]; - if (!sel) return `${dim.name}=?`; - const range = - sel.mode === 'scalar' - ? sel.scalar || '0' - : `${sel.start !== '' ? sel.start : '0'}:${sel.stop !== '' ? sel.stop : ':'}`; - return `${dim.name}=${range}`; - }); - return `[ ${parts.join(', ')} ]`; -} + interface SlicerRow { id: number; @@ -356,10 +341,7 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, const updateCollapsedSel = (dimName: string, sel: SliceSelectionState) => setCollapsedSels((prev) => ({ ...prev, [dimName]: { ...sel, mode: 'scalar' } })); - const summary = useMemo( - () => selectionSummary(availableDims, rows, collapsedSels), - [availableDims, rows, collapsedSels], - ); + const activeDimNames = new Set(rows.map((r) => r.dimName)); const collapsedDims = availableDims.filter((d) => !activeDimNames.has(d.name)); @@ -437,293 +419,305 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, }; return ( - <> - {`${meta.long_name ?? ''} `} - {isMobile ? ( - - - Attributes - - - - Attributes - Metadata Information for variable - -
-
- {renderAttributes(metadata, defaultAttributes)} -
+
+
+ {/* Top Header: Name, Attributes, Options, and Plot button */} +
+
+ {`${meta.long_name ?? meta.name ?? ''} `} + {isMobile ? ( + + + Attributes + + + + Attributes + Metadata Information for variable + +
+
+ {renderAttributes(metadata, defaultAttributes)} +
+
+
+
+ ) : ( + + + Attributes + + + {renderAttributes(metadata, defaultAttributes)} + + + )}
- -
- ) : ( - - - Attributes - - - {renderAttributes(metadata, defaultAttributes)} - - - )} -
- -
- {'selection'} {summary} -
+ + {/* Options */} +
+ {/* Coarsen Toggle */} +
+ + setCoarsen(e)}/> +
-
- {rows.map((row) => { - const originalIndex = availableDims.findIndex( - (d) => d.name === row.dimName - ); - - return ( - - ( - {row.dimName} - ,{' '} - {row.axis} - ,{' '} - - {originalIndex} - - ) - - ); - })} -
+ {/* Compress Toggle */} +
+ + setCompress(e)}/> +
-
-
- Data Shape - {`[${formatArray(dataShape ?? [])}]`} -
-
- Chunk Shape - {`[${formatArray(chunkShape ?? [])}]`} -
-
+ {/* Size info badge */} +
+ Raw: {formatBytes(currentSize)} + | + Stored: {compress ? "<" : ""}{formatBytes(cachedSize)} +
-
- - - {addTooltip && ( -
-
- {addTooltip} + {/* Plot Button & Info */} +
+ {tooBig && +
+ + Too many textures ({texCount}/14). Won't fit. + +
+ } + {!tooBig && } + {cached && Already cached} +
- )} -
- {/* Active slicers — locked to slice, z/y/x axes only, trash only on last */} -
- {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" + {/* Coarsen Expand UI */} + +
+
= 3 ? 'visible' : 'hidden' }} + > + Temporal Coarsening +
+ { + const val = parseInt(e.target.value) + setDisplayDepth(e.target.value) + setKernelDepth(Math.pow(2,val)) + }} /> - ))} +
- )} -
- )} - - {/* Coarsen Section */} -
-
- - setCoarsen(e)}/> -
-
- -
-
= 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 2n -
-
-
- - {/* Size and Cache UI */} -
-
-
- Raw Size: {formatBytes(currentSize)} -
-
- Stored size: {compress ? "<" : null} {formatBytes(cachedSize)} -
-
- - {currentSize > maxSize && ( - <> -
-
- - {smallCache ? "Selection won't fit in Cache" : "Data Will Fit"} - +
+ Spatial Coarsening +
+ { + const val = parseInt(e.target.value) + setDisplaySpat(e.target.value) + setKernelSize(Math.pow(2, val)) + }} + />
-
- Decrease selection or Increase cache size
-
- Expand Cache: -
+
+
+ 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 +
+ setCacheSize(maxSize+e[0]*(1024*1024))} + className="flex-1 min-w-0" + /> +
setCacheSize(parseInt(e.target.value)*(1024*1024))} /> - MB -
- - - - - - - Increasing this too far can cause crashes. Mobile users beware - - + MB + + + + + + Increasing this too far can cause crashes. Mobile users beware + + +
- setCacheSize(maxSize+e[0]*(1024*1024))} - /> -
- - )} - -
-
- +
+ + + )} + + {/* Dimension Table */} +
+
+ + + + + + + + + + + + {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 ( + + + + + + + + ); + })} + +
DimAxisSelectionData ShapeChunk Shape
+ {dim.name} + {axis.toUpperCase()}{range}{dataSize}{chunkSize}
+
- setCompress(e)}/>
-
- {cached && -
- This data is already cached. + {/* DimSlicers Area */} +
+
+

Active Dimensions

+
+ + + {addTooltip && ( +
+
+ {addTooltip} +
+
+ )}
- } - {tooBig && -
- - Not only will this certainly not fit in memory, but it also won't fit in a single shader call. You are wild for this one. Textures: {texCount}/14 - +
+ + {/* 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" + /> + ))} +
+ )}
- } - {!tooBig && } + )}
- +
); } \ No newline at end of file diff --git a/src/components/ui/MainPanel/Variables.tsx b/src/components/ui/MainPanel/Variables.tsx index f86a41f4..3276dfa5 100644 --- a/src/components/ui/MainPanel/Variables.tsx +++ b/src/components/ui/MainPanel/Variables.tsx @@ -373,7 +373,7 @@ const Variables = () => { )} {popoverSide === "top" && ( - + { } Variables configuration dialog
From 04d176c9e29cba068c1eb721e4e617cf0be85ec7 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 5 Jul 2026 22:56:40 +0200 Subject: [PATCH 43/58] always Dialog for colorbar --- src/components/ui/Colorbar.tsx | 2 +- src/components/ui/MainPanel/MetaDimSelector.tsx | 4 +++- src/components/ui/MetaData.tsx | 15 ++++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/components/ui/Colorbar.tsx b/src/components/ui/Colorbar.tsx index 5ef73864..561423e3 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) && { 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] @@ -425,7 +427,7 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta,
{`${meta.long_name ?? meta.name ?? ''} `} - {isMobile ? ( + {mounted && isMobile ? ( Attributes diff --git a/src/components/ui/MetaData.tsx b/src/components/ui/MetaData.tsx index 812253dd..05662529 100644 --- a/src/components/ui/MetaData.tsx +++ b/src/components/ui/MetaData.tsx @@ -72,8 +72,17 @@ export function renderAttributes( ); }); } -const Metadata = ({ data, variable }: { data: Record, variable: string }) => { - const isMobile = useIsMobile(); +const Metadata = ({ data, variable, isMobile: isMobileProp }: { data: Record, variable: string, isMobile?: boolean }) => { + const isMobileHook = useIsMobile(); + const [mounted, setMounted] = React.useState(false); + + React.useEffect(() => { + setMounted(true); + }, []); + + // If isMobileProp is provided, we use it directly (no hydration mismatch). + // Otherwise, we default to false during SSR to match the initial Popover render, and swap after mount. + const isMobile = isMobileProp !== undefined ? isMobileProp : (mounted ? isMobileHook : false); const trigger = (
) : ( -
+
{ if (typeof value === 'string') return value; @@ -68,10 +67,6 @@ const AXIS_COLOR: Record = { c: 'text-yellow-500', }; - - - - interface SlicerRow { id: number; dimName: string; @@ -136,7 +131,7 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, const [displaySpat, setDisplaySpat] = useState(String(kernelSize)) const [displayDepth, setDisplayDepth] = useState(String(kernelDepth)) - React.useEffect(() => { + useEffect(() => { setDimArrays(dimArrays); setDimNames(dimNames); setDimUnits(dimUnits); diff --git a/src/components/ui/MainPanel/Variables.tsx b/src/components/ui/MainPanel/Variables.tsx index 3276dfa5..fe1ec3ef 100644 --- a/src/components/ui/MainPanel/Variables.tsx +++ b/src/components/ui/MainPanel/Variables.tsx @@ -352,7 +352,7 @@ const Variables = () => { data-meta-popover side="left" align="start" - className="max-h-[80vh] overflow-y-auto w-[400px]" + className="max-h-[80vh] overflow-y-auto w-[350px]" > {metadata && meta && ( Date: Tue, 7 Jul 2026 14:50:01 +0200 Subject: [PATCH 45/58] plus one --- src/components/ui/MainPanel/MetaDimSelector.tsx | 12 ++++++++++-- src/components/zarr/GetArray.ts | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index 83569821..3c9bc1d6 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -212,6 +212,7 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, 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) }; }; @@ -246,6 +247,7 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, 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; @@ -365,7 +367,10 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, const val = parseInt(sel.scalar) || 0; return [val, val + 1]; } - return [parseInt(sel.start) || 0, parseInt(sel.stop) || defaultLast]; + const start = parseInt(sel.start) || 0; + let stop = parseInt(sel.stop); + stop = isNaN(stop) ? defaultLast : stop + 1; + return [start, stop]; }; setZSlice(getSliceArray(rowZ, dataShape ? dataShape[getOrigIdx(rowZ?.dimName || '')] : 0)); @@ -376,7 +381,10 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, const row = rows.find((r) => r.dimName === dim.name); if (row) { if (row.sel.mode === 'scalar') return parseInt(row.sel.scalar) || 0; - return [parseInt(row.sel.start) || 0, parseInt(row.sel.stop) || dim.size]; + const start = parseInt(row.sel.start) || 0; + let stop = parseInt(row.sel.stop); + stop = isNaN(stop) ? dim.size : stop + 1; + return [start, stop]; } const colSel = collapsedSels[dim.name]; if (colSel && colSel.mode === 'scalar') return parseInt(colSel.scalar) || 0; diff --git a/src/components/zarr/GetArray.ts b/src/components/zarr/GetArray.ts index 5d4cfe12..7dd2efeb 100644 --- a/src/components/zarr/GetArray.ts +++ b/src/components/zarr/GetArray.ts @@ -32,7 +32,7 @@ export async function GetArray(varOveride?: string) { const xDimIndex = mappedX >= 0 ? mappedX : (unmappedDims.length > 0 ? unmappedDims.shift()! : rank - 1); const yDimIndex = mappedY >= 0 ? mappedY : (unmappedDims.length > 0 ? unmappedDims.shift()! : rank - 2); - const zDimIndex = mappedZ >= 0 ? mappedZ : -1; + const zDimIndex = mappedZ >= 0 ? mappedZ : (unmappedDims.length > 0 ? unmappedDims.shift()! : -1); const hasZ = zDimIndex >= 0; From b173aceff880d026df2dd8673bc391ce42257300 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 7 Jul 2026 16:03:16 +0200 Subject: [PATCH 46/58] revert --- src/components/computation/WGSLShaders.ts | 245 +++++----------------- src/components/computation/webGPU.ts | 69 +++--- 2 files changed, 81 insertions(+), 233 deletions(-) diff --git a/src/components/computation/WGSLShaders.ts b/src/components/computation/WGSLShaders.ts index 59fb48d1..8711383e 100644 --- a/src/components/computation/WGSLShaders.ts +++ b/src/components/computation/WGSLShaders.ts @@ -91,9 +91,10 @@ export const createShaders = (precision: Precision) => { return; } - let globalIdx = outZ * ySize * xSize + - outY * xSize + - outX; + let total_threads_per_slice = workGroups.x * workGroups.y * 16; + let globalIdx = global_id.z * total_threads_per_slice + + global_id.y * (workGroups.x * 4) + + global_id.x; let xy_radius: i32 = i32(kernelSize/2u); let z_radius: i32 = i32(kernelDepth/2u); @@ -150,7 +151,6 @@ 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,7 +159,6 @@ export const createShaders = (precision: Precision) => { let inputIndex = cCoord + (z * zStride); let newVal = f32(inputData[u32(inputIndex)]); if (isNaN(newVal)){ - nanCount++; continue; } sum += newVal; @@ -170,7 +169,6 @@ export const createShaders = (precision: Precision) => { let inputIndex = cCoord + (y * yStride); let newVal = f32(inputData[u32(inputIndex)]); if (isNaN(newVal)){ - nanCount++; continue; } sum += newVal; @@ -181,27 +179,20 @@ export const createShaders = (precision: Precision) => { let inputIndex = cCoord + (x * xStride); let newVal = f32(inputData[u32(inputIndex)]); if (isNaN(newVal)){ - nanCount++; continue; } sum += newVal; } } - let N = f32(dimLength - nanCount); let outputIndex = outY * xSize + outX; - if (N > 0.0) { - outputData[outputIndex] = ${precision}(sum / N); - } else { - outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); // NaN - } + outputData[outputIndex] = ${precision}(sum / f32(dimLength)); } `, MinReduction: /* wgsl */` ${ReductionBoilerPlate} var min: f32 = 1e12; - var nanCount: u32 = 0u; // Iterate along the dimension we're averaging if (reduceDim == 0u) { // Average along Z @@ -209,7 +200,6 @@ 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; } @@ -219,7 +209,6 @@ 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; } @@ -229,7 +218,6 @@ 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; } @@ -237,11 +225,7 @@ export const createShaders = (precision: Precision) => { } let outputIndex = outY * xSize + outX; - if (nanCount == dimLength) { - outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); - } else { - outputData[outputIndex] = ${precision}(min); - } + outputData[outputIndex] = ${precision}(min); } `, @@ -249,7 +233,6 @@ 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 @@ -257,7 +240,6 @@ 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; } @@ -267,7 +249,6 @@ 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; } @@ -277,7 +258,6 @@ 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; } @@ -285,18 +265,13 @@ export const createShaders = (precision: Precision) => { } let outputIndex = outY * xSize + outX; - if (nanCount == dimLength) { - outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); - } else { - outputData[outputIndex] = ${precision}(max); - } + 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; @@ -304,7 +279,6 @@ export const createShaders = (precision: Precision) => { let inputIndex = cCoord + (z * zStride); let newVal = f32(inputData[u32(inputIndex)]); if (isNaN(newVal)){ - nanCount++; continue; } sum += newVal; @@ -315,7 +289,6 @@ export const createShaders = (precision: Precision) => { let inputIndex = cCoord + (y * yStride); let newVal = f32(inputData[u32(inputIndex)]); if (isNaN(newVal)){ - nanCount++; continue; } sum += newVal; @@ -326,20 +299,13 @@ export const createShaders = (precision: Precision) => { let inputIndex = cCoord + (x * xStride); let newVal = f32(inputData[u32(inputIndex)]); if (isNaN(newVal)){ - nanCount++; continue; } sum += newVal; } } - let N = f32(dimLength - nanCount); - if (N <= 1.0) { - let outputIndex = outY * xSize + outX; - outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); - return; - } - let mean: f32 = sum / N; + let mean: f32 = sum / f32(dimLength); var squaredDiffSum: f32 = 0.0; @@ -379,7 +345,7 @@ export const createShaders = (precision: Precision) => { } } - let stDev: f32 = sqrt(squaredDiffSum / N); + let stDev: f32 = sqrt(squaredDiffSum / f32(dimLength)); let outputIndex = outY * xSize + outX; outputData[outputIndex] = ${precision}(stDev); } @@ -591,14 +557,8 @@ export const createShaders = (precision: Precision) => { } } - let N: f32 = f32(dimLength - nanCount); - if (N <= 1.0) { - let outputIndex = outY * xSize + outX; - outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); // NaN - return; - } - let xMean: f32 = xSum / N; - let yMean: f32 = ySum / N; + let xMean: f32 = xSum / f32(dimLength - nanCount); + let yMean: f32 = ySum / f32(dimLength - nanCount); var numSum: f32 = 0; var denomSum: f32 = 0; @@ -642,11 +602,7 @@ export const createShaders = (precision: Precision) => { } let outputIndex = outY * xSize + outX; - if (denomSum > 1e-7) { - outputData[outputIndex] = ${precision}(numSum / denomSum); - } else { - outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); // NaN - } + outputData[outputIndex] = ${precision}(numSum/(denomSum+1e-4)); } `, @@ -718,11 +674,6 @@ export const createShaders = (precision: Precision) => { } var N: f32 = f32(dimLength - nanCount); - if (N <= 1.0) { - let outputIndex = outY * xSize + outX; - outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); // NaN - return; - } let xMean: f32 = xSum / N; let yMean: f32 = ySum / N; @@ -739,7 +690,7 @@ export const createShaders = (precision: Precision) => { } let outputIndex = outY * xSize + outX; - outputData[outputIndex] = ${precision}(numSum / (N - 1.0)); + outputData[outputIndex] = ${precision}(numSum / (N - 1)); } `, @@ -779,10 +730,13 @@ export const createShaders = (precision: Precision) => { } var xSum: f32 = 0.0; + var xxSum: f32 = 0.0; var ySum: f32 = 0.0; - var nanCount: u32 = 0u; - - // --- First Pass: Calculate Means --- + var yySum: f32 = 0.0; + var xySum: f32 = 0.0; + + var nanCount: u32 = 0; + // 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++) { @@ -794,7 +748,10 @@ export const createShaders = (precision: Precision) => { continue; } xSum += xI; + xxSum += xI * xI; ySum += yI; + yySum += yI * yI; + xySum += xI * yI; } } else if (reduceDim == 1u) { // Average along Y let cCoord = outX * xStride + outY * zStride; @@ -807,7 +764,10 @@ export const createShaders = (precision: Precision) => { continue; } xSum += xI; + xxSum += xI * xI; ySum += yI; + yySum += yI * yI; + xySum += xI * yI; } } else { // Average along X let cCoord = outX * yStride + outY * zStride; @@ -820,77 +780,24 @@ export const createShaders = (precision: Precision) => { continue; } xSum += xI; + xxSum += xI * xI; ySum += yI; + yySum += yI * yI; + xySum += xI * yI; } } let N: f32 = f32(dimLength - nanCount); - if (N <= 1.0) { - let outputIndex = outY * xSize + outX; - outputData[outputIndex] = ${precision}(bitcast(0x7fc00000u)); // NaN - return; - } - let meanX = xSum / N; let meanY = ySum / N; - - // --- Second Pass: Calculate Variance and Covariance --- - var varXSum: f32 = 0.0; - var varYSum: f32 = 0.0; - var covXYSum: f32 = 0.0; - - 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); - let xI = f32(firstData[inputIndex]); - let yI = f32(secondData[inputIndex]); - if (isNaN(xI) || isNaN(yI)){ continue; } - let dx = xI - meanX; - let dy = yI - meanY; - varXSum += dx * dx; - varYSum += dy * dy; - covXYSum += dx * dy; - } - } 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); - let xI = f32(firstData[inputIndex]); - let yI = f32(secondData[inputIndex]); - if (isNaN(xI) || isNaN(yI)){ continue; } - let dx = xI - meanX; - let dy = yI - meanY; - varXSum += dx * dx; - varYSum += dy * dy; - covXYSum += dx * dy; - } - } else { // Average along X - let cCoord = outX * yStride + outY * zStride; - for (var x: u32 = 0u; x < dimLength; x++) { - let inputIndex = cCoord + (x * xStride); - let xI = f32(firstData[inputIndex]); - let yI = f32(secondData[inputIndex]); - if (isNaN(xI) || isNaN(yI)){ continue; } - let dx = xI - meanX; - let dy = yI - meanY; - varXSum += dx * dx; - varYSum += dy * dy; - covXYSum += dx * dy; - } - } - - let varX = varXSum / N; - let varY = varYSum / N; - let covXY = covXYSum / N; + let varX = (xxSum / N) - (meanX * meanX); + let varY = (yySum / N) - (meanY * meanY); + let covXY = (xySum / N) - (meanX * meanY); let sigmaX = sqrt(max(0.0, varX)); let sigmaY = sqrt(max(0.0, varY)); - - let denominator = sigmaX * sigmaY; - var correlation: f32 = 0.0; - if (denominator > 1e-7) { - correlation = covXY / denominator; - } + let epsilon = 1e-6; + let denominator = sigmaX * sigmaY + epsilon; + let correlation = covXY / denominator; let outputIndex = outY * xSize + outX; outputData[outputIndex] = ${precision}(correlation); @@ -914,19 +821,14 @@ export const createShaders = (precision: Precision) => { let yOffset = ky * i32(yStride); let zOffset = kz * i32(zStride); let newIdx = i32(globalIdx) + xOffset + yOffset + zOffset; - let val = f32(inputData[u32(newIdx)]); - if (isNaN(val)) { continue; } - sum += val; + + sum += f32(inputData[u32(newIdx)]); count ++; } } } } - if (count > 0u) { - outputData[globalIdx] = ${precision}(sum / f32(count)); - } else { - outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); - } + outputData[globalIdx] = ${precision}(sum / f32(count)); } `, @@ -947,21 +849,15 @@ 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++; } } } } - if (count > 0u) { - outputData[globalIdx] = ${precision}(minVal); - } else { - outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); - } + outputData[globalIdx] = ${precision}(minVal); } `, @@ -983,21 +879,14 @@ 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++; } } } } - - if (count > 0u) { - outputData[globalIdx] = ${precision}(maxVal); - } else { - outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); - } + outputData[globalIdx] = ${precision}(maxVal); } `, @@ -1028,11 +917,6 @@ export const createShaders = (precision: Precision) => { } } - if (count <= 1u) { - outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); - return; - } - let mean: f32 = sum / f32(count); var squaredDiffSum: f32 = 0.0; @@ -1107,9 +991,10 @@ export const createShaders = (precision: Precision) => { return; } - let globalIdx = outZ * zStride + - outY * yStride + - outX * xStride; + let total_threads_per_slice = workGroups.x * workGroups.y * 16; + let globalIdx = global_id.z * total_threads_per_slice + + global_id.y * (workGroups.x * 4) + + global_id.x; let xy_radius: i32 = i32(kernelSize/2u); let z_radius: i32 = i32(kernelDepth/2u); @@ -1158,11 +1043,6 @@ export const createShaders = (precision: Precision) => { } } - if (count <= 1u) { - outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); - return; - } - let N: f32 = f32(count); let meanX = xSum / N; let meanY = ySum / N; @@ -1171,11 +1051,9 @@ export const createShaders = (precision: Precision) => { let covXY = (xySum / N) - (meanX * meanY); let sigmaX = sqrt(max(0.0, varX)); let sigmaY = sqrt(max(0.0, varY)); - let denominator = sigmaX * sigmaY; - var correlation: f32 = bitcast(0x7fc00000u); - if (denominator > 1e-7) { - correlation = covXY / denominator; - } + let epsilon = 1e-6; + let denominator = sigmaX * sigmaY + epsilon; + let correlation = covXY / denominator; outputData[globalIdx] = ${precision}(correlation); } @@ -1219,9 +1097,10 @@ export const createShaders = (precision: Precision) => { return; } - let globalIdx = outZ * zStride + - outY * yStride + - outX * xStride; + let total_threads_per_slice = workGroups.x * workGroups.y * 16; + let globalIdx = global_id.z * total_threads_per_slice + + global_id.y * (workGroups.x * 4) + + global_id.x; let xy_radius: i32 = i32(kernelSize/2u); let z_radius: i32 = i32(kernelDepth/2u); @@ -1264,10 +1143,6 @@ export const createShaders = (precision: Precision) => { } } - if (count <= 1u) { - outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); - return; - } let N: f32 = f32(count); let meanX = xSum / N; let meanY = ySum / N; @@ -1336,9 +1211,10 @@ export const createShaders = (precision: Precision) => { return; } - let globalIdx = outZ * zStride + - outY * yStride + - outX * xStride; + let total_threads_per_slice = workGroups.x * workGroups.y * 16; + let globalIdx = global_id.z * total_threads_per_slice + + global_id.y * (workGroups.x * 4) + + global_id.x; let xy_radius: i32 = i32(kernelSize/2u); let z_radius: i32 = i32(kernelDepth/2u); @@ -1381,11 +1257,6 @@ export const createShaders = (precision: Precision) => { } - if (count <= 1u) { - outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); - return; - } - let N: f32 = f32(count); let meanX = xSum / N; let meanY = ySum / N; @@ -1414,11 +1285,7 @@ export const createShaders = (precision: Precision) => { } } } - if (denomSum > 1e-7) { - outputData[globalIdx] = ${precision}(numSum/denomSum); - } else { - outputData[globalIdx] = ${precision}(bitcast(0x7fc00000u)); - } + outputData[globalIdx] = ${precision}(numSum/denomSum); } `, // #endregion @@ -1633,4 +1500,4 @@ export const createShaders = (precision: Precision) => { ` } return allShaders; -} +} \ No newline at end of file diff --git a/src/components/computation/webGPU.ts b/src/components/computation/webGPU.ts index 8d24d363..fbbfe21d 100644 --- a/src/components/computation/webGPU.ts +++ b/src/components/computation/webGPU.ts @@ -50,25 +50,6 @@ const InitializeDevice = async () => { Error('need a browser that supports WebGPU'); return {device, hasF16}; } else{ - const originalCreateBuffer = device.createBuffer.bind(device); - device.createBuffer = (descriptor: GPUBufferDescriptor) => { - return originalCreateBuffer({ - ...descriptor, - size: Math.ceil(descriptor.size / 4) * 4 - }); - }; - - const originalWriteBuffer = device.queue.writeBuffer.bind(device.queue); - device.queue.writeBuffer = (buffer: GPUBuffer, bufferOffset: GPUSize64, data: BufferSource | SharedArrayBuffer, dataOffset?: GPUSize64, size?: GPUSize64) => { - let dataToWrite = data as ArrayBufferView; - if (dataToWrite.byteLength % 4 !== 0) { - const paddedLength = Math.ceil(dataToWrite.byteLength / 4) * 4; - const paddedBuffer = new Uint8Array(paddedLength); - paddedBuffer.set(new Uint8Array(dataToWrite.buffer, dataToWrite.byteOffset, dataToWrite.byteLength)); - dataToWrite = paddedBuffer; - } - originalWriteBuffer(buffer, bufferOffset, dataToWrite as any, dataOffset, size); - }; return {device, hasF16} } } @@ -127,7 +108,7 @@ export async function DataReduction(inputArray : ArrayBufferView, dimInfo : {sha const outputBuffer = device.createBuffer({ label: 'Output Buffer', - size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, + size: outputSize * (hasF16 ? 2 : 4), usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); @@ -138,7 +119,7 @@ export async function DataReduction(inputArray : ArrayBufferView, dimInfo : {sha const readBuffer = device.createBuffer({ label:'Output Buffer', - size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, + size: outputSize * (hasF16 ? 2 : 4), usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); // Write Buffers to GPU @@ -168,7 +149,7 @@ export async function DataReduction(inputArray : ArrayBufferView, dimInfo : {sha encoder.copyBufferToBuffer( outputBuffer, 0, readBuffer, 0, - Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4 + outputSize * (hasF16 ? 2 : 4) ); // Submit work to GPU @@ -181,7 +162,7 @@ export async function DataReduction(inputArray : ArrayBufferView, dimInfo : {sha // Clean up readBuffer.unmap(); - return results.slice(0, outputSize); + return results; } @@ -238,7 +219,7 @@ export async function Convolve(inputArray : ArrayBufferView, dimInfo : {shape: const outputBuffer = device.createBuffer({ label: 'Output Buffer', - size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, + size: outputSize * (hasF16 ? 2 : 4), usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); @@ -250,7 +231,7 @@ export async function Convolve(inputArray : ArrayBufferView, dimInfo : {shape: const readBuffer = device.createBuffer({ label:'Read Buffer', - size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, + size: outputSize * (hasF16 ? 2 : 4), usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); @@ -281,7 +262,7 @@ export async function Convolve(inputArray : ArrayBufferView, dimInfo : {shape: encoder.copyBufferToBuffer( outputBuffer, 0, readBuffer, 0, - Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4 + outputSize * (hasF16 ? 2 : 4) ); // Submit work to GPU @@ -294,7 +275,7 @@ export async function Convolve(inputArray : ArrayBufferView, dimInfo : {shape: // Clean up readBuffer.unmap(); - return results.slice(0, outputSize); + return results; } export async function Multivariate2D(firstArray: ArrayBufferView, secondArray: ArrayBufferView, dimInfo : {shape: number[], strides: number[]}, reduceDim: number, operation:string){ @@ -357,7 +338,7 @@ export async function Multivariate2D(firstArray: ArrayBufferView, secondArray: A const outputBuffer = device.createBuffer({ label: 'Output Buffer', - size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, + size: outputSize * (hasF16 ? 2 : 4), usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); @@ -368,7 +349,7 @@ export async function Multivariate2D(firstArray: ArrayBufferView, secondArray: A const readBuffer = device.createBuffer({ label:'Output Buffer', - size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, + size: outputSize * (hasF16 ? 2 : 4), usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); @@ -402,7 +383,7 @@ export async function Multivariate2D(firstArray: ArrayBufferView, secondArray: A encoder.copyBufferToBuffer( outputBuffer, 0, readBuffer, 0, - Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4 + outputSize * (hasF16 ? 2 : 4) ); // Submit work to GPU @@ -415,7 +396,7 @@ export async function Multivariate2D(firstArray: ArrayBufferView, secondArray: A // Clean up readBuffer.unmap(); - return results.slice(0, outputSize); + return results; } export async function Multivariate3D(firstArray: ArrayBufferView, secondArray: ArrayBufferView, dimInfo : {shape: number[], strides: number[]}, kernel: {kernelSize: number, kernelDepth: number}, operation: string){ @@ -479,7 +460,7 @@ export async function Multivariate3D(firstArray: ArrayBufferView, secondArray: A const outputBuffer = device.createBuffer({ label: 'Output Buffer', - size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, + size: outputSize * (hasF16 ? 2 : 4), usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); @@ -490,7 +471,7 @@ export async function Multivariate3D(firstArray: ArrayBufferView, secondArray: A const readBuffer = device.createBuffer({ label:'Read Buffer', - size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, + size: outputSize * (hasF16 ? 2 : 4), usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); @@ -524,7 +505,7 @@ export async function Multivariate3D(firstArray: ArrayBufferView, secondArray: A encoder.copyBufferToBuffer( outputBuffer, 0, readBuffer, 0, - Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4 + outputSize * (hasF16 ? 2 : 4) ); // Submit work to GPU @@ -537,7 +518,7 @@ export async function Multivariate3D(firstArray: ArrayBufferView, secondArray: A // Clean up readBuffer.unmap(); - return results.slice(0, outputSize); + return results; } export async function CUMSUM3D(inputArray : ArrayBufferView, dimInfo : {shape: number[], strides: number[]}, reduceDim: number, reverse: number){ @@ -648,7 +629,7 @@ export async function CUMSUM3D(inputArray : ArrayBufferView, dimInfo : {shape: // Clean up readBuffer.unmap(); - return results.slice(0, outputSize); + return results; } export async function Convolve2D(inputArray : ArrayBufferView, dimInfo : {shape: number[], strides: number[]}, operation: string, kernelSize: number){ @@ -698,7 +679,7 @@ export async function Convolve2D(inputArray : ArrayBufferView, dimInfo : {shape const outputBuffer = device.createBuffer({ label: 'Output Buffer', - size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, + size: outputSize * (hasF16 ? 2 : 4), usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); @@ -710,7 +691,7 @@ export async function Convolve2D(inputArray : ArrayBufferView, dimInfo : {shape const readBuffer = device.createBuffer({ label:'Read Buffer', - size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, + size: outputSize * (hasF16 ? 2 : 4), usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); @@ -742,7 +723,7 @@ export async function Convolve2D(inputArray : ArrayBufferView, dimInfo : {shape encoder.copyBufferToBuffer( outputBuffer, 0, readBuffer, 0, - Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4 + outputSize * (hasF16 ? 2 : 4) ); // Submit work to GPU @@ -755,7 +736,7 @@ export async function Convolve2D(inputArray : ArrayBufferView, dimInfo : {shape // Clean up readBuffer.unmap(); - return results.slice(0, outputSize); + return results; } export async function CustomShader(inputArray : ArrayBufferView, dimInfo : {dataShape: number[], outputShape: number[], strides: number[]}, kernel: {kernelSize: number, kernelDepth: number}, reduceDim: number, shaderCode: string){ @@ -816,7 +797,7 @@ export async function CustomShader(inputArray : ArrayBufferView, dimInfo : {dat const outputBuffer = device.createBuffer({ label: 'Output Buffer', - size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, + size: outputSize * (hasF16 ? 2 : 4), usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, }); @@ -828,7 +809,7 @@ export async function CustomShader(inputArray : ArrayBufferView, dimInfo : {dat const readBuffer = device.createBuffer({ label:'Read Buffer', - size: Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4, + size: outputSize * (hasF16 ? 2 : 4), usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, }); @@ -862,7 +843,7 @@ export async function CustomShader(inputArray : ArrayBufferView, dimInfo : {dat encoder.copyBufferToBuffer( outputBuffer, 0, readBuffer, 0, - Math.ceil(outputSize * (hasF16 ? 2 : 4) / 4) * 4 + outputSize * (hasF16 ? 2 : 4) ); // Submit work to GPU @@ -875,5 +856,5 @@ export async function CustomShader(inputArray : ArrayBufferView, dimInfo : {dat // Clean up readBuffer.unmap(); - return results.slice(0, outputSize); + return results; } \ No newline at end of file From ef1617aea06ab9c191dda7d13e15ef5613677813 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Thu, 9 Jul 2026 16:11:15 +0200 Subject: [PATCH 47/58] cached --- .../ui/MainPanel/MetaDimSelector.tsx | 142 ++++++++++++++---- 1 file changed, 116 insertions(+), 26 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index 3c9bc1d6..5c64f0e9 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -127,6 +127,7 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, 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)) @@ -137,14 +138,7 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, setDimUnits(dimUnits); }, [dimArrays, dimNames, dimUnits, setDimArrays, setDimNames, setDimUnits]); - useEffect(()=>{ - setCompress(false) - if (cache.has(`${initStore}_${meta.name}`)){ - setCached(true); - } else { - setCached(false); - } - },[meta, cache, initStore, setCompress]) + const availableDims: DimOption[] = useMemo( () => @@ -195,6 +189,93 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, 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'); @@ -487,31 +568,40 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, setCompress(e)}/>
- {/* Size info badge */} -
- Raw: {formatBytes(currentSize)} - | - Stored: {compress ? "<" : ""}{formatBytes(cachedSize)} -
- - {/* Plot Button & Info */} -
- {tooBig && -
- - Too many textures ({texCount}/14). Won't fit. - -
- } + {/* Plot Button */} +
{!tooBig && } - {cached && Already cached} +
+
+ + {/* 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"} + + }
From 8e681df2bb3c1fae68249772130b06e5dd0aa50b Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Thu, 9 Jul 2026 16:47:01 +0200 Subject: [PATCH 48/58] lock sliders --- .../ui/MainPanel/MetaDimSelector.tsx | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index 5c64f0e9..9dd2098d 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -117,7 +117,8 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, const { maxSize, cache, setMaxSize } = useCacheStore(useShallow(state => ({ maxSize: state.maxSize, cache: state.cache, setMaxSize: state.setMaxSize }))) const [cacheSize, setCacheSize] = useState(maxSize) - const { setZSlice, setYSlice, setXSlice, ReFetch, compress, setCompress, coarsen, setCoarsen, kernelSize, setKernelSize, kernelDepth, setKernelDepth } = useZarrStore(useShallow(state => ({ + 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 @@ -161,10 +162,48 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, const dimsKey = availableDims.map((d) => `${d.name}:${d.size}`).join('|'); - const makeInitialCollapsedSels = (dims: DimOption[]): Record => - Object.fromEntries(dims.map((d) => [d.name, { ...defaultSelection(d.size), mode: 'scalar' as const }])); + 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); From 69c6c2159a5322de3c407b33f8771dab672ee377 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 10 Jul 2026 11:54:35 +0200 Subject: [PATCH 49/58] fix +1 parsing --- src/components/ui/MainPanel/MetaDimSelector.tsx | 6 ++---- src/utils/HelperFuncs.ts | 7 +++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index 9dd2098d..e1906a41 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -489,8 +489,7 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, } const start = parseInt(sel.start) || 0; let stop = parseInt(sel.stop); - stop = isNaN(stop) ? defaultLast : stop + 1; - return [start, stop]; + return [start, isNaN(stop) ? null : stop + 1]; }; setZSlice(getSliceArray(rowZ, dataShape ? dataShape[getOrigIdx(rowZ?.dimName || '')] : 0)); @@ -503,8 +502,7 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, if (row.sel.mode === 'scalar') return parseInt(row.sel.scalar) || 0; const start = parseInt(row.sel.start) || 0; let stop = parseInt(row.sel.stop); - stop = isNaN(stop) ? dim.size : stop + 1; - return [start, stop]; + return [start, isNaN(stop) ? null : stop + 1]; } const colSel = collapsedSels[dim.name]; if (colSel && colSel.mode === 'scalar') return parseInt(colSel.scalar) || 0; diff --git a/src/utils/HelperFuncs.ts b/src/utils/HelperFuncs.ts index c2fe085d..63b251af 100644 --- a/src/utils/HelperFuncs.ts +++ b/src/utils/HelperFuncs.ts @@ -218,9 +218,12 @@ export function ParseExtent(dimUnits: string[], dimArrays: any[][]){ const yArray = dimArrays[yIdx] || []; const minLat = Number(yArray[ySlice[0]]); - const maxLat = Number(yArray[ySlice[1] ?? yArray.length-1]); + const maxLatIdx = ySlice[1] !== null ? ySlice[1] - 1 : yArray.length - 1; + const maxLat = Number(yArray[maxLatIdx]); + let minLon = Number(xArray[xSlice[0]]); - let maxLon = Number(xArray[xSlice[1] ?? xArray.length-1]); + const maxLonIdx = xSlice[1] !== null ? xSlice[1] - 1 : xArray.length - 1; + let maxLon = Number(xArray[maxLonIdx]); if (maxLon > 180){ maxLon -= 180 From d1b6e2c7f9b9aeef48bc6afb0cfde0f49e93c8a8 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 10 Jul 2026 12:38:55 +0200 Subject: [PATCH 50/58] fixes nan counts --- src/components/computation/WGSLShaders.ts | 33 +++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/components/computation/WGSLShaders.ts b/src/components/computation/WGSLShaders.ts index 8711383e..b043b801 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,7 +190,8 @@ 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); } `, @@ -272,6 +277,7 @@ export const createShaders = (precision: Precision) => { 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 +285,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 +296,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 +307,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 +355,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); } @@ -822,7 +832,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 ++; } } @@ -1029,9 +1041,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; @@ -1132,9 +1142,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 ++; @@ -1160,11 +1168,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 ++; } } } From fc84e94a3a1997d011d476f2212b9fe64a80104e Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 10 Jul 2026 14:05:03 +0200 Subject: [PATCH 51/58] more nan fixes --- src/components/computation/WGSLShaders.ts | 136 +++++++++++++++++++--- 1 file changed, 119 insertions(+), 17 deletions(-) diff --git a/src/components/computation/WGSLShaders.ts b/src/components/computation/WGSLShaders.ts index b043b801..d9b78f77 100644 --- a/src/components/computation/WGSLShaders.ts +++ b/src/components/computation/WGSLShaders.ts @@ -198,6 +198,7 @@ export const createShaders = (precision: Precision) => { MinReduction: /* wgsl */` ${ReductionBoilerPlate} var min: f32 = 1e12; + var nanCount: u32 = 0u; // Iterate along the dimension we're averaging if (reduceDim == 0u) { // Average along Z @@ -205,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; } @@ -214,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; } @@ -223,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; } @@ -230,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); + } } `, @@ -238,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 @@ -245,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; } @@ -254,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; } @@ -263,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; } @@ -270,7 +283,12 @@ 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); + } } `, @@ -425,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; @@ -457,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); } @@ -465,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); } @@ -473,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); } @@ -567,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; @@ -684,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; @@ -798,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); @@ -861,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); + } } `, @@ -891,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); + } } `, @@ -1054,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); @@ -1152,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; @@ -1263,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; @@ -1339,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); + } } `, @@ -1364,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); + } } `, @@ -1435,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) @@ -1472,7 +1568,9 @@ 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; } } else if (reduceDim == 1u) { // CUMSUM along Y @@ -1485,7 +1583,9 @@ 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; } } else { // CUMSUM along X if (reverse == u32(1)){ @@ -1497,7 +1597,9 @@ 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; } } outputData[baseIdx] = accum; From 54a2eec4b81e175f60bb437c083946f111426b46 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 10 Jul 2026 14:29:07 +0200 Subject: [PATCH 52/58] fixes nan cumsum --- src/components/computation/WGSLShaders.ts | 12 +++++++++++- src/components/computation/webGPU.ts | 8 ++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/computation/WGSLShaders.ts b/src/components/computation/WGSLShaders.ts index d9b78f77..6adfb034 100644 --- a/src/components/computation/WGSLShaders.ts +++ b/src/components/computation/WGSLShaders.ts @@ -1556,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 @@ -1571,6 +1572,7 @@ export const createShaders = (precision: Precision) => { let val = f32(inputData[idx]); if (isNaN(val)) { continue; } accum += val; + validCount++; } } else if (reduceDim == 1u) { // CUMSUM along Y @@ -1586,6 +1588,7 @@ export const createShaders = (precision: Precision) => { let val = f32(inputData[idx]); if (isNaN(val)) { continue; } accum += val; + validCount++; } } else { // CUMSUM along X if (reverse == u32(1)){ @@ -1600,9 +1603,16 @@ export const createShaders = (precision: Precision) => { 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); + } } ` } diff --git a/src/components/computation/webGPU.ts b/src/components/computation/webGPU.ts index fbbfe21d..aa83cd61 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(); From 3c624450bce44ecc2d70fb0421a75b86bec446e0 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 10 Jul 2026 14:46:31 +0200 Subject: [PATCH 53/58] asChild --- src/components/ui/MainPanel/AnalysisOptions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/MainPanel/AnalysisOptions.tsx b/src/components/ui/MainPanel/AnalysisOptions.tsx index fd6f1347..894e2ffa 100644 --- a/src/components/ui/MainPanel/AnalysisOptions.tsx +++ b/src/components/ui/MainPanel/AnalysisOptions.tsx @@ -384,7 +384,7 @@ const AnalysisOptions = () => {
- + {setReverseDirection(e ? 1 : 0)}}/>
From 61e10a85126d444a8e4abf4ee2875d4099e67bb9 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 10 Jul 2026 17:56:25 +0200 Subject: [PATCH 54/58] test NaN math --- src/__tests__/setup.ts | 5 +- src/__tests__/webGPU-nan.test.ts | 280 +++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/webGPU-nan.test.ts diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts index 1be21787..b3062afd 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 00000000..8922a047 --- /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) + }) + }) + }) +}) From 3a243dfd504f109b295c777480335de425380c57 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 10 Jul 2026 18:57:37 +0200 Subject: [PATCH 55/58] export parameters --- src/components/ui/NavBar/ParameterExport.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/ui/NavBar/ParameterExport.tsx b/src/components/ui/NavBar/ParameterExport.tsx index 446206fc..0892227a 100644 --- a/src/components/ui/NavBar/ParameterExport.tsx +++ b/src/components/ui/NavBar/ParameterExport.tsx @@ -71,12 +71,15 @@ const plotValues = [ 'borderWidth', 'cameraPosition', 'disablePointScale', + 'is360Deg', ] const zarrValues = [ 'zSlice', 'ySlice', 'xSlice', + 'ndSlices', + 'axisMapping', 'compress', 'useNC', // This one is more static and so toggling switch doesn't break all other logic 'fetchNC', From 55d526139dda8e8c525a71eb057f4e5d6ffa802e Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 10 Jul 2026 19:35:13 +0200 Subject: [PATCH 56/58] export colormap parameters --- src/GlobalStates/GlobalStore.ts | 8 +++++++ src/components/ui/MainPanel/Colormaps.tsx | 24 +++++++++++--------- src/components/ui/NavBar/ParameterExport.tsx | 2 ++ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/GlobalStates/GlobalStore.ts b/src/GlobalStates/GlobalStore.ts index bdeca52a..d80a5e65 100644 --- a/src/GlobalStates/GlobalStore.ts +++ b/src/GlobalStates/GlobalStore.ts @@ -21,6 +21,8 @@ type StoreState = { shape: THREE.Vector3; valueScales: { maxVal: number; minVal: number }; colormap: THREE.DataTexture; + colormapName: string; + flipColormap: boolean; timeSeries: Record>; strides: number[]; metadata: Record | null; @@ -55,6 +57,8 @@ type StoreState = { 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; @@ -92,6 +96,8 @@ export const useGlobalStore = create((set, get) => ({ 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, @@ -126,6 +132,8 @@ export const useGlobalStore = create((set, get) => ({ 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/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index 1e02d72a..8713d351 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) => ( {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/NavBar/ParameterExport.tsx b/src/components/ui/NavBar/ParameterExport.tsx index 0892227a..046f6c5b 100644 --- a/src/components/ui/NavBar/ParameterExport.tsx +++ b/src/components/ui/NavBar/ParameterExport.tsx @@ -16,6 +16,8 @@ const globalValues = [ 'initStore', 'storeFromURL', 'variable', + 'colormapName', + 'flipColormap', ] const plotValues = [ From f546e692a42c53b1aa165bd847e8bf99291b52dc Mon Sep 17 00:00:00 2001 From: Jeran Date: Sat, 11 Jul 2026 11:36:49 +0200 Subject: [PATCH 57/58] Textures back --- src/components/plots/DataCube.tsx | 2 +- src/components/plots/FlatBlocks.tsx | 4 ++-- src/components/plots/FlatMap.tsx | 2 +- src/components/plots/Sphere.tsx | 2 +- src/components/plots/SphereBlocks.tsx | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/plots/DataCube.tsx b/src/components/plots/DataCube.tsx index 88983690..bc183e2f 100644 --- a/src/components/plots/DataCube.tsx +++ b/src/components/plots/DataCube.tsx @@ -47,7 +47,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] ?? volTexture?.[0])}, + map: { value: volTexture}, maskTexture: { value: maskTexture }, maskValue: {value: maskValue }, textureDepths: {value: new THREE.Vector3(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0])}, diff --git a/src/components/plots/FlatBlocks.tsx b/src/components/plots/FlatBlocks.tsx index 0a29834d..2bddce35 100644 --- a/src/components/plots/FlatBlocks.tsx +++ b/src/components/plots/FlatBlocks.tsx @@ -81,7 +81,7 @@ const FlatBlocks = ({textures} : {textures: THREE.Data3DTexture[] | THREE.DataTe const shader = new THREE.ShaderMaterial({ glslVersion: THREE.GLSL3, uniforms: { - map: { value: Array.from({ length: 14 }, (_, idx) => textures?.[idx] ?? textures?.[0]) }, + map: { value: textures }, maskTexture: {value: maskTexture}, maskValue: {value: maskValue}, threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, @@ -111,7 +111,7 @@ const FlatBlocks = ({textures} : {textures: THREE.Data3DTexture[] | THREE.DataTe useEffect(()=>{ if (shaderMaterial){ const uniforms = shaderMaterial.uniforms; - uniforms.map.value = Array.from({ length: 14 }, (_, idx) => textures?.[idx] ?? textures?.[0]); + uniforms.map.value = textures; uniforms.animateProg.value = animProg uniforms.displaceZero.value = -valueScales.minVal/(valueScales.maxVal-valueScales.minVal) uniforms.displacement.value = displacement diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index 1c76f8b3..1c37fcf3 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -187,7 +187,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] ?? textures?.[0])}, + map : {value: textures}, maskTexture: {value: maskTexture}, maskValue: {value: maskValue}, threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, diff --git a/src/components/plots/Sphere.tsx b/src/components/plots/Sphere.tsx index a24595f4..87f00e49 100644 --- a/src/components/plots/Sphere.tsx +++ b/src/components/plots/Sphere.tsx @@ -83,7 +83,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] ?? textures?.[0]) }, + 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 67d453c9..d32641ff 100644 --- a/src/components/plots/SphereBlocks.tsx +++ b/src/components/plots/SphereBlocks.tsx @@ -79,7 +79,7 @@ const SphereBlocks = ({textures} : {textures: THREE.Data3DTexture[] | THREE.Data const shader = new THREE.ShaderMaterial({ glslVersion: THREE.GLSL3, uniforms: { - map: { value: Array.from({ length: 14 }, (_, idx) => textures?.[idx] ?? textures?.[0]) }, + map: { value: textures }, maskTexture: {value: maskTexture}, maskValue: {value: maskValue}, threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, @@ -108,7 +108,7 @@ const SphereBlocks = ({textures} : {textures: THREE.Data3DTexture[] | THREE.Data useEffect(()=>{ if (shaderMaterial){ const uniforms = shaderMaterial.uniforms; - uniforms.map.value = Array.from({ length: 14 }, (_, idx) => textures?.[idx] ?? textures?.[0]); + uniforms.map.value = textures; uniforms.animateProg.value = animProg uniforms.displaceZero.value = -valueScales.minVal/(valueScales.maxVal-valueScales.minVal) uniforms.displacement.value = sphereDisplacement From fb326ab06a971621ad35258473d85dfa6590b079 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Mon, 13 Jul 2026 18:21:21 +0200 Subject: [PATCH 58/58] usePaddedTextures --- src/components/plots/DataCube.tsx | 4 +++- src/components/plots/FlatBlocks.tsx | 4 +++- src/components/plots/FlatMap.tsx | 4 +++- src/components/plots/Sphere.tsx | 4 +++- src/components/plots/SphereBlocks.tsx | 4 +++- src/hooks/usePaddedTextures.ts | 10 ++++++++++ 6 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 src/hooks/usePaddedTextures.ts diff --git a/src/components/plots/DataCube.tsx b/src/components/plots/DataCube.tsx index bc183e2f..035f7122 100644 --- a/src/components/plots/DataCube.tsx +++ b/src/components/plots/DataCube.tsx @@ -9,12 +9,14 @@ import { deg2rad } from '@/utils/HelperFuncs'; import { useCoordBounds } from '@/hooks/useCoordBounds'; import { UVCube } from '@/components/plots' import { ColumnMeshes } from './TransectMeshes'; +import { usePaddedTextures } from '@/hooks/usePaddedTextures'; interface DataCubeProps { volTexture: THREE.Data3DTexture[] | THREE.DataTexture[] | null, } -export const DataCube = ({ volTexture }: DataCubeProps ) => { +export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { + const volTexture = usePaddedTextures(propVolTexture); const {shape, colormap, flipY, textureArrayDepths} = useGlobalStore(useShallow(state=>({ shape:state.shape, colormap:state.colormap, diff --git a/src/components/plots/FlatBlocks.tsx b/src/components/plots/FlatBlocks.tsx index 2bddce35..83696fd0 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 1c37fcf3..0894d92e 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, diff --git a/src/components/plots/Sphere.tsx b/src/components/plots/Sphere.tsx index 87f00e49..f8d15b01 100644 --- a/src/components/plots/Sphere.tsx +++ b/src/components/plots/Sphere.tsx @@ -10,6 +10,7 @@ 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); @@ -18,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, diff --git a/src/components/plots/SphereBlocks.tsx b/src/components/plots/SphereBlocks.tsx index d32641ff..ef9dad6c 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/hooks/usePaddedTextures.ts b/src/hooks/usePaddedTextures.ts new file mode 100644 index 00000000..f410d725 --- /dev/null +++ b/src/hooks/usePaddedTextures.ts @@ -0,0 +1,10 @@ +import { useMemo } from 'react'; +import * as THREE from 'three'; + +type TextureArray = THREE.Data3DTexture[] | THREE.DataTexture[] | null | undefined; + +export const usePaddedTextures = (textures: TextureArray, length: number = 14) => { + return useMemo(() => { + return Array.from({ length }, (_, idx) => textures?.[idx] ?? textures?.[0]); + }, [textures, length]); +};