Skip to content

feat(components): add gs-mutation-cooccurrence component - #1130

Open
fhennig wants to merge 35 commits into
mainfrom
feat/mutation-cooccurrence
Open

feat(components): add gs-mutation-cooccurrence component#1130
fhennig wants to merge 35 commits into
mainfrom
feat/mutation-cooccurrence

Conversation

@fhennig

@fhennig fhennig commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

resolves #1129

Summary

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.

Screenshot

PR Checklist

  • All necessary documentation has been adapted.
  • The implemented feature is covered by an appropriate test.

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>
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
dashboard-components Ready Ready Preview Aug 25, 2026 10:13am

Request Review

Felix Hennig and others added 3 commits July 15, 2026 14:46
…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>
Felix Hennig and others added 3 commits July 15, 2026 15:22
…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>
@fhennig

fhennig commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

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.

Felix Hennig and others added 11 commits July 30, 2026 11:18
…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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 queryMutationCooccurrence and CooccurrenceOverTimeDataMap for 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

  • MutationCooccurrenceGridTooltipProps requires a positions array, but this story omits it, which will cause a runtime error when the tooltip tries to render positions.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

  • MutationCooccurrenceGridTooltipProps requires a positions array, but this story omits it, which will cause a runtime error when the tooltip tries to render positions.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

  • MutationCooccurrenceGridTooltipProps requires a positions array, but this story omits it, which will cause a runtime error when the tooltip tries to render positions.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.

Comment thread components/src/query/queryMutationCooccurrence.ts
Comment thread components/src/query/queryMutationCooccurrence.ts Outdated
Comment thread components/.storybook/main.ts Outdated
…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>
fhennig and others added 2 commits August 24, 2026 09:11
…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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 100 sorts ahead of 011 even 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));

Comment thread components/src/query/queryMutationCooccurrence.ts Outdated
Comment thread components/src/preact/mutationCooccurrence/mutation-cooccurrence-over-time.tsx Outdated
Comment thread components/src/preact/mutationCooccurrence/CooccurrenceOverTimeData.ts Outdated
Comment thread components/src/preact/mutationCooccurrence/mutation-cooccurrence-over-time.tsx Outdated
Comment on lines +163 to +168
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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 -> null and total === 0 -> null cell paths
  • formatPosition ([501] -> 501, ORF1a[501] -> ORF1a: 501, non-matching fallthrough)
  • getFilteredCooccurrenceData proportion-interval filtering and hideGaps (only story-tested today)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBD which of these make sense.

Comment on lines +96 to +101
<span
className='text-nowrap font-mono text-xs'
style={{ display: 'block', transform: 'rotate(-60deg)', transformOrigin: 'center center' }}
>
{formatPosition(pos)}
</span>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

The formatting looks a bit weird but I also don't know how to improve it...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?)

Comment on lines +31 to +38
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.',
);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Felix Hennig and others added 5 commits August 25, 2026 10:08
…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>
Felix Hennig and others added 2 commits August 25, 2026 11:22
…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>
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.

New co-occurences over time component

3 participants