From 68540b6a7551fd49748fee416d74f34a1bd447b6 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 22 Feb 2026 17:25:26 +0100 Subject: [PATCH 01/31] stride and slice --- .../components/loading/LocalNetCDFMeta.tsx | 479 +++++++----------- .../components/loading/SliceTester.tsx | 271 ++++++++++ scripts/build-wasm.sh | 85 ++++ src/index.ts | 2 + src/netcdf-getters.ts | 196 +++++++ src/netcdf-worker.ts | 11 + src/netcdf4.ts | 57 +++ src/slice.ts | 94 ++++ src/types.ts | 16 + src/wasm-module.ts | 243 +++++++++ 10 files changed, 1167 insertions(+), 287 deletions(-) create mode 100644 docs/next-js/components/loading/SliceTester.tsx create mode 100644 src/slice.ts diff --git a/docs/next-js/components/loading/LocalNetCDFMeta.tsx b/docs/next-js/components/loading/LocalNetCDFMeta.tsx index 8146c3a..cd9cdcc 100644 --- a/docs/next-js/components/loading/LocalNetCDFMeta.tsx +++ b/docs/next-js/components/loading/LocalNetCDFMeta.tsx @@ -30,6 +30,11 @@ import { } from '@/components/ui/tooltip'; import { Badge } from '@/components/ui/badge'; import { NetCDF4, DataTree, GroupNode } from '@earthyscience/netcdf4-wasm'; +import SliceTesterSection, { + SliceSelectionState, + defaultSelection, + buildSelection, +} from './SliceTester'; const NETCDF_EXT_REGEX = /\.(nc|netcdf|nc3|nc4)$/i; @@ -68,9 +73,62 @@ const LocalNetCDFMeta = () => { const [showVariableMenu, setShowVariableMenu] = useState(false); const [expandedGroups, setExpandedGroups] = useState>(new Set(['/'])); - // --------------------------------------------------------------------------- + // ── Slice tester state ───────────────────────────────────────────────────── + const [expandedSliceTester, setExpandedSliceTester] = useState(false); + const [sliceSelections, setSliceSelections] = useState([]); + const [sliceResult, setSliceResult] = useState(null); + const [sliceError, setSliceError] = useState(null); + const [loadingSlice, setLoadingSlice] = useState(false); + + // Reset slice tester when selected variable changes + useEffect(() => { + setSliceResult(null); + setSliceError(null); + setExpandedSliceTester(false); + if (selectedVariable && variables[selectedVariable]?.info?.shape) { + setSliceSelections( + (variables[selectedVariable].info.shape as any[]).map(() => defaultSelection()) + ); + } else { + setSliceSelections([]); + } + }, [selectedVariable]); // eslint-disable-line react-hooks/exhaustive-deps + + // Init slice selections when info first loads for the selected variable + useEffect(() => { + if ( + selectedVariable && + variables[selectedVariable]?.info?.shape && + sliceSelections.length === 0 + ) { + setSliceSelections( + (variables[selectedVariable].info.shape as any[]).map(() => defaultSelection()) + ); + } + }, [variables, selectedVariable]); // eslint-disable-line react-hooks/exhaustive-deps + + const runSliceTest = useCallback(async () => { + if (!dataset || !selectedVariable) return; + setLoadingSlice(true); + setSliceError(null); + setSliceResult(null); + try { + const info = variables[selectedVariable].info; + const selection = buildSelection(sliceSelections, info.shape); + const data = await dataset.get( + selectedVariable, + selection, + currentGroupPath === '/' ? undefined : currentGroupPath + ); + setSliceResult(data); + } catch (e: any) { + setSliceError(e?.message ?? String(e)); + } finally { + setLoadingSlice(false); + } + }, [dataset, selectedVariable, variables, sliceSelections, currentGroupPath]); + // Helpers wrapped in useCallback - // --------------------------------------------------------------------------- const refreshGroup = useCallback((path: string, dataTree: DataTree) => { setCurrentGroupPath(path); @@ -139,7 +197,6 @@ const LocalNetCDFMeta = () => { // Create start and count arrays const start = new Array(shape.length).fill(0); const count = [...shape]; - // Slice along the first dimension count[0] = Math.min(actualSize, shape[0]); const data = await dataset.getSlicedVariableArray( varName, @@ -186,24 +243,18 @@ const LocalNetCDFMeta = () => { setError('Please enter a URL.'); return false; } - - // Check for common protocols const validProtocols = ['http://', 'https://', 's3://', 'gs://', 'ftp://']; const hasValidProtocol = validProtocols.some(protocol => urlString.toLowerCase().startsWith(protocol) ); - if (!hasValidProtocol) { setError('URL must start with a valid protocol (http://, https://, s3://, gs://, or ftp://)'); return false; } - - // Check if URL ends with NetCDF extension if (!NETCDF_EXT_REGEX.test(urlString)) { setError('URL should point to a NetCDF file (.nc, .netcdf, .nc3, .nc4)'); return false; } - return true; }; @@ -211,24 +262,18 @@ const LocalNetCDFMeta = () => { setError(null); const files = event.target.files; if (!files || files.length === 0) return; - const file = files[0]; - if (!NETCDF_EXT_REGEX.test(file.name)) { setError('Please select a valid NetCDF file.'); return; } - try { setIsLoading(true); - const ds = await NetCDF4.fromBlobLazy(file); const dt = new DataTree(ds); await dt.buildTree(); - setDataset(ds); setTree(dt); - refreshGroup('/', dt); } catch (err) { console.error(err); @@ -240,22 +285,14 @@ const LocalNetCDFMeta = () => { const handleUrlFetch = async () => { setError(null); - - if (!validateUrl(url)) { - return; - } - + if (!validateUrl(url)) return; try { setIsLoading(true); - // const urlTry = await NetCDF4.Dataset("s3://its-live-data/test-space/sample-data/sst.mnmean.nc") - // console.log(urlTry) const ds = await NetCDF4.Dataset(url); const dt = new DataTree(ds); await dt.buildTree(); - setDataset(ds); setTree(dt); - refreshGroup('/', dt); } catch (err) { console.error(err); @@ -272,12 +309,9 @@ const LocalNetCDFMeta = () => { const selectGroup = (path: string) => { if (!tree) return; refreshGroup(path, tree); - // Open the group menu and close the variable menu setShowGroupMenu(true); setShowVariableMenu(false); - // Expand the selected group and all its parents const newExpanded = new Set(expandedGroups); - // Add all parent paths const parts = path.split('/').filter(Boolean); let currentPath = '/'; newExpanded.add(currentPath); @@ -299,8 +333,6 @@ const LocalNetCDFMeta = () => { setShowSearchResults(false); return; } - - // Show partial matches while typing if (tree) { const results = tree.searchVariables(value); setSearchResults(results); @@ -414,10 +446,7 @@ const LocalNetCDFMeta = () => { size="sm" onClick={() => { setShowVariableMenu(!showVariableMenu); - // Close group menu when opening variable menu - if (!showVariableMenu) { - setShowGroupMenu(false); - } + if (!showVariableMenu) setShowGroupMenu(false); }} className="cursor-pointer flex-shrink-0" > @@ -435,8 +464,7 @@ const LocalNetCDFMeta = () => {

Variables in current group

- ) - } + )} {/* Search */}
@@ -476,12 +504,8 @@ const LocalNetCDFMeta = () => {
-
- {result.name} -
-
- {result.groupPath} -
+
{result.name}
+
{result.groupPath}
@@ -496,58 +520,50 @@ const LocalNetCDFMeta = () => { )}
+ {/* Group Summary */}
- {groupSummary && ( -
- - {groupSummary.variableCount} variables - - - {groupSummary.dimensionCount} dimensions - - - {groupSummary.attributeCount} attributes - - {groupSummary.subgroupCount > 0 && ( - - {groupSummary.subgroupCount} subgroups + {groupSummary && ( +
+ {groupSummary.variableCount} variables + {groupSummary.dimensionCount} dimensions + {groupSummary.attributeCount} attributes + {groupSummary.subgroupCount > 0 && ( + {groupSummary.subgroupCount} subgroups + )} +
+ )} + {/* Breadcrumbs */} +
+
+ {breadcrumbs.map((crumb, idx) => ( + + + {idx < breadcrumbs.length - 1 && ( + + )} + + ))} +
+ {selectedVariable && ( + + + {selectedVariable} )}
- )} - {/* Breadcrumbs */} -
-
- {breadcrumbs.map((crumb, idx) => ( - - - {idx < breadcrumbs.length - 1 && ( - - )} - - ))}
- - {selectedVariable && ( - - - {selectedVariable} - - )}
-
-
{/* Group Menu (Expandable) */} {showGroupMenu && ( @@ -557,18 +573,13 @@ const LocalNetCDFMeta = () => { const toggleGroup = (path: string) => { const newExpanded = new Set(expandedGroups); - if (newExpanded.has(path)) { - newExpanded.delete(path); - } else { - newExpanded.add(path); - } + if (newExpanded.has(path)) newExpanded.delete(path); + else newExpanded.add(path); setExpandedGroups(newExpanded); }; const handleVariableClick = (varName: string, groupPath: string) => { - if (currentGroupPath !== groupPath) { - selectGroup(groupPath); - } + if (currentGroupPath !== groupPath) selectGroup(groupPath); setPendingVariableLoad({ name: varName, groupPath }); setShowGroupMenu(false); }; @@ -583,7 +594,6 @@ const LocalNetCDFMeta = () => { return (
- {/* Group button - now just expands/collapses */}
- {/* Variables for this group (when expanded) */} {isExpanded && varNames.length > 0 && (
{varNames.map(name => ( @@ -640,21 +640,17 @@ const LocalNetCDFMeta = () => {
)} - {/* Child groups (when expanded) */} {isExpanded && hasChildren && ( -
- {node.children.map(child => renderGroupItem(child, level + 1))} -
+
{node.children.map(child => renderGroupItem(child, level + 1))}
)}
); }; - // Root group handling const rootVars = tree.getAllVariables('/'); const rootVarNames = Object.keys(rootVars); const isRootExpanded = expandedGroups.has('/'); - const ROOT_VARS_KEY = '/__root_vars__'; // Special key for root variables + const ROOT_VARS_KEY = '/__root_vars__'; const isRootVarsExpanded = expandedGroups.has(ROOT_VARS_KEY); return ( @@ -667,13 +663,10 @@ const LocalNetCDFMeta = () => { }`} >
- {/* Chevron for root */} {(groupTree.children.length > 0 || rootVarNames.length > 0) ? ( - isRootExpanded ? ( - - ) : ( - - ) + isRootExpanded + ? + : ) : (
)} @@ -688,22 +681,18 @@ const LocalNetCDFMeta = () => {
- {/* Root variables section (when root is expanded) */} {isRootExpanded && rootVarNames.length > 0 && (
- - {/* Root variables list (when variables section is expanded) */} {isRootVarsExpanded && (
{rootVarNames.map(name => ( @@ -721,7 +710,6 @@ const LocalNetCDFMeta = () => {
)} - {/* Root child groups (when expanded) */} {isRootExpanded && groupTree.children.map(child => renderGroupItem(child, 0))} ); @@ -766,133 +754,66 @@ const LocalNetCDFMeta = () => { className="w-full flex items-center justify-between p-3 hover:bg-accent/50 transition-colors cursor-pointer" > Variable Info - {expandedVariableInfo ? ( - - ) : ( - - )} + {expandedVariableInfo + ? + : + } {expandedVariableInfo && (
- {/* Name */} -
- name: - - {variables[selectedVariable].info.name} - -
- - {/* Data Type */} -
- dtype: - - {variables[selectedVariable].info.dtype} - -
- - {/* NC Type */} - {variables[selectedVariable].info.nctype !== undefined && ( -
- nctype: - - {variables[selectedVariable].info.nctype} - + {[ + { label: 'name', value: variables[selectedVariable].info.name }, + { label: 'dtype', value: variables[selectedVariable].info.dtype }, + ...(variables[selectedVariable].info.nctype !== undefined + ? [{ label: 'nctype', value: variables[selectedVariable].info.nctype }] + : []), + { label: 'shape', value: `[${variables[selectedVariable].info.shape.join(', ')}]` }, + { label: 'dimensions', value: `[${variables[selectedVariable].info.dimensions?.join(', ') || 'N/A'}]` }, + { label: 'size', value: variables[selectedVariable].info.size.toLocaleString() }, + ...(variables[selectedVariable].info.totalSize !== undefined + ? [{ label: 'totalSize', value: `${variables[selectedVariable].info.totalSize.toLocaleString()} bytes` }] + : []), + ...(variables[selectedVariable].info.chunked !== undefined + ? [{ label: 'chunked', value: String(variables[selectedVariable].info.chunked) }] + : []), + ...(variables[selectedVariable].info.chunks + ? [{ label: 'chunks', value: `[${variables[selectedVariable].info.chunks.join(', ')}]` }] + : []), + ...(variables[selectedVariable].info.chunkSize !== undefined + ? [{ label: 'chunkSize', value: `${variables[selectedVariable].info.chunkSize.toLocaleString()} bytes` }] + : []), + ].map(({ label, value }) => ( +
+ {label}: + {value}
- )} - - {/* Shape */} -
- shape: - - [{variables[selectedVariable].info.shape.join(', ')}] - -
- - {/* Dimensions */} -
- dimensions: - - [{variables[selectedVariable].info.dimensions?.join(', ') || 'N/A'}] - -
- - {/* Size */} -
- size: - - {variables[selectedVariable].info.size.toLocaleString()} - -
- - {/* Total Size */} - {variables[selectedVariable].info.totalSize !== undefined && ( -
- totalSize: - - {variables[selectedVariable].info.totalSize.toLocaleString()} bytes - -
- )} - - {/* Chunked */} - {variables[selectedVariable].info.chunked !== undefined && ( -
- chunked: - - {String(variables[selectedVariable].info.chunked)} - -
- )} - - {/* Chunks */} - {variables[selectedVariable].info.chunks && ( -
- chunks: - - [{variables[selectedVariable].info.chunks.join(', ')}] - -
- )} - - {/* Chunk Size */} - {variables[selectedVariable].info.chunkSize !== undefined && ( -
- chunkSize: - - {variables[selectedVariable].info.chunkSize.toLocaleString()} bytes - -
- )} + ))}
)}
{/* Collapsible Enum Dictionary */} {variables[selectedVariable].info.enum && - Object.keys(variables[selectedVariable].info.enum).length > 0 && ( + Object.keys(variables[selectedVariable].info.enum).length > 0 && (
- {expandedEnumDict && (
@@ -900,23 +821,18 @@ const LocalNetCDFMeta = () => { .sort(([a], [b]) => Number(a) - Number(b)) .map(([value, label]) => ( - - {value}: - - - {String(label)} - + {value}: + {String(label)} ))}
- {variables[selectedVariable].info.enumType && (
Base Type: {variables[selectedVariable].info.dtype_base || - `NC_TYPE_${variables[selectedVariable].info.enumType.baseType}`} + `NC_TYPE_${variables[selectedVariable].info.enumType.baseType}`}
@@ -937,24 +853,19 @@ const LocalNetCDFMeta = () => { Variable Attributes ({Object.keys(variables[selectedVariable].info.attributes).length}) - {expandedVariableAttrs ? ( - - ) : ( - - )} + {expandedVariableAttrs + ? + : + } - {expandedVariableAttrs && (
- {/*
*/} {Object.entries(variables[selectedVariable].info.attributes).map(([k, v]) => (
{k}: {typeof v === 'object' - ? JSON.stringify(v, (_k, val) => - typeof val === 'bigint' ? Number(val) : val - ) + ? JSON.stringify(v, (_k, val) => typeof val === 'bigint' ? Number(val) : val) : String(v)}
@@ -965,17 +876,12 @@ const LocalNetCDFMeta = () => { )} {/* Load data controls */} - {/* variables[selectedVariable].info.dtype !== 'str' */} {true && (
- {/* Slice controls - hide for S1 and char */} - {/* !['S1', 'char'].includes(variables[selectedVariable].info.dtype) */} {!['S1', 'char'].includes(variables[selectedVariable].info.dtype) && ( <>
- + {
)} - {/* Show only Load All for S1 and char */} {['S1', 'char'].includes(variables[selectedVariable].info.dtype) && (
)} + + {/* ── Slice & Index Tester ── */} +
)} @@ -1060,31 +978,25 @@ const LocalNetCDFMeta = () => { {/* Collapsible Dimensions */} {Object.keys(dimensions).length > 0 && ( - - {expandedDimensions && (
{Object.entries(dimensions).map(([name, dim]: [string, any]) => (
- {/*
*/} {name}: {dim.size || dim.len || dim.length || 'unlimited'} @@ -1100,25 +1012,20 @@ const LocalNetCDFMeta = () => { {/* Collapsible Attributes */} {Object.keys(attributes).length > 0 && ( - - {expandedAttributes && (
@@ -1127,9 +1034,7 @@ const LocalNetCDFMeta = () => { {k}: {typeof v === 'object' - ? JSON.stringify(v, (_k, val) => - typeof val === 'bigint' ? Number(val) : val - ) + ? JSON.stringify(v, (_k, val) => typeof val === 'bigint' ? Number(val) : val) : String(v)}
diff --git a/docs/next-js/components/loading/SliceTester.tsx b/docs/next-js/components/loading/SliceTester.tsx new file mode 100644 index 0000000..6e70f0b --- /dev/null +++ b/docs/next-js/components/loading/SliceTester.tsx @@ -0,0 +1,271 @@ +'use client'; +import React from 'react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Terminal, ChevronRight, ChevronDown } from 'lucide-react'; +import { slice as ncSlice } from '@earthyscience/netcdf4-wasm'; + +// Types +export type SelectionMode = 'all' | 'scalar' | 'slice'; + +export interface SliceSelectionState { + mode: SelectionMode; + scalar: string; + start: string; + stop: string; + step: string; +} + +export function defaultSelection(): SliceSelectionState { + return { mode: 'all', scalar: '0', start: '0', stop: '', step: '1' }; +} + +// buildSelection — converts UI state → DimSelection[] for dataset.get() + +export function buildSelection( + sels: SliceSelectionState[], + shape: Array +): Array> { + + return sels.map((s, i) => { + const dimSize = Number(shape[i]); + + // ALL + if (s.mode === 'all') { + return ncSlice(0, dimSize, 1); + } + + // SCALAR + if (s.mode === 'scalar') { + let idx = parseInt(s.scalar); + + if (Number.isNaN(idx)) idx = 0; + + // normalize negative indices + if (idx < 0) idx = dimSize + idx; + + if (idx < 0 || idx >= dimSize) { + throw new Error(`index ${idx} out of bounds for dim ${i} size ${dimSize}`); + } + + return idx; + } + + // SLICE + let start = s.start !== '' ? parseInt(s.start) : 0; + let stop = s.stop !== '' ? parseInt(s.stop) : dimSize; + let step = s.step !== '' ? parseInt(s.step) : 1; + + if (Number.isNaN(start)) start = 0; + if (Number.isNaN(stop)) stop = dimSize; + if (Number.isNaN(step)) step = 1; + + // normalize negatives + if (start < 0) start = dimSize + start; + if (stop < 0) stop = dimSize + stop; + + return ncSlice(start, stop, step); + }); +} + +// SliceTesterSection component + +interface SliceTesterSectionProps { + info: any; + sliceSelections: SliceSelectionState[]; + setSliceSelections: React.Dispatch>; + expandedSliceTester: boolean; + setExpandedSliceTester: (v: boolean) => void; + sliceResult: any; + sliceError: string | null; + loadingSlice: boolean; + onRun: () => void; +} + +const SliceTesterSection: React.FC = ({ + info, + sliceSelections, + setSliceSelections, + expandedSliceTester, + setExpandedSliceTester, + sliceResult, + sliceError, + loadingSlice, + onRun, +}) => { + if (!info?.shape || info.shape.length === 0) return null; + + // Coerce shape to plain numbers — nc_inq_dimlen uses i64 so values may be BigInt + const shape: number[] = (info.shape as any[]).map(Number); + + const updateSel = (i: number, patch: Partial) => + setSliceSelections(prev => prev.map((s, idx) => idx === i ? { ...s, ...patch } : s)); + + const resultPreview = sliceResult + ? (() => { + const arr = Array.isArray(sliceResult) ? sliceResult : Array.from(sliceResult as any); + const items = (arr as any[]).slice(0, 30).map((v: any) => + typeof v === 'number' ? v.toFixed(4) : String(v) + ); + const suffix = arr.length > 30 ? `, … (${arr.length} total)` : ''; + return `[${items.join(', ')}${suffix}]`; + })() + : null; + + const selectionPreview = sliceSelections.map((s, i) => { + if (s.mode === 'all') return 'null'; + if (s.mode === 'scalar') return s.scalar || '0'; + const parts: string[] = [s.start || '0', s.stop || String(shape[i])]; + if (s.step && s.step !== '1') parts.push(s.step); + return `slice(${parts.join(', ')})`; + }).join(', '); + + return ( +
+ {/* Header */} + + + {expandedSliceTester && ( +
+ + {/* Dimension rows */} +
+ {shape.map((dimSize: number, i: number) => { + const dimName = info.dimensions?.[i] ?? `dim_${i}`; + const sel = sliceSelections[i] ?? defaultSelection(); + + return ( +
+ {/* Label + mode tabs */} +
+ + {dimName} + [{dimSize}] + +
+ {(['all', 'scalar', 'slice'] as SelectionMode[]).map(m => ( + + ))} +
+
+ + {/* Scalar input */} + {sel.mode === 'scalar' && ( +
+ index + updateSel(i, { scalar: e.target.value })} + className="h-7 text-xs w-28 font-mono" + placeholder="0" + /> + + (0 … {dimSize - 1}, or negative) + +
+ )} + + {/* Slice inputs */} + {sel.mode === 'slice' && ( +
+ {[ + { label: 'start', key: 'start' as const, placeholder: '0' }, + { label: 'stop', key: 'stop' as const, placeholder: String(dimSize) }, + { label: 'step', key: 'step' as const, placeholder: '1' }, + ].map(({ label, key, placeholder }) => ( +
+ {label} + updateSel(i, { [key]: e.target.value })} + className="h-7 text-xs w-20 font-mono" + placeholder={placeholder} + /> +
+ ))} +
+ )} +
+ ); + })} +
+ + {/* Selection preview */} +
+ get("{info.name}", [{selectionPreview}]) +
+ + {/* Run + result count */} +
+ + {sliceResult && ( + + {(() => { + const arr = Array.isArray(sliceResult) ? sliceResult : Array.from(sliceResult as any); + return `${arr.length} elements`; + })()} + + )} +
+ + {/* Error */} + {sliceError && ( + + + {sliceError} + + )} + + {/* Result preview */} + {resultPreview && ( +
+              {resultPreview}
+            
+ )} +
+ )} +
+ ); +}; + +export default SliceTesterSection; \ No newline at end of file diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index b199f0f..a99ad75 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -963,6 +963,91 @@ int nc_get_var_ulonglong_wrapper(int ncid, int varid, unsigned long long* value) return nc_get_var_ulonglong(ncid, varid, value); } +// ========================= +// Strided Data Access (nc_get_vars_*) +// Add these alongside your existing nc_get_vara_*_wrapper functions +// ========================= + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_schar_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, signed char* value) { + return nc_get_vars_schar(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_uchar_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, unsigned char* value) { + return nc_get_vars_uchar(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_short_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, short* value) { + return nc_get_vars_short(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_ushort_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, unsigned short* value) { + return nc_get_vars_ushort(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_int_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, int* value) { + return nc_get_vars_int(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_uint_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, unsigned int* value) { + return nc_get_vars_uint(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_float_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, float* value) { + return nc_get_vars_float(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_double_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, double* value) { + return nc_get_vars_double(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_longlong_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, long long* value) { + return nc_get_vars_longlong(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_ulonglong_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, unsigned long long* value) { + return nc_get_vars_ulonglong(ncid, varid, start, count, stride, value); +} + +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_string_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, char** value) { + return nc_get_vars_string(ncid, varid, start, count, stride, value); +} + +// Generic strided wrapper — mirrors your existing nc_get_vara_wrapper +// Uses nc_get_vars which accepts void* output and nc_type for dispatch +EMSCRIPTEN_KEEPALIVE +int nc_get_vars_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, void* value) { + // nc_get_vars_* requires knowing the type — use nc_get_vara with the generic + // approach by getting the variable type first and dispatching + nc_type xtype; + int stat = nc_inq_vartype(ncid, varid, &xtype); + if (stat != NC_NOERR) return stat; + + switch (xtype) { + case NC_BYTE: return nc_get_vars_schar(ncid, varid, start, count, stride, (signed char*)value); + case NC_UBYTE: return nc_get_vars_uchar(ncid, varid, start, count, stride, (unsigned char*)value); + case NC_SHORT: return nc_get_vars_short(ncid, varid, start, count, stride, (short*)value); + case NC_USHORT: return nc_get_vars_ushort(ncid, varid, start, count, stride, (unsigned short*)value); + case NC_INT: return nc_get_vars_int(ncid, varid, start, count, stride, (int*)value); + case NC_UINT: return nc_get_vars_uint(ncid, varid, start, count, stride, (unsigned int*)value); + case NC_FLOAT: return nc_get_vars_float(ncid, varid, start, count, stride, (float*)value); + case NC_DOUBLE: return nc_get_vars_double(ncid, varid, start, count, stride, (double*)value); + case NC_INT64: return nc_get_vars_longlong(ncid, varid, start, count, stride, (long long*)value); + case NC_UINT64: return nc_get_vars_ulonglong(ncid, varid, start, count, stride, (unsigned long long*)value); + default: return NC_EBADTYPE; + } +} + // ========================= // Data Writing // ========================= diff --git a/src/index.ts b/src/index.ts index 7920845..6e5bf02 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,8 @@ // Export all classes and types export { NetCDF4, DataTree } from './netcdf4.js'; +export { slice, Slice } from './slice.js'; +export type { DimSelection, ResolvedDim } from './slice.js'; export type { GroupNode } from './netcdf4.js'; export { Variable } from './variable.js'; export { Dimension } from './dimension.js'; diff --git a/src/netcdf-getters.ts b/src/netcdf-getters.ts index 1ebf71d..3eaf841 100644 --- a/src/netcdf-getters.ts +++ b/src/netcdf-getters.ts @@ -1,5 +1,6 @@ import { NC_CONSTANTS, DATA_TYPE_SIZE, CONSTANT_DTYPE_MAP } from './constants.js'; import type { NetCDF4Module } from './types.js'; +import { DimSelection, ResolvedDim, resolveDim } from "./slice.js"; export function getGroupVariables( module: NetCDF4Module, @@ -645,6 +646,201 @@ export function getSlicedVariableArray( return arrayData.data; } +type AnyTypedArray = + | Int8Array | Uint8Array + | Int16Array | Uint16Array + | Int32Array | Uint32Array + | Float32Array | Float64Array + | BigInt64Array | BigUint64Array + | string[]; + +export function getVariableArrayWithSelection( + module: NetCDF4Module, + ncid: number, + variable: number | string, + selection: DimSelection[], + groupPath?: string, + options?: { convertEnumsToNames?: boolean } +): AnyTypedArray { + const workingNcid = groupPath ? getGroupNCID(module, ncid, groupPath) : ncid; + + // Resolve varid + let varid: number; + if (typeof variable === "number") { + varid = variable; + } else { + const r = module.nc_inq_varid(workingNcid, variable); + if (r.result !== NC_CONSTANTS.NC_NOERR) { + throw new Error(`Failed to get variable id for '${variable}' (error: ${r.result})`); + } + varid = r.varid as number; + } + + // Query variable dimensions — use workingNcid throughout + const varInfo = module.nc_inq_var(workingNcid, varid); + if (varInfo.result !== NC_CONSTANTS.NC_NOERR) { + throw new Error(`Failed to query variable info (error: ${varInfo.result})`); + } + + const dimids: number[] = Array.from(varInfo.dimids ?? []); + const ndim = dimids.length; + + if (selection.length !== ndim) { + throw new Error( + `Selection has ${selection.length} dimension(s) but variable has ${ndim}` + ); + } + + // Fetch each dimension's size — resolveDim handles BigInt safely + const dimSizes = dimids.map((dimid: number) => { + const r = module.nc_inq_dimlen(workingNcid, dimid); + if (r.result !== NC_CONSTANTS.NC_NOERR) { + throw new Error(`Failed to query dim length for dimid ${dimid} (error: ${r.result})`); + } + return r.len as number | bigint; + }); + + // Resolve each selection — resolveDim coerces BigInt internally + const resolved: ResolvedDim[] = selection.map((sel, i) => + resolveDim(sel, dimSizes[i]) + ); + + const start = resolved.map(d => d.start); + const count = resolved.map(d => d.count); + const stride = resolved.map(d => Math.abs(d.step)); + + const useStride = stride.some(s => s !== 1); + const hasNegativeStep = resolved.some(d => d.step < 0); + + // Resolve variable type — use workingNcid so enum lookup is in the right group + const { enumCtx } = resolveVariableType(module, workingNcid, varid); + const arrayType = enumCtx.baseType; + + let arrayData: { result: number; data?: any }; + + // ── Read data ───────────────────────────────────────────────────────────── + // All reads go directly through workingNcid + varid — no group re-resolution. + + if (enumCtx.isEnum) { + if (useStride) { + arrayData = module.nc_get_vars_generic(workingNcid, varid, start, count, stride, arrayType); + } else { + arrayData = module.nc_get_vara_generic(workingNcid, varid, start, count, arrayType); + } + } else if (useStride) { + type VarsArgs = [number, number, number[], number[], number[]]; + type VarsResult = { result: number; data?: any }; + + const stridedReaders: Record VarsResult> = { + [NC_CONSTANTS.NC_BYTE]: (...args) => module.nc_get_vars_schar(...args), + [NC_CONSTANTS.NC_UBYTE]: (...args) => module.nc_get_vars_uchar(...args), + [NC_CONSTANTS.NC_SHORT]: (...args) => module.nc_get_vars_short(...args), + [NC_CONSTANTS.NC_USHORT]: (...args) => module.nc_get_vars_ushort(...args), + [NC_CONSTANTS.NC_INT]: (...args) => module.nc_get_vars_int(...args), + [NC_CONSTANTS.NC_UINT]: (...args) => module.nc_get_vars_uint(...args), + [NC_CONSTANTS.NC_FLOAT]: (...args) => module.nc_get_vars_float(...args), + [NC_CONSTANTS.NC_DOUBLE]: (...args) => module.nc_get_vars_double(...args), + [NC_CONSTANTS.NC_INT64]: (...args) => module.nc_get_vars_longlong(...args), + [NC_CONSTANTS.NC_LONGLONG]: (...args) => module.nc_get_vars_longlong(...args), + [NC_CONSTANTS.NC_UINT64]: (...args) => module.nc_get_vars_ulonglong(...args), + [NC_CONSTANTS.NC_ULONGLONG]: (...args) => module.nc_get_vars_ulonglong(...args), + [NC_CONSTANTS.NC_STRING]: (...args) => module.nc_get_vars_string(...args), + }; + + const reader = stridedReaders[arrayType]; + if (!reader) { + console.warn(`Unknown NetCDF type ${arrayType} for strided read, falling back to double`); + arrayData = module.nc_get_vars_double(workingNcid, varid, start, count, stride); + } else { + arrayData = reader(workingNcid, varid, start, count, stride); + } + } else { + type VaraArgs = [number, number, number[], number[]]; + type VaraResult = { result: number; data?: any }; + + const readers: Record VaraResult> = { + [NC_CONSTANTS.NC_BYTE]: (...args) => module.nc_get_vara_schar(...args), + [NC_CONSTANTS.NC_UBYTE]: (...args) => module.nc_get_vara_uchar(...args), + [NC_CONSTANTS.NC_SHORT]: (...args) => module.nc_get_vara_short(...args), + [NC_CONSTANTS.NC_USHORT]: (...args) => module.nc_get_vara_ushort(...args), + [NC_CONSTANTS.NC_INT]: (...args) => module.nc_get_vara_int(...args), + [NC_CONSTANTS.NC_UINT]: (...args) => module.nc_get_vara_uint(...args), + [NC_CONSTANTS.NC_FLOAT]: (...args) => module.nc_get_vara_float(...args), + [NC_CONSTANTS.NC_DOUBLE]: (...args) => module.nc_get_vara_double(...args), + [NC_CONSTANTS.NC_INT64]: (...args) => module.nc_get_vara_longlong(...args), + [NC_CONSTANTS.NC_LONGLONG]: (...args) => module.nc_get_vara_longlong(...args), + [NC_CONSTANTS.NC_UINT64]: (...args) => module.nc_get_vara_ulonglong(...args), + [NC_CONSTANTS.NC_ULONGLONG]: (...args) => module.nc_get_vara_ulonglong(...args), + [NC_CONSTANTS.NC_STRING]: (...args) => module.nc_get_vara_string(...args), + }; + + const reader = readers[arrayType]; + if (!reader) { + console.warn(`Unknown NetCDF type ${arrayType}, falling back to double`); + arrayData = module.nc_get_vara_double(workingNcid, varid, start, count); + } else { + arrayData = reader(workingNcid, varid, start, count); + } + } + + if (arrayData.result !== NC_CONSTANTS.NC_NOERR) { + throw new Error(`Failed to read array data (error: ${arrayData.result})`); + } + if (!arrayData.data) { + throw new Error("nc_get_vara/vars returned no data"); + } + + let result: AnyTypedArray = arrayData.data; + + if (enumCtx.isEnum && options?.convertEnumsToNames && enumCtx.enumDict) { + result = convertEnumValuesToNames(result, enumCtx.enumDict); + } + + // Reverse dimensions that had a negative step — cheap, operates on already-small output + if (hasNegativeStep) { + result = applyNegativeStepReversal(result, resolved); + } + + return result; +} + +// applyNegativeStepReversal + +function applyNegativeStepReversal( + data: T, + resolved: ResolvedDim[] +): T { + const ndim = resolved.length; + const outCount = resolved.map(d => + d.collapsed ? 1 : Math.ceil(d.count / Math.abs(d.step)) + ); + const totalOut = outCount.reduce((a, b) => a * b, 1); + + const outStrides = new Array(ndim); + outStrides[ndim - 1] = 1; + for (let i = ndim - 2; i >= 0; i--) { + outStrides[i] = outStrides[i + 1] * outCount[i + 1]; + } + + const out: AnyTypedArray = Array.isArray(data) + ? new Array(totalOut) + : new (data.constructor as any)(totalOut); + + for (let outIdx = 0; outIdx < totalOut; outIdx++) { + let rem = outIdx; + let srcIdx = 0; + for (let d = 0; d < ndim; d++) { + const logI = Math.floor(rem / outStrides[d]); + rem -= logI * outStrides[d]; + const srcI = resolved[d].step < 0 ? outCount[d] - 1 - logI : logI; + srcIdx += srcI * outStrides[d]; + } + (out as any)[outIdx] = (data as any)[srcIdx]; + } + + return out as T; +} + //---- Group Functions ----// /** diff --git a/src/netcdf-worker.ts b/src/netcdf-worker.ts index ff85a19..662b186 100644 --- a/src/netcdf-worker.ts +++ b/src/netcdf-worker.ts @@ -136,6 +136,17 @@ self.onmessage = async (e: MessageEvent) => { result = NCGet.getAttributeValues(mod, data.ncid, data.varid, data.attname); break; + case 'getVariableArrayWithSelection': + result = NCGet.getVariableArrayWithSelection( + mod, + data.ncid, + data.variable, + data.selection, + data.groupPath, + data.options + ); + break; + // ---- Arrays ---- case 'getVariableArray': result = NCGet.getVariableArray(mod, data.ncid, data.variable, data.groupPath); diff --git a/src/netcdf4.ts b/src/netcdf4.ts index 78e261a..a8d5a3e 100644 --- a/src/netcdf4.ts +++ b/src/netcdf4.ts @@ -5,6 +5,7 @@ import { WasmModuleLoader } from './wasm-module.js'; import { NC_CONSTANTS } from './constants.js'; import type { NetCDF4Module, DatasetOptions, MemoryDatasetSource } from './types.js'; import * as NCGet from './netcdf-getters.js' +import { DimSelection } from './slice.js'; export class NetCDF4 extends Group { private module: NetCDF4Module | null = null; @@ -444,6 +445,62 @@ export class NetCDF4 extends Group { } } + /** + * slicing and indexing convenience method. + * + * Each element of `selection` corresponds to one dimension of the variable: + * - `null` → all elements of that dimension + * - `number` → scalar index (dimension is collapsed) + * - `slice(stop)` → elements [0, stop) + * - `slice(start, stop)` → elements [start, stop) + * - `slice(start, stop, step)` → strided subset; step may be negative + * + * Strided reads use nc_get_vars_* under the hood. + * Negative step reads forward then reverses the affected dimension. + * Returns a flat typed array; caller infers shape from non-collapsed dims. + * + * @example + * // Variable shape [time, lat, lon] + * // Read 10 time steps, all lats, first lon: + * const data = await arr.get("temperature", [slice(0, 10), null, 0]); + * + * @example + * // Every other element along time: + * const data = await arr.get("temperature", [slice(0, 100, 2), null, null]); + * + * @example + * // Reversed time axis: + * const data = await arr.get("temperature", [slice(null, null, -1), null, null]); + */ + async get( + variable: number | string, + selection: DimSelection[], + groupPath?: string, + options?: { convertEnumsToNames?: boolean } + ): Promise< + Int8Array | Uint8Array | + Int16Array | Uint16Array | + Int32Array | Uint32Array | + Float32Array | Float64Array | + BigInt64Array | BigUint64Array | + string[] + > { + if (this.worker) { + return this.callWorker('getVariableArrayWithSelection', { + ncid: this.ncid, variable, selection, groupPath, options + }); + } else { + return NCGet.getVariableArrayWithSelection( + this.module as NetCDF4Module, + this.ncid, + variable, + selection, + groupPath, + options + ); + } + } + // Group functions async getGroups(ncid: number = this.ncid): Promise> { if (this.worker) { diff --git a/src/slice.ts b/src/slice.ts new file mode 100644 index 0000000..4db996f --- /dev/null +++ b/src/slice.ts @@ -0,0 +1,94 @@ +/** + * Represents a dimension slice. + * Null fields are resolved against the actual dimension size at read time. + */ +export class Slice { + constructor( + public readonly start: number | null, + public readonly stop: number | null, + public readonly step: number | null + ) {} +} + +/** + * slice(stop) + * slice(start, stop) + * slice(start, stop, step) + * slice(null) — equivalent to null (all elements) + */ +export function slice(stop: number | null): Slice; +export function slice(start: number | null, stop: number | null): Slice; +export function slice(start: number | null, stop: number | null, step: number | null): Slice; +export function slice( + startOrStop: number | null, + stop?: number | null, + step?: number | null +): Slice { + if (stop === undefined) { + return new Slice(null, startOrStop, null); + } + return new Slice(startOrStop, stop ?? null, step ?? null); +} + +/** A single dimension selection: null = all, number = scalar index, Slice = range */ +export type DimSelection = null | number | Slice; + +/** + * Resolved, concrete read parameters for one dimension after applying a + * DimSelection against a known dimension size. + */ +export interface ResolvedDim { + /** Start index in the NetCDF dimension (always the lower bound, even for negative step) */ + start: number; + /** Number of elements to read from NetCDF (contiguous span, always positive) */ + count: number; + /** Step. Positive = forward, negative = reversed output. Never 0. */ + step: number; + /** True when the selection was a scalar index — dimension is collapsed */ + collapsed: boolean; +} + +/** + * Resolve a DimSelection against a concrete dimension size. + * Accepts BigInt dimension sizes (from nc_inq_dimlen i64 reads) and coerces safely. + */ +export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): ResolvedDim { + // Coerce up front — nc_inq_dimlen returns i64 so dimSize may arrive as BigInt + const dimSize = Number(dimSizeRaw); + + // null or empty Slice → full dimension + if ( + sel === null || + (sel instanceof Slice && sel.start === null && sel.stop === null && sel.step === null) + ) { + return { start: 0, count: dimSize, step: 1, collapsed: false }; + } + + // Scalar index → collapsed dimension + if (typeof sel === "number") { + const idx = sel < 0 ? dimSize + sel : sel; + const clamped = Math.max(0, Math.min(idx, dimSize - 1)); + return { start: clamped, count: 1, step: 1, collapsed: true }; + } + + // Slice + const step = sel.step ?? 1; + if (step === 0) throw new Error("Slice step cannot be zero"); + + if (step > 0) { + const rawStart = sel.start ?? 0; + const rawStop = sel.stop ?? dimSize; + const start = Math.max(0, Math.min(rawStart < 0 ? dimSize + rawStart : rawStart, dimSize)); + const stop = Math.max(0, Math.min(rawStop < 0 ? dimSize + rawStop : rawStop, dimSize)); + const count = Math.max(0, stop - start); + return { start, count, step, collapsed: false }; + } else { + // Negative step: read [stop+1 .. start] forward from NetCDF, reverse client-side + const rawStart = sel.start ?? dimSize - 1; + const rawStop = sel.stop ?? -1; + const start = Math.max(0, Math.min(rawStart < 0 ? dimSize + rawStart : rawStart, dimSize - 1)); + const stop = Math.max(-1, Math.min(rawStop < 0 ? dimSize + rawStop : rawStop, dimSize - 1)); + const count = Math.max(0, start - stop); + return { start: stop + 1, count, step, collapsed: false }; + } +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 7379e3b..795d81d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -119,6 +119,22 @@ export interface NetCDF4Module extends EmscriptenModule { nc_get_vara_ulonglong: (ncid: number, varid: number, startp: number[], countp: number[]) => { result: number; data?: BigUint64Array }; nc_get_vara_string: (ncid: number, varid: number, startp: number[], countp: number[]) => { result: number; data?: string[] }; + + // Strided Variable Getters (nc_get_vars_*) + // stride[i] = step along dimension i (must be >= 1; negatives handled at higher level) + nc_get_vars_schar: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Int8Array }; + nc_get_vars_uchar: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Uint8Array }; + nc_get_vars_short: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Int16Array }; + nc_get_vars_ushort: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Uint16Array }; + nc_get_vars_int: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Int32Array }; + nc_get_vars_uint: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Uint32Array }; + nc_get_vars_float: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Float32Array }; + nc_get_vars_double: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: Float64Array }; + nc_get_vars_longlong: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: BigInt64Array }; + nc_get_vars_ulonglong: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: BigUint64Array }; + nc_get_vars_string: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: string[] }; + nc_get_vars_generic: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[], nctype: number) => { result: number; data?: Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | BigInt64Array | BigUint64Array }; + // group types and functions nc_inq_grps: (ncid: number) => { result: number; numgrps?: number; grpids?: Int32Array }; nc_inq_grp_ncid: (ncid: number, grp_name: string) => { result: number; grp_ncid?: number }; diff --git a/src/wasm-module.ts b/src/wasm-module.ts index c7f3e1f..f501512 100644 --- a/src/wasm-module.ts +++ b/src/wasm-module.ts @@ -7,6 +7,10 @@ const NC_MAX_NAME = 256; const NC_MAX_DIMS = 1024; const NC_MAX_VARS = 8192; +function stridedLength(count: number[], stride: number[]): number { + return count.reduce((acc, c, i) => acc * Math.ceil(c / stride[i]), 1); +} + export class WasmModuleLoader { static async loadModule(options: NetCDF4WasmOptions = {}): Promise { try { @@ -130,6 +134,21 @@ export class WasmModuleLoader { const nc_get_var_ulonglong_wrapper = module.cwrap('nc_get_var_ulonglong_wrapper', 'number', ['number', 'number', 'number']); const nc_get_var_string_wrapper = module.cwrap('nc_get_var_string_wrapper', 'number', ['number', 'number', 'number']); + // Stride inquiry wrappers + + const nc_get_vars_schar_wrapper = module.cwrap('nc_get_vars_schar_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_uchar_wrapper = module.cwrap('nc_get_vars_uchar_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_short_wrapper = module.cwrap('nc_get_vars_short_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_ushort_wrapper = module.cwrap('nc_get_vars_ushort_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_int_wrapper = module.cwrap('nc_get_vars_int_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_uint_wrapper = module.cwrap('nc_get_vars_uint_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_float_wrapper = module.cwrap('nc_get_vars_float_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_double_wrapper = module.cwrap('nc_get_vars_double_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_longlong_wrapper = module.cwrap('nc_get_vars_longlong_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_ulonglong_wrapper = module.cwrap('nc_get_vars_ulonglong_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_string_wrapper = module.cwrap('nc_get_vars_string_wrapper', 'number', ['number','number','number','number','number','number']); + const nc_get_vars_wrapper = module.cwrap('nc_get_vars_wrapper', 'number', ['number','number','number','number','number','number']); + // Group inquiry wrappers const nc_inq_grps_wrapper = module.cwrap('nc_inq_grps_wrapper', 'number', ['number', 'number', 'number']); @@ -1294,6 +1313,230 @@ export class WasmModuleLoader { module._free(countPtr); return { result, data }; }, + + // start/count/stride → i64 / 8 bytes per element (matching nc_get_vara_double etc.) + + nc_get_vars_schar: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count, stride); + const dataPtr = module._malloc(totalLength * 1); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const result = nc_get_vars_schar_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Int8Array(module.HEAP8.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_uchar: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count, stride); + const dataPtr = module._malloc(totalLength * 1); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const result = nc_get_vars_uchar_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Uint8Array(module.HEAPU8.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_short: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count, stride); + const dataPtr = module._malloc(totalLength * 2); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const result = nc_get_vars_short_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Int16Array(module.HEAP16.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_ushort: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count, stride); + const dataPtr = module._malloc(totalLength * 2); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const result = nc_get_vars_ushort_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Uint16Array(module.HEAPU16.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_int: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count, stride); + const dataPtr = module._malloc(totalLength * 4); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const result = nc_get_vars_int_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Int32Array(module.HEAP32.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_uint: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count, stride); + const dataPtr = module._malloc(totalLength * 4); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const result = nc_get_vars_uint_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Uint32Array(module.HEAPU32.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_float: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count, stride); + const dataPtr = module._malloc(totalLength * 4); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const result = nc_get_vars_float_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Float32Array(module.HEAPF32.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_double: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count, stride); + const dataPtr = module._malloc(totalLength * 8); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const result = nc_get_vars_double_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new Float64Array(module.HEAPF64.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_longlong: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count, stride); + const dataPtr = module._malloc(totalLength * 8); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const result = nc_get_vars_longlong_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new BigInt64Array(module.HEAP64.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_ulonglong: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count, stride); + const dataPtr = module._malloc(totalLength * 8); + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const result = nc_get_vars_ulonglong_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + const data = result === NC_CONSTANTS.NC_NOERR + ? new BigUint64Array(module.HEAPU64.buffer, dataPtr, totalLength).slice() + : undefined; + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + nc_get_vars_string: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { + const totalLength = stridedLength(count, stride); + const dataPtr = module._malloc(totalLength * 4); // char* pointers (4 bytes in wasm32) + const startPtr = module._malloc(start.length * 8); + const countPtr = module._malloc(count.length * 8); + const stridePtr = module._malloc(stride.length * 8); + start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); + count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const result = nc_get_vars_string_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + let data: string[] | undefined; + if (result === NC_CONSTANTS.NC_NOERR) { + data = []; + for (let i = 0; i < totalLength; i++) { + const strPtr = module.getValue(dataPtr + i * 4, '*'); + data.push(module.UTF8ToString(strPtr)); + } + } + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, + + // Generic strided reader — mirrors nc_get_vara_generic but with stride. + // Uses i32 for start/count/stride to match nc_get_vara_generic's convention, should it be i64? + nc_get_vars_generic: (ncid: number, varid: number, start: number[], count: number[], stride: number[], nctype: number) => { + const elementSize = DATA_TYPE_SIZE[nctype]; + const totalLength = stridedLength(count, stride); + const dataPtr = module._malloc(totalLength * elementSize); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + const result = nc_get_vars_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + let data; + if (result === NC_CONSTANTS.NC_NOERR) { + switch (nctype) { + case NC_CONSTANTS.NC_BYTE: data = new Int8Array(module.HEAP8.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_UBYTE: data = new Uint8Array(module.HEAPU8.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_SHORT: data = new Int16Array(module.HEAP16.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_USHORT: data = new Uint16Array(module.HEAPU16.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_INT: data = new Int32Array(module.HEAP32.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_UINT: data = new Uint32Array(module.HEAPU32.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_INT64: data = new BigInt64Array(module.HEAP64.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_UINT64: data = new BigUint64Array(module.HEAPU64.buffer, dataPtr, totalLength).slice(); break; + } + } + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + }, }; } } \ No newline at end of file From 923f3665f767c1190545ba056d8b4dbcbc2a2b35 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Fri, 27 Feb 2026 16:21:11 +0100 Subject: [PATCH 02/31] adapt to ui --- .../loading/{ => netcdf}/SliceTester.tsx | 81 ++++++++----------- .../components/loading/netcdf/Viewer.tsx | 27 ++++++- .../viewer/components/loading/netcdf/index.ts | 1 + .../loading/netcdf/useViewerState.ts | 48 +++++++++++ 4 files changed, 109 insertions(+), 48 deletions(-) rename docs/viewer/components/loading/{ => netcdf}/SliceTester.tsx (82%) diff --git a/docs/viewer/components/loading/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx similarity index 82% rename from docs/viewer/components/loading/SliceTester.tsx rename to docs/viewer/components/loading/netcdf/SliceTester.tsx index 6e70f0b..8dffab2 100644 --- a/docs/viewer/components/loading/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -6,6 +6,7 @@ import { Spinner } from '@/components/ui/spinner'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Terminal, ChevronRight, ChevronDown } from 'lucide-react'; import { slice as ncSlice } from '@earthyscience/netcdf4-wasm'; +import { VariableInfo, VariableArrayData } from './types'; // Types export type SelectionMode = 'all' | 'scalar' | 'slice'; @@ -23,37 +24,25 @@ export function defaultSelection(): SliceSelectionState { } // buildSelection — converts UI state → DimSelection[] for dataset.get() - export function buildSelection( sels: SliceSelectionState[], shape: Array ): Array> { - return sels.map((s, i) => { const dimSize = Number(shape[i]); - // ALL - if (s.mode === 'all') { - return ncSlice(0, dimSize, 1); - } + if (s.mode === 'all') return ncSlice(0, dimSize, 1); - // SCALAR if (s.mode === 'scalar') { let idx = parseInt(s.scalar); - if (Number.isNaN(idx)) idx = 0; - - // normalize negative indices if (idx < 0) idx = dimSize + idx; - if (idx < 0 || idx >= dimSize) { throw new Error(`index ${idx} out of bounds for dim ${i} size ${dimSize}`); } - return idx; } - // SLICE let start = s.start !== '' ? parseInt(s.start) : 0; let stop = s.stop !== '' ? parseInt(s.stop) : dimSize; let step = s.step !== '' ? parseInt(s.step) : 1; @@ -62,7 +51,6 @@ export function buildSelection( if (Number.isNaN(stop)) stop = dimSize; if (Number.isNaN(step)) step = 1; - // normalize negatives if (start < 0) start = dimSize + start; if (stop < 0) stop = dimSize + stop; @@ -70,21 +58,19 @@ export function buildSelection( }); } -// SliceTesterSection component - interface SliceTesterSectionProps { - info: any; - sliceSelections: SliceSelectionState[]; - setSliceSelections: React.Dispatch>; - expandedSliceTester: boolean; - setExpandedSliceTester: (v: boolean) => void; - sliceResult: any; - sliceError: string | null; - loadingSlice: boolean; - onRun: () => void; + info: VariableInfo; + sliceSelections: SliceSelectionState[]; + setSliceSelections: React.Dispatch>; + expandedSliceTester: boolean; + setExpandedSliceTester: (v: boolean) => void; + sliceResult: VariableArrayData | null; + sliceError: string | null; + loadingSlice: boolean; + onRun: () => void; } -const SliceTesterSection: React.FC = ({ +const SliceTester: React.FC = ({ info, sliceSelections, setSliceSelections, @@ -97,25 +83,29 @@ const SliceTesterSection: React.FC = ({ }) => { if (!info?.shape || info.shape.length === 0) return null; - // Coerce shape to plain numbers — nc_inq_dimlen uses i64 so values may be BigInt - const shape: number[] = (info.shape as any[]).map(Number); + const shape: number[] = info.shape.map(Number); const updateSel = (i: number, patch: Partial) => setSliceSelections(prev => prev.map((s, idx) => idx === i ? { ...s, ...patch } : s)); const resultPreview = sliceResult ? (() => { - const arr = Array.isArray(sliceResult) ? sliceResult : Array.from(sliceResult as any); - const items = (arr as any[]).slice(0, 30).map((v: any) => - typeof v === 'number' ? v.toFixed(4) : String(v) - ); - const suffix = arr.length > 30 ? `, … (${arr.length} total)` : ''; + const len = sliceResult.length ?? 0; + const count = Math.min(30, len); + const items: string[] = []; + for (let i = 0; i < count; i++) { + const v = sliceResult[i] as number | bigint | string; + items.push(typeof v === 'number' ? v.toFixed(4) : String(v)); + } + const suffix = len > 30 ? `, … (${len} total)` : ''; return `[${items.join(', ')}${suffix}]`; })() : null; + const elementCount = sliceResult ? sliceResult.length ?? 0 : 0; + const selectionPreview = sliceSelections.map((s, i) => { - if (s.mode === 'all') return 'null'; + if (s.mode === 'all') return 'null'; if (s.mode === 'scalar') return s.scalar || '0'; const parts: string[] = [s.start || '0', s.stop || String(shape[i])]; if (s.step && s.step !== '1') parts.push(s.step); @@ -146,7 +136,7 @@ const SliceTesterSection: React.FC = ({ {/* Dimension rows */}
- {shape.map((dimSize: number, i: number) => { + {shape.map((dimSize, i) => { const dimName = info.dimensions?.[i] ?? `dim_${i}`; const sel = sliceSelections[i] ?? defaultSelection(); @@ -198,9 +188,9 @@ const SliceTesterSection: React.FC = ({ {sel.mode === 'slice' && (
{[ - { label: 'start', key: 'start' as const, placeholder: '0' }, + { label: 'start', key: 'start' as const, placeholder: '0' }, { label: 'stop', key: 'stop' as const, placeholder: String(dimSize) }, - { label: 'step', key: 'step' as const, placeholder: '1' }, + { label: 'step', key: 'step' as const, placeholder: '1' }, ].map(({ label, key, placeholder }) => (
{label} @@ -222,7 +212,7 @@ const SliceTesterSection: React.FC = ({ {/* Selection preview */}
- get("{info.name}", [{selectionPreview}]) + {`dataset.get("${info.name}", [${selectionPreview}])`}
{/* Run + result count */} @@ -234,16 +224,14 @@ const SliceTesterSection: React.FC = ({ style={{ backgroundColor: '#644FF0', color: 'white' }} className="flex-shrink-0" > - {loadingSlice ? ( - <>Running… - ) : 'Run'} + {loadingSlice + ? <>Running… + : 'Run' + } {sliceResult && ( - {(() => { - const arr = Array.isArray(sliceResult) ? sliceResult : Array.from(sliceResult as any); - return `${arr.length} elements`; - })()} + {elementCount} elements )}
@@ -268,4 +256,5 @@ const SliceTesterSection: React.FC = ({ ); }; -export default SliceTesterSection; \ No newline at end of file +export { SliceTester }; +export default SliceTester; \ No newline at end of file diff --git a/docs/viewer/components/loading/netcdf/Viewer.tsx b/docs/viewer/components/loading/netcdf/Viewer.tsx index c6377a0..8706dcc 100644 --- a/docs/viewer/components/loading/netcdf/Viewer.tsx +++ b/docs/viewer/components/loading/netcdf/Viewer.tsx @@ -13,11 +13,11 @@ import { SearchBar, VariableDetails, VariableDataLoader, + SliceTester, DimensionsCard, AttributesCard, } from '@/components/loading/netcdf'; - -import { useViewerState } from '@/components/loading/netcdf'; +import { useViewerState } from '@/components/loading/netcdf/useViewerState'; const Viewer = () => { const { @@ -72,6 +72,14 @@ const Viewer = () => { handleToggleGroupExpand, handleSearchInputChange, selectSearchResult, + sliceSelections, + setSliceSelections, + expandedSliceTester, + setExpandedSliceTester, + sliceResult, + sliceError, + loadingSlice, + handleRunSlice, // Derived breadcrumbs, groupSummary, @@ -246,6 +254,21 @@ const Viewer = () => { /> )} + {/* Slice Tester */} + {selectedVariable && variables[selectedVariable]?.info && ( + + )} + { setSearchResults([]); }; + // Slice tester state + const [sliceSelections, setSliceSelections] = useState([]); + const [expandedSliceTester, setExpandedSliceTester] = useState(true); + const [sliceResult, setSliceResult] = useState(null); + const [sliceError, setSliceError] = useState(null); + const [loadingSlice, setLoadingSlice] = useState(false); + + // Reset slice tester when selected variable changes + useEffect(() => { + if (!selectedVariable) return; + const info = variables[selectedVariable]?.info; + if (!info?.shape) return; + setSliceSelections(info.shape.map(() => defaultSelection())); + setSliceResult(null); + setSliceError(null); + }, [selectedVariable, variables]); + + const handleRunSlice = useCallback(async () => { + if (!dataset || !selectedVariable) return; + const info = variables[selectedVariable]?.info; + if (!info?.shape) return; + setLoadingSlice(true); + setSliceError(null); + try { + const selection = buildSelection(sliceSelections, info.shape); + const data = await (dataset as any).get( + selectedVariable, + selection, + currentGroupPath === '/' ? undefined : currentGroupPath + ) as VariableArrayData; + setSliceResult(data); + } catch (err) { + setSliceError(err instanceof Error ? err.message : String(err)); + } finally { + setLoadingSlice(false); + } + }, [dataset, selectedVariable, variables, sliceSelections, currentGroupPath]); + // Derived values const breadcrumbs = tree ? tree.getBreadcrumbs(currentGroupPath) : []; @@ -310,6 +349,15 @@ export const useViewerState = () => { handleToggleGroupExpand, handleSearchInputChange, selectSearchResult, + // Slice tester + sliceSelections, + setSliceSelections, + expandedSliceTester, + setExpandedSliceTester, + sliceResult, + sliceError, + loadingSlice, + handleRunSlice, // Derived breadcrumbs, groupSummary, From d4d8025d57591a831c682565cf8782e4e39aaeb0 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 1 Mar 2026 12:31:20 +0100 Subject: [PATCH 03/31] maybe --- .../components/loading/netcdf/SliceTester.tsx | 2 +- src/netcdf-getters.ts | 5 +- src/slice.ts | 7 +- src/types.ts | 2 +- src/wasm-module.ts | 198 ++++++++++-------- 5 files changed, 116 insertions(+), 98 deletions(-) diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx index 8dffab2..9c07938 100644 --- a/docs/viewer/components/loading/netcdf/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -31,7 +31,7 @@ export function buildSelection( return sels.map((s, i) => { const dimSize = Number(shape[i]); - if (s.mode === 'all') return ncSlice(0, dimSize, 1); + if (s.mode === 'all') return null; if (s.mode === 'scalar') { let idx = parseInt(s.scalar); diff --git a/src/netcdf-getters.ts b/src/netcdf-getters.ts index 214b4ed..6f39eef 100644 --- a/src/netcdf-getters.ts +++ b/src/netcdf-getters.ts @@ -720,6 +720,7 @@ export function getVariableArrayWithSelection( // ── Read data ───────────────────────────────────────────────────────────── // All reads go directly through workingNcid + varid — no group re-resolution. + console.log('stride read', { start, count, stride, arrayType }); if (enumCtx.isEnum) { if (useStride) { @@ -811,9 +812,7 @@ function applyNegativeStepReversal( resolved: ResolvedDim[] ): T { const ndim = resolved.length; - const outCount = resolved.map(d => - d.collapsed ? 1 : Math.ceil(d.count / Math.abs(d.step)) - ); + const outCount = resolved.map(d => d.collapsed ? 1 : d.count); const totalOut = outCount.reduce((a, b) => a * b, 1); const outStrides = new Array(ndim); diff --git a/src/slice.ts b/src/slice.ts index 4db996f..e1b8cef 100644 --- a/src/slice.ts +++ b/src/slice.ts @@ -80,15 +80,16 @@ export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): Reso const rawStop = sel.stop ?? dimSize; const start = Math.max(0, Math.min(rawStart < 0 ? dimSize + rawStart : rawStart, dimSize)); const stop = Math.max(0, Math.min(rawStop < 0 ? dimSize + rawStop : rawStop, dimSize)); - const count = Math.max(0, stop - start); + const span = Math.max(0, stop - start); + const count = Math.ceil(span / step); // ← actual element count return { start, count, step, collapsed: false }; } else { - // Negative step: read [stop+1 .. start] forward from NetCDF, reverse client-side const rawStart = sel.start ?? dimSize - 1; const rawStop = sel.stop ?? -1; const start = Math.max(0, Math.min(rawStart < 0 ? dimSize + rawStart : rawStart, dimSize - 1)); const stop = Math.max(-1, Math.min(rawStop < 0 ? dimSize + rawStop : rawStop, dimSize - 1)); - const count = Math.max(0, start - stop); + const span = Math.max(0, start - stop); + const count = Math.ceil(span / Math.abs(step)); // ← actual element count return { start: stop + 1, count, step, collapsed: false }; } } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 795d81d..8a6f8f4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -133,7 +133,7 @@ export interface NetCDF4Module extends EmscriptenModule { nc_get_vars_longlong: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: BigInt64Array }; nc_get_vars_ulonglong: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: BigUint64Array }; nc_get_vars_string: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[]) => { result: number; data?: string[] }; - nc_get_vars_generic: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[], nctype: number) => { result: number; data?: Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | BigInt64Array | BigUint64Array }; + nc_get_vars_generic: (ncid: number, varid: number, startp: number[], countp: number[], stridep: number[], nctype: number) => { result: number; data?: Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | BigInt64Array | BigUint64Array | string[] }; // group types and functions nc_inq_grps: (ncid: number) => { result: number; numgrps?: number; grpids?: Int32Array }; diff --git a/src/wasm-module.ts b/src/wasm-module.ts index 18d9878..777e81d 100644 --- a/src/wasm-module.ts +++ b/src/wasm-module.ts @@ -7,8 +7,8 @@ const NC_MAX_NAME = 256; const NC_MAX_DIMS = 1024; const NC_MAX_VARS = 8192; -function stridedLength(count: number[], stride: number[]): number { - return count.reduce((acc, c, i) => acc * Math.ceil(c / stride[i]), 1); +function stridedLength(count: number[]): number { + return count.reduce((acc, c) => acc * c, 1); } export class WasmModuleLoader { @@ -1314,17 +1314,17 @@ export class WasmModuleLoader { return { result, data }; }, - // start/count/stride → i64 / 8 bytes per element (matching nc_get_vara_double etc.) + // start/count/stride → i32 / 4 bytes per element (size_t and ptrdiff_t are 4 bytes in wasm32) nc_get_vars_schar: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { - const totalLength = stridedLength(count, stride); + const totalLength = stridedLength(count); const dataPtr = module._malloc(totalLength * 1); - const startPtr = module._malloc(start.length * 8); - const countPtr = module._malloc(count.length * 8); - const stridePtr = module._malloc(stride.length * 8); - start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); - count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); - stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); const result = nc_get_vars_schar_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); const data = result === NC_CONSTANTS.NC_NOERR ? new Int8Array(module.HEAP8.buffer, dataPtr, totalLength).slice() @@ -1334,14 +1334,14 @@ export class WasmModuleLoader { }, nc_get_vars_uchar: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { - const totalLength = stridedLength(count, stride); + const totalLength = stridedLength(count); const dataPtr = module._malloc(totalLength * 1); - const startPtr = module._malloc(start.length * 8); - const countPtr = module._malloc(count.length * 8); - const stridePtr = module._malloc(stride.length * 8); - start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); - count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); - stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); const result = nc_get_vars_uchar_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); const data = result === NC_CONSTANTS.NC_NOERR ? new Uint8Array(module.HEAPU8.buffer, dataPtr, totalLength).slice() @@ -1351,14 +1351,14 @@ export class WasmModuleLoader { }, nc_get_vars_short: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { - const totalLength = stridedLength(count, stride); + const totalLength = stridedLength(count); const dataPtr = module._malloc(totalLength * 2); - const startPtr = module._malloc(start.length * 8); - const countPtr = module._malloc(count.length * 8); - const stridePtr = module._malloc(stride.length * 8); - start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); - count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); - stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); const result = nc_get_vars_short_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); const data = result === NC_CONSTANTS.NC_NOERR ? new Int16Array(module.HEAP16.buffer, dataPtr, totalLength).slice() @@ -1368,14 +1368,14 @@ export class WasmModuleLoader { }, nc_get_vars_ushort: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { - const totalLength = stridedLength(count, stride); + const totalLength = stridedLength(count); const dataPtr = module._malloc(totalLength * 2); - const startPtr = module._malloc(start.length * 8); - const countPtr = module._malloc(count.length * 8); - const stridePtr = module._malloc(stride.length * 8); - start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); - count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); - stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); const result = nc_get_vars_ushort_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); const data = result === NC_CONSTANTS.NC_NOERR ? new Uint16Array(module.HEAPU16.buffer, dataPtr, totalLength).slice() @@ -1385,14 +1385,14 @@ export class WasmModuleLoader { }, nc_get_vars_int: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { - const totalLength = stridedLength(count, stride); + const totalLength = stridedLength(count); const dataPtr = module._malloc(totalLength * 4); - const startPtr = module._malloc(start.length * 8); - const countPtr = module._malloc(count.length * 8); - const stridePtr = module._malloc(stride.length * 8); - start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); - count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); - stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); const result = nc_get_vars_int_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); const data = result === NC_CONSTANTS.NC_NOERR ? new Int32Array(module.HEAP32.buffer, dataPtr, totalLength).slice() @@ -1402,14 +1402,14 @@ export class WasmModuleLoader { }, nc_get_vars_uint: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { - const totalLength = stridedLength(count, stride); + const totalLength = stridedLength(count); const dataPtr = module._malloc(totalLength * 4); - const startPtr = module._malloc(start.length * 8); - const countPtr = module._malloc(count.length * 8); - const stridePtr = module._malloc(stride.length * 8); - start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); - count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); - stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); const result = nc_get_vars_uint_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); const data = result === NC_CONSTANTS.NC_NOERR ? new Uint32Array(module.HEAPU32.buffer, dataPtr, totalLength).slice() @@ -1419,14 +1419,14 @@ export class WasmModuleLoader { }, nc_get_vars_float: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { - const totalLength = stridedLength(count, stride); + const totalLength = stridedLength(count); const dataPtr = module._malloc(totalLength * 4); - const startPtr = module._malloc(start.length * 8); - const countPtr = module._malloc(count.length * 8); - const stridePtr = module._malloc(stride.length * 8); - start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); - count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); - stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); const result = nc_get_vars_float_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); const data = result === NC_CONSTANTS.NC_NOERR ? new Float32Array(module.HEAPF32.buffer, dataPtr, totalLength).slice() @@ -1436,14 +1436,14 @@ export class WasmModuleLoader { }, nc_get_vars_double: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { - const totalLength = stridedLength(count, stride); + const totalLength = stridedLength(count); const dataPtr = module._malloc(totalLength * 8); - const startPtr = module._malloc(start.length * 8); - const countPtr = module._malloc(count.length * 8); - const stridePtr = module._malloc(stride.length * 8); - start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); - count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); - stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); const result = nc_get_vars_double_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); const data = result === NC_CONSTANTS.NC_NOERR ? new Float64Array(module.HEAPF64.buffer, dataPtr, totalLength).slice() @@ -1453,14 +1453,14 @@ export class WasmModuleLoader { }, nc_get_vars_longlong: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { - const totalLength = stridedLength(count, stride); + const totalLength = stridedLength(count); const dataPtr = module._malloc(totalLength * 8); - const startPtr = module._malloc(start.length * 8); - const countPtr = module._malloc(count.length * 8); - const stridePtr = module._malloc(stride.length * 8); - start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); - count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); - stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); const result = nc_get_vars_longlong_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); const data = result === NC_CONSTANTS.NC_NOERR ? new BigInt64Array(module.HEAP64.buffer, dataPtr, totalLength).slice() @@ -1470,14 +1470,14 @@ export class WasmModuleLoader { }, nc_get_vars_ulonglong: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { - const totalLength = stridedLength(count, stride); + const totalLength = stridedLength(count); const dataPtr = module._malloc(totalLength * 8); - const startPtr = module._malloc(start.length * 8); - const countPtr = module._malloc(count.length * 8); - const stridePtr = module._malloc(stride.length * 8); - start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); - count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); - stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); const result = nc_get_vars_ulonglong_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); const data = result === NC_CONSTANTS.NC_NOERR ? new BigUint64Array(module.HEAPU64.buffer, dataPtr, totalLength).slice() @@ -1487,14 +1487,14 @@ export class WasmModuleLoader { }, nc_get_vars_string: (ncid: number, varid: number, start: number[], count: number[], stride: number[]) => { - const totalLength = stridedLength(count, stride); + const totalLength = stridedLength(count); const dataPtr = module._malloc(totalLength * 4); // char* pointers (4 bytes in wasm32) - const startPtr = module._malloc(start.length * 8); - const countPtr = module._malloc(count.length * 8); - const stridePtr = module._malloc(stride.length * 8); - start .forEach((v, i) => module.setValue(startPtr + i * 8, v, 'i64')); - count .forEach((v, i) => module.setValue(countPtr + i * 8, v, 'i64')); - stride.forEach((v, i) => module.setValue(stridePtr + i * 8, v, 'i64')); + const startPtr = module._malloc(start.length * 4); + const countPtr = module._malloc(count.length * 4); + const stridePtr = module._malloc(stride.length * 4); + start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); + count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); + stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); const result = nc_get_vars_string_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); let data: string[] | undefined; if (result === NC_CONSTANTS.NC_NOERR) { @@ -1508,28 +1508,46 @@ export class WasmModuleLoader { return { result, data }; }, - // Generic strided reader — mirrors nc_get_vara_generic but with stride. - // Uses i32 for start/count/stride to match nc_get_vara_generic's convention, should it be i64? + // Generic strided reader — routes by nctype, including NC_STRING. + // NC_STRING is handled by delegating to nc_get_vars_string_wrapper directly, + // since the generic nc_get_vars_wrapper C switch omits it (returns NC_EBADTYPE). nc_get_vars_generic: (ncid: number, varid: number, start: number[], count: number[], stride: number[], nctype: number) => { const elementSize = DATA_TYPE_SIZE[nctype]; - const totalLength = stridedLength(count, stride); - const dataPtr = module._malloc(totalLength * elementSize); + const totalLength = stridedLength(count); const startPtr = module._malloc(start.length * 4); const countPtr = module._malloc(count.length * 4); const stridePtr = module._malloc(stride.length * 4); start .forEach((v, i) => module.setValue(startPtr + i * 4, v, 'i32')); count .forEach((v, i) => module.setValue(countPtr + i * 4, v, 'i32')); stride.forEach((v, i) => module.setValue(stridePtr + i * 4, v, 'i32')); + + // NC_STRING: generic C wrapper omits this case — delegate to typed string wrapper + if (nctype === NC_CONSTANTS.NC_STRING) { + const dataPtr = module._malloc(totalLength * 4); // char* pointers (4 bytes in wasm32) + const result = nc_get_vars_string_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); + let data: string[] | undefined; + if (result === NC_CONSTANTS.NC_NOERR) { + data = []; + for (let i = 0; i < totalLength; i++) { + const strPtr = module.getValue(dataPtr + i * 4, '*'); + data.push(module.UTF8ToString(strPtr)); + } + } + module._free(dataPtr); module._free(startPtr); module._free(countPtr); module._free(stridePtr); + return { result, data }; + } + + const dataPtr = module._malloc(totalLength * elementSize); const result = nc_get_vars_wrapper(ncid, varid, startPtr, countPtr, stridePtr, dataPtr); let data; if (result === NC_CONSTANTS.NC_NOERR) { switch (nctype) { - case NC_CONSTANTS.NC_BYTE: data = new Int8Array(module.HEAP8.buffer, dataPtr, totalLength).slice(); break; - case NC_CONSTANTS.NC_UBYTE: data = new Uint8Array(module.HEAPU8.buffer, dataPtr, totalLength).slice(); break; - case NC_CONSTANTS.NC_SHORT: data = new Int16Array(module.HEAP16.buffer, dataPtr, totalLength).slice(); break; - case NC_CONSTANTS.NC_USHORT: data = new Uint16Array(module.HEAPU16.buffer, dataPtr, totalLength).slice(); break; - case NC_CONSTANTS.NC_INT: data = new Int32Array(module.HEAP32.buffer, dataPtr, totalLength).slice(); break; - case NC_CONSTANTS.NC_UINT: data = new Uint32Array(module.HEAPU32.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_BYTE: data = new Int8Array(module.HEAP8.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_UBYTE: data = new Uint8Array(module.HEAPU8.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_SHORT: data = new Int16Array(module.HEAP16.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_USHORT: data = new Uint16Array(module.HEAPU16.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_INT: data = new Int32Array(module.HEAP32.buffer, dataPtr, totalLength).slice(); break; + case NC_CONSTANTS.NC_UINT: data = new Uint32Array(module.HEAPU32.buffer, dataPtr, totalLength).slice(); break; case NC_CONSTANTS.NC_INT64: data = new BigInt64Array(module.HEAP64.buffer, dataPtr, totalLength).slice(); break; case NC_CONSTANTS.NC_UINT64: data = new BigUint64Array(module.HEAPU64.buffer, dataPtr, totalLength).slice(); break; } From d950aa3d1e852e610d49e4344c02d065c1a4c349 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Mon, 2 Mar 2026 08:57:53 +0100 Subject: [PATCH 04/31] cleaner Array Display --- .../loading/netcdf/ArrayDisplay.tsx | 332 ++++++++++++++++++ .../components/loading/netcdf/SliceTester.tsx | 71 ++-- 2 files changed, 373 insertions(+), 30 deletions(-) create mode 100644 docs/viewer/components/loading/netcdf/ArrayDisplay.tsx diff --git a/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx b/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx new file mode 100644 index 0000000..754c171 --- /dev/null +++ b/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx @@ -0,0 +1,332 @@ +'use client'; +import React, { useMemo, useState } from 'react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface ArrayDisplayProps { + data: ArrayLike; + shape: number[]; + dimNames?: string[]; + varName?: string; + dtype?: string; + maxRows?: number; + maxCols?: number; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function fmtVal(v: number | bigint | string, dtype?: string): string { + if (typeof v === 'bigint') return String(v); + if (typeof v === 'string') return v; + if (!Number.isFinite(v)) return String(v); + if (dtype?.startsWith('int') || dtype?.startsWith('uint')) return String(Math.trunc(v)); + const abs = Math.abs(v); + if (abs === 0) return '0'; + if (abs >= 1e5 || (abs < 1e-3 && abs > 0)) return v.toExponential(3); + return v.toPrecision(6).replace(/\.?0+$/, ''); +} + +function lpad(s: string, w: number) { + return ' '.repeat(Math.max(0, w - s.length)) + s; +} + +// ─── Dim colors (cycles per outer dim index) ───────────────────────────────── + +const DIM_COLORS = [ + '#a78bfa', // purple + '#f87171', // tomato + '#fb923c', // orange + '#facc15', // amber +]; + +// ─── Scalar ─────────────────────────────────────────────────────────────────── + +const ScalarDisplay: React.FC<{ value: string; dtype?: string }> = ({ value, dtype }) => ( +
+ {dtype &&
{dtype}
} +
{value}
+
+); + +// ─── Vector ─────────────────────────────────────────────────────────────────── + +interface VectorProps { + data: ArrayLike; + len: number; + dimName: string; + dtype?: string; + maxRows: number; +} +const VectorDisplay: React.FC = ({ data, len, dimName, dtype, maxRows }) => { + const vals = useMemo(() => { + const out: string[] = []; + for (let i = 0; i < len; i++) out.push(fmtVal(data[i] as never, dtype)); + return out; + }, [data, len, dtype]); + + const valW = Math.max(...vals.map(s => s.length)); + const idxW = String(len - 1).length; + const shown = Math.min(len, maxRows); + + return ( +
+
+ ↓ {dimName} + {dtype && {dtype}} +
+
+ + + {Array.from({ length: shown }, (_, i) => ( + + + + + ))} + {len > maxRows && ( + + )} + +
{i}{vals[i]}
⋮ ({len - maxRows} more)
+
+
+ ); +}; + +// ─── Matrix ─────────────────────────────────────────────────────────────────── + +interface MatrixProps { + data: ArrayLike; + rows: number; + cols: number; + rowDim: string; + colDim: string; + dtype?: string; + maxRows: number; + maxCols: number; + offset?: number; + showHeader?: boolean; +} +const MatrixDisplay: React.FC = ({ + data, rows, cols, rowDim, colDim, dtype, maxRows, maxCols, offset = 0, showHeader = true, +}) => { + const shownRows = Math.min(rows, maxRows); + const shownCols = Math.min(cols, maxCols); + const truncR = rows > maxRows; + const truncC = cols > maxCols; + + const grid = useMemo(() => { + const g: string[][] = []; + for (let r = 0; r < shownRows; r++) { + const row: string[] = []; + for (let c = 0; c < shownCols; c++) { + row.push(fmtVal((data as never)[offset + r * cols + c], dtype)); + } + g.push(row); + } + return g; + }, [data, shownRows, shownCols, cols, dtype, offset]); + + const colHeaders = Array.from({ length: shownCols }, (_, i) => String(i)); + const rowHeaders = Array.from({ length: shownRows }, (_, i) => String(i)); + const cw = Array.from({ length: shownCols }, (_, ci) => + Math.max(colHeaders[ci].length, ...grid.map(row => row[ci]?.length ?? 0)) + ); + const rhW = Math.max(rowDim.length + 2, ...rowHeaders.map(s => s.length)); + + return ( +
+
+ + + + + + {showHeader && dtype && ( + + )} + + + + ))} + {truncC && } + + + + {grid.map((row, ri) => ( + + + {row.map((v, ci) => ( + + ))} + {truncC && } + + ))} + {truncR && ( + + + + + )} + +
+ ↓ {rowDim} + + → {colDim} + {dtype}
+ {colHeaders.map((h, ci) => ( + + {lpad(h, cw[ci])} +
{lpad(rowHeaders[ri], rhW)} + {lpad(v, cw[ci])} +
+ ({rows - maxRows} more rows) +
+
+
+ ); +}; + +// ─── ND (3D+) ───────────────────────────────────────────────────────────────── + +interface NDProps { + data: ArrayLike; + shape: number[]; + dimNames: string[]; + dtype?: string; + maxRows: number; + maxCols: number; +} +const NDDisplay: React.FC = ({ data, shape, dimNames, dtype, maxRows, maxCols }) => { + const ndim = shape.length; + const rows = shape[ndim - 2]; + const cols = shape[ndim - 1]; + const rowDim = dimNames[ndim - 2] ?? `dim_${ndim - 2}`; + const colDim = dimNames[ndim - 1] ?? `dim_${ndim - 1}`; + const outerShape = shape.slice(0, ndim - 2); + const outerDims = dimNames.slice(0, ndim - 2); + const numSlices = outerShape.reduce((a, b) => a * b, 1); + + const [sliceIdx, setSliceIdx] = useState(0); + + const outerIdxForSlice = (si: number): number[] => { + const idx: number[] = []; + let rem = si; + for (let d = outerShape.length - 1; d >= 0; d--) { + idx[d] = rem % outerShape[d]; + rem = Math.floor(rem / outerShape[d]); + } + return idx; + }; + + const outerIdx = outerIdxForSlice(sliceIdx); + const offset = sliceIdx * rows * cols; + + const sliceProxy = new Proxy(data as ArrayLike, { + get(target, prop) { + if (typeof prop === 'string' && !Number.isNaN(+prop)) return target[offset + +prop]; + return (target as never)[prop]; + }, + }); + + return ( +
+
+ + {/* bracket: colored outer indices + muted colons for matrix dims */} + + {'['} + {outerIdx.map((idx, i) => ( + + {i > 0 && , } + {idx} + + ))} + {outerIdx.length > 0 ? ', ' : ''}:, : + {']'} + + {/* outer dim name=value, colored */} + {outerDims.map((d, i) => ( + + {d}={outerIdx[i]} + + ))} + {/* matrix dim names, no value */} + {rowDim} + {colDim} + {/* dtype */} + {dtype && {dtype}} + + {sliceIdx + 1}/{numSlices} +
+ + +
+ ); +}; + +// ─── Main ───────────────────────────────────────────────────────────────────── + +export const ArrayDisplay: React.FC = ({ + data, + shape, + dimNames = [], + varName, + dtype, + maxRows = 24, + maxCols = 16, +}) => { + const ndim = shape.length; + const total = shape.reduce((a, b) => a * b, 1); + const names = shape.map((_, i) => dimNames[i] ?? `dim_${i}`); + + if (ndim === 0 || total === 1) { + return ; + } + + if (ndim === 1) { + return ; + } + + if (ndim === 2) { + return ( + + ); + } + + return ( + + ); +}; + +export default ArrayDisplay; \ No newline at end of file diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx index 9c07938..40e57cf 100644 --- a/docs/viewer/components/loading/netcdf/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -7,6 +7,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert'; import { Terminal, ChevronRight, ChevronDown } from 'lucide-react'; import { slice as ncSlice } from '@earthyscience/netcdf4-wasm'; import { VariableInfo, VariableArrayData } from './types'; +import ArrayDisplay from './ArrayDisplay'; // Types export type SelectionMode = 'all' | 'scalar' | 'slice'; @@ -58,6 +59,23 @@ export function buildSelection( }); } +export function resultShape( + sels: SliceSelectionState[], + shape: Array +): number[] { + const out: number[] = []; + sels.forEach((s, i) => { + const dimSize = Number(shape[i]); + if (s.mode === 'scalar') return; + if (s.mode === 'all') { out.push(dimSize); return; } + const start = s.start !== '' ? parseInt(s.start) : 0; + const stop = s.stop !== '' ? parseInt(s.stop) : dimSize; + const step = s.step !== '' ? parseInt(s.step) : 1; + out.push(Math.max(0, Math.ceil((stop - start) / step))); + }); + return out; +} + interface SliceTesterSectionProps { info: VariableInfo; sliceSelections: SliceSelectionState[]; @@ -88,29 +106,8 @@ const SliceTester: React.FC = ({ const updateSel = (i: number, patch: Partial) => setSliceSelections(prev => prev.map((s, idx) => idx === i ? { ...s, ...patch } : s)); - const resultPreview = sliceResult - ? (() => { - const len = sliceResult.length ?? 0; - const count = Math.min(30, len); - const items: string[] = []; - for (let i = 0; i < count; i++) { - const v = sliceResult[i] as number | bigint | string; - items.push(typeof v === 'number' ? v.toFixed(4) : String(v)); - } - const suffix = len > 30 ? `, … (${len} total)` : ''; - return `[${items.join(', ')}${suffix}]`; - })() - : null; - - const elementCount = sliceResult ? sliceResult.length ?? 0 : 0; - - const selectionPreview = sliceSelections.map((s, i) => { - if (s.mode === 'all') return 'null'; - if (s.mode === 'scalar') return s.scalar || '0'; - const parts: string[] = [s.start || '0', s.stop || String(shape[i])]; - if (s.step && s.step !== '1') parts.push(s.step); - return `slice(${parts.join(', ')})`; - }).join(', '); + const rShape = resultShape(sliceSelections, info.shape); + const rDims = info.dimensions?.filter((_, i) => sliceSelections[i]?.mode !== 'scalar'); return (
@@ -212,7 +209,16 @@ const SliceTester: React.FC = ({ {/* Selection preview */}
- {`dataset.get("${info.name}", [${selectionPreview}])`} + {`dataset.get("${info.name}", [${ + sliceSelections.map((s, i) => { + if (s.mode === 'all') return 'null'; + if (s.mode === 'scalar') return s.scalar || '0'; + const dimSize = shape[i]; + const parts: string[] = [s.start || '0', s.stop || String(dimSize)]; + if (s.step && s.step !== '1') parts.push(s.step); + return `slice(${parts.join(', ')})`; + }).join(', ') + }])`}
{/* Run + result count */} @@ -231,7 +237,7 @@ const SliceTester: React.FC = ({ {sliceResult && ( - {elementCount} elements + {sliceResult.length ?? 0} elements )}
@@ -244,12 +250,17 @@ const SliceTester: React.FC = ({ )} - {/* Result preview */} - {resultPreview && ( -
-              {resultPreview}
-            
+ {/* Result display */} + {sliceResult && ( + )} +
)}
From 778b0dd71999cb649056b8743c4f287975a07b37 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Mon, 2 Mar 2026 09:47:34 +0100 Subject: [PATCH 05/31] meta --- .../loading/netcdf/ArrayDisplay.tsx | 128 +++++++++++++++--- .../components/loading/netcdf/SliceTester.tsx | 1 + 2 files changed, 111 insertions(+), 18 deletions(-) diff --git a/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx b/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx index 754c171..710ede7 100644 --- a/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx +++ b/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx @@ -5,13 +5,15 @@ import { ChevronLeft, ChevronRight } from 'lucide-react'; // ─── Types ──────────────────────────────────────────────────────────────────── interface ArrayDisplayProps { - data: ArrayLike; - shape: number[]; - dimNames?: string[]; - varName?: string; - dtype?: string; - maxRows?: number; - maxCols?: number; + data: ArrayLike; + shape: number[]; + dimNames?: string[]; + varName?: string; + dtype?: string; + /** Original full shape before slicing, for the footer comparison */ + totalShape?: number[]; + maxRows?: number; + maxCols?: number; } // ─── Helpers ────────────────────────────────────────────────────────────────── @@ -288,6 +290,75 @@ const NDDisplay: React.FC = ({ data, shape, dimNames, dtype, maxRows, m ); }; + + +// ─── Byte helpers ───────────────────────────────────────────────────────────── + +export function formatBytes(bytes: number): string { + const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; + let value = bytes; + let unitIndex = 0; + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024; + unitIndex++; + } + return `${value.toFixed(2)} ${units[unitIndex]}`; +} + +function dtypeBytes(dtype?: string): number { + if (!dtype) return 4; + if (dtype === 'S1') return 1; // NC_CHAR + if (dtype === 'str') return 0; // NC_STRING (variable length) + if (dtype === 'NAT') return 0; + // NetCDF shorthand: i1/u1 → 1, i2/u2 → 2, i4/u4/f4 → 4, i8/u8/f8 → 8 + const shorthand = dtype.match(/^[iufc](\d+)$/); + if (shorthand) return parseInt(shorthand[1]); + // Named: float64/int16/uint8 etc + if (dtype.includes('64')) return 8; + if (dtype.includes('32')) return 4; + if (dtype.includes('16')) return 2; + if (dtype.includes('8')) return 1; + return 4; +} + +// ─── Footer ─────────────────────────────────────────────────────────────────── + +interface FooterProps { + varName?: string; + shape: number[]; + totalShape?: number[]; + dtype?: string; +} +const Footer: React.FC = ({ varName, shape, totalShape, dtype }) => { + const bpp = dtypeBytes(dtype); + const sliceTotal = shape.reduce((a, b) => a * b, 1); + const sliceBytes = sliceTotal * bpp; + const hasTotal = totalShape && totalShape.length > 0; + const totalTotal = hasTotal ? totalShape!.reduce((a, b) => a * b, 1) : null; + const totalBytes = totalTotal !== null ? totalTotal * bpp : null; + + return ( +
+ {varName && ( + {varName} + )} + + [{shape.join(', ')}] + {formatBytes(sliceBytes)} + + {hasTotal && totalBytes !== null && ( + <> + / + + [{totalShape!.join(', ')}] + {formatBytes(totalBytes)} + + + )} +
+ ); +}; + // ─── Main ───────────────────────────────────────────────────────────────────── export const ArrayDisplay: React.FC = ({ @@ -296,6 +367,7 @@ export const ArrayDisplay: React.FC = ({ dimNames = [], varName, dtype, + totalShape, maxRows = 24, maxCols = 16, }) => { @@ -303,29 +375,49 @@ export const ArrayDisplay: React.FC = ({ const total = shape.reduce((a, b) => a * b, 1); const names = shape.map((_, i) => dimNames[i] ?? `dim_${i}`); + const footer = ( +
+ ); + if (ndim === 0 || total === 1) { - return ; + return ( +
+ + {footer} +
+ ); } if (ndim === 1) { - return ; + return ( +
+ + {footer} +
+ ); } if (ndim === 2) { return ( - +
+ + {footer} +
); } return ( - +
+ + {footer} +
); }; diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx index 40e57cf..5f5b47b 100644 --- a/docs/viewer/components/loading/netcdf/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -258,6 +258,7 @@ const SliceTester: React.FC = ({ dimNames={rDims} varName={info.name} dtype={info.dtype} + totalShape={info.shape.map(Number)} /> )} From 72fd4ae43f513752af8beb31ab6e415ef0904c2e Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Mon, 2 Mar 2026 14:26:52 +0100 Subject: [PATCH 06/31] log slices --- src/netcdf-getters.ts | 24 +++++++++++++++++++++++- src/slice.ts | 11 +++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/netcdf-getters.ts b/src/netcdf-getters.ts index 6f39eef..7d4a27c 100644 --- a/src/netcdf-getters.ts +++ b/src/netcdf-getters.ts @@ -712,6 +712,28 @@ export function getVariableArrayWithSelection( const useStride = stride.some(s => s !== 1); const hasNegativeStep = resolved.some(d => d.step < 0); + // Validate stride parameters against dimension sizes + for (let i = 0; i < start.length; i++) { + const dimSize = Number(dimSizes[i]); + if (start[i] < 0 || start[i] >= dimSize) { + throw new Error( + `Invalid start[${i}] = ${start[i]} for dimension size ${dimSize}` + ); + } + if (count[i] <= 0) { + throw new Error( + `Invalid count[${i}] = ${count[i]} (must be > 0)` + ); + } + const lastIdx = start[i] + (count[i] - 1) * stride[i]; + if (lastIdx >= dimSize) { + throw new Error( + `Stride read would exceed dimension bounds: start[${i}]=${start[i]}, ` + + `count[${i}]=${count[i]}, stride[${i}]=${stride[i]}, last_idx=${lastIdx}, dimSize=${dimSize}` + ); + } + } + // Resolve variable type — use workingNcid so enum lookup is in the right group const { enumCtx } = resolveVariableType(module, workingNcid, varid); const arrayType = enumCtx.baseType; @@ -720,7 +742,7 @@ export function getVariableArrayWithSelection( // ── Read data ───────────────────────────────────────────────────────────── // All reads go directly through workingNcid + varid — no group re-resolution. - console.log('stride read', { start, count, stride, arrayType }); + console.log('stride read', { start, count, stride, dimSizes: dimSizes.map(Number), arrayType, hasNegativeStep }); if (enumCtx.isEnum) { if (useStride) { diff --git a/src/slice.ts b/src/slice.ts index e1b8cef..79e586d 100644 --- a/src/slice.ts +++ b/src/slice.ts @@ -81,7 +81,10 @@ export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): Reso const start = Math.max(0, Math.min(rawStart < 0 ? dimSize + rawStart : rawStart, dimSize)); const stop = Math.max(0, Math.min(rawStop < 0 ? dimSize + rawStop : rawStop, dimSize)); const span = Math.max(0, stop - start); - const count = Math.ceil(span / step); // ← actual element count + const count = span === 0 ? 0 : Math.ceil(span / step); + if (count < 0) { + throw new Error(`Invalid slice: positive step produced negative count (start=${start}, stop=${stop}, step=${step})`); + } return { start, count, step, collapsed: false }; } else { const rawStart = sel.start ?? dimSize - 1; @@ -89,7 +92,11 @@ export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): Reso const start = Math.max(0, Math.min(rawStart < 0 ? dimSize + rawStart : rawStart, dimSize - 1)); const stop = Math.max(-1, Math.min(rawStop < 0 ? dimSize + rawStop : rawStop, dimSize - 1)); const span = Math.max(0, start - stop); - const count = Math.ceil(span / Math.abs(step)); // ← actual element count + const count = span === 0 ? 0 : Math.ceil(span / Math.abs(step)); + if (count < 0) { + throw new Error(`Invalid slice: negative step produced negative count (start=${start}, stop=${stop}, step=${step})`); + } + // For negative step: we read from (stop+1) forward with positive stride, then reverse return { start: stop + 1, count, step, collapsed: false }; } } \ No newline at end of file From da788ce6a318fa63dfa4c739ed1cd166f4bd5320 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Mon, 2 Mar 2026 14:59:56 +0100 Subject: [PATCH 07/31] mv up --- docs/viewer/components/loading/netcdf/SliceTester.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx index 5f5b47b..d124be1 100644 --- a/docs/viewer/components/loading/netcdf/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -117,14 +117,15 @@ const SliceTester: React.FC = ({ className="w-full flex items-center justify-between p-3 hover:bg-accent/50 transition-colors cursor-pointer" >
- Slice & Index Tester + {/* use variable name in header instead of fixed title */} + {info.name} - shape: [{shape.join(', ')}] + [{shape.join(', ')}] Data viewer
{expandedSliceTester - ? - : + ? + : } @@ -197,6 +198,7 @@ const SliceTester: React.FC = ({ onChange={e => updateSel(i, { [key]: e.target.value })} className="h-7 text-xs w-20 font-mono" placeholder={placeholder} + {...(label === 'step' ? { min: 1, max: dimSize } : {})} />
))} From 9e1e81bcaca681230d704c0ba2e07121e56a80e9 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Mon, 2 Mar 2026 15:50:16 +0100 Subject: [PATCH 08/31] groups --- docs/viewer/app/globals.css | 10 +++ .../components/loading/netcdf/SliceTester.tsx | 68 ++++++++++++++----- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/docs/viewer/app/globals.css b/docs/viewer/app/globals.css index 84a086b..a9f59d2 100644 --- a/docs/viewer/app/globals.css +++ b/docs/viewer/app/globals.css @@ -186,6 +186,16 @@ } } +/* remove native number-input spinners across browsers */ +input[type=number] { + -moz-appearance: textfield; /* Firefox */ +} +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + .glow-effect { animation: glow-pulse 2s ease-in-out infinite; border-radius: 0.375rem; diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx index d124be1..6499189 100644 --- a/docs/viewer/components/loading/netcdf/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; +import { ButtonGroup } from '@/components/ui/button-group'; +import { PlusIcon, MinusIcon } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Terminal, ChevronRight, ChevronDown } from 'lucide-react'; @@ -106,6 +108,22 @@ const SliceTester: React.FC = ({ const updateSel = (i: number, patch: Partial) => setSliceSelections(prev => prev.map((s, idx) => idx === i ? { ...s, ...patch } : s)); + const changeBy = (i: number, key: keyof Omit, delta: number) => { + setSliceSelections(prev => prev.map((s, idx) => { + if (idx !== i) return s; + let val = parseInt(s[key] || '0'); + if (Number.isNaN(val)) val = 0; + val += delta; + // enforce bounds for step + if (key === 'step') { + if (val < 1) val = 1; + const dimSize = Number(shape[i]); + if (val > dimSize) val = dimSize; + } + return { ...s, [key]: String(val) } as SliceSelectionState; + })); + }; + const rShape = resultShape(sliceSelections, info.shape); const rDims = info.dimensions?.filter((_, i) => sliceSelections[i]?.mode !== 'scalar'); @@ -167,15 +185,23 @@ const SliceTester: React.FC = ({ {sel.mode === 'scalar' && (
index - updateSel(i, { scalar: e.target.value })} - className="h-7 text-xs w-28 font-mono" - placeholder="0" - /> + + + updateSel(i, { scalar: e.target.value })} + className="h-7 text-xs w-20 font-mono text-center appearance-none" + placeholder="0" + /> + + (0 … {dimSize - 1}, or negative) @@ -192,14 +218,22 @@ const SliceTester: React.FC = ({ ].map(({ label, key, placeholder }) => (
{label} - updateSel(i, { [key]: e.target.value })} - className="h-7 text-xs w-20 font-mono" - placeholder={placeholder} - {...(label === 'step' ? { min: 1, max: dimSize } : {})} - /> + + + updateSel(i, { [key]: e.target.value })} + className="h-7 text-xs w-20 font-mono text-center appearance-none" + placeholder={placeholder} + {...(label === 'step' ? { min: 1, max: dimSize } : {})} + /> + +
))}
From 46d6050d548075b19d4481c38663237889b51e87 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Mon, 2 Mar 2026 16:52:11 +0100 Subject: [PATCH 09/31] wide --- .../components/loading/netcdf/SliceTester.tsx | 217 +++++++++++------- 1 file changed, 131 insertions(+), 86 deletions(-) diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx index 6499189..9964438 100644 --- a/docs/viewer/components/loading/netcdf/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -26,37 +26,40 @@ export function defaultSelection(): SliceSelectionState { return { mode: 'all', scalar: '0', start: '0', stop: '', step: '1' }; } -// buildSelection — converts UI state → DimSelection[] for dataset.get() +const MODE_ACCENT: Record = { + all: 'border-l-muted-foreground/30', + scalar: 'border-l-blue-500', + slice: 'border-l-violet-500', +}; + +const MODE_BADGE: Record = { + all: 'text-muted-foreground/50', + scalar: 'text-blue-500', + slice: 'text-violet-500', +}; + export function buildSelection( sels: SliceSelectionState[], shape: Array ): Array> { return sels.map((s, i) => { const dimSize = Number(shape[i]); - if (s.mode === 'all') return null; - if (s.mode === 'scalar') { let idx = parseInt(s.scalar); if (Number.isNaN(idx)) idx = 0; - if (idx < 0) idx = dimSize + idx; - if (idx < 0 || idx >= dimSize) { + if (idx < -dimSize || idx >= dimSize) throw new Error(`index ${idx} out of bounds for dim ${i} size ${dimSize}`); - } + if (idx < 0) idx = dimSize + idx; return idx; } - let start = s.start !== '' ? parseInt(s.start) : 0; let stop = s.stop !== '' ? parseInt(s.stop) : dimSize; let step = s.step !== '' ? parseInt(s.step) : 1; - if (Number.isNaN(start)) start = 0; if (Number.isNaN(stop)) stop = dimSize; if (Number.isNaN(step)) step = 1; - - if (start < 0) start = dimSize + start; - if (stop < 0) stop = dimSize + stop; - + if (step === 0) step = 1; return ncSlice(start, stop, step); }); } @@ -73,11 +76,37 @@ export function resultShape( const start = s.start !== '' ? parseInt(s.start) : 0; const stop = s.stop !== '' ? parseInt(s.stop) : dimSize; const step = s.step !== '' ? parseInt(s.step) : 1; - out.push(Math.max(0, Math.ceil((stop - start) / step))); + const normStart = start < 0 ? Math.max(0, dimSize + start) : Math.min(start, dimSize); + const normStop = stop < 0 ? Math.max(0, dimSize + stop) : Math.min(stop, dimSize); + if (step === 0) { out.push(0); return; } + out.push(Math.max(0, Math.ceil((normStop - normStart) / Math.abs(step)))); }); return out; } +function dimElementCount(s: SliceSelectionState, dimSize: number): number | null { + if (s.mode === 'scalar') return null; + if (s.mode === 'all') return dimSize; + const start = s.start !== '' ? parseInt(s.start) : 0; + const stop = s.stop !== '' ? parseInt(s.stop) : dimSize; + const step = s.step !== '' ? parseInt(s.step) : 1; + if (Number.isNaN(start) || Number.isNaN(stop) || Number.isNaN(step) || step === 0) return null; + const normStart = start < 0 ? Math.max(0, dimSize + start) : Math.min(start, dimSize); + const normStop = stop < 0 ? Math.max(0, dimSize + stop) : Math.min(stop, dimSize); + return Math.max(0, Math.ceil((normStop - normStart) / Math.abs(step))); +} + +/** Format the dim badge: for slice → "start:step:stop", for all → count, for scalar → "collapsed" */ +function dimBadge(s: SliceSelectionState, dimSize: number): string | null { + if (s.mode === 'scalar') return 'collapsed'; + if (s.mode === 'all') return String(dimSize); + // slice: show start:step:stop + const start = s.start !== '' ? s.start : '0'; + const stop = s.stop !== '' ? s.stop : String(dimSize); + const step = s.step !== '' ? s.step : '1'; + return `${start}:${step}:${stop}`; +} + interface SliceTesterSectionProps { info: VariableInfo; sliceSelections: SliceSelectionState[]; @@ -108,17 +137,20 @@ const SliceTester: React.FC = ({ const updateSel = (i: number, patch: Partial) => setSliceSelections(prev => prev.map((s, idx) => idx === i ? { ...s, ...patch } : s)); - const changeBy = (i: number, key: keyof Omit, delta: number) => { + const changeBy = (i: number, key: keyof Omit, delta: number) => { setSliceSelections(prev => prev.map((s, idx) => { if (idx !== i) return s; + const dimSize = Number(shape[i]); let val = parseInt(s[key] || '0'); if (Number.isNaN(val)) val = 0; val += delta; - // enforce bounds for step if (key === 'step') { - if (val < 1) val = 1; - const dimSize = Number(shape[i]); - if (val > dimSize) val = dimSize; + if (val === 0) val = delta > 0 ? 1 : -1; + val = Math.max(-dimSize, Math.min(dimSize, val)); + } else { + const lo = -dimSize; + const hi = key === 'stop' ? dimSize : dimSize - 1; + val = Math.max(lo, Math.min(hi, val)); } return { ...s, [key]: String(val) } as SliceSelectionState; })); @@ -134,48 +166,60 @@ const SliceTester: React.FC = ({ onClick={() => setExpandedSliceTester(!expandedSliceTester)} className="w-full flex items-center justify-between p-3 hover:bg-accent/50 transition-colors cursor-pointer" > -
- {/* use variable name in header instead of fixed title */} +
{info.name} - + [{shape.join(', ')}] Data viewer
{expandedSliceTester - ? - : + ? + : } {expandedSliceTester && ( -
+
{/* Dimension rows */} -
+
{shape.map((dimSize, i) => { const dimName = info.dimensions?.[i] ?? `dim_${i}`; - const sel = sliceSelections[i] ?? defaultSelection(); + const sel = sliceSelections[i] ?? defaultSelection(); + const badge = dimBadge(sel, dimSize); return ( -
- {/* Label + mode tabs */} -
- - {dimName} - [{dimSize}] - -
+
+ {/* Row: dim name + badge + mode tabs */} +
+
+ + {dimName} + [{dimSize}] + + {badge !== null && ( + + → {badge} + + )} +
+ + {/* Mode tabs — right-aligned */} +
{(['all', 'scalar', 'slice'] as SelectionMode[]).map(m => ( ))}
@@ -183,10 +227,10 @@ const SliceTester: React.FC = ({ {/* Scalar input */} {sel.mode === 'scalar' && ( -
+
index - = ({ className="h-7 text-xs w-20 font-mono text-center appearance-none" placeholder="0" /> - - (0 … {dimSize - 1}, or negative) + ({-dimSize} to {dimSize - 1})
)} - {/* Slice inputs */} + {/* Slice inputs — all 3 in one responsive row, equal width */} {sel.mode === 'slice' && ( -
+
{[ - { label: 'start', key: 'start' as const, placeholder: '0' }, - { label: 'stop', key: 'stop' as const, placeholder: String(dimSize) }, - { label: 'step', key: 'step' as const, placeholder: '1' }, - ].map(({ label, key, placeholder }) => ( -
- {label} - - updateSel(i, { [key]: e.target.value })} - className="h-7 text-xs w-20 font-mono text-center appearance-none" + className="h-7 text-xs min-w-0 flex-1 font-mono text-center appearance-none" placeholder={placeholder} - {...(label === 'step' ? { min: 1, max: dimSize } : {})} /> - @@ -243,39 +288,39 @@ const SliceTester: React.FC = ({ })}
- {/* Selection preview */} -
- {`dataset.get("${info.name}", [${ - sliceSelections.map((s, i) => { - if (s.mode === 'all') return 'null'; - if (s.mode === 'scalar') return s.scalar || '0'; - const dimSize = shape[i]; - const parts: string[] = [s.start || '0', s.stop || String(dimSize)]; - if (s.step && s.step !== '1') parts.push(s.step); - return `slice(${parts.join(', ')})`; - }).join(', ') - }])`} -
- - {/* Run + result count */} -
- - {sliceResult && ( - - {sliceResult.length ?? 0} elements - - )} + {/* Selection preview + Run — two separate lines so Run never overflows */} +
+
+ {`dataset.get("${info.name}", [${ + sliceSelections.map((s, i) => { + if (s.mode === 'all') return 'null'; + if (s.mode === 'scalar') return s.scalar || '0'; + const dimSize = shape[i]; + const parts: string[] = [s.start || '0', s.stop || String(dimSize)]; + if (s.step && s.step !== '1') parts.push(s.step); + return `slice(${parts.join(', ')})`; + }).join(', ') + }])`} +
+
+ + {sliceResult && ( + + {sliceResult.length ?? 0} elements + + )} +
{/* Error */} From dc50599c4e009cc5e7daa0cc0d50b2d0d7f158b2 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Mon, 2 Mar 2026 17:31:06 +0100 Subject: [PATCH 10/31] colors --- .../components/loading/netcdf/SliceTester.tsx | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx index 9964438..c80f0ba 100644 --- a/docs/viewer/components/loading/netcdf/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -28,14 +28,14 @@ export function defaultSelection(): SliceSelectionState { const MODE_ACCENT: Record = { all: 'border-l-muted-foreground/30', - scalar: 'border-l-blue-500', - slice: 'border-l-violet-500', + scalar: 'border-l-teal-700', + slice: 'border-l-[#644FF0]', }; const MODE_BADGE: Record = { all: 'text-muted-foreground/50', - scalar: 'text-blue-500', - slice: 'text-violet-500', + scalar: 'text-teal-700', + slice: 'text-[#644FF0]', }; export function buildSelection( @@ -96,11 +96,9 @@ function dimElementCount(s: SliceSelectionState, dimSize: number): number | null return Math.max(0, Math.ceil((normStop - normStart) / Math.abs(step))); } -/** Format the dim badge: for slice → "start:step:stop", for all → count, for scalar → "collapsed" */ function dimBadge(s: SliceSelectionState, dimSize: number): string | null { - if (s.mode === 'scalar') return 'collapsed'; + if (s.mode === 'scalar') return s.scalar || '0'; // was 'collapsed' if (s.mode === 'all') return String(dimSize); - // slice: show start:step:stop const start = s.start !== '' ? s.start : '0'; const stop = s.stop !== '' ? s.stop : String(dimSize); const step = s.step !== '' ? s.step : '1'; @@ -227,10 +225,9 @@ const SliceTester: React.FC = ({ {/* Scalar input */} {sel.mode === 'scalar' && ( -
- index - - = ({ max={dimSize - 1} value={sel.scalar} onChange={e => updateSel(i, { scalar: e.target.value })} - className="h-7 text-xs w-20 font-mono text-center appearance-none" + className="h-7 text-xs w-16 font-mono text-center appearance-none" placeholder="0" /> - - + ({-dimSize} to {dimSize - 1})
)} - {/* Slice inputs — all 3 in one responsive row, equal width */} + {/* Slice inputs */} {sel.mode === 'slice' && ( -
+
{[ { label: 'start', key: 'start' as const, placeholder: '0', min: -dimSize, max: dimSize - 1 }, { label: 'step', key: 'step' as const, placeholder: '1', min: -dimSize, max: dimSize }, @@ -262,7 +259,7 @@ const SliceTester: React.FC = ({ ].map(({ label, key, placeholder, min, max }) => (
{label} - + @@ -272,7 +269,7 @@ const SliceTester: React.FC = ({ max={max} value={sel[key]} onChange={e => updateSel(i, { [key]: e.target.value })} - className="h-7 text-xs min-w-0 flex-1 font-mono text-center appearance-none" + className="h-7 text-xs w-16 font-mono text-center appearance-none" placeholder={placeholder} />
- {/* Selection preview + Run — two separate lines so Run never overflows */} + {/* Selection preview + Run */}
{`dataset.get("${info.name}", [${ @@ -302,7 +299,7 @@ const SliceTester: React.FC = ({ }).join(', ') }])`}
-
+
{sliceResult && ( - + {sliceResult.length ?? 0} elements )} From f1a26a10fad7c00e76a2515d08e16ee0a46a1fc8 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Mon, 2 Mar 2026 22:45:09 +0100 Subject: [PATCH 11/31] no --- .../components/loading/netcdf/SliceTester.tsx | 40 +++---- .../components/loading/netcdf/Viewer.tsx | 4 +- src/index.ts | 2 +- src/netcdf-getters.ts | 55 ++------- src/slice.ts | 111 ++++++++++++------ 5 files changed, 104 insertions(+), 108 deletions(-) diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx index c80f0ba..49d83be 100644 --- a/docs/viewer/components/loading/netcdf/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -7,7 +7,7 @@ import { PlusIcon, MinusIcon } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Terminal, ChevronRight, ChevronDown } from 'lucide-react'; -import { slice as ncSlice } from '@earthyscience/netcdf4-wasm'; +import { slice as ncSlice, all } from '@earthyscience/netcdf4-wasm'; import { VariableInfo, VariableArrayData } from './types'; import ArrayDisplay from './ArrayDisplay'; @@ -38,13 +38,14 @@ const MODE_BADGE: Record = { slice: 'text-[#644FF0]', }; +// Build canonical DimSelection array export function buildSelection( sels: SliceSelectionState[], shape: Array -): Array> { +) { return sels.map((s, i) => { const dimSize = Number(shape[i]); - if (s.mode === 'all') return null; + if (s.mode === 'all') return all(); if (s.mode === 'scalar') { let idx = parseInt(s.scalar); if (Number.isNaN(idx)) idx = 0; @@ -64,41 +65,28 @@ export function buildSelection( }); } +// Compute output shape export function resultShape( sels: SliceSelectionState[], shape: Array ): number[] { - const out: number[] = []; - sels.forEach((s, i) => { + return sels.map((s, i) => { const dimSize = Number(shape[i]); - if (s.mode === 'scalar') return; - if (s.mode === 'all') { out.push(dimSize); return; } + if (s.mode === 'scalar') return 0; // collapsed + if (s.mode === 'all') return dimSize; const start = s.start !== '' ? parseInt(s.start) : 0; const stop = s.stop !== '' ? parseInt(s.stop) : dimSize; const step = s.step !== '' ? parseInt(s.step) : 1; const normStart = start < 0 ? Math.max(0, dimSize + start) : Math.min(start, dimSize); - const normStop = stop < 0 ? Math.max(0, dimSize + stop) : Math.min(stop, dimSize); - if (step === 0) { out.push(0); return; } - out.push(Math.max(0, Math.ceil((normStop - normStart) / Math.abs(step)))); + const normStop = stop < 0 ? Math.max(0, dimSize + stop) : Math.min(stop, dimSize); + return step === 0 ? 0 : Math.max(0, Math.ceil((normStop - normStart) / Math.abs(step))); }); - return out; -} - -function dimElementCount(s: SliceSelectionState, dimSize: number): number | null { - if (s.mode === 'scalar') return null; - if (s.mode === 'all') return dimSize; - const start = s.start !== '' ? parseInt(s.start) : 0; - const stop = s.stop !== '' ? parseInt(s.stop) : dimSize; - const step = s.step !== '' ? parseInt(s.step) : 1; - if (Number.isNaN(start) || Number.isNaN(stop) || Number.isNaN(step) || step === 0) return null; - const normStart = start < 0 ? Math.max(0, dimSize + start) : Math.min(start, dimSize); - const normStop = stop < 0 ? Math.max(0, dimSize + stop) : Math.min(stop, dimSize); - return Math.max(0, Math.ceil((normStop - normStart) / Math.abs(step))); } +// Badge text for UI function dimBadge(s: SliceSelectionState, dimSize: number): string | null { - if (s.mode === 'scalar') return s.scalar || '0'; // was 'collapsed' - if (s.mode === 'all') return String(dimSize); + if (s.mode === 'scalar') return s.scalar || '0'; + if (s.mode === 'all') return 'all'; const start = s.start !== '' ? s.start : '0'; const stop = s.stop !== '' ? s.stop : String(dimSize); const step = s.step !== '' ? s.step : '1'; @@ -290,7 +278,7 @@ const SliceTester: React.FC = ({
{`dataset.get("${info.name}", [${ sliceSelections.map((s, i) => { - if (s.mode === 'all') return 'null'; + if (s.mode === 'all') return 'all'; if (s.mode === 'scalar') return s.scalar || '0'; const dimSize = shape[i]; const parts: string[] = [s.start || '0', s.stop || String(dimSize)]; diff --git a/docs/viewer/components/loading/netcdf/Viewer.tsx b/docs/viewer/components/loading/netcdf/Viewer.tsx index 8706dcc..0be5512 100644 --- a/docs/viewer/components/loading/netcdf/Viewer.tsx +++ b/docs/viewer/components/loading/netcdf/Viewer.tsx @@ -242,7 +242,7 @@ const Viewer = () => { )} {/* Variable Data Loader */} - {selectedVariable && variables[selectedVariable]?.info && ( + {/* {selectedVariable && variables[selectedVariable]?.info && ( { onLoadSlice={loadVariableSlice} onLoadAll={loadVariableData} /> - )} + )} */} {/* Slice Tester */} {selectedVariable && variables[selectedVariable]?.info && ( diff --git a/src/index.ts b/src/index.ts index 6e5bf02..7da1545 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ // Export all classes and types export { NetCDF4, DataTree } from './netcdf4.js'; -export { slice, Slice } from './slice.js'; +export { slice, Slice, all, isAll} from './slice.js'; export type { DimSelection, ResolvedDim } from './slice.js'; export type { GroupNode } from './netcdf4.js'; export { Variable } from './variable.js'; diff --git a/src/netcdf-getters.ts b/src/netcdf-getters.ts index 7d4a27c..059b733 100644 --- a/src/netcdf-getters.ts +++ b/src/netcdf-getters.ts @@ -709,10 +709,10 @@ export function getVariableArrayWithSelection( const count = resolved.map(d => d.count); const stride = resolved.map(d => Math.abs(d.step)); - const useStride = stride.some(s => s !== 1); const hasNegativeStep = resolved.some(d => d.step < 0); - // Validate stride parameters against dimension sizes + + // Validate parameters against dimension sizes for (let i = 0; i < start.length; i++) { const dimSize = Number(dimSizes[i]); if (start[i] < 0 || start[i] >= dimSize) { @@ -740,21 +740,19 @@ export function getVariableArrayWithSelection( let arrayData: { result: number; data?: any }; - // ── Read data ───────────────────────────────────────────────────────────── - // All reads go directly through workingNcid + varid — no group re-resolution. - console.log('stride read', { start, count, stride, dimSizes: dimSizes.map(Number), arrayType, hasNegativeStep }); + // Read data + // Always use nc_get_vars_* (strided) — nc_get_vara has marshalling issues + // with null/all selections. When stride is all-1s the netCDF-C library takes + // the same fast contiguous path internally, so there is no performance cost. + console.log('vars read', { start, count, stride, dimSizes: dimSizes.map(Number), arrayType, hasNegativeStep }); if (enumCtx.isEnum) { - if (useStride) { - arrayData = module.nc_get_vars_generic(workingNcid, varid, start, count, stride, arrayType); - } else { - arrayData = module.nc_get_vara_generic(workingNcid, varid, start, count, arrayType); - } - } else if (useStride) { + arrayData = module.nc_get_vars_generic(workingNcid, varid, start, count, stride, arrayType); + } else { type VarsArgs = [number, number, number[], number[], number[]]; type VarsResult = { result: number; data?: any }; - const stridedReaders: Record VarsResult> = { + const readers: Record VarsResult> = { [NC_CONSTANTS.NC_BYTE]: (...args) => module.nc_get_vars_schar(...args), [NC_CONSTANTS.NC_UBYTE]: (...args) => module.nc_get_vars_uchar(...args), [NC_CONSTANTS.NC_SHORT]: (...args) => module.nc_get_vars_short(...args), @@ -770,39 +768,12 @@ export function getVariableArrayWithSelection( [NC_CONSTANTS.NC_STRING]: (...args) => module.nc_get_vars_string(...args), }; - const reader = stridedReaders[arrayType]; - if (!reader) { - console.warn(`Unknown NetCDF type ${arrayType} for strided read, falling back to double`); - arrayData = module.nc_get_vars_double(workingNcid, varid, start, count, stride); - } else { - arrayData = reader(workingNcid, varid, start, count, stride); - } - } else { - type VaraArgs = [number, number, number[], number[]]; - type VaraResult = { result: number; data?: any }; - - const readers: Record VaraResult> = { - [NC_CONSTANTS.NC_BYTE]: (...args) => module.nc_get_vara_schar(...args), - [NC_CONSTANTS.NC_UBYTE]: (...args) => module.nc_get_vara_uchar(...args), - [NC_CONSTANTS.NC_SHORT]: (...args) => module.nc_get_vara_short(...args), - [NC_CONSTANTS.NC_USHORT]: (...args) => module.nc_get_vara_ushort(...args), - [NC_CONSTANTS.NC_INT]: (...args) => module.nc_get_vara_int(...args), - [NC_CONSTANTS.NC_UINT]: (...args) => module.nc_get_vara_uint(...args), - [NC_CONSTANTS.NC_FLOAT]: (...args) => module.nc_get_vara_float(...args), - [NC_CONSTANTS.NC_DOUBLE]: (...args) => module.nc_get_vara_double(...args), - [NC_CONSTANTS.NC_INT64]: (...args) => module.nc_get_vara_longlong(...args), - [NC_CONSTANTS.NC_LONGLONG]: (...args) => module.nc_get_vara_longlong(...args), - [NC_CONSTANTS.NC_UINT64]: (...args) => module.nc_get_vara_ulonglong(...args), - [NC_CONSTANTS.NC_ULONGLONG]: (...args) => module.nc_get_vara_ulonglong(...args), - [NC_CONSTANTS.NC_STRING]: (...args) => module.nc_get_vara_string(...args), - }; - const reader = readers[arrayType]; if (!reader) { console.warn(`Unknown NetCDF type ${arrayType}, falling back to double`); - arrayData = module.nc_get_vara_double(workingNcid, varid, start, count); + arrayData = module.nc_get_vars_double(workingNcid, varid, start, count, stride); } else { - arrayData = reader(workingNcid, varid, start, count); + arrayData = reader(workingNcid, varid, start, count, stride); } } @@ -810,7 +781,7 @@ export function getVariableArrayWithSelection( throw new Error(`Failed to read array data (error: ${arrayData.result})`); } if (!arrayData.data) { - throw new Error("nc_get_vara/vars returned no data"); + throw new Error("nc_get_vars returned no data"); } let result: AnyTypedArray = arrayData.data; diff --git a/src/slice.ts b/src/slice.ts index 79e586d..ca4353c 100644 --- a/src/slice.ts +++ b/src/slice.ts @@ -1,12 +1,28 @@ +/** + * Represents selecting the entire dimension. + */ +export interface All { + readonly type: "all"; +} + +/** Canonical full-dimension selector */ +export function all(): All { + return { type: "all" }; +} + +export function isAll(v: unknown): v is All { + return typeof v === "object" && v !== null && (v as any).type === "all"; +} + /** * Represents a dimension slice. - * Null fields are resolved against the actual dimension size at read time. + * Undefined fields are resolved against the actual dimension size at read time. */ export class Slice { constructor( - public readonly start: number | null, - public readonly stop: number | null, - public readonly step: number | null + public readonly start?: number, + public readonly stop?: number, + public readonly step?: number ) {} } @@ -14,24 +30,28 @@ export class Slice { * slice(stop) * slice(start, stop) * slice(start, stop, step) - * slice(null) — equivalent to null (all elements) */ -export function slice(stop: number | null): Slice; -export function slice(start: number | null, stop: number | null): Slice; -export function slice(start: number | null, stop: number | null, step: number | null): Slice; +export function slice(stop: number): Slice; +export function slice(start: number, stop: number): Slice; +export function slice(start: number, stop: number, step: number): Slice; export function slice( - startOrStop: number | null, - stop?: number | null, - step?: number | null + startOrStop: number, + stop?: number, + step?: number ): Slice { if (stop === undefined) { - return new Slice(null, startOrStop, null); + return new Slice(undefined, startOrStop, undefined); } - return new Slice(startOrStop, stop ?? null, step ?? null); + return new Slice(startOrStop, stop, step); } -/** A single dimension selection: null = all, number = scalar index, Slice = range */ -export type DimSelection = null | number | Slice; +/** + * A single dimension selection: + * - all() → full dimension + * - number → scalar index + * - Slice → range selection + */ +export type DimSelection = All | number | Slice; /** * Resolved, concrete read parameters for one dimension after applying a @@ -40,10 +60,13 @@ export type DimSelection = null | number | Slice; export interface ResolvedDim { /** Start index in the NetCDF dimension (always the lower bound, even for negative step) */ start: number; + /** Number of elements to read from NetCDF (contiguous span, always positive) */ count: number; + /** Step. Positive = forward, negative = reversed output. Never 0. */ step: number; + /** True when the selection was a scalar index — dimension is collapsed */ collapsed: boolean; } @@ -53,18 +76,15 @@ export interface ResolvedDim { * Accepts BigInt dimension sizes (from nc_inq_dimlen i64 reads) and coerces safely. */ export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): ResolvedDim { - // Coerce up front — nc_inq_dimlen returns i64 so dimSize may arrive as BigInt + const dimSize = Number(dimSizeRaw); - // null or empty Slice → full dimension - if ( - sel === null || - (sel instanceof Slice && sel.start === null && sel.stop === null && sel.step === null) - ) { + // full dimension + if (isAll(sel)) { return { start: 0, count: dimSize, step: 1, collapsed: false }; } - // Scalar index → collapsed dimension + // scalar index if (typeof sel === "number") { const idx = sel < 0 ? dimSize + sel : sel; const clamped = Math.max(0, Math.min(idx, dimSize - 1)); @@ -76,27 +96,44 @@ export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): Reso if (step === 0) throw new Error("Slice step cannot be zero"); if (step > 0) { + const rawStart = sel.start ?? 0; const rawStop = sel.stop ?? dimSize; - const start = Math.max(0, Math.min(rawStart < 0 ? dimSize + rawStart : rawStart, dimSize)); - const stop = Math.max(0, Math.min(rawStop < 0 ? dimSize + rawStop : rawStop, dimSize)); - const span = Math.max(0, stop - start); - const count = span === 0 ? 0 : Math.ceil(span / step); - if (count < 0) { - throw new Error(`Invalid slice: positive step produced negative count (start=${start}, stop=${stop}, step=${step})`); - } + + const start = Math.max( + 0, + Math.min(rawStart < 0 ? dimSize + rawStart : rawStart, dimSize) + ); + + const stop = Math.max( + 0, + Math.min(rawStop < 0 ? dimSize + rawStop : rawStop, dimSize) + ); + + const span = Math.max(0, stop - start); + const count = span === 0 ? 0 : Math.ceil(span / step); + return { start, count, step, collapsed: false }; + } else { + const rawStart = sel.start ?? dimSize - 1; const rawStop = sel.stop ?? -1; - const start = Math.max(0, Math.min(rawStart < 0 ? dimSize + rawStart : rawStart, dimSize - 1)); - const stop = Math.max(-1, Math.min(rawStop < 0 ? dimSize + rawStop : rawStop, dimSize - 1)); - const span = Math.max(0, start - stop); - const count = span === 0 ? 0 : Math.ceil(span / Math.abs(step)); - if (count < 0) { - throw new Error(`Invalid slice: negative step produced negative count (start=${start}, stop=${stop}, step=${step})`); - } - // For negative step: we read from (stop+1) forward with positive stride, then reverse + + const start = Math.max( + 0, + Math.min(rawStart < 0 ? dimSize + rawStart : rawStart, dimSize - 1) + ); + + const stop = Math.max( + -1, + Math.min(rawStop < 0 ? dimSize + rawStop : rawStop, dimSize - 1) + ); + + const span = Math.max(0, start - stop); + const count = span === 0 ? 0 : Math.ceil(span / Math.abs(step)); + + // For negative step: read forward then reverse later return { start: stop + 1, count, step, collapsed: false }; } } \ No newline at end of file From 9b858a81f1cf10db58a677afe13cb4fcbe1103b3 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 3 Mar 2026 09:18:49 +0100 Subject: [PATCH 12/31] all fix --- .../components/loading/netcdf/SliceTester.tsx | 6 ++++-- src/__tests__/test-slice.test.ts | 16 ++++++++++++++++ src/netcdf-getters.ts | 9 +++++++++ src/slice.ts | 12 ++++++------ src/wasm-module.ts | 15 +++++++++++---- 5 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 src/__tests__/test-slice.test.ts diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx index 49d83be..63eb1ce 100644 --- a/docs/viewer/components/loading/netcdf/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -7,7 +7,7 @@ import { PlusIcon, MinusIcon } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Terminal, ChevronRight, ChevronDown } from 'lucide-react'; -import { slice as ncSlice, all } from '@earthyscience/netcdf4-wasm'; +import { slice as ncSlice } from '@earthyscience/netcdf4-wasm'; import { VariableInfo, VariableArrayData } from './types'; import ArrayDisplay from './ArrayDisplay'; @@ -45,7 +45,9 @@ export function buildSelection( ) { return sels.map((s, i) => { const dimSize = Number(shape[i]); - if (s.mode === 'all') return all(); + // Avoid relying on `all()` runtime shape across package versions; + // canonicalize "all" to an explicit full slice. + if (s.mode === 'all') return ncSlice(0, dimSize, 1); if (s.mode === 'scalar') { let idx = parseInt(s.scalar); if (Number.isNaN(idx)) idx = 0; diff --git a/src/__tests__/test-slice.test.ts b/src/__tests__/test-slice.test.ts new file mode 100644 index 0000000..7b30631 --- /dev/null +++ b/src/__tests__/test-slice.test.ts @@ -0,0 +1,16 @@ +import { all, slice, resolveDim } from '../slice.js'; + +describe('slice selection compatibility', () => { + test('all() behaves like slice(0, dimSize, 1)', () => { + const dimSize = 17; + expect(resolveDim(all(), dimSize)).toEqual(resolveDim(slice(0, dimSize, 1), dimSize)); + }); + + test('null and "all" behave like all()', () => { + const dimSize = 5; + const expected = resolveDim(all(), dimSize); + expect(resolveDim(null, dimSize)).toEqual(expected); + expect(resolveDim("all", dimSize)).toEqual(expected); + }); +}); + diff --git a/src/netcdf-getters.ts b/src/netcdf-getters.ts index 059b733..c9dfa9a 100644 --- a/src/netcdf-getters.ts +++ b/src/netcdf-getters.ts @@ -690,10 +690,18 @@ export function getVariableArrayWithSelection( `Selection has ${selection.length} dimension(s) but variable has ${ndim}` ); } + console.log('[varInfo]', { + dimids: Array.from(varInfo.dimids ?? []), + varid, + workingNcid, + varInfoRaw: JSON.stringify(varInfo) + }); // Fetch each dimension's size — resolveDim handles BigInt safely const dimSizes = dimids.map((dimid: number) => { + console.log('[nc_inq_dimlen] calling with', { workingNcid, dimid }); const r = module.nc_inq_dimlen(workingNcid, dimid); + console.log('[nc_inq_dimlen raw]', r); if (r.result !== NC_CONSTANTS.NC_NOERR) { throw new Error(`Failed to query dim length for dimid ${dimid} (error: ${r.result})`); } @@ -715,6 +723,7 @@ export function getVariableArrayWithSelection( // Validate parameters against dimension sizes for (let i = 0; i < start.length; i++) { const dimSize = Number(dimSizes[i]); + console.log(`[validate dim ${i}]`, { start: start[i], count: count[i], stride: stride[i], dimSize, lastIdx: start[i] + (count[i] - 1) * stride[i] }); if (start[i] < 0 || start[i] >= dimSize) { throw new Error( `Invalid start[${i}] = ${start[i]} for dimension size ${dimSize}` diff --git a/src/slice.ts b/src/slice.ts index ca4353c..ba18754 100644 --- a/src/slice.ts +++ b/src/slice.ts @@ -48,10 +48,12 @@ export function slice( /** * A single dimension selection: * - all() → full dimension + * - "all" → full dimension (compat) + * - null → full dimension (compat; matches older docs/examples) * - number → scalar index * - Slice → range selection */ -export type DimSelection = All | number | Slice; +export type DimSelection = All | "all" | null | number | Slice; /** * Resolved, concrete read parameters for one dimension after applying a @@ -78,9 +80,8 @@ export interface ResolvedDim { export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): ResolvedDim { const dimSize = Number(dimSizeRaw); - // full dimension - if (isAll(sel)) { + if (sel === null || sel === "all" || isAll(sel)) { return { start: 0, count: dimSize, step: 1, collapsed: false }; } @@ -112,7 +113,6 @@ export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): Reso const span = Math.max(0, stop - start); const count = span === 0 ? 0 : Math.ceil(span / step); - return { start, count, step, collapsed: false }; } else { @@ -132,8 +132,8 @@ export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): Reso const span = Math.max(0, start - stop); const count = span === 0 ? 0 : Math.ceil(span / Math.abs(step)); - + const result = { start: stop + 1, count, step, collapsed: false }; // For negative step: read forward then reverse later - return { start: stop + 1, count, step, collapsed: false }; + return result; } } \ No newline at end of file diff --git a/src/wasm-module.ts b/src/wasm-module.ts index 777e81d..b7d8036 100644 --- a/src/wasm-module.ts +++ b/src/wasm-module.ts @@ -231,12 +231,14 @@ export class WasmModuleLoader { nc_inq_dim: (ncid: number, dimid: number) => { const namePtr = module._malloc(NC_MAX_NAME + 1); - const lenPtr = module._malloc(8); // size_t (use 8 bytes for safety in wasm64) + // In Emscripten wasm32 builds, size_t is 32-bit. + // (If we ever build wasm64, this will need revisiting.) + const lenPtr = module._malloc(4); const result = nc_inq_dim_wrapper(ncid, dimid, namePtr, lenPtr); let name, len; if (result === NC_CONSTANTS.NC_NOERR) { name = module.UTF8ToString(namePtr); - len = module.getValue(lenPtr, 'i32'); // or 'i32' if your build uses 32-bit size_t + len = module.getValue(lenPtr, 'i32') >>> 0; } module._free(namePtr); module._free(lenPtr); @@ -252,9 +254,14 @@ export class WasmModuleLoader { }, nc_inq_dimlen: (ncid: number, dimid: number) => { - const lenPtr = module._malloc(8); + // In Emscripten wasm32 builds, size_t is 32-bit. + // Allocate 4 bytes and read as unsigned i32 to avoid garbage high bits. + const lenPtr = module._malloc(4); const result = nc_inq_dimlen_wrapper(ncid, dimid, lenPtr); - const len = result === NC_CONSTANTS.NC_NOERR ? module.getValue(lenPtr, 'i64') : undefined; + let len: number | undefined; + if (result === NC_CONSTANTS.NC_NOERR) { + len = module.getValue(lenPtr, 'i32') >>> 0; + } module._free(lenPtr); return { result, len }; }, From 137c436a214c2099e2e73107058d01bb06ce0a0b Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 3 Mar 2026 09:48:40 +0100 Subject: [PATCH 13/31] scalar --- docs/viewer/components/loading/netcdf/SliceTester.tsx | 10 ++++++---- src/slice.ts | 10 ++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx index 63eb1ce..d69c05f 100644 --- a/docs/viewer/components/loading/netcdf/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -72,16 +72,18 @@ export function resultShape( sels: SliceSelectionState[], shape: Array ): number[] { - return sels.map((s, i) => { + // Only include non-collapsed dimensions; scalar indices remove that dimension. + return sels.flatMap((s, i) => { const dimSize = Number(shape[i]); - if (s.mode === 'scalar') return 0; // collapsed - if (s.mode === 'all') return dimSize; + if (s.mode === 'scalar') return []; + if (s.mode === 'all') return [dimSize]; const start = s.start !== '' ? parseInt(s.start) : 0; const stop = s.stop !== '' ? parseInt(s.stop) : dimSize; const step = s.step !== '' ? parseInt(s.step) : 1; const normStart = start < 0 ? Math.max(0, dimSize + start) : Math.min(start, dimSize); const normStop = stop < 0 ? Math.max(0, dimSize + stop) : Math.min(stop, dimSize); - return step === 0 ? 0 : Math.max(0, Math.ceil((normStop - normStart) / Math.abs(step))); + const n = step === 0 ? 0 : Math.max(0, Math.ceil((normStop - normStart) / Math.abs(step))); + return [n]; }); } diff --git a/src/slice.ts b/src/slice.ts index ba18754..3b39ec8 100644 --- a/src/slice.ts +++ b/src/slice.ts @@ -118,7 +118,13 @@ export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): Reso } else { const rawStart = sel.start ?? dimSize - 1; - const rawStop = sel.stop ?? -1; + // For negative step, Python's default stop is -1 (exclusive, "before index 0"), + // but only when stop is omitted. If the user explicitly passes -1, it should + // be treated as an index (-1 -> dimSize-1), which may yield an empty slice. + const rawStopProvided = sel.stop; + const rawStop = rawStopProvided === undefined + ? -1 + : (rawStopProvided < 0 ? dimSize + rawStopProvided : rawStopProvided); const start = Math.max( 0, @@ -127,7 +133,7 @@ export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): Reso const stop = Math.max( -1, - Math.min(rawStop < 0 ? dimSize + rawStop : rawStop, dimSize - 1) + Math.min(rawStop, dimSize - 1) ); const span = Math.max(0, start - stop); From cb908d0c61bd0b937b851e15955d47e4ba4f1528 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 3 Mar 2026 11:08:04 +0100 Subject: [PATCH 14/31] resolved --- .../components/loading/netcdf/SliceTester.tsx | 76 +++++++++++++++---- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx index d69c05f..5224cdc 100644 --- a/docs/viewer/components/loading/netcdf/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -45,8 +45,6 @@ export function buildSelection( ) { return sels.map((s, i) => { const dimSize = Number(shape[i]); - // Avoid relying on `all()` runtime shape across package versions; - // canonicalize "all" to an explicit full slice. if (s.mode === 'all') return ncSlice(0, dimSize, 1); if (s.mode === 'scalar') { let idx = parseInt(s.scalar); @@ -63,6 +61,9 @@ export function buildSelection( if (Number.isNaN(stop)) stop = dimSize; if (Number.isNaN(step)) step = 1; if (step === 0) step = 1; + // Resolve negative indices to absolute positions — ncSlice does not handle them + if (start < 0) start = Math.max(0, dimSize + start); + if (stop < 0) stop = Math.max(0, dimSize + stop); return ncSlice(start, stop, step); }); } @@ -72,7 +73,6 @@ export function resultShape( sels: SliceSelectionState[], shape: Array ): number[] { - // Only include non-collapsed dimensions; scalar indices remove that dimension. return sels.flatMap((s, i) => { const dimSize = Number(shape[i]); if (s.mode === 'scalar') return []; @@ -81,13 +81,14 @@ export function resultShape( const stop = s.stop !== '' ? parseInt(s.stop) : dimSize; const step = s.step !== '' ? parseInt(s.step) : 1; const normStart = start < 0 ? Math.max(0, dimSize + start) : Math.min(start, dimSize); - const normStop = stop < 0 ? Math.max(0, dimSize + stop) : Math.min(stop, dimSize); - const n = step === 0 ? 0 : Math.max(0, Math.ceil((normStop - normStart) / Math.abs(step))); + const normStop = stop < 0 ? Math.max(0, dimSize + stop) : Math.min(stop, dimSize); + const absStep = Math.abs(step === 0 ? 1 : step); + const n = Math.max(0, Math.ceil(Math.abs(normStop - normStart) / absStep)); return [n]; }); } -// Badge text for UI +// Badge text for UI — shows raw user input function dimBadge(s: SliceSelectionState, dimSize: number): string | null { if (s.mode === 'scalar') return s.scalar || '0'; if (s.mode === 'all') return 'all'; @@ -97,6 +98,29 @@ function dimBadge(s: SliceSelectionState, dimSize: number): string | null { return `${start}:${step}:${stop}`; } +// Resolved badge — shows absolute indices after negative resolution, only when they differ +function resolvedBadge(s: SliceSelectionState, dimSize: number): string | null { + if (s.mode !== 'slice') return null; + const rawStart = s.start !== '' ? parseInt(s.start) : 0; + const rawStop = s.stop !== '' ? parseInt(s.stop) : dimSize; + const step = s.step !== '' ? s.step : '1'; + if (Number.isNaN(rawStart) || Number.isNaN(rawStop)) return null; + const absStart = rawStart < 0 ? Math.max(0, dimSize + rawStart) : rawStart; + const absStop = rawStop < 0 ? Math.max(0, dimSize + rawStop) : rawStop; + if (absStart === rawStart && absStop === rawStop) return null; + return `${absStart}:${step}:${absStop}`; +} + +// Step is always positive — negative indices resolve to absolute positions, +// so traversal is always forward. +function clampStep(sel: SliceSelectionState, dimSize: number): SliceSelectionState { + if (sel.mode !== 'slice') return sel; + let step = sel.step !== '' ? parseInt(sel.step) : 1; + if (Number.isNaN(step) || step <= 0) step = 1; + const clamped = Math.min(dimSize, step); + return sel.step === String(clamped) ? sel : { ...sel, step: String(clamped) }; +} + interface SliceTesterSectionProps { info: VariableInfo; sliceSelections: SliceSelectionState[]; @@ -124,25 +148,42 @@ const SliceTester: React.FC = ({ const shape: number[] = info.shape.map(Number); + const parseOr = (v: string, fallback: number) => { + const n = parseInt(v); + return Number.isNaN(n) ? fallback : n; + }; + + // Apply a partial patch to dimension i, then normalize step direction on the result const updateSel = (i: number, patch: Partial) => - setSliceSelections(prev => prev.map((s, idx) => idx === i ? { ...s, ...patch } : s)); + setSliceSelections(prev => prev.map((s, idx) => { + if (idx !== i) return s; + const next = { ...s, ...patch }; + // Normalize AFTER applying the full patch so direction is evaluated correctly + return clampStep(next, Number(shape[i])); + })); const changeBy = (i: number, key: keyof Omit, delta: number) => { setSliceSelections(prev => prev.map((s, idx) => { if (idx !== i) return s; const dimSize = Number(shape[i]); - let val = parseInt(s[key] || '0'); - if (Number.isNaN(val)) val = 0; - val += delta; + if (key === 'step') { - if (val === 0) val = delta > 0 ? 1 : -1; - val = Math.max(-dimSize, Math.min(dimSize, val)); + let step = s.step !== '' ? parseOr(s.step, 1) : 1; + if (step <= 0) step = 1; + const nextStep = Math.max(1, Math.min(dimSize, step + delta)); + const next = { ...s, step: String(nextStep) }; + return clampStep(next, dimSize); } else { + let val = parseInt(s[key] || '0'); + if (Number.isNaN(val)) val = 0; + val += delta; const lo = -dimSize; const hi = key === 'stop' ? dimSize : dimSize - 1; val = Math.max(lo, Math.min(hi, val)); + // Apply then normalize — this is what fixes the direction-flip on start/stop changes + const next = { ...s, [key]: String(val) } as SliceSelectionState; + return clampStep(next, dimSize); } - return { ...s, [key]: String(val) } as SliceSelectionState; })); }; @@ -177,7 +218,7 @@ const SliceTester: React.FC = ({ const dimName = info.dimensions?.[i] ?? `dim_${i}`; const sel = sliceSelections[i] ?? defaultSelection(); const badge = dimBadge(sel, dimSize); - + const resBadge = resolvedBadge(sel, dimSize); return (
= ({ → {badge} )} + {resBadge !== null && ( + + [{resBadge}] + + )}
{/* Mode tabs — right-aligned */} @@ -246,8 +292,8 @@ const SliceTester: React.FC = ({
{[ { label: 'start', key: 'start' as const, placeholder: '0', min: -dimSize, max: dimSize - 1 }, - { label: 'step', key: 'step' as const, placeholder: '1', min: -dimSize, max: dimSize }, { label: 'stop', key: 'stop' as const, placeholder: String(dimSize), min: -dimSize, max: dimSize }, + { label: 'step', key: 'step' as const, placeholder: '1', min: 1, max: dimSize }, ].map(({ label, key, placeholder, min, max }) => (
{label} From e5b1ba87258fba75a764327bdd8f6e9cbca57b8a Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 3 Mar 2026 11:13:52 +0100 Subject: [PATCH 15/31] steps --- docs/viewer/components/loading/netcdf/SliceTester.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx index 5224cdc..d1c9a51 100644 --- a/docs/viewer/components/loading/netcdf/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -64,6 +64,8 @@ export function buildSelection( // Resolve negative indices to absolute positions — ncSlice does not handle them if (start < 0) start = Math.max(0, dimSize + start); if (stop < 0) stop = Math.max(0, dimSize + stop); + // Ensure start <= stop since step is always positive + if (start > stop) { const tmp = start; start = stop; stop = tmp; } return ncSlice(start, stop, step); }); } @@ -107,8 +109,9 @@ function resolvedBadge(s: SliceSelectionState, dimSize: number): string | null { if (Number.isNaN(rawStart) || Number.isNaN(rawStop)) return null; const absStart = rawStart < 0 ? Math.max(0, dimSize + rawStart) : rawStart; const absStop = rawStop < 0 ? Math.max(0, dimSize + rawStop) : rawStop; - if (absStart === rawStart && absStop === rawStop) return null; - return `${absStart}:${step}:${absStop}`; + const [resolvedStart, resolvedStop] = absStart > absStop ? [absStop, absStart] : [absStart, absStop]; + if (resolvedStart === rawStart && resolvedStop === rawStop) return null; + return `${resolvedStart}:${step}:${resolvedStop}`; } // Step is always positive — negative indices resolve to absolute positions, From df5f491f7a570fcc8a5ba7aeb40c7ba4eae8443c Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 3 Mar 2026 12:22:07 +0100 Subject: [PATCH 16/31] scalar --- docs/viewer/components/loading/netcdf/SliceTester.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/viewer/components/loading/netcdf/SliceTester.tsx b/docs/viewer/components/loading/netcdf/SliceTester.tsx index d1c9a51..2b45448 100644 --- a/docs/viewer/components/loading/netcdf/SliceTester.tsx +++ b/docs/viewer/components/loading/netcdf/SliceTester.tsx @@ -102,6 +102,11 @@ function dimBadge(s: SliceSelectionState, dimSize: number): string | null { // Resolved badge — shows absolute indices after negative resolution, only when they differ function resolvedBadge(s: SliceSelectionState, dimSize: number): string | null { + if (s.mode === 'scalar') { + const raw = parseInt(s.scalar); + if (Number.isNaN(raw) || raw >= 0) return null; + return String(dimSize + raw); + } if (s.mode !== 'slice') return null; const rawStart = s.start !== '' ? parseInt(s.start) : 0; const rawStop = s.stop !== '' ? parseInt(s.stop) : dimSize; From d093abca038e8d697806a6220450936fc3fff325 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 3 Mar 2026 16:57:15 +0100 Subject: [PATCH 17/31] more --- docs/viewer/components/loading/netcdf/ArrayDisplay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx b/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx index 710ede7..d7eee81 100644 --- a/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx +++ b/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx @@ -26,7 +26,7 @@ function fmtVal(v: number | bigint | string, dtype?: string): string { const abs = Math.abs(v); if (abs === 0) return '0'; if (abs >= 1e5 || (abs < 1e-3 && abs > 0)) return v.toExponential(3); - return v.toPrecision(6).replace(/\.?0+$/, ''); + return v.toPrecision(8).replace(/\.?0+$/, ''); } function lpad(s: string, w: number) { From 48fab6524aaa9c9403e6dfe3e883fb298586bd58 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Tue, 3 Mar 2026 22:12:07 +0100 Subject: [PATCH 18/31] comments --- scripts/build-wasm.sh | 6 ++---- src/slice.ts | 3 --- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index a99ad75..b48b3e5 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -963,10 +963,7 @@ int nc_get_var_ulonglong_wrapper(int ncid, int varid, unsigned long long* value) return nc_get_var_ulonglong(ncid, varid, value); } -// ========================= // Strided Data Access (nc_get_vars_*) -// Add these alongside your existing nc_get_vara_*_wrapper functions -// ========================= EMSCRIPTEN_KEEPALIVE int nc_get_vars_schar_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, signed char* value) { @@ -1023,7 +1020,7 @@ int nc_get_vars_string_wrapper(int ncid, int varid, const size_t* start, const s return nc_get_vars_string(ncid, varid, start, count, stride, value); } -// Generic strided wrapper — mirrors your existing nc_get_vara_wrapper +// Generic strided wrapper — mirrors existing nc_get_vara_wrapper // Uses nc_get_vars which accepts void* output and nc_type for dispatch EMSCRIPTEN_KEEPALIVE int nc_get_vars_wrapper(int ncid, int varid, const size_t* start, const size_t* count, const ptrdiff_t* stride, void* value) { @@ -1044,6 +1041,7 @@ int nc_get_vars_wrapper(int ncid, int varid, const size_t* start, const size_t* case NC_DOUBLE: return nc_get_vars_double(ncid, varid, start, count, stride, (double*)value); case NC_INT64: return nc_get_vars_longlong(ncid, varid, start, count, stride, (long long*)value); case NC_UINT64: return nc_get_vars_ulonglong(ncid, varid, start, count, stride, (unsigned long long*)value); + case NC_STRING: return nc_get_vars_string(ncid, varid, start, count, stride, (char**)value); default: return NC_EBADTYPE; } } diff --git a/src/slice.ts b/src/slice.ts index 3b39ec8..ce90015 100644 --- a/src/slice.ts +++ b/src/slice.ts @@ -118,9 +118,6 @@ export function resolveDim(sel: DimSelection, dimSizeRaw: number | bigint): Reso } else { const rawStart = sel.start ?? dimSize - 1; - // For negative step, Python's default stop is -1 (exclusive, "before index 0"), - // but only when stop is omitted. If the user explicitly passes -1, it should - // be treated as an index (-1 -> dimSize-1), which may yield an empty slice. const rawStopProvided = sel.stop; const rawStop = rawStopProvided === undefined ? -1 From 26b6a204cc10ccec1dd289f7e2efa03c96aba22b Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Wed, 4 Mar 2026 09:27:58 +0100 Subject: [PATCH 19/31] scroll --- .../loading/netcdf/ArrayDisplay.tsx | 825 +++++++++++------- 1 file changed, 503 insertions(+), 322 deletions(-) diff --git a/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx b/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx index d7eee81..4661f98 100644 --- a/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx +++ b/docs/viewer/components/loading/netcdf/ArrayDisplay.tsx @@ -1,424 +1,605 @@ -'use client'; -import React, { useMemo, useState } from 'react'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; +import React, { useState, useRef, useCallback, useMemo, useEffect, type RefObject } from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; + +// ─── Theme / Config ─────────────────────────────────────────────────────────── + +const THEME = { + colors: { + value: "#e2e8f0", + muted: "#64748b", + accent: "#94a3b8", + varName: "#a78bfa", + dims: ["#a78bfa", "#f87171", "#fb923c", "#facc15"] as const, + }, + font: { + family: "monospace", + size: 12, + sizeXs: 11, + }, +} as const; + +const CONFIG = { + fadePx: 48, + cellH: 18, + chW: 8, + overscan: 3, + maxViewH: 220, + minViewW: 80, + scrollSlop: 1, // px threshold to show edge fade + rhPadRight: 12, + colCellPad: 8, + colHeadPad: 4, + rhExtraChar: 2, + rhPadPx: 12, +} as const; // ─── Types ──────────────────────────────────────────────────────────────────── -interface ArrayDisplayProps { +type Dtype = string | undefined; + +type ScrollEdges = { + scrollLeft: number; + scrollTop: number; + L: boolean; + R: boolean; + T: boolean; + B: boolean; +}; + +interface FrozenMatrixProps { + data: ArrayLike; + offset: number; + rows: number; + cols: number; + rowHeaders: string[]; + colHeaders: string[]; + rowDim: string; + colDim: string; + dtype: Dtype; + showHeader: boolean; + containerWidth: number; +} + +interface MatrixDisplayProps { + data: ArrayLike; + rows: number; + cols: number; + rowDim: string; + colDim: string; + dtype: Dtype; + offset?: number; + showHeader?: boolean; + containerWidth: number; +} + +interface VectorDisplayProps { + data: ArrayLike; + len: number; + dimName: string; + dtype: Dtype; + containerWidth: number; +} + +interface NDDisplayProps { + data: ArrayLike; + shape: number[]; + dimNames: string[]; + dtype: Dtype; + containerWidth: number; +} + +interface FooterProps { + varName?: string; + shape: number[]; + totalShape?: number[]; + dtype: Dtype; +} + +export interface ArrayDisplayProps { data: ArrayLike; shape: number[]; dimNames?: string[]; varName?: string; - dtype?: string; - /** Original full shape before slicing, for the footer comparison */ + dtype?: Dtype; totalShape?: number[]; - maxRows?: number; - maxCols?: number; } // ─── Helpers ────────────────────────────────────────────────────────────────── -function fmtVal(v: number | bigint | string, dtype?: string): string { - if (typeof v === 'bigint') return String(v); - if (typeof v === 'string') return v; +function fmtVal(v: number | bigint | string, dtype: Dtype): string { + if (typeof v === "bigint") return String(v); + if (typeof v === "string") return v; if (!Number.isFinite(v)) return String(v); - if (dtype?.startsWith('int') || dtype?.startsWith('uint')) return String(Math.trunc(v)); + if (dtype?.startsWith("int") || dtype?.startsWith("uint")) return String(Math.trunc(v)); const abs = Math.abs(v); - if (abs === 0) return '0'; + if (abs === 0) return "0"; if (abs >= 1e5 || (abs < 1e-3 && abs > 0)) return v.toExponential(3); - return v.toPrecision(8).replace(/\.?0+$/, ''); + return v.toPrecision(8).replace(/\.?0+$/, ""); } -function lpad(s: string, w: number) { - return ' '.repeat(Math.max(0, w - s.length)) + s; +function lpad(s: string, w: number): string { + return "\u00a0".repeat(Math.max(0, w - s.length)) + s; } -// ─── Dim colors (cycles per outer dim index) ───────────────────────────────── +function formatBytes(b: number): string { + const units = ["bytes", "KB", "MB", "GB"]; + let v = b, i = 0; + while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; } + return `${v.toFixed(2)} ${units[i]}`; +} -const DIM_COLORS = [ - '#a78bfa', // purple - '#f87171', // tomato - '#fb923c', // orange - '#facc15', // amber -]; +function dtypeBytes(d: Dtype): number { + if (!d) return 4; + if (d === "S1") return 1; + if (d === "str" || d === "NAT") return 0; + const m = d.match(/^[iufc](\d+)$/); + if (m) return parseInt(m[1], 10); + if (d.includes("64")) return 8; + if (d.includes("32")) return 4; + if (d.includes("16")) return 2; + if (d.includes("8")) return 1; + return 4; +} -// ─── Scalar ─────────────────────────────────────────────────────────────────── +// ─── useContainerWidth ──────────────────────────────────────────────────────── -const ScalarDisplay: React.FC<{ value: string; dtype?: string }> = ({ value, dtype }) => ( -
- {dtype &&
{dtype}
} -
{value}
-
-); +function useContainerWidth(ref: RefObject): number { + const [width, setWidth] = useState(0); -// ─── Vector ─────────────────────────────────────────────────────────────────── + useEffect(() => { + const el = ref.current; + if (!el) return; + const ro = new ResizeObserver(entries => setWidth(entries[0].contentRect.width)); + ro.observe(el); + return () => ro.disconnect(); + }, [ref]); -interface VectorProps { - data: ArrayLike; - len: number; - dimName: string; - dtype?: string; - maxRows: number; + return width; } -const VectorDisplay: React.FC = ({ data, len, dimName, dtype, maxRows }) => { - const vals = useMemo(() => { - const out: string[] = []; - for (let i = 0; i < len; i++) out.push(fmtVal(data[i] as never, dtype)); - return out; - }, [data, len, dtype]); - const valW = Math.max(...vals.map(s => s.length)); - const idxW = String(len - 1).length; - const shown = Math.min(len, maxRows); +// ─── useScrollState ─────────────────────────────────────────────────────────── - return ( -
-
- ↓ {dimName} - {dtype && {dtype}} -
-
- - - {Array.from({ length: shown }, (_, i) => ( - - - - - ))} - {len > maxRows && ( - - )} - -
{i}{vals[i]}
⋮ ({len - maxRows} more)
-
-
- ); -}; +function useScrollState(ref: RefObject): ScrollEdges { + const [state, setState] = useState({ + scrollLeft: 0, scrollTop: 0, + L: false, R: false, T: false, B: false, + }); + + const update = useCallback((): void => { + const el = ref.current; + if (!el) return; + const { scrollLeft, scrollTop, scrollWidth, scrollHeight, clientWidth, clientHeight } = el; + setState({ + scrollLeft, + scrollTop, + L: scrollLeft > CONFIG.scrollSlop, + R: scrollLeft < scrollWidth - clientWidth - CONFIG.scrollSlop, + T: scrollTop > CONFIG.scrollSlop, + B: scrollTop < scrollHeight - clientHeight - CONFIG.scrollSlop, + }); + }, [ref]); + + useEffect(() => { + const el = ref.current; + if (!el) return; + update(); + el.addEventListener("scroll", update, { passive: true }); + const ro = new ResizeObserver(update); + ro.observe(el); + return () => { + el.removeEventListener("scroll", update); + ro.disconnect(); + }; + }, [update]); + + return state; +} -// ─── Matrix ─────────────────────────────────────────────────────────────────── +// ─── GlassEdge ──────────────────────────────────────────────────────────────── -interface MatrixProps { - data: ArrayLike; - rows: number; - cols: number; - rowDim: string; - colDim: string; - dtype?: string; - maxRows: number; - maxCols: number; - offset?: number; - showHeader?: boolean; +type EdgeDir = "L" | "R" | "T" | "B"; + +function GlassEdge({ dir }: { dir: EdgeDir }): React.ReactElement { + const isH = dir === "L" || dir === "R"; + const size = `${CONFIG.fadePx}px`; + const pos: React.CSSProperties = { + L: { top: 0, left: 0 }, + R: { top: 0, right: 0 }, + T: { top: 0, left: 0 }, + B: { bottom: 0, left: 0 }, + }[dir]; + const gradDir = { L: "to right", R: "to left", T: "to bottom", B: "to top" }[dir]; + const mask = `linear-gradient(${gradDir}, rgba(0,0,0,0.9) 0%, transparent 100%)`; + + return ( +
+ ); } -const MatrixDisplay: React.FC = ({ - data, rows, cols, rowDim, colDim, dtype, maxRows, maxCols, offset = 0, showHeader = true, -}) => { - const shownRows = Math.min(rows, maxRows); - const shownCols = Math.min(cols, maxCols); - const truncR = rows > maxRows; - const truncC = cols > maxCols; - - const grid = useMemo(() => { - const g: string[][] = []; - for (let r = 0; r < shownRows; r++) { - const row: string[] = []; - for (let c = 0; c < shownCols; c++) { - row.push(fmtVal((data as never)[offset + r * cols + c], dtype)); + +// ─── FrozenMatrix ───────────────────────────────────────────────────────────── + +function FrozenMatrix({ + data, offset, rows, cols, + rowHeaders, colHeaders, + rowDim, colDim, dtype, + showHeader, containerWidth, +}: FrozenMatrixProps): React.ReactElement { + + const cw = useMemo(() => { + const widths = Array.from({ length: cols }, (_, ci) => colHeaders[ci].length); + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const len = fmtVal(data[offset + r * cols + c], dtype).length; + if (len > widths[c]) widths[c] = len; } - g.push(row); } - return g; - }, [data, shownRows, shownCols, cols, dtype, offset]); + return widths; + }, [data, offset, rows, cols, colHeaders, dtype]); - const colHeaders = Array.from({ length: shownCols }, (_, i) => String(i)); - const rowHeaders = Array.from({ length: shownRows }, (_, i) => String(i)); - const cw = Array.from({ length: shownCols }, (_, ci) => - Math.max(colHeaders[ci].length, ...grid.map(row => row[ci]?.length ?? 0)) - ); - const rhW = Math.max(rowDim.length + 2, ...rowHeaders.map(s => s.length)); + const colOffsets = useMemo(() => { + const offs = [0]; + for (let c = 0; c < cols; c++) { + offs.push(offs[c] + cw[c] * CONFIG.chW + CONFIG.colCellPad); + } + return offs; // length = cols + 1; colOffsets[cols] = totalGridW + }, [cw, cols]); + + const rhW_ch = useMemo(() => + Math.max(rowDim.length + CONFIG.rhExtraChar, ...rowHeaders.map(s => s.length)), + [rowDim, rowHeaders]); + + const rhW = rhW_ch * CONFIG.chW + CONFIG.rhPadPx; + const totalGridW = colOffsets[cols]; + const totalGridH = rows * CONFIG.cellH; + const viewW = containerWidth ? Math.max(CONFIG.minViewW, containerWidth - rhW) : 200; + const viewH = Math.min(totalGridH, CONFIG.maxViewH); + + const gridRef = useRef(null); + const colHeadRef = useRef(null); + const rowHeadRef = useRef(null); + + const scroll = useScrollState(gridRef as RefObject); + + const onGridScroll = useCallback((): void => { + const g = gridRef.current; + if (!g) return; + if (colHeadRef.current) colHeadRef.current.scrollLeft = g.scrollLeft; + if (rowHeadRef.current) rowHeadRef.current.scrollTop = g.scrollTop; + }, []); + + const rowStart = Math.max(0, Math.floor(scroll.scrollTop / CONFIG.cellH) - CONFIG.overscan); + const rowEnd = Math.min(rows, Math.ceil((scroll.scrollTop + viewH) / CONFIG.cellH) + CONFIG.overscan); + + const colStart = useMemo(() => { + let lo = 0, hi = cols; + while (lo < hi) { + const mid = (lo + hi) >> 1; + colOffsets[mid + 1] <= scroll.scrollLeft ? (lo = mid + 1) : (hi = mid); + } + return Math.max(0, lo - CONFIG.overscan); + }, [colOffsets, cols, scroll.scrollLeft]); + + const colEnd = useMemo(() => { + let lo = colStart, hi = cols; + while (lo < hi) { + const mid = (lo + hi) >> 1; + colOffsets[mid] < scroll.scrollLeft + viewW ? (lo = mid + 1) : (hi = mid); + } + return Math.min(cols, lo + CONFIG.overscan); + }, [colOffsets, cols, colStart, scroll.scrollLeft, viewW]); + + const paddingTop = rowStart * CONFIG.cellH; + const paddingBottom = (rows - rowEnd) * CONFIG.cellH; + const paddingLeft = colOffsets[colStart]; + const paddingRight = totalGridW - colOffsets[colEnd]; + + const S = { + mono: { + fontFamily: THEME.font.family, + fontSize: THEME.font.size, + lineHeight: `${CONFIG.cellH}px`, + whiteSpace: "nowrap" as const, + }, + muted: { color: THEME.colors.muted }, + value: { color: THEME.colors.value }, + }; return ( -
-
- - - - - - {showHeader && dtype && ( - - )} - - - - ))} - {truncC && } - - - - {grid.map((row, ri) => ( - - - {row.map((v, ci) => ( - - ))} - {truncC && } - - ))} - {truncR && ( - - - - - )} - -
- ↓ {rowDim} - - → {colDim} - {dtype}
- {colHeaders.map((h, ci) => ( - - {lpad(h, cw[ci])} -
{lpad(rowHeaders[ri], rhW)} - {lpad(v, cw[ci])} -
- ({rows - maxRows} more rows) -
+
+ + {/* dim label row */} +
+
+ ↓ {rowDim} +
+
+
+ → {colDim}{showHeader && dtype ? ` ${dtype}` : ""} +
+
+
+ + {/* col-index header (mirrors scrollLeft, windowed) */} +
+
+
+
+ {paddingLeft > 0 &&
} + {Array.from({ length: colEnd - colStart }, (_, i) => { + const ci = colStart + i; + return ( +
+ {colHeaders[ci]} +
+ ); + })} + {paddingRight > 0 &&
} +
+
+
+ + {/* main area */} +
+ + {/* frozen row-index column */} +
+
+ {paddingTop > 0 &&
} + {Array.from({ length: rowEnd - rowStart }, (_, i) => { + const ri = rowStart + i; + return ( +
+ {rowHeaders[ri]} +
+ ); + })} + {paddingBottom > 0 &&
} +
+
+ + {/* scrollable cell grid */} +
+ {scroll.L && } + {scroll.R && } + {scroll.T && } + {scroll.B && } + +
+
+ {paddingTop > 0 &&
} + + {Array.from({ length: rowEnd - rowStart }, (_, i) => { + const ri = rowStart + i; + return ( +
+ {paddingLeft > 0 &&
} + + {Array.from({ length: colEnd - colStart }, (_, j) => { + const ci = colStart + j; + const v = fmtVal(data[offset + ri * cols + ci], dtype); + return ( +
+ {lpad(v, cw[ci])} +
+ ); + })} + + {paddingRight > 0 &&
} +
+ ); + })} + + {paddingBottom > 0 &&
} +
+
+
+
); -}; +} + +// ─── MatrixDisplay ──────────────────────────────────────────────────────────── + +function MatrixDisplay({ + data, rows, cols, rowDim, colDim, dtype, + offset = 0, showHeader = true, containerWidth, +}: MatrixDisplayProps): React.ReactElement { + const colHeaders = useMemo(() => Array.from({ length: cols }, (_, i) => String(i)), [cols]); + const rowHeaders = useMemo(() => Array.from({ length: rows }, (_, i) => String(i)), [rows]); + + return ( + + ); +} -// ─── ND (3D+) ───────────────────────────────────────────────────────────────── +// ─── VectorDisplay ──────────────────────────────────────────────────────────── -interface NDProps { - data: ArrayLike; - shape: number[]; - dimNames: string[]; - dtype?: string; - maxRows: number; - maxCols: number; +function VectorDisplay({ data, len, dimName, dtype, containerWidth }: VectorDisplayProps): React.ReactElement { + const rowHeaders = useMemo(() => Array.from({ length: len }, (_, i) => String(i)), [len]); + const colHeaders = useMemo(() => [dimName], [dimName]); + + return ( + + ); } -const NDDisplay: React.FC = ({ data, shape, dimNames, dtype, maxRows, maxCols }) => { - const ndim = shape.length; - const rows = shape[ndim - 2]; - const cols = shape[ndim - 1]; - const rowDim = dimNames[ndim - 2] ?? `dim_${ndim - 2}`; - const colDim = dimNames[ndim - 1] ?? `dim_${ndim - 1}`; - const outerShape = shape.slice(0, ndim - 2); - const outerDims = dimNames.slice(0, ndim - 2); + +// ─── NDDisplay ──────────────────────────────────────────────────────────────── + +function NDDisplay({ data, shape, dimNames, dtype, containerWidth }: NDDisplayProps): React.ReactElement { + const ndim = shape.length; + const rows = shape[ndim - 2]; + const cols = shape[ndim - 1]; + const rowDim = dimNames[ndim - 2] ?? `dim_${ndim - 2}`; + const colDim = dimNames[ndim - 1] ?? `dim_${ndim - 1}`; + + const outerShape = useMemo(() => shape.slice(0, ndim - 2), [shape, ndim]); + const outerDims = useMemo(() => dimNames.slice(0, ndim - 2), [dimNames, ndim]); const numSlices = outerShape.reduce((a, b) => a * b, 1); - const [sliceIdx, setSliceIdx] = useState(0); + const [sliceIdx, setSliceIdx] = useState(0); - const outerIdxForSlice = (si: number): number[] => { + const outerIdx = useMemo(() => { const idx: number[] = []; - let rem = si; + let rem = sliceIdx; for (let d = outerShape.length - 1; d >= 0; d--) { idx[d] = rem % outerShape[d]; rem = Math.floor(rem / outerShape[d]); } return idx; - }; + }, [sliceIdx, outerShape]); - const outerIdx = outerIdxForSlice(sliceIdx); - const offset = sliceIdx * rows * cols; - - const sliceProxy = new Proxy(data as ArrayLike, { - get(target, prop) { - if (typeof prop === 'string' && !Number.isNaN(+prop)) return target[offset + +prop]; - return (target as never)[prop]; - }, - }); + const offset = sliceIdx * rows * cols; + const colHeaders = useMemo(() => Array.from({ length: cols }, (_, i) => String(i)), [cols]); + const rowHeaders = useMemo(() => Array.from({ length: rows }, (_, i) => String(i)), [rows]); return ( -
-
+
+
- {/* bracket: colored outer indices + muted colons for matrix dims */} + - {'['} + {"["} {outerIdx.map((idx, i) => ( - - {i > 0 && , } - {idx} + + {i > 0 && , } + {idx} ))} - {outerIdx.length > 0 ? ', ' : ''}:, : - {']'} + {outerIdx.length > 0 ? ", " : ""}:, : + {"]"} - {/* outer dim name=value, colored */} + {outerDims.map((d, i) => ( - + {d}={outerIdx[i]} ))} - {/* matrix dim names, no value */} - {rowDim} - {colDim} - {/* dtype */} - {dtype && {dtype}} + - {sliceIdx + 1}/{numSlices} + + {sliceIdx + 1}/{numSlices}
-
); -}; - - - -// ─── Byte helpers ───────────────────────────────────────────────────────────── - -export function formatBytes(bytes: number): string { - const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; - let value = bytes; - let unitIndex = 0; - while (value >= 1024 && unitIndex < units.length - 1) { - value /= 1024; - unitIndex++; - } - return `${value.toFixed(2)} ${units[unitIndex]}`; -} - -function dtypeBytes(dtype?: string): number { - if (!dtype) return 4; - if (dtype === 'S1') return 1; // NC_CHAR - if (dtype === 'str') return 0; // NC_STRING (variable length) - if (dtype === 'NAT') return 0; - // NetCDF shorthand: i1/u1 → 1, i2/u2 → 2, i4/u4/f4 → 4, i8/u8/f8 → 8 - const shorthand = dtype.match(/^[iufc](\d+)$/); - if (shorthand) return parseInt(shorthand[1]); - // Named: float64/int16/uint8 etc - if (dtype.includes('64')) return 8; - if (dtype.includes('32')) return 4; - if (dtype.includes('16')) return 2; - if (dtype.includes('8')) return 1; - return 4; } // ─── Footer ─────────────────────────────────────────────────────────────────── -interface FooterProps { - varName?: string; - shape: number[]; - totalShape?: number[]; - dtype?: string; -} -const Footer: React.FC = ({ varName, shape, totalShape, dtype }) => { - const bpp = dtypeBytes(dtype); - const sliceTotal = shape.reduce((a, b) => a * b, 1); - const sliceBytes = sliceTotal * bpp; - const hasTotal = totalShape && totalShape.length > 0; - const totalTotal = hasTotal ? totalShape!.reduce((a, b) => a * b, 1) : null; - const totalBytes = totalTotal !== null ? totalTotal * bpp : null; +function Footer({ varName, shape, totalShape, dtype }: FooterProps): React.ReactElement { + const bpp = dtypeBytes(dtype); + const sb = shape.reduce((a, b) => a * b, 1) * bpp; + const has = (totalShape?.length ?? 0) > 0; + const tb = has && totalShape ? totalShape.reduce((a, b) => a * b, 1) * bpp : null; return ( -
- {varName && ( - {varName} - )} +
+ {varName && {varName}} - [{shape.join(', ')}] - {formatBytes(sliceBytes)} + [{shape.join(", ")}] + {formatBytes(sb)} - {hasTotal && totalBytes !== null && ( - <> - / - - [{totalShape!.join(', ')}] - {formatBytes(totalBytes)} - - - )} + {has && tb !== null && <> + / + + [{totalShape!.join(", ")}] + {formatBytes(tb)} + + }
); -}; +} + +// ─── ArrayDisplay ───────────────────────────────────────────────────────────── + +export function ArrayDisplay({ + data, shape, dimNames = [], varName, dtype, totalShape, +}: ArrayDisplayProps): React.ReactElement { + const containerRef = useRef(null); + const containerWidth = useContainerWidth(containerRef as RefObject); -// ─── Main ───────────────────────────────────────────────────────────────────── - -export const ArrayDisplay: React.FC = ({ - data, - shape, - dimNames = [], - varName, - dtype, - totalShape, - maxRows = 24, - maxCols = 16, -}) => { const ndim = shape.length; const total = shape.reduce((a, b) => a * b, 1); const names = shape.map((_, i) => dimNames[i] ?? `dim_${i}`); - const footer = ( -