Skip to content

Comparison → Mutations tab: the two-group lollipop (mutcompare view) - #166

Open
alisman wants to merge 1 commit into
masterfrom
session/cmp-mutations
Open

alisman wants to merge 1 commit into
masterfrom
session/cmp-mutations

Conversation

@alisman

@alisman alisman commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Summary

This adds a new view kind, mutcompare, laid out as cBioPortal's group-comparison Mutations tab, and wires it into /portal/comparison right after Genomic Alterations, where the portal puts it.

The plot

  • One gene's mutations in exactly two groups, mirrored about the protein.
    • Group A is drawn above the protein, group B below it.
    • The codon axis is on top and the Pfam domains sit in the middle.
    • Each half's y axis is labelled 0 and its max, the same way the portal labels it.
  • Stems, colours, labels and domains come from the existing lib/lollipop.ts, the same code the results page's lollipop uses. There is no second lollipop implementation.
  • The Y-Axis % / # switch: % is a share of the group's patients profiled for the gene (the portal's default); # is a count of patients. Both halves share one scale.
  • Each group has a Driver / VUS badge selector beside the plot.
  • Clicking a stem selects its codon and filters the table to it. Shift-click adds more codons; clicking the background clears the selection.

The gene picker

  • A gene field, plus pills for the Highest Frequency: ten.
  • The ranking follows the portal's genesSortedByMutationFrequency: genes are ordered by their higher share of mutated samples over profiled samples. That is frequency, not significance. The task brief said "ranked by significance", but the portal ranks by frequency, so this follows the portal.

Below the plot

  • The Fisher line for the whole gene.
  • A table with a row per protein change:
    • Protein Change, Annotation (OncoKB), Mutation Type
    • (A) and (B): patients (share %)
    • Log2 Ratio, shown as >10 / <-10 when either share is 0
    • p-Value: two-sided Fisher exact test
    • q-Value: Benjamini–Hochberg (BH)
    • Enriched in: a tie goes to B; the cell is coloured when q < 0.05
    • The header tips are the portal's, word for word. The table has the "Enriched in …" filter and "Significant only", and is sorted by p.
  • Other than two groups is refused with the portal's message. The page leaves the tab out and says why, as it already does for the expression tabs, for other than two groups or a study with no mutation profile.

