@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.
- package/README.md +4 -0
- package/custom-elements.json +37 -2
- package/dist/{NumberRangeFilterChangedEvent-CQ32Qy8D.js → NumberRangeFilterChangedEvent-BnPI-Asz.js} +17 -3
- package/dist/NumberRangeFilterChangedEvent-BnPI-Asz.js.map +1 -0
- package/dist/assets/{mutationOverTimeWorker-BJ_P2T8Y.js.map → mutationOverTimeWorker-DPS3tmOd.js.map} +1 -1
- package/dist/components.d.ts +50 -29
- package/dist/components.js +155 -61
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +53 -29
- package/dist/util.js +2 -1
- package/package.json +1 -1
- package/src/preact/genomeViewer/CDSPlot.tsx +13 -2
- package/src/preact/genomeViewer/loadGff3.ts +6 -0
- package/src/preact/mutationFilter/mutation-filter-info.tsx +2 -2
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +72 -1
- package/src/preact/mutationFilter/mutation-filter.tsx +75 -37
- package/src/preact/mutationFilter/parseAndValidateMutation.ts +11 -11
- package/src/preact/mutationFilter/parseMutation.spec.ts +32 -22
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +7 -4
- package/src/query/queryMutationsOverTime.ts +1 -1
- package/src/types.ts +17 -1
- package/src/utilEntrypoint.ts +4 -0
- package/src/utils/mutations.spec.ts +32 -0
- package/src/utils/mutations.ts +57 -10
- package/src/web-components/input/gs-mutation-filter.stories.ts +30 -1
- package/src/web-components/input/gs-mutation-filter.tsx +25 -2
- package/standalone-bundle/assets/{mutationOverTimeWorker-CkeGpKWp.js.map → mutationOverTimeWorker-Dp-A14AP.js.map} +1 -1
- package/standalone-bundle/dashboard-components.js +8187 -8116
- package/standalone-bundle/dashboard-components.js.map +1 -1
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
1104
|
+
'gs-date-range-filter': DateRangeFilterComponent;
|
|
1081
1105
|
}
|
|
1082
1106
|
interface HTMLElementEventMap {
|
|
1083
|
-
[gsEventNames.
|
|
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-
|
|
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-
|
|
1124
|
+
'gs-location-filter': LocationFilterComponent;
|
|
1116
1125
|
}
|
|
1117
1126
|
interface HTMLElementEventMap {
|
|
1118
|
-
[gsEventNames.
|
|
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-
|
|
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-
|
|
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
|
@@ -20,9 +20,16 @@ function getMaxTickNumber(fullWidth: number): number {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function getTicks(zoomStart: number, zoomEnd: number, fullWidth: number) {
|
|
23
|
-
|
|
23
|
+
let maxTickNumber = getMaxTickNumber(fullWidth);
|
|
24
24
|
const length = zoomEnd - zoomStart;
|
|
25
|
-
|
|
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><gene>:<position><base></b> or
|
|
141
141
|
<b><gene>:<base_ref><position><base></b>. A <b><base></b> can be one of
|
|
142
|
-
the 20 amino acid codes. It can also be <b>-</b> for deletion and <b>X</b>
|
|
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
|
|
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 {
|
|
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:
|
|
37
|
+
type: typeof mutationType.nucleotideMutations;
|
|
31
38
|
value: SubstitutionClass | DeletionClass;
|
|
32
39
|
};
|
|
33
40
|
|
|
34
41
|
type SelectedAminoAcidMutation = {
|
|
35
|
-
type:
|
|
42
|
+
type: typeof mutationType.aminoAcidMutations;
|
|
36
43
|
value: SubstitutionClass | DeletionClass;
|
|
37
44
|
};
|
|
38
45
|
|
|
39
46
|
type SelectedNucleotideInsertion = {
|
|
40
|
-
type:
|
|
47
|
+
type: typeof mutationType.nucleotideInsertions;
|
|
41
48
|
value: InsertionClass;
|
|
42
49
|
};
|
|
43
50
|
|
|
44
51
|
type SelectedAminoAcidInsertion = {
|
|
45
|
-
type:
|
|
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({
|
|
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
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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 =
|
|
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
|
|
329
|
+
case mutationType.nucleotideMutations:
|
|
292
330
|
return singleGraphColorRGBByName('green', alpha);
|
|
293
|
-
case
|
|
331
|
+
case mutationType.aminoAcidMutations:
|
|
294
332
|
return singleGraphColorRGBByName('teal', alpha);
|
|
295
|
-
case
|
|
333
|
+
case mutationType.nucleotideInsertions:
|
|
296
334
|
return singleGraphColorRGBByName('indigo', alpha);
|
|
297
|
-
case
|
|
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
|
|
370
|
+
case mutationType.nucleotideMutations:
|
|
333
371
|
return { ...acc, nucleotideMutations: [...acc.nucleotideMutations, filter.value.toString()] };
|
|
334
|
-
case
|
|
372
|
+
case mutationType.aminoAcidMutations:
|
|
335
373
|
return { ...acc, aminoAcidMutations: [...acc.aminoAcidMutations, filter.value.toString()] };
|
|
336
|
-
case
|
|
374
|
+
case mutationType.nucleotideInsertions:
|
|
337
375
|
return { ...acc, nucleotideInsertions: [...acc.nucleotideInsertions, filter.value.toString()] };
|
|
338
|
-
case
|
|
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
|
|
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
|
|
26
|
-
case
|
|
25
|
+
case mutationType.nucleotideInsertions:
|
|
26
|
+
case mutationType.nucleotideMutations:
|
|
27
27
|
return 'nucleotide';
|
|
28
|
-
case
|
|
29
|
-
case
|
|
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:
|
|
40
|
+
return { type: mutationType.nucleotideInsertions, value: possibleInsertion };
|
|
41
41
|
}
|
|
42
42
|
case 'amino acid':
|
|
43
|
-
return { type:
|
|
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:
|
|
54
|
+
return { type: mutationType.nucleotideMutations, value: possibleDeletion };
|
|
55
55
|
case 'amino acid':
|
|
56
|
-
return { type:
|
|
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:
|
|
67
|
+
return { type: mutationType.nucleotideMutations, value: possibleSubstitution };
|
|
68
68
|
}
|
|
69
69
|
case 'amino acid': {
|
|
70
|
-
return { type:
|
|
70
|
+
return { type: mutationType.aminoAcidMutations, value: possibleSubstitution };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
case undefined:
|