@genspectrum/dashboard-components 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -109,6 +109,6 @@
109
109
  "typescript": "~5.5.2",
110
110
  "vite": "^5.2.10",
111
111
  "vite-plugin-dts": "^3.8.3",
112
- "vitest": "^1.5.0"
112
+ "vitest": "^2.0.1"
113
113
  }
114
114
  }
@@ -28,3 +28,5 @@ export const getSegmentNames = (referenceGenome: ReferenceGenome, sequenceType:
28
28
  }
29
29
  }
30
30
  };
31
+
32
+ export const isSingleSegmented = (referenceGenome: ReferenceGenome) => referenceGenome.nucleotideSequences.length === 1;
@@ -0,0 +1,117 @@
1
+ import { useContext } from 'preact/hooks';
2
+
3
+ import { isSingleSegmented } from '../../lapisApi/ReferenceGenome';
4
+ import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
5
+ import Info, { InfoHeadline1, InfoHeadline2, InfoParagraph } from '../components/info';
6
+
7
+ export const MutationFilterInfo = () => {
8
+ const referenceGenome = useContext(ReferenceGenomeContext);
9
+
10
+ const firstGene = referenceGenome.genes[0].name;
11
+ return (
12
+ <Info height={'80vh'}>
13
+ <InfoHeadline1> Mutation Filter</InfoHeadline1>
14
+ <InfoParagraph>This component allows you to filter for mutations at specific positions.</InfoParagraph>
15
+
16
+ <InfoHeadline2> Nucleotide Mutations and Insertions</InfoHeadline2>
17
+ {isSingleSegmented(referenceGenome) ? (
18
+ <SingleSegmentedNucleotideMutationsInfo />
19
+ ) : (
20
+ <MultiSegmentedNucleotideMutationsInfo />
21
+ )}
22
+
23
+ <InfoHeadline2>Amino Acid Mutations and Insertions</InfoHeadline2>
24
+ <InfoParagraph>
25
+ An amino acid mutation has the format <b>&lt;gene&gt;:&lt;position&gt;&lt;base&gt;</b> or
26
+ <b>&lt;gene&gt;:&lt;base_ref&gt;&lt;position&gt;&lt;base&gt;</b>. A <b>&lt;base&gt;</b> can be one of
27
+ the 20 amino acid codes. It can also be <b>-</b> for deletion and <b>X</b> for unknown. Example:{' '}
28
+ <b>E:57Q</b>.
29
+ </InfoParagraph>
30
+ <InfoParagraph>
31
+ Insertions can be searched for in the same manner, they just need to have <b>ins_</b> appended to the
32
+ start of the mutation. Example: <b>ins_{firstGene}:31:N</b> would filter for sequences with an insertion
33
+ of N between positions 31 and 32 in the gene {firstGene}.
34
+ </InfoParagraph>
35
+ <InfoParagraph>
36
+ This organism has the following genes: {referenceGenome.genes.map((gene) => gene.name).join(', ')}.
37
+ </InfoParagraph>
38
+
39
+ <InfoHeadline2>Insertion Wildcards</InfoHeadline2>
40
+ <InfoParagraph>
41
+ This component supports insertion queries that contain wildcards <b>?</b>. For example{' '}
42
+ <b>ins_{firstGene}:214:?EP?</b> will match all cases where segment <b>{firstGene}</b> has an insertion
43
+ of <b>EP</b> between the positions <b>214</b> and <b>215</b> but also an insertion of other amino acids
44
+ which include the <b>EP</b>, e.g. the insertion <b>EPE</b> will be matched.
45
+ </InfoParagraph>
46
+ <InfoParagraph>
47
+ You can also use wildcards to match any insertion at a given position. For example{' '}
48
+ <b>ins_{firstGene}:214:?</b> match any (but at least one) insertion between the positions 214 and 215.
49
+ </InfoParagraph>
50
+
51
+ <InfoHeadline2>Multiple Mutations</InfoHeadline2>
52
+ <InfoParagraph>
53
+ Multiple mutation filters can be provided by adding one mutation after the other.
54
+ </InfoParagraph>
55
+
56
+ <InfoHeadline2>Any Mutation</InfoHeadline2>
57
+ <InfoParagraph>
58
+ To filter for any mutation at a given position you can omit the <b>&lt;base&gt;</b>. Example:{' '}
59
+ <b>{firstGene}:20</b>.
60
+ </InfoParagraph>
61
+
62
+ <InfoHeadline2>No Mutation</InfoHeadline2>
63
+ <InfoParagraph>
64
+ You can write a <b>.</b> for the <b>&lt;base&gt;</b> to filter for sequences for which it is confirmed
65
+ that no mutation occurred, i.e. has the same base as the reference genome at the specified position.
66
+ </InfoParagraph>
67
+ </Info>
68
+ );
69
+ };
70
+
71
+ const SingleSegmentedNucleotideMutationsInfo = () => {
72
+ return (
73
+ <>
74
+ <InfoParagraph>
75
+ This organism is single-segmented. Thus, nucleotide mutations have the format{' '}
76
+ <b>&lt;position&gt;&lt;base&gt;</b> or <b>&lt;base_ref&gt;&lt;position&gt;&lt;base&gt;</b>. The{' '}
77
+ <b>&lt;base_ref&gt;</b> is the reference base at the position. It is optional. A <b>&lt;base&gt;</b> can
78
+ be one of the four nucleotides <b>A</b>, <b>T</b>, <b>C</b>, and <b>G</b>. It can also be <b>-</b> for
79
+ deletion and <b>N</b> for unknown. For example if the reference sequence is <b>A</b> at position{' '}
80
+ <b>23</b> both: <b>23T</b> and <b>A23T</b> will yield the same results.
81
+ </InfoParagraph>
82
+ <InfoParagraph>
83
+ Insertions can be searched for in the same manner, they just need to have <b>ins_</b> appended to the
84
+ start of the mutation. Example: <b>ins_1046:A</b> would filter for sequences with an insertion of A
85
+ between the positions 1046 and 1047 in the nucleotide sequence.
86
+ </InfoParagraph>
87
+ </>
88
+ );
89
+ };
90
+
91
+ const MultiSegmentedNucleotideMutationsInfo = () => {
92
+ const referenceGenome = useContext(ReferenceGenomeContext);
93
+
94
+ const firstSegment = referenceGenome.nucleotideSequences[0].name;
95
+
96
+ return (
97
+ <>
98
+ <InfoParagraph>
99
+ This organism is multi-segmented. Thus, nucleotide mutations have the format{' '}
100
+ <b>&lt;segment&gt;:&lt;position&gt;&lt;base&gt;</b> or{' '}
101
+ <b>&lt;segment&gt;:&lt;base_ref&gt;&lt;position&gt;&lt;base&gt;</b>. <b>&lt;base_ref&gt;</b> is the
102
+ reference base at the position. It is optional. A <b>&lt;base&gt;</b> can be one of the four nucleotides{' '}
103
+ <b>A</b>, <b>T</b>, <b>C</b>, and <b>G</b>. It can also be <b>-</b> for deletion and <b>N</b> for
104
+ unknown. For example if the reference sequence is <b>A</b> at position <b>23</b> both:{' '}
105
+ <b>{firstSegment}:23T</b> and <b>{firstSegment}:A23T</b> will yield the same results.
106
+ </InfoParagraph>
107
+ <InfoParagraph>
108
+ Insertions can be searched for in the same manner, they just need to have <b>ins_</b> appended to the
109
+ start of the mutation. Example: <b>ins_{firstSegment}:10462:A</b>.
110
+ </InfoParagraph>
111
+ <InfoParagraph>
112
+ This organism has the following segments:{' '}
113
+ {referenceGenome.nucleotideSequences.map((gene) => gene.name).join(', ')}.
114
+ </InfoParagraph>{' '}
115
+ </>
116
+ );
117
+ };
@@ -1,12 +1,12 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
  import { useContext, useRef, useState } from 'preact/hooks';
