@genspectrum/dashboard-components 0.18.5 → 0.19.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 (46) hide show
  1. package/README.md +12 -2
  2. package/custom-elements.json +3 -3
  3. package/dist/assets/{mutationOverTimeWorker--b8ZHlji.js.map → mutationOverTimeWorker-ChQTFL68.js.map} +1 -1
  4. package/dist/components.d.ts +15 -14
  5. package/dist/components.js +1602 -332
  6. package/dist/components.js.map +1 -1
  7. package/dist/util.d.ts +14 -14
  8. package/package.json +3 -4
  9. package/src/preact/MutationAnnotationsContext.tsx +34 -27
  10. package/src/preact/components/dropdown.tsx +1 -1
  11. package/src/preact/components/info.tsx +1 -2
  12. package/src/preact/components/min-max-range-slider.tsx +0 -2
  13. package/src/preact/components/mutations-over-time-text-filter.stories.tsx +57 -0
  14. package/src/preact/components/mutations-over-time-text-filter.tsx +63 -0
  15. package/src/preact/components/segment-selector.tsx +1 -1
  16. package/src/preact/components/table.tsx +0 -2
  17. package/src/preact/dateRangeFilter/date-picker.tsx +15 -10
  18. package/src/preact/mutationFilter/mutation-filter.stories.tsx +169 -50
  19. package/src/preact/mutationFilter/mutation-filter.tsx +239 -234
  20. package/src/preact/mutationFilter/parseAndValidateMutation.ts +62 -10
  21. package/src/preact/mutationFilter/parseMutation.spec.ts +62 -47
  22. package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +128 -0
  23. package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +39 -2
  24. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +8 -11
  25. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +27 -0
  26. package/src/preact/mutationsOverTime/mutations-over-time.tsx +26 -5
  27. package/src/preact/shared/tanstackTable/pagination-context.tsx +30 -0
  28. package/src/preact/shared/tanstackTable/pagination.tsx +19 -6
  29. package/src/preact/shared/tanstackTable/tanstackTable.tsx +17 -3
  30. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +19 -1
  31. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +6 -1
  32. package/src/styles/replaceCssProperties.stories.tsx +49 -0
  33. package/src/styles/replaceCssProperties.ts +25 -0
  34. package/src/styles/tailwind.css +1 -0
  35. package/src/web-components/PreactLitAdapter.tsx +6 -3
  36. package/src/web-components/PreactLitAdapterWithGridJsStyles.tsx +0 -2
  37. package/src/web-components/gs-app.stories.ts +6 -2
  38. package/src/web-components/gs-app.ts +4 -1
  39. package/src/web-components/input/gs-date-range-filter.tsx +6 -0
  40. package/src/web-components/input/gs-mutation-filter.stories.ts +4 -4
  41. package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +1 -1
  42. package/standalone-bundle/assets/mutationOverTimeWorker-jChgWnwp.js.map +1 -1
  43. package/standalone-bundle/dashboard-components.js +10836 -11289
  44. package/standalone-bundle/dashboard-components.js.map +1 -1
  45. package/dist/style.css +0 -392
  46. package/standalone-bundle/style.css +0 -1
