@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.
- package/dist/TrackSelect/Data/modifiedHumanTracks.json.d.ts +37222 -0
- package/dist/TrackSelect/DataGrid/CustomToolbar.d.ts +12 -0
- package/dist/TrackSelect/DataGrid/DataGridWrapper.d.ts +2 -0
- package/dist/TrackSelect/DataGrid/columns.d.ts +4 -0
- package/dist/TrackSelect/DataGrid/dataGridHelpers.d.ts +30 -0
- package/dist/TrackSelect/TrackSelect.d.ts +5 -0
- package/dist/TrackSelect/TreeView/TreeViewWrapper.d.ts +2 -0
- package/dist/TrackSelect/TreeView/treeViewHelpers.d.ts +49 -0
- package/dist/TrackSelect/consts.d.ts +21 -0
- package/dist/TrackSelect/store.d.ts +4 -0
- package/dist/TrackSelect/types.d.ts +123 -0
- package/dist/genomebrowser-ui.es.js +2299 -0
- package/dist/genomebrowser-ui.es.js.map +1 -0
- package/dist/lib.d.ts +4 -0
- package/eslint.config.js +30 -0
- package/index.html +14 -0
- package/package.json +47 -0
- package/src/TrackSelect/Data/humanTracks.json +35711 -0
- package/src/TrackSelect/Data/human_chromhmm_biosamples_with_all_urls.json +35716 -0
- package/src/TrackSelect/Data/modifiedHumanTracks.json +37220 -0
- package/src/TrackSelect/DataGrid/CustomToolbar.tsx +160 -0
- package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +119 -0
- package/src/TrackSelect/DataGrid/columns.tsx +134 -0
- package/src/TrackSelect/DataGrid/dataGridHelpers.tsx +114 -0
- package/src/TrackSelect/TrackSelect.tsx +258 -0
- package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +115 -0
- package/src/TrackSelect/TreeView/treeViewHelpers.tsx +428 -0
- package/src/TrackSelect/bug.md +4 -0
- package/src/TrackSelect/consts.ts +92 -0
- package/src/TrackSelect/store.ts +26 -0
- package/src/TrackSelect/types.ts +139 -0
- package/src/lib.ts +8 -0
- package/test/main.tsx +13 -0
- package/tsconfig.app.json +25 -0
- package/tsconfig.json +4 -0
- package/tsconfig.node.json +25 -0
- 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
|
+
}
|