@weng-lab/genomebrowser-ui 0.1.8 → 0.1.10

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 (35) hide show
  1. package/dist/TrackSelect/Data/{modifiedHumanTracks.json.d.ts → humanBiosamples.json.d.ts} +40705 -20804
  2. package/dist/TrackSelect/Data/mouseBiosamples.json.d.ts +10346 -0
  3. package/dist/TrackSelect/DataGrid/GroupingCell.d.ts +2 -0
  4. package/dist/TrackSelect/DataGrid/dataGridHelpers.d.ts +25 -6
  5. package/dist/TrackSelect/TrackSelect.d.ts +4 -1
  6. package/dist/TrackSelect/TreeView/treeViewHelpers.d.ts +1 -1
  7. package/dist/TrackSelect/consts.d.ts +6 -17
  8. package/dist/TrackSelect/store.d.ts +2 -1
  9. package/dist/TrackSelect/types.d.ts +5 -0
  10. package/dist/genomebrowser-ui.es.js +1173 -950
  11. package/dist/genomebrowser-ui.es.js.map +1 -1
  12. package/dist/lib.d.ts +2 -0
  13. package/package.json +2 -2
  14. package/src/TrackSelect/Data/formatBiosamples.go +254 -0
  15. package/src/TrackSelect/Data/{modifiedHumanTracks.json → humanBiosamples.json} +40704 -20804
  16. package/src/TrackSelect/Data/mouseBiosamples.json +10343 -0
  17. package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +13 -6
  18. package/src/TrackSelect/DataGrid/GroupingCell.tsx +144 -0
  19. package/src/TrackSelect/DataGrid/columns.tsx +7 -0
  20. package/src/TrackSelect/DataGrid/dataGridHelpers.tsx +64 -19
  21. package/src/TrackSelect/TrackSelect.tsx +86 -27
  22. package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +1 -1
  23. package/src/TrackSelect/TreeView/treeViewHelpers.tsx +65 -17
  24. package/src/TrackSelect/consts.ts +30 -30
  25. package/src/TrackSelect/issues.md +404 -0
  26. package/src/TrackSelect/store.ts +16 -6
  27. package/src/TrackSelect/types.ts +8 -0
  28. package/src/lib.ts +3 -0
  29. package/test/main.tsx +419 -4
  30. package/dist/TrackSelect/treeViewHelpers.d.ts +0 -1
  31. package/src/TrackSelect/.claude/settings.local.json +0 -7
  32. package/src/TrackSelect/Data/humanTracks.json +0 -35711
  33. package/src/TrackSelect/Data/human_chromhmm_biosamples_with_all_urls.json +0 -35716
  34. package/src/TrackSelect/bug.md +0 -4
  35. package/src/TrackSelect/treeViewHelpers.tsx +0 -0
package/src/lib.ts CHANGED
@@ -6,3 +6,6 @@ import {
6
6
  type SelectionStoreInstance,
7
7
  } from "./TrackSelect/store.ts";
8
8
  export { createSelectionStore, SelectionStoreInstance };
9
+
10
+ import type { RowInfo } from "./TrackSelect/types.ts";
11
+ export { RowInfo };
package/test/main.tsx CHANGED
@@ -1,10 +1,425 @@
1
- import { createSelectionStore } from "../src/TrackSelect/store";
2
- import TrackSelect from "../src/TrackSelect/TrackSelect";
1
+ // react
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
3
  import { createRoot } from "react-dom/client";
4
4
 
