@weng-lab/genomebrowser-ui 0.1.11 → 0.2.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 (84) hide show
  1. package/.env.local +1 -0
  2. package/dist/TrackSelect/DataGrid/DefaultGroupingCell.d.ts +6 -0
  3. package/dist/TrackSelect/FolderList/Breadcrumb.d.ts +6 -0
  4. package/dist/TrackSelect/FolderList/FolderCard.d.ts +6 -0
  5. package/dist/TrackSelect/FolderList/FolderList.d.ts +6 -0
  6. package/dist/TrackSelect/{Data/humanBiosamples.json.d.ts → Folders/biosamples/data/human.json.d.ts} +1940 -1919
  7. package/dist/TrackSelect/{Data/mouseBiosamples.json.d.ts → Folders/biosamples/data/mouse.json.d.ts} +408 -357
  8. package/dist/TrackSelect/Folders/biosamples/human.d.ts +7 -0
  9. package/dist/TrackSelect/Folders/biosamples/mouse.d.ts +7 -0
  10. package/dist/TrackSelect/Folders/biosamples/shared/AssayToggle.d.ts +14 -0
  11. package/dist/TrackSelect/Folders/biosamples/shared/BiosampleGroupingCell.d.ts +6 -0
  12. package/dist/TrackSelect/Folders/biosamples/shared/BiosampleTreeItem.d.ts +7 -0
  13. package/dist/TrackSelect/Folders/biosamples/shared/columns.d.ts +14 -0
  14. package/dist/TrackSelect/Folders/biosamples/shared/constants.d.ts +19 -0
  15. package/dist/TrackSelect/Folders/biosamples/shared/createFolder.d.ts +24 -0
  16. package/dist/TrackSelect/Folders/biosamples/shared/treeBuilder.d.ts +25 -0
  17. package/dist/TrackSelect/Folders/biosamples/shared/types.d.ts +44 -0
  18. package/dist/TrackSelect/Folders/genes/data/human.json.d.ts +10 -0
  19. package/dist/TrackSelect/Folders/genes/data/mouse.json.d.ts +10 -0
  20. package/dist/TrackSelect/Folders/genes/human.d.ts +7 -0
  21. package/dist/TrackSelect/Folders/genes/mouse.d.ts +7 -0
  22. package/dist/TrackSelect/Folders/genes/shared/columns.d.ts +14 -0
  23. package/dist/TrackSelect/Folders/genes/shared/createFolder.d.ts +12 -0
  24. package/dist/TrackSelect/Folders/genes/shared/treeBuilder.d.ts +13 -0
  25. package/dist/TrackSelect/Folders/genes/shared/types.d.ts +26 -0
  26. package/dist/TrackSelect/Folders/index.d.ts +14 -0
  27. package/dist/TrackSelect/Folders/types.d.ts +76 -0
  28. package/dist/TrackSelect/TrackSelect.d.ts +12 -5
  29. package/dist/TrackSelect/TreeView/CustomTreeItem.d.ts +3 -0
  30. package/dist/TrackSelect/TreeView/TreeViewWrapper.d.ts +1 -1
  31. package/dist/TrackSelect/store.d.ts +1 -2
  32. package/dist/TrackSelect/types.d.ts +24 -62
  33. package/dist/genomebrowser-ui.es.js +1373 -2117
  34. package/dist/genomebrowser-ui.es.js.map +1 -1
  35. package/dist/lib.d.ts +2 -2
  36. package/package.json +3 -3
  37. package/src/TrackSelect/DataGrid/DataGridWrapper.tsx +36 -20
  38. package/src/TrackSelect/DataGrid/DefaultGroupingCell.tsx +64 -0
  39. package/src/TrackSelect/FolderList/Breadcrumb.tsx +38 -0
  40. package/src/TrackSelect/FolderList/FolderCard.tsx +51 -0
  41. package/src/TrackSelect/FolderList/FolderList.tsx +47 -0
  42. package/src/TrackSelect/Folders/NEW.md +929 -0
  43. package/src/TrackSelect/{Data → Folders/biosamples/data}/formatBiosamples.go +2 -2
  44. package/src/TrackSelect/{Data/humanBiosamples.json → Folders/biosamples/data/human.json} +1940 -1919
  45. package/src/TrackSelect/{Data/mouseBiosamples.json → Folders/biosamples/data/mouse.json} +408 -357
  46. package/src/TrackSelect/Folders/biosamples/human.ts +17 -0
  47. package/src/TrackSelect/Folders/biosamples/mouse.ts +17 -0
  48. package/src/TrackSelect/Folders/biosamples/shared/AssayToggle.tsx +65 -0
  49. package/src/TrackSelect/{DataGrid/GroupingCell.tsx → Folders/biosamples/shared/BiosampleGroupingCell.tsx} +7 -5
  50. package/src/TrackSelect/Folders/biosamples/shared/BiosampleTreeItem.tsx +15 -0
  51. package/src/TrackSelect/{DataGrid → Folders/biosamples/shared}/columns.tsx +31 -17
  52. package/src/TrackSelect/Folders/biosamples/shared/constants.tsx +116 -0
  53. package/src/TrackSelect/Folders/biosamples/shared/createFolder.ts +116 -0
  54. package/src/TrackSelect/Folders/biosamples/shared/treeBuilder.ts +227 -0
  55. package/src/TrackSelect/Folders/biosamples/shared/types.ts +48 -0
  56. package/src/TrackSelect/Folders/genes/data/human.json +7 -0
  57. package/src/TrackSelect/Folders/genes/data/mouse.json +7 -0
  58. package/src/TrackSelect/Folders/genes/human.ts +16 -0
  59. package/src/TrackSelect/Folders/genes/mouse.ts +16 -0
  60. package/src/TrackSelect/Folders/genes/shared/columns.tsx +42 -0
  61. package/src/TrackSelect/Folders/genes/shared/createFolder.ts +68 -0
  62. package/src/TrackSelect/Folders/genes/shared/treeBuilder.ts +45 -0
  63. package/src/TrackSelect/Folders/genes/shared/types.ts +29 -0
  64. package/src/TrackSelect/Folders/index.ts +27 -0
  65. package/src/TrackSelect/Folders/types.ts +95 -0
  66. package/src/TrackSelect/TrackSelect.tsx +409 -311
  67. package/src/TrackSelect/TreeView/CustomTreeItem.tsx +217 -0
  68. package/src/TrackSelect/TreeView/TreeViewWrapper.tsx +47 -42
  69. package/src/TrackSelect/store.ts +103 -46
  70. package/src/TrackSelect/types.ts +28 -74
  71. package/src/lib.ts +2 -2
  72. package/test/main.tsx +113 -169
  73. package/.claude/settings.local.json +0 -7
  74. package/dist/TrackSelect/DataGrid/CustomToolbar.d.ts +0 -12
  75. package/dist/TrackSelect/DataGrid/GroupingCell.d.ts +0 -2
  76. package/dist/TrackSelect/DataGrid/columns.d.ts +0 -4
  77. package/dist/TrackSelect/DataGrid/dataGridHelpers.d.ts +0 -49
  78. package/dist/TrackSelect/TreeView/treeViewHelpers.d.ts +0 -49
  79. package/dist/TrackSelect/consts.d.ts +0 -11
  80. package/src/TrackSelect/DataGrid/CustomToolbar.tsx +0 -152
  81. package/src/TrackSelect/DataGrid/dataGridHelpers.tsx +0 -155
  82. package/src/TrackSelect/TreeView/treeViewHelpers.tsx +0 -475
  83. package/src/TrackSelect/consts.ts +0 -92
  84. package/src/TrackSelect/issues.md +0 -404
