@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,428 @@
|
|
|
1
|
+
import Folder from "@mui/icons-material/Folder";
|
|
2
|
+
import IndeterminateCheckBoxRoundedIcon from "@mui/icons-material/IndeterminateCheckBoxRounded";
|
|
3
|
+
import { Box, Typography, Stack } from "@mui/material";
|
|
4
|
+
import Collapse from "@mui/material/Collapse";
|
|
5
|
+
import { alpha, styled } from "@mui/material/styles";
|
|
6
|
+
import { TreeViewBaseItem } from "@mui/x-tree-view";
|
|
7
|
+
import {
|
|
8
|
+
TreeItemCheckbox,
|
|
9
|
+
TreeItemIconContainer,
|
|
10
|
+
TreeItemLabel,
|
|
11
|
+
} from "@mui/x-tree-view/TreeItem";
|
|
12
|
+
import { TreeItemIcon } from "@mui/x-tree-view/TreeItemIcon";
|
|
13
|
+
import { TreeItemProvider } from "@mui/x-tree-view/TreeItemProvider";
|
|
14
|
+
import { useTreeItemModel } from "@mui/x-tree-view/hooks";
|
|
15
|
+
import { useTreeItem } from "@mui/x-tree-view/useTreeItem";
|
|
16
|
+
import React from "react";
|
|
17
|
+
import {
|
|
18
|
+
CustomLabelProps,
|
|
19
|
+
CustomTreeItemProps,
|
|
20
|
+
ExtendedTreeItemProps,
|
|
21
|
+
RowInfo,
|
|
22
|
+
} from "../types";
|
|
23
|
+
import Fuse, { FuseResult } from "fuse.js";
|
|
24
|
+
import { SearchTracksProps } from "../types";
|
|
25
|
+
import { assayTypes, ontologyTypes } from "../consts";
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Builds tree in the sorted by assay view
|
|
30
|
+
* @param selectedIds: list of ids (from useSelectionStore)
|
|
31
|
+
* @param root: Root TreeViewBaseItem
|
|
32
|
+
* @param rowById: Mapping between an id (experimentAccession) and its RowInfo object
|
|
33
|
+
* @returns all of the items for the RichTreeView in TreeViewWrapper
|
|
34
|
+
*/
|
|
35
|
+
export function buildSortedAssayTreeView(
|
|
36
|
+
selectedIds: string[],
|
|
37
|
+
root: TreeViewBaseItem<ExtendedTreeItemProps>,
|
|
38
|
+
rowById: Map<string, RowInfo>,
|
|
39
|
+
): TreeViewBaseItem<ExtendedTreeItemProps>[] {
|
|
40
|
+
const assayMap = new Map<string, TreeViewBaseItem<ExtendedTreeItemProps>>(); // keep track of top level nodes
|
|
41
|
+
const ontologyMap = new Map<
|
|
42
|
+
string,
|
|
43
|
+
TreeViewBaseItem<ExtendedTreeItemProps>
|
|
44
|
+
>();
|
|
45
|
+
const sampleAssayMap = new Map<
|
|
46
|
+
string,
|
|
47
|
+
TreeViewBaseItem<ExtendedTreeItemProps>
|
|
48
|
+
>();
|
|
49
|
+
let idx = 1;
|
|
50
|
+
|
|
51
|
+
const selectedRows = selectedIds.reduce<RowInfo[]>((acc, id) => {
|
|
52
|
+
const row = rowById.get(id);
|
|
53
|
+
if (row) acc.push(row);
|
|
54
|
+
return acc;
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
selectedRows.forEach((row) => {
|
|
58
|
+
let assayNode = assayMap.get(row.assay);
|
|
59
|
+
if (!assayNode) {
|
|
60
|
+
assayNode = {
|
|
61
|
+
id: row.assay,
|
|
62
|
+
isAssayItem: true,
|
|
63
|
+
label: row.assay,
|
|
64
|
+
icon: "removeable",
|
|
65
|
+
children: [],
|
|
66
|
+
allExpAccessions: [],
|
|
67
|
+
};
|
|
68
|
+
assayMap.set(row.assay, assayNode);
|
|
69
|
+
root.children!.push(assayNode);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let ontologyNode = ontologyMap.get(row.ontology + row.assay);
|
|
73
|
+
if (!ontologyNode) {
|
|
74
|
+
ontologyNode = {
|
|
75
|
+
id: row.ontology + "_" + idx++,
|
|
76
|
+
isAssayItem: false,
|
|
77
|
+
label: row.ontology,
|
|
78
|
+
icon: "removeable",
|
|
79
|
+
children: [],
|
|
80
|
+
allExpAccessions: [],
|
|
81
|
+
};
|
|
82
|
+
assayNode.children!.push(ontologyNode);
|
|
83
|
+
ontologyMap.set(row.ontology + row.assay, ontologyNode);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const displayNameNode: TreeViewBaseItem<ExtendedTreeItemProps> = {
|
|
87
|
+
id: row.displayname + "_" + idx++,
|
|
88
|
+
isAssayItem: false,
|
|
89
|
+
label: row.displayname,
|
|
90
|
+
icon: "removeable",
|
|
91
|
+
children: [],
|
|
92
|
+
allExpAccessions: [],
|
|
93
|
+
};
|
|
94
|
+
ontologyNode.children!.push(displayNameNode);
|
|
95
|
+
|
|
96
|
+
let expNode = sampleAssayMap.get(row.displayname + row.experimentAccession);
|
|
97
|
+
if (!expNode) {
|
|
98
|
+
expNode = {
|
|
99
|
+
id: row.experimentAccession,
|
|
100
|
+
isAssayItem: false,
|
|
101
|
+
label: row.experimentAccession,
|
|
102
|
+
icon: row.assay,
|
|
103
|
+
children: [],
|
|
104
|
+
};
|
|
105
|
+
sampleAssayMap.set(row.displayname + row.assay, expNode);
|
|
106
|
+
displayNameNode.children!.push(expNode);
|
|
107
|
+
}
|
|
108
|
+
assayNode.allExpAccessions!.push(row.experimentAccession);
|
|
109
|
+
ontologyNode.allExpAccessions!.push(row.experimentAccession);
|
|
110
|
+
displayNameNode.allExpAccessions!.push(row.experimentAccession);
|
|
111
|
+
root.allRowInfo!.push(row);
|
|
112
|
+
});
|
|
113
|
+
// standardize the order of the assay folders everytime one is added
|
|
114
|
+
root.children!.sort((a, b): number => {
|
|
115
|
+
return assayTypes.indexOf(a.id) - assayTypes.indexOf(b.id);
|
|
116
|
+
});
|
|
117
|
+
return [root];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Builds tree in the sorted by assay view
|
|
122
|
+
* @param selectedIds: list of ids (from useSelectionStore)
|
|
123
|
+
* @param root: Root TreeViewBaseItem
|
|
124
|
+
* @param rowById: Mapping between an id (experimentAccession) and its RowInfo object
|
|
125
|
+
* @returns all of the items for the RichTreeView in TreeViewWrapper
|
|
126
|
+
*/
|
|
127
|
+
export function buildTreeView(
|
|
128
|
+
selectedIds: string[],
|
|
129
|
+
root: TreeViewBaseItem<ExtendedTreeItemProps>,
|
|
130
|
+
rowById: Map<string, RowInfo>,
|
|
131
|
+
): TreeViewBaseItem<ExtendedTreeItemProps>[] {
|
|
132
|
+
const ontologyMap = new Map<
|
|
133
|
+
string,
|
|
134
|
+
TreeViewBaseItem<ExtendedTreeItemProps>
|
|
135
|
+
>(); // keep track of top level nodes
|
|
136
|
+
const displayNameMap = new Map<
|
|
137
|
+
string,
|
|
138
|
+
TreeViewBaseItem<ExtendedTreeItemProps>
|
|
139
|
+
>();
|
|
140
|
+
const sampleAssayMap = new Map<
|
|
141
|
+
string,
|
|
142
|
+
TreeViewBaseItem<ExtendedTreeItemProps>
|
|
143
|
+
>();
|
|
144
|
+
|
|
145
|
+
const selectedRows = selectedIds.reduce<RowInfo[]>((acc, id) => {
|
|
146
|
+
const row = rowById.get(id);
|
|
147
|
+
if (row) acc.push(row);
|
|
148
|
+
return acc;
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
selectedRows.forEach((row) => {
|
|
152
|
+
if (!row) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let ontologyNode = ontologyMap.get(row.ontology);
|
|
156
|
+
if (!ontologyNode) {
|
|
157
|
+
ontologyNode = {
|
|
158
|
+
id: row.ontology,
|
|
159
|
+
label: row.ontology,
|
|
160
|
+
icon: "removeable",
|
|
161
|
+
children: [],
|
|
162
|
+
allExpAccessions: [],
|
|
163
|
+
};
|
|
164
|
+
ontologyMap.set(row.ontology, ontologyNode);
|
|
165
|
+
root.children!.push(ontologyNode);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let displayNameNode = displayNameMap.get(row.displayname);
|
|
169
|
+
if (!displayNameNode) {
|
|
170
|
+
displayNameNode = {
|
|
171
|
+
id: row.displayname,
|
|
172
|
+
label: row.displayname,
|
|
173
|
+
icon: "removeable",
|
|
174
|
+
children: [],
|
|
175
|
+
allExpAccessions: [],
|
|
176
|
+
};
|
|
177
|
+
ontologyNode.children!.push(displayNameNode);
|
|
178
|
+
displayNameMap.set(row.displayname, displayNameNode);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let expNode = sampleAssayMap.get(row.displayname + row.assay);
|
|
182
|
+
if (!expNode) {
|
|
183
|
+
expNode = {
|
|
184
|
+
id: row.experimentAccession,
|
|
185
|
+
label: row.experimentAccession,
|
|
186
|
+
icon: row.assay,
|
|
187
|
+
children: [],
|
|
188
|
+
};
|
|
189
|
+
sampleAssayMap.set(row.displayname + row.assay, expNode);
|
|
190
|
+
displayNameNode.children!.push(expNode);
|
|
191
|
+
}
|
|
192
|
+
ontologyNode.allExpAccessions!.push(row.experimentAccession);
|
|
193
|
+
displayNameNode.allExpAccessions!.push(row.experimentAccession);
|
|
194
|
+
root.allRowInfo!.push(row);
|
|
195
|
+
});
|
|
196
|
+
// standardize the order of the assay folders everytime one is added
|
|
197
|
+
root.children!.sort((a, b): number => {
|
|
198
|
+
return ontologyTypes.indexOf(a.id) - ontologyTypes.indexOf(b.id);
|
|
199
|
+
});
|
|
200
|
+
return [root];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Fuzzy search of active tracks.
|
|
205
|
+
*
|
|
206
|
+
* @param treeItems - TreeBaseViewItems from the tree.
|
|
207
|
+
* @param query - The search query string.
|
|
208
|
+
* @param keyWeightMap - Array of keys to search within each track object.
|
|
209
|
+
* Can look like ["name", "author"] or if weighted, [
|
|
210
|
+
{
|
|
211
|
+
name: 'title',
|
|
212
|
+
weight: 0.3
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'author',
|
|
216
|
+
weight: 0.7
|
|
217
|
+
}
|
|
218
|
+
].
|
|
219
|
+
* @param threshold - (Optional) Threshold for the fuzzy search (default is 0.5).
|
|
220
|
+
* Smaller = stricter match, larger = fuzzier since 0 is perfect match and 1 is worst match.
|
|
221
|
+
* @param limit - (Optional) Maximum number of results to return (default is 10).
|
|
222
|
+
* @returns FuseResult object containing the search results.
|
|
223
|
+
*/
|
|
224
|
+
export function searchTreeItems({
|
|
225
|
+
treeItems,
|
|
226
|
+
query,
|
|
227
|
+
keyWeightMap,
|
|
228
|
+
threshold,
|
|
229
|
+
limit = 10
|
|
230
|
+
}: SearchTracksProps): FuseResult<RowInfo>[] {
|
|
231
|
+
const data = treeItems![0].allRowInfo ?? [];
|
|
232
|
+
const fuse = new Fuse(data, {
|
|
233
|
+
includeScore: true,
|
|
234
|
+
shouldSort: true,
|
|
235
|
+
threshold: threshold,
|
|
236
|
+
keys: keyWeightMap,
|
|
237
|
+
});
|
|
238
|
+
return fuse.search(query, { limit: limit });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Creates the assay icon for DataGrid and RichTreeView
|
|
243
|
+
* @param type: assay type
|
|
244
|
+
* @returns an icon of the assay's respective color
|
|
245
|
+
*/
|
|
246
|
+
export function AssayIcon(type: string) {
|
|
247
|
+
const colorMap: { [key: string]: string } = {
|
|
248
|
+
DNase: "#06da93",
|
|
249
|
+
ATAC: "#02c7b9",
|
|
250
|
+
H3K4me3: "#ff2020",
|
|
251
|
+
ChromHMM: "#0097a7",
|
|
252
|
+
H3K27ac: "#fdc401",
|
|
253
|
+
CTCF: "#01a6f1",
|
|
254
|
+
};
|
|
255
|
+
const color = colorMap[type];
|
|
256
|
+
return (
|
|
257
|
+
<Box
|
|
258
|
+
sx={{
|
|
259
|
+
width: 12,
|
|
260
|
+
height: 12,
|
|
261
|
+
borderRadius: "20%",
|
|
262
|
+
bgcolor: color,
|
|
263
|
+
}}
|
|
264
|
+
/>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Everything below is styling for the custom directory look of the tree view
|
|
269
|
+
const TreeItemRoot = styled("li")(({ theme }) => ({
|
|
270
|
+
listStyle: "none",
|
|
271
|
+
margin: 0,
|
|
272
|
+
padding: 0,
|
|
273
|
+
outline: 4,
|
|
274
|
+
color: theme.palette.grey[400],
|
|
275
|
+
...theme.applyStyles("light", {
|
|
276
|
+
color: theme.palette.grey[600], // controls colors of the MUI icons
|
|
277
|
+
}),
|
|
278
|
+
}));
|
|
279
|
+
|
|
280
|
+
const TreeItemLabelText = styled(Typography)({
|
|
281
|
+
color: "black",
|
|
282
|
+
fontFamily: "inherit",
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
function CustomLabel({ icon: Icon, children, ...other }: CustomLabelProps) {
|
|
286
|
+
const variant = other.isAssayItem ? "subtitle2" : "body2";
|
|
287
|
+
const fontWeight = other.isAssayItem ? "bold" : 500;
|
|
288
|
+
return (
|
|
289
|
+
<TreeItemLabel
|
|
290
|
+
{...other}
|
|
291
|
+
sx={{
|
|
292
|
+
display: "flex",
|
|
293
|
+
alignItems: "center",
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
{Icon && React.isValidElement(Icon) ? (
|
|
297
|
+
<Box className="labelIcon" sx={{ mr: 1 }}>
|
|
298
|
+
{Icon}
|
|
299
|
+
</Box>
|
|
300
|
+
) : (
|
|
301
|
+
<Box
|
|
302
|
+
component={Icon as React.ElementType}
|
|
303
|
+
className="labelIcon"
|
|
304
|
+
color="inherit"
|
|
305
|
+
sx={{ mr: 1, fontSize: "1.2rem" }}
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
308
|
+
<Stack direction="row" spacing={2} alignItems="center">
|
|
309
|
+
{ other.isAssayItem && AssayIcon(other.id) }
|
|
310
|
+
<TreeItemLabelText fontWeight={fontWeight} variant={variant}>{children}</TreeItemLabelText>
|
|
311
|
+
</Stack>
|
|
312
|
+
</TreeItemLabel>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const TreeItemContent = styled("div")(({ theme }) => ({
|
|
317
|
+
padding: theme.spacing(0.5),
|
|
318
|
+
paddingRight: theme.spacing(2),
|
|
319
|
+
paddingLeft: `calc(${theme.spacing(1)} + var(--TreeView-itemChildrenIndentation) * var(--TreeView-itemDepth))`,
|
|
320
|
+
width: "100%",
|
|
321
|
+
boxSizing: "border-box", // prevent width + padding to overflow
|
|
322
|
+
position: "relative",
|
|
323
|
+
display: "flex",
|
|
324
|
+
alignItems: "center",
|
|
325
|
+
gap: theme.spacing(1),
|
|
326
|
+
cursor: "pointer",
|
|
327
|
+
WebkitTapHighlightColor: "transparent",
|
|
328
|
+
flexDirection: "row-reverse",
|
|
329
|
+
borderRadius: theme.spacing(0.7),
|
|
330
|
+
marginBottom: theme.spacing(0.5),
|
|
331
|
+
marginTop: theme.spacing(0.5),
|
|
332
|
+
fontWeight: 500,
|
|
333
|
+
[`&[data-focused], &[data-selected]`]: {
|
|
334
|
+
backgroundColor: theme.palette.primary.dark,
|
|
335
|
+
color: theme.palette.primary.contrastText,
|
|
336
|
+
...theme.applyStyles("light", {
|
|
337
|
+
backgroundColor: theme.palette.primary.main,
|
|
338
|
+
}),
|
|
339
|
+
},
|
|
340
|
+
"&:not([data-focused], [data-selected]):hover": {
|
|
341
|
+
backgroundColor: alpha(theme.palette.primary.main, 0.1),
|
|
342
|
+
color: "white",
|
|
343
|
+
...theme.applyStyles("light", {
|
|
344
|
+
color: theme.palette.primary.main,
|
|
345
|
+
}),
|
|
346
|
+
},
|
|
347
|
+
}));
|
|
348
|
+
|
|
349
|
+
const getIconFromTreeItemType = (itemType: string) => {
|
|
350
|
+
switch (itemType) {
|
|
351
|
+
case "folder":
|
|
352
|
+
return Folder;
|
|
353
|
+
case "removeable":
|
|
354
|
+
return IndeterminateCheckBoxRoundedIcon;
|
|
355
|
+
default:
|
|
356
|
+
return AssayIcon(itemType);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
export const CustomTreeItem = React.forwardRef(function CustomTreeItem(
|
|
361
|
+
props: CustomTreeItemProps,
|
|
362
|
+
ref: React.Ref<HTMLLIElement>,
|
|
363
|
+
) {
|
|
364
|
+
const { id, itemId, label, disabled, children, onRemove, ...other } = props;
|
|
365
|
+
|
|
366
|
+
const {
|
|
367
|
+
getContextProviderProps,
|
|
368
|
+
getRootProps,
|
|
369
|
+
getContentProps,
|
|
370
|
+
getIconContainerProps,
|
|
371
|
+
getCheckboxProps,
|
|
372
|
+
getLabelProps,
|
|
373
|
+
getGroupTransitionProps,
|
|
374
|
+
status,
|
|
375
|
+
} = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref });
|
|
376
|
+
|
|
377
|
+
const item = useTreeItemModel<ExtendedTreeItemProps>(itemId)!;
|
|
378
|
+
const icon = getIconFromTreeItemType(item.icon);
|
|
379
|
+
|
|
380
|
+
const handleRemoveIconClick = (e: React.MouseEvent) => {
|
|
381
|
+
e.stopPropagation(); // prevent item expand/select
|
|
382
|
+
onRemove?.(item);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<TreeItemProvider {...getContextProviderProps()}>
|
|
387
|
+
<TreeItemRoot {...getRootProps(other)}>
|
|
388
|
+
<TreeItemContent {...getContentProps()}>
|
|
389
|
+
<TreeItemIconContainer {...getIconContainerProps()}>
|
|
390
|
+
<TreeItemIcon status={status} />
|
|
391
|
+
</TreeItemIconContainer>
|
|
392
|
+
<TreeItemCheckbox {...getCheckboxProps()} />
|
|
393
|
+
<CustomLabel
|
|
394
|
+
{...getLabelProps({
|
|
395
|
+
icon:
|
|
396
|
+
item.icon === "removeable" ? (
|
|
397
|
+
<Box
|
|
398
|
+
onClick={handleRemoveIconClick}
|
|
399
|
+
sx={{
|
|
400
|
+
width: 20,
|
|
401
|
+
height: 20,
|
|
402
|
+
display: "flex",
|
|
403
|
+
alignItems: "center",
|
|
404
|
+
justifyContent: "center",
|
|
405
|
+
borderRadius: "4px",
|
|
406
|
+
cursor: "pointer",
|
|
407
|
+
mr: 1,
|
|
408
|
+
"&:hover": {
|
|
409
|
+
backgroundColor: "rgba(0,0,0,0.1)",
|
|
410
|
+
},
|
|
411
|
+
}}
|
|
412
|
+
>
|
|
413
|
+
<IndeterminateCheckBoxRoundedIcon fontSize="small" />
|
|
414
|
+
</Box>
|
|
415
|
+
) : (
|
|
416
|
+
icon
|
|
417
|
+
),
|
|
418
|
+
expandable: (status.expandable && status.expanded).toString(),
|
|
419
|
+
isAssayItem: item.isAssayItem,
|
|
420
|
+
id: item.id
|
|
421
|
+
})}
|
|
422
|
+
/>
|
|
423
|
+
</TreeItemContent>
|
|
424
|
+
{children && <Collapse {...getGroupTransitionProps()} />}
|
|
425
|
+
</TreeItemRoot>
|
|
426
|
+
</TreeItemProvider>
|
|
427
|
+
);
|
|
428
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Known Bugs/Issues
|
|
2
|
+
|
|
3
|
+
1. There's a lag in the rendering of the search results + the words in the search bar.
|
|
4
|
+
2. Upon reaching the limit of added tracks, you're unable to remove tracks from the DataGrid and repeatedly triggers the dialog to pop up whenever you try to unselect tracks from the DataGrid.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTracksByAssayAndOntology,
|
|
3
|
+
flattenIntoRow,
|
|
4
|
+
} from "./DataGrid/dataGridHelpers";
|
|
5
|
+
import { RowInfo, TrackInfo } from "./types";
|
|
6
|
+
|
|
7
|
+
export const assayTypes = [
|
|
8
|
+
"DNase",
|
|
9
|
+
"H3K4me3",
|
|
10
|
+
"H3K27ac",
|
|
11
|
+
"ATAC",
|
|
12
|
+
"CTCF",
|
|
13
|
+
"ChromHMM",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const ontologyTypes = [
|
|
17
|
+
"Adipose",
|
|
18
|
+
"Adrenal gland",
|
|
19
|
+
"Blood",
|
|
20
|
+
"Blood vessel",
|
|
21
|
+
"Bone",
|
|
22
|
+
"Bone marrow",
|
|
23
|
+
"Brain",
|
|
24
|
+
"Breast",
|
|
25
|
+
"Connective tissue",
|
|
26
|
+
"Embryo",
|
|
27
|
+
"Epithelium",
|
|
28
|
+
"Esophagus",
|
|
29
|
+
"Eye",
|
|
30
|
+
"Fallopian Tube",
|
|
31
|
+
"Gallbladder",
|
|
32
|
+
"Heart",
|
|
33
|
+
"Kidney",
|
|
34
|
+
"Large Intestine",
|
|
35
|
+
"Limb",
|
|
36
|
+
"Liver",
|
|
37
|
+
"Lung",
|
|
38
|
+
"Lymphoid Tissue",
|
|
39
|
+
"Muscle",
|
|
40
|
+
"Mouth",
|
|
41
|
+
"Nerve",
|
|
42
|
+
"Nose",
|
|
43
|
+
"Pancreas",
|
|
44
|
+
"Parathyroid Gland",
|
|
45
|
+
"Ovary",
|
|
46
|
+
"Penis",
|
|
47
|
+
"Placenta",
|
|
48
|
+
"Prostate",
|
|
49
|
+
"Skin",
|
|
50
|
+
"Small Intestine",
|
|
51
|
+
"Spinal Cord",
|
|
52
|
+
"Spleen",
|
|
53
|
+
"Stomach",
|
|
54
|
+
"Testis",
|
|
55
|
+
"Thymus",
|
|
56
|
+
"Thyroid",
|
|
57
|
+
"Urinary Bladder",
|
|
58
|
+
"Uterus",
|
|
59
|
+
"Vagina",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export const rows = ontologyTypes.flatMap((ontology) =>
|
|
63
|
+
assayTypes.flatMap((assay) =>
|
|
64
|
+
getTracksByAssayAndOntology(
|
|
65
|
+
assay.toLowerCase(),
|
|
66
|
+
ontology.toLowerCase(),
|
|
67
|
+
).map((r: TrackInfo) => {
|
|
68
|
+
const flat = flattenIntoRow(r);
|
|
69
|
+
return {
|
|
70
|
+
...flat,
|
|
71
|
+
assay,
|
|
72
|
+
ontology,
|
|
73
|
+
};
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// map of experimentAccession -> rowInfo for faster row lookup
|
|
79
|
+
export const rowById = new Map<string, RowInfo>(
|
|
80
|
+
rows.map((r) => [r.experimentAccession, r]),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if an ID is a real track (exists in rowById) vs an auto-generated group ID
|
|
85
|
+
*/
|
|
86
|
+
export const isTrackId = (id: string): boolean => rowById.has(id);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Filter a set of IDs to return only real track IDs (no auto-generated group IDs)
|
|
90
|
+
*/
|
|
91
|
+
export const getActiveTracks = (selectedIds: Set<string>): Set<string> =>
|
|
92
|
+
new Set(Array.from(selectedIds).filter(isTrackId));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { create, StoreApi, UseBoundStore } from "zustand";
|
|
2
|
+
import { SelectionState, SelectionAction } from "./types";
|
|
3
|
+
|
|
4
|
+
export type SelectionStoreInstance = UseBoundStore<
|
|
5
|
+
StoreApi<SelectionState & SelectionAction>
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
export function createSelectionStore() {
|
|
9
|
+
return create<SelectionState & SelectionAction>((set) => ({
|
|
10
|
+
maxTracks: 30,
|
|
11
|
+
selectedIds: new Set<string>(),
|
|
12
|
+
setSelected: (ids: Set<string>) =>
|
|
13
|
+
set(() => ({
|
|
14
|
+
selectedIds: new Set(ids),
|
|
15
|
+
})),
|
|
16
|
+
removeIds: (removedIds: Set<string>) =>
|
|
17
|
+
set((state) => {
|
|
18
|
+
const next = new Set(state.selectedIds);
|
|
19
|
+
removedIds.forEach((id) => {
|
|
20
|
+
next.delete(id);
|
|
21
|
+
});
|
|
22
|
+
return { selectedIds: next };
|
|
23
|
+
}),
|
|
24
|
+
clear: () => set(() => ({ selectedIds: new Set<string>() })),
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { FuseOptionKey } from "fuse.js";
|
|
2
|
+
import { UseTreeItemParameters } from "@mui/x-tree-view/useTreeItem";
|
|
3
|
+
import { TreeViewBaseItem } from "@mui/x-tree-view";
|
|
4
|
+
import {
|
|
5
|
+
DataGridPremiumProps,
|
|
6
|
+
GridRowSelectionModel,
|
|
7
|
+
} from "@mui/x-data-grid-premium";
|
|
8
|
+
import { ReactElement, ReactNode } from "react";
|
|
9
|
+
import { SvgIconOwnProps } from "@mui/material";
|
|
10
|
+
import { SelectionStoreInstance } from "./store";
|
|
11
|
+
|
|
12
|
+
export interface SearchTracksProps {
|
|
13
|
+
query: string;
|
|
14
|
+
keyWeightMap: FuseOptionKey<any>[];
|
|
15
|
+
jsonStructure?: string;
|
|
16
|
+
treeItems?: TreeViewBaseItem<ExtendedTreeItemProps>[];
|
|
17
|
+
threshold?: number;
|
|
18
|
+
limit?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Types for the JSON-formatted tracks fomr modifiedHumanTracks.json
|
|
23
|
+
*/
|
|
24
|
+
export type AssayInfo = {
|
|
25
|
+
assay: string;
|
|
26
|
+
url: string;
|
|
27
|
+
experimentAccession: string;
|
|
28
|
+
fileAccession: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type TrackInfo = {
|
|
32
|
+
name: string;
|
|
33
|
+
ontology: string;
|
|
34
|
+
lifeStage: string;
|
|
35
|
+
sampleType: string;
|
|
36
|
+
displayname: string;
|
|
37
|
+
assays: AssayInfo[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Row format for DataGrid
|
|
42
|
+
*/
|
|
43
|
+
export type RowInfo = {
|
|
44
|
+
ontology: string;
|
|
45
|
+
lifeStage: string;
|
|
46
|
+
sampleType: string;
|
|
47
|
+
displayname: string;
|
|
48
|
+
assay: string;
|
|
49
|
+
experimentAccession: string;
|
|
50
|
+
fileAccession: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Custom Tree Props for RichTreeView Panel
|
|
55
|
+
*/
|
|
56
|
+
export type ExtendedTreeItemProps = {
|
|
57
|
+
id: string;
|
|
58
|
+
label: string;
|
|
59
|
+
icon: string;
|
|
60
|
+
isAssayItem?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* list of all the experimentAccession values in the children/grandchildren of the item, or the accession of the item itself
|
|
63
|
+
* this is used in updating the rowSelectionModel when removing items from the Tree View panel
|
|
64
|
+
*/
|
|
65
|
+
allExpAccessions?: string[];
|
|
66
|
+
// list to allow search functionality in the treeview
|
|
67
|
+
allRowInfo?: RowInfo[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type TreeViewWrapperProps = {
|
|
71
|
+
store: SelectionStoreInstance;
|
|
72
|
+
items: TreeViewBaseItem<ExtendedTreeItemProps>[];
|
|
73
|
+
selectedIds: Set<string>;
|
|
74
|
+
activeTracks: Set<string>; // doesn't have the autogenerated row groupings to provide accurate number of tracks
|
|
75
|
+
isSearchResult: boolean;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export interface CustomLabelProps {
|
|
79
|
+
id: string;
|
|
80
|
+
children: React.ReactNode;
|
|
81
|
+
isAssayItem?: boolean;
|
|
82
|
+
icon: React.ElementType | React.ReactElement;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface CustomTreeItemProps
|
|
86
|
+
extends Omit<UseTreeItemParameters, "rootRef">,
|
|
87
|
+
Omit<React.HTMLAttributes<HTMLLIElement>, "onFocus"> {
|
|
88
|
+
onRemove?: (item: TreeViewBaseItem<ExtendedTreeItemProps>) => void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Types for useSelectionStore to keep track of selected DataGrid row ids/tracks
|
|
93
|
+
*/
|
|
94
|
+
export type SelectionState = {
|
|
95
|
+
maxTracks: number;
|
|
96
|
+
selectedIds: Set<string>;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type SelectionAction = {
|
|
100
|
+
setSelected: (ids: Set<string>) => void;
|
|
101
|
+
removeIds: (removedIds: Set<string>) => void;
|
|
102
|
+
clear: () => void;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* DataGrid Props
|
|
107
|
+
*/
|
|
108
|
+
interface BaseTableProps extends Omit<DataGridPremiumProps, "columns"> {
|
|
109
|
+
toolbarSlot?: ReactNode;
|
|
110
|
+
/**
|
|
111
|
+
* If anything besides an element, renders tooltip icon to the right of the table label with specified string as tooltip contents.
|
|
112
|
+
* If an element, renders the element to the right of the table label.
|
|
113
|
+
*/
|
|
114
|
+
labelTooltip?: ReactNode;
|
|
115
|
+
/**
|
|
116
|
+
* Styling object for the toolbar
|
|
117
|
+
*/
|
|
118
|
+
toolbarStyle?: React.CSSProperties;
|
|
119
|
+
/**
|
|
120
|
+
* Color passed as `htmlColor` to columns, filter, download and search icons
|
|
121
|
+
*/
|
|
122
|
+
toolbarIconColor?: SvgIconOwnProps["htmlColor"];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type DataGridWrapperProps = {
|
|
126
|
+
rows: RowInfo[];
|
|
127
|
+
selectedIds: Set<string>;
|
|
128
|
+
handleSelection: (newSelection: GridRowSelectionModel) => void;
|
|
129
|
+
sortedAssay: boolean;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
//This enforces that a downloadFileName is specified if a ReactElement is used as the label (no default )
|
|
133
|
+
export type DataGridProps = DataGridWrapperProps &
|
|
134
|
+
BaseTableProps &
|
|
135
|
+
(
|
|
136
|
+
| { label?: string; downloadFileName?: string }
|
|
137
|
+
| { label: ReactElement; downloadFileName: string }
|
|
138
|
+
| { label?: undefined; downloadFileName?: string }
|
|
139
|
+
);
|
package/src/lib.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import TrackSelect, { type TrackSelectProps } from "./TrackSelect/TrackSelect";
|
|
2
|
+
export { TrackSelect, TrackSelectProps };
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createSelectionStore,
|
|
6
|
+
type SelectionStoreInstance,
|
|
7
|
+
} from "./TrackSelect/store.ts";
|
|
8
|
+
export { createSelectionStore, SelectionStoreInstance };
|