@genspectrum/dashboard-components 1.6.0 → 1.8.0

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 (44) hide show
  1. package/README.md +4 -0
  2. package/custom-elements.json +82 -1
  3. package/dist/{NumberRangeFilterChangedEvent-CQ32Qy8D.js → NumberRangeFilterChangedEvent-BnPI-Asz.js} +17 -3
  4. package/dist/NumberRangeFilterChangedEvent-BnPI-Asz.js.map +1 -0
  5. package/dist/assets/{mutationOverTimeWorker-BmB6BvVM.js.map → mutationOverTimeWorker-DPS3tmOd.js.map} +1 -1
  6. package/dist/components.d.ts +52 -25
  7. package/dist/components.js +209 -75
  8. package/dist/components.js.map +1 -1
  9. package/dist/util.d.ts +48 -24
  10. package/dist/util.js +2 -1
  11. package/package.json +1 -1
  12. package/src/preact/MutationLinkTemplateContext.tsx +56 -0
  13. package/src/preact/components/annotated-mutation.stories.tsx +69 -17
  14. package/src/preact/components/annotated-mutation.tsx +45 -19
  15. package/src/preact/genomeViewer/CDSPlot.tsx +13 -2
  16. package/src/preact/genomeViewer/loadGff3.ts +6 -0
  17. package/src/preact/mutationComparison/mutation-comparison-table.tsx +3 -0
  18. package/src/preact/mutationFilter/mutation-filter.stories.tsx +2 -1
  19. package/src/preact/mutationFilter/mutation-filter.tsx +24 -27
  20. package/src/preact/mutationFilter/parseAndValidateMutation.ts +11 -11
  21. package/src/preact/mutationFilter/parseMutation.spec.ts +32 -22
  22. package/src/preact/mutations/mutations-table.tsx +3 -0
  23. package/src/preact/mutationsOverTime/mutations-over-time.tsx +7 -4
  24. package/src/preact/shared/stories/expectMutationAnnotation.ts +3 -1
  25. package/src/types.ts +17 -1
  26. package/src/utilEntrypoint.ts +4 -0
  27. package/src/utils/mutations.spec.ts +19 -0
  28. package/src/utils/mutations.ts +57 -10
  29. package/src/web-components/gs-app.spec-d.ts +7 -0
  30. package/src/web-components/gs-app.stories.ts +32 -2
  31. package/src/web-components/gs-app.ts +17 -0
  32. package/src/web-components/input/gs-mutation-filter.stories.ts +2 -1
  33. package/src/web-components/input/gs-mutation-filter.tsx +2 -6
  34. package/src/web-components/mutation-link-template-context.ts +13 -0
  35. package/src/web-components/mutationLinks.mdx +27 -0
  36. package/src/web-components/visualization/gs-mutation-comparison.tsx +18 -8
  37. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +12 -1
  38. package/src/web-components/visualization/gs-mutations-over-time.tsx +24 -14
  39. package/src/web-components/visualization/gs-mutations.tsx +19 -9
  40. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +17 -7
  41. package/standalone-bundle/assets/{mutationOverTimeWorker-B_xP8pIC.js.map → mutationOverTimeWorker-Dp-A14AP.js.map} +1 -1
  42. package/standalone-bundle/dashboard-components.js +7295 -7193
  43. package/standalone-bundle/dashboard-components.js.map +1 -1
  44. package/dist/NumberRangeFilterChangedEvent-CQ32Qy8D.js.map +0 -1
@@ -7,7 +7,13 @@ import { getExampleMutation } from './ExampleMutation';
7
7
  import { MutationFilterInfo } from './mutation-filter-info';
8
8
  import { parseAndValidateMutation } from './parseAndValidateMutation';
9
9
  import { type ReferenceGenome } from '../../lapisApi/ReferenceGenome';
10
- import { type MutationsFilter, mutationsFilterSchema } from '../../types';
10
+ import {
11
+ type MutationsFilter,
12
+ mutationsFilterSchema,
13
+ mutationType,
14
+ mutationTypeSchema,
15
+ type MutationType,
16
+ } from '../../types';
11
17
  import { gsEventNames } from '../../utils/gsEventNames';
12
18
  import { type DeletionClass, type InsertionClass, type SubstitutionClass } from '../../utils/mutations';
13
19
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
@@ -15,15 +21,6 @@ import { ErrorBoundary } from '../components/error-boundary';
15
21
  import { UserFacingError } from '../components/error-display';
16
22
  import { singleGraphColorRGBByName } from '../shared/charts/colors';
17
23
 
