@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.
- package/CHANGELOG.md +16 -0
- package/README.md +18 -5
- package/dist/package-test.js +1 -1
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/docs/personas-and-capabilities.md +16 -0
- package/docs/publishing-design-rationale.md +140 -0
- package/package.json +17 -3
- package/package.png +0 -0
- package/src/analysis/imputation.ts +3 -1
- package/src/analysis/normalization.ts +9 -4
- package/src/analysis/spc-storage.ts +1 -1
- package/src/menu.ts +5 -2
- package/src/package-api.ts +10 -2
- package/src/package-test.ts +3 -1
- package/src/package.g.ts +17 -3
- package/src/package.ts +31 -15
- package/src/panels/published-analysis-panel.ts +2 -2
- package/src/publishing/assert-published-shape.ts +2 -2
- package/src/publishing/package-settings-editor.ts +125 -0
- package/src/publishing/publish-project.ts +8 -8
- package/src/publishing/publish-settings.ts +57 -3
- package/src/publishing/publish-state.ts +45 -16
- package/src/publishing/reviewer-groups.ts +24 -0
- package/src/publishing/share-dialog.ts +42 -27
- package/src/publishing/trim-dataframe.ts +8 -1
- package/src/tests/abundance-correlation.ts +135 -0
- package/src/tests/analysis.ts +63 -0
- package/src/tests/enrichment-visualization.ts +95 -9
- package/src/tests/enrichment.ts +2 -1
- package/src/tests/project-vocabulary.ts +39 -0
- package/src/tests/publish-roundtrip.ts +40 -40
- package/src/tests/publish-spike.ts +2 -2
- package/src/tests/rank-abundance.ts +85 -0
- package/src/utils/abundance-detection.ts +145 -0
- package/src/viewers/abundance-correlation.ts +208 -0
- package/src/viewers/enrichment-viewers.ts +58 -24
- package/src/viewers/rank-abundance.ts +153 -0
- package/src/viewers/volcano.ts +3 -0
- package/test-console-output-1.log +1103 -1066
- package/test-record-1.mp4 +0 -0
- package/src/tests/group-mean-correlation.ts +0 -139
- package/src/viewers/group-mean-correlation.ts +0 -218
|
@@ -15,6 +15,12 @@ boundary to be real, not conventional.
|
|
|
15
15
|
| **Proteomics analyst** (data generator) | Builds the analysis: import → annotate → normalize → impute → differential expression | A defensible, reproducible, FDR-controlled result they can stand behind and hand off |
|
|
16
16
|
| **Biology scientist** (data consumer / reviewer) | Interprets a finished analysis: volcano, enrichment, per-protein annotation | To understand *what changed and what it means* — without touching (or being able to change) the analysis itself |
|
|
17
17
|
|
|
18
|
+
A third role sits **outside** this pair: the **package administrator** (deployment owner)
|
|
19
|
+
configures the Share-for-review governance — the controlled list of projects analysts may
|
|
20
|
+
share under, the default reviewer team, and the review Spaces (see
|
|
21
|
+
[Configuration](../README.md#configuration)). They neither build nor interpret analyses; they
|
|
22
|
+
set, once per deployment, *where* shared results land and *under what project names*.
|
|
23
|
+
|
|
18
24
|
The dividing line is **"the interpretation"** — the set of decisions that determine what the
|
|
19
25
|
result *says*: group assignment, normalization, imputation, the DE method, and the
|
|
20
26
|
significance thresholds. The analyst owns these. The biologist reads the consequences and
|
|
@@ -85,6 +91,13 @@ group has any Edit / Share / Delete right at the project, child-space, or umbrel
|
|
|
85
91
|
level. So through the **shared snapshot**, a biologist genuinely cannot change the
|
|
86
92
|
interpretation — they get a read-only copy.
|
|
87
93
|
|
|
94
|
+
The dialog itself is **governed by package settings the administrator maintains** (Plugins →
|
|
95
|
+
Proteomics → Settings). The analyst files the snapshot under a **project** chosen from an
|
|
96
|
+
admin-curated controlled vocabulary — they cannot invent project names, and cannot share at
|
|
97
|
+
all until an admin has populated the list — and it goes to a reviewer **team** whose default
|
|
98
|
+
the admin can preset. This keeps shared analyses landing under approved project names, in the
|
|
99
|
+
intended teams and Spaces, rather than letting each analyst decide that governance ad hoc.
|
|
100
|
+
|
|
88
101
|
## The gap
|
|
89
102
|
|
|
90
103
|
Enforcement today is **data-shape + access**, not **identity**. Concretely:
|
|
@@ -163,3 +176,6 @@ an explicit read-only "Reviewer mode" toggle vs. inferring it from group members
|
|
|
163
176
|
- `src/menu.ts` — `sampleLevelDisabledReason`, the `sampleOnly` gate
|
|
164
177
|
- `src/package.ts` — `requireSampleLevelData`, the handler-level guards
|
|
165
178
|
- `src/publishing/` — the Share for Review view-only publish flow
|
|
179
|
+
- `src/publishing/package-settings-editor.ts` — the administrator's settings editor
|
|
180
|
+
(project vocabulary, default reviewer team)
|
|
181
|
+
- [`README.md` → Configuration](../README.md#configuration) — the admin-configurable package settings
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Publishing (Share for Review) — design rationale
|
|
2
|
+
|
|
3
|
+
Why the Share-for-review subsystem is built the way it is, and **which parts are permanent
|
|
4
|
+
architecture vs. workaround weight that the base platform could absorb.**
|
|
5
|
+
|
|
6
|
+
This is the reference behind the [personas & capabilities](./personas-and-capabilities.md) doc's
|
|
7
|
+
enforcement claims. That doc says *what* the read-only boundary is; this says *why the code takes
|
|
8
|
+
the shape it does to make that boundary real* — and where we'd rather the platform did the work.
|
|
9
|
+
|
|
10
|
+
Audience: anyone questioning the complexity of `src/publishing/`, and anyone planning what
|
|
11
|
+
platform capabilities to grow so this package can shed code. Per our standing preference, a
|
|
12
|
+
workaround defended as *temporary, with a named platform successor* is a stronger position than
|
|
13
|
+
one defended as permanent design.
|
|
14
|
+
|
|
15
|
+
## The one-sentence defense
|
|
16
|
+
|
|
17
|
+
Most of `src/publishing/` (~1,900 lines across 9 files) is not "sharing." It is the machinery to
|
|
18
|
+
**guarantee three properties** a compliance-sensitive review needs, none of which the platform
|
|
19
|
+
gave us for free at build time:
|
|
20
|
+
|
|
21
|
+
1. **Provably read-only** — the reviewer group *cannot* alter the interpretation.
|
|
22
|
+
2. **Provably minimized** — only the interpretation ships; raw/again-identifying data does not.
|
|
23
|
+
3. **Provably reload-survivable + versioned** — what the reviewer opens is what was published,
|
|
24
|
+
every version is immutable, and supersession is auditable.
|
|
25
|
+
|
|
26
|
+
The naming scheme and the use of Spaces (the two most visible choices) are *downstream* of these
|
|
27
|
+
guarantees, not the point of them.
|
|
28
|
+
|
|
29
|
+
## Design choices, grouped by the guarantee they serve
|
|
30
|
+
|
|
31
|
+
Legend for **Verdict**: 🟢 keep in package (genuinely Proteomics-specific) · 🟡 push to platform
|
|
32
|
+
(generic capability we had to hand-roll) · 🔵 hybrid (platform does the mechanism, package keeps the policy)
|
|
33
|
+
|
|
34
|
+
### A. Read-only is enforced, not assumed
|
|
35
|
+
|
|
36
|
+
| # | Choice | Where | Why | Verdict |
|
|
37
|
+
|---|---|---|---|---|
|
|
38
|
+
| A1 | Grant **View directly on the Project**, not via Space inheritance | `publish-project.ts:323` | Spike A2: `permissions.get(project)` does **not** surface Space-inherited grants, so an inherited grant is unverifiable. | 🟡 |
|
|
39
|
+
| A2 | **Verify-and-rollback ACL gate** — after granting, re-read permissions across three rings (project, child Space, umbrella Space); any `edit`/`share`/`delete` for the reviewer group ⇒ delete the project and abort | `publish-project.ts:331-370` | The feature refuses to ship what it cannot prove is read-only. This is the single most important — and most expensive — thing the code does. | 🔵 |
|
|
40
|
+
| A3 | Share to a **group/team, never individuals**; eligible groups exclude hidden, personal, and "All users" | `reviewer-groups.ts:9-19` | Review is a team activity; "All users" would defeat the point of a scoped share. | 🔵 |
|
|
41
|
+
|
|
42
|
+
### B. Data minimization — a trimmed snapshot, not a live link
|
|
43
|
+
|
|
44
|
+
| # | Choice | Where | Why | Verdict |
|
|
45
|
+
|---|---|---|---|---|
|
|
46
|
+
| B1 | **Deep clone; source never mutated** — the publish path operates only on the clone | `trim-dataframe.ts:75-120` | Reviewers get a frozen point-in-time copy; later edits to the working analysis cannot leak. | 🟢 |
|
|
47
|
+
| B2 | **Column allowlist** (Protein ID, Gene, log2FC, p, adj.p, significant, [direction, quantities]); everything else dropped | `trim-dataframe.ts:108-120` | The generalized form of the 1.2.0 "client-name removal." A *whitelist* means a new, unexpected column is excluded by default — safer than a blocklist. | 🟢 |
|
|
48
|
+
|
|
49
|
+
### C. Provenance is embedded and redundant
|
|
50
|
+
|
|
51
|
+
| # | Choice | Where | Why | Verdict |
|
|
52
|
+
|---|---|---|---|---|
|
|
53
|
+
| C1 | Rich metadata baked into the snapshot (who/email/DE method/thresholds/version/UUID/date) | `publish-state.ts:71-92`, `trim-dataframe.ts:158-174` | The snapshot is self-describing — provenance travels with the data, not a side channel. | 🟢 |
|
|
54
|
+
| C2 | **Dual-write belt-and-braces** — every field written as *both* a DataFrame tag *and* a hidden `_meta_*` column; read column-first, tag-second | `publish-state.ts:24-60,137-225` | Defends against serialization paths that strip tags but keep columns (or vice versa) — an observed failure mode. | 🟡 |
|
|
55
|
+
| C3 | Numeric metadata stored **as strings** | `trim-dataframe.ts:153-167` | `addNewFloat` is Float32; string storage preserves exact double thresholds. | 🔵 |
|
|
56
|
+
|
|
57
|
+
### D. Versioning is first-class and non-destructive
|
|
58
|
+
|
|
59
|
+
| # | Choice | Where | Why | Verdict |
|
|
60
|
+
|---|---|---|---|---|
|
|
61
|
+
| D1 | **Opinionated naming** `Proteomics-Review-<slug>-v<N>-<date>`; structural suffix fixed (not configurable) | `publish-project.ts:244-250`, `publish-settings.ts` | The `-v<N>-` segment is parsed to detect prior versions; letting it vary would break version detection. Prefix/Space *are* configurable. | 🔵 |
|
|
62
|
+
| D2 | **Republish detection** by name pattern → bump to v(N+1); prior version stays available (never overwritten) | `publish-state.ts:279-328`, `publish-project.ts:138-140` | Immutable, auditable version history. | 🔵 |
|
|
63
|
+
| D3 | **Bidirectional supersede pointers** (`supersedes`/`supersededBy`) on both projects, dual-written to project options *and* the DataFrame | `publish-project.ts:436-478` | Either version can be navigated to the other; survives whichever persistence path holds. | 🟡 |
|
|
64
|
+
|
|
65
|
+
### E. Round-trip fidelity is verified
|
|
66
|
+
|
|
67
|
+
| # | Choice | Where | Why | Verdict |
|
|
68
|
+
|---|---|---|---|---|
|
|
69
|
+
| E1 | **Reopen-and-assert gate** (default on) — `closeAll` → reopen → assert published shape survives a reload; rollback on failure | `publish-project.ts:372-434`, `assert-published-shape.ts` | Proves the reviewer sees exactly what was published, before telling the analyst it worked. | 🟡 |
|
|
70
|
+
| E2 | **Formula-line self-heal** — a serializer strips volcano threshold lines; re-apply from tags and re-check once before rollback | `publish-project.ts:401-433`, `post-open-recovery.ts` | Works around look-config loss on project round-trip (Phase 13 evidence). | 🟡 |
|
|
71
|
+
| E3 | **Re-render viewers on the trimmed snapshot** so the shared dashboard is self-contained | `publish-project.ts:265-295` | The published project must stand alone — no dependency on the analyst's session tables. | 🔵 |
|
|
72
|
+
| E4 | **Post-open recovery autostart** — re-applies formula lines and re-wires enrichment↔volcano cross-highlight on reopen | `post-open-recovery.ts`, `package.ts:1010-1037` | Interaction subscriptions and look config don't survive serialization; rebuild them on open. | 🟡 |
|
|
73
|
+
|
|
74
|
+
### F. Where content lands + idempotency
|
|
75
|
+
|
|
76
|
+
| # | Choice | Where | Why | Verdict |
|
|
77
|
+
|---|---|---|---|---|
|
|
78
|
+
| F1 | Publish to **named Spaces, not a personal workspace** | `publish-project.ts:178-239` | A shared, governed location the reviewer team can browse — not tied to one user's private space. | 🔵 |
|
|
79
|
+
| F2 | **Two-level Space hierarchy** — umbrella `Proteomics-Reviews` + per-project child Space | `publish-project.ts:178-239` | Groups all reviews under one root, one child per project — a navigable tree instead of a flat pile. | 🔵 |
|
|
80
|
+
| F3 | **Create-first-then-enumerate idempotency** for Spaces; fall back to `spaces.list()` on "already exists" | `publish-project.ts:184-238` | The smart-filter API does not reliably surface Space-flagged Projects, so enumeration is the reliable lookup. | 🟡 |
|
|
81
|
+
|
|
82
|
+
### G. Governance + workflow UX
|
|
83
|
+
|
|
84
|
+
| # | Choice | Where | Why | Verdict |
|
|
85
|
+
|---|---|---|---|---|
|
|
86
|
+
| G1 | **Controlled project vocabulary** — analyst picks from an admin-maintained list, never free-types | `share-dialog.ts:57-73`, `publish-settings.ts` | Keeps shared results filed under sanctioned project names; a governance control. | 🟢 |
|
|
87
|
+
| G2 | **Precondition gate** — share only available after DE completes | `share-dialog.ts:31-34` | There is no interpretation to review until DE has run. | 🟢 |
|
|
88
|
+
| G3 | **Reactive confirmation summary** — exact resulting name/team/supersede status shown before OK | `share-dialog.ts:109-151` | No surprises; the analyst sees the outcome before committing. | 🟢 |
|
|
89
|
+
| G4 | **Workspace restoration** after publish — tear down verification artifacts, restore the analyst's original view | `publish-project.ts:480-506` | The verify/supersede steps `closeAll` repeatedly; put the analyst back where they were. | 🔵 |
|
|
90
|
+
| G5 | **Full package-settings configurability** (Space, prefix, default team, vocabulary, verify default) | `publish-settings.ts` | Retarget a deployment without a code change. | 🟢 |
|
|
91
|
+
|
|
92
|
+
## Mapping to platform capabilities
|
|
93
|
+
|
|
94
|
+
Reading the 🟡/🔵 rows top-down, the workaround weight collapses to a small number of platform
|
|
95
|
+
gaps. If the platform closed these, the package could delete or thin most of `src/publishing/`.
|
|
96
|
+
|
|
97
|
+
| Platform gap (observed) | Evidence in code | What a platform capability would give us | Package code it retires |
|
|
98
|
+
|---|---|---|---|
|
|
99
|
+
| **P1. No first-class "publish read-only snapshot to a group" primitive** | The entire orchestrator exists to compose this from lower-level parts | A single `publishReadOnly(entity, group, {snapshot, immutable})` that grants view-only, freezes, and returns a stable handle | Most of `publish-project.ts` (A1, A2, F1–F3) |
|
|
100
|
+
| **P2. `permissions.get` doesn't reflect Space-inherited grants; may not report share/delete** | Spike A1/A2 notes at `publish-project.ts:113-118,331-370` | A permissions read that returns the *effective* ACL (own + inherited) across all verbs | A2 three-ring check collapses to one `get`; A1 direct-grant workaround unneeded |
|
|
101
|
+
| **P3. Serialization strips look config (formula lines) and interaction subscriptions** | E2/E4; `post-open-recovery.ts`, `assert-published-shape.ts:11-17` | Viewer look + formula lines + declared subscriptions persist across `save→open` | E2, E4 self-heal and recovery hook; part of E1's assertions |
|
|
102
|
+
| **P4. Tag map / metadata not reliably durable across `clone`/`save`** | C2 dual-write; `publish-state.ts:227-255`, `trim-dataframe.ts:20` | Durable, typed entity/DataFrame metadata that survives clone and round-trip | C2 `_meta_*` columns; the column-first/tag-second reader in `getPublishedMetadata` |
|
|
103
|
+
| **P5. Smart-filter can't reliably find Space-flagged Projects** | F3 note at `publish-project.ts:184-203` | `dapi.projects.filter(isSpace=…).first()` that actually works | F3 create-first-then-enumerate dance |
|
|
104
|
+
| **P6. No immutable/versioned entity semantics** | D1–D3 name-parsing + manual supersede pointers | Native entity versioning with supersede links | D2/D3 name-pattern versioning and dual-written pointers; D1's suffix could relax |
|
|
105
|
+
| **P7. No round-trip integrity check** | E1; `assert-published-shape.ts` | If P3+P4 land, publish *is* faithful by construction and E1 becomes optional | E1 gate downgrades to a test-only assertion |
|
|
106
|
+
|
|
107
|
+
### What stays in the package regardless
|
|
108
|
+
|
|
109
|
+
The 🟢 rows are the genuine Proteomics policy and don't belong in the platform:
|
|
110
|
+
|
|
111
|
+
- **B1/B2** — *which* columns constitute "the interpretation" (the allowlist) is domain knowledge.
|
|
112
|
+
- **C1** — *which* provenance fields matter (DE method, FC/p thresholds) is domain knowledge.
|
|
113
|
+
- **G1/G2/G3/G5** — the vocabulary, the DE precondition, the confirmation copy, the settings surface.
|
|
114
|
+
|
|
115
|
+
Even under a fully-featured platform, the package would still call a `publishReadOnly` primitive,
|
|
116
|
+
hand it a domain-trimmed snapshot with domain provenance, and gate the UI on DE completion. That
|
|
117
|
+
residue is small — roughly `trim-dataframe.ts` + `share-dialog.ts` + `publish-settings.ts`.
|
|
118
|
+
|
|
119
|
+
## Recommended platform asks, prioritized
|
|
120
|
+
|
|
121
|
+
1. **P1 + P2 together** — the read-only-to-group primitive with a trustworthy effective-permissions
|
|
122
|
+
read. Highest leverage: retires the bulk of the orchestrator *and* the code we're least confident
|
|
123
|
+
in (the ACL gate can only be as correct as `permissions.get`).
|
|
124
|
+
2. **P3 + P4** — durable look/subscription/metadata across round-trip. Retires the self-heal and
|
|
125
|
+
recovery machinery, which is the most brittle code in the subsystem.
|
|
126
|
+
3. **P6** — native immutable versioning. Retires the name-parsing version scheme and lets the naming
|
|
127
|
+
suffix relax.
|
|
128
|
+
4. **P5, P7** — fall out naturally once the above land.
|
|
129
|
+
|
|
130
|
+
## Honest caveats to raise proactively
|
|
131
|
+
|
|
132
|
+
- **The ACL gate is only as strong as `permissions.get`.** If P2 is real (the API may not report
|
|
133
|
+
`share`/`delete`), the three-ring check at `publish-project.ts:358-361` cannot detect those leaks
|
|
134
|
+
today. Worth verifying against the current platform release before claiming the guarantee is
|
|
135
|
+
airtight. *(This claim is inferred from the A1 spike note and should be re-checked live.)*
|
|
136
|
+
- **Several verify/rollback steps `closeAll` the user's session.** G4 restoration mitigates it, but
|
|
137
|
+
it's inherent to doing round-trip verification in the same client session; P7 landing would remove
|
|
138
|
+
the need entirely.
|
|
139
|
+
- **This is workaround-heavy by design, and that's defensible** *because* each workaround has a named
|
|
140
|
+
platform successor above — not because the complexity is intrinsic to sharing.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datagrok/proteomics",
|
|
3
3
|
"friendlyName": "Proteomics",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ed Jaeger",
|
|
7
7
|
"email": "ejaeger@datagrok.ai"
|
|
@@ -61,24 +61,38 @@
|
|
|
61
61
|
"properties": [
|
|
62
62
|
{
|
|
63
63
|
"name": "reviewSpaceName",
|
|
64
|
-
"description": "Umbrella Space that shared review snapshots are published under. Each
|
|
64
|
+
"description": "Umbrella Space that shared review snapshots are published under. Each project gets a child Space inside it.",
|
|
65
65
|
"propertyType": "string",
|
|
66
66
|
"defaultValue": "Proteomics-Reviews",
|
|
67
67
|
"nullable": false
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"name": "reviewNamePrefix",
|
|
71
|
-
"description": "Prefix for shared review project names. The structural '-<
|
|
71
|
+
"description": "Prefix for shared review project names. The structural '-<project>-v<version>-<date>' suffix stays fixed so re-share version detection keeps working.",
|
|
72
72
|
"propertyType": "string",
|
|
73
73
|
"defaultValue": "Proteomics-Review",
|
|
74
74
|
"nullable": false
|
|
75
75
|
},
|
|
76
|
+
{
|
|
77
|
+
"name": "defaultReviewerGroup",
|
|
78
|
+
"description": "Team pre-selected in the Share for Review dialog's 'Share with team' picker. Leave blank to default to the first available team.",
|
|
79
|
+
"propertyType": "string",
|
|
80
|
+
"defaultValue": "",
|
|
81
|
+
"nullable": true
|
|
82
|
+
},
|
|
76
83
|
{
|
|
77
84
|
"name": "verifyPublishedDashboard",
|
|
78
85
|
"description": "After publishing, re-open the shared dashboard to verify it survives a reload. Safer for client deliverables; turn off for faster demo shares.",
|
|
79
86
|
"propertyType": "bool",
|
|
80
87
|
"defaultValue": true,
|
|
81
88
|
"nullable": false
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "projectVocabulary",
|
|
92
|
+
"description": "Comma-separated controlled list of project names an analyst can share under. Package administrators add, edit, or remove names here; the Share for Review dialog offers exactly these as a dropdown (no free text).",
|
|
93
|
+
"propertyType": "string",
|
|
94
|
+
"defaultValue": "",
|
|
95
|
+
"nullable": true
|
|
82
96
|
}
|
|
83
97
|
],
|
|
84
98
|
"overrides": {
|
package/package.png
ADDED
|
Binary file
|
|
@@ -7,7 +7,9 @@ import {getGroups} from './experiment-setup';
|
|
|
7
7
|
|
|
8
8
|
/** Generates a random number from a normal distribution using Box-Muller transform. */
|
|
9
9
|
function randomNormal(mean: number, stdev: number): number {
|
|
10
|
-
|
|
10
|
+
// Draw u1 in (0, 1] rather than [0, 1): Math.random() can return exactly 0,
|
|
11
|
+
// which would give log(0) = -Infinity and an Infinity/NaN draw.
|
|
12
|
+
const u1 = 1 - Math.random();
|
|
11
13
|
const u2 = Math.random();
|
|
12
14
|
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
13
15
|
return mean + stdev * z;
|
|
@@ -59,7 +59,10 @@ export function quantileNormalize(df: DG.DataFrame, colNames: string[]): void {
|
|
|
59
59
|
|
|
60
60
|
// Find the maximum number of non-null values across columns
|
|
61
61
|
const maxValid = Math.max(...colData.map((d) => d.indices.length));
|
|
62
|
-
|
|
62
|
+
// Need at least 2 values in the most-populated column: the rank interpolation
|
|
63
|
+
// below divides by (maxValid - 1), and quantile normalization is undefined for
|
|
64
|
+
// a single quantile point.
|
|
65
|
+
if (maxValid < 2) return;
|
|
63
66
|
|
|
64
67
|
// Compute rank means using scaled rank alignment
|
|
65
68
|
// For each rank position r (0..maxValid-1), compute the mean of values
|
|
@@ -69,7 +72,7 @@ export function quantileNormalize(df: DG.DataFrame, colNames: string[]): void {
|
|
|
69
72
|
let sum = 0;
|
|
70
73
|
let count = 0;
|
|
71
74
|
for (const d of colData) {
|
|
72
|
-
if (d.indices.length
|
|
75
|
+
if (d.indices.length < 2) continue;
|
|
73
76
|
// Map rank r in [0..maxValid-1] to position in this column's sorted values
|
|
74
77
|
const pos = (r / (maxValid - 1)) * (d.indices.length - 1);
|
|
75
78
|
// Interpolate between floor and ceil positions
|
|
@@ -86,10 +89,12 @@ export function quantileNormalize(df: DG.DataFrame, colNames: string[]): void {
|
|
|
86
89
|
// Replace each column's sorted values with the corresponding rank mean
|
|
87
90
|
for (const d of colData) {
|
|
88
91
|
const n = d.indices.length;
|
|
89
|
-
|
|
92
|
+
// n < 2: a single-value column has no rank spread; (r / (n - 1)) would be
|
|
93
|
+
// 0/0 = NaN. Leave its lone value unchanged rather than corrupting it.
|
|
94
|
+
if (n < 2) continue;
|
|
90
95
|
for (let r = 0; r < n; r++) {
|
|
91
96
|
// Map this column's rank to the global rank mean position
|
|
92
|
-
const globalPos =
|
|
97
|
+
const globalPos = (r / (n - 1)) * (maxValid - 1);
|
|
93
98
|
const lo = Math.floor(globalPos);
|
|
94
99
|
const hi = Math.min(Math.ceil(globalPos), maxValid - 1);
|
|
95
100
|
const frac = globalPos - lo;
|
|
@@ -111,7 +111,7 @@ function baselinePath(instrumentId: string, opts?: StorageOpts): string {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
// =====================================================================================
|
|
114
|
-
// Slugify — T-16-01 mitigation. Mirrors publishing/publish-state.ts:
|
|
114
|
+
// Slugify — T-16-01 mitigation. Mirrors publishing/publish-state.ts:slugifyProject but
|
|
115
115
|
// PRESERVES case so instrument names like 'QExactive-01' round-trip exactly.
|
|
116
116
|
// =====================================================================================
|
|
117
117
|
|
package/src/menu.ts
CHANGED
|
@@ -40,10 +40,11 @@ export interface ProteomicsMenuHandlers {
|
|
|
40
40
|
showVolcanoPlot: () => void;
|
|
41
41
|
showHeatmap: () => void;
|
|
42
42
|
showPcaPlot: () => void;
|
|
43
|
-
showGroupMeanCorrelation: () => void;
|
|
44
43
|
showQcDashboard: () => void;
|
|
45
44
|
showSpcDashboard: () => void;
|
|
46
45
|
enrichmentCharts: () => void;
|
|
46
|
+
rankAbundance: () => void;
|
|
47
|
+
abundanceCorrelation: () => void;
|
|
47
48
|
showAllVisualizations: () => void;
|
|
48
49
|
shareAnalysisForReview: () => void;
|
|
49
50
|
}
|
|
@@ -121,10 +122,12 @@ export function buildProteomicsRibbonMenu(view: DG.TableView, h: ProteomicsMenuH
|
|
|
121
122
|
viz.item('Volcano Plot...', h.showVolcanoPlot);
|
|
122
123
|
viz.item('Heatmap...', h.showHeatmap, null, sampleOnly);
|
|
123
124
|
viz.item('PCA...', h.showPcaPlot, null, sampleOnly);
|
|
124
|
-
viz.item('Group-Mean Correlation...', h.showGroupMeanCorrelation, null, sampleOnly);
|
|
125
125
|
viz.item('QC Dashboard...', h.showQcDashboard, null, sampleOnly);
|
|
126
126
|
viz.item('SPC Dashboard...', h.showSpcDashboard);
|
|
127
127
|
viz.item('Enrichment Charts...', h.enrichmentCharts);
|
|
128
|
+
// Not sampleOnly: both work on Candidates (AVG Group Quantity) and annotated Reports.
|
|
129
|
+
viz.item('Rank–Abundance', h.rankAbundance);
|
|
130
|
+
viz.item('Abundance Correlation', h.abundanceCorrelation);
|
|
128
131
|
viz.item('Show All Visualizations...', h.showAllVisualizations, null, sampleOnly);
|
|
129
132
|
|
|
130
133
|
const share = root.group('Share');
|
package/src/package-api.ts
CHANGED
|
@@ -95,8 +95,8 @@ export namespace funcs {
|
|
|
95
95
|
return await grok.functions.call('Proteomics:ShowPcaPlot', {});
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
export async function
|
|
99
|
-
return await grok.functions.call('Proteomics:
|
|
98
|
+
export async function abundanceCorrelation(): Promise<void> {
|
|
99
|
+
return await grok.functions.call('Proteomics:AbundanceCorrelation', {});
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
export async function showQcDashboard(): Promise<void> {
|
|
@@ -119,6 +119,10 @@ export namespace funcs {
|
|
|
119
119
|
return await grok.functions.call('Proteomics:ExportEnrichmentInputs', {});
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
export async function rankAbundance(): Promise<void> {
|
|
123
|
+
return await grok.functions.call('Proteomics:RankAbundance', {});
|
|
124
|
+
}
|
|
125
|
+
|
|
122
126
|
export async function enrichmentCharts(): Promise<void> {
|
|
123
127
|
return await grok.functions.call('Proteomics:EnrichmentCharts', {});
|
|
124
128
|
}
|
|
@@ -153,6 +157,10 @@ export namespace funcs {
|
|
|
153
157
|
return await grok.functions.call('Proteomics:PublishedAnalysisPanelWidget', { proteinId });
|
|
154
158
|
}
|
|
155
159
|
|
|
160
|
+
export async function proteomicsSettingsEditor(propList: any ): Promise<any> {
|
|
161
|
+
return await grok.functions.call('Proteomics:ProteomicsSettingsEditor', { propList });
|
|
162
|
+
}
|
|
163
|
+
|
|
156
164
|
export async function recoverPublishedProjectsOnStartup(): Promise<void> {
|
|
157
165
|
return await grok.functions.call('Proteomics:RecoverPublishedProjectsOnStartup', {});
|
|
158
166
|
}
|
package/src/package-test.ts
CHANGED
|
@@ -11,6 +11,8 @@ import './tests/enrichment-visualization';
|
|
|
11
11
|
import './tests/spectronaut-parser';
|
|
12
12
|
import './tests/spectronaut-candidates-parser';
|
|
13
13
|
import './tests/spectronaut-candidates-e2e';
|
|
14
|
+
import './tests/rank-abundance';
|
|
15
|
+
import './tests/abundance-correlation';
|
|
14
16
|
import './tests/fragpipe-parser';
|
|
15
17
|
import './tests/fragpipe-e2e';
|
|
16
18
|
import './tests/subcellular-location';
|
|
@@ -20,9 +22,9 @@ import './tests/volcano';
|
|
|
20
22
|
import './tests/gene-label-resolver';
|
|
21
23
|
import './tests/smart-pathway-filter';
|
|
22
24
|
import './tests/uniprot-panel';
|
|
23
|
-
import './tests/group-mean-correlation';
|
|
24
25
|
import './tests/publish-spike';
|
|
25
26
|
import './tests/publish-roundtrip';
|
|
27
|
+
import './tests/project-vocabulary';
|
|
26
28
|
import './tests/spc-formula-lines-spike';
|
|
27
29
|
import './tests/spc';
|
|
28
30
|
|
package/src/package.g.ts
CHANGED
|
@@ -83,9 +83,9 @@ export async function showPcaPlot() : Promise<void> {
|
|
|
83
83
|
await PackageFunctions.showPcaPlot();
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
//name:
|
|
87
|
-
export async function
|
|
88
|
-
await PackageFunctions.
|
|
86
|
+
//name: abundanceCorrelation
|
|
87
|
+
export async function abundanceCorrelation() : Promise<void> {
|
|
88
|
+
await PackageFunctions.abundanceCorrelation();
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
//name: showQcDashboard
|
|
@@ -113,6 +113,11 @@ export async function exportEnrichmentInputs() : Promise<void> {
|
|
|
113
113
|
await PackageFunctions.exportEnrichmentInputs();
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
//name: rankAbundance
|
|
117
|
+
export async function rankAbundance() : Promise<void> {
|
|
118
|
+
await PackageFunctions.rankAbundance();
|
|
119
|
+
}
|
|
120
|
+
|
|
116
121
|
//name: enrichmentCharts
|
|
117
122
|
export async function enrichmentCharts() : Promise<void> {
|
|
118
123
|
await PackageFunctions.enrichmentCharts();
|
|
@@ -155,6 +160,15 @@ export function publishedAnalysisPanelWidget(proteinId: string) : any {
|
|
|
155
160
|
return PackageFunctions.publishedAnalysisPanelWidget(proteinId);
|
|
156
161
|
}
|
|
157
162
|
|
|
163
|
+
//name: Proteomics package settings editor
|
|
164
|
+
//tags: packageSettingsEditor
|
|
165
|
+
//input: object propList
|
|
166
|
+
//output: widget result
|
|
167
|
+
//meta.role: packageSettingsEditor
|
|
168
|
+
export async function proteomicsSettingsEditor(propList: any) : Promise<any> {
|
|
169
|
+
return await PackageFunctions.proteomicsSettingsEditor(propList);
|
|
170
|
+
}
|
|
171
|
+
|
|
158
172
|
//tags: autostart
|
|
159
173
|
//meta.autostartImmediate: true
|
|
160
174
|
export async function recoverPublishedProjectsOnStartup() : Promise<void> {
|
package/src/package.ts
CHANGED
|
@@ -30,15 +30,17 @@ import {createExpressionHeatmap} from './viewers/heatmap';
|
|
|
30
30
|
import {createPcaPlot} from './viewers/pca-plot';
|
|
31
31
|
import {openQcDashboard} from './viewers/qc-dashboard';
|
|
32
32
|
import {openSpcDashboard} from './viewers/spc-dashboard';
|
|
33
|
-
import {
|
|
33
|
+
import {openAbundanceCorrelation} from './viewers/abundance-correlation';
|
|
34
34
|
import {uniprotPanel} from './panels/uniprot-panel';
|
|
35
35
|
import {focusProtein} from './panels/protein-focus';
|
|
36
36
|
import {publishedAnalysisPanel} from './panels/published-analysis-panel';
|
|
37
37
|
import {showShareForReviewDialog} from './publishing/share-dialog';
|
|
38
|
+
import {proteomicsSettingsEditorWidget} from './publishing/package-settings-editor';
|
|
38
39
|
import {recoverPublishedProject} from './publishing/post-open-recovery';
|
|
39
40
|
import {isPublished} from './publishing/publish-state';
|
|
40
41
|
import {showEnrichmentDialog} from './analysis/enrichment';
|
|
41
42
|
import {openEnrichmentVisualization} from './viewers/enrichment-viewers';
|
|
43
|
+
import {openRankAbundance} from './viewers/rank-abundance';
|
|
42
44
|
import {findColumn} from './utils/column-detection';
|
|
43
45
|
import {SEMTYPE, DEFAULT_FC_THRESHOLD, DEFAULT_P_THRESHOLD} from './utils/proteomics-types';
|
|
44
46
|
import {buildProteomicsRibbonMenu} from './menu';
|
|
@@ -341,10 +343,11 @@ export class PackageFunctions {
|
|
|
341
343
|
showVolcanoPlot: () => PackageFunctions.showVolcanoPlot(),
|
|
342
344
|
showHeatmap: () => PackageFunctions.showHeatmap(),
|
|
343
345
|
showPcaPlot: () => PackageFunctions.showPcaPlot(),
|
|
344
|
-
|
|
346
|
+
abundanceCorrelation: () => PackageFunctions.abundanceCorrelation(),
|
|
345
347
|
showQcDashboard: () => PackageFunctions.showQcDashboard(),
|
|
346
348
|
showSpcDashboard: () => PackageFunctions.showSpcDashboard(),
|
|
347
349
|
enrichmentCharts: () => PackageFunctions.enrichmentCharts(),
|
|
350
|
+
rankAbundance: () => PackageFunctions.rankAbundance(),
|
|
348
351
|
showAllVisualizations: () => PackageFunctions.showAllVisualizations(),
|
|
349
352
|
shareAnalysisForReview: () => PackageFunctions.shareAnalysisForReview(),
|
|
350
353
|
};
|
|
@@ -824,19 +827,12 @@ export class PackageFunctions {
|
|
|
824
827
|
}
|
|
825
828
|
|
|
826
829
|
@grok.decorators.func()
|
|
827
|
-
static async
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
const groups = getGroups(df);
|
|
834
|
-
if (!groups) {
|
|
835
|
-
grok.shell.warning('Annotate experimental groups first (Proteomics | Annotate Experiment)');
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
const sp = createGroupMeanCorrelation(df);
|
|
839
|
-
tv.addViewer(sp);
|
|
830
|
+
static async abundanceCorrelation(): Promise<void> {
|
|
831
|
+
const df = grok.shell.tv?.dataFrame;
|
|
832
|
+
if (!df) { grok.shell.warning('No table open'); return; }
|
|
833
|
+
// Works on any analyzed protein table (Candidates or annotated Report) —
|
|
834
|
+
// openAbundanceCorrelation itself warns when no per-condition abundance is present.
|
|
835
|
+
openAbundanceCorrelation(df);
|
|
840
836
|
}
|
|
841
837
|
|
|
842
838
|
@grok.decorators.func()
|
|
@@ -898,6 +894,15 @@ export class PackageFunctions {
|
|
|
898
894
|
showEnrichmentInputExportDialog(df);
|
|
899
895
|
}
|
|
900
896
|
|
|
897
|
+
@grok.decorators.func()
|
|
898
|
+
static async rankAbundance(): Promise<void> {
|
|
899
|
+
const df = grok.shell.tv?.dataFrame;
|
|
900
|
+
if (!df) { grok.shell.warning('No table open'); return; }
|
|
901
|
+
// Works on any analyzed protein table (Candidates or annotated Report) —
|
|
902
|
+
// openRankAbundance itself warns when no per-condition abundance is present.
|
|
903
|
+
openRankAbundance(df);
|
|
904
|
+
}
|
|
905
|
+
|
|
901
906
|
@grok.decorators.func()
|
|
902
907
|
static async enrichmentCharts(): Promise<void> {
|
|
903
908
|
const tv = grok.shell.tv;
|
|
@@ -990,6 +995,17 @@ export class PackageFunctions {
|
|
|
990
995
|
return publishedAnalysisPanel(proteinId);
|
|
991
996
|
}
|
|
992
997
|
|
|
998
|
+
@grok.decorators.func({
|
|
999
|
+
name: 'Proteomics package settings editor',
|
|
1000
|
+
meta: {role: 'packageSettingsEditor'},
|
|
1001
|
+
tags: ['packageSettingsEditor'],
|
|
1002
|
+
})
|
|
1003
|
+
static async proteomicsSettingsEditor(
|
|
1004
|
+
@grok.decorators.param({name: 'propList', type: 'object'}) propList: DG.Property[],
|
|
1005
|
+
): Promise<DG.Widget> {
|
|
1006
|
+
return proteomicsSettingsEditorWidget(propList);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
993
1009
|
@grok.decorators.func({tags: ['autostart'], meta: {autostartImmediate: 'true'}})
|
|
994
1010
|
static async recoverPublishedProjectsOnStartup(): Promise<void> {
|
|
995
1011
|
const tryEvent: any =
|
|
@@ -12,7 +12,7 @@ import {getGroups} from '../analysis/experiment-setup';
|
|
|
12
12
|
/**
|
|
13
13
|
* Reviewer-side audit context panel. Opens when a reviewer clicks a protein
|
|
14
14
|
* row in a published Project. Surfaces the metadata the analyst stamped at
|
|
15
|
-
* publish time (DE method, thresholds, group names,
|
|
15
|
+
* publish time (DE method, thresholds, group names, project, share date, sharer
|
|
16
16
|
* name) plus PUB-13 mailto button. First-line `isPublished(df)` guard ensures
|
|
17
17
|
* it does NOT render on the analyst's live working DataFrame.
|
|
18
18
|
*
|
|
@@ -123,7 +123,7 @@ export function publishedAnalysisPanel(proteinId: string): DG.Widget {
|
|
|
123
123
|
? `${meta.pThreshold} (FDR-adjusted)`
|
|
124
124
|
: '(unknown)';
|
|
125
125
|
|
|
126
|
-
body.appendChild(fieldRow('
|
|
126
|
+
body.appendChild(fieldRow('Project', meta.project || '(unknown)'));
|
|
127
127
|
body.appendChild(fieldRow('Shared', dateSlice(meta.publishedAt)));
|
|
128
128
|
body.appendChild(fieldRow('Shared by', meta.publishedBy || '(unknown)'));
|
|
129
129
|
body.appendChild(fieldRow('Method', methodLabel(meta.deMethod)));
|
|
@@ -127,8 +127,8 @@ export async function assertPublishedShape(
|
|
|
127
127
|
}
|
|
128
128
|
const expectedMeta = contract.expectedMeta;
|
|
129
129
|
const eqFloat = (a: number, b: number) => !(Math.abs(a - b) > 1e-9);
|
|
130
|
-
if (meta.
|
|
131
|
-
throw new Error(`assertPublishedShape: meta.
|
|
130
|
+
if (meta.project !== expectedMeta.project)
|
|
131
|
+
throw new Error(`assertPublishedShape: meta.project: got "${meta.project}", expected "${expectedMeta.project}"`);
|
|
132
132
|
if (meta.deMethod !== expectedMeta.deMethod)
|
|
133
133
|
throw new Error(`assertPublishedShape: meta.deMethod: got "${meta.deMethod}", expected "${expectedMeta.deMethod}"`);
|
|
134
134
|
if (!eqFloat(meta.fcThreshold, expectedMeta.fcThreshold))
|