@genspectrum/dashboard-components 0.13.5 → 0.13.7

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.
Files changed (46) hide show
  1. package/custom-elements.json +29 -29
  2. package/dist/components.d.ts +15 -17
  3. package/dist/components.js +462 -285
  4. package/dist/components.js.map +1 -1
  5. package/dist/style.css +20 -5
  6. package/dist/util.d.ts +14 -14
  7. package/package.json +1 -1
  8. package/src/preact/components/downshift-combobox.tsx +2 -2
  9. package/src/preact/components/mutation-info.tsx +36 -0
  10. package/src/preact/components/tabs.tsx +3 -5
  11. package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +13 -13
  12. package/src/preact/locationFilter/fetchAutocompletionList.ts +55 -19
  13. package/src/preact/locationFilter/location-filter.stories.tsx +1 -1
  14. package/src/preact/locationFilter/location-filter.tsx +18 -12
  15. package/src/preact/mutationComparison/mutation-comparison.tsx +26 -2
  16. package/src/preact/mutationFilter/ExampleMutation.tsx +68 -0
  17. package/src/preact/mutationFilter/mutation-filter-info.tsx +179 -112
  18. package/src/preact/mutationFilter/mutation-filter.tsx +10 -5
  19. package/src/preact/mutations/mutations.tsx +5 -23
  20. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +26 -4
  21. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +2 -6
  22. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +1 -1
  23. package/src/query/queryWastewaterMutationsOverTime.spec.ts +29 -1
  24. package/src/query/queryWastewaterMutationsOverTime.ts +30 -16
  25. package/src/web-components/{app.stories.ts → gs-app.stories.ts} +1 -1
  26. package/src/web-components/{app.ts → gs-app.ts} +2 -2
  27. package/src/web-components/index.ts +1 -1
  28. package/src/web-components/input/gs-date-range-selector.stories.ts +1 -1
  29. package/src/web-components/input/gs-lineage-filter.stories.ts +1 -1
  30. package/src/web-components/input/gs-location-filter.stories.ts +2 -2
  31. package/src/web-components/input/gs-mutation-filter.stories.ts +2 -2
  32. package/src/web-components/input/gs-text-input.stories.ts +1 -1
  33. package/src/web-components/visualization/gs-aggregate.stories.ts +1 -1
  34. package/src/web-components/visualization/gs-mutation-comparison.stories.ts +1 -1
  35. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +1 -1
  36. package/src/web-components/visualization/gs-mutations.stories.ts +1 -1
  37. package/src/web-components/visualization/gs-number-sequences-over-time.stories.ts +1 -1
  38. package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +1 -1
  39. package/src/web-components/visualization/gs-relative-growth-advantage.stories.ts +1 -1
  40. package/src/web-components/visualization/gs-sequences-by-location.stories.ts +1 -1
  41. package/src/web-components/visualization/gs-statistics.stories.ts +1 -1
  42. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +4 -1
  43. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +6 -2
  44. package/standalone-bundle/dashboard-components.js +5561 -5445
  45. package/standalone-bundle/dashboard-components.js.map +1 -1
  46. package/standalone-bundle/style.css +1 -1
@@ -1,58 +1,140 @@
1
1
  import { useContext } from 'preact/hooks';
2
- import { type FC } from 'react';
3
2
 
4
- import { isSingleSegmented } from '../../lapisApi/ReferenceGenome';
5
- import { type SequenceType } from '../../types';
3
+ import { isSingleSegmented, type ReferenceGenome } from '../../lapisApi/ReferenceGenome';
6
4
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
5
+ import { ExampleMutation } from './ExampleMutation';
7
6
  import Info, { InfoHeadline1, InfoHeadline2, InfoParagraph } from '../components/info';
8
7
 
