@datagrok/proteomics 1.0.1 → 1.2.1
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/CHANGELOG.md +52 -3
- package/CLAUDE.md +178 -0
- package/README.md +195 -3
- package/detectors.js +152 -9
- package/dist/package-test.js +1 -1744
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -146
- package/dist/package.js.map +1 -1
- package/docs/personas-and-capabilities.md +165 -0
- package/files/demo/README.md +264 -0
- package/files/demo/cptac-spike-in.txt +1571 -0
- package/files/demo/enrichment-demo.csv +120 -0
- package/files/demo/fragpipe-smoke-test.tsv +11 -0
- package/files/demo/proteinGroups.txt +28 -0
- package/files/demo/spectronaut-hye-candidates.tsv +94 -0
- package/files/demo/spectronaut-hye-demo.tsv +8761 -0
- package/files/demo/spectronaut-hye-mix.tsv +8761 -0
- package/files/demo/spectronaut-hye-precursor-golden.json +938 -0
- package/files/demo/spectronaut-hye-precursor-golden.tsv +235 -0
- package/files/demo/spectronaut-hye-precursor.tsv +493 -0
- package/images/enrichment-crosslink.png +0 -0
- package/images/enrichment-term-selected.png +0 -0
- package/images/hero.png +0 -0
- package/images/pipeline.svg +80 -0
- package/package.json +88 -63
- package/scripts/deqms_de.R +60 -0
- package/scripts/limma_de.R +42 -0
- package/scripts/vsn_normalize.R +19 -0
- package/src/analysis/differential-expression.ts +450 -0
- package/src/analysis/enrichment-export.ts +101 -0
- package/src/analysis/enrichment.ts +602 -0
- package/src/analysis/experiment-setup.ts +199 -0
- package/src/analysis/imputation.ts +407 -0
- package/src/analysis/log2-scale.ts +139 -0
- package/src/analysis/normalization.ts +255 -0
- package/src/analysis/pca.ts +254 -0
- package/src/analysis/spc-storage.ts +515 -0
- package/src/analysis/spc.ts +544 -0
- package/src/analysis/subcellular-location.ts +431 -0
- package/src/demo/enrichment-demo.ts +94 -0
- package/src/demo/proteomics-demo.ts +123 -0
- package/src/global.d.ts +15 -0
- package/src/menu.ts +133 -0
- package/src/package-api.ts +136 -14
- package/src/package-test.ts +45 -20
- package/src/package.g.ts +161 -0
- package/src/package.ts +1029 -17
- package/src/panels/protein-focus.ts +63 -0
- package/src/panels/published-analysis-panel.ts +151 -0
- package/src/panels/uniprot-panel.ts +349 -0
- package/src/parsers/fragpipe-parser.ts +200 -0
- package/src/parsers/generic-parser.ts +197 -0
- package/src/parsers/maxquant-parser.ts +162 -0
- package/src/parsers/shared-utils.ts +163 -0
- package/src/parsers/spectronaut-candidates-parser.ts +307 -0
- package/src/parsers/spectronaut-parser.ts +604 -0
- package/src/publishing/assert-published-shape.ts +260 -0
- package/src/publishing/post-open-recovery.ts +104 -0
- package/src/publishing/publish-project.ts +515 -0
- package/src/publishing/publish-settings.ts +59 -0
- package/src/publishing/publish-state.ts +316 -0
- package/src/publishing/share-dialog.ts +171 -0
- package/src/publishing/trim-dataframe.ts +247 -0
- package/src/tests/analysis.ts +658 -0
- package/src/tests/enrichment-export.ts +61 -0
- package/src/tests/enrichment-visualization.ts +340 -0
- package/src/tests/enrichment.ts +224 -0
- package/src/tests/fragpipe-e2e.ts +74 -0
- package/src/tests/fragpipe-parser.ts +147 -0
- package/src/tests/gene-label-resolver.ts +387 -0
- package/src/tests/generic-parser.ts +152 -0
- package/src/tests/group-mean-correlation.ts +139 -0
- package/src/tests/log2-scale.ts +93 -0
- package/src/tests/organisms.ts +56 -0
- package/src/tests/parsers.ts +182 -0
- package/src/tests/publish-roundtrip.ts +584 -0
- package/src/tests/publish-spike.ts +377 -0
- package/src/tests/qc-dashboard.ts +210 -0
- package/src/tests/smart-pathway-filter.ts +193 -0
- package/src/tests/spc-formula-lines-spike.ts +129 -0
- package/src/tests/spc.ts +640 -0
- package/src/tests/spectronaut-candidates-e2e.ts +140 -0
- package/src/tests/spectronaut-candidates-parser.ts +398 -0
- package/src/tests/spectronaut-parser.ts +668 -0
- package/src/tests/subcellular-location.ts +361 -0
- package/src/tests/uniprot-panel.ts +202 -0
- package/src/tests/volcano.ts +603 -0
- package/src/utils/column-detection.ts +28 -0
- package/src/utils/gene-label-resolver.ts +443 -0
- package/src/utils/organisms.ts +82 -0
- package/src/utils/proteomics-types.ts +30 -0
- package/src/viewers/enrichment-viewers.ts +274 -0
- package/src/viewers/group-mean-correlation.ts +218 -0
- package/src/viewers/heatmap.ts +168 -0
- package/src/viewers/pca-plot.ts +169 -0
- package/src/viewers/qc-computations.ts +266 -0
- package/src/viewers/qc-dashboard.ts +176 -0
- package/src/viewers/spc-dashboard.ts +755 -0
- package/src/viewers/volcano.ts +690 -0
- package/test-console-output-1.log +2073 -0
- package/test-record-1.mp4 +0 -0
- package/tools/derive-precursor-golden-sidecar.mjs +81 -0
- package/tools/generate-enrichment-fixture.sh +160 -0
- package/tools/generate-spectronaut-candidates-fixture.mjs +212 -0
- package/tools/generate-spectronaut-precursor-fixture.mjs +128 -0
- package/tools/spectronaut-aggregate.sh +46 -0
- package/tools/spectronaut-aggregate.sql +80 -0
- package/tsconfig.json +18 -71
- package/webpack.config.js +86 -45
- package/.eslintignore +0 -1
- package/.eslintrc.json +0 -56
- package/LICENSE +0 -674
- package/package.png +0 -0
- package/scripts/number_antibody.py +0 -190
- package/scripts/number_antibody_abnumber.py +0 -177
- package/scripts/number_antibody_anarci.py +0 -200
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared UniProt subcellular-location service.
|
|
5
|
+
*
|
|
6
|
+
* The classifier, keyword map, and hex palette are ported VERBATIM from the
|
|
7
|
+
* locked CK-omics contract (~/Downloads/ck/CKomics_tool2.py:1384-1436
|
|
8
|
+
* `parse_subcellular_location` / `get_location_colors` and
|
|
9
|
+
* Subcellular_Location_Classification_README.txt). Any divergence is a parity
|
|
10
|
+
* regression — do not re-derive. Pure functions do no I/O.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// --- Verbatim CK-omics 11-category keyword map (dict insertion order matters:
|
|
14
|
+
// it is the tie-break for equal-position matches). ---
|
|
15
|
+
|
|
16
|
+
export const LOCATION_KEYWORDS: Record<string, string[]> = {
|
|
17
|
+
'Nucleus': ['nucleus', 'nuclear', 'nucleoplasm', 'chromatin', 'nucleolus',
|
|
18
|
+
'nuclear envelope', 'nuclear membrane', 'nuclear pore', 'nuclear speck',
|
|
19
|
+
'cajal body', 'nuclear body'],
|
|
20
|
+
'Cytoplasm': ['cytoplasm', 'cytosol', 'cytoplasmic', 'cytoskeleton',
|
|
21
|
+
'microtubule', 'actin', 'intermediate filament', 'sarcomere', 'myofibril',
|
|
22
|
+
'z disc', 'z disk', 'spindle', 'centriole', 'centrosome', 'cilium',
|
|
23
|
+
'flagellum', 'stress fiber'],
|
|
24
|
+
'Mitochondria': ['mitochondria', 'mitochondrial', 'mitochondrion',
|
|
25
|
+
'mitochondrial matrix', 'mitochondrial membrane',
|
|
26
|
+
'mitochondrial inner membrane', 'mitochondrial outer membrane',
|
|
27
|
+
'mitochondrial intermembrane space'],
|
|
28
|
+
'ER': ['endoplasmic reticulum', 'sarcoplasmic reticulum',
|
|
29
|
+
'rough endoplasmic reticulum', 'smooth endoplasmic reticulum',
|
|
30
|
+
'er-golgi intermediate compartment', 'ergic'],
|
|
31
|
+
'Golgi': ['golgi', 'trans-golgi', 'golgi apparatus', 'golgi membrane',
|
|
32
|
+
'cis-golgi', 'trans-golgi network', 'tgn'],
|
|
33
|
+
'Plasma Membrane': ['plasma membrane', 'cell membrane', 'cell surface',
|
|
34
|
+
'tight junction', 'gap junction', 'adherens junction', 'desmosome',
|
|
35
|
+
'focal adhesion', 'cell junction', 'synapse', 'postsynaptic',
|
|
36
|
+
'presynaptic', 'neuromuscular junction'],
|
|
37
|
+
'Lysosome': ['lysosome', 'lysosomal', 'vacuole', 'lysosomal membrane',
|
|
38
|
+
'autophagosome', 'phagosome'],
|
|
39
|
+
'Peroxisome': ['peroxisome', 'peroxisomal', 'glyoxysome'],
|
|
40
|
+
'Ribosome': ['ribosome', 'ribosomal', 'ribosomal protein', 'polysome'],
|
|
41
|
+
'Extracellular': ['extracellular', 'secreted', 'extracellular space',
|
|
42
|
+
'extracellular matrix', 'basement membrane', 'basal lamina', 'collagen',
|
|
43
|
+
'ecm'],
|
|
44
|
+
'Vesicles': ['vesicle', 'endosome', 'transport vesicle', 'secretory vesicle',
|
|
45
|
+
'early endosome', 'late endosome', 'recycling endosome',
|
|
46
|
+
'clathrin-coated vesicle', 'coated vesicle'],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Locked hex palette (README §COLOR SCHEME) as ARGB ints — same 0xFF… form as
|
|
50
|
+
* volcano.ts:60-62. 11 categories + Unknown. */
|
|
51
|
+
export const LOCATION_COLORS: Record<string, number> = {
|
|
52
|
+
'Nucleus': 0xFF000000 | parseInt('FF6B6B', 16),
|
|
53
|
+
'Cytoplasm': 0xFF000000 | parseInt('ECDC44', 16),
|
|
54
|
+
'Mitochondria': 0xFF000000 | parseInt('45B7D1', 16),
|
|
55
|
+
'ER': 0xFF000000 | parseInt('96CEB4', 16),
|
|
56
|
+
'Golgi': 0xFF000000 | parseInt('FAAFFE', 16),
|
|
57
|
+
'Plasma Membrane': 0xFF000000 | parseInt('DDA0DD', 16),
|
|
58
|
+
'Lysosome': 0xFF000000 | parseInt('F39C12', 16),
|
|
59
|
+
'Peroxisome': 0xFF000000 | parseInt('E17055', 16),
|
|
60
|
+
'Ribosome': 0xFF000000 | parseInt('A29BFE', 16),
|
|
61
|
+
'Extracellular': 0xFF000000 | parseInt('FD79A8', 16),
|
|
62
|
+
'Vesicles': 0xFF000000 | parseInt('FDCB6E', 16),
|
|
63
|
+
'Unknown': 0xFF000000 | parseInt('CCCCCC', 16),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Escapes regex metacharacters (mirrors Python `re.escape` faithfully — the
|
|
67
|
+
* current keywords contain none, but stay defensive). */
|
|
68
|
+
function escapeRegex(s: string): string {
|
|
69
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** First location keyword (by earliest character position across ALL
|
|
73
|
+
* categories) in the lowercased RAW text. Ties keep the first-iterated
|
|
74
|
+
* category (strict `<`), so keyword-map insertion order is the tie-break.
|
|
75
|
+
* The raw string is NOT pre-stripped (Pitfall 2). */
|
|
76
|
+
function findFirstLocation(text: string): string | null {
|
|
77
|
+
if (!text) return null;
|
|
78
|
+
const lower = text.toLowerCase();
|
|
79
|
+
let earliestMatch: string | null = null;
|
|
80
|
+
let earliestPos = Infinity;
|
|
81
|
+
for (const category of Object.keys(LOCATION_KEYWORDS)) {
|
|
82
|
+
for (const keyword of LOCATION_KEYWORDS[category]) {
|
|
83
|
+
const m = new RegExp('\\b' + escapeRegex(keyword) + '\\b', 'i').exec(lower);
|
|
84
|
+
if (m && m.index < earliestPos) {
|
|
85
|
+
earliestPos = m.index;
|
|
86
|
+
earliestMatch = category;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return earliestMatch;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Verbatim port of CK-omics `parse_subcellular_location`: subcellular field is
|
|
94
|
+
* scanned fully first, GO cellular-component is fallback only, else 'Unknown'. */
|
|
95
|
+
export function parseSubcellularLocation(
|
|
96
|
+
subcell: string | null | undefined,
|
|
97
|
+
go: string | null | undefined,
|
|
98
|
+
): string {
|
|
99
|
+
const subcellText = subcell == null ? '' : subcell;
|
|
100
|
+
const goText = go == null ? '' : go;
|
|
101
|
+
let location = findFirstLocation(subcellText);
|
|
102
|
+
if (location) return location;
|
|
103
|
+
location = findFirstLocation(goText);
|
|
104
|
+
if (location) return location;
|
|
105
|
+
return 'Unknown';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Result of parsing one UniProt stream TSV body. */
|
|
109
|
+
export interface StreamTsvResult {
|
|
110
|
+
locByAcc: Map<string, string>;
|
|
111
|
+
geneByAcc: Map<string, string>;
|
|
112
|
+
/** Whether the winning location for an accession came from a reviewed entry. */
|
|
113
|
+
reviewedByAcc: Map<string, boolean>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Pure positional parse of a UniProt stream TSV (fields order:
|
|
118
|
+
* accession, cc_subcellular_location, go_c, reviewed, gene_primary). The first
|
|
119
|
+
* line is the display-name header and is skipped; columns are read by POSITION
|
|
120
|
+
* (`parts[0..4]`), never by header name (Pitfall 1). Within the text, the
|
|
121
|
+
* priority merge is: first occurrence sets; a reviewed non-Unknown overwrites;
|
|
122
|
+
* an existing Unknown is overwritten by any non-Unknown.
|
|
123
|
+
*/
|
|
124
|
+
export function mergeStreamTsv(text: string): StreamTsvResult {
|
|
125
|
+
const locByAcc = new Map<string, string>();
|
|
126
|
+
const geneByAcc = new Map<string, string>();
|
|
127
|
+
const reviewedByAcc = new Map<string, boolean>();
|
|
128
|
+
const lines = text.trim().split('\n');
|
|
129
|
+
for (let li = 1; li < lines.length; li++) {
|
|
130
|
+
const line = lines[li];
|
|
131
|
+
if (!line) continue;
|
|
132
|
+
const p = line.split('\t');
|
|
133
|
+
const acc = (p[0] ?? '').trim();
|
|
134
|
+
if (!acc) continue;
|
|
135
|
+
const subcell = p[1] ?? '';
|
|
136
|
+
const goc = p[2] ?? '';
|
|
137
|
+
const reviewed = (p[3] ?? '').toLowerCase() === 'reviewed';
|
|
138
|
+
const gene = (p[4] ?? '').trim();
|
|
139
|
+
const cat = parseSubcellularLocation(subcell, goc);
|
|
140
|
+
|
|
141
|
+
if (!locByAcc.has(acc)) {
|
|
142
|
+
locByAcc.set(acc, cat);
|
|
143
|
+
reviewedByAcc.set(acc, reviewed);
|
|
144
|
+
} else if (reviewed && cat !== 'Unknown') {
|
|
145
|
+
locByAcc.set(acc, cat);
|
|
146
|
+
reviewedByAcc.set(acc, true);
|
|
147
|
+
} else if (locByAcc.get(acc) === 'Unknown' && cat !== 'Unknown') {
|
|
148
|
+
locByAcc.set(acc, cat);
|
|
149
|
+
reviewedByAcc.set(acc, reviewed);
|
|
150
|
+
}
|
|
151
|
+
if (gene && (!geneByAcc.has(acc) || reviewed))
|
|
152
|
+
geneByAcc.set(acc, gene);
|
|
153
|
+
}
|
|
154
|
+
return {locByAcc, geneByAcc, reviewedByAcc};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Fetch + cache + D-03 fallback (Task 2) ---
|
|
158
|
+
|
|
159
|
+
/** Organism g:Profiler code → NCBI (species-level) taxonomy id. Covers all 9
|
|
160
|
+
* organisms in enrichment's `ORGANISM_LIST`. Species-level ids are used so the
|
|
161
|
+
* UniProt `taxonomy_id:` filter (hierarchical — matches the taxon and its
|
|
162
|
+
* descendants) stays inclusive of strains (e.g. E. coli K-12 under 562).
|
|
163
|
+
* Only the gene-fallback pass narrows by this; accession queries do not (A1:
|
|
164
|
+
* accessions are globally unique, so filtering them risks excluding valid hits). */
|
|
165
|
+
export const ORGANISM_TAXONOMY: Record<string, number> = {
|
|
166
|
+
hsapiens: 9606, mmusculus: 10090, rnorvegicus: 10116, scerevisiae: 4932,
|
|
167
|
+
ecoli: 562, drerio: 7955, dmelanogaster: 7227, athaliana: 3702, celegans: 6239,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** userDataStorage key for the cross-session accession → category cache.
|
|
171
|
+
* Exported so the volcanoOptions OK handler can peek the cache to decide
|
|
172
|
+
* which pre-OK toast wording (cold vs warm) to show — see package.ts
|
|
173
|
+
* volcanoOptions. */
|
|
174
|
+
export const STORE = 'proteomics-subcell-loc';
|
|
175
|
+
/** Bump only if the locked keyword map ever changes (D-04: it is a contract,
|
|
176
|
+
* so this is effectively fixed). Mismatch → stale cache discarded. */
|
|
177
|
+
const SCHEMA_V = '13-04-1';
|
|
178
|
+
const SCHEMA_KEY = '__schema_v';
|
|
179
|
+
const ACC_CHUNK = 100;
|
|
180
|
+
const GENE_CHUNK = 20;
|
|
181
|
+
|
|
182
|
+
/** Progress callback shape used by getSubcellularLocations and forwarded by
|
|
183
|
+
* ensureLocationColumn / recomputeVolcano / volcanoOptions. The dialog's
|
|
184
|
+
* DG.TaskBarProgressIndicator.update is the call site that consumes this.
|
|
185
|
+
*
|
|
186
|
+
* - 'fetch-acc' — Pass 1 chunked accession-stream UniProt queries.
|
|
187
|
+
* - 'fetch-gene' — Pass 2 reviewed-by-gene fallback queries.
|
|
188
|
+
* - 'init-column' — bulk-init of the LOCATION_COL column on the DataFrame
|
|
189
|
+
* (in volcano.ts) AFTER the network work completes. */
|
|
190
|
+
export type ProgressCb = (done: number, total: number,
|
|
191
|
+
phase: 'fetch-acc' | 'fetch-gene' | 'init-column') => void;
|
|
192
|
+
|
|
193
|
+
/** Maximum in-flight UniProt stream chunks. 6 is a compromise: 4× the
|
|
194
|
+
* sequential baseline on a real ~80-chunk Spectronaut Candidates file while
|
|
195
|
+
* staying well under rest.uniprot.org's documented limits. Increase only
|
|
196
|
+
* with measured evidence; UniProt's stream endpoint is shared infrastructure. */
|
|
197
|
+
const FETCH_CONCURRENCY = 6;
|
|
198
|
+
|
|
199
|
+
/** Wall-clock interval between incremental cache flushes during a fetch.
|
|
200
|
+
* A single setInterval-driven flush from the orchestrator (not from inside
|
|
201
|
+
* workers) sidesteps the per-chunk-counter race that a previous draft hit
|
|
202
|
+
* when two workers reached the flush check between `await` points. */
|
|
203
|
+
const CACHE_FLUSH_INTERVAL_MS = 5000;
|
|
204
|
+
|
|
205
|
+
const VALID_CATEGORIES = new Set<string>([...Object.keys(LOCATION_KEYWORDS), 'Unknown']);
|
|
206
|
+
|
|
207
|
+
const STREAM_FIELDS = 'accession,cc_subcellular_location,go_c,reviewed,gene_primary';
|
|
208
|
+
const GENE_FIELDS = 'accession,gene_primary,cc_subcellular_location,go_c';
|
|
209
|
+
|
|
210
|
+
function chunk<T>(arr: T[], size: number): T[][] {
|
|
211
|
+
const out: T[][] = [];
|
|
212
|
+
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Index-based worker pool. Maintains a shared `nextIndex` counter; launches
|
|
217
|
+
* `limit` async workers; each worker pulls indices and awaits `work(items[i], i)`,
|
|
218
|
+
* storing results at `results[i]`; resolves with the full results array when
|
|
219
|
+
* every worker drains. Used by getSubcellularLocations Pass 1/2 to cap
|
|
220
|
+
* concurrent UniProt fetchProxy calls at FETCH_CONCURRENCY.
|
|
221
|
+
*
|
|
222
|
+
* Exported only so the test suite can verify the concurrency cap directly
|
|
223
|
+
* with synthetic deferred-promise tasks (no network). */
|
|
224
|
+
export async function runWithConcurrency<T, R>(
|
|
225
|
+
items: T[], limit: number, work: (item: T, idx: number) => Promise<R>,
|
|
226
|
+
): Promise<R[]> {
|
|
227
|
+
const results: R[] = new Array(items.length);
|
|
228
|
+
let nextIndex = 0;
|
|
229
|
+
const workerCount = Math.min(Math.max(1, limit), items.length || 1);
|
|
230
|
+
const workers: Promise<void>[] = [];
|
|
231
|
+
for (let w = 0; w < workerCount; w++) {
|
|
232
|
+
workers.push((async () => {
|
|
233
|
+
while (true) {
|
|
234
|
+
const i = nextIndex++;
|
|
235
|
+
if (i >= items.length) return;
|
|
236
|
+
results[i] = await work(items[i], i);
|
|
237
|
+
}
|
|
238
|
+
})());
|
|
239
|
+
}
|
|
240
|
+
await Promise.all(workers);
|
|
241
|
+
return results;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function taxonomyClause(organism?: string): string {
|
|
245
|
+
const tax = organism ? ORGANISM_TAXONOMY[organism] : undefined;
|
|
246
|
+
return tax ? ` AND (taxonomy_id:${tax})` : '';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function loadCache(): Promise<Record<string, string>> {
|
|
250
|
+
try {
|
|
251
|
+
const raw = (await grok.dapi.userDataStorage.get(STORE)) ?? {};
|
|
252
|
+
if (raw[SCHEMA_KEY] !== SCHEMA_V)
|
|
253
|
+
return {[SCHEMA_KEY]: SCHEMA_V}; // stale → discard
|
|
254
|
+
return raw as Record<string, string>;
|
|
255
|
+
} catch {
|
|
256
|
+
return {[SCHEMA_KEY]: SCHEMA_V};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Resolves accession → subcellular category for every requested accession.
|
|
262
|
+
* Cache-first (cross-session userDataStorage map, `__schema_v`-keyed), then a
|
|
263
|
+
* chunked UniProt stream fetch via grok.dapi.fetchProxy (no raw fetch — CORS),
|
|
264
|
+
* then the D-03 reviewed-by-gene fallback for Unknown+gene accessions. Never
|
|
265
|
+
* throws out of the fetch loop: a failed batch warns and continues.
|
|
266
|
+
*/
|
|
267
|
+
export async function getSubcellularLocations(
|
|
268
|
+
accessions: string[],
|
|
269
|
+
organism?: string,
|
|
270
|
+
progress?: ProgressCb,
|
|
271
|
+
): Promise<Map<string, string>> {
|
|
272
|
+
const unique = [...new Set(accessions.filter((a) => a && a.length > 0))];
|
|
273
|
+
const cache = await loadCache();
|
|
274
|
+
|
|
275
|
+
const resolved = new Map<string, string>();
|
|
276
|
+
const misses: string[] = [];
|
|
277
|
+
for (const acc of unique) {
|
|
278
|
+
const c = cache[acc];
|
|
279
|
+
if (c !== undefined && VALID_CATEGORIES.has(c)) resolved.set(acc, c);
|
|
280
|
+
else misses.push(acc);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const geneByAcc = new Map<string, string>();
|
|
284
|
+
const fetched: Record<string, string> = {};
|
|
285
|
+
// Applied ONLY to the Pass 2 gene fallback below — gene symbols are shared
|
|
286
|
+
// across species, so without this a rat gene resolves to the human entry.
|
|
287
|
+
const taxClause = taxonomyClause(organism);
|
|
288
|
+
|
|
289
|
+
// Single timer-driven incremental flush. Workers DO NOT touch the cache —
|
|
290
|
+
// they only mutate `resolved`/`fetched`/`geneByAcc`. The timer reads the
|
|
291
|
+
// current `fetched` map at its tick and writes through, so a session
|
|
292
|
+
// interrupted mid-fetch keeps every accession that finished. The previous
|
|
293
|
+
// draft folded the flush into the per-chunk worker body and hit a documented
|
|
294
|
+
// double-flush race when two workers reached the check between `await`
|
|
295
|
+
// points — see plan 13-08 BLOCKER FIX notes.
|
|
296
|
+
const flushCache = async (): Promise<void> => {
|
|
297
|
+
if (Object.keys(fetched).length === 0) return;
|
|
298
|
+
try {
|
|
299
|
+
await grok.dapi.userDataStorage.put(STORE,
|
|
300
|
+
{...cache, ...fetched, [SCHEMA_KEY]: SCHEMA_V});
|
|
301
|
+
} catch (e: any) {
|
|
302
|
+
console.warn(`Subcellular-location cache write failed: ${e?.message ?? e}`);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const flushTimer = setInterval(() => { flushCache().catch(() => {}); },
|
|
306
|
+
CACHE_FLUSH_INTERVAL_MS);
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
// Pass 1: chunked accession stream queries, run with bounded concurrency.
|
|
310
|
+
const accChunks = chunk(misses, ACC_CHUNK);
|
|
311
|
+
const totalAcc = accChunks.length;
|
|
312
|
+
let doneAcc = 0;
|
|
313
|
+
await runWithConcurrency(accChunks, FETCH_CONCURRENCY, async (group) => {
|
|
314
|
+
// Pass 1 is deliberately NOT taxonomy-filtered: accessions are globally
|
|
315
|
+
// unique, so a wrong/mis-set organism must never exclude a valid protein.
|
|
316
|
+
const q = group.map((a) => `accession:${a}`).join(' OR ');
|
|
317
|
+
const url = 'https://rest.uniprot.org/uniprotkb/stream' +
|
|
318
|
+
`?query=(${encodeURIComponent(q)})` +
|
|
319
|
+
`&fields=${STREAM_FIELDS}&format=tsv`;
|
|
320
|
+
try {
|
|
321
|
+
const resp = await grok.dapi.fetchProxy(url);
|
|
322
|
+
if (!resp.ok) {
|
|
323
|
+
console.warn(`UniProt stream returned status ${resp.status}; continuing`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const {locByAcc, geneByAcc: g} = mergeStreamTsv(await resp.text());
|
|
327
|
+
for (const [acc, loc] of locByAcc) {
|
|
328
|
+
resolved.set(acc, loc);
|
|
329
|
+
fetched[acc] = loc;
|
|
330
|
+
}
|
|
331
|
+
for (const [acc, gene] of g) geneByAcc.set(acc, gene);
|
|
332
|
+
} catch (e: any) {
|
|
333
|
+
console.warn(`UniProt stream batch failed: ${e?.message ?? e}; continuing`);
|
|
334
|
+
}
|
|
335
|
+
// Increment AFTER the chunk completes — bounded concurrency means
|
|
336
|
+
// worker order can interleave, so the callback signal is non-decreasing
|
|
337
|
+
// (not strictly monotonic). Tests assert the non-decreasing invariant.
|
|
338
|
+
doneAcc++;
|
|
339
|
+
progress?.(doneAcc, totalAcc, 'fetch-acc');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Pass 2 (D-03): reviewed-by-gene fallback for Unknown accessions with a gene.
|
|
343
|
+
const geneToUnknownAccs = new Map<string, string[]>();
|
|
344
|
+
for (const acc of misses) {
|
|
345
|
+
const loc = resolved.get(acc) ?? 'Unknown';
|
|
346
|
+
const gene = geneByAcc.get(acc);
|
|
347
|
+
if (loc === 'Unknown' && gene) {
|
|
348
|
+
if (!geneToUnknownAccs.has(gene)) geneToUnknownAccs.set(gene, []);
|
|
349
|
+
geneToUnknownAccs.get(gene)!.push(acc);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const genes = [...geneToUnknownAccs.keys()];
|
|
353
|
+
const geneChunks = chunk(genes, GENE_CHUNK);
|
|
354
|
+
const totalGene = geneChunks.length;
|
|
355
|
+
let doneGene = 0;
|
|
356
|
+
await runWithConcurrency(geneChunks, FETCH_CONCURRENCY, async (group) => {
|
|
357
|
+
const q = group.map((g) => `gene_exact:${g}`).join(' OR ');
|
|
358
|
+
const url = 'https://rest.uniprot.org/uniprotkb/stream' +
|
|
359
|
+
`?query=(${encodeURIComponent(q)}) AND (reviewed:true)${encodeURIComponent(taxClause)}` +
|
|
360
|
+
`&fields=${GENE_FIELDS}&format=tsv`;
|
|
361
|
+
try {
|
|
362
|
+
const resp = await grok.dapi.fetchProxy(url);
|
|
363
|
+
if (!resp.ok) {
|
|
364
|
+
console.warn(`UniProt gene fallback returned status ${resp.status}; continuing`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// Positional: accession, gene_primary, cc_subcellular_location, go_c.
|
|
368
|
+
const lines = (await resp.text()).trim().split('\n');
|
|
369
|
+
for (let li = 1; li < lines.length; li++) {
|
|
370
|
+
const parts = lines[li].split('\t');
|
|
371
|
+
const gene = (parts[1] ?? '').trim();
|
|
372
|
+
const loc = parseSubcellularLocation(parts[2] ?? '', parts[3] ?? '');
|
|
373
|
+
if (!gene || loc === 'Unknown') continue;
|
|
374
|
+
for (const acc of geneToUnknownAccs.get(gene) ?? []) {
|
|
375
|
+
resolved.set(acc, loc);
|
|
376
|
+
fetched[acc] = loc;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} catch (e: any) {
|
|
380
|
+
console.warn(`UniProt gene fallback batch failed: ${e?.message ?? e}; continuing`);
|
|
381
|
+
}
|
|
382
|
+
doneGene++;
|
|
383
|
+
progress?.(doneGene, totalGene, 'fetch-gene');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Anything still unresolved → Unknown (explicit, also cached so we don't
|
|
387
|
+
// re-query it every session).
|
|
388
|
+
for (const acc of unique) {
|
|
389
|
+
if (!resolved.has(acc)) resolved.set(acc, 'Unknown');
|
|
390
|
+
if (fetched[acc] === undefined) fetched[acc] = resolved.get(acc)!;
|
|
391
|
+
}
|
|
392
|
+
} finally {
|
|
393
|
+
clearInterval(flushTimer);
|
|
394
|
+
// Single final write-through. Schema invariant preserved on every flush —
|
|
395
|
+
// tests assert this.
|
|
396
|
+
await flushCache();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const out = new Map<string, string>();
|
|
400
|
+
for (const acc of unique) out.set(acc, resolved.get(acc) ?? 'Unknown');
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// --- Cached per-accession UniProt JSON (closes the folded cache-uniprot todo;
|
|
405
|
+
// uniprot-panel.ts delegates here so re-clicking a protein doesn't refetch). ---
|
|
406
|
+
|
|
407
|
+
const _entryCache = new Map<string, unknown>();
|
|
408
|
+
|
|
409
|
+
/** Cached single-accession UniProt JSON fetch. Mirrors the original panel
|
|
410
|
+
* discipline: grok.dapi.fetchProxy, warn + null on !resp.ok, warn + null on
|
|
411
|
+
* throw. Successful and definitive-miss (non-OK) results are cached for the
|
|
412
|
+
* session; transient throws are not. */
|
|
413
|
+
export async function fetchUniProtEntry(accession: string): Promise<unknown | null> {
|
|
414
|
+
if (_entryCache.has(accession)) return _entryCache.get(accession) ?? null;
|
|
415
|
+
const url = `https://rest.uniprot.org/uniprotkb/${encodeURIComponent(accession)}.json` +
|
|
416
|
+
'?fields=accession,protein_name,gene_names,organism_name,cc_function,go';
|
|
417
|
+
try {
|
|
418
|
+
const resp = await grok.dapi.fetchProxy(url);
|
|
419
|
+
if (!resp.ok) {
|
|
420
|
+
console.warn(`UniProt fetch for ${accession} returned status ${resp.status}`);
|
|
421
|
+
_entryCache.set(accession, null);
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
const json = await resp.json();
|
|
425
|
+
_entryCache.set(accession, json);
|
|
426
|
+
return json;
|
|
427
|
+
} catch (e: any) {
|
|
428
|
+
console.warn(`UniProt fetch for ${accession} failed:`, e?.message ?? e);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as DG from 'datagrok-api/dg';
|
|
3
|
+
|
|
4
|
+
import {_package} from '../package';
|
|
5
|
+
import {setGroups} from '../analysis/experiment-setup';
|
|
6
|
+
import {runEnrichmentPipeline} from '../analysis/enrichment';
|
|
7
|
+
import {openEnrichmentVisualization} from '../viewers/enrichment-viewers';
|
|
8
|
+
import {createVolcanoPlot} from '../viewers/volcano';
|
|
9
|
+
import {focusProtein} from '../panels/protein-focus';
|
|
10
|
+
import {SEMTYPE, DEFAULT_FC_THRESHOLD, DEFAULT_P_THRESHOLD} from '../utils/proteomics-types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Single-organism pathway-enrichment demo. Loads a small engineered HUMAN
|
|
14
|
+
* differential-expression result (Treatment vs Control) whose regulated proteins
|
|
15
|
+
* form two coherent, textbook-enriched sets — cell-cycle / mitosis UP and
|
|
16
|
+
* oxidative phosphorylation DOWN — over a diverse non-significant background, so
|
|
17
|
+
* g:Profiler returns crisp, recognizable terms. It opens the volcano, runs
|
|
18
|
+
* enrichment (GO / KEGG / Reactome / WikiPathways), and docks the enrichment
|
|
19
|
+
* charts cross-linked to the volcano: selecting a term highlights its proteins.
|
|
20
|
+
*
|
|
21
|
+
* This is the counterpart to the main `Proteomics Demo`, which uses the
|
|
22
|
+
* three-species HYE benchmark — a mix that is deliberately unsuitable for
|
|
23
|
+
* enrichment (g:Profiler assumes a single organism). The dataset ships at
|
|
24
|
+
* `files/demo/enrichment-demo.csv` (see `files/demo/README.md`).
|
|
25
|
+
*
|
|
26
|
+
* Enrichment calls g:Profiler over the network; if the service is unreachable
|
|
27
|
+
* the demo still opens the DE table + volcano and explains how to retry.
|
|
28
|
+
*/
|
|
29
|
+
export async function runEnrichmentDemo(): Promise<void> {
|
|
30
|
+
const pi = DG.TaskBarProgressIndicator.create('Loading enrichment demo…');
|
|
31
|
+
try {
|
|
32
|
+
pi.update(10, 'Loading human DE result…');
|
|
33
|
+
const text = await _package.files.readAsText('demo/enrichment-demo.csv');
|
|
34
|
+
const df = DG.DataFrame.fromCsv(text);
|
|
35
|
+
// Kept free of "Enrichment" so the derived enrichment view reads cleanly as
|
|
36
|
+
// "<source> — Enrichment" (mirrors the QC demo's "<source> — QC" naming).
|
|
37
|
+
df.name = 'Treatment vs Control (human)';
|
|
38
|
+
|
|
39
|
+
// Semantic-type the columns so the pipeline and panels find them by type,
|
|
40
|
+
// not by name — mirrors what the parsers do on a real import.
|
|
41
|
+
const setSem = (name: string, sem: string): void => {
|
|
42
|
+
const c = df.col(name);
|
|
43
|
+
if (c) c.semType = sem;
|
|
44
|
+
};
|
|
45
|
+
setSem('Protein ID', SEMTYPE.PROTEIN_ID);
|
|
46
|
+
setSem('Gene', SEMTYPE.GENE_SYMBOL);
|
|
47
|
+
setSem('log2FC', SEMTYPE.LOG2FC);
|
|
48
|
+
setSem('p-value', SEMTYPE.P_VALUE);
|
|
49
|
+
setSem('adj.p-value', SEMTYPE.P_VALUE);
|
|
50
|
+
|
|
51
|
+
// A pre-computed DE result (like a Spectronaut Candidates import): mark DE
|
|
52
|
+
// complete and record group NAMES so the volcano legend reads naturally.
|
|
53
|
+
// There are no per-sample columns, so the group column lists stay empty.
|
|
54
|
+
df.setTag('proteomics.source', 'generic');
|
|
55
|
+
df.setTag('proteomics.de_method', 'precomputed');
|
|
56
|
+
df.setTag('proteomics.de_complete', 'true');
|
|
57
|
+
setGroups(df, {group1: {name: 'Treatment', columns: []}, group2: {name: 'Control', columns: []}});
|
|
58
|
+
|
|
59
|
+
const tv = grok.shell.addTableView(df);
|
|
60
|
+
const volcano = createVolcanoPlot(df);
|
|
61
|
+
tv.dockManager.dock(volcano, DG.DOCK_TYPE.RIGHT, null, 'Volcano', 0.5);
|
|
62
|
+
|
|
63
|
+
// Headline protein (row 0 = a cell-cycle hit) selected for its UniProt panel.
|
|
64
|
+
focusProtein(df, 0);
|
|
65
|
+
|
|
66
|
+
pi.update(45, 'Running g:Profiler enrichment (GO / KEGG / Reactome)…');
|
|
67
|
+
try {
|
|
68
|
+
const {enrichmentDf, mapped, total} = await runEnrichmentPipeline(
|
|
69
|
+
df, DEFAULT_FC_THRESHOLD, DEFAULT_P_THRESHOLD, 'hsapiens',
|
|
70
|
+
['GO:BP', 'GO:MF', 'GO:CC', 'KEGG', 'REAC', 'WP'], true);
|
|
71
|
+
pi.update(90, 'Opening enrichment charts…');
|
|
72
|
+
// openEnrichmentVisualization opens the enrichment view and makes it current.
|
|
73
|
+
// Name it "<source> — Enrichment" (mirrors the DE demo's "<source> — QC" tab)
|
|
74
|
+
// and land back on the main view, so the demo-app framework's leaf-rename puts
|
|
75
|
+
// "Enrichment Analysis" on the main view — matching where the DE demo lands.
|
|
76
|
+
const enrichTv = openEnrichmentVisualization(enrichmentDf, df);
|
|
77
|
+
enrichTv.name = `${df.name} — Enrichment`;
|
|
78
|
+
grok.shell.v = tv;
|
|
79
|
+
grok.shell.info(
|
|
80
|
+
`Enrichment demo: ${enrichmentDf.rowCount} over-represented terms across ${mapped}/${total} genes. ` +
|
|
81
|
+
'Up-regulated proteins are enriched for cell-cycle / mitosis; down-regulated for oxidative ' +
|
|
82
|
+
'phosphorylation. Select a term row to highlight its proteins on the volcano.');
|
|
83
|
+
} catch (e: any) {
|
|
84
|
+
grok.shell.warning(
|
|
85
|
+
`Enrichment demo: g:Profiler enrichment could not run (${e?.message ?? e}). ` +
|
|
86
|
+
'The DE table and volcano are open — retry via Proteomics | Enrichment Analysis ' +
|
|
87
|
+
'once the service is reachable.');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pi.update(100, 'Done');
|
|
91
|
+
} finally {
|
|
92
|
+
pi.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as DG from 'datagrok-api/dg';
|
|
3
|
+
|
|
4
|
+
import {_package} from '../package';
|
|
5
|
+
import {parseSpectronautText} from '../parsers/spectronaut-parser';
|
|
6
|
+
import {getGroups} from '../analysis/experiment-setup';
|
|
7
|
+
import {imputeKnn} from '../analysis/imputation';
|
|
8
|
+
import {runDifferentialExpression} from '../analysis/differential-expression';
|
|
9
|
+
import {createVolcanoPlot} from '../viewers/volcano';
|
|
10
|
+
import {createExpressionHeatmap} from '../viewers/heatmap';
|
|
11
|
+
import {openQcDashboard} from '../viewers/qc-dashboard';
|
|
12
|
+
import {focusProtein} from '../panels/protein-focus';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The package's single "richest endpoint" demo. It takes the HYE benchmark (a
|
|
16
|
+
* Spectronaut DIA report where Human, Yeast, and E. coli proteomes are mixed at
|
|
17
|
+
* known, different ratios between two conditions — HYE mix A vs B, four
|
|
18
|
+
* replicates each) all the way through the analysis pipeline and lands on the
|
|
19
|
+
* fully-analyzed protein table with the Volcano and Expression Heatmap docked,
|
|
20
|
+
* a QC dashboard on a second tab, and the most significant protein pre-selected
|
|
21
|
+
* so its UniProt entry is already showing in the context panel.
|
|
22
|
+
*
|
|
23
|
+
* HYE is chosen deliberately: two whole species shift abundance between the
|
|
24
|
+
* conditions, so hundreds of proteins are genuinely differential and the
|
|
25
|
+
* volcano shows a strong, self-evident signal — the point of a "richest
|
|
26
|
+
* endpoint" demo. (A spike-in benchmark like CPTAC is real but deliberately
|
|
27
|
+
* subtle: at n=3 a plain t-test can't clear FDR, leaving an all-grey volcano.)
|
|
28
|
+
*
|
|
29
|
+
* Enrichment is intentionally NOT part of this demo: g:Profiler enrichment
|
|
30
|
+
* assumes a single organism, and HYE is a three-species mix, so GO/pathway
|
|
31
|
+
* enrichment would only map the human subset and mislead. Enrichment belongs in
|
|
32
|
+
* a single-organism demo.
|
|
33
|
+
*
|
|
34
|
+
* The Spectronaut parser auto-annotates the two conditions from R.Condition and
|
|
35
|
+
* tags the data preNormalized, so the demo skips straight to imputation + DE and
|
|
36
|
+
* does NOT re-normalize — re-normalizing an already-normalized export would
|
|
37
|
+
* distort the known species-ratio signal. Differential expression runs the
|
|
38
|
+
* client-side Welch t-test, so the demo needs no R environment. The only network
|
|
39
|
+
* touches are best-effort and cached: the parser's Ensembl gene-label lookup and
|
|
40
|
+
* the UniProt context panel, both of which degrade gracefully offline. The
|
|
41
|
+
* dataset ships at `files/demo/spectronaut-hye-demo.tsv` (see
|
|
42
|
+
* `files/demo/README.md`).
|
|
43
|
+
*/
|
|
44
|
+
export async function runProteomicsDemo(): Promise<void> {
|
|
45
|
+
const pi = DG.TaskBarProgressIndicator.create('Loading proteomics demo…');
|
|
46
|
+
try {
|
|
47
|
+
pi.update(5, 'Importing HYE benchmark (Spectronaut DIA)…');
|
|
48
|
+
const text = await _package.files.readAsText('demo/spectronaut-hye-demo.tsv');
|
|
49
|
+
const df = await parseSpectronautText(text);
|
|
50
|
+
df.name = 'HYE benchmark';
|
|
51
|
+
const analysisTv = grok.shell.addTableView(df);
|
|
52
|
+
|
|
53
|
+
// Groups (HYE mix A / B) are auto-populated by the parser from R.Condition.
|
|
54
|
+
const groups = getGroups(df);
|
|
55
|
+
if (!groups) {
|
|
56
|
+
grok.shell.warning('Demo: expected two auto-annotated conditions — ' +
|
|
57
|
+
'opened the table without running differential expression.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const allCols = [...groups.group1.columns, ...groups.group2.columns];
|
|
61
|
+
|
|
62
|
+
pi.update(40, 'Imputing missing values (KNN)…');
|
|
63
|
+
imputeKnn(df, allCols);
|
|
64
|
+
df.setTag('proteomics.imputed', 'true');
|
|
65
|
+
|
|
66
|
+
pi.update(60, 'Differential expression (t-test)…');
|
|
67
|
+
runDifferentialExpression(df, groups.group1.columns, groups.group2.columns,
|
|
68
|
+
groups.group1.name, groups.group2.name);
|
|
69
|
+
df.setTag('proteomics.de_method', 't-test');
|
|
70
|
+
|
|
71
|
+
pi.update(78, 'Building volcano and heatmap…');
|
|
72
|
+
const volcano = createVolcanoPlot(df);
|
|
73
|
+
analysisTv.dockManager.dock(volcano, DG.DOCK_TYPE.RIGHT, null, 'Volcano', 0.5);
|
|
74
|
+
|
|
75
|
+
const heatmap = await createExpressionHeatmap(df, {title: 'Heatmap: Top 50 DE Proteins'});
|
|
76
|
+
analysisTv.dockManager.dock(heatmap, DG.DOCK_TYPE.DOWN, null, 'Heatmap', 0.45);
|
|
77
|
+
|
|
78
|
+
// QC dashboard on a SECOND tab. openQcDashboard docks into grok.shell.tv, so
|
|
79
|
+
// open a second table view over the same DataFrame (it becomes current), build
|
|
80
|
+
// QC there, then land back on the analysis endpoint. Sharing one DataFrame
|
|
81
|
+
// across two views is fine — QC adds its own columns; the volcano keeps its own.
|
|
82
|
+
pi.update(90, 'Building QC dashboard tab…');
|
|
83
|
+
const qcTv = grok.shell.addTableView(df);
|
|
84
|
+
qcTv.name = 'HYE benchmark — QC';
|
|
85
|
+
openQcDashboard(df);
|
|
86
|
+
grok.shell.v = analysisTv;
|
|
87
|
+
|
|
88
|
+
// Pre-select the most significant protein so the UniProt context panel is
|
|
89
|
+
// already showing — a real accession from a spiked species, not the flat
|
|
90
|
+
// human background. Shared with the import handlers (which focus row 0).
|
|
91
|
+
focusProtein(df, topHitRow(df));
|
|
92
|
+
|
|
93
|
+
pi.update(100, 'Done');
|
|
94
|
+
grok.shell.info('Proteomics demo: HYE benchmark analyzed end to end ' +
|
|
95
|
+
'(import → impute → differential expression). Yeast and E. coli proteins ' +
|
|
96
|
+
'shift abundance between the two conditions, so the volcano separates the ' +
|
|
97
|
+
'genuinely differential proteins from the constant human background. ' +
|
|
98
|
+
'QC charts are on the second tab; the top hit is selected for its UniProt panel.');
|
|
99
|
+
} finally {
|
|
100
|
+
pi.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Row index of the most significant protein (smallest adj.p among the
|
|
106
|
+
* significant rows) to pre-select for the UniProt panel. Falls back to row 0
|
|
107
|
+
* when there is no significant protein or no adj.p column.
|
|
108
|
+
*/
|
|
109
|
+
function topHitRow(df: DG.DataFrame): number {
|
|
110
|
+
const adj = df.col('adj.p-value');
|
|
111
|
+
const sig = df.col('significant');
|
|
112
|
+
if (!adj) return 0;
|
|
113
|
+
|
|
114
|
+
let best = -1;
|
|
115
|
+
let bestP = Infinity;
|
|
116
|
+
for (let i = 0; i < df.rowCount; i++) {
|
|
117
|
+
if (sig && !sig.get(i)) continue;
|
|
118
|
+
if (adj.isNone(i)) continue;
|
|
119
|
+
const p = adj.get(i) as number;
|
|
120
|
+
if (p < bestP) { bestP = p; best = i; }
|
|
121
|
+
}
|
|
122
|
+
return best < 0 ? 0 : best;
|
|
123
|
+
}
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Ambient module declarations for webpack `asset/source` raw-text imports.
|
|
2
|
+
// Used by src/tests/spectronaut-parser.ts to inline the committed
|
|
3
|
+
// tools/spectronaut-aggregate.{sql,sh} as strings (Plan 12-03 R5 drift guard);
|
|
4
|
+
// the tools/ dir is not deployed under files/, so it is unreachable via
|
|
5
|
+
// _package.files at runtime — webpack asset/source is the only way to pin the
|
|
6
|
+
// committed fallback content into the test bundle.
|
|
7
|
+
declare module '*.sql' {
|
|
8
|
+
const content: string;
|
|
9
|
+
export default content;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare module '*.sh' {
|
|
13
|
+
const content: string;
|
|
14
|
+
export default content;
|
|
15
|
+
}
|