@genspectrum/dashboard-components 1.5.0 → 1.7.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 (30) hide show
  1. package/README.md +4 -0
  2. package/custom-elements.json +37 -2
  3. package/dist/{NumberRangeFilterChangedEvent-CQ32Qy8D.js → NumberRangeFilterChangedEvent-BnPI-Asz.js} +17 -3
  4. package/dist/NumberRangeFilterChangedEvent-BnPI-Asz.js.map +1 -0
  5. package/dist/assets/{mutationOverTimeWorker-BJ_P2T8Y.js.map → mutationOverTimeWorker-DPS3tmOd.js.map} +1 -1
  6. package/dist/components.d.ts +50 -29
  7. package/dist/components.js +155 -61
  8. package/dist/components.js.map +1 -1
  9. package/dist/util.d.ts +53 -29
  10. package/dist/util.js +2 -1
  11. package/package.json +1 -1
  12. package/src/preact/genomeViewer/CDSPlot.tsx +13 -2
  13. package/src/preact/genomeViewer/loadGff3.ts +6 -0
  14. package/src/preact/mutationFilter/mutation-filter-info.tsx +2 -2
  15. package/src/preact/mutationFilter/mutation-filter.stories.tsx +72 -1
  16. package/src/preact/mutationFilter/mutation-filter.tsx +75 -37
  17. package/src/preact/mutationFilter/parseAndValidateMutation.ts +11 -11
  18. package/src/preact/mutationFilter/parseMutation.spec.ts +32 -22
  19. package/src/preact/mutationsOverTime/mutations-over-time.tsx +7 -4
  20. package/src/query/queryMutationsOverTime.ts +1 -1
  21. package/src/types.ts +17 -1
  22. package/src/utilEntrypoint.ts +4 -0
  23. package/src/utils/mutations.spec.ts +32 -0
  24. package/src/utils/mutations.ts +57 -10
  25. package/src/web-components/input/gs-mutation-filter.stories.ts +30 -1
  26. package/src/web-components/input/gs-mutation-filter.tsx +25 -2
  27. package/standalone-bundle/assets/{mutationOverTimeWorker-CkeGpKWp.js.map → mutationOverTimeWorker-Dp-A14AP.js.map} +1 -1
  28. package/standalone-bundle/dashboard-components.js +8187 -8116
  29. package/standalone-bundle/dashboard-components.js.map +1 -1
  30. package/dist/NumberRangeFilterChangedEvent-CQ32Qy8D.js.map +0 -1
package/dist/util.d.ts CHANGED
@@ -194,6 +194,19 @@ declare const mapSourceSchema: default_2.ZodObject<{
194
194
  topologyObjectsKey: string;
195
195
  }>;
196
196
 
197
+ export declare type MeanProportionInterval = default_2.infer<typeof meanProportionIntervalSchema>;
198
+
199
+ declare const meanProportionIntervalSchema: default_2.ZodObject<{
200
+ min: default_2.ZodNumber;
201
+ max: default_2.ZodNumber;
202
+ }, "strip", default_2.ZodTypeAny, {
203
+ min: number;
204
+ max: number;
205
+ }, {
206
+ min: number;
207
+ max: number;
208
+ }>;
209
+
197
210
  export declare type MutationAnnotation = default_2.infer<typeof mutationAnnotationSchema>;
198
211
 
199
212
  export declare type MutationAnnotations = default_2.infer<typeof mutationAnnotationsSchema>;
@@ -430,6 +443,17 @@ export declare type MutationsView = default_2.infer<typeof mutationsViewSchema>;
430
443
 
431
444
  declare const mutationsViewSchema: default_2.ZodUnion<[default_2.ZodLiteral<"table">, default_2.ZodLiteral<"grid">, default_2.ZodLiteral<"insertions">]>;
432
445
 
446
+ export declare type MutationType = default_2.infer<typeof mutationTypeSchema>;
447
+
448
+ export declare const mutationType: {
449
+ readonly nucleotideMutations: "nucleotideMutations";
450
+ readonly nucleotideInsertions: "nucleotideInsertions";
451
+ readonly aminoAcidMutations: "aminoAcidMutations";
452
+ readonly aminoAcidInsertions: "aminoAcidInsertions";
453
+ };
454
+
455
+ declare const mutationTypeSchema: default_2.ZodEnum<["nucleotideMutations", "nucleotideInsertions", "aminoAcidMutations", "aminoAcidInsertions"]>;
456
+
433
457
  export declare type NamedLapisFilter = default_2.infer<typeof namedLapisFilterSchema>;
