From 2a27544a5e33fd78764c41376069a491e31cf09e Mon Sep 17 00:00:00 2001 From: Jeran Date: Tue, 4 Aug 2026 18:00:53 +0200 Subject: [PATCH 1/5] Metadim Transfer --- .../ui/MainPanel/MetaDimSelector.tsx | 1346 +++++++++-------- 1 file changed, 744 insertions(+), 602 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index effef3907..f6f0ba319 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -1,6 +1,7 @@ "use client"; -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useMemo, useState, useEffect, createContext, useContext } from 'react'; +import { createStore, useStore } from 'zustand'; import DimSlicer, { Axis, defaultSelection, DimOption, SliceSelectionState } from '@/components/ui/DimSlicer'; import { defaultAttributes, renderAttributes } from "@/components/ui/MetaData"; import { Button } from '@/components/ui/button-enhanced'; @@ -21,33 +22,31 @@ import { SliderThumbs } from "@/components/ui/Widgets/SliderThumbs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { BsFillQuestionCircleFill } from "react-icons/bs"; +// Maximum allowed active dimensions shown in the slicer panel const MAX_ACTIVE_DIMS = 3; -const formatArray = (value: string | number[]): string => { - if (typeof value === 'string') return value; - return Array.isArray(value) ? value.join(', ') : String(value); -}; - +// Helper to format byte counts into human-readable strings (KB, MB, GB) 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] -} + 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]; +}; +// Metadata payload shape for dimension arrays, names, and units interface DimInfo { dimArrays: ArrayLike[]; dimNames: string[]; dimUnits: (string | null)[]; } +// Props accepted by MetaDimSelector type Props = { meta: { name?: string; shape?: number[]; chunks?: number[]; - chunkSize?: number; totalSize?: number; dtype?: string; long_name?: string; @@ -56,10 +55,9 @@ type Props = { }; metadata?: Record; onApply?: (sels: SliceSelectionState[], axes: Axis[], dimNames: string[]) => void; - setShowMeta?: React.Dispatch>; - setOpenVariables?: (open: boolean) => void; }; +// Color mapping badges for standard coordinate axes const AXIS_COLOR: Record = { x: 'text-pink-500', y: 'text-green-500', @@ -67,284 +65,163 @@ const AXIS_COLOR: Record = { c: 'text-yellow-500', }; +// Internal active slicer row state storing name and selection interface SlicerRow { - id: number; dimName: string; sel: SliceSelectionState; - axis: Axis; } -let _nextId = 0; -const nextId = () => ++_nextId; - +// Parses original dimension index from formatted name string (e.g. "lat::1" -> 1) const getOrigIdx = (dimName: string) => { const parts = dimName.split('::'); return parseInt(parts[parts.length - 1]); }; -export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, setOpenVariables }: Props) { - const isMobile = useIsMobile(); - const [mounted, setMounted] = useState(false); - useEffect(() => setMounted(true), []); - const dimArrays = useMemo( - () => (meta?.dimInfo?.dimArrays ?? []).map((a) => Array.from(a)), - [meta?.dimInfo?.dimArrays] - ); - const dimUnits = useMemo( - () => (meta?.dimInfo?.dimUnits ?? []).map((u) => u ?? ''), - [meta?.dimInfo?.dimUnits] - ); - const dimNames = useMemo( - () => meta?.dimInfo?.dimNames ?? [], - [meta?.dimInfo?.dimNames] - ); - const dataShape = meta?.shape || []; - const chunkShape = meta?.chunks || []; - - - const { setDimArrays, setDimNames, setDimUnits, initStore, setVariable, setTextureArrayDepths, variable, idx4D } = useGlobalStore( - useShallow((state) => ({ - setDimArrays: state.setDimArrays, - setDimNames: state.setDimNames, - setDimUnits: state.setDimUnits, - initStore: state.initStore, - setVariable: state.setVariable, - setTextureArrayDepths: state.setTextureArrayDepths, - variable: state.variable, - idx4D: state.idx4D, - })), - ); - - const { maxSize, cache, setMaxSize } = useCacheStore(useShallow(state => ({ maxSize: state.maxSize, cache: state.cache, setMaxSize: state.setMaxSize }))) - const [cacheSize, setCacheSize] = useState(maxSize) - - const { ndSlices, axisMapping, setZSlice, setYSlice, setXSlice, ReFetch, compress, setCompress, coarsen, setCoarsen, kernelSize, setKernelSize, kernelDepth, setKernelDepth } = useZarrStore(useShallow(state => ({ - ndSlices: state.ndSlices, axisMapping: state.axisMapping, - setZSlice: state.setZSlice, setYSlice: state.setYSlice, setXSlice: state.setXSlice, - ReFetch: state.ReFetch, compress: state.compress, setCompress: state.setCompress, - coarsen: state.coarsen, setCoarsen: state.setCoarsen, kernelSize: state.kernelSize, setKernelSize: state.setKernelSize, kernelDepth: state.kernelDepth, setKernelDepth: state.setKernelDepth - }))) - - const { maxTextureSize, max3DTextureSize } = usePlotStore(useShallow(state => ({ maxTextureSize: state.maxTextureSize, max3DTextureSize: state.max3DTextureSize }))) - - const [tooBig, setTooBig] = useState(false) - const [cached, setCached] = useState(false) - const [cachedChunks, setCachedChunks] = useState(null) - const [texCount, setTexCount] = useState(0) - const [displaySpat, setDisplaySpat] = useState(String(kernelSize)) - const [displayDepth, setDisplayDepth] = useState(String(kernelDepth)) - - useEffect(() => { - setDimArrays(dimArrays); - setDimNames(dimNames); - setDimUnits(dimUnits); - }, [dimArrays, dimNames, dimUnits, setDimArrays, setDimNames, setDimUnits]); - - +// Positionally derives spatial axis name ('z', 'y', 'x') based on active row index +const getActiveAxis = (index: number, totalRows: number): Axis => { + const axes: Axis[] = ['z', 'y', 'x']; + return axes[axes.length - totalRows + index] ?? 'x'; +}; - const availableDims: DimOption[] = useMemo( - () => - dimArrays.map((values, idx) => { - const baseName = dimNames[idx] ?? `dim${idx}`; - // Always include idx to guarantee uniqueness during the "Default" fallback phase - const name = `${baseName}::${idx}`; - const label = baseName; - return { - name, - label, - size: values.length, - values, - formatValue: (v: number): string => - String(parseLoc(values[v] ?? v, dimUnits[idx] || undefined)), - }; - }), - [dimArrays, dimNames, dimUnits], - ); +// Extracted numeric slice range bounds for slicing calculation +interface ParsedSliceRange { + first: number; + last: number; + steps: number; +} - const dimsKey = availableDims.map((d) => `${d.name}:${d.size}`).join('|'); +// Helper parsing selection state into numerical start, stop, and step counts +const parseSliceRange = (sel: SliceSelectionState | undefined, defaultSize: number): ParsedSliceRange => { + if (!sel) return { first: 0, last: defaultSize, steps: Math.max(1, defaultSize) }; + 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 = defaultSize; + else stop = Math.min(stop + 1, defaultSize > 0 ? defaultSize : stop + 1); + return { first: start, last: stop, steps: Math.max(1, stop - start) }; +}; - 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]; - })); - }; +// --- SCOPED STATE ISOLATION STORE --- +interface SelectorStoreState { + rows: SlicerRow[]; + collapsedSels: Record; + updateDimName: (oldDimName: string, newDimName: string, availableDims: DimOption[], dataShape: number[]) => void; + updateSel: (dimName: string, sel: SliceSelectionState) => void; + updateCollapsedSel: (dimName: string, sel: SliceSelectionState) => void; + addRow: (availableDims: DimOption[], dataShape: number[]) => void; + removeLastRow: () => void; +} - 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]; - const dimShape = dataShape[mappedIdx] ?? dim.size; - let sel = defaultSelection(dimShape); - 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 - }); +type SelectorStore = ReturnType; + +const createMetaSelectorStore = (initialRows: SlicerRow[], initialCollapsed: Record) => + createStore((set) => ({ + rows: initialRows, + collapsedSels: initialCollapsed, + updateDimName: (oldDimName, newDimName, availableDims, dataShape) => { + if (oldDimName === newDimName) return; + set((state) => { + const existingIdx = state.rows.findIndex((r) => r.dimName === newDimName); + const newDimIndex = availableDims.findIndex((d) => d.name === newDimName); + const newDim = availableDims[newDimIndex]; + const newDimShape = dataShape[newDimIndex] ?? newDim?.size ?? 0; + + if (existingIdx >= 0) { + const oldDimIndex = availableDims.findIndex((d) => d.name === oldDimName); + const oldDim = availableDims[oldDimIndex]; + const oldDimShape = dataShape[oldDimIndex] ?? oldDim?.size ?? 0; + + return { + rows: state.rows.map((r) => { + if (r.dimName === oldDimName) return { dimName: newDimName, sel: defaultSelection(newDimShape) }; + if (r.dimName === newDimName) return { dimName: oldDimName, sel: defaultSelection(oldDimShape) }; + return r; + }), + }; } - } - - if (initRows.length > 0) return initRows; - } - const activeDims = dims.slice(-Math.min(MAX_ACTIVE_DIMS, dims.length)); - const defaultAxes: Axis[] = ['z', 'y', 'x']; - const axes = defaultAxes.slice(-activeDims.length); - return activeDims.map((d, i) => { - const dimShape = dataShape[availableDims.indexOf(d)] ?? d.size; - const sel = defaultSelection(dimShape); return { - id: nextId(), - dimName: d.name, - sel, - axis: axes[i], + rows: state.rows.map((r) => (r.dimName === oldDimName ? { dimName: newDimName, sel: defaultSelection(newDimShape) } : r)), }; - }); - }; - - const [rows, setRows] = useState(() => makeInitialRows(availableDims)); - const [collapsedSels, setCollapsedSels] = useState>( - () => makeInitialCollapsedSels(availableDims), - ); - const [lastKey, setLastKey] = useState(dimsKey); - const [collapsedOpen, setCollapsedOpen] = useState(false); - - if (dimsKey !== lastKey) { - setLastKey(dimsKey); - setRows(makeInitialRows(availableDims)); - setCollapsedSels(makeInitialCollapsedSels(availableDims)); - } - - useEffect(()=>{ - setCompress(false) - setCachedChunks(null) - setCached(false) - - if (!meta || !meta.chunks || !meta.shape) { - if (meta && cache.has(`${initStore}_${meta.name}`)) { - setCached(true); - } - return; - } - - const ndSlicesTemp = availableDims.map((d) => { - const activeRow = rows.find((r) => r.dimName === d.name); - if (activeRow) { - return [parseInt(activeRow.sel.start) || 0, parseInt(activeRow.sel.stop) || d.size] as [number, number]; - } - const colSel = collapsedSels[d.name]; - if (colSel && colSel.mode === 'scalar') return parseInt(colSel.scalar) || 0; - return 0; - }); - - const scalarIndices = ndSlicesTemp.filter(s => typeof s === "number").join("_"); - let cacheBase = scalarIndices !== "" ? `${initStore}_${meta.name}_${scalarIndices}` : `${initStore}_${meta.name}`; - if (meta.shape && meta.shape.length >= 4 && idx4D !== undefined && idx4D !== null) { - cacheBase = `${cacheBase}_time${idx4D}`; - } - - 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; + }); + }, + updateSel: (dimName, sel) => { + set((state) => ({ + rows: state.rows.map((r) => (r.dimName === dimName ? { ...r, sel: { ...sel, mode: 'slice' } } : r)), + })); + }, + updateCollapsedSel: (dimName, sel) => { + set((state) => ({ + collapsedSels: { ...state.collapsedSels, [dimName]: { ...sel, mode: 'scalar' } }, + })); + }, + addRow: (availableDims, dataShape) => { + set((state) => { + if (state.rows.length >= MAX_ACTIVE_DIMS) return state; + const usedNames = new Set(state.rows.map((r) => r.dimName)); + const dimName = availableDims.find((d) => !usedNames.has(d.name))?.name; + if (!dimName) return state; + const dim = availableDims.find((d) => d.name === dimName)!; + const dimShape = dataShape[availableDims.indexOf(dim)] ?? dim.size; + return { rows: [...state.rows, { dimName, sel: defaultSelection(dimShape) }] }; + }); + }, + removeLastRow: () => { + set((state) => ({ rows: state.rows.slice(0, -1) })); + }, + })); + +const MetaSelectorContext = createContext(null); + +const useMetaSelectorStore = (selector: (state: SelectorStoreState) => T): T => { + const store = useContext(MetaSelectorContext); + if (!store) throw new Error("MetaSelectorContext missing"); + return useStore(store, selector); +}; - 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 = Math.min(stop + 1, defaultLast > 0 ? defaultLast : stop + 1); - return { first: start, last: stop }; - }; +// --- ISOLATED SUB-COMPONENTS (Zero Parent Re-renders) --- + +// Status badges for size, cache, and texture counts +const MetaStatusBadges: React.FC<{ + meta: Props['meta']; + availableDims: DimOption[]; + cacheSize: number; + setCacheSize: React.Dispatch>; +}> = React.memo(({ meta, availableDims, cacheSize, setCacheSize }) => { + const rows = useMetaSelectorStore((s) => s.rows); + const collapsedSels = useMetaSelectorStore((s) => s.collapsedSels); + + const initStore = useGlobalStore((s) => s.initStore); + const idx4D = useGlobalStore((s) => s.idx4D); + const cache = useCacheStore((s) => s.cache); + const maxSize = useCacheStore((s) => s.maxSize); + const compress = useZarrStore((s) => s.compress); + const coarsen = useZarrStore((s) => s.coarsen); + const kernelSize = useZarrStore((s) => s.kernelSize); + const kernelDepth = useZarrStore((s) => s.kernelDepth); + const setTextureArrayDepths = useGlobalStore((s) => s.setTextureArrayDepths); + const maxTextureSize = usePlotStore((s) => s.maxTextureSize); + const max3DTextureSize = usePlotStore((s) => s.max3DTextureSize); - const zSlice = getSliceDims(rowZ, origIdxZ); - const ySlice = getSliceDims(rowY, origIdxY); - const xSlice = getSliceDims(rowX, origIdxX); + const dataShape = meta?.shape || []; + const chunkShape = meta?.chunks || []; - 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) }; + // Compute size data + const sizeData = useMemo(() => { + const getRowByAxis = (axis: Axis) => { + const idx = rows.findIndex((_, i) => getActiveAxis(i, rows.length) === axis); + return idx >= 0 ? rows[idx] : undefined; }; - const zDim = calcDim(zSlice, origIdxZ); - const yDim = calcDim(ySlice, origIdxY); - const xDim = calcDim(xSlice, origIdxX); - - let accum = 0; - let total = 0; - for (let z = zDim.start; z < zDim.end; z++) { - for (let y = yDim.start; y < yDim.end; y++) { - for (let x = xDim.start; x < xDim.end; x++) { - total++; - const chunkID = `z${z}_y${y}_x${x}`; - const cacheName = `${cacheBase}_chunk_${chunkID}`; - if (cache.has(cacheName)) { - accum++; - } - } - } - } - - if (total > 0 && accum > 0) { - setCachedChunks(`${accum}/${total}`); - setCached(true); - } else if (cache.has(`${initStore}_${meta.name}`)) { - setCached(true); - } - }, [meta, cache, initStore, setCompress, rows, collapsedSels, availableDims]) - - const sizeData = useMemo(() => { - const rowZ = rows.find((r) => r.axis === 'z'); - const rowY = rows.find((r) => r.axis === 'y'); - const rowX = rows.find((r) => r.axis === 'x'); + const rowZ = getRowByAxis('z'); + const rowY = getRowByAxis('y'); + const rowX = getRowByAxis('x'); const is2D = dataShape.length === 2 || !rowZ; - const getSliceDims = (row?: SlicerRow, defaultLast = 0) => { - if (!row) return { first: 0, last: defaultLast, steps: defaultLast }; - const sel = row.sel; - if (sel.mode === 'scalar') { - const val = parseInt(sel.scalar) || 0; - return { first: val, last: val + 1, steps: 1 }; - } - const start = parseInt(sel.start) || 0; - let stop = parseInt(sel.stop); - if (isNaN(stop)) stop = defaultLast; - else stop = Math.min(stop + 1, defaultLast > 0 ? defaultLast : stop + 1); - return { first: start, last: stop, steps: Math.max(1, stop - start) }; - }; - const origIdxZ = rowZ ? getOrigIdx(rowZ.dimName) : -1; const origIdxY = rowY ? getOrigIdx(rowY.dimName) : -1; const origIdxX = rowX ? getOrigIdx(rowX.dimName) : -1; @@ -353,38 +230,31 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, 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 z = is2D ? { first: 0, last: 1, steps: 1 } : parseSliceRange(rowZ?.sel, lenZ); + const y = parseSliceRange(rowY?.sel, lenY); + const x = parseSliceRange(rowX?.sel, lenX); const maxSizeLimit = is2D ? maxTextureSize : max3DTextureSize; const texCounts = [z.steps / maxSizeLimit, y.steps / maxSizeLimit, x.steps / maxSizeLimit]; - - const depths = texCounts.some(count => count > 1) - ? texCounts.map(val => Math.ceil(val)) - : [1, 1, 1]; - const thisCount = texCounts.reduce((prod, val) => prod * Math.ceil(val), 1) + const depths = texCounts.some((count) => count > 1) + ? texCounts.map((val) => Math.ceil(val)) + : [1, 1, 1]; + + const thisCount = texCounts.reduce((prod, val) => prod * Math.ceil(val), 1); const getSelSteps = (dimName: string, defaultLast: number) => { - const row = rows.find(r => r.dimName === dimName); - if (row) return getSliceDims(row, defaultLast).steps; - + const row = rows.find((r) => r.dimName === dimName); + if (row) return parseSliceRange(row.sel, defaultLast).steps; + const collSel = collapsedSels[dimName]; - if (collSel) { - if (collSel.mode === 'scalar') return 1; - const start = parseInt(collSel.start) || 0; - let stop = parseInt(collSel.stop); - if (isNaN(stop)) stop = defaultLast; - else stop = Math.min(stop + 1, defaultLast > 0 ? defaultLast : stop + 1); - return Math.max(1, stop - start); - } + if (collSel) return parseSliceRange(collSel, defaultLast).steps; return defaultLast; }; const totalSteps = availableDims.reduce((prod, d, idx) => { - const dimShape = dataShape[idx] ?? d.size; - return prod * getSelSteps(d.name, dimShape); + const dimShape = dataShape[idx] ?? d.size; + return prod * getSelSteps(d.name, dimShape); }, 1); const sizeRatio = totalSteps / (dataShape.reduce((a, b) => a * b, 1) || 1); let calculatedSize = (meta.totalSize || 0) * sizeRatio; @@ -392,96 +262,336 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, if (!is2D) { calculatedSize = calculatedSize / (coarsen ? kernelDepth * Math.pow(kernelSize, 2) : 1); } - + return { size: calculatedSize, thisCount, depths }; }, [meta, rows, collapsedSels, availableDims, dataShape, chunkShape, coarsen, kernelSize, kernelDepth, maxTextureSize, max3DTextureSize]); useEffect(() => { setTextureArrayDepths(sizeData.depths); - setTexCount(sizeData.thisCount); - setTooBig(sizeData.thisCount > 14); - }, [sizeData, setTextureArrayDepths]); + }, [sizeData.depths, setTextureArrayDepths]); const currentSize = sizeData.size; + const texCount = sizeData.thisCount; + const tooBig = texCount > 14; - const cachedSize = useMemo(()=>{ + const cachedSize = useMemo(() => { const thisDtype = (meta?.dtype as string) || ''; - if (thisDtype.includes("32") || thisDtype.includes("f4")){ + if (thisDtype.includes("32") || thisDtype.includes("f4")) { return currentSize / 2; - } else if (thisDtype.includes("64") || thisDtype.includes("f8")){ + } else if (thisDtype.includes("64") || thisDtype.includes("f8")) { return currentSize / 4; - } else if (thisDtype.includes("8") || thisDtype.includes("i1") ){ + } else if (thisDtype.includes("8") || thisDtype.includes("i1")) { return currentSize * 2; } else { return currentSize; } - },[currentSize, meta]) + }, [currentSize, meta]); const smallCache = cachedSize > cacheSize; - const firstUnusedDim = (currentRows: SlicerRow[]): string => { - const usedNames = new Set(currentRows.map((r) => r.dimName)); - return availableDims.find((d) => !usedNames.has(d.name))?.name ?? ''; - }; + const [cached, setCached] = useState(false); + const [cachedChunks, setCachedChunks] = useState(null); - 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'; - }; + useEffect(() => { + let newCached = false; + let newCachedChunks: string | null = null; + + if (meta && meta.chunks && meta.shape) { + const ndSlicesTemp = availableDims.map((d) => { + const activeRow = rows.find((r) => r.dimName === d.name); + if (activeRow) { + const range = parseSliceRange(activeRow.sel, d.size); + return [range.first, range.last] 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("_"); + let cacheBase = scalarIndices !== "" ? `${initStore}_${meta.name}_${scalarIndices}` : `${initStore}_${meta.name}`; + if (meta.shape && meta.shape.length >= 4 && idx4D !== undefined && idx4D !== null) { + cacheBase = `${cacheBase}_time${idx4D}`; + } - 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 dimShape = dataShape[availableDims.indexOf(dim)] ?? dim.size; - const newRows: SlicerRow[] = [...prev, { - id: nextId(), - dimName, - sel: defaultSelection(dimShape), - 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 getRowByAxis = (axis: Axis) => { + const idx = rows.findIndex((_, i) => getActiveAxis(i, rows.length) === axis); + return idx >= 0 ? rows[idx] : undefined; + }; - const removeLastRow = () => - setRows((prev) => { - const newRows = prev.slice(0, -1); - const defaultAxes: Axis[] = ['z', 'y', 'x']; - const axes = defaultAxes.slice(-newRows.length); - return newRows.map((r, i) => ({ ...r, axis: axes[i] })); - }); + const rowZ = getRowByAxis('z'); + const rowY = getRowByAxis('y'); + const rowX = getRowByAxis('x'); - const updateDimName = (id: number, dimName: string) => { - setRows((prev) => - prev.map((r) => { - if (r.id !== id) return r; - const dimIndex = availableDims.findIndex((d) => d.name === dimName); - const dim = availableDims[dimIndex]; - const dimShape = dataShape[dimIndex] ?? dim?.size ?? 0; - return { ...r, dimName, sel: defaultSelection(dimShape) }; - }), - ); - }; + const origIdxZ = rowZ ? getOrigIdx(rowZ.dimName) : -1; + const origIdxY = rowY ? getOrigIdx(rowY.dimName) : -1; + const origIdxX = rowX ? getOrigIdx(rowX.dimName) : -1; + + const zSlice = parseSliceRange(rowZ?.sel, origIdxZ >= 0 ? meta.shape?.[origIdxZ] ?? 1 : 1); + const ySlice = parseSliceRange(rowY?.sel, origIdxY >= 0 ? meta.shape?.[origIdxY] ?? 1 : 1); + const xSlice = parseSliceRange(rowX?.sel, origIdxX >= 0 ? meta.shape?.[origIdxX] ?? 1 : 1); + + 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) { + newCachedChunks = `${accum}/${total}`; + newCached = true; + } else if (cache.has(`${initStore}_${meta.name}`)) { + newCached = true; + } + } else if (meta && cache.has(`${initStore}_${meta.name}`)) { + newCached = true; + } + + setCached((prev) => (prev !== newCached ? newCached : prev)); + setCachedChunks((prev) => (prev !== newCachedChunks ? newCachedChunks : prev)); + }, [meta, cache, initStore, rows, collapsedSels, availableDims]); + + return ( +
+ {/* 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"} + + )} +
- const updateSel = (id: number, sel: SliceSelectionState) => - setRows((prev) => prev.map((r) => (r.id === id ? { ...r, sel: { ...sel, mode: 'slice' } } : r))); + {/* 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 + + +
+
+
+
+
+ )} +
+ ); +}); - const updateCollapsedSel = (dimName: string, sel: SliceSelectionState) => - setCollapsedSels((prev) => ({ ...prev, [dimName]: { ...sel, mode: 'scalar' } })); +// Dimension Table isolated sub-component +const MetaDimTable: React.FC<{ + availableDims: DimOption[]; + dataShape: number[]; + chunkShape: number[]; +}> = React.memo(({ availableDims, dataShape, chunkShape }) => { + const rows = useMetaSelectorStore((s) => s.rows); + const collapsedSels = useMetaSelectorStore((s) => s.collapsedSels); + return ( +
+
+ + + + + + + + + + + + {availableDims.map((dim, originalIndex) => { + const activeIndex = rows.findIndex((r) => r.dimName === dim.name); + const activeRow = activeIndex >= 0 ? rows[activeIndex] : undefined; + 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 = activeIndex >= 0 ? getActiveAxis(activeIndex, rows.length) : 'c'; + const dataSize = dataShape[originalIndex] ?? '?'; + const chunkSize = chunkShape[originalIndex] ?? '?'; + + return ( + + + + + + + + ); + })} + +
DimAxisSelectionData ShapeChunk Shape
{dim.name}{axis.toUpperCase()}{range}{dataSize}{chunkSize}
+
+
+ ); +}); + +// Active Slicers list isolated sub-component +const MetaActiveSlicers: React.FC<{ + availableDims: DimOption[]; + dataShape: number[]; +}> = React.memo(({ availableDims, dataShape }) => { + const rows = useMetaSelectorStore((s) => s.rows); + const updateDimNameAction = useMetaSelectorStore((s) => s.updateDimName); + const updateSelAction = useMetaSelectorStore((s) => s.updateSel); + const removeLastRow = useMetaSelectorStore((s) => s.removeLastRow); + return ( +
+ {rows.map((row, i) => { + const dim = availableDims.find((d) => d.name === row.dimName); + const isLast = i === rows.length - 1; + const axis = getActiveAxis(i, rows.length); + return ( + updateDimNameAction(row.dimName, name, availableDims, dataShape)} + onRemove={isLast && rows.length > 1 ? removeLastRow : undefined} + dimSize={dim?.size ?? 0} + selection={row.sel} + axis={axis} + onChange={(sel) => updateSelAction(row.dimName, sel)} + values={dim?.values} + formatValue={dim?.formatValue} + lockMode="slice" + allowedAxes={['z', 'y', 'x']} + /> + ); + })} +
+ ); +}); + +// Collapsed Slicers list isolated sub-component +const MetaCollapsedSlicers: React.FC<{ + availableDims: DimOption[]; +}> = React.memo(({ availableDims }) => { + const rows = useMetaSelectorStore((s) => s.rows); + const collapsedSels = useMetaSelectorStore((s) => s.collapsedSels); + const updateCollapsedSelAction = useMetaSelectorStore((s) => s.updateCollapsedSel); + + const [collapsedOpen, setCollapsedOpen] = useState(false); + + const activeDimNames = new Set(rows.map((r) => r.dimName)); + const collapsedDims = availableDims.filter((d) => !activeDimNames.has(d.name)); + + if (collapsedDims.length === 0) return null; + + return ( +
+ + + {collapsedOpen && ( +
+ {collapsedDims.map((dim) => ( + { }} + dimSize={dim.size} + selection={collapsedSels[dim.name] ?? { ...defaultSelection(dim.size), mode: 'scalar' }} + axis="c" + onChange={(sel) => updateCollapsedSelAction(dim.name, sel)} + values={dim.values} + formatValue={dim.formatValue} + lockMode="scalar" + /> + ))} +
+ )} +
+ ); +}); + +// Controls for adding dimensions +const MetaAddDimensionControl: React.FC<{ + availableDims: DimOption[]; + dataShape: number[]; +}> = React.memo(({ availableDims, dataShape }) => { + const rows = useMetaSelectorStore((s) => s.rows); + const addRowAction = useMetaSelectorStore((s) => s.addRow); 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 noUnused = collapsedDims.length === 0; const canAdd = !atMax && !noUnused; const addTooltip = atMax @@ -490,22 +600,205 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, ? 'All dimensions are already active.' : undefined; + return ( +
+ + + {addTooltip && ( +
+
+ {addTooltip} +
+
+ )} +
+ ); +}); + +// --- MAIN PANEL CONTAINER (Zero Re-renders during Slider Movements) --- +export default function MetaDimSelector({ meta, metadata, onApply }: Props) { + const isMobile = useIsMobile(); + const [mounted, setMounted] = useState(false); + + // Set mounted state after initial client render + useEffect(() => setMounted(true), []); + + // Extract dimension coordinate arrays from metadata props + const dimArrays = useMemo( + () => (meta?.dimInfo?.dimArrays ?? []).map((a) => Array.from(a)), + [meta?.dimInfo?.dimArrays] + ); + // Extract dimension unit strings from metadata props + const dimUnits = useMemo( + () => (meta?.dimInfo?.dimUnits ?? []).map((u) => u ?? ''), + [meta?.dimInfo?.dimUnits] + ); + // Extract dimension names from metadata props + const dimNames = useMemo( + () => meta?.dimInfo?.dimNames ?? [], + [meta?.dimInfo?.dimNames] + ); + const dataShape = meta?.shape || []; + const chunkShape = meta?.chunks || []; + + const { setDimArrays, setDimNames, setDimUnits, setVariable, variable, idx4D } = useGlobalStore( + useShallow((state) => ({ + setDimArrays: state.setDimArrays, + setDimNames: state.setDimNames, + setDimUnits: state.setDimUnits, + setVariable: state.setVariable, + variable: state.variable, + idx4D: state.idx4D, + })) + ); + + const { maxSize, setMaxSize } = useCacheStore( + useShallow((state) => ({ maxSize: state.maxSize, setMaxSize: state.setMaxSize })) + ); + const [cacheSize, setCacheSize] = useState(maxSize); + + // Bind Zarr dataset store 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, + })) + ); + + const [displaySpat, setDisplaySpat] = useState(String(kernelSize)); + const [displayDepth, setDisplayDepth] = useState(String(kernelDepth)); + + const availableDims: DimOption[] = useMemo( + () => + dimArrays.map((values, idx) => { + const baseName = dimNames[idx] ?? `dim${idx}`; + const name = `${baseName}::${idx}`; + const label = baseName; + const unit = dimUnits[idx] || undefined; + return { + name, + label, + size: values.length, + values, + formatValue: (v: number): string => String(parseLoc(v, unit)), + }; + }), + [dimArrays, dimNames, dimUnits], + ); + + const dimsKey = availableDims.map((d) => `${d.name}:${d.size}`).join('|'); + + const initialCollapsed = useMemo(() => { + const isCurrentVar = variable === meta.name && ndSlices && ndSlices.length === availableDims.length; + return Object.fromEntries( + availableDims.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]; + }) + ); + }, [availableDims, variable, meta.name, ndSlices]); + + const initialRows = useMemo(() => { + const isCurrentVar = variable === meta.name && ndSlices && ndSlices.length === availableDims.length && axisMapping; + + if (isCurrentVar) { + const initRows: SlicerRow[] = []; + const axes: Axis[] = ['z', 'y', 'x']; + const seenNames = new Set(); + + for (const axis of axes) { + const mappedIdx = (axisMapping as Record)[axis]; + if (mappedIdx !== undefined && mappedIdx >= 0 && mappedIdx < availableDims.length) { + const dim = availableDims[mappedIdx]; + if (!seenNames.has(dim.name)) { + seenNames.add(dim.name); + const s = ndSlices[mappedIdx]; + const dimShape = dataShape[mappedIdx] ?? dim.size; + let sel = defaultSelection(dimShape); + if (Array.isArray(s)) { + sel = { start: String(s[0]), stop: s[1] !== null ? String(s[1]) : '', scalar: '', mode: 'slice' }; + } + initRows.push({ dimName: dim.name, sel }); + } + } + } + + if (initRows.length > 0) return initRows; + } + + const activeDims = availableDims.slice(-Math.min(MAX_ACTIVE_DIMS, availableDims.length)); + return activeDims.map((d) => { + const dimShape = dataShape[availableDims.indexOf(d)] ?? d.size; + return { + dimName: d.name, + sel: defaultSelection(dimShape), + }; + }); + }, [availableDims, variable, meta.name, ndSlices, axisMapping, dataShape]); + + // Create isolated store instance per variable key + const selectorStore = useMemo( + () => createMetaSelectorStore(initialRows, initialCollapsed), + [dimsKey] // Re-create clean store instance when dimensions change + ); + + // Reset compression state when variable name changes + useEffect(() => { + setCompress(false); + }, [meta?.name, setCompress]); + + // Plot handler executed ONLY when user clicks the Plot button 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 { rows, collapsedSels } = selectorStore.getState(); + + // Update global store dimension arrays, names, and units on explicit plot action + setDimArrays(dimArrays); + setDimNames(dimNames); + setDimUnits(dimUnits); + + const getRowByAxis = (axis: Axis) => { + const idx = rows.findIndex((_, i) => getActiveAxis(i, rows.length) === axis); + return idx >= 0 ? rows[idx] : undefined; + }; + + const rowZ = getRowByAxis('z'); + const rowY = getRowByAxis('y'); + const rowX = getRowByAxis('x'); const getSliceArray = (row?: SlicerRow, defaultLast = 0): [number, number | null] => { if (!row) return [0, null]; - const sel = row.sel; - if (sel.mode === 'scalar') { - const val = parseInt(sel.scalar) || 0; - return [val, val + 1]; - } - const start = parseInt(sel.start) || 0; - let stop = parseInt(sel.stop); - if (isNaN(stop)) return [start, null]; - return [start, Math.min(stop + 1, defaultLast > 0 ? defaultLast : stop + 1)]; + const range = parseSliceRange(row.sel, defaultLast); + if (row.sel.mode === 'scalar') return [range.first, range.last]; + return [range.first, range.last === defaultLast ? null : range.last]; }; setZSlice(getSliceArray(rowZ, dataShape ? dataShape[getOrigIdx(rowZ?.dimName || '')] : 0)); @@ -517,10 +810,8 @@ 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; - const start = parseInt(row.sel.start) || 0; - let stop = parseInt(row.sel.stop); - if (isNaN(stop)) return [start, null]; - return [start, Math.min(stop + 1, dimShape)]; + const range = parseSliceRange(row.sel, dimShape); + return range.last === dimShape ? [range.first, null] : [range.first, range.last]; } const colSel = collapsedSels[dim.name]; if (colSel && colSel.mode === 'scalar') return parseInt(colSel.scalar) || 0; @@ -536,6 +827,9 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, useZarrStore.getState().setNdSlices(ndSlices); useZarrStore.getState().setAxisMapping(axisMapping); + const activeDimNames = new Set(rows.map((r) => r.dimName)); + const collapsedDims = availableDims.filter((d) => !activeDimNames.has(d.name)); + if (collapsedDims.length > 0) { const firstCollapsed = collapsedDims[0]; const sel = collapsedSels[firstCollapsed.name]; @@ -552,20 +846,23 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, 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)); + onApply?.( + rows.map((r) => r.sel), + rows.map((_, i) => getActiveAxis(i, rows.length)), + rows.map((r) => r.dimName) + ); }; return ( -
-
- {/* Top Header: Name, Attributes, Options, and Plot button */} -
-
- {`${meta.long_name ?? meta.name ?? ''} `} + +
+
+ {/* Top Header: Name, Attributes, Options, and Plot button */} +
+
+ {`${meta.long_name ?? meta.name ?? ''} `} {mounted && isMobile ? ( @@ -598,276 +895,121 @@ export default function MetaDimSelector({ meta, metadata, onApply, setShowMeta, )}
- + {/* Options */}
{/* Coarsen Toggle */}
- setCoarsen(e)}/> + setCoarsen(e)} />
{/* Compress Toggle */}
- setCompress(e)}/> + setCompress(e)} />
{/* Plot Button */}
- {!tooBig && } +
- {/* Status Information */} -
- {/* Size info badge */} -
- Raw: {formatBytes(currentSize)} - | - Stored: {compress ? "<" : ""}{formatBytes(cachedSize)} -
- - {/* Messages */} -
- {tooBig && - - Too many textures ({texCount}/14). Won't fit. - - } - {cached && - - {cachedChunks ? `${cachedChunks} chunks already cached` : "Already cached"} - - } -
-
+ {/* Status Information (Isolated Sub-component) */} +
- {/* Coarsen Expand UI */} - -
-
= 3 ? 'visible' : 'hidden' }} - > - Temporal Coarsening -
- { - const val = parseInt(e.target.value) - setDisplayDepth(e.target.value) - setKernelDepth(Math.pow(2,val)) - }} - /> -
-
-
- Spatial Coarsening -
- { - const val = parseInt(e.target.value) - setDisplaySpat(e.target.value) - setKernelSize(Math.pow(2, val)) - }} - /> + {/* Coarsen Expand UI */} + +
+
= 3 ? 'visible' : 'hidden' }} + > + Temporal Coarsening +
+ { + const val = parseInt(e.target.value); + setDisplayDepth(e.target.value); + setKernelDepth(Math.pow(2, val)); + }} + /> +
-
-
- Values represent 2ⁿ -
-
- - - {/* Cache expand UI if needed */} - {currentSize > maxSize && ( - - {smallCache ? : } - - {smallCache ? "Selection won't fit in Cache" : "Data Will Fit"} - - -
- Decrease selection or expand cache size -
- setCacheSize(maxSize+e[0]*(1024*1024))} - className="flex-1 min-w-0" +
+ Spatial Coarsening +
+ { + const val = parseInt(e.target.value); + setDisplaySpat(e.target.value); + setKernelSize(Math.pow(2, val)); + }} /> -
- setCacheSize(parseInt(e.target.value)*(1024*1024))} - /> - MB - - - - - - Increasing this too far can cause crashes. Mobile users beware - - -
- - - )} - - {/* 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}
-
+
+ Values represent 2ⁿ +
+
+ + + {/* Dimension Table (Isolated Sub-component) */} +
-
- {/* DimSlicers Area */} -
-
-

