@weng-lab/genomebrowser-ui 0.2.0-beta.1 → 0.2.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@weng-lab/genomebrowser-ui",
3
3
  "private": false,
4
- "version": "0.2.0-beta.1",
4
+ "version": "0.2.0-beta.3",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "publishConfig": {
@@ -1,5 +1,4 @@
1
1
  import { FormControlLabel, Switch } from "@mui/material";
2
- import { useState } from "react";
3
2
  import { FolderRuntimeConfig } from "../../types";
4
3
  import {
5
4
  defaultColumns,
@@ -9,9 +8,13 @@ import {
9
8
  sortedByAssayGroupingModel,
10
9
  sortedByAssayLeafField,
11
10
  } from "./columns";
11
+ import { buildTreeView, buildSortedAssayTreeView } from "./treeBuilder";
12
12
 
13
13
  export interface AssayToggleProps {
14
14
  updateConfig: (partial: Partial<FolderRuntimeConfig>) => void;
15
+ folderId: string;
16
+ label: string;
17
+ config: FolderRuntimeConfig;
15
18
  }
16
19
 
17
20
  /**
@@ -20,15 +23,21 @@ export interface AssayToggleProps {
20
23
  *
21
24
  * When toggled, it updates the folder's runtime config to switch:
22
25
  * - columns: Different column definitions for each view
23
- * - groupingModel: ["ontology", "displayName"] vs ["assay", "ontology", "displayName"]
24
- * - leafField: "assay" vs "id"
26
+ * - groupingModel: ["ontology", "displayName"] vs ["assay", "ontology"]
27
+ * - leafField: "assay" vs "displayName"
28
+ * - buildTree: Different tree builder function
25
29
  */
26
- export function AssayToggle({ updateConfig }: AssayToggleProps) {
27
- const [sortedByAssay, setSortedByAssay] = useState(false);
30
+ export function AssayToggle({
31
+ updateConfig,
32
+ folderId,
33
+ label,
34
+ config,
35
+ }: AssayToggleProps) {
36
+ // Derive toggle state from current config's leafField
37
+ const sortedByAssay = config.leafField === sortedByAssayLeafField;
28
38
 
29
39
  const handleToggle = () => {
30
40
  const newValue = !sortedByAssay;
31
- setSortedByAssay(newValue);
32
41
 
33
42
  if (newValue) {
34
43
  // Switch to assay-grouped view
@@ -36,6 +45,8 @@ export function AssayToggle({ updateConfig }: AssayToggleProps) {
36
45
  columns: sortedByAssayColumns,
37
46
  groupingModel: sortedByAssayGroupingModel,
38
47
  leafField: sortedByAssayLeafField,
48
+ buildTree: (selectedIds, rowById) =>
49
+ buildSortedAssayTreeView(selectedIds, rowById, label, folderId),
39
50
  });
40
51
  } else {
41
52
  // Switch back to default (sample-grouped) view
@@ -43,6 +54,8 @@ export function AssayToggle({ updateConfig }: AssayToggleProps) {
43
54
  columns: defaultColumns,
44
55
  groupingModel: defaultGroupingModel,
45
56
  leafField: defaultLeafField,
57
+ buildTree: (selectedIds, rowById) =>
58
+ buildTreeView(selectedIds, rowById, label, folderId),
46
59
  });
47
60
  }
48
61
  };
@@ -153,13 +153,13 @@ export const defaultColumns: GridColDef<BiosampleRowInfo>[] = [
153
153
  ];
154
154
 
155
155
  /** Grouping model for sorted-by-assay view */
156
- export const sortedByAssayGroupingModel = ["assay", "ontology", "displayName"];
156
+ export const sortedByAssayGroupingModel = ["assay", "ontology"];
157
157
 
158
158
  /** Default grouping model (ontology-based) */
159
159
  export const defaultGroupingModel = ["ontology", "displayName"];
160
160
 
161
161
  /** Leaf field for sorted-by-assay view */
162
- export const sortedByAssayLeafField = "id";
162
+ export const sortedByAssayLeafField = "displayName";
163
163
 
164
164
  /** Default leaf field */
165
165
  export const defaultLeafField = "assay";
@@ -35,7 +35,10 @@ function createRootNode(
35
35
 
36
36
  /**
37
37
  * Builds tree in the sorted by assay view
38
- * Hierarchy: Assay -> Ontology -> DisplayName -> Experiment
38
+ * Hierarchy: Assay -> Ontology -> DisplayName (leaf)
39
+ *
40
+ * This is the reverse of the default view - instead of grouping by sample first,
41
+ * we group by assay first, making displayName the leaf node.
39
42
  *
40
43
  * @param selectedIds - list of selected row IDs
41
44
  * @param rowById - Mapping between an id and its BiosampleRowInfo object
@@ -55,6 +58,8 @@ export function buildSortedAssayTreeView(
55
58
  string,
56
59
  TreeViewBaseItem<ExtendedTreeItemProps>
57
60
  >();
61
+ // Track which displayName nodes exist per assay+ontology combination
62
+ // and which experiment IDs they contain
58
63
  const displayNameMap = new Map<
59
64
  string,
60
65
  TreeViewBaseItem<ExtendedTreeItemProps>
@@ -75,6 +80,7 @@ export function buildSortedAssayTreeView(
75
80
  isAssayItem: true,
76
81
  label: row.assay,
77
82
  icon: "removeable",
83
+ assayName: row.assay, // Add assayName so the icon renders correctly
78
84
  children: [],
79
85
  allExpAccessions: [],
80
86
  };
@@ -97,6 +103,7 @@ export function buildSortedAssayTreeView(
97
103
  ontologyMap.set(ontologyKey, ontologyNode);
98
104
  }
99
105
 
106
+ // DisplayName is now the leaf node (no children, no assay icon)
100
107
  const displayNameKey = `${folderId}::${row.assay}-${row.ontology}-${row.displayName}`;
101
108
  let displayNameNode = displayNameMap.get(displayNameKey);
102
109
  if (!displayNameNode) {
@@ -112,17 +119,7 @@ export function buildSortedAssayTreeView(
112
119
  displayNameMap.set(displayNameKey, displayNameNode);
113
120
  }
114
121
 
115
- const expNode: TreeViewBaseItem<ExtendedTreeItemProps> = {
116
- id: row.id,
117
- isAssayItem: false,
118
- label: formatIdLabel(row.id),
119
- icon: "removeable",
120
- assayName: row.assay,
121
- children: [],
122
- allExpAccessions: [row.id],
123
- };
124
- displayNameNode.children!.push(expNode);
125
-
122
+ // Add this experiment ID to all parent nodes' allExpAccessions
126
123
  assayNode.allExpAccessions!.push(row.id);
127
124
  ontologyNode.allExpAccessions!.push(row.id);
128
125
  displayNameNode.allExpAccessions!.push(row.id);
@@ -1,6 +1,6 @@
1
1
  [
2
2
  {
3
- "id": "genocode-basic",
3
+ "id": "gencode-basic",
4
4
  "displayName": "GENCODE Basic Genes",
5
5
  "versions": [29, 40]
6
6
  }
@@ -1,6 +1,6 @@
1
1
  [
2
2
  {
3
- "id": "genocode-basic",
3
+ "id": "gencode-basic",
4
4
  "displayName": "GENCODE Basic Genes",
5
5
  "versions": [21, 25]
6
6
  }
@@ -7,12 +7,17 @@ export type Assembly = "GRCh38" | "mm10";
7
7
  /**
8
8
  * Runtime configuration that can be modified by ToolbarExtras components.
9
9
  * This allows folder-specific UI (like AssayToggle) to dynamically update
10
- * how the DataGrid displays data.
10
+ * how the DataGrid and TreeView display data.
11
11
  */
12
12
  export interface FolderRuntimeConfig {
13
13
  columns: GridColDef[];
14
14
  groupingModel: string[];
15
15
  leafField: string;
16
+ /** Optional override for the tree builder function */
17
+ buildTree?: (
18
+ selectedIds: string[],
19
+ rowById: Map<string, any>,
20
+ ) => TreeViewBaseItem<ExtendedTreeItemProps>[];
16
21
  }
17
22
 
18
23
  /**
@@ -74,9 +79,15 @@ export interface FolderDefinition<TRow = any> {
74
79
  * that switches between sample-grouped and assay-grouped views.
75
80
  *
76
81
  * @param updateConfig - Callback to update the folder's runtime config
82
+ * @param folderId - The folder's unique identifier
83
+ * @param label - The folder's display label
84
+ * @param config - The current runtime config for this folder
77
85
  */
78
86
  ToolbarExtras?: React.FC<{
79
87
  updateConfig: (partial: Partial<FolderRuntimeConfig>) => void;
88
+ folderId: string;
89
+ label: string;
90
+ config: FolderRuntimeConfig;
80
91
  }>;
81
92
 
82
93
  /**
@@ -167,23 +167,29 @@ export default function TrackSelect({
167
167
  return Array.from(activeFolder.rowById.values());
168
168
  }, [activeFolder]);
169
169
 
170
- const treeItems = useMemo(() => {
171
- if (!activeFolder) return [];
170
+ const folderTrees = useMemo(() => {
172
171
  return folders
173
172
  .filter((folder) => {
174
173
  const selected = selectedByFolder.get(folder.id);
175
174
  return selected && selected.size > 0;
176
175
  })
177
- .flatMap((folder) =>
178
- attachFolderId(
179
- folder.buildTree(
180
- Array.from(selectedByFolder.get(folder.id) ?? []),
181
- folder.rowById,
176
+ .map((folder) => {
177
+ const config = runtimeConfigByFolder.get(folder.id);
178
+ const buildTree = config?.buildTree ?? folder.buildTree;
179
+
180
+ return {
181
+ folderId: folder.id,
182
+ items: attachFolderId(
183
+ buildTree(
184
+ Array.from(selectedByFolder.get(folder.id) ?? []),
185
+ folder.rowById,
186
+ ),
187
+ folder.id,
182
188
  ),
183
- folder.id,
184
- ),
185
- );
186
- }, [folders, selectedByFolder, activeFolder]);
189
+ TreeItemComponent: folder.TreeItemComponent,
190
+ };
191
+ });
192
+ }, [folders, selectedByFolder, runtimeConfigByFolder]);
187
193
 
188
194
  const updateActiveFolderConfig = useCallback(
189
195
  (partial: Partial<FolderRuntimeConfig>) => {
@@ -335,9 +341,16 @@ export default function TrackSelect({
335
341
  ) : (
336
342
  <Box />
337
343
  )}
338
- {currentView === "folder-detail" && ToolbarExtras && (
339
- <ToolbarExtras updateConfig={updateActiveFolderConfig} />
340
- )}
344
+ {currentView === "folder-detail" &&
345
+ ToolbarExtras &&
346
+ activeConfig && (
347
+ <ToolbarExtras
348
+ updateConfig={updateActiveFolderConfig}
349
+ folderId={activeFolder.id}
350
+ label={activeFolder.label}
351
+ config={activeConfig}
352
+ />
353
+ )}
341
354
  </Box>
342
355
  )}
343
356
 
@@ -365,10 +378,9 @@ export default function TrackSelect({
365
378
  {/* Right panel - always visible */}
366
379
  <Box sx={{ flex: 2, minWidth: 0 }}>
367
380
  <TreeViewWrapper
368
- items={treeItems}
381
+ folderTrees={folderTrees}
369
382
  selectedCount={selectedCount}
370
383
  onRemove={handleRemoveTreeItem}
371
- TreeItemComponent={activeFolder.TreeItemComponent}
372
384
  />
373
385
  </Box>
374
386
  </Stack>
@@ -47,8 +47,8 @@ function CustomLabel({
47
47
  renderIcon,
48
48
  ...other
49
49
  }: CustomLabelProps) {
50
- const variant = isAssayItem ? "subtitle2" : "body2";
51
- const fontWeight = isAssayItem ? "bold" : 500;
50
+ const variant = "body2";
51
+ const fontWeight = 500;
52
52
  const labelText = typeof children === "string" ? children : "";
53
53
  return (
54
54
  <TreeItemLabel
@@ -79,9 +79,6 @@ function CustomLabel({
79
79
  alignItems="center"
80
80
  sx={{ minWidth: 0, overflow: "hidden", flex: 1 }}
81
81
  >
82
- {isAssayItem && renderIcon && (
83
- <Box sx={{ flexShrink: 0 }}>{renderIcon(other.id)}</Box>
84
- )}
85
82
  {assayName && renderIcon && (
86
83
  <Box sx={{ flexShrink: 0 }}>{renderIcon(assayName)}</Box>
87
84
  )}
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react";
4
4
  import {
5
5
  CustomTreeItemProps,
6
6
  ExtendedTreeItemProps,
7
+ FolderTreeConfig,
7
8
  TreeViewWrapperProps,
8
9
  } from "../types";
9
10
  import { CustomTreeItem } from "./CustomTreeItem";
@@ -24,12 +25,18 @@ function getAllExpandableItemIds(
24
25
  return ids;
25
26
  }
26
27
 
27
- export function TreeViewWrapper({
28
+ /**
29
+ * Internal component that renders a single folder's tree with its own expanded state.
30
+ */
31
+ function FolderTree({
28
32
  items,
29
- selectedCount,
30
- onRemove,
31
33
  TreeItemComponent,
32
- }: TreeViewWrapperProps) {
34
+ onRemove,
35
+ }: {
36
+ items: FolderTreeConfig["items"];
37
+ TreeItemComponent: FolderTreeConfig["TreeItemComponent"];
38
+ onRemove: (item: TreeViewBaseItem<ExtendedTreeItemProps>) => void;
39
+ }) {
33
40
  const allExpandableIds = useMemo(
34
41
  () => getAllExpandableItemIds(items),
35
42
  [items],
@@ -56,6 +63,31 @@ export function TreeViewWrapper({
56
63
 
57
64
  const TreeItem = TreeItemComponent ?? CustomTreeItem;
58
65
 
66
+ return (
67
+ <RichTreeView
68
+ items={items}
69
+ expandedItems={expandedItems}
70
+ onExpandedItemsChange={(_event, ids) => setExpandedItems(ids)}
71
+ slots={{ item: TreeItem }}
72
+ slotProps={{
73
+ item: {
74
+ onRemove: handleRemoveTreeItem,
75
+ } as Partial<CustomTreeItemProps>,
76
+ }}
77
+ sx={{
78
+ ml: 1,
79
+ mr: 1,
80
+ }}
81
+ itemChildrenIndentation={0}
82
+ />
83
+ );
84
+ }
85
+
86
+ export function TreeViewWrapper({
87
+ folderTrees,
88
+ selectedCount,
89
+ onRemove,
90
+ }: TreeViewWrapperProps) {
59
91
  return (
60
92
  <Paper
61
93
  sx={{
@@ -99,23 +131,14 @@ export function TreeViewWrapper({
99
131
  overflow: "auto",
100
132
  }}
101
133
  >
102
- <RichTreeView
103
- items={items}
104
- expandedItems={expandedItems}
105
- onExpandedItemsChange={(_event, ids) => setExpandedItems(ids)}
106
- slots={{ item: TreeItem }}
107
- slotProps={{
108
- item: {
109
- onRemove: handleRemoveTreeItem,
110
- } as Partial<CustomTreeItemProps>,
111
- }}
112
- sx={{
113
- ml: 1,
114
- mr: 1,
115
- height: "100%",
116
- }}
117
- itemChildrenIndentation={0}
118
- />
134
+ {folderTrees.map((folderTree) => (
135
+ <FolderTree
136
+ key={folderTree.folderId}
137
+ items={folderTree.items}
138
+ TreeItemComponent={folderTree.TreeItemComponent}
139
+ onRemove={onRemove}
140
+ />
141
+ ))}
119
142
  </Box>
120
143
  </Paper>
121
144
  );
@@ -28,16 +28,26 @@ export type ExtendedTreeItemProps = {
28
28
  allExpAccessions?: string[];
29
29
  };
30
30
 
31
- export type TreeViewWrapperProps = {
31
+ /**
32
+ * Configuration for a single folder's tree in the TreeViewWrapper.
33
+ * Each folder gets its own tree with its own TreeItemComponent.
34
+ */
35
+ export type FolderTreeConfig = {
36
+ folderId: string;
32
37
  items: TreeViewBaseItem<ExtendedTreeItemProps>[];
33
- selectedCount: number;
34
- onRemove: (item: TreeViewBaseItem<ExtendedTreeItemProps>) => void;
35
- /** Optional custom TreeItem component */
38
+ /** Optional custom TreeItem component for this folder */
36
39
  TreeItemComponent?: React.ForwardRefExoticComponent<
37
40
  CustomTreeItemProps & React.RefAttributes<HTMLLIElement>
38
41
  >;
39
42
  };
40
43
 
44
+ export type TreeViewWrapperProps = {
45
+ /** Array of folder tree configurations, one per folder with selections */
46
+ folderTrees: FolderTreeConfig[];
47
+ selectedCount: number;
48
+ onRemove: (item: TreeViewBaseItem<ExtendedTreeItemProps>) => void;
49
+ };
50
+
41
51
  export interface CustomLabelProps {
42
52
  id: string;
43
53
  children: React.ReactNode;
package/test/main.tsx CHANGED
@@ -82,8 +82,8 @@ function Main() {
82
82
  const currentAssembly: Assembly = "mm10";
83
83
 
84
84
  const browserStore = createBrowserStoreMemo({
85
- // chr12:53,380,176-53,416,446
86
- domain: { chromosome: "chr12", start: 53380176, end: 53416446 },
85
+ // chr7:19,695,494-19,699,803
86
+ domain: { chromosome: "chr7", start: 19695494, end: 19699803 },
87
87
  marginWidth: 100,
88
88
  trackWidth: 1400,
89
89
  multiplier: 3,
@@ -359,11 +359,11 @@ export function setLocalTracks(tracks: Track[], assembly: string) {
359
359
 
360
360
  // Default selections for TrackSelect UI (uses folder row IDs)
361
361
  const defaultHumanSelections = new Map<string, Set<string>>([
362
- ["human-genes", new Set(["genocode-basic"])],
362
+ ["human-genes", new Set(["gencode-basic"])],
363
363
  ["human-biosamples", new Set(["ccre-aggregate", "dnase-aggregate"])],
364
364
  ]);
365
365
 
366
366
  const defaultMouseSelections = new Map<string, Set<string>>([
367
- ["mouse-genes", new Set(["genocode-basic"])],
367
+ ["mouse-genes", new Set(["gencode-basic"])],
368
368
  ["mouse-biosamples", new Set(["ccre-aggregate", "dnase-aggregate"])],
369
369
  ]);