5
+ // mui
6
+ import EditIcon from "@mui/icons-material/Edit";
7
+ import CloseIcon from "@mui/icons-material/Close";
8
+ import {
9
+ Box,
10
+ Button,
11
+ Dialog,
12
+ DialogContent,
13
+ DialogTitle,
14
+ IconButton,
15
+ Typography,
16
+ } 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 { createSelectionStore, TrackSelect, RowInfo } from "../src/lib";
36
+ import { Exon } from "@weng-lab/genomebrowser/dist/components/tracks/transcript/types";
37
+
38
+ interface Transcript {
39
+ id: string;
40
+ name: string;
41
+ coordinates: Domain;
42
+ strand: string;
43
+ exons?: Exon[];
44
+ color?: string;
45
+ }
46
+
47
+ const enum Assembly {
48
+ human = "GRCh38",
49
+ mouse = "mm10",
50
+ }
51
+
52
+ // Callback types for track interactions (using any to avoid type conflicts with library types)
53
+ interface TrackCallbacks {
54
+ onHover: (item: any) => void;
55
+ onLeave: () => void;
56
+ onCCREClick: (item: any) => void;
57
+ onGeneClick: (item: any) => void;
58
+ }
59
+
60
+ // Helper to inject callbacks based on track type
61
+ function injectCallbacks(track: Track, callbacks: TrackCallbacks): Track {
62
+ if (track.trackType === TrackType.Transcript) {
63
+ return {
64
+ ...track,
65
+ onHover: callbacks.onHover,
66
+ onLeave: callbacks.onLeave,
67
+ onClick: callbacks.onGeneClick,
68
+ };
69
+ }
70
+ if (track.trackType === TrackType.BigBed) {
71
+ return {
72
+ ...track,
73
+ onHover: callbacks.onHover,
74
+ onLeave: callbacks.onLeave,
75
+ onClick: callbacks.onCCREClick,
76
+ };
77
+ }
78
+ return track;
79
+ }
80
+
81
+ function getLocalStorage(assembly: Assembly): Set<string> | null {
82
+ if (typeof window === "undefined" || !window.sessionStorage) return null;
83
+
84
+ const selectedIds = sessionStorage.getItem(assembly + "-selected-tracks");
85
+ if (!selectedIds) return null;
86
+ const idsArray = JSON.parse(selectedIds) as string[];
87
+ return new Set(idsArray);
88
+ }
89
+
90
+ function setLocalStorage(trackIds: Set<string>, assembly: Assembly) {
91
+ sessionStorage.setItem(
92
+ assembly + "-selected-tracks",
93
+ JSON.stringify([...trackIds]),
94
+ );
95
+ }
96
+
5
97
  function Main() {
6
- const store = createSelectionStore();
7
- return <TrackSelect store={store} />;
98
+ const [open, setOpen] = useState(false);
99
+ const currentAssembly = Assembly.mouse;
100
+
101
+ const browserStore = createBrowserStoreMemo({
102
+ // chr12:53,380,176-53,416,446
103
+ domain: { chromosome: "chr12", start: 53380176, end: 53416446 },
104
+ marginWidth: 100,
105
+ trackWidth: 1400,
106
+ multiplier: 3,
107
+ });
108
+
109
+ const addHighlight = browserStore((s) => s.addHighlight);
110
+ const removeHighlight = browserStore((s) => s.removeHighlight);
111
+ const onHover = useCallback(
112
+ (item: Rect | Transcript) => {
113
+ const domain =
114
+ "start" in item
115
+ ? { start: item.start, end: item.end }
116
+ : { start: item.coordinates.start, end: item.coordinates.end };
117
+
118
+ addHighlight({
119
+ id: "hover-highlight",
120
+ domain,
121
+ color: item.color || "blue",
122
+ });
123
+ },
124
+ [addHighlight],
125
+ );
126
+ const onLeave = useCallback(() => {
127
+ removeHighlight("hover-highlight");
128
+ }, [removeHighlight]);
129
+
130
+ const onCCREClick = useCallback((item: Rect) => {
131
+ console.log(item);
132
+ }, []);
133
+ const onGeneClick = useCallback((item: Transcript) => {
134
+ console.log(item);
135
+ }, []);
136
+
137
+ // Bundle callbacks for track injection
138
+ const callbacks = useMemo<TrackCallbacks>(
139
+ () => ({
140
+ onHover,
141
+ onLeave,
142
+ onCCREClick,
143
+ onGeneClick,
144
+ }),
145
+ [onHover, onLeave, onCCREClick, onGeneClick],
146
+ );
147
+
148
+ const trackStore = useLocalTracks(currentAssembly, callbacks);
149
+
150
+ const tracks = trackStore((s) => s.tracks);
151
+ const insertTrack = trackStore((s) => s.insertTrack);
152
+ const removeTrack = trackStore((s) => s.removeTrack);
153
+
154
+ const selectionStore = useMemo(() => {
155
+ const localIds = getLocalStorage(currentAssembly);
156
+ const ids = localIds != null ? localIds : new Set<string>();
157
+ return createSelectionStore(currentAssembly, ids);
158
+ }, [currentAssembly]);
159
+
160
+ const rowById = selectionStore((s) => s.rowById);
161
+
162
+ // Handle submit: sync tracks to browser and save to localStorage
163
+ const handleSubmit = useCallback(
164
+ (newTrackIds: Set<string>) => {
165
+ const currentIds = new Set(tracks.map((t) => t.id));
166
+
167
+ // Build tracks to add from newTrackIds + rowById lookup
168
+ const tracksToAdd = Array.from(newTrackIds)
169
+ .filter((id) => !currentIds.has(id)) // not in current track list
170
+ .map((id) => rowById.get(id)) // get RowInfo object
171
+ .filter((track): track is RowInfo => track !== undefined); // filter out undefined
172
+
173
+ const tracksToRemove = tracks.filter((t) => {
174
+ return !t.id.includes("ignore") && !newTrackIds.has(t.id);
175
+ });
176
+
177
+ console.log("removing", tracksToRemove);
178
+ for (const t of tracksToRemove) {
179
+ removeTrack(t.id);
180
+ }
181
+
182
+ for (const s of tracksToAdd) {
183
+ const track = generateTrack(s, callbacks);
184
+ if (track === null) continue;
185
+ insertTrack(track);
186
+ }
187
+
188
+ // Save the track IDs (not the auto-generated group IDs)
189
+ setLocalStorage(newTrackIds, currentAssembly);
190
+ // Close the dialog
191
+ setOpen(false);
192
+ },
193
+ [tracks, removeTrack, insertTrack, callbacks],
194
+ );
195
+
196
+ const handleCancel = () => {
197
+ setOpen(false);
198
+ };
199
+
200
+ // Handle reset: clear selections and remove non-default tracks
201
+ const handleReset = () => {
202
+ // Clear the selection store
203
+ selectionStore.getState().clear();
204
+
205
+ // Remove all non-default tracks from the browser
206
+ const tracksToRemove = tracks.filter((t) => !t.id.includes("ignore"));
207
+ for (const t of tracksToRemove) {
208
+ removeTrack(t.id);
209
+ }
210
+
211
+ // Clear localStorage for selected tracks
212
+ setLocalStorage(new Set(), currentAssembly);
213
+ };
214
+
215
+ return (
216
+ <>
217
+ <Button
218
+ variant="contained"
219
+ startIcon={<EditIcon />}
220
+ size="small"
221
+ onClick={() => setOpen(true)}
222
+ >
223
+ Select Tracks
224
+ </Button>
225
+ <Dialog
226
+ open={open}
227
+ onClose={() => setOpen(false)}
228
+ maxWidth="lg"
229
+ fullWidth
230
+ >
231
+ <DialogTitle
232
+ bgcolor="#0c184a"
233
+ color="white"
234
+ display={"flex"}
235
+ justifyContent={"space-between"}
236
+ alignItems={"center"}
237
+ fontWeight={"bold"}
238
+ >
239
+ Biosample Tracks
240
+ <IconButton
241
+ size="large"
242
+ onClick={() => setOpen(false)}
243
+ sx={{ color: "white", padding: 0 }}
244
+ >
245
+ <CloseIcon fontSize="large" />
246
+ </IconButton>
247
+ </DialogTitle>
248
+ <DialogContent sx={{ marginTop: "5px" }}>
249
+ <TrackSelect
250
+ store={selectionStore}
251
+ onSubmit={handleSubmit}
252
+ onCancel={handleCancel}
253
+ onReset={handleReset}
254
+ />
255
+ </DialogContent>
256
+ </Dialog>
257
+ <GQLWrapper>
258
+ <Browser browserStore={browserStore} trackStore={trackStore} />
259
+ </GQLWrapper>
260
+ </>
261
+ );
8
262
  }
