@genspectrum/dashboard-components 0.11.6 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/custom-elements.json +50 -15
  2. package/dist/{dateRangeOption-Bh2p78z0.js → LocationChangedEvent-CORvQvXv.js} +11 -1
  3. package/dist/LocationChangedEvent-CORvQvXv.js.map +1 -0
  4. package/dist/assets/{mutationOverTimeWorker-CWneD7i5.js.map → mutationOverTimeWorker-DTv93Ere.js.map} +1 -1
  5. package/dist/components.d.ts +79 -51
  6. package/dist/components.js +3951 -621
  7. package/dist/components.js.map +1 -1
  8. package/dist/style.css +151 -4
  9. package/dist/util.d.ts +78 -44
  10. package/dist/util.js +2 -1
  11. package/package.json +2 -1
  12. package/src/preact/components/csv-download-button.tsx +2 -2
  13. package/src/preact/downshift_types.d.ts +3 -0
  14. package/src/preact/locationFilter/LocationChangedEvent.ts +11 -0
  15. package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +5 -5
  16. package/src/preact/locationFilter/fetchAutocompletionList.ts +9 -2
  17. package/src/preact/locationFilter/location-filter.stories.tsx +94 -10
  18. package/src/preact/locationFilter/location-filter.tsx +183 -62
  19. package/src/preact/mutationFilter/mutation-filter-info.tsx +73 -10
  20. package/src/preact/mutations/__mockData__/baselineNucleotideMutations.json +337412 -0
  21. package/src/preact/mutations/__mockData__/overallVariantCount.json +14 -0
  22. package/src/preact/mutations/getMutationsTableData.spec.ts +20 -3
  23. package/src/preact/mutations/getMutationsTableData.ts +37 -2
  24. package/src/preact/mutations/mutations-table.tsx +47 -27
  25. package/src/preact/mutations/mutations.stories.tsx +41 -9
  26. package/src/preact/mutations/mutations.tsx +22 -6
  27. package/src/preact/mutations/queryMutations.ts +28 -8
  28. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay.ts +11077 -3062
  29. package/src/preact/mutationsOverTime/__mockData__/byWeek.ts +3883 -6606
  30. package/src/preact/mutationsOverTime/__mockData__/defaultMockData.ts +17624 -2203
  31. package/src/preact/mutationsOverTime/mutations-over-time.tsx +1 -1
  32. package/src/query/queryMutationsOverTime.spec.ts +144 -4
  33. package/src/query/queryMutationsOverTime.ts +17 -1
  34. package/src/utilEntrypoint.ts +2 -0
  35. package/src/web-components/input/gs-location-filter.stories.ts +34 -29
  36. package/src/web-components/input/gs-location-filter.tsx +6 -13
  37. package/src/web-components/visualization/gs-mutations.stories.ts +62 -4
  38. package/src/web-components/visualization/gs-mutations.tsx +44 -0
  39. package/standalone-bundle/assets/{mutationOverTimeWorker-x1ipPFL0.js.map → mutationOverTimeWorker-DEybsZ5r.js.map} +1 -1
  40. package/standalone-bundle/dashboard-components.js +11021 -8621
  41. package/standalone-bundle/dashboard-components.js.map +1 -1
  42. package/standalone-bundle/style.css +1 -1
  43. package/dist/dateRangeOption-Bh2p78z0.js.map +0 -1
