@weng-lab/genomebrowser-ui 0.3.6-beta.0 → 0.4.0-beta.0

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 (90) hide show
  1. package/.env.local +1 -1
  2. package/dist/TrackSelect/Custom/TfPeaks.d.ts +1 -2
  3. package/dist/TrackSelect/Custom/green-motifs.json.d.ts +20430 -0
  4. package/dist/TrackSelect/Folders/biosamples/shared/BiosampleViewSelector.d.ts +7 -0
  5. package/dist/TrackSelect/Folders/biosamples/shared/createFolder.d.ts +1 -13
  6. package/dist/TrackSelect/Folders/biosamples/shared/toTrack.d.ts +20 -0
  7. package/dist/TrackSelect/Folders/biosamples/shared/types.d.ts +4 -13
  8. package/dist/TrackSelect/Folders/genes/shared/createFolder.d.ts +1 -3
  9. package/dist/TrackSelect/Folders/genes/shared/toTrack.d.ts +18 -0
  10. package/dist/TrackSelect/Folders/index.d.ts +6 -12
  11. package/dist/TrackSelect/Folders/mohd/data/human.json.d.ts +2948 -0
  12. package/dist/TrackSelect/Folders/mohd/human.d.ts +1 -0
  13. package/dist/TrackSelect/Folders/mohd/shared/MohdGroupingCell.d.ts +2 -0
  14. package/dist/TrackSelect/Folders/mohd/shared/MohdTreeItem.d.ts +3 -0
  15. package/dist/TrackSelect/Folders/mohd/shared/MohdViewSelector.d.ts +7 -0
  16. package/dist/TrackSelect/Folders/mohd/shared/columns.d.ts +5 -0
  17. package/dist/TrackSelect/Folders/mohd/shared/config.d.ts +42 -0
  18. package/dist/TrackSelect/Folders/mohd/shared/createFolder.d.ts +9 -0
  19. package/dist/TrackSelect/Folders/mohd/shared/toTrack.d.ts +9 -0
  20. package/dist/TrackSelect/Folders/mohd/shared/types.d.ts +40 -0
  21. package/dist/TrackSelect/Folders/other-tracks/shared/toTrack.d.ts +5 -0
  22. package/dist/TrackSelect/Folders/other-tracks/shared/types.d.ts +1 -0
  23. package/dist/TrackSelect/Folders/types.d.ts +23 -55
  24. package/dist/TrackSelect/TrackSelect.d.ts +10 -7
  25. package/dist/TrackSelect/TreeView/TreeViewWrapper.d.ts +1 -1
  26. package/dist/TrackSelect/buildSelectedTree.d.ts +15 -0
  27. package/dist/TrackSelect/managedTracks.d.ts +13 -0
  28. package/dist/TrackSelect/resolveFolderView.d.ts +2 -0
  29. package/dist/TrackSelect/trackContext.d.ts +5 -0
  30. package/dist/TrackSelect/types.d.ts +12 -33
  31. package/dist/genomebrowser-ui.es.js +2470 -2258
  32. package/dist/genomebrowser-ui.es.js.map +1 -1
  33. package/dist/lib.d.ts +4 -4
  34. package/dist/muiLicense.d.ts +1 -0
  35. package/package.json +6 -3
  36. package/src/TrackSelect/Custom/TfPeaks.tsx +81 -60
  37. package/src/TrackSelect/Dialogs/ResetDialog.tsx +3 -2
  38. package/src/TrackSelect/FolderList/FolderCard.tsx +1 -1
  39. package/src/TrackSelect/Folders/biosamples/shared/BiosampleViewSelector.tsx +33 -0
  40. package/src/TrackSelect/Folders/biosamples/shared/createFolder.ts +39 -58
  41. package/src/TrackSelect/Folders/biosamples/shared/toTrack.ts +138 -0
  42. package/src/TrackSelect/Folders/biosamples/shared/types.ts +4 -16
  43. package/src/TrackSelect/Folders/genes/shared/createFolder.ts +10 -31
  44. package/src/TrackSelect/Folders/genes/shared/toTrack.ts +59 -0
  45. package/src/TrackSelect/Folders/index.ts +15 -18
  46. package/src/TrackSelect/Folders/mohd/data/human.json +2945 -0
  47. package/src/TrackSelect/Folders/mohd/human.ts +10 -0
  48. package/src/TrackSelect/Folders/mohd/shared/MohdGroupingCell.tsx +68 -0
  49. package/src/TrackSelect/Folders/mohd/shared/MohdTreeItem.tsx +17 -0
  50. package/src/TrackSelect/Folders/mohd/shared/MohdViewSelector.tsx +33 -0
  51. package/src/TrackSelect/Folders/mohd/shared/columns.tsx +79 -0
  52. package/src/TrackSelect/Folders/mohd/shared/config.tsx +71 -0
  53. package/src/TrackSelect/Folders/mohd/shared/createFolder.ts +144 -0
  54. package/src/TrackSelect/Folders/mohd/shared/toTrack.ts +164 -0
  55. package/src/TrackSelect/Folders/mohd/shared/types.ts +46 -0
  56. package/src/TrackSelect/Folders/other-tracks/shared/createFolder.ts +13 -14
  57. package/src/TrackSelect/Folders/other-tracks/shared/toTrack.ts +17 -0
  58. package/src/TrackSelect/Folders/other-tracks/shared/types.ts +1 -0
  59. package/src/TrackSelect/Folders/types.ts +26 -69
  60. package/src/TrackSelect/TrackSelect.tsx +299 -255
  61. package/src/TrackSelect/TreeView/CustomTreeItem.tsx +6 -6
  62. package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +84 -6
  63. package/src/TrackSelect/buildSelectedTree.ts +145 -0
  64. package/src/TrackSelect/managedTracks.ts +92 -0
  65. package/src/TrackSelect/resolveFolderView.ts +20 -0
  66. package/src/TrackSelect/trackContext.ts +9 -0
  67. package/src/TrackSelect/types.ts +14 -39
  68. package/src/lib.ts +13 -7
  69. package/src/muiLicense.ts +9 -0
  70. package/src/vite-env.d.ts +9 -0
  71. package/test/TrackSelect.test.tsx +435 -0
  72. package/test/main.tsx +37 -353
  73. package/test/mocks/logo-test.tsx +11 -0
  74. package/test/mohdDisplay.test.tsx +45 -0
  75. package/test/startup.test.ts +206 -0
  76. package/test/trackSelectState.test.ts +176 -0
  77. package/vite.config.ts +1 -0
  78. package/vitest.config.ts +20 -0
  79. package/dist/TrackSelect/Custom/TF-ChIP-Canonical-Motifs-w-Trimmed.json.d.ts +0 -42210
  80. package/dist/TrackSelect/Folders/biosamples/shared/AssayToggle.d.ts +0 -18
  81. package/dist/TrackSelect/Folders/biosamples/shared/treeBuilder.d.ts +0 -28
  82. package/dist/TrackSelect/Folders/genes/shared/treeBuilder.d.ts +0 -13
  83. package/dist/TrackSelect/Folders/other-tracks/shared/treeBuilder.d.ts +0 -4
  84. package/dist/TrackSelect/store.d.ts +0 -4
  85. package/src/TrackSelect/Folders/NEW.md +0 -929
  86. package/src/TrackSelect/Folders/biosamples/shared/AssayToggle.tsx +0 -78
  87. package/src/TrackSelect/Folders/biosamples/shared/treeBuilder.ts +0 -224
  88. package/src/TrackSelect/Folders/genes/shared/treeBuilder.ts +0 -45
  89. package/src/TrackSelect/Folders/other-tracks/shared/treeBuilder.ts +0 -34
  90. package/src/TrackSelect/store.ts +0 -117