Active Dimensions

-
- - - {addTooltip && ( -
-
- {addTooltip} -
-
- )} + {/* DimSlicers Area */} +
+
+

Active Dimensions

+ {/* Add Dimension Control (Isolated Sub-component) */} +
-
- {/* 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']} - /> - ); - })} -
+ {/* Active Slicers (Isolated Sub-component) */} + - {/* 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" - /> - ))} -
- )} -
- )} + {/* Collapsed Dimensions (Isolated Sub-component) */} + +
-
+ ); } \ No newline at end of file From 4af279e48b709a38a589fd86c721d71176110e87 Mon Sep 17 00:00:00 2001 From: Jeran Date: Tue, 4 Aug 2026 18:01:46 +0200 Subject: [PATCH 2/5] DimSlicer Update --- src/components/ui/DimSlicer/DimSlicer.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ui/DimSlicer/DimSlicer.tsx b/src/components/ui/DimSlicer/DimSlicer.tsx index 6d2d7bed6..3bb89ce02 100644 --- a/src/components/ui/DimSlicer/DimSlicer.tsx +++ b/src/components/ui/DimSlicer/DimSlicer.tsx @@ -1,5 +1,5 @@ 'use client'; -import React, { useState, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { Slider } from '@/components/ui/slider'; import { Trash2 } from 'lucide-react'; import { @@ -10,7 +10,6 @@ import { SelectValue, } from '@/components/ui/select'; -import { DimSlicerAxisToggle } from './DimSlicerAxisToggle'; import { DimSlicerModeToggle } from './DimSlicerModeToggle'; import { DimSlicerNumericControl } from './DimSlicerNumericControl'; import { DimSlicerTimeControl } from './DimSlicerTimeControl'; @@ -62,7 +61,7 @@ export interface DimSlicerProps { allowedAxes?: Axis[]; } -const DimSlicer: React.FC = ({ +const DimSlicerComponent: React.FC = ({ availableDims, dimName, onDimChange, @@ -78,7 +77,6 @@ const DimSlicer: React.FC = ({ 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; @@ -148,7 +146,9 @@ const DimSlicer: React.FC = ({ [values, effectiveDimSize, formatValue] ); - const isTimeDimension = dimName.toLowerCase().includes('time'); + const isTimeDimension = + /time|date|hour|hr|step|lead|period/i.test(dimName) || + Boolean(values && values.length > 0 && formatValue && /\b(h|hr|hrs|hours|min|sec|s|d|days|ms|since)\b/i.test(formatValue(values[0]) || '')); const isDateDimension = isTimeDimension || dimName.toLowerCase().includes('date'); const showTimeControls = Boolean(values && isTimeDimension); @@ -404,5 +404,5 @@ const DimSlicer: React.FC = ({ ); }; -export { DimSlicer }; +export const DimSlicer = React.memo(DimSlicerComponent); export default DimSlicer; \ No newline at end of file From 9c0b158f07e5641c05f411b8aee8b524b86e5036 Mon Sep 17 00:00:00 2001 From: Jeran Date: Wed, 5 Aug 2026 09:12:26 +0200 Subject: [PATCH 3/5] Claude suggestions --- .../ui/MainPanel/MetaDimSelector.tsx | 179 ++++++------------ 1 file changed, 55 insertions(+), 124 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index f6f0ba319..2410eed16 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -14,7 +14,6 @@ import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; import { parseLoc } from '@/utils/HelperFuncs'; import { ChevronDown, ChevronRight, AlertCircle, CheckCircle2 } from 'lucide-react'; import { useIsMobile } from "@/hooks/use-mobile"; - import { useCacheStore } from "@/GlobalStates/CacheStore"; import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useZarrStore } from '@/GlobalStates/ZarrStore'; @@ -22,26 +21,22 @@ import { SliderThumbs } from "@/components/ui/Widgets/SliderThumbs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { BsFillQuestionCircleFill } from "react-icons/bs"; -// Maximum allowed active dimensions shown in the slicer panel const MAX_ACTIVE_DIMS = 3; -// Helper to format byte counts into human-readable strings (KB, MB, GB) 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]; + return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`; }; -// Metadata payload shape for dimension arrays, names, and units interface DimInfo { dimArrays: ArrayLike[]; dimNames: string[]; dimUnits: (string | null)[]; } -// Props accepted by MetaDimSelector type Props = { meta: { name?: string; @@ -57,7 +52,6 @@ type Props = { onApply?: (sels: SliceSelectionState[], axes: Axis[], dimNames: string[]) => void; }; -// Color mapping badges for standard coordinate axes const AXIS_COLOR: Record = { x: 'text-pink-500', y: 'text-green-500', @@ -65,32 +59,26 @@ const AXIS_COLOR: Record = { c: 'text-yellow-500', }; -// Internal active slicer row state storing name and selection interface SlicerRow { dimName: string; sel: SliceSelectionState; } -// Parses original dimension index from formatted name string (e.g. "lat::1" -> 1) -const getOrigIdx = (dimName: string) => { - const parts = dimName.split('::'); - return parseInt(parts[parts.length - 1]); -}; +// "lat::1" -> 1 +const getOrigIdx = (dimName: string) => parseInt(dimName.split('::').pop() ?? ''); -// Positionally derives spatial axis name ('z', 'y', 'x') based on active row index +// Positionally derives spatial axis ('z' | 'y' | 'x') from a row's index among active rows const getActiveAxis = (index: number, totalRows: number): Axis => { const axes: Axis[] = ['z', 'y', 'x']; return axes[axes.length - totalRows + index] ?? 'x'; }; -// Extracted numeric slice range bounds for slicing calculation interface ParsedSliceRange { first: number; last: number; steps: number; } -// Helper parsing selection state into numerical start, stop, and step counts const parseSliceRange = (sel: SliceSelectionState | undefined, defaultSize: number): ParsedSliceRange => { if (!sel) return { first: 0, last: defaultSize, steps: Math.max(1, defaultSize) }; if (sel.mode === 'scalar') { @@ -104,7 +92,28 @@ const parseSliceRange = (sel: SliceSelectionState | undefined, defaultSize: numb return { first: start, last: stop, steps: Math.max(1, stop - start) }; }; +// Shared by MetaStatusBadges, the cache-status effect, and handlePlot — all three +// need "which row is currently z/y/x" plus each row's original dim index. +const getRowByAxis = (rows: SlicerRow[], axis: Axis) => { + const idx = rows.findIndex((_, i) => getActiveAxis(i, rows.length) === axis); + return idx >= 0 ? rows[idx] : undefined; +}; + +const getAxisRows = (rows: SlicerRow[]) => { + const rowZ = getRowByAxis(rows, 'z'); + const rowY = getRowByAxis(rows, 'y'); + const rowX = getRowByAxis(rows, 'x'); + return { + rowZ, rowY, rowX, + origIdxZ: rowZ ? getOrigIdx(rowZ.dimName) : -1, + origIdxY: rowY ? getOrigIdx(rowY.dimName) : -1, + origIdxX: rowX ? getOrigIdx(rowX.dimName) : -1, + }; +}; + // --- SCOPED STATE ISOLATION STORE --- +// Slider drags update this store, not the parent's React state, so dragging a +// slice range doesn't re-render the whole panel — only the isolated sub-components below. interface SelectorStoreState { rows: SlicerRow[]; collapsedSels: Record; @@ -182,9 +191,8 @@ const useMetaSelectorStore = (selector: (state: SelectorStoreState) => T): T return useStore(store, selector); }; -// --- ISOLATED SUB-COMPONENTS (Zero Parent Re-renders) --- +// --- ISOLATED SUB-COMPONENTS --- -// Status badges for size, cache, and texture counts const MetaStatusBadges: React.FC<{ meta: Props['meta']; availableDims: DimOption[]; @@ -207,25 +215,11 @@ const MetaStatusBadges: React.FC<{ const max3DTextureSize = usePlotStore((s) => s.max3DTextureSize); const dataShape = meta?.shape || []; - const chunkShape = meta?.chunks || []; - // Compute size data const sizeData = useMemo(() => { - const getRowByAxis = (axis: Axis) => { - const idx = rows.findIndex((_, i) => getActiveAxis(i, rows.length) === axis); - return idx >= 0 ? rows[idx] : undefined; - }; - - const rowZ = getRowByAxis('z'); - const rowY = getRowByAxis('y'); - const rowX = getRowByAxis('x'); - + const { rowZ, rowY, rowX, origIdxZ, origIdxY, origIdxX } = getAxisRows(rows); const is2D = dataShape.length === 2 || !rowZ; - 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; @@ -264,7 +258,7 @@ const MetaStatusBadges: React.FC<{ } return { size: calculatedSize, thisCount, depths }; - }, [meta, rows, collapsedSels, availableDims, dataShape, chunkShape, coarsen, kernelSize, kernelDepth, maxTextureSize, max3DTextureSize]); + }, [meta, rows, collapsedSels, availableDims, dataShape, coarsen, kernelSize, kernelDepth, maxTextureSize, max3DTextureSize]); useEffect(() => { setTextureArrayDepths(sizeData.depths); @@ -275,16 +269,12 @@ const MetaStatusBadges: React.FC<{ const tooBig = texCount > 14; 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; - } + const dtype = (meta?.dtype as string) || ''; + const scale = dtype.includes("32") || dtype.includes("f4") ? 0.5 + : dtype.includes("64") || dtype.includes("f8") ? 0.25 + : dtype.includes("8") || dtype.includes("i1") ? 2 + : 1; + return currentSize * scale; }, [currentSize, meta]); const smallCache = cachedSize > cacheSize; @@ -304,44 +294,29 @@ const MetaStatusBadges: React.FC<{ return [range.first, range.last] as [number, number]; } const colSel = collapsedSels[d.name]; - if (colSel && colSel.mode === 'scalar') return parseInt(colSel.scalar) || 0; - return 0; + return colSel && colSel.mode === 'scalar' ? parseInt(colSel.scalar) || 0 : 0; }); const scalarIndices = ndSlicesTemp.filter((s) => typeof s === "number").join("_"); let cacheBase = scalarIndices !== "" ? `${initStore}_${meta.name}_${scalarIndices}` : `${initStore}_${meta.name}`; - if (meta.shape && meta.shape.length >= 4 && idx4D !== undefined && idx4D !== null) { + if (meta.shape.length >= 4 && idx4D !== undefined && idx4D !== null) { cacheBase = `${cacheBase}_time${idx4D}`; } - const getRowByAxis = (axis: Axis) => { - const idx = rows.findIndex((_, i) => getActiveAxis(i, rows.length) === axis); - return idx >= 0 ? rows[idx] : undefined; - }; - - const rowZ = getRowByAxis('z'); - const rowY = getRowByAxis('y'); - const rowX = getRowByAxis('x'); - - const origIdxZ = rowZ ? getOrigIdx(rowZ.dimName) : -1; - const origIdxY = rowY ? getOrigIdx(rowY.dimName) : -1; - const origIdxX = rowX ? getOrigIdx(rowX.dimName) : -1; - - const zSlice = parseSliceRange(rowZ?.sel, origIdxZ >= 0 ? meta.shape?.[origIdxZ] ?? 1 : 1); - const ySlice = parseSliceRange(rowY?.sel, origIdxY >= 0 ? meta.shape?.[origIdxY] ?? 1 : 1); - const xSlice = parseSliceRange(rowX?.sel, origIdxX >= 0 ? meta.shape?.[origIdxX] ?? 1 : 1); + const { rowZ, rowY, rowX, origIdxZ, origIdxY, origIdxX } = getAxisRows(rows); - const calcDim = (slice: { first: number; last: number }, dimIdx: number) => { + // Which chunk indices (in dim units, not element units) a slice range touches + const calcDim = (sel: SliceSelectionState | undefined, 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 { first, last } = parseSliceRange(sel, meta.shape?.[dimIdx] ?? 1); + return { start: Math.floor(first / chunkDim), end: Math.ceil(last / chunkDim) }; }; - const zDim = calcDim(zSlice, origIdxZ); - const yDim = calcDim(ySlice, origIdxY); - const xDim = calcDim(xSlice, origIdxX); + const zDim = calcDim(rowZ?.sel, origIdxZ); + const yDim = calcDim(rowY?.sel, origIdxY); + const xDim = calcDim(rowX?.sel, origIdxX); let accum = 0; let total = 0; @@ -349,11 +324,7 @@ const MetaStatusBadges: React.FC<{ 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 (cache.has(`${cacheBase}_chunk_z${z}_y${y}_x${x}`)) accum++; } } } @@ -443,7 +414,6 @@ const MetaStatusBadges: React.FC<{ ); }); -// Dimension Table isolated sub-component const MetaDimTable: React.FC<{ availableDims: DimOption[]; dataShape: number[]; @@ -492,7 +462,6 @@ const MetaDimTable: React.FC<{ ); }); -// Active Slicers list isolated sub-component const MetaActiveSlicers: React.FC<{ availableDims: DimOption[]; dataShape: number[]; @@ -530,7 +499,6 @@ const MetaActiveSlicers: React.FC<{ ); }); -// Collapsed Slicers list isolated sub-component const MetaCollapsedSlicers: React.FC<{ availableDims: DimOption[]; }> = React.memo(({ availableDims }) => { @@ -579,7 +547,6 @@ const MetaCollapsedSlicers: React.FC<{ ); }); -// Controls for adding dimensions const MetaAddDimensionControl: React.FC<{ availableDims: DimOption[]; dataShape: number[]; @@ -626,29 +593,17 @@ const MetaAddDimensionControl: React.FC<{ ); }); -// --- MAIN PANEL CONTAINER (Zero Re-renders during Slider Movements) --- export default function MetaDimSelector({ meta, metadata, onApply }: Props) { const isMobile = useIsMobile(); const [mounted, setMounted] = useState(false); - - // Set mounted state after initial client render useEffect(() => setMounted(true), []); - // Extract dimension coordinate arrays from metadata props - const dimArrays = useMemo( - () => (meta?.dimInfo?.dimArrays ?? []).map((a) => Array.from(a)), - [meta?.dimInfo?.dimArrays] - ); - // Extract dimension unit strings from metadata props - const dimUnits = useMemo( - () => (meta?.dimInfo?.dimUnits ?? []).map((u) => u ?? ''), - [meta?.dimInfo?.dimUnits] - ); - // Extract dimension names from metadata props - const dimNames = useMemo( - () => meta?.dimInfo?.dimNames ?? [], - [meta?.dimInfo?.dimNames] - ); + const { dimArrays, dimNames, dimUnits } = useMemo(() => ({ + dimArrays: (meta?.dimInfo?.dimArrays ?? []).map((a) => Array.from(a)), + dimNames: meta?.dimInfo?.dimNames ?? [], + dimUnits: (meta?.dimInfo?.dimUnits ?? []).map((u) => u ?? ''), + }), [meta?.dimInfo]); + const dataShape = meta?.shape || []; const chunkShape = meta?.chunks || []; @@ -668,7 +623,6 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { ); const [cacheSize, setCacheSize] = useState(maxSize); - // Bind Zarr dataset store state const { ndSlices, axisMapping, setZSlice, setYSlice, setXSlice, ReFetch, compress, setCompress, coarsen, setCoarsen, kernelSize, setKernelSize, kernelDepth, setKernelDepth } = useZarrStore( useShallow((state) => ({ ndSlices: state.ndSlices, @@ -687,7 +641,7 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { setKernelDepth: state.setKernelDepth, })) ); - + console.log(ndSlices) const [displaySpat, setDisplaySpat] = useState(String(kernelSize)); const [displayDepth, setDisplayDepth] = useState(String(kernelDepth)); @@ -765,34 +719,24 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { }); }, [availableDims, variable, meta.name, ndSlices, axisMapping, dataShape]); - // Create isolated store instance per variable key + // Re-created (clean slate) whenever the active variable's dimensions change const selectorStore = useMemo( () => createMetaSelectorStore(initialRows, initialCollapsed), - [dimsKey] // Re-create clean store instance when dimensions change + [dimsKey] ); - // Reset compression state when variable name changes useEffect(() => { setCompress(false); }, [meta?.name, setCompress]); - // Plot handler executed ONLY when user clicks the Plot button const handlePlot = () => { const { rows, collapsedSels } = selectorStore.getState(); - // Update global store dimension arrays, names, and units on explicit plot action setDimArrays(dimArrays); setDimNames(dimNames); setDimUnits(dimUnits); - const getRowByAxis = (axis: Axis) => { - const idx = rows.findIndex((_, i) => getActiveAxis(i, rows.length) === axis); - return idx >= 0 ? rows[idx] : undefined; - }; - - const rowZ = getRowByAxis('z'); - const rowY = getRowByAxis('y'); - const rowX = getRowByAxis('x'); + const { rowZ, rowY, rowX } = getAxisRows(rows); const getSliceArray = (row?: SlicerRow, defaultLast = 0): [number, number | null] => { if (!row) return [0, null]; @@ -859,7 +803,6 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) {
- {/* Top Header: Name, Attributes, Options, and Plot button */}
{`${meta.long_name ?? meta.name ?? ''} `} @@ -896,15 +839,12 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { )}
- {/* Options */}
- {/* Coarsen Toggle */}
setCoarsen(e)} />
- {/* Compress Toggle */}
- {/* Plot Button */}
- {/* Status Information (Isolated Sub-component) */}
- {/* Coarsen Expand UI */}
- {/* Dimension Table (Isolated Sub-component) */}
- {/* DimSlicers Area */}

Active Dimensions

- {/* Add Dimension Control (Isolated Sub-component) */}
- {/* Active Slicers (Isolated Sub-component) */} - - {/* Collapsed Dimensions (Isolated Sub-component) */}
From 1f3a9c8cb9b580bc564700a74fbdbb7fbb5bbe4f Mon Sep 17 00:00:00 2001 From: Jeran Date: Fri, 7 Aug 2026 15:45:55 +0200 Subject: [PATCH 4/5] MetaDim update --- src/components/ui/DimSlicer/DimSlicer.tsx | 193 +++--- .../ui/DimSlicer/DimSlicerNumericControl.tsx | 4 +- .../DimSlicerNumericInputWithStepper.tsx | 8 +- .../ui/DimSlicer/DimSlicerTimeControl.tsx | 10 +- src/components/ui/DimSlicer/TimeCombobox.tsx | 15 +- .../ui/MainPanel/MetaDimSelector.tsx | 547 ++++++++---------- 6 files changed, 364 insertions(+), 413 deletions(-) diff --git a/src/components/ui/DimSlicer/DimSlicer.tsx b/src/components/ui/DimSlicer/DimSlicer.tsx index 3bb89ce02..dd13f609d 100644 --- a/src/components/ui/DimSlicer/DimSlicer.tsx +++ b/src/components/ui/DimSlicer/DimSlicer.tsx @@ -1,5 +1,5 @@ 'use client'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { Slider } from '@/components/ui/slider'; import { Trash2 } from 'lucide-react'; import { @@ -45,21 +45,24 @@ export interface DimOption { export interface DimSlicerProps { availableDims: DimOption[]; dimName: string; - onDimChange: (dimName: string) => void; + onDimChange: (dimName: string, newName: string) => void; onRemove?: () => void; dimSize: number; selection: SliceSelectionState; - onChange: (next: SliceSelectionState) => void; + onChange: (dimName: string, 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 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 DimSlicerComponent: React.FC = ({ availableDims, @@ -71,11 +74,9 @@ const DimSlicerComponent: React.FC = ({ onChange, step = 1, axis: propAxis = 'x', - onAxisChange, values, formatValue, lockMode, - allowedAxes, }) => { const effectiveDimSize = values ? values.length : dimSize; const rawSel = selection ?? defaultSelection(effectiveDimSize); @@ -97,38 +98,33 @@ const DimSlicerComponent: React.FC = ({ 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) => { + const changeScalarBy = useCallback((delta: number) => { let val = parseOr(sel.scalar, 0) + delta; val = clamp(val, 0, maxIndex); - onChange({ ...sel, scalar: String(val) }); - }; + onChange(dimName,{ ...sel, scalar: String(val) }); + },[onChange, clamp, parseOr]) - const changeStartBy = (delta: number) => { + const changeStartBy = useCallback((delta: number) => { let val = parseOr(sel.start, 0) + delta; val = clamp(val, 0, maxIndex); - onChange({ ...sel, start: String(val) }); - }; + onChange(dimName,{ ...sel, start: String(val) }); + },[onChange, clamp, parseOr]) - const changeStopBy = (delta: number) => { + const changeStopBy = useCallback((delta: number) => { let val = parseOr(sel.stop, maxIndex) + delta; val = clamp(val, 0, maxIndex); - onChange({ ...sel, stop: String(val) }); - }; + onChange(dimName,{ ...sel, stop: String(val) }); + },[onChange, clamp, parseOr]) - const updateSelection = (patch: Partial) => { + const updateSelection = useCallback((patch: Partial) => { const next = { ...sel, ...patch }; if (lockMode) next.mode = lockMode; - onChange(next); - }; + onChange(dimName,next); + },[onChange]) const startIndex = clamp(parseOr(sel.start, 0), 0, maxIndex); const stopIndex = clamp(parseOr(sel.stop, maxIndex), 0, maxIndex); @@ -152,6 +148,27 @@ const DimSlicerComponent: React.FC = ({ const isDateDimension = isTimeDimension || dimName.toLowerCase().includes('date'); const showTimeControls = Boolean(values && isTimeDimension); + const updateScalar = useCallback((newScalar: string | number) => { + if (typeof newScalar === 'string') { + const parsed = parseFloat(newScalar); + if (!Number.isNaN(parsed)) updateSelection({ scalar: String(getIndexFromValue(parsed)) }); + } else updateSelection({ scalar: String(newScalar) }) + },[updateSelection]) + + const updateStart = useCallback((newStart: string | number) => { + if (typeof newStart === 'string') { + const parsed = parseFloat(newStart); + if (!Number.isNaN(parsed)) updateSelection({ start: String(getIndexFromValue(parsed))}) + } else updateSelection({ start: String(newStart)}) + },[updateSelection]) + + const updateStop = useCallback((newStop: string | number) => { + if (typeof newStop === 'string') { + const parsed = parseFloat(newStop); + if (!Number.isNaN(parsed)) updateSelection({ stop: String(getIndexFromValue(parsed))}) + } else updateSelection({ stop: String(newStop)}) + },[updateSelection]) + return (
@@ -167,7 +184,7 @@ const DimSlicerComponent: React.FC = ({ {/* Top row: dim select + mode toggle + axis toggle */}
- onDimChange(dimName, name)}> @@ -236,62 +253,52 @@ const DimSlicerComponent: React.FC = ({ layout="row" showInput={false} currentIndex={scalarIndex} - onIndexChange={(newScalar: number) => updateSelection({ scalar: String(newScalar) })} + onIndexChange={updateScalar} 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)} + onValueChange={updateScalar} + onIncrement={changeScalarBy} + onDecrement={changeScalarBy} />
) : ( -
+
updateSelection({ start: String(newStart) })} + onIndexChange={updateStart} 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)} + onValueChange={updateStart} + onIncrement={changeStartBy} + onDecrement={changeStartBy} /> -
- 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 - /> -
+ +
) ) : ( @@ -300,30 +307,24 @@ const DimSlicerComponent: React.FC = ({ showTimeControls ? ( updateSelection({ start: String(newStart) })} + onIndexChange={updateStart} 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)} + onValueChange={updateStart} + onIncrement={changeStartBy} + onDecrement={changeStartBy} /> ) : ( { - const parsed = parseFloat(value); - if (!Number.isNaN(parsed)) updateSelection({ start: String(getIndexFromValue(parsed)) }); - }} - onIncrement={() => changeStartBy(+1)} - onDecrement={() => changeStartBy(-1)} + onValueChange={updateStart} + onIncrement={changeStartBy} + onDecrement={changeStartBy} ariaLabel="Start value" showInput={!isDateDimension} /> @@ -338,31 +339,25 @@ const DimSlicerComponent: React.FC = ({ layout="row" showInput={false} currentIndex={stopIndex} - onIndexChange={(newStop: number) => updateSelection({ stop: String(newStop) })} + onIndexChange={updateStop} 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)} + onValueChange={updateStop} + onIncrement={changeStopBy} + onDecrement={changeStopBy} includeEnd /> ) : ( { - const parsed = parseFloat(value); - if (!Number.isNaN(parsed)) updateSelection({ stop: String(getIndexFromValue(parsed)) }); - }} - onIncrement={() => changeStopBy(+1)} - onDecrement={() => changeStopBy(-1)} + onValueChange={updateStop} + onIncrement={changeStopBy} + onDecrement={changeStopBy} ariaLabel="Stop value" showInput={!isDateDimension} /> @@ -370,30 +365,24 @@ const DimSlicerComponent: React.FC = ({ ) : showTimeControls ? ( updateSelection({ scalar: String(newScalar) })} + onIndexChange={updateScalar} 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)} + onValueChange={updateScalar} + onIncrement={changeScalarBy} + onDecrement={changeScalarBy} /> ) : ( { - const parsed = parseFloat(value); - if (!Number.isNaN(parsed)) updateSelection({ scalar: String(getIndexFromValue(parsed)) }); - }} - onIncrement={() => changeScalarBy(+1)} - onDecrement={() => changeScalarBy(-1)} + onValueChange={updateScalar} + onIncrement={changeScalarBy} + onDecrement={changeScalarBy} ariaLabel="Scalar value" showInput={!isDateDimension} /> diff --git a/src/components/ui/DimSlicer/DimSlicerNumericControl.tsx b/src/components/ui/DimSlicer/DimSlicerNumericControl.tsx index 2a9494b65..c6c6d5c17 100644 --- a/src/components/ui/DimSlicer/DimSlicerNumericControl.tsx +++ b/src/components/ui/DimSlicer/DimSlicerNumericControl.tsx @@ -8,8 +8,8 @@ interface DimSlicerNumericControlProps { placeholder: string ariaLabel: string onValueChange: (value: string) => void - onIncrement: () => void - onDecrement: () => void + onIncrement: (delta: number) => void + onDecrement: (delta: number) => void showInput: boolean } diff --git a/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx b/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx index 3b9aa9a08..d8a2e28ee 100644 --- a/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx +++ b/src/components/ui/DimSlicer/DimSlicerNumericInputWithStepper.tsx @@ -9,8 +9,8 @@ interface DimSlicerNumericInputWithStepperProps { value: string; placeholder: string; onValueChange: (value: string) => void; - onIncrement: () => void; - onDecrement: () => void; + onIncrement: (delta: number) => void; + onDecrement: (delta: number) => void; ariaLabel: string; showInput?: boolean; } @@ -73,10 +73,10 @@ export const DimSlicerNumericInputWithStepper: React.FC - - diff --git a/src/components/ui/DimSlicer/DimSlicerTimeControl.tsx b/src/components/ui/DimSlicer/DimSlicerTimeControl.tsx index c74e4e704..868d8af5f 100644 --- a/src/components/ui/DimSlicer/DimSlicerTimeControl.tsx +++ b/src/components/ui/DimSlicer/DimSlicerTimeControl.tsx @@ -14,14 +14,14 @@ interface DimSlicerTimeControlProps { effectiveDimSize: number formattedValue: (index: number) => string onValueChange: (value: string) => void - onIncrement: () => void - onDecrement: () => void + onIncrement: (delta: number) => void + onDecrement: (delta: number) => void includeEnd?: boolean layout?: 'row' | 'column' showInput?: boolean } -export function DimSlicerTimeControl({ +export const DimSlicerTimeControl = React.memo(({ currentIndex, onIndexChange, value, @@ -36,7 +36,7 @@ export function DimSlicerTimeControl({ includeEnd = false, layout = 'column', showInput = true, -}: DimSlicerTimeControlProps) { +}: DimSlicerTimeControlProps) => { return (
@@ -62,4 +62,4 @@ export function DimSlicerTimeControl({ />
) -} +}) \ No newline at end of file diff --git a/src/components/ui/DimSlicer/TimeCombobox.tsx b/src/components/ui/DimSlicer/TimeCombobox.tsx index 71acfd650..67d7ccb6d 100644 --- a/src/components/ui/DimSlicer/TimeCombobox.tsx +++ b/src/components/ui/DimSlicer/TimeCombobox.tsx @@ -20,7 +20,7 @@ interface TimeComboboxProps { includeEnd?: boolean } -export default function TimeCombobox({ +const TimeCombobox = React.memo(({ currentIndex, onIndexChange, ariaLabel, @@ -29,7 +29,7 @@ export default function TimeCombobox({ effectiveDimSize, formattedValue, includeEnd = false, -}: TimeComboboxProps) { +}: TimeComboboxProps) => { const selectedLabel = includeEnd && currentIndex === effectiveDimSize ? formattedValue(Math.max(effectiveDimSize - 1, 0)) @@ -111,10 +111,7 @@ export default function TimeCombobox({ } }, [filteredData]); - const targetWidth = Math.min( - Math.max(Math.max(selectedLabel.length, placeholder.length) + 2, 12), - 40 - ) + const targetWidth = 11 return ( {filtered.length === 0 ? No items found. : null} @@ -192,4 +189,6 @@ export default function TimeCombobox({ ) -} \ No newline at end of file +}) + +export default TimeCombobox \ No newline at end of file diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index fbceca10e..8706c27c8 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useMemo, useState, useEffect, createContext, useContext } from 'react'; +import React, { useMemo, useState, useEffect, createContext, useContext, useCallback } from 'react'; import { createStore, useStore } from 'zustand'; import DimSlicer, { Axis, defaultSelection, DimOption, SliceSelectionState } from '@/components/ui/DimSlicer'; import { defaultAttributes, renderAttributes } from "@/components/ui/MetaData"; @@ -127,61 +127,62 @@ interface SelectorStoreState { type SelectorStore = ReturnType; +const getDimShape = (availableDims: DimOption[], dataShape: number[], dimName: string): number => { + const idx = availableDims.findIndex((d) => d.name === dimName); + return dataShape[idx] ?? availableDims[idx]?.size ?? 0; +}; + const createMetaSelectorStore = (initialRows: SlicerRow[], initialCollapsed: Record) => createStore((set) => ({ rows: initialRows, collapsedSels: initialCollapsed, + updateDimName: (oldDimName, newDimName, availableDims, dataShape) => { if (oldDimName === newDimName) return; set((state) => { - const existingIdx = state.rows.findIndex((r) => r.dimName === newDimName); - const newDimIndex = availableDims.findIndex((d) => d.name === newDimName); - const newDim = availableDims[newDimIndex]; - const newDimShape = dataShape[newDimIndex] ?? newDim?.size ?? 0; - - if (existingIdx >= 0) { - const oldDimIndex = availableDims.findIndex((d) => d.name === oldDimName); - const oldDim = availableDims[oldDimIndex]; - const oldDimShape = dataShape[oldDimIndex] ?? oldDim?.size ?? 0; + const newShape = getDimShape(availableDims, dataShape, newDimName); + const isSwap = state.rows.some((r) => r.dimName === newDimName); + if (!isSwap) { return { - rows: state.rows.map((r) => { - if (r.dimName === oldDimName) return { dimName: newDimName, sel: defaultSelection(newDimShape) }; - if (r.dimName === newDimName) return { dimName: oldDimName, sel: defaultSelection(oldDimShape) }; - return r; - }), + rows: state.rows.map((r) => + r.dimName === oldDimName ? { dimName: newDimName, sel: defaultSelection(newShape) } : r + ), }; } + const oldShape = getDimShape(availableDims, dataShape, oldDimName); return { - rows: state.rows.map((r) => (r.dimName === oldDimName ? { dimName: newDimName, sel: defaultSelection(newDimShape) } : r)), + rows: state.rows.map((r) => { + if (r.dimName === oldDimName) return { dimName: newDimName, sel: defaultSelection(newShape) }; + if (r.dimName === newDimName) return { dimName: oldDimName, sel: defaultSelection(oldShape) }; + return r; + }), }; }); }, - updateSel: (dimName, sel) => { + + updateSel: (dimName, sel) => set((state) => ({ rows: state.rows.map((r) => (r.dimName === dimName ? { ...r, sel: { ...sel, mode: 'slice' } } : r)), - })); - }, - updateCollapsedSel: (dimName, sel) => { + })), + + updateCollapsedSel: (dimName, sel) => set((state) => ({ collapsedSels: { ...state.collapsedSels, [dimName]: { ...sel, mode: 'scalar' } }, - })); - }, - addRow: (availableDims, dataShape) => { + })), + + addRow: (availableDims, dataShape) => set((state) => { if (state.rows.length >= MAX_ACTIVE_DIMS) return state; - const usedNames = new Set(state.rows.map((r) => r.dimName)); - const dimName = availableDims.find((d) => !usedNames.has(d.name))?.name; - if (!dimName) return state; - const dim = availableDims.find((d) => d.name === dimName)!; - const dimShape = dataShape[availableDims.indexOf(dim)] ?? dim.size; - return { rows: [...state.rows, { dimName, sel: defaultSelection(dimShape) }] }; - }); - }, - removeLastRow: () => { - set((state) => ({ rows: state.rows.slice(0, -1) })); - }, + const used = new Set(state.rows.map((r) => r.dimName)); + const dim = availableDims.find((d) => !used.has(d.name)); + if (!dim) return state; + const shape = getDimShape(availableDims, dataShape, dim.name); + return { rows: [...state.rows, { dimName: dim.name, sel: defaultSelection(shape) }] }; + }), + + removeLastRow: () => set((state) => ({ rows: state.rows.slice(0, -1) })), })); const MetaSelectorContext = createContext(null); @@ -200,82 +201,46 @@ const MetaStatusBadges: React.FC<{ cacheSize: number; setCacheSize: React.Dispatch>; }> = React.memo(({ meta, availableDims, cacheSize, setCacheSize }) => { - const rows = useMetaSelectorStore((s) => s.rows); - const collapsedSels = useMetaSelectorStore((s) => s.collapsedSels); - - const initStore = useGlobalStore((s) => s.initStore); - const idx4D = useGlobalStore((s) => s.idx4D); - const cache = useCacheStore((s) => s.cache); - const maxSize = useCacheStore((s) => s.maxSize); - const compress = useZarrStore((s) => s.compress); - const coarsen = useZarrStore((s) => s.coarsen); - const kernelSize = useZarrStore((s) => s.kernelSize); - const kernelDepth = useZarrStore((s) => s.kernelDepth); - const setTextureArrayDepths = useGlobalStore((s) => s.setTextureArrayDepths); - const maxTextureSize = usePlotStore((s) => s.maxTextureSize); - const max3DTextureSize = usePlotStore((s) => s.max3DTextureSize); - - const dataShape = meta?.shape || []; - - const sizeData = useMemo(() => { - const { rowZ, rowY, rowX, origIdxZ, origIdxY, origIdxX } = getAxisRows(rows); - const is2D = dataShape.length === 2 || !rowZ; - - const lenZ = origIdxZ >= 0 ? dataShape[origIdxZ] : 1; - const lenY = origIdxY >= 0 ? dataShape[origIdxY] : 1; - const lenX = origIdxX >= 0 ? dataShape[origIdxX] : 1; + const {rows, collapsedSels} = useMetaSelectorStore((s) => s); - const z = is2D ? { first: 0, last: 1, steps: 1 } : parseSliceRange(rowZ?.sel, lenZ); - const y = parseSliceRange(rowY?.sel, lenY); - const x = parseSliceRange(rowX?.sel, lenX); + const {initStore, idx4D, setTextureArrayDepths} = useGlobalStore((s) => s); + const {cache, maxSize} = useCacheStore((s) => s); + const {compress, coarsen, kernelSize, kernelDepth} = useZarrStore((s) => s); + const {maxTextureSize, max3DTextureSize} = usePlotStore((s) => s); - const maxSizeLimit = is2D ? maxTextureSize : max3DTextureSize; - const texCounts = [z.steps / maxSizeLimit, y.steps / maxSizeLimit, x.steps / maxSizeLimit]; - - const depths = texCounts.some((count) => count > 1) - ? texCounts.map((val) => Math.ceil(val)) - : [1, 1, 1]; - - const thisCount = texCounts.reduce((prod, val) => prod * Math.ceil(val), 1); - - const getSelSteps = (dimName: string, defaultLast: number) => { - const row = rows.find((r) => r.dimName === dimName); - if (row) return parseSliceRange(row.sel, defaultLast).steps; - - const collSel = collapsedSels[dimName]; - if (collSel) return parseSliceRange(collSel, defaultLast).steps; - return defaultLast; - }; - - const totalSteps = availableDims.reduce((prod, d, idx) => { - const dimShape = dataShape[idx] ?? d.size; - return prod * getSelSteps(d.name, dimShape); - }, 1); - const sizeRatio = totalSteps / (dataShape.reduce((a, b) => a * b, 1) || 1); - let calculatedSize = (meta.totalSize || 0) * sizeRatio; - - if (!is2D) { - calculatedSize = calculatedSize / (coarsen ? kernelDepth * Math.pow(kernelSize, 2) : 1); - } - - return { size: calculatedSize, thisCount, depths }; - }, [meta, rows, collapsedSels, availableDims, dataShape, coarsen, kernelSize, kernelDepth, maxTextureSize, max3DTextureSize]); - - useEffect(() => { - setTextureArrayDepths(sizeData.depths); - }, [sizeData.depths, setTextureArrayDepths]); + const dataShape = meta?.shape || []; + const dtype = meta.totalSize ? Math.round(meta.totalSize/dataShape.reduce((a,b) => a * b, 1)) : 4 + + const sizeData = useMemo(()=>{ + let prod = 1; + const sizes = [] + for (const [_key, value] of Object.entries(rows)) { + if (value.sel.mode != 'slice') continue; + const start = parseInt(value.sel.start) + const stop = parseInt(value.sel.stop) + const size = Math.abs(stop-start) + sizes.push(size) + prod *= size + } + + const is2D = sizes.length == 2; + const texSize = is2D ? maxTextureSize : max3DTextureSize; + let texProd = 1; + for (const size of sizes){ + const texCount = Math.ceil(size/texSize); + texProd *= texCount; + } + return{ + size: prod * dtype, texCount:texProd + } + },[rows]) const currentSize = sizeData.size; - const texCount = sizeData.thisCount; - const tooBig = texCount > 14; + const texCount = sizeData.texCount; + const tooBig = texCount > 12; const cachedSize = useMemo(() => { - const dtype = (meta?.dtype as string) || ''; - const scale = dtype.includes("32") || dtype.includes("f4") ? 0.5 - : dtype.includes("64") || dtype.includes("f8") ? 0.25 - : dtype.includes("8") || dtype.includes("i1") ? 2 - : 1; - return currentSize * scale; + return currentSize * 2/dtype; }, [currentSize, meta]); const smallCache = cachedSize > cacheSize; @@ -420,8 +385,7 @@ const MetaDimTable: React.FC<{ dataShape: number[]; chunkShape: number[]; }> = React.memo(({ availableDims, dataShape, chunkShape }) => { - const rows = useMetaSelectorStore((s) => s.rows); - const collapsedSels = useMetaSelectorStore((s) => s.collapsedSels); + const {rows, collapsedSels} = useMetaSelectorStore((s) => s); return (
@@ -467,51 +431,68 @@ const MetaActiveSlicers: React.FC<{ availableDims: DimOption[]; dataShape: number[]; }> = React.memo(({ availableDims, dataShape }) => { - const rows = useMetaSelectorStore((s) => s.rows); - const updateDimNameAction = useMetaSelectorStore((s) => s.updateDimName); - const updateSelAction = useMetaSelectorStore((s) => s.updateSel); - const removeLastRow = useMetaSelectorStore((s) => s.removeLastRow); - - return ( -
- {rows.map((row, i) => { - const dim = availableDims.find((d) => d.name === row.dimName); - const isLast = i === rows.length - 1; - const axis = getActiveAxis(i, rows.length); - return ( - updateDimNameAction(row.dimName, name, availableDims, dataShape)} - onRemove={isLast && rows.length > 1 ? removeLastRow : undefined} - dimSize={dim?.size ?? 0} - selection={row.sel} - axis={axis} - onChange={(sel) => updateSelAction(row.dimName, sel)} - values={dim?.values} - formatValue={dim?.formatValue} - lockMode="slice" - allowedAxes={['z', 'y', 'x']} - /> - ); - })} -
- ); + const rows = useMetaSelectorStore((s) => s.rows); + const updateDimNameAction = useMetaSelectorStore((s) => s.updateDimName); + const updateSelAction = useMetaSelectorStore((s) => s.updateSel); + const removeLastRow = useMetaSelectorStore((s) => s.removeLastRow); + + const handleDimChange = useCallback( + (dimName: string, newName: string) => + updateDimNameAction(dimName, newName, availableDims, dataShape), + [availableDims, dataShape] + ); + + const handleSelChange = useCallback( + (dimName: string, sel: SliceSelectionState) => updateSelAction(dimName, sel), + [] // updateSelAction should itself be stable + ); + + const dimByName = useMemo( + () => new Map(availableDims.map((d) => [d.name, d])), + [availableDims] + ); + + return ( +
+ {rows.map((row, i) => { + const dim = dimByName.get(row.dimName); + const isLast = i === rows.length - 1; + const axis = getActiveAxis(i, rows.length); + return ( + 1 ? removeLastRow : undefined} + dimSize={dim?.size ?? 0} + selection={row.sel} + axis={axis} + onChange={handleSelChange} + values={dim?.values} + formatValue={dim?.formatValue} + lockMode="slice" + /> + ); + })} +
+ ); }); const MetaCollapsedSlicers: React.FC<{ availableDims: DimOption[]; }> = React.memo(({ availableDims }) => { - const rows = useMetaSelectorStore((s) => s.rows); - const collapsedSels = useMetaSelectorStore((s) => s.collapsedSels); - const updateCollapsedSelAction = useMetaSelectorStore((s) => s.updateCollapsedSel); - const [collapsedOpen, setCollapsedOpen] = useState(false); + const {rows, collapsedSels, updateCollapsedSel} = useMetaSelectorStore(s=>s) - const activeDimNames = new Set(rows.map((r) => r.dimName)); - const collapsedDims = availableDims.filter((d) => !activeDimNames.has(d.name)); + const [collapsedOpen, setCollapsedOpen] = useState(false); + const activeDimNames = new Set(rows.map((r) => r.dimName)); + const collapsedDims = availableDims.filter((d) => !activeDimNames.has(d.name)); + const handleSelChange = useCallback( + (dimName: string, sel: SliceSelectionState) => updateCollapsedSel(dimName, sel), + [updateCollapsedSel] + ); if (collapsedDims.length === 0) return null; return ( @@ -536,7 +517,7 @@ const MetaCollapsedSlicers: React.FC<{ dimSize={dim.size} selection={collapsedSels[dim.name] ?? { ...defaultSelection(dim.size), mode: 'scalar' }} axis="c" - onChange={(sel) => updateCollapsedSelAction(dim.name, sel)} + onChange={handleSelChange} values={dim.values} formatValue={dim.formatValue} lockMode="scalar" @@ -595,140 +576,134 @@ const MetaAddDimensionControl: React.FC<{ }); export default function MetaDimSelector({ meta, metadata, onApply }: Props) { - const isMobile = useIsMobile(); - const [mounted, setMounted] = useState(false); - useEffect(() => setMounted(true), []); - - const { dimArrays, dimNames, dimUnits } = useMemo(() => ({ - dimArrays: (meta?.dimInfo?.dimArrays ?? []).map((a) => Array.from(a)), - dimNames: meta?.dimInfo?.dimNames ?? [], - dimUnits: (meta?.dimInfo?.dimUnits ?? []).map((u) => u ?? ''), - }), [meta?.dimInfo]); - - const dataShape = meta?.shape || []; - const chunkShape = meta?.chunks || []; - - const { setDimArrays, setDimNames, setDimUnits, setVariable, variable, idx4D } = useGlobalStore( - useShallow((state) => ({ - setDimArrays: state.setDimArrays, - setDimNames: state.setDimNames, - setDimUnits: state.setDimUnits, - setVariable: state.setVariable, - variable: state.variable, - idx4D: state.idx4D, - })) - ); - - const { maxSize, setMaxSize } = useCacheStore( - useShallow((state) => ({ maxSize: state.maxSize, setMaxSize: state.setMaxSize })) - ); - const [cacheSize, setCacheSize] = useState(maxSize); - - const { ndSlices, axisMapping, setZSlice, setYSlice, setXSlice, ReFetch, compress, setCompress, coarsen, setCoarsen, kernelSize, setKernelSize, kernelDepth, setKernelDepth } = useZarrStore( - useShallow((state) => ({ - ndSlices: state.ndSlices, - axisMapping: state.axisMapping, - setZSlice: state.setZSlice, - setYSlice: state.setYSlice, - setXSlice: state.setXSlice, - ReFetch: state.ReFetch, - compress: state.compress, - setCompress: state.setCompress, - coarsen: state.coarsen, - setCoarsen: state.setCoarsen, - kernelSize: state.kernelSize, - setKernelSize: state.setKernelSize, - kernelDepth: state.kernelDepth, - setKernelDepth: state.setKernelDepth, - })) - ); - console.log(ndSlices) - const [displaySpat, setDisplaySpat] = useState(String(kernelSize)); - const [displayDepth, setDisplayDepth] = useState(String(kernelDepth)); - - const availableDims: DimOption[] = useMemo( - () => - dimArrays.map((values, idx) => { - const baseName = dimNames[idx] ?? `dim${idx}`; - const name = `${baseName}::${idx}`; - const label = baseName; - const unit = dimUnits[idx] || undefined; - return { - name, - label, - size: values.length, - values, - formatValue: (v: number): string => String(parseLoc(v, unit)), - }; - }), - [dimArrays, dimNames, dimUnits], - ); - - const dimsKey = availableDims.map((d) => `${d.name}:${d.size}`).join('|'); - - const initialCollapsed = useMemo(() => { - const isCurrentVar = variable === meta.name && ndSlices && ndSlices.length === availableDims.length; - return Object.fromEntries( - availableDims.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]; - }) - ); - }, [availableDims, variable, meta.name, ndSlices]); - - const initialRows = useMemo(() => { - const isCurrentVar = variable === meta.name && ndSlices && ndSlices.length === availableDims.length && axisMapping; - - if (isCurrentVar) { - const initRows: SlicerRow[] = []; - const axes: Axis[] = ['z', 'y', 'x']; - const seenNames = new Set(); - - for (const axis of axes) { - const mappedIdx = (axisMapping as Record)[axis]; - if (mappedIdx !== undefined && mappedIdx >= 0 && mappedIdx < availableDims.length) { - const dim = availableDims[mappedIdx]; - if (!seenNames.has(dim.name)) { - seenNames.add(dim.name); - const s = ndSlices[mappedIdx]; - const dimShape = dataShape[mappedIdx] ?? dim.size; - let sel = defaultSelection(dimShape); - if (Array.isArray(s)) { - sel = { start: String(s[0]), stop: s[1] !== null ? String(s[1]) : '', scalar: '', mode: 'slice' }; - } - initRows.push({ dimName: dim.name, sel }); - } - } - } - - if (initRows.length > 0) return initRows; - } - - const activeDims = availableDims.slice(-Math.min(MAX_ACTIVE_DIMS, availableDims.length)); - return activeDims.map((d) => { - const dimShape = dataShape[availableDims.indexOf(d)] ?? d.size; - return { - dimName: d.name, - sel: defaultSelection(dimShape), - }; - }); - }, [availableDims, variable, meta.name, ndSlices, axisMapping, dataShape]); - - // Re-created (clean slate) whenever the active variable's dimensions change - const selectorStore = useMemo( - () => createMetaSelectorStore(initialRows, initialCollapsed), - [dimsKey] - ); - - useEffect(() => { - setCompress(false); - }, [meta?.name, setCompress]); + const isMobile = useIsMobile(); + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + + const { dimArrays, dimNames, dimUnits } = useMemo(() => ({ + dimArrays: (meta?.dimInfo?.dimArrays ?? []).map((a) => Array.from(a)), + dimNames: meta?.dimInfo?.dimNames ?? [], + dimUnits: (meta?.dimInfo?.dimUnits ?? []).map((u) => u ?? ''), + }), [meta?.dimInfo]); + + const dataShape = meta?.shape || []; + const chunkShape = meta?.chunks || []; + + const { setDimArrays, setDimNames, setDimUnits, setVariable, variable } = useGlobalStore(useShallow(s => s)); + const { maxSize, setMaxSize } = useCacheStore(useShallow(s => s)) + const { ndSlices, axisMapping, ReFetch, compress, setCompress, coarsen, setCoarsen, kernelSize, setKernelSize, kernelDepth, setKernelDepth } = useZarrStore( + useShallow(s => s)) + const [cacheSize, setCacheSize] = useState(maxSize); + + const [displaySpat, setDisplaySpat] = useState(String(kernelSize)); + const [displayDepth, setDisplayDepth] = useState(String(kernelDepth)); + + const availableDims: DimOption[] = useMemo( + () => + dimArrays.map((values, idx) => { + const baseName = dimNames[idx] ?? `dim${idx}`; + const name = `${baseName}::${idx}`; + const label = baseName; + const unit = dimUnits[idx] || undefined; + return { + name, + label, + size: values.length, + values, + formatValue: (v: number): string => String(parseLoc(v, unit)), + }; + }), + [dimArrays, dimNames, dimUnits]); + useEffect(()=>console.log("Gotcha") + ,[availableDims]) + const dimsKey = availableDims.map((d) => `${d.name}:${d.size}`).join('|'); + + const initialCollapsed = useMemo(() => { + const isCurrentVar = variable === meta.name && ndSlices && ndSlices.length === availableDims.length; + return Object.fromEntries( + availableDims.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]; + }) + ); + }, [availableDims, variable, meta.name, ndSlices]); + + const initialRows = useMemo(() => { + const isCurrentVar = variable === meta.name && ndSlices && ndSlices.length === availableDims.length && axisMapping; + + if (isCurrentVar) { + const initRows: SlicerRow[] = []; + const axes: Axis[] = ['z', 'y', 'x']; + const seenNames = new Set(); + + for (const axis of axes) { + const mappedIdx = (axisMapping as Record)[axis]; + if (mappedIdx !== undefined && mappedIdx >= 0 && mappedIdx < availableDims.length) { + const dim = availableDims[mappedIdx]; + if (!seenNames.has(dim.name)) { + seenNames.add(dim.name); + const s = ndSlices[mappedIdx]; + const dimShape = dataShape[mappedIdx] ?? dim.size; + let sel = defaultSelection(dimShape); + if (Array.isArray(s)) { + sel = { start: String(s[0]), stop: s[1] !== null ? String(s[1]) : '', scalar: '', mode: 'slice' }; + } + initRows.push({ dimName: dim.name, sel }); + } + } + } + + if (initRows.length > 0) return initRows; + } + + const activeDims = availableDims.slice(-Math.min(MAX_ACTIVE_DIMS, availableDims.length)); + return activeDims.map((d) => { + const dimShape = dataShape[availableDims.indexOf(d)] ?? d.size; + return { + dimName: d.name, + sel: defaultSelection(dimShape), + }; + }); + }, [availableDims, variable, meta.name, ndSlices, axisMapping, dataShape]); + + // Re-created (clean slate) whenever the active variable's dimensions change + const selectorStore = useMemo( + () => createMetaSelectorStore(initialRows, initialCollapsed), + [dimsKey] + ); + + useEffect(() => { + setCompress(false); + }, [meta?.name, setCompress]); + + function setTextureDepths(){ + const {rows} = selectorStore.getState() + const {maxTextureSize, max3DTextureSize} = usePlotStore.getState() + const { rowZ, rowY, rowX, origIdxZ, origIdxY, origIdxX } = getAxisRows(rows); + const is2D = dataShape.length === 2 || !rowZ; + + 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 } : parseSliceRange(rowZ?.sel, lenZ); + const y = parseSliceRange(rowY?.sel, lenY); + const x = parseSliceRange(rowX?.sel, lenX); + + const maxSizeLimit = is2D ? maxTextureSize : max3DTextureSize; + const texCounts = [z.steps / maxSizeLimit, y.steps / maxSizeLimit, x.steps / maxSizeLimit]; + + const depths = texCounts.some((count) => count > 1) + ? texCounts.map((val) => Math.ceil(val)) + : [1, 1, 1]; + useGlobalStore.setState({textureArrayDepths:depths}) + } const handlePlot = () => { const { rows, collapsedSels } = selectorStore.getState(); @@ -739,17 +714,6 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { const { rowZ, rowY, rowX } = getAxisRows(rows); - const getSliceArray = (row?: SlicerRow, defaultLast = 0): [number, number | null] => { - if (!row) return [0, null]; - const range = parseSliceRange(row.sel, defaultLast); - if (row.sel.mode === 'scalar') return [range.first, range.last]; - return [range.first, range.last === defaultLast ? null : range.last]; - }; - - setZSlice(getSliceArray(rowZ, dataShape ? dataShape[getOrigIdx(rowZ?.dimName || '')] : 0)); - setYSlice(getSliceArray(rowY, dataShape ? dataShape[getOrigIdx(rowY?.dimName || '')] : 0)); - setXSlice(getSliceArray(rowX, dataShape ? dataShape[getOrigIdx(rowX?.dimName || '')] : 0)); - const ndSlices: (number | [number, number | null])[] = availableDims.map((dim, idx) => { const dimShape = dataShape ? dataShape[idx] ?? dim.size : dim.size; const row = rows.find((r) => r.dimName === dim.name); @@ -769,8 +733,7 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { z: getOrigIdx(rowZ?.dimName || '') }; - useZarrStore.getState().setNdSlices(ndSlices); - useZarrStore.getState().setAxisMapping(axisMapping); + useZarrStore.setState({ndSlices, axisMapping}) const activeDimNames = new Set(rows.map((r) => r.dimName)); const collapsedDims = availableDims.filter((d) => !activeDimNames.has(d.name)); @@ -793,7 +756,7 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { } usePlotStore.setState({ coarsen, kernel: { kernelDepth, kernelSize } }); - + setTextureDepths(); onApply?.( rows.map((r) => r.sel), rows.map((_, i) => getActiveAxis(i, rows.length)), From c4a7aad6f22d19167d78d584ff7583422f98500f Mon Sep 17 00:00:00 2001 From: Jeran Date: Fri, 7 Aug 2026 16:24:15 +0200 Subject: [PATCH 5/5] bug squash --- .../ui/MainPanel/MetaDimSelector.tsx | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/ui/MainPanel/MetaDimSelector.tsx b/src/components/ui/MainPanel/MetaDimSelector.tsx index 8706c27c8..cbec65758 100644 --- a/src/components/ui/MainPanel/MetaDimSelector.tsx +++ b/src/components/ui/MainPanel/MetaDimSelector.tsx @@ -203,7 +203,7 @@ const MetaStatusBadges: React.FC<{ }> = React.memo(({ meta, availableDims, cacheSize, setCacheSize }) => { const {rows, collapsedSels} = useMetaSelectorStore((s) => s); - const {initStore, idx4D, setTextureArrayDepths} = useGlobalStore((s) => s); + const {initStore, idx4D} = useGlobalStore((s) => s); const {cache, maxSize} = useCacheStore((s) => s); const {compress, coarsen, kernelSize, kernelDepth} = useZarrStore((s) => s); const {maxTextureSize, max3DTextureSize} = usePlotStore((s) => s); @@ -214,15 +214,18 @@ const MetaStatusBadges: React.FC<{ const sizeData = useMemo(()=>{ let prod = 1; const sizes = [] + // ---- Get total Size ----// for (const [_key, value] of Object.entries(rows)) { if (value.sel.mode != 'slice') continue; + const idx = getOrigIdx(value.dimName) const start = parseInt(value.sel.start) - const stop = parseInt(value.sel.stop) + let stop = parseInt(value.sel.stop) + stop = Number.isFinite(stop) ? stop : dataShape[idx] const size = Math.abs(stop-start) sizes.push(size) prod *= size } - + // ---- Get Texture Counts ---- // const is2D = sizes.length == 2; const texSize = is2D ? maxTextureSize : max3DTextureSize; let texProd = 1; @@ -230,22 +233,25 @@ const MetaStatusBadges: React.FC<{ const texCount = Math.ceil(size/texSize); texProd *= texCount; } + // ---- Apply Coarsen ---- // + if (coarsen){ + prod /= Math.pow(kernelSize,2) + if (!is2D) prod /= kernelDepth + prod = Math.round(prod) + } return{ size: prod * dtype, texCount:texProd } - },[rows]) + },[rows, coarsen, kernelSize, kernelDepth]) const currentSize = sizeData.size; const texCount = sizeData.texCount; const tooBig = texCount > 12; - const cachedSize = useMemo(() => { return currentSize * 2/dtype; }, [currentSize, meta]); const smallCache = cachedSize > cacheSize; - - const [cached, setCached] = useState(false); const [cachedChunks, setCachedChunks] = useState(null); useEffect(() => { @@ -304,8 +310,6 @@ const MetaStatusBadges: React.FC<{ } else if (meta && cache.has(`${initStore}_${meta.name}`)) { newCached = true; } - - setCached((prev) => (prev !== newCached ? newCached : prev)); setCachedChunks((prev) => (prev !== newCachedChunks ? newCachedChunks : prev)); }, [meta, cache, initStore, rows, collapsedSels, availableDims]); @@ -322,12 +326,12 @@ const MetaStatusBadges: React.FC<{
{tooBig && ( - Too many textures ({texCount}/14). Won't fit. + Too many textures ({texCount}/12). Won't fit. )} - {cached && ( + {cachedChunks && ( - {cachedChunks ? `${cachedChunks} chunks already cached` : "Already cached"} + {`${cachedChunks} chunks already cached`} )}
@@ -614,8 +618,6 @@ export default function MetaDimSelector({ meta, metadata, onApply }: Props) { }; }), [dimArrays, dimNames, dimUnits]); - useEffect(()=>console.log("Gotcha") - ,[availableDims]) const dimsKey = availableDims.map((d) => `${d.name}:${d.size}`).join('|'); const initialCollapsed = useMemo(() => {