@genspectrum/dashboard-components 0.6.2 → 0.6.4

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 (42) hide show
  1. package/custom-elements.json +220 -0
  2. package/dist/dashboard-components.js +675 -178
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +70 -2
  5. package/dist/style.css +10 -4
  6. package/package.json +3 -1
  7. package/src/constants.ts +1 -1
  8. package/src/lapisApi/lapisTypes.ts +1 -0
  9. package/src/operator/FillMissingOperator.spec.ts +3 -1
  10. package/src/operator/FillMissingOperator.ts +4 -2
  11. package/src/preact/components/tooltip.stories.tsx +54 -0
  12. package/src/preact/components/tooltip.tsx +31 -0
  13. package/src/preact/mutationComparison/queryMutationData.ts +12 -4
  14. package/src/preact/mutationsOverTime/__mockData__/aggregated_date.json +642 -0
  15. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_01.json +1747 -0
  16. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_02.json +1774 -0
  17. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_03.json +1819 -0
  18. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_04.json +1864 -0
  19. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_05.json +1927 -0
  20. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_06.json +1864 -0
  21. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_07.json +9 -0
  22. package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +98 -0
  23. package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +66 -0
  24. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +127 -0
  25. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +206 -0
  26. package/src/preact/mutationsOverTime/mutations-over-time.tsx +170 -0
  27. package/src/preact/numberSequencesOverTime/getNumberOfSequencesOverTimeTableData.ts +1 -1
  28. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +1 -0
  29. package/src/preact/shared/table/formatProportion.ts +2 -2
  30. package/src/query/queryAggregatedDataOverTime.ts +8 -33
  31. package/src/query/queryMutationsOverTime.spec.ts +378 -0
  32. package/src/query/queryMutationsOverTime.ts +179 -0
  33. package/src/query/queryNumberOfSequencesOverTime.ts +0 -1
  34. package/src/query/queryRelativeGrowthAdvantage.ts +3 -3
  35. package/src/utils/Map2d.ts +75 -0
  36. package/src/utils/map2d.spec.ts +94 -0
  37. package/src/utils/mutations.ts +5 -1
  38. package/src/utils/temporal.spec.ts +5 -0
  39. package/src/utils/temporal.ts +88 -5
  40. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +225 -0
  41. package/src/web-components/visualization/gs-mutations-over-time.tsx +112 -0
  42. package/src/web-components/visualization/index.ts +1 -0
@@ -33,6 +33,7 @@ export default {
33
33
  height: { control: 'text' },
34
34
  pageSize: { control: 'object' },
35
35
  yAxisMaxConfig: { control: 'object' },
36
+ lapisDateField: { control: 'text' },
36
37
  },
37
38
  };
38
39
 
@@ -1,3 +1,3 @@
1
- export const formatProportion = (proportion: number) => {
2
- return `${(proportion * 100).toFixed(2)}%`;
1
+ export const formatProportion = (proportion: number, digits: number = 2) => {
2
+ return `${(proportion * 100).toFixed(digits)}%`;
3
3
  };
@@ -7,11 +7,11 @@ import { SlidingOperator } from '../operator/SlidingOperator';
7
7
  import { SortOperator } from '../operator/SortOperator';
8
8
  import type { LapisFilter, TemporalGranularity } from '../types';
9
9
  import {
10
- compareTemporal,
10
+ dateRangeCompare,
11
11
  generateAllInRange,
12
12
  getMinMaxTemporal,
13
+ parseDateStringToTemporal,
13
14
  type Temporal,
14
- TemporalCache,
15
15
  } from '../utils/temporal';
16
16
 
17
17
  export function queryAggregatedDataOverTime<LapisDateField extends string>(
@@ -36,41 +36,16 @@ export function queryAggregatedDataOverTime<LapisDateField extends string>(
36
36
  return smoothingWindow >= 1 ? new SlidingOperator(sortData, smoothingWindow, averageSmoothing) : sortData;
37
37
  }
38
38
 
39
- function mapDateToGranularityRange(d: { date: string | null; count: number }, granularity: TemporalGranularity) {
40
- let dateRange: Temporal | null = null;
41
- if (d.date !== null) {
42
- const date = TemporalCache.getInstance().getYearMonthDay(d.date);
43
- switch (granularity) {
44
- case 'day':
45
- dateRange = date;
46
- break;
47
- case 'week':
48
- dateRange = date.week;
49
- break;
50
- case 'month':
51
- dateRange = date.month;
52
- break;
53
- case 'year':
54
- dateRange = date.year;
55
- break;
56
- }
57
- }
39
+ export function mapDateToGranularityRange(
40
+ data: { date: string | null; count: number },
41
+ granularity: TemporalGranularity,
42
+ ) {
58
43
  return {
59
- dateRange,
60
- count: d.count,
44
+ dateRange: data.date === null ? null : parseDateStringToTemporal(data.date, granularity),
45
+ count: data.count,
61
46
  };
62
47
  }
63
48
 
64
- function dateRangeCompare(a: { dateRange: Temporal | null }, b: { dateRange: Temporal | null }) {
65
- if (a.dateRange === null) {
66
- return 1;
67
- }
68
- if (b.dateRange === null) {
69
- return -1;
70
- }
71
- return compareTemporal(a.dateRange, b.dateRange);
72
- }
73
-
74
49
  function averageSmoothing(slidingWindow: { dateRange: Temporal | null; count: number }[]) {
75
50
  const average = slidingWindow.reduce((acc, curr) => acc + curr.count, 0) / slidingWindow.length;
76
51
  const centerIndex = Math.floor(slidingWindow.length / 2);
@@ -0,0 +1,378 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { queryMutationsOverTimeData } from './queryMutationsOverTime';
4
+ import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../vitest.setup';
5
+
6
+ describe('queryMutationsOverTime', () => {
7
+ it('should fetch for a filter without date', async () => {
8
+ const lapisFilter = { field1: 'value1', field2: 'value2' };
9
+ const dateField = 'dateField';
10
+
11
+ lapisRequestMocks.aggregated(
12
+ { ...lapisFilter, fields: [dateField] },
13
+ {
14
+ data: [
15
+ { count: 1, [dateField]: '2023-01-01' },
16
+ { count: 2, [dateField]: '2023-01-03' },
17
+ ],
18
+ },
19
+ );
20
+
21
+ lapisRequestMocks.multipleMutations(
22
+ [
23
+ {
24
+ body: {
25
+ ...lapisFilter,
26
+ dateFieldFrom: '2023-01-01',
27
+ dateFieldTo: '2023-01-01',
28
+ minProportion: 0.001,
29
+ },
30
+ response: { data: [getSomeTestMutation(0.1, 1), getSomeOtherTestMutation(0.4, 4)] },
31
+ },
32
+ {
33
+ body: {
34
+ ...lapisFilter,
35
+ dateFieldFrom: '2023-01-02',
36
+ dateFieldTo: '2023-01-02',
37
+ minProportion: 0.001,
38
+ },
39
+ response: { data: [getSomeTestMutation(0.2, 2)] },
40
+ },
41
+ {
42
+ body: {
43
+ ...lapisFilter,
44
+ dateFieldFrom: '2023-01-03',
45
+ dateFieldTo: '2023-01-03',
46
+ minProportion: 0.001,
47
+ },
48
+ response: { data: [getSomeTestMutation(0.3, 3)] },
49
+ },
50
+ ],
51
+ 'nucleotide',
52
+ );
53
+
54
+ const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
55
+
56
+ expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([
57
+ [
58
+ { proportion: 0.1, count: 1 },
59
+ { proportion: 0.2, count: 2 },
60
+ { proportion: 0.3, count: 3 },
61
+ ],
62
+ [
63
+ { proportion: 0.4, count: 4 },
64
+ { proportion: 0, count: 0 },
65
+ { proportion: 0, count: 0 },
66
+ ],
67
+ ]);
68
+
69
+ const sequences = result.getFirstAxisKeys();
70
+ expect(sequences[0].code).toBe('sequenceName:A123T');
71
+ expect(sequences[1].code).toBe('otherSequenceName:G234C');
72
+
73
+ const dates = result.getSecondAxisKeys();
74
+ expect(dates[0].toString()).toBe('2023-01-01');
75
+ expect(dates[1].toString()).toBe('2023-01-02');
76
+ expect(dates[2].toString()).toBe('2023-01-03');
77
+ });
78
+
79
+ it('should fetch for dates with no mutations', async () => {
80
+ const lapisFilter = { field1: 'value1', field2: 'value2' };
81
+ const dateField = 'dateField';
82
+
83
+ lapisRequestMocks.aggregated(
84
+ { ...lapisFilter, fields: [dateField] },
85
+ {
86
+ data: [
87
+ { count: 1, [dateField]: '2023-01-01' },
88
+ { count: 2, [dateField]: '2023-01-03' },
89
+ ],
90
+ },
91
+ );
92
+
93
+ lapisRequestMocks.multipleMutations(
94
+ [
95
+ {
96
+ body: {
97
+ ...lapisFilter,
98
+ dateFieldFrom: '2023-01-01',
99
+ dateFieldTo: '2023-01-01',
100
+ minProportion: 0.001,
101
+ },
102
+ response: { data: [getSomeTestMutation(0.1, 1), getSomeOtherTestMutation(0.4, 4)] },
103
+ },
104
+ {
105
+ body: {
106
+ ...lapisFilter,
107
+ dateFieldFrom: '2023-01-02',
108
+ dateFieldTo: '2023-01-02',
109
+ minProportion: 0.001,
110
+ },
111
+ response: { data: [] },
112
+ },
113
+ {
114
+ body: {
115
+ ...lapisFilter,
116
+ dateFieldFrom: '2023-01-03',
117
+ dateFieldTo: '2023-01-03',
118
+ minProportion: 0.001,
119
+ },
120
+ response: { data: [getSomeTestMutation(0.3, 3)] },
121
+ },
122
+ ],
123
+ 'nucleotide',
124
+ );
125
+
126
+ const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
127
+
128
+ expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([
129
+ [
130
+ { proportion: 0.1, count: 1 },
131
+ { proportion: 0.3, count: 3 },
132
+ { proportion: 0, count: 0 },
133
+ ],
134
+ [
135
+ { proportion: 0.4, count: 4 },
136
+ { proportion: 0, count: 0 },
137
+ { proportion: 0, count: 0 },
138
+ ],
139
+ ]);
140
+
141
+ const sequences = result.getFirstAxisKeys();
142
+ expect(sequences[0].code).toBe('sequenceName:A123T');
143
+ expect(sequences[1].code).toBe('otherSequenceName:G234C');
144
+
145
+ const dates = result.getSecondAxisKeys();
146
+ expect(dates[0].toString()).toBe('2023-01-01');
147
+ expect(dates[1].toString()).toBe('2023-01-03');
148
+ expect(dates[2].toString()).toBe('2023-01-02');
149
+ });
150
+
151
+ it('should return empty map when no mutations are found', async () => {
152
+ const lapisFilter = { field1: 'value1', field2: 'value2' };
153
+ const dateField = 'dateField';
154
+
155
+ lapisRequestMocks.aggregated(
156
+ { ...lapisFilter, fields: [dateField] },
157
+ {
158
+ data: [
159
+ { count: 1, [dateField]: '2023-01-01' },
160
+ { count: 2, [dateField]: '2023-01-03' },
161
+ ],
162
+ },
163
+ );
164
+
165
+ lapisRequestMocks.multipleMutations(
166
+ [
167
+ {
168
+ body: {
169
+ ...lapisFilter,
170
+ dateFieldFrom: '2023-01-01',
171
+ dateFieldTo: '2023-01-01',
172
+ minProportion: 0.001,
173
+ },
174
+ response: { data: [] },
175
+ },
176
+ {
177
+ body: {
178
+ ...lapisFilter,
179
+ dateFieldFrom: '2023-01-02',
180
+ dateFieldTo: '2023-01-02',
181
+ minProportion: 0.001,
182
+ },
183
+ response: { data: [] },
184
+ },
185
+ {
186
+ body: {
187
+ ...lapisFilter,
188
+ dateFieldFrom: '2023-01-03',
189
+ dateFieldTo: '2023-01-03',
190
+ minProportion: 0.001,
191
+ },
192
+ response: { data: [] },
193
+ },
194
+ ],
195
+ 'nucleotide',
196
+ );
197
+
198
+ const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
199
+
200
+ expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([]);
201
+ expect(result.getFirstAxisKeys()).to.deep.equal([]);
202
+ expect(result.getSecondAxisKeys()).to.deep.equal([]);
203
+ });
204
+
205
+ it('should use dateFrom from filter', async () => {
206
+ const dateField = 'dateField';
207
+ const lapisFilter = { field1: 'value1', field2: 'value2', [`${dateField}From`]: '2023-01-02' };
208
+
209
+ lapisRequestMocks.aggregated(
210
+ { ...lapisFilter, fields: [dateField] },
211
+ {
212
+ data: [
213
+ { count: 1, [dateField]: '2023-01-01' },
214
+ { count: 2, [dateField]: '2023-01-03' },
215
+ ],
216
+ },
217
+ );
218
+
219
+ lapisRequestMocks.multipleMutations(
220
+ [
221
+ {
222
+ body: {
223
+ ...lapisFilter,
224
+ dateFieldFrom: '2023-01-02',
225
+ dateFieldTo: '2023-01-02',
226
+ minProportion: 0.001,
227
+ },
228
+ response: { data: [getSomeTestMutation(0.2, 2)] },
229
+ },
230
+ {
231
+ body: {
232
+ ...lapisFilter,
233
+ dateFieldFrom: '2023-01-03',
234
+ dateFieldTo: '2023-01-03',
235
+ minProportion: 0.001,
236
+ },
237
+ response: { data: [getSomeTestMutation(0.3, 3)] },
238
+ },
239
+ ],
240
+ 'nucleotide',
241
+ );
242
+
243
+ const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
244
+
245
+ expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([
246
+ [
247
+ { proportion: 0.2, count: 2 },
248
+ { proportion: 0.3, count: 3 },
249
+ ],
250
+ ]);
251
+
252
+ const sequences = result.getFirstAxisKeys();
253
+ expect(sequences[0].code).toBe('sequenceName:A123T');
254
+
255
+ const dates = result.getSecondAxisKeys();
256
+ expect(dates[0].toString()).toBe('2023-01-02');
257
+ expect(dates[1].toString()).toBe('2023-01-03');
258
+ });
259
+
260
+ it('should use dateTo from filter', async () => {
261
+ const dateField = 'dateField';
262
+ const lapisFilter = { field1: 'value1', field2: 'value2', [`${dateField}To`]: '2023-01-02' };
263
+
264
+ lapisRequestMocks.aggregated(
265
+ { ...lapisFilter, fields: [dateField] },
266
+ {
267
+ data: [
268
+ { count: 1, [dateField]: '2023-01-01' },
269
+ { count: 2, [dateField]: '2023-01-03' },
270
+ ],
271
+ },
272
+ );
273
+
274
+ lapisRequestMocks.multipleMutations(
275
+ [
276
+ {
277
+ body: {
278
+ ...lapisFilter,
279
+ dateFieldFrom: '2023-01-01',
280
+ dateFieldTo: '2023-01-01',
281
+ minProportion: 0.001,
282
+ },
283
+ response: { data: [getSomeTestMutation(0.1, 1)] },
284
+ },
285
+ {
286
+ body: {
287
+ ...lapisFilter,
288
+ dateFieldFrom: '2023-01-02',
289
+ dateFieldTo: '2023-01-02',
290
+ minProportion: 0.001,
291
+ },
292
+ response: { data: [getSomeTestMutation(0.2, 2)] },
293
+ },
294
+ ],
295
+ 'nucleotide',
296
+ );
297
+
298
+ const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
299
+
300
+ expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([
301
+ [
302
+ { proportion: 0.1, count: 1 },
303
+ { proportion: 0.2, count: 2 },
304
+ ],
305
+ ]);
306
+
307
+ const sequences = result.getFirstAxisKeys();
308
+ expect(sequences[0].code).toBe('sequenceName:A123T');
309
+
310
+ const dates = result.getSecondAxisKeys();
311
+ expect(dates[0].toString()).toBe('2023-01-01');
312
+ expect(dates[1].toString()).toBe('2023-01-02');
313
+ });
314
+
315
+ it('should use date from filter', async () => {
316
+ const dateField = 'dateField';
317
+ const lapisFilter = { field1: 'value1', field2: 'value2', [dateField]: '2023-01-02' };
318
+
319
+ lapisRequestMocks.aggregated(
320
+ { ...lapisFilter, fields: [dateField] },
321
+ {
322
+ data: [
323
+ { count: 1, [dateField]: '2023-01-01' },
324
+ { count: 2, [dateField]: '2023-01-03' },
325
+ ],
326
+ },
327
+ );
328
+
329
+ lapisRequestMocks.multipleMutations(
330
+ [
331
+ {
332
+ body: {
333
+ ...lapisFilter,
334
+ dateFieldFrom: '2023-01-02',
335
+ dateFieldTo: '2023-01-02',
336
+ minProportion: 0.001,
337
+ },
338
+ response: { data: [getSomeTestMutation(0.2, 2)] },
339
+ },
340
+ ],
341
+ 'nucleotide',
342
+ );
343
+
344
+ const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
345
+
346
+ expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([[{ proportion: 0.2, count: 2 }]]);
347
+
348
+ const sequences = result.getFirstAxisKeys();
349
+ expect(sequences[0].code).toBe('sequenceName:A123T');
350
+
351
+ const dates = result.getSecondAxisKeys();
352
+ expect(dates[0].toString()).toBe('2023-01-02');
353
+ });
354
+
355
+ function getSomeTestMutation(proportion: number, count: number) {
356
+ return {
357
+ mutation: 'sequenceName:A123T',
358
+ proportion,
359
+ count,
360
+ sequenceName: 'sequenceName',
361
+ mutationFrom: 'A',
362
+ mutationTo: 'T',
363
+ position: 123,
364
+ };
365
+ }
366
+
367
+ function getSomeOtherTestMutation(proportion: number, count: number) {
368
+ return {
369
+ mutation: 'otherSequenceName:A123T',
370
+ proportion,
371
+ count,
372
+ sequenceName: 'otherSequenceName',
373
+ mutationFrom: 'G',
374
+ mutationTo: 'C',
375
+ position: 234,
376
+ };
377
+ }
378
+ });
@@ -0,0 +1,179 @@
1
+ import { mapDateToGranularityRange } from './queryAggregatedDataOverTime';
2
+ import { FetchAggregatedOperator } from '../operator/FetchAggregatedOperator';
3
+ import { FetchSubstitutionsOrDeletionsOperator } from '../operator/FetchSubstitutionsOrDeletionsOperator';
4
+ import { GroupByAndSumOperator } from '../operator/GroupByAndSumOperator';
5
+ import { MapOperator } from '../operator/MapOperator';
6
+ import { RenameFieldOperator } from '../operator/RenameFieldOperator';
7
+ import { SortOperator } from '../operator/SortOperator';
8
+ import { UserFacingError } from '../preact/components/error-display';
9
+ import {
10
+ type LapisFilter,
11
+ type SequenceType,
12
+ type SubstitutionOrDeletionEntry,
13
+ type TemporalGranularity,
14
+ } from '../types';
15
+ import { Map2d } from '../utils/Map2d';
16
+ import { type Deletion, type Substitution } from '../utils/mutations';
17
+ import {
18
+ dateRangeCompare,
19
+ generateAllInRange,
20
+ getMinMaxTemporal,
21
+ parseDateStringToTemporal,
22
+ type Temporal,
23
+ } from '../utils/temporal';
24
+
25
+ export type MutationOverTimeData = {
26
+ date: Temporal;
27
+ mutations: SubstitutionOrDeletionEntry[];
28
+ };
29
+
30
+ export type MutationOverTimeMutationValue = { proportion: number; count: number };
31
+ export type MutationOverTimeDataGroupedByMutation = Map2d<
32
+ Substitution | Deletion,
33
+ Temporal,
34
+ MutationOverTimeMutationValue
35
+ >;
36
+
37
+ const MAX_NUMBER_OF_GRID_COLUMNS = 200;
38
+
39
+ export async function queryMutationsOverTimeData(
40
+ lapisFilter: LapisFilter,
41
+ sequenceType: 'nucleotide' | 'amino acid',
42
+ lapis: string,
43
+ lapisDateField: string,
44
+ granularity: TemporalGranularity,
45
+ signal?: AbortSignal,
46
+ ) {
47
+ const allDates = await getDatesInDataset(lapisFilter, lapis, granularity, lapisDateField, signal);
48
+
49
+ if (allDates.length > MAX_NUMBER_OF_GRID_COLUMNS) {
50
+ throw new UserFacingError(
51
+ 'Too many dates',
52
+ `The dataset would contain ${allDates.length} date intervals. ` +
53
+ `Please reduce the number to below ${MAX_NUMBER_OF_GRID_COLUMNS} to display the data. ` +
54
+ 'You can achieve this by either narrowing the date range in the provided LAPIS filter or by selecting a larger granularity.',
55
+ );
56
+ }
57
+
58
+ const subQueries = allDates.map(async (date) => {
59
+ const dateFrom = date.firstDay.toString();
60
+ const dateTo = date.lastDay.toString();
61
+
62
+ const filter = {
63
+ ...lapisFilter,
64
+ [`${lapisDateField}From`]: dateFrom,
65
+ [`${lapisDateField}To`]: dateTo,
66
+ };
67
+
68
+ const data = await fetchAndPrepareSubstitutionsOrDeletions(filter, sequenceType).evaluate(lapis, signal);
69
+ return {
70
+ date,
71
+ mutations: data.content,
72
+ };
73
+ });
74
+
75
+ const data = await Promise.all(subQueries);
76
+
77
+ return groupByMutation(data);
78
+ }
79
+
80
+ async function getDatesInDataset(
81
+ lapisFilter: LapisFilter,
82
+ lapis: string,
83
+ granularity: TemporalGranularity,
84
+ lapisDateField: string,
85
+ signal: AbortSignal | undefined,
86
+ ) {
87
+ const { content: availableDates } = await queryAvailableDates(
88
+ lapisFilter,
89
+ lapis,
90
+ granularity,
91
+ lapisDateField,
92
+ signal,
93
+ );
94
+
95
+ const { dateFrom, dateTo } = getDateRangeFromFilter(lapisFilter, lapisDateField, granularity);
96
+ const { min, max } = getMinMaxTemporal(availableDates);
97
+
98
+ return generateAllInRange(dateFrom ?? min, dateTo ?? max);
99
+ }
100
+
101
+ function getDateRangeFromFilter(lapisFilter: LapisFilter, lapisDateField: string, granularity: TemporalGranularity) {
102
+ const valueFromFilter = lapisFilter[lapisDateField] as string | null;
103
+
104
+ if (valueFromFilter) {
105
+ return {
106
+ dateFrom: parseDateStringToTemporal(valueFromFilter, granularity),
107
+ dateTo: parseDateStringToTemporal(valueFromFilter, granularity),
108
+ };
109
+ }
110
+
111
+ const minFromFilter = lapisFilter[`${lapisDateField}From`] as string | null;
112
+ const maxFromFilter = lapisFilter[`${lapisDateField}To`] as string | null;
113
+
114
+ return {
115
+ dateFrom: minFromFilter ? parseDateStringToTemporal(minFromFilter, granularity) : null,
116
+ dateTo: maxFromFilter ? parseDateStringToTemporal(maxFromFilter, granularity) : null,
117
+ };
118
+ }
119
+
120
+ function queryAvailableDates(
121
+ lapisFilter: LapisFilter,
122
+ lapis: string,
123
+ granularity: TemporalGranularity,
124
+ lapisDateField: string,
125
+ signal?: AbortSignal,
126
+ ) {
127
+ return fetchAndPrepareDates(lapisFilter, granularity, lapisDateField).evaluate(lapis, signal);
128
+ }
129
+
130
+ function fetchAndPrepareDates<LapisDateField extends string>(
131
+ filter: LapisFilter,
132
+ granularity: TemporalGranularity,
133
+ lapisDateField: LapisDateField,
134
+ ) {
135
+ const fetchData = new FetchAggregatedOperator<{ [key in LapisDateField]: string | null }>(filter, [lapisDateField]);
136
+ const dataWithFixedDateKey = new RenameFieldOperator(fetchData, lapisDateField, 'date');
137
+ const mapData = new MapOperator(dataWithFixedDateKey, (data) => mapDateToGranularityRange(data, granularity));
138
+ const groupByData = new GroupByAndSumOperator(mapData, 'dateRange', 'count');
139
+ const sortData = new SortOperator(groupByData, dateRangeCompare);
140
+ return new MapOperator(sortData, (data) => data.dateRange);
141
+ }
142
+
143
+ function fetchAndPrepareSubstitutionsOrDeletions(filter: LapisFilter, sequenceType: SequenceType) {
144
+ return new FetchSubstitutionsOrDeletionsOperator(filter, sequenceType, 0.001);
145
+ }
146
+
147
+ export function groupByMutation(data: MutationOverTimeData[]) {
148
+ const dataArray = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>(
149
+ (mutation) => mutation.code,
150
+ (date) => date.toString(),
151
+ );
152
+
153
+ data.forEach((mutationData) => {
154
+ mutationData.mutations.forEach((mutationEntry) => {
155
+ dataArray.set(mutationEntry.mutation, mutationData.date, {
156
+ count: mutationEntry.count,
157
+ proportion: mutationEntry.proportion,
158
+ });
159
+ });
160
+ });
161
+
162
+ addZeroValuesForDatesWithNoMutationData(dataArray, data);
163
+
164
+ return dataArray;
165
+ }
166
+
167
+ function addZeroValuesForDatesWithNoMutationData(
168
+ dataArray: Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>,
169
+ data: MutationOverTimeData[],
170
+ ) {
171
+ if (dataArray.getFirstAxisKeys().length !== 0) {
172
+ const someMutation = dataArray.getFirstAxisKeys()[0];
173
+ data.forEach((mutationData) => {
174
+ if (mutationData.mutations.length === 0) {
175
+ dataArray.set(someMutation, mutationData.date, { count: 0, proportion: 0 });
176
+ }
177
+ });
178
+ }
179
+ }
@@ -4,7 +4,6 @@ import { sortNullToBeginningThenByDate } from '../utils/sort';
4
4
  import { makeArray } from '../utils/utils';
5
5
 
6
6
  export type NumberOfSequencesDatasets = Awaited<ReturnType<typeof queryNumberOfSequencesOverTime>>;
7
- export type NumberOfSequencesDataset = NumberOfSequencesDatasets[number];
8
7
 
9
8
  export async function queryNumberOfSequencesOverTime(
10
9
  lapis: string,
@@ -28,11 +28,11 @@ export async function queryRelativeGrowthAdvantage<LapisDateField extends string
28
28
  mapNumerator.evaluate(lapis, signal),
29
29
  mapDenominator.evaluate(lapis, signal),
30
30
  ]);
31
- const minMaxDate = getMinMaxTemporal(denominatorData.content.map((d) => d.date));
32
- if (!minMaxDate) {
31
+ const { min: minDate, max: maxDate } = getMinMaxTemporal(denominatorData.content.map((d) => d.date));
32
+ if (!minDate && !maxDate) {
33
33
  return null;
34
34
  }
35
- const [minDate, maxDate] = minMaxDate as [YearMonthDay, YearMonthDay];
35
+
36
36
  const numeratorCounts = new Map<YearMonthDay, number>();
37
37
  numeratorData.content.forEach((d) => {
38
38
  if (d.date) {