package/dist/lib.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { default as TrackSelect, TrackSelectProps } from './TrackSelect/TrackSelect';
2
- import { createSelectionStore, SelectionStoreInstance } from './TrackSelect/store.ts';
1
+ import { default as TrackSelect, InitialSelectedIdsByAssembly, TrackSelectProps } from './TrackSelect/TrackSelect';
3
2
  import { foldersByAssembly } from './TrackSelect/Folders/index.ts';
4
3
  import { tfPeaksTrack } from './TrackSelect/Custom/TfPeaks.tsx';
5
4
  export { TrackSelect, TrackSelectProps };
6
- export { createSelectionStore, SelectionStoreInstance };
5
+ export type { TrackSelectTrackContext } from './TrackSelect/trackContext';
6
+ export type { InitialSelectedIdsByAssembly };
7
7
  export { foldersByAssembly };
8
- export type { BiosampleRowInfo, GeneRowInfo, OtherTrackInfo, } from './TrackSelect/Folders';
8
+ export type { BiosampleRowInfo, BiosampleTrackContext, GeneRowInfo, GeneTrackContext, MohdRowInfo, MohdTrackContext, OtherTrackInfo, OtherTracksTrackContext, } from './TrackSelect/Folders';
9
9
  export { tfPeaksTrack };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@weng-lab/genomebrowser-ui",
