From 361a5196bdc06cc14cf85d556f11d7b54310cb47 Mon Sep 17 00:00:00 2001 From: Fabian Engelniederhammer Date: Mon, 15 Jul 2024 07:48:15 +0200 Subject: [PATCH] feat(components): mutation filter: add explanation to info box closes #182 --- components/src/lapisApi/ReferenceGenome.ts | 2 + .../mutationFilter/mutation-filter-info.tsx | 117 ++++++++++++++++++ .../preact/mutationFilter/mutation-filter.tsx | 4 +- .../sequenceTypeFromSegment.spec.ts | 2 +- .../mutationFilter/sequenceTypeFromSegment.ts | 4 +- .../input/gs-mutation-filter.stories.ts | 58 ++++++++- 6 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 components/src/preact/mutationFilter/mutation-filter-info.tsx diff --git a/components/src/lapisApi/ReferenceGenome.ts b/components/src/lapisApi/ReferenceGenome.ts index 788da2cf..5eeb0f3d 100644 --- a/components/src/lapisApi/ReferenceGenome.ts +++ b/components/src/lapisApi/ReferenceGenome.ts @@ -28,3 +28,5 @@ export const getSegmentNames = (referenceGenome: ReferenceGenome, sequenceType: } } }; + +export const isSingleSegmented = (referenceGenome: ReferenceGenome) => referenceGenome.nucleotideSequences.length === 1; diff --git a/components/src/preact/mutationFilter/mutation-filter-info.tsx b/components/src/preact/mutationFilter/mutation-filter-info.tsx new file mode 100644 index 00000000..821d44e1 --- /dev/null +++ b/components/src/preact/mutationFilter/mutation-filter-info.tsx @@ -0,0 +1,117 @@ +import { useContext } from 'preact/hooks'; + +import { isSingleSegmented } from '../../lapisApi/ReferenceGenome'; +import { ReferenceGenomeContext } from '../ReferenceGenomeContext'; +import Info, { InfoHeadline1, InfoHeadline2, InfoParagraph } from '../components/info'; + +export const MutationFilterInfo = () => { + const referenceGenome = useContext(ReferenceGenomeContext); + + const firstGene = referenceGenome.genes[0].name; + return ( + + Mutation Filter + This component allows you to filter for mutations at specific positions. + + Nucleotide Mutations and Insertions + {isSingleSegmented(referenceGenome) ? ( + + ) : ( + + )} + + Amino Acid Mutations and Insertions + + An amino acid mutation has the format <gene>:<position><base> or + <gene>:<base_ref><position><base>. A <base> can be one of + the 20 amino acid codes. It can also be - for deletion and X for unknown. Example:{' '} + E:57Q. + + + Insertions can be searched for in the same manner, they just need to have ins_ appended to the + start of the mutation. Example: ins_{firstGene}:31:N would filter for sequences with an insertion + of N between positions 31 and 32 in the gene {firstGene}. + + + This organism has the following genes: {referenceGenome.genes.map((gene) => gene.name).join(', ')}. + + + Insertion Wildcards + + This component supports insertion queries that contain wildcards ?. For example{' '} + ins_{firstGene}:214:?EP? will match all cases where segment {firstGene} has an insertion + of EP between the positions 214 and 215 but also an insertion of other amino acids + which include the EP, e.g. the insertion EPE will be matched. + + + You can also use wildcards to match any insertion at a given position. For example{' '} + ins_{firstGene}:214:? match any (but at least one) insertion between the positions 214 and 215. + + + Multiple Mutations + + Multiple mutation filters can be provided by adding one mutation after the other. + + + Any Mutation + + To filter for any mutation at a given position you can omit the <base>. Example:{' '} + {firstGene}:20. + + + No Mutation + + You can write a . for the <base> to filter for sequences for which it is confirmed + that no mutation occurred, i.e. has the same base as the reference genome at the specified position. + + + ); +}; + +const SingleSegmentedNucleotideMutationsInfo = () => { + return ( + <> + + This organism is single-segmented. Thus, nucleotide mutations have the format{' '} + <position><base> or <base_ref><position><base>. The{' '} + <base_ref> is the reference base at the position. It is optional. A <base> can + be one of the four nucleotides A, T, C, and G. It can also be - for + deletion and N for unknown. For example if the reference sequence is A at position{' '} + 23 both: 23T and A23T will yield the same results. + + + Insertions can be searched for in the same manner, they just need to have ins_ appended to the + start of the mutation. Example: ins_1046:A would filter for sequences with an insertion of A + between the positions 1046 and 1047 in the nucleotide sequence. + + + ); +}; + +const MultiSegmentedNucleotideMutationsInfo = () => { + const referenceGenome = useContext(ReferenceGenomeContext); + + const firstSegment = referenceGenome.nucleotideSequences[0].name; + + return ( + <> + + This organism is multi-segmented. Thus, nucleotide mutations have the format{' '} + <segment>:<position><base> or{' '} + <segment>:<base_ref><position><base>. <base_ref> is the + reference base at the position. It is optional. A <base> can be one of the four nucleotides{' '} + A, T, C, and G. It can also be - for deletion and N for + unknown. For example if the reference sequence is A at position 23 both:{' '} + {firstSegment}:23T and {firstSegment}:A23T will yield the same results. + + + Insertions can be searched for in the same manner, they just need to have ins_ appended to the + start of the mutation. Example: ins_{firstSegment}:10462:A. + + + This organism has the following segments:{' '} + {referenceGenome.nucleotideSequences.map((gene) => gene.name).join(', ')}. + {' '} + + ); +}; diff --git a/components/src/preact/mutationFilter/mutation-filter.tsx b/components/src/preact/mutationFilter/mutation-filter.tsx index 3a47f3a1..19a27bdc 100644 --- a/components/src/preact/mutationFilter/mutation-filter.tsx +++ b/components/src/preact/mutationFilter/mutation-filter.tsx @@ -1,12 +1,12 @@ import { type FunctionComponent } from 'preact'; import { useContext, useRef, useState } from 'preact/hooks'; +import { MutationFilterInfo } from './mutation-filter-info'; import { parseAndValidateMutation } from './parseAndValidateMutation'; import { type ReferenceGenome } from '../../lapisApi/ReferenceGenome'; import { type Deletion, type Insertion, type Mutation, type Substitution } from '../../utils/mutations'; import { ReferenceGenomeContext } from '../ReferenceGenomeContext'; import { ErrorBoundary } from '../components/error-boundary'; -import Info from '../components/info'; import { singleGraphColorRGBByName } from '../shared/charts/colors'; import { DeleteIcon } from '../shared/icons/DeleteIcon'; @@ -103,7 +103,7 @@ export const MutationFilterInner: FunctionComponent = return (
- Info for mutation filter +
{ ], }; - it('should return nucleotide when the segment is undefined for singe segmented genome', () => { + it('should return nucleotide when the segment is undefined for single segmented genome', () => { expect(sequenceTypeFromSegment('nuc1', singleSegmentedReferenceGenome)).toBe('nucleotide'); expect(sequenceTypeFromSegment(undefined, singleSegmentedReferenceGenome)).toBe('nucleotide'); }); diff --git a/components/src/preact/mutationFilter/sequenceTypeFromSegment.ts b/components/src/preact/mutationFilter/sequenceTypeFromSegment.ts index 26057559..3198c923 100644 --- a/components/src/preact/mutationFilter/sequenceTypeFromSegment.ts +++ b/components/src/preact/mutationFilter/sequenceTypeFromSegment.ts @@ -1,4 +1,4 @@ -import type { ReferenceGenome } from '../../lapisApi/ReferenceGenome'; +import { isSingleSegmented, type ReferenceGenome } from '../../lapisApi/ReferenceGenome'; import type { SequenceType } from '../../types'; export const sequenceTypeFromSegment = ( @@ -6,7 +6,7 @@ export const sequenceTypeFromSegment = ( referenceGenome: ReferenceGenome, ): SequenceType | undefined => { if (possibleSegment === undefined) { - return referenceGenome.nucleotideSequences.length === 1 ? 'nucleotide' : undefined; + return isSingleSegmented(referenceGenome) ? 'nucleotide' : undefined; } if (referenceGenome.nucleotideSequences.some((sequence) => sequence.name === possibleSegment)) { diff --git a/components/src/web-components/input/gs-mutation-filter.stories.ts b/components/src/web-components/input/gs-mutation-filter.stories.ts index 02a15378..0b68aa4a 100644 --- a/components/src/web-components/input/gs-mutation-filter.stories.ts +++ b/components/src/web-components/input/gs-mutation-filter.stories.ts @@ -4,7 +4,7 @@ import type { Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit'; import { withComponentDocs } from '../../../.storybook/ComponentDocsBlock'; -import { LAPIS_URL } from '../../constants'; +import { LAPIS_URL, REFERENCE_GENOME_ENDPOINT } from '../../constants'; import '../app'; import { type MutationFilterProps } from '../../preact/mutationFilter/mutation-filter'; import { withinShadowRoot } from '../withinShadowRoot.story'; @@ -128,3 +128,59 @@ export const FiresFilterOnBlurEvent: StoryObj = { }); }, }; + +export const MultiSegmentedReferenceGenomes: StoryObj = { + ...Template, + args: { + initialValue: ['seg1:123T', 'gene2:56', 'ins_seg2:78:AAA'], + }, + parameters: { + fetchMock: { + mocks: [ + { + matcher: { + name: 'referenceGenome', + url: REFERENCE_GENOME_ENDPOINT, + }, + response: { + status: 200, + body: { + nucleotideSequences: [ + { + name: 'seg1', + sequence: 'dummy', + }, + { + name: 'seg2', + sequence: 'dummy', + }, + ], + genes: [ + { + name: 'gene1', + sequence: 'dummy', + }, + { + name: 'gene2', + sequence: 'dummy', + }, + ], + }, + }, + options: { + overwriteRoutes: false, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = await withinShadowRoot(canvasElement, 'gs-mutation-filter'); + + await waitFor(() => { + expect(canvas.getByText('seg1:123T')).toBeVisible(); + expect(canvas.getByText('gene2:56')).toBeVisible(); + return expect(canvas.getByText('ins_seg2:78:AAA')).toBeVisible(); + }); + }, +};