@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.
Files changed (57) hide show
  1. package/.env.local +1 -1
  2. package/dist/TrackSelect/Dialogs/ClearDialog.d.ts +9 -0
  3. package/dist/TrackSelect/Dialogs/LimitDialog.d.ts +7 -0
  4. package/dist/TrackSelect/Dialogs/ResetDialog.d.ts +7 -0
  5. package/dist/TrackSelect/Folders/biosamples/data/human.json.d.ts +57141 -57141
  6. package/dist/TrackSelect/Folders/biosamples/data/mouse.json.d.ts +10394 -10394
  7. package/dist/TrackSelect/Folders/genes/data/human.json.d.ts +7 -7
  8. package/dist/TrackSelect/Folders/genes/data/mouse.json.d.ts +7 -7
  9. package/dist/genomebrowser-ui.es.js +729 -645
  10. package/dist/genomebrowser-ui.es.js.map +1 -1
  11. package/eslint.config.js +30 -30
  12. package/index.html +14 -14
  13. package/package.json +2 -1
  14. package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +137 -137
  15. package/src/TrackSelect/DataGrid/DefaultGroupingCell.tsx +64 -64
  16. package/src/TrackSelect/Dialogs/ClearDialog.tsx +63 -0
  17. package/src/TrackSelect/Dialogs/LimitDialog.tsx +33 -0
  18. package/src/TrackSelect/Dialogs/ResetDialog.tsx +43 -0
  19. package/src/TrackSelect/FolderList/Breadcrumb.tsx +38 -38
  20. package/src/TrackSelect/FolderList/FolderCard.tsx +51 -51
  21. package/src/TrackSelect/FolderList/FolderList.tsx +47 -47
  22. package/src/TrackSelect/Folders/NEW.md +929 -929
  23. package/src/TrackSelect/Folders/biosamples/data/formatBiosamples.go +254 -254
  24. package/src/TrackSelect/Folders/biosamples/data/human.json +57141 -57141
  25. package/src/TrackSelect/Folders/biosamples/data/mouse.json +10394 -10394
  26. package/src/TrackSelect/Folders/biosamples/human.ts +17 -17
  27. package/src/TrackSelect/Folders/biosamples/mouse.ts +17 -17
  28. package/src/TrackSelect/Folders/biosamples/shared/AssayToggle.tsx +78 -78
  29. package/src/TrackSelect/Folders/biosamples/shared/BiosampleGroupingCell.tsx +146 -146
  30. package/src/TrackSelect/Folders/biosamples/shared/BiosampleTreeItem.tsx +15 -15
  31. package/src/TrackSelect/Folders/biosamples/shared/columns.tsx +165 -165
  32. package/src/TrackSelect/Folders/biosamples/shared/constants.tsx +116 -116
  33. package/src/TrackSelect/Folders/biosamples/shared/createFolder.ts +116 -116
  34. package/src/TrackSelect/Folders/biosamples/shared/treeBuilder.ts +224 -224
  35. package/src/TrackSelect/Folders/biosamples/shared/types.ts +48 -48
  36. package/src/TrackSelect/Folders/genes/data/human.json +7 -7
  37. package/src/TrackSelect/Folders/genes/data/mouse.json +7 -7
  38. package/src/TrackSelect/Folders/genes/human.ts +16 -16
  39. package/src/TrackSelect/Folders/genes/mouse.ts +16 -16
  40. package/src/TrackSelect/Folders/genes/shared/columns.tsx +42 -42
  41. package/src/TrackSelect/Folders/genes/shared/createFolder.ts +68 -68
  42. package/src/TrackSelect/Folders/genes/shared/treeBuilder.ts +45 -45
  43. package/src/TrackSelect/Folders/genes/shared/types.ts +29 -29
  44. package/src/TrackSelect/Folders/index.ts +30 -30
  45. package/src/TrackSelect/Folders/types.ts +106 -106
  46. package/src/TrackSelect/TrackSelect.tsx +82 -74
  47. package/src/TrackSelect/TreeView/CustomTreeItem.tsx +214 -214
  48. package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +145 -145
  49. package/src/TrackSelect/store.ts +117 -117
  50. package/src/TrackSelect/types.ts +121 -121
  51. package/src/lib.ts +13 -13
  52. package/src/vite-env.d.ts +1 -1
  53. package/test/main.tsx +369 -369
  54. package/tsconfig.app.json +25 -25
  55. package/tsconfig.json +7 -7
  56. package/tsconfig.node.json +25 -25
  57. 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
+ ]);