@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.
- 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 -62
- 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,450 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as ui from 'datagrok-api/ui';
|
|
3
|
+
import * as DG from 'datagrok-api/dg';
|
|
4
|
+
import {tTest} from '@datagrok-libraries/statistics/src/tests';
|
|
5
|
+
import {fdrcorrection} from '@datagrok-libraries/statistics/src/multiple-tests';
|
|
6
|
+
import {SEMTYPE, DEFAULT_FC_THRESHOLD, DEFAULT_P_THRESHOLD} from '../utils/proteomics-types';
|
|
7
|
+
import {getGroups} from './experiment-setup';
|
|
8
|
+
|
|
9
|
+
/** Guard for menu/dialog handlers that need DE results. Returns true if DE has been
|
|
10
|
+
* run on `df`; otherwise warns the user via `grok.shell.warning` and returns false.
|
|
11
|
+
* Each caller passes its own warning text so existing user-visible copy is preserved.
|
|
12
|
+
* Use as: `if (!requireDifferentialExpression(df, '...')) return;` */
|
|
13
|
+
export function requireDifferentialExpression(df: DG.DataFrame, message: string): boolean {
|
|
14
|
+
if (df.getTag('proteomics.de_complete') === 'true')
|
|
15
|
+
return true;
|
|
16
|
+
grok.shell.warning(message);
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Extract non-null float values across named columns for a single row. */
|
|
21
|
+
function getGroupValues(df: DG.DataFrame, colNames: string[], rowIdx: number): number[] {
|
|
22
|
+
const values: number[] = [];
|
|
23
|
+
for (const name of colNames) {
|
|
24
|
+
const col = df.columns.byName(name);
|
|
25
|
+
if (col && !col.isNone(rowIdx))
|
|
26
|
+
values.push(col.get(rowIdx));
|
|
27
|
+
}
|
|
28
|
+
return values;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Compute arithmetic mean of a number array. */
|
|
32
|
+
function mean(vals: number[]): number {
|
|
33
|
+
let sum = 0;
|
|
34
|
+
for (let i = 0; i < vals.length; i++)
|
|
35
|
+
sum += vals[i];
|
|
36
|
+
return sum / vals.length;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Run client-side Welch's t-test differential expression with BH FDR correction.
|
|
40
|
+
* Compares group2 (treatment) vs group1 (control). Adds log2FC, p-value, adj.p-value,
|
|
41
|
+
* and significant columns to the DataFrame in-place. */
|
|
42
|
+
export function runDifferentialExpression(
|
|
43
|
+
df: DG.DataFrame,
|
|
44
|
+
group1Cols: string[],
|
|
45
|
+
group2Cols: string[],
|
|
46
|
+
group1Name: string,
|
|
47
|
+
group2Name: string,
|
|
48
|
+
fcThreshold: number = DEFAULT_FC_THRESHOLD,
|
|
49
|
+
pThreshold: number = DEFAULT_P_THRESHOLD,
|
|
50
|
+
): {tested: number; untestable: number} {
|
|
51
|
+
// Compute per-row stats into typed arrays first, then bulk-init the columns.
|
|
52
|
+
// Cheaper than column.set() per row, which hops the JS/native bridge each cell.
|
|
53
|
+
const nRows = df.rowCount;
|
|
54
|
+
const fcArr = new Float32Array(nRows);
|
|
55
|
+
const pArr = new Float32Array(nRows);
|
|
56
|
+
const adjArr = new Float32Array(nRows);
|
|
57
|
+
fcArr.fill(DG.FLOAT_NULL);
|
|
58
|
+
pArr.fill(DG.FLOAT_NULL);
|
|
59
|
+
adjArr.fill(DG.FLOAT_NULL);
|
|
60
|
+
|
|
61
|
+
const testableIndices: number[] = [];
|
|
62
|
+
const rawPValues: number[] = [];
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < nRows; i++) {
|
|
65
|
+
const vals1 = getGroupValues(df, group1Cols, i);
|
|
66
|
+
const vals2 = getGroupValues(df, group2Cols, i);
|
|
67
|
+
if (vals1.length < 2 || vals2.length < 2) continue;
|
|
68
|
+
|
|
69
|
+
fcArr[i] = mean(vals2) - mean(vals1);
|
|
70
|
+
const pVal = tTest(vals1, vals2)['p-value'];
|
|
71
|
+
pArr[i] = pVal;
|
|
72
|
+
testableIndices.push(i);
|
|
73
|
+
rawPValues.push(pVal);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// BH FDR correction on testable proteins only
|
|
77
|
+
if (rawPValues.length > 0) {
|
|
78
|
+
// alpha is passed for intent/consistency with the user's cutoff; it only
|
|
79
|
+
// affects the (discarded) reject array — BH-adjusted p-values are
|
|
80
|
+
// alpha-independent, so this does not change `corrected`.
|
|
81
|
+
const [, corrected] = fdrcorrection(new Float32Array(rawPValues), pThreshold, 'i');
|
|
82
|
+
for (let j = 0; j < testableIndices.length; j++)
|
|
83
|
+
adjArr[testableIndices[j]] = corrected[j];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const log2fcCol = df.columns.addNewFloat('log2FC');
|
|
87
|
+
const pValCol = df.columns.addNewFloat('p-value');
|
|
88
|
+
const adjPValCol = df.columns.addNewFloat('adj.p-value');
|
|
89
|
+
log2fcCol.init((i) => fcArr[i]);
|
|
90
|
+
pValCol.init((i) => pArr[i]);
|
|
91
|
+
adjPValCol.init((i) => adjArr[i]);
|
|
92
|
+
|
|
93
|
+
const sigCol = df.columns.addNewBool('significant');
|
|
94
|
+
sigCol.init((i) => {
|
|
95
|
+
const adjP = adjArr[i];
|
|
96
|
+
const fc = fcArr[i];
|
|
97
|
+
if (adjP === DG.FLOAT_NULL || fc === DG.FLOAT_NULL) return false;
|
|
98
|
+
return Math.abs(fc) >= fcThreshold && adjP <= pThreshold;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Assign semantic types
|
|
102
|
+
log2fcCol.semType = SEMTYPE.LOG2FC;
|
|
103
|
+
pValCol.semType = SEMTYPE.P_VALUE;
|
|
104
|
+
adjPValCol.semType = SEMTYPE.P_VALUE;
|
|
105
|
+
|
|
106
|
+
// Mark DE as complete
|
|
107
|
+
df.setTag('proteomics.de_complete', 'true');
|
|
108
|
+
df.fireValuesChanged();
|
|
109
|
+
|
|
110
|
+
const tested = testableIndices.length;
|
|
111
|
+
const untestable = df.rowCount - tested;
|
|
112
|
+
return {tested, untestable};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Build a clean expression dataframe with simple column names (s1, s2, ...)
|
|
116
|
+
* to avoid encoding issues with special characters in column names.
|
|
117
|
+
* Bulk-copies via getRawData/fromFloat32Array — per-row set() crosses the JS/native
|
|
118
|
+
* bridge once per cell and stalls the main thread on real-size frames (see
|
|
119
|
+
* feedback_dg_column_bulk_init memory note). */
|
|
120
|
+
function buildExpressionDf(
|
|
121
|
+
df: DG.DataFrame, group1Cols: string[], group2Cols: string[],
|
|
122
|
+
): DG.DataFrame {
|
|
123
|
+
const allCols = [...group1Cols, ...group2Cols];
|
|
124
|
+
const cols: DG.Column[] = [];
|
|
125
|
+
for (let i = 0; i < allCols.length; i++) {
|
|
126
|
+
const src = df.columns.byName(allCols[i]);
|
|
127
|
+
// getRawData returns the underlying Float32/Float64Array; nulls are already
|
|
128
|
+
// encoded as DG.FLOAT_NULL, so a straight copy preserves missingness.
|
|
129
|
+
const raw = src.getRawData();
|
|
130
|
+
const buf = new Float32Array(raw.length);
|
|
131
|
+
buf.set(raw);
|
|
132
|
+
cols.push(DG.Column.fromFloat32Array(`s${i + 1}`, buf));
|
|
133
|
+
}
|
|
134
|
+
return DG.DataFrame.fromColumns(cols);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Copy log2FC / p.value / adj.p.value / significant columns from an R-script
|
|
138
|
+
* result frame into `df` as new columns, returning the count of TRUE
|
|
139
|
+
* significant rows.
|
|
140
|
+
*
|
|
141
|
+
* Alignment is by the result's `row` column (1-based input index emitted by
|
|
142
|
+
* the R scripts), NOT by row position. This is load-bearing: DEqMS's
|
|
143
|
+
* `outputResult()` returns rows sorted by significance, so a positional copy
|
|
144
|
+
* silently assigns every protein another protein's statistics. When `row` is
|
|
145
|
+
* absent (defensive fallback for any R output that doesn't emit it) the copy
|
|
146
|
+
* degrades to positional.
|
|
147
|
+
*
|
|
148
|
+
* R serialization sometimes flattens booleans to 0/1 or "TRUE"/"FALSE"
|
|
149
|
+
* strings — the truthiness check covers all three forms. */
|
|
150
|
+
export function copyDEResultsToFrame(df: DG.DataFrame, result: DG.DataFrame): number {
|
|
151
|
+
const nRows = df.rowCount;
|
|
152
|
+
const m = result.rowCount;
|
|
153
|
+
|
|
154
|
+
const fcRaw = result.columns.byName('log2FC').getRawData() as Float32Array | Float64Array;
|
|
155
|
+
const pRaw = result.columns.byName('p.value').getRawData() as Float32Array | Float64Array;
|
|
156
|
+
const aRaw = result.columns.byName('adj.p.value').getRawData() as Float32Array | Float64Array;
|
|
157
|
+
const rSig = result.columns.byName('significant');
|
|
158
|
+
|
|
159
|
+
// result row j -> df row index. With the `row` key, realign by input index
|
|
160
|
+
// (1-based); without it, identity (positional) as a defensive fallback.
|
|
161
|
+
const rowCol = result.col('row');
|
|
162
|
+
const target = new Int32Array(m);
|
|
163
|
+
if (rowCol) {
|
|
164
|
+
const rowRaw = rowCol.getRawData();
|
|
165
|
+
for (let j = 0; j < m; j++) target[j] = (rowRaw[j] | 0) - 1;
|
|
166
|
+
} else {
|
|
167
|
+
for (let j = 0; j < m; j++) target[j] = j;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Pre-fill with FLOAT_NULL so any df row the result doesn't cover stays null
|
|
171
|
+
// rather than a spurious 0 (positional copy used to assume full coverage).
|
|
172
|
+
const log2fcBuf = new Float32Array(nRows); log2fcBuf.fill(DG.FLOAT_NULL);
|
|
173
|
+
const pBuf = new Float32Array(nRows); pBuf.fill(DG.FLOAT_NULL);
|
|
174
|
+
const aBuf = new Float32Array(nRows); aBuf.fill(DG.FLOAT_NULL);
|
|
175
|
+
const sigArr = new Uint8Array(nRows);
|
|
176
|
+
let sigCount = 0;
|
|
177
|
+
|
|
178
|
+
for (let j = 0; j < m; j++) {
|
|
179
|
+
const t = target[j];
|
|
180
|
+
if (t < 0 || t >= nRows) continue;
|
|
181
|
+
log2fcBuf[t] = fcRaw[j];
|
|
182
|
+
pBuf[t] = pRaw[j];
|
|
183
|
+
aBuf[t] = aRaw[j];
|
|
184
|
+
const v = rSig.get(j);
|
|
185
|
+
if (v === true || v === 1 || v === '1' || v === 'TRUE') {
|
|
186
|
+
sigArr[t] = 1;
|
|
187
|
+
sigCount++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const log2fcCol = DG.Column.fromFloat32Array('log2FC', log2fcBuf);
|
|
192
|
+
const pValCol = DG.Column.fromFloat32Array('p-value', pBuf);
|
|
193
|
+
const adjPValCol = DG.Column.fromFloat32Array('adj.p-value', aBuf);
|
|
194
|
+
const sigCol = df.columns.addNewBool('significant');
|
|
195
|
+
sigCol.init((i) => sigArr[i] === 1);
|
|
196
|
+
|
|
197
|
+
log2fcCol.semType = SEMTYPE.LOG2FC;
|
|
198
|
+
pValCol.semType = SEMTYPE.P_VALUE;
|
|
199
|
+
adjPValCol.semType = SEMTYPE.P_VALUE;
|
|
200
|
+
|
|
201
|
+
df.columns.add(log2fcCol);
|
|
202
|
+
df.columns.add(pValCol);
|
|
203
|
+
df.columns.add(adjPValCol);
|
|
204
|
+
|
|
205
|
+
return sigCount;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Run limma moderated t-test via server-side R script.
|
|
209
|
+
* Returns the number of significant proteins found. */
|
|
210
|
+
async function runLimmaDE(
|
|
211
|
+
df: DG.DataFrame,
|
|
212
|
+
group1Cols: string[],
|
|
213
|
+
group2Cols: string[],
|
|
214
|
+
fcThreshold: number,
|
|
215
|
+
pThreshold: number,
|
|
216
|
+
): Promise<number> {
|
|
217
|
+
const exprDf = buildExpressionDf(df, group1Cols, group2Cols);
|
|
218
|
+
|
|
219
|
+
const result: DG.DataFrame = await grok.functions.call('Proteomics:LimmaDE', {
|
|
220
|
+
exprDf: exprDf,
|
|
221
|
+
nGroup1: group1Cols.length,
|
|
222
|
+
fcThreshold: fcThreshold,
|
|
223
|
+
pThreshold: pThreshold,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const sigCount = copyDEResultsToFrame(df, result);
|
|
227
|
+
|
|
228
|
+
df.setTag('proteomics.de_complete', 'true');
|
|
229
|
+
df.fireValuesChanged();
|
|
230
|
+
|
|
231
|
+
return sigCount;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Run DEqMS peptide-count-weighted differential expression via server-side R script.
|
|
235
|
+
* Returns the number of significant proteins found. */
|
|
236
|
+
async function runDeqmsDE(
|
|
237
|
+
df: DG.DataFrame,
|
|
238
|
+
group1Cols: string[],
|
|
239
|
+
group2Cols: string[],
|
|
240
|
+
peptideCountCol: DG.Column,
|
|
241
|
+
fcThreshold: number,
|
|
242
|
+
pThreshold: number,
|
|
243
|
+
): Promise<number> {
|
|
244
|
+
const exprDf = buildExpressionDf(df, group1Cols, group2Cols);
|
|
245
|
+
|
|
246
|
+
// Build single-column peptide count DataFrame
|
|
247
|
+
const peptideDf = DG.DataFrame.create(df.rowCount);
|
|
248
|
+
const countCol = peptideDf.columns.addNewInt('count');
|
|
249
|
+
for (let r = 0; r < df.rowCount; r++)
|
|
250
|
+
countCol.set(r, peptideCountCol.isNone(r) ? 1 : Math.round(peptideCountCol.get(r) as number));
|
|
251
|
+
|
|
252
|
+
const result: DG.DataFrame = await grok.functions.call('Proteomics:DeqmsDE', {
|
|
253
|
+
exprDf: exprDf,
|
|
254
|
+
nGroup1: group1Cols.length,
|
|
255
|
+
peptideDf: peptideDf,
|
|
256
|
+
fcThreshold: fcThreshold,
|
|
257
|
+
pThreshold: pThreshold,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const sigCount = copyDEResultsToFrame(df, result);
|
|
261
|
+
|
|
262
|
+
df.setTag('proteomics.de_complete', 'true');
|
|
263
|
+
df.setTag('proteomics.de_method', 'deqms');
|
|
264
|
+
df.fireValuesChanged();
|
|
265
|
+
|
|
266
|
+
return sigCount;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* R3/D-09: the DE dialog's default Comparison. Returns the declared/intended
|
|
271
|
+
* contrast `${g1} vs ${g2}` — group1 is the parser's first condition, which is
|
|
272
|
+
* Spectronaut's declared Numerator (13-WAVE0-FINDINGS.md A3). The OK-handler's
|
|
273
|
+
* `value === pairs[1]` index logic maps this string to numerator = group1.
|
|
274
|
+
* Previously the default was the alphabetical `pairs[0]` (`${g2} vs ${g1}`),
|
|
275
|
+
* the reverse of the declared contrast — the documented mirror defect
|
|
276
|
+
* (memory project_proteomics_spectronaut_de_direction_default). The dropdown
|
|
277
|
+
* still offers both orientations; only this default changed (direction-only).
|
|
278
|
+
*/
|
|
279
|
+
export function getDefaultComparison(g1Name: string, g2Name: string): string {
|
|
280
|
+
return `${g1Name} vs ${g2Name}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Show differential expression dialog with prerequisite checks.
|
|
284
|
+
* @param onComplete Optional callback invoked after DE completes successfully. */
|
|
285
|
+
export function showDEDialog(df: DG.DataFrame, onComplete?: () => void): void {
|
|
286
|
+
const groups = getGroups(df);
|
|
287
|
+
if (!groups) {
|
|
288
|
+
grok.shell.warning('Please annotate experimental groups first (Proteomics | Annotate Experiment)');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (df.getTag('proteomics.de_complete') === 'true') {
|
|
293
|
+
grok.shell.warning('Differential expression already performed');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const g1 = groups.group1;
|
|
298
|
+
const g2 = groups.group2;
|
|
299
|
+
|
|
300
|
+
const infoText = ui.divText(
|
|
301
|
+
`Group 1: ${g1.name} (${g1.columns.length} samples), ` +
|
|
302
|
+
`Group 2: ${g2.name} (${g2.columns.length} samples)`,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Comparison direction picker
|
|
306
|
+
const pairs = [`${g2.name} vs ${g1.name}`, `${g1.name} vs ${g2.name}`];
|
|
307
|
+
const comparisonInput = ui.input.choice('Comparison', {
|
|
308
|
+
value: getDefaultComparison(g1.name, g2.name), // D-09: declared contrast, not pairs[0]
|
|
309
|
+
items: pairs,
|
|
310
|
+
nullable: false,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Dynamic hint text for FC interpretation. Derive direction from pairs index
|
|
314
|
+
// (not string-splitting) so group names containing " vs " stay intact.
|
|
315
|
+
const hintDiv = ui.div();
|
|
316
|
+
hintDiv.style.cssText = 'font-style:italic; color:#888; font-size:12px; margin-bottom:8px;';
|
|
317
|
+
const updateHint = () => {
|
|
318
|
+
const isReversed = comparisonInput.value === pairs[1];
|
|
319
|
+
const numerator = isReversed ? g1.name : g2.name;
|
|
320
|
+
const denominator = isReversed ? g2.name : g1.name;
|
|
321
|
+
hintDiv.textContent = `Positive log2FC = higher in ${numerator}, Negative log2FC = higher in ${denominator}`;
|
|
322
|
+
};
|
|
323
|
+
comparisonInput.onChanged.subscribe(updateHint);
|
|
324
|
+
updateHint();
|
|
325
|
+
|
|
326
|
+
const methodInput = ui.input.choice('Method', {
|
|
327
|
+
value: 'limma',
|
|
328
|
+
items: ['limma', 'DEqMS', 't-test'],
|
|
329
|
+
nullable: false,
|
|
330
|
+
});
|
|
331
|
+
methodInput.setTooltip('limma: moderated t-test; DEqMS: peptide-count-weighted variance; t-test: client-side Welch\'s t-test');
|
|
332
|
+
|
|
333
|
+
// Find default peptide count column
|
|
334
|
+
const defaultPeptideCol = df.columns.toList().find((c) =>
|
|
335
|
+
c.name === 'Unique peptides' || c.name === 'Peptides',
|
|
336
|
+
) ?? undefined;
|
|
337
|
+
|
|
338
|
+
const peptideColInput = ui.input.column('Peptide count column', {
|
|
339
|
+
table: df,
|
|
340
|
+
value: defaultPeptideCol,
|
|
341
|
+
filter: (col: DG.Column) => col.type === DG.COLUMN_TYPE.INT || col.type === DG.COLUMN_TYPE.FLOAT,
|
|
342
|
+
nullable: false,
|
|
343
|
+
});
|
|
344
|
+
peptideColInput.setTooltip('Column with peptide/spectra counts per protein');
|
|
345
|
+
|
|
346
|
+
// Show/hide peptide count picker based on method selection
|
|
347
|
+
const peptideRow = peptideColInput.root;
|
|
348
|
+
peptideRow.style.display = 'none';
|
|
349
|
+
methodInput.onChanged.subscribe(() => {
|
|
350
|
+
peptideRow.style.display = methodInput.value === 'DEqMS' ? '' : 'none';
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const fcInput = ui.input.float('|log2FC| threshold', {value: DEFAULT_FC_THRESHOLD});
|
|
354
|
+
fcInput.setTooltip('Minimum absolute fold change for significance');
|
|
355
|
+
|
|
356
|
+
const pInput = ui.input.float('Adj. p-value threshold', {value: DEFAULT_P_THRESHOLD});
|
|
357
|
+
pInput.setTooltip('Maximum adjusted p-value for significance');
|
|
358
|
+
|
|
359
|
+
ui.dialog('Differential Expression')
|
|
360
|
+
.add(infoText)
|
|
361
|
+
.add(comparisonInput)
|
|
362
|
+
.add(hintDiv)
|
|
363
|
+
.add(methodInput)
|
|
364
|
+
.add(peptideColInput)
|
|
365
|
+
.add(fcInput)
|
|
366
|
+
.add(pInput)
|
|
367
|
+
.onOK(async () => {
|
|
368
|
+
const fc = fcInput.value ?? DEFAULT_FC_THRESHOLD;
|
|
369
|
+
const p = pInput.value ?? DEFAULT_P_THRESHOLD;
|
|
370
|
+
const method = methodInput.value;
|
|
371
|
+
|
|
372
|
+
// Determine numerator/denominator from pairs index, not string-splitting,
|
|
373
|
+
// so group names containing " vs " don't break the comparison.
|
|
374
|
+
const reversed = comparisonInput.value === pairs[1];
|
|
375
|
+
const numeratorCols = reversed ? g1.columns : g2.columns;
|
|
376
|
+
const denominatorCols = reversed ? g2.columns : g1.columns;
|
|
377
|
+
const numeratorName = reversed ? g1.name : g2.name;
|
|
378
|
+
const denominatorName = reversed ? g2.name : g1.name;
|
|
379
|
+
|
|
380
|
+
const pi = DG.TaskBarProgressIndicator.create(`Running ${method} analysis...`);
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
if (method === 't-test') {
|
|
384
|
+
const result = runDifferentialExpression(
|
|
385
|
+
df, denominatorCols, numeratorCols, denominatorName, numeratorName, fc, p,
|
|
386
|
+
);
|
|
387
|
+
df.setTag('proteomics.de_method', 't-test');
|
|
388
|
+
grok.shell.info(
|
|
389
|
+
`DE complete (t-test): ${result.tested} proteins tested, ${result.untestable} untestable`,
|
|
390
|
+
);
|
|
391
|
+
} else if (method === 'DEqMS') {
|
|
392
|
+
const pepCol = peptideColInput.value;
|
|
393
|
+
if (!pepCol) {
|
|
394
|
+
grok.shell.error('Please select a peptide count column for DEqMS');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const sigCount = await runDeqmsDE(df, denominatorCols, numeratorCols, pepCol, fc, p);
|
|
399
|
+
df.setTag('proteomics.de_method', 'deqms');
|
|
400
|
+
grok.shell.info(`DE complete (DEqMS): ${sigCount} significant proteins`);
|
|
401
|
+
} catch (deqmsErr: any) {
|
|
402
|
+
console.warn('DEqMS failed, trying limma:', deqmsErr);
|
|
403
|
+
pi.update(50, 'DEqMS unavailable, trying limma...');
|
|
404
|
+
grok.shell.warning('DEqMS unavailable, trying limma...');
|
|
405
|
+
try {
|
|
406
|
+
const sigCount = await runLimmaDE(df, denominatorCols, numeratorCols, fc, p);
|
|
407
|
+
df.setTag('proteomics.de_method', 'limma');
|
|
408
|
+
grok.shell.info(`DE complete (limma fallback): ${sigCount} significant proteins`);
|
|
409
|
+
} catch (limmaErr: any) {
|
|
410
|
+
console.warn('Limma DE also failed, using client-side fallback:', limmaErr);
|
|
411
|
+
pi.update(75, 'Server-side DE unavailable, using client-side t-test...');
|
|
412
|
+
grok.shell.warning('R environment unavailable — using client-side t-test');
|
|
413
|
+
const result = runDifferentialExpression(
|
|
414
|
+
df, denominatorCols, numeratorCols, denominatorName, numeratorName, fc, p,
|
|
415
|
+
);
|
|
416
|
+
df.setTag('proteomics.de_method', 't-test');
|
|
417
|
+
grok.shell.info(
|
|
418
|
+
`DE complete (t-test): ${result.tested} proteins tested, ` +
|
|
419
|
+
`${result.untestable} untestable`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
try {
|
|
425
|
+
const sigCount = await runLimmaDE(df, denominatorCols, numeratorCols, fc, p);
|
|
426
|
+
df.setTag('proteomics.de_method', 'limma');
|
|
427
|
+
grok.shell.info(`DE complete (limma): ${sigCount} significant proteins`);
|
|
428
|
+
} catch (e: any) {
|
|
429
|
+
pi.update(50, 'Limma unavailable, using client-side t-test...');
|
|
430
|
+
grok.shell.warning('R environment unavailable — using client-side t-test');
|
|
431
|
+
console.warn('Limma DE failed, using client-side fallback:', e);
|
|
432
|
+
const result = runDifferentialExpression(
|
|
433
|
+
df, denominatorCols, numeratorCols, denominatorName, numeratorName, fc, p,
|
|
434
|
+
);
|
|
435
|
+
df.setTag('proteomics.de_method', 't-test');
|
|
436
|
+
grok.shell.info(
|
|
437
|
+
`DE complete (t-test): ${result.tested} proteins tested, ` +
|
|
438
|
+
`${result.untestable} untestable`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} finally {
|
|
443
|
+
pi.close();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (onComplete)
|
|
447
|
+
onComplete();
|
|
448
|
+
})
|
|
449
|
+
.show();
|
|
450
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as ui from 'datagrok-api/ui';
|
|
3
|
+
import * as DG from 'datagrok-api/dg';
|
|
4
|
+
|
|
5
|
+
import {findProteomicsColumns} from '../utils/column-detection';
|
|
6
|
+
import {DEFAULT_FC_THRESHOLD, DEFAULT_P_THRESHOLD} from '../utils/proteomics-types';
|
|
7
|
+
import {splitGenesByDirection, countSignificantProteins} from './enrichment';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Exports the exact inputs an enrichment run would use — the up-regulated gene
|
|
11
|
+
* list, the down-regulated gene list, and the background (every detected gene) —
|
|
12
|
+
* so the analyst can reproduce or hand off the g:Profiler query outside Datagrok
|
|
13
|
+
* (CK-omics CKomics_tool2.py:4531-4640 wrote the same three lists).
|
|
14
|
+
*
|
|
15
|
+
* Deliberately network-free: it reads the resolved gene-name column (falling
|
|
16
|
+
* back to protein IDs) rather than re-running g:Convert, mirroring the pipeline's
|
|
17
|
+
* non-mapping path via the SAME `splitGenesByDirection` so the exported lists
|
|
18
|
+
* match what Enrichment Analysis would query at identical thresholds.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Tidy long-format CSV: `gene,list` with list ∈ {up, down, background}. Each
|
|
22
|
+
* list is filter-and-copy friendly for g:Profiler; a gene appears under
|
|
23
|
+
* `background` and (if significant) also under `up`/`down` — faithful to
|
|
24
|
+
* g:Profiler semantics where the background is the full universe. */
|
|
25
|
+
export function buildEnrichmentInputsCsv(
|
|
26
|
+
df: DG.DataFrame, fcThreshold: number, pThreshold: number,
|
|
27
|
+
): {csv: string; counts: {up: number; down: number; background: number}} {
|
|
28
|
+
const cols = findProteomicsColumns(df);
|
|
29
|
+
if (!cols.log2fc || !cols.pValue)
|
|
30
|
+
throw new Error('No log2FC or adjusted p-value columns found. Run Differential Expression first.');
|
|
31
|
+
|
|
32
|
+
// Prefer resolved gene symbols; fall back to protein IDs so the export still
|
|
33
|
+
// works before gene mapping (honest — the header says what was used).
|
|
34
|
+
const idCol = cols.geneName ?? cols.proteinId;
|
|
35
|
+
if (!idCol)
|
|
36
|
+
throw new Error('No gene-name or protein-ID column found to export.');
|
|
37
|
+
|
|
38
|
+
const geneForRow = new Map<number, string>();
|
|
39
|
+
for (let i = 0; i < df.rowCount; i++) {
|
|
40
|
+
const v = idCol.get(i) as string | null;
|
|
41
|
+
if (v) geneForRow.set(i, String(v));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const fcRaw = cols.log2fc.getRawData() as Float32Array | Float64Array;
|
|
45
|
+
const pRaw = cols.pValue.getRawData() as Float32Array | Float64Array;
|
|
46
|
+
const {upGenes, downGenes, background} =
|
|
47
|
+
splitGenesByDirection(geneForRow, fcRaw, pRaw, fcThreshold, pThreshold);
|
|
48
|
+
|
|
49
|
+
const esc = (s: string): string =>
|
|
50
|
+
/[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
51
|
+
const rows: string[] = ['gene,list'];
|
|
52
|
+
for (const g of upGenes) rows.push(`${esc(g)},up`);
|
|
53
|
+
for (const g of downGenes) rows.push(`${esc(g)},down`);
|
|
54
|
+
for (const g of background) rows.push(`${esc(g)},background`);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
csv: rows.join('\n'),
|
|
58
|
+
counts: {up: upGenes.length, down: downGenes.length, background: background.length},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Small dialog to pick significance thresholds (seeded from the package
|
|
63
|
+
* defaults, matching the Enrichment dialog) and download the input lists as one
|
|
64
|
+
* CSV. Immediate download on OK via `DG.Utils.download`. */
|
|
65
|
+
export function showEnrichmentInputExportDialog(df: DG.DataFrame): void {
|
|
66
|
+
const cols = findProteomicsColumns(df);
|
|
67
|
+
if (!cols.log2fc || !cols.pValue) {
|
|
68
|
+
grok.shell.warning('No log2FC or p-value columns found. Run Differential Expression first.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const fcInput = ui.input.float('|log2FC| threshold', {value: DEFAULT_FC_THRESHOLD});
|
|
73
|
+
fcInput.setTooltip('Minimum absolute fold change for a gene to count as up/down.');
|
|
74
|
+
const pInput = ui.input.float('Adj. p-value threshold', {value: DEFAULT_P_THRESHOLD});
|
|
75
|
+
pInput.setTooltip('Maximum adjusted p-value for a gene to count as up/down.');
|
|
76
|
+
|
|
77
|
+
const countDiv = ui.divText('');
|
|
78
|
+
const updateCount = () => {
|
|
79
|
+
const r = countSignificantProteins(df, fcInput.value ?? DEFAULT_FC_THRESHOLD,
|
|
80
|
+
pInput.value ?? DEFAULT_P_THRESHOLD, cols.log2fc!, cols.pValue!);
|
|
81
|
+
countDiv.textContent = `${r.significant} of ${r.total.toLocaleString()} proteins significant`;
|
|
82
|
+
};
|
|
83
|
+
updateCount();
|
|
84
|
+
fcInput.onChanged.subscribe(updateCount);
|
|
85
|
+
pInput.onChanged.subscribe(updateCount);
|
|
86
|
+
|
|
87
|
+
ui.dialog('Export Enrichment Inputs')
|
|
88
|
+
.add(countDiv)
|
|
89
|
+
.add(fcInput)
|
|
90
|
+
.add(pInput)
|
|
91
|
+
.onOK(() => {
|
|
92
|
+
const fc = fcInput.value ?? DEFAULT_FC_THRESHOLD;
|
|
93
|
+
const p = pInput.value ?? DEFAULT_P_THRESHOLD;
|
|
94
|
+
const {csv, counts} = buildEnrichmentInputsCsv(df, fc, p);
|
|
95
|
+
const base = df.name ? `${df.name} — enrichment inputs` : 'enrichment inputs';
|
|
96
|
+
DG.Utils.download(`${base}.csv`, csv, 'text/csv');
|
|
97
|
+
grok.shell.info(
|
|
98
|
+
`Exported ${counts.up} up, ${counts.down} down, ${counts.background} background genes.`);
|
|
99
|
+
})
|
|
100
|
+
.show();
|
|
101
|
+
}
|