@@ -27,135 +27,150 @@ describe('parseMutation', () => {
27
27
  insertions: [
28
28
  {
29
29
  name: 'should parse nucleotide insertions',
30
- input: 'ins_10:ACGT',
31
- expected: { type: 'nucleotideInsertions', value: new InsertionClass(undefined, 10, 'ACGT') },
30
+ input: 'ins_3:ACGT',
31
+ expected: { type: 'nucleotideInsertions', value: new InsertionClass(undefined, 3, 'ACGT') },
32
32
  },
33
33
  {
34
34
  name: 'should parse amino acid insertions',
35
- input: 'ins_gene1:10:ACGT',
36
- expected: { type: 'aminoAcidInsertions', value: new InsertionClass('gene1', 10, 'ACGT') },
35
+ input: 'ins_gene1:3:ACGT',
36
+ expected: { type: 'aminoAcidInsertions', value: new InsertionClass('gene1', 3, 'ACGT') },
37
37
  },
38
38
  {
39
39
  name: 'should parse amino acid insertions in all upper case',
40
- input: 'INS_GENE1:10:ACGT',
41
- expected: { type: 'aminoAcidInsertions', value: new InsertionClass('GENE1', 10, 'ACGT') },
40
+ input: 'INS_GENE1:3:ACGT',
41
+ expected: { type: 'aminoAcidInsertions', value: new InsertionClass('GENE1', 3, 'ACGT') },
42
42
  },
43
43
  {
44
44
  name: 'should parse amino acid insertions in all lower case',
45
- input: 'ins_gene1:10:acgt',
46
- expected: { type: 'aminoAcidInsertions', value: new InsertionClass('gene1', 10, 'acgt') },
45
+ input: 'ins_gene1:3:acgt',
46
+ expected: { type: 'aminoAcidInsertions', value: new InsertionClass('gene1', 3, 'acgt') },
47
47
  },
48
48
  {
49
49
  name: 'should parse amino acid insertion with LAPIS-style wildcard',
50
- input: 'ins_gene1:10:?AC?GT',
51
- expected: { type: 'aminoAcidInsertions', value: new InsertionClass('gene1', 10, '?AC?GT') },
50
+ input: 'ins_gene1:3:?AC?GT',
51
+ expected: { type: 'aminoAcidInsertions', value: new InsertionClass('gene1', 3, '?AC?GT') },
52
52
  },
53
53
  {
54
54
  name: 'should parse amino acid insertion with SILO-style wildcard',
55
- input: 'ins_gene1:10:.*AC.*GT',
56
- expected: { type: 'aminoAcidInsertions', value: new InsertionClass('gene1', 10, '.*AC.*GT') },
55
+ input: 'ins_gene1:3:.*AC.*GT',
56
+ expected: { type: 'aminoAcidInsertions', value: new InsertionClass('gene1', 3, '.*AC.*GT') },
57
57
  },
58
58
  {
59
59
  name: 'should return null for insertion with segment not in reference genome',
60
- input: 'INS_notInReferenceGenome:10:ACGT',
60
+ input: 'INS_notInReferenceGenome:3:ACGT',
61
61
  expected: null,
62
62
  },
63
63
  { name: 'should return null for insertion with missing position', input: 'ins_gene1:ACGT', expected: null },
64
+ {
65
+ name: 'should return null for insertion with position outside of reference genome',
66
+ input: 'ins_gene1:10:ACGT',
67
+ expected: null,
68
+ },
64
69
  ],
65
70
  deletions: [
66
71
  {
67
72
  name: 'should parse nucleotide deletion in single segmented reference genome, when no segment is given',
68
- input: 'A123-',
69
- expected: { type: 'nucleotideMutations', value: new DeletionClass(undefined, 'A', 123) },
73
+ input: 'A3-',
74
+ expected: { type: 'nucleotideMutations', value: new DeletionClass(undefined, 'A', 3) },
70
75
  },
71
76
  {
72
77
  name: 'should parse nucleotide deletion without valueAtReference when no segment is given',
73
- input: '123-',
74
- expected: { type: 'nucleotideMutations', value: new DeletionClass(undefined, undefined, 123) },
78
+ input: '3-',
79
+ expected: { type: 'nucleotideMutations', value: new DeletionClass(undefined, undefined, 3) },
75
80
  },
76
81
  {
77
82
  name: 'should parse nucleotide deletion',
78
- input: 'nuc1:A123-',
79
- expected: { type: 'nucleotideMutations', value: new DeletionClass('nuc1', 'A', 123) },
83
+ input: 'nuc1:A3-',
84
+ expected: { type: 'nucleotideMutations', value: new DeletionClass('nuc1', 'A', 3) },
80
85
  },
81
86
  {
82
87
  name: 'should parse nucleotide deletion without valueAtReference',
83
- input: 'nuc1:123-',
84
- expected: { type: 'nucleotideMutations', value: new DeletionClass('nuc1', undefined, 123) },
88
+ input: 'nuc1:3-',
89
+ expected: { type: 'nucleotideMutations', value: new DeletionClass('nuc1', undefined, 3) },
85
90
  },
86
91
  {
87
92
  name: 'should parse amino acid deletion',
88
- input: 'gene1:A123-',
89
- expected: { type: 'aminoAcidMutations', value: new DeletionClass('gene1', 'A', 123) },
93
+ input: 'gene1:A3-',
94
+ expected: { type: 'aminoAcidMutations', value: new DeletionClass('gene1', 'A', 3) },
90
95
  },
91
96
  {
92
97
  name: 'should parse amino acid deletion in all upper case',
93
- input: 'GENE1:A123-',
94
- expected: { type: 'aminoAcidMutations', value: new DeletionClass('GENE1', 'A', 123) },
98
+ input: 'GENE1:A3-',
99
+ expected: { type: 'aminoAcidMutations', value: new DeletionClass('GENE1', 'A', 3) },
95
100
  },
96
101
  {
97
102
  name: 'should parse amino acid deletion in all lower case',
98
- input: 'gene1:a123-',
99
- expected: { type: 'aminoAcidMutations', value: new DeletionClass('gene1', 'a', 123) },
103
+ input: 'gene1:a3-',
104
+ expected: { type: 'aminoAcidMutations', value: new DeletionClass('gene1', 'a', 3) },
100
105
  },
101
106
  {
102
107
  name: 'should parse amino acid deletion without valueAtReference',
103
- input: 'gene1:123-',
104
- expected: { type: 'aminoAcidMutations', value: new DeletionClass('gene1', undefined, 123) },
108
+ input: 'gene1:3-',
109
+ expected: { type: 'aminoAcidMutations', value: new DeletionClass('gene1', undefined, 3) },
105
110
  },
106
111
  {
107
112
  name: 'should return null for deletion with segment not in reference genome',
108
- input: 'notInReferenceGenome:A123-',
113
+ input: 'notInReferenceGenome:A3-',
114
+ expected: null,
115
+ },
116
+ {
117
+ name: 'should return null for deletion with position outside reference genome',
118
+ input: 'gene1:A10-',
109
119
  expected: null,
110
120
  },
111
121
  ],
112
122
  substitutions: [
113
123
  {
114
124
  name: 'should parse nucleotide substitution in single segmented reference genome, when no segment is given',
115
- input: 'A123T',
116
- expected: { type: 'nucleotideMutations', value: new SubstitutionClass(undefined, 'A', 'T', 123) },
125
+ input: 'A3T',
126
+ expected: { type: 'nucleotideMutations', value: new SubstitutionClass(undefined, 'A', 'T', 3) },
117
127
  },
118
128
  {
119
129
  name: 'should parse substitution without valueAtReference',
120
- input: '123T',
121
- expected: { type: 'nucleotideMutations', value: new SubstitutionClass(undefined, undefined, 'T', 123) },
130
+ input: '3T',
131
+ expected: { type: 'nucleotideMutations', value: new SubstitutionClass(undefined, undefined, 'T', 3) },
122
132
  },
123
133
  {
124
134
  name: 'should parse substitution with neither valueAtReference not substitutionValue',
125
- input: '123',
135
+ input: '3',
126
136
  expected: {
127
137
  type: 'nucleotideMutations',
128
- value: new SubstitutionClass(undefined, undefined, undefined, 123),
138
+ value: new SubstitutionClass(undefined, undefined, undefined, 3),
129
139
  },
130
140
  },
131
141
  {
132
142
  name: 'should parse a "no mutation" substitution',
133
- input: '123.',
134
- expected: { type: 'nucleotideMutations', value: new SubstitutionClass(undefined, undefined, '.', 123) },
143
+ input: '3.',
144
+ expected: { type: 'nucleotideMutations', value: new SubstitutionClass(undefined, undefined, '.', 3) },
135
145
  },
136
146
  {
137
147
  name: 'should parse nucleotide substitution',
138
- input: 'nuc1:A123T',
139
- expected: { type: 'nucleotideMutations', value: new SubstitutionClass('nuc1', 'A', 'T', 123) },
148
+ input: 'nuc1:A3T',
149
+ expected: { type: 'nucleotideMutations', value: new SubstitutionClass('nuc1', 'A', 'T', 3) },
140
150
  },
141
151
  {
142
152
  name: 'should parse amino acid substitution',
143
- input: 'gene1:A123T',
144
- expected: { type: 'aminoAcidMutations', value: new SubstitutionClass('gene1', 'A', 'T', 123) },
153
+ input: 'gene1:A3T',
154
+ expected: { type: 'aminoAcidMutations', value: new SubstitutionClass('gene1', 'A', 'T', 3) },
145
155
  },
146
156
  {
147
157
  name: 'should parse amino acid substitution in all upper case',
148
- input: 'GENE1:A123T',
149
- expected: { type: 'aminoAcidMutations', value: new SubstitutionClass('GENE1', 'A', 'T', 123) },
158
+ input: 'GENE1:A3T',
159
+ expected: { type: 'aminoAcidMutations', value: new SubstitutionClass('GENE1', 'A', 'T', 3) },
150
160
  },
151
161
  {
152
162
  name: 'should parse amino acid substitution in all lower case',
153
- input: 'gene1:a123t',
154
- expected: { type: 'aminoAcidMutations', value: new SubstitutionClass('gene1', 'a', 't', 123) },
163
+ input: 'gene1:a3t',
164
+ expected: { type: 'aminoAcidMutations', value: new SubstitutionClass('gene1', 'a', 't', 3) },
155
165
  },
156
166
  {
157
167
  name: 'should return null for substitution with segment not in reference genome',
158
- input: 'notInReferenceGenome:A123T',
168
+ input: 'notInReferenceGenome:A3T',
169
+ expected: null,
170
+ },
171
+ {
172
+ name: 'should return null for substitution with position outside reference genome',
173
+ input: 'gene1:A10T',
159
174
  expected: null,
160
175
  },
161
176
  ],
@@ -7,6 +7,8 @@ import { type DeletionEntry, type SubstitutionEntry } from '../../types';
7
7
  import { type Deletion, type Substitution } from '../../utils/mutations';
8
8
  import { type TemporalClass } from '../../utils/temporalClass';
9
9
  import { yearMonthDay } from '../../utils/temporalTestHelpers';
10
+ import { type MutationAnnotations } from '../../web-components/mutation-annotations-context';
11
+ import { getMutationAnnotationsContext, getMutationAnnotationsProvider } from '../MutationAnnotationsContext';
10
12
 
11
13
  describe('getFilteredMutationOverTimeData', () => {
12
14
  it('should filter by displayed segments', () => {
@@ -26,6 +28,11 @@ describe('getFilteredMutationOverTimeData', () => {
26
28
  displayedMutationTypes: [],
27
29
  proportionInterval,
28
30
  displayMutations: undefined,
31
+ mutationFilterValue: '',
32
+ sequenceType: 'nucleotide',
33
+ annotationProvider: () => {
34
+ return [];
35
+ },
29
36
  });
30
37
 
31
38
  expect(result.getFirstAxisKeys()).to.deep.equal([anotherSubstitution]);
@@ -55,6 +62,11 @@ describe('getFilteredMutationOverTimeData', () => {
55
62
  },
56
63
  ],
57
64
  proportionInterval,
65
+ mutationFilterValue: '',
66
+ sequenceType: 'nucleotide',
67
+ annotationProvider: () => {
68
+ return [];
69
+ },
58
70
  });
59
71
 
60
72
  expect(result.getFirstAxisKeys()).to.deep.equal([someDeletion]);
@@ -73,6 +85,11 @@ describe('getFilteredMutationOverTimeData', () => {
73
85
  displayedSegments: [],
74
86
  displayedMutationTypes: [],
75
87
  proportionInterval,
88
+ mutationFilterValue: '',
89
+ sequenceType: 'nucleotide',
90
+ annotationProvider: () => {
91
+ return [];
92
+ },
76
93
  });
77
94
 
78
95
  expect(result.getFirstAxisKeys()).to.deep.equal([anotherSubstitution, someDeletion]);
@@ -91,6 +108,11 @@ describe('getFilteredMutationOverTimeData', () => {
91
108
  displayedSegments: [],
92
109
  displayedMutationTypes: [],
93
110
  proportionInterval,
111
+ mutationFilterValue: '',
112
+ sequenceType: 'nucleotide',
113
+ annotationProvider: () => {
114
+ return [];
115
+ },
94
116
  });
95
117
 
96
118
  expect(result.getFirstAxisKeys()).to.deep.equal([anotherSubstitution, someDeletion]);
@@ -110,6 +132,11 @@ describe('getFilteredMutationOverTimeData', () => {
110
132
  displayedSegments: [],
111
133
  displayedMutationTypes: [],
112
134
  proportionInterval,
135
+ mutationFilterValue: '',
136
+ sequenceType: 'nucleotide',
137
+ annotationProvider: () => {
138
+ return [];
139
+ },
113
140
  });
114
141
 
115
142
  expect(result.getFirstAxisKeys()).to.deep.equal([someSubstitution, anotherSubstitution, someDeletion]);
@@ -129,6 +156,11 @@ describe('getFilteredMutationOverTimeData', () => {
129
156
  displayedSegments: [],
130
157
  displayedMutationTypes: [],
131
158
  proportionInterval,
159
+ mutationFilterValue: '',
160
+ sequenceType: 'nucleotide',
161
+ annotationProvider: () => {
162
+ return [];
163
+ },
132
164
  });
133
165
 
134
166
  expect(result.getFirstAxisKeys()).to.deep.equal([someSubstitution, anotherSubstitution, someDeletion]);
@@ -146,6 +178,11 @@ describe('getFilteredMutationOverTimeData', () => {
146
178
  displayedSegments: [],
147
179
  displayedMutationTypes: [],
148
180
  proportionInterval,
181
+ mutationFilterValue: '',
182
+ sequenceType: 'nucleotide',
183
+ annotationProvider: () => {
184
+ return [];
185
+ },
149
186
  });
150
187
 
151
188
  expect(result.getFirstAxisKeys()).to.deep.equal([someSubstitution, anotherSubstitution, someDeletion]);
@@ -164,6 +201,11 @@ describe('getFilteredMutationOverTimeData', () => {
164
201
  displayedSegments: [],
165
202
  displayedMutationTypes: [],
166
203
  proportionInterval,
204
+ mutationFilterValue: '',
205
+ sequenceType: 'nucleotide',
206
+ annotationProvider: () => {
207
+ return [];
208
+ },
167
209
  });
168
210
 
169
211
  expect(result.getFirstAxisKeys()).to.deep.equal([someSubstitution, anotherSubstitution, someDeletion]);
@@ -183,11 +225,97 @@ describe('getFilteredMutationOverTimeData', () => {
183
225
  displayedMutationTypes: [],
184
226
  proportionInterval,
185
227
  displayMutations: [anotherSubstitution.code, someDeletion.code],
228
+ mutationFilterValue: '',
229
+ sequenceType: 'nucleotide',
230
+ annotationProvider: () => {
231
+ return [];
232
+ },
186
233
  });
187
234
 
188
235
  expect(result.getFirstAxisKeys()).to.deep.equal([anotherSubstitution, someDeletion]);
189
236
  });
190
237
 
238
+ it('should filter by mutation filter value', () => {
239
+ const { data, overallMutationData } = prepareMutationOverTimeData([
240
+ someSubstitutionEntry,
241
+ anotherSubstitutionEntry,
242
+ someDeletionEntry,
243
+ ]);
244
+
245
+ const result = getFilteredMutationOverTimeData({
246
+ data,
247
+ overallMutationData,
248
+ displayedSegments: [],
249
+ displayedMutationTypes: [],
250
+ proportionInterval,
251
+ mutationFilterValue: '23T',
252
+ sequenceType: 'nucleotide',
253
+ annotationProvider: () => {
254
+ return [];
255
+ },
256
+ });
257
+
258
+ expect(result.getFirstAxisKeys()).to.deep.equal([someSubstitution]);
259
+ });
260
+
261
+ describe('should filter by annotation', () => {
262
+ const { data, overallMutationData } = prepareMutationOverTimeData([
263
+ someSubstitutionEntry,
264
+ anotherSubstitutionEntry,
265
+ someDeletionEntry,
266
+ ]);
267
+
268
+ const expectFilteredValue = (filterValue: string, annotations: MutationAnnotations) => {
269
+ const annotationProvider = getMutationAnnotationsProvider(getMutationAnnotationsContext(annotations));
270
+
271
+ const result = getFilteredMutationOverTimeData({
272
+ data,
273
+ overallMutationData,
274
+ displayedSegments: [],
275
+ displayedMutationTypes: [],
276
+ proportionInterval,
277
+ mutationFilterValue: filterValue,
278
+ sequenceType: 'nucleotide',
279
+ annotationProvider,
280
+ });
281
+
282
+ expect(result.getFirstAxisKeys()).to.deep.equal([someSubstitution]);
283
+ };
284
+
285
+ it('with filter value in symbol', () => {
286
+ expectFilteredValue('#', [
287
+ {
288
+ name: 'Annotation 1',
289
+ description: 'Description 1',
290
+ symbol: '#',
291
+ nucleotideMutations: ['A123T'],
292
+ },
293
+ ]);
294
+ });
295
+
296
+ it('with filter value in name', () => {
297
+ expectFilteredValue('Annota', [
298
+ {
299
+ name: 'Annotation 1 #',
300
+ description: 'Description 1',
301
+ symbol: '+',
302
+ nucleotideMutations: ['A123T'],
303
+ },
304
+ ]);
305
+ });
306
+
307
+ it('with filter value in name', () => {
308
+ expectFilteredValue('Descr', [
309
+ {
310
+ name: 'Annotation 1',
311
+ description: 'Description 1',
312
+ symbol: '#',
313
+ nucleotideMutations: ['A123T'],
314
+ },
315
+ ]);
316
+ });
317
+ });
318
+
191
319
  const belowFilter = 0.1;
192
320
  const atFilterMin = 0.2;
193
321
  const inFilter = 0.5;
@@ -1,9 +1,10 @@
1
1
  import z from 'zod';
2
2
 
3
3
  import { type MutationOverTimeDataMap } from './MutationOverTimeData';
4
- import { type SubstitutionOrDeletionEntry } from '../../types';
4
+ import { type SequenceType, type SubstitutionOrDeletionEntry } from '../../types';
5
5
  import { Map2dView } from '../../utils/map2d';
6
- import type { Deletion, Substitution } from '../../utils/mutations';
6
+ import type { Deletion, Mutation, Substitution } from '../../utils/mutations';
7
+ import { type useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
7
8
  import type { DisplayedMutationType } from '../components/mutation-type-selector';
8
9
  import type { DisplayedSegment } from '../components/segment-selector';
9
10
 
@@ -17,6 +18,9 @@ export type GetFilteredMutationOverTimeDataArgs = {
17
18
  displayedMutationTypes: DisplayedMutationType[];
18
19
  proportionInterval: { min: number; max: number };
19
20
  displayMutations?: DisplayMutations;
21
+ mutationFilterValue: string;
22
+ sequenceType: SequenceType;
23
+ annotationProvider: ReturnType<typeof useMutationAnnotationsProvider>;
20
24
  };
21
25
 
22
26
  export function getFilteredMutationOverTimeData({
@@ -26,6 +30,9 @@ export function getFilteredMutationOverTimeData({
26
30
  displayedMutationTypes,
27
31
  proportionInterval,
28
32
  displayMutations,
33
+ mutationFilterValue,
34
+ sequenceType,
35
+ annotationProvider,
29
36
  }: GetFilteredMutationOverTimeDataArgs) {
30
37
  const filteredData = new Map2dView(data);
31
38
 
@@ -44,6 +51,10 @@ export function getFilteredMutationOverTimeData({
44
51
  return true;
45
52
  }
46
53
 
54
+ if (applySearchFilter(entry.mutation, sequenceType, mutationFilterValue, annotationProvider)) {
55
+ return true;
56
+ }
57
+
47
58
  return displayedMutationTypes.some(
48
59
  (mutationType) => mutationType.type === entry.mutation.type && !mutationType.checked,
49
60
  );
@@ -55,3 +66,29 @@ export function getFilteredMutationOverTimeData({
55
66
 
56
67
  return filteredData;
57
68
  }
69
+
70
+ export function applySearchFilter(
71
+ mutation: Mutation,
72
+ sequenceType: SequenceType,
73
+ filterValue: string,
74
+ annotationProvider: ReturnType<typeof useMutationAnnotationsProvider>,
75
+ ) {
76
+ if (filterValue === '') {
77
+ return false;
78
+ }
79
+
80
+ if (mutation.code.includes(filterValue)) {
81
+ return false;
82
+ }
83
+
84
+ const mutationAnnotations = annotationProvider(mutation, sequenceType);
85
+ if (mutationAnnotations === undefined || mutationAnnotations.length === 0) {
86
+ return true;
87
+ }
88
+ return !mutationAnnotations.some(
89
+ (annotation) =>
90
+ annotation.description.includes(filterValue) ||
91
+ annotation.name.includes(filterValue) ||
92
+ annotation.symbol.includes(filterValue),
93
+ );
94
+ }
@@ -1,6 +1,5 @@
1
- import { type PaginationState } from '@tanstack/table-core';
2
1
  import { type FunctionComponent } from 'preact';
3
- import { useMemo, useState } from 'preact/hooks';
2
+ import { useMemo } from 'preact/hooks';
4
3
 
5
4
  import { type MutationOverTimeDataMap } from './MutationOverTimeData';
6
5
  import { MutationsOverTimeGridTooltip } from './mutations-over-time-grid-tooltip';
@@ -13,6 +12,7 @@ import { type ColorScale, getColorWithinScale, getTextColorForScale } from '../c
13
12
  import Tooltip, { type TooltipPosition } from '../components/tooltip';
14
13
  import { formatProportion } from '../shared/table/formatProportion';
15
14
  import { type PageSizes, Pagination } from '../shared/tanstackTable/pagination';
15
+ import { usePageSizeContext } from '../shared/tanstackTable/pagination-context';
16
16
  import {
17
17
  createColumnHelper,
18
18
  flexRender,
@@ -43,11 +43,6 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
43
43
  });
44
44
  }, [data]);
45
45
 
46
- const [pagination, setPagination] = useState<PaginationState>({
47
- pageIndex: 0,
48
- pageSize: typeof pageSizes === 'number' ? pageSizes : (pageSizes.at(0) ?? 10),
49
- });
50
-
51
46
  const columns = useMemo(() => {
52
47
  const columnHelper = createColumnHelper<RowType>();
53
48
  const dates = data.getSecondAxisKeys();
@@ -104,15 +99,17 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
104
99
  return [mutationHeader, ...dateHeaders];
105
100
  }, [colorScale, data, sequenceType]);
106
101
 
102
+ const { pageSize } = usePageSizeContext();
107
103
  const table = usePreactTable({
108
104
  data: tableData,
109
105
  columns,
110
106
  getCoreRowModel: getCoreRowModel(),
111
107
  getPaginationRowModel: getPaginationRowModel(),
112
- debugTable: true,
113
- onPaginationChange: setPagination,
114
- state: {
115
- pagination,
108
+ initialState: {
109
+ pagination: {
110
+ pageIndex: 0,
111
+ pageSize,
112
+ },
116
113
  },
117
114
  });
118
115
 
@@ -81,6 +81,10 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
81
81
  initialMeanProportionInterval: { min: 0.05, max: 0.9 },
82
82
  pageSizes: [10, 20, 30, 40, 50],
83
83
  },
84
+ };
85
+
86
+ export const ShowsMutationAnnotations: StoryObj<MutationsOverTimeProps> = {
87
+ ...Default,
84
88
  play: async ({ canvasElement }) => {
85
89
  await expectMutationAnnotation(canvasElement, 'C44T');
86
90
  },
@@ -133,6 +137,29 @@ export const UsesPagination: StoryObj<MutationsOverTimeProps> = {
133
137
  },
134
138
  };
135
139
 
140
+ export const UsesMutationFilter: StoryObj<MutationsOverTimeProps> = {
141
+ ...Default,
142
+ play: async ({ canvas, step }) => {
143
+ await expectMutationOnPage(canvas, 'C44T');
144
+
145
+ await step('input filter', async () => {
146
+ const filterButton = canvas.getByRole('button', { name: 'Filter mutations' });
147
+ await userEvent.click(filterButton);
148
+
149
+ const filterInput = canvas.getByPlaceholderText('Filter');
150
+ await userEvent.type(filterInput, 'T21');
151
+ });
152
+
153
+ await step('should show only matching filter', async () => {
154
+ await expectMutationOnPage(canvas, 'T21653-');
155
+ await expectMutationOnPage(canvas, 'T21655-');
156
+
157
+ const filteredMutation = canvas.queryByText('C44T');
158
+ await expect(filteredMutation).not.toBeInTheDocument();
159
+ });
160
+ },
161
+ };
162
+
136
163
  async function expectMutationOnPage(canvas: Canvas, mutation: string) {
137
164
  await waitFor(async () => {
138
165
  const mutationOnFirstPage = canvas.getAllByText(mutation)[0];
@@ -19,6 +19,7 @@ import {
19
19
  import { type Deletion, type Substitution } from '../../utils/mutations';
20
20
  import { toTemporalClass } from '../../utils/temporalClass';
21
21
  import { useLapisUrl } from '../LapisUrlContext';
22
+ import { useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
22
23
  import { type ColorScale } from '../components/color-scale-selector';
23
24
  import { ColorScaleSelectorDropdown } from '../components/color-scale-selector-dropdown';
24
25
  import { CsvDownloadButton } from '../components/csv-download-button';
@@ -27,6 +28,7 @@ import { Fullscreen } from '../components/fullscreen';
27
28
  import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../components/info';
28
29
  import { LoadingDisplay } from '../components/loading-display';
29
30
  import { type DisplayedMutationType, MutationTypeSelector } from '../components/mutation-type-selector';
31
+ import { MutationsOverTimeTextFilter } from '../components/mutations-over-time-text-filter';
30
32
  import { NoDataDisplay } from '../components/no-data-display';
31
33
  import type { ProportionInterval } from '../components/proportion-selector';
32
34
  import { ProportionSelectorDropdown } from '../components/proportion-selector-dropdown';
@@ -34,6 +36,7 @@ import { ResizeContainer } from '../components/resize-container';
34
36
  import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '../components/segment-selector';
35
37
  import Tabs from '../components/tabs';
36
38
  import { pageSizesSchema } from '../shared/tanstackTable/pagination';
39
+ import { PageSizeContextProvider } from '../shared/tanstackTable/pagination-context';
37
40
  import { useWebWorker } from '../webWorkers/useWebWorker';
38
41
 
39
42
  const mutationsOverTimeViewSchema = z.literal(views.grid);
@@ -123,6 +126,9 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
123
126
  originalComponentProps,
124
127
  overallMutationData,
125
128
  }) => {
129
+ const [mutationFilterValue, setMutationFilterValue] = useState('');
130
+ const annotationProvider = useMutationAnnotationsProvider();
131
+
126
132
  const [proportionInterval, setProportionInterval] = useState(originalComponentProps.initialMeanProportionInterval);
127
133
  const [colorScale, setColorScale] = useState<ColorScale>({ min: 0, max: 1, color: 'indigo' });
128
134
 
@@ -132,8 +138,6 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
132
138
  { label: 'Deletions', checked: true, type: 'deletion' },
133
139
  ]);
134
140
 
135
- const displayMutations = originalComponentProps.displayMutations;
136
-
137
141
  const filteredData = useMemo(() => {
138
142
  return getFilteredMutationOverTimeData({
139
143
  data: mutationOverTimeData,
@@ -141,7 +145,10 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
141
145
  displayedSegments,
142
146
  displayedMutationTypes,
143
147
  proportionInterval,
144
- displayMutations,
148
+ displayMutations: originalComponentProps.displayMutations,
149
+ mutationFilterValue,
150
+ sequenceType: originalComponentProps.sequenceType,
151
+ annotationProvider,
145
152
  });
146
153
  }, [
147
154
  mutationOverTimeData,
@@ -149,7 +156,10 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
149
156
  displayedSegments,
150
157
  displayedMutationTypes,
151
158
  proportionInterval,
152
- displayMutations,
159
+ originalComponentProps.displayMutations,
160
+ originalComponentProps.sequenceType,
161
+ mutationFilterValue,
162
+ annotationProvider,
153
163
  ]);
154
164
 
155
165
  const getTab = (view: MutationsOverTimeView) => {
@@ -190,10 +200,16 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
190
200
  colorScale={colorScale}
191
201
  setColorScale={setColorScale}
192
202
  originalComponentProps={originalComponentProps}
203
+ setFilterValue={setMutationFilterValue}
204
+ mutationFilterValue={mutationFilterValue}
193
205
  />
194
206
  );
195
207
 
196
- return <Tabs tabs={tabs} toolbar={toolbar} />;
208
+ return (
209
+ <PageSizeContextProvider pageSizes={originalComponentProps.pageSizes}>
210
+ <Tabs tabs={tabs} toolbar={toolbar} />
211
+ </PageSizeContextProvider>
212
+ );
197
213
  };
198
214
 
199
215
  type ToolbarProps = {
@@ -208,6 +224,8 @@ type ToolbarProps = {
208
224
  colorScale: ColorScale;
209
225
  setColorScale: Dispatch<StateUpdater<ColorScale>>;
210
226
  originalComponentProps: MutationsOverTimeProps;
227
+ mutationFilterValue: string;
228
+ setFilterValue: (filterValue: string) => void;
211
229
  };
212
230
 
213
231
  const Toolbar: FunctionComponent<ToolbarProps> = ({
@@ -222,9 +240,12 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
222
240
  colorScale,
223
241
  setColorScale,
224
242
  originalComponentProps,
243
+ setFilterValue,
244
+ mutationFilterValue,
225
245
  }) => {
226
246
  return (
227
247
  <>
248
+ <MutationsOverTimeTextFilter setFilterValue={setFilterValue} value={mutationFilterValue} />
228
249
  {activeTab === 'Grid' && (
229
250
  <ColorScaleSelectorDropdown colorScale={colorScale} setColorScale={setColorScale} />
230
251
  )}