434
458
 
435
459
  declare const namedLapisFilterSchema: default_2.ZodObject<{
@@ -1013,7 +1037,7 @@ declare global {
1013
1037
 
1014
1038
  declare global {
1015
1039
  interface HTMLElementTagNameMap {
1016
- 'gs-mutations-over-time': MutationsOverTimeComponent;
1040
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1017
1041
  }
1018
1042
  }
1019
1043
 
@@ -1021,7 +1045,7 @@ declare global {
1021
1045
  declare global {
1022
1046
  namespace JSX {
1023
1047
  interface IntrinsicElements {
1024
- 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1048
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1025
1049
  }
1026
1050
  }
1027
1051
  }
@@ -1029,7 +1053,7 @@ declare global {
1029
1053
 
1030
1054
  declare global {
1031
1055
  interface HTMLElementTagNameMap {
1032
- 'gs-sequences-by-location': SequencesByLocationComponent;
1056
+ 'gs-mutations-over-time': MutationsOverTimeComponent;
1033
1057
  }
1034
1058
  }
1035
1059
 
@@ -1037,7 +1061,7 @@ declare global {
1037
1061
  declare global {
1038
1062
  namespace JSX {
1039
1063
  interface IntrinsicElements {
1040
- 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1064
+ 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1041
1065
  }
1042
1066
  }
1043
1067
  }
@@ -1045,7 +1069,7 @@ declare global {
1045
1069
 
1046
1070
  declare global {
1047
1071
  interface HTMLElementTagNameMap {
1048
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1072
+ 'gs-sequences-by-location': SequencesByLocationComponent;
1049
1073
  }
1050
1074
  }
1051
1075
 
@@ -1053,7 +1077,7 @@ declare global {
1053
1077
  declare global {
1054
1078
  namespace JSX {
1055
1079
  interface IntrinsicElements {
1056
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1080
+ 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1057
1081
  }
1058
1082
  }
1059
1083
  }
@@ -1077,26 +1101,11 @@ declare global {
1077
1101
 
1078
1102
  declare global {
1079
1103
  interface HTMLElementTagNameMap {
1080
- 'gs-location-filter': LocationFilterComponent;
1104
+ 'gs-date-range-filter': DateRangeFilterComponent;
1081
1105
  }
1082
1106
  interface HTMLElementEventMap {
1083
- [gsEventNames.locationChanged]: LocationChangedEvent;
1084
- }
1085
- }
1086
-
1087
-
1088
- declare global {
1089
- namespace JSX {
1090
- interface IntrinsicElements {
1091
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1092
- }
1093
- }
1094
- }
1095
-
1096
-
1097
- declare global {
1098
- interface HTMLElementTagNameMap {
1099
- 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1107
+ [gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
1108
+ [gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
1100
1109
  }
1101
1110
  }
1102
1111
 
@@ -1104,7 +1113,7 @@ declare global {
1104
1113
  declare global {
1105
1114
  namespace JSX {
1106
1115
  interface IntrinsicElements {
1107
- 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1116
+ 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1108
1117
  }
1109
1118
  }
1110
1119
  }
@@ -1112,11 +1121,10 @@ declare global {
1112
1121
 
1113
1122
  declare global {
1114
1123
  interface HTMLElementTagNameMap {
1115
- 'gs-date-range-filter': DateRangeFilterComponent;
1124
+ 'gs-location-filter': LocationFilterComponent;
1116
1125
  }
1117
1126
  interface HTMLElementEventMap {
1118
- [gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
1119
- [gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
1127
+ [gsEventNames.locationChanged]: LocationChangedEvent;
1120
1128
  }
1121
1129
  }
1122
1130
 
@@ -1124,7 +1132,7 @@ declare global {
1124
1132
  declare global {
1125
1133
  namespace JSX {
1126
1134
  interface IntrinsicElements {
1127
- 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1135
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1128
1136
  }
1129
1137
  }
1130
1138
  }
@@ -1207,6 +1215,22 @@ declare global {
1207
1215
  }
1208
1216
 
1209
1217
 
1218
+ declare global {
1219
+ interface HTMLElementTagNameMap {
1220
+ 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1221
+ }
1222
+ }
1223
+
1224
+
1225
+ declare global {
1226
+ namespace JSX {
1227
+ interface IntrinsicElements {
1228
+ 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1229
+ }
1230
+ }
1231
+ }
1232
+
1233
+
1210
1234
  declare module 'chart.js' {
1211
1235
  interface CartesianScaleTypeRegistry {
1212
1236
  logit: {
package/dist/util.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D, a, L, N, b, T, d, g, v } from "./NumberRangeFilterChangedEvent-CQ32Qy8D.js";
1
+ import { D, a, L, N, b, T, d, g, m, v } from "./NumberRangeFilterChangedEvent-BnPI-Asz.js";
2
2
  export {
3
3
  D as DateRangeOptionChangedEvent,
4
4
  a as LineageFilterChangedEvent,
@@ -8,6 +8,7 @@ export {
8
8
  T as TextFilterChangedEvent,
9
9
  d as dateRangeOptionPresets,
10
10
  g as gsEventNames,
11
+ m as mutationType,
11
12
  v as views
12
13
  };
13
14
  //# sourceMappingURL=util.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -20,9 +20,16 @@ function getMaxTickNumber(fullWidth: number): number {
20
20
  }
21
21
 
22
22
  function getTicks(zoomStart: number, zoomEnd: number, fullWidth: number) {
23
- const maxTickNumber = getMaxTickNumber(fullWidth);
23
+ let maxTickNumber = getMaxTickNumber(fullWidth);
24
24
  const length = zoomEnd - zoomStart;
25
- const minTickSize = length / maxTickNumber;
25
+ let minTickSize = length / maxTickNumber;
26
+ if (minTickSize <= 1) {
27
+ maxTickNumber = MIN_TICK_NUMBER;
28
+ minTickSize = length / maxTickNumber;
29
+ }
30
+ if (minTickSize <= 1) {
31
+ return [];
32
+ }
26
33
  let maxTickSize = 10 ** Math.round(Math.log(minTickSize) / Math.log(10));
27
34
  const numTicks = Math.round(length / maxTickSize);
28
35
  if (numTicks > maxTickNumber) {
@@ -78,6 +85,7 @@ const XAxis: FunctionComponent<XAxisProps> = (componentProps) => {
78
85
  width: `calc(${widthPercent}% - 1px)`,
79
86
  }}
80
87
  >
88
+ {/* TODO(#994): determine if text can be shown based on text width */}
81
89
  {width >= averageWidth ? tick.start : ''}
82
90
  </div>
83
91
  );
@@ -142,6 +150,9 @@ const CDSBars: FunctionComponent<CDSBarsProps> = (componentProps) => {
142
150
  if (start >= end) {
143
151
  return null;
144
152
  }
153
+ if (zoomEnd - zoomStart <= 2) {
154
+ return null;
155
+ }
145
156
 
146
157
  const widthPercent = ((end - start) / visibleRegionLength) * 100;
147
158
  const leftPercent = ((start - zoomStart) / visibleRegionLength) * 100;
@@ -22,6 +22,12 @@ export async function loadGff3(gff3Source: string, genomeLength: number | undefi
22
22
  }
23
23
 
24
24
  const response = await fetch(gff3Source);
25
+ if (!response.ok) {
26
+ throw new UserFacingError(
27
+ 'GFF3 download failed',
28
+ `Server returned ${response.status} ${response.statusText} for ${response.url}`,
29
+ );
30
+ }
25
31
  const content = await response.text();
26
32
  genomeLength ??= loadGenomeLength(content);
27
33
  return { features: parseGFF3(content), length: genomeLength };
@@ -139,8 +139,8 @@ const AminoAcidMutationsInfo = () => {
139
139
  <InfoParagraph>
140
140
  An amino acid mutation has the format <b>&lt;gene&gt;:&lt;position&gt;&lt;base&gt;</b> or
141
141
  <b>&lt;gene&gt;:&lt;base_ref&gt;&lt;position&gt;&lt;base&gt;</b>. A <b>&lt;base&gt;</b> can be one of
142
- the 20 amino acid codes. It can also be <b>-</b> for deletion and <b>X</b> for unknown. Example:{' '}
143
- <ExampleMutation mutationType='substitution' sequenceType='amino acid' />.
142
+ the 20 amino acid codes. It can also be <b>*</b> for a stop codon, <b>-</b> for deletion and <b>X</b>{' '}
143
+ for unknown. Example: <ExampleMutation mutationType='substitution' sequenceType='amino acid' />.
144
144
  </InfoParagraph>
145
145
  <InfoParagraph>
146
146
  Insertions can be searched for in the same manner, they just need to have <b>ins_</b> appended to the
@@ -6,6 +6,7 @@ import { MutationFilter, type MutationFilterProps } from './mutation-filter';
6
6
  import { previewHandles } from '../../../.storybook/preview';
7
7
  import { LAPIS_URL } from '../../constants';
8
8
  import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
9
+ import { mutationType } from '../../types';
9
10
  import { gsEventNames } from '../../utils/gsEventNames';
10
11
  import { LapisUrlContextProvider } from '../LapisUrlContext';
11
12
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
@@ -27,6 +28,11 @@ const meta: Meta<MutationFilterProps> = {
27
28
  type: 'object',
28
29
  },
29
30
  },
31
+ enabledMutationTypes: {
32
+ control: {
33
+ type: 'object',
34
+ },
35
+ },
30
36
  },
31
37
  };
32
38
 
@@ -36,7 +42,11 @@ export const Default: StoryObj<MutationFilterProps> = {
36
42
  render: (args) => (
37
43
  <LapisUrlContextProvider value={LAPIS_URL}>
38
44
  <ReferenceGenomeContext.Provider value={referenceGenome}>
39
- <MutationFilter width={args.width} initialValue={args.initialValue} />
45
+ <MutationFilter
46
+ width={args.width}
47
+ initialValue={args.initialValue}
48
+ enabledMutationTypes={args.enabledMutationTypes}
49
+ />
40
50
  </ReferenceGenomeContext.Provider>
41
51
  </LapisUrlContextProvider>
42
52
  ),
@@ -217,6 +227,67 @@ export const IgnoresDuplicatesOnPasteCommaSeparatedList: StoryObj<MutationFilter
217
227
  },
218
228
  };
219
229
 
230
+ export const FiltersOutDisabledMutationTypes: StoryObj<MutationFilterProps> = {
231
+ ...Default,
232
+ args: {
233
+ ...Default.args,
234
+ enabledMutationTypes: [mutationType.nucleotideMutations],
235
+ },
236
+ play: async ({ canvasElement, step }) => {
237
+ const { canvas, changedListenerMock } = await prepare(canvasElement, step);
238
+
239
+ await step('Enters an invalid insertion mutation', async () => {
240
+ await testNoOptionsExist(canvas, 'ins_23:T');
241
+ await expect(changedListenerMock).not.toHaveBeenCalled();
242
+
243
+ await userEvent.type(inputField(canvas), '{backspace>12/}', INPUT_DELAY);
244
+ });
245
+
246
+ await step('Enters an invalid amino acid mutation', async () => {
247
+ await testNoOptionsExist(canvas, 'S:A1234T');
248
+ await expect(changedListenerMock).not.toHaveBeenCalled();
249
+
250
+ await userEvent.type(inputField(canvas), '{backspace>12/}', INPUT_DELAY);
251
+ });
252
+
253
+ await step('Enter a comma separated list of invalid mutations', async () => {
254
+ await pasteMutations(canvas, 'insX, ins_123:AA');
255
+
256
+ await waitFor(() =>
257
+ expect(changedListenerMock).toHaveBeenCalledWith(
258
+ expect.objectContaining({
259
+ detail: {
260
+ nucleotideMutations: [],
261
+ aminoAcidMutations: [],
262
+ nucleotideInsertions: [],
263
+ aminoAcidInsertions: [],
264
+ },
265
+ }),
266
+ ),
267
+ );
268
+
269
+ await userEvent.type(inputField(canvas), '{backspace>24/}', INPUT_DELAY);
270
+ });
271
+
272
+ await step('Enter a valid mutation', async () => {
273
+ await submitMutation(canvas, 'A123T');
274
+
275
+ await waitFor(() =>
276
+ expect(changedListenerMock).toHaveBeenCalledWith(
277
+ expect.objectContaining({
278
+ detail: {
279
+ nucleotideMutations: ['A123T'],
280
+ aminoAcidMutations: [],
281
+ nucleotideInsertions: [],
282
+ aminoAcidInsertions: [],
283
+ },
284
+ }),
285
+ ),
286
+ );
287
+ });
288
+ },
289
+ };
290
+
220
291
  export const FiresFilterChangedEvents: StoryObj<MutationFilterProps> = {
221
292
  ...Default,
222
293
  play: async ({ canvasElement, step }) => {
@@ -1,13 +1,19 @@
1
1
  import { useCombobox, useMultipleSelection } from 'downshift/preact';
2
2
  import { type FunctionComponent } from 'preact';
3
- import { useContext, useMemo, useRef, useState } from 'preact/hooks';
3
+ import { useContext, useEffect, useMemo, useRef, useState } from 'preact/hooks';
4
4
  import z from 'zod';
5
5
 
6
6
  import { getExampleMutation } from './ExampleMutation';
7
7
  import { MutationFilterInfo } from './mutation-filter-info';
8
8
  import { parseAndValidateMutation } from './parseAndValidateMutation';
9
9
  import { type ReferenceGenome } from '../../lapisApi/ReferenceGenome';
10
- import { type MutationsFilter, mutationsFilterSchema } from '../../types';
10
+ import {
11
+ type MutationsFilter,
12
+ mutationsFilterSchema,
13
+ mutationType,
14
+ mutationTypeSchema,
15
+ type MutationType,
16
+ } from '../../types';
11
17
  import { gsEventNames } from '../../utils/gsEventNames';
12
18
  import { type DeletionClass, type InsertionClass, type SubstitutionClass } from '../../utils/mutations';
13
19
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
@@ -17,6 +23,7 @@ import { singleGraphColorRGBByName } from '../shared/charts/colors';
17
23
 
18
24
  const mutationFilterInnerPropsSchema = z.object({
19
25
  initialValue: z.union([mutationsFilterSchema.optional(), z.array(z.string()), z.undefined()]),
26
+ enabledMutationTypes: z.array(mutationTypeSchema).optional(),
20
27
  });
21
28
 
22
29
  const mutationFilterPropsSchema = mutationFilterInnerPropsSchema.extend({
@@ -27,22 +34,22 @@ export type MutationFilterInnerProps = z.infer<typeof mutationFilterInnerPropsSc
27
34
  export type MutationFilterProps = z.infer<typeof mutationFilterPropsSchema>;
28
35
 
29
36
  type SelectedNucleotideMutation = {
30
- type: 'nucleotideMutations';
37
+ type: typeof mutationType.nucleotideMutations;
31
38
  value: SubstitutionClass | DeletionClass;
32
39
  };
33
40
 
34
41
  type SelectedAminoAcidMutation = {
35
- type: 'aminoAcidMutations';
42
+ type: typeof mutationType.aminoAcidMutations;
36
43
  value: SubstitutionClass | DeletionClass;
37
44
  };
38
45
 
39
46
  type SelectedNucleotideInsertion = {
40
- type: 'nucleotideInsertions';
47
+ type: typeof mutationType.nucleotideInsertions;
41
48
  value: InsertionClass;
42
49
  };
43
50
 
44
51
  type SelectedAminoAcidInsertion = {
45
- type: 'aminoAcidInsertions';
52
+ type: typeof mutationType.aminoAcidInsertions;
46
53
  value: InsertionClass;
47
54
  };
48
55
 
@@ -53,7 +60,7 @@ export type MutationFilterItem =
53
60
  | SelectedAminoAcidInsertion;
54
61
 
55
62
  export const MutationFilter: FunctionComponent<MutationFilterProps> = (props) => {
56
- const { width, initialValue } = props;
63
+ const { width, initialValue, enabledMutationTypes } = props;
57
64
  return (
58
65
  <ErrorBoundary
59
66
  size={{ height: '40px', width }}
@@ -62,20 +69,23 @@ export const MutationFilter: FunctionComponent<MutationFilterProps> = (props) =>
62
69
  componentProps={props}
63
70
  >
64
71
  <div style={{ width }}>
65
- <MutationFilterInner initialValue={initialValue} />
72
+ <MutationFilterInner initialValue={initialValue} enabledMutationTypes={enabledMutationTypes} />
66
73
  </div>
67
74
  </ErrorBoundary>
68
75
  );
69
76
  };
70
77
 
71
- function MutationFilterInner({ initialValue }: MutationFilterInnerProps) {
78
+ function MutationFilterInner({
79
+ initialValue,
80
+ enabledMutationTypes = Object.values(mutationType),
81
+ }: MutationFilterInnerProps) {
72
82
  const referenceGenome = useContext(ReferenceGenomeContext);
73
83
  const filterRef = useRef<HTMLDivElement>(null);
74
84
  const [inputValue, setInputValue] = useState('');
75
85
 
76
86
  const initialState = useMemo(() => {
77
- return getInitialState(initialValue, referenceGenome);
78
- }, [initialValue, referenceGenome]);
87
+ return getInitialState(initialValue, referenceGenome, enabledMutationTypes);
88
+ }, [initialValue, referenceGenome, enabledMutationTypes]);
79
89
 
80
90
  const [selectedItems, setSelectedItems] = useState<MutationFilterItem[]>(initialState);
81
91
  const [itemCandidate, setItemCandidate] = useState<MutationFilterItem | null>(null);
@@ -83,6 +93,12 @@ function MutationFilterInner({ initialValue }: MutationFilterInnerProps) {
83
93
 
84
94
  const items = itemCandidate ? [itemCandidate] : [];
85
95
 
96
+ useEffect(() => {
97
+ setSelectedItems((prevSelectedItems) =>
98
+ prevSelectedItems.filter((mutFilterItem) => enabledMutationTypes.includes(mutFilterItem.type)),
99
+ );
100
+ }, [enabledMutationTypes, selectedItems]);
101
+
86
102
  const fireChangeEvent = (selectedFilters: MutationFilterItem[]) => {
87
103
  const detail = mapToMutationFilterStrings(selectedFilters);
88
104
 
@@ -115,16 +131,24 @@ function MutationFilterInner({ initialValue }: MutationFilterInnerProps) {
115
131
  const values = newInputValue.split(',').map((value) => {
116
132
  return { value, parsedValue: parseAndValidateMutation(value.trim(), referenceGenome) };
117
133
  });
118
- const validEntries = values.map((value) => value.parsedValue).filter((value) => value !== null);
119
- const invalidInput = values
120
- .filter((value) => value.parsedValue === null)
121
- .map((value) => value.value.trim())
122
- .join(',');
134
+
135
+ const validEntries: MutationFilterItem[] = [];
136
+ const rejected: string[] = [];
137
+
138
+ for (const v of values) {
139
+ if (v.parsedValue === null) {
140
+ rejected.push(v.value.trim());
141
+ } else if (enabledMutationTypes.includes(v.parsedValue.type)) {
142
+ validEntries.push(v.parsedValue);
143
+ } else {
144
+ rejected.push(v.parsedValue.value.code);
145
+ }
146
+ }
123
147
 
124
148
  const selectedItemCandidates = [...selectedItems, ...validEntries];
125
149
 
126
150
  handleSelectedItemsChanged(extractUniqueValues(selectedItemCandidates));
127
- setInputValue(invalidInput);
151
+ setInputValue(rejected.join(','));
128
152
  setItemCandidate(null);
129
153
  } else {
130
154
  setInputValue(newInputValue ?? '');
@@ -133,7 +157,8 @@ function MutationFilterInner({ initialValue }: MutationFilterInnerProps) {
133
157
  const alreadyExists = selectedItems.find(
134
158
  (selectedItem) => selectedItem.value.code === candidate?.value.code,
135
159
  );
136
- if (!alreadyExists) {
160
+ const allowedType = candidate !== null && enabledMutationTypes.includes(candidate.type);
161
+ if (!alreadyExists && allowedType) {
137
162
  setItemCandidate(candidate);
138
163
  }
139
164
  }
@@ -216,7 +241,7 @@ function MutationFilterInner({ initialValue }: MutationFilterInnerProps) {
216
241
  })}
217
242
  <div className='flex gap-0.5 grow p-1'>
218
243
  <input
219
- placeholder={getPlaceholder(referenceGenome)}
244
+ placeholder={getPlaceholder(referenceGenome, enabledMutationTypes)}
220
245
  className='w-full focus:outline-none min-w-8'
221
246
  {...getInputProps(getDropdownProps({ preventKeyAction: isOpen }))}
222
247
  onBlur={() => {
@@ -261,7 +286,11 @@ function extractUniqueValues(newSelectedItems: MutationFilterItem[]) {
261
286
  return Array.from(uniqueMutationsMap.values());
262
287
  }
263
288
 
264
- function getInitialState(initialValue: MutationsFilter | string[] | undefined, referenceGenome: ReferenceGenome) {
289
+ function getInitialState(
290
+ initialValue: MutationsFilter | string[] | undefined,
291
+ referenceGenome: ReferenceGenome,
292
+ enabledMutationTypes: MutationType[],
293
+ ) {
265
294
  if (initialValue === undefined) {
266
295
  return [];
267
296
  }
@@ -270,31 +299,40 @@ function getInitialState(initialValue: MutationsFilter | string[] | undefined, r
270
299
 
271
300
  return values
272
301
  .map((value) => parseAndValidateMutation(value, referenceGenome))
273
- .filter((parsedMutation) => parsedMutation !== null);
302
+ .filter((parsedMutation): parsedMutation is MutationFilterItem => parsedMutation !== null)
303
+ .filter((mutation) => enabledMutationTypes.includes(mutation.type));
274
304
  }
275
305
 
276
- function getPlaceholder(referenceGenome: ReferenceGenome) {
277
- const nucleotideSubstitution = getExampleMutation(referenceGenome, 'nucleotide', 'substitution');
278
- const nucleotideInsertion = getExampleMutation(referenceGenome, 'nucleotide', 'insertion');
279
- const aminoAcidSubstitution = getExampleMutation(referenceGenome, 'amino acid', 'substitution');
280
- const aminoAcidInsertion = getExampleMutation(referenceGenome, 'amino acid', 'insertion');
306
+ function getPlaceholder(referenceGenome: ReferenceGenome, enabledMutationTypes: MutationType[]) {
307
+ const exampleMutationList = [];
308
+
309
+ if (enabledMutationTypes.includes(mutationType.nucleotideMutations)) {
310
+ exampleMutationList.push(getExampleMutation(referenceGenome, 'nucleotide', 'substitution'));
311
+ }
312
+ if (enabledMutationTypes.includes(mutationType.nucleotideInsertions)) {
313
+ exampleMutationList.push(getExampleMutation(referenceGenome, 'nucleotide', 'insertion'));
314
+ }
315
+ if (enabledMutationTypes.includes(mutationType.aminoAcidMutations)) {
316
+ exampleMutationList.push(getExampleMutation(referenceGenome, 'amino acid', 'substitution'));
317
+ }
318
+ if (enabledMutationTypes.includes(mutationType.aminoAcidInsertions)) {
319
+ exampleMutationList.push(getExampleMutation(referenceGenome, 'amino acid', 'insertion'));
320
+ }
281
321
 
282
- const exampleMutations = [nucleotideSubstitution, nucleotideInsertion, aminoAcidSubstitution, aminoAcidInsertion]
283
- .filter((example) => example !== '')
284
- .join(', ');
322
+ const exampleMutations = exampleMutationList.filter((example) => example !== '').join(', ');
285
323
 
286
324
  return `Enter a mutation (e.g. ${exampleMutations})`;
287
325
  }
288
326
 
289
327
  const backgroundColorMap = (data: MutationFilterItem, alpha: number = 0.4) => {
290
328
  switch (data.type) {
291
- case 'nucleotideMutations':
329
+ case mutationType.nucleotideMutations:
292
330
  return singleGraphColorRGBByName('green', alpha);
293
- case 'aminoAcidMutations':
331
+ case mutationType.aminoAcidMutations:
294
332
  return singleGraphColorRGBByName('teal', alpha);
295
- case 'nucleotideInsertions':
333
+ case mutationType.nucleotideInsertions:
296
334
  return singleGraphColorRGBByName('indigo', alpha);
297
- case 'aminoAcidInsertions':
335
+ case mutationType.aminoAcidInsertions:
298
336
  return singleGraphColorRGBByName('purple', alpha);
299
337
  }
300
338
  };
@@ -329,13 +367,13 @@ function mapToMutationFilterStrings(selectedFilters: MutationFilterItem[]) {
329
367
  return selectedFilters.reduce<MutationsFilter>(
330
368
  (acc, filter) => {
331
369
  switch (filter.type) {
332
- case 'nucleotideMutations':
370
+ case mutationType.nucleotideMutations:
333
371
  return { ...acc, nucleotideMutations: [...acc.nucleotideMutations, filter.value.toString()] };
334
- case 'aminoAcidMutations':
372
+ case mutationType.aminoAcidMutations:
335
373
  return { ...acc, aminoAcidMutations: [...acc.aminoAcidMutations, filter.value.toString()] };
336
- case 'nucleotideInsertions':
374
+ case mutationType.nucleotideInsertions:
337
375
  return { ...acc, nucleotideInsertions: [...acc.nucleotideInsertions, filter.value.toString()] };
338
- case 'aminoAcidInsertions':
376
+ case mutationType.aminoAcidInsertions:
339
377
  return { ...acc, aminoAcidInsertions: [...acc.aminoAcidInsertions, filter.value.toString()] };
340
378
  }
341
379
  },
@@ -1,7 +1,7 @@
1
1
  import { type MutationFilterItem } from './mutation-filter';
2
2
  import { sequenceTypeFromSegment } from './sequenceTypeFromSegment';
3
3
  import type { ReferenceGenome } from '../../lapisApi/ReferenceGenome';
4
- import type { SequenceType } from '../../types';
4
+ import { type SequenceType, mutationType } from '../../types';
5
5
  import { DeletionClass, InsertionClass, type Mutation, SubstitutionClass } from '../../utils/mutations';
6
6
 
7
7
  export const parseAndValidateMutation = (
@@ -22,11 +22,11 @@ export const parseAndValidateMutation = (
22
22
 
23
23
  const getSequenceType = (type: MutationFilterItem['type']) => {
24
24
  switch (type) {
25
- case 'nucleotideInsertions':
26
- case 'nucleotideMutations':
25
+ case mutationType.nucleotideInsertions:
26
+ case mutationType.nucleotideMutations:
27
27
  return 'nucleotide';
28
- case 'aminoAcidInsertions':
29
- case 'aminoAcidMutations':
28
+ case mutationType.aminoAcidInsertions:
29
+ case mutationType.aminoAcidMutations:
30
30
  return 'amino acid';
31
31
  }
32
32
  };
@@ -37,10 +37,10 @@ const parseMutation = (value: string, referenceGenome: ReferenceGenome): Mutatio
37
37
  const sequenceType = sequenceTypeFromSegment(possibleInsertion.segment, referenceGenome);
38
38
  switch (sequenceType) {
39
39
  case 'nucleotide': {
40
- return { type: 'nucleotideInsertions', value: possibleInsertion };
40
+ return { type: mutationType.nucleotideInsertions, value: possibleInsertion };
41
41
  }
42
42
  case 'amino acid':
43
- return { type: 'aminoAcidInsertions', value: possibleInsertion };
43
+ return { type: mutationType.aminoAcidInsertions, value: possibleInsertion };
44
44
  case undefined:
45
45
  return null;
46
46
  }
@@ -51,9 +51,9 @@ const parseMutation = (value: string, referenceGenome: ReferenceGenome): Mutatio
51
51
  const sequenceType = sequenceTypeFromSegment(possibleDeletion.segment, referenceGenome);
52
52
  switch (sequenceType) {
53
53
  case 'nucleotide':
54
- return { type: 'nucleotideMutations', value: possibleDeletion };
54
+ return { type: mutationType.nucleotideMutations, value: possibleDeletion };
55
55
  case 'amino acid':
56
- return { type: 'aminoAcidMutations', value: possibleDeletion };
56
+ return { type: mutationType.aminoAcidMutations, value: possibleDeletion };
57
57
  case undefined:
58
58
  return null;
59
59
  }
@@ -64,10 +64,10 @@ const parseMutation = (value: string, referenceGenome: ReferenceGenome): Mutatio
64
64
  const sequenceType = sequenceTypeFromSegment(possibleSubstitution.segment, referenceGenome);
65
65
  switch (sequenceType) {
66
66
  case 'nucleotide': {
67
- return { type: 'nucleotideMutations', value: possibleSubstitution };
67
+ return { type: mutationType.nucleotideMutations, value: possibleSubstitution };
68
68
  }
69
69
  case 'amino acid': {
70
- return { type: 'aminoAcidMutations', value: possibleSubstitution };
70
+ return { type: mutationType.aminoAcidMutations, value: possibleSubstitution };
71
71
  }
72
72
 
73
73
  case undefined: