@genspectrum/dashboard-components 0.11.6 → 0.11.7

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 (26) hide show
  1. package/custom-elements.json +38 -3
  2. package/dist/assets/{mutationOverTimeWorker-CWneD7i5.js.map → mutationOverTimeWorker-DTv93Ere.js.map} +1 -1
  3. package/dist/components.d.ts +50 -16
  4. package/dist/components.js +129 -66
  5. package/dist/components.js.map +1 -1
  6. package/dist/util.d.ts +44 -16
  7. package/package.json +1 -1
  8. package/src/preact/mutations/__mockData__/baselineNucleotideMutations.json +337412 -0
  9. package/src/preact/mutations/__mockData__/overallVariantCount.json +14 -0
  10. package/src/preact/mutations/getMutationsTableData.spec.ts +20 -3
  11. package/src/preact/mutations/getMutationsTableData.ts +37 -2
  12. package/src/preact/mutations/mutations-table.tsx +47 -27
  13. package/src/preact/mutations/mutations.stories.tsx +41 -9
  14. package/src/preact/mutations/mutations.tsx +22 -6
  15. package/src/preact/mutations/queryMutations.ts +28 -8
  16. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay.ts +11077 -3062
  17. package/src/preact/mutationsOverTime/__mockData__/byWeek.ts +3883 -6606
  18. package/src/preact/mutationsOverTime/__mockData__/defaultMockData.ts +17624 -2203
  19. package/src/preact/mutationsOverTime/mutations-over-time.tsx +1 -1
  20. package/src/query/queryMutationsOverTime.spec.ts +144 -4
  21. package/src/query/queryMutationsOverTime.ts +17 -1
  22. package/src/web-components/visualization/gs-mutations.stories.ts +62 -4
  23. package/src/web-components/visualization/gs-mutations.tsx +44 -0
  24. package/standalone-bundle/assets/{mutationOverTimeWorker-x1ipPFL0.js.map → mutationOverTimeWorker-DEybsZ5r.js.map} +1 -1
  25. package/standalone-bundle/dashboard-components.js +2515 -2464
  26. package/standalone-bundle/dashboard-components.js.map +1 -1
@@ -0,0 +1,14 @@
1
+ {
2
+ "data": [
3
+ {
4
+ "count": 16616
5
+ }
6
+ ],
7
+ "info": {
8
+ "dataVersion": "1736354557",
9
+ "requestId": "a11bb8e3-cd57-4178-8f51-7ff1210ffebf",
10
+ "requestInfo": "sars_cov-2_nextstrain_open on lapis.cov-spectrum.org at 2025-01-09T08:05:35.534992279",
11
+ "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.",
12
+ "lapisVersion": "0.3.10"
13
+ }
14
+ }
@@ -22,9 +22,24 @@ describe('getMutationsTableData', () => {
22
22
 
23
23
  const proportionInterval = { min: 0, max: 1 };
24
24
 
25
- const result = getMutationsTableData(data, proportionInterval);
25
+ const result = getMutationsTableData(data, data, 2, proportionInterval);
26
26
 
27
- expect(result).toEqual(data);
27
+ expect(result).toEqual([
28
+ {
29
+ type: 'substitution' as const,
30
+ mutation: new SubstitutionClass('segment1', 'A', 'T', 123),
31
+ count: 1,
32
+ proportion: 0.1,
33
+ jaccardSimilarity: 0.5,
34
+ },
35
+ {
36
+ type: 'deletion' as const,
37
+ mutation: new DeletionClass('segment2', 'C', 123),
38
+ count: 2,
39
+ proportion: 0.2,
40
+ jaccardSimilarity: 1,
41
+ },
42
+ ]);
28
43
  });
29
44
 
