@weng-lab/genomebrowser-ui 0.1.1

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.
Files changed (37) hide show
  1. package/dist/TrackSelect/Data/modifiedHumanTracks.json.d.ts +37222 -0
  2. package/dist/TrackSelect/DataGrid/CustomToolbar.d.ts +12 -0
  3. package/dist/TrackSelect/DataGrid/DataGridWrapper.d.ts +2 -0
  4. package/dist/TrackSelect/DataGrid/columns.d.ts +4 -0
  5. package/dist/TrackSelect/DataGrid/dataGridHelpers.d.ts +30 -0
  6. package/dist/TrackSelect/TrackSelect.d.ts +5 -0
  7. package/dist/TrackSelect/TreeView/TreeViewWrapper.d.ts +2 -0
  8. package/dist/TrackSelect/TreeView/treeViewHelpers.d.ts +49 -0
  9. package/dist/TrackSelect/consts.d.ts +21 -0
  10. package/dist/TrackSelect/store.d.ts +4 -0
  11. package/dist/TrackSelect/types.d.ts +123 -0
  12. package/dist/genomebrowser-ui.es.js +2299 -0
  13. package/dist/genomebrowser-ui.es.js.map +1 -0
  14. package/dist/lib.d.ts +4 -0
  15. package/eslint.config.js +30 -0
  16. package/index.html +14 -0
  17. package/package.json +47 -0
  18. package/src/TrackSelect/Data/humanTracks.json +35711 -0
  19. package/src/TrackSelect/Data/human_chromhmm_biosamples_with_all_urls.json +35716 -0
  20. package/src/TrackSelect/Data/modifiedHumanTracks.json +37220 -0
  21. package/src/TrackSelect/DataGrid/CustomToolbar.tsx +160 -0
  22. package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +119 -0
  23. package/src/TrackSelect/DataGrid/columns.tsx +134 -0
  24. package/src/TrackSelect/DataGrid/dataGridHelpers.tsx +114 -0
  25. package/src/TrackSelect/TrackSelect.tsx +258 -0
  26. package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +115 -0
  27. package/src/TrackSelect/TreeView/treeViewHelpers.tsx +428 -0
  28. package/src/TrackSelect/bug.md +4 -0
  29. package/src/TrackSelect/consts.ts +92 -0
  30. package/src/TrackSelect/store.ts +26 -0
  31. package/src/TrackSelect/types.ts +139 -0
  32. package/src/lib.ts +8 -0
  33. package/test/main.tsx +13 -0
  34. package/tsconfig.app.json +25 -0
  35. package/tsconfig.json +4 -0
  36. package/tsconfig.node.json +25 -0
  37. package/vite.config.ts +66 -0