@@ -0,0 +1,17 @@
1
+ import { createBiosampleFolder } from "./shared/createFolder";
2
+ import humanData from "./data/human.json";
3
+ import { BiosampleDataFile } from "./shared/types";
4
+
5
+ /**
6
+ * Human biosamples folder configuration for GRCh38 assembly.
7
+ *
8
+ * Contains epigenomic data (DNase, ATAC, H3K4me3, H3K27ac, CTCF, cCRE, RNA-seq, ChromHMM)
9
+ * from human tissue samples, primary cells, cell lines, and organoids.
10
+ */
11
+ export const humanBiosamplesFolder = createBiosampleFolder({
12
+ id: "human-biosamples",
13
+ label: "Human Biosamples",
14
+ description:
15
+ "Epigenomic data from human tissue samples, primary cells, cell lines, and organoids.",
16
+ data: humanData as BiosampleDataFile,
17
+ });
@@ -0,0 +1,17 @@
1
+ import { createBiosampleFolder } from "./shared/createFolder";
2
+ import mouseData from "./data/mouse.json";
3
+ import { BiosampleDataFile } from "./shared/types";
4
+
5
+ /**
6
+ * Mouse biosamples folder configuration for mm10 assembly.
7
+ *
8
+ * Contains epigenomic data (DNase, ATAC, H3K4me3, H3K27ac, CTCF, cCRE, RNA-seq, ChromHMM)
9
+ * from mouse tissue samples, primary cells, cell lines, and organoids.
10
+ */
11
+ export const mouseBiosamplesFolder = createBiosampleFolder({
12
+ id: "mouse-biosamples",
13
+ label: "Mouse Biosamples",
14
+ description:
15
+ "Epigenomic data from mouse tissue samples, primary cells, cell lines, and organoids.",
16
+ data: mouseData as BiosampleDataFile,
17
+ });
@@ -0,0 +1,65 @@
1
+ import { FormControlLabel, Switch } from "@mui/material";
2
+ import { useState } from "react";
3
+ import { FolderRuntimeConfig } from "../../types";
4
+ import {
5
+ defaultColumns,
6
+ defaultGroupingModel,
7
+ defaultLeafField,
8
+ sortedByAssayColumns,
9
+ sortedByAssayGroupingModel,
10
+ sortedByAssayLeafField,
11
+ } from "./columns";
12
+
13
+ export interface AssayToggleProps {
14
+ updateConfig: (partial: Partial<FolderRuntimeConfig>) => void;
15
+ }
16
+
17
+ /**
18
+ * Biosample-specific toolbar component that toggles between
19
+ * sample-grouped and assay-grouped views.
20
+ *
21
+ * When toggled, it updates the folder's runtime config to switch:
22
+ * - columns: Different column definitions for each view
23
+ * - groupingModel: ["ontology", "displayName"] vs ["assay", "ontology", "displayName"]
24
+ * - leafField: "assay" vs "id"
25
+ */
26
+ export function AssayToggle({ updateConfig }: AssayToggleProps) {
27
+ const [sortedByAssay, setSortedByAssay] = useState(false);
28
+
29
+ const handleToggle = () => {
30
+ const newValue = !sortedByAssay;
31
+ setSortedByAssay(newValue);
32
+
33
+ if (newValue) {
34
+ // Switch to assay-grouped view
35
+ updateConfig({
36
+ columns: sortedByAssayColumns,
37
+ groupingModel: sortedByAssayGroupingModel,
38
+ leafField: sortedByAssayLeafField,
39
+ });
40
+ } else {
41
+ // Switch back to default (sample-grouped) view
42
+ updateConfig({
43
+ columns: defaultColumns,
44
+ groupingModel: defaultGroupingModel,
45
+ leafField: defaultLeafField,
46
+ });
47
+ }
48
+ };
49
+
50
+ return (
51
+ <FormControlLabel
52
+ sx={{ display: "flex", justifyContent: "flex-end" }}
53
+ value="Sort by assay"
54
+ control={
55
+ <Switch
56
+ color="primary"
57
+ checked={sortedByAssay}
58
+ onChange={handleToggle}
59
+ />
60
+ }
61
+ label="Sort by assay"
62
+ labelPlacement="end"
63
+ />
64
+ );
65
+ }
@@ -4,13 +4,15 @@ import {
4
4
  useGridApiContext,
5
5
  GridGroupNode,
6
6
  } from "@mui/x-data-grid-premium";
