@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,274 @@
|
|
|
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 * as rxjs from 'rxjs';
|
|
5
|
+
import {findColumn} from '../utils/column-detection';
|
|
6
|
+
import {SEMTYPE} from '../utils/proteomics-types';
|
|
7
|
+
|
|
8
|
+
/** Module-level subscriptions for cleanup on re-open. */
|
|
9
|
+
let activeSubscriptions: rxjs.Subscription[] = [];
|
|
10
|
+
|
|
11
|
+
/** Default number of terms each dot/bar chart shows (per direction). */
|
|
12
|
+
export const CHART_TOP_N = 15;
|
|
13
|
+
|
|
14
|
+
/** Value/color column the dot + bar charts read. Kept as a plain (non-`~`)
|
|
15
|
+
* name because it is a viewer VALUE binding — a missing value column hard-fails
|
|
16
|
+
* a bar chart on reopen (the original "Column negLog10FDR does not exist" bug). */
|
|
17
|
+
export const NEG_LOG10_FDR_COL = 'negLog10FDR';
|
|
18
|
+
/** Hidden marker column (auto-hidden via `~` prefix, ignored by the publish
|
|
19
|
+
* shape contract) flagging the top-N terms per direction. Used only inside the
|
|
20
|
+
* per-viewer formula filter, where a parse failure degrades to "show all"
|
|
21
|
+
* rather than crashing — so the `~` name is safe here but not for the value. */
|
|
22
|
+
export const CHART_TOP_MARKER_COL = '~enrichChartTop';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Adds the two derived columns the enrichment dot/bar charts bind to, directly
|
|
26
|
+
* onto `enrichDf` (idempotent — ensureFreshFloat pattern):
|
|
27
|
+
* - `negLog10FDR` — -log10(FDR); dot color + bar value. FDR=0 (underflow /
|
|
28
|
+
* extreme enrichment) maps to the float-underflow ceiling so it stays
|
|
29
|
+
* visible without colliding with real values like FDR=1e-10.
|
|
30
|
+
* - `~enrichChartTop` — true for the top-N terms by FDR (ascending) within
|
|
31
|
+
* each Direction, or overall when there is no Direction column.
|
|
32
|
+
*
|
|
33
|
+
* Both charts bind to `enrichDf` itself rather than to cloned subsets: docked
|
|
34
|
+
* viewers rebind to their host view's primary frame on project reopen, so the
|
|
35
|
+
* columns they read must live on that frame (else the bar chart fatal-errors).
|
|
36
|
+
* Keeping everything on one frame also stops the subset tables from leaking
|
|
37
|
+
* into a published project's table tree.
|
|
38
|
+
*/
|
|
39
|
+
export function prepareEnrichmentChartColumns(enrichDf: DG.DataFrame, topN: number = CHART_TOP_N): void {
|
|
40
|
+
const fdrCol = enrichDf.col('FDR');
|
|
41
|
+
if (!fdrCol) return;
|
|
42
|
+
|
|
43
|
+
const UNDERFLOW_NEGLOG10 = -Math.log10(Number.MIN_VALUE);
|
|
44
|
+
const fdrRaw = fdrCol.getRawData() as Float32Array | Float64Array;
|
|
45
|
+
if (enrichDf.columns.contains(NEG_LOG10_FDR_COL)) enrichDf.columns.remove(NEG_LOG10_FDR_COL);
|
|
46
|
+
const negLogCol = enrichDf.columns.addNewFloat(NEG_LOG10_FDR_COL);
|
|
47
|
+
negLogCol.init((i) => fdrRaw[i] > 0 ? -Math.log10(fdrRaw[i]) : UNDERFLOW_NEGLOG10);
|
|
48
|
+
// Keep it out of the reviewer's grid (best-effort; `~` isn't usable on a
|
|
49
|
+
// viewer value binding). If the tag is stripped on round-trip the only cost
|
|
50
|
+
// is one extra, meaningful numeric column in the grid.
|
|
51
|
+
negLogCol.setTag('.hidden', 'true');
|
|
52
|
+
|
|
53
|
+
// Rank terms by FDR (ascending) within each direction and mark the top N.
|
|
54
|
+
const dirCol = enrichDf.col('Direction');
|
|
55
|
+
const groups = new Map<string, {idx: number; fdr: number}[]>();
|
|
56
|
+
for (let i = 0; i < enrichDf.rowCount; i++) {
|
|
57
|
+
if (fdrCol.isNone(i)) continue;
|
|
58
|
+
const key = dirCol ? String(dirCol.get(i)) : '';
|
|
59
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
60
|
+
groups.get(key)!.push({idx: i, fdr: fdrCol.get(i) as number});
|
|
61
|
+
}
|
|
62
|
+
const topMask = new Array<boolean>(enrichDf.rowCount).fill(false);
|
|
63
|
+
for (const arr of groups.values()) {
|
|
64
|
+
arr.sort((a, b) => a.fdr - b.fdr);
|
|
65
|
+
for (let k = 0; k < Math.min(topN, arr.length); k++)
|
|
66
|
+
topMask[arr[k].idx] = true;
|
|
67
|
+
}
|
|
68
|
+
if (enrichDf.columns.contains(CHART_TOP_MARKER_COL)) enrichDf.columns.remove(CHART_TOP_MARKER_COL);
|
|
69
|
+
enrichDf.columns.addNewBool(CHART_TOP_MARKER_COL).init((i) => topMask[i]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Per-viewer formula filter: the top-N marked terms, optionally narrowed to one
|
|
73
|
+
* regulation direction. Degrades to "show all rows" if the platform drops the
|
|
74
|
+
* formula on reopen — never a missing-column crash. */
|
|
75
|
+
function chartFilter(direction?: 'Up' | 'Down'): string {
|
|
76
|
+
const clauses = [`\${${CHART_TOP_MARKER_COL}}`];
|
|
77
|
+
if (direction) clauses.push(`\${Direction} == "${direction}"`);
|
|
78
|
+
return clauses.join(' and ');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates an enrichment dot plot bound to the shared enrichment frame.
|
|
83
|
+
* X = Gene Ratio, Y = Term Name, size = Gene Count, color = -log10(FDR).
|
|
84
|
+
* Pass `direction` to split into Up/Down via a per-viewer formula filter.
|
|
85
|
+
*/
|
|
86
|
+
export function createEnrichmentDotPlot(enrichDf: DG.DataFrame, direction?: 'Up' | 'Down'): DG.ScatterPlotViewer {
|
|
87
|
+
const sp = DG.Viewer.scatterPlot(enrichDf, {
|
|
88
|
+
x: 'Gene Ratio',
|
|
89
|
+
y: 'Term Name',
|
|
90
|
+
sizeColumnName: 'Gene Count',
|
|
91
|
+
colorColumnName: NEG_LOG10_FDR_COL,
|
|
92
|
+
markerMinSize: 5,
|
|
93
|
+
markerMaxSize: 25,
|
|
94
|
+
filter: chartFilter(direction),
|
|
95
|
+
title: direction ? `Enrichment — ${direction}` : 'Enrichment Dot Plot',
|
|
96
|
+
} as any);
|
|
97
|
+
return sp;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Creates an enrichment bar chart (top terms ranked by -log10(FDR)) bound to the
|
|
102
|
+
* shared enrichment frame. Pass `direction` to split into Up/Down.
|
|
103
|
+
*/
|
|
104
|
+
export function createEnrichmentBarChart(enrichDf: DG.DataFrame, direction?: 'Up' | 'Down'): DG.Viewer {
|
|
105
|
+
const bar = DG.Viewer.barChart(enrichDf, {
|
|
106
|
+
splitColumnName: 'Term Name',
|
|
107
|
+
valueColumnName: NEG_LOG10_FDR_COL,
|
|
108
|
+
valueAggrType: 'avg',
|
|
109
|
+
barSortType: 'by value',
|
|
110
|
+
barSortOrder: 'desc',
|
|
111
|
+
orientation: 'Horizontal',
|
|
112
|
+
filter: chartFilter(direction),
|
|
113
|
+
title: direction ? `Top Enriched — ${direction}` : 'Top Enriched Terms',
|
|
114
|
+
} as any);
|
|
115
|
+
return bar;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Wires enrichment row changes to protein DataFrame selection.
|
|
120
|
+
* When a row is selected in the enrichment table, matching protein rows
|
|
121
|
+
* (by gene symbol from the Intersection column) are highlighted.
|
|
122
|
+
*/
|
|
123
|
+
export function wireEnrichmentToVolcano(
|
|
124
|
+
enrichDf: DG.DataFrame,
|
|
125
|
+
proteinDf: DG.DataFrame,
|
|
126
|
+
): rxjs.Subscription {
|
|
127
|
+
const geneCol = findColumn(proteinDf, SEMTYPE.GENE_SYMBOL, ['gene name', 'gene symbol']);
|
|
128
|
+
if (!geneCol) return rxjs.Subscription.EMPTY;
|
|
129
|
+
|
|
130
|
+
// Build gene-to-row index
|
|
131
|
+
const geneToRows = new Map<string, number[]>();
|
|
132
|
+
for (let i = 0; i < proteinDf.rowCount; i++) {
|
|
133
|
+
if (!geneCol.isNone(i)) {
|
|
134
|
+
const gene = (geneCol.get(i) as string).trim();
|
|
135
|
+
if (!geneToRows.has(gene)) geneToRows.set(gene, []);
|
|
136
|
+
geneToRows.get(gene)!.push(i);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return enrichDf.onCurrentRowChanged.subscribe(() => {
|
|
141
|
+
const rowIdx = enrichDf.currentRowIdx;
|
|
142
|
+
if (rowIdx < 0) return;
|
|
143
|
+
|
|
144
|
+
const intersectionCol = enrichDf.col('Intersection');
|
|
145
|
+
if (!intersectionCol) return;
|
|
146
|
+
|
|
147
|
+
const memberGenesStr = intersectionCol.get(rowIdx) as string;
|
|
148
|
+
if (!memberGenesStr) return;
|
|
149
|
+
|
|
150
|
+
const memberGenes = memberGenesStr.split(',').map((g) => g.trim()).filter(Boolean);
|
|
151
|
+
|
|
152
|
+
// Clear previous selection
|
|
153
|
+
proteinDf.selection.setAll(false, false);
|
|
154
|
+
|
|
155
|
+
// Set selection for matching genes
|
|
156
|
+
for (const gene of memberGenes) {
|
|
157
|
+
const rows = geneToRows.get(gene);
|
|
158
|
+
if (rows) {
|
|
159
|
+
for (const row of rows)
|
|
160
|
+
proteinDf.selection.set(row, true, false);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
proteinDf.selection.fireChanged();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** True when `enrichDf` carries a Direction column with at least one row in
|
|
168
|
+
* `direction`. Drives the Up/Down 2×2 vs single-chart layout choice. */
|
|
169
|
+
function hasDirection(enrichDf: DG.DataFrame, direction: 'Up' | 'Down'): boolean {
|
|
170
|
+
const dirCol = enrichDf.col('Direction');
|
|
171
|
+
if (!dirCol) return false;
|
|
172
|
+
for (let i = 0; i < enrichDf.rowCount; i++) {
|
|
173
|
+
if (dirCol.get(i) === direction) return true;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Opens a full enrichment visualization dashboard with dot plot, bar chart,
|
|
180
|
+
* and cross-DataFrame selection wiring to the protein table. When the result
|
|
181
|
+
* carries an R2 Direction column with both Up and Down rows, the Up and Down
|
|
182
|
+
* dot+bar charts are docked side-by-side; otherwise the original single
|
|
183
|
+
* dot+bar layout is used. The Phase-9 volcano cross-link is unchanged.
|
|
184
|
+
*/
|
|
185
|
+
export function openEnrichmentVisualization(
|
|
186
|
+
enrichDf: DG.DataFrame,
|
|
187
|
+
proteinDf: DG.DataFrame,
|
|
188
|
+
): DG.TableView {
|
|
189
|
+
// Clean up previous subscriptions
|
|
190
|
+
for (const sub of activeSubscriptions)
|
|
191
|
+
sub.unsubscribe();
|
|
192
|
+
activeSubscriptions = [];
|
|
193
|
+
|
|
194
|
+
// Find or reuse the enrichment results table view. Match by stable .dart
|
|
195
|
+
// identity, NOT `=== enrichDf`: grok.shell.tableViews returns a fresh toJs
|
|
196
|
+
// wrapper per access, so strict-equality misses an already-open view and
|
|
197
|
+
// spawns a duplicate "Enrichment Results (2)" tab (precedent: the protein-TV
|
|
198
|
+
// lookup that used to live here; 13-07 SUMMARY).
|
|
199
|
+
let existing: DG.TableView | undefined;
|
|
200
|
+
for (const v of grok.shell.tableViews) {
|
|
201
|
+
if ((v.dataFrame as any)?.dart === (enrichDf as any).dart) { existing = v; break; }
|
|
202
|
+
}
|
|
203
|
+
const tv = existing ?? grok.shell.addTableView(enrichDf);
|
|
204
|
+
|
|
205
|
+
// Dock the dot/bar charts onto the ENRICHMENT results view — not the protein
|
|
206
|
+
// view where the volcano lives — so the volcano tab stays uncluttered
|
|
207
|
+
// (light-touch declutter). Surface this view FIRST: docking onto a
|
|
208
|
+
// backgrounded host leaves zero-dimension dock splitters and crashes the Dart
|
|
209
|
+
// scatter plot's smart-labels with
|
|
210
|
+
// "Failed to execute 'drawImage'... canvas with width or height of 0".
|
|
211
|
+
grok.shell.v = tv;
|
|
212
|
+
const chartHost = tv;
|
|
213
|
+
|
|
214
|
+
// D-14 — surface the smart-filter transform stats above the grid when it
|
|
215
|
+
// actually dropped rows. Tag is set only by runEnrichmentPipeline when
|
|
216
|
+
// kept < total; the banner copy is locked in 14-UI-SPEC.md §"Copywriting".
|
|
217
|
+
if (enrichDf.getTag('proteomics.enrichment_smart_filtered') === 'true') {
|
|
218
|
+
const kept = enrichDf.getTag('proteomics.enrichment_smart_filtered_kept') ?? '?';
|
|
219
|
+
const total = enrichDf.getTag('proteomics.enrichment_smart_filtered_total') ?? '?';
|
|
220
|
+
const parents = enrichDf.getTag('proteomics.enrichment_smart_filtered_dropped_parents') ?? '?';
|
|
221
|
+
const cap = enrichDf.getTag('proteomics.enrichment_smart_filtered_cap') ?? '15';
|
|
222
|
+
const banner = ui.divText(
|
|
223
|
+
`Smart pathway filter active: showing ${kept} of ${total} terms ` +
|
|
224
|
+
`(dropped ${parents} generic parents, capped at ${cap} per source). ` +
|
|
225
|
+
`Re-run with the filter off to see all.`);
|
|
226
|
+
banner.dataset.smartFilterBanner = 'true';
|
|
227
|
+
tv.dockManager.dock(banner, DG.DOCK_TYPE.TOP, null, 'Smart Filter Info', 0.1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Dock the dot/bar charts (extracted so the publish path reuses the exact
|
|
231
|
+
// 2×2) and cross-link the frame. Every chart binds to `enrichDf` itself, so
|
|
232
|
+
// the single full-frame wiring covers all of them (and keeps the Intersection
|
|
233
|
+
// column the Phase-9 wiring keys on).
|
|
234
|
+
dockEnrichmentCharts(chartHost, enrichDf);
|
|
235
|
+
activeSubscriptions.push(wireEnrichmentToVolcano(enrichDf, proteinDf));
|
|
236
|
+
|
|
237
|
+
// The dot/bar charts, the merged enrichment grid, and the smart-filter banner
|
|
238
|
+
// all share this enrichment tab; the volcano stays on its own (protein) tab.
|
|
239
|
+
// The cross-link keeps the two tabs in sync on selection.
|
|
240
|
+
return tv;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Docks the directional (Up|Down 2×2) or single Dot+Bar enrichment charts onto
|
|
244
|
+
* `host`. Every chart binds to `enrichDf` itself — the derived `negLog10FDR` /
|
|
245
|
+
* `~enrichChartTop` columns are added to that frame and the Up/Down split is a
|
|
246
|
+
* per-viewer formula filter. No cloned subsets, so nothing leaks into a
|
|
247
|
+
* published project's table tree and the charts survive a project round-trip
|
|
248
|
+
* (docked viewers rebind to their host frame on reopen). Pure layout — no
|
|
249
|
+
* subscriptions, no focus changes — shared by the live and publish paths. */
|
|
250
|
+
export function dockEnrichmentCharts(host: DG.TableView, enrichDf: DG.DataFrame): void {
|
|
251
|
+
prepareEnrichmentChartColumns(enrichDf);
|
|
252
|
+
|
|
253
|
+
if (hasDirection(enrichDf, 'Up') && hasDirection(enrichDf, 'Down')) {
|
|
254
|
+
// Balanced 2×2: [Up Dot | Down Dot] over [Up Bar | Down Bar]. Split the
|
|
255
|
+
// chart region into the two columns FIRST, THEN drop each bar under its dot
|
|
256
|
+
// — docking the bars before the split carved up only the top-left cell and
|
|
257
|
+
// produced a lopsided layout.
|
|
258
|
+
const upDotNode = host.dockManager.dock(
|
|
259
|
+
createEnrichmentDotPlot(enrichDf, 'Up'), DG.DOCK_TYPE.RIGHT, null, 'Up — Dot Plot', 0.6);
|
|
260
|
+
const downDotNode = host.dockManager.dock(
|
|
261
|
+
createEnrichmentDotPlot(enrichDf, 'Down'), DG.DOCK_TYPE.RIGHT, upDotNode, 'Down — Dot Plot', 0.5);
|
|
262
|
+
host.dockManager.dock(
|
|
263
|
+
createEnrichmentBarChart(enrichDf, 'Up'), DG.DOCK_TYPE.DOWN, upDotNode, 'Up — Bar Chart', 0.5);
|
|
264
|
+
host.dockManager.dock(
|
|
265
|
+
createEnrichmentBarChart(enrichDf, 'Down'), DG.DOCK_TYPE.DOWN, downDotNode, 'Down — Bar Chart', 0.5);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Single-direction or no Direction column → one Dot + Bar.
|
|
270
|
+
const dotNode = host.dockManager.dock(
|
|
271
|
+
createEnrichmentDotPlot(enrichDf), DG.DOCK_TYPE.RIGHT, null, 'Dot Plot', 0.5);
|
|
272
|
+
host.dockManager.dock(
|
|
273
|
+
createEnrichmentBarChart(enrichDf), DG.DOCK_TYPE.DOWN, dotNode, 'Bar Chart', 0.5);
|
|
274
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import * as DG from 'datagrok-api/dg';
|
|
2
|
+
import {findColumn} from '../utils/column-detection';
|
|
3
|
+
import {SEMTYPE} from '../utils/proteomics-types';
|
|
4
|
+
import {getGroups, GroupAssignment} from '../analysis/experiment-setup';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* R4 / D-12 — Group-Mean Correlation viewer.
|
|
8
|
+
*
|
|
9
|
+
* Adds two derived columns to the host DataFrame (Numerator Mean, Denominator
|
|
10
|
+
* Mean — means of the group1/group2 intensity columns per row) and renders
|
|
11
|
+
* them as a scatter colored by the existing `direction` column (magenta/cyan/
|
|
12
|
+
* gray per D-04). The viewer title carries inline Pearson r and Spearman ρ
|
|
13
|
+
* over the current filter; a y=x diagonal reference line is drawn in #888888.
|
|
14
|
+
*
|
|
15
|
+
* The derived columns are re-run-safe via `ensureFreshFloat` — a second
|
|
16
|
+
* invocation replaces the columns instead of duplicating them.
|
|
17
|
+
*
|
|
18
|
+
* Statistics: `@datagrok-libraries/statistics` exports only Kendall's tau,
|
|
19
|
+
* so Pearson and Spearman are implemented inline (Pearson over raw values,
|
|
20
|
+
* Spearman = Pearson over fractional ranks).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const NUMERATOR_MEAN_COL = 'Numerator Mean';
|
|
24
|
+
const DENOMINATOR_MEAN_COL = 'Denominator Mean';
|
|
25
|
+
const DIRECTION_COL = 'direction';
|
|
26
|
+
const DIAGONAL_FORMULA = `\${${NUMERATOR_MEAN_COL}} = \${${DENOMINATOR_MEAN_COL}}`;
|
|
27
|
+
const DIAGONAL_COLOR = '#888888';
|
|
28
|
+
|
|
29
|
+
/** Bulk-init pattern — removes an existing column first to avoid duplicates on
|
|
30
|
+
* re-run. Mirrors src/viewers/qc-computations.ts:28-32. */
|
|
31
|
+
function ensureFreshFloat(df: DG.DataFrame, name: string): DG.Column {
|
|
32
|
+
if (df.columns.contains(name))
|
|
33
|
+
df.columns.remove(name);
|
|
34
|
+
return df.columns.addNewFloat(name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Mean of non-null values across `cols` at `rowIdx`. Returns NaN if every
|
|
38
|
+
* value is null. Mirrors qc-computations.ts:15-25. */
|
|
39
|
+
function groupMean(cols: DG.Column[], rowIdx: number): number {
|
|
40
|
+
let sum = 0;
|
|
41
|
+
let n = 0;
|
|
42
|
+
for (const c of cols) {
|
|
43
|
+
if (c.isNone(rowIdx)) continue;
|
|
44
|
+
sum += c.get(rowIdx) as number;
|
|
45
|
+
n++;
|
|
46
|
+
}
|
|
47
|
+
return n > 0 ? sum / n : NaN;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Derives Numerator Mean (group1) and Denominator Mean (group2) columns on
|
|
52
|
+
* `df`. The join key is the protein row index — never a derived key like
|
|
53
|
+
* gene name (project memory project_proteomics_deqms_result_misalignment).
|
|
54
|
+
*
|
|
55
|
+
* The columns carry the dedicated SEMTYPE.NUMERATOR_MEAN / SEMTYPE.DENOMINATOR_MEAN
|
|
56
|
+
* tags so downstream tooling can find them via `findColumn`.
|
|
57
|
+
*/
|
|
58
|
+
export function computeGroupMeans(df: DG.DataFrame, groups: GroupAssignment): void {
|
|
59
|
+
const g1Cols = groups.group1.columns
|
|
60
|
+
.map((n) => df.col(n))
|
|
61
|
+
.filter((c): c is DG.Column => c != null);
|
|
62
|
+
const g2Cols = groups.group2.columns
|
|
63
|
+
.map((n) => df.col(n))
|
|
64
|
+
.filter((c): c is DG.Column => c != null);
|
|
65
|
+
|
|
66
|
+
const nRows = df.rowCount;
|
|
67
|
+
const numArr = new Float32Array(nRows);
|
|
68
|
+
const denArr = new Float32Array(nRows);
|
|
69
|
+
for (let i = 0; i < nRows; i++) {
|
|
70
|
+
const m1 = groupMean(g1Cols, i);
|
|
71
|
+
const m2 = groupMean(g2Cols, i);
|
|
72
|
+
numArr[i] = isNaN(m1) ? DG.FLOAT_NULL : m1;
|
|
73
|
+
denArr[i] = isNaN(m2) ? DG.FLOAT_NULL : m2;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const numCol = ensureFreshFloat(df, NUMERATOR_MEAN_COL);
|
|
77
|
+
numCol.init((i) => numArr[i]);
|
|
78
|
+
numCol.semType = SEMTYPE.NUMERATOR_MEAN;
|
|
79
|
+
|
|
80
|
+
const denCol = ensureFreshFloat(df, DENOMINATOR_MEAN_COL);
|
|
81
|
+
denCol.init((i) => denArr[i]);
|
|
82
|
+
denCol.semType = SEMTYPE.DENOMINATOR_MEAN;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Pearson correlation. Skips paired entries containing NaN or FLOAT_NULL. */
|
|
86
|
+
export function pearson(xs: number[], ys: number[]): number {
|
|
87
|
+
let n = 0;
|
|
88
|
+
let sumX = 0;
|
|
89
|
+
let sumY = 0;
|
|
90
|
+
for (let i = 0; i < xs.length; i++) {
|
|
91
|
+
const x = xs[i];
|
|
92
|
+
const y = ys[i];
|
|
93
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || x === DG.FLOAT_NULL || y === DG.FLOAT_NULL) continue;
|
|
94
|
+
sumX += x;
|
|
95
|
+
sumY += y;
|
|
96
|
+
n++;
|
|
97
|
+
}
|
|
98
|
+
if (n === 0) return NaN;
|
|
99
|
+
const meanX = sumX / n;
|
|
100
|
+
const meanY = sumY / n;
|
|
101
|
+
let num = 0;
|
|
102
|
+
let denX = 0;
|
|
103
|
+
let denY = 0;
|
|
104
|
+
for (let i = 0; i < xs.length; i++) {
|
|
105
|
+
const x = xs[i];
|
|
106
|
+
const y = ys[i];
|
|
107
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || x === DG.FLOAT_NULL || y === DG.FLOAT_NULL) continue;
|
|
108
|
+
const dx = x - meanX;
|
|
109
|
+
const dy = y - meanY;
|
|
110
|
+
num += dx * dy;
|
|
111
|
+
denX += dx * dx;
|
|
112
|
+
denY += dy * dy;
|
|
113
|
+
}
|
|
114
|
+
const den = Math.sqrt(denX * denY);
|
|
115
|
+
return den === 0 ? NaN : num / den;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Fractional ranks (1-based). Ties get the average of the positions they span. */
|
|
119
|
+
function rank(arr: number[]): number[] {
|
|
120
|
+
const indexed = arr.map((v, i) => ({v, i}));
|
|
121
|
+
indexed.sort((a, b) => a.v - b.v);
|
|
122
|
+
const ranks = new Array<number>(arr.length);
|
|
123
|
+
let i = 0;
|
|
124
|
+
while (i < indexed.length) {
|
|
125
|
+
let j = i;
|
|
126
|
+
while (j + 1 < indexed.length && indexed[j + 1].v === indexed[i].v) j++;
|
|
127
|
+
const avg = (i + j) / 2 + 1; // 1-based average of positions [i..j]
|
|
128
|
+
for (let k = i; k <= j; k++) ranks[indexed[k].i] = avg;
|
|
129
|
+
i = j + 1;
|
|
130
|
+
}
|
|
131
|
+
return ranks;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Spearman = Pearson over fractional ranks. */
|
|
135
|
+
export function spearman(xs: number[], ys: number[]): number {
|
|
136
|
+
return pearson(rank(xs), rank(ys));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Computes Pearson + Spearman over the filtered subset of two columns. */
|
|
140
|
+
function computePearsonSpearman(
|
|
141
|
+
df: DG.DataFrame, xName: string, yName: string,
|
|
142
|
+
): {r: number; rho: number} {
|
|
143
|
+
const xCol = df.col(xName);
|
|
144
|
+
const yCol = df.col(yName);
|
|
145
|
+
if (!xCol || !yCol) return {r: NaN, rho: NaN};
|
|
146
|
+
const xRaw = xCol.getRawData() as Float32Array | Float64Array;
|
|
147
|
+
const yRaw = yCol.getRawData() as Float32Array | Float64Array;
|
|
148
|
+
|
|
149
|
+
const xs: number[] = [];
|
|
150
|
+
const ys: number[] = [];
|
|
151
|
+
for (let i = 0; i < df.rowCount; i++) {
|
|
152
|
+
if (!df.filter.get(i)) continue;
|
|
153
|
+
const x = xRaw[i];
|
|
154
|
+
const y = yRaw[i];
|
|
155
|
+
if (x === DG.FLOAT_NULL || y === DG.FLOAT_NULL) continue;
|
|
156
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
157
|
+
xs.push(x);
|
|
158
|
+
ys.push(y);
|
|
159
|
+
}
|
|
160
|
+
return {r: pearson(xs, ys), rho: spearman(xs, ys)};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Main factory — derives Numerator/Denominator Mean, plots them as a scatter
|
|
165
|
+
* colored by direction, draws the y=x diagonal, and writes an inline
|
|
166
|
+
* Pearson/Spearman annotation into the title.
|
|
167
|
+
*/
|
|
168
|
+
export function createGroupMeanCorrelation(
|
|
169
|
+
df: DG.DataFrame,
|
|
170
|
+
options?: {title?: string},
|
|
171
|
+
): DG.ScatterPlotViewer {
|
|
172
|
+
const groups = getGroups(df);
|
|
173
|
+
if (!groups)
|
|
174
|
+
throw new Error('Annotate experimental groups first (Proteomics | Annotate Experiment)');
|
|
175
|
+
|
|
176
|
+
computeGroupMeans(df, groups);
|
|
177
|
+
|
|
178
|
+
const sp = df.plot.scatter({
|
|
179
|
+
x: NUMERATOR_MEAN_COL,
|
|
180
|
+
y: DENOMINATOR_MEAN_COL,
|
|
181
|
+
color: DIRECTION_COL,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Display Name primary, Gene name fallback (Plan 02 pattern).
|
|
185
|
+
const labelCol = findColumn(df, SEMTYPE.DISPLAY_NAME, ['display name']) ??
|
|
186
|
+
findColumn(df, SEMTYPE.GENE_SYMBOL, ['gene name', 'gene symbol']);
|
|
187
|
+
if (labelCol) {
|
|
188
|
+
sp.props.labelColumnNames = [labelCol.name];
|
|
189
|
+
sp.props.showLabelsFor = 'MouseOverRow';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// y = x diagonal reference — replace any prior diagonal so re-run doesn't stack.
|
|
193
|
+
df.meta.formulaLines.items = df.meta.formulaLines.items.filter((line) => {
|
|
194
|
+
const f = (line.formula ?? '') as string;
|
|
195
|
+
return !(typeof f === 'string' &&
|
|
196
|
+
f.includes(NUMERATOR_MEAN_COL) && f.includes(DENOMINATOR_MEAN_COL));
|
|
197
|
+
});
|
|
198
|
+
df.meta.formulaLines.addLine({
|
|
199
|
+
formula: DIAGONAL_FORMULA,
|
|
200
|
+
color: DIAGONAL_COLOR,
|
|
201
|
+
width: 1,
|
|
202
|
+
visible: true,
|
|
203
|
+
});
|
|
204
|
+
sp.props.showViewerFormulaLines = true;
|
|
205
|
+
|
|
206
|
+
// Inline correlation annotation over the current filter.
|
|
207
|
+
const {r, rho} = computePearsonSpearman(df, NUMERATOR_MEAN_COL, DENOMINATOR_MEAN_COL);
|
|
208
|
+
const baseTitle = options?.title ?? 'Group-Mean Correlation';
|
|
209
|
+
const rStr = Number.isFinite(r) ? r.toFixed(2) : 'NA';
|
|
210
|
+
const rhoStr = Number.isFinite(rho) ? rho.toFixed(2) : 'NA';
|
|
211
|
+
sp.setOptions({
|
|
212
|
+
title: `${baseTitle} — r=${rStr} (Pearson), ρ=${rhoStr} (Spearman)`,
|
|
213
|
+
xColumnLabel: `${groups.group1.name} mean (log2 intensity)`,
|
|
214
|
+
yColumnLabel: `${groups.group2.name} mean (log2 intensity)`,
|
|
215
|
+
} as any);
|
|
216
|
+
|
|
217
|
+
return sp;
|
|
218
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as DG from 'datagrok-api/dg';
|
|
2
|
+
import {getGroups} from '../analysis/experiment-setup';
|
|
3
|
+
import {findColumn} from '../utils/column-detection';
|
|
4
|
+
import {SEMTYPE} from '../utils/proteomics-types';
|
|
5
|
+
|
|
6
|
+
/** Z-score normalize a value given mean and standard deviation. */
|
|
7
|
+
function zScore(value: number, mean: number, std: number): number {
|
|
8
|
+
return std > 0 ? (value - mean) / std : 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates an expression heatmap (Grid in heatmap mode) on a cloned DataFrame
|
|
13
|
+
* containing only the top-N most significant proteins. The clone isolates
|
|
14
|
+
* the heatmap's filter, z-score columns, and sort order from the original
|
|
15
|
+
* DataFrame so that other viewers (volcano plot) are not affected.
|
|
16
|
+
*
|
|
17
|
+
* Rows are hierarchically clustered via the Dendrogram package service if available,
|
|
18
|
+
* otherwise sorted by adj.p-value ascending (significance-based fallback).
|
|
19
|
+
*
|
|
20
|
+
* Z-score normalization uses statistics computed across ALL rows of the original
|
|
21
|
+
* DataFrame for statistical correctness, but z-score columns are only added to the
|
|
22
|
+
* cloned DataFrame.
|
|
23
|
+
*/
|
|
24
|
+
export async function createExpressionHeatmap(
|
|
25
|
+
df: DG.DataFrame,
|
|
26
|
+
options?: {topN?: number; labelCol?: string; title?: string},
|
|
27
|
+
): Promise<DG.Grid> {
|
|
28
|
+
const topN = options?.topN ?? 50;
|
|
29
|
+
const labelColName = options?.labelCol ?? 'Gene Name';
|
|
30
|
+
|
|
31
|
+
// 1. Check prerequisites
|
|
32
|
+
if (df.getTag('proteomics.de_complete') !== 'true')
|
|
33
|
+
throw new Error('Differential expression must be run first');
|
|
34
|
+
|
|
35
|
+
const groups = getGroups(df);
|
|
36
|
+
if (!groups)
|
|
37
|
+
throw new Error('Experimental groups must be annotated first');
|
|
38
|
+
|
|
39
|
+
// 2. Select top N proteins by adj.p-value (ascending)
|
|
40
|
+
const adjPCol = df.col('adj.p-value');
|
|
41
|
+
if (!adjPCol)
|
|
42
|
+
throw new Error('adj.p-value column not found');
|
|
43
|
+
|
|
44
|
+
// Collect row indices with non-null adj.p-value, sorted ascending
|
|
45
|
+
const indexedPValues: {idx: number; pVal: number}[] = [];
|
|
46
|
+
for (let i = 0; i < df.rowCount; i++) {
|
|
47
|
+
if (!adjPCol.isNone(i))
|
|
48
|
+
indexedPValues.push({idx: i, pVal: adjPCol.get(i) as number});
|
|
49
|
+
}
|
|
50
|
+
indexedPValues.sort((a, b) => a.pVal - b.pVal);
|
|
51
|
+
|
|
52
|
+
const topNIndices = new Set<number>();
|
|
53
|
+
for (let i = 0; i < Math.min(topN, indexedPValues.length); i++)
|
|
54
|
+
topNIndices.add(indexedPValues[i].idx);
|
|
55
|
+
|
|
56
|
+
// 3. Clone the DataFrame with only top-N rows (filter isolation)
|
|
57
|
+
const filter = DG.BitSet.create(df.rowCount);
|
|
58
|
+
for (const idx of topNIndices)
|
|
59
|
+
filter.set(idx, true);
|
|
60
|
+
const heatmapDf = df.clone(filter);
|
|
61
|
+
|
|
62
|
+
// 4. Compute z-score columns on the cloned DataFrame
|
|
63
|
+
// Statistics (mean, std) are computed across ALL rows of the original df for correctness,
|
|
64
|
+
// then z-scores are computed on the cloned rows only.
|
|
65
|
+
const allIntensityCols = [...groups.group1.columns, ...groups.group2.columns];
|
|
66
|
+
const zScoreColNames: string[] = [];
|
|
67
|
+
|
|
68
|
+
for (const colName of allIntensityCols) {
|
|
69
|
+
const zColName = `_zscore_${colName}`;
|
|
70
|
+
zScoreColNames.push(zColName);
|
|
71
|
+
|
|
72
|
+
const srcColOrig = df.col(colName);
|
|
73
|
+
if (!srcColOrig) continue;
|
|
74
|
+
|
|
75
|
+
// Compute mean and std across all non-null values in the ORIGINAL column.
|
|
76
|
+
// Use direct typed-array access to skip the per-row JS->native interop.
|
|
77
|
+
const origRaw = srcColOrig.getRawData() as Float32Array | Float64Array;
|
|
78
|
+
let sum = 0;
|
|
79
|
+
let sumSq = 0;
|
|
80
|
+
let count = 0;
|
|
81
|
+
for (let i = 0; i < df.rowCount; i++) {
|
|
82
|
+
const v = origRaw[i];
|
|
83
|
+
if (v !== DG.FLOAT_NULL) {
|
|
84
|
+
sum += v;
|
|
85
|
+
sumSq += v * v;
|
|
86
|
+
count++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const colMean = count > 0 ? sum / count : 0;
|
|
91
|
+
const variance = count > 1 ? (sumSq - sum * sum / count) / (count - 1) : 0;
|
|
92
|
+
const colStd = Math.sqrt(variance);
|
|
93
|
+
|
|
94
|
+
// Apply z-scores to the cloned DataFrame
|
|
95
|
+
const srcColClone = heatmapDf.col(colName);
|
|
96
|
+
if (!srcColClone) continue;
|
|
97
|
+
|
|
98
|
+
const srcRaw = srcColClone.getRawData() as Float32Array | Float64Array;
|
|
99
|
+
const zCol = heatmapDf.columns.addNewFloat(zColName);
|
|
100
|
+
zCol.init((i) => srcRaw[i] === DG.FLOAT_NULL ? DG.FLOAT_NULL : zScore(srcRaw[i], colMean, colStd));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 5. Create Grid on the cloned DataFrame
|
|
104
|
+
const grid = heatmapDf.plot.grid();
|
|
105
|
+
|
|
106
|
+
// 6. Column visibility -- hide everything, then show label + z-score columns
|
|
107
|
+
for (let i = 0; i < grid.columns.length; i++) {
|
|
108
|
+
const gc = grid.columns.byIndex(i);
|
|
109
|
+
if (gc) gc.visible = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Show label column (gene symbol preferred, protein ID as fallback)
|
|
113
|
+
const labelCol = findColumn(heatmapDf, SEMTYPE.GENE_SYMBOL, [labelColName.toLowerCase(), 'gene symbol'])
|
|
114
|
+
?? findColumn(heatmapDf, SEMTYPE.PROTEIN_ID, ['protein id', 'majority protein id', 'accession']);
|
|
115
|
+
if (labelCol) {
|
|
116
|
+
const labelGc = grid.columns.byName(labelCol.name);
|
|
117
|
+
if (labelGc) labelGc.visible = true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Show z-score columns in group order (control left, treatment right)
|
|
121
|
+
const group1ZCols = groups.group1.columns.map((c) => `_zscore_${c}`);
|
|
122
|
+
const group2ZCols = groups.group2.columns.map((c) => `_zscore_${c}`);
|
|
123
|
+
|
|
124
|
+
for (const zColName of [...group1ZCols, ...group2ZCols]) {
|
|
125
|
+
const gc = grid.columns.byName(zColName);
|
|
126
|
+
if (gc) gc.visible = true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 7. Attempt hierarchical clustering via Dendrogram package; fall back to
|
|
130
|
+
// significance-based row ordering if the package is unavailable or its
|
|
131
|
+
// function signature has drifted.
|
|
132
|
+
let clustered = false;
|
|
133
|
+
const pi = DG.TaskBarProgressIndicator.create('Calculating distance matrix...');
|
|
134
|
+
try {
|
|
135
|
+
const dFunc = DG.Func.find({package: 'Dendrogram', name: 'hierarchicalClustering'})[0];
|
|
136
|
+
if (!dFunc)
|
|
137
|
+
throw new Error('Dendrogram package not installed (hierarchicalClustering not found)');
|
|
138
|
+
if (dFunc.inputs.length !== 4)
|
|
139
|
+
throw new Error(`Dendrogram.hierarchicalClustering signature changed (expected 4 inputs, got ${dFunc.inputs.length})`);
|
|
140
|
+
await dFunc.apply({
|
|
141
|
+
df: grid, colNameList: zScoreColNames,
|
|
142
|
+
distance: 'euclidean', linkage: 'complete',
|
|
143
|
+
});
|
|
144
|
+
clustered = true;
|
|
145
|
+
} catch (e: any) {
|
|
146
|
+
console.warn(`Dendrogram clustering unavailable, falling back to significance-based row ordering: ${e?.message ?? e}`);
|
|
147
|
+
} finally {
|
|
148
|
+
pi.close();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!clustered) {
|
|
152
|
+
// Sort by adjusted p-value ascending (most significant first). R scripts
|
|
153
|
+
// sometimes return the column as `adj.p.value` (dots) instead of `adj.p-value`.
|
|
154
|
+
const sortCol = heatmapDf.col('adj.p-value') ? 'adj.p-value' :
|
|
155
|
+
(heatmapDf.col('adj.p.value') ? 'adj.p.value' : null);
|
|
156
|
+
if (sortCol)
|
|
157
|
+
grid.sort([sortCol]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 8. Configure Grid for heatmap display
|
|
161
|
+
grid.props.isHeatmap = true;
|
|
162
|
+
grid.props.isGrid = false;
|
|
163
|
+
|
|
164
|
+
if (options?.title)
|
|
165
|
+
grid.setOptions({title: options.title});
|
|
166
|
+
|
|
167
|
+
return grid;
|
|
168
|
+
}
|