@genspectrum/dashboard-components 1.4.0 → 1.6.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 (41) hide show
  1. package/custom-elements.json +134 -7
  2. package/dist/assets/{mutationOverTimeWorker-CQxrFo53.js.map → mutationOverTimeWorker-BmB6BvVM.js.map} +1 -1
  3. package/dist/components.d.ts +52 -11
  4. package/dist/components.js +189 -45
  5. package/dist/components.js.map +1 -1
  6. package/dist/util.d.ts +11 -11
  7. package/package.json +1 -1
  8. package/src/preact/lineageFilter/lineage-filter.stories.tsx +24 -0
  9. package/src/preact/lineageFilter/lineage-filter.tsx +13 -2
  10. package/src/preact/locationFilter/location-filter.stories.tsx +24 -0
  11. package/src/preact/locationFilter/location-filter.tsx +19 -3
  12. package/src/preact/mutationFilter/mutation-filter-info.tsx +2 -2
  13. package/src/preact/mutationFilter/mutation-filter.stories.tsx +71 -1
  14. package/src/preact/mutationFilter/mutation-filter.tsx +65 -24
  15. package/src/preact/mutationsOverTime/__mockData__/withGaps.ts +352 -0
  16. package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +38 -0
  17. package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +10 -0
  18. package/src/preact/mutationsOverTime/mutationOverTimeWorker.mock.ts +2 -0
  19. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +35 -0
  20. package/src/preact/mutationsOverTime/mutations-over-time.tsx +28 -4
  21. package/src/preact/textFilter/text-filter.stories.tsx +29 -4
  22. package/src/preact/textFilter/text-filter.tsx +13 -2
  23. package/src/query/queryMutationsOverTime.ts +37 -4
  24. package/src/query/queryMutationsOverTimeNewEndpoint.spec.ts +122 -0
  25. package/src/utils/map2d.spec.ts +30 -0
  26. package/src/utils/map2d.ts +14 -1
  27. package/src/utils/mutations.spec.ts +13 -0
  28. package/src/utils/mutations.ts +3 -3
  29. package/src/web-components/input/gs-lineage-filter.stories.ts +7 -0
  30. package/src/web-components/input/gs-lineage-filter.tsx +8 -0
  31. package/src/web-components/input/gs-location-filter.stories.ts +7 -0
  32. package/src/web-components/input/gs-location-filter.tsx +9 -1
  33. package/src/web-components/input/gs-mutation-filter.stories.ts +29 -1
  34. package/src/web-components/input/gs-mutation-filter.tsx +29 -2
  35. package/src/web-components/input/gs-text-filter.stories.ts +7 -0
  36. package/src/web-components/input/gs-text-filter.tsx +8 -0
  37. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +14 -1
  38. package/src/web-components/visualization/gs-mutations-over-time.tsx +8 -0
  39. package/standalone-bundle/assets/{mutationOverTimeWorker-CDACUs6w.js.map → mutationOverTimeWorker-B_xP8pIC.js.map} +1 -1
  40. package/standalone-bundle/dashboard-components.js +3666 -3553
  41. package/standalone-bundle/dashboard-components.js.map +1 -1
@@ -46,6 +46,11 @@ const meta: Meta<TextFilterProps> = {
46
46
  type: 'text',
47
47
  },
48
48
  },