3
3
  "private": false,
4
- "version": "0.3.6-beta.0",
4
+ "version": "0.4.0-beta.0",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "publishConfig": {
@@ -23,7 +23,7 @@
23
23
  "@mui/x-data-grid-premium": "^8.19.0",
24
24
  "react": "^19.0.0",
25
25
  "react-dom": "^19.0.0",
26
- "@weng-lab/genomebrowser": "1.8.3-beta.0"
26
+ "@weng-lab/genomebrowser": "1.8.5-beta.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@eslint/js": "^9.34.0",
@@ -34,15 +34,18 @@
34
34
  "eslint": "^9.34.0",
35
35
  "eslint-plugin-react-hooks": "^5.2.0",
36
36
  "eslint-plugin-react-refresh": "^0.4.20",
37
+ "jsdom": "^26.1.0",
37
38
  "typescript": "^5.7.3",
38
39
  "typescript-eslint": "^8.42.0",
39
40
  "vite": "^6.3.5",
40
- "vite-plugin-dts": "^4.5.4"
41
+ "vite-plugin-dts": "^4.5.4",
42
+ "vitest": "^3.2.4"
41
43
  },
42
44
  "scripts": {
43
45
  "dev": "vite",
44
46
  "build": "tsc -b && vite build",
45
47
  "lint": "eslint .",
48
+ "test": "vitest run",
46
49
  "preview": "vite preview",
47
50
  "format": "prettier --write ."
48
51
  }
