@genspectrum/dashboard-components 1.11.1 → 1.13.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 (58) hide show
  1. package/README.md +0 -7
  2. package/custom-elements.json +6 -25
  3. package/dist/components.d.ts +24 -30
  4. package/dist/components.js +929 -742
  5. package/dist/components.js.map +1 -1
  6. package/dist/util.d.ts +40 -24
  7. package/package.json +1 -5
  8. package/src/lapisApi/lapisApi.ts +21 -1
  9. package/src/lapisApi/lapisTypes.ts +37 -0
  10. package/src/preact/components/annotated-mutation.tsx +2 -2
  11. package/src/preact/{mutationsOverTime/mutations-over-time-grid.tsx → components/features-over-time-grid.tsx} +45 -52
  12. package/src/preact/genomeViewer/genome-data-viewer.tsx +2 -2
  13. package/src/preact/mutationsOverTime/MutationOverTimeData.ts +6 -4
  14. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay/aminoAcidMutations.json +5482 -0
  15. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay/aminoAcidMutationsOverTime.json +5496 -0
  16. package/src/preact/mutationsOverTime/__mockData__/byWeek/mutationsOverTime.json +7100 -0
  17. package/src/preact/mutationsOverTime/__mockData__/byWeek/nucleotideMutations.json +10122 -0
  18. package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mutationsOverTime.json +12646 -0
  19. package/src/preact/mutationsOverTime/__mockData__/defaultMockData/nucleotideMutations.json +12632 -0
  20. package/src/preact/mutationsOverTime/__mockData__/request1800s/mutationsOverTime.json +16 -0
  21. package/src/preact/mutationsOverTime/__mockData__/request1800s/nucleotideMutations.json +11 -0
  22. package/src/preact/mutationsOverTime/__mockData__/withDisplayMutations/mutationsOverTime.json +52 -0
  23. package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +3 -3
  24. package/src/preact/mutationsOverTime/mutations-over-time-grid-tooltip.tsx +3 -6
  25. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +199 -12
  26. package/src/preact/mutationsOverTime/mutations-over-time.tsx +30 -35
  27. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +30 -3
  28. package/src/query/queryDatesInDataset.ts +89 -0
  29. package/src/query/queryMutationsOverTime.spec.ts +526 -548
  30. package/src/query/queryMutationsOverTime.ts +22 -245
  31. package/src/query/queryQueriesOverTime.spec.ts +432 -0
  32. package/src/query/queryQueriesOverTime.ts +125 -0
  33. package/src/utilEntrypoint.ts +3 -1
  34. package/src/utils/mutations.spec.ts +6 -0
  35. package/src/utils/mutations.ts +1 -1
  36. package/src/utils/temporalClass.ts +4 -0
  37. package/src/web-components/visualization/gs-mutation-comparison.tsx +2 -2
  38. package/src/web-components/visualization/gs-mutations-over-time.spec-d.ts +0 -3
  39. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +283 -17
  40. package/src/web-components/visualization/gs-mutations-over-time.tsx +0 -9
  41. package/standalone-bundle/dashboard-components.js +8935 -8780
  42. package/standalone-bundle/dashboard-components.js.map +1 -1
  43. package/dist/assets/mutationOverTimeWorker-CQQFRoK4.js.map +0 -1
  44. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay.ts +0 -47170
  45. package/src/preact/mutationsOverTime/__mockData__/byWeek.ts +0 -54026
  46. package/src/preact/mutationsOverTime/__mockData__/defaultMockData.ts +0 -108385
  47. package/src/preact/mutationsOverTime/__mockData__/mockConversion.ts +0 -54
  48. package/src/preact/mutationsOverTime/__mockData__/noDataWhenNoMutationsAreInFilter.ts +0 -23
  49. package/src/preact/mutationsOverTime/__mockData__/noDataWhenThereAreNoDatesInFilter.ts +0 -23
  50. package/src/preact/mutationsOverTime/__mockData__/showsMessageWhenTooManyMutations.ts +0 -65527
  51. package/src/preact/mutationsOverTime/__mockData__/withDisplayMutations.ts +0 -352
  52. package/src/preact/mutationsOverTime/__mockData__/withGaps.ts +0 -298
  53. package/src/preact/mutationsOverTime/mutationOverTimeWorker.mock.ts +0 -33
  54. package/src/preact/mutationsOverTime/mutationOverTimeWorker.ts +0 -29
  55. package/src/preact/webWorkers/useWebWorker.ts +0 -74
  56. package/src/preact/webWorkers/workerFunction.ts +0 -30
  57. package/src/query/queryMutationsOverTimeNewEndpoint.spec.ts +0 -1179
  58. package/standalone-bundle/assets/mutationOverTimeWorker-DIpJukJC.js.map +0 -1
@@ -0,0 +1,432 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { queryQueriesOverTimeData } from './queryQueriesOverTime';
4
+ import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../vitest.setup';
5
+
6
+ describe('queryQueriesOverTime', () => {
7
+ it('should fetch queries over time and build Map2D structure', async () => {
8
+ const lapisFilter = { field1: 'value1', field2: 'value2' };
9
+ const dateField = 'dateField';
10
+
11
+ // Mock date aggregation call
12
+ lapisRequestMocks.aggregated(
13
+ { ...lapisFilter, fields: [dateField] },
14
+ {
15
+ data: [
16
+ { count: 1, [dateField]: '2023-01-01' },
17
+ { count: 2, [dateField]: '2023-01-03' },
18
+ ],
19
+ },
20
+ );
21
+
22
+ const dateRanges = [
23
+ { dateFrom: '2023-01-01', dateTo: '2023-01-01' },
24
+ { dateFrom: '2023-01-02', dateTo: '2023-01-02' },
25
+ { dateFrom: '2023-01-03', dateTo: '2023-01-03' },
26
+ ];
27
+
28
+ // Mock queries over time API call
29
+ lapisRequestMocks.queriesOverTime([
30
+ {
31
+ body: {
32
+ filters: lapisFilter,
33
+ dateRanges,
34
+ queries: [
35
+ {
36
+ displayLabel: 'Query A',
37
+ countQuery: 'gene1:123G AND gene2:456T',
38
+ coverageQuery: '!gene1:123N AND !gene2:456N',
39
+ },
40
+ {
41
+ displayLabel: 'Query B',
42
+ countQuery: 'gene1:789C',
43
+ coverageQuery: '!gene1:789N',
44
+ },
45
+ ],
46
+ dateField,
47
+ },
48
+ response: {
49
+ data: {
50
+ queries: ['Query A', 'Query B'],
51
+ dateRanges,
52
+ data: [
53
+ [
54
+ { count: 10, coverage: 100 },
55
+ { count: 15, coverage: 100 },
56
+ { count: 20, coverage: 100 },
57
+ ],
58
+ [
59
+ { count: 5, coverage: 100 },
60
+ { count: 8, coverage: 100 },
61
+ { count: 12, coverage: 100 },
62
+ ],
63
+ ],
64
+ totalCountsByDateRange: [100, 120, 150],
65
+ },
66
+ },
67
+ },
68
+ ]);
69
+
70
+ const { queryOverTimeData } = await queryQueriesOverTimeData(
71
+ lapisFilter,
72
+ [
73
+ {
74
+ displayLabel: 'Query A',
75
+ countQuery: 'gene1:123G AND gene2:456T',
76
+ coverageQuery: '!gene1:123N AND !gene2:456N',
77
+ },
78
+ {
79
+ displayLabel: 'Query B',
80
+ countQuery: 'gene1:789C',
81
+ coverageQuery: '!gene1:789N',
82
+ },
83
+ ],
84
+ DUMMY_LAPIS_URL,
85
+ dateField,
86
+ 'day',
87
+ );
88
+
89
+ // Verify Map2D structure
90
+ const expectedData = [
91
+ [
92
+ { type: 'valueWithCoverage', count: 10, coverage: 100, totalCount: 100 },
93
+ { type: 'valueWithCoverage', count: 15, coverage: 100, totalCount: 120 },
94
+ { type: 'valueWithCoverage', count: 20, coverage: 100, totalCount: 150 },
95
+ ],
96
+ [
97
+ { type: 'valueWithCoverage', count: 5, coverage: 100, totalCount: 100 },
98
+ { type: 'valueWithCoverage', count: 8, coverage: 100, totalCount: 120 },
99
+ { type: 'valueWithCoverage', count: 12, coverage: 100, totalCount: 150 },
100
+ ],
101
+ ];
102
+
103
+ // Convert Map2DContents to array for comparison
104
+ const actualData = Array.from(queryOverTimeData.keysFirstAxis.values()).map((query) => {
105
+ return Array.from(queryOverTimeData.keysSecondAxis.keys()).map((dateKey) => {
106
+ return queryOverTimeData.data.get(query)?.get(dateKey);
107
+ });
108
+ });
109
+
110
+ expect(actualData).to.deep.equal(expectedData);
111
+
112
+ // Verify first axis keys (queries)
113
+ const queries = Array.from(queryOverTimeData.keysFirstAxis.values());
114
+ expect(queries).to.deep.equal(['Query A', 'Query B']);
115
+
116
+ // Verify second axis keys (dates)
117
+ const dates = Array.from(queryOverTimeData.keysSecondAxis.values());
118
+ expect(dates[0].dateString).toBe('2023-01-01');
119
+ expect(dates[1].dateString).toBe('2023-01-02');
120
+ expect(dates[2].dateString).toBe('2023-01-03');
121
+ });
122
+
123
+ it('should handle dates with no data (totalCount = 0)', async () => {
124
+ const lapisFilter = { field1: 'value1' };
125
+ const dateField = 'dateField';
126
+
127
+ lapisRequestMocks.aggregated(
128
+ { ...lapisFilter, fields: [dateField] },
129
+ {
130
+ data: [
131
+ { count: 1, [dateField]: '2023-01-01' },
132
+ { count: 1, [dateField]: '2023-01-03' },
133
+ ],
134
+ },
135
+ );
136
+
137
+ const dateRanges = [
138
+ { dateFrom: '2023-01-01', dateTo: '2023-01-01' },
139
+ { dateFrom: '2023-01-02', dateTo: '2023-01-02' },
140
+ { dateFrom: '2023-01-03', dateTo: '2023-01-03' },
141
+ ];
142
+
143
+ lapisRequestMocks.queriesOverTime([
144
+ {
145
+ body: {
146
+ filters: lapisFilter,
147
+ dateRanges,
148
+ queries: [
149
+ {
150
+ displayLabel: 'Test Query',
151
+ countQuery: 'gene1:123G',
152
+ coverageQuery: '!gene1:123N',
153
+ },
154
+ ],
155
+ dateField,
156
+ },
157
+ response: {
158
+ data: {
159
+ queries: ['Test Query'],
160
+ dateRanges,
161
+ data: [
162
+ [
163
+ { count: 5, coverage: 50 },
164
+ { count: 0, coverage: 0 },
165
+ { count: 8, coverage: 60 },
166
+ ],
167
+ ],
168
+ totalCountsByDateRange: [50, 0, 60],
169
+ },
170
+ },
171
+ },
172
+ ]);
173
+
174
+ const { queryOverTimeData } = await queryQueriesOverTimeData(
175
+ lapisFilter,
176
+ [
177
+ {
178
+ displayLabel: 'Test Query',
179
+ countQuery: 'gene1:123G',
180
+ coverageQuery: '!gene1:123N',
181
+ },
182
+ ],
183
+ DUMMY_LAPIS_URL,
184
+ dateField,
185
+ 'day',
186
+ );
187
+
188
+ const actualData = Array.from(queryOverTimeData.keysFirstAxis.values()).map((query) => {
189
+ return Array.from(queryOverTimeData.keysSecondAxis.keys()).map((dateKey) => {
190
+ return queryOverTimeData.data.get(query)?.get(dateKey);
191
+ });
192
+ });
193
+
194
+ // Middle date should be null (no data)
195
+ expect(actualData).to.deep.equal([
196
+ [
197
+ { type: 'valueWithCoverage', count: 5, coverage: 50, totalCount: 50 },
198
+ null,
199
+ { type: 'valueWithCoverage', count: 8, coverage: 60, totalCount: 60 },
200
+ ],
201
+ ]);
202
+ });
203
+
204
+ it('should handle zero coverage (belowThreshold)', async () => {
205
+ const lapisFilter = { field1: 'value1' };
206
+ const dateField = 'dateField';
207
+
208
+ lapisRequestMocks.aggregated(
209
+ { ...lapisFilter, fields: [dateField] },
210
+ {
211
+ data: [{ count: 1, [dateField]: '2023-01-01' }],
212
+ },
213
+ );
214
+
215
+ const dateRanges = [{ dateFrom: '2023-01-01', dateTo: '2023-01-01' }];
216
+
217
+ lapisRequestMocks.queriesOverTime([
218
+ {
219
+ body: {
220
+ filters: lapisFilter,
221
+ dateRanges,
222
+ queries: [
223
+ {
224
+ displayLabel: 'Zero Coverage Query',
225
+ countQuery: 'gene1:123G',
226
+ coverageQuery: '!gene1:123N',
227
+ },
228
+ ],
229
+ dateField,
230
+ },
231
+ response: {
232
+ data: {
233
+ queries: ['Zero Coverage Query'],
234
+ dateRanges,
235
+ data: [[{ count: 0, coverage: 0 }]],
236
+ totalCountsByDateRange: [100],
237
+ },
238
+ },
239
+ },
240
+ ]);
241
+
242
+ const { queryOverTimeData } = await queryQueriesOverTimeData(
243
+ lapisFilter,
244
+ [
245
+ {
246
+ displayLabel: 'Zero Coverage Query',
247
+ countQuery: 'gene1:123G',
248
+ coverageQuery: '!gene1:123N',
249
+ },
250
+ ],
251
+ DUMMY_LAPIS_URL,
252
+ dateField,
253
+ 'day',
254
+ );
255
+
256
+ const actualData = Array.from(queryOverTimeData.keysFirstAxis.values()).map((query) => {
257
+ return Array.from(queryOverTimeData.keysSecondAxis.keys()).map((dateKey) => {
258
+ return queryOverTimeData.data.get(query)?.get(dateKey);
259
+ });
260
+ });
261
+
262
+ expect(actualData).to.deep.equal([[{ type: 'belowThreshold', totalCount: 100 }]]);
263
+ });
264
+
265
+ it('should return empty structure when no dates are available', async () => {
266
+ const lapisFilter = { field1: 'value1' };
267
+ const dateField = 'dateField';
268
+
269
+ lapisRequestMocks.aggregated(
270
+ { ...lapisFilter, fields: [dateField] },
271
+ {
272
+ data: [],
273
+ },
274
+ );
275
+
276
+ const { queryOverTimeData } = await queryQueriesOverTimeData(
277
+ lapisFilter,
278
+ [
279
+ {
280
+ displayLabel: 'Test Query',
281
+ countQuery: 'gene1:123G',
282
+ coverageQuery: '!gene1:123N',
283
+ },
284
+ ],
285
+ DUMMY_LAPIS_URL,
286
+ dateField,
287
+ 'day',
288
+ );
289
+
290
+ expect(Array.from(queryOverTimeData.keysFirstAxis.values())).to.deep.equal([]);
291
+ expect(Array.from(queryOverTimeData.keysSecondAxis.values())).to.deep.equal([]);
292
+ expect(queryOverTimeData.data.size).toBe(0);
293
+ });
294
+
295
+ it('should respect dateFrom filter', async () => {
296
+ const dateField = 'dateField';
297
+ const lapisFilter = { field1: 'value1', [`${dateField}From`]: '2023-01-02' };
298
+
299
+ lapisRequestMocks.aggregated(
300
+ { ...lapisFilter, fields: [dateField] },
301
+ {
302
+ data: [
303
+ { count: 1, [dateField]: '2023-01-02' },
304
+ { count: 1, [dateField]: '2023-01-03' },
305
+ ],
306
+ },
307
+ );
308
+
309
+ const dateRanges = [
310
+ { dateFrom: '2023-01-02', dateTo: '2023-01-02' },
311
+ { dateFrom: '2023-01-03', dateTo: '2023-01-03' },
312
+ ];
313
+
314
+ lapisRequestMocks.queriesOverTime([
315
+ {
316
+ body: {
317
+ filters: lapisFilter,
318
+ dateRanges,
319
+ queries: [
320
+ {
321
+ displayLabel: 'Test',
322
+ countQuery: 'gene1:123G',
323
+ coverageQuery: '!gene1:123N',
324
+ },
325
+ ],
326
+ dateField,
327
+ },
328
+ response: {
329
+ data: {
330
+ queries: ['Test'],
331
+ dateRanges,
332
+ data: [
333
+ [
334
+ { count: 5, coverage: 50 },
335
+ { count: 8, coverage: 60 },
336
+ ],
337
+ ],
338
+ totalCountsByDateRange: [50, 60],
339
+ },
340
+ },
341
+ },
342
+ ]);
343
+
344
+ const { queryOverTimeData } = await queryQueriesOverTimeData(
345
+ lapisFilter,
346
+ [
347
+ {
348
+ displayLabel: 'Test',
349
+ countQuery: 'gene1:123G',
350
+ coverageQuery: '!gene1:123N',
351
+ },
352
+ ],
353
+ DUMMY_LAPIS_URL,
354
+ dateField,
355
+ 'day',
356
+ );
357
+
358
+ const dates = Array.from(queryOverTimeData.keysSecondAxis.values());
359
+ expect(dates.length).toBe(2);
360
+ expect(dates[0].dateString).toBe('2023-01-02');
361
+ expect(dates[1].dateString).toBe('2023-01-03');
362
+ });
363
+
364
+ it('should work with different granularities', async () => {
365
+ const lapisFilter = { field1: 'value1' };
366
+ const dateField = 'dateField';
367
+
368
+ lapisRequestMocks.aggregated(
369
+ { ...lapisFilter, fields: [dateField] },
370
+ {
371
+ data: [
372
+ { count: 1, [dateField]: '2023-01-15' },
373
+ { count: 1, [dateField]: '2023-02-20' },
374
+ ],
375
+ },
376
+ );
377
+
378
+ const dateRanges = [
379
+ { dateFrom: '2023-01-01', dateTo: '2023-01-31' },
380
+ { dateFrom: '2023-02-01', dateTo: '2023-02-28' },
381
+ ];
382
+
383
+ lapisRequestMocks.queriesOverTime([
384
+ {
385
+ body: {
386
+ filters: lapisFilter,
387
+ dateRanges,
388
+ queries: [
389
+ {
390
+ displayLabel: 'Monthly Query',
391
+ countQuery: 'gene1:123G',
392
+ coverageQuery: '!gene1:123N',
393
+ },
394
+ ],
395
+ dateField,
396
+ },
397
+ response: {
398
+ data: {
399
+ queries: ['Monthly Query'],
400
+ dateRanges,
401
+ data: [
402
+ [
403
+ { count: 10, coverage: 100 },
404
+ { count: 20, coverage: 150 },
405
+ ],
406
+ ],
407
+ totalCountsByDateRange: [100, 150],
408
+ },
409
+ },
410
+ },
411
+ ]);
412
+
413
+ const { queryOverTimeData } = await queryQueriesOverTimeData(
414
+ lapisFilter,
415
+ [
416
+ {
417
+ displayLabel: 'Monthly Query',
418
+ countQuery: 'gene1:123G',
419
+ coverageQuery: '!gene1:123N',
420
+ },
421
+ ],
422
+ DUMMY_LAPIS_URL,
423
+ dateField,
424
+ 'month',
425
+ );
426
+
427
+ const dates = Array.from(queryOverTimeData.keysSecondAxis.values());
428
+ expect(dates.length).toBe(2);
429
+ expect(dates[0].dateString).toBe('2023-01');
430
+ expect(dates[1].dateString).toBe('2023-02');
431
+ });
432
+ });
@@ -0,0 +1,125 @@
1
+ import { queryDatesInDataset } from './queryDatesInDataset';
2
+ import { fetchQueriesOverTime } from '../lapisApi/lapisApi';
3
+ import { type QueryDefinition } from '../lapisApi/lapisTypes';
4
+ import { UserFacingError } from '../preact/components/error-display';
5
+ import { type LapisFilter, type TemporalGranularity } from '../types';
6
+ import { type ProportionValue } from './queryMutationsOverTime';
7
+ import { type Map2DContents } from '../utils/map2d';
8
+ import { type Temporal } from '../utils/temporalClass';
9
+
10
+ const MAX_NUMBER_OF_GRID_COLUMNS = 200;
11
+
12
+ /**
13
+ * Query data for multiple queries over time periods.
14
+ *
15
+ * @param lapisFilter - Standard LAPIS filters to apply
16
+ * @param queries - Array of query definitions with countQuery and coverageQuery
17
+ * @param lapis - LAPIS URL
18
+ * @param lapisDateField - The metadata field to use for dates (e.g., "date", "submissionDate")
19
+ * @param granularity - Temporal granularity (day, week, month, year)
20
+ * @param signal - Optional abort signal for cancellation
21
+ * @returns Map2D structure with queries as first axis, dates as second axis
22
+ */
23
+ export async function queryQueriesOverTimeData(
24
+ lapisFilter: LapisFilter,
25
+ queries: QueryDefinition[],
26
+ lapis: string,
27
+ lapisDateField: string,
28
+ granularity: TemporalGranularity,
29
+ signal?: AbortSignal,
30
+ ) {
31
+ const requestedDateRanges = await queryDatesInDataset(lapisFilter, lapis, granularity, lapisDateField, signal);
32
+
33
+ if (requestedDateRanges.length > MAX_NUMBER_OF_GRID_COLUMNS) {
34
+ throw new UserFacingError(
35
+ 'Too many dates',
36
+ `The dataset would contain ${requestedDateRanges.length} date intervals. ` +
37
+ `Please reduce the number to below ${MAX_NUMBER_OF_GRID_COLUMNS} to display the data. ` +
38
+ 'You can achieve this by either narrowing the date range in the provided LAPIS filter or by selecting a larger granularity.',
39
+ );
40
+ }
41
+
42
+ if (requestedDateRanges.length === 0) {
43
+ return {
44
+ queryOverTimeData: {
45
+ keysFirstAxis: new Map<string, string>(),
46
+ keysSecondAxis: new Map<string, Temporal>(),
47
+ data: new Map<string, Map<string, ProportionValue>>(),
48
+ } as Map2DContents<string, Temporal, ProportionValue>,
49
+ };
50
+ }
51
+
52
+ const apiResult = await fetchQueriesOverTime(
53
+ lapis,
54
+ {
55
+ filters: lapisFilter,
56
+ queries: queries.map((q) => ({
57
+ displayLabel: q.displayLabel,
58
+ countQuery: q.countQuery,
59
+ coverageQuery: q.coverageQuery,
60
+ })),
61
+ dateRanges: requestedDateRanges.map((date) => ({
62
+ dateFrom: date.firstDay.toString(),
63
+ dateTo: date.lastDay.toString(),
64
+ })),
65
+ dateField: lapisDateField,
66
+ },
67
+ signal,
68
+ );
69
+
70
+ const totalCounts = apiResult.data.totalCountsByDateRange;
71
+ const responseQueries = apiResult.data.queries;
72
+
73
+ const queryOverTimeData: Map2DContents<string, Temporal, ProportionValue> = {
74
+ keysFirstAxis: new Map(responseQueries.map((query) => [query, query])),
75
+ keysSecondAxis: new Map(requestedDateRanges.map((date) => [date.dateString, date])),
76
+ data: new Map(
77
+ responseQueries.map((query, i) => [
78
+ query,
79
+ new Map(
80
+ requestedDateRanges.map((date, j): [string, ProportionValue] => {
81
+ if (totalCounts[j] === 0) {
82
+ return [date.dateString, null];
83
+ }
84
+
85
+ const count = apiResult.data.data[i][j].count;
86
+ const coverage = apiResult.data.data[i][j].coverage;
87
+ const totalCount = totalCounts[j];
88
+
89
+ if (coverage === 0) {
90
+ return [
91
+ date.dateString,
92
+ {
93
+ type: 'belowThreshold',
94
+ totalCount,
95
+ },
96
+ ];
97
+ }
98
+
99
+ return [
100
+ date.dateString,
101
+ {
102
+ type: 'valueWithCoverage',
103
+ count,
104
+ coverage,
105
+ totalCount,
106
+ },
107
+ ];
108
+ }),
109
+ ),
110
+ ]),
111
+ ),
112
+ };
113
+
114
+ return {
115
+ queryOverTimeData,
116
+ };
117
+ }
118
+
119
+ export function serializeQuery(displayLabel: string): string {
120
+ return displayLabel;
121
+ }
122
+
123
+ export function serializeTemporal(date: Temporal): string {
124
+ return date.dateString;
125
+ }
@@ -51,4 +51,6 @@ export {
51
51
  } from './preact/numberRangeFilter/NumberRangeFilterChangedEvent';