49
+ hideCounts: {
50
+ control: {
51
+ type: 'boolean',
52
+ },
53
+ },
49
54
  value: {
50
55
  control: {
51
56
  type: 'text',
@@ -75,6 +80,7 @@ export const Default: StoryObj<TextFilterProps> = {
75
80
  args: {
76
81
  lapisField: 'host',
77
82
  placeholderText: 'Enter a host name',
83
+ hideCounts: false,
78
84
  value: '',
79
85
  width: '100%',
80
86
  lapisFilter: {
@@ -128,10 +134,6 @@ export const KeepsPartialInputInInputField: StoryObj<TextFilterProps> = {
128
134
  </LapisUrlContextProvider>
129
135
  </>
130
136
  ),
131
- args: {
132
- ...Default.args,
133
- value: '',
134
- },
135
137
  play: async ({ canvasElement, step }) => {
136
138
  const canvas = within(canvasElement);
137
139
 
@@ -176,6 +178,29 @@ export const KeepsPartialInputInInputField: StoryObj<TextFilterProps> = {
176
178
  },
177
179
  };
178
180
 
181
+ export const WithHideCountsTrue: StoryObj<TextFilterProps> = {
182
+ ...Default,
183
+ args: {
184
+ ...Default.args,
185
+ hideCounts: true,
186
+ },
187
+ play: async ({ canvasElement, step }) => {
188
+ const canvas = within(canvasElement);
189
+ const inputField = () => canvas.getByPlaceholderText('Enter a host name', { exact: false });
190
+
191
+ await waitFor(async () => {
192
+ await expect(inputField()).toHaveValue('');
193
+ });
194
+
195
+ await step('visible without counts', async () => {
196
+ const input = inputField();
197
+ await userEvent.clear(input);
198
+ await userEvent.type(input, 'Homo');
199
+ await expect(canvas.getByRole('option', { name: 'Homo' })).toBeVisible();
200
+ });
201
+ },
202
+ };
203
+
179
204
  export const WithNoLapisField: StoryObj<TextFilterProps> = {
180
205
  ...Default,
181
206
  args: {
@@ -15,6 +15,7 @@ const textSelectorPropsSchema = z.object({
15
15
  lapisField: z.string().min(1),
16
16
  placeholderText: z.string().optional(),
17
17
  value: z.string().optional(),
18
+ hideCounts: z.boolean().optional(),
18
19
  });
19
20
  const textFilterInnerPropsSchema = textSelectorPropsSchema.extend({ lapisFilter: lapisFilterSchema });
20
21
  const textFilterPropsSchema = textFilterInnerPropsSchema.extend({
@@ -42,6 +43,7 @@ const TextFilterInner: FunctionComponent<TextFilterInnerProps> = ({
42
43
  value,
43
44
  lapisField,
44
45
  placeholderText,
46
+ hideCounts,
45
47
  lapisFilter,
46
48
  }) => {
47
49
  const lapis = useLapisUrl();
@@ -59,7 +61,15 @@ const TextFilterInner: FunctionComponent<TextFilterInnerProps> = ({
59
61
  throw error;
60
62
  }
61
63
 
62
- return <TextSelector lapisField={lapisField} value={value} placeholderText={placeholderText} data={data} />;
64
+ return (
65
+ <TextSelector
66
+ lapisField={lapisField}
67
+ value={value}
68
+ placeholderText={placeholderText}
69
+ hideCounts={hideCounts}
70
+ data={data}
71
+ />
72
+ );
63
73
  };
64
74
 
65
75
  type SelectItem = {
@@ -72,6 +82,7 @@ const TextSelector = ({
72
82
  value,
73
83
  placeholderText,
74
84
  data,
85
+ hideCounts = false,
75
86
  }: TextSelectorProps & {
76
87
  data: SelectItem[];
77
88
  }) => {
@@ -89,7 +100,7 @@ const TextSelector = ({
89
100
  return (
90
101
  <p>
91
102
  <span>{item.value}</span>
92
- <span className='ml-2 text-gray-500'>({item.count})</span>
103
+ {!hideCounts && <span className='ml-2 text-gray-500'>({item.count})</span>}
93
104
  </p>
94
105
  );
95
106
  }}
@@ -267,12 +267,33 @@ async function queryMutationsOverTimeDataDirectEndpoint(
267
267
  signal,
268
268
  );
269
269
 
270
+ const responseMutations = apiResult.data.mutations.map(parseMutationCode);
271
+ const mutationEntries: SubstitutionOrDeletionEntry[] = responseMutations.map((mutation, i) => {
272
+ const numbers = {
273
+ count: overallMutationData[i].count,
274
+ proportion: overallMutationData[i].proportion,
275
+ };
276
+ if (mutation.type === 'deletion') {
277
+ return {
278
+ type: 'deletion',
279
+ mutation,
280
+ ...numbers,
281
+ };
282
+ } else {
283
+ return {
284
+ type: 'substitution',
285
+ mutation,
286
+ ...numbers,
287
+ };
288
+ }
289
+ });
290
+
270
291
  const mutationOverTimeData: Map2DContents<Substitution | Deletion, Temporal, MutationOverTimeMutationValue> = {
271
- keysFirstAxis: new Map(overallMutationData.map((value) => [value.mutation.code, value.mutation])),
292
+ keysFirstAxis: new Map(responseMutations.map((mutation) => [mutation.code, mutation])),
272
293
  keysSecondAxis: new Map(allDates.map((date) => [date.dateString, date])),
273
294
  data: new Map(
274
- overallMutationData.map((mutation, i) => [
275
- mutation.mutation.code,
295
+ responseMutations.map((mutation, i) => [
296
+ mutation.code,
276
297
  new Map(
277
298
  allDates.map((date, j): [string, MutationOverTimeMutationValue] => [
278
299
  date.dateString,
@@ -292,10 +313,22 @@ async function queryMutationsOverTimeDataDirectEndpoint(
292
313
 
293
314
  return {
294
315
  mutationOverTimeData: new BaseMutationOverTimeDataMap(mutationOverTimeData),
295
- overallMutationData,
316
+ overallMutationData: mutationEntries,
296
317
  };
297
318
  }
298
319
 
320
+ function parseMutationCode(code: string): SubstitutionClass | DeletionClass {
321
+ const maybeDeletion = DeletionClass.parse(code);
322
+ if (maybeDeletion) {
323
+ return maybeDeletion;
324
+ }
325
+ const maybeSubstitution = SubstitutionClass.parse(code);
326
+ if (maybeSubstitution) {
327
+ return maybeSubstitution;
328
+ }
329
+ throw Error(`Given code is not valid: ${code}`);
330
+ }
331
+
299
332
  /**
300
333
  * Returns a list of date ranges as TemporalClass.
301
334
  * Respects date range filters given in the lapisFilter as <lapisDateField>From and <lapisDateField>To.
@@ -1031,6 +1031,128 @@ describe('queryMutationsOverTimeNewEndpoint', () => {
1031
1031
  expect(dates[1].dateString).toBe('2023-02');
1032
1032
  });
1033
1033
 
1034
+ it('should return full mutation codes even if partial includeMutations are given', async () => {
1035
+ const lapisFilter = { field1: 'value1', field2: 'value2' };
1036
+ const dateField = 'dateField';
1037
+
1038
+ lapisRequestMocks.multipleAggregated([
1039
+ {
1040
+ body: { ...lapisFilter, fields: [dateField] },
1041
+ response: {
1042
+ data: [
1043
+ { count: 1, [dateField]: '2023-01-05' },
1044
+ { count: 2, [dateField]: '2023-02-15' },
1045
+ ],
1046
+ },
1047
+ },
1048
+ {
1049
+ body: {
1050
+ ...lapisFilter,
1051
+ dateFieldFrom: '2023-01-01',
1052
+ dateFieldTo: '2023-01-31',
1053
+ fields: [],
1054
+ },
1055
+ response: { data: [{ count: 11 }] },
1056
+ },
1057
+ {
1058
+ body: {
1059
+ ...lapisFilter,
1060
+ dateFieldFrom: '2023-02-01',
1061
+ dateFieldTo: '2023-02-28',
1062
+ fields: [],
1063
+ },
1064
+ response: { data: [{ count: 12 }] },
1065
+ },
1066
+ ]);
1067
+
1068
+ lapisRequestMocks.multipleMutations(
1069
+ [
1070
+ {
1071
+ body: {
1072
+ ...lapisFilter,
1073
+ dateFieldFrom: '2023-01-01',
1074
+ dateFieldTo: '2023-02-28',
1075
+ minProportion: 0.001,
1076
+ },
1077
+ response: {
1078
+ data: [getSomeTestMutation(0.21, 6), getSomeOtherTestMutation(0.22, 4)],
1079
+ },
1080
+ },
1081
+ ],
1082
+ 'nucleotide',
1083
+ );
1084
+
1085
+ const dateRanges = [
1086
+ {
1087
+ dateFrom: '2023-01-01',
1088
+ dateTo: '2023-01-31',
1089
+ },
1090
+ {
1091
+ dateFrom: '2023-02-01',
1092
+ dateTo: '2023-02-28',
1093
+ },
1094
+ ];
1095
+
1096
+ lapisRequestMocks.mutationsOverTime(
1097
+ [
1098
+ {
1099
+ body: {
1100
+ filters: lapisFilter,
1101
+ dateRanges,
1102
+ includeMutations: ['122', 'otherSequenceName:G234C'],
1103
+ dateField,
1104
+ },
1105
+ response: {
1106
+ data: {
1107
+ data: [
1108
+ [
1109
+ { count: 0, coverage: 0 },
1110
+ { count: 0, coverage: 0 },
1111
+ ],
1112
+ [
1113
+ { count: 2, coverage: 10 },
1114
+ { count: 3, coverage: 10 },
1115
+ ],
1116
+ ],
1117
+ dateRanges,
1118
+ mutations: ['A122T', 'otherSequenceName:G234C'],
1119
+ },
1120
+ },
1121
+ },
1122
+ ],
1123
+ 'nucleotide',
1124
+ );
1125
+
1126
+ const { mutationOverTimeData } = await queryMutationsOverTimeData({
1127
+ lapisFilter,
1128
+ sequenceType: 'nucleotide',
1129
+ lapis: DUMMY_LAPIS_URL,
1130
+ lapisDateField: dateField,
1131
+ granularity: 'month',
1132
+ useNewEndpoint: true,
1133
+ displayMutations: ['otherSequenceName:G234C', '122'],
1134
+ });
1135
+
1136
+ expect(mutationOverTimeData.getAsArray()).to.deep.equal([
1137
+ [
1138
+ { type: 'value', proportion: NaN, count: 0, totalCount: 11 },
1139
+ { type: 'value', proportion: NaN, count: 0, totalCount: 12 },
1140
+ ],
1141
+ [
1142
+ { type: 'value', proportion: 0.2, count: 2, totalCount: 11 },
1143
+ { type: 'value', proportion: 0.3, count: 3, totalCount: 12 },
1144
+ ],
1145
+ ]);
1146
+
1147
+ const sequences = mutationOverTimeData.getFirstAxisKeys();
1148
+ expect(sequences[0].code).toBe('A122T');
1149
+ expect(sequences[1].code).toBe('otherSequenceName:G234C');
1150
+
1151
+ const dates = mutationOverTimeData.getSecondAxisKeys();
1152
+ expect(dates[0].dateString).toBe('2023-01');
1153
+ expect(dates[1].dateString).toBe('2023-02');
1154
+ });
1155
+
1034
1156
  function getSomeTestMutation(proportion: number, count: number) {
1035
1157
  return {
1036
1158
  mutation: 'sequenceName:A123T',
@@ -186,6 +186,11 @@ describe('Map2dView', () => {
186
186
  view.deleteRow('c');
187
187
 
188
188
  expect(view.getAsArray()).toEqual([[1, undefined]]);
189
+
190
+ const view2 = new Map2dView<string, string, number>(container);
191
+ view2.deleteColumn('b');
192
+
193
+ expect(view2.getAsArray()).toEqual([[undefined], [4]]);
189
194
  });
190
195
 
191
196
  it('should throw an error when trying to set a value', () => {
@@ -201,12 +206,31 @@ describe('Map2dView', () => {
201
206
  expect(view.getRow('a')).toEqual([1, undefined]);
202
207
  });
203
208
 
209
+ it('should return a column by key', () => {
210
+ const container = createBaseContainer();
211
+ const view = new Map2dView<string, string, number>(container);
212
+
213
+ expect(view.getColumn('b')).toEqual([1, 3]);
214
+ expect(view.getColumn('d')).toEqual([undefined, 4]);
215
+ });
216
+
204
217
  it('should return an empty array when the row does not exist', () => {
205
218
  const container = createBaseContainer();
206
219
  const view = new Map2dView<string, string, number>(container);
207
220
  view.deleteRow('c');
208
221
 
209
222
  expect(view.getRow('c')).toEqual([]);
223
+ expect(view.getColumn('b')).toEqual([1]);
224
+ });
225
+
226
+ it('should return an empty array when the column does not exist', () => {
227
+ const container = createBaseContainer();
228
+ const view = new Map2dView<string, string, number>(container);
229
+ view.deleteColumn('b');
230
+
231
+ expect(view.getColumn('b')).toEqual([]);
232
+ expect(view.getRow('a')).toEqual([undefined]);
233
+ expect(view.getRow('c')).toEqual([4]);
210
234
  });
211
235
 
212
236
  function createBaseContainer() {
@@ -217,6 +241,12 @@ describe('Map2dView', () => {
217
241
  container.set('a', 'b', 1);
218
242
  container.set('c', 'b', 3);
219
243
  container.set('c', 'd', 4);
244
+
245
+ // | | b | d |
246
+ // |---|---|---|
247
+ // | a | 1 | |
248
+ // | c | 3 | 4 |
249
+
220
250
  return container;
221
251
  }
222
252
  });
@@ -168,6 +168,10 @@ export class Map2dView<Key1 extends object | string, Key2 extends object | strin
168
168
  this.keysFirstAxis.delete(this.serializeFirstAxis(key));
169
169
  }
170
170
 
171
+ deleteColumn(key: Key2) {
172
+ this.keysSecondAxis.delete(this.serializeSecondAxis(key));
173
+ }
174
+
171
175
  get(keyFirstAxis: Key1, keySecondAxis: Key2) {
172
176
  const firstAxisKey = this.serializeFirstAxis(keyFirstAxis);
173
177
  const secondAxisKey = this.serializeSecondAxis(keySecondAxis);
@@ -205,7 +209,16 @@ export class Map2dView<Key1 extends object | string, Key2 extends object | strin
205
209
  return [];
206
210
  }
207
211
 
208
- return this.baseMap.getRow(key);
212
+ return this.getSecondAxisKeys().map((k2) => this.baseMap.get(key, k2));
213
+ }
214
+
215
+ getColumn(key: Key2) {
216
+ const serializedKeySecondAxis = this.serializeSecondAxis(key);
217
+ if (!this.keysSecondAxis.has(serializedKeySecondAxis)) {
218
+ return [];
219
+ }
220
+
221
+ return this.getFirstAxisKeys().map((k1) => this.baseMap.get(k1, key));
209
222
  }
210
223
 
211
224
  getContents() {
@@ -8,6 +8,11 @@ describe('SubstitutionClass', () => {
8
8
  expect(SubstitutionClass.parse('seg1:A1T')).deep.equal(new SubstitutionClass('seg1', 'A', 'T', 1));
9
9
  });
10
10
 
11
+ it('should be parsed with stop codons', () => {
12
+ expect(SubstitutionClass.parse('S:*1247T')).deep.equal(new SubstitutionClass('S', '*', 'T', 1247));
13
+ expect(SubstitutionClass.parse('S:T1247*')).deep.equal(new SubstitutionClass('S', 'T', '*', 1247));
14
+ });
15
+
11
16
  it('should render to string correctly', () => {
12
17
  const substitutions = [
13
18
  {
@@ -30,6 +35,10 @@ describe('DeletionClass', () => {
30
35
  expect(DeletionClass.parse('seg1:A1-')).deep.equal(new DeletionClass('seg1', 'A', 1));
31
36
  });
32
37
 
38
+ it('should be parsed with stop codons', () => {
39
+ expect(DeletionClass.parse('seg1:*1-')).deep.equal(new DeletionClass('seg1', '*', 1));
40
+ });
41
+
33
42
  it('should render to string correctly', () => {
34
43
  const substitutions = [
35
44
  {
@@ -61,4 +70,8 @@ describe('InsertionClass', () => {
61
70
  expect(InsertionClass.parse('ins_geNe1:1:A')).deep.equal(new InsertionClass('geNe1', 1, 'A'));
62
71
  expect(InsertionClass.parse('ins_1:aA')).deep.equal(new InsertionClass(undefined, 1, 'aA'));
63
72
  });
73
+
74
+ it('should be parsed with stop codon insertion', () => {
75
+ expect(InsertionClass.parse('ins_134:*')).deep.equal(new InsertionClass(undefined, 134, '*'));
76
+ });
64
77
  });
@@ -14,7 +14,7 @@ export interface MutationClass extends Mutation {
14
14
  }
15
15
 
16
16
  export const substitutionRegex =
17
- /^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z])?(?<position>\d+)(?<substitutionValue>[A-Z.*])?$/i;
17
+ /^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z*])?(?<position>\d+)(?<substitutionValue>[A-Z.*])?$/i;
18
18
 
19
19
  export interface Substitution extends Mutation {
20
20
  type: 'substitution';
@@ -68,7 +68,7 @@ export class SubstitutionClass implements MutationClass, Substitution {
68
68
  }
69
69
  }
70
70
 
71
- export const deletionRegex = /^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z])?(?<position>\d+)(-)$/i;
71
+ export const deletionRegex = /^((?<segment>[A-Z0-9_-]+)(?=:):)?(?<valueAtReference>[A-Z*])?(?<position>\d+)(-)$/i;
72
72
 
73
73
  export interface Deletion extends Mutation {
74
74
  type: 'deletion';
@@ -119,7 +119,7 @@ export class DeletionClass implements MutationClass, Deletion {
119
119
  }
120
120
 
121
121
  export const insertionRegexp =
122
- /^ins_((?<segment>[A-Z0-9_-]+)(?=:):)?(?<position>\d+):(?<insertedSymbols>(([A-Z?]|(\.\*))+))$/i;
122
+ /^ins_((?<segment>[A-Z0-9_-]+)(?=:):)?(?<position>\d+):(?<insertedSymbols>(([A-Z?*]|(\.\*))+))$/i;
123
123
 
124
124
  export interface Insertion extends Mutation {
125
125
  type: 'insertion';
@@ -92,6 +92,11 @@ const meta: Meta<Required<LineageFilterProps>> = {
92
92
  type: 'object',
93
93
  },
94
94
  },
95
+ hideCounts: {
96
+ control: {
97
+ type: 'boolean',
98
+ },
99
+ },
95
100
  },
96
101
  };
97
102
 
@@ -105,6 +110,7 @@ const Template: StoryObj<Required<LineageFilterProps>> = {
105
110
  .lapisField=${args.lapisField}
106
111
  .lapisFilter=${args.lapisFilter}
107
112
  .placeholderText=${args.placeholderText}
113
+ .hideCounts=${args.hideCounts}
108
114
  .value=${args.value}
109
115
  .width=${args.width}
110
116
  ></gs-lineage-filter>
@@ -119,6 +125,7 @@ const Template: StoryObj<Required<LineageFilterProps>> = {
119
125
  placeholderText: 'Enter a lineage',
120
126
  value: 'B.1.1.7',
121
127
  width: '100%',
128
+ hideCounts: false,
122
129
  },
123
130
  };
124
131
 
@@ -73,6 +73,13 @@ export class LineageFilterComponent extends PreactLitAdapter {
73
73
  @property({ type: String })
74
74
  width: string = '100%';
75
75
 
76
+ /**
77
+ * Whether to hide counts behind lineage options in the drop down selection.
78
+ * Defaults to false.
79
+ */
80
+ @property({ type: Boolean })
81
+ hideCounts: boolean | undefined = false;
82
+
76
83
  override render() {
77
84
  return (
78
85
  <LineageFilter
@@ -81,6 +88,7 @@ export class LineageFilterComponent extends PreactLitAdapter {
81
88
  placeholderText={this.placeholderText}
82
89
  value={this.value}
83
90
  width={this.width}
91
+ hideCounts={this.hideCounts}
84
92
  />
85
93
  );
86
94
  }
@@ -56,6 +56,11 @@ const meta: Meta = {
56
56
  type: 'text',
57
57
  },
58
58
  },
59
+ hideCounts: {
60
+ control: {
61
+ type: 'boolean',
62
+ },
63
+ },
59
64
  lapisFilter: {
60
65
  age: 18,
61
66
  },
@@ -75,6 +80,7 @@ const Template: StoryObj<LocationFilterProps> = {
75
80
  .value=${args.value}
76
81
  .width=${args.width}
77
82
  placeholderText=${ifDefined(args.placeholderText)}
83
+ .hideCounts=${args.hideCounts}
78
84
  ></gs-location-filter>
79
85
  </div>
80
86
  </gs-app>`;
@@ -87,6 +93,7 @@ const Template: StoryObj<LocationFilterProps> = {
87
93
  value: undefined,
88
94
  width: '100%',
89
95
  placeholderText: 'Enter a location',
96
+ hideCounts: false,
90
97
  },
91
98
  };
92
99
 
@@ -74,9 +74,16 @@ export class LocationFilterComponent extends PreactLitAdapter {
74
74
  /**
75
75
  * The placeholder text to display in the input field, if it is empty.
76
76
  */
77
- @property()
77
+ @property({ type: String })
78
78
  placeholderText: string | undefined = undefined;
79
79
 
80
+ /**
81
+ * Whether to hide counts behind location options in the drop down selection.
82
+ * Defaults to false.
83
+ */
84
+ @property({ type: Boolean })
85
+ hideCounts: boolean | undefined = false;
86
+
80
87
  override render() {
81
88
  return (
82
89
  <LocationFilter
@@ -85,6 +92,7 @@ export class LocationFilterComponent extends PreactLitAdapter {
85
92
  lapisFilter={this.lapisFilter}
86
93
  width={this.width}
87
94
  placeholderText={this.placeholderText}
95
+ hideCounts={this.hideCounts}
88
96
  />
89
97
  );
90
98
  }
@@ -38,6 +38,11 @@ const meta: Meta<MutationFilterProps> = {
38
38
  },
39
39
  },
40
40
  width: { control: 'text' },
41
+ enabledMutationTypes: {
42
+ control: {
43
+ type: 'object',
44
+ },
45
+ },
41
46
  },
42
47
  tags: ['autodocs'],
43
48
  };
@@ -48,7 +53,11 @@ const Template: StoryObj<MutationFilterProps> = {
48
53
  render: (args) => {
49
54
  return html` <gs-app lapis="${LAPIS_URL}">
50
55
  <div class="max-w-(--breakpoint-lg)">
51
- <gs-mutation-filter .initialValue=${args.initialValue} .width=${args.width}></gs-mutation-filter>
56
+ <gs-mutation-filter
57
+ .initialValue=${args.initialValue}
58
+ .width=${args.width}
59
+ .enabledMutationTypes=${args.enabledMutationTypes}
60
+ ></gs-mutation-filter>
52
61
  </div>
53
62
  </gs-app>`;
54
63
  },
@@ -104,6 +113,25 @@ export const FiresFilterChangedEvent: StoryObj<MutationFilterProps> = {
104
113
  },
105
114
  };
106
115
 
116
+ export const RestrictEnabledMutationTypes: StoryObj<MutationFilterProps> = {
117
+ ...Template,
118
+ args: {
119
+ ...Template.args,
120
+ enabledMutationTypes: ['nucleotideMutations', 'aminoAcidMutations'],
121
+ },
122
+ play: async ({ canvasElement }) => {
123
+ const canvas = await withinShadowRoot(canvasElement, 'gs-mutation-filter');
124
+
125
+ const inputField = () => canvas.getByPlaceholderText('Enter a mutation', { exact: false });
126
+
127
+ await waitFor(async () => {
128
+ const placeholderText = inputField().getAttribute('placeholder');
129
+
130
+ await expect(placeholderText).toEqual('Enter a mutation (e.g. 23T, E:57Q)');
131
+ });
132
+ },
133
+ };
134
+
107
135
  export const MultiSegmentedReferenceGenomes: StoryObj<MutationFilterProps> = {
108
136
  ...Template,
109
137
  args: {
@@ -2,7 +2,11 @@ import { customElement, property } from 'lit/decorators.js';
2
2
  import type { DetailedHTMLProps, HTMLAttributes } from 'react';
3
3
 
4
4
  import { ReferenceGenomesAwaiter } from '../../preact/components/ReferenceGenomesAwaiter';
5
- import { MutationFilter, type MutationFilterProps } from '../../preact/mutationFilter/mutation-filter';
5
+ import {
6
+ MutationFilter,
7
+ type MutationType,
8
+ type MutationFilterProps,
9
+ } from '../../preact/mutationFilter/mutation-filter';
6
10
  import type { MutationsFilter } from '../../types';
7
11
  import { type gsEventNames } from '../../utils/gsEventNames';
8
12
  import type { Equals, Expect } from '../../utils/typeAssertions';
@@ -43,6 +47,11 @@ import { PreactLitAdapter } from '../PreactLitAdapter';
43
47
  *
44
48
  * Examples: `ins_S:614:G`, `ins_614:G`
45
49
  *
50
+ * ### Enabled mutation types
51
+ *
52
+ * After parsing, the entered mutation/insertion also has to match the enabled mutation types,
53
+ * which are configured with the `enabledMutationTypes` attribute.
54
+ *
46
55
  * @fires {CustomEvent<{
47
56
  * nucleotideMutations: string[],
48
57
  * aminoAcidMutations: string[],
@@ -81,10 +90,28 @@ export class MutationFilterComponent extends PreactLitAdapter {
81
90
  @property({ type: String })
82
91
  width: string = '100%';
83
92
 
93
+ /**
94
+ * Which mutation types this input will accept.
95
+ * Any (or all) of the following can be given in a list:
96
+ *
97
+ * - `nucleotideMutations`
98
+ * - `nucleotideInsertions`
99
+ * - `aminoAcidMutations`
100
+ * - `aminoAcidInsertions`
101
+ *
102
+ * By default or if none are given, all types are accepted.
103
+ */
104
+ @property({ type: Object })
105
+ enabledMutationTypes: MutationType[] | undefined = undefined;
106
+
84
107
  override render() {
85
108
  return (
86
109
  <ReferenceGenomesAwaiter>
87
- <MutationFilter initialValue={this.initialValue} width={this.width} />
110
+ <MutationFilter
111
+ initialValue={this.initialValue}
112
+ width={this.width}
113
+ enabledMutationTypes={this.enabledMutationTypes}
114
+ />
88
115
  </ReferenceGenomesAwaiter>
89
116
  );
90
117
  }
@@ -63,6 +63,11 @@ const meta: Meta<Required<TextFilterProps>> = {
63
63
  type: 'text',
64
64
  },
65
65
  },
66
+ hideCounts: {
67
+ control: {
68
+ type: 'boolean',
69
+ },
70
+ },
66
71
  value: {
67
72
  control: {
68
73
  type: 'text',
@@ -92,6 +97,7 @@ export const Default: StoryObj<Required<TextFilterProps>> = {
92
97
  .lapisField=${args.lapisField}
93
98
  .lapisFilter=${args.lapisFilter}
94
99
  .placeholderText=${args.placeholderText}
100
+ .hideCounts=${args.hideCounts}
95
101
  .value=${args.value}
96
102
  .width=${args.width}
97
103
  ></gs-text-filter>
@@ -102,6 +108,7 @@ export const Default: StoryObj<Required<TextFilterProps>> = {
102
108
  lapisField: 'host',
103
109
  lapisFilter: { country: 'Germany' },
104
110
  placeholderText: 'Enter host name',
111
+ hideCounts: false,
105
112
  value: 'Homo sapiens',
106
113
  width: '100%',
107
114
  },