@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.
Files changed (35) hide show
  1. package/custom-elements.json +178 -0
  2. package/dist/assets/mutationOverTimeWorker-B1-WrM4b.js.map +1 -0
  3. package/dist/components.d.ts +69 -4
  4. package/dist/components.js +536 -286
  5. package/dist/components.js.map +1 -1
  6. package/dist/style.css +3 -0
  7. package/dist/util.d.ts +20 -4
  8. package/package.json +2 -2
  9. package/src/constants.ts +6 -0
  10. package/src/lapisApi/__mockData__/wiseReferenceGenome.json +9 -0
  11. package/src/lapisApi/lapisApi.ts +17 -0
  12. package/src/lapisApi/lapisTypes.ts +7 -1
  13. package/src/operator/FetchDetailsOperator.ts +28 -0
  14. package/src/preact/components/tabs.tsx +1 -1
  15. package/src/preact/mutationsOverTime/MutationOverTimeData.ts +9 -5
  16. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +5 -3
  17. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +4 -7
  18. package/src/preact/wastewater/mutationsOverTime/__mockData__/details.json +88 -0
  19. package/src/preact/wastewater/mutationsOverTime/computeWastewaterMutationsOverTimeDataPerLocation.spec.ts +159 -0
  20. package/src/preact/wastewater/mutationsOverTime/computeWastewaterMutationsOverTimeDataPerLocation.ts +51 -0
  21. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +71 -0
  22. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +151 -0
  23. package/src/query/queryMutationsOverTime.ts +6 -14
  24. package/src/query/queryWastewaterMutationsOverTime.spec.ts +94 -0
  25. package/src/query/queryWastewaterMutationsOverTime.ts +55 -0
  26. package/src/utils/map2d.ts +39 -0
  27. package/src/web-components/index.ts +1 -0
  28. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +82 -0
  29. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +112 -0
  30. package/src/web-components/wastewaterVisualization/index.ts +1 -0
  31. package/standalone-bundle/assets/{mutationOverTimeWorker-DEybsZ5r.js.map → mutationOverTimeWorker-Cls1J0cl.js.map} +1 -1
  32. package/standalone-bundle/dashboard-components.js +6203 -5996
  33. package/standalone-bundle/dashboard-components.js.map +1 -1
  34. package/standalone-bundle/style.css +1 -1
  35. package/dist/assets/mutationOverTimeWorker-DTv93Ere.js.map +0 -1
package/dist/style.css CHANGED
@@ -3199,6 +3199,9 @@ input.tab:checked + .tab-content,
3199
3199
  .grid {
3200
3200
  display: grid;
3201
3201
  }
3202
+ .contents {
3203
+ display: contents;
3204
+ }
3202
3205
  .hidden {
3203
3206
  display: none;
3204
3207
  }
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-mutations-over-time': MutationsOverTimeComponent;
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-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
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-sequences-by-location': SequencesByLocationComponent;
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-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
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.0",
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": "^2.0.1"
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`;
@@ -0,0 +1,9 @@
1
+ {
2
+ "nucleotideSequences": [
3
+ {
4
+ "name": "main",
5
+ "sequence": "NNN"
6
+ }
7
+ ],
8
+ "genes": []
9
+ }
@@ -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
- export const aggregatedItem = z.object({ count: z.number() }).catchall(z.union([z.string(), z.number(), z.null()]));
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 = Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>;
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
- Temporal,
18
+ T,
15
19
  MutationOverTimeMutationValue
16
20
  > {
17
- constructor(initialContent?: Map2DContents<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>) {
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
- <p>
115
- Count: {value.count} / {value.totalCount} total
116
- </p>
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 { DeletionClass, type SubstitutionClass } from '../../../utils/mutations';
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 instanceof DeletionClass;
16
- const bIsDeletion = b instanceof DeletionClass;
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
+ });
@@ -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
+ };