Repository navigation
Plots: axes from the study's own profiles, OQL by statement, Not profiled never wild type - #48
Merged
Merged
Conversation
Each point layer is now its own group in the SVG, <g data-layer="i">, with the plotted cohort as data-layer="base". A layer's cases that an earlier layer drew (the overlap rule) travel as `shared` and are drawn as a second group with display="none". The figure for MCP and exports looks exactly as before. On the page, PlotControls shows a "show" zone above the figure: one chip per drawn layer, in its colour and with its count. Clicking a chip sets data-solo on the figure's wrapper, and one CSS rule steps every other group back to 8% opacity while revealing that layer's shared group. So a layer shown alone is complete, e.g. KRAS G12C's 480 samples, not only the 414 it drew itself. Clicking the chip again or pressing Esc restores every layer. It is a reading gesture kept in browser state like a hover (invariant 9): it re-runs nothing and does not change the document. Verified on msk_chord_2024 TP53-mutant samples with [outliers, extremes x 20, KRAS G12C]: soloing KRAS shows its 414 drawn and 66 shared points at full opacity, the other groups at 0.08, and Esc restores them. A figure test checks the groups and the hidden shared group; an e2e check clicks a chip, checks the opacities, presses Esc and checks the document is untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BhP3GAdWXr2EtfBDHvRS1L
The plot renderer drew a scatter and a box at 5k, 10k, 20k and 50k points, and headless Chrome on the dev server inserted and painted each SVG (median of 3): points paint scatter/box one hit-test JSON raw/gzip 5,000 84 / 117 ms 0.4 ms 220 / 35 KB 10,000 180 / 190 ms 1.7 ms 440 / 70 KB 20,000 352 / 337 ms 3.2 ms 880 / 140 KB 50,000 904 / 752 ms 7 ms 2.2 MB / 350 KB Painting, not building the string, is the cost: about 18 ms per thousand points, paid again on every redraw. 10,000 keeps a redraw near 200 ms on this CPU-only machine; 5,000 was inherited from the box plot's first version, not measured. POINTS_CAP moves to lib/plot-layers.ts (browser-safe) so the sidebar's labels read it, and the DSL reference and the controls print the constant instead of a typed "5,000". The measurement is recorded in docs/plot-case-level.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BhP3GAdWXr2EtfBDHvRS1L
…iled" never counted as unaltered
An OQL axis's statements may now be separated by commas as well as ";" and new lines,
everywhere OQL is read: a cohort's oql leaf, a plot axis, splits. The engine's parser
and the gene query dialog share OQL_SEPARATOR / oqlStatements in lib/oql-parse.ts; a
comma used to be an error, so nothing that worked changes meaning.
`by: "statement"` gives each statement its own group, named as written with the gene
in capitals. A case matching two statements is the pair ("KRAS: G12C + STK11: MUT"),
three or more is "3 or more lines", and there are at most 6 statements. `by: "any"`, the
default, stays Altered/Unaltered, which Cox and logistic covariates take with
"Unaltered" as reference.
In both, a case that matches no statement is "Unaltered" only when it is profiled for
every statement's gene in the alteration types that statement asks about. Otherwise it
is "Not profiled". Before, the axis counted a sample whose panel omits the gene as
Unaltered, the wild-type error docs/PERFORMANCE.md's denominator rule forbids. The
panel test is the `profiled` clause's own (sample_to_gene_panel_derived, WES counted),
exported as cohort2's profiledCasesSql. EXP/PROT statements read a value, not a panel,
so they need no profiling. Inner columns are named oql_case / profiled_id, because
ClickHouse resolved a bare `id` to another axis's alias once the query was embedded
in the plot's.
The axis control offers "matches OQL (any line)" and "OQL, one group per line". The OQL
gallery example gains a by-line view (STK11, KEAP1, KRAS G12C in lung cancer). The DSL
reference and docs/plot-case-level.md say all of this.
Verified on msk_chord_2024, all 25,040 samples, `KRAS: G12C, RRAGC: MUT` (RRAGC is on
the panels of 20,048 samples):
- by line: KRAS G12C 1,111, RRAGC 61, both 3, Unaltered 19,115, Not profiled 4,750
- any: Altered 1,175
Each equals a direct ClickHouse query over the panel and event tables. The OQL
example's stated numbers (1,535 Altered, 6,274 Unaltered) still hold. docs:check runs
every example and check:methods reproduces every view's numbers, Cox and logistic with
OQL covariates included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhP3GAdWXr2EtfBDHvRS1L
Every axis field that takes genes or OQL gets a button beside it that opens GeneQueryDialog, the app's one way of entering genes, with search over genes, aliases and gene sets, a check of every entry as it is typed, alias fixes, and recent and saved lists. That covers an alteration axis's OQL query or gene(s), a molecular axis's gene, and the colour/ring pickers, which share AxisPicker. The fields stay editable as text. - OQL opens one line per statement and applies them comma-separated. The limit is 6 statements when grouping by line, 30 otherwise, and the description says how lines are read for the chosen grouping. - A gene set opens in gene-list mode, capped at ALTERATION_GENE_SET_CAP (20). A molecular axis takes exactly one gene. ALTERATION_GENE_SET_CAP moves to the browser-safe lib/plot-oql.ts so the dialog can enforce it. The dialog's help says commas separate entries too. Verified in the browser: the OQL dialog opens with "KRAS: G12C\nRRAGC: MUT", and adding a line applies "KRAS: G12C, RRAGC: MUT, STK11: MUT" with by: "statement" kept; the gene dialog turns a TP53 axis into the set [TP53, KRAS]; no page errors. A new e2e check opens, edits and applies the OQL dialog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BhP3GAdWXr2EtfBDHvRS1L
… axis is follows the profile
An axis is now a clinical attribute, one of the study's own molecular profiles, or a query
across genes. A profile axis, {source: "profile", profile, gene}, is read as the study
describes the profile in genetic_profile (alteration type and datatype), never from its
name:
- a mutation profile (MAF) by mutation type, or `by: "mutated"`;
- a structural-variant profile by whether the case has one;
- a discrete copy-number profile by its own calls (Deep deletion, Shallow deletion,
Diploid, Gain, Amplification, drawn in that order), not only the ±2 events;
- a continuous one (mRNA, z-scores, log2 copy number, protein) as a number.
lib/profile-catalog.ts (browser-safe) holds that reading, and lib/plot-profile.ts resolves
a profile axis against every study a view draws over into the forms the engine already
runs (molecular with `discrete`, alteration with `by`). A profile a study lacks is refused
with the list it has, and one two pooled studies describe differently is refused.
Generic-assay profiles are offered but disabled until they get an entity picker. Plots,
Cox and logistic all resolve their axes this way.
Panel-aware single-gene axes: a case with no mutation, high-level copy-number call or SV
in the gene is "Not mutated" / "Neutral" / "No structural variant" only when it is
profiled for that gene in that alteration type; otherwise it is "Not profiled", as the
OQL axis already is. In TCGA BRCA, 18 samples outside the mutation profile were counted as
not mutated before.
The page: studyProfiles() also returns each study's catalog; Explore returns it
(profileCatalog), and the lab and portal catalogs carry it. AxisPicker's Data is one
grouped list: Clinical, then each alteration type with the study's own profile names under
it, then Across genes (OQL, gene set). The fields that follow come from the choice:
attribute; gene (with the gene query dialog) plus "Plot mutations by" for a mutation
profile; or the query. The Results page picks its expression and discrete copy-number
profiles by type and datatype, no longer by matching profile names, and its Plots tab uses
profile axes. The co-expression pair plot uses profile axes too. The DSL reference
documents the profile axis, and a gallery example shows it.
Verified on brca_tcga_pan_can_atlas_2018:
- ERBB2 GISTIC x mRNA z-scores: Diploid 480, Shallow deletion 260, Gain 206,
Amplification 122.
- TP53 mutation type: Missense 209, Truncating 102, Splice 24, Inframe 5, Multiple 4,
Not mutated 706, Not profiled 18.
- ERBB2 SV: 15 / 1,037 / 18 not profiled.
- msk_chord's `cna` profile shows its five levels, plus one sample whose stored value is
-1.5, shown as written.
- The quoted APC Cox and logistic results in the gallery are unchanged.
In the browser, picking Mutations in the Data list rewrites the axis and redraws, and the
Cox covariates use the same picker; no page errors. Unit tests cover the reading,
resolution, refusals and SQL; the new example's cohort is pinned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhP3GAdWXr2EtfBDHvRS1L
…ady asks
The gene-set entry gave two groups, mutated in any of the genes or not. "OQL query",
grouped by any line, asks the same with `GENE: MUT` per gene, and can say more (any
alteration, amplification, a protein change), so the two entries read as one choice under
two names. "Across genes" now offers only the OQL query.
The engine still accepts the `genes` form, so documents and the gene-set Cox example work
unchanged. The picker shows such an axis as its OQL equivalent ("TP53: MUT, KRAS: MUT",
any line), and the first edit makes it an OQL axis. GeneSetInput, used only there, is
removed.
Verified in the browser: an axis of genes [TP53, KRAS] shows Data "OQL query" with
"TP53: MUT, KRAS: MUT"; "Across genes" lists only OQL query; switching to one group per
line writes {oql: "TP53: MUT, KRAS: MUT", by: "statement"}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhP3GAdWXr2EtfBDHvRS1L
…t's groups read as groups The Gene and OQL query fields of an axis are no longer text boxes. Each shows its value and opens the app's gene query dialog when clicked or typed into, so a half-typed or misspelt symbol never reaches the document; every entry is checked in the dialog first, with alias fixes offered. The fields do not open on keyboard focus alone: the dialog hands focus back on closing, and tabbing past would open it. The separate checklist button goes, since the field is now the trigger. GeneInput and OqlInput, used only there, are removed. The Data list's headers are a tinted band in small bold text, with a rule between groups and the profiles indented under them, so a header no longer reads as one more choice. Verified in the browser: - the axis block holds no text input; clicking Gene opens the dialog with TP53, and applying KRAS sets the axis; - focusing the field opens nothing, and typing a letter opens the dialog; - the menu's headers render as bands over indented items; - no page errors. The e2e OQL check now also asserts the axis has no free-text field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BhP3GAdWXr2EtfBDHvRS1L
… with a group per gene
A mutation profile axis reads one gene, because its groups (mutation types, not mutated,
not profiled) describe one gene's mutations. Applying more than one gene in its gene
dialog no longer stops at the one-gene limit: the axis becomes the OQL query
"GENE: MUT, …" grouped one group per line, which is what several genes' mutations can
honestly be (each gene, each pair, three or more, unaltered, not profiled). The dialog's
description says so, and the axis then shows as Data "OQL query". Other profile axes
still take one gene.
Verified in the browser on brca_tcga_pan_can_atlas_2018: applying TP53 and PIK3CA to a
TP53 mutation axis wrote {oql: "TP53: MUT, PIK3CA: MUT", by: "statement"} and drew
Unaltered 443, PIK3CA 263, TP53 263, both 81, Not profiled 18.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhP3GAdWXr2EtfBDHvRS1L
…ne, and say how many A gene's alteration axis (and a discrete copy-number profile) answers "Not profiled" for a case whose panel omits the gene. A plot shows that as its own group. A model has no use for it as a level: an unknown status is neither wild type nor a hazard ratio worth reporting. Before panel-aware axes, those patients were silently fitted as wild type, in the reference group. coxRows and logisticRows now drop "Not profiled" values of the outcome and of each undecidable covariate in the row query itself, the query the method record's script rebuilds and re-fits, so the script reproduces exactly the rows the view fitted. A second query (coxNotProfiled:<endpoint>, logisticNotProfiled, labels the method data does not read as rows) counts those patients. The caption says how many were left out, separately from those missing the endpoint or a value, and each study's fetch step in the method record says how many of its missing rows were not profiled. Verified on brca_tcga_pan_can_atlas_2018: - Cox OS ~ TP53 mutated + ESR1 z-score: 1,064 patients, TP53 HR 1.07, no "Not profiled" term; the caption names 18 not profiled and 2 without the endpoint. - Logistic PIK3CA mutated ~ TP53 mutated: 1,066 patients, OR 0.52, 18 named. check:methods: every gallery Cox and logistic script reproduces its view, unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BhP3GAdWXr2EtfBDHvRS1L
…ta list's groups as bold text
The row above the figure was "show one layer alone", which with a single layer could
only fade the plotted cohort, never the layer's own points. It is now one toggle chip
per drawn thing: the plotted cases ("all samples (13,087)") and each layer. Any of them
can be hidden, alone or together, and Esc shows everything. It stays a reading gesture
kept in browser state (invariant 9), re-running nothing.
A case two layers share is drawn by the first. Its copy in a later layer is now tagged
with the layer that drew it: the runner records sharedOwners beside `shared`, and the
renderer writes one hidden group per owner, <g data-layer="k" data-shared
data-owner="j">. So hiding layer j brings its shared cases back in layer k's colour,
rather than hiding them from both. One CSS rule per key fades a hidden group (0.08) and
shows the copies a hidden owner leaves.
The Data list's group headers are plain bold text, a little space between groups, the
profiles indented beneath, with no band or rule.
Verified in the browser on msk_chord TP53-mutant samples with [outliers, trend]:
- hiding outliers fades its group and shows trend's copies of the cases outliers drew;
- hiding "all samples" fades the density;
- Esc restores every group; the document is unchanged; no page errors.
The figure test checks the owner-tagged shared groups; the e2e check is rewritten for
the toggles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhP3GAdWXr2EtfBDHvRS1L
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The follow-up to #47: plot axes read the study's own molecular profiles, multiple genes as OQL statements, and "Not profiled" handled honestly everywhere.
What changes
Axes from the study's own profiles
profileaxis ({"source": "profile", "profile", "gene"}) is read from the profile's own type and datatype ingenetic_profile, never from its name:Genes and OQL
by: "statement"gives each statement its own group, a pair as "A + B", and three or more as "3 or more lines".Not profiled, never wild type
KRAS: G12C, RRAGC: MUThas 4,750 samples on panels without RRAGC; they were counted as Unaltered before.Point layers and figures
Verified
check:methodsreproduces every model.e47d599, 271 checks, all pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01BhP3GAdWXr2EtfBDHvRS1L