@datagrok/proteomics 1.2.1 → 1.3.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 (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +18 -5
  3. package/dist/package-test.js +1 -1
  4. package/dist/package-test.js.map +1 -1
  5. package/dist/package.js +1 -1
  6. package/dist/package.js.map +1 -1
  7. package/docs/personas-and-capabilities.md +16 -0
  8. package/docs/publishing-design-rationale.md +140 -0
  9. package/package.json +17 -3
  10. package/package.png +0 -0
  11. package/src/analysis/imputation.ts +3 -1
  12. package/src/analysis/normalization.ts +9 -4
  13. package/src/analysis/spc-storage.ts +1 -1
  14. package/src/menu.ts +5 -2
  15. package/src/package-api.ts +10 -2
  16. package/src/package-test.ts +3 -1
  17. package/src/package.g.ts +17 -3
  18. package/src/package.ts +31 -15
  19. package/src/panels/published-analysis-panel.ts +2 -2
  20. package/src/publishing/assert-published-shape.ts +2 -2
  21. package/src/publishing/package-settings-editor.ts +125 -0
  22. package/src/publishing/publish-project.ts +8 -8
  23. package/src/publishing/publish-settings.ts +57 -3
  24. package/src/publishing/publish-state.ts +45 -16
  25. package/src/publishing/reviewer-groups.ts +24 -0
  26. package/src/publishing/share-dialog.ts +42 -27
  27. package/src/publishing/trim-dataframe.ts +8 -1
  28. package/src/tests/abundance-correlation.ts +135 -0
  29. package/src/tests/analysis.ts +63 -0
  30. package/src/tests/enrichment-visualization.ts +95 -9
  31. package/src/tests/enrichment.ts +2 -1
  32. package/src/tests/project-vocabulary.ts +39 -0
  33. package/src/tests/publish-roundtrip.ts +40 -40
  34. package/src/tests/publish-spike.ts +2 -2
  35. package/src/tests/rank-abundance.ts +85 -0
  36. package/src/utils/abundance-detection.ts +145 -0
  37. package/src/viewers/abundance-correlation.ts +208 -0
  38. package/src/viewers/enrichment-viewers.ts +58 -24
  39. package/src/viewers/rank-abundance.ts +153 -0
  40. package/src/viewers/volcano.ts +3 -0
  41. package/test-console-output-1.log +1103 -1066
  42. package/test-record-1.mp4 +0 -0
  43. package/src/tests/group-mean-correlation.ts +0 -139
  44. package/src/viewers/group-mean-correlation.ts +0 -218
package/test-record-1.mp4 CHANGED
Binary file
@@ -1,139 +0,0 @@
1
- import * as DG from 'datagrok-api/dg';
2
- import {category, test, expect} from '@datagrok-libraries/test/src/test';
3
- import {
4
- createGroupMeanCorrelation,
5
- computeGroupMeans,
6
- pearson,
7
- spearman,
8
- } from '../viewers/group-mean-correlation';
9
- import {setGroups} from '../analysis/experiment-setup';
10
- import {SEMTYPE} from '../utils/proteomics-types';
11
-
12
- /** Builds a 5-row, 4-sample fixture DataFrame with a `direction` column so the
13
- * correlation viewer's color binding has something to point at. */
14
- function makeFixtureDf(): DG.DataFrame {
15
- const cols: DG.Column[] = [];
16
- const pid = DG.Column.fromStrings('Primary Protein ID',
17
- ['P1', 'P2', 'P3', 'P4', 'P5']);
18
- pid.semType = SEMTYPE.PROTEIN_ID;
19
- cols.push(pid);
20
-
21
- const display = DG.Column.fromStrings('Display Name',
22
- ['Gene1', 'Gene2', 'Gene3', 'Gene4', 'Gene5']);
23
- display.semType = SEMTYPE.DISPLAY_NAME;
24
- cols.push(display);
25
-
26
- // Intensities — known per-row mean of [g1,g2] is monotonic for groupMeanColumnsCreated.
27
- // Row 0: g1={1,3}→mean=2, g2={5,7}→mean=6
28
- // Row 1: g1={2,4}→mean=3, g2={6,8}→mean=7
29
- // Row 2: g1={3,5}→mean=4, g2={7,9}→mean=8
30
- // Row 3: g1={4,6}→mean=5, g2={8,10}→mean=9
31
- // Row 4: g1={5,7}→mean=6, g2={9,11}→mean=10
32
- cols.push(DG.Column.fromList('double' as DG.ColumnType, 'S1', [1, 2, 3, 4, 5]));
33
- cols.push(DG.Column.fromList('double' as DG.ColumnType, 'S2', [3, 4, 5, 6, 7]));
34
- cols.push(DG.Column.fromList('double' as DG.ColumnType, 'S3', [5, 6, 7, 8, 9]));
35
- cols.push(DG.Column.fromList('double' as DG.ColumnType, 'S4', [7, 8, 9, 10, 11]));
36
-
37
- // direction column — the viewer reads this for color binding (set by Plan 14-02 ensureDirectionColumn).
38
- cols.push(DG.Column.fromStrings('direction',
39
- ['Enriched in Control', 'Enriched in Treatment', 'Not significant',
40
- 'Enriched in Control', 'Enriched in Treatment']));
41
-
42
- const df = DG.DataFrame.fromColumns(cols);
43
- setGroups(df, {
44
- group1: {name: 'Control', columns: ['S1', 'S2']},
45
- group2: {name: 'Treatment', columns: ['S3', 'S4']},
46
- });
47
- df.name = `correlation-fixture-${Math.random().toString(36).slice(2, 8)}`;
48
- return df;
49
- }
50
-
51
- category('Proteomics: 14-04', () => {
52
- test('groupMeanColumnsCreated', async () => {
53
- const df = makeFixtureDf();
54
- computeGroupMeans(df, {
55
- group1: {name: 'Control', columns: ['S1', 'S2']},
56
- group2: {name: 'Treatment', columns: ['S3', 'S4']},
57
- });
58
- const num = df.col('Numerator Mean');
59
- const den = df.col('Denominator Mean');
60
- expect(num !== null, true);
61
- expect(den !== null, true);
62
- expect(num!.semType, SEMTYPE.NUMERATOR_MEAN);
63
- expect(den!.semType, SEMTYPE.DENOMINATOR_MEAN);
64
- expect(num!.length, df.rowCount);
65
- expect(den!.length, df.rowCount);
66
- // Row 0 means: g1=(1+3)/2=2, g2=(5+7)/2=6.
67
- expect(Math.abs((num!.get(0) as number) - 2) < 1e-6, true);
68
- expect(Math.abs((den!.get(0) as number) - 6) < 1e-6, true);
69
- // Row 4 means: g1=(5+7)/2=6, g2=(9+11)/2=10.
70
- expect(Math.abs((num!.get(4) as number) - 6) < 1e-6, true);
71
- expect(Math.abs((den!.get(4) as number) - 10) < 1e-6, true);
72
- });
73
-
74
- test('groupMeanColumnsReRunSafe', async () => {
75
- const df = makeFixtureDf();
76
- const groups = {
77
- group1: {name: 'Control', columns: ['S1', 'S2']},
78
- group2: {name: 'Treatment', columns: ['S3', 'S4']},
79
- };
80
- computeGroupMeans(df, groups);
81
- computeGroupMeans(df, groups);
82
- // No duplicates after a second invocation.
83
- const numCount = df.columns.toList().filter((c) => c.name === 'Numerator Mean').length;
84
- const denCount = df.columns.toList().filter((c) => c.name === 'Denominator Mean').length;
85
- expect(numCount, 1);
86
- expect(denCount, 1);
87
- // Values still correct.
88
- const num = df.col('Numerator Mean')!;
89
- expect(Math.abs((num.get(0) as number) - 2) < 1e-6, true);
90
- });
91
-
92
- test('pearsonPerfectCorrelation', async () => {
93
- const r = pearson([1, 2, 3, 4, 5], [2, 4, 6, 8, 10]);
94
- expect(Math.abs(r - 1) < 1e-9, true);
95
- });
96
-
97
- test('pearsonNoCorrelation', async () => {
98
- const r = pearson([1, 2, 3, 4, 5], [3, 1, 4, 1, 5]);
99
- // Not exactly 0 — just within a reasonable tolerance away from 1.
100
- expect(Math.abs(r) < 0.6, true);
101
- });
102
-
103
- test('spearmanRankTies', async () => {
104
- const rho = spearman([1, 2, 2, 3, 4], [1, 2, 2, 3, 4]);
105
- expect(Math.abs(rho - 1) < 1e-9, true);
106
- });
107
-
108
- test('createGroupMeanCorrelationFactory', async () => {
109
- const df = makeFixtureDf();
110
- const sp = createGroupMeanCorrelation(df);
111
- expect(sp !== null, true);
112
- const opts: any = (sp as any).getOptions?.();
113
- const title: string = opts?.look?.title ?? '';
114
- expect(title.includes('r='), true);
115
- expect(title.includes('ρ='), true);
116
- expect(sp.props.xColumnName, 'Numerator Mean');
117
- expect(sp.props.yColumnName, 'Denominator Mean');
118
- expect(sp.props.colorColumnName, 'direction');
119
- });
120
-
121
- test('correlationDiagonalLine', async () => {
122
- const df = makeFixtureDf();
123
- createGroupMeanCorrelation(df);
124
- const lines = df.meta.formulaLines.items;
125
- const diagonal = lines.find((l: any) => {
126
- const f = (l.formula ?? '') as string;
127
- return typeof f === 'string' &&
128
- f.includes('Numerator Mean') && f.includes('Denominator Mean');
129
- });
130
- expect(diagonal !== undefined, true);
131
- expect((diagonal as any).color, '#888888');
132
- });
133
-
134
- test('correlationLabelBindsToDisplayName', async () => {
135
- const df = makeFixtureDf();
136
- const sp = createGroupMeanCorrelation(df);
137
- expect((sp.props.labelColumnNames as string[])?.includes('Display Name'), true);
138
- });
139
- });
@@ -1,218 +0,0 @@
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
- }