Skip to content

Commit b5dea7a

Browse files
committed
add untracked files
1 parent 1f5c434 commit b5dea7a

5 files changed

Lines changed: 191 additions & 0 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Sidebar grouping
2+
3+
The probe sidebar turns a manufacturer's flat probe list into a hierarchy from
4+
an explicit, hand-curated config. There are no rules and no inference: every
5+
probe is placed by model id under a path of named nodes. A manufacturer with no
6+
config (everything except IMEC today) renders as a flat list.
7+
8+
This replaced an earlier rule-based engine that derived the grouping by regex on
9+
the part number and substring-matching the free-text description. Those rules
10+
were fragile because the probe metadata does not carry the facts we group on
11+
(generation, family, length), so we were reconstructing editorial decisions from
12+
strings. The explicit file states those decisions directly instead.
13+
14+
## Format
15+
16+
JSON (`imec_neuropixels.json`), because Vite imports it with no extra
17+
dependency. The config is `{ hierarchy }`, a tree of nodes. Each node has a
18+
`label` and either `children` (more nodes) or `probes` (model ids, in display
19+
order) at the leaves.
20+
21+
`collapsible` is a per-node property that **propagates to descendants**: a node
22+
uses its own `collapsible` when set, otherwise it inherits the resolved value of
23+
its parent, and the top-level default is `true`. So each platform sets
24+
`collapsible: true` and that flows down to its families, while each length band
25+
sets `collapsible: false` (a static, always-open divider) and that flows down to
26+
its probes. `true` is a foldable header with a caret; `false` is a static
27+
divider.
28+
29+
The walker (`groupEntries.ts`) attaches each manifest entry to the node listing
30+
its model, prunes empty branches, and collects anything not placed into a
31+
trailing `Ungrouped` group. That bucket is the signal that a probe new to the
32+
manifest needs a home in the config.
33+
34+
## Adding or moving a probe
35+
36+
Edit the relevant `probes` list. To add a new manufacturer, add its JSON file
37+
and one line in `index.ts`. No engine changes. JSON has no comments, so if a
38+
placement is non-obvious, record the rationale here or in a `note` field on the
39+
node.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { ManifestEntry } from "../types/probe";
2+
import type { GroupNode, HierarchyConfig, HierarchyNode } from "./types";
3+
4+
// Label for the trailing bucket that catches any probe present in the manifest
5+
// but not placed anywhere in the hierarchy. It is the visible signal that a new
6+
// probe needs a home in the config.
7+
const UNGROUPED_LABEL = "Ungrouped";
8+
9+
// Top-level default when a node does not set `collapsible` and has no parent.
10+
const ROOT_COLLAPSIBLE = true;
11+
12+
// Resolves the explicit hierarchy against the given (already search-filtered)
13+
// entries: attaches each entry to the node that lists its model id, prunes
14+
// empty branches, and gathers anything unplaced into a trailing "Ungrouped"
15+
// group. `collapsible` is resolved per node, inheriting the parent's value when
16+
// the node does not set its own.
17+
export function groupEntries(
18+
entries: ManifestEntry[],
19+
config: HierarchyConfig,
20+
): GroupNode[] {
21+
const byModel = new Map<string, ManifestEntry>();
22+
for (const entry of entries) byModel.set(entry.model, entry);
23+
const placed = new Set<string>();
24+
25+
const walk = (node: HierarchyNode, inherited: boolean): GroupNode | null => {
26+
const collapsible = node.collapsible ?? inherited;
27+
28+
if (node.children) {
29+
const children = node.children
30+
.map((child) => walk(child, collapsible))
31+
.filter((child): child is GroupNode => child !== null);
32+
if (children.length === 0) return null;
33+
const count = children.reduce((sum, child) => sum + child.count, 0);
34+
return { label: node.label, collapsible, count, children };
35+
}
36+
37+
const found: ManifestEntry[] = [];
38+
for (const model of node.probes ?? []) {
39+
const entry = byModel.get(model);
40+
if (entry) {
41+
found.push(entry);
42+
placed.add(model);
43+
}
44+
}
45+
if (found.length === 0) return null;
46+
return { label: node.label, collapsible, count: found.length, entries: found };
47+
};
48+
49+
const groups = config.hierarchy
50+
.map((node) => walk(node, ROOT_COLLAPSIBLE))
51+
.filter((group): group is GroupNode => group !== null);
52+
53+
const leftovers = entries.filter((entry) => !placed.has(entry.model));
54+
if (leftovers.length > 0) {
55+
groups.push({
56+
label: UNGROUPED_LABEL,
57+
collapsible: true,
58+
count: leftovers.length,
59+
entries: leftovers,
60+
});
61+
}
62+
63+
return groups;
64+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"hierarchy": [
3+
{ "label": "Neuropixels 1.0", "collapsible": true,
4+
"children": [
5+
{ "label": "Standard", "probes": ["NP1000", "NP1001"] },
6+
{ "label": "Non-human-primate",
7+
"children": [
8+
{ "label": "short", "collapsible": false, "probes": ["NP1010", "NP1011", "NP1012", "NP1013", "NP1014", "NP1015", "NP1016", "NP1017"] },
9+
{ "label": "medium", "collapsible": false, "probes": ["NP1020", "NP1021", "NP1022"] },
10+
{ "label": "long / max", "collapsible": false, "probes": ["NP1030", "NP1031", "NP1032", "NP1033", "NP1040", "NP1041", "NP1042", "NP1050", "NP1051"] },
11+
{ "label": "passive", "collapsible": false, "probes": ["NP1200", "NP1210"] }
12+
] },
13+
{ "label": "Ultra High Density", "probes": ["NP1100", "NP1120", "NP1121", "NP1122", "NP1123"] },
14+
{ "label": "Optogenetics", "probes": ["NP1300"] },
15+
{ "label": "Legacy", "probes": ["PRB_1_2_0480_2", "PRB_1_4_0480_1", "PRB_1_4_0480_1_C"] }
16+
] },
17+
{ "label": "Neuropixels 2.0", "collapsible": true,
18+
"children": [
19+
{ "label": "Non-human-primate",
20+
"children": [
21+
{ "label": "short", "collapsible": false, "probes": ["NP2005", "NP2006"] }
22+
] },
23+
{ "label": "Single-shank", "probes": ["NP2000", "NP2003", "NP2004"] },
24+
{ "label": "Multi-shank", "probes": ["NP2010", "NP2013", "NP2014", "NP2020", "NP2021"] },
25+
{ "label": "Legacy", "probes": ["PRB2_1_2_0640_0", "PRB2_4_2_0640_0"] }
26+
] },
27+
{ "label": "Neuropixels NXT", "collapsible": true,
28+
"children": [
29+
{ "label": "Single-shank", "probes": ["NP3010", "NP3011"] },
30+
{ "label": "Multi-shank", "probes": ["NP3020", "NP3021", "NP3022", "NP3023", "NP3024"] },
31+
{ "label": "Passive", "probes": ["NP3000"] }
32+
] }
33+
]
34+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { HierarchyConfig } from "./types";
2+
import imecHierarchy from "./imec_neuropixels.json";
3+
4+
// Registry mapping a manufacturer key (as it appears in the manifest) to its
5+
// explicit sidebar hierarchy. A manufacturer absent here has no hierarchy and
6+
// renders as a flat list, which is the default for every manufacturer other
7+
// than IMEC. Adding grouping for a new manufacturer is one JSON file plus one
8+
// line here, with no engine changes.
9+
//
10+
// JSON is used because Vite imports it with no extra dependency.
11+
const REGISTRY: Record<string, HierarchyConfig> = {
12+
imec: imecHierarchy as HierarchyConfig,
13+
};
14+
15+
export function getGroupingConfig(manufacturer: string): HierarchyConfig | undefined {
16+
return REGISTRY[manufacturer];
17+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { ManifestEntry } from "../types/probe";
2+
3+
// Explicit, hand-curated sidebar hierarchy for one manufacturer. There are no
4+
// rules: every probe is placed by model id under the tree in imec.json, with
5+
// model ids at the leaves. A manufacturer with no config renders as a flat list
6+
// (see ./index.ts).
7+
//
8+
// Collapsibility is a per-node property that propagates to descendants: a node
9+
// uses its own `collapsible` when set, otherwise it inherits the resolved value
10+
// of its parent. The top-level default is `true`. So a platform marked
11+
// collapsible flows that down to its families, and a length band marked
12+
// non-collapsible flows that down to its probes, without either having to be
13+
// repeated on every node.
14+
15+
export interface HierarchyNode {
16+
label: string;
17+
// true => a collapsible group header; false => a static, always-open divider.
18+
// Omitted => inherit the parent's resolved value (root default: true).
19+
collapsible?: boolean;
20+
children?: HierarchyNode[];
21+
probes?: string[]; // model ids, in display order
22+
}
23+
24+
export interface HierarchyConfig {
25+
hierarchy: HierarchyNode[];
26+
}
27+
28+
// One node of the resolved tree the walker returns, with manifest entries
29+
// attached and `collapsible` resolved to a concrete boolean. A node has either
30+
// `children` (sub-divided) or `entries` (a leaf), never both.
31+
export interface GroupNode {
32+
label: string;
33+
collapsible: boolean;
34+
count: number;
35+
children?: GroupNode[];
36+
entries?: ManifestEntry[];
37+
}

0 commit comments

Comments
 (0)