@genspectrum/dashboard-components 0.1.1
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/LICENSE +661 -0
- package/README.md +109 -0
- package/custom-elements.json +1587 -0
- package/dist/dashboard-components.js +7322 -0
- package/dist/dashboard-components.js.map +1 -0
- package/dist/genspectrum-components.d.ts +298 -0
- package/dist/style.css +2930 -0
- package/package.json +109 -0
- package/src/constants.ts +6 -0
- package/src/index.ts +1 -0
- package/src/lapisApi/ReferenceGenome.ts +30 -0
- package/src/lapisApi/__mockData__/referenceGenome.json +58 -0
- package/src/lapisApi/lapisApi.ts +99 -0
- package/src/lapisApi/lapisTypes.ts +51 -0
- package/src/operator/Dataset.ts +3 -0
- package/src/operator/DivisionOperator.spec.ts +27 -0
- package/src/operator/DivisionOperator.ts +60 -0
- package/src/operator/FetchAggregatedOperator.ts +44 -0
- package/src/operator/FetchInsertionsOperator.ts +24 -0
- package/src/operator/FetchSubstitutionsOrDeletionsOperator.ts +49 -0
- package/src/operator/FillMissingOperator.spec.ts +26 -0
- package/src/operator/FillMissingOperator.ts +30 -0
- package/src/operator/GroupByAndSumOperator.spec.ts +26 -0
- package/src/operator/GroupByAndSumOperator.ts +26 -0
- package/src/operator/GroupByOperator.spec.ts +43 -0
- package/src/operator/GroupByOperator.ts +32 -0
- package/src/operator/MapOperator.spec.ts +13 -0
- package/src/operator/MapOperator.ts +16 -0
- package/src/operator/MockOperator.spec.ts +11 -0
- package/src/operator/MockOperator.ts +12 -0
- package/src/operator/Operator.ts +5 -0
- package/src/operator/SlidingOperator.spec.ts +52 -0
- package/src/operator/SlidingOperator.ts +23 -0
- package/src/operator/SortOperator.spec.ts +13 -0
- package/src/operator/SortOperator.ts +16 -0
- package/src/preact/LapisUrlContext.ts +3 -0
- package/src/preact/ReferenceGenomeContext.ts +5 -0
- package/src/preact/components/SegmentSelector.tsx +62 -0
- package/src/preact/components/chart.stories.tsx +42 -0
- package/src/preact/components/chart.tsx +32 -0
- package/src/preact/components/checkbox-selector.stories.tsx +56 -0
- package/src/preact/components/checkbox-selector.tsx +46 -0
- package/src/preact/components/confidence-interval-selector.tsx +45 -0
- package/src/preact/components/csv-download-button.stories.tsx +25 -0
- package/src/preact/components/csv-download-button.tsx +51 -0
- package/src/preact/components/error-display.stories.tsx +22 -0
- package/src/preact/components/error-display.tsx +5 -0
- package/src/preact/components/headline.stories.tsx +29 -0
- package/src/preact/components/headline.tsx +16 -0
- package/src/preact/components/info.stories.tsx +22 -0
- package/src/preact/components/info.tsx +16 -0
- package/src/preact/components/loading-display.stories.tsx +20 -0
- package/src/preact/components/loading-display.tsx +5 -0
- package/src/preact/components/min-max-percent-slider.css +40 -0
- package/src/preact/components/min-max-range-slider.tsx +95 -0
- package/src/preact/components/mutation-type-selector.tsx +30 -0
- package/src/preact/components/no-data-display.stories.tsx +20 -0
- package/src/preact/components/no-data-display.tsx +5 -0
- package/src/preact/components/percent-intput.tsx +49 -0
- package/src/preact/components/proportion-selector-dropdown.stories.tsx +66 -0
- package/src/preact/components/proportion-selector-dropdown.tsx +33 -0
- package/src/preact/components/proportion-selector.stories.tsx +81 -0
- package/src/preact/components/proportion-selector.tsx +43 -0
- package/src/preact/components/scaling-selector.stories.tsx +25 -0
- package/src/preact/components/scaling-selector.tsx +36 -0
- package/src/preact/components/select.stories.tsx +42 -0
- package/src/preact/components/select.tsx +21 -0
- package/src/preact/components/table.stories.tsx +24 -0
- package/src/preact/components/table.tsx +51 -0
- package/src/preact/components/tabs.stories.tsx +60 -0
- package/src/preact/components/tabs.tsx +49 -0
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +32 -0
- package/src/preact/dateRangeSelector/date-range-selector.tsx +228 -0
- package/src/preact/dateRangeSelector/dateConversion.ts +8 -0
- package/src/preact/locationFilter/__mockData__/aggregated.json +775 -0
- package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +36 -0
- package/src/preact/locationFilter/fetchAutocompletionList.ts +43 -0
- package/src/preact/locationFilter/location-filter.stories.tsx +50 -0
- package/src/preact/locationFilter/location-filter.tsx +112 -0
- package/src/preact/mutationComparison/__mockData__/nucleotideMutationsOtherVariant.json +295 -0
- package/src/preact/mutationComparison/__mockData__/nucleotideMutationsSomeVariant.json +304 -0
- package/src/preact/mutationComparison/fetchMutationData.spec.ts +118 -0
- package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +125 -0
- package/src/preact/mutationComparison/getMutationComparisonTableData.ts +40 -0
- package/src/preact/mutationComparison/mutation-comparison-table.tsx +43 -0
- package/src/preact/mutationComparison/mutation-comparison-venn.tsx +122 -0
- package/src/preact/mutationComparison/mutation-comparison.stories.tsx +152 -0
- package/src/preact/mutationComparison/mutation-comparison.tsx +179 -0
- package/src/preact/mutationComparison/queryMutationData.ts +53 -0
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +164 -0
- package/src/preact/mutationFilter/mutation-filter.tsx +268 -0
- package/src/preact/mutationFilter/parseAndValidateMutation.ts +54 -0
- package/src/preact/mutationFilter/parseMutation.spec.ts +150 -0
- package/src/preact/mutationFilter/sequenceTypeFromSegment.spec.ts +66 -0
- package/src/preact/mutationFilter/sequenceTypeFromSegment.ts +20 -0
- package/src/preact/mutations/__mockData__/nucleotideInsertions.json +252 -0
- package/src/preact/mutations/__mockData__/nucleotideMutations.json +880 -0
- package/src/preact/mutations/getInsertionsTableData.spec.ts +36 -0
- package/src/preact/mutations/getInsertionsTableData.ts +10 -0
- package/src/preact/mutations/getMutationsGridData.spec.ts +135 -0
- package/src/preact/mutations/getMutationsGridData.ts +92 -0
- package/src/preact/mutations/getMutationsTableData.spec.ts +94 -0
- package/src/preact/mutations/getMutationsTableData.ts +17 -0
- package/src/preact/mutations/mutations-grid.tsx +84 -0
- package/src/preact/mutations/mutations-insertions-table.tsx +33 -0
- package/src/preact/mutations/mutations-table.tsx +47 -0
- package/src/preact/mutations/mutations.stories.tsx +95 -0
- package/src/preact/mutations/mutations.tsx +192 -0
- package/src/preact/mutations/queryMutations.ts +55 -0
- package/src/preact/prevalenceOverTime/__mockData__/denominator.json +1700 -0
- package/src/preact/prevalenceOverTime/__mockData__/denominatorOneVariant.json +608 -0
- package/src/preact/prevalenceOverTime/__mockData__/numeratorEG.json +1560 -0
- package/src/preact/prevalenceOverTime/__mockData__/numeratorJN1.json +592 -0
- package/src/preact/prevalenceOverTime/__mockData__/numeratorOneVariant.json +604 -0
- package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.spec.ts +67 -0
- package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.ts +18 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +105 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +86 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +141 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-table.tsx +46 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +165 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +202 -0
- package/src/preact/relativeGrowthAdvantage/__mockData__/denominator.json +376 -0
- package/src/preact/relativeGrowthAdvantage/__mockData__/numerator.json +332 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +138 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +71 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +136 -0
- package/src/preact/shared/charts/LogitScale.ts +48 -0
- package/src/preact/shared/charts/colors.ts +26 -0
- package/src/preact/shared/charts/confideceInterval.ts +29 -0
- package/src/preact/shared/charts/getYAxisScale.ts +16 -0
- package/src/preact/shared/charts/scales.ts +16 -0
- package/src/preact/shared/icons/DeleteIcon.tsx +17 -0
- package/src/preact/shared/sort/sortInsertions.spec.ts +47 -0
- package/src/preact/shared/sort/sortInsertions.ts +21 -0
- package/src/preact/shared/sort/sortMutationPositions.spec.ts +31 -0
- package/src/preact/shared/sort/sortMutationPositions.ts +14 -0
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +47 -0
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +17 -0
- package/src/preact/shared/table/formatProportion.ts +3 -0
- package/src/preact/textInput/__mockData__/aggregated_hosts.json +24 -0
- package/src/preact/textInput/fetchAutocompleteList.ts +9 -0
- package/src/preact/textInput/text-input.stories.tsx +49 -0
- package/src/preact/textInput/text-input.tsx +73 -0
- package/src/preact/useQuery.ts +27 -0
- package/src/query/queryInsertions.ts +14 -0
- package/src/query/queryPrevalenceOverTime.ts +126 -0
- package/src/query/queryRelativeGrowthAdvantage.ts +131 -0
- package/src/query/querySubstitutionsOrDeletions.ts +19 -0
- package/src/styles/tailwind.css +3 -0
- package/src/styles/tailwind.d.ts +3 -0
- package/src/types.ts +23 -0
- package/src/utils/mutations.spec.ts +64 -0
- package/src/utils/mutations.ts +165 -0
- package/src/utils/temporal.spec.ts +97 -0
- package/src/utils/temporal.ts +348 -0
- package/src/utils/test-utils.ts +5 -0
- package/src/utils/type-utils.ts +15 -0
- package/src/utils/utils.spec.ts +16 -0
- package/src/utils/utils.ts +38 -0
- package/src/web-components/PreactLitAdapter.tsx +62 -0
- package/src/web-components/PreactLitAdapterWithGridJsStyles.tsx +12 -0
- package/src/web-components/app.ts +51 -0
- package/src/web-components/display/index.ts +4 -0
- package/src/web-components/display/mutation-comparison-component.stories.ts +138 -0
- package/src/web-components/display/mutation-comparison-component.tsx +31 -0
- package/src/web-components/display/mutations-component.stories.ts +107 -0
- package/src/web-components/display/mutations-component.tsx +27 -0
- package/src/web-components/display/prevalence-over-time-component.stories.ts +205 -0
- package/src/web-components/display/prevalence-over-time-component.tsx +46 -0
- package/src/web-components/display/relative-growth-advantage-component.stories.ts +89 -0
- package/src/web-components/display/relative-growth-advantage-component.tsx +37 -0
- package/src/web-components/index.ts +3 -0
- package/src/web-components/input/date-range-selector-component.stories.ts +53 -0
- package/src/web-components/input/date-range-selector-component.tsx +33 -0
- package/src/web-components/input/index.ts +4 -0
- package/src/web-components/input/location-filter-component.stories.ts +184 -0
- package/src/web-components/input/location-filter-component.tsx +68 -0
- package/src/web-components/input/location-filter.mdx +25 -0
- package/src/web-components/input/mutation-filter-component.stories.ts +97 -0
- package/src/web-components/input/mutation-filter-component.tsx +27 -0
- package/src/web-components/input/text-input-component.stories.ts +92 -0
- package/src/web-components/input/text-input-component.tsx +30 -0
- package/src/web-components/lapis-context.ts +3 -0
- package/src/web-components/reference-genome-context.ts +5 -0
- package/src/web-components/withinShadowRoot.story.ts +34 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { useContext, useRef, useState } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { parseAndValidateMutation } from './parseAndValidateMutation';
|
|
5
|
+
import { type Deletion, type Insertion, type Mutation, type Substitution } from '../../utils/mutations';
|
|
6
|
+
import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
7
|
+
import { singleGraphColorRGBByName } from '../shared/charts/colors';
|
|
8
|
+
import { DeleteIcon } from '../shared/icons/DeleteIcon';
|
|
9
|
+
|
|
10
|
+
export type MutationFilterProps = {};
|
|
11
|
+
|
|
12
|
+
export type SelectedFilters = {
|
|
13
|
+
nucleotideMutations: (Substitution | Deletion)[];
|
|
14
|
+
aminoAcidMutations: (Substitution | Deletion)[];
|
|
15
|
+
nucleotideInsertions: Insertion[];
|
|
16
|
+
aminoAcidInsertions: Insertion[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type SelectedMutationFilterStrings = {
|
|
20
|
+
[Key in keyof SelectedFilters]: string[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const MutationFilter: FunctionComponent<MutationFilterProps> = () => {
|
|
24
|
+
const referenceGenome = useContext(ReferenceGenomeContext);
|
|
25
|
+
const [selectedFilters, setSelectedFilters] = useState<SelectedFilters>({
|
|
26
|
+
nucleotideMutations: [],
|
|
27
|
+
aminoAcidMutations: [],
|
|
28
|
+
nucleotideInsertions: [],
|
|
29
|
+
aminoAcidInsertions: [],
|
|
30
|
+
});
|
|
31
|
+
const [inputValue, setInputValue] = useState('');
|
|
32
|
+
const [isError, setIsError] = useState(false);
|
|
33
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
34
|
+
|
|
35
|
+
const handleSubmit = (event: Event) => {
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
|
|
38
|
+
const parsedMutation = parseAndValidateMutation(inputValue, referenceGenome);
|
|
39
|
+
|
|
40
|
+
if (parsedMutation === null) {
|
|
41
|
+
setIsError(true);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const newSelectedValues = {
|
|
46
|
+
...selectedFilters,
|
|
47
|
+
[parsedMutation.type]: [...selectedFilters[parsedMutation.type], parsedMutation.value],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
setSelectedFilters(newSelectedValues);
|
|
51
|
+
fireChangeEvent(newSelectedValues);
|
|
52
|
+
setInputValue('');
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const fireChangeEvent = (selectedFilters: SelectedFilters) => {
|
|
56
|
+
const detail = mapToMutationFilterStrings(selectedFilters);
|
|
57
|
+
|
|
58
|
+
formRef.current?.dispatchEvent(
|
|
59
|
+
new CustomEvent<SelectedMutationFilterStrings>('gs-mutation-filter-changed', {
|
|
60
|
+
detail,
|
|
61
|
+
bubbles: true,
|
|
62
|
+
composed: true,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleOnBlur = () => {
|
|
68
|
+
const detail = mapToMutationFilterStrings(selectedFilters);
|
|
69
|
+
|
|
70
|
+
formRef.current?.dispatchEvent(
|
|
71
|
+
new CustomEvent<SelectedMutationFilterStrings>('gs-mutation-filter-on-blur', {
|
|
72
|
+
detail,
|
|
73
|
+
bubbles: true,
|
|
74
|
+
composed: true,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleInputChange = (event: Event) => {
|
|
80
|
+
setInputValue((event.target as HTMLInputElement).value);
|
|
81
|
+
setIsError(false);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div class={`rounded-lg border border-gray-300 bg-white p-2`}>
|
|
86
|
+
<SelectedMutationDisplay
|
|
87
|
+
selectedFilters={selectedFilters}
|
|
88
|
+
setSelectedFilters={setSelectedFilters}
|
|
89
|
+
fireChangeEvent={fireChangeEvent}
|
|
90
|
+
/>
|
|
91
|
+
|
|
92
|
+
<form className='mt-2 w-full' onSubmit={handleSubmit} ref={formRef}>
|
|
93
|
+
<label className={`input flex items-center gap-2 ${isError ? 'input-error' : 'input-bordered'}`}>
|
|
94
|
+
<input
|
|
95
|
+
className='grow min-w-0'
|
|
96
|
+
type='text'
|
|
97
|
+
value={inputValue}
|
|
98
|
+
onInput={handleInputChange}
|
|
99
|
+
placeholder={'Enter a mutation'}
|
|
100
|
+
onBlur={handleOnBlur}
|
|
101
|
+
/>
|
|
102
|
+
<button className='btn btn-sm'>+</button>
|
|
103
|
+
</label>
|
|
104
|
+
</form>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const SelectedMutationDisplay: FunctionComponent<{
|
|
110
|
+
selectedFilters: SelectedFilters;
|
|
111
|
+
setSelectedFilters: (selectedFilters: SelectedFilters) => void;
|
|
112
|
+
fireChangeEvent: (selectedFilters: SelectedFilters) => void;
|
|
113
|
+
}> = ({ selectedFilters, setSelectedFilters, fireChangeEvent }) => {
|
|
114
|
+
const onSelectedRemoved = <MutationType extends keyof SelectedFilters>(
|
|
115
|
+
mutation: SelectedFilters[MutationType][number],
|
|
116
|
+
key: MutationType,
|
|
117
|
+
) => {
|
|
118
|
+
const newSelectedValues = {
|
|
119
|
+
...selectedFilters,
|
|
120
|
+
[key]: selectedFilters[key].filter((i) => !mutation.equals(i)),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
setSelectedFilters(newSelectedValues);
|
|
124
|
+
|
|
125
|
+
fireChangeEvent(newSelectedValues);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<ul class='flex flex-wrap'>
|
|
130
|
+
{selectedFilters.nucleotideMutations.map((mutation) => (
|
|
131
|
+
<li key={mutation.toString()}>
|
|
132
|
+
<SelectedNucleotideMutation
|
|
133
|
+
mutation={mutation}
|
|
134
|
+
onDelete={(mutation: Substitution | Deletion) =>
|
|
135
|
+
onSelectedRemoved(mutation, 'nucleotideMutations')
|
|
136
|
+
}
|
|
137
|
+
/>
|
|
138
|
+
</li>
|
|
139
|
+
))}
|
|
140
|
+
{selectedFilters.aminoAcidMutations.map((mutation) => (
|
|
141
|
+
<li key={mutation.toString()}>
|
|
142
|
+
<SelectedAminoAcidMutation
|
|
143
|
+
mutation={mutation}
|
|
144
|
+
onDelete={(mutation: Substitution | Deletion) =>
|
|
145
|
+
onSelectedRemoved(mutation, 'aminoAcidMutations')
|
|
146
|
+
}
|
|
147
|
+
/>
|
|
148
|
+
</li>
|
|
149
|
+
))}
|
|
150
|
+
{selectedFilters.nucleotideInsertions.map((insertion) => (
|
|
151
|
+
<li key={insertion.toString()}>
|
|
152
|
+
<SelectedNucleotideInsertion
|
|
153
|
+
insertion={insertion}
|
|
154
|
+
onDelete={(insertion) => onSelectedRemoved(insertion, 'nucleotideInsertions')}
|
|
155
|
+
/>
|
|
156
|
+
</li>
|
|
157
|
+
))}
|
|
158
|
+
{selectedFilters.aminoAcidInsertions.map((insertion) => (
|
|
159
|
+
<li key={insertion.toString()}>
|
|
160
|
+
<SelectedAminoAcidInsertion
|
|
161
|
+
insertion={insertion}
|
|
162
|
+
onDelete={(insertion: Insertion) => onSelectedRemoved(insertion, 'aminoAcidInsertions')}
|
|
163
|
+
/>
|
|
164
|
+
</li>
|
|
165
|
+
))}
|
|
166
|
+
</ul>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const SelectedAminoAcidInsertion: FunctionComponent<{
|
|
171
|
+
insertion: Insertion;
|
|
172
|
+
onDelete: (insertion: Insertion) => void;
|
|
173
|
+
}> = ({ insertion, onDelete }) => {
|
|
174
|
+
const backgroundColor = singleGraphColorRGBByName('teal', 0.3);
|
|
175
|
+
const textColor = singleGraphColorRGBByName('teal', 1);
|
|
176
|
+
return (
|
|
177
|
+
<SelectedFilter
|
|
178
|
+
mutation={insertion}
|
|
179
|
+
onDelete={onDelete}
|
|
180
|
+
backgroundColor={backgroundColor}
|
|
181
|
+
textColor={textColor}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const SelectedAminoAcidMutation: FunctionComponent<{
|
|
187
|
+
mutation: Substitution | Deletion;
|
|
188
|
+
onDelete: (mutation: Substitution | Deletion) => void;
|
|
189
|
+
}> = ({ mutation, onDelete }) => {
|
|
190
|
+
const backgroundColor = singleGraphColorRGBByName('rose', 0.3);
|
|
191
|
+
const textColor = singleGraphColorRGBByName('rose', 1);
|
|
192
|
+
return (
|
|
193
|
+
<SelectedFilter
|
|
194
|
+
mutation={mutation}
|
|
195
|
+
onDelete={onDelete}
|
|
196
|
+
backgroundColor={backgroundColor}
|
|
197
|
+
textColor={textColor}
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const SelectedNucleotideMutation: FunctionComponent<{
|
|
203
|
+
mutation: Substitution | Deletion;
|
|
204
|
+
onDelete: (insertion: Substitution | Deletion) => void;
|
|
205
|
+
}> = ({ mutation, onDelete }) => {
|
|
206
|
+
const backgroundColor = singleGraphColorRGBByName('indigo', 0.3);
|
|
207
|
+
const textColor = singleGraphColorRGBByName('indigo', 1);
|
|
208
|
+
return (
|
|
209
|
+
<SelectedFilter
|
|
210
|
+
mutation={mutation}
|
|
211
|
+
onDelete={onDelete}
|
|
212
|
+
backgroundColor={backgroundColor}
|
|
213
|
+
textColor={textColor}
|
|
214
|
+
/>
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const SelectedNucleotideInsertion: FunctionComponent<{
|
|
219
|
+
insertion: Insertion;
|
|
220
|
+
onDelete: (insertion: Insertion) => void;
|
|
221
|
+
}> = ({ insertion, onDelete }) => {
|
|
222
|
+
const backgroundColor = singleGraphColorRGBByName('green', 0.3);
|
|
223
|
+
const textColor = singleGraphColorRGBByName('green', 1);
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<SelectedFilter
|
|
227
|
+
mutation={insertion}
|
|
228
|
+
onDelete={onDelete}
|
|
229
|
+
backgroundColor={backgroundColor}
|
|
230
|
+
textColor={textColor}
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
type SelectedFilterProps<MutationType extends Mutation> = {
|
|
236
|
+
mutation: MutationType;
|
|
237
|
+
onDelete: (mutation: MutationType) => void;
|
|
238
|
+
backgroundColor: string;
|
|
239
|
+
textColor: string;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const SelectedFilter = <MutationType extends Mutation>({
|
|
243
|
+
mutation,
|
|
244
|
+
onDelete,
|
|
245
|
+
backgroundColor,
|
|
246
|
+
textColor,
|
|
247
|
+
}: SelectedFilterProps<MutationType>) => {
|
|
248
|
+
return (
|
|
249
|
+
<div
|
|
250
|
+
class='flex items-center flex-nowrap gap-1 rounded me-1 px-2.5 py-0.5 font-medium text-xs mb-1'
|
|
251
|
+
style={{ backgroundColor, color: textColor }}
|
|
252
|
+
>
|
|
253
|
+
<div>{mutation.toString()}</div>
|
|
254
|
+
<button onClick={() => onDelete(mutation)}>
|
|
255
|
+
<DeleteIcon />
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
function mapToMutationFilterStrings(selectedFilters: SelectedFilters) {
|
|
262
|
+
return {
|
|
263
|
+
aminoAcidMutations: selectedFilters.aminoAcidMutations.map((mutation) => mutation.toString()),
|
|
264
|
+
nucleotideMutations: selectedFilters.nucleotideMutations.map((mutation) => mutation.toString()),
|
|
265
|
+
aminoAcidInsertions: selectedFilters.aminoAcidInsertions.map((insertion) => insertion.toString()),
|
|
266
|
+
nucleotideInsertions: selectedFilters.nucleotideInsertions.map((insertion) => insertion.toString()),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type SelectedFilters } from './mutation-filter';
|
|
2
|
+
import { sequenceTypeFromSegment } from './sequenceTypeFromSegment';
|
|
3
|
+
import type { ReferenceGenome } from '../../lapisApi/ReferenceGenome';
|
|
4
|
+
import { Deletion, Insertion, Substitution } from '../../utils/mutations';
|
|
5
|
+
|
|
6
|
+
type ParsedMutationFilter = {
|
|
7
|
+
[MutationType in keyof SelectedFilters]: { type: MutationType; value: SelectedFilters[MutationType][number] };
|
|
8
|
+
}[keyof SelectedFilters];
|
|
9
|
+
|
|
10
|
+
export const parseAndValidateMutation = (
|
|
11
|
+
value: string,
|
|
12
|
+
referenceGenome: ReferenceGenome,
|
|
13
|
+
): ParsedMutationFilter | null => {
|
|
14
|
+
const possibleInsertion = Insertion.parse(value);
|
|
15
|
+
if (possibleInsertion !== null) {
|
|
16
|
+
const sequenceType = sequenceTypeFromSegment(possibleInsertion.segment, referenceGenome);
|
|
17
|
+
switch (sequenceType) {
|
|
18
|
+
case 'nucleotide':
|
|
19
|
+
return { type: 'nucleotideInsertions', value: possibleInsertion };
|
|
20
|
+
case 'amino acid':
|
|
21
|
+
return { type: 'aminoAcidInsertions', value: possibleInsertion };
|
|
22
|
+
case undefined:
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const possibleDeletion = Deletion.parse(value);
|
|
28
|
+
if (possibleDeletion !== null) {
|
|
29
|
+
const sequenceType = sequenceTypeFromSegment(possibleDeletion.segment, referenceGenome);
|
|
30
|
+
switch (sequenceType) {
|
|
31
|
+
case 'nucleotide':
|
|
32
|
+
return { type: 'nucleotideMutations', value: possibleDeletion };
|
|
33
|
+
case 'amino acid':
|
|
34
|
+
return { type: 'aminoAcidMutations', value: possibleDeletion };
|
|
35
|
+
case undefined:
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const possibleSubstitution = Substitution.parse(value);
|
|
41
|
+
if (possibleSubstitution !== null) {
|
|
42
|
+
const sequenceType = sequenceTypeFromSegment(possibleSubstitution.segment, referenceGenome);
|
|
43
|
+
switch (sequenceType) {
|
|
44
|
+
case 'nucleotide':
|
|
45
|
+
return { type: 'nucleotideMutations', value: possibleSubstitution };
|
|
46
|
+
case 'amino acid':
|
|
47
|
+
return { type: 'aminoAcidMutations', value: possibleSubstitution };
|
|
48
|
+
case undefined:
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { parseAndValidateMutation } from './parseAndValidateMutation';
|
|
4
|
+
import { Deletion, Insertion, Substitution } from '../../utils/mutations';
|
|
5
|
+
|
|
6
|
+
describe('parseMutation', () => {
|
|
7
|
+
const singleSegmentedReferenceGenome = {
|
|
8
|
+
nucleotideSequences: [
|
|
9
|
+
{
|
|
10
|
+
name: 'nuc1',
|
|
11
|
+
sequence: 'ACGT',
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
genes: [
|
|
15
|
+
{
|
|
16
|
+
name: 'gene1',
|
|
17
|
+
sequence: 'ACGT',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'gene2',
|
|
21
|
+
sequence: 'ACGT',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const testCases = {
|
|
27
|
+
insertions: [
|
|
28
|
+
{
|
|
29
|
+
name: 'should parse nucleotide insertions',
|
|
30
|
+
input: 'ins_10:ACGT',
|
|
31
|
+
expected: { type: 'nucleotideInsertions', value: new Insertion(undefined, 10, 'ACGT') },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'should parse amino acid insertions',
|
|
35
|
+
input: 'ins_gene1:10:ACGT',
|
|
36
|
+
expected: { type: 'aminoAcidInsertions', value: new Insertion('gene1', 10, 'ACGT') },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'should parse amino acid insertion with LAPIS-style wildcard',
|
|
40
|
+
input: 'ins_gene1:10:?AC?GT',
|
|
41
|
+
expected: { type: 'aminoAcidInsertions', value: new Insertion('gene1', 10, '?AC?GT') },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'should parse amino acid insertion with SILO-style wildcard',
|
|
45
|
+
input: 'ins_gene1:10:.*AC.*GT',
|
|
46
|
+
expected: { type: 'aminoAcidInsertions', value: new Insertion('gene1', 10, '.*AC.*GT') },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'should return null for insertion with segment not in reference genome',
|
|
50
|
+
input: 'INS_notInReferenceGenome:10:ACGT',
|
|
51
|
+
expected: null,
|
|
52
|
+
},
|
|
53
|
+
{ name: 'should return null for insertion with missing position', input: 'ins_gene1:ACGT', expected: null },
|
|
54
|
+
],
|
|
55
|
+
deletions: [
|
|
56
|
+
{
|
|
57
|
+
name: 'should parse nucleotide deletion in single segmented reference genome, when no segment is given',
|
|
58
|
+
input: 'A123-',
|
|
59
|
+
expected: { type: 'nucleotideMutations', value: new Deletion(undefined, 'A', 123) },
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'should parse nucleotide deletion without valueAtReference when no segment is given',
|
|
63
|
+
input: '123-',
|
|
64
|
+
expected: { type: 'nucleotideMutations', value: new Deletion(undefined, undefined, 123) },
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'should parse nucleotide deletion',
|
|
68
|
+
input: 'nuc1:A123-',
|
|
69
|
+
expected: { type: 'nucleotideMutations', value: new Deletion('nuc1', 'A', 123) },
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'should parse nucleotide deletion without valueAtReference',
|
|
73
|
+
input: 'nuc1:123-',
|
|
74
|
+
expected: { type: 'nucleotideMutations', value: new Deletion('nuc1', undefined, 123) },
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'should parse amino acid deletion',
|
|
78
|
+
input: 'gene1:A123-',
|
|
79
|
+
expected: { type: 'aminoAcidMutations', value: new Deletion('gene1', 'A', 123) },
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'should parse amino acid deletion without valueAtReference',
|
|
83
|
+
input: 'gene1:123-',
|
|
84
|
+
expected: { type: 'aminoAcidMutations', value: new Deletion('gene1', undefined, 123) },
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'should return null for deletion with segment not in reference genome',
|
|
88
|
+
input: 'notInReferenceGenome:A123-',
|
|
89
|
+
expected: null,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
substitutions: [
|
|
93
|
+
{
|
|
94
|
+
name: 'should parse nucleotide substitution in single segmented reference genome, when no segment is given',
|
|
95
|
+
input: 'A123T',
|
|
96
|
+
expected: { type: 'nucleotideMutations', value: new Substitution(undefined, 'A', 'T', 123) },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'should parse substitution without valueAtReference',
|
|
100
|
+
input: '123T',
|
|
101
|
+
expected: { type: 'nucleotideMutations', value: new Substitution(undefined, undefined, 'T', 123) },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'should parse substitution with neither valueAtReference not substitutionValue',
|
|
105
|
+
input: '123',
|
|
106
|
+
expected: {
|
|
107
|
+
type: 'nucleotideMutations',
|
|
108
|
+
value: new Substitution(undefined, undefined, undefined, 123),
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'should parse a "no mutation" substitution',
|
|
113
|
+
input: '123.',
|
|
114
|
+
expected: { type: 'nucleotideMutations', value: new Substitution(undefined, undefined, '.', 123) },
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'should parse nucleotide substitution',
|
|
118
|
+
input: 'nuc1:A123T',
|
|
119
|
+
expected: { type: 'nucleotideMutations', value: new Substitution('nuc1', 'A', 'T', 123) },
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'should parse amino acid substitution',
|
|
123
|
+
input: 'gene1:A123T',
|
|
124
|
+
expected: { type: 'aminoAcidMutations', value: new Substitution('gene1', 'A', 'T', 123) },
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'should return null for substitution with segment not in reference genome',
|
|
128
|
+
input: 'notInReferenceGenome:A123T',
|
|
129
|
+
expected: null,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
Object.entries(testCases).forEach(([type, cases]) => {
|
|
135
|
+
describe(type, () => {
|
|
136
|
+
cases.forEach(({ name, input, expected }) => {
|
|
137
|
+
it(name, () => {
|
|
138
|
+
const result = parseAndValidateMutation(input, singleSegmentedReferenceGenome);
|
|
139
|
+
|
|
140
|
+
expect(result).deep.equals(expected);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should return null for invalid mutation', () => {
|
|
147
|
+
const result = parseAndValidateMutation('invalidMutation', singleSegmentedReferenceGenome);
|
|
148
|
+
expect(result).toBe(null);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { sequenceTypeFromSegment } from './sequenceTypeFromSegment';
|
|
4
|
+
|
|
5
|
+
describe('getSequenceType', () => {
|
|
6
|
+
const singleSegmentedReferenceGenome = {
|
|
7
|
+
nucleotideSequences: [
|
|
8
|
+
{
|
|
9
|
+
name: 'nuc1',
|
|
10
|
+
sequence: 'ACGT',
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
genes: [
|
|
14
|
+
{
|
|
15
|
+
name: 'gene1',
|
|
16
|
+
sequence: 'ACGT',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'gene2',
|
|
20
|
+
sequence: 'ACGT',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const multiSegmentedReferenceGenome = {
|
|
26
|
+
nucleotideSequences: [
|
|
27
|
+
{
|
|
28
|
+
name: 'nuc1',
|
|
29
|
+
sequence: 'ACGT',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'nuc2',
|
|
33
|
+
sequence: 'ACGT',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
genes: [
|
|
37
|
+
{
|
|
38
|
+
name: 'gene1',
|
|
39
|
+
sequence: 'ACGT',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'gene2',
|
|
43
|
+
sequence: 'ACGT',
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
it('should return nucleotide when the segment is undefined for singe segmented genome', () => {
|
|
49
|
+
expect(sequenceTypeFromSegment('nuc1', singleSegmentedReferenceGenome)).toBe('nucleotide');
|
|
50
|
+
expect(sequenceTypeFromSegment(undefined, singleSegmentedReferenceGenome)).toBe('nucleotide');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return undefined when the segment is undefined for multi segmented genome', () => {
|
|
54
|
+
expect(sequenceTypeFromSegment('nuc1', multiSegmentedReferenceGenome)).toBe('nucleotide');
|
|
55
|
+
expect(sequenceTypeFromSegment(undefined, multiSegmentedReferenceGenome)).toBe(undefined);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return amino acid when the segment is a gene', () => {
|
|
59
|
+
expect(sequenceTypeFromSegment('gene1', singleSegmentedReferenceGenome)).toBe('amino acid');
|
|
60
|
+
expect(sequenceTypeFromSegment('gene2', singleSegmentedReferenceGenome)).toBe('amino acid');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return undefined when the segment is not found in the reference genome', () => {
|
|
64
|
+
expect(sequenceTypeFromSegment('not-existing', singleSegmentedReferenceGenome)).toBe(undefined);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReferenceGenome } from '../../lapisApi/ReferenceGenome';
|
|
2
|
+
import type { SequenceType } from '../../types';
|
|
3
|
+
|
|
4
|
+
export const sequenceTypeFromSegment = (
|
|
5
|
+
possibleSegment: string | undefined,
|
|
6
|
+
referenceGenome: ReferenceGenome,
|
|
7
|
+
): SequenceType | undefined => {
|
|
8
|
+
if (possibleSegment === undefined) {
|
|
9
|
+
return referenceGenome.nucleotideSequences.length === 1 ? 'nucleotide' : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (referenceGenome.nucleotideSequences.some((sequence) => sequence.name === possibleSegment)) {
|
|
13
|
+
return 'nucleotide';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (referenceGenome.genes.some((gene) => gene.name === possibleSegment)) {
|
|
17
|
+
return 'amino acid';
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
};
|