@weng-lab/genomebrowser-ui 0.2.1 → 0.2.2
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/.env.local +1 -1
- package/dist/TrackSelect/Dialogs/ClearDialog.d.ts +9 -0
- package/dist/TrackSelect/Dialogs/LimitDialog.d.ts +7 -0
- package/dist/TrackSelect/Dialogs/ResetDialog.d.ts +7 -0
- package/dist/TrackSelect/Folders/biosamples/data/human.json.d.ts +57141 -57141
- package/dist/TrackSelect/Folders/biosamples/data/mouse.json.d.ts +10394 -10394
- package/dist/TrackSelect/Folders/genes/data/human.json.d.ts +7 -7
- package/dist/TrackSelect/Folders/genes/data/mouse.json.d.ts +7 -7
- package/dist/genomebrowser-ui.es.js +729 -645
- package/dist/genomebrowser-ui.es.js.map +1 -1
- package/eslint.config.js +30 -30
- package/index.html +14 -14
- package/package.json +2 -1
- package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +137 -137
- package/src/TrackSelect/DataGrid/DefaultGroupingCell.tsx +64 -64
- package/src/TrackSelect/Dialogs/ClearDialog.tsx +63 -0
- package/src/TrackSelect/Dialogs/LimitDialog.tsx +33 -0
- package/src/TrackSelect/Dialogs/ResetDialog.tsx +43 -0
- package/src/TrackSelect/FolderList/Breadcrumb.tsx +38 -38
- package/src/TrackSelect/FolderList/FolderCard.tsx +51 -51
- package/src/TrackSelect/FolderList/FolderList.tsx +47 -47
- package/src/TrackSelect/Folders/NEW.md +929 -929
- package/src/TrackSelect/Folders/biosamples/data/formatBiosamples.go +254 -254
- package/src/TrackSelect/Folders/biosamples/data/human.json +57141 -57141
- package/src/TrackSelect/Folders/biosamples/data/mouse.json +10394 -10394
- package/src/TrackSelect/Folders/biosamples/human.ts +17 -17
- package/src/TrackSelect/Folders/biosamples/mouse.ts +17 -17
- package/src/TrackSelect/Folders/biosamples/shared/AssayToggle.tsx +78 -78
- package/src/TrackSelect/Folders/biosamples/shared/BiosampleGroupingCell.tsx +146 -146
- package/src/TrackSelect/Folders/biosamples/shared/BiosampleTreeItem.tsx +15 -15
- package/src/TrackSelect/Folders/biosamples/shared/columns.tsx +165 -165
- package/src/TrackSelect/Folders/biosamples/shared/constants.tsx +116 -116
- package/src/TrackSelect/Folders/biosamples/shared/createFolder.ts +116 -116
- package/src/TrackSelect/Folders/biosamples/shared/treeBuilder.ts +224 -224
- package/src/TrackSelect/Folders/biosamples/shared/types.ts +48 -48
- package/src/TrackSelect/Folders/genes/data/human.json +7 -7
- package/src/TrackSelect/Folders/genes/data/mouse.json +7 -7
- package/src/TrackSelect/Folders/genes/human.ts +16 -16
- package/src/TrackSelect/Folders/genes/mouse.ts +16 -16
- package/src/TrackSelect/Folders/genes/shared/columns.tsx +42 -42
- package/src/TrackSelect/Folders/genes/shared/createFolder.ts +68 -68
- package/src/TrackSelect/Folders/genes/shared/treeBuilder.ts +45 -45
- package/src/TrackSelect/Folders/genes/shared/types.ts +29 -29
- package/src/TrackSelect/Folders/index.ts +30 -30
- package/src/TrackSelect/Folders/types.ts +106 -106
- package/src/TrackSelect/TrackSelect.tsx +82 -74
- package/src/TrackSelect/TreeView/CustomTreeItem.tsx +214 -214
- package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +145 -145
- package/src/TrackSelect/store.ts +117 -117
- package/src/TrackSelect/types.ts +121 -121
- package/src/lib.ts +13 -13
- package/src/vite-env.d.ts +1 -1
- package/test/main.tsx +369 -369
- package/tsconfig.app.json +25 -25
- package/tsconfig.json +7 -7
- package/tsconfig.node.json +25 -25
- package/vite.config.ts +66 -66
package/test/main.tsx
CHANGED
|
@@ -1,369 +1,369 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|
|
2
|
-
|
|
3
|
-
// react
|
|
4
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
5
|
-
import { createRoot } from "react-dom/client";
|
|
6
|
-
|
|
7
|
-
// license
|
|
8
|
-
import { LicenseInfo } from "@mui/x-license";
|
|
9
|
-
const muiLicenseKey = import.meta.env.VITE_MUI_X_LICENSE_KEY;
|
|
10
|
-
if (muiLicenseKey) {
|
|
11
|
-
LicenseInfo.setLicenseKey(muiLicenseKey);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// mui
|
|
15
|
-
import EditIcon from "@mui/icons-material/Edit";
|
|
16
|
-
import { Button } from "@mui/material";
|
|
17
|
-
|
|
18
|
-
// weng lab
|
|
19
|
-
import {
|
|
20
|
-
BigBedConfig,
|
|
21
|
-
BigWigConfig,
|
|
22
|
-
Browser,
|
|
23
|
-
createBrowserStoreMemo,
|
|
24
|
-
createTrackStoreMemo,
|
|
25
|
-
DisplayMode,
|
|
26
|
-
Domain,
|
|
27
|
-
GQLWrapper,
|
|
28
|
-
Rect,
|
|
29
|
-
Track,
|
|
30
|
-
TrackType,
|
|
31
|
-
TranscriptConfig,
|
|
32
|
-
} from "@weng-lab/genomebrowser";
|
|
33
|
-
|
|
34
|
-
// local
|
|
35
|
-
import { foldersByAssembly, TrackSelect } from "../src/lib";
|
|
36
|
-
import type { BiosampleRowInfo } from "../src/TrackSelect/Folders/biosamples/shared/types";
|
|
37
|
-
import type { GeneRowInfo } from "../src/TrackSelect/Folders/genes/shared/types";
|
|
38
|
-
import { Exon } from "@weng-lab/genomebrowser/dist/components/tracks/transcript/types";
|
|
39
|
-
|
|
40
|
-
interface Transcript {
|
|
41
|
-
id: string;
|
|
42
|
-
name: string;
|
|
43
|
-
coordinates: Domain;
|
|
44
|
-
strand: string;
|
|
45
|
-
exons?: Exon[];
|
|
46
|
-
color?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
type Assembly = "GRCh38" | "mm10";
|
|
50
|
-
|
|
51
|
-
// Callback types for track interactions (using any to avoid type conflicts with library types)
|
|
52
|
-
interface TrackCallbacks {
|
|
53
|
-
onHover: (item: any) => void;
|
|
54
|
-
onLeave: () => void;
|
|
55
|
-
onCCREClick: (item: any) => void;
|
|
56
|
-
onGeneClick: (item: any) => void;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Helper to inject callbacks based on track type
|
|
60
|
-
function injectCallbacks(track: Track, callbacks: TrackCallbacks): Track {
|
|
61
|
-
if (track.trackType === TrackType.Transcript) {
|
|
62
|
-
return {
|
|
63
|
-
...track,
|
|
64
|
-
onHover: callbacks.onHover,
|
|
65
|
-
onLeave: callbacks.onLeave,
|
|
66
|
-
onClick: callbacks.onGeneClick,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
if (track.trackType === TrackType.BigBed) {
|
|
70
|
-
return {
|
|
71
|
-
...track,
|
|
72
|
-
onHover: callbacks.onHover,
|
|
73
|
-
onLeave: callbacks.onLeave,
|
|
74
|
-
onClick: callbacks.onCCREClick,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
return track;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function Main() {
|
|
81
|
-
const [open, setOpen] = useState(false);
|
|
82
|
-
const currentAssembly: Assembly = "mm10";
|
|
83
|
-
|
|
84
|
-
const browserStore = createBrowserStoreMemo({
|
|
85
|
-
// chr7:19,695,494-19,699,803
|
|
86
|
-
domain: { chromosome: "chr7", start: 19695494, end: 19699803 },
|
|
87
|
-
marginWidth: 100,
|
|
88
|
-
trackWidth: 1400,
|
|
89
|
-
multiplier: 3,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const addHighlight = browserStore((s) => s.addHighlight);
|
|
93
|
-
const removeHighlight = browserStore((s) => s.removeHighlight);
|
|
94
|
-
const onHover = useCallback(
|
|
95
|
-
(item: Rect | Transcript) => {
|
|
96
|
-
const domain =
|
|
97
|
-
"start" in item
|
|
98
|
-
? { start: item.start, end: item.end }
|
|
99
|
-
: { start: item.coordinates.start, end: item.coordinates.end };
|
|
100
|
-
|
|
101
|
-
addHighlight({
|
|
102
|
-
id: "hover-highlight",
|
|
103
|
-
domain,
|
|
104
|
-
color: item.color || "blue",
|
|
105
|
-
});
|
|
106
|
-
},
|
|
107
|
-
[addHighlight],
|
|
108
|
-
);
|
|
109
|
-
const onLeave = useCallback(() => {
|
|
110
|
-
removeHighlight("hover-highlight");
|
|
111
|
-
}, [removeHighlight]);
|
|
112
|
-
|
|
113
|
-
const onCCREClick = useCallback((item: Rect) => {
|
|
114
|
-
console.log(item);
|
|
115
|
-
}, []);
|
|
116
|
-
const onGeneClick = useCallback((item: Transcript) => {
|
|
117
|
-
console.log(item);
|
|
118
|
-
}, []);
|
|
119
|
-
|
|
120
|
-
// Bundle callbacks for track injection
|
|
121
|
-
const callbacks = useMemo<TrackCallbacks>(
|
|
122
|
-
() => ({
|
|
123
|
-
onHover,
|
|
124
|
-
onLeave,
|
|
125
|
-
onCCREClick,
|
|
126
|
-
onGeneClick,
|
|
127
|
-
}),
|
|
128
|
-
[onHover, onLeave, onCCREClick, onGeneClick],
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
const trackStore = useLocalTracks(currentAssembly, callbacks);
|
|
132
|
-
|
|
133
|
-
const tracks = trackStore((s) => s.tracks);
|
|
134
|
-
const insertTrack = trackStore((s) => s.insertTrack);
|
|
135
|
-
const removeTrack = trackStore((s) => s.removeTrack);
|
|
136
|
-
|
|
137
|
-
const folders = useMemo(
|
|
138
|
-
() => foldersByAssembly[currentAssembly],
|
|
139
|
-
[currentAssembly],
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
const storageKey = `${currentAssembly}-selected-tracks`;
|
|
143
|
-
|
|
144
|
-
const initialSelection = useMemo(
|
|
145
|
-
() =>
|
|
146
|
-
(currentAssembly as Assembly) === "GRCh38"
|
|
147
|
-
? defaultHumanSelections
|
|
148
|
-
: defaultMouseSelections,
|
|
149
|
-
[currentAssembly],
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
// sync tracks to browser and save to localStorage
|
|
153
|
-
const handleSubmit = useCallback(
|
|
154
|
-
(selectedByFolder: Map<string, Set<string>>) => {
|
|
155
|
-
const currentIds = new Set(tracks.map((t) => t.id));
|
|
156
|
-
const selectedIds = new Set<string>();
|
|
157
|
-
const tracksToAdd: Array<{ row: unknown; folderId: string }> = [];
|
|
158
|
-
|
|
159
|
-
for (const folder of folders) {
|
|
160
|
-
const folderSelection =
|
|
161
|
-
selectedByFolder.get(folder.id) ?? new Set<string>();
|
|
162
|
-
folderSelection.forEach((id) => {
|
|
163
|
-
selectedIds.add(id);
|
|
164
|
-
if (currentIds.has(id)) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
const row = folder.rowById.get(id);
|
|
168
|
-
if (row) {
|
|
169
|
-
tracksToAdd.push({ row, folderId: folder.id });
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const tracksToRemove = tracks.filter((t) => !selectedIds.has(t.id));
|
|
175
|
-
for (const t of tracksToRemove) {
|
|
176
|
-
removeTrack(t.id);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
for (const { row, folderId } of tracksToAdd) {
|
|
180
|
-
const track = generateTrack(
|
|
181
|
-
row as BiosampleRowInfo | GeneRowInfo,
|
|
182
|
-
folderId,
|
|
183
|
-
currentAssembly,
|
|
184
|
-
callbacks,
|
|
185
|
-
);
|
|
186
|
-
if (track === null) continue;
|
|
187
|
-
insertTrack(track);
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
[tracks, removeTrack, insertTrack, callbacks, folders, currentAssembly],
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
// clear selections and remove all tracks
|
|
194
|
-
const handleClear = () => {
|
|
195
|
-
for (const t of tracks) {
|
|
196
|
-
removeTrack(t.id);
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
// On first load, if no stored selection exists, apply initial selection
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
const stored = sessionStorage.getItem(storageKey);
|
|
203
|
-
if (!stored) {
|
|
204
|
-
handleSubmit(initialSelection);
|
|
205
|
-
}
|
|
206
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
207
|
-
}, []);
|
|
208
|
-
|
|
209
|
-
return (
|
|
210
|
-
<>
|
|
211
|
-
<Button
|
|
212
|
-
variant="contained"
|
|
213
|
-
startIcon={<EditIcon />}
|
|
214
|
-
size="small"
|
|
215
|
-
onClick={() => setOpen(true)}
|
|
216
|
-
>
|
|
217
|
-
Select Tracks
|
|
218
|
-
</Button>
|
|
219
|
-
<TrackSelect
|
|
220
|
-
folders={folders}
|
|
221
|
-
storageKey={storageKey}
|
|
222
|
-
initialSelection={initialSelection}
|
|
223
|
-
onSubmit={handleSubmit}
|
|
224
|
-
onClear={handleClear}
|
|
225
|
-
maxTracks={30}
|
|
226
|
-
open={open}
|
|
227
|
-
onClose={() => setOpen(false)}
|
|
228
|
-
title="Biosample Tracks"
|
|
229
|
-
/>
|
|
230
|
-
<GQLWrapper>
|
|
231
|
-
<Browser browserStore={browserStore} trackStore={trackStore} />
|
|
232
|
-
</GQLWrapper>
|
|
233
|
-
</>
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
createRoot(document.getElementById("root")!).render(<Main />);
|
|
238
|
-
|
|
239
|
-
const ASSAY_COLORS: Record<string, string> = {
|
|
240
|
-
dnase: "#06da93",
|
|
241
|
-
h3k4me3: "#ff0000",
|
|
242
|
-
h3k27ac: "#ffcd00",
|
|
243
|
-
ctcf: "#00b0d0",
|
|
244
|
-
atac: "#02c7b9",
|
|
245
|
-
rnaseq: "#00aa00",
|
|
246
|
-
chromhmm: "#00ff00",
|
|
247
|
-
ccre: "#000000",
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
function generateTrack(
|
|
251
|
-
row: BiosampleRowInfo | GeneRowInfo,
|
|
252
|
-
folderId: string,
|
|
253
|
-
assembly: Assembly,
|
|
254
|
-
callbacks?: TrackCallbacks,
|
|
255
|
-
): Track | null {
|
|
256
|
-
// Handle gene folders
|
|
257
|
-
if (folderId.includes("genes")) {
|
|
258
|
-
const geneRow = row as GeneRowInfo;
|
|
259
|
-
const track: Track = {
|
|
260
|
-
...defaultTranscript,
|
|
261
|
-
id: geneRow.id,
|
|
262
|
-
assembly,
|
|
263
|
-
version: geneRow.versions[geneRow.versions.length - 1], // latest version
|
|
264
|
-
};
|
|
265
|
-
return callbacks ? injectCallbacks(track, callbacks) : track;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Handle biosample folders
|
|
269
|
-
const sel = row as BiosampleRowInfo;
|
|
270
|
-
const color = ASSAY_COLORS[sel.assay.toLowerCase()] || "#000000";
|
|
271
|
-
let track: Track;
|
|
272
|
-
|
|
273
|
-
switch (sel.assay.toLowerCase()) {
|
|
274
|
-
case "chromhmm":
|
|
275
|
-
case "ccre":
|
|
276
|
-
track = {
|
|
277
|
-
...defaultBigBed,
|
|
278
|
-
id: sel.id,
|
|
279
|
-
url: sel.url,
|
|
280
|
-
title: sel.displayName,
|
|
281
|
-
color,
|
|
282
|
-
};
|
|
283
|
-
break;
|
|
284
|
-
default:
|
|
285
|
-
track = {
|
|
286
|
-
...defaultBigWig,
|
|
287
|
-
id: sel.id,
|
|
288
|
-
url: sel.url,
|
|
289
|
-
title: sel.displayName,
|
|
290
|
-
color,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return callbacks ? injectCallbacks(track, callbacks) : track;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export const defaultBigWig: Omit<BigWigConfig, "id" | "title" | "url"> = {
|
|
298
|
-
trackType: TrackType.BigWig,
|
|
299
|
-
height: 50,
|
|
300
|
-
displayMode: DisplayMode.Full,
|
|
301
|
-
titleSize: 12,
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
export const defaultBigBed: Omit<BigBedConfig, "id" | "title" | "url"> = {
|
|
305
|
-
trackType: TrackType.BigBed,
|
|
306
|
-
height: 20,
|
|
307
|
-
displayMode: DisplayMode.Dense,
|
|
308
|
-
titleSize: 12,
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
export const defaultTranscript: Omit<
|
|
312
|
-
TranscriptConfig,
|
|
313
|
-
"id" | "assembly" | "version"
|
|
314
|
-
> = {
|
|
315
|
-
title: "GENCODE Genes",
|
|
316
|
-
trackType: TrackType.Transcript,
|
|
317
|
-
displayMode: DisplayMode.Squish,
|
|
318
|
-
height: 100,
|
|
319
|
-
color: "#0c184a", // screen theme default
|
|
320
|
-
canonicalColor: "#100e98", // screen theme light
|
|
321
|
-
highlightColor: "#3c69e8", // bright blue
|
|
322
|
-
titleSize: 12,
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
export function useLocalTracks(assembly: string, callbacks?: TrackCallbacks) {
|
|
326
|
-
const localTracks = getLocalTracks(assembly);
|
|
327
|
-
|
|
328
|
-
// Start empty if no stored tracks - TrackSelect will populate defaults
|
|
329
|
-
let initialTracks: Track[] = localTracks || [];
|
|
330
|
-
|
|
331
|
-
// Inject callbacks if provided (callbacks are lost on JSON serialization)
|
|
332
|
-
if (callbacks) {
|
|
333
|
-
initialTracks = initialTracks.map((t) => injectCallbacks(t, callbacks));
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const trackStore = createTrackStoreMemo(initialTracks, []);
|
|
337
|
-
const tracks = trackStore((state) => state.tracks);
|
|
338
|
-
|
|
339
|
-
// any time the track list changes, update local storage
|
|
340
|
-
useEffect(() => {
|
|
341
|
-
setLocalTracks(tracks, assembly);
|
|
342
|
-
}, [tracks, assembly]);
|
|
343
|
-
|
|
344
|
-
return trackStore;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export function getLocalTracks(assembly: string): Track[] | null {
|
|
348
|
-
if (typeof window === "undefined" || !window.sessionStorage) return null;
|
|
349
|
-
|
|
350
|
-
const localTracks = sessionStorage.getItem(assembly + "-tracks");
|
|
351
|
-
if (!localTracks) return null;
|
|
352
|
-
const localTracksJson = JSON.parse(localTracks) as Track[];
|
|
353
|
-
return localTracksJson;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
export function setLocalTracks(tracks: Track[], assembly: string) {
|
|
357
|
-
sessionStorage.setItem(assembly + "-tracks", JSON.stringify(tracks));
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Default selections for TrackSelect UI (uses folder row IDs)
|
|
361
|
-
const defaultHumanSelections = new Map<string, Set<string>>([
|
|
362
|
-
["human-genes", new Set(["gencode-basic"])],
|
|
363
|
-
["human-biosamples", new Set(["ccre-aggregate", "dnase-aggregate"])],
|
|
364
|
-
]);
|
|
365
|
-
|
|
366
|
-
const defaultMouseSelections = new Map<string, Set<string>>([
|
|
367
|
-
["mouse-genes", new Set(["gencode-basic"])],
|
|
368
|
-
["mouse-biosamples", new Set(["ccre-aggregate", "dnase-aggregate"])],
|
|
369
|
-
]);
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
// react
|
|
4
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
5
|
+
import { createRoot } from "react-dom/client";
|
|
6
|
+
|
|
7
|
+
// license
|
|
8
|
+
import { LicenseInfo } from "@mui/x-license";
|
|
9
|
+
const muiLicenseKey = import.meta.env.VITE_MUI_X_LICENSE_KEY;
|
|
10
|
+
if (muiLicenseKey) {
|
|
11
|
+
LicenseInfo.setLicenseKey(muiLicenseKey);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// mui
|
|
15
|
+
import EditIcon from "@mui/icons-material/Edit";
|
|
16
|
+
import { Button } from "@mui/material";
|
|
17
|
+
|
|
18
|
+
// weng lab
|
|
19
|
+
import {
|
|
20
|
+
BigBedConfig,
|
|
21
|
+
BigWigConfig,
|
|
22
|
+
Browser,
|
|
23
|
+
createBrowserStoreMemo,
|
|
24
|
+
createTrackStoreMemo,
|
|
25
|
+
DisplayMode,
|
|
26
|
+
Domain,
|
|
27
|
+
GQLWrapper,
|
|
28
|
+
Rect,
|
|
29
|
+
Track,
|
|
30
|
+
TrackType,
|
|
31
|
+
TranscriptConfig,
|
|
32
|
+
} from "@weng-lab/genomebrowser";
|
|
33
|
+
|
|
34
|
+
// local
|
|
35
|
+
import { foldersByAssembly, TrackSelect } from "../src/lib";
|
|
36
|
+
import type { BiosampleRowInfo } from "../src/TrackSelect/Folders/biosamples/shared/types";
|
|
37
|
+
import type { GeneRowInfo } from "../src/TrackSelect/Folders/genes/shared/types";
|
|
38
|
+
import { Exon } from "@weng-lab/genomebrowser/dist/components/tracks/transcript/types";
|
|
39
|
+
|
|
40
|
+
interface Transcript {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
coordinates: Domain;
|
|
44
|
+
strand: string;
|
|
45
|
+
exons?: Exon[];
|
|
46
|
+
color?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type Assembly = "GRCh38" | "mm10";
|
|
50
|
+
|
|
51
|
+
// Callback types for track interactions (using any to avoid type conflicts with library types)
|
|
52
|
+
interface TrackCallbacks {
|
|
53
|
+
onHover: (item: any) => void;
|
|
54
|
+
onLeave: () => void;
|
|
55
|
+
onCCREClick: (item: any) => void;
|
|
56
|
+
onGeneClick: (item: any) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Helper to inject callbacks based on track type
|
|
60
|
+
function injectCallbacks(track: Track, callbacks: TrackCallbacks): Track {
|
|
61
|
+
if (track.trackType === TrackType.Transcript) {
|
|
62
|
+
return {
|
|
63
|
+
...track,
|
|
64
|
+
onHover: callbacks.onHover,
|
|
65
|
+
onLeave: callbacks.onLeave,
|
|
66
|
+
onClick: callbacks.onGeneClick,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (track.trackType === TrackType.BigBed) {
|
|
70
|
+
return {
|
|
71
|
+
...track,
|
|
72
|
+
onHover: callbacks.onHover,
|
|
73
|
+
onLeave: callbacks.onLeave,
|
|
74
|
+
onClick: callbacks.onCCREClick,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return track;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function Main() {
|
|
81
|
+
const [open, setOpen] = useState(false);
|
|
82
|
+
const currentAssembly: Assembly = "mm10";
|
|
83
|
+
|
|
84
|
+
const browserStore = createBrowserStoreMemo({
|
|
85
|
+
// chr7:19,695,494-19,699,803
|
|
86
|
+
domain: { chromosome: "chr7", start: 19695494, end: 19699803 },
|
|
87
|
+
marginWidth: 100,
|
|
88
|
+
trackWidth: 1400,
|
|
89
|
+
multiplier: 3,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const addHighlight = browserStore((s) => s.addHighlight);
|
|
93
|
+
const removeHighlight = browserStore((s) => s.removeHighlight);
|
|
94
|
+
const onHover = useCallback(
|
|
95
|
+
(item: Rect | Transcript) => {
|
|
96
|
+
const domain =
|
|
97
|
+
"start" in item
|
|
98
|
+
? { start: item.start, end: item.end }
|
|
99
|
+
: { start: item.coordinates.start, end: item.coordinates.end };
|
|
100
|
+
|
|
101
|
+
addHighlight({
|
|
102
|
+
id: "hover-highlight",
|
|
103
|
+
domain,
|
|
104
|
+
color: item.color || "blue",
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
[addHighlight],
|
|
108
|
+
);
|
|
109
|
+
const onLeave = useCallback(() => {
|
|
110
|
+
removeHighlight("hover-highlight");
|
|
111
|
+
}, [removeHighlight]);
|
|
112
|
+
|
|
113
|
+
const onCCREClick = useCallback((item: Rect) => {
|
|
114
|
+
console.log(item);
|
|
115
|
+
}, []);
|
|
116
|
+
const onGeneClick = useCallback((item: Transcript) => {
|
|
117
|
+
console.log(item);
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
// Bundle callbacks for track injection
|
|
121
|
+
const callbacks = useMemo<TrackCallbacks>(
|
|
122
|
+
() => ({
|
|
123
|
+
onHover,
|
|
124
|
+
onLeave,
|
|
125
|
+
onCCREClick,
|
|
126
|
+
onGeneClick,
|
|
127
|
+
}),
|
|
128
|
+
[onHover, onLeave, onCCREClick, onGeneClick],
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const trackStore = useLocalTracks(currentAssembly, callbacks);
|
|
132
|
+
|
|
133
|
+
const tracks = trackStore((s) => s.tracks);
|
|
134
|
+
const insertTrack = trackStore((s) => s.insertTrack);
|
|
135
|
+
const removeTrack = trackStore((s) => s.removeTrack);
|
|
136
|
+
|
|
137
|
+
const folders = useMemo(
|
|
138
|
+
() => foldersByAssembly[currentAssembly],
|
|
139
|
+
[currentAssembly],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const storageKey = `${currentAssembly}-selected-tracks`;
|
|
143
|
+
|
|
144
|
+
const initialSelection = useMemo(
|
|
145
|
+
() =>
|
|
146
|
+
(currentAssembly as Assembly) === "GRCh38"
|
|
147
|
+
? defaultHumanSelections
|
|
148
|
+
: defaultMouseSelections,
|
|
149
|
+
[currentAssembly],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// sync tracks to browser and save to localStorage
|
|
153
|
+
const handleSubmit = useCallback(
|
|
154
|
+
(selectedByFolder: Map<string, Set<string>>) => {
|
|
155
|
+
const currentIds = new Set(tracks.map((t) => t.id));
|
|
156
|
+
const selectedIds = new Set<string>();
|
|
157
|
+
const tracksToAdd: Array<{ row: unknown; folderId: string }> = [];
|
|
158
|
+
|
|
159
|
+
for (const folder of folders) {
|
|
160
|
+
const folderSelection =
|
|
161
|
+
selectedByFolder.get(folder.id) ?? new Set<string>();
|
|
162
|
+
folderSelection.forEach((id) => {
|
|
163
|
+
selectedIds.add(id);
|
|
164
|
+
if (currentIds.has(id)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const row = folder.rowById.get(id);
|
|
168
|
+
if (row) {
|
|
169
|
+
tracksToAdd.push({ row, folderId: folder.id });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const tracksToRemove = tracks.filter((t) => !selectedIds.has(t.id));
|
|
175
|
+
for (const t of tracksToRemove) {
|
|
176
|
+
removeTrack(t.id);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const { row, folderId } of tracksToAdd) {
|
|
180
|
+
const track = generateTrack(
|
|
181
|
+
row as BiosampleRowInfo | GeneRowInfo,
|
|
182
|
+
folderId,
|
|
183
|
+
currentAssembly,
|
|
184
|
+
callbacks,
|
|
185
|
+
);
|
|
186
|
+
if (track === null) continue;
|
|
187
|
+
insertTrack(track);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
[tracks, removeTrack, insertTrack, callbacks, folders, currentAssembly],
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// clear selections and remove all tracks
|
|
194
|
+
const handleClear = () => {
|
|
195
|
+
for (const t of tracks) {
|
|
196
|
+
removeTrack(t.id);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// On first load, if no stored selection exists, apply initial selection
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const stored = sessionStorage.getItem(storageKey);
|
|
203
|
+
if (!stored) {
|
|
204
|
+
handleSubmit(initialSelection);
|
|
205
|
+
}
|
|
206
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
207
|
+
}, []);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<>
|
|
211
|
+
<Button
|
|
212
|
+
variant="contained"
|
|
213
|
+
startIcon={<EditIcon />}
|
|
214
|
+
size="small"
|
|
215
|
+
onClick={() => setOpen(true)}
|
|
216
|
+
>
|
|
217
|
+
Select Tracks
|
|
218
|
+
</Button>
|
|
219
|
+
<TrackSelect
|
|
220
|
+
folders={folders}
|
|
221
|
+
storageKey={storageKey}
|
|
222
|
+
initialSelection={initialSelection}
|
|
223
|
+
onSubmit={handleSubmit}
|
|
224
|
+
onClear={handleClear}
|
|
225
|
+
maxTracks={30}
|
|
226
|
+
open={open}
|
|
227
|
+
onClose={() => setOpen(false)}
|
|
228
|
+
title="Biosample Tracks"
|
|
229
|
+
/>
|
|
230
|
+
<GQLWrapper>
|
|
231
|
+
<Browser browserStore={browserStore} trackStore={trackStore} />
|
|
232
|
+
</GQLWrapper>
|
|
233
|
+
</>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
createRoot(document.getElementById("root")!).render(<Main />);
|
|
238
|
+
|
|
239
|
+
const ASSAY_COLORS: Record<string, string> = {
|
|
240
|
+
dnase: "#06da93",
|
|
241
|
+
h3k4me3: "#ff0000",
|
|
242
|
+
h3k27ac: "#ffcd00",
|
|
243
|
+
ctcf: "#00b0d0",
|
|
244
|
+
atac: "#02c7b9",
|
|
245
|
+
rnaseq: "#00aa00",
|
|
246
|
+
chromhmm: "#00ff00",
|
|
247
|
+
ccre: "#000000",
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
function generateTrack(
|
|
251
|
+
row: BiosampleRowInfo | GeneRowInfo,
|
|
252
|
+
folderId: string,
|
|
253
|
+
assembly: Assembly,
|
|
254
|
+
callbacks?: TrackCallbacks,
|
|
255
|
+
): Track | null {
|
|
256
|
+
// Handle gene folders
|
|
257
|
+
if (folderId.includes("genes")) {
|
|
258
|
+
const geneRow = row as GeneRowInfo;
|
|
259
|
+
const track: Track = {
|
|
260
|
+
...defaultTranscript,
|
|
261
|
+
id: geneRow.id,
|
|
262
|
+
assembly,
|
|
263
|
+
version: geneRow.versions[geneRow.versions.length - 1], // latest version
|
|
264
|
+
};
|
|
265
|
+
return callbacks ? injectCallbacks(track, callbacks) : track;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Handle biosample folders
|
|
269
|
+
const sel = row as BiosampleRowInfo;
|
|
270
|
+
const color = ASSAY_COLORS[sel.assay.toLowerCase()] || "#000000";
|
|
271
|
+
let track: Track;
|
|
272
|
+
|
|
273
|
+
switch (sel.assay.toLowerCase()) {
|
|
274
|
+
case "chromhmm":
|
|
275
|
+
case "ccre":
|
|
276
|
+
track = {
|
|
277
|
+
...defaultBigBed,
|
|
278
|
+
id: sel.id,
|
|
279
|
+
url: sel.url,
|
|
280
|
+
title: sel.displayName,
|
|
281
|
+
color,
|
|
282
|
+
};
|
|
283
|
+
break;
|
|
284
|
+
default:
|
|
285
|
+
track = {
|
|
286
|
+
...defaultBigWig,
|
|
287
|
+
id: sel.id,
|
|
288
|
+
url: sel.url,
|
|
289
|
+
title: sel.displayName,
|
|
290
|
+
color,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return callbacks ? injectCallbacks(track, callbacks) : track;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export const defaultBigWig: Omit<BigWigConfig, "id" | "title" | "url"> = {
|
|
298
|
+
trackType: TrackType.BigWig,
|
|
299
|
+
height: 50,
|
|
300
|
+
displayMode: DisplayMode.Full,
|
|
301
|
+
titleSize: 12,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export const defaultBigBed: Omit<BigBedConfig, "id" | "title" | "url"> = {
|
|
305
|
+
trackType: TrackType.BigBed,
|
|
306
|
+
height: 20,
|
|
307
|
+
displayMode: DisplayMode.Dense,
|
|
308
|
+
titleSize: 12,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export const defaultTranscript: Omit<
|
|
312
|
+
TranscriptConfig,
|
|
313
|
+
"id" | "assembly" | "version"
|
|
314
|
+
> = {
|
|
315
|
+
title: "GENCODE Genes",
|
|
316
|
+
trackType: TrackType.Transcript,
|
|
317
|
+
displayMode: DisplayMode.Squish,
|
|
318
|
+
height: 100,
|
|
319
|
+
color: "#0c184a", // screen theme default
|
|
320
|
+
canonicalColor: "#100e98", // screen theme light
|
|
321
|
+
highlightColor: "#3c69e8", // bright blue
|
|
322
|
+
titleSize: 12,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export function useLocalTracks(assembly: string, callbacks?: TrackCallbacks) {
|
|
326
|
+
const localTracks = getLocalTracks(assembly);
|
|
327
|
+
|
|
328
|
+
// Start empty if no stored tracks - TrackSelect will populate defaults
|
|
329
|
+
let initialTracks: Track[] = localTracks || [];
|
|
330
|
+
|
|
331
|
+
// Inject callbacks if provided (callbacks are lost on JSON serialization)
|
|
332
|
+
if (callbacks) {
|
|
333
|
+
initialTracks = initialTracks.map((t) => injectCallbacks(t, callbacks));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const trackStore = createTrackStoreMemo(initialTracks, []);
|
|
337
|
+
const tracks = trackStore((state) => state.tracks);
|
|
338
|
+
|
|
339
|
+
// any time the track list changes, update local storage
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
setLocalTracks(tracks, assembly);
|
|
342
|
+
}, [tracks, assembly]);
|
|
343
|
+
|
|
344
|
+
return trackStore;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function getLocalTracks(assembly: string): Track[] | null {
|
|
348
|
+
if (typeof window === "undefined" || !window.sessionStorage) return null;
|
|
349
|
+
|
|
350
|
+
const localTracks = sessionStorage.getItem(assembly + "-tracks");
|
|
351
|
+
if (!localTracks) return null;
|
|
352
|
+
const localTracksJson = JSON.parse(localTracks) as Track[];
|
|
353
|
+
return localTracksJson;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function setLocalTracks(tracks: Track[], assembly: string) {
|
|
357
|
+
sessionStorage.setItem(assembly + "-tracks", JSON.stringify(tracks));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Default selections for TrackSelect UI (uses folder row IDs)
|
|
361
|
+
const defaultHumanSelections = new Map<string, Set<string>>([
|
|
362
|
+
["human-genes", new Set(["gencode-basic"])],
|
|
363
|
+
["human-biosamples", new Set(["ccre-aggregate", "dnase-aggregate"])],
|
|
364
|
+
]);
|
|
365
|
+
|
|
366
|
+
const defaultMouseSelections = new Map<string, Set<string>>([
|
|
367
|
+
["mouse-genes", new Set(["gencode-basic"])],
|
|
368
|
+
["mouse-biosamples", new Set(["ccre-aggregate", "dnase-aggregate"])],
|
|
369
|
+
]);
|