7
- import { assayTypes } from "../consts";
8
- import { AssayIcon } from "../TreeView/treeViewHelpers";
7
+ import { assayTypes, AssayIcon } from "./constants";
9
8
  import ChevronRightIcon from "@mui/icons-material/ChevronRight";
10
9
  import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
11
10
 
12
- // Custom grouping cell that preserves expand/collapse while adding truncation + tooltip
13
- export default function GroupingCell(params: GridRenderCellParams) {
11
+ /**
12
+ * Biosample-specific grouping cell that renders AssayIcon for assay groups/values.
13
+ * Handles expand/collapse, truncation, and tooltips.
14
+ */
15
+ export default function BiosampleGroupingCell(params: GridRenderCellParams) {
14
16
  const apiRef = useGridApiContext();
15
17
  const isGroup = params.rowNode.type === "group";
16
18
  const groupNode = params.rowNode as GridGroupNode;
@@ -53,7 +55,7 @@ export default function GroupingCell(params: GridRenderCellParams) {
53
55
  );
54
56
  }
55
57
 
56
- // For other groups (ontology, displayname), show bold text
58
+ // For other groups (ontology, displayName), show bold text
57
59
  if (isGroup) {
58
60
  return (
59
61
  <Tooltip title={value} placement="top-start" enterDelay={500}>
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { CustomTreeItem } from "../../../TreeView/CustomTreeItem";
3
+ import { CustomTreeItemProps } from "../../../types";
4
+ import { AssayIcon } from "./constants";
5
+
6
+ /**
7
+ * Biosample-specific TreeItem that renders AssayIcon for assay items.
8
+ * Wraps the generic CustomTreeItem with the AssayIcon renderer.
9
+ */
10
+ export const BiosampleTreeItem = React.forwardRef<
11
+ HTMLLIElement,
12
+ CustomTreeItemProps
13
+ >(function BiosampleTreeItem(props, ref) {
14
+ return <CustomTreeItem {...props} ref={ref} renderIcon={AssayIcon} />;
15
+ });
@@ -1,17 +1,16 @@
1
1
  import { GridColDef } from "@mui/x-data-grid-premium";
2
- import { RowInfo } from "../types";
3
2
  import { Stack, capitalize } from "@mui/material";
4
- import { AssayIcon } from "../TreeView/treeViewHelpers";
5
- import { ontologyTypes, assayTypes } from "../consts";
3
+ import { AssayIcon, ontologyTypes, assayTypes } from "./constants";
4
+ import { BiosampleRowInfo } from "./types";
6
5
 
7
- const displayNameCol: GridColDef<RowInfo> = {
8
- field: "displayname",
6
+ const displayNameCol: GridColDef<BiosampleRowInfo> = {
7
+ field: "displayName",
9
8
  headerName: "Name",
10
9
  valueFormatter: (value) => value && capitalize(value),
11
10
  maxWidth: 300,
12
11
  };
13
12
 
14
- const sortedByAssayOntologyCol: GridColDef<RowInfo> = {
13
+ const sortedByAssayOntologyCol: GridColDef<BiosampleRowInfo> = {
15
14
  field: "ontology",
16
15
  headerName: "Ontology",
17
16
  type: "singleSelect",
@@ -31,7 +30,7 @@ const sortedByAssayOntologyCol: GridColDef<RowInfo> = {
31
30
  },
32
31
  };
33
32
 
34
- const sortedByAssayAssayCol: GridColDef<RowInfo> = {
33
+ const sortedByAssayAssayCol: GridColDef<BiosampleRowInfo> = {
35
34
  field: "assay",
36
35
  headerName: "Assay",
37
36
  valueOptions: assayTypes,
@@ -53,7 +52,7 @@ const sortedByAssayAssayCol: GridColDef<RowInfo> = {
53
52
  },
54
53
  };
55
54
 
56
- const defaultOntologyCol: GridColDef<RowInfo> = {
55
+ const defaultOntologyCol: GridColDef<BiosampleRowInfo> = {
57
56
  field: "ontology",
58
57
  headerName: "Ontology",
59
58
  type: "singleSelect",
@@ -73,7 +72,7 @@ const defaultOntologyCol: GridColDef<RowInfo> = {
73
72
  },
74
73
  };
75
74
 
76
- const defaultAssayCol: GridColDef<RowInfo> = {
75
+ const defaultAssayCol: GridColDef<BiosampleRowInfo> = {
77
76
  field: "assay",
78
77
  headerName: "Assay",
79
78
  valueOptions: assayTypes,
@@ -91,11 +90,12 @@ const defaultAssayCol: GridColDef<RowInfo> = {
91
90
  },
92
91
  };
93
92
 
94
- const sampleTypeCol: GridColDef<RowInfo> = {
93
+ const sampleTypeCol: GridColDef<BiosampleRowInfo> = {
95
94
  field: "sampleType",
96
95
  headerName: "Sample Type",
97
96
  type: "singleSelect",
98
97
  valueOptions: [
98
+ "aggregate",
99
99
  "tissue",
100
100
  "primary cell",
101
101
  "cell line",
@@ -105,30 +105,31 @@ const sampleTypeCol: GridColDef<RowInfo> = {
105
105
  valueFormatter: (value) => value && capitalize(value),
106
106
  };
107
107
 
108
- const lifeStageCol: GridColDef<RowInfo> = {
108
+ const lifeStageCol: GridColDef<BiosampleRowInfo> = {
109
109
  field: "lifeStage",
110
110
  headerName: "Life Stage",
111
111
  type: "singleSelect",
112
- valueOptions: ["adult", "embryonic"],
112
+ valueOptions: ["adult", "embryonic", "N/A"],
113
113
  valueFormatter: (value) => value && capitalize(value),
114
114
  };
115
115
 
116
- const experimentCol: GridColDef<RowInfo> = {
116
+ const experimentCol: GridColDef<BiosampleRowInfo> = {
117
117
  field: "experimentAccession",
118
118
  headerName: "Experiment Accession",
119
119
  };
120
120
 
121
- const fileCol: GridColDef<RowInfo> = {
121
+ const fileCol: GridColDef<BiosampleRowInfo> = {
122
122
  field: "fileAccession",
123
123
  headerName: "File Accession",
124
124
  };
125
125
 
126
- const idCol: GridColDef<RowInfo> = {
126
+ const idCol: GridColDef<BiosampleRowInfo> = {
127
127
  field: "id",
128
128
  headerName: "ID",
129
129
  };
130
130
 
131
- export const sortedByAssayColumns: GridColDef<RowInfo>[] = [
131
+ /** Columns for sorted-by-assay view (assay as top-level grouping) */
132
+ export const sortedByAssayColumns: GridColDef<BiosampleRowInfo>[] = [
132
133
  displayNameCol,
133
134
  sortedByAssayOntologyCol,
134
135
  sampleTypeCol,
@@ -139,7 +140,8 @@ export const sortedByAssayColumns: GridColDef<RowInfo>[] = [
139
140
  idCol,
140
141
  ];
141
142
 
142
- export const defaultColumns: GridColDef<RowInfo>[] = [
143
+ /** Default columns (ontology as top-level grouping) */
144
+ export const defaultColumns: GridColDef<BiosampleRowInfo>[] = [
143
145
  defaultAssayCol,
144
146
  sampleTypeCol,
145
147
  lifeStageCol,
@@ -149,3 +151,15 @@ export const defaultColumns: GridColDef<RowInfo>[] = [
149
151
  fileCol,
150
152
  idCol,
151
153
  ];
154
+
155
+ /** Grouping model for sorted-by-assay view */
156
+ export const sortedByAssayGroupingModel = ["assay", "ontology", "displayName"];
157
+
158
+ /** Default grouping model (ontology-based) */
159
+ export const defaultGroupingModel = ["ontology", "displayName"];
160
+
161
+ /** Leaf field for sorted-by-assay view */
162
+ export const sortedByAssayLeafField = "id";
163
+
164
+ /** Default leaf field */
165
+ export const defaultLeafField = "assay";
@@ -0,0 +1,116 @@
1
+ import { Box } from "@mui/material";
2
+ import type { Assembly } from "../../types";
3
+
4
+ export type { Assembly };
5
+
6
+ export const assayTypes = [
7
+ "cCRE",
8
+ "DNase",
9
+ "H3K4me3",
10
+ "H3K27ac",
11
+ "ATAC",
12
+ "CTCF",
13
+ "RNA-seq",
14
+ "ChromHMM",
15
+ ];
16
+
17
+ export const ontologyTypes = [
18
+ "Aggregate",
19
+ "Adipose",
20
+ "Adrenal gland",
21
+ "Blood",
22
+ "Blood vessel",
23
+ "Bone",
24
+ "Bone marrow",
25
+ "Brain",
26
+ "Breast",
27
+ "Connective tissue",
28
+ "Embryo",
29
+ "Epithelium",
30
+ "Esophagus",
31
+ "Eye",
32
+ "Fallopian Tube",
33
+ "Gallbladder",
34
+ "Heart",
35
+ "Kidney",
36
+ "Large Intestine",
37
+ "Limb",
38
+ "Liver",
39
+ "Lung",
40
+ "Lymphoid Tissue",
41
+ "Muscle",
42
+ "Mouth",
43
+ "Nerve",
44
+ "Nose",
45
+ "Pancreas",
46
+ "Parathyroid Gland",
47
+ "Ovary",
48
+ "Penis",
49
+ "Placenta",
50
+ "Prostate",
51
+ "Skin",
52
+ "Small Intestine",
53
+ "Spinal Cord",
54
+ "Spleen",
55
+ "Stomach",
56
+ "Testis",
57
+ "Thymus",
58
+ "Thyroid",
59
+ "Urinary Bladder",
60
+ "Uterus",
61
+ "Vagina",
62
+ ];
63
+
64
+ /** Color mapping for assay types */
65
+ export const assayColorMap: { [key: string]: string } = {
66
+ DNase: "#06da93",
67
+ ATAC: "#02c7b9",
68
+ H3K4me3: "#ff2020",
69
+ ChromHMM: "#0097a7",
70
+ H3K27ac: "#fdc401",
71
+ CTCF: "#01a6f1",
72
+ cCRE: "#000000",
73
+ "RNA-seq": "#00aa00",
74
+ };
75
+
76
+ /**
77
+ * Creates the assay icon for DataGrid and RichTreeView
78
+ * @param type - assay type
79
+ * @returns an icon of the assay's respective color
80
+ */
81
+ export function AssayIcon(type: string) {
82
+ const color = assayColorMap[type];
83
+ return (
84
+ <Box
85
+ sx={{
86
+ width: 12,
87
+ height: 12,
88
+ borderRadius: "20%",
89
+ bgcolor: color,
90
+ }}
91
+ />
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Mapping from JSON assay keys to display names.
97
+ * This is the single source of truth for assay name formatting.
98
+ */
99
+ const assayJsonToDisplay: Record<string, string> = {
100
+ dnase: "DNase",
101
+ atac: "ATAC",
102
+ h3k4me3: "H3K4me3",
103
+ h3k27ac: "H3K27ac",
104
+ ctcf: "CTCF",
105
+ chromhmm: "ChromHMM",
106
+ ccre: "cCRE",
107
+ rnaseq: "RNA-seq",
108
+ };
109
+
110
+ /**
111
+ * Convert JSON assay key to display name.
112
+ * Used only during data loading to normalize assay names.
113
+ */
114
+ export function formatAssayType(jsonKey: string): string {
115
+ return assayJsonToDisplay[jsonKey.toLowerCase()] ?? jsonKey;
116
+ }
@@ -0,0 +1,116 @@
1
+ import { capitalize } from "@mui/material";
2
+ import { FolderDefinition } from "../../types";
3
+ import {
4
+ BiosampleDataFile,
5
+ BiosampleRowInfo,
6
+ BiosampleTrackInfo,
7
+ } from "./types";
8
+ import {
9
+ defaultColumns,
10
+ defaultGroupingModel,
11
+ defaultLeafField,
12
+ } from "./columns";
13
+ import { buildTreeView } from "./treeBuilder";
14
+ import { formatAssayType } from "./constants";
15
+ import { AssayToggle } from "./AssayToggle";
16
+ import BiosampleGroupingCell from "./BiosampleGroupingCell";
17
+ import { BiosampleTreeItem } from "./BiosampleTreeItem";
18
+
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[] {
27
+ const { ontology, lifeStage, sampleType, displayName } = track;
28
+
29
+ return track.assays.map(
30
+ ({ id, assay, experimentAccession, fileAccession, url }) => ({
31
+ id,
32
+ ontology: capitalize(ontology),
33
+ lifeStage: capitalize(lifeStage),
34
+ sampleType: capitalize(sampleType),
35
+ displayName: capitalize(displayName),
36
+ assay: formatAssayType(assay),
37
+ experimentAccession,
38
+ fileAccession,
39
+ url,
40
+ }),
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Transforms raw JSON data into flattened rows and a lookup map.
46
+ * Prefixes each row ID with the folder ID to ensure uniqueness across folders.
47
+ *
48
+ * @param data - Raw biosample data from JSON file
49
+ * @param folderId - Folder ID to prefix row IDs with
50
+ * @returns Object containing rows array and rowById map
51
+ */
52
+ function transformData(data: BiosampleDataFile): {
53
+ rowById: Map<string, BiosampleRowInfo>;
54
+ } {
55
+ const rows = data.tracks.flatMap(flattenTrackIntoRows).map((row) => ({
56
+ ...row,
57
+ id: row.id,
58
+ }));
59
+ const rowById = new Map<string, BiosampleRowInfo>(
60
+ rows.map((row) => [row.id, row]),
61
+ );
62
+ return { rowById };
63
+ }
64
+
65
+ export interface CreateBiosampleFolderOptions {
66
+ /** Unique identifier for this folder */
67
+ id: string;
68
+ /** Display label shown in the UI */
69
+ label: string;
70
+ /** Optional description shown in folder cards */
71
+ description?: string;
72
+ /** Raw biosample data from JSON file */
73
+ data: BiosampleDataFile;
74
+ }
75
+
76
+ /**
77
+ * Factory function that creates a FolderDefinition for biosample data.
78
+ *
79
+ * This handles all the common setup for biosample folders:
80
+ * - Transforms JSON data into flattened rows
81
+ * - Creates the rowById lookup map
82
+ * - Configures columns, grouping, and tree building
83
+ *
84
+ * @param options - Configuration options for the folder
85
+ * @returns A complete FolderDefinition for the biosample data
86
+ */
87
+ export function createBiosampleFolder(
88
+ options: CreateBiosampleFolderOptions,
89
+ ): FolderDefinition<BiosampleRowInfo> {
90
+ const { id, label, description, data } = options;
91
+ const { rowById } = transformData(data);
92
+
93
+ return {
94
+ id,
95
+ label,
96
+ description,
97
+ rowById,
98
+ getRowId: (row) => row.id,
99
+
100
+ // Default view: ontology-based grouping
101
+ columns: defaultColumns,
102
+ groupingModel: defaultGroupingModel,
103
+ leafField: defaultLeafField,
104
+
105
+ // Build tree for selected items panel
106
+ buildTree: (selectedIds, rowById) =>
107
+ buildTreeView(selectedIds, rowById, label, id),
108
+
109
+ // Biosample-specific toolbar: toggle between sample-grouped and assay-grouped views
110
+ ToolbarExtras: AssayToggle,
111
+
112
+ // Biosample-specific custom components
113
+ GroupingCellComponent: BiosampleGroupingCell,
114
+ TreeItemComponent: BiosampleTreeItem,
115
+ };
116
+ }