18
- const mutationTypeSchema = z.enum([
19
- 'nucleotideMutations',
20
- 'aminoAcidMutations',
21
- 'nucleotideInsertions',
22
- 'aminoAcidInsertions',
23
- ]);
24
-
25
- export type MutationType = z.infer<typeof mutationTypeSchema>;
26
-
27
24
  const mutationFilterInnerPropsSchema = z.object({
28
25
  initialValue: z.union([mutationsFilterSchema.optional(), z.array(z.string()), z.undefined()]),
29
26
  enabledMutationTypes: z.array(mutationTypeSchema).optional(),
@@ -37,22 +34,22 @@ export type MutationFilterInnerProps = z.infer<typeof mutationFilterInnerPropsSc
37
34
  export type MutationFilterProps = z.infer<typeof mutationFilterPropsSchema>;
38
35
 
39
36
  type SelectedNucleotideMutation = {
40
- type: 'nucleotideMutations';
37
+ type: typeof mutationType.nucleotideMutations;
41
38
  value: SubstitutionClass | DeletionClass;
42
39
  };
43
40
 
44
41
  type SelectedAminoAcidMutation = {
45
- type: 'aminoAcidMutations';
42
+ type: typeof mutationType.aminoAcidMutations;
46
43
  value: SubstitutionClass | DeletionClass;
47
44
  };
48
45
 
49
46
  type SelectedNucleotideInsertion = {
50
- type: 'nucleotideInsertions';
47
+ type: typeof mutationType.nucleotideInsertions;
51
48
  value: InsertionClass;
52
49
  };
53
50
 
54
51
  type SelectedAminoAcidInsertion = {
55
- type: 'aminoAcidInsertions';
52
+ type: typeof mutationType.aminoAcidInsertions;
56
53
  value: InsertionClass;
57
54
  };
58
55
 
@@ -80,7 +77,7 @@ export const MutationFilter: FunctionComponent<MutationFilterProps> = (props) =>
80
77
 
81
78
  function MutationFilterInner({
82
79
  initialValue,
83
- enabledMutationTypes = ['nucleotideMutations', 'nucleotideInsertions', 'aminoAcidMutations', 'aminoAcidInsertions'],
80
+ enabledMutationTypes = Object.values(mutationType),
84
81
  }: MutationFilterInnerProps) {
85
82
  const referenceGenome = useContext(ReferenceGenomeContext);
86
83
  const filterRef = useRef<HTMLDivElement>(null);
@@ -309,16 +306,16 @@ function getInitialState(
309
306
  function getPlaceholder(referenceGenome: ReferenceGenome, enabledMutationTypes: MutationType[]) {
310
307
  const exampleMutationList = [];
311
308
 
312
- if (enabledMutationTypes.includes('nucleotideMutations')) {
309
+ if (enabledMutationTypes.includes(mutationType.nucleotideMutations)) {
313
310
  exampleMutationList.push(getExampleMutation(referenceGenome, 'nucleotide', 'substitution'));
314
311
  }
315
- if (enabledMutationTypes.includes('nucleotideInsertions')) {
312
+ if (enabledMutationTypes.includes(mutationType.nucleotideInsertions)) {
316
313
  exampleMutationList.push(getExampleMutation(referenceGenome, 'nucleotide', 'insertion'));
317
314
  }
318
- if (enabledMutationTypes.includes('aminoAcidMutations')) {
315
+ if (enabledMutationTypes.includes(mutationType.aminoAcidMutations)) {
319
316
  exampleMutationList.push(getExampleMutation(referenceGenome, 'amino acid', 'substitution'));
320
317
  }
321
- if (enabledMutationTypes.includes('aminoAcidInsertions')) {
318
+ if (enabledMutationTypes.includes(mutationType.aminoAcidInsertions)) {
322
319
  exampleMutationList.push(getExampleMutation(referenceGenome, 'amino acid', 'insertion'));
323
320
  }
324
321
 
@@ -329,13 +326,13 @@ function getPlaceholder(referenceGenome: ReferenceGenome, enabledMutationTypes:
329
326
 
330
327
  const backgroundColorMap = (data: MutationFilterItem, alpha: number = 0.4) => {
331
328
  switch (data.type) {
332
- case 'nucleotideMutations':
329
+ case mutationType.nucleotideMutations:
333
330
  return singleGraphColorRGBByName('green', alpha);
334
- case 'aminoAcidMutations':
331
+ case mutationType.aminoAcidMutations:
335
332
  return singleGraphColorRGBByName('teal', alpha);
336
- case 'nucleotideInsertions':
333
+ case mutationType.nucleotideInsertions:
337
334
  return singleGraphColorRGBByName('indigo', alpha);
338
- case 'aminoAcidInsertions':
335
+ case mutationType.aminoAcidInsertions:
339
336
  return singleGraphColorRGBByName('purple', alpha);
340
337
  }
341
338
  };
@@ -370,13 +367,13 @@ function mapToMutationFilterStrings(selectedFilters: MutationFilterItem[]) {
370
367
  return selectedFilters.reduce<MutationsFilter>(
371
368
  (acc, filter) => {
372
369
  switch (filter.type) {
373
- case 'nucleotideMutations':
370
+ case mutationType.nucleotideMutations:
374
371
  return { ...acc, nucleotideMutations: [...acc.nucleotideMutations, filter.value.toString()] };
375
- case 'aminoAcidMutations':
372
+ case mutationType.aminoAcidMutations:
376
373
  return { ...acc, aminoAcidMutations: [...acc.aminoAcidMutations, filter.value.toString()] };
377
- case 'nucleotideInsertions':
374
+ case mutationType.nucleotideInsertions:
378
375
  return { ...acc, nucleotideInsertions: [...acc.nucleotideInsertions, filter.value.toString()] };
379
- case 'aminoAcidInsertions':
376
+ case mutationType.aminoAcidInsertions:
380
377
  return { ...acc, aminoAcidInsertions: [...acc.aminoAcidInsertions, filter.value.toString()] };
381
378
  }
382
379
  },
@@ -1,7 +1,7 @@
1
1
  import { type MutationFilterItem } from './mutation-filter';
2
2
  import { sequenceTypeFromSegment } from './sequenceTypeFromSegment';
3
3
  import type { ReferenceGenome } from '../../lapisApi/ReferenceGenome';
4
- import type { SequenceType } from '../../types';
4
+ import { type SequenceType, mutationType } from '../../types';
5
5
  import { DeletionClass, InsertionClass, type Mutation, SubstitutionClass } from '../../utils/mutations';
6
6
 
7
7
  export const parseAndValidateMutation = (
@@ -22,11 +22,11 @@ export const parseAndValidateMutation = (
22
22
 
23
23
  const getSequenceType = (type: MutationFilterItem['type']) => {
24
24
  switch (type) {
25
- case 'nucleotideInsertions':
26
- case 'nucleotideMutations':
25
+ case mutationType.nucleotideInsertions:
26
+ case mutationType.nucleotideMutations:
27
27
  return 'nucleotide';
28
- case 'aminoAcidInsertions':
29
- case 'aminoAcidMutations':
28
+ case mutationType.aminoAcidInsertions:
29
+ case mutationType.aminoAcidMutations:
30
30
  return 'amino acid';
31
31
  }
32
32
  };
@@ -37,10 +37,10 @@ const parseMutation = (value: string, referenceGenome: ReferenceGenome): Mutatio
37
37
  const sequenceType = sequenceTypeFromSegment(possibleInsertion.segment, referenceGenome);
38
38
  switch (sequenceType) {
39
39
  case 'nucleotide': {
40
- return { type: 'nucleotideInsertions', value: possibleInsertion };
40
+ return { type: mutationType.nucleotideInsertions, value: possibleInsertion };
41
41
  }
42
42
  case 'amino acid':
43
- return { type: 'aminoAcidInsertions', value: possibleInsertion };
43
+ return { type: mutationType.aminoAcidInsertions, value: possibleInsertion };
44
44
  case undefined:
45
45
  return null;
46
46
  }
@@ -51,9 +51,9 @@ const parseMutation = (value: string, referenceGenome: ReferenceGenome): Mutatio
51
51
  const sequenceType = sequenceTypeFromSegment(possibleDeletion.segment, referenceGenome);
52
52
  switch (sequenceType) {
53
53
  case 'nucleotide':
54
- return { type: 'nucleotideMutations', value: possibleDeletion };
54
+ return { type: mutationType.nucleotideMutations, value: possibleDeletion };
55
55
  case 'amino acid':
56
- return { type: 'aminoAcidMutations', value: possibleDeletion };
56
+ return { type: mutationType.aminoAcidMutations, value: possibleDeletion };
57
57
  case undefined:
58
58
  return null;
59
59
  }
@@ -64,10 +64,10 @@ const parseMutation = (value: string, referenceGenome: ReferenceGenome): Mutatio
64
64
  const sequenceType = sequenceTypeFromSegment(possibleSubstitution.segment, referenceGenome);
65
65
  switch (sequenceType) {
66
66
  case 'nucleotide': {
67
- return { type: 'nucleotideMutations', value: possibleSubstitution };
67
+ return { type: mutationType.nucleotideMutations, value: possibleSubstitution };
68
68
  }
69
69
  case 'amino acid': {
70
- return { type: 'aminoAcidMutations', value: possibleSubstitution };
70
+ return { type: mutationType.aminoAcidMutations, value: possibleSubstitution };
71
71
  }
72
72
 
73
73
  case undefined:
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
3
  import { parseAndValidateMutation } from './parseAndValidateMutation';
4
+ import { mutationType } from '../../types';
4
5
  import { DeletionClass, InsertionClass, SubstitutionClass } from '../../utils/mutations';
5
6
 
6
7
  describe('parseMutation', () => {
@@ -28,32 +29,32 @@ describe('parseMutation', () => {
28
29
  {
29
30
  name: 'should parse nucleotide insertions',
30
31
  input: 'ins_3:ACGT',
31
- expected: { type: 'nucleotideInsertions', value: new InsertionClass(undefined, 3, 'ACGT') },
32
+ expected: { type: mutationType.nucleotideInsertions, value: new InsertionClass(undefined, 3, 'ACGT') },
32
33
  },
33
34
  {
34
35
  name: 'should parse amino acid insertions',
35
36
  input: 'ins_gene1:3:ACGT',
36
- expected: { type: 'aminoAcidInsertions', value: new InsertionClass('gene1', 3, 'ACGT') },
37
+ expected: { type: mutationType.aminoAcidInsertions, value: new InsertionClass('gene1', 3, 'ACGT') },
37
38
  },
38
39
  {
39
40
  name: 'should parse amino acid insertions in all upper case',
40
41
  input: 'INS_GENE1:3:ACGT',
41
- expected: { type: 'aminoAcidInsertions', value: new InsertionClass('GENE1', 3, 'ACGT') },
42
+ expected: { type: mutationType.aminoAcidInsertions, value: new InsertionClass('GENE1', 3, 'ACGT') },
42
43
  },
43
44
  {
44
45
  name: 'should parse amino acid insertions in all lower case',
45
46
  input: 'ins_gene1:3:acgt',
46
- expected: { type: 'aminoAcidInsertions', value: new InsertionClass('gene1', 3, 'acgt') },
47
+ expected: { type: mutationType.aminoAcidInsertions, value: new InsertionClass('gene1', 3, 'acgt') },
47
48
  },
48
49
  {
49
50
  name: 'should parse amino acid insertion with LAPIS-style wildcard',
50
51
  input: 'ins_gene1:3:?AC?GT',
51
- expected: { type: 'aminoAcidInsertions', value: new InsertionClass('gene1', 3, '?AC?GT') },
52
+ expected: { type: mutationType.aminoAcidInsertions, value: new InsertionClass('gene1', 3, '?AC?GT') },
52
53
  },
53
54
  {
54
55
  name: 'should parse amino acid insertion with SILO-style wildcard',
55
56
  input: 'ins_gene1:3:.*AC.*GT',
56
- expected: { type: 'aminoAcidInsertions', value: new InsertionClass('gene1', 3, '.*AC.*GT') },
57
+ expected: { type: mutationType.aminoAcidInsertions, value: new InsertionClass('gene1', 3, '.*AC.*GT') },
57
58
  },
58
59
  {
59
60
  name: 'should return null for insertion with segment not in reference genome',
@@ -71,42 +72,42 @@ describe('parseMutation', () => {
71
72
  {
72
73
  name: 'should parse nucleotide deletion in single segmented reference genome, when no segment is given',
73
74
  input: 'A3-',
74
- expected: { type: 'nucleotideMutations', value: new DeletionClass(undefined, 'A', 3) },
75
+ expected: { type: mutationType.nucleotideMutations, value: new DeletionClass(undefined, 'A', 3) },
75
76
  },
76
77
  {
77
78
  name: 'should parse nucleotide deletion without valueAtReference when no segment is given',
78
79
  input: '3-',
79
- expected: { type: 'nucleotideMutations', value: new DeletionClass(undefined, undefined, 3) },
80
+ expected: { type: mutationType.nucleotideMutations, value: new DeletionClass(undefined, undefined, 3) },
80
81
  },
81
82
  {
82
83
  name: 'should parse nucleotide deletion',
83
84
  input: 'nuc1:A3-',
84
- expected: { type: 'nucleotideMutations', value: new DeletionClass('nuc1', 'A', 3) },
85
+ expected: { type: mutationType.nucleotideMutations, value: new DeletionClass('nuc1', 'A', 3) },
85
86
  },
86
87
  {
87
88
  name: 'should parse nucleotide deletion without valueAtReference',
88
89
  input: 'nuc1:3-',
89
- expected: { type: 'nucleotideMutations', value: new DeletionClass('nuc1', undefined, 3) },
90
+ expected: { type: mutationType.nucleotideMutations, value: new DeletionClass('nuc1', undefined, 3) },
90
91
  },
91
92
  {
92
93
  name: 'should parse amino acid deletion',
93
94
  input: 'gene1:A3-',
94
- expected: { type: 'aminoAcidMutations', value: new DeletionClass('gene1', 'A', 3) },
95
+ expected: { type: mutationType.aminoAcidMutations, value: new DeletionClass('gene1', 'A', 3) },
95
96
  },
96
97
  {
97
98
  name: 'should parse amino acid deletion in all upper case',
98
99
  input: 'GENE1:A3-',
99
- expected: { type: 'aminoAcidMutations', value: new DeletionClass('GENE1', 'A', 3) },
100
+ expected: { type: mutationType.aminoAcidMutations, value: new DeletionClass('GENE1', 'A', 3) },
100
101
  },
101
102
  {
102
103
  name: 'should parse amino acid deletion in all lower case',
103
104
  input: 'gene1:a3-',
104
- expected: { type: 'aminoAcidMutations', value: new DeletionClass('gene1', 'a', 3) },
105
+ expected: { type: mutationType.aminoAcidMutations, value: new DeletionClass('gene1', 'a', 3) },
105
106
  },
106
107
  {
107
108
  name: 'should parse amino acid deletion without valueAtReference',
108
109
  input: 'gene1:3-',
109
- expected: { type: 'aminoAcidMutations', value: new DeletionClass('gene1', undefined, 3) },
110
+ expected: { type: mutationType.aminoAcidMutations, value: new DeletionClass('gene1', undefined, 3) },
110
111
  },
111
112
  {
112
113
  name: 'should return null for deletion with segment not in reference genome',
@@ -123,45 +124,54 @@ describe('parseMutation', () => {
123
124
  {
124
125
  name: 'should parse nucleotide substitution in single segmented reference genome, when no segment is given',
125
126
  input: 'A3T',
126
- expected: { type: 'nucleotideMutations', value: new SubstitutionClass(undefined, 'A', 'T', 3) },
127
+ expected: {
128
+ type: mutationType.nucleotideMutations,
129
+ value: new SubstitutionClass(undefined, 'A', 'T', 3),
130
+ },
127
131
  },
128
132
  {
129
133
  name: 'should parse substitution without valueAtReference',
130
134
  input: '3T',
131
- expected: { type: 'nucleotideMutations', value: new SubstitutionClass(undefined, undefined, 'T', 3) },
135
+ expected: {
136
+ type: mutationType.nucleotideMutations,
137
+ value: new SubstitutionClass(undefined, undefined, 'T', 3),
138
+ },
132
139
  },
133
140
  {
134
141
  name: 'should parse substitution with neither valueAtReference not substitutionValue',
135
142
  input: '3',
136
143
  expected: {
137
- type: 'nucleotideMutations',
144
+ type: mutationType.nucleotideMutations,
138
145
  value: new SubstitutionClass(undefined, undefined, undefined, 3),
139
146
  },
140
147
  },
141
148
  {
142
149
  name: 'should parse a "no mutation" substitution',
143
150
  input: '3.',
144
- expected: { type: 'nucleotideMutations', value: new SubstitutionClass(undefined, undefined, '.', 3) },
151
+ expected: {
152
+ type: mutationType.nucleotideMutations,
153
+ value: new SubstitutionClass(undefined, undefined, '.', 3),
154
+ },
145
155
  },
146
156
  {
147
157
  name: 'should parse nucleotide substitution',
148
158
  input: 'nuc1:A3T',
149
- expected: { type: 'nucleotideMutations', value: new SubstitutionClass('nuc1', 'A', 'T', 3) },
159
+ expected: { type: mutationType.nucleotideMutations, value: new SubstitutionClass('nuc1', 'A', 'T', 3) },
150
160
  },
151
161
  {
152
162
  name: 'should parse amino acid substitution',
153
163
  input: 'gene1:A3T',
154
- expected: { type: 'aminoAcidMutations', value: new SubstitutionClass('gene1', 'A', 'T', 3) },
164
+ expected: { type: mutationType.aminoAcidMutations, value: new SubstitutionClass('gene1', 'A', 'T', 3) },
155
165
  },
156
166
  {
157
167
  name: 'should parse amino acid substitution in all upper case',
158
168
  input: 'GENE1:A3T',
159
- expected: { type: 'aminoAcidMutations', value: new SubstitutionClass('GENE1', 'A', 'T', 3) },
169
+ expected: { type: mutationType.aminoAcidMutations, value: new SubstitutionClass('GENE1', 'A', 'T', 3) },
160
170
  },
161
171
  {
162
172
  name: 'should parse amino acid substitution in all lower case',
163
173
  input: 'gene1:a3t',
164
- expected: { type: 'aminoAcidMutations', value: new SubstitutionClass('gene1', 'a', 't', 3) },
174
+ expected: { type: mutationType.aminoAcidMutations, value: new SubstitutionClass('gene1', 'a', 't', 3) },
165
175
  },
166
176
  {
167
177
  name: 'should return null for substitution with segment not in reference genome',
@@ -5,6 +5,7 @@ import { getMutationsTableData } from './getMutationsTableData';
5
5
  import { type SequenceType, type SubstitutionOrDeletionEntry } from '../../types';
6
6
  import { type DeletionClass, type SubstitutionClass } from '../../utils/mutations';
7
7
  import { useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
8
+ import { useMutationLinkProvider } from '../MutationLinkTemplateContext';
8
9
  import { GridJsAnnotatedMutation } from '../components/annotated-mutation';
9
10
  import type { ProportionInterval } from '../components/proportion-selector';
10
11
  import { Table } from '../components/table';
@@ -29,6 +30,7 @@ const MutationsTable: FunctionComponent<MutationsTableProps> = ({
29
30
  sequenceType,
30
31
  }) => {
31
32
  const annotationsProvider = useMutationAnnotationsProvider();
33
+ const linkProvider = useMutationLinkProvider();
32
34
 
33
35
  const headers = [
34
36
  {
@@ -43,6 +45,7 @@ const MutationsTable: FunctionComponent<MutationsTableProps> = ({
43
45
  mutation={cell}
44
46
  sequenceType={sequenceType}
45
47
  annotationsProvider={annotationsProvider}
48
+ linkProvider={linkProvider}
46
49
  />
47
50
  ),
48
51
  },
@@ -47,6 +47,12 @@ import { useWebWorker } from '../webWorkers/useWebWorker';
47
47
  const mutationsOverTimeViewSchema = z.literal(views.grid);
48
48
  export type MutationsOverTimeView = z.infer<typeof mutationsOverTimeViewSchema>;
49
49
 
50
+ const meanProportionIntervalSchema = z.object({
51
+ min: z.number().min(0).max(1),
52
+ max: z.number().min(0).max(1),
53
+ });
54
+ export type MeanProportionInterval = z.infer<typeof meanProportionIntervalSchema>;
55
+
50
56
  const mutationOverTimeSchema = z.object({
51
57
  lapisFilter: lapisFilterSchema,
52
58
  sequenceType: sequenceTypeSchema,
@@ -55,10 +61,7 @@ const mutationOverTimeSchema = z.object({
55
61
  lapisDateField: z.string().min(1),
56
62
  useNewEndpoint: z.boolean().optional(),
57
63
  displayMutations: displayMutationsSchema.optional(),
58
- initialMeanProportionInterval: z.object({
59
- min: z.number().min(0).max(1),
60
- max: z.number().min(0).max(1),
61
- }),
64
+ initialMeanProportionInterval: meanProportionIntervalSchema,
62
65
  hideGaps: z.boolean().optional(),
63
66
  width: z.string(),
64
67
  height: z.string().optional(),
@@ -6,7 +6,9 @@ export async function expectMutationAnnotation(canvasElement: HTMLElement, mutat
6
6
  await waitFor(async () => {
7
7
  const annotatedMutation = canvas.getAllByText(mutation)[0];
8
8
  await expect(annotatedMutation).toBeVisible();
9
- await userEvent.click(annotatedMutation);
9
+ const button = within(annotatedMutation).getByRole('button');
10
+ await expect(button).toBeVisible();
11
+ await userEvent.click(button);
10
12
  });
11
13
 
12
14
  await waitFor(() => expect(canvas.getByText(`Annotations for ${mutation}`)).toBeVisible());
package/src/types.ts CHANGED
@@ -44,7 +44,7 @@ export type SequenceType = z.infer<typeof sequenceTypeSchema>;
44
44
 
45
45
  export type SubstitutionOrDeletion = 'substitution' | 'deletion';
46
46
 
47
- export type MutationType = SubstitutionOrDeletion | 'insertion';
47
+ export type SubstitutionOrDeletionOrInsertion = SubstitutionOrDeletion | 'insertion';
48
48
 
49
49
  export type SubstitutionEntry<T extends Substitution = SubstitutionClass> = {
50
50
  type: 'substitution';
@@ -79,3 +79,19 @@ export const views = {
79
79
  bubble: 'bubble',
80
80
  map: 'map',
81
81
  } as const;
82
+
83
+ export const mutationType = {
84
+ nucleotideMutations: 'nucleotideMutations',
85
+ nucleotideInsertions: 'nucleotideInsertions',
86
+ aminoAcidMutations: 'aminoAcidMutations',
87
+ aminoAcidInsertions: 'aminoAcidInsertions',
88
+ } as const;
89
+
90
+ export const mutationTypeSchema = z.enum([
91
+ mutationType.nucleotideMutations,
92
+ mutationType.nucleotideInsertions,
93
+ mutationType.aminoAcidMutations,
94
+ mutationType.aminoAcidInsertions,
95
+ ]);
96
+
97
+ export type MutationType = z.infer<typeof mutationTypeSchema>;
@@ -12,6 +12,8 @@ export {
12
12
  views,
13
13
  type TemporalGranularity,
14
14
  type MutationsFilter,
15
+ mutationType,
16
+ type MutationType,
15
17
  } from './types';
16
18
 
17
19
  export type { MutationComparisonView, MutationComparisonProps } from './preact/mutationComparison/mutation-comparison';
@@ -47,3 +49,5 @@ export {
47
49
  NumberRangeFilterChangedEvent,
48
50
  NumberRangeValueChangedEvent,
49
51
  } from './preact/numberRangeFilter/NumberRangeFilterChangedEvent';
52
+
53
+ export { type MeanProportionInterval } from './preact/mutationsOverTime/mutations-over-time';
@@ -6,6 +6,7 @@ describe('SubstitutionClass', () => {
6
6
  it('should be parsed from string', () => {
7
7
  expect(SubstitutionClass.parse('A1T')).deep.equal(new SubstitutionClass(undefined, 'A', 'T', 1));
8
8
  expect(SubstitutionClass.parse('seg1:A1T')).deep.equal(new SubstitutionClass('seg1', 'A', 'T', 1));
9
+ expect(SubstitutionClass.parse('1')).deep.equal(new SubstitutionClass(undefined, undefined, undefined, 1));
9
10
  });
10
11
 
11
12
  it('should be parsed with stop codons', () => {
@@ -13,6 +14,12 @@ describe('SubstitutionClass', () => {
13
14
  expect(SubstitutionClass.parse('S:T1247*')).deep.equal(new SubstitutionClass('S', 'T', '*', 1247));
14
15
  });
15
16
 
17
+ it('invalid substitution strings should return null', () => {
18
+ expect(SubstitutionClass.parse('A1-')).to.equal(null);
19
+ expect(SubstitutionClass.parse('ins_1:A')).to.equal(null);
20
+ expect(SubstitutionClass.parse('E34Q')).to.equal(null);
21
+ });
22
+
16
23
  it('should render to string correctly', () => {
17
24
  const substitutions = [
18
25
  {
@@ -39,6 +46,12 @@ describe('DeletionClass', () => {
39
46
  expect(DeletionClass.parse('seg1:*1-')).deep.equal(new DeletionClass('seg1', '*', 1));
40
47
  });
41
48
 
49
+ it('invalid deletion strings should return null', () => {
50
+ expect(DeletionClass.parse('seg1:A1T')).to.equal(null);
51
+ expect(DeletionClass.parse('ins_1:A')).to.equal(null);
52
+ expect(DeletionClass.parse('E34-')).to.equal(null);
53
+ });
54
+
42
55
  it('should render to string correctly', () => {
43
56
  const substitutions = [
44
57
  {
@@ -74,4 +87,10 @@ describe('InsertionClass', () => {
74
87
  it('should be parsed with stop codon insertion', () => {
75
88
  expect(InsertionClass.parse('ins_134:*')).deep.equal(new InsertionClass(undefined, 134, '*'));
76
89
  });
90
+
91
+ it('invalid insertion strings should return null', () => {
92
+ expect(InsertionClass.parse('A1-')).to.equal(null);
93
+ expect(InsertionClass.parse('seg1:A1T')).to.equal(null);
94
+ expect(InsertionClass.parse('ins_34:Q')).to.equal(null);
95
+ });
77
96
  });
@@ -1,9 +1,9 @@
1
- import { type MutationType, type SequenceType } from '../types';
1
+ import { type SubstitutionOrDeletionOrInsertion, type SequenceType } from '../types';
2
2
 
3
3
  export interface Mutation {
4
4
  readonly position: number;
5
5
  readonly code: string;
6
- readonly type: MutationType;
6
+ readonly type: SubstitutionOrDeletionOrInsertion;
7
7
  readonly segment?: string;
8
8
  }
9
9
 
@@ -13,8 +13,28 @@ export interface MutationClass extends Mutation {
13
13
  toString(): string;
14
14
  }
15
15
 
16
- export const substitutionRegex =
17
- /^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z*])?(?<position>\d+)(?<substitutionValue>[A-Z.*])?$/i;
16
+ // Allowed IUPAC characters: https://www.bioinformatics.org/sms/iupac.html
17
+ const nucleotideChars = 'ACGTRYKMSWBDHVN';
18
+ const aminoAcidChars = 'ACDEFGHIKLMNPQRSTVWY';
19
+
20
+ function segmentPart(type: 'nucleotide' | 'aminoAcid') {
21
+ return type === 'aminoAcid' ? `(?<segment>[A-Z0-9_-]+):` : `((?<segment>[A-Z0-9_-]+)(?=:):)?`;
22
+ }
23
+
24
+ function buildSubstitutionRegex(type: 'nucleotide' | 'aminoAcid') {
25
+ const chars = type === 'nucleotide' ? nucleotideChars : aminoAcidChars;
26
+
27
+ return new RegExp(
28
+ `^${segmentPart(type)}` +
29
+ `(?<valueAtReference>[${chars}*])?` +
30
+ `(?<position>\\d+)` +
31
+ `(?<substitutionValue>[${chars}.*])?$`,
32
+ 'i',
33
+ );
34
+ }
35
+
36
+ const nucleotideSubstitutionRegex = buildSubstitutionRegex('nucleotide');
37
+ const aminoAcidSubstitutionRegex = buildSubstitutionRegex('aminoAcid');
18
38
 
19
39
  export interface Substitution extends Mutation {
20
40
  type: 'substitution';
@@ -55,7 +75,9 @@ export class SubstitutionClass implements MutationClass, Substitution {
55
75
  }
56
76
 
57
77
  static parse(mutationStr: string): SubstitutionClass | null {
58
- const match = substitutionRegex.exec(mutationStr);
78
+ const matchNucleotide = nucleotideSubstitutionRegex.exec(mutationStr);
79
+ const matchAminoAcid = aminoAcidSubstitutionRegex.exec(mutationStr);
80
+ const match = matchNucleotide ?? matchAminoAcid;
59
81
  if (match?.groups === undefined) {
60
82
  return null;
61
83
  }
@@ -68,7 +90,17 @@ export class SubstitutionClass implements MutationClass, Substitution {
68
90
  }
69
91
  }
70
92
 
71
- export const deletionRegex = /^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z*])?(?<position>\d+)(-)$/i;
93
+ function buildDeletionRegex(type: 'nucleotide' | 'aminoAcid') {
94
+ const chars = type === 'nucleotide' ? nucleotideChars : aminoAcidChars;
95
+
96
+ return new RegExp(
97
+ `^${segmentPart(type)}` + `(?<valueAtReference>[${chars}*])?` + `(?<position>\\d+)` + `(-)$`,
98
+ 'i',
99
+ );
100
+ }
101
+
102
+ const nucleotideDeletionRegex = buildDeletionRegex('nucleotide');
103
+ const aminoAcidDeletionRegex = buildDeletionRegex('aminoAcid');
72
104
 
73
105
  export interface Deletion extends Mutation {
74
106
  type: 'deletion';
@@ -105,7 +137,9 @@ export class DeletionClass implements MutationClass, Deletion {
105
137
  }
106
138
 
107
139
  static parse(mutationStr: string): DeletionClass | null {
108
- const match = deletionRegex.exec(mutationStr);
140
+ const matchNucleotide = nucleotideDeletionRegex.exec(mutationStr);
141
+ const matchAminoAcid = aminoAcidDeletionRegex.exec(mutationStr);
142
+ const match = matchNucleotide ?? matchAminoAcid;
109
143
  if (match?.groups === undefined) {
110
144
  return null;
111
145
  }
@@ -118,8 +152,19 @@ export class DeletionClass implements MutationClass, Deletion {
118
152
  }
119
153
  }
120
154
 
121
- export const insertionRegexp =
122
- /^ins_((?<segment>[A-Z0-9_-]+)(?=:):)?(?<position>\d+):(?<insertedSymbols>(([A-Z?*]|(\.\*))+))$/i;
155
+ function buildInsertionRegex(type: 'nucleotide' | 'aminoAcid') {
156
+ const chars = type === 'nucleotide' ? nucleotideChars : aminoAcidChars;
157
+
158
+ const wildcardToken = `(?:\\.\\*)`;
159
+
160
+ return new RegExp(
161
+ `^ins_${segmentPart(type)}(?<position>\\d+):(?<insertedSymbols>(?:[${chars}?*]|${wildcardToken})+)$`,
162
+ 'i',
163
+ );
164
+ }
165
+
166
+ const nucleotideInsertionRegex = buildInsertionRegex('nucleotide');
167
+ const aminoAcidInsertionRegex = buildInsertionRegex('aminoAcid');
123
168
 
124
169
  export interface Insertion extends Mutation {
125
170
  type: 'insertion';
@@ -154,7 +199,9 @@ export class InsertionClass implements MutationClass {
154
199
  }
155
200
 
156
201
  static parse(mutationStr: string): InsertionClass | null {
157
- const match = insertionRegexp.exec(mutationStr);
202
+ const matchNucleotide = nucleotideInsertionRegex.exec(mutationStr);
203
+ const matchAminoAcid = aminoAcidInsertionRegex.exec(mutationStr);
204
+ const match = matchNucleotide ?? matchAminoAcid;
158
205
  if (match?.groups === undefined) {
159
206
  return null;
160
207
  }
@@ -2,9 +2,16 @@ import { describe, expectTypeOf, test } from 'vitest';
2
2
 
3
3
  import { AppComponent } from './gs-app';
4
4
  import { type MutationAnnotations } from './mutation-annotations-context';
5
+ import { type MutationLinkTemplate } from './mutation-link-template-context';
5
6
 
6
7
  describe('gs-app types', () => {
7
8
  test('mutationAnnotations type should match', () => {
8
9
  expectTypeOf(AppComponent.prototype).toHaveProperty('mutationAnnotations').toEqualTypeOf<MutationAnnotations>();
9
10
  });
11
+
12
+ test('mutationLinkTemplate type should match', () => {
13
+ expectTypeOf(AppComponent.prototype)
14
+ .toHaveProperty('mutationLinkTemplate')
15
+ .toEqualTypeOf<MutationLinkTemplate>();
16
+ });
10
17
  });