@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,160 @@
1
+ import * as React from "react";
2
+ import {
3
+ Toolbar,
4
+ ToolbarButton,
5
+ ColumnsPanelTrigger,
6
+ FilterPanelTrigger,
7
+ ExportCsv,
8
+ ExportPrint,
9
+ GridToolbarProps,
10
+ ToolbarPropsOverrides,
11
+ ExportExcel,
12
+ } from "@mui/x-data-grid-premium";
13
+ import Tooltip from "@mui/material/Tooltip";
14
+ import Menu from "@mui/material/Menu";
15
+ import Badge from "@mui/material/Badge";
16
+ import ViewColumnIcon from "@mui/icons-material/ViewColumn";
17
+ import FilterListIcon from "@mui/icons-material/FilterList";
18
+ import FileDownloadIcon from "@mui/icons-material/FileDownload";
19
+ import MenuItem from "@mui/material/MenuItem";
20
+ import Divider from "@mui/material/Divider";
21
+ import Typography from "@mui/material/Typography";
22
+ import { DataGridProps } from "../types";
23
+ import { InfoOutline } from "@mui/icons-material";
24
+
25
+ type CustomToolbarProps = {
26
+ label: DataGridProps["label"];
27
+ downloadFileName: DataGridProps["downloadFileName"];
28
+ labelTooltip: DataGridProps["labelTooltip"];
29
+ toolbarSlot?: DataGridProps["toolbarSlot"];
30
+ toolbarStyle?: DataGridProps["toolbarStyle"];
31
+ toolbarIconColor?: DataGridProps["toolbarIconColor"];
32
+ } & GridToolbarProps &
33
+ ToolbarPropsOverrides;
34
+
35
+ export function CustomToolbar({
36
+ label,
37
+ downloadFileName,
38
+ labelTooltip,
39
+ toolbarSlot,
40
+ toolbarStyle,
41
+ toolbarIconColor,
42
+ ...restToolbarProps
43
+ }: CustomToolbarProps) {
44
+ const [exportMenuOpen, setExportMenuOpen] = React.useState(false);
45
+ const exportMenuTriggerRef = React.useRef<HTMLButtonElement>(null);
46
+
47
+ const iconColor = toolbarIconColor ?? "inherit";
48
+
49
+ return (
50
+ <Toolbar style={{ ...toolbarStyle }}>
51
+ {typeof label !== "string" && label}
52
+ <Typography
53
+ fontWeight="medium"
54
+ sx={{ flex: 1, mx: 0.5, display: "flex", alignItems: "center", gap: 1 }}
55
+ >
56
+ {typeof label === "string" && label}
57
+ {/* ReactNode can be more than just an element, string, or number but not accounting for that for simplicity */}
58
+ {labelTooltip &&
59
+ (typeof labelTooltip === "string" ||
60
+ typeof labelTooltip === "number") ? (
61
+ <Tooltip title={labelTooltip}>
62
+ <InfoOutline fontSize="inherit" color="primary" />
63
+ </Tooltip>
64
+ ) : (
65
+ labelTooltip
66
+ )}
67
+ </Typography>
68
+ {toolbarSlot && (
69
+ <>
70
+ {toolbarSlot}
71
+ <Divider
72
+ orientation="vertical"
73
+ variant="middle"
74
+ flexItem
75
+ sx={{ mx: 0.5 }}
76
+ />
77
+ </>
78
+ )}
79
+
80
+ <Tooltip title="Columns">
81
+ <ColumnsPanelTrigger render={<ToolbarButton />}>
82
+ <ViewColumnIcon fontSize="small" htmlColor={iconColor} />
83
+ </ColumnsPanelTrigger>
84
+ </Tooltip>
85
+
86
+ <Tooltip title="Filters">
87
+ <FilterPanelTrigger
88
+ render={(props, state) => (
89
+ <ToolbarButton {...props} color="default">
90
+ <Badge
91
+ badgeContent={state.filterCount}
92
+ color="primary"
93
+ variant="dot"
94
+ >
95
+ <FilterListIcon fontSize="small" htmlColor={iconColor} />
96
+ </Badge>
97
+ </ToolbarButton>
98
+ )}
99
+ />
100
+ </Tooltip>
101
+ <Divider
102
+ orientation="vertical"
103
+ variant="middle"
104
+ flexItem
105
+ sx={{ mx: 0.5 }}
106
+ />
107
+ <Tooltip title="Export">
108
+ <ToolbarButton
109
+ ref={exportMenuTriggerRef}
110
+ id="export-menu-trigger"
111
+ aria-controls="export-menu"
112
+ aria-haspopup="true"
113
+ aria-expanded={exportMenuOpen ? "true" : undefined}
114
+ onClick={() => setExportMenuOpen(true)}
115
+ >
116
+ <FileDownloadIcon fontSize="small" htmlColor={iconColor} />
117
+ </ToolbarButton>
118
+ </Tooltip>
119
+
120
+ <Menu
121
+ id="export-menu"
122
+ anchorEl={exportMenuTriggerRef.current}
123
+ open={exportMenuOpen}
124
+ onClose={() => setExportMenuOpen(false)}
125
+ anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
126
+ transformOrigin={{ vertical: "top", horizontal: "right" }}
127
+ slotProps={{
128
+ list: {
129
+ "aria-labelledby": "export-menu-trigger",
130
+ },
131
+ }}
132
+ >
133
+ <ExportPrint
134
+ options={{ ...restToolbarProps.printOptions }}
135
+ render={<MenuItem />}
136
+ onClick={() => setExportMenuOpen(false)}
137
+ >
138
+ Print
139
+ </ExportPrint>
140
+ <ExportCsv
141
+ options={{
142
+ fileName: typeof label === "string" ? label : downloadFileName,
143
+ ...restToolbarProps.csvOptions,
144
+ }}
145
+ render={<MenuItem />}
146
+ onClick={() => setExportMenuOpen(false)}
147
+ >
148
+ Download as CSV
149
+ </ExportCsv>
150
+ <ExportExcel
151
+ options={{ ...restToolbarProps.excelOptions }}
152
+ render={<MenuItem />}
153
+ onClick={() => setExportMenuOpen(false)}
154
+ >
155
+ Download as Excel
156
+ </ExportExcel>
157
+ </Menu>
158
+ </Toolbar>
159
+ );
160
+ }
@@ -0,0 +1,119 @@
1
+ import { Box, Paper } from "@mui/material";
2
+ import {
3
+ DataGridPremium,
4
+ GridToolbarProps,
5
+ ToolbarPropsOverrides,
6
+ GridAutosizeOptions,
7
+ useGridApiRef,
8
+ GridColDef,
9
+ FilterColumnsArgs
10
+ } from "@mui/x-data-grid-premium";
11
+ import { DataGridProps } from "../types";
12
+ import { CustomToolbar } from "./CustomToolbar";
13
+ import { useEffect, useMemo } from "react";
14
+ import { sortedByAssayColumns, defaultColumns } from "./columns";
15
+
16
+ const autosizeOptions: GridAutosizeOptions = {
17
+ expand: true,
18
+ includeHeaders: true,
19
+ outliersFactor: 1.5,
20
+ };
21
+
22
+ // TODO: figure out where mui stores the number of rows in a row grouping so that can be bolded too
23
+ export function DataGridWrapper(props: DataGridProps) {
24
+ const {
25
+ label,
26
+ labelTooltip,
27
+ downloadFileName,
28
+ toolbarSlot,
29
+ toolbarStyle,
30
+ toolbarIconColor,
31
+ sortedAssay,
32
+ handleSelection,
33
+ rows,
34
+ selectedIds
35
+ } = props;
36
+
37
+ const CustomToolbarWrapper = useMemo(() => {
38
+ const customToolbarProps = {
39
+ label,
40
+ downloadFileName,
41
+ labelTooltip,
42
+ toolbarSlot,
43
+ toolbarStyle,
44
+ toolbarIconColor,
45
+ };
46
+ return (props: GridToolbarProps & ToolbarPropsOverrides) => <CustomToolbar {...props} {...customToolbarProps} />;
47
+ }, [label, labelTooltip, toolbarSlot]);
48
+
49
+ const apiRef = useGridApiRef();
50
+ const groupingModel = sortedAssay ? ["assay", "ontology"] : ["ontology", "assay"];
51
+ const columnModel = sortedAssay ? sortedByAssayColumns : defaultColumns;
52
+
53
+ // functions to customize the column and filter panel in the toolbar
54
+ const filterColumns = ({ columns }: FilterColumnsArgs) => {
55
+ return columns.filter((column) => column.type !== 'custom').map((column) => column.field);
56
+ };
57
+
58
+ const getTogglableColumns = (columns: GridColDef[]) => {
59
+ return columns.filter((column) => column.type !== 'custom').map((column) => column.field);
60
+ };
61
+
62
+ const handleResizeCols = () => {
63
+ // need to check .autosizeColumns since the current was being set with an empty object
64
+ if (!apiRef.current?.autosizeColumns) return;
65
+ apiRef.current.autosizeColumns(autosizeOptions);
66
+ };
67
+
68
+ // trigger resize when rows or columns change so that rows/columns don't need to be memoized outisde of this component
69
+ // otherwise sometimes would snap back to default widths when rows/columns change
70
+ useEffect(() => {
71
+ handleResizeCols();
72
+ }, [rows, defaultColumns, sortedByAssayColumns, handleResizeCols]);
73
+
74
+ return (
75
+ <Paper sx={{ width: "100%" }}>
76
+ <Box sx={{
77
+ height: 500,
78
+ width: "100%",
79
+ overflow: "auto",
80
+ }}>
81
+ <DataGridPremium
82
+ apiRef={apiRef}
83
+ rows={rows}
84
+ columns={columnModel}
85
+ getRowId={(row) => row.experimentAccession}
86
+ autosizeOptions={autosizeOptions}
87
+ rowGroupingModel={groupingModel}
88
+ groupingColDef={{ leafField: "displayname", display: "flex" }}
89
+ columnVisibilityModel={{ displayname: false }} // so you don't see a second name column
90
+ onRowSelectionModelChange={handleSelection}
91
+ rowSelectionPropagation={{ descendants: true }}
92
+ rowSelectionModel={{ type: "include", ids: new Set(selectedIds) }}
93
+ slots={{
94
+ toolbar: CustomToolbarWrapper,
95
+ }}
96
+ slotProps={{
97
+ filterPanel: {
98
+ filterFormProps: {
99
+ filterColumns,
100
+ },
101
+ },
102
+ columnsManagement: {
103
+ getTogglableColumns,
104
+ },
105
+ }}
106
+ keepNonExistentRowsSelected
107
+ showToolbar
108
+ disableAggregation
109
+ disableRowSelectionExcludeModel
110
+ disablePivoting
111
+ checkboxSelection
112
+ autosizeOnMount
113
+ pagination
114
+ hideFooterSelectedRowCount
115
+ />
116
+ </Box>
117
+ </Paper>
118
+ );
119
+ }
@@ -0,0 +1,134 @@
1
+ import { GridColDef } from "@mui/x-data-grid-premium";
2
+ import { RowInfo } from "../types";
3
+ import { Stack, capitalize } from "@mui/material";
4
+ import { AssayIcon } from "../TreeView/treeViewHelpers";
5
+ import { ontologyTypes, assayTypes } from "../consts";
6
+
7
+ const displayNameCol: GridColDef<RowInfo> = {
8
+ field: "displayname",
9
+ headerName: "Name",
10
+ valueFormatter: (value) => value && capitalize(value),
11
+ maxWidth: 300,
12
+ };
13
+
14
+ const sortedByAssayOntologyCol: GridColDef<RowInfo> = {
15
+ field: "ontology",
16
+ headerName: "Ontology",
17
+ type: "singleSelect",
18
+ valueOptions: ontologyTypes,
19
+ renderCell: (params) => {
20
+ if (params.rowNode.type === "group") {
21
+ if (params.value === undefined) {
22
+ return null;
23
+ }
24
+ const val = params.value;
25
+ return (
26
+ <div><b>{val}</b></div>
27
+ )
28
+ }
29
+ },
30
+ };
31
+
32
+ const sortedByAssayAssayCol : GridColDef<RowInfo> = {
33
+ field: "assay",
34
+ headerName: "Assay",
35
+ valueOptions: assayTypes,
36
+ renderCell: (params) => {
37
+ if (params.rowNode.type === "group") {
38
+ if (params.value === undefined) {
39
+ return null;
40
+ }
41
+ const val = params.value;
42
+ return (
43
+ <Stack direction="row" spacing={2} alignItems="center">
44
+ {AssayIcon(val)}
45
+ <div><b>{val}</b></div>
46
+ </Stack>
47
+ )
48
+ }
49
+ }
50
+ }
51
+
52
+ const defaultOntologyCol: GridColDef<RowInfo> = {
53
+ field: "ontology",
54
+ headerName: "Ontology",
55
+ type: "singleSelect",
56
+ valueOptions: ontologyTypes,
57
+ renderCell: (params) => {
58
+ if (params.rowNode.type === "group") {
59
+ if (params.value === undefined) {
60
+ return null;
61
+ }
62
+ const val = params.value;
63
+ return (
64
+ <div><b>{val}</b></div>
65
+ )
66
+ }
67
+ }
68
+ };
69
+
70
+ const defaultAssayCol: GridColDef<RowInfo> = {
71
+ field: "assay",
72
+ headerName: "Assay",
73
+ valueOptions: assayTypes,
74
+ renderCell: (params) => {
75
+ if (params.rowNode.type === "group") {
76
+ if (params.value === undefined) {
77
+ return null;
78
+ }
79
+ const val = params.value;
80
+ return (
81
+ <Stack direction="row" spacing={2} alignItems="center">
82
+ {AssayIcon(val)}
83
+ <div>{val}</div>
84
+ </Stack>
85
+ )
86
+ }
87
+ }
88
+ }
89
+
90
+ const sampleTypeCol: GridColDef<RowInfo> = {
91
+ field: "sampleType",
92
+ headerName: "Sample Type",
93
+ type: "singleSelect",
94
+ valueOptions: ["tissue", "primary cell", "cell line", "in vitro differentiated cells", "organoid"],
95
+ valueFormatter: (value) => value && capitalize(value),
96
+ };
97
+
98
+ const lifeStageCol: GridColDef<RowInfo> = {
99
+ field: "lifeStage",
100
+ headerName: "Life Stage",
101
+ type: "singleSelect",
102
+ valueOptions: ["adult", "embryonic"],
103
+ valueFormatter: (value) => value && capitalize(value),
104
+ };
105
+
106
+ const experimentCol: GridColDef<RowInfo> = {
107
+ field: "experimentAccession",
108
+ headerName: "Experiment Accession"
109
+ }
110
+
111
+ const fileCol: GridColDef<RowInfo> = {
112
+ field: "fileAccession",
113
+ headerName: "File Accession"
114
+ }
115
+
116
+ export const sortedByAssayColumns: GridColDef<RowInfo>[] = [
117
+ displayNameCol,
118
+ sortedByAssayOntologyCol,
119
+ sampleTypeCol,
120
+ lifeStageCol,
121
+ sortedByAssayAssayCol,
122
+ experimentCol,
123
+ fileCol
124
+ ]
125
+
126
+ export const defaultColumns: GridColDef<RowInfo>[] = [
127
+ displayNameCol,
128
+ sampleTypeCol,
129
+ lifeStageCol,
130
+ defaultOntologyCol,
131
+ defaultAssayCol,
132
+ experimentCol,
133
+ fileCol
134
+ ]
@@ -0,0 +1,114 @@
1
+ import { capitalize } from "@mui/material";
2
+ import Fuse, { FuseResult } from "fuse.js";
3
+ import tracksData from "../Data/modifiedHumanTracks.json";
4
+ import {
5
+ AssayInfo,
6
+ RowInfo,
7
+ SearchTracksProps,
8
+ TrackInfo
9
+ } from "../types";
10
+
11
+ function formatAssayType(assay: string): string {
12
+ switch (assay) {
13
+ case "dnase":
14
+ return "DNase";
15
+ case "atac":
16
+ return "ATAC";
17
+ case "h3k4me3":
18
+ return "H3K4me3";
19
+ case "h3k27ac":
20
+ return "H3K27ac";
21
+ case "ctcf":
22
+ return "CTCF";
23
+ case "chromhmm":
24
+ return "ChromHMM";
25
+ default:
26
+ return assay;
27
+ }
28
+ }
29
+
30
+ // use to get nested data in JSON file
31
+ function getNestedValue(obj: any, path: string): any {
32
+ return path.split(".").reduce((acc, key) => acc && acc[key], obj);
33
+ }
34
+
35
+ export function getTracksByAssayAndOntology(
36
+ assay: string,
37
+ ontology: string,
38
+ ): TrackInfo[] {
39
+ let res: TrackInfo[] = [];
40
+ const data = getNestedValue(tracksData, "tracks");
41
+
42
+ data.forEach((track: TrackInfo) => {
43
+ const filteredAssays =
44
+ track.assays?.filter((e: AssayInfo) => e.assay === assay.toLowerCase()) ||
45
+ [];
46
+ if (
47
+ filteredAssays.length > 0 &&
48
+ track.ontology === ontology.toLowerCase()
49
+ ) {
50
+ res.push({
51
+ ...track,
52
+ assays: filteredAssays,
53
+ });
54
+ }
55
+ });
56
+ return res;
57
+ }
58
+
59
+ /** Flatten TrackInfo or FuseResult into RowInfo for DataGrid display.
60
+ * @param track TrackInfo object or FuseResult containing information from JSON file
61
+ * @returns Flattened RowInfo object
62
+ */
63
+ export function flattenIntoRow(track: TrackInfo): RowInfo {
64
+ const { ontology, lifeStage, sampleType, displayname } = track;
65
+ const { assay, experimentAccession, fileAccession } = track.assays[0];
66
+
67
+ return {
68
+ ontology: capitalize(ontology),
69
+ lifeStage: capitalize(lifeStage),
70
+ sampleType: capitalize(sampleType),
71
+ displayname: capitalize(displayname),
72
+ assay: formatAssayType(assay),
73
+ experimentAccession,
74
+ fileAccession,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Fuzzy search in tracks stored in a JSON file.
80
+ *
81
+ * @param jsonStructure - Dot-separated path to the data array in the JSON structure.
82
+ * @param query - The search query string.
83
+ * @param keyWeightMap - Array of keys to search within each track object.
84
+ * Can look like ["name", "author"] or if weighted, [
85
+ {
86
+ name: 'title',
87
+ weight: 0.3
88
+ },
89
+ {
90
+ name: 'author',
91
+ weight: 0.7
92
+ }
93
+ ].
94
+ * @param threshold - (Optional) Threshold for the fuzzy search (default is 0.5).
95
+ * Smaller = stricter match, larger = fuzzier since 0 is perfect match and 1 is worst match.
96
+ * @param limit - (Optional) Maximum number of results to return (default is 10).
97
+ * @returns FuseResult object containing the search results.
98
+ */
99
+ export function searchTracks({
100
+ jsonStructure,
101
+ query,
102
+ keyWeightMap,
103
+ threshold = 0.75,
104
+ }: SearchTracksProps): FuseResult<TrackInfo>[] {
105
+ const data = getNestedValue(tracksData, jsonStructure ?? "");
106
+
107
+ const fuse = new Fuse(data, {
108
+ includeScore: true,
109
+ shouldSort: true,
110
+ threshold: threshold,
111
+ keys: keyWeightMap,
112
+ });
113
+ return fuse.search(query);
114
+ }