@datagrok/proteomics 1.0.0 → 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.
Files changed (116) hide show
  1. package/CHANGELOG.md +52 -3
  2. package/CLAUDE.md +178 -0
  3. package/README.md +195 -3
  4. package/detectors.js +152 -9
  5. package/dist/package-test.js +1 -1744
  6. package/dist/package-test.js.map +1 -1
  7. package/dist/package.js +1 -146
  8. package/dist/package.js.map +1 -1
  9. package/docs/personas-and-capabilities.md +165 -0
  10. package/files/demo/README.md +264 -0
  11. package/files/demo/cptac-spike-in.txt +1571 -0
  12. package/files/demo/enrichment-demo.csv +120 -0
  13. package/files/demo/fragpipe-smoke-test.tsv +11 -0
  14. package/files/demo/proteinGroups.txt +28 -0
  15. package/files/demo/spectronaut-hye-candidates.tsv +94 -0
  16. package/files/demo/spectronaut-hye-demo.tsv +8761 -0
  17. package/files/demo/spectronaut-hye-mix.tsv +8761 -0
  18. package/files/demo/spectronaut-hye-precursor-golden.json +938 -0
  19. package/files/demo/spectronaut-hye-precursor-golden.tsv +235 -0
  20. package/files/demo/spectronaut-hye-precursor.tsv +493 -0
  21. package/images/enrichment-crosslink.png +0 -0
  22. package/images/enrichment-term-selected.png +0 -0
  23. package/images/hero.png +0 -0
  24. package/images/pipeline.svg +80 -0
  25. package/package.json +87 -62
  26. package/scripts/deqms_de.R +60 -0
  27. package/scripts/limma_de.R +42 -0
  28. package/scripts/vsn_normalize.R +19 -0
  29. package/src/analysis/differential-expression.ts +450 -0
  30. package/src/analysis/enrichment-export.ts +101 -0
  31. package/src/analysis/enrichment.ts +602 -0
  32. package/src/analysis/experiment-setup.ts +199 -0
  33. package/src/analysis/imputation.ts +407 -0
  34. package/src/analysis/log2-scale.ts +139 -0
  35. package/src/analysis/normalization.ts +255 -0
  36. package/src/analysis/pca.ts +254 -0
  37. package/src/analysis/spc-storage.ts +515 -0
  38. package/src/analysis/spc.ts +544 -0
  39. package/src/analysis/subcellular-location.ts +431 -0
  40. package/src/demo/enrichment-demo.ts +94 -0
  41. package/src/demo/proteomics-demo.ts +123 -0
  42. package/src/global.d.ts +15 -0
  43. package/src/menu.ts +133 -0
  44. package/src/package-api.ts +136 -14
  45. package/src/package-test.ts +45 -20
  46. package/src/package.g.ts +161 -0
  47. package/src/package.ts +1029 -17
  48. package/src/panels/protein-focus.ts +63 -0
  49. package/src/panels/published-analysis-panel.ts +151 -0
  50. package/src/panels/uniprot-panel.ts +349 -0
  51. package/src/parsers/fragpipe-parser.ts +200 -0
  52. package/src/parsers/generic-parser.ts +197 -0
  53. package/src/parsers/maxquant-parser.ts +162 -0
  54. package/src/parsers/shared-utils.ts +163 -0
  55. package/src/parsers/spectronaut-candidates-parser.ts +307 -0
  56. package/src/parsers/spectronaut-parser.ts +604 -0
  57. package/src/publishing/assert-published-shape.ts +260 -0
  58. package/src/publishing/post-open-recovery.ts +104 -0
  59. package/src/publishing/publish-project.ts +515 -0
  60. package/src/publishing/publish-settings.ts +59 -0
  61. package/src/publishing/publish-state.ts +316 -0
  62. package/src/publishing/share-dialog.ts +171 -0
  63. package/src/publishing/trim-dataframe.ts +247 -0
  64. package/src/tests/analysis.ts +658 -0
  65. package/src/tests/enrichment-export.ts +61 -0
  66. package/src/tests/enrichment-visualization.ts +340 -0
  67. package/src/tests/enrichment.ts +224 -0
  68. package/src/tests/fragpipe-e2e.ts +74 -0
  69. package/src/tests/fragpipe-parser.ts +147 -0
  70. package/src/tests/gene-label-resolver.ts +387 -0
  71. package/src/tests/generic-parser.ts +152 -0
  72. package/src/tests/group-mean-correlation.ts +139 -0
  73. package/src/tests/log2-scale.ts +93 -0
  74. package/src/tests/organisms.ts +56 -0
  75. package/src/tests/parsers.ts +182 -0
  76. package/src/tests/publish-roundtrip.ts +584 -0
  77. package/src/tests/publish-spike.ts +377 -0
  78. package/src/tests/qc-dashboard.ts +210 -0
  79. package/src/tests/smart-pathway-filter.ts +193 -0
  80. package/src/tests/spc-formula-lines-spike.ts +129 -0
  81. package/src/tests/spc.ts +640 -0
  82. package/src/tests/spectronaut-candidates-e2e.ts +140 -0
  83. package/src/tests/spectronaut-candidates-parser.ts +398 -0
  84. package/src/tests/spectronaut-parser.ts +668 -0
  85. package/src/tests/subcellular-location.ts +361 -0
  86. package/src/tests/uniprot-panel.ts +202 -0
  87. package/src/tests/volcano.ts +603 -0
  88. package/src/utils/column-detection.ts +28 -0
  89. package/src/utils/gene-label-resolver.ts +443 -0
  90. package/src/utils/organisms.ts +82 -0
  91. package/src/utils/proteomics-types.ts +30 -0
  92. package/src/viewers/enrichment-viewers.ts +274 -0
  93. package/src/viewers/group-mean-correlation.ts +218 -0
  94. package/src/viewers/heatmap.ts +168 -0
  95. package/src/viewers/pca-plot.ts +169 -0
  96. package/src/viewers/qc-computations.ts +266 -0
  97. package/src/viewers/qc-dashboard.ts +176 -0
  98. package/src/viewers/spc-dashboard.ts +755 -0
  99. package/src/viewers/volcano.ts +690 -0
  100. package/test-console-output-1.log +2055 -0
  101. package/test-record-1.mp4 +0 -0
  102. package/tools/derive-precursor-golden-sidecar.mjs +81 -0
  103. package/tools/generate-enrichment-fixture.sh +160 -0
  104. package/tools/generate-spectronaut-candidates-fixture.mjs +212 -0
  105. package/tools/generate-spectronaut-precursor-fixture.mjs +128 -0
  106. package/tools/spectronaut-aggregate.sh +46 -0
  107. package/tools/spectronaut-aggregate.sql +80 -0
  108. package/tsconfig.json +18 -71
  109. package/webpack.config.js +86 -45
  110. package/.eslintignore +0 -1
  111. package/.eslintrc.json +0 -56
  112. package/LICENSE +0 -674
  113. package/package.png +0 -0
  114. package/scripts/number_antibody.py +0 -190
  115. package/scripts/number_antibody_abnumber.py +0 -177
  116. 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
+ }