@genspectrum/dashboard-components 1.16.0 → 1.17.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 (36) hide show
  1. package/custom-elements.json +3 -3
  2. package/dist/components.d.ts +4 -4
  3. package/dist/components.js +449 -246
  4. package/dist/components.js.map +1 -1
  5. package/dist/util.d.ts +6 -6
  6. package/package.json +1 -1
  7. package/src/preact/MutationAnnotationsContext.tsx +1 -1
  8. package/src/preact/components/csv-download-button.tsx +22 -14
  9. package/src/preact/components/features-over-time-grid.tsx +189 -43
  10. package/src/preact/components/mutations-over-time-mutations-filter.stories.tsx +1 -1
  11. package/src/preact/components/mutations-over-time-mutations-filter.tsx +1 -1
  12. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay/aminoAcidMutationsOverTimePage1.json +52 -0
  13. package/src/preact/mutationsOverTime/__mockData__/byWeek/mutationsOverTimePage1.json +76 -0
  14. package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mockDefaultMutationsOverTimeWithFilter.json +43 -0
  15. package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mutationsOverTimePage1.json +126 -0
  16. package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mutationsOverTimePage2.json +116 -0
  17. package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mutationsOverTimePageSize20.json +216 -0
  18. package/src/preact/mutationsOverTime/getFilteredMutationCodes.spec.ts +236 -0
  19. package/src/preact/mutationsOverTime/{getFilteredMutationsOverTimeData.ts → getFilteredMutationCodes.ts} +29 -44
  20. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +128 -23
  21. package/src/preact/mutationsOverTime/mutations-over-time.tsx +139 -74
  22. package/src/preact/mutationsOverTime/useMutationsOverTimePageData.ts +111 -0
  23. package/src/preact/shared/tanstackTable/pagination-context.tsx +5 -2
  24. package/src/preact/shared/tanstackTable/pagination.tsx +11 -9
  25. package/src/preact/shared/tanstackTable/tanstackTable.tsx +7 -4
  26. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +1 -1
  27. package/src/query/queryMutationsOverTime.spec.ts +187 -662
  28. package/src/query/queryMutationsOverTime.ts +46 -33
  29. package/src/utils/useControlledState.ts +15 -0
  30. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +78 -22
  31. package/standalone-bundle/dashboard-components.js +6872 -6690
  32. package/standalone-bundle/dashboard-components.js.map +1 -1
  33. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay/aminoAcidMutationsOverTime.json +0 -5496
  34. package/src/preact/mutationsOverTime/__mockData__/byWeek/mutationsOverTime.json +0 -7100
  35. package/src/preact/mutationsOverTime/__mockData__/defaultMockData/mutationsOverTime.json +0 -12646
  36. package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +0 -417
@@ -1,13 +1,14 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import { queryMutationsOverTimeData } from './queryMutationsOverTime';
3
+ import { queryMutationsOverTimeMetadata, queryMutationsOverTimePage } from './queryMutationsOverTime';
4
4
  import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../vitest.setup';
5
+ import { yearMonthDay } from '../utils/temporalTestHelpers';
5
6
 
