@datagrok/proteomics 1.0.1 → 1.2.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.
- 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 +87 -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 +2055 -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,247 @@
|
|
|
1
|
+
import * as DG from 'datagrok-api/dg';
|
|
2
|
+
|
|
3
|
+
import {SEMTYPE} from '../utils/proteomics-types';
|
|
4
|
+
import {findColumn, findProteomicsColumns} from '../utils/column-detection';
|
|
5
|
+
import {META_COLUMNS, PUBLISHED_TAGS, PublishedMetadata, setPublishedTags} from './publish-state';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Deep-clone + allowlist primitives that produce frozen DataFrame snapshots
|
|
9
|
+
* ready for `DG.Project.save`. Pitfall 1 (stale-snapshot leak) and Pitfall 3
|
|
10
|
+
* (tag-stripping serializer) are mitigated HERE, not in `publishAnalysis`.
|
|
11
|
+
*
|
|
12
|
+
* The orchestrator (Plan 04) never touches the source DataFrame directly — it
|
|
13
|
+
* hands the source to {@link trimForPublish} and operates on the returned
|
|
14
|
+
* clone. This is the only file in the package where `df.clone(...)` lives for
|
|
15
|
+
* publishing.
|
|
16
|
+
*
|
|
17
|
+
* Spike 15-00 confirmed that `proteomics.*` tags and `Proteomics-*` semTypes
|
|
18
|
+
* survive `DG.Project.save → find → open` on `release/1.27.3`, but does NOT
|
|
19
|
+
* confirm they survive `df.clone(...)`. The defensive re-set is no-cost either
|
|
20
|
+
* way — re-setting an already-present tag is idempotent.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
interface MetadataColumnSpec {
|
|
24
|
+
name: string;
|
|
25
|
+
value: string | number | Date | boolean | null;
|
|
26
|
+
/** 'string' | 'datetime' | 'float' | 'int' — drives `columns.addNew*` */
|
|
27
|
+
type: 'string' | 'datetime' | 'float' | 'int';
|
|
28
|
+
/** When true, write '' (string) or 0 (numeric) instead of skipping null. */
|
|
29
|
+
emptyForNull?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function addMetadataColumn(df: DG.DataFrame, spec: MetadataColumnSpec): void {
|
|
33
|
+
if (df.columns.contains(spec.name)) df.columns.remove(spec.name);
|
|
34
|
+
let col: DG.Column;
|
|
35
|
+
switch (spec.type) {
|
|
36
|
+
case 'datetime':
|
|
37
|
+
col = df.columns.addNewDateTime(spec.name);
|
|
38
|
+
break;
|
|
39
|
+
case 'float':
|
|
40
|
+
col = df.columns.addNewFloat(spec.name);
|
|
41
|
+
break;
|
|
42
|
+
case 'int':
|
|
43
|
+
col = df.columns.addNewInt(spec.name);
|
|
44
|
+
break;
|
|
45
|
+
case 'string':
|
|
46
|
+
default:
|
|
47
|
+
col = df.columns.addNewString(spec.name);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
let constant: any;
|
|
51
|
+
if (spec.value == null) {
|
|
52
|
+
if (!spec.emptyForNull) return;
|
|
53
|
+
constant = spec.type === 'string' ? '' : 0;
|
|
54
|
+
} else if (spec.value instanceof Date) {
|
|
55
|
+
constant = spec.value;
|
|
56
|
+
} else if (typeof spec.value === 'boolean') {
|
|
57
|
+
constant = spec.value ? 'true' : 'false';
|
|
58
|
+
} else if (spec.type === 'string') {
|
|
59
|
+
constant = String(spec.value);
|
|
60
|
+
} else {
|
|
61
|
+
constant = spec.value;
|
|
62
|
+
}
|
|
63
|
+
col.init(() => constant);
|
|
64
|
+
col.setTag('.hidden', 'true');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns a deep clone of `source` with only the protein-level allowlist
|
|
69
|
+
* (Protein ID, Gene, log2FC, p-value, adj.p-value, significant, [direction])
|
|
70
|
+
* + 13 belt-and-braces `_meta_*` columns + every required
|
|
71
|
+
* `proteomics.published*` tag re-set explicitly. Source DataFrame is NOT
|
|
72
|
+
* mutated. Throws on missing required columns (defensive — the Plan 07 menu
|
|
73
|
+
* handler should have already gated via `requireDifferentialExpression`).
|
|
74
|
+
*/
|
|
75
|
+
export function trimForPublish(source: DG.DataFrame, meta: PublishedMetadata): DG.DataFrame {
|
|
76
|
+
const cols = findProteomicsColumns(source);
|
|
77
|
+
const proteinId = cols.proteinId;
|
|
78
|
+
const geneName = cols.geneName;
|
|
79
|
+
const log2fc = cols.log2fc;
|
|
80
|
+
// 'adj.p-value' and 'p-value' both substring-match 'p-value'; resolve adj first
|
|
81
|
+
// and then exclude its name when looking for the raw p-value column so the
|
|
82
|
+
// allowlist doesn't end up with duplicates.
|
|
83
|
+
const adjPValue = findColumn(source, '', ['adj.p-value', 'adj.p', 'fdr', 'q-value']);
|
|
84
|
+
const pValue = source.columns.toList().find((c) => {
|
|
85
|
+
const n = c.name.toLowerCase();
|
|
86
|
+
return (n === 'p-value' || n === 'pvalue') && c.name !== (adjPValue?.name ?? '');
|
|
87
|
+
}) ?? null;
|
|
88
|
+
const significant = findColumn(source, '', ['significant', 'sig']);
|
|
89
|
+
const direction = findColumn(source, '', ['direction', 'regulation', 'up_down']);
|
|
90
|
+
|
|
91
|
+
const required: Array<[string, DG.Column | null]> = [
|
|
92
|
+
['Protein ID', proteinId],
|
|
93
|
+
['log2FC', log2fc],
|
|
94
|
+
['p-value', pValue],
|
|
95
|
+
['adj.p-value', adjPValue],
|
|
96
|
+
['significant', significant],
|
|
97
|
+
];
|
|
98
|
+
for (const [label, col] of required) {
|
|
99
|
+
if (col == null)
|
|
100
|
+
throw new Error(`Cannot publish: missing required column [${label}]. Run Differential Expression to completion before publishing.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const allowlist: string[] = Array.from(new Set([
|
|
104
|
+
proteinId!.name,
|
|
105
|
+
geneName?.name,
|
|
106
|
+
log2fc!.name,
|
|
107
|
+
pValue!.name,
|
|
108
|
+
adjPValue!.name,
|
|
109
|
+
significant!.name,
|
|
110
|
+
direction?.name,
|
|
111
|
+
].filter((n): n is string => typeof n === 'string' && n.length > 0)));
|
|
112
|
+
|
|
113
|
+
const frozen = source.clone(null, allowlist);
|
|
114
|
+
|
|
115
|
+
const carryForwardTags = [
|
|
116
|
+
'proteomics.source',
|
|
117
|
+
'proteomics.de_method',
|
|
118
|
+
'proteomics.groups',
|
|
119
|
+
'proteomics.de_complete',
|
|
120
|
+
];
|
|
121
|
+
for (const k of carryForwardTags) {
|
|
122
|
+
const v = source.getTag(k);
|
|
123
|
+
if (v != null && v !== '') frozen.setTag(k, v);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setPublishedTags(frozen, meta);
|
|
127
|
+
|
|
128
|
+
const dateSlice = meta.publishedAt instanceof Date
|
|
129
|
+
? meta.publishedAt.toISOString().slice(0, 10)
|
|
130
|
+
: new Date(String(meta.publishedAt)).toISOString().slice(0, 10);
|
|
131
|
+
frozen.name = `${source.name || 'analysis'}_published_${dateSlice}`;
|
|
132
|
+
|
|
133
|
+
const semTypeAssignments: Array<[DG.Column | null, string]> = [
|
|
134
|
+
[proteinId, SEMTYPE.PROTEIN_ID],
|
|
135
|
+
[geneName, SEMTYPE.GENE_SYMBOL],
|
|
136
|
+
[log2fc, SEMTYPE.LOG2FC],
|
|
137
|
+
[pValue, SEMTYPE.P_VALUE],
|
|
138
|
+
];
|
|
139
|
+
for (const [src, sem] of semTypeAssignments) {
|
|
140
|
+
if (src == null) continue;
|
|
141
|
+
const c = frozen.col(src.name);
|
|
142
|
+
if (c != null) c.semType = sem;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const includesEnrichmentStr = meta.includesEnrichment ? 'true' : 'false';
|
|
146
|
+
// Numeric metadata (FC / p threshold, version) stored as STRINGS to avoid
|
|
147
|
+
// Float32 precision loss in `addNewFloat` (which is a single-precision
|
|
148
|
+
// column type). The threshold values are exact doubles in the source meta;
|
|
149
|
+
// string storage preserves them exactly, and `getPublishedMetadata` parses
|
|
150
|
+
// them back via parseFloat / parseInt.
|
|
151
|
+
const specs: MetadataColumnSpec[] = [
|
|
152
|
+
{name: META_COLUMNS.PUBLISHED, value: 'true', type: 'string'},
|
|
153
|
+
{name: META_COLUMNS.PUBLISHED_AT, value: meta.publishedAt, type: 'datetime'},
|
|
154
|
+
{name: META_COLUMNS.PUBLISHED_BY, value: meta.publishedBy, type: 'string'},
|
|
155
|
+
{name: META_COLUMNS.PUBLISHED_BY_EMAIL, value: meta.publishedByEmail ?? '', type: 'string', emptyForNull: true},
|
|
156
|
+
{name: META_COLUMNS.PUBLISHED_TARGET, value: meta.target, type: 'string'},
|
|
157
|
+
{name: META_COLUMNS.PUBLISHED_DE_METHOD, value: meta.deMethod, type: 'string'},
|
|
158
|
+
{name: META_COLUMNS.PUBLISHED_FC_THRESHOLD, value: String(meta.fcThreshold), type: 'string'},
|
|
159
|
+
{name: META_COLUMNS.PUBLISHED_P_THRESHOLD, value: String(meta.pThreshold), type: 'string'},
|
|
160
|
+
{name: META_COLUMNS.PUBLISHED_VERSION, value: String(meta.version), type: 'string'},
|
|
161
|
+
{name: META_COLUMNS.PUBLISHED_ID, value: meta.publishId, type: 'string'},
|
|
162
|
+
{name: META_COLUMNS.PUBLISHED_INCLUDES_ENRICHMENT, value: includesEnrichmentStr, type: 'string'},
|
|
163
|
+
{name: META_COLUMNS.SUPERSEDES, value: meta.supersedes ?? '', type: 'string', emptyForNull: true},
|
|
164
|
+
{name: META_COLUMNS.SUPERSEDED_BY, value: meta.supersededBy ?? '', type: 'string', emptyForNull: true},
|
|
165
|
+
];
|
|
166
|
+
for (const spec of specs)
|
|
167
|
+
addMetadataColumn(frozen, spec);
|
|
168
|
+
|
|
169
|
+
return frozen;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Returns a deep clone of an enrichment DataFrame containing only the term /
|
|
174
|
+
* source / p / adj.p / intersection allowlist (+ optional direction column
|
|
175
|
+
* from the Phase 13 split). Called by the Plan 04 orchestrator opportunistically
|
|
176
|
+
* (D-05): only when the source protein DataFrame carries
|
|
177
|
+
* `proteomics.enrichment === 'true'` AND an enrichment DataFrame is present
|
|
178
|
+
* in `grok.shell.tables`. If neither precondition holds, the orchestrator
|
|
179
|
+
* skips this call entirely and the published Project ships protein-only.
|
|
180
|
+
*
|
|
181
|
+
* Throws on missing required columns. Direction column is soft: omitted from
|
|
182
|
+
* the allowlist when absent rather than failing.
|
|
183
|
+
*/
|
|
184
|
+
export function trimEnrichmentForPublish(enrichSource: DG.DataFrame, meta: PublishedMetadata): DG.DataFrame {
|
|
185
|
+
const term = findColumn(enrichSource, '', ['term name', 'term', 'pathway']);
|
|
186
|
+
const sourceCol = findColumn(enrichSource, '', ['source']);
|
|
187
|
+
// The g:Profiler producer (buildEnrichmentDf, enrichment.ts) emits ONE
|
|
188
|
+
// FDR-corrected significance column named 'FDR' — there is no separate raw
|
|
189
|
+
// p-value. Accept any single significance column (FDR / adj.p-value /
|
|
190
|
+
// p-value) rather than requiring a p-value + adj.p-value pair that real
|
|
191
|
+
// enrichment tables never carry.
|
|
192
|
+
const sigCol = findColumn(enrichSource, '', ['fdr', 'adj.p-value', 'adj.p', 'q-value', 'p-value', 'pvalue']);
|
|
193
|
+
const intersection = findColumn(enrichSource, '', ['intersection', 'genes']);
|
|
194
|
+
const directionCol = findColumn(enrichSource, '', ['direction', 'regulation']);
|
|
195
|
+
// Soft (omitted when absent): the published dot plot needs these to render
|
|
196
|
+
// (x = Gene Ratio, size = Gene Count). Older enrichment frames may lack them.
|
|
197
|
+
const geneRatioCol = findColumn(enrichSource, '', ['gene ratio']);
|
|
198
|
+
const geneCountCol = findColumn(enrichSource, '', ['gene count']);
|
|
199
|
+
|
|
200
|
+
const required: Array<[string, DG.Column | null]> = [
|
|
201
|
+
['Term Name', term],
|
|
202
|
+
['Source', sourceCol],
|
|
203
|
+
['significance (FDR / adj.p-value)', sigCol],
|
|
204
|
+
['Intersection', intersection],
|
|
205
|
+
];
|
|
206
|
+
for (const [label, col] of required) {
|
|
207
|
+
if (col == null)
|
|
208
|
+
throw new Error(`Cannot publish enrichment: missing required column [${label}]. Re-run enrichment before publishing.`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const allowlist: string[] = [
|
|
212
|
+
term!.name,
|
|
213
|
+
sourceCol!.name,
|
|
214
|
+
sigCol!.name,
|
|
215
|
+
intersection!.name,
|
|
216
|
+
directionCol?.name,
|
|
217
|
+
geneRatioCol?.name,
|
|
218
|
+
geneCountCol?.name,
|
|
219
|
+
].filter((n): n is string => typeof n === 'string' && n.length > 0);
|
|
220
|
+
|
|
221
|
+
const enrichClone = enrichSource.clone(null, allowlist);
|
|
222
|
+
enrichClone.setTag('proteomics.enrichment', 'true');
|
|
223
|
+
|
|
224
|
+
const dateSlice = meta.publishedAt instanceof Date
|
|
225
|
+
? meta.publishedAt.toISOString().slice(0, 10)
|
|
226
|
+
: new Date(String(meta.publishedAt)).toISOString().slice(0, 10);
|
|
227
|
+
enrichClone.name = `enrichment_published_${dateSlice}`;
|
|
228
|
+
|
|
229
|
+
addMetadataColumn(enrichClone, {
|
|
230
|
+
name: META_COLUMNS.PUBLISHED_ID,
|
|
231
|
+
value: meta.publishId,
|
|
232
|
+
type: 'string',
|
|
233
|
+
});
|
|
234
|
+
addMetadataColumn(enrichClone, {
|
|
235
|
+
name: META_COLUMNS.PUBLISHED_INCLUDES_ENRICHMENT,
|
|
236
|
+
value: 'true',
|
|
237
|
+
type: 'string',
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Belt-and-braces tag so a reviewer reopening the enrichment DataFrame
|
|
241
|
+
// alone can recover its provenance.
|
|
242
|
+
enrichClone.setTag(PUBLISHED_TAGS.PUBLISHED, 'true');
|
|
243
|
+
enrichClone.setTag(PUBLISHED_TAGS.PUBLISHED_ID, meta.publishId);
|
|
244
|
+
enrichClone.setTag(PUBLISHED_TAGS.PUBLISHED_INCLUDES_ENRICHMENT, 'true');
|
|
245
|
+
|
|
246
|
+
return enrichClone;
|
|
247
|
+
}
|