@genspectrum/dashboard-components 0.18.5 → 0.18.6
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 +12 -0
- package/custom-elements.json +1 -1
- package/dist/components.d.ts +16 -16
- package/dist/components.js +765 -315
- package/dist/components.js.map +1 -1
- package/dist/style.css +2 -2
- package/dist/util.d.ts +16 -16
- package/package.json +2 -2
- package/src/preact/MutationAnnotationsContext.tsx +34 -27
- package/src/preact/components/dropdown.tsx +1 -1
- package/src/preact/components/info.tsx +1 -1
- package/src/preact/components/mutations-over-time-text-filter.stories.tsx +57 -0
- package/src/preact/components/mutations-over-time-text-filter.tsx +63 -0
- package/src/preact/components/segment-selector.tsx +1 -1
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +169 -50
- package/src/preact/mutationFilter/mutation-filter.tsx +239 -234
- package/src/preact/mutationFilter/parseAndValidateMutation.ts +62 -10
- package/src/preact/mutationFilter/parseMutation.spec.ts +62 -47
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +128 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +39 -2
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +8 -11
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +27 -0
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +26 -5
- package/src/preact/shared/tanstackTable/pagination-context.tsx +30 -0
- package/src/preact/shared/tanstackTable/pagination.tsx +19 -6
- package/src/preact/shared/tanstackTable/tanstackTable.tsx +17 -3
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +19 -1
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +6 -1
- package/src/web-components/input/gs-mutation-filter.stories.ts +4 -4
- package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +1 -1
- package/standalone-bundle/dashboard-components.js +12896 -13366
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { useCombobox, useMultipleSelection } from 'downshift/preact';
|
|
1
2
|
import { type FunctionComponent } from 'preact';
|
|
2
|
-
import { useContext, useRef, useState } from 'preact/hooks';
|
|
3
|
+
import { useContext, useMemo, useRef, useState } from 'preact/hooks';
|
|
3
4
|
import z from 'zod';
|
|
4
5
|
|
|
5
6
|
import { getExampleMutation } from './ExampleMutation';
|
|
6
7
|
import { MutationFilterInfo } from './mutation-filter-info';
|
|
7
|
-
import { parseAndValidateMutation
|
|
8
|
+
import { parseAndValidateMutation } from './parseAndValidateMutation';
|
|
8
9
|
import { type ReferenceGenome } from '../../lapisApi/ReferenceGenome';
|
|
9
10
|
import { type MutationsFilter, mutationsFilterSchema } from '../../types';
|
|
10
11
|
import { type DeletionClass, type InsertionClass, type SubstitutionClass } from '../../utils/mutations';
|
|
@@ -24,55 +25,64 @@ const mutationFilterPropsSchema = mutationFilterInnerPropsSchema.extend({
|
|
|
24
25
|
export type MutationFilterInnerProps = z.infer<typeof mutationFilterInnerPropsSchema>;
|
|
25
26
|
export type MutationFilterProps = z.infer<typeof mutationFilterPropsSchema>;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
nucleotideInsertions: InsertionClass[];
|
|
31
|
-
aminoAcidInsertions: InsertionClass[];
|
|
28
|
+
type SelectedNucleotideMutation = {
|
|
29
|
+
type: 'nucleotideMutations';
|
|
30
|
+
value: SubstitutionClass | DeletionClass;
|
|
32
31
|
};
|
|
33
32
|
|
|
33
|
+
type SelectedAminoAcidMutation = {
|
|
34
|
+
type: 'aminoAcidMutations';
|
|
35
|
+
value: SubstitutionClass | DeletionClass;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type SelectedNucleotideInsertion = {
|
|
39
|
+
type: 'nucleotideInsertions';
|
|
40
|
+
value: InsertionClass;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type SelectedAminoAcidInsertion = {
|
|
44
|
+
type: 'aminoAcidInsertions';
|
|
45
|
+
value: InsertionClass;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type MutationFilterItem =
|
|
49
|
+
| SelectedNucleotideMutation
|
|
50
|
+
| SelectedAminoAcidMutation
|
|
51
|
+
| SelectedNucleotideInsertion
|
|
52
|
+
| SelectedAminoAcidInsertion;
|
|
53
|
+
|
|
34
54
|
export const MutationFilter: FunctionComponent<MutationFilterProps> = (props) => {
|
|
35
55
|
const { width, initialValue } = props;
|
|
36
56
|
return (
|
|
37
57
|
<ErrorBoundary
|
|
38
|
-
size={{ height: '
|
|
58
|
+
size={{ height: '40px', width }}
|
|
39
59
|
layout='horizontal'
|
|
40
60
|
schema={mutationFilterPropsSchema}
|
|
41
61
|
componentProps={props}
|
|
42
62
|
>
|
|
43
|
-
<div style={width}>
|
|
63
|
+
<div style={{ width }}>
|
|
44
64
|
<MutationFilterInner initialValue={initialValue} />
|
|
45
65
|
</div>
|
|
46
66
|
</ErrorBoundary>
|
|
47
67
|
);
|
|
48
68
|
};
|
|
49
69
|
|
|
50
|
-
|
|
70
|
+
function MutationFilterInner({ initialValue }: MutationFilterInnerProps) {
|
|
51
71
|
const referenceGenome = useContext(ReferenceGenomeContext);
|
|
52
|
-
const [selectedFilters, setSelectedFilters] = useState<SelectedFilters>(
|
|
53
|
-
getInitialState(initialValue, referenceGenome),
|
|
54
|
-
);
|
|
55
|
-
|
|
56
72
|
const filterRef = useRef<HTMLDivElement>(null);
|
|
73
|
+
const [inputValue, setInputValue] = useState('');
|
|
57
74
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
'This organism has neither nucleotide nor amino acid sequences configured in its reference genome. You cannot filter by mutations.',
|
|
62
|
-
);
|
|
63
|
-
}
|
|
75
|
+
const initialState = useMemo(() => {
|
|
76
|
+
return getInitialState(initialValue, referenceGenome);
|
|
77
|
+
}, [initialValue, referenceGenome]);
|
|
64
78
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
[option.type]: selectedFilters[option.type].filter((i) => option.value.toString() != i.toString()),
|
|
69
|
-
};
|
|
79
|
+
const [selectedItems, setSelectedItems] = useState<MutationFilterItem[]>(initialState);
|
|
80
|
+
const [itemCandidate, setItemCandidate] = useState<MutationFilterItem | null>(null);
|
|
81
|
+
const [showErrorIndicator, setShowErrorIndicator] = useState(false);
|
|
70
82
|
|
|
71
|
-
|
|
72
|
-
fireChangeEvent(newSelectedFilters);
|
|
73
|
-
};
|
|
83
|
+
const items = itemCandidate ? [itemCandidate] : [];
|
|
74
84
|
|
|
75
|
-
const fireChangeEvent = (selectedFilters:
|
|
85
|
+
const fireChangeEvent = (selectedFilters: MutationFilterItem[]) => {
|
|
76
86
|
const detail = mapToMutationFilterStrings(selectedFilters);
|
|
77
87
|
|
|
78
88
|
filterRef.current?.dispatchEvent(
|
|
@@ -84,170 +94,183 @@ export const MutationFilterInner: FunctionComponent<MutationFilterInnerProps> =
|
|
|
84
94
|
);
|
|
85
95
|
};
|
|
86
96
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
</div>
|
|
92
|
-
|
|
93
|
-
<div className='relative w-full p-1'>
|
|
94
|
-
<MutationFilterSelector
|
|
95
|
-
referenceGenome={referenceGenome}
|
|
96
|
-
setSelectedFilters={(newSelectedFilters) => {
|
|
97
|
-
setSelectedFilters(newSelectedFilters);
|
|
98
|
-
fireChangeEvent(newSelectedFilters);
|
|
99
|
-
}}
|
|
100
|
-
selectedFilters={selectedFilters}
|
|
101
|
-
/>
|
|
102
|
-
<SelectedMutationFilterDisplay
|
|
103
|
-
selectedFilters={selectedFilters}
|
|
104
|
-
handleRemoveValue={handleRemoveValue}
|
|
105
|
-
/>
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
function getInitialState(initialValue: MutationsFilter | string[] | undefined, referenceGenome: ReferenceGenome) {
|
|
112
|
-
if (initialValue === undefined) {
|
|
113
|
-
return {
|
|
114
|
-
nucleotideMutations: [],
|
|
115
|
-
aminoAcidMutations: [],
|
|
116
|
-
nucleotideInsertions: [],
|
|
117
|
-
aminoAcidInsertions: [],
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const values = Array.isArray(initialValue) ? initialValue : Object.values(initialValue).flatMap((it) => it);
|
|
122
|
-
|
|
123
|
-
return values.reduce(
|
|
124
|
-
(selectedFilters, value) => {
|
|
125
|
-
const parsedMutation = parseAndValidateMutation(value, referenceGenome);
|
|
126
|
-
if (parsedMutation === null) {
|
|
127
|
-
return selectedFilters;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
...selectedFilters,
|
|
132
|
-
[parsedMutation.type]: [...selectedFilters[parsedMutation.type], parsedMutation.value],
|
|
133
|
-
};
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
nucleotideMutations: [],
|
|
137
|
-
aminoAcidMutations: [],
|
|
138
|
-
nucleotideInsertions: [],
|
|
139
|
-
aminoAcidInsertions: [],
|
|
140
|
-
} as SelectedFilters,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const MutationFilterSelector: FunctionComponent<{
|
|
145
|
-
referenceGenome: ReferenceGenome;
|
|
146
|
-
setSelectedFilters: (option: SelectedFilters) => void;
|
|
147
|
-
selectedFilters: SelectedFilters;
|
|
148
|
-
}> = ({ referenceGenome, setSelectedFilters, selectedFilters }) => {
|
|
149
|
-
const [option, setOption] = useState<ParsedMutationFilter | null>(null);
|
|
150
|
-
const [inputValue, setInputValue] = useState('');
|
|
151
|
-
|
|
152
|
-
const selectorRef = useRef<HTMLDivElement>(null);
|
|
97
|
+
const handleSelectedItemsChanged = (newSelectedItems: MutationFilterItem[]) => {
|
|
98
|
+
fireChangeEvent(newSelectedItems);
|
|
99
|
+
setSelectedItems(newSelectedItems);
|
|
100
|
+
};
|
|
153
101
|
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
setOption(result);
|
|
102
|
+
const handleNewSelectedItem = (selectedItem: MutationFilterItem | null | undefined) => {
|
|
103
|
+
if (selectedItem) {
|
|
104
|
+
handleSelectedItemsChanged([...selectedItems, selectedItem]);
|
|
105
|
+
setInputValue('');
|
|
106
|
+
setItemCandidate(null);
|
|
107
|
+
setShowErrorIndicator(false);
|
|
161
108
|
}
|
|
162
109
|
};
|
|
163
110
|
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
111
|
+
const handleInputChange = (newInputValue: string | undefined) => {
|
|
112
|
+
setShowErrorIndicator(false);
|
|
113
|
+
if (newInputValue?.includes(',')) {
|
|
114
|
+
const values = newInputValue?.split(',').map((value) => {
|
|
115
|
+
return { value, parsedValue: parseAndValidateMutation(value.trim(), referenceGenome) };
|
|
116
|
+
});
|
|
117
|
+
const validEntries = values.map((value) => value.parsedValue).filter((value) => value !== null);
|
|
118
|
+
const invalidInput = values
|
|
119
|
+
.filter((value) => value.parsedValue === null)
|
|
120
|
+
.map((value) => value.value.trim())
|
|
121
|
+
.join(',');
|
|
122
|
+
|
|
123
|
+
const selectedItemCandidates = [...selectedItems, ...validEntries];
|
|
124
|
+
|
|
125
|
+
handleSelectedItemsChanged(extractUniqueValues(selectedItemCandidates));
|
|
126
|
+
setInputValue(invalidInput);
|
|
127
|
+
setItemCandidate(null);
|
|
128
|
+
} else {
|
|
129
|
+
setInputValue(newInputValue ?? '');
|
|
130
|
+
if (newInputValue !== undefined) {
|
|
131
|
+
const candidate = parseAndValidateMutation(newInputValue, referenceGenome);
|
|
132
|
+
const alreadyExists = selectedItems.find(
|
|
133
|
+
(selectedItem) => selectedItem.value.code === candidate?.value.code,
|
|
134
|
+
);
|
|
135
|
+
if (!alreadyExists) {
|
|
136
|
+
setItemCandidate(candidate);
|
|
180
137
|
}
|
|
181
|
-
} else {
|
|
182
|
-
invalidQueries.push(trimmedValue);
|
|
183
138
|
}
|
|
184
139
|
}
|
|
140
|
+
};
|
|
185
141
|
|
|
186
|
-
|
|
142
|
+
const shadowRoot = filterRef.current?.shadowRoot ?? undefined;
|
|
143
|
+
|
|
144
|
+
const environment =
|
|
145
|
+
shadowRoot !== undefined
|
|
146
|
+
? {
|
|
147
|
+
addEventListener: window.addEventListener.bind(window),
|
|
148
|
+
removeEventListener: window.removeEventListener.bind(window),
|
|
149
|
+
document: shadowRoot.ownerDocument,
|
|
150
|
+
Node: window.Node,
|
|
151
|
+
}
|
|
152
|
+
: undefined;
|
|
153
|
+
|
|
154
|
+
const { getDropdownProps, removeSelectedItem } = useMultipleSelection({
|
|
155
|
+
selectedItems,
|
|
156
|
+
onStateChange({ selectedItems: newSelectedItems, type }) {
|
|
157
|
+
switch (type) {
|
|
158
|
+
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
|
|
159
|
+
handleSelectedItemsChanged(newSelectedItems ?? []);
|
|
160
|
+
break;
|
|
161
|
+
default:
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
environment,
|
|
166
|
+
});
|
|
187
167
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
168
|
+
const { isOpen, getMenuProps, getInputProps, highlightedIndex, getItemProps, selectedItem } = useCombobox({
|
|
169
|
+
items,
|
|
170
|
+
itemToString(item: MutationFilterItem | undefined | null) {
|
|
171
|
+
return item ? item.value.code : '';
|
|
172
|
+
},
|
|
173
|
+
defaultHighlightedIndex: 0,
|
|
174
|
+
inputValue,
|
|
175
|
+
onStateChange({ inputValue: newInputValue, type, selectedItem: newSelectedItem }) {
|
|
176
|
+
switch (type) {
|
|
177
|
+
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
|
178
|
+
case useCombobox.stateChangeTypes.ItemClick:
|
|
179
|
+
case useCombobox.stateChangeTypes.InputBlur:
|
|
180
|
+
handleNewSelectedItem(newSelectedItem);
|
|
181
|
+
break;
|
|
182
|
+
|
|
183
|
+
case useCombobox.stateChangeTypes.InputChange: {
|
|
184
|
+
handleInputChange(newInputValue);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
default:
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
environment,
|
|
192
|
+
});
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
194
|
+
if (referenceGenome.nucleotideSequences.length === 0 && referenceGenome.genes.length === 0) {
|
|
195
|
+
throw new UserFacingError(
|
|
196
|
+
'No reference sequences available',
|
|
197
|
+
'This organism has neither nucleotide nor amino acid sequences configured in its reference genome. You cannot filter by mutations.',
|
|
198
|
+
);
|
|
199
|
+
}
|
|
198
200
|
|
|
199
|
-
|
|
201
|
+
return (
|
|
202
|
+
<div className='w-full' ref={filterRef}>
|
|
203
|
+
<div className={`flex gap-x-1 flex-wrap p-1 input h-fit w-full ${showErrorIndicator ? 'input-error' : ''}`}>
|
|
204
|
+
{selectedItems.map((selectedItemForRender, index) => {
|
|
205
|
+
return (
|
|
206
|
+
<div className='my-1' key={`selected-item-${index}`}>
|
|
207
|
+
<SelectedFilter
|
|
208
|
+
handleRemoveValue={() => {
|
|
209
|
+
removeSelectedItem(selectedItemForRender);
|
|
210
|
+
}}
|
|
211
|
+
mutationFilter={selectedItemForRender}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
})}
|
|
216
|
+
<div className='flex gap-0.5 grow p-1'>
|
|
217
|
+
<input
|
|
218
|
+
placeholder={getPlaceholder(referenceGenome)}
|
|
219
|
+
className='w-full focus:outline-none min-w-8'
|
|
220
|
+
{...getInputProps(getDropdownProps({ preventKeyAction: isOpen }))}
|
|
221
|
+
onBlur={() => {
|
|
222
|
+
setShowErrorIndicator(inputValue !== '');
|
|
223
|
+
}}
|
|
224
|
+
size={10}
|
|
225
|
+
/>
|
|
226
|
+
<MutationFilterInfo />
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
<ul
|
|
230
|
+
className={`absolute w-inherit bg-white mt-1 shadow-md max-h-80 overflow-scroll p-0 z-10 ${
|
|
231
|
+
!isOpen && 'hidden'
|
|
232
|
+
}`}
|
|
233
|
+
{...getMenuProps()}
|
|
234
|
+
>
|
|
235
|
+
{items.map((item, index) => (
|
|
236
|
+
<li
|
|
237
|
+
className={`${highlightedIndex === index && 'bg-blue-300'} ${selectedItem === item && 'font-bold'} py-2 px-3 shadow-sm flex flex-col cursor-pointer`}
|
|
238
|
+
key={`${item.value.code}${index}`}
|
|
239
|
+
{...getItemProps({ item, index })}
|
|
240
|
+
style={{
|
|
241
|
+
backgroundColor: backgroundColorMap(item, highlightedIndex === index ? 0.4 : 0.2),
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
<span>{item.value.code}</span>
|
|
245
|
+
</li>
|
|
246
|
+
))}
|
|
247
|
+
</ul>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
200
251
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
setSelectedFilters(newSelectedValues);
|
|
252
|
+
function extractUniqueValues(newSelectedItems: MutationFilterItem[]) {
|
|
253
|
+
const uniqueMutationsMap = new Map<string, MutationFilterItem>();
|
|
254
|
+
for (const mutation of newSelectedItems) {
|
|
255
|
+
if (!uniqueMutationsMap.has(mutation.value.code)) {
|
|
256
|
+
uniqueMutationsMap.set(mutation.value.code, mutation);
|
|
207
257
|
}
|
|
258
|
+
}
|
|
208
259
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
};
|
|
260
|
+
return Array.from(uniqueMutationsMap.values());
|
|
261
|
+
}
|
|
212
262
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
};
|
|
263
|
+
function getInitialState(initialValue: MutationsFilter | string[] | undefined, referenceGenome: ReferenceGenome) {
|
|
264
|
+
if (initialValue === undefined) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
218
267
|
|
|
219
|
-
const
|
|
220
|
-
if (!selectorRef.current?.contains(event.relatedTarget as Node)) {
|
|
221
|
-
setOption(null);
|
|
222
|
-
}
|
|
223
|
-
};
|
|
268
|
+
const values = Array.isArray(initialValue) ? initialValue : Object.values(initialValue).flatMap((it) => it);
|
|
224
269
|
|
|
225
|
-
return
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
className='w-full p-2 border-gray-300 border rounded-md'
|
|
230
|
-
placeholder={getPlaceholder(referenceGenome)}
|
|
231
|
-
value={inputValue}
|
|
232
|
-
onInput={(e: Event) => {
|
|
233
|
-
handleInputChange((e.target as HTMLInputElement).value);
|
|
234
|
-
}}
|
|
235
|
-
onKeyDown={(e) => handleEnterPress(e)}
|
|
236
|
-
onFocus={() => handleInputChange(inputValue)}
|
|
237
|
-
onBlur={handleBlur}
|
|
238
|
-
/>
|
|
239
|
-
{option != null && (
|
|
240
|
-
<div
|
|
241
|
-
role='option'
|
|
242
|
-
className='hover:bg-gray-300 absolute cursor-pointer p-2 border-1 border-slate-500 bg-slate-200'
|
|
243
|
-
onClick={() => handleOptionClick()}
|
|
244
|
-
>
|
|
245
|
-
{option.value.toString()}
|
|
246
|
-
</div>
|
|
247
|
-
)}
|
|
248
|
-
</div>
|
|
249
|
-
);
|
|
250
|
-
};
|
|
270
|
+
return values
|
|
271
|
+
.map((value) => parseAndValidateMutation(value, referenceGenome))
|
|
272
|
+
.filter((parsedMutation) => parsedMutation !== null);
|
|
273
|
+
}
|
|
251
274
|
|
|
252
275
|
function getPlaceholder(referenceGenome: ReferenceGenome) {
|
|
253
276
|
const nucleotideSubstitution = getExampleMutation(referenceGenome, 'nucleotide', 'substitution');
|
|
@@ -262,82 +285,64 @@ function getPlaceholder(referenceGenome: ReferenceGenome) {
|
|
|
262
285
|
return `Enter a mutation (e.g. ${exampleMutations})`;
|
|
263
286
|
}
|
|
264
287
|
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const SelectedMutationFilterDisplay: FunctionComponent<{
|
|
277
|
-
selectedFilters: SelectedFilters;
|
|
278
|
-
handleRemoveValue: (option: ParsedMutationFilter) => void;
|
|
279
|
-
}> = ({ selectedFilters, handleRemoveValue }) => {
|
|
280
|
-
return (
|
|
281
|
-
<div className='flex flex-wrap'>
|
|
282
|
-
{selectedFilters.nucleotideMutations.map((mutation) => (
|
|
283
|
-
<SelectedFilter
|
|
284
|
-
key={mutation.toString()}
|
|
285
|
-
handleRemoveValue={handleRemoveValue}
|
|
286
|
-
mutationFilter={{ type: 'nucleotideMutations', value: mutation }}
|
|
287
|
-
/>
|
|
288
|
-
))}
|
|
289
|
-
{selectedFilters.aminoAcidMutations.map((mutation) => (
|
|
290
|
-
<SelectedFilter
|
|
291
|
-
key={mutation.toString()}
|
|
292
|
-
handleRemoveValue={handleRemoveValue}
|
|
293
|
-
mutationFilter={{ type: 'aminoAcidMutations', value: mutation }}
|
|
294
|
-
/>
|
|
295
|
-
))}
|
|
296
|
-
{selectedFilters.nucleotideInsertions.map((mutation) => (
|
|
297
|
-
<SelectedFilter
|
|
298
|
-
key={mutation.toString()}
|
|
299
|
-
handleRemoveValue={handleRemoveValue}
|
|
300
|
-
mutationFilter={{ type: 'nucleotideInsertions', value: mutation }}
|
|
301
|
-
/>
|
|
302
|
-
))}
|
|
303
|
-
{selectedFilters.aminoAcidInsertions.map((mutation) => (
|
|
304
|
-
<SelectedFilter
|
|
305
|
-
key={mutation.toString()}
|
|
306
|
-
handleRemoveValue={handleRemoveValue}
|
|
307
|
-
mutationFilter={{ type: 'aminoAcidInsertions', value: mutation }}
|
|
308
|
-
/>
|
|
309
|
-
))}
|
|
310
|
-
</div>
|
|
311
|
-
);
|
|
288
|
+
const backgroundColorMap = (data: MutationFilterItem, alpha: number = 0.4) => {
|
|
289
|
+
switch (data.type) {
|
|
290
|
+
case 'nucleotideMutations':
|
|
291
|
+
return singleGraphColorRGBByName('green', alpha);
|
|
292
|
+
case 'aminoAcidMutations':
|
|
293
|
+
return singleGraphColorRGBByName('teal', alpha);
|
|
294
|
+
case 'nucleotideInsertions':
|
|
295
|
+
return singleGraphColorRGBByName('indigo', alpha);
|
|
296
|
+
case 'aminoAcidInsertions':
|
|
297
|
+
return singleGraphColorRGBByName('purple', alpha);
|
|
298
|
+
}
|
|
312
299
|
};
|
|
313
300
|
|
|
314
301
|
type SelectedFilterProps = {
|
|
315
|
-
handleRemoveValue: (mutation:
|
|
316
|
-
mutationFilter:
|
|
302
|
+
handleRemoveValue: (mutation: MutationFilterItem) => void;
|
|
303
|
+
mutationFilter: MutationFilterItem;
|
|
317
304
|
};
|
|
318
305
|
|
|
319
306
|
const SelectedFilter = ({ handleRemoveValue, mutationFilter }: SelectedFilterProps) => {
|
|
320
307
|
return (
|
|
321
308
|
<span
|
|
322
309
|
key={mutationFilter.value.toString()}
|
|
323
|
-
className='center
|
|
310
|
+
className='center px-2 py-1 inline-flex text-black rounded-md'
|
|
324
311
|
style={{
|
|
325
312
|
backgroundColor: backgroundColorMap(mutationFilter),
|
|
326
313
|
}}
|
|
327
314
|
>
|
|
328
315
|
{mutationFilter.value.toString()}
|
|
329
|
-
<button
|
|
316
|
+
<button
|
|
317
|
+
className='ml-1 cursor-pointer'
|
|
318
|
+
aria-label={`remove mutation filter ${mutationFilter.value.code}`}
|
|
319
|
+
onClick={() => handleRemoveValue(mutationFilter)}
|
|
320
|
+
>
|
|
330
321
|
×
|
|
331
322
|
</button>
|
|
332
323
|
</span>
|
|
333
324
|
);
|
|
334
325
|
};
|
|
335
326
|
|
|
336
|
-
function mapToMutationFilterStrings(selectedFilters:
|
|
337
|
-
return
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
327
|
+
function mapToMutationFilterStrings(selectedFilters: MutationFilterItem[]) {
|
|
328
|
+
return selectedFilters.reduce<MutationsFilter>(
|
|
329
|
+
(acc, filter) => {
|
|
330
|
+
switch (filter.type) {
|
|
331
|
+
case 'nucleotideMutations':
|
|
332
|
+
return { ...acc, nucleotideMutations: [...acc.nucleotideMutations, filter.value.toString()] };
|
|
333
|
+
case 'aminoAcidMutations':
|
|
334
|
+
return { ...acc, aminoAcidMutations: [...acc.aminoAcidMutations, filter.value.toString()] };
|
|
335
|
+
case 'nucleotideInsertions':
|
|
336
|
+
return { ...acc, nucleotideInsertions: [...acc.nucleotideInsertions, filter.value.toString()] };
|
|
337
|
+
case 'aminoAcidInsertions':
|
|
338
|
+
return { ...acc, aminoAcidInsertions: [...acc.aminoAcidInsertions, filter.value.toString()] };
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
aminoAcidMutations: [],
|
|
343
|
+
nucleotideMutations: [],
|
|
344
|
+
aminoAcidInsertions: [],
|
|
345
|
+
nucleotideInsertions: [],
|
|
346
|
+
},
|
|
347
|
+
);
|
|
343
348
|
}
|
|
@@ -1,22 +1,44 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type MutationFilterItem } from './mutation-filter';
|
|
2
2
|
import { sequenceTypeFromSegment } from './sequenceTypeFromSegment';
|
|
3
3
|
import type { ReferenceGenome } from '../../lapisApi/ReferenceGenome';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
export type ParsedMutationFilter = {
|
|
7
|
-
[MutationType in keyof SelectedFilters]: { type: MutationType; value: SelectedFilters[MutationType][number] };
|
|
8
|
-
}[keyof SelectedFilters];
|
|
4
|
+
import type { SequenceType } from '../../types';
|
|
5
|
+
import { DeletionClass, InsertionClass, type Mutation, SubstitutionClass } from '../../utils/mutations';
|
|
9
6
|
|
|
10
7
|
export const parseAndValidateMutation = (
|
|
11
8
|
value: string,
|
|
12
9
|
referenceGenome: ReferenceGenome,
|
|
13
|
-
):
|
|
10
|
+
): MutationFilterItem | null => {
|
|
11
|
+
const possibleMutation = parseMutation(value, referenceGenome);
|
|
12
|
+
if (possibleMutation === null) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const sequenceType = getSequenceType(possibleMutation.type);
|
|
16
|
+
const isOutside = isOutsideReferenceGenome(possibleMutation.value, referenceGenome, sequenceType);
|
|
17
|
+
if (isOutside) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return possibleMutation;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getSequenceType = (type: MutationFilterItem['type']) => {
|
|
24
|
+
switch (type) {
|
|
25
|
+
case 'nucleotideInsertions':
|
|
26
|
+
case 'nucleotideMutations':
|
|
27
|
+
return 'nucleotide';
|
|
28
|
+
case 'aminoAcidInsertions':
|
|
29
|
+
case 'aminoAcidMutations':
|
|
30
|
+
return 'amino acid';
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const parseMutation = (value: string, referenceGenome: ReferenceGenome): MutationFilterItem | null => {
|
|
14
35
|
const possibleInsertion = InsertionClass.parse(value);
|
|
15
36
|
if (possibleInsertion !== null) {
|
|
16
37
|
const sequenceType = sequenceTypeFromSegment(possibleInsertion.segment, referenceGenome);
|
|
17
38
|
switch (sequenceType) {
|
|
18
|
-
case 'nucleotide':
|
|
39
|
+
case 'nucleotide': {
|
|
19
40
|
return { type: 'nucleotideInsertions', value: possibleInsertion };
|
|
41
|
+
}
|
|
20
42
|
case 'amino acid':
|
|
21
43
|
return { type: 'aminoAcidInsertions', value: possibleInsertion };
|
|
22
44
|
case undefined:
|
|
@@ -41,10 +63,13 @@ export const parseAndValidateMutation = (
|
|
|
41
63
|
if (possibleSubstitution !== null) {
|
|
42
64
|
const sequenceType = sequenceTypeFromSegment(possibleSubstitution.segment, referenceGenome);
|
|
43
65
|
switch (sequenceType) {
|
|
44
|
-
case 'nucleotide':
|
|
66
|
+
case 'nucleotide': {
|
|
45
67
|
return { type: 'nucleotideMutations', value: possibleSubstitution };
|
|
46
|
-
|
|
68
|
+
}
|
|
69
|
+
case 'amino acid': {
|
|
47
70
|
return { type: 'aminoAcidMutations', value: possibleSubstitution };
|
|
71
|
+
}
|
|
72
|
+
|
|
48
73
|
case undefined:
|
|
49
74
|
return null;
|
|
50
75
|
}
|
|
@@ -52,3 +77,30 @@ export const parseAndValidateMutation = (
|
|
|
52
77
|
|
|
53
78
|
return null;
|
|
54
79
|
};
|
|
80
|
+
|
|
81
|
+
function isOutsideReferenceGenome(mutation: Mutation, referenceGenome: ReferenceGenome, sequenceType: SequenceType) {
|
|
82
|
+
const lengthOfSegment = getLengthOfSegment(mutation.segment, referenceGenome, sequenceType);
|
|
83
|
+
if (lengthOfSegment === undefined) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return mutation.position >= lengthOfSegment;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getLengthOfSegment(segment: string | undefined, referenceGenome: ReferenceGenome, sequenceType: SequenceType) {
|
|
91
|
+
switch (sequenceType) {
|
|
92
|
+
case 'nucleotide': {
|
|
93
|
+
if (referenceGenome.nucleotideSequences.length === 1) {
|
|
94
|
+
return referenceGenome.nucleotideSequences.at(0)?.sequence.length;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return referenceGenome.nucleotideSequences.find(
|
|
98
|
+
(sequence) => sequence.name.toUpperCase() === segment?.toUpperCase(),
|
|
99
|
+
)?.sequence.length;
|
|
100
|
+
}
|
|
101
|
+
case 'amino acid': {
|
|
102
|
+
return referenceGenome.genes.find((gene) => gene.name.toUpperCase() === segment?.toUpperCase())?.sequence
|
|
103
|
+
.length;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|