9
8
  export const MutationFilterInfo = () => {
10
- const referenceGenome = useContext(ReferenceGenomeContext);
11
-
12
- const firstGene = referenceGenome.genes[0].name;
13
9
  return (
14
10
  <Info>
15
11
  <InfoHeadline1> Mutation Filter</InfoHeadline1>
16
12
  <InfoParagraph>This component allows you to filter for mutations at specific positions.</InfoParagraph>
17
13
 
14
+ <QuickStart />
15
+ <NucleotideMutationsInfo />
16
+ <AminoAcidMutationsInfo />
17
+ <InsertionWildcards />
18
+ <MultipleMutations />
19
+ <AnyMutation />
20
+ <NoMutation />
21
+ </Info>
22
+ );
23
+ };
24
+
25
+ const QuickStart = () => {
26
+ const referenceGenome = useContext(ReferenceGenomeContext);
27
+ return (
28
+ <>
18
29
  <InfoHeadline2>Quickstart</InfoHeadline2>
19
30
  <InfoParagraph>
20
31
  <ul className='list-disc list-inside'>
21
- <li>
22
- Filter for nucleotide mutations:{' '}
23
- <ExampleMutation mutationType='substitution' sequenceType='nucleotide' />
24
- </li>
25
- <li>
26
- Filter for amino acid mutations:{' '}
27
- <ExampleMutation mutationType='insertion' sequenceType='nucleotide' />
28
- </li>
29
- <li>
30
- Filter for nucleotide insertions:{' '}
31
- <ExampleMutation mutationType='substitution' sequenceType='amino acid' />
32
- </li>
33
- <li>
34
- Filter for amino acid insertions:{' '}
35
- <ExampleMutation mutationType='insertion' sequenceType='amino acid' />
36
- </li>
32
+ {referenceGenome.nucleotideSequences.length > 0 && (
33
+ <li>
34
+ Filter for nucleotide mutations:{' '}
35
+ <ExampleMutation mutationType='substitution' sequenceType='nucleotide' />
36
+ </li>
37
+ )}
38
+ {referenceGenome.genes.length > 0 && (
39
+ <li>
40
+ Filter for amino acid mutations:{' '}
41
+ <ExampleMutation mutationType='substitution' sequenceType='amino acid' />
42
+ </li>
43
+ )}
44
+ {referenceGenome.nucleotideSequences.length > 0 && (
45
+ <li>
46
+ Filter for nucleotide insertions:{' '}
47
+ <ExampleMutation mutationType='insertion' sequenceType='nucleotide' />
48
+ </li>
49
+ )}
50
+ {referenceGenome.genes.length > 0 && (
51
+ <li>
52
+ Filter for amino acid insertions:{' '}
53
+ <ExampleMutation mutationType='insertion' sequenceType='amino acid' />
54
+ </li>
55
+ )}
37
56
  </ul>
38
57
  </InfoParagraph>
39
- {!isSingleSegmented(referenceGenome) && (
58
+
59
+ {referenceGenome.nucleotideSequences.length > 1 ? (
40
60
  <InfoParagraph>
41
61
  This organism has the following segments:{' '}
42
62
  {referenceGenome.nucleotideSequences.map((gene) => gene.name).join(', ')}.
43
63
  </InfoParagraph>
64
+ ) : (
65
+ <InfoParagraph>This organism doesn't support nucleotide sequences.</InfoParagraph>
66
+ )}
67
+ {referenceGenome.genes.length !== 0 ? (
68
+ <InfoParagraph>
69
+ This organism has the following genes: {referenceGenome.genes.map((gene) => gene.name).join(', ')}.
70
+ </InfoParagraph>
71
+ ) : (
72
+ <InfoParagraph>This organism doesn't support amino acid sequences.</InfoParagraph>
44
73
  )}
74
+ </>
75
+ );
76
+ };
77
+
78
+ const NucleotideMutationsInfo = () => {
79
+ const referenceGenome = useContext(ReferenceGenomeContext);
80
+
81
+ if (referenceGenome.nucleotideSequences.length === 0) {
82
+ return null;
83
+ }
84
+
85
+ if (isSingleSegmented(referenceGenome)) {
86
+ return (
87
+ <>
88
+ <InfoHeadline2>Nucleotide Mutations and Insertions</InfoHeadline2>
89
+ <InfoParagraph>
90
+ This organism is single-segmented. Thus, nucleotide mutations have the format{' '}
91
+ <b>&lt;position&gt;&lt;base&gt;</b> or <b>&lt;base_ref&gt;&lt;position&gt;&lt;base&gt;</b>. The{' '}
92
+ <b>&lt;base_ref&gt;</b> is the reference base at the position. It is optional. A <b>&lt;base&gt;</b>{' '}
93
+ can be one of the four nucleotides <b>A</b>, <b>T</b>, <b>C</b>, and <b>G</b>. It can also be{' '}
94
+ <b>-</b> for deletion and <b>N</b> for unknown. For example if the reference sequence is <b>A</b> at
95
+ position <b>23</b> both: <b>23T</b> and <b>A23T</b> will yield the same results.
96
+ </InfoParagraph>
97
+ <InfoParagraph>
98
+ Insertions can be searched for in the same manner, they just need to have <b>ins_</b> appended to
99
+ the start of the mutation. Example: <b>ins_1046:A</b> would filter for sequences with an insertion
100
+ of A between the positions 1046 and 1047 in the nucleotide sequence.
101
+ </InfoParagraph>
102
+ </>
103
+ );
104
+ }
105
+
106
+ const firstSegment = referenceGenome.nucleotideSequences[0].name;
107
+ return (
108
+ <>
109
+ <InfoHeadline2>Nucleotide Mutations and Insertions</InfoHeadline2>
45
110
  <InfoParagraph>
46
- This organism has the following genes: {referenceGenome.genes.map((gene) => gene.name).join(', ')}.
111
+ This organism is multi-segmented. Thus, nucleotide mutations have the format{' '}
112
+ <b>&lt;segment&gt;:&lt;position&gt;&lt;base&gt;</b> or{' '}
113
+ <b>&lt;segment&gt;:&lt;base_ref&gt;&lt;position&gt;&lt;base&gt;</b>. <b>&lt;base_ref&gt;</b> is the
114
+ reference base at the position. It is optional. A <b>&lt;base&gt;</b> can be one of the four nucleotides{' '}
115
+ <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
116
+ unknown. For example if the reference sequence is <b>A</b> at position <b>23</b> both:{' '}
117
+ <b>{firstSegment}:23T</b> and <b>{firstSegment}:A23T</b> will yield the same results.
47
118
  </InfoParagraph>
119
+ <InfoParagraph>
120
+ Insertions can be searched for in the same manner, they just need to have <b>ins_</b> appended to the
121
+ start of the mutation. Example: <ExampleMutation mutationType='insertion' sequenceType='nucleotide' />.
122
+ </InfoParagraph>
123
+ </>
124
+ );
125
+ };
48
126
 
49
- <InfoHeadline2>Nucleotide Mutations and Insertions</InfoHeadline2>
50
- {isSingleSegmented(referenceGenome) ? (
51
- <SingleSegmentedNucleotideMutationsInfo />
52
- ) : (
53
- <MultiSegmentedNucleotideMutationsInfo />
54
- )}
127
+ const AminoAcidMutationsInfo = () => {
128
+ const referenceGenome = useContext(ReferenceGenomeContext);
55
129
 
130
+ if (referenceGenome.genes.length === 0) {
131
+ return null;
132
+ }
133
+
134
+ const firstGene = referenceGenome.genes[0].name;
135
+
136
+ return (
137
+ <>
56
138
  <InfoHeadline2>Amino Acid Mutations and Insertions</InfoHeadline2>
57
139
  <InfoParagraph>
58
140
  An amino acid mutation has the format <b>&lt;gene&gt;:&lt;position&gt;&lt;base&gt;</b> or
@@ -65,116 +147,101 @@ export const MutationFilterInfo = () => {
65
147
  start of the mutation. Example: <b>ins_{firstGene}:31:N</b> would filter for sequences with an insertion
66
148
  of N between positions 31 and 32 in the gene {firstGene}.
67
149
  </InfoParagraph>
150
+ </>
151
+ );
152
+ };
153
+
154
+ const InsertionWildcards = () => {
155
+ const referenceGenome = useContext(ReferenceGenomeContext);
68
156
 
157
+ if (referenceGenome.nucleotideSequences.length === 0 && referenceGenome.genes.length === 0) {
158
+ return null;
159
+ }
160
+
161
+ return (
162
+ <>
69
163
  <InfoHeadline2>Insertion Wildcards</InfoHeadline2>
70
164
  <InfoParagraph>
71
- This component supports insertion queries that contain wildcards <b>?</b>. For example{' '}
72
- <b>ins_{firstGene}:214:?EP?</b> will match all cases where segment <b>{firstGene}</b> has an insertion
73
- of <b>EP</b> between the positions <b>214</b> and <b>215</b> but also an insertion of other amino acids
74
- which include the <b>EP</b>, e.g. the insertion <b>EPE</b> will be matched.
165
+ This component supports nucleotide and amino acid insertion queries that contain wildcards <b>?</b>. For
166
+ example{' '}
167
+ <b>
168
+ ins_{exampleSegmentString(referenceGenome)}214:?{exampleWildcardInsertion(referenceGenome)}?
169
+ </b>{' '}
170
+ will match all cases where segment <b>{exampleSegment(referenceGenome)}</b> has an insertion of{' '}
171
+ <b>{exampleWildcardInsertion(referenceGenome)}</b> between the positions <b>214</b> and <b>215</b> but
172
+ also an insertion of other amino acids which include the <b>EP</b>, e.g. the insertion{' '}
173
+ <b>{exampleWildcardInsertion(referenceGenome)}T</b> will be matched.
75
174
  </InfoParagraph>
76
175
  <InfoParagraph>
77
176
  You can also use wildcards to match any insertion at a given position. For example{' '}
78
- <b>ins_{firstGene}:214:?</b> match any (but at least one) insertion between the positions 214 and 215.
177
+ <b>ins_{exampleSegmentString(referenceGenome)}214:?</b> match any (but at least one) insertion between
178
+ the positions 214 and 215.
79
179
  </InfoParagraph>
180
+ </>
181
+ );
182
+ };
80
183
 
81
- <InfoHeadline2>Multiple Mutations</InfoHeadline2>
82
- <InfoParagraph>
83
- Multiple mutation filters can be provided by adding one mutation after the other.
84
- </InfoParagraph>
184
+ const exampleSegmentString = (referenceGenome: ReferenceGenome) => {
185
+ const segment = exampleSegment(referenceGenome);
186
+ if (segment === '') {
187
+ return '';
188
+ }
189
+ return `${segment}:`;
190
+ };
85
191
 
86
- <InfoHeadline2>Any Mutation</InfoHeadline2>
87
- <InfoParagraph>
88
- To filter for any mutation at a given position you can omit the <b>&lt;base&gt;</b>. Example:{' '}
89
- <b>{firstGene}:20</b>.
90
- </InfoParagraph>
192
+ const exampleSegment = (referenceGenome: ReferenceGenome) => {
193
+ if (referenceGenome.genes.length > 0) {
194
+ return `${referenceGenome.genes[0].name}`;
195
+ }
196
+ if (referenceGenome.nucleotideSequences.length > 1) {
197
+ return `${referenceGenome.nucleotideSequences[0].name}`;
198
+ }
199
+ return '';
200
+ };
91
201
 
92
- <InfoHeadline2>No Mutation</InfoHeadline2>
93
- <InfoParagraph>
94
- You can write a <b>.</b> for the <b>&lt;base&gt;</b> to filter for sequences for which it is confirmed
95
- that no mutation occurred, i.e. has the same base as the reference genome at the specified position.
96
- </InfoParagraph>
97
- </Info>
98
- );
202
+ const exampleWildcardInsertion = (referenceGenome: ReferenceGenome) => {
203
+ if (referenceGenome.genes.length > 0) {
204
+ return 'EP';
205
+ }
206
+ if (referenceGenome.nucleotideSequences.length > 0) {
207
+ return 'CG';
208
+ }
209
+ return '';
99
210
  };
100
211
 
101
- const SingleSegmentedNucleotideMutationsInfo = () => {
212
+ const MultipleMutations = () => {
102
213
  return (
103
214
  <>
215
+ <InfoHeadline2>Multiple Mutations</InfoHeadline2>
104
216
  <InfoParagraph>
105
- This organism is single-segmented. Thus, nucleotide mutations have the format{' '}
106
- <b>&lt;position&gt;&lt;base&gt;</b> or <b>&lt;base_ref&gt;&lt;position&gt;&lt;base&gt;</b>. The{' '}
107
- <b>&lt;base_ref&gt;</b> is the reference base at the position. It is optional. A <b>&lt;base&gt;</b> can
108
- 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
109
- deletion and <b>N</b> for unknown. For example if the reference sequence is <b>A</b> at position{' '}
110
- <b>23</b> both: <b>23T</b> and <b>A23T</b> will yield the same results.
111
- </InfoParagraph>
112
- <InfoParagraph>
113
- Insertions can be searched for in the same manner, they just need to have <b>ins_</b> appended to the
114
- start of the mutation. Example: <b>ins_1046:A</b> would filter for sequences with an insertion of A
115
- between the positions 1046 and 1047 in the nucleotide sequence.
217
+ Multiple mutation filters can be provided by adding one mutation after the other.
116
218
  </InfoParagraph>
117
219
  </>
118
220
  );
119
221
  };
120
222
 
121
- const MultiSegmentedNucleotideMutationsInfo = () => {
223
+ const AnyMutation = () => {
122
224
  const referenceGenome = useContext(ReferenceGenomeContext);
123
225
 
124
- const firstSegment = referenceGenome.nucleotideSequences[0].name;
125
-
126
226
  return (
127
227
  <>
228
+ <InfoHeadline2>Any Mutation</InfoHeadline2>
128
229
  <InfoParagraph>
129
- This organism is multi-segmented. Thus, nucleotide mutations have the format{' '}
130
- <b>&lt;segment&gt;:&lt;position&gt;&lt;base&gt;</b> or{' '}
131
- <b>&lt;segment&gt;:&lt;base_ref&gt;&lt;position&gt;&lt;base&gt;</b>. <b>&lt;base_ref&gt;</b> is the
132
- reference base at the position. It is optional. A <b>&lt;base&gt;</b> can be one of the four nucleotides{' '}
133
- <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
134
- unknown. For example if the reference sequence is <b>A</b> at position <b>23</b> both:{' '}
135
- <b>{firstSegment}:23T</b> and <b>{firstSegment}:A23T</b> will yield the same results.
136
- </InfoParagraph>
137
- <InfoParagraph>
138
- Insertions can be searched for in the same manner, they just need to have <b>ins_</b> appended to the
139
- start of the mutation. Example: <ExampleMutation mutationType='insertion' sequenceType='nucleotide' />.
230
+ To filter for any mutation at a given position you can omit the <b>&lt;base&gt;</b>. Example:{' '}
231
+ <b>{exampleSegmentString(referenceGenome)}20</b>.
140
232
  </InfoParagraph>
141
233
  </>
142
234
  );
143
235
  };
144
236
 
145
- type ExampleMutationProps = {
146
- sequenceType: SequenceType;
147
- mutationType: 'substitution' | 'insertion';
148
- };
149
-
150
- const ExampleMutation: FC<ExampleMutationProps> = ({ sequenceType, mutationType }) => {
151
- const referenceGenome = useContext(ReferenceGenomeContext);
152
-
153
- const firstSegment = referenceGenome.nucleotideSequences[0].name;
154
- const firstGene = referenceGenome.genes[0].name;
155
-
156
- if (sequenceType === 'amino acid') {
157
- switch (mutationType) {
158
- case 'substitution':
159
- return <b>{firstGene}:57Q</b>;
160
- case 'insertion':
161
- return <b>ins_{firstGene}:31:N</b>;
162
- }
163
- }
164
-
165
- if (isSingleSegmented(referenceGenome)) {
166
- switch (mutationType) {
167
- case 'substitution':
168
- return <b>23T</b>;
169
- case 'insertion':
170
- return <b>ins_1046:A</b>;
171
- }
172
- }
173
-
174
- switch (mutationType) {
175
- case 'substitution':
176
- return <b>{firstSegment}:23T</b>;
177
- case 'insertion':
178
- return <b>ins_{firstSegment}:10462:A</b>;
179
- }
237
+ const NoMutation = () => {
238
+ return (
239
+ <>
240
+ <InfoHeadline2>No Mutation</InfoHeadline2>
241
+ <InfoParagraph>
242
+ You can write a <b>.</b> for the <b>&lt;base&gt;</b> to filter for sequences for which it is confirmed
243
+ that no mutation occurred, i.e. has the same base as the reference genome at the specified position.
244
+ </InfoParagraph>
245
+ </>
246
+ );
180
247
  };
@@ -2,6 +2,7 @@ import { type FunctionComponent } from 'preact';
2
2
  import { useContext, useRef, useState } from 'preact/hooks';
3
3
  import z from 'zod';
4
4
 
5
+ import { getExampleMutation } from './ExampleMutation';
5
6
  import { MutationFilterInfo } from './mutation-filter-info';
6
7
  import { parseAndValidateMutation, type ParsedMutationFilter } from './parseAndValidateMutation';
7
8
  import { type ReferenceGenome } from '../../lapisApi/ReferenceGenome';
@@ -208,7 +209,6 @@ const MutationFilterSelector: FunctionComponent<{
208
209
  };
209
210
 
210
211
  const handleBlur = (event: FocusEvent) => {
211
- // Check if click was inside the selector
212
212
  if (!selectorRef.current?.contains(event.relatedTarget as Node)) {
213
213
  setOption(null);
214
214
  }
@@ -242,11 +242,16 @@ const MutationFilterSelector: FunctionComponent<{
242
242
  };
243
243
 
244
244
  function getPlaceholder(referenceGenome: ReferenceGenome) {
245
- const segmentPrefix =
246
- referenceGenome.nucleotideSequences.length > 1 ? `${referenceGenome.nucleotideSequences[0].name}:` : '';
247
- const firstGene = referenceGenome.genes[0].name;
245
+ const nucleotideSubstitution = getExampleMutation(referenceGenome, 'nucleotide', 'substitution');
246
+ const nucleotideInsertion = getExampleMutation(referenceGenome, 'nucleotide', 'insertion');
247
+ const aminoAcidSubstitution = getExampleMutation(referenceGenome, 'amino acid', 'substitution');
248
+ const aminoAcidInsertion = getExampleMutation(referenceGenome, 'amino acid', 'insertion');
248
249
 
249
- return `Enter a mutation (e.g. ${segmentPrefix}A123T, ins_${segmentPrefix}123:AT, ${firstGene}:M123E, ins_${firstGene}:123:ME)`;
250
+ const exampleMutations = [nucleotideSubstitution, nucleotideInsertion, aminoAcidSubstitution, aminoAcidInsertion]
251
+ .filter((example) => example !== '')
252
+ .join(', ');
253
+
254
+ return `Enter a mutation (e.g. ${exampleMutations})`;
250
255
  }
251
256
 
252
257
  const backgroundColor: { [key in keyof SelectedFilters]: string } = {
@@ -19,8 +19,9 @@ import { LapisUrlContext } from '../LapisUrlContext';
19
19
  import { CsvDownloadButton } from '../components/csv-download-button';
20
20
  import { ErrorBoundary } from '../components/error-boundary';
21
21
  import { Fullscreen } from '../components/fullscreen';
22
- import Info, { InfoComponentCode, InfoHeadline1, InfoHeadline2, InfoLink, InfoParagraph } from '../components/info';
22
+ import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../components/info';
23
23
  import { LoadingDisplay } from '../components/loading-display';
24
+ import { DeletionsLink, InsertionsLink, ProportionExplanation, SubstitutionsLink } from '../components/mutation-info';
24
25
  import { type DisplayedMutationType, MutationTypeSelector } from '../components/mutation-type-selector';
25
26
  import { NoDataDisplay } from '../components/no-data-display';
26
27
  import type { ProportionInterval } from '../components/proportion-selector';
@@ -242,29 +243,10 @@ const MutationsInfo: FunctionComponent<MutationsInfoProps> = ({ originalComponen
242
243
  <Info>
243
244
  <InfoHeadline1>Mutations</InfoHeadline1>
244
245
  <InfoParagraph>
245
- This shows mutations of a variant. There are three types of mutations:{' '}
246
- <InfoLink href='https://www.genome.gov/genetics-glossary/Substitution'>substitutions</InfoLink>,{' '}
247
- <InfoLink href='https://www.genome.gov/genetics-glossary/Deletion'>deletions</InfoLink> and{' '}
248
- <InfoLink href='https://www.genome.gov/genetics-glossary/Insertion'>insertions</InfoLink>.
249
- </InfoParagraph>
250
- <InfoHeadline2>Proportion calculation</InfoHeadline2>
251
- <InfoParagraph>
252
- The proportion of a mutation is calculated by dividing the number of sequences with the mutation by the
253
- total number of sequences with a non-ambiguous symbol at the position.
254
- </InfoParagraph>
255
- <InfoParagraph>
256
- <b>Example:</b> Assume we look at nucleotide mutations at position 5 where the reference has a T and
257
- assume there are 10 sequences in total:
258
- <ul className='list-disc list-inside ml-2'>
259
- <li>3 sequences have a C,</li>
260
- <li>2 sequences have a T,</li>
261
- <li>1 sequence has a G,</li>
262
- <li>3 sequences have an N,</li>
263
- <li>1 sequence has a Y (which means T or C),</li>
264
- </ul>
265
- then the proportion of the T5C mutation is 50%. The 4 sequences that have an N or Y are excluded from
266
- the calculation.
246
+ This shows mutations of a variant. There are three types of mutations: <SubstitutionsLink />,{' '}
247
+ <DeletionsLink /> and <InsertionsLink />.
267
248
  </InfoParagraph>
249
+ <ProportionExplanation />
268
250
  <InfoComponentCode componentName='mutations' params={originalComponentProps} lapisUrl={lapis} />
269
251
  </Info>
270
252
  );
@@ -49,13 +49,23 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
49
49
  gridTemplateRows: `repeat(${shownMutations.length}, 24px)`,
50
50
  gridTemplateColumns: `${MUTATION_CELL_WIDTH_REM}rem repeat(${dates.length}, minmax(0.05rem, 1fr))`,
51
51
  }}
52
+ className='text-center'
52
53
  >
54
+ {dates.map((date, columnIndex) => (
55
+ <div
56
+ className='@container font-semibold'
57
+ style={{ gridRowStart: 1, gridColumnStart: columnIndex + 2 }}
58
+ key={date.dateString}
59
+ >
60
+ <p {...styleGridHeader(columnIndex, dates)}>{date.dateString}</p>
61
+ </div>
62
+ ))}
53
63
  {shownMutations.map((mutation, rowIndex) => {
54
64
  return (
55
65
  <Fragment key={`fragment-${mutation.toString()}`}>
56
66
  <div
57
67
  key={`mutation-${mutation.toString()}`}
58
- style={{ gridRowStart: rowIndex + 1, gridColumnStart: 1 }}
68
+ style={{ gridRowStart: rowIndex + 2, gridColumnStart: 1 }}
59
69
  >
60
70
  <MutationCell mutation={mutation} />
61
71
  </div>
@@ -69,7 +79,7 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
69
79
  );
70
80
  return (
71
81
  <div
72
- style={{ gridRowStart: rowIndex + 1, gridColumnStart: columnIndex + 2 }}
82
+ style={{ gridRowStart: rowIndex + 2, gridColumnStart: columnIndex + 2 }}
73
83
  key={`${mutation.toString()}-${date.toString()}`}
74
84
  >
75
85
  <ProportionCell
@@ -90,6 +100,18 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
90
100
  );
91
101
  };
92
102
 
103
+ function styleGridHeader(columnIndex: number, dates: Temporal[]) {
104
+ if (columnIndex === 0) {
105
+ return { className: 'overflow-visible text-nowrap' };
106
+ }
107
+
108
+ if (columnIndex === dates.length - 1) {
109
+ return { className: 'overflow-visible text-nowrap', style: { direction: 'rtl' } };
110
+ }
111
+
112
+ return { className: 'invisible @[6rem]:visible' };
113
+ }
114
+
93
115
  function getTooltipPosition(rowIndex: number, rows: number, columnIndex: number, columns: number) {
94
116
  const tooltipX = rowIndex < rows / 2 || rowIndex < 6 ? 'bottom' : 'top';
95
117
  const tooltipY = columnIndex < columns / 2 ? 'start' : 'end';
@@ -135,7 +157,7 @@ const ProportionCell: FunctionComponent<{
135
157
  backgroundColor: getColorWithingScale(value?.proportion, colorScale),
136
158
  color: getTextColorForScale(value?.proportion, colorScale),
137
159
  }}
138
- className={`w-full h-full text-center hover:font-bold text-xs group @container`}
160
+ className={`w-full h-full hover:font-bold text-xs group @container`}
139
161
  >
140
162
  <span className='invisible @[2rem]:visible'>
141
163
  {value === null ? '' : formatProportion(value.proportion, 0)}
@@ -155,7 +177,7 @@ const timeIntervalDisplay = (date: TemporalClass) => {
155
177
  };
156
178
 
157
179
  const MutationCell: FunctionComponent<{ mutation: Substitution | Deletion }> = ({ mutation }) => {
158
- return <div className='text-center'>{mutation.code}</div>;
180
+ return <div>{mutation.code}</div>;
159
181
  };
160
182
 
161
183
  export default MutationsOverTimeGrid;
@@ -30,12 +30,7 @@ const Template = {
30
30
  render: (args: WastewaterMutationsOverTimeProps) => (
31
31
  <LapisUrlContext.Provider value={WISE_LAPIS_URL}>
32
32
  <ReferenceGenomeContext.Provider value={referenceGenome}>
33
- <WastewaterMutationsOverTime
34
- width={args.width}
35
- height={args.height}
36
- lapisFilter={args.lapisFilter}
37
- sequenceType={args.sequenceType}
38
- />
33
+ <WastewaterMutationsOverTime {...args} />
39
34
  </ReferenceGenomeContext.Provider>
40
35
  </LapisUrlContext.Provider>
41
36
  ),
@@ -48,6 +43,7 @@ export const Default: StoryObj<WastewaterMutationsOverTimeProps> = {
48
43
  height: '700px',
49
44
  lapisFilter: {},
50
45
  sequenceType: 'nucleotide',
46
+ maxNumberOfGridRows: 100,
51
47
  },
52
48
  parameters: {
53
49
  fetchMock: {
@@ -23,7 +23,7 @@ const wastewaterMutationOverTimeSchema = z.object({
23
23
  sequenceType: sequenceTypeSchema,
24
24
  width: z.string(),
25
25
  height: z.string(),
26
- maxNumberOfGridRows: z.number().optional(),
26
+ maxNumberOfGridRows: z.number(),
27
27
  });
28
28
 
29
29
  export type WastewaterMutationsOverTimeProps = z.infer<typeof wastewaterMutationOverTimeSchema>;
@@ -88,7 +88,35 @@ describe('queryWastewaterMutationsOverTime', () => {
88
88
  );
89
89
 
90
90
  await expect(queryWastewaterMutationsOverTime(DUMMY_LAPIS_URL, lapisFilter)).rejects.toThrowError(
91
- /^Failed to parse mutation frequency/,
91
+ /Failed to parse mutation frequency/,
92
+ );
93
+ });
94
+
95
+ it('should error on invalid mutation', async () => {
96
+ const lapisFilter = { country: 'Germany' };
97
+
98
+ lapisRequestMocks.details(
99
+ {
100
+ country: 'Germany',
101
+ fields: ['date', 'location', 'nucleotideMutationFrequency', 'aminoAcidMutationFrequency'],
102
+ },
103
+ {
104
+ data: [
105
+ {
106
+ date: '2021-01-01',
107
+ location: 'Germany',
108
+ reference: 'organismA',
109
+ nucleotideMutationFrequency: JSON.stringify({
110
+ 'not a mutation': 0.4,
111
+ }),
112
+ aminoAcidMutationFrequency: null,
113
+ },
114
+ ],
115
+ },
116
+ );
117
+
118
+ await expect(queryWastewaterMutationsOverTime(DUMMY_LAPIS_URL, lapisFilter)).rejects.toThrowError(
119
+ /Failed to parse mutation: "not a mutation"/,
92
120
  );
93
121
  });
94
122
  });
@@ -25,18 +25,26 @@ export async function queryWastewaterMutationsOverTime(
25
25
  ]);
26
26
  const data = (await fetchData.evaluate(lapis, signal)).content;
27
27
 
28
- return data.map((row) => ({
29
- location: row.location as string,
30
- date: toTemporalClass(parseDateStringToTemporal(row.date as string, 'day')),
31
- nucleotideMutationFrequency:
32
- row.nucleotideMutationFrequency !== null
33
- ? transformMutations(JSON.parse(row.nucleotideMutationFrequency as string))
34
- : [],
35
- aminoAcidMutationFrequency:
36
- row.aminoAcidMutationFrequency !== null
37
- ? transformMutations(JSON.parse(row.aminoAcidMutationFrequency as string))
38
- : [],
39
- }));
28
+ return data.map((row) => {
29
+ try {
30
+ return {
31
+ location: row.location as string,
32
+ date: toTemporalClass(parseDateStringToTemporal(row.date as string, 'day')),
33
+ nucleotideMutationFrequency:
34
+ row.nucleotideMutationFrequency !== null
35
+ ? transformMutations(JSON.parse(row.nucleotideMutationFrequency as string))
36
+ : [],
37
+ aminoAcidMutationFrequency:
38
+ row.aminoAcidMutationFrequency !== null
39
+ ? transformMutations(JSON.parse(row.aminoAcidMutationFrequency as string))
40
+ : [],
41
+ };
42
+ } catch (e) {
43
+ throw new Error(
44
+ `Failed to parse row of wastewater data: ${JSON.stringify(row)}: ${(e as Error)?.message ?? 'Unknown error'}`,
45
+ );
46
+ }
47
+ });
40
48
  }
41
49
 
42
50
  const mutationFrequencySchema = z.record(z.number().nullable());
@@ -48,8 +56,14 @@ function transformMutations(input: unknown): { mutation: Substitution; proportio
48
56
  throw new Error(`Failed to parse mutation frequency: ${mutationFrequency.error.message}`);
49
57
  }
50
58
 
51
- return Object.entries(mutationFrequency.data).map(([key, value]) => ({
52
- mutation: SubstitutionClass.parse(key)!,
53
- proportion: value,
54
- }));
59
+ return Object.entries(mutationFrequency.data).map(([key, value]) => {
60
+ const mutation = SubstitutionClass.parse(key);
61
+ if (mutation === null) {
62
+ throw new Error(`Failed to parse mutation: "${key}"`);
63
+ }
64
+ return {
65
+ mutation,
66
+ proportion: value,
67
+ };
68
+ });
55
69
  }
@@ -4,7 +4,7 @@ import type { Meta, StoryObj } from '@storybook/web-components';
4
4
  import { html, LitElement } from 'lit';
5
5
  import { customElement } from 'lit/decorators.js';
6
6
 
7
- import './app';
7
+ import './gs-app';
8
8
 
9
9
  import { lapisContext } from './lapis-context';
10
10
  import { referenceGenomeContext } from './reference-genome-context';