@@ -172,6 +172,10 @@ function TooltipRow({
172
172
  function TfPeaksTooltip(rect: OverlayInteractionRect) {
173
173
  const pwm = rect.pwm;
174
174
  const label = tfDisplayName(rect.name);
175
+ const totalWidth = 340;
176
+ const pad = 8;
177
+ const lineH = 14;
178
+ const titleH = 18;
175
179
 
176
180
  // Build metadata rows (single-value rows)
177
181
  const metaRows: { label: string; value: string }[] = [];
@@ -181,33 +185,47 @@ function TfPeaksTooltip(rect: OverlayInteractionRect) {
181
185
  label: "Position",
182
186
  value: `${rect.chr ? rect.chr + ":" : ""}${rect.start.toLocaleString()}-${rect.end.toLocaleString()}`,
183
187
  });
184
- if (rect.expRatio) metaRows.push({ label: "Exps", value: rect.expRatio });
185
188
 
186
- // Multi-value rows: split comma-separated cCREs, group 3 per row
187
- const cCREItems = rect.cCREId
189
+ // Multi-value rows: split comma-separated cCREs, group 4 per row, cap at 5
190
+ const allCCREItems = rect.cCREId
188
191
  ? rect.cCREId
189
192
  .split(",")
190
193
  .map((s) => s.trim())
191
194
  .filter(Boolean)
192
195
  : [];
196
+ const maxCCREs = 5;
197
+ const cCREItems = allCCREItems.slice(0, maxCCREs);
198
+ const hiddenCCREs = Math.max(0, allCCREItems.length - maxCCREs);
193
199
  const cCRERows: string[][] = [];
194
200
  for (let i = 0; i < cCREItems.length; i += 4) {
195
201
  cCRERows.push(cCREItems.slice(i, i + 4));
196
202
  }
197
203
 
198
- // Parse expSupport JSON into flat rows
199
- const supportRows: { cellLine: string; expId: string; fileId: string }[] = [];
204
+ // Extract unique sorted biosamples from expSupport
205
+ const allBiosamples: string[] = [];
200
206
  if (rect.expSupport) {
201
- for (const [cellLine, exps] of Object.entries(rect.expSupport)) {
202
- for (const [expId, fileId] of Object.entries(exps)) {
203
- supportRows.push({ cellLine, expId, fileId });
207
+ const seen = new Set<string>();
208
+ for (const cellLine of Object.keys(rect.expSupport)) {
209
+ if (!seen.has(cellLine)) {
210
+ seen.add(cellLine);
211
+ allBiosamples.push(cellLine);
204
212
  }
205
213
  }
214
+ allBiosamples.sort((a, b) => a.localeCompare(b));
206
215
  }
207
-
208
- const pad = 8;
209
- const lineH = 14;
210
- const titleH = 18;
216
+ const maxBiosamples = 12;
217
+ const biosampleItems = allBiosamples.slice(0, maxBiosamples);
218
+ const hiddenBiosamples = Math.max(0, allBiosamples.length - maxBiosamples);
219
+ const biosampleText = biosampleItems.join(", ");
220
+ const biosampleMoreText =
221
+ hiddenBiosamples > 0 ? ` ...and ${hiddenBiosamples} more` : "";
222
+ const biosampleCharsPerLine = Math.floor((totalWidth - 2 * pad) / 5.2);
223
+ const biosampleContentLines = Math.max(
224
+ 1,
225
+ Math.ceil(
226
+ (biosampleText.length + biosampleMoreText.length) / biosampleCharsPerLine,
227
+ ),
228
+ );
211
229
 
212
230
  // Layout: compute y offsets upfront
213
231
  const hasLogo = pwm && pwm.length > 0;
@@ -216,26 +234,25 @@ function TfPeaksTooltip(rect: OverlayInteractionRect) {
216
234
  const logoSectionH = hasLogo ? logoHeight + 4 : 0;
217
235
  const metaSectionH = metaRows.length * lineH;
218
236
 
219
- // cCRE section: label row + one row per group of 3
237
+ // cCRE section: label row + one row per group of 4 + optional "+N more" row
220
238
  const cCREGap = cCRERows.length > 0 ? 8 : 0;
221
239
  const cCREHeaderH = cCRERows.length > 0 ? lineH : 0;
222
- const cCRESectionH = cCRERows.length * lineH;
240
+ const cCREMoreH = hiddenCCREs > 0 ? lineH : 0;
241
+ const cCRESectionH = cCRERows.length * lineH + cCREMoreH;
223
242
 
224
- // Support section
225
- const supportGap = supportRows.length > 0 ? 8 : 0;
226
- const supportHeaderH = supportRows.length > 0 ? lineH : 0;
227
- const supportSectionH = supportRows.length * lineH;
243
+ // Biosamples section
244
+ const biosampleGap = biosampleContentLines > 0 ? 8 : 0;
245
+ const biosampleHeaderH = biosampleContentLines > 0 ? lineH : 0;
246
+ const biosampleSectionH = biosampleContentLines * lineH;
228
247
 
229
248
  const titleY = pad;
230
249
  const logoY = titleY + titleH;
231
250
  const metaY = logoY + logoSectionH;
232
251
  const cCREY = metaY + metaSectionH + cCREGap;
233
252
  const cCREDataY = cCREY + cCREHeaderH;
234
- const supportY = cCREDataY + cCRESectionH + supportGap;
235
- const supportDataY = supportY + supportHeaderH;
236
- const totalHeight = supportDataY + supportSectionH + pad;
237
-
238
- const totalWidth = 340;
253
+ const biosampleY = cCREDataY + cCRESectionH + biosampleGap;
254
+ const biosampleDataY = biosampleY + biosampleHeaderH;
255
+ const totalHeight = biosampleDataY + biosampleSectionH + pad;
239
256
 
240
257
  return (
241
258
  <g>
@@ -306,60 +323,64 @@ function TfPeaksTooltip(rect: OverlayInteractionRect) {
306
323
  {group.join(", ")}
307
324
  </text>
308
325
  ))}
326
+ {hiddenCCREs > 0 && (
327
+ <text
328
+ x={pad}
329
+ y={cCREDataY + cCRERows.length * lineH + 2}
330
+ fontSize={9}
331
+ fill="#aaa"
332
+ dominantBaseline="hanging"
333
+ >
334
+ +{hiddenCCREs} more...
335
+ </text>
336
+ )}
309
337
  </g>
310
338
  )}
311
339
 
312
- {/* Experiments supporting this peak */}
313
- {supportRows.length > 0 && (
340
+ {/* Biosamples */}
341
+ {biosampleContentLines > 0 && (
314
342
  <g>
315
343
  <line
316
344
  x1={pad}
317
345
  x2={totalWidth - pad}
318
- y1={supportY - 4}
319
- y2={supportY - 4}
346
+ y1={biosampleY - 4}
347
+ y2={biosampleY - 4}
320
348
  stroke="#ddd"
321
349
  />
322
350
  <text
323
351
  x={pad}
324
- y={supportY + 2}
352
+ y={biosampleY + 2}
325
353
  fontSize={9}
326
354
  fontWeight="bold"
327
355
  fill="#666"
328
356
  dominantBaseline="hanging"
329
357
  >
330
- Experiments supporting this peak
358
+ Biosamples
331
359
  </text>
332
- {supportRows.map((row, i) => (
333
- <g key={i} transform={`translate(0, ${supportDataY + i * lineH})`}>
334
- <text
335
- x={8}
336
- y={0}
337
- fontSize={9}
338
- fill="#666"
339
- dominantBaseline="hanging"
340
- >
341
- {row.cellLine}
342
- </text>
343
- <text
344
- x={100}
345
- y={0}
346
- fontSize={9}
347
- fill="#666"
348
- dominantBaseline="hanging"
349
- >
350
- {row.expId}
351
- </text>
352
- <text
353
- x={210}
354
- y={0}
355
- fontSize={9}
356
- fill="#666"
357
- dominantBaseline="hanging"
358
- >
359
- {row.fileId}
360
- </text>
361
- </g>
362
- ))}
360
+ <foreignObject
361
+ x={pad}
362
+ y={biosampleDataY}
363
+ width={totalWidth - 2 * pad}
364
+ height={biosampleSectionH}
365
+ >
366
+ <div
367
+ style={{
368
+ color: "#333",
369
+ fontSize: "9px",
370
+ lineHeight: `${lineH}px`,
371
+ margin: 0,
372
+ padding: 0,
373
+ overflow: "hidden",
374
+ whiteSpace: "normal",
375
+ wordBreak: "break-word",
376
+ }}
377
+ >
378
+ {biosampleText}
379
+ {hiddenBiosamples > 0 && (
380
+ <span style={{ color: "#aaa" }}>{biosampleMoreText}</span>
381
+ )}
382
+ </div>
383
+ </foreignObject>
363
384
  </g>
364
385
  )}
365
386
  </g>
@@ -23,11 +23,12 @@ export function ResetDialog({ open, onClose, onConfirm }: ResetDialogProps) {
23
23
  fontWeight: "bold",
24
24
  }}
25
25
  >
26
- Reset to Default
26
+ Reset to Browser State
27
27
  </DialogTitle>
28
28
  <DialogContent sx={{ mt: 2 }}>
29
29
  <DialogContentText>
30
- Are you sure you want to reset all selections to the default?
30
+ Are you sure you want to reset all selections to the current browser
31
+ state?
31
32
  </DialogContentText>
32
33
  </DialogContent>
33
34
  <DialogActions sx={{ justifyContent: "center", gap: 2, pb: 2 }}>
@@ -44,7 +44,7 @@ export function FolderCard({ folder, onClick }: FolderCardProps) {
44
44
  </Typography>
45
45
  )}
46
46
  <Typography variant="caption" color="text.secondary">
47
- {folder.rowById.size.toLocaleString()} tracks available
47
+ {folder.rows.length.toLocaleString()} tracks available
48
48
  </Typography>
49
49
  </Paper>
50
50
  );
@@ -0,0 +1,33 @@
1
+ import { ToggleButton, ToggleButtonGroup } from "@mui/material";
2
+ import { FolderView } from "../../types";
3
+
4
+ export interface BiosampleViewSelectorProps {
5
+ views: FolderView[];
6
+ activeViewId: string;
7
+ onChange: (viewId: string) => void;
8
+ }
9
+
10
+ export function BiosampleViewSelector({
11
+ views,
12
+ activeViewId,
13
+ onChange,
14
+ }: BiosampleViewSelectorProps) {
15
+ return (
16
+ <ToggleButtonGroup
17
+ exclusive
18
+ value={activeViewId}
19
+ size="small"
20
+ onChange={(_event, viewId: string | null) => {
21
+ if (viewId) {
22
+ onChange(viewId);
23
+ }
24
+ }}
25
+ >
26
+ {views.map((view) => (
27
+ <ToggleButton key={view.id} value={view.id}>
28
+ {view.label}
29
+ </ToggleButton>
30
+ ))}
31
+ </ToggleButtonGroup>
32
+ );
33
+ }
@@ -9,24 +9,24 @@ import {
9
9
  defaultColumns,
10
10
  defaultGroupingModel,
11
11
  defaultLeafField,
12
+ sortedByAssayColumns,
13
+ sortedByAssayGroupingModel,
14
+ sortedByAssayLeafField,
12
15
  } from "./columns";
13
- import { buildTreeView } from "./treeBuilder";
14
16
  import { formatAssayType } from "./constants";
15
- import { AssayToggle } from "./AssayToggle";
17
+ import { BiosampleViewSelector } from "./BiosampleViewSelector";
16
18
  import BiosampleGroupingCell from "./BiosampleGroupingCell";
17
19
  import { BiosampleTreeItem } from "./BiosampleTreeItem";
20
+ import { createBiosampleTrack } from "./toTrack";
18
21
 
19
- /**
20
- * Flattens TrackInfo into RowInfo objects for DataGrid display.
21
- * Each track can have multiple assays, so this creates one row per assay.
22
- *
23
- * @param track - TrackInfo object from JSON data
24
- * @returns Array of flattened BiosampleRowInfo objects, one per assay
25
- */
26
- function flattenTrackIntoRows(track: BiosampleTrackInfo): BiosampleRowInfo[] {
22
+ /** Flatten a biosample track into one row per assay. */
23
+ function flattenTrackIntoRows(
24
+ folderId: string,
25
+ track: BiosampleTrackInfo,
26
+ ): BiosampleRowInfo[] {
27
27
  const { ontology, lifeStage, sampleType, displayName, collection } = track;
28
28
 
29
- // Sort assays so cCRE comes first, then maintain original order for the rest
29
+ // Keep cCRE rows first so aggregate selections stay prominent in the UI.
30
30
  const sortedAssays = [...track.assays].sort((a, b) => {
31
31
  const aIsCcre = a.assay.toLowerCase() === "ccre";
32
32
  const bIsCcre = b.assay.toLowerCase() === "ccre";
@@ -46,7 +46,7 @@ function flattenTrackIntoRows(track: BiosampleTrackInfo): BiosampleRowInfo[] {
46
46
  cpgMinus,
47
47
  coverage,
48
48
  }) => ({
49
- id,
49
+ id: `${folderId}/${id}`,
50
50
  ontology: capitalize(ontology),
51
51
  lifeStage: capitalize(lifeStage),
52
52
  sampleType: capitalize(sampleType),
@@ -63,75 +63,56 @@ function flattenTrackIntoRows(track: BiosampleTrackInfo): BiosampleRowInfo[] {
63
63
  );
64
64
  }
65
65
 
66
- /**
67
- * Transforms raw JSON data into flattened rows and a lookup map.
68
- * Prefixes each row ID with the folder ID to ensure uniqueness across folders.
69
- *
70
- * @param data - Raw biosample data from JSON file
71
- * @param folderId - Folder ID to prefix row IDs with
72
- * @returns Object containing rows array and rowById map
73
- */
74
- function transformData(data: BiosampleDataFile): {
75
- rowById: Map<string, BiosampleRowInfo>;
76
- } {
77
- const rows = data.tracks.flatMap(flattenTrackIntoRows).map((row) => ({
78
- ...row,
79
- id: row.id,
80
- }));
81
- const rowById = new Map<string, BiosampleRowInfo>(
82
- rows.map((row) => [row.id, row]),
83
- );
84
- return { rowById };
66
+ function transformData(
67
+ folderId: string,
68
+ data: BiosampleDataFile,
69
+ ): BiosampleRowInfo[] {
70
+ return data.tracks.flatMap((track) => flattenTrackIntoRows(folderId, track));
85
71
  }
86
72
 
87
73
  export interface CreateBiosampleFolderOptions {
88
- /** Unique identifier for this folder */
89
74
  id: string;
90
- /** Display label shown in the UI */
91
75
  label: string;
92
- /** Optional description shown in folder cards */
93
76
  description?: string;
94
- /** Raw biosample data from JSON file */
95
77
  data: BiosampleDataFile;
96
78
  }
97
79
 
98
80
  /**
99
- * Factory function that creates a FolderDefinition for biosample data.
100
- *
101
- * This handles all the common setup for biosample folders:
102
- * - Transforms JSON data into flattened rows
103
- * - Creates the rowById lookup map
104
- * - Configures columns, grouping, and tree building
105
- *
106
- * @param options - Configuration options for the folder
107
- * @returns A complete FolderDefinition for the biosample data
81
+ * Build a biosample folder with its data, tree builder, and track factory.
108
82
  */
109
83
  export function createBiosampleFolder(
110
84
  options: CreateBiosampleFolderOptions,
111
85
  ): FolderDefinition<BiosampleRowInfo> {
112
86
  const { id, label, description, data } = options;
113
- const { rowById } = transformData(data);
87
+ const rows = transformData(id, data);
88
+ const views = [
89
+ {
90
+ id: "default",
91
+ label: "Tissue",
92
+ columns: defaultColumns,
93
+ groupingModel: defaultGroupingModel,
94
+ leafField: defaultLeafField,
95
+ },
96
+ {
97
+ id: "by-assay",
98
+ label: "Assay",
99
+ columns: sortedByAssayColumns,
100
+ groupingModel: sortedByAssayGroupingModel,
101
+ leafField: sortedByAssayLeafField,
102
+ },
103
+ ];
114
104
 
115
105
  return {
116
106
  id,
117
107
  label,
118
108
  description,
119
- rowById,
120
- getRowId: (row) => row.id,
121
-
122
- // Default view: ontology-based grouping
109
+ rows,
123
110
  columns: defaultColumns,
124
111
  groupingModel: defaultGroupingModel,
125
112
  leafField: defaultLeafField,
126
-
127
- // Build tree for selected items panel
128
- buildTree: (selectedIds, rowById) =>
129
- buildTreeView(selectedIds, rowById, label, id),
130
-
131
- // Biosample-specific toolbar: toggle between sample-grouped and assay-grouped views
132
- ToolbarExtras: AssayToggle,
133
-
134
- // Biosample-specific custom components
113
+ createTrack: createBiosampleTrack,
114
+ views,
115
+ ViewSelector: BiosampleViewSelector,
135
116
  GroupingCellComponent: BiosampleGroupingCell,
136
117
  TreeItemComponent: BiosampleTreeItem,
137
118
  };
@@ -0,0 +1,138 @@
1
+ import {
2
+ BigBedConfig,
3
+ BigWigConfig,
4
+ DisplayMode,
5
+ MethylCConfig,
6
+ Rect,
7
+ Track,
8
+ TrackType,
9
+ ValuedPoint,
10
+ } from "@weng-lab/genomebrowser";
11
+ import type { FC } from "react";
12
+ import { CreateTrackOptions } from "../../types";
13
+ import { BiosampleRowInfo } from "./types";
14
+
15
+ export type BiosampleTrackContext = {
16
+ onBiosampleFeatureClick?: (args: {
17
+ trackId: string;
18
+ row: BiosampleRowInfo;
19
+ rect: Rect;
20
+ }) => void;
21
+ onBiosampleFeatureHover?: (args: {
22
+ trackId: string;
23
+ row: BiosampleRowInfo;
24
+ rect: Rect;
25
+ }) => void;
26
+ biosampleFeatureTooltip?: FC<Rect>;
27
+ biosampleSignalTooltip?: FC<ValuedPoint[]>;
28
+ biosampleMethylTooltip?: FC<ValuedPoint[]>;
29
+ };
30
+
31
+ const assayColors: Record<string, string> = {
32
+ dnase: "#06da93",
33
+ h3k4me3: "#ff0000",
34
+ h3k27ac: "#ffcd00",
35
+ ctcf: "#00b0d0",
36
+ atac: "#02c7b9",
37
+ rnaseq: "#00aa00",
38
+ chromhmm: "#00ff00",
39
+ ccre: "#000000",
40
+ wgbs: "#648bd8",
41
+ };
42
+
43
+ const defaultBigWig: Omit<BigWigConfig, "id" | "title" | "url"> = {
44
+ trackType: TrackType.BigWig,
45
+ height: 50,
46
+ displayMode: DisplayMode.Full,
47
+ titleSize: 12,
48
+ };
49
+
50
+ const defaultBigBed: Omit<BigBedConfig, "id" | "title" | "url"> = {
51
+ trackType: TrackType.BigBed,
52
+ height: 20,
53
+ displayMode: DisplayMode.Dense,
54
+ titleSize: 12,
55
+ };
56
+
57
+ const defaultMethylC: Omit<MethylCConfig, "id" | "title" | "urls"> = {
58
+ trackType: TrackType.MethylC,
59
+ height: 100,
60
+ displayMode: DisplayMode.Split,
61
+ titleSize: 12,
62
+ color: "#648bd8",
63
+ colors: {
64
+ cpg: "#648bd8",
65
+ chg: "#ff944d",
66
+ chh: "#ff00ff",
67
+ depth: "#525252",
68
+ },
69
+ };
70
+
71
+ export function createBiosampleTrack(
72
+ row: BiosampleRowInfo,
73
+ options: CreateTrackOptions,
74
+ ): Track {
75
+ const assay = row.assay.toLowerCase();
76
+ const color = assayColors[assay] ?? "#000000";
77
+ const trackContext = options.trackContext;
78
+
79
+ switch (assay) {
80
+ case "chromhmm":
81
+ case "ccre":
82
+ return {
83
+ ...defaultBigBed,
84
+ id: row.id,
85
+ url: row.url ?? "",
86
+ title: row.displayName,
87
+ color,
88
+ onClick: trackContext?.onBiosampleFeatureClick
89
+ ? (rect: Rect) =>
90
+ trackContext.onBiosampleFeatureClick?.({
91
+ trackId: row.id,
92
+ row,
93
+ rect,
94
+ })
95
+ : undefined,
96
+ onHover: trackContext?.onBiosampleFeatureHover
97
+ ? (rect: Rect) =>
98
+ trackContext.onBiosampleFeatureHover?.({
99
+ trackId: row.id,
100
+ row,
101
+ rect,
102
+ })
103
+ : undefined,
104
+ tooltip: trackContext?.biosampleFeatureTooltip,
105
+ };
106
+ case "wgbs":
107
+ return {
108
+ ...defaultMethylC,
109
+ id: row.id,
110
+ title: row.displayName,
111
+ maskCpgByCoverage: true,
112
+ tooltip: trackContext?.biosampleMethylTooltip,
113
+ urls: {
114
+ plusStrand: {
115
+ cpg: { url: row.cpgPlus ?? "" },
116
+ chg: { url: "" },
117
+ chh: { url: "" },
118
+ depth: { url: row.coverage ?? "" },
119
+ },
120
+ minusStrand: {
121
+ cpg: { url: row.cpgMinus ?? "" },
122
+ chg: { url: "" },
123
+ chh: { url: "" },
124
+ depth: { url: row.coverage ?? "" },
125
+ },
126
+ },
127
+ };
128
+ default:
129
+ return {
130
+ ...defaultBigWig,
131
+ id: row.id,
132
+ url: row.url ?? "",
133
+ title: row.displayName,
134
+ color,
135
+ tooltip: trackContext?.biosampleSignalTooltip,
136
+ };
137
+ }
138
+ }
@@ -1,12 +1,7 @@
1
- /**
2
- * Types for biosample folder data
3
- */
4
-
5
1
  export type CollectionType = "Core" | "Ancillary" | "Partial";
6
2
 
7
3
  /**
8
- * Assay information from the JSON data.
9
- * Standard assays have a single `url`, while WGBS assays have `cpgPlus`, `cpgMinus`, `coverage`.
4
+ * One assay entry from the source data. WGBS rows carry strand-specific URLs.
10
5
  */
11
6
  export type BiosampleAssayInfo = {
12
7
  id: string;
@@ -14,15 +9,12 @@ export type BiosampleAssayInfo = {
14
9
  url?: string;
15
10
  experimentAccession: string;
16
11
  fileAccession?: string;
17
- // WGBS-specific fields
18
12
  cpgPlus?: string;
19
13
  cpgMinus?: string;
20
14
  coverage?: string;
21
15
  };
22
16
 
23
- /**
24
- * Track information from the JSON data
25
- */
17
+ /** One biosample entry from the source data. */
26
18
  export type BiosampleTrackInfo = {
27
19
  name: string;
28
20
  ontology: string;
@@ -34,8 +26,7 @@ export type BiosampleTrackInfo = {
34
26
  };
35
27
 
36
28
  /**
37
- * Row format for DataGrid (flattened from TrackInfo).
38
- * Standard assays have a single `url`, while WGBS assays have `cpgPlus`, `cpgMinus`, `coverage`.
29
+ * Flattened table row used by TrackSelect and track creation.
39
30
  */
40
31
  export type BiosampleRowInfo = {
41
32
  id: string;
@@ -48,15 +39,12 @@ export type BiosampleRowInfo = {
48
39
  fileAccession?: string;
49
40
  url?: string;
50
41
  collection: CollectionType;
51
- // WGBS-specific fields
52
42
  cpgPlus?: string;
53
43
  cpgMinus?: string;
54
44
  coverage?: string;
55
45
  };
56
46
 
57
- /**
58
- * Structure of the biosample JSON data files
59
- */
47
+ /** Root shape for biosample JSON files. */
60
48
  export type BiosampleDataFile = {
61
49
  tracks: BiosampleTrackInfo[];
62
50
  };