@genspectrum/dashboard-components 0.6.1 → 0.6.3

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