Skip to content

Commit 5abec83

Browse files
authored
better display of variables within groups (#397)
* accordion * open if only root * simplify * pointer * units maybe not * do recursive tree * new line
1 parent b0e3ddb commit 5abec83

File tree

9 files changed

+276
-65
lines changed

9 files changed

+276
-65
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"lint": "next lint --max-warnings 100"
4646
},
4747
"dependencies": {
48+
"@radix-ui/react-accordion": "^1.2.12",
4849
"@radix-ui/react-dialog": "^1.1.15",
4950
"@radix-ui/react-scroll-area": "^1.2.9",
5051
"@radix-ui/react-separator": "^1.1.7",

src/components/ui/MainPanel/MetaDataInfo.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ const MetaDataInfo = ({ meta, metadata, setShowMeta, setOpenVariables, popoverSi
196196
<b>{`${meta.long_name} `}</b>
197197
{ popoverSide=="left" ? <Popover>
198198
<PopoverTrigger className="cursor-pointer" asChild>
199-
<Badge variant="default">
199+
<Badge variant="default" className="block">
200200
Attributes
201201
</Badge>
202202
</PopoverTrigger>
@@ -209,7 +209,7 @@ const MetaDataInfo = ({ meta, metadata, setShowMeta, setOpenVariables, popoverSi
209209
</PopoverContent>
210210
</Popover>
211211
:
212-
<Metadata data={metadata} variable ={'Attributes'} />
212+
<div> <Metadata data={metadata} variable ={'Attributes'} /> </div>
213213
}
214214
<br/>
215215
<br/>

src/components/ui/MainPanel/Variables.tsx

Lines changed: 187 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ import {
2020
DialogContent,
2121
DialogTitle,
2222
} from "@/components/ui/dialog";
23+
import {
24+
Accordion,
25+
AccordionContent,
26+
AccordionItem,
27+
AccordionTrigger,
28+
} from "@/components/ui/accordion";
2329
import { ZarrDataset } from "@/components/zarr/ZarrLoaderLRU";
2430

2531
const Variables = ({
@@ -44,42 +50,105 @@ const Variables = ({
4450
);
4551
const { currentStore } = useZarrStore(useShallow(state => ({
4652
currentStore: state.currentStore,
47-
})))
48-
const ZarrDS = useMemo(() => new ZarrDataset(currentStore), [currentStore])
53+
})));
54+
const ZarrDS = useMemo(() => new ZarrDataset(currentStore), [currentStore]);
4955

50-
const [dimArrays, setDimArrays] = useState([[0],[0],[0]])
51-
const [dimUnits, setDimUnits] = useState([null,null,null])
52-
const [dimNames, setDimNames] = useState<string[]> (["Default"])
56+
const [dimArrays, setDimArrays] = useState([[0],[0],[0]]);
57+
const [dimUnits, setDimUnits] = useState([null,null,null]);
58+
const [dimNames, setDimNames] = useState<string[]>(["Default"]);
5359

5460
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
5561
const [selectedVar, setSelectedVar] = useState<string | null>(null);
5662
const [meta, setMeta] = useState<any>(null);
5763
const [query, setQuery] = useState("");
64+
// root *open by default* (collapsible but starts open)
65+
const [openAccordionItems, setOpenAccordionItems] = useState<string[]>(["root"]);
5866

59-
const filtered = useMemo(() => {
67+
// Build nested variable tree
68+
const tree = useMemo(() => {
6069
const q = query.toLowerCase().trim();
61-
if (!q) return variables;
62-
return variables.filter((variable) =>
63-
variable.toLowerCase().includes(q)
64-
);
70+
let filteredVars = variables ?? [];
71+
72+
if (q) {
73+
filteredVars = filteredVars.filter((variable) =>
74+
variable.toLowerCase().includes(q)
75+
);
76+
}
77+
78+
const buildTree = (vars: string[]) => {
79+
const t: any = {};
80+
vars.forEach((v) => {
81+
const parts = v.split("/");
82+
let current = t;
83+
parts.forEach((p, i) => {
84+
if (!current[p]) current[p] = i === parts.length - 1 ? null : {};
85+
current = current[p];
86+
});
87+
});
88+
return t;
89+
};
90+
91+
return buildTree(filteredVars);
6592
}, [query, variables]);
6693

94+
// Get all group paths (for auto-open when searching)
95+
const getGroupPaths = (subtree: any, basePath = ""): string[] => {
96+
let paths: string[] = [];
97+
Object.entries(subtree).forEach(([key, value]) => {
98+
const currentPath = basePath ? `${basePath}/${key}` : key;
99+
if (value && typeof value === "object") {
100+
paths.push(currentPath);
101+
paths = paths.concat(getGroupPaths(value, currentPath));
102+
}
103+
});
104+
return paths;
105+
};
106+
107+
// Auto-open accordions that contain matches
108+
useEffect(() => {
109+
if (query.trim()) {
110+
const openPaths = getGroupPaths(tree);
111+
setOpenAccordionItems(["root", ...openPaths]);
112+
} else {
113+
// when not searching keep root open by default
114+
setOpenAccordionItems(["root"]);
115+
}
116+
}, [query, tree]);
117+
118+
// Handle variable selection
119+
const handleVariableSelect = (val: string, idx: number) => {
120+
setSelectedIndex(idx);
121+
setSelectedVar(val);
122+
GetDimInfo(val).then(e => {
123+
setDimNames(e.dimNames);
124+
setDimArrays(e.dimArrays);
125+
setDimUnits(e.dimUnits);
126+
});
127+
128+
if (popoverSide === "left") {
129+
setOpenMetaPopover(true);
130+
} else {
131+
setShowMeta(true);
132+
}
133+
};
134+
67135
useEffect(() => {
68136
if (variables && zMeta && selectedVar) {
69137
const relevant = zMeta.find((e: any) => e.name === selectedVar);
70138
if (relevant){
71139
setMeta({...relevant, dimInfo : {dimArrays, dimNames, dimUnits}});
72-
ZarrDS.GetAttributes(selectedVar).then(e=>setMetadata(e))
140+
ZarrDS.GetAttributes(selectedVar).then(e=>setMetadata(e));
73141
}
74142
}
143+
// eslint-disable-next-line react-hooks/exhaustive-deps
75144
}, [selectedVar, variables, zMeta, dimArrays, dimNames, dimUnits]);
76145

77146
useEffect(()=>{
78-
setSelectedIndex(null)
79-
setSelectedVar(null)
80-
setMeta(null)
81-
setMetadata(null)
82-
},[initStore])
147+
setSelectedIndex(null);
148+
setSelectedVar(null);
149+
setMeta(null);
150+
setMetadata(null);
151+
},[initStore, setMetadata]);
83152

84153
useEffect(() => {
85154
const handleResize = () => {
@@ -90,38 +159,99 @@ const Variables = ({
90159
return () => window.removeEventListener("resize", handleResize);
91160
}, []);
92161

162+
// Variable item renderer (keeps separator between variables in same group)
163+
const VariableItem = ({ val, idx, arrayLength }: { val: string; idx: number; arrayLength: number }) => {
164+
const variableName = val.split('/').pop() || val;
165+
const isLastItem = idx === arrayLength - 1;
166+
167+
return (
168+
<React.Fragment key={val}>
169+
<div
170+
className="cursor-pointer pl-2 py-1 text-sm hover:bg-muted rounded"
171+
style={{
172+
background: selectedVar === val ? "var(--muted-foreground)" : "",
173+
}}
174+
onClick={() => handleVariableSelect(val, idx)}
175+
>
176+
{variableName}
177+
</div>
178+
{!isLastItem && <Separator className="my-1" />}
179+
</React.Fragment>
180+
);
181+
};
182+
183+
// render a subtree inside an Accordion (ensures AccordionItem children are direct children of an Accordion)
184+
const renderSubtreeAccordion = (subtree: any, basePath = "") => {
185+
// Determine entries (keep variables first then groups for clarity)
186+
const entries = Object.entries(subtree);
187+
const variableEntries = entries.filter(([_, v]) => v === null);
188+
const groupEntries = entries.filter(([_, v]) => v && typeof v === "object");
189+
190+
return (
191+
<Accordion
192+
key={basePath || "__root_inner__"}
193+
type="multiple"
194+
value={openAccordionItems}
195+
onValueChange={setOpenAccordionItems}
196+
className="w-full"
197+
>
198+
{/* variables at this level */}
199+
{variableEntries.length > 0 && (
200+
<div className="px-1">
201+
{variableEntries.map(([name], idx) => {
202+
const varPath = basePath ? `${basePath}/${name}` : name;
203+
return (
204+
<VariableItem
205+
key={varPath}
206+
val={varPath}
207+
idx={idx}
208+
arrayLength={variableEntries.length}
209+
/>
210+
);
211+
})}
212+
</div>
213+
)}
214+
215+
{/* groups at this level */}
216+
{groupEntries.map(([name, subtreeValue]) => {
217+
const currentPath = basePath ? `${basePath}/${name}` : name;
218+
return (
219+
<AccordionItem key={currentPath} value={currentPath}>
220+
<AccordionTrigger className="cursor-pointer pl-2">
221+
{name}
222+
</AccordionTrigger>
223+
<AccordionContent className="flex flex-col pl-2">
224+
{/* recursively render children inside their own Accordion */}
225+
{renderSubtreeAccordion(subtreeValue, currentPath)}
226+
</AccordionContent>
227+
</AccordionItem>
228+
);
229+
})}
230+
</Accordion>
231+
);
232+
};
233+
234+
// render full Variable list under "root" accordion item
93235
const VariableList = (
94236
<div className="overflow-y-auto flex-1 [&::-webkit-scrollbar]:hidden">
95-
{filtered.length > 0 ? (
96-
filtered.map((val, idx) => (
97-
<React.Fragment key={idx}>
98-
<div
99-
className="cursor-pointer pl-2 py-1 text-sm hover:bg-muted rounded"
100-
style={{
101-
background:
102-
idx === selectedIndex ? "var(--muted-foreground)" : "",
103-
}}
104-
onClick={() => {
105-
setSelectedIndex(idx);
106-
setSelectedVar(val);
107-
GetDimInfo(val).then(e=>{setDimNames(e.dimNames); setDimArrays(e.dimArrays); setDimUnits(e.dimUnits)})
108-
if (popoverSide === "left") {
109-
setOpenMetaPopover(true);
110-
} else {
111-
setShowMeta(true);
112-
}
113-
}}
114-
>
115-
{val}
116-
</div>
117-
{idx !== filtered.length - 1 && <Separator className="my-1" />}
118-
</React.Fragment>
119-
))
237+
{Object.keys(tree).length > 0 ? (
238+
<Accordion
239+
type="multiple"
240+
className="w-full"
241+
value={openAccordionItems}
242+
onValueChange={setOpenAccordionItems}
243+
>
244+
<AccordionItem key="root" value="root">
245+
<AccordionTrigger className="cursor-pointer">/</AccordionTrigger>
246+
<AccordionContent className="flex flex-col">
247+
{/* render the top-level subtree inside its own Accordion so nested AccordionItems are legal */}
248+
{renderSubtreeAccordion(tree, "")}
249+
</AccordionContent>
250+
</AccordionItem>
251+
</Accordion>
120252
) : (
121253
<div className="text-center text-muted-foreground py-2">
122-
{query
123-
? "No variables found matching your search."
124-
: "No variables available."}
254+
{query ? "No variables found matching your search." : "No variables available."}
125255
</div>
126256
)}
127257
</div>
@@ -187,11 +317,11 @@ const Variables = ({
187317
<Popover open={openMetaPopover} onOpenChange={setOpenMetaPopover}>
188318
<PopoverTrigger asChild>
189319
<div
190-
className="absolute -top-8" // adjust top position as needed
191-
style={{
192-
left: `-${280}px`, // move left
193-
}}
194-
/>
320+
className="absolute -top-8" // adjust top position as needed
321+
style={{
322+
left: `-${280}px`, // move to left of variable popover
323+
}}
324+
/>
195325
</PopoverTrigger>
196326
<PopoverContent
197327
data-meta-popover
@@ -200,13 +330,13 @@ const Variables = ({
200330
className="max-h-[80vh] overflow-y-auto w-[300px]"
201331
>
202332
{meta && (
203-
<MetaDataInfo
204-
meta={meta}
205-
metadata={metadata??{}}
206-
setShowMeta={setOpenMetaPopover}
207-
setOpenVariables={setOpenVariables}
208-
popoverSide={"left"}
209-
/>
333+
<MetaDataInfo
334+
meta={meta}
335+
metadata={metadata ?? {}}
336+
setShowMeta={setOpenMetaPopover}
337+
setOpenVariables={setOpenVariables}
338+
popoverSide={"left"}
339+
/>
210340
)}
211341
</PopoverContent>
212342
</Popover>
@@ -219,7 +349,7 @@ const Variables = ({
219349
{meta && (
220350
<MetaDataInfo
221351
meta={meta}
222-
metadata={metadata??{}}
352+
metadata={metadata ?? {}}
223353
setShowMeta={setShowMeta}
224354
setOpenVariables={setOpenVariables}
225355
popoverSide={"top"}

0 commit comments

Comments
 (0)