@@ -252,7 +252,7 @@ function getDownloadData(filteredData: MutationOverTimeDataMap) {
252
252
  return filteredData.getFirstAxisKeys().map((mutation) => {
253
253
  return dates.reduce(
254
254
  (accumulated, date) => {
255
- const proportion = filteredData.get(mutation, date)?.proportion ?? 0;
255
+ const proportion = filteredData.get(mutation, date)?.proportion ?? '';
256
256
  return {
257
257
  ...accumulated,
258
258
  [date.dateString]: proportion,
@@ -100,7 +100,11 @@ describe('queryMutationsOverTime', () => {
100
100
  });
101
101
 
102
102
  expect(mutationOverTimeData.getAsArray()).to.deep.equal([
103
- [{ proportion: 0.4, count: 4, totalCount: 11 }, null, null],
103
+ [
104
+ { proportion: 0.4, count: 4, totalCount: 11 },
105
+ { proportion: 0, count: 0, totalCount: 12 },
106
+ { proportion: 0, count: 0, totalCount: 13 },
107
+ ],
104
108
  [
105
109
  { proportion: 0.1, count: 1, totalCount: 11 },
106
110
  { proportion: 0.2, count: 2, totalCount: 12 },
@@ -177,7 +181,7 @@ describe('queryMutationsOverTime', () => {
177
181
  dateFieldTo: '2023-01-02',
178
182
  fields: [],
179
183
  },
180
- response: { data: [{ count: 12 }] },
184
+ response: { data: [{ count: 0 }] },
181
185
  },
182
186
  {
183
187
  body: {
@@ -247,7 +251,7 @@ describe('queryMutationsOverTime', () => {
247
251
  });
248
252
 
249
253
  expect(mutationOverTimeData.getAsArray()).to.deep.equal([
250
- [{ proportion: 0.4, count: 4, totalCount: 11 }, null, null],
254
+ [{ proportion: 0.4, count: 4, totalCount: 11 }, null, { proportion: 0, count: 0, totalCount: 13 }],
251
255
  [{ proportion: 0.1, count: 1, totalCount: 11 }, null, { proportion: 0.3, count: 3, totalCount: 13 }],
252
256
  ]);
253
257
 
@@ -688,7 +692,10 @@ describe('queryMutationsOverTime', () => {
688
692
  });
689
693
 
690
694
  expect(mutationOverTimeData.getAsArray()).to.deep.equal([
691
- [{ proportion: 0.4, count: 4, totalCount: 11 }, null],
695
+ [
696
+ { proportion: 0.4, count: 4, totalCount: 11 },
697
+ { proportion: 0, count: 0, totalCount: 12 },
698
+ ],
692
699
  [
693
700
  { proportion: 0.1, count: 1, totalCount: 11 },
694
701
  { proportion: 0.2, count: 2, totalCount: 12 },
@@ -734,6 +741,139 @@ describe('queryMutationsOverTime', () => {
734
741
  expect(dates.length).toBe(0);
735
742
  });
736
743
 
744
+ it('should fill with 0 if the mutation does not exist in a date range but count > 0', async () => {
745
+ const lapisFilter = { field1: 'value1', field2: 'value2' };
746
+ const dateField = 'dateField';
747
+
748
+ lapisRequestMocks.multipleAggregated([
749
+ {
750
+ body: { ...lapisFilter, fields: [dateField] },
751
+ response: {
752
+ data: [
753
+ { count: 1, [dateField]: '2023-01-01' },
754
+ { count: 1, [dateField]: '2023-01-02' },
755
+ ],
756
+ },
757
+ },
758
+ {
759
+ body: {
760
+ ...lapisFilter,
761
+ dateFieldFrom: '2023-01-01',
762
+ dateFieldTo: '2023-01-01',
763
+ fields: [],
764
+ },
765
+ response: { data: [{ count: 11 }] },
766
+ },
767
+ {
768
+ body: {
769
+ ...lapisFilter,
770
+ dateFieldFrom: '2023-01-02',
771
+ dateFieldTo: '2023-01-02',
772
+ fields: [],
773
+ },
774
+ response: { data: [{ count: 11 }] },
775
+ },
776
+ ]);
777
+
778
+ lapisRequestMocks.multipleMutations(
779
+ [
780
+ {
781
+ body: {
782
+ ...lapisFilter,
783
+ dateFieldFrom: '2023-01-01',
784
+ dateFieldTo: '2023-01-01',
785
+ minProportion: 0.001,
786
+ },
787
+ response: { data: [getSomeTestMutation(0.1, 1)] },
788
+ },
789
+ {
790
+ body: {
791
+ ...lapisFilter,
792
+ dateFieldFrom: '2023-01-02',
793
+ dateFieldTo: '2023-01-02',
794
+ minProportion: 0.001,
795
+ },
796
+ response: { data: [] },
797
+ },
798
+ {
799
+ body: {
800
+ ...lapisFilter,
801
+ dateFieldFrom: '2023-01-01',
802
+ dateFieldTo: '2023-01-02',
803
+ minProportion: 0.001,
804
+ },
805
+ response: {
806
+ data: [getSomeTestMutation(0.21, 6)],
807
+ },
808
+ },
809
+ ],
810
+ 'nucleotide',
811
+ );
812
+
813
+ const { mutationOverTimeData } = await queryMutationsOverTimeData({
814
+ lapisFilter,
815
+ sequenceType: 'nucleotide',
816
+ lapis: DUMMY_LAPIS_URL,
817
+ lapisDateField: dateField,
818
+ granularity: 'day',
819
+ });
820
+
821
+ expect(mutationOverTimeData.getAsArray()).to.deep.equal([
822
+ [
823
+ { proportion: 0.1, count: 1, totalCount: 11 },
824
+ { proportion: 0, count: 0, totalCount: 11 },
825
+ ],
826
+ ]);
827
+ });
828
+
829
+ it('should return null if count in a date range is 0', async () => {
830
+ const lapisFilter = { field1: 'value1', field2: 'value2' };
831
+ const dateField = 'dateField';
832
+
833
+ lapisRequestMocks.multipleAggregated([
834
+ {
835
+ body: { ...lapisFilter, fields: [dateField] },
836
+ response: {
837
+ data: [{ count: 0, [dateField]: '2023-01-01' }],
838
+ },
839
+ },
840
+ {
841
+ body: {
842
+ ...lapisFilter,
843
+ dateFieldFrom: '2023-01-01',
844
+ dateFieldTo: '2023-01-01',
845
+ fields: [],
846
+ },
847
+ response: { data: [{ count: 0 }] },
848
+ },
849
+ ]);
850
+
851
+ lapisRequestMocks.multipleMutations(
852
+ [
853
+ {
854
+ body: {
855
+ ...lapisFilter,
856
+ dateFieldFrom: '2023-01-01',
857
+ dateFieldTo: '2023-01-01',
858
+ minProportion: 0.001,
859
+ },
860
+ response: { data: [getSomeTestMutation(0.1, 1)] },
861
+ },
862
+ ],
863
+ 'nucleotide',
864
+ );
865
+
866
+ const { mutationOverTimeData } = await queryMutationsOverTimeData({
867
+ lapisFilter,
868
+ sequenceType: 'nucleotide',
869
+ lapis: DUMMY_LAPIS_URL,
870
+ lapisDateField: dateField,
871
+ granularity: 'day',
872
+ });
873
+
874
+ expect(mutationOverTimeData.getAsArray()).to.deep.equal([[null]]);
875
+ });
876
+
737
877
  function getSomeTestMutation(proportion: number, count: number) {
738
878
  return {
739
879
  mutation: 'sequenceName:A123T',
@@ -122,6 +122,7 @@ export async function queryMutationsOverTimeData({
122
122
 
123
123
  const data = await fetchAndPrepareSubstitutionsOrDeletions(filter, sequenceType).evaluate(lapis, signal);
124
124
  const totalCountQuery = await getTotalNumberOfSequencesInDateRange(filter).evaluate(lapis, signal);
125
+
125
126
  return {
126
127
  date,
127
128
  mutations: data.content,
@@ -244,9 +245,14 @@ export function groupByMutation(
244
245
  });
245
246
 
246
247
  data.forEach((mutationData) => {
248
+ if (mutationData.totalCount == 0) {
249
+ return;
250
+ }
251
+
252
+ const date = toTemporal(mutationData.date);
253
+
247
254
  mutationData.mutations.forEach((mutationEntry) => {
248
255
  const mutation = toSubstitutionOrDeletion(mutationEntry.mutation);
249
- const date = toTemporal(mutationData.date);
250
256
 
251
257
  if (dataArray.get(mutation, date) !== undefined) {
252
258
  dataArray.set(mutation, date, {
@@ -256,6 +262,16 @@ export function groupByMutation(
256
262
  });
257
263
  }
258
264
  });
265
+
266
+ for (const firstAxisKey of dataArray.getFirstAxisKeys()) {
267
+ if (dataArray.get(firstAxisKey, date) === null) {
268
+ dataArray.set(firstAxisKey, date, {
269
+ count: 0,
270
+ proportion: 0,
271
+ totalCount: mutationData.totalCount,
272
+ });
273
+ }
274
+ }
259
275
  });
260
276
 
261
277
  return dataArray;
@@ -32,3 +32,5 @@ export type { MapSource } from './preact/map/loadMapSource';
32
32
  export type { ConfidenceIntervalMethod } from './preact/shared/charts/confideceInterval';
33
33
 
34
34
  export type { AxisMax, YAxisMaxConfig } from './preact/shared/charts/getYAxisMax';
35
+
36
+ export { LocationChangedEvent } from './preact/locationFilter/LocationChangedEvent';
@@ -15,7 +15,7 @@ import { withinShadowRoot } from '../withinShadowRoot.story';
15
15
  const codeExample = String.raw`
16
16
  <gs-location-filter
17
17
  fields='["region", "country"]'
18
- initialValue='Europe / Switzerland'
18
+ value='{ "region": "Europe", "country": null}'
19
19
  width="100%"
20
20
  placeholderText="Enter a location"
21
21
  ></gs-location-filter>`;
@@ -39,9 +39,9 @@ const meta: Meta = {
39
39
  type: 'object',
40
40
  },
41
41
  },
42
- initialValue: {
42
+ value: {
43
43
  control: {
44
- type: 'text',
44
+ type: 'object',
45
45
  },
46
46
  },
47
47
  width: {
@@ -66,7 +66,7 @@ const Template: StoryObj<LocationFilterProps> = {
66
66
  <div class="max-w-screen-lg">
67
67
  <gs-location-filter
68
68
  .fields=${args.fields}
69
- initialValue=${ifDefined(args.initialValue)}
69
+ .value=${args.value}
70
70
  .width=${args.width}
71
71
  placeholderText=${ifDefined(args.placeholderText)}
72
72
  ></gs-location-filter>
@@ -75,7 +75,7 @@ const Template: StoryObj<LocationFilterProps> = {
75
75
  },
76
76
  args: {
77
77
  fields: ['region', 'country', 'division', 'location'],
78
- initialValue: '',
78
+ value: undefined,
79
79
  width: '100%',
80
80
  placeholderText: 'Enter a location',
81
81
  },
@@ -107,10 +107,7 @@ export const LocationFilter: StoryObj<LocationFilterProps> = {
107
107
  play: async ({ canvasElement }) => {
108
108
  const canvas = await withinShadowRoot(canvasElement, 'gs-location-filter');
109
109
  await waitFor(() => {
110
- return expect(canvas.getByRole('combobox')).toBeEnabled();
111
- });
112
- await waitFor(() => {
113
- return expect(canvas.getByPlaceholderText('Enter a location')).toBeInTheDocument();
110
+ return expect(canvas.getByPlaceholderText('Enter a location')).toBeVisible();
114
111
  });
115
112
  },
116
113
  };
@@ -199,36 +196,44 @@ export const FiresEvent: StoryObj<LocationFilterProps> = {
199
196
 
200
197
  await step('Empty input', async () => {
201
198
  await userEvent.type(inputField(), '{backspace>18/}');
202
- await expect(listenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
203
- region: undefined,
204
- country: undefined,
205
- division: undefined,
206
- location: undefined,
199
+ await userEvent.click(canvas.getByLabelText('toggle menu'));
200
+
201
+ await waitFor(() => {
202
+ return expect(listenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
203
+ region: undefined,
204
+ country: undefined,
205
+ division: undefined,
206
+ location: undefined,
207
+ });
207
208
  });
208
209
  });
209
210
 
210
211
  await step('Select Asia', async () => {
211
212
  await userEvent.type(inputField(), 'Asia');
212
- await expect(listenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
213
- region: 'Asia',
214
- country: undefined,
215
- division: undefined,
216
- location: undefined,
213
+ await userEvent.click(canvas.getByRole('option', { name: 'Asia Asia' }));
214
+
215
+ await waitFor(() => {
216
+ return expect(listenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
217
+ region: 'Asia',
218
+ country: undefined,
219
+ division: undefined,
220
+ location: undefined,
221
+ });
217
222
  });
218
223
  });
219
224
 
220
225
  await step('Select Asia / Bangladesh / Rajshahi / Chapainawabgonj', async () => {
221
226
  await userEvent.type(inputField(), ' / Bangladesh / Rajshahi / Chapainawabgonj');
222
- await expect(listenerMock).toHaveBeenCalledWith(
223
- expect.objectContaining({
224
- detail: {
225
- region: 'Asia',
226
- country: 'Bangladesh',
227
- division: 'Rajshahi',
228
- location: 'Chapainawabgonj',
229
- },
230
- }),
231
- );
227
+ await userEvent.click(canvas.getByText('Asia / Bangladesh / Rajshahi / Chapainawabgonj'));
228
+
229
+ await waitFor(() => {
230
+ return expect(listenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
231
+ region: 'Asia',
232
+ country: 'Bangladesh',
233
+ division: 'Rajshahi',
234
+ location: 'Chapainawabgonj',
235
+ });
236
+ });
232
237
  });
233
238
  },
234
239
  };
@@ -1,6 +1,7 @@
1
1
  import { customElement, property } from 'lit/decorators.js';
2
2
  import type { DetailedHTMLProps, HTMLAttributes } from 'react';
3
3
 
4
+ import { type LocationChangedEvent } from '../../preact/locationFilter/LocationChangedEvent';
4
5
  import { LocationFilter, type LocationFilterProps } from '../../preact/locationFilter/location-filter';
5
6
  import type { Equals, Expect } from '../../utils/typeAssertions';
6
7
  import { PreactLitAdapter } from '../PreactLitAdapter';
@@ -14,11 +15,6 @@ import { PreactLitAdapter } from '../PreactLitAdapter';
14
15
  * The component retrieves a list of all possible values for these fields from the Lapis instance.
15
16
  * This list is then utilized to display autocomplete suggestions and to validate the input.
16
17
  *
17
- * Given `fields` are `['field1', 'field2', ..., 'fieldN']`,
18
- * then valid values for the location filter must be in the form `valueForField1 / valueForField2 / ... / valueForFieldK`,
19
- * where `1 <= K <= N`.
20
- * Values for the fields `i > K` are considered `undefined`.
21
- *
22
18
  * @fires {CustomEvent<Record<string, string>>} gs-location-changed
23
19
  * Fired when a value from the datalist is selected or when a valid value is typed into the field.
24
20
  * The `details` of this event contain an object with all `fields` as keys
@@ -37,10 +33,9 @@ import { PreactLitAdapter } from '../PreactLitAdapter';
37
33
  export class LocationFilterComponent extends PreactLitAdapter {
38
34
  /**
39
35
  * The initial value to use for this location filter.
40
- * Must be of the form `valueForField1 / valueForField2 / ... / valueForFieldN`.
41
36
  */
42
- @property()
43
- initialValue: string | undefined = undefined;
37
+ @property({ type: Object })
38
+ value: Record<string, string | null | undefined> | undefined = undefined;
44
39
 
45
40
  /**
46
41
  * Required.
@@ -70,7 +65,7 @@ export class LocationFilterComponent extends PreactLitAdapter {
70
65
  override render() {
71
66
  return (
72
67
  <LocationFilter
73
- initialValue={this.initialValue}
68
+ value={this.value}
74
69
  fields={this.fields}
75
70
  width={this.width}
76
71
  placeholderText={this.placeholderText}
@@ -85,7 +80,7 @@ declare global {
85
80
  }
86
81
 
87
82
  interface HTMLElementEventMap {
88
- 'gs-location-changed': CustomEvent<Record<string, string>>;
83
+ 'gs-location-changed': LocationChangedEvent;
89
84
  }
90
85
  }
91
86
 
@@ -99,9 +94,7 @@ declare global {
99
94
  }
100
95
 
101
96
  /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars */
102
- type InitialValueMatches = Expect<
103
- Equals<typeof LocationFilterComponent.prototype.initialValue, LocationFilterProps['initialValue']>
104
- >;
97
+ type InitialValueMatches = Expect<Equals<typeof LocationFilterComponent.prototype.value, LocationFilterProps['value']>>;
105
98
  type FieldsMatches = Expect<Equals<typeof LocationFilterComponent.prototype.fields, LocationFilterProps['fields']>>;
106
99
  type PlaceholderTextMatches = Expect<
107
100
  Equals<typeof LocationFilterComponent.prototype.placeholderText, LocationFilterProps['placeholderText']>
@@ -2,18 +2,27 @@ import { expect, fireEvent, waitFor } from '@storybook/test';
2
2
  import type { Meta, StoryObj } from '@storybook/web-components';
3
3
  import { html } from 'lit';
4
4
 
5
- import './gs-mutations';
6
- import '../app';
7
5
  import { withComponentDocs } from '../../../.storybook/ComponentDocsBlock';
8
- import { LAPIS_URL, NUCLEOTIDE_INSERTIONS_ENDPOINT, NUCLEOTIDE_MUTATIONS_ENDPOINT } from '../../constants';
6
+ import {
7
+ AGGREGATED_ENDPOINT,
8
+ LAPIS_URL,
9
+ NUCLEOTIDE_INSERTIONS_ENDPOINT,
10
+ NUCLEOTIDE_MUTATIONS_ENDPOINT,
11
+ } from '../../constants';
12
+ import baselineNucleotideMutations from '../../preact/mutations/__mockData__/baselineNucleotideMutations.json';
9
13
  import nucleotideInsertions from '../../preact/mutations/__mockData__/nucleotideInsertions.json';
10
14
  import nucleotideMutations from '../../preact/mutations/__mockData__/nucleotideMutations.json';
15
+ import overallVariantCount from '../../preact/mutations/__mockData__/overallVariantCount.json';
11
16
  import { type MutationsProps } from '../../preact/mutations/mutations';
12
17
  import { withinShadowRoot } from '../withinShadowRoot.story';
13
18
 
19
+ import './gs-mutations';
20
+ import '../app';
21
+
14
22
  const codeExample = String.raw`
15
23
  <gs-mutations
16
- lapisFilter='{ "country": "Switzerland", "pangoLineage": "B.1.1.7", "dateTo": "2022-01-01" }'
24
+ lapisFilter='{ "country": "Switzerland", "dateTo": "2022-01-01", "pangoLineage": "B.1.1.7"}'
25
+ baselineLapisFilter='{ "country": "Switzerland", "dateTo": "2022-01-01" }'
17
26
  sequenceType="nucleotide"
18
27
  views='["grid", "table", "insertions"]'
19
28
  width='100%'
@@ -26,6 +35,7 @@ const meta: Meta<Required<MutationsProps>> = {
26
35
  component: 'gs-mutations',
27
36
  argTypes: {
28
37
  lapisFilter: { control: 'object' },
38
+ baselineLapisFilter: { control: 'object' },
29
39
  sequenceType: {
30
40
  options: ['nucleotide', 'amino acid'],
31
41
  control: { type: 'radio' },
@@ -40,6 +50,7 @@ const meta: Meta<Required<MutationsProps>> = {
40
50
  },
41
51
  args: {
42
52
  lapisFilter: { country: 'Switzerland', pangoLineage: 'B.1.1.7', dateTo: '2022-01-01' },
53
+ baselineLapisFilter: { country: 'Switzerland', dateTo: '2022-01-01' },
43
54
  sequenceType: 'nucleotide',
44
55
  views: ['grid', 'table', 'insertions'],
45
56
  width: '100%',
@@ -63,6 +74,7 @@ const Template: StoryObj<Required<MutationsProps>> = {
63
74
  <gs-app lapis="${LAPIS_URL}">
64
75
  <gs-mutations
65
76
  .lapisFilter=${args.lapisFilter}
77
+ .baselineLapisFilter=${args.baselineLapisFilter}
66
78
  .sequenceType=${args.sequenceType}
67
79
  .views=${args.views}
68
80
  .width=${args.width}
@@ -94,6 +106,37 @@ export const Default: StoryObj<Required<MutationsProps>> = {
94
106
  body: nucleotideMutations,
95
107
  },
96
108
  },
109
+ {
110
+ matcher: {
111
+ name: 'baselineNucleotideMutations',
112
+ url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
113
+ body: {
114
+ country: 'Switzerland',
115
+ dateTo: '2022-01-01',
116
+ minProportion: 0,
117
+ },
118
+ },
119
+ response: {
120
+ status: 200,
121
+ body: baselineNucleotideMutations,
122
+ },
123
+ },
124
+ {
125
+ matcher: {
126
+ name: 'overallVariantCount',
127
+ url: AGGREGATED_ENDPOINT,
128
+ body: {
129
+ country: 'Switzerland',
130
+ pangoLineage: 'B.1.1.7',
131
+ dateTo: '2022-01-01',
132
+ fields: [],
133
+ },
134
+ },
135
+ response: {
136
+ status: 200,
137
+ body: overallVariantCount,
138
+ },
139
+ },
97
140
  {
98
141
  matcher: {
99
142
  name: 'nucleotideInsertions',
@@ -121,6 +164,21 @@ export const OnTableTab: StoryObj<Required<MutationsProps>> = {
121
164
  },
122
165
  };
123
166
 
167
+ export const OnTableTabWithoutJaccardSimilarity: StoryObj<Required<MutationsProps>> = {
168
+ ...Default,
169
+ args: {
170
+ ...Default.args,
171
+ baselineLapisFilter: undefined,
172
+ },
173
+ play: async ({ canvasElement }) => {
174
+ const canvas = await withinShadowRoot(canvasElement, 'gs-mutations');
175
+
176
+ await waitFor(() => expect(canvas.getByRole('button', { name: 'Table' })).toBeInTheDocument());
177
+
178
+ await fireEvent.click(canvas.getByRole('button', { name: 'Table' }));
179
+ },
180
+ };
181
+
124
182
  export const OnInsertionsTab: StoryObj<Required<MutationsProps>> = {
125
183
  ...Default,
126
184
  play: async ({ canvasElement }) => {
@@ -22,6 +22,29 @@ import { PreactLitAdapterWithGridJsStyles } from '../PreactLitAdapterWithGridJsS
22
22
  *
23
23
  * The proportion interval filter can be used to filter the displayed mutations on client side.
24
24
  *
25
+ * #### Jaccard Similarity
26
+ *
27
+ * If the `baselineLapisFilter` attribute is set,
28
+ * the [Jaccard similarity](https://en.wikipedia.org/wiki/Jaccard_index) is computed for each mutation.
29
+ * It is computed as `variantWithMutationCount / (variantCount + mutationCount - variantWithMutationCount)`,
30
+ * - `variantCount` is the number of sequences of the variant (i.e. the number of sequences that match the `lapisFilter`),
31
+ * - `mutationCount` is the number of sequences with the mutation
32
+ * (i.e. the number of sequences matching the `baselineLapisFilter` that have the mutation),
33
+ * - `variantWithMutationCount` is the number of sequences that belong to the variant and have the mutation
34
+ * (i.e. the `count` value that is shown in the table).
35
+ *
36
+ * Typically, this is useful when you query mutations of a certain "variant"
37
+ * (i.e. a certain lineage or a certain set of mutations).
38
+ * Then the `baselineLapisFilter` should be the `lapisFilter` but without the lineage or mutations.
39
+ *
40
+ * For example:
41
+ * You are interested in a certain lineage in a certain country: `lapisFilter={country: 'Switzerland', linage: 'XY.1.2.3'}`.
42
+ * Then the "baseline" should be the same filter but without the lineage: `baselineLapisFilter={country: 'Switzerland'}`.
43
+ *
44
+ * Computing the Jaccard similarity is not always meaningful, because you might not have a "variant"
45
+ * (e.g. when you only query for a certain country).
46
+ * In this case you can simply omit the `baselineLapisFilter`.
47
+ *
25
48
  * ### Grid View
26
49
  *
27
50
  * The grid view shows the proportion of each sequence symbol (nucleotide or amino acid) for each position that has a mutation.
@@ -45,6 +68,23 @@ export class MutationsComponent extends PreactLitAdapterWithGridJsStyles {
45
68
  aminoAcidInsertions?: string[];
46
69
  } = {};
47
70
 
71
+ // prettier-ignore
72
+ // The multiline union type must not start with `|` because it looks weird in the Storybook docs
73
+ /**
74
+ * LAPIS filter to select the mutation counts that are used to compute the Jaccard similarity.
75
+ * If not provided, the Jaccard similarity is not computed.
76
+ * For details, see the [Jaccard Similarity](#jaccard-similarity) section in the component description.
77
+ */
78
+ @property({ type: Object })
79
+ baselineLapisFilter:
80
+ (Record<string, string | string[] | number | null | boolean | undefined> & {
81
+ nucleotideMutations?: string[];
82
+ aminoAcidMutations?: string[];
83
+ nucleotideInsertions?: string[];
84
+ aminoAcidInsertions?: string[];
85
+ })
86
+ | undefined = undefined;
87
+
48
88
  /**
49
89
  * The type of the sequence for which the mutations should be shown.
50
90
  */
@@ -89,6 +129,7 @@ export class MutationsComponent extends PreactLitAdapterWithGridJsStyles {
89
129
  width={this.width}
90
130
  height={this.height}
91
131
  pageSize={this.pageSize}
132
+ baselineLapisFilter={this.baselineLapisFilter}
92
133
  />
93
134
  );
94
135
  }
@@ -113,6 +154,9 @@ declare global {
113
154
  type LapisFilterMatches = Expect<
114
155
  Equals<typeof MutationsComponent.prototype.lapisFilter, MutationsProps['lapisFilter']>
115
156
  >;
157
+ type BaselineLapisFilterMatches = Expect<
158
+ Equals<typeof MutationsComponent.prototype.baselineLapisFilter, MutationsProps['baselineLapisFilter']>
159
+ >;
116
160
  type SequenceTypeMatches = Expect<
117
161
  Equals<typeof MutationsComponent.prototype.sequenceType, MutationsProps['sequenceType']>
118
162
  >;