30
45
  test('should filter out data below/above proportionInterval', () => {
@@ -64,7 +79,7 @@ describe('getMutationsTableData', () => {
64
79
 
65
80
  const proportionInterval = { min: 0.05, max: 0.9 };
66
81
 
67
- const result = getMutationsTableData(data, proportionInterval);
82
+ const result = getMutationsTableData(data, data, 2, proportionInterval);
68
83
 
69
84
  expect(result).toEqual([
70
85
  {
@@ -72,12 +87,14 @@ describe('getMutationsTableData', () => {
72
87
  type: 'substitution',
73
88
  count: 1,
74
89
  proportion: inInterval,
90
+ jaccardSimilarity: 0.5,
75
91
  },
76
92
  {
77
93
  mutation: deletionInInterval,
78
94
  type: 'deletion',
79
95
  count: 2,
80
96
  proportion: inInterval,
97
+ jaccardSimilarity: 1,
81
98
  },
82
99
  ]);
83
100
  });
@@ -1,12 +1,17 @@
1
1
  import { type SubstitutionOrDeletionEntry } from '../../types';
2
2
  import { type ProportionInterval } from '../components/proportion-selector';
3
3
 
4
- export function getMutationsTableData(data: SubstitutionOrDeletionEntry[], proportionInterval: ProportionInterval) {
4
+ export function getMutationsTableData(
5
+ data: SubstitutionOrDeletionEntry[],
6
+ baselineSubstitutionsOrDeletions: SubstitutionOrDeletionEntry[] | undefined,
7
+ overallVariantCount: number,
8
+ proportionInterval: ProportionInterval,
9
+ ) {
5
10
  const byProportion = (mutationEntry: SubstitutionOrDeletionEntry) => {
6
11
  return mutationEntry.proportion >= proportionInterval.min && mutationEntry.proportion <= proportionInterval.max;
7
12
  };
8
13
 
9
- return data.filter(byProportion).map((mutationEntry) => {
14
+ const tableData = data.filter(byProportion).map((mutationEntry) => {
10
15
  return {
11
16
  mutation: mutationEntry.mutation,
12
17
  type: mutationEntry.type,
@@ -14,4 +19,34 @@ export function getMutationsTableData(data: SubstitutionOrDeletionEntry[], propo
14
19
  proportion: mutationEntry.proportion,
15
20
  };
16
21
  });
22
+
23
+ if (baselineSubstitutionsOrDeletions === undefined) {
24
+ return tableData;
25
+ }
26
+
27
+ const baselineMutationCounts = baselineSubstitutionsOrDeletions.reduce((baselineMutationCounts, mutation) => {
28
+ baselineMutationCounts.set(mutation.mutation.code, mutation.count);
29
+ return baselineMutationCounts;
30
+ }, new Map<string, number>());
31
+
32
+ return tableData.map((datum) => {
33
+ const baselineMutationCount = baselineMutationCounts.get(datum.mutation.code) || 0;
34
+ const jaccardSimilarity = calculateJaccardSimilarity(overallVariantCount, baselineMutationCount, datum.count);
35
+
36
+ return {
37
+ ...datum,
38
+ jaccardSimilarity,
39
+ };
40
+ });
17
41
  }
42
+
43
+ /**
44
+ * Taken from https://github.com/GenSpectrum/cov-spectrum-website/blob/9372a618dfd2d0dc5106fb07dce4e9f02fe24471/src/components/VariantMutations.tsx#L484-L486
45
+ *
46
+ * @param variantCount The number of sequences of the variant
47
+ * @param mutationCount The number of sequences with the mutation
48
+ * @param variantWithMutationCount The number of sequences that belong to the variant and have the mutation
49
+ */
50
+ const calculateJaccardSimilarity = (variantCount: number, mutationCount: number, variantWithMutationCount: number) => {
51
+ return variantWithMutationCount / (variantCount + mutationCount - variantWithMutationCount);
52
+ };
@@ -1,4 +1,5 @@
1
1
  import { type FunctionComponent } from 'preact';
2
+ import { useMemo } from 'preact/hooks';
2
3
 
3
4
  import { getMutationsTableData } from './getMutationsTableData';
4
5
  import { type SubstitutionOrDeletionEntry } from '../../types';
@@ -10,41 +11,60 @@ import { formatProportion } from '../shared/table/formatProportion';
10
11
 
11
12
  export interface MutationsTableProps {
12
13
  data: SubstitutionOrDeletionEntry[];
14
+ baselineSubstitutionsOrDeletions: SubstitutionOrDeletionEntry[] | undefined;
15
+ overallVariantCount: number;
13
16
  proportionInterval: ProportionInterval;
14
17
  pageSize: boolean | number;
15
18
  }
16
19
 
17
- const MutationsTable: FunctionComponent<MutationsTableProps> = ({ data, proportionInterval, pageSize }) => {
18
- const getHeaders = () => {
19
- return [
20
- {
21
- name: 'Mutation',
22
- sort: {
23
- compare: (a: SubstitutionClass | DeletionClass, b: SubstitutionClass | DeletionClass) => {
24
- return sortSubstitutionsAndDeletions(a, b);
25
- },
20
+ const MutationsTable: FunctionComponent<MutationsTableProps> = ({
21
+ data,
22
+ baselineSubstitutionsOrDeletions,
23
+ overallVariantCount,
24
+ proportionInterval,
25
+ pageSize,
26
+ }) => {
27
+ const headers = [
28
+ {
29
+ name: 'Mutation',
30
+ sort: {
31
+ compare: (a: SubstitutionClass | DeletionClass, b: SubstitutionClass | DeletionClass) => {
32
+ return sortSubstitutionsAndDeletions(a, b);
26
33
  },
27
- formatter: (cell: SubstitutionClass | DeletionClass) => cell.toString(),
28
34
  },
29
- {
30
- name: 'Type',
31
- sort: true,
32
- },
33
- {
34
- name: 'Count',
35
- sort: true,
36
- },
37
- {
38
- name: 'Proportion',
39
- sort: true,
40
- formatter: (cell: number) => formatProportion(cell),
41
- },
42
- ];
43
- };
35
+ formatter: (cell: SubstitutionClass | DeletionClass) => cell.toString(),
36
+ },
37
+ {
38
+ name: 'Type',
39
+ sort: true,
40
+ },
41
+ {
42
+ name: 'Count',
43
+ sort: true,
44
+ },
45
+ {
46
+ name: 'Proportion',
47
+ sort: true,
48
+ formatter: (cell: number) => formatProportion(cell),
49
+ },
50
+ ];
51
+ if (baselineSubstitutionsOrDeletions !== undefined) {
52
+ headers.push({
53
+ name: 'Jaccard similarity',
54
+ sort: true,
55
+ formatter: (cell: number) => cell.toFixed(2),
56
+ });
57
+ }
44
58
 
45
- const tableData = getMutationsTableData(data, proportionInterval).map((row) => Object.values(row));
59
+ const tableData = useMemo(
60
+ () =>
61
+ getMutationsTableData(data, baselineSubstitutionsOrDeletions, overallVariantCount, proportionInterval).map(
62
+ (row) => Object.values(row),
63
+ ),
64
+ [data, baselineSubstitutionsOrDeletions, overallVariantCount, proportionInterval],
65
+ );
46
66
 
47
- return <Table data={tableData} columns={getHeaders()} pageSize={pageSize} />;
67
+ return <Table data={tableData} columns={headers} pageSize={pageSize} />;
48
68
  };
49
69
 
50
70
  export default MutationsTable;
@@ -4,8 +4,15 @@ import { expect, waitFor, within } from '@storybook/test';
4
4
  import nucleotideInsertions from './__mockData__/nucleotideInsertions.json';
5
5
  import nucleotideMutations from './__mockData__/nucleotideMutations.json';
6
6
  import { Mutations, type MutationsProps } from './mutations';
7
- import { LAPIS_URL, NUCLEOTIDE_INSERTIONS_ENDPOINT, NUCLEOTIDE_MUTATIONS_ENDPOINT } from '../../constants';
7
+ import {
8
+ AGGREGATED_ENDPOINT,
9
+ LAPIS_URL,
10
+ NUCLEOTIDE_INSERTIONS_ENDPOINT,
11
+ NUCLEOTIDE_MUTATIONS_ENDPOINT,
12
+ } from '../../constants';
8
13
  import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
14
+ import baselineNucleotideMutations from '../../preact/mutations/__mockData__/baselineNucleotideMutations.json';
15
+ import overallVariantCount from '../../preact/mutations/__mockData__/overallVariantCount.json';
9
16
  import { LapisUrlContext } from '../LapisUrlContext';
10
17
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
11
18
 
@@ -34,14 +41,7 @@ const Template = {
34
41
  render: (args: MutationsProps) => (
35
42
  <LapisUrlContext.Provider value={LAPIS_URL}>
36
43
  <ReferenceGenomeContext.Provider value={referenceGenome}>
37
- <Mutations
38
- lapisFilter={args.lapisFilter}
39
- sequenceType={args.sequenceType}
40
- views={args.views}
41
- width={args.width}
42
- height={args.height}
43
- pageSize={args.pageSize}
44
- />
44
+ <Mutations {...args} />
45
45
  </ReferenceGenomeContext.Provider>
46
46
  </LapisUrlContext.Provider>
47
47
  ),
@@ -51,6 +51,7 @@ export const Default: StoryObj<MutationsProps> = {
51
51
  ...Template,
52
52
  args: {
53
53
  lapisFilter: { country: 'Switzerland', pangoLineage: 'B.1.1.7', dateTo: '2022-01-01' },
54
+ baselineLapisFilter: { country: 'Switzerland', dateTo: '2022-01-01' },
54
55
  sequenceType: 'nucleotide',
55
56
  views: ['grid', 'table', 'insertions'],
56
57
  width: '100%',
@@ -76,6 +77,37 @@ export const Default: StoryObj<MutationsProps> = {
76
77
  body: nucleotideMutations,
77
78
  },
78
79
  },
80
+ {
81
+ matcher: {
82
+ name: 'baselineNucleotideMutations',
83
+ url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
84
+ body: {
85
+ country: 'Switzerland',
86
+ dateTo: '2022-01-01',
87
+ minProportion: 0,
88
+ },
89
+ },
90
+ response: {
91
+ status: 200,
92
+ body: baselineNucleotideMutations,
93
+ },
94
+ },
95
+ {
96
+ matcher: {
97
+ name: 'overallVariantCount',
98
+ url: AGGREGATED_ENDPOINT,
99
+ body: {
100
+ country: 'Switzerland',
101
+ pangoLineage: 'B.1.1.7',
102
+ dateTo: '2022-01-01',
103
+ fields: [],
104
+ },
105
+ },
106
+ response: {
107
+ status: 200,
108
+ body: overallVariantCount,
109
+ },
110
+ },
79
111
  {
80
112
  matcher: {
81
113
  name: 'nucleotideInsertions',
@@ -7,7 +7,7 @@ import { getMutationsTableData } from './getMutationsTableData';
7
7
  import { MutationsGrid } from './mutations-grid';
8
8
  import { InsertionsTable } from './mutations-insertions-table';
9
9
  import MutationsTable from './mutations-table';
10
- import { filterMutationsData, queryMutationsData } from './queryMutations';
10
+ import { filterMutationsData, type QueriedMutationsData, queryMutationsData } from './queryMutations';
11
11
  import {
12
12
  type InsertionEntry,
13
13
  lapisFilterSchema,
@@ -35,6 +35,7 @@ export type MutationsView = z.infer<typeof mutationsViewSchema>;
35
35
 
36
36
  const mutationsPropsSchema = z.object({
37
37
  lapisFilter: lapisFilterSchema,
38
+ baselineLapisFilter: lapisFilterSchema.optional(),
38
39
  sequenceType: sequenceTypeSchema,
39
40
  views: mutationsViewSchema.array(),
40
41
  pageSize: z.union([z.boolean(), z.number()]),
@@ -58,11 +59,11 @@ export const Mutations: FunctionComponent<MutationsProps> = (componentProps) =>
58
59
 
59
60
  export const MutationsInner: FunctionComponent<MutationsProps> = (componentProps) => {
60
61
  const lapis = useContext(LapisUrlContext);
61
- const { lapisFilter, sequenceType } = componentProps;
62
+ const { lapisFilter, baselineLapisFilter, sequenceType } = componentProps;
62
63
 
63
64
  const { data, error, isLoading } = useQuery(async () => {
64
- return queryMutationsData(lapisFilter, sequenceType, lapis);
65
- }, [lapisFilter, sequenceType, lapis]);
65
+ return queryMutationsData(lapisFilter, baselineLapisFilter, sequenceType, lapis);
66
+ }, [lapisFilter, baselineLapisFilter, sequenceType, lapis]);
66
67
 
67
68
  if (isLoading) {
68
69
  return <LoadingDisplay />;
@@ -80,7 +81,7 @@ export const MutationsInner: FunctionComponent<MutationsProps> = (componentProps
80
81
  };
81
82
 
82
83
  type MutationTabsProps = {
83
- mutationsData: { insertions: InsertionEntry[]; substitutionsOrDeletions: SubstitutionOrDeletionEntry[] };
84
+ mutationsData: QueriedMutationsData;
84
85
  originalComponentProps: MutationsProps;
85
86
  };
86
87
 
@@ -103,6 +104,8 @@ const MutationsTabs: FunctionComponent<MutationTabsProps> = ({ mutationsData, or
103
104
  content: (
104
105
  <MutationsTable
105
106
  data={filteredData.tableData}
107
+ baselineSubstitutionsOrDeletions={mutationsData.baselineSubstitutionsOrDeletions}
108
+ overallVariantCount={mutationsData.overallVariantCount}
106
109
  proportionInterval={proportionInterval}
107
110
  pageSize={originalComponentProps.pageSize}
108
111
  />
@@ -143,6 +146,8 @@ const MutationsTabs: FunctionComponent<MutationTabsProps> = ({ mutationsData, or
143
146
  proportionInterval={proportionInterval}
144
147
  setProportionInterval={setProportionInterval}
145
148
  originalComponentProps={originalComponentProps}
149
+ baselineSubstitutionsOrDeletions={mutationsData.baselineSubstitutionsOrDeletions}
150
+ overallVariantCount={mutationsData.overallVariantCount}
146
151
  />
147
152
  );
148
153
 
@@ -159,6 +164,8 @@ type ToolbarProps = {
159
164
  proportionInterval: ProportionInterval;
160
165
  setProportionInterval: Dispatch<StateUpdater<ProportionInterval>>;
161
166
  originalComponentProps: MutationsProps;
167
+ baselineSubstitutionsOrDeletions: SubstitutionOrDeletionEntry[] | undefined;
168
+ overallVariantCount: number;
162
169
  };
163
170
 
164
171
  const Toolbar: FunctionComponent<ToolbarProps> = ({
@@ -171,6 +178,8 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
171
178
  proportionInterval,
172
179
  setProportionInterval,
173
180
  originalComponentProps,
181
+ baselineSubstitutionsOrDeletions,
182
+ overallVariantCount,
174
183
  }) => {
175
184
  return (
176
185
  <>
@@ -190,7 +199,14 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
190
199
  />
191
200
  <CsvDownloadButton
192
201
  className='mx-1 btn btn-xs'
193
- getData={() => getMutationsTableData(filteredData.tableData, proportionInterval)}
202
+ getData={() =>
203
+ getMutationsTableData(
204
+ filteredData.tableData,
205
+ baselineSubstitutionsOrDeletions,
206
+ overallVariantCount,
207
+ proportionInterval,
208
+ )
209
+ }
194
210
  filename='substitutions_and_deletions.csv'
195
211
  />
196
212
  </>
@@ -1,30 +1,49 @@
1
+ import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
1
2
  import { queryInsertions } from '../../query/queryInsertions';
2
3
  import { querySubstitutionsOrDeletions } from '../../query/querySubstitutionsOrDeletions';
3
- import {
4
- type InsertionEntry,
5
- type LapisFilter,
6
- type MutationEntry,
7
- type SubstitutionOrDeletionEntry,
8
- } from '../../types';
4
+ import type { InsertionEntry, LapisFilter, MutationEntry, SubstitutionOrDeletionEntry } from '../../types';
9
5
  import { type DisplayedMutationType } from '../components/mutation-type-selector';
10
6
  import { type DisplayedSegment } from '../components/segment-selector';
11
7
 
8
+ export type QueriedMutationsData = {
9
+ insertions: InsertionEntry[];
10
+ substitutionsOrDeletions: SubstitutionOrDeletionEntry[];
11
+ baselineSubstitutionsOrDeletions: SubstitutionOrDeletionEntry[] | undefined;
12
+ overallVariantCount: number;
13
+ };
14
+
12
15
  export async function queryMutationsData(
13
16
  lapisFilter: LapisFilter,
17
+ baselineLapisFilter: LapisFilter | undefined,
14
18
  sequenceType: 'nucleotide' | 'amino acid',
15
19
  lapis: string,
16
- ) {
20
+ ): Promise<QueriedMutationsData> {
17
21
  const substitutionsOrDeletions = (await querySubstitutionsOrDeletions(lapisFilter, sequenceType, lapis)).content;
22
+ const baselineSubstitutionsOrDeletions =
23
+ baselineLapisFilter === undefined
24
+ ? undefined
25
+ : (await querySubstitutionsOrDeletions(baselineLapisFilter, sequenceType, lapis)).content;
18
26
  const insertions = (await queryInsertions(lapisFilter, sequenceType, lapis)).content;
19
27
 
28
+ const aggregatedData = await new FetchAggregatedOperator<Record<string, string | null | number>>(
29
+ lapisFilter,
30
+ [],
31
+ ).evaluate(lapis);
32
+ if (aggregatedData.content.length === 0) {
33
+ throw new Error('No aggregated data found for the given filters - did LAPIS respond properly?');
34
+ }
35
+ const overallVariantCount = aggregatedData.content[0].count;
36
+
20
37
  return {
21
38
  substitutionsOrDeletions,
39
+ baselineSubstitutionsOrDeletions,
22
40
  insertions,
41
+ overallVariantCount,
23
42
  };
24
43
  }
25
44
 
26
45
  export function filterMutationsData(
27
- data: { insertions: InsertionEntry[]; substitutionsOrDeletions: SubstitutionOrDeletionEntry[] },
46
+ data: QueriedMutationsData,
28
47
  displayedSegments: DisplayedSegment[],
29
48
  displayedMutationTypes: DisplayedMutationType[],
30
49
  ) {
@@ -51,5 +70,6 @@ export function filterMutationsData(
51
70
  insertions: data.insertions.filter(bySelectedSegments),
52
71
  tableData: filteredSubstitutionsOrDeletions.filter(byDisplayedMutationTypes),
53
72
  gridData: filteredSubstitutionsOrDeletions,
73
+ baselineSubstitutionsOrDeletions: data.baselineSubstitutionsOrDeletions,
54
74
  };
55
75
  }