52
52
 
53
53
  export { type MeanProportionInterval } from './preact/mutationsOverTime/mutations-over-time';
54
- export { type CustomColumn } from './preact/mutationsOverTime/mutations-over-time-grid';
54
+ export { type CustomColumn } from './preact/components/features-over-time-grid';
55
+
56
+ export { type QueryDefinition } from './lapisApi/lapisTypes';
@@ -34,6 +34,12 @@ describe('SubstitutionClass', () => {
34
34
  expect(substitution.toString()).to.equal(expected);
35
35
  }
36
36
  });
37
+
38
+ it('should parse X correclty', () => {
39
+ expect(SubstitutionClass.parse('NP:X114D')).deep.equal(new SubstitutionClass('NP', 'X', 'D', 114));
40
+ expect(SubstitutionClass.parse('NP:D114X')).deep.equal(new SubstitutionClass('NP', 'D', 'X', 114));
41
+ expect(SubstitutionClass.parse('NP:X114X')).deep.equal(new SubstitutionClass('NP', 'X', 'X', 114));
42
+ });
37
43
  });
38
44
 
39
45
  describe('DeletionClass', () => {
@@ -15,7 +15,7 @@ export interface MutationClass extends Mutation {
15
15
 
16
16
  // Allowed IUPAC characters: https://www.bioinformatics.org/sms/iupac.html
17
17
  const nucleotideChars = 'ACGTRYKMSWBDHVN';
18
- const aminoAcidChars = 'ACDEFGHIKLMNPQRSTVWY';
18
+ const aminoAcidChars = 'ACDEFGHIKLMNPQRSTVWYX';
19
19
 
20
20
  function segmentPart(isOptional: boolean) {
21
21
  const segmentPart = `(?<segment>[A-Z0-9_-]+):`;
@@ -501,6 +501,10 @@ export function addUnit(temporal: TemporalClass, amount: number): TemporalClass
501
501
  throw new Error(`Invalid argument: ${temporal}`);
502
502
  }
503
503
 
504
+ /**
505
+ * Given a date like 2025-10-15 and a granularity ("day", "week", ...), returns a date range
506
+ * like "2025-10-15" (day) or "2025-W42" (week) etc.
507
+ */
504
508
  export function parseDateStringToTemporal(date: string, granularity: TemporalGranularity) {
505
509
  const cache = TemporalCache.getInstance();
506
510
  const day = cache.getYearMonthDay(date);
@@ -131,7 +131,7 @@ export class MutationComparisonComponent extends PreactLitAdapterWithGridJsStyle
131
131
 
132
132
  declare global {
133
133
  interface HTMLElementTagNameMap {
134
- 'gs-mutation-comparison-component': MutationComparisonComponent;
134
+ 'gs-mutation-comparison': MutationComparisonComponent;
135
135
  }
136
136
  }
137
137
 
@@ -139,7 +139,7 @@ declare global {
139
139
  // eslint-disable-next-line @typescript-eslint/no-namespace
140
140
  namespace JSX {
141
141
  interface IntrinsicElements {
142
- 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
142
+ 'gs-mutation-comparison': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
143
143
  }
144
144
  }
145
145
  }
@@ -32,9 +32,6 @@ describe('gs-mutations-over-time', () => {
32
32
  expectTypeOf<typeof MutationsOverTimeComponent.prototype.initialMeanProportionInterval>().toEqualTypeOf<
33
33
  MutationsOverTimeProps['initialMeanProportionInterval']
34
34
  >();
35
- expectTypeOf<typeof MutationsOverTimeComponent.prototype.useNewEndpoint>().toEqualTypeOf<
36
- MutationsOverTimeProps['useNewEndpoint']
37
- >();
38
35
  expectTypeOf<typeof MutationsOverTimeComponent.prototype.pageSizes>().toEqualTypeOf<
39
36
  MutationsOverTimeProps['pageSizes']
40
37
  >();