6
- describe('queryMutationsOverTimeNewEndpoint', () => {
7
- it('should fetch for a filter without date and sort by mutation and date', async () => {
8
- const lapisFilter = { field1: 'value1', field2: 'value2' };
9
- const dateField = 'dateField';
7
+ const lapisFilter = { field1: 'value1', field2: 'value2' };
8
+ const dateField = 'dateField';
10
9
 
10
+ describe('queryMutationsOverTimeMetadata', () => {
11
+ it('should fetch overall mutation data and sort by mutation code', async () => {
11
12
  lapisRequestMocks.aggregated(
12
13
  { ...lapisFilter, fields: [dateField] },
13
14
  {
@@ -33,54 +34,8 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
33
34
  ],
34
35
  'nucleotide',
35
36
  );
36
- const dateRanges = [
37
- {
38
- dateFrom: '2023-01-01',
39
- dateTo: '2023-01-01',
40
- },
41
- {
42
- dateFrom: '2023-01-02',
43
- dateTo: '2023-01-02',
44
- },
45
- {
46
- dateFrom: '2023-01-03',
47
- dateTo: '2023-01-03',
48
- },
49
- ];
50
- lapisRequestMocks.mutationsOverTime(
51
- [
52
- {
53
- body: {
54
- filters: lapisFilter,
55
- dateRanges,
56
- includeMutations: ['otherSequenceName:G234C', 'sequenceName:A123T'],
57
- dateField,
58
- },
59
- response: {
60
- data: {
61
- data: [
62
- [
63
- { count: 4, coverage: 10 },
64
- { count: 0, coverage: 10 },
65
- { count: 0, coverage: 10 },
66
- ],
67
- [
68
- { count: 1, coverage: 10 },
69
- { count: 2, coverage: 10 },
70
- { count: 3, coverage: 10 },
71
- ],
72
- ],
73
- dateRanges,
74
- mutations: ['otherSequenceName:G234C', 'sequenceName:A123T'],
75
- totalCountsByDateRange: [11, 12, 13],
76
- },
77
- },
78
- },
79
- ],
80
- 'nucleotide',
81
- );
82
37
 
83
- const { mutationOverTimeData, overallMutationData } = await queryMutationsOverTimeData(
38
+ const { overallMutationData, requestedDateRanges } = await queryMutationsOverTimeMetadata(
84
39
  lapisFilter,
85
40
  'nucleotide',
86
41
  DUMMY_LAPIS_URL,
@@ -88,29 +43,6 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
88
43
  'day',
89
44
  );
90
45
 
91
- const expectedData = [
92
- [
93
- { type: 'valueWithCoverage', count: 4, coverage: 10, totalCount: 11 },
94
- { type: 'valueWithCoverage', count: 0, coverage: 10, totalCount: 12 },
95
- { type: 'valueWithCoverage', count: 0, coverage: 10, totalCount: 13 },
96
- ],
97
- [
98
- { type: 'valueWithCoverage', count: 1, coverage: 10, totalCount: 11 },
99
- { type: 'valueWithCoverage', count: 2, coverage: 10, totalCount: 12 },
100
- { type: 'valueWithCoverage', count: 3, coverage: 10, totalCount: 13 },
101
- ],
102
- ];
103
- expect(mutationOverTimeData.getAsArray()).to.deep.equal(expectedData);
104
-
105
- const sequences = mutationOverTimeData.getFirstAxisKeys();
106
- expect(sequences[0].code).toBe('otherSequenceName:G234C');
107
- expect(sequences[1].code).toBe('sequenceName:A123T');
108
-
109
- const dates = mutationOverTimeData.getSecondAxisKeys();
110
- expect(dates[0].dateString).toBe('2023-01-01');
111
- expect(dates[1].dateString).toBe('2023-01-02');
112
- expect(dates[2].dateString).toBe('2023-01-03');
113
-
114
46
  expect(overallMutationData).to.deep.equal([
115
47
  {
116
48
  type: 'substitution',
@@ -139,124 +71,51 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
139
71
  proportion: 0.21,
140
72
  },
141
73
  ]);
142
- });
143
74
 
144
- it('should fetch for dates with no mutations', async () => {
145
- const lapisFilter = { field1: 'value1', field2: 'value2' };
146
- const dateField = 'dateField';
75
+ expect(requestedDateRanges.map((d) => d.dateString)).to.deep.equal(['2023-01-01', '2023-01-02', '2023-01-03']);
76
+ });
147
77
 
78
+ it('should expand date ranges to cover first and last day of the granularity', async () => {
148
79
  lapisRequestMocks.aggregated(
149
80
  { ...lapisFilter, fields: [dateField] },
150
81
  {
151
82
  data: [
152
- { count: 1, [dateField]: '2023-01-01' },
153
- { count: 2, [dateField]: '2023-01-03' },
83
+ { count: 1, [dateField]: '2023-01-05' },
84
+ { count: 2, [dateField]: '2023-02-15' },
154
85
  ],
155
86
  },
156
87
  );
157
-
158
88
  lapisRequestMocks.multipleMutations(
159
89
  [
160
90
  {
161
91
  body: {
162
92
  ...lapisFilter,
163
93
  dateFieldFrom: '2023-01-01',
164
- dateFieldTo: '2023-01-03',
94
+ dateFieldTo: '2023-02-28',
165
95
  minProportion: 0.001,
166
96
  },
167
- response: {
168
- data: [getSomeTestMutation(0.2, 4), getSomeOtherTestMutation(0.4, 4)],
169
- },
170
- },
171
- ],
172
- 'nucleotide',
173
- );
174
-
175
- const dateRanges = [
176
- {
177
- dateFrom: '2023-01-01',
178
- dateTo: '2023-01-01',
179
- },
180
- {
181
- dateFrom: '2023-01-02',
182
- dateTo: '2023-01-02',
183
- },
184
- {
185
- dateFrom: '2023-01-03',
186
- dateTo: '2023-01-03',
187
- },
188
- ];
189
-
190
- lapisRequestMocks.mutationsOverTime(
191
- [
192
- {
193
- body: {
194
- filters: lapisFilter,
195
- dateRanges,
196
- includeMutations: ['otherSequenceName:G234C', 'sequenceName:A123T'],
197
- dateField,
198
- },
199
- response: {
200
- data: {
201
- data: [
202
- [
203
- { count: 4, coverage: 10 },
204
- { count: 0, coverage: 10 },
205
- { count: 0, coverage: 10 },
206
- ],
207
- [
208
- { count: 1, coverage: 10 },
209
- { count: 0, coverage: 10 },
210
- { count: 3, coverage: 10 },
211
- ],
212
- ],
213
- dateRanges,
214
- mutations: ['otherSequenceName:G234C', 'sequenceName:A123T'],
215
- totalCountsByDateRange: [11, 0, 13],
216
- },
217
- },
97
+ response: { data: [getSomeTestMutation(0.21, 6)] },
218
98
  },
219
99
  ],
220
100
  'nucleotide',
221
101
  );
222
102
 
223
- const { mutationOverTimeData } = await queryMutationsOverTimeData(
103
+ const { requestedDateRanges } = await queryMutationsOverTimeMetadata(
224
104
  lapisFilter,
225
105
  'nucleotide',
226
106
  DUMMY_LAPIS_URL,
227
107
  dateField,
228
- 'day',
108
+ 'month',
229
109
  );
230
110
 
231
- expect(mutationOverTimeData.getAsArray()).to.deep.equal([
232
- [
233
- { type: 'valueWithCoverage', count: 4, coverage: 10, totalCount: 11 },
234
- null,
235
- { type: 'valueWithCoverage', count: 0, coverage: 10, totalCount: 13 },
236
- ],
237
- [
238
- { type: 'valueWithCoverage', count: 1, coverage: 10, totalCount: 11 },
239
- null,
240
- { type: 'valueWithCoverage', count: 3, coverage: 10, totalCount: 13 },
241
- ],
242
- ]);
243
-
244
- const sequences = mutationOverTimeData.getFirstAxisKeys();
245
- expect(sequences[0].code).toBe('otherSequenceName:G234C');
246
- expect(sequences[1].code).toBe('sequenceName:A123T');
247
-
248
- const dates = mutationOverTimeData.getSecondAxisKeys();
249
- expect(dates[0].dateString).toBe('2023-01-01');
250
- expect(dates[1].dateString).toBe('2023-01-02');
251
- expect(dates[2].dateString).toBe('2023-01-03');
111
+ expect(requestedDateRanges.map((d) => d.dateString)).to.deep.equal(['2023-01', '2023-02']);
252
112
  });
253
113
 
254
- it('should return empty map when no mutations are found', async () => {
255
- const lapisFilter = { field1: 'value1', field2: 'value2' };
256
- const dateField = 'dateField';
114
+ it('should restrict date range when dateFrom is in the filter', async () => {
115
+ const filterWithDateFrom = { ...lapisFilter, [`${dateField}From`]: '2023-01-02' };
257
116
 
258
117
  lapisRequestMocks.aggregated(
259
- { ...lapisFilter, fields: [dateField] },
118
+ { ...filterWithDateFrom, fields: [dateField] },
260
119
  {
261
120
  data: [
262
121
  { count: 1, [dateField]: '2023-01-01' },
@@ -264,84 +123,37 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
264
123
  ],
265
124
  },
266
125
  );
267
-
268
126
  lapisRequestMocks.multipleMutations(
269
127
  [
270
128
  {
271
129
  body: {
272
- ...lapisFilter,
273
- dateFieldFrom: '2023-01-01',
130
+ ...filterWithDateFrom,
131
+ dateFieldFrom: '2023-01-02',
274
132
  dateFieldTo: '2023-01-03',
275
133
  minProportion: 0.001,
276
134
  },
277
- response: {
278
- data: [],
279
- },
280
- },
281
- ],
282
- 'nucleotide',
283
- );
284
-
285
- const dateRanges = [
286
- {
287
- dateFrom: '2023-01-01',
288
- dateTo: '2023-01-01',
289
- },
290
- {
291
- dateFrom: '2023-01-02',
292
- dateTo: '2023-01-02',
293
- },
294
- {
295
- dateFrom: '2023-01-03',
296
- dateTo: '2023-01-03',
297
- },
298
- ];
299
-
300
- lapisRequestMocks.mutationsOverTime(
301
- [
302
- {
303
- body: {
304
- filters: lapisFilter,
305
- dateRanges,
306
- includeMutations: [],
307
- dateField,
308
- },
309
- response: {
310
- data: {
311
- data: [],
312
- dateRanges,
313
- mutations: [],
314
- totalCountsByDateRange: [],
315
- },
316
- },
135
+ response: { data: [getSomeTestMutation(0.25, 5)] },
317
136
  },
318
137
  ],
319
138
  'nucleotide',
320
139
  );
321
140
 
322
- const { mutationOverTimeData } = await queryMutationsOverTimeData(
323
- lapisFilter,
141
+ const { requestedDateRanges } = await queryMutationsOverTimeMetadata(
142
+ filterWithDateFrom,
324
143
  'nucleotide',
325
144
  DUMMY_LAPIS_URL,
326
145
  dateField,
327
146
  'day',
328
147
  );
329
148
 
330
- expect(mutationOverTimeData.getAsArray()).to.deep.equal([]);
331
- expect(mutationOverTimeData.getFirstAxisKeys()).to.deep.equal([]);
332
- const dates = mutationOverTimeData.getSecondAxisKeys();
333
- expect(dates.length).toBe(3);
334
- expect(dates[0].dateString).toBe('2023-01-01');
335
- expect(dates[1].dateString).toBe('2023-01-02');
336
- expect(dates[2].dateString).toBe('2023-01-03');
149
+ expect(requestedDateRanges.map((d) => d.dateString)).to.deep.equal(['2023-01-02', '2023-01-03']);
337
150
  });
338
151
 
339
- it('should use dateFrom from filter', async () => {
340
- const dateField = 'dateField';
341
- const lapisFilter = { field1: 'value1', field2: 'value2', [`${dateField}From`]: '2023-01-02' };
152
+ it('should restrict date range when dateTo is in the filter', async () => {
153
+ const filterWithDateTo = { ...lapisFilter, [`${dateField}To`]: '2023-01-02' };
342
154
 
343
155
  lapisRequestMocks.aggregated(
344
- { ...lapisFilter, fields: [dateField] },
156
+ { ...filterWithDateTo, fields: [dateField] },
345
157
  {
346
158
  data: [
347
159
  { count: 1, [dateField]: '2023-01-01' },
@@ -349,91 +161,37 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
349
161
  ],
350
162
  },
351
163
  );
352
-
353
164
  lapisRequestMocks.multipleMutations(
354
165
  [
355
166
  {
356
167
  body: {
357
- ...lapisFilter,
358
- dateFieldFrom: '2023-01-02',
359
- dateFieldTo: '2023-01-03',
168
+ ...filterWithDateTo,
169
+ dateFieldFrom: '2023-01-01',
170
+ dateFieldTo: '2023-01-02',
360
171
  minProportion: 0.001,
361
172
  },
362
- response: {
363
- data: [getSomeTestMutation(0.25, 5)],
364
- },
365
- },
366
- ],
367
- 'nucleotide',
368
- );
369
-
370
- const dateRanges = [
371
- {
372
- dateFrom: '2023-01-02',
373
- dateTo: '2023-01-02',
374
- },
375
- {
376
- dateFrom: '2023-01-03',
377
- dateTo: '2023-01-03',
378
- },
379
- ];
380
-
381
- lapisRequestMocks.mutationsOverTime(
382
- [
383
- {
384
- body: {
385
- filters: lapisFilter,
386
- dateRanges,
387
- includeMutations: ['sequenceName:A123T'],
388
- dateField,
389
- },
390
- response: {
391
- data: {
392
- data: [
393
- [
394
- { count: 2, coverage: 10 },
395
- { count: 3, coverage: 10 },
396
- ],
397
- ],
398
- dateRanges,
399
- mutations: ['sequenceName:A123T'],
400
- totalCountsByDateRange: [11, 12],
401
- },
402
- },
173
+ response: { data: [getSomeTestMutation(0.15, 3)] },
403
174
  },
404
175
  ],
405
176
  'nucleotide',
406
177
  );
407
178
 
408
- const { mutationOverTimeData } = await queryMutationsOverTimeData(
409
- lapisFilter,
179
+ const { requestedDateRanges } = await queryMutationsOverTimeMetadata(
180
+ filterWithDateTo,
410
181
  'nucleotide',
411
182
  DUMMY_LAPIS_URL,
412
183
  dateField,
413
184
  'day',
414
185
  );
415
186
 
416
- expect(mutationOverTimeData.getAsArray()).to.deep.equal([
417
- [
418
- { type: 'valueWithCoverage', count: 2, coverage: 10, totalCount: 11 },
419
- { type: 'valueWithCoverage', count: 3, coverage: 10, totalCount: 12 },
420
- ],
421
- ]);
422
-
423
- const sequences = mutationOverTimeData.getFirstAxisKeys();
424
- expect(sequences[0].code).toBe('sequenceName:A123T');
425
-
426
- const dates = mutationOverTimeData.getSecondAxisKeys();
427
- expect(dates[0].dateString).toBe('2023-01-02');
428
- expect(dates[1].dateString).toBe('2023-01-03');
187
+ expect(requestedDateRanges.map((d) => d.dateString)).to.deep.equal(['2023-01-01', '2023-01-02']);
429
188
  });
430
189
 
431
- it('should use dateTo from filter', async () => {
432
- const dateField = 'dateField';
433
- const lapisFilter = { field1: 'value1', field2: 'value2', [`${dateField}To`]: '2023-01-02' };
190
+ it('should restrict date range when an exact date is in the filter', async () => {
191
+ const filterWithDate = { ...lapisFilter, [dateField]: '2023-01-02' };
434
192
 
435
193
  lapisRequestMocks.aggregated(
436
- { ...lapisFilter, fields: [dateField] },
194
+ { ...filterWithDate, fields: [dateField] },
437
195
  {
438
196
  data: [
439
197
  { count: 1, [dateField]: '2023-01-01' },
@@ -441,210 +199,118 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
441
199
  ],
442
200
  },
443
201
  );
444
-
445
202
  lapisRequestMocks.multipleMutations(
446
203
  [
447
204
  {
448
205
  body: {
449
- ...lapisFilter,
450
- dateFieldFrom: '2023-01-01',
206
+ ...filterWithDate,
207
+ dateFieldFrom: '2023-01-02',
451
208
  dateFieldTo: '2023-01-02',
452
209
  minProportion: 0.001,
453
210
  },
454
- response: {
455
- data: [getSomeTestMutation(0.15, 3)],
456
- },
211
+ response: { data: [getSomeTestMutation(0.2, 2)] },
457
212
  },
458
213
  ],
459
214
  'nucleotide',
460
215
  );
461
216
 
462
- const dateRanges = [
463
- {
464
- dateFrom: '2023-01-01',
465
- dateTo: '2023-01-01',
466
- },
467
- {
468
- dateFrom: '2023-01-02',
469
- dateTo: '2023-01-02',
470
- },
471
- ];
472
-
473
- lapisRequestMocks.mutationsOverTime(
474
- [
475
- {
476
- body: {
477
- filters: lapisFilter,
478
- dateRanges,
479
- includeMutations: ['sequenceName:A123T'],
480
- dateField,
481
- },
482
- response: {
483
- data: {
484
- data: [
485
- [
486
- { count: 1, coverage: 10 },
487
- { count: 2, coverage: 10 },
488
- ],
489
- ],
490
- dateRanges,
491
- mutations: ['sequenceName:A123T'],
492
- totalCountsByDateRange: [11, 12],
493
- },
494
- },
495
- },
496
- ],
217
+ const { requestedDateRanges } = await queryMutationsOverTimeMetadata(
218
+ filterWithDate,
497
219
  'nucleotide',
220
+ DUMMY_LAPIS_URL,
221
+ dateField,
222
+ 'day',
498
223
  );
499
224
 
500
- const { mutationOverTimeData } = await queryMutationsOverTimeData(
225
+ expect(requestedDateRanges.map((d) => d.dateString)).to.deep.equal(['2023-01-02']);
226
+ });
227
+
228
+ it('should return empty data when there are no dates in the dataset', async () => {
229
+ lapisRequestMocks.aggregated({ ...lapisFilter, fields: [dateField] }, { data: [] });
230
+
231
+ const { overallMutationData, requestedDateRanges } = await queryMutationsOverTimeMetadata(
501
232
  lapisFilter,
502
233
  'nucleotide',
503
234
  DUMMY_LAPIS_URL,
504
235
  dateField,
505
- 'day',
236
+ 'month',
506
237
  );
507
238
 
508
- expect(mutationOverTimeData.getAsArray()).to.deep.equal([
509
- [
510
- { type: 'valueWithCoverage', count: 1, coverage: 10, totalCount: 11 },
511
- { type: 'valueWithCoverage', count: 2, coverage: 10, totalCount: 12 },
512
- ],
513
- ]);
514
-
515
- const sequences = mutationOverTimeData.getFirstAxisKeys();
516
- expect(sequences[0].code).toBe('sequenceName:A123T');
517
-
518
- const dates = mutationOverTimeData.getSecondAxisKeys();
519
- expect(dates[0].dateString).toBe('2023-01-01');
520
- expect(dates[1].dateString).toBe('2023-01-02');
239
+ expect(overallMutationData).to.deep.equal([]);
240
+ expect(requestedDateRanges).to.deep.equal([]);
521
241
  });
522
242
 
523
- it('should use date from filter', async () => {
524
- const dateField = 'dateField';
525
- const lapisFilter = { field1: 'value1', field2: 'value2', [dateField]: '2023-01-02' };
526
-
243
+ it('should filter overall mutations by includeMutations', async () => {
527
244
  lapisRequestMocks.aggregated(
528
245
  { ...lapisFilter, fields: [dateField] },
529
246
  {
530
247
  data: [
531
- { count: 1, [dateField]: '2023-01-01' },
532
- { count: 2, [dateField]: '2023-01-03' },
248
+ { count: 1, [dateField]: '2023-01-05' },
249
+ { count: 2, [dateField]: '2023-02-15' },
533
250
  ],
534
251
  },
535
252
  );
536
-
537
253
  lapisRequestMocks.multipleMutations(
538
254
  [
539
255
  {
540
256
  body: {
541
257
  ...lapisFilter,
542
- dateFieldFrom: '2023-01-02',
543
- dateFieldTo: '2023-01-02',
258
+ dateFieldFrom: '2023-01-01',
259
+ dateFieldTo: '2023-02-28',
544
260
  minProportion: 0.001,
545
261
  },
546
- response: { data: [getSomeTestMutation(0.2, 2)] },
547
- },
548
- ],
549
- 'nucleotide',
550
- );
551
-
552
- const dateRanges = [
553
- {
554
- dateFrom: '2023-01-02',
555
- dateTo: '2023-01-02',
556
- },
557
- ];
558
-
559
- lapisRequestMocks.mutationsOverTime(
560
- [
561
- {
562
- body: {
563
- filters: lapisFilter,
564
- dateRanges,
565
- includeMutations: ['sequenceName:A123T'],
566
- dateField,
567
- },
568
262
  response: {
569
- data: {
570
- data: [[{ count: 2, coverage: 10 }]],
571
- dateRanges,
572
- mutations: ['sequenceName:A123T'],
573
- totalCountsByDateRange: [11],
574
- },
263
+ data: [getSomeTestMutation(0.21, 6), getSomeOtherTestMutation(0.22, 4)],
575
264
  },
576
265
  },
577
266
  ],
578
267
  'nucleotide',
579
268
  );
580
269
 
581
- const { mutationOverTimeData } = await queryMutationsOverTimeData(
270
+ const { overallMutationData } = await queryMutationsOverTimeMetadata(
582
271
  lapisFilter,
583
272
  'nucleotide',
584
273
  DUMMY_LAPIS_URL,
585
274
  dateField,
586
- 'day',
275
+ 'month',
276
+ ['otherSequenceName:G234C', 'A122T'],
587
277
  );
588
278
 
589
- expect(mutationOverTimeData.getAsArray()).to.deep.equal([
590
- [{ type: 'valueWithCoverage', count: 2, coverage: 10, totalCount: 11 }],
591
- ]);
592
-
593
- const sequences = mutationOverTimeData.getFirstAxisKeys();
594
- expect(sequences[0].code).toBe('sequenceName:A123T');
595
-
596
- const dates = mutationOverTimeData.getSecondAxisKeys();
597
- expect(dates[0].dateString).toBe('2023-01-02');
279
+ // Only otherSequenceName:G234C was in the dataset; A122T gets count/proportion 0
280
+ expect(overallMutationData.map((m) => m.mutation.code)).to.deep.equal(['A122T', 'otherSequenceName:G234C']);
281
+ expect(overallMutationData.find((m) => m.mutation.code === 'A122T')?.proportion).toBe(0);
282
+ expect(overallMutationData.find((m) => m.mutation.code === 'otherSequenceName:G234C')?.proportion).toBe(0.22);
598
283
  });
284
+ });
599
285
 
600
- it('should fetch data including the first and last day of the granularity', async () => {
601
- const lapisFilter = { field1: 'value1', field2: 'value2' };
602
- const dateField = 'dateField';
286
+ describe('queryMutationsOverTimePage', () => {
287
+ const threeDays = [yearMonthDay('2023-01-01'), yearMonthDay('2023-01-02'), yearMonthDay('2023-01-03')];
288
+ const twoDays = [yearMonthDay('2023-01-01'), yearMonthDay('2023-01-02')];
603
289
 
604
- lapisRequestMocks.aggregated(
605
- { ...lapisFilter, fields: [dateField] },
606
- {
607
- data: [
608
- { count: 1, [dateField]: '2023-01-05' },
609
- { count: 2, [dateField]: '2023-02-15' },
610
- ],
611
- },
612
- );
290
+ const threeDayDateRanges = threeDays.map((d) => ({
291
+ dateFrom: d.firstDay.toString(),
292
+ dateTo: d.lastDay.toString(),
293
+ }));
294
+ const twoDayDateRanges = twoDays.map((d) => ({ dateFrom: d.firstDay.toString(), dateTo: d.lastDay.toString() }));
613
295
 
614
- lapisRequestMocks.multipleMutations(
615
- [
616
- {
617
- body: {
618
- ...lapisFilter,
619
- dateFieldFrom: '2023-01-01',
620
- dateFieldTo: '2023-02-28',
621
- minProportion: 0.001,
622
- },
623
- response: {
624
- data: [getSomeTestMutation(0.21, 6), getSomeOtherTestMutation(0.22, 4)],
625
- },
626
- },
627
- ],
296
+ function callQueryMutationsOverTimePage(requestedDateRanges: typeof threeDays, includeMutations: string[]) {
297
+ return queryMutationsOverTimePage(
298
+ lapisFilter,
299
+ DUMMY_LAPIS_URL,
300
+ dateField,
628
301
  'nucleotide',
302
+ requestedDateRanges,
303
+ includeMutations,
629
304
  );
305
+ }
630
306
 
631
- const dateRanges = [
632
- {
633
- dateFrom: '2023-01-01',
634
- dateTo: '2023-01-31',
635
- },
636
- {
637
- dateFrom: '2023-02-01',
638
- dateTo: '2023-02-28',
639
- },
640
- ];
641
-
307
+ it('should build the data map with valueWithCoverage entries', async () => {
642
308
  lapisRequestMocks.mutationsOverTime(
643
309
  [
644
310
  {
645
311
  body: {
646
312
  filters: lapisFilter,
647
- dateRanges,
313
+ dateRanges: threeDayDateRanges,
648
314
  includeMutations: ['otherSequenceName:G234C', 'sequenceName:A123T'],
649
315
  dateField,
650
316
  },
@@ -652,17 +318,19 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
652
318
  data: {
653
319
  data: [
654
320
  [
655
- { count: 2, coverage: 10 },
656
- { count: 3, coverage: 10 },
321
+ { count: 4, coverage: 10 },
322
+ { count: 0, coverage: 10 },
323
+ { count: 0, coverage: 10 },
657
324
  ],
658
325
  [
659
- { count: 4, coverage: 10 },
660
- { count: 5, coverage: 10 },
326
+ { count: 1, coverage: 10 },
327
+ { count: 2, coverage: 10 },
328
+ { count: 3, coverage: 10 },
661
329
  ],
662
330
  ],
663
- dateRanges,
331
+ dateRanges: threeDayDateRanges,
664
332
  mutations: ['otherSequenceName:G234C', 'sequenceName:A123T'],
665
- totalCountsByDateRange: [11, 12],
333
+ totalCountsByDateRange: [11, 12, 13],
666
334
  },
667
335
  },
668
336
  },
@@ -670,150 +338,61 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
670
338
  'nucleotide',
671
339
  );
672
340
 
673
- const { mutationOverTimeData } = await queryMutationsOverTimeData(
674
- lapisFilter,
675
- 'nucleotide',
676
- DUMMY_LAPIS_URL,
677
- dateField,
678
- 'month',
679
- );
341
+ const result = await callQueryMutationsOverTimePage(threeDays, [
342
+ 'otherSequenceName:G234C',
343
+ 'sequenceName:A123T',
344
+ ]);
680
345
 
681
- expect(mutationOverTimeData.getAsArray()).to.deep.equal([
682
- [
683
- { type: 'valueWithCoverage', count: 2, coverage: 10, totalCount: 11 },
684
- { type: 'valueWithCoverage', count: 3, coverage: 10, totalCount: 12 },
685
- ],
346
+ expect(result.getAsArray()).to.deep.equal([
686
347
  [
687
348
  { type: 'valueWithCoverage', count: 4, coverage: 10, totalCount: 11 },
688
- { type: 'valueWithCoverage', count: 5, coverage: 10, totalCount: 12 },
349
+ { type: 'valueWithCoverage', count: 0, coverage: 10, totalCount: 12 },
350
+ { type: 'valueWithCoverage', count: 0, coverage: 10, totalCount: 13 },
689
351
  ],
690
- ]);
691
-
692
- const sequences = mutationOverTimeData.getFirstAxisKeys();
693
- expect(sequences[0].code).toBe('otherSequenceName:G234C');
694
- expect(sequences[1].code).toBe('sequenceName:A123T');
695
-
696
- const dates = mutationOverTimeData.getSecondAxisKeys();
697
- expect(dates[0].dateString).toBe('2023-01');
698
- expect(dates[1].dateString).toBe('2023-02');
699
- });
700
-
701
- it('should return empty data when there are no dates in filter', async () => {
702
- const lapisFilter = { field1: 'value1', field2: 'value2' };
703
- const dateField = 'dateField';
704
-
705
- lapisRequestMocks.aggregated(
706
- { ...lapisFilter, fields: [dateField] },
707
- {
708
- data: [],
709
- },
710
- );
711
-
712
- lapisRequestMocks.mutationsOverTime(
713
352
  [
714
- {
715
- body: {
716
- filters: lapisFilter,
717
- dateRanges: [],
718
- includeMutations: [],
719
- dateField,
720
- },
721
- response: {
722
- data: {
723
- data: [],
724
- dateRanges: [],
725
- mutations: [],
726
- totalCountsByDateRange: [],
727
- },
728
- },
729
- },
353
+ { type: 'valueWithCoverage', count: 1, coverage: 10, totalCount: 11 },
354
+ { type: 'valueWithCoverage', count: 2, coverage: 10, totalCount: 12 },
355
+ { type: 'valueWithCoverage', count: 3, coverage: 10, totalCount: 13 },
730
356
  ],
731
- 'nucleotide',
732
- );
733
-
734
- const { mutationOverTimeData } = await queryMutationsOverTimeData(
735
- lapisFilter,
736
- 'nucleotide',
737
- DUMMY_LAPIS_URL,
738
- dateField,
739
- 'month',
740
- );
741
-
742
- expect(mutationOverTimeData.getAsArray()).to.deep.equal([]);
743
-
744
- const sequences = mutationOverTimeData.getFirstAxisKeys();
745
- expect(sequences.length).toBe(0);
746
-
747
- const dates = mutationOverTimeData.getSecondAxisKeys();
748
- expect(dates.length).toBe(0);
357
+ ]);
358
+ expect(result.getFirstAxisKeys().map((m) => m.code)).to.deep.equal([
359
+ 'otherSequenceName:G234C',
360
+ 'sequenceName:A123T',
361
+ ]);
362
+ expect(result.getSecondAxisKeys().map((d) => d.dateString)).to.deep.equal([
363
+ '2023-01-01',
364
+ '2023-01-02',
365
+ '2023-01-03',
366
+ ]);
749
367
  });
750
368
 
751
- it('should respect the includeMutations parameter', async () => {
752
- const lapisFilter = { field1: 'value1', field2: 'value2' };
753
- const dateField = 'dateField';
754
-
755
- lapisRequestMocks.aggregated(
756
- { ...lapisFilter, fields: [dateField] },
757
- {
758
- data: [
759
- { count: 1, [dateField]: '2023-01-05' },
760
- { count: 2, [dateField]: '2023-02-15' },
761
- ],
762
- },
763
- );
764
-
765
- lapisRequestMocks.multipleMutations(
766
- [
767
- {
768
- body: {
769
- ...lapisFilter,
770
- dateFieldFrom: '2023-01-01',
771
- dateFieldTo: '2023-02-28',
772
- minProportion: 0.001,
773
- },
774
- response: {
775
- data: [getSomeTestMutation(0.21, 6), getSomeOtherTestMutation(0.22, 4)],
776
- },
777
- },
778
- ],
779
- 'nucleotide',
780
- );
781
-
782
- const dateRanges = [
783
- {
784
- dateFrom: '2023-01-01',
785
- dateTo: '2023-01-31',
786
- },
787
- {
788
- dateFrom: '2023-02-01',
789
- dateTo: '2023-02-28',
790
- },
791
- ];
792
-
369
+ it('should set cell to null when totalCount for a date range is zero', async () => {
793
370
  lapisRequestMocks.mutationsOverTime(
794
371
  [
795
372
  {
796
373
  body: {
797
374
  filters: lapisFilter,
798
- dateRanges,
799
- includeMutations: ['A122T', 'otherSequenceName:G234C'],
375
+ dateRanges: threeDayDateRanges,
376
+ includeMutations: ['otherSequenceName:G234C', 'sequenceName:A123T'],
800
377
  dateField,
801
378
  },
802
379
  response: {
803
380
  data: {
804
381
  data: [
805
382
  [
806
- { count: 0, coverage: 0 },
807
- { count: 0, coverage: 0 },
383
+ { count: 4, coverage: 10 },
384
+ { count: 0, coverage: 10 },
385
+ { count: 0, coverage: 10 },
808
386
  ],
809
387
  [
810
- { count: 2, coverage: 10 },
388
+ { count: 1, coverage: 10 },
389
+ { count: 0, coverage: 10 },
811
390
  { count: 3, coverage: 10 },
812
391
  ],
813
392
  ],
814
- dateRanges,
815
- mutations: ['A122T', 'otherSequenceName:G234C'],
816
- totalCountsByDateRange: [11, 12],
393
+ dateRanges: threeDayDateRanges,
394
+ mutations: ['otherSequenceName:G234C', 'sequenceName:A123T'],
395
+ totalCountsByDateRange: [11, 0, 13],
817
396
  },
818
397
  },
819
398
  },
@@ -821,84 +400,33 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
821
400
  'nucleotide',
822
401
  );
823
402
 
824
- const { mutationOverTimeData } = await queryMutationsOverTimeData(
825
- lapisFilter,
826
- 'nucleotide',
827
- DUMMY_LAPIS_URL,
828
- dateField,
829
- 'month',
830
- ['otherSequenceName:G234C', 'A122T'],
831
- );
403
+ const result = await callQueryMutationsOverTimePage(threeDays, [
404
+ 'otherSequenceName:G234C',
405
+ 'sequenceName:A123T',
406
+ ]);
832
407
 
833
- expect(mutationOverTimeData.getAsArray()).to.deep.equal([
408
+ expect(result.getAsArray()).to.deep.equal([
834
409
  [
835
- { type: 'belowThreshold', totalCount: 11 },
836
- { type: 'belowThreshold', totalCount: 12 },
410
+ { type: 'valueWithCoverage', count: 4, coverage: 10, totalCount: 11 },
411
+ null,
412
+ { type: 'valueWithCoverage', count: 0, coverage: 10, totalCount: 13 },
837
413
  ],
838
414
  [
839
- { type: 'valueWithCoverage', count: 2, coverage: 10, totalCount: 11 },
840
- { type: 'valueWithCoverage', count: 3, coverage: 10, totalCount: 12 },
415
+ { type: 'valueWithCoverage', count: 1, coverage: 10, totalCount: 11 },
416
+ null,
417
+ { type: 'valueWithCoverage', count: 3, coverage: 10, totalCount: 13 },
841
418
  ],
842
419
  ]);
843
-
844
- const sequences = mutationOverTimeData.getFirstAxisKeys();
845
- expect(sequences[0].code).toBe('A122T');
846
- expect(sequences[1].code).toBe('otherSequenceName:G234C');
847
-
848
- const dates = mutationOverTimeData.getSecondAxisKeys();
849
- expect(dates[0].dateString).toBe('2023-01');
850
- expect(dates[1].dateString).toBe('2023-02');
851
420
  });
852
421
 
853
- it('should return full mutation codes even if partial includeMutations are given', async () => {
854
- const lapisFilter = { field1: 'value1', field2: 'value2' };
855
- const dateField = 'dateField';
856
-
857
- lapisRequestMocks.aggregated(
858
- { ...lapisFilter, fields: [dateField] },
859
- {
860
- data: [
861
- { count: 1, [dateField]: '2023-01-05' },
862
- { count: 2, [dateField]: '2023-02-15' },
863
- ],
864
- },
865
- );
866
-
867
- lapisRequestMocks.multipleMutations(
868
- [
869
- {
870
- body: {
871
- ...lapisFilter,
872
- dateFieldFrom: '2023-01-01',
873
- dateFieldTo: '2023-02-28',
874
- minProportion: 0.001,
875
- },
876
- response: {
877
- data: [getSomeTestMutation(0.21, 6), getSomeOtherTestMutation(0.22, 4)],
878
- },
879
- },
880
- ],
881
- 'nucleotide',
882
- );
883
-
884
- const dateRanges = [
885
- {
886
- dateFrom: '2023-01-01',
887
- dateTo: '2023-01-31',
888
- },
889
- {
890
- dateFrom: '2023-02-01',
891
- dateTo: '2023-02-28',
892
- },
893
- ];
894
-
422
+ it('should set cell to belowThreshold when coverage is zero', async () => {
895
423
  lapisRequestMocks.mutationsOverTime(
896
424
  [
897
425
  {
898
426
  body: {
899
427
  filters: lapisFilter,
900
- dateRanges,
901
- includeMutations: ['122', 'otherSequenceName:G234C'],
428
+ dateRanges: twoDayDateRanges,
429
+ includeMutations: ['sequenceName:A123T'],
902
430
  dateField,
903
431
  },
904
432
  response: {
@@ -906,15 +434,11 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
906
434
  data: [
907
435
  [
908
436
  { count: 0, coverage: 0 },
909
- { count: 0, coverage: 0 },
910
- ],
911
- [
912
437
  { count: 2, coverage: 10 },
913
- { count: 3, coverage: 10 },
914
438
  ],
915
439
  ],
916
- dateRanges,
917
- mutations: ['A122T', 'otherSequenceName:G234C'],
440
+ dateRanges: twoDayDateRanges,
441
+ mutations: ['sequenceName:A123T'],
918
442
  totalCountsByDateRange: [11, 12],
919
443
  },
920
444
  },
@@ -923,56 +447,57 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
923
447
  'nucleotide',
924
448
  );
925
449
 
926
- const { mutationOverTimeData } = await queryMutationsOverTimeData(
927
- lapisFilter,
928
- 'nucleotide',
929
- DUMMY_LAPIS_URL,
930
- dateField,
931
- 'month',
932
- ['otherSequenceName:G234C', '122'],
933
- );
450
+ const result = await callQueryMutationsOverTimePage(twoDays, ['sequenceName:A123T']);
934
451
 
935
- expect(mutationOverTimeData.getAsArray()).to.deep.equal([
452
+ expect(result.getAsArray()).to.deep.equal([
936
453
  [
937
454
  { type: 'belowThreshold', totalCount: 11 },
938
- { type: 'belowThreshold', totalCount: 12 },
939
- ],
940
- [
941
- { type: 'valueWithCoverage', count: 2, coverage: 10, totalCount: 11 },
942
- { type: 'valueWithCoverage', count: 3, coverage: 10, totalCount: 12 },
455
+ { type: 'valueWithCoverage', count: 2, coverage: 10, totalCount: 12 },
943
456
  ],
944
457
  ]);
458
+ });
945
459
 
946
- const sequences = mutationOverTimeData.getFirstAxisKeys();
947
- expect(sequences[0].code).toBe('A122T');
948
- expect(sequences[1].code).toBe('otherSequenceName:G234C');
460
+ it('should return an empty map with date columns when no mutations are requested', async () => {
461
+ const result = await callQueryMutationsOverTimePage(threeDays, []);
949
462
 
950
- const dates = mutationOverTimeData.getSecondAxisKeys();
951
- expect(dates[0].dateString).toBe('2023-01');
952
- expect(dates[1].dateString).toBe('2023-02');
463
+ expect(result.getAsArray()).to.deep.equal([]);
464
+ expect(result.getFirstAxisKeys()).to.deep.equal([]);
465
+ expect(result.getSecondAxisKeys().map((d) => d.dateString)).to.deep.equal([
466
+ '2023-01-01',
467
+ '2023-01-02',
468
+ '2023-01-03',
469
+ ]);
953
470
  });
954
471
 
955
- function getSomeTestMutation(proportion: number, count: number) {
956
- return {
957
- mutation: 'sequenceName:A123T',
958
- proportion,
959
- count,
960
- sequenceName: 'sequenceName',
961
- mutationFrom: 'A',
962
- mutationTo: 'T',
963
- position: 123,
964
- };
965
- }
472
+ it('should return an empty map when requestedDateRanges is empty', async () => {
473
+ const result = await callQueryMutationsOverTimePage([], ['sequenceName:A123T']);
966
474
 
967
- function getSomeOtherTestMutation(proportion: number, count: number) {
968
- return {
969
- mutation: 'otherSequenceName:G234C',
970
- proportion,
971
- count,
972
- sequenceName: 'otherSequenceName',
973
- mutationFrom: 'G',
974
- mutationTo: 'C',
975
- position: 234,
976
- };
977
- }
475
+ expect(result.getAsArray()).to.deep.equal([]);
476
+ expect(result.getFirstAxisKeys()).to.deep.equal([]);
477
+ expect(result.getSecondAxisKeys()).to.deep.equal([]);
478
+ });
978
479
  });
480
+
481
+ function getSomeTestMutation(proportion: number, count: number) {
482
+ return {
483
+ mutation: 'sequenceName:A123T',
484
+ proportion,
485
+ count,
486
+ sequenceName: 'sequenceName',
487
+ mutationFrom: 'A',
488
+ mutationTo: 'T',
489
+ position: 123,
490
+ };
491
+ }
492
+
493
+ function getSomeOtherTestMutation(proportion: number, count: number) {
494
+ return {
495
+ mutation: 'otherSequenceName:G234C',
496
+ proportion,
497
+ count,
498
+ sequenceName: 'otherSequenceName',
499
+ mutationFrom: 'G',
500
+ mutationTo: 'C',
501
+ position: 234,
502
+ };
503
+ }