Other changes

  • The axis, the badges and the codon selection are applied in the browser from the whole result (dataPart), so none of them re-runs the view. Changing the gene does re-run it.
  • The per-gene mutated/profiled SQL that enrichment used moved, unchanged, from lib/views/enrichment/sql.ts to lib/federation-views.ts. Kinds can't import each other, so this lets both kinds share one copy.
  • docs/PORTAL-PARITY.md: ticks this gap, with the leftovers as sub-items. It also ticks Per-sample CN segments (Copy-number segments view: per-sample segment tracks (the portal's CN Segments tab) #161), which is merged; its two sub-items stay unticked.

Legacy reference

Studied:

  • GroupComparisonMutationsTab.tsx, GroupComparisonMutationMapper(Wrapper).tsx, GroupComparisonMutationTable.tsx
  • LollipopGeneSelector.tsx, LollipopTooltipCountInfo.tsx, FisherExactTwoSidedTestLabel.tsx
  • GroupComparisonUtils (getProteinChangeToMutationRowData, the MUTATIONS_* messages)
  • In GroupComparisonStore / ComparisonStore: genesSortedByMutationFrequency, groupToProfiledPatients, showMutationsTab
  • In react-mutation-mapper: LollipopPlotNoTooltip (the two-group layout) and AxisScaleSwitch

Matched:

  • Shares are per profiled patient.
  • A table count is the distinct patients with that protein change.
  • A stem's height is the distinct (patient, change) pairs at that codon.
  • Fisher's test is per change and BH is across the changes.
  • Log ratio and Enriched-in follow the portal's rules, and the column tips are the portal's.
  • The axis default is %, with a minimum max of 5 on #.
  • The tooltip's percent uses the portal's format.
  • The heading is "GENE mutations: (A) x vs (B) y".
  • The Fisher line uses the portal's wording.

Deliberately different (cBioLab conventions):

  • The page lists a left-out tab, with its reason, rather than showing an alert inside the tab.
  • The caveat states the denominators and the counting rules.

Verification

Cross-checked against cbioportal.org's REST API on brca_tcga_pan_can_atlas_2018, comparing PAM50 basal-like (SUBTYPE=BRCA_Basal) against luminal A (BRCA_LumA). Data came from /clinical-data, /molecular-profiles/…_mutations/mutations/fetch and /sample-lists/…_sequenced, with scipy for the tests. Everything below is identical:

cBioLab cbioportal.org
TP53 profiled patients 171 / 499 171 / 499
TP53 mutated patients 153 / 53 153 / 53
TP53 protein changes 132 132
R342* 6 / 0, p 2.587e-4 6 / 0, p 2.587e-4
R175H 9 / 4, p 1.036e-3 9 / 4, p 1.036e-3
R273H 7 / 2, p 1.422e-3 7 / 2, p 1.422e-3
R196* 6 / 1, p 1.425e-3 6 / 1, p 1.425e-3
…the rest of the top 10 same same
Ranking: PIK3CA (luminal A) 47.49% 47.49% (237/499)
Ranking: TTN (basal-like) 23.98% 23.98% (41/171)

Tests and checks:

  • npm test passes (3,290 tests, 0 fail).
  • New tests: test/mutcompare.test.ts, plus cases in test/comparison-layout.test.ts.
  • node --test test/view-snapshots*.test.ts: a new mutcompare case, with answers recorded from the public warehouse. No outside HTTP calls needed recording: TP53's Genome Nexus answers were already recorded.
  • test/boundary.test.ts covers the new mutcompareProfiledSql.
  • npm run report:coverage && npm run check:coverage passes: 100% of changed lib lines run.
  • npm run docs:gen and npm run docs:counts: only my example was added; no existing count moved.
  • eslint and knip are clean. ruff did not run locally (no Python venv), and nothing in Python changed.
  • I did not run the e2e gate locally. CI runs it.

Looked at in a browser (Playwright, dev server) with no console errors:

  • The TP53 tab.
  • Clicking the R175 stem: the table is filtered to R175H and the banner shows "Remove filter".
  • The PIK3CA pill together with the # axis: H1047R is 6 vs 87, and the axis max is 98.

Not done

These are listed as unticked sub-items in PORTAL-PARITY.md:

  • The Top / Bottom Y-Axis Max sliders.
  • The lollipop's tracks (hotspots, OncoKB, PTM, exons, topology, PDB), its Legend toggle, and its SVG/PNG download. The table's copy and download.
  • The table's Co-occurrence Pattern column (a mini oncoprint, which needs patient-level data), and the Custom Driver, Custom Driver Tier and MS columns.
  • On the portal each group's badges filter that group alone. Here a badge hides that class in both groups.
  • The Fisher line covers all of the gene's mutations only. The portal also gives it for filtered or selected mutations, which needs distinct patients over the selected changes.
  • A stem's tooltip counts the stem's (patient, change) pairs, not distinct patients, and it doesn't show the other group's stem at the same codon.
  • The gene picker is a field plus ten pills, not the portal's searchable 200-gene dropdown (the result already carries the 200).
  • The ranking ignores the page's "OncoKB drivers only" setting and the event-type settings.
  • The overlap banner.
  • There is no browser check yet: the kind is e2ePending.

Preview

  • /portal/comparison?doc=eyJzdHVkeSI6ICJicmNhX3RjZ2FfcGFuX2Nhbl9hdGxhc18yMDE4IiwgInZpZXdzIjogW10sICJjb2hvcnRzIjogeyJCYXNhbC1saWtlIjogeyJhbGwiOiBbeyJraW5kIjogImNsaW5pY2FsIiwgImF0dHJpYnV0ZSI6ICJTVUJUWVBFIiwgInZhbHVlcyI6IFsiQlJDQV9CYXNhbCJdfV19LCAiTHVtaW5hbCBBIjogeyJhbGwiOiBbeyJraW5kIjogImNsaW5pY2FsIiwgImF0dHJpYnV0ZSI6ICJTVUJUWVBFIiwgInZhbHVlcyI6IFsiQlJDQV9MdW1BIl19XX19fQ&tab=Mutations

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q3ezTWaXhwWhadqM9gbFNK

@vercel

vercel Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cbiolab-genie Ready Ready Preview Nov 9, 2026 11:06pm UTC
cbiolab-new Ready Ready Preview Nov 9, 2026 11:06pm UTC
cohort-lab Ready Ready Preview Nov 9, 2026 11:06pm UTC

Request Review

A new view kind, `mutcompare`, laid out as cBioPortal's group-comparison
Mutations tab and wired into /portal/comparison after Genomic Alterations.
It shows one gene's mutations in exactly two cohorts. The first cohort is drawn
above the protein and the second below it, with the codon axis on top and the
Pfam domains in the middle. Each half's y axis is a share of that cohort's
patients profiled for the gene (the default) or a count of patients. Both
halves share one scale, labelled 0 and max as the portal labels it. The stems,
colours, labels and domains come from lib/lollipop.ts, shared with the results
page's lollipop.

The table has a row per protein change. Each row gives the patients carrying
the change in each cohort and their share of the profiled patients, and log2
of the first share over the second. It also gives a two-sided Fisher's exact
test against the rest of the profiled patients, BH q across the changes, and
the cohort the change is enriched in. The ratio, the ties and the column tips
follow the portal's. The gene picker ranks genes by their higher share of
mutated samples over profiled samples, as the portal's
genesSortedByMutationFrequency does: by frequency, not significance. The
picker offers the ten "Highest Frequency" genes as pills, and the result
carries the first 200. Four settings are applied in the browser from the whole
result, so none re-runs the view: the y-axis % / # switch, each group's
Driver/VUS badges (`hide`), the selected codons (`positions`, a click on a
stem, which filters the table) and the gene's Fisher line. A comparison of
other than two groups is refused with the portal's message. The page leaves
the tab out, saying why, for other than two groups or a study with no mutation
profile.

The per-gene mutated and profiled SQL that the enrichment view used moved to
lib/federation-views.ts unchanged, so both kinds share one copy. The method
record recomputes every p, q and the gene's p with scipy and statsmodels.

Verified against cbioportal.org's REST API on brca_tcga_pan_can_atlas_2018,
PAM50 basal-like (171) against luminal A (499), TP53:
- profiled patients, 171 / 499
- mutated patients, 153 / 53
- 132 protein changes
- the top 10 changes' counts and Fisher p (R342* 6/0, p 2.587e-4; R175H 9/4,
  p 1.036e-3; …), all identical
- ranking shares PIK3CA 47.49% and TTN 23.98%, also identical
Also: unit tests, view snapshots (recorded), boundary, coverage and lint. I
looked at the tab in a browser: a stem click filters the table, and the gene
pill and the # axis work.

Also ticks Per-sample CN segments (#161), which is merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3ezTWaXhwWhadqM9gbFNK

This branch was successfully deployed

1 active deployment
Preview – cohort-lab — 009b2de9 Deployed Nov 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant