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