@genspectrum/dashboard-components 0.13.0 → 0.13.2
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/custom-elements.json +178 -0
- package/dist/assets/mutationOverTimeWorker-B1-WrM4b.js.map +1 -0
- package/dist/components.d.ts +108 -43
- package/dist/components.js +564 -295
- package/dist/components.js.map +1 -1
- package/dist/style.css +3 -0
- package/dist/util.d.ts +59 -43
- package/package.json +2 -2
- package/src/constants.ts +6 -0
- package/src/lapisApi/__mockData__/wiseReferenceGenome.json +9 -0
- package/src/lapisApi/lapisApi.ts +17 -0
- package/src/lapisApi/lapisTypes.ts +7 -1
- package/src/operator/FetchDetailsOperator.ts +28 -0
- package/src/preact/components/downshift-combobox.tsx +18 -20
- package/src/preact/components/tabs.tsx +1 -1
- package/src/preact/mutationsOverTime/MutationOverTimeData.ts +9 -5
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +5 -3
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +4 -7
- package/src/preact/textInput/fetchStringAutocompleteList.spec.ts +34 -0
- package/src/preact/textInput/fetchStringAutocompleteList.ts +16 -2
- package/src/preact/textInput/text-input.tsx +22 -8
- package/src/preact/wastewater/mutationsOverTime/__mockData__/details.json +88 -0
- package/src/preact/wastewater/mutationsOverTime/computeWastewaterMutationsOverTimeDataPerLocation.spec.ts +159 -0
- package/src/preact/wastewater/mutationsOverTime/computeWastewaterMutationsOverTimeDataPerLocation.ts +51 -0
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +71 -0
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +151 -0
- package/src/query/queryMutationsOverTime.ts +6 -14
- package/src/query/queryWastewaterMutationsOverTime.spec.ts +94 -0
- package/src/query/queryWastewaterMutationsOverTime.ts +55 -0
- package/src/utils/map2d.ts +39 -0
- package/src/web-components/index.ts +1 -0
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +82 -0
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +112 -0
- package/src/web-components/wastewaterVisualization/index.ts +1 -0
- package/standalone-bundle/assets/{mutationOverTimeWorker-DEybsZ5r.js.map → mutationOverTimeWorker-Cls1J0cl.js.map} +1 -1
- package/standalone-bundle/dashboard-components.js +6228 -6008
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
- package/dist/assets/mutationOverTimeWorker-DTv93Ere.js.map +0 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"data": [
|
|
3
|
+
{
|
|
4
|
+
"aminoAcidMutationFrequency": null,
|
|
5
|
+
"date": "2024-11-13",
|
|
6
|
+
"location": "Lugano",
|
|
7
|
+
"nucleotideMutationFrequency": "{\"A12183T\": null, \"C2554T\": null, \"C3422A\": 0.9598659873008728, \"A966C\": null, \"G6661A\": 0.5527499914169312, \"G7731A\": 0.9832900166511536, \"T4026G\": 0.9991809725761414, \"T5260C\": null, \"T5287C\": null}",
|
|
8
|
+
"reference": "RSV-B"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"aminoAcidMutationFrequency": null,
|
|
12
|
+
"date": "2024-11-17",
|
|
13
|
+
"location": "Genève",
|
|
14
|
+
"nucleotideMutationFrequency": "{\"A12183T\": 1.0, \"C2554T\": null, \"C3422A\": 0.0, \"A966C\": null, \"G6661A\": 0.8785049915313721, \"G7731A\": 0.9855599999427795, \"T4026G\": null, \"T5260C\": 1.0, \"T5287C\": 0.9932659864425659}",
|
|
15
|
+
"reference": "RSV-B"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"aminoAcidMutationFrequency": "{\"ORF1a:S4286C\": null, \"S:R346T\": 0.25, \"S:Q493E\": 0.60, \"N:G204P\": null}",
|
|
19
|
+
"date": "2024-11-17",
|
|
20
|
+
"location": "Lugano",
|
|
21
|
+
"nucleotideMutationFrequency": "{\"A12183T\": 0.998993992805481, \"C2554T\": null, \"C3422A\": null, \"A966C\": null, \"G6661A\": 0.8917459845542908, \"G7731A\": 0.9770470261573792, \"T4026G\": 0.0, \"T5260C\": null, \"T5287C\": null}",
|
|
22
|
+
"reference": "RSV-B"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"aminoAcidMutationFrequency": "{\"ORF1a:S4286C\": 0.42, \"S:R346T\": 0.22, \"S:Q493E\": 0.66, \"N:G204P\": null}",
|
|
26
|
+
"date": "2024-11-16",
|
|
27
|
+
"location": "Lugano",
|
|
28
|
+
"nucleotideMutationFrequency": "{\"A12183T\": null, \"C2554T\": null, \"C3422A\": null, \"A966C\": null, \"G6661A\": 0.9476320147514343, \"G7731A\": 0.9809200167655945, \"T4026G\": 0.9992259740829468, \"T5260C\": null, \"T5287C\": null}",
|
|
29
|
+
"reference": "RSV-B"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"aminoAcidMutationFrequency": null,
|
|
33
|
+
"date": "2024-11-18",
|
|
34
|
+
"location": "Genève",
|
|
35
|
+
"nucleotideMutationFrequency": "{\"A12183T\": null, \"C2554T\": 0.9978219866752625, \"C3422A\": null, \"A966C\": 1.0, \"G6661A\": null, \"G7731A\": null, \"T4026G\": null, \"T5260C\": null, \"T5287C\": null}",
|
|
36
|
+
"reference": "RSV-B"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"aminoAcidMutationFrequency": null,
|
|
40
|
+
"date": "2024-11-14",
|
|
41
|
+
"location": "Genève",
|
|
42
|
+
"nucleotideMutationFrequency": "{\"A12183T\": null, \"C2554T\": 0.9994590282440186, \"C3422A\": 0.05333299934864044, \"A966C\": 1.0, \"G6661A\": 0.9282640218734741, \"G7731A\": 0.9803630113601685, \"T4026G\": 0.0, \"T5260C\": 0.9970409870147705, \"T5287C\": 0.996694028377533}",
|
|
43
|
+
"reference": "RSV-B"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"aminoAcidMutationFrequency": null,
|
|
47
|
+
"date": "2024-11-13",
|
|
48
|
+
"location": "Laupen",
|
|
49
|
+
"nucleotideMutationFrequency": "{\"A12668G\": null, \"A3564G\": null, \"C10862T\": null, \"C12710T\": null, \"C3624T\": null, \"G11123A\": null, \"G3616A\": null, \"G7379A\": 0.9470750093460083, \"T11034A\": null, \"T3483C\": null}",
|
|
50
|
+
"reference": "RSV-A"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"aminoAcidMutationFrequency": null,
|
|
54
|
+
"date": "2024-11-13",
|
|
55
|
+
"location": "Zürich",
|
|
56
|
+
"nucleotideMutationFrequency": "{\"A12668G\": 0.9988809823989868, \"A3564G\": null, \"C10862T\": 1.0, \"C12710T\": 0.9991809725761414, \"C3624T\": null, \"G11123A\": 1.0, \"G3616A\": null, \"G7379A\": 0.9414680004119873, \"T11034A\": 0.9988250136375427, \"T3483C\": null}",
|
|
57
|
+
"reference": "RSV-A"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"aminoAcidMutationFrequency": null,
|
|
61
|
+
"date": "2024-11-16",
|
|
62
|
+
"location": "Zürich",
|
|
63
|
+
"nucleotideMutationFrequency": "{\"A12668G\": 0.999222993850708, \"A3564G\": 0.9998199939727783, \"C10862T\": 1.0, \"C12710T\": 0.9996299743652344, \"C3624T\": 0.9999099969863892, \"G11123A\": 1.0, \"G3616A\": 0.08799900114536285, \"G7379A\": null, \"T11034A\": 0.9970099925994873, \"T3483C\": 0.9993579983711243}",
|
|
64
|
+
"reference": "RSV-A"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"aminoAcidMutationFrequency": null,
|
|
68
|
+
"date": "2024-11-16",
|
|
69
|
+
"location": "Chur",
|
|
70
|
+
"nucleotideMutationFrequency": "{\"A12668G\": null, \"A3564G\": null, \"C10862T\": null, \"C12710T\": null, \"C3624T\": null, \"G11123A\": null, \"G3616A\": null, \"G7379A\": 0.6464089751243591, \"T11034A\": null, \"T3483C\": null}",
|
|
71
|
+
"reference": "RSV-A"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"aminoAcidMutationFrequency": null,
|
|
75
|
+
"date": "2024-11-18",
|
|
76
|
+
"location": "Basel",
|
|
77
|
+
"nucleotideMutationFrequency": "{\"A12668G\": null, \"A3564G\": 1.0, \"C10862T\": null, \"C12710T\": null, \"C3624T\": 1.0, \"G11123A\": null, \"G3616A\": 0.08222199976444244, \"G7379A\": null, \"T11034A\": null, \"T3483C\": 0.9998080134391785}",
|
|
78
|
+
"reference": "RSV-A"
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"info": {
|
|
82
|
+
"dataVersion": "1737327031",
|
|
83
|
+
"requestId": "5591b455-3d84-438c-8434-2e57ee2ad569",
|
|
84
|
+
"requestInfo": "RSV on api.wise-loculus.genspectrum.org at 2025-01-20T11:58:03.498206810",
|
|
85
|
+
"reportTo": "Please report to https://github.com/GenSpectrum/LAPIS/issues in case you encounter any unexpected issues. Please include the request ID and the requestInfo in your report.",
|
|
86
|
+
"lapisVersion": "0.3.10"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { groupMutationDataByLocation } from './computeWastewaterMutationsOverTimeDataPerLocation';
|
|
4
|
+
import type { WastewaterData } from '../../../query/queryWastewaterMutationsOverTime';
|
|
5
|
+
import { SubstitutionClass } from '../../../utils/mutations';
|
|
6
|
+
import { TemporalCache } from '../../../utils/temporalClass';
|
|
7
|
+
|
|
8
|
+
const temporalCache = TemporalCache.getInstance();
|
|
9
|
+
|
|
10
|
+
const mutation1 = SubstitutionClass.parse('1T')!;
|
|
11
|
+
const mutation2 = SubstitutionClass.parse('2G')!;
|
|
12
|
+
const mutation3 = SubstitutionClass.parse('3C')!;
|
|
13
|
+
|
|
14
|
+
const location1 = 'location1';
|
|
15
|
+
const location2 = 'location2';
|
|
16
|
+
|
|
17
|
+
describe('groupMutationDataByLocation', () => {
|
|
18
|
+
test('should group nucleotide mutations by location', () => {
|
|
19
|
+
const input: WastewaterData = [
|
|
20
|
+
{
|
|
21
|
+
location: location1,
|
|
22
|
+
date: temporalCache.getYearMonthDay('2025-01-01'),
|
|
23
|
+
nucleotideMutationFrequency: [
|
|
24
|
+
{ mutation: mutation1, proportion: 0.1 },
|
|
25
|
+
{ mutation: mutation2, proportion: 0.2 },
|
|
26
|
+
],
|
|
27
|
+
aminoAcidMutationFrequency: [],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
location: location1,
|
|
31
|
+
date: temporalCache.getYearMonthDay('2025-01-02'),
|
|
32
|
+
nucleotideMutationFrequency: [
|
|
33
|
+
{ mutation: mutation1, proportion: null },
|
|
34
|
+
{ mutation: mutation2, proportion: 0.3 },
|
|
35
|
+
],
|
|
36
|
+
aminoAcidMutationFrequency: [],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
location: location2,
|
|
40
|
+
date: temporalCache.getYearMonthDay('2025-01-01'),
|
|
41
|
+
nucleotideMutationFrequency: [
|
|
42
|
+
{ mutation: mutation1, proportion: 0.1 },
|
|
43
|
+
{ mutation: mutation3, proportion: 0.2 },
|
|
44
|
+
],
|
|
45
|
+
aminoAcidMutationFrequency: [],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const result = groupMutationDataByLocation(input, 'nucleotide');
|
|
50
|
+
|
|
51
|
+
expect(result).to.have.length(2);
|
|
52
|
+
expect(result[0].location).to.equal(location1);
|
|
53
|
+
expect(result[1].location).to.equal(location2);
|
|
54
|
+
|
|
55
|
+
const location1Data = result[0].data;
|
|
56
|
+
expect(location1Data.getFirstAxisKeys()).to.deep.equal([mutation1, mutation2]);
|
|
57
|
+
expect(location1Data.getSecondAxisKeys()).to.deep.equal([
|
|
58
|
+
temporalCache.getYearMonthDay('2025-01-01'),
|
|
59
|
+
temporalCache.getYearMonthDay('2025-01-02'),
|
|
60
|
+
]);
|
|
61
|
+
expect(location1Data.getAsArray()).to.deep.equal([
|
|
62
|
+
[{ count: null, proportion: 0.1, totalCount: null }, null],
|
|
63
|
+
[
|
|
64
|
+
{ count: null, proportion: 0.2, totalCount: null },
|
|
65
|
+
{ count: null, proportion: 0.3, totalCount: null },
|
|
66
|
+
],
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('should group amino acid mutations by location', () => {
|
|
71
|
+
const input: WastewaterData = [
|
|
72
|
+
{
|
|
73
|
+
location: location1,
|
|
74
|
+
date: temporalCache.getYearMonthDay('2025-01-01'),
|
|
75
|
+
nucleotideMutationFrequency: [{ mutation: mutation1, proportion: 0.1 }],
|
|
76
|
+
aminoAcidMutationFrequency: [
|
|
77
|
+
{ mutation: mutation2, proportion: 0.2 },
|
|
78
|
+
{ mutation: mutation3, proportion: 0.3 },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
location: location2,
|
|
83
|
+
date: temporalCache.getYearMonthDay('2025-01-01'),
|
|
84
|
+
nucleotideMutationFrequency: [],
|
|
85
|
+
aminoAcidMutationFrequency: [{ mutation: mutation3, proportion: 0.3 }],
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const result = groupMutationDataByLocation(input, 'amino acid');
|
|
90
|
+
|
|
91
|
+
expect(result).to.have.length(2);
|
|
92
|
+
expect(result[0].location).to.equal(location1);
|
|
93
|
+
expect(result[1].location).to.equal(location2);
|
|
94
|
+
|
|
95
|
+
const location1Data = result[0].data;
|
|
96
|
+
expect(location1Data.getFirstAxisKeys()).to.deep.equal([mutation2, mutation3]);
|
|
97
|
+
expect(location1Data.getSecondAxisKeys()).to.deep.equal([temporalCache.getYearMonthDay('2025-01-01')]);
|
|
98
|
+
expect(location1Data.getAsArray()).to.deep.equal([
|
|
99
|
+
[{ count: null, proportion: 0.2, totalCount: null }],
|
|
100
|
+
[{ count: null, proportion: 0.3, totalCount: null }],
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('should sort date axis correctly', () => {
|
|
105
|
+
const input: WastewaterData = [
|
|
106
|
+
{
|
|
107
|
+
location: location1,
|
|
108
|
+
date: temporalCache.getYearMonthDay('2025-01-02'),
|
|
109
|
+
nucleotideMutationFrequency: [{ mutation: mutation1, proportion: 0.2 }],
|
|
110
|
+
aminoAcidMutationFrequency: [],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
location: location1,
|
|
114
|
+
date: temporalCache.getYearMonthDay('2025-01-01'),
|
|
115
|
+
nucleotideMutationFrequency: [{ mutation: mutation1, proportion: 0.1 }],
|
|
116
|
+
aminoAcidMutationFrequency: [],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
location: location1,
|
|
120
|
+
date: temporalCache.getYearMonthDay('2025-01-03'),
|
|
121
|
+
nucleotideMutationFrequency: [{ mutation: mutation1, proportion: 0.3 }],
|
|
122
|
+
aminoAcidMutationFrequency: [],
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const result = groupMutationDataByLocation(input, 'nucleotide');
|
|
127
|
+
|
|
128
|
+
expect(result).to.have.length(1);
|
|
129
|
+
const location1Data = result[0].data;
|
|
130
|
+
|
|
131
|
+
expect(location1Data.getSecondAxisKeys()).to.deep.equal([
|
|
132
|
+
temporalCache.getYearMonthDay('2025-01-01'),
|
|
133
|
+
temporalCache.getYearMonthDay('2025-01-02'),
|
|
134
|
+
temporalCache.getYearMonthDay('2025-01-03'),
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should sort mutations correctly', () => {
|
|
139
|
+
const input: WastewaterData = [
|
|
140
|
+
{
|
|
141
|
+
location: location1,
|
|
142
|
+
date: temporalCache.getYearMonthDay('2025-01-01'),
|
|
143
|
+
nucleotideMutationFrequency: [
|
|
144
|
+
{ mutation: mutation3, proportion: 0.3 },
|
|
145
|
+
{ mutation: mutation1, proportion: 0.1 },
|
|
146
|
+
{ mutation: mutation2, proportion: 0.2 },
|
|
147
|
+
],
|
|
148
|
+
aminoAcidMutationFrequency: [],
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const result = groupMutationDataByLocation(input, 'nucleotide');
|
|
153
|
+
|
|
154
|
+
expect(result).to.have.length(1);
|
|
155
|
+
const location1Data = result[0].data;
|
|
156
|
+
|
|
157
|
+
expect(location1Data.getFirstAxisKeys()).to.deep.equal([mutation1, mutation2, mutation3]);
|
|
158
|
+
});
|
|
159
|
+
});
|
package/src/preact/wastewater/mutationsOverTime/computeWastewaterMutationsOverTimeDataPerLocation.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { queryWastewaterMutationsOverTime, type WastewaterData } from '../../../query/queryWastewaterMutationsOverTime';
|
|
2
|
+
import type { LapisFilter, SequenceType } from '../../../types';
|
|
3
|
+
import { SortedMap2d } from '../../../utils/map2d';
|
|
4
|
+
import { compareTemporal, type TemporalClass } from '../../../utils/temporalClass';
|
|
5
|
+
import {
|
|
6
|
+
BaseMutationOverTimeDataMap,
|
|
7
|
+
type MutationOverTimeDataMap,
|
|
8
|
+
} from '../../mutationsOverTime/MutationOverTimeData';
|
|
9
|
+
import { sortSubstitutionsAndDeletions } from '../../shared/sort/sortSubstitutionsAndDeletions';
|
|
10
|
+
|
|
11
|
+
export async function computeWastewaterMutationsOverTimeDataPerLocation(
|
|
12
|
+
lapis: string,
|
|
13
|
+
lapisFilter: LapisFilter,
|
|
14
|
+
sequenceType: SequenceType,
|
|
15
|
+
signal?: AbortSignal,
|
|
16
|
+
) {
|
|
17
|
+
const data = await queryWastewaterMutationsOverTime(lapis, lapisFilter, signal);
|
|
18
|
+
|
|
19
|
+
return groupMutationDataByLocation(data, sequenceType);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function groupMutationDataByLocation(data: WastewaterData, sequenceType: 'nucleotide' | 'amino acid') {
|
|
23
|
+
const locationMap = new Map<string, MutationOverTimeDataMap<TemporalClass>>();
|
|
24
|
+
for (const row of data) {
|
|
25
|
+
if (!locationMap.has(row.location)) {
|
|
26
|
+
locationMap.set(row.location, new BaseMutationOverTimeDataMap<TemporalClass>());
|
|
27
|
+
}
|
|
28
|
+
const map = locationMap.get(row.location)!;
|
|
29
|
+
|
|
30
|
+
const mutationFrequencies =
|
|
31
|
+
sequenceType === 'nucleotide' ? row.nucleotideMutationFrequency : row.aminoAcidMutationFrequency;
|
|
32
|
+
for (const mutation of mutationFrequencies) {
|
|
33
|
+
map.set(
|
|
34
|
+
mutation.mutation,
|
|
35
|
+
row.date,
|
|
36
|
+
mutation.proportion !== null
|
|
37
|
+
? { proportion: mutation.proportion, count: null, totalCount: null }
|
|
38
|
+
: null,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [...locationMap.entries()].map(([location, data]) => ({
|
|
44
|
+
location,
|
|
45
|
+
data: new SortedMap2d(
|
|
46
|
+
data,
|
|
47
|
+
(a, b) => sortSubstitutionsAndDeletions(a, b),
|
|
48
|
+
(a, b) => compareTemporal(a, b),
|
|
49
|
+
),
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
|
|
3
|
+
import { WastewaterMutationsOverTime, type WastewaterMutationsOverTimeProps } from './wastewater-mutations-over-time';
|
|
4
|
+
import { WISE_DETAILS_ENDPOINT, WISE_LAPIS_URL } from '../../../constants';
|
|
5
|
+
import referenceGenome from '../../../lapisApi/__mockData__/referenceGenome.json';
|
|
6
|
+
import { LapisUrlContext } from '../../LapisUrlContext';
|
|
7
|
+
import { ReferenceGenomeContext } from '../../ReferenceGenomeContext';
|
|
8
|
+
import details from './__mockData__/details.json';
|
|
9
|
+
|
|
10
|
+
const meta: Meta<WastewaterMutationsOverTimeProps> = {
|
|
11
|
+
title: 'Wastewater visualization/Wastewater mutations over time',
|
|
12
|
+
component: WastewaterMutationsOverTime,
|
|
13
|
+
argTypes: {
|
|
14
|
+
width: { control: 'text' },
|
|
15
|
+
height: { control: 'text' },
|
|
16
|
+
lapisFilter: { control: 'object' },
|
|
17
|
+
sequenceType: {
|
|
18
|
+
options: ['nucleotide', 'amino acid'],
|
|
19
|
+
control: { type: 'radio' },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
parameters: {
|
|
23
|
+
fetchMock: {},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
|
|
29
|
+
const Template = {
|
|
30
|
+
render: (args: WastewaterMutationsOverTimeProps) => (
|
|
31
|
+
<LapisUrlContext.Provider value={WISE_LAPIS_URL}>
|
|
32
|
+
<ReferenceGenomeContext.Provider value={referenceGenome}>
|
|
33
|
+
<WastewaterMutationsOverTime
|
|
34
|
+
width={args.width}
|
|
35
|
+
height={args.height}
|
|
36
|
+
lapisFilter={args.lapisFilter}
|
|
37
|
+
sequenceType={args.sequenceType}
|
|
38
|
+
/>
|
|
39
|
+
</ReferenceGenomeContext.Provider>
|
|
40
|
+
</LapisUrlContext.Provider>
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Default: StoryObj<WastewaterMutationsOverTimeProps> = {
|
|
45
|
+
...Template,
|
|
46
|
+
args: {
|
|
47
|
+
width: '100%',
|
|
48
|
+
height: '700px',
|
|
49
|
+
lapisFilter: {},
|
|
50
|
+
sequenceType: 'nucleotide',
|
|
51
|
+
},
|
|
52
|
+
parameters: {
|
|
53
|
+
fetchMock: {
|
|
54
|
+
mocks: [
|
|
55
|
+
{
|
|
56
|
+
matcher: {
|
|
57
|
+
name: 'details',
|
|
58
|
+
url: WISE_DETAILS_ENDPOINT,
|
|
59
|
+
body: {
|
|
60
|
+
fields: ['date', 'location', 'nucleotideMutationFrequency', 'aminoAcidMutationFrequency'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
response: {
|
|
64
|
+
status: 200,
|
|
65
|
+
body: details,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { type Dispatch, type StateUpdater, useContext, useState } from 'preact/hooks';
|
|
3
|
+
import z from 'zod';
|
|
4
|
+
|
|
5
|
+
import { computeWastewaterMutationsOverTimeDataPerLocation } from './computeWastewaterMutationsOverTimeDataPerLocation';
|
|
6
|
+
import { lapisFilterSchema, sequenceTypeSchema } from '../../../types';
|
|
7
|
+
import { LapisUrlContext } from '../../LapisUrlContext';
|
|
8
|
+
import { type ColorScale } from '../../components/color-scale-selector';
|
|
9
|
+
import { ColorScaleSelectorDropdown } from '../../components/color-scale-selector-dropdown';
|
|
10
|
+
import { ErrorBoundary } from '../../components/error-boundary';
|
|
11
|
+
import { Fullscreen } from '../../components/fullscreen';
|
|
12
|
+
import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../../components/info';
|
|
13
|
+
import { LoadingDisplay } from '../../components/loading-display';
|
|
14
|
+
import { NoDataDisplay } from '../../components/no-data-display';
|
|
15
|
+
import { ResizeContainer } from '../../components/resize-container';
|
|
16
|
+
import Tabs from '../../components/tabs';
|
|
17
|
+
import { type MutationOverTimeDataMap } from '../../mutationsOverTime/MutationOverTimeData';
|
|
18
|
+
import MutationsOverTimeGrid from '../../mutationsOverTime/mutations-over-time-grid';
|
|
19
|
+
import { useQuery } from '../../useQuery';
|
|
20
|
+
|
|
21
|
+
const wastewaterMutationOverTimeSchema = z.object({
|
|
22
|
+
lapisFilter: lapisFilterSchema,
|
|
23
|
+
sequenceType: sequenceTypeSchema,
|
|
24
|
+
width: z.string(),
|
|
25
|
+
height: z.string(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type WastewaterMutationsOverTimeProps = z.infer<typeof wastewaterMutationOverTimeSchema>;
|
|
29
|
+
|
|
30
|
+
export const WastewaterMutationsOverTime: FunctionComponent<WastewaterMutationsOverTimeProps> = (componentProps) => {
|
|
31
|
+
const { width, height } = componentProps;
|
|
32
|
+
const size = { height, width };
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<ErrorBoundary size={size} schema={wastewaterMutationOverTimeSchema} componentProps={componentProps}>
|
|
36
|
+
<ResizeContainer size={size}>
|
|
37
|
+
<WastewaterMutationsOverTimeInner {...componentProps} />
|
|
38
|
+
</ResizeContainer>
|
|
39
|
+
</ErrorBoundary>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const WastewaterMutationsOverTimeInner: FunctionComponent<WastewaterMutationsOverTimeProps> = (
|
|
44
|
+
componentProps,
|
|
45
|
+
) => {
|
|
46
|
+
const lapis = useContext(LapisUrlContext);
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
data: mutationOverTimeDataPerLocation,
|
|
50
|
+
error,
|
|
51
|
+
isLoading,
|
|
52
|
+
} = useQuery(
|
|
53
|
+
() =>
|
|
54
|
+
computeWastewaterMutationsOverTimeDataPerLocation(
|
|
55
|
+
lapis,
|
|
56
|
+
componentProps.lapisFilter,
|
|
57
|
+
componentProps.sequenceType,
|
|
58
|
+
),
|
|
59
|
+
[],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (isLoading) {
|
|
63
|
+
return <LoadingDisplay />;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (error !== null) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (mutationOverTimeDataPerLocation.length === 0) {
|
|
71
|
+
return <NoDataDisplay />;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<MutationsOverTimeTabs
|
|
76
|
+
mutationOverTimeDataPerLocation={mutationOverTimeDataPerLocation}
|
|
77
|
+
originalComponentProps={componentProps}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type MutationOverTimeDataPerLocation = {
|
|
83
|
+
location: string;
|
|
84
|
+
data: MutationOverTimeDataMap;
|
|
85
|
+
}[];
|
|
86
|
+
|
|
87
|
+
type MutationOverTimeTabsProps = {
|
|
88
|
+
mutationOverTimeDataPerLocation: MutationOverTimeDataPerLocation;
|
|
89
|
+
originalComponentProps: WastewaterMutationsOverTimeProps;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
93
|
+
mutationOverTimeDataPerLocation,
|
|
94
|
+
originalComponentProps,
|
|
95
|
+
}) => {
|
|
96
|
+
const [colorScale, setColorScale] = useState<ColorScale>({ min: 0, max: 1, color: 'indigo' });
|
|
97
|
+
|
|
98
|
+
const tabs = mutationOverTimeDataPerLocation.map(({ location, data }) => ({
|
|
99
|
+
title: location,
|
|
100
|
+
content: <MutationsOverTimeGrid data={data} colorScale={colorScale} />,
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
const toolbar = (
|
|
104
|
+
<Toolbar
|
|
105
|
+
colorScale={colorScale}
|
|
106
|
+
setColorScale={setColorScale}
|
|
107
|
+
originalComponentProps={originalComponentProps}
|
|
108
|
+
data={mutationOverTimeDataPerLocation}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return <Tabs tabs={tabs} toolbar={toolbar} />;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
type ToolbarProps = {
|
|
116
|
+
colorScale: ColorScale;
|
|
117
|
+
setColorScale: Dispatch<StateUpdater<ColorScale>>;
|
|
118
|
+
originalComponentProps: WastewaterMutationsOverTimeProps;
|
|
119
|
+
data: MutationOverTimeDataPerLocation;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const Toolbar: FunctionComponent<ToolbarProps> = ({ colorScale, setColorScale, originalComponentProps }) => {
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
<ColorScaleSelectorDropdown colorScale={colorScale} setColorScale={setColorScale} />
|
|
126
|
+
<WastewaterMutationsOverTimeInfo originalComponentProps={originalComponentProps} />
|
|
127
|
+
<Fullscreen />
|
|
128
|
+
</>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
type WastewaterMutationsOverTimeInfoProps = {
|
|
133
|
+
originalComponentProps: WastewaterMutationsOverTimeProps;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const WastewaterMutationsOverTimeInfo: FunctionComponent<WastewaterMutationsOverTimeInfoProps> = ({
|
|
137
|
+
originalComponentProps,
|
|
138
|
+
}) => {
|
|
139
|
+
const lapis = useContext(LapisUrlContext);
|
|
140
|
+
return (
|
|
141
|
+
<Info>
|
|
142
|
+
<InfoHeadline1>Info for mutations over time</InfoHeadline1>
|
|
143
|
+
<InfoParagraph> </InfoParagraph>
|
|
144
|
+
<InfoComponentCode
|
|
145
|
+
componentName='wastewater-mutations-over-time'
|
|
146
|
+
params={originalComponentProps}
|
|
147
|
+
lapisUrl={lapis}
|
|
148
|
+
/>
|
|
149
|
+
</Info>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
@@ -16,14 +16,7 @@ import {
|
|
|
16
16
|
type SubstitutionOrDeletionEntry,
|
|
17
17
|
type TemporalGranularity,
|
|
18
18
|
} from '../types';
|
|
19
|
-
import { type
|
|
20
|
-
import {
|
|
21
|
-
type Deletion,
|
|
22
|
-
type DeletionClass,
|
|
23
|
-
type Substitution,
|
|
24
|
-
type SubstitutionClass,
|
|
25
|
-
toSubstitutionOrDeletion,
|
|
26
|
-
} from '../utils/mutations';
|
|
19
|
+
import { type Deletion, type Substitution, toSubstitutionOrDeletion } from '../utils/mutations';
|
|
27
20
|
import {
|
|
28
21
|
compareTemporal,
|
|
29
22
|
dateRangeCompare,
|
|
@@ -41,12 +34,11 @@ export type MutationOverTimeData = {
|
|
|
41
34
|
totalCount: number;
|
|
42
35
|
};
|
|
43
36
|
|
|
44
|
-
export type MutationOverTimeMutationValue = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
>;
|
|
37
|
+
export type MutationOverTimeMutationValue = {
|
|
38
|
+
proportion: number;
|
|
39
|
+
count: number | null;
|
|
40
|
+
totalCount: number | null;
|
|
41
|
+
} | null;
|
|
50
42
|
|
|
51
43
|
const MAX_NUMBER_OF_GRID_COLUMNS = 200;
|
|
52
44
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { queryWastewaterMutationsOverTime } from './queryWastewaterMutationsOverTime';
|
|
4
|
+
import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../vitest.setup';
|
|
5
|
+
import { SubstitutionClass } from '../utils/mutations';
|
|
6
|
+
import { TemporalCache } from '../utils/temporalClass';
|
|
7
|
+
|
|
8
|
+
const temporalCache = TemporalCache.getInstance();
|
|
9
|
+
|
|
10
|
+
describe('queryWastewaterMutationsOverTime', () => {
|
|
11
|
+
it('should fetch data', async () => {
|
|
12
|
+
const lapisFilter = { country: 'Germany' };
|
|
13
|
+
|
|
14
|
+
lapisRequestMocks.details(
|
|
15
|
+
{
|
|
16
|
+
country: 'Germany',
|
|
17
|
+
fields: ['date', 'location', 'nucleotideMutationFrequency', 'aminoAcidMutationFrequency'],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
data: [
|
|
21
|
+
{
|
|
22
|
+
date: '2021-01-01',
|
|
23
|
+
location: 'Germany',
|
|
24
|
+
reference: 'organismA',
|
|
25
|
+
nucleotideMutationFrequency: JSON.stringify({
|
|
26
|
+
A123T: 0.4,
|
|
27
|
+
'123G': null,
|
|
28
|
+
}),
|
|
29
|
+
aminoAcidMutationFrequency: null,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
date: '2021-01-02',
|
|
33
|
+
location: 'Germany',
|
|
34
|
+
reference: 'organismA',
|
|
35
|
+
nucleotideMutationFrequency: null,
|
|
36
|
+
aminoAcidMutationFrequency: JSON.stringify({
|
|
37
|
+
'S:A123T': 0.4,
|
|
38
|
+
'S:123G': 0.1,
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const result = await queryWastewaterMutationsOverTime(DUMMY_LAPIS_URL, lapisFilter);
|
|
46
|
+
|
|
47
|
+
expect(result).to.deep.equal([
|
|
48
|
+
{
|
|
49
|
+
location: 'Germany',
|
|
50
|
+
date: temporalCache.getYearMonthDay('2021-01-01'),
|
|
51
|
+
nucleotideMutationFrequency: [
|
|
52
|
+
{ mutation: SubstitutionClass.parse('A123T'), proportion: 0.4 },
|
|
53
|
+
{ mutation: SubstitutionClass.parse('123G'), proportion: null },
|
|
54
|
+
],
|
|
55
|
+
aminoAcidMutationFrequency: [],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
location: 'Germany',
|
|
59
|
+
date: temporalCache.getYearMonthDay('2021-01-02'),
|
|
60
|
+
nucleotideMutationFrequency: [],
|
|
61
|
+
aminoAcidMutationFrequency: [
|
|
62
|
+
{ mutation: SubstitutionClass.parse('S:A123T'), proportion: 0.4 },
|
|
63
|
+
{ mutation: SubstitutionClass.parse('S:123G'), proportion: 0.1 },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should error when the mutation frequency object is invalid', async () => {
|
|
70
|
+
const lapisFilter = { country: 'Germany' };
|
|
71
|
+
|
|
72
|
+
lapisRequestMocks.details(
|
|
73
|
+
{
|
|
74
|
+
country: 'Germany',
|
|
75
|
+
fields: ['date', 'location', 'nucleotideMutationFrequency', 'aminoAcidMutationFrequency'],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
data: [
|
|
79
|
+
{
|
|
80
|
+
date: '2021-01-01',
|
|
81
|
+
location: 'Germany',
|
|
82
|
+
reference: 'organismA',
|
|
83
|
+
nucleotideMutationFrequency: JSON.stringify({ key: 'not an object of the expected type' }),
|
|
84
|
+
aminoAcidMutationFrequency: null,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
await expect(queryWastewaterMutationsOverTime(DUMMY_LAPIS_URL, lapisFilter)).rejects.toThrowError(
|
|
91
|
+
/^Failed to parse mutation frequency/,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|