@@ -0,0 +1,258 @@
1
+ import {
2
+ Box,
3
+ Stack,
4
+ TextField,
5
+ FormControlLabel,
6
+ Switch,
7
+ Button,
8
+ } from "@mui/material";
9
+ import { DataGridWrapper } from "./DataGrid/DataGridWrapper";
10
+ import { searchTracks, flattenIntoRow } from "./DataGrid/dataGridHelpers";
11
+ import { TreeViewWrapper } from "./TreeView/TreeViewWrapper";
12
+ import {
13
+ buildSortedAssayTreeView,
14
+ buildTreeView,
15
+ searchTreeItems,
16
+ } from "./TreeView/treeViewHelpers";
17
+ import { SearchTracksProps, ExtendedTreeItemProps } from "./types";
18
+ import { rows, rowById, getActiveTracks } from "./consts";
19
+ import React, { useState, useMemo, useEffect } from "react";
20
+ import { TreeViewBaseItem } from "@mui/x-tree-view";
21
+ import { GridRowSelectionModel } from "@mui/x-data-grid";
22
+ import {
23
+ Dialog,
24
+ DialogTitle,
25
+ DialogContent,
26
+ DialogContentText,
27
+ DialogActions,
28
+ } from "@mui/material";
29
+ import { SelectionStoreInstance } from "./store";
30
+
31
+ export interface TrackSelectProps {
32
+ store: SelectionStoreInstance;
33
+ }
34
+
35
+ export default function TrackSelect({ store }: TrackSelectProps) {
36
+ const [limitDialogOpen, setLimitDialogOpen] = useState(false);
37
+ const [sortedAssay, setSortedAssay] = useState(false);
38
+ const [searchQuery, setSearchQuery] = useState("");
39
+ const [isSearchResult, setIsSearchResult] = useState(false);
40
+ const selectedIds = store((s) => s.selectedIds);
41
+ const setSelected = store((s) => s.setSelected);
42
+ const clear = store((s) => s.clear);
43
+ const MAX_ACTIVE = store((s) => s.maxTracks);
44
+
45
+ // Derive active tracks from selectedIds (filters out auto-generated group IDs)
46
+ const activeTracks = useMemo(
47
+ () => getActiveTracks(selectedIds),
48
+ [selectedIds],
49
+ );
50
+
51
+ const treeItems = useMemo(() => {
52
+ return sortedAssay
53
+ ? buildSortedAssayTreeView(
54
+ Array.from(selectedIds),
55
+ {
56
+ id: "1",
57
+ isAssayItem: false,
58
+ label: "Biosamples",
59
+ icon: "folder",
60
+ children: [],
61
+ allRowInfo: [],
62
+ },
63
+ rowById,
64
+ )
65
+ : buildTreeView(
66
+ Array.from(selectedIds),
67
+ {
68
+ id: "1",
69
+ isAssayItem: false,
70
+ label: "Biosamples",
71
+ icon: "folder",
72
+ children: [],
73
+ allRowInfo: [],
74
+ },
75
+ rowById,
76
+ );
77
+ }, [selectedIds, sortedAssay]);
78
+
79
+ const [filteredRows, setFilteredRows] = useState(rows);
80
+ const [filteredTreeItems, setFilteredTreeItems] = useState([
81
+ {
82
+ id: "1",
83
+ isAssayItem: false,
84
+ label: "Biosamples",
85
+ icon: "folder",
86
+ children: [],
87
+ allRowInfo: [],
88
+ },
89
+ ] as TreeViewBaseItem<ExtendedTreeItemProps>[]);
90
+
91
+ useEffect(() => {
92
+ if (searchQuery === "") {
93
+ setFilteredTreeItems(treeItems);
94
+ setFilteredRows(rows);
95
+ setIsSearchResult(false);
96
+ }
97
+ }, [treeItems, searchQuery]);
98
+
99
+ const handleToggle = () => {
100
+ setSortedAssay(!sortedAssay);
101
+ };
102
+
103
+ const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
104
+ setSearchQuery(e.target.value);
105
+
106
+ const dataGridSearchProps: SearchTracksProps = {
107
+ jsonStructure: "tracks",
108
+ query: e.target.value,
109
+ keyWeightMap: [
110
+ "displayname",
111
+ "ontology",
112
+ "lifeStage",
113
+ "sampleType",
114
+ "type",
115
+ "experimentAccession",
116
+ "fileAccession",
117
+ ],
118
+ };
119
+
120
+ const treeSearchProps: SearchTracksProps = {
121
+ treeItems: treeItems,
122
+ query: e.target.value,
123
+ keyWeightMap: [
124
+ "displayname",
125
+ "ontology",
126
+ "lifeStage",
127
+ "sampleType",
128
+ "type",
129
+ "experimentAccession",
130
+ "fileAccession",
131
+ ],
132
+ };
133
+ const newDataGridRows = searchTracks(dataGridSearchProps)
134
+ .map((t) => t.item)
135
+ .map(flattenIntoRow);
136
+
137
+ // we only want the intersection of filtered tracks displayed on the DataGrid and user-selected tracks to be displayed on the tree
138
+ const newDataGridIds = newDataGridRows.map((r) => r.experimentAccession);
139
+ const retIds = searchTreeItems(treeSearchProps).map(
140
+ (r) => r.item.experimentAccession,
141
+ );
142
+ const newTreeIds = retIds.filter((i) => newDataGridIds.includes(i));
143
+
144
+ // build new tree from the newTreeIds...maybe it would be faster to prune the current tree instead of rebuilding it?
145
+ const newTreeItems = sortedAssay
146
+ ? buildSortedAssayTreeView(
147
+ newTreeIds,
148
+ {
149
+ id: "1",
150
+ isAssayItem: false,
151
+ label: "Biosamples",
152
+ icon: "folder",
153
+ children: [],
154
+ allRowInfo: [],
155
+ },
156
+ rowById,
157
+ )
158
+ : buildTreeView(
159
+ newTreeIds,
160
+ {
161
+ id: "1",
162
+ isAssayItem: false,
163
+ label: "Biosamples",
164
+ icon: "folder",
165
+ children: [],
166
+ allRowInfo: [],
167
+ },
168
+ rowById,
169
+ );
170
+
171
+ setFilteredRows(newDataGridRows);
172
+ setIsSearchResult(true);
173
+ setFilteredTreeItems(newTreeItems);
174
+ };
175
+
176
+ const handleSelection = (newSelection: GridRowSelectionModel) => {
177
+ const idsSet =
178
+ (newSelection && (newSelection as any).ids) ?? new Set<string>();
179
+ const newActiveTracks = getActiveTracks(idsSet);
180
+
181
+ // Block only if the new selection would exceed the limit
182
+ if (newActiveTracks.size > MAX_ACTIVE) {
183
+ setLimitDialogOpen(true);
184
+ return;
185
+ }
186
+
187
+ setSelected(idsSet);
188
+ };
189
+
190
+ return (
191
+ <Box sx={{ flex: 1 }}>
192
+ <Box display="flex" justifyContent="space-between" sx={{ mb: 3 }}>
193
+ <TextField
194
+ id="outlined-suffix-shrink"
195
+ label="Search tracks"
196
+ variant="outlined"
197
+ onChange={handleSearch}
198
+ sx={{ width: "400px" }}
199
+ />
200
+ <FormControlLabel
201
+ sx={{ display: "flex", justifyContent: "flex-end", mb: 1 }}
202
+ value="Sort by assay"
203
+ control={<Switch color="primary" onChange={handleToggle} />}
204
+ label="Sort by assay"
205
+ labelPlacement="end"
206
+ />
207
+ </Box>
208
+ <Stack direction="row" spacing={2} sx={{ width: "100%" }}>
209
+ <Box sx={{ flex: 3, minWidth: 0 }}>
210
+ <DataGridWrapper
211
+ rows={filteredRows}
212
+ label={
213
+ isSearchResult
214
+ ? `${filteredRows.length} Search Results`
215
+ : `${rows.length} Available Tracks`
216
+ }
217
+ selectedIds={selectedIds}
218
+ handleSelection={handleSelection}
219
+ sortedAssay={sortedAssay}
220
+ />
221
+ </Box>
222
+ <Box sx={{ flex: 2, minWidth: 0 }}>
223
+ <TreeViewWrapper
224
+ store={store}
225
+ items={filteredTreeItems}
226
+ selectedIds={selectedIds}
227
+ activeTracks={activeTracks}
228
+ isSearchResult={isSearchResult}
229
+ />
230
+ </Box>
231
+ </Stack>
232
+ <Box sx={{ justifyContent: "flex-end" }}>
233
+ <Button
234
+ variant="contained"
235
+ color="primary"
236
+ onClick={clear}
237
+ sx={{ mt: 2, justifyContent: "flex-end" }}
238
+ >
239
+ Clear Selection
240
+ </Button>
241
+ </Box>
242
+ <Dialog open={limitDialogOpen} onClose={() => setLimitDialogOpen(false)}>
243
+ <DialogTitle>Track Limit Reached</DialogTitle>
244
+ <DialogContent>
245
+ <DialogContentText>
246
+ You can select up to {MAX_ACTIVE} tracks at a time. Please remove a
247
+ track before adding another.
248
+ </DialogContentText>
249
+ </DialogContent>
250
+ <DialogActions>
251
+ <Button onClick={() => setLimitDialogOpen(false)} autoFocus>
252
+ OK
253
+ </Button>
254
+ </DialogActions>
255
+ </Dialog>
256
+ </Box>
257
+ );
258
+ }
@@ -0,0 +1,115 @@
1
+ import { Box, Paper, Typography } from "@mui/material";
2
+ import { RichTreeView, TreeViewBaseItem } from "@mui/x-tree-view";
3
+ import { rowById } from "../consts";
4
+ import {
5
+ CustomTreeItemProps,
6
+ ExtendedTreeItemProps,
7
+ TreeViewWrapperProps,
8
+ } from "../types";
9
+ import { CustomTreeItem } from "./treeViewHelpers";
10
+ import { Avatar } from "@mui/material";
11
+
12
+ export function TreeViewWrapper({
13
+ store,
14
+ items,
15
+ activeTracks,
16
+ isSearchResult,
17
+ }: TreeViewWrapperProps) {
18
+ const removeIds = store((s) => s.removeIds);
19
+
20
+ const handleRemoveTreeItem = (
21
+ item: TreeViewBaseItem<ExtendedTreeItemProps>,
22
+ ) => {
23
+ const removedIds = item.allExpAccessions;
24
+ if (removedIds && removedIds.length) {
25
+ const idsToRemove = new Set(removedIds);
26
+
27
+ // Also remove any auto-generated group IDs that contain these tracks
28
+ removedIds.forEach((id) => {
29
+ const row = rowById.get(id);
30
+ if (row) {
31
+ // Add the auto-generated group IDs for this track's ontology and assay
32
+ idsToRemove.add(`auto-generated-row-ontology/${row.ontology}`);
33
+ idsToRemove.add(
34
+ `auto-generated-row-ontology/${row.ontology}-assay/${row.assay}`,
35
+ );
36
+ idsToRemove.add(`auto-generated-row-assay/${row.assay}`);
37
+ idsToRemove.add(
38
+ `auto-generated-row-assay/${row.assay}-ontology/${row.ontology}`,
39
+ );
40
+ }
41
+ });
42
+ removeIds(idsToRemove);
43
+ }
44
+ };
45
+
46
+ return (
47
+ <Paper
48
+ sx={{
49
+ height: 500,
50
+ width: "100%",
51
+ border: "10px solid",
52
+ borderColor: "grey.200",
53
+ boxSizing: "border-box",
54
+ borderRadius: 2,
55
+ display: "flex",
56
+ flexDirection: "column",
57
+ }}
58
+ >
59
+ <Box
60
+ sx={{
61
+ display: "flex",
62
+ alignItems: "center",
63
+ gap: 1,
64
+ py: 1,
65
+ backgroundColor: "grey.200",
66
+ flexShrink: 0,
67
+ }}
68
+ >
69
+ <Avatar
70
+ sx={{
71
+ width: 30,
72
+ height: 30,
73
+ fontSize: 14,
74
+ fontWeight: "bold",
75
+ bgcolor: "white",
76
+ color: "text.primary",
77
+ }}
78
+ >
79
+ {activeTracks.size}
80
+ </Avatar>
81
+ <Typography fontWeight="bold">
82
+ Active Tracks
83
+ {isSearchResult && (
84
+ <Typography component="span" color="text.secondary" sx={{ ml: 1 }}>
85
+ ({items[0].allRowInfo!.length} search results)
86
+ </Typography>
87
+ )}
88
+ </Typography>
89
+ </Box>
90
+ <Box
91
+ sx={{
92
+ flex: 1,
93
+ overflow: "auto",
94
+ }}
95
+ >
96
+ <RichTreeView
97
+ items={items}
98
+ defaultExpandedItems={["1"]}
99
+ slots={{ item: CustomTreeItem }}
100
+ slotProps={{
101
+ item: {
102
+ onRemove: handleRemoveTreeItem,
103
+ } as Partial<CustomTreeItemProps>,
104
+ }}
105
+ sx={{
106
+ ml: 1,
107
+ mr: 1,
108
+ height: "100%",
109
+ }}
110
+ itemChildrenIndentation={0}
111
+ />
112
+ </Box>
113
+ </Paper>
114
+ );
115
+ }