@genspectrum/dashboard-components 1.12.0 → 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 (57) hide show
  1. package/README.md +0 -7
  2. package/custom-elements.json +6 -25
  3. package/dist/components.d.ts +30 -36
  4. package/dist/components.js +943 -757
  5. package/dist/components.js.map +1 -1
  6. package/dist/util.d.ts +46 -30
  7. package/package.json +1 -5
  8. package/src/lapisApi/lapisApi.ts +21 -1
  9. package/src/lapisApi/lapisTypes.ts +36 -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 +21 -232
  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-mutations-over-time.spec-d.ts +0 -3
  38. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +283 -17
  39. package/src/web-components/visualization/gs-mutations-over-time.tsx +0 -9
  40. package/standalone-bundle/dashboard-components.js +8935 -8781
  41. package/standalone-bundle/dashboard-components.js.map +1 -1
  42. package/dist/assets/mutationOverTimeWorker-f8Kp0S6V.js.map +0 -1
  43. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay.ts +0 -47170
  44. package/src/preact/mutationsOverTime/__mockData__/byWeek.ts +0 -54026
  45. package/src/preact/mutationsOverTime/__mockData__/defaultMockData.ts +0 -108385
  46. package/src/preact/mutationsOverTime/__mockData__/mockConversion.ts +0 -54
  47. package/src/preact/mutationsOverTime/__mockData__/noDataWhenNoMutationsAreInFilter.ts +0 -23
  48. package/src/preact/mutationsOverTime/__mockData__/noDataWhenThereAreNoDatesInFilter.ts +0 -23
  49. package/src/preact/mutationsOverTime/__mockData__/showsMessageWhenTooManyMutations.ts +0 -65527
  50. package/src/preact/mutationsOverTime/__mockData__/withDisplayMutations.ts +0 -352
  51. package/src/preact/mutationsOverTime/__mockData__/withGaps.ts +0 -298
  52. package/src/preact/mutationsOverTime/mutationOverTimeWorker.mock.ts +0 -33
  53. package/src/preact/mutationsOverTime/mutationOverTimeWorker.ts +0 -29
  54. package/src/preact/webWorkers/useWebWorker.ts +0 -74
  55. package/src/preact/webWorkers/workerFunction.ts +0 -30
  56. package/src/query/queryMutationsOverTimeNewEndpoint.spec.ts +0 -988
  57. package/standalone-bundle/assets/mutationOverTimeWorker-AhhjjklP.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);
@@ -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
  >();