9
263
 
10
264
  createRoot(document.getElementById("root")!).render(<Main />);
265
+
266
+ const ASSAY_COLORS: Record<string, string> = {
267
+ dnase: "#06da93",
268
+ h3k4me3: "#ff0000",
269
+ h3k27ac: "#ffcd00",
270
+ ctcf: "#00b0d0",
271
+ atac: "#02c7b9",
272
+ rnaseq: "#00aa00",
273
+ chromhmm: "#00ff00",
274
+ ccre: "#0c184a",
275
+ };
276
+
277
+ function generateTrack(sel: RowInfo, callbacks?: TrackCallbacks): Track {
278
+ const color = ASSAY_COLORS[sel.assay.toLowerCase()] || "#000000";
279
+ let track: Track;
280
+
281
+ switch (sel.assay.toLowerCase()) {
282
+ case "chromhmm":
283
+ track = {
284
+ ...defaultBigBed,
285
+ id: sel.id,
286
+ url: sel.url,
287
+ title: sel.displayname,
288
+ color,
289
+ };
290
+ break;
291
+ case "ccre":
292
+ track = {
293
+ ...defaultBigBed,
294
+ id: sel.id,
295
+ url: sel.url,
296
+ title: sel.displayname,
297
+ color,
298
+ };
299
+ break;
300
+ default:
301
+ track = {
302
+ ...defaultBigWig,
303
+ id: sel.id,
304
+ url: sel.url,
305
+ title: sel.displayname,
306
+ color,
307
+ };
308
+ }
309
+
310
+ return callbacks ? injectCallbacks(track, callbacks) : track;
311
+ }
312
+
313
+ export const defaultBigWig: Omit<BigWigConfig, "id" | "title" | "url"> = {
314
+ trackType: TrackType.BigWig,
315
+ height: 50,
316
+ displayMode: DisplayMode.Full,
317
+ titleSize: 12,
318
+ };
319
+
320
+ export const defaultBigBed: Omit<BigBedConfig, "id" | "title" | "url"> = {
321
+ trackType: TrackType.BigBed,
322
+ height: 20,
323
+ displayMode: DisplayMode.Dense,
324
+ titleSize: 12,
325
+ };
326
+
327
+ export const defaultTranscript: Omit<
328
+ TranscriptConfig,
329
+ "id" | "assembly" | "version"
330
+ > = {
331
+ title: "GENCODE Genes",
332
+ trackType: TrackType.Transcript,
333
+ displayMode: DisplayMode.Squish,
334
+ height: 100,
335
+ color: "#0c184a", // screen theme default
336
+ canonicalColor: "#100e98", // screen theme light
337
+ highlightColor: "#3c69e8", // bright blue
338
+ titleSize: 12,
339
+ };
340
+
341
+ export function useLocalTracks(assembly: string, callbacks?: TrackCallbacks) {
342
+ const localTracks = getLocalTracks(assembly);
343
+
344
+ const defaultTracks =
345
+ assembly === "GRCh38" ? defaultHumanTracks : defaultMouseTracks;
346
+
347
+ // Get base tracks (from storage or defaults)
348
+ let initialTracks = localTracks || defaultTracks;
349
+
350
+ // Inject callbacks if provided (callbacks are lost on JSON serialization)
351
+ if (callbacks) {
352
+ initialTracks = initialTracks.map((t) => injectCallbacks(t, callbacks));
353
+ }
354
+
355
+ const trackStore = createTrackStoreMemo(initialTracks, []);
356
+ const tracks = trackStore((state) => state.tracks);
357
+
358
+ // any time the track list changes, update local storage
359
+ useEffect(() => {
360
+ setLocalTracks(tracks, assembly);
361
+ }, [tracks, assembly]);
362
+
363
+ return trackStore;
364
+ }
365
+
366
+ export function getLocalTracks(assembly: string): Track[] | null {
367
+ if (typeof window === "undefined" || !window.sessionStorage) return null;
368
+
369
+ const localTracks = sessionStorage.getItem(assembly + "-" + "tracks");
370
+ if (!localTracks) return null;
371
+ const localTracksJson = JSON.parse(localTracks) as Track[];
372
+ return localTracksJson;
373
+ }
374
+
375
+ export function setLocalTracks(tracks: Track[], assembly: string) {
376
+ sessionStorage.setItem(assembly + "-tracks", JSON.stringify(tracks));
377
+ }
378
+
379
+ const defaultHumanTracks = [
380
+ {
381
+ ...defaultTranscript,
382
+ color: ASSAY_COLORS.ccre,
383
+ id: "human-genes-ignore",
384
+ assembly: "GRCh38",
385
+ version: 40,
386
+ },
387
+ {
388
+ ...defaultBigBed,
389
+ color: ASSAY_COLORS.ccre,
390
+ id: "human-ccre-ignore",
391
+ title: "All cCREs colored by group",
392
+ url: "https://downloads.wenglab.org/GRCh38-cCREs.DCC.bigBed",
393
+ },
394
+ {
395
+ ...defaultBigWig,
396
+ color: ASSAY_COLORS.dnase,
397
+ id: "human-dnase-aggregate-ignore",
398
+ title: "Aggregated DNase signal, all ENCODE biosamples",
399
+ url: "https://downloads.wenglab.org/DNAse_All_ENCODE_MAR20_2024_merged.bw",
400
+ },
401
+ ];
402
+
403
+ const defaultMouseTracks = [
404
+ {
405
+ ...defaultTranscript,
406
+ color: ASSAY_COLORS.ccre,
407
+ id: "mouse-genes-ignore",
408
+ assembly: "mm10",
409
+ version: 21,
410
+ },
411
+ {
412
+ ...defaultBigBed,
413
+ color: ASSAY_COLORS.ccre,
414
+ id: "mouse-ccre-ignore",
415
+ title: "All cCREs colored by group",
416
+ url: "https://downloads.wenglab.org/mm10-cCREs.DCC.bigBed",
417
+ },
418
+ {
419
+ ...defaultBigWig,
420
+ color: ASSAY_COLORS.dnase,
421
+ id: "mouse-dnase-aggregate-ignore",
422
+ title: "Aggregated DNase signal, all ENCODE biosamples",
423
+ url: "https://downloads.wenglab.org/DNase_MM10_ENCODE_DEC2024_merged_nanrm.bigWig",
424
+ },
425
+ ];
@@ -1 +0,0 @@
1
- export {};
@@ -1,7 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "mcp__Ref__ref_read_url"
5
- ]
6
- }
7
- }