feat(components): add gs-mutation-cooccurrence component - #1130
Conversation
Adds a new `gs-mutation-cooccurrence` web component that visualizes mutation co-occurrence patterns across time periods as a heatmap. Each row represents a unique combination of alleles at the specified positions (using LAPIS aggregated field syntax: `[501]`, `S[501]`, `ORF1a[501]`). Left columns show the allele at each position; right columns show the proportion of sequences with that pattern per time period, colored by frequency. Implementation follows the 3-layer architecture: - CooccurrenceOverTimeDataMap (Map2dBase subclass with own serializer) - queryMutationCooccurrence (fetchAggregated with position fields) - CooccurrenceOverTimeGrid (new grid sharing ProportionCell etc.) - gs-mutation-cooccurrence Lit wrapper Shared grid pieces (ProportionCell, FeaturesOverTimeGridDisplay, styleGridHeader, getTooltipPosition) extracted from features-over-time-grid into features-over-time-grid-shared so both grids can use them. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ooccurrence Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…occurrence stories Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eaders Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gs-mutation-cooccurrence-over-time Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tooltip duplication Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
It'd be good if we would make a release for this feature: GenSpectrum/LAPIS@bd2c7c8 And then also put the released LAPIS somewhere so it's easier to develop the UI component. |
…tation-cooccurrence-over-time Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ead of one combined call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…stance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nstant - Add useEffect to re-sync hideGaps state when the web component attribute changes after mount, matching the pattern in mutations-over-time - Replace three local MIN_PROPORTION = 0.001 definitions with imports of the canonical MUTATIONS_OVER_TIME_MIN_PROPORTION from queryMutationsOverTime Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…idDisplay Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…erage mask - Store all LAPIS groups per date to compute per-pattern coverage: for a pattern, coverage = sequences with a real (non-N) allele at every position the pattern specifies. Produces valueWithCoverage instead of value. - Filter out all-N/null patterns (no meaningful allele information). - Sort rows by treating each position as a bit (1 = real allele, 0 = N/null), first position = most significant bit; sort descending so fully-covered patterns appear first. Secondary sort by total count descending. - Update Storybook mock data to 4 positions with 2 weeks for two pages, and update story args/matchers accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add alleleColors.ts with colors for all nucleotide and amino acid symbols (property-based scheme for AAs, classic A/T/C/G scheme for nucleotides; position format distinguishes the two). Apply colors as cell backgrounds in the cooccurrence grid. Shrink position columns to 20px with angled headers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e bold Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace pos:allele pairs with a compact colored letter sequence, matching the cell display style. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new dashboard web component for visualizing mutation co-occurrence patterns over time as a heatmap/grid, and refactors shared “over-time grid” UI pieces to be reused across components.
Changes:
- Introduces
gs-mutation-cooccurrence-over-time(Lit wrapper) and the underlying Preact implementation + stories/mocks. - Adds
queryMutationCooccurrenceandCooccurrenceOverTimeDataMapfor fetching/structuring co-occurrence data. - Extracts reusable grid display utilities (cell rendering, tooltip positioning, header styling) and introduces a shared
OverTimeGridTooltip.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| components/src/web-components/visualization/index.ts | Exports the new gs-mutation-cooccurrence-over-time web component. |
| components/src/web-components/visualization/gs-mutation-cooccurrence-over-time.tsx | New Lit web component wrapper passing props into the Preact implementation. |
| components/src/web-components/visualization/gs-mutation-cooccurrence-over-time.stories.ts | Storybook story + fetch mocks for the web component. |
| components/src/query/queryMutationCooccurrence.ts | New query that fetches aggregated co-occurrence data and builds an over-time data map. |
| components/src/preact/queriesOverTime/queries-over-time-grid-tooltip.tsx | Refactors tooltip rendering to use shared OverTimeGridTooltip. |
| components/src/preact/mutationsOverTime/mutations-over-time-grid-tooltip.tsx | Refactors tooltip rendering to use shared OverTimeGridTooltip. |
| components/src/preact/mutationCooccurrence/mutation-cooccurrence-over-time.tsx | New Preact visualization (tabs + toolbar + CSV export + grid). |
| components/src/preact/mutationCooccurrence/mutation-cooccurrence-over-time.stories.tsx | Preact story for the new visualization with fetch mocks. |
| components/src/preact/mutationCooccurrence/mutation-cooccurrence-grid-tooltip.tsx | New tooltip component for co-occurrence grid cells. |
| components/src/preact/mutationCooccurrence/mutation-cooccurrence-grid-tooltip.stories.tsx | Storybook stories for the new tooltip component. |
| components/src/preact/mutationCooccurrence/CooccurrenceOverTimeData.ts | New Map2dBase subclass and serializer for co-occurrence patterns. |
| components/src/preact/mutationCooccurrence/mockData/cooccurrence.json | Mock aggregated response data used in stories. |
| components/src/preact/components/over-time-grid-tooltip.tsx | New shared tooltip component used by multiple over-time grids. |
| components/src/preact/components/features-over-time-grid.tsx | Refactors to import shared grid pieces from features-over-time-grid-shared. |
| components/src/preact/components/features-over-time-grid-shared.tsx | New shared grid utilities extracted for reuse by multiple grids. |
| components/src/preact/components/cooccurrence-over-time-grid.tsx | New grid component for rendering co-occurrence patterns over time. |
| components/.storybook/main.ts | Storybook Vite config change (allowedHosts: true). |
Suppressed comments (3)
components/src/preact/mutationCooccurrence/mutation-cooccurrence-grid-tooltip.stories.tsx:31
MutationCooccurrenceGridTooltipPropsrequires apositionsarray, but this story omits it, which will cause a runtime error when the tooltip tries to renderpositions.map(...).
args: {
pattern: { alleles: { '[123]': 'A', '[124]': 'T', '[126]': 'G' } },
date: weekDate,
value: null,
},
components/src/preact/mutationCooccurrence/mutation-cooccurrence-grid-tooltip.stories.tsx:39
MutationCooccurrenceGridTooltipPropsrequires apositionsarray, but this story omits it, which will cause a runtime error when the tooltip tries to renderpositions.map(...).
args: {
pattern: { alleles: { '[123]': null, '[124]': 'T', '[126]': null } },
date: weekDate,
value: { type: 'value', count: 5, proportion: 0.024, totalCount: 212 },
},
components/src/preact/mutationCooccurrence/mutation-cooccurrence-grid-tooltip.stories.tsx:47
MutationCooccurrenceGridTooltipPropsrequires apositionsarray, but this story omits it, which will cause a runtime error when the tooltip tries to renderpositions.map(...).
args: {
pattern: { alleles: { '[123]': 'A', '[124]': 'T', '[126]': 'G' } },
date: weekDate,
value: { type: 'belowThreshold', totalCount: 100 },
},
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…duplicate formatAllele All four MutationCooccurrenceGridTooltip stories were missing the required positions prop, causing a crash on render. Also extracted the duplicated allele display logic (null/N → ?/-) into a shared formatAllele function in CooccurrenceOverTimeData.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… ESLint Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eGaps duplication The mutations-over-time, queries-over-time, and mutation-cooccurrence-over-time components each reimplemented the same logic for removing empty date columns. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ming in mutation co-occurrence Format gene-scoped positions like ORF1a[501] as "ORF1a: 501", collapse LAPIS's 'N' into null everywhere so coverage checks only need one condition, rename the alleles field to symbols since positions can be amino acids too, and extract sub-functions plus a spec file out of queryMutationCooccurrence. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Suppressed comments (1)
components/src/query/queryMutationCooccurrence.ts:167
- This bitmask sorts by which positions are covered, not by the number of covered positions promised by the function contract. For example, with three positions, coverage
100sorts ahead of011even though it covers one position instead of two; bitwise shifts also wrap beyond 31 positions. Sort by the count of non-null symbols instead.
const coverageBits = (key: string) => {
const symbols = patternByKey.get(key)!.symbols;
return positions.reduce((acc, pos, i) => {
const bit = symbols[pos] !== null ? 1 : 0;
return acc | (bit << (positions.length - 1 - i));
| const coverageBits = (key: string) => { | ||
| const symbols = patternByKey.get(key)!.symbols; | ||
| return positions.reduce((acc, pos, i) => { | ||
| const bit = symbols[pos] !== null ? 1 : 0; | ||
| return acc | (bit << (positions.length - 1 - i)); | ||
| }, 0); |
There was a problem hiding this comment.
AI: coverageBits uses 1 << (positions.length - 1 - i); JS bitwise ops are 32-bit, so with >31 positions the shift wraps and the sort order breaks silently. It also sorts by which positions are covered (leftmost weighted heaviest: 110 > 101), not by the number of covered positions the doc comment promises. If the intent is "more covered positions first, then count", sort by positions.filter(p => symbols[p] !== null).length instead of a bitmask.
There was a problem hiding this comment.
Yes the logic is weird, something simpler would be good. Maybe we can first sort by covered positions and then just do string sorting within.
There was a problem hiding this comment.
AI: Test-coverage gaps for the co-occurrence logic — currently only single-date happy path is covered. Consider adding:
- multi-date aggregation (counts summed across dates per pattern)
- pattern sort order (coverage-priority + count tiebreak in
sortPatternKeysByCoverageAndCount) coverage === 0 -> nullandtotal === 0 -> nullcell pathsformatPosition([501]->501,ORF1a[501]->ORF1a: 501, non-matching fallthrough)getFilteredCooccurrenceDataproportion-interval filtering andhideGaps(only story-tested today)
There was a problem hiding this comment.
TBD which of these make sense.
| <span | ||
| className='text-nowrap font-mono text-xs' | ||
| style={{ display: 'block', transform: 'rotate(-60deg)', transformOrigin: 'center center' }} | ||
| > | ||
| {formatPosition(pos)} | ||
| </span> |
There was a problem hiding this comment.
ok let's maybe remove the space, and see what it looks like with 4 digits too (maybe needs to be made smaller in those cases?)
| if (requestedDateRanges.length > MAX_NUMBER_OF_GRID_COLUMNS) { | ||
| throw new UserFacingError( | ||
| 'Too many dates', | ||
| `The dataset would contain ${requestedDateRanges.length} date intervals. ` + | ||
| `Please reduce the number to below ${MAX_NUMBER_OF_GRID_COLUMNS} to display the data. ` + | ||
| 'You can achieve this by either narrowing the date range in the provided LAPIS filter or by selecting a larger granularity.', | ||
| ); | ||
| } |
There was a problem hiding this comment.
Do we also need to limit the number of positions? I can imagine that the component doesn't make much sense with 200 positions either.
There was a problem hiding this comment.
I want to check what it looks like with 20, if that's fine we can use that, if not we can maybe start with a max of 10 for now.
…ccurrence query Nucleotide positions (e.g. [123]) use N as the uncovered symbol; amino acid positions (e.g. S[501]) use X. Previously N was hardcoded for all positions, which would suppress asparagine (a real amino acid allele) from the results. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ry strings Adds a regex check to the positions schema so values like "country" are rejected with a clear error. Updates the InvalidAttributes story to test this case specifically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…yle them lighter Distinguishes uncovered positions (null symbols) from real deletion alleles (which LAPIS returns as '-'). Uncovered positions now show '?' in a lighter grey so they are visually distinct from real alleles. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three components duplicated the same hide-gaps toggle button. Extract into a shared HideGapsButton component in preact/components/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…occurrence Add MAX_NUMBER_OF_POSITIONS = 10 validation to queryMutationCooccurrence, matching the existing MAX_NUMBER_OF_GRID_COLUMNS guard for dates. Also fix the bitmask-based sort which overflowed JS 32-bit integers for >31 positions by replacing it with a simple covered-position count. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ion cooccurrence over time Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace total-count secondary sort with alphabetical order on the pattern key, giving a stable deterministic order that doesn't shift as data changes. Also drop the now-unused countsByPatternAndDate parameter from the sort function. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

resolves #1129
Summary
Adds a new
gs-mutation-cooccurrenceweb component that visualizes mutation co-occurrence patterns across time periods as a heatmap.Each row represents a unique combination of alleles at the specified positions (using LAPIS aggregated field syntax:
[501],S[501],ORF1a[501]). Left columns show the allele at each position; right columns show the proportion of sequences with that pattern per time period, colored by frequency.Implementation follows the 3-layer architecture:
Shared grid pieces (ProportionCell, FeaturesOverTimeGridDisplay, styleGridHeader, getTooltipPosition) extracted from features-over-time-grid into features-over-time-grid-shared so both grids can use them.
Screenshot
PR Checklist