@genspectrum/dashboard-components 0.13.0 → 0.13.1
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 +69 -4
- package/dist/components.js +536 -286
- package/dist/components.js.map +1 -1
- package/dist/style.css +3 -0
- package/dist/util.d.ts +20 -4
- 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/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/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 +6203 -5996
- 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
package/dist/style.css
CHANGED
package/dist/util.d.ts
CHANGED
|
@@ -836,6 +836,22 @@ declare global {
|
|
|
836
836
|
}
|
|
837
837
|
|
|
838
838
|
|
|
839
|
+
declare global {
|
|
840
|
+
interface HTMLElementTagNameMap {
|
|
841
|
+
'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
declare global {
|
|
847
|
+
namespace JSX {
|
|
848
|
+
interface IntrinsicElements {
|
|
849
|
+
'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
|
|
839
855
|
declare global {
|
|
840
856
|
interface HTMLElementTagNameMap {
|
|
841
857
|
'gs-mutation-comparison-component': MutationComparisonComponent;
|
|
@@ -934,7 +950,7 @@ declare global {
|
|
|
934
950
|
|
|
935
951
|
declare global {
|
|
936
952
|
interface HTMLElementTagNameMap {
|
|
937
|
-
'gs-
|
|
953
|
+
'gs-sequences-by-location': SequencesByLocationComponent;
|
|
938
954
|
}
|
|
939
955
|
}
|
|
940
956
|
|
|
@@ -942,7 +958,7 @@ declare global {
|
|
|
942
958
|
declare global {
|
|
943
959
|
namespace JSX {
|
|
944
960
|
interface IntrinsicElements {
|
|
945
|
-
'gs-
|
|
961
|
+
'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
946
962
|
}
|
|
947
963
|
}
|
|
948
964
|
}
|
|
@@ -950,7 +966,7 @@ declare global {
|
|
|
950
966
|
|
|
951
967
|
declare global {
|
|
952
968
|
interface HTMLElementTagNameMap {
|
|
953
|
-
'gs-
|
|
969
|
+
'gs-mutations-over-time': MutationsOverTimeComponent;
|
|
954
970
|
}
|
|
955
971
|
}
|
|
956
972
|
|
|
@@ -958,7 +974,7 @@ declare global {
|
|
|
958
974
|
declare global {
|
|
959
975
|
namespace JSX {
|
|
960
976
|
interface IntrinsicElements {
|
|
961
|
-
'gs-
|
|
977
|
+
'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
962
978
|
}
|
|
963
979
|
}
|
|
964
980
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genspectrum/dashboard-components",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "GenSpectrum web components for building dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -142,6 +142,6 @@
|
|
|
142
142
|
"typescript": "~5.7.2",
|
|
143
143
|
"vite": "^6.0.3",
|
|
144
144
|
"vite-plugin-dts": "^4.0.3",
|
|
145
|
-
"vitest": "^
|
|
145
|
+
"vitest": "^3.0.2"
|
|
146
146
|
}
|
|
147
147
|
}
|
package/src/constants.ts
CHANGED
|
@@ -5,3 +5,9 @@ export const NUCLEOTIDE_MUTATIONS_ENDPOINT = `${LAPIS_URL}/sample/nucleotideMuta
|
|
|
5
5
|
export const AMINO_ACID_MUTATIONS_ENDPOINT = `${LAPIS_URL}/sample/aminoAcidMutations`;
|
|
6
6
|
export const NUCLEOTIDE_INSERTIONS_ENDPOINT = `${LAPIS_URL}/sample/nucleotideInsertions`;
|
|
7
7
|
export const REFERENCE_GENOME_ENDPOINT = `${LAPIS_URL}/sample/referenceGenome`;
|
|
8
|
+
|
|
9
|
+
// WISE Wastewater
|
|
10
|
+
// This is a special instance for storing Swiss wastewater data generated by the WISE consortium
|
|
11
|
+
export const WISE_LAPIS_URL = 'https://api.wise-loculus.genspectrum.org/rsv';
|
|
12
|
+
export const WISE_DETAILS_ENDPOINT = `${WISE_LAPIS_URL}/sample/details`;
|
|
13
|
+
export const WISE_REFERENCE_GENOME_ENDPOINT = `${WISE_LAPIS_URL}/sample/referenceGenome`;
|
package/src/lapisApi/lapisApi.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { referenceGenomeResponse } from './ReferenceGenome';
|
|
2
2
|
import {
|
|
3
3
|
aggregatedResponse,
|
|
4
|
+
detailsResponse,
|
|
4
5
|
insertionsResponse,
|
|
5
6
|
type LapisBaseRequest,
|
|
6
7
|
lapisError,
|
|
@@ -51,6 +52,21 @@ export async function fetchAggregated(lapisUrl: string, body: LapisBaseRequest,
|
|
|
51
52
|
return aggregatedResponse.parse(await response.json());
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
export async function fetchDetails(lapisUrl: string, body: LapisBaseRequest, signal?: AbortSignal) {
|
|
56
|
+
const response = await fetch(detailsEndpoint(lapisUrl), {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
signal,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await handleErrors(response, 'aggregated data');
|
|
66
|
+
|
|
67
|
+
return detailsResponse.parse(await response.json());
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
export async function fetchInsertions(
|
|
55
71
|
lapisUrl: string,
|
|
56
72
|
body: LapisBaseRequest,
|
|
@@ -163,6 +179,7 @@ const handleErrors = async (response: Response, requestedData: string) => {
|
|
|
163
179
|
};
|
|
164
180
|
|
|
165
181
|
export const aggregatedEndpoint = (lapisUrl: string) => `${lapisUrl}/sample/aggregated`;
|
|
182
|
+
export const detailsEndpoint = (lapisUrl: string) => `${lapisUrl}/sample/details`;
|
|
166
183
|
export const insertionsEndpoint = (lapisUrl: string, sequenceType: SequenceType) => {
|
|
167
184
|
return sequenceType === 'amino acid'
|
|
168
185
|
? `${lapisUrl}/sample/aminoAcidInsertions`
|
|
@@ -41,10 +41,16 @@ const insertionCount = z.object({
|
|
|
41
41
|
});
|
|
42
42
|
export const insertionsResponse = makeLapisResponse(z.array(insertionCount));
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
const baseResponseValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
45
|
+
|
|
46
|
+
export const aggregatedItem = z.object({ count: z.number() }).catchall(baseResponseValueSchema);
|
|
45
47
|
export const aggregatedResponse = makeLapisResponse(z.array(aggregatedItem));
|
|
46
48
|
export type AggregatedItem = z.infer<typeof aggregatedItem>;
|
|
47
49
|
|
|
50
|
+
export const detailsItem = z.object({}).catchall(baseResponseValueSchema);
|
|
51
|
+
export const detailsResponse = makeLapisResponse(z.array(detailsItem));
|
|
52
|
+
export type DetailsItem = z.infer<typeof detailsItem>;
|
|
53
|
+
|
|
48
54
|
function makeLapisResponse<T extends ZodTypeAny>(data: T) {
|
|
49
55
|
return z.object({
|
|
50
56
|
data,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Dataset } from './Dataset';
|
|
2
|
+
import { type Operator } from './Operator';
|
|
3
|
+
import { fetchDetails } from '../lapisApi/lapisApi';
|
|
4
|
+
import { type LapisFilter } from '../types';
|
|
5
|
+
|
|
6
|
+
type Details<Fields extends string> = { [field in Fields]: string | number | boolean | null };
|
|
7
|
+
|
|
8
|
+
export class FetchDetailsOperator<Fields extends string> implements Operator<Details<Fields>> {
|
|
9
|
+
constructor(
|
|
10
|
+
private filter: LapisFilter,
|
|
11
|
+
private fields: Fields[] = [],
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
async evaluate(lapisUrl: string, signal?: AbortSignal): Promise<Dataset<Details<Fields>>> {
|
|
15
|
+
const detailsResponse = (
|
|
16
|
+
await fetchDetails(
|
|
17
|
+
lapisUrl,
|
|
18
|
+
{
|
|
19
|
+
...this.filter,
|
|
20
|
+
fields: this.fields,
|
|
21
|
+
},
|
|
22
|
+
signal,
|
|
23
|
+
)
|
|
24
|
+
).data;
|
|
25
|
+
|
|
26
|
+
return { content: detailsResponse as Details<Fields>[] };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -16,7 +16,7 @@ const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
|
|
|
16
16
|
const [activeTab, setActiveTab] = useState(tabs[0]?.title);
|
|
17
17
|
|
|
18
18
|
const tabElements = (
|
|
19
|
-
<div className='flex flex-row'>
|
|
19
|
+
<div className='flex flex-row flex-wrap'>
|
|
20
20
|
{tabs.map((tab) => {
|
|
21
21
|
return (
|
|
22
22
|
<button
|
|
@@ -5,16 +5,20 @@ import {
|
|
|
5
5
|
} from '../../query/queryMutationsOverTime';
|
|
6
6
|
import { type Map2d, Map2dBase, type Map2DContents } from '../../utils/map2d';
|
|
7
7
|
import type { Deletion, Substitution } from '../../utils/mutations';
|
|
8
|
-
import type { Temporal } from '../../utils/temporalClass';
|
|
8
|
+
import type { Temporal, TemporalClass } from '../../utils/temporalClass';
|
|
9
9
|
|
|
10
|
-
export type MutationOverTimeDataMap
|
|
10
|
+
export type MutationOverTimeDataMap<T extends Temporal | TemporalClass = Temporal> = Map2d<
|
|
11
|
+
Substitution | Deletion,
|
|
12
|
+
T,
|
|
13
|
+
MutationOverTimeMutationValue
|
|
14
|
+
>;
|
|
11
15
|
|
|
12
|
-
export class BaseMutationOverTimeDataMap extends Map2dBase<
|
|
16
|
+
export class BaseMutationOverTimeDataMap<T extends Temporal | TemporalClass = Temporal> extends Map2dBase<
|
|
13
17
|
Substitution | Deletion,
|
|
14
|
-
|
|
18
|
+
T,
|
|
15
19
|
MutationOverTimeMutationValue
|
|
16
20
|
> {
|
|
17
|
-
constructor(initialContent?: Map2DContents<Substitution | Deletion,
|
|
21
|
+
constructor(initialContent?: Map2DContents<Substitution | Deletion, T, MutationOverTimeMutationValue>) {
|
|
18
22
|
super(serializeSubstitutionOrDeletion, serializeTemporal, initialContent);
|
|
19
23
|
}
|
|
20
24
|
}
|
|
@@ -111,9 +111,11 @@ const ProportionCell: FunctionComponent<{
|
|
|
111
111
|
) : (
|
|
112
112
|
<>
|
|
113
113
|
<p>Proportion: {formatProportion(value.proportion)}</p>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
{value.count !== null && value.totalCount !== null && (
|
|
115
|
+
<p>
|
|
116
|
+
Count: {value.count} / {value.totalCount} total
|
|
117
|
+
</p>
|
|
118
|
+
)}
|
|
117
119
|
</>
|
|
118
120
|
)}
|
|
119
121
|
</div>
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Deletion, Substitution } from '../../../utils/mutations';
|
|
2
2
|
|
|
3
|
-
export const sortSubstitutionsAndDeletions = (
|
|
4
|
-
a: SubstitutionClass | DeletionClass,
|
|
5
|
-
b: SubstitutionClass | DeletionClass,
|
|
6
|
-
) => {
|
|
3
|
+
export const sortSubstitutionsAndDeletions = (a: Substitution | Deletion, b: Substitution | Deletion) => {
|
|
7
4
|
if (a.segment !== b.segment) {
|
|
8
5
|
return compareSegments(a.segment, b.segment);
|
|
9
6
|
}
|
|
@@ -12,8 +9,8 @@ export const sortSubstitutionsAndDeletions = (
|
|
|
12
9
|
return comparePositions(a.position, b.position);
|
|
13
10
|
}
|
|
14
11
|
|
|
15
|
-
const aIsDeletion = a
|
|
16
|
-
const bIsDeletion = b
|
|
12
|
+
const aIsDeletion = a.type === 'deletion';
|
|
13
|
+
const bIsDeletion = b.type === 'deletion';
|
|
17
14
|
|
|
18
15
|
if (aIsDeletion !== bIsDeletion) {
|
|
19
16
|
return aIsDeletion ? 1 : -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
|
+
};
|