3
3
 
4
+ import { MutationFilterInfo } from './mutation-filter-info';
4
5
  import { parseAndValidateMutation } from './parseAndValidateMutation';
5
6
  import { type ReferenceGenome } from '../../lapisApi/ReferenceGenome';
6
7
  import { type Deletion, type Insertion, type Mutation, type Substitution } from '../../utils/mutations';
7
8
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
8
9
  import { ErrorBoundary } from '../components/error-boundary';
9
- import Info from '../components/info';
10
10
  import { singleGraphColorRGBByName } from '../shared/charts/colors';
11
11
  import { DeleteIcon } from '../shared/icons/DeleteIcon';
12
12
 
@@ -103,7 +103,7 @@ export const MutationFilterInner: FunctionComponent<MutationFilterInnerProps> =
103
103
  return (
104
104
  <form className='w-full border boder-gray-300 rounded-md relative' onSubmit={handleSubmit} ref={formRef}>
105
105
  <div className='absolute -top-3 -right-3'>
106
- <Info height={'100px'}>Info for mutation filter</Info>
106
+ <MutationFilterInfo />
107
107
  </div>
108
108
  <div className='w-full flex p-2 flex-wrap items-center'>
109
109
  <SelectedMutationDisplay
@@ -45,7 +45,7 @@ describe('getSequenceType', () => {
45
45
  ],
46
46
  };
47
47
 
48
- it('should return nucleotide when the segment is undefined for singe segmented genome', () => {
48
+ it('should return nucleotide when the segment is undefined for single segmented genome', () => {
49
49
  expect(sequenceTypeFromSegment('nuc1', singleSegmentedReferenceGenome)).toBe('nucleotide');
50
50
  expect(sequenceTypeFromSegment(undefined, singleSegmentedReferenceGenome)).toBe('nucleotide');
51
51
  });
@@ -1,4 +1,4 @@
1
- import type { ReferenceGenome } from '../../lapisApi/ReferenceGenome';
1
+ import { isSingleSegmented, type ReferenceGenome } from '../../lapisApi/ReferenceGenome';
2
2
  import type { SequenceType } from '../../types';
3
3
 
4
4
  export const sequenceTypeFromSegment = (
@@ -6,7 +6,7 @@ export const sequenceTypeFromSegment = (
6
6
  referenceGenome: ReferenceGenome,
7
7
  ): SequenceType | undefined => {
8
8
  if (possibleSegment === undefined) {
9
- return referenceGenome.nucleotideSequences.length === 1 ? 'nucleotide' : undefined;
9
+ return isSingleSegmented(referenceGenome) ? 'nucleotide' : undefined;
10
10
  }
11
11
 
12
12
  if (referenceGenome.nucleotideSequences.some((sequence) => sequence.name === possibleSegment)) {
@@ -4,7 +4,7 @@ import type { Meta, StoryObj } from '@storybook/web-components';
4
4
  import { html } from 'lit';
5
5
 
6
6
  import { withComponentDocs } from '../../../.storybook/ComponentDocsBlock';
7
- import { LAPIS_URL } from '../../constants';
7
+ import { LAPIS_URL, REFERENCE_GENOME_ENDPOINT } from '../../constants';
8
8
  import '../app';
9
9
  import { type MutationFilterProps } from '../../preact/mutationFilter/mutation-filter';
10
10
  import { withinShadowRoot } from '../withinShadowRoot.story';
@@ -128,3 +128,59 @@ export const FiresFilterOnBlurEvent: StoryObj<MutationFilterProps> = {
128
128
  });
129
129
  },
130
130
  };
131
+
132
+ export const MultiSegmentedReferenceGenomes: StoryObj<MutationFilterProps> = {
133
+ ...Template,
134
+ args: {
135
+ initialValue: ['seg1:123T', 'gene2:56', 'ins_seg2:78:AAA'],
136
+ },
137
+ parameters: {
138
+ fetchMock: {
139
+ mocks: [
140
+ {
141
+ matcher: {
142
+ name: 'referenceGenome',
143
+ url: REFERENCE_GENOME_ENDPOINT,
144
+ },
145
+ response: {
146
+ status: 200,
147
+ body: {
148
+ nucleotideSequences: [
149
+ {
150
+ name: 'seg1',
151
+ sequence: 'dummy',
152
+ },
153
+ {
154
+ name: 'seg2',
155
+ sequence: 'dummy',
156
+ },
157
+ ],
158
+ genes: [
159
+ {
160
+ name: 'gene1',
161
+ sequence: 'dummy',
162
+ },
163
+ {
164
+ name: 'gene2',
165
+ sequence: 'dummy',
166
+ },
167
+ ],
168
+ },
169
+ },
170
+ options: {
171
+ overwriteRoutes: false,
172
+ },
173
+ },
174
+ ],
175
+ },
176
+ },
177
+ play: async ({ canvasElement }) => {
178
+ const canvas = await withinShadowRoot(canvasElement, 'gs-mutation-filter');
179
+
180
+ await waitFor(() => {
181
+ expect(canvas.getByText('seg1:123T')).toBeVisible();
182
+ expect(canvas.getByText('gene2:56')).toBeVisible();
183
+ return expect(canvas.getByText('ins_seg2:78:AAA')).toBeVisible();
184
+ });
185
+ },
186
+ };
@@ -72,7 +72,7 @@ export class MutationFilterComponent extends PreactLitAdapter {
72
72
  * All values provided must be valid mutations or insertions.
73
73
  * Invalid values will be ignored.
74
74
  */
75
- @property()
75
+ @property({ type: Object })
76
76
  initialValue:
77
77
  {
78
78
  nucleotideMutations: string[];