@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,122 @@
|
|
|
1
|
+
import { type ActiveElement, Chart, type ChartConfiguration, type ChartEvent, registerables } from 'chart.js';
|
|
2
|
+
import { ArcSlice, extractSets, VennDiagramController } from 'chartjs-chart-venn';
|
|
3
|
+
import { type FunctionComponent } from 'preact';
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
5
|
+
|
|
6
|
+
import { type MutationData } from './queryMutationData';
|
|
7
|
+
import { type Dataset } from '../../operator/Dataset';
|
|
8
|
+
import GsChart from '../components/chart';
|
|
9
|
+
import { type ProportionInterval } from '../components/proportion-selector';
|
|
10
|
+
|
|
11
|
+
Chart.register(...registerables, VennDiagramController, ArcSlice);
|
|
12
|
+
|
|
13
|
+
export interface MutationComparisonVennProps {
|
|
14
|
+
data: Dataset<MutationData>;
|
|
15
|
+
proportionInterval: ProportionInterval;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const MutationComparisonVenn: FunctionComponent<MutationComparisonVennProps> = ({
|
|
19
|
+
data,
|
|
20
|
+
proportionInterval,
|
|
21
|
+
}) => {
|
|
22
|
+
const divRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
const noElementSelectedMessage = 'You have no elements selected. Click in the venn diagram to select.';
|
|
24
|
+
const [selectedDatasetIndex, setSelectedDatasetIndex] = useState<null | number>(null);
|
|
25
|
+
|
|
26
|
+
const sets = useMemo(
|
|
27
|
+
() =>
|
|
28
|
+
extractSets(
|
|
29
|
+
data.content
|
|
30
|
+
.map((mutationData) => ({
|
|
31
|
+
displayName: mutationData.displayName,
|
|
32
|
+
data: mutationData.data.filter(
|
|
33
|
+
(mutationEntry) =>
|
|
34
|
+
mutationEntry.proportion >= proportionInterval.min &&
|
|
35
|
+
mutationEntry.proportion <= proportionInterval.max,
|
|
36
|
+
),
|
|
37
|
+
}))
|
|
38
|
+
.map((mutationData) => {
|
|
39
|
+
return {
|
|
40
|
+
label: mutationData.displayName,
|
|
41
|
+
values: mutationData.data.map((mutationEntry) => mutationEntry.mutation.toString()),
|
|
42
|
+
};
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
[data, proportionInterval],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (divRef.current === null) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (selectedDatasetIndex === null) {
|
|
53
|
+
divRef.current.innerText = noElementSelectedMessage;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const values = sets.datasets[0].data[selectedDatasetIndex].values;
|
|
58
|
+
const label = sets.datasets[0].data[selectedDatasetIndex].label;
|
|
59
|
+
divRef.current!.innerText = `${label}: ${values.join(', ')}` || '';
|
|
60
|
+
}, [divRef, selectedDatasetIndex, sets]);
|
|
61
|
+
|
|
62
|
+
const config: ChartConfiguration = useMemo(
|
|
63
|
+
() => ({
|
|
64
|
+
type: 'venn',
|
|
65
|
+
data: sets,
|
|
66
|
+
options: {
|
|
67
|
+
scales: {
|
|
68
|
+
x: {
|
|
69
|
+
ticks: {
|
|
70
|
+
color: 'black',
|
|
71
|
+
font: {
|
|
72
|
+
size: 20,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
y: {
|
|
77
|
+
ticks: {
|
|
78
|
+
color: 'blue',
|
|
79
|
+
font: {
|
|
80
|
+
size: 20,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
events: ['click'],
|
|
86
|
+
onClick(_: ChartEvent, elements: ActiveElement[]) {
|
|
87
|
+
if (elements.length === 0) {
|
|
88
|
+
setSelectedDatasetIndex(null);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
backgroundColor: '#f5f5f5',
|
|
92
|
+
animation: false,
|
|
93
|
+
layout: {
|
|
94
|
+
padding: 30,
|
|
95
|
+
},
|
|
96
|
+
plugins: {
|
|
97
|
+
legend: {
|
|
98
|
+
display: false,
|
|
99
|
+
},
|
|
100
|
+
tooltip: {
|
|
101
|
+
filter: ({ dataIndex }) => {
|
|
102
|
+
setSelectedDatasetIndex(dataIndex);
|
|
103
|
+
return false;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
[sets],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (data.content.length > 5) {
|
|
113
|
+
return <div>Too many variants to display. Maximum are five. </div>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<>
|
|
118
|
+
<GsChart configuration={config} />
|
|
119
|
+
<div class='flex flex-wrap break-words m-2' ref={divRef} />
|
|
120
|
+
</>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
|
3
|
+
|
|
4
|
+
import nucleotideMutationsOtherVariant from './__mockData__/nucleotideMutationsOtherVariant.json';
|
|
5
|
+
import nucleotideMutationsSomeVariant from './__mockData__/nucleotideMutationsSomeVariant.json';
|
|
6
|
+
import { MutationComparison, type MutationComparisonProps } from './mutation-comparison';
|
|
7
|
+
import { LAPIS_URL, NUCLEOTIDE_MUTATIONS_ENDPOINT } from '../../constants';
|
|
8
|
+
import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
|
|
9
|
+
import { LapisUrlContext } from '../LapisUrlContext';
|
|
10
|
+
import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
11
|
+
|
|
12
|
+
const dateToSomeVariant = '2022-01-01';
|
|
13
|
+
|
|
14
|
+
const dateFromOtherVariant = '2021-01-01';
|
|
15
|
+
const dateToOtherVariant = '2022-01-02';
|
|
16
|
+
|
|
17
|
+
const meta: Meta<MutationComparisonProps> = {
|
|
18
|
+
title: 'Visualization/Mutation comparison',
|
|
19
|
+
component: MutationComparison,
|
|
20
|
+
argTypes: {
|
|
21
|
+
variants: [{ control: 'object' }],
|
|
22
|
+
sequenceType: {
|
|
23
|
+
options: ['nucleotide', 'amino acid'],
|
|
24
|
+
control: { type: 'radio' },
|
|
25
|
+
},
|
|
26
|
+
views: {
|
|
27
|
+
options: ['table', 'venn'],
|
|
28
|
+
control: { type: 'check' },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
parameters: {
|
|
32
|
+
fetchMock: {
|
|
33
|
+
mocks: [
|
|
34
|
+
{
|
|
35
|
+
matcher: {
|
|
36
|
+
name: 'nucleotideMutationsSomeVariant',
|
|
37
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
38
|
+
body: {
|
|
39
|
+
country: 'Switzerland',
|
|
40
|
+
pangoLineage: 'B.1.1.7',
|
|
41
|
+
dateTo: dateToSomeVariant,
|
|
42
|
+
minProportion: 0,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
response: {
|
|
46
|
+
status: 200,
|
|
47
|
+
body: nucleotideMutationsSomeVariant,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
matcher: {
|
|
52
|
+
name: 'nucleotideMutationsOtherVariant',
|
|
53
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
54
|
+
body: {
|
|
55
|
+
country: 'Switzerland',
|
|
56
|
+
pangoLineage: 'B.1.1.7',
|
|
57
|
+
dateFrom: dateFromOtherVariant,
|
|
58
|
+
dateTo: dateToOtherVariant,
|
|
59
|
+
minProportion: 0,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
response: {
|
|
63
|
+
status: 200,
|
|
64
|
+
body: nucleotideMutationsOtherVariant,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default meta;
|
|
73
|
+
|
|
74
|
+
const Template: StoryObj<MutationComparisonProps> = {
|
|
75
|
+
render: (args) => (
|
|
76
|
+
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
77
|
+
<ReferenceGenomeContext.Provider value={referenceGenome}>
|
|
78
|
+
<MutationComparison variants={args.variants} sequenceType={args.sequenceType} views={args.views} />
|
|
79
|
+
</ReferenceGenomeContext.Provider>
|
|
80
|
+
</LapisUrlContext.Provider>
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const TwoVariants: StoryObj<MutationComparisonProps> = {
|
|
85
|
+
...Template,
|
|
86
|
+
args: {
|
|
87
|
+
variants: [
|
|
88
|
+
{
|
|
89
|
+
displayName: 'Some variant',
|
|
90
|
+
lapisFilter: { country: 'Switzerland', pangoLineage: 'B.1.1.7', dateTo: dateToSomeVariant },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
displayName: 'Other variant',
|
|
94
|
+
lapisFilter: {
|
|
95
|
+
country: 'Switzerland',
|
|
96
|
+
pangoLineage: 'B.1.1.7',
|
|
97
|
+
dateFrom: dateFromOtherVariant,
|
|
98
|
+
dateTo: dateToOtherVariant,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
sequenceType: 'nucleotide',
|
|
103
|
+
views: ['table', 'venn'],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const FilterForOnlyDeletions: StoryObj<MutationComparisonProps> = {
|
|
108
|
+
...TwoVariants,
|
|
109
|
+
play: async ({ canvasElement }) => {
|
|
110
|
+
const canvas = within(canvasElement);
|
|
111
|
+
|
|
112
|
+
const someSubstitution = () => canvas.queryByText('C241T');
|
|
113
|
+
const someDeletion = () => canvas.queryByText('G199-');
|
|
114
|
+
|
|
115
|
+
await waitFor(() => expect(someSubstitution()).toBeVisible());
|
|
116
|
+
await waitFor(() => expect(someDeletion()).toBeVisible());
|
|
117
|
+
|
|
118
|
+
canvas.getByRole('button', { name: /Types:/ }).click();
|
|
119
|
+
canvas.getByLabelText('Substitutions').click();
|
|
120
|
+
|
|
121
|
+
await waitFor(() => expect(someSubstitution()).not.toBeInTheDocument());
|
|
122
|
+
await waitFor(() => expect(someDeletion()).toBeVisible());
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const FilterByProportion: StoryObj<MutationComparisonProps> = {
|
|
127
|
+
...TwoVariants,
|
|
128
|
+
play: async ({ canvasElement }) => {
|
|
129
|
+
const canvas = within(canvasElement);
|
|
130
|
+
|
|
131
|
+
const rareSubstitution = () => canvas.queryByText('T1234C');
|
|
132
|
+
const frequentSubstitution = () => canvas.queryByText('C241T');
|
|
133
|
+
const minInput = () => canvas.getAllByLabelText('%')[0];
|
|
134
|
+
const maxInput = () => canvas.getAllByLabelText('%')[1];
|
|
135
|
+
|
|
136
|
+
await waitFor(() => expect(rareSubstitution()).not.toBeInTheDocument());
|
|
137
|
+
await waitFor(() => expect(frequentSubstitution()).toBeVisible());
|
|
138
|
+
|
|
139
|
+
await userEvent.click(canvas.getByRole('button', { name: /Proportion/ }));
|
|
140
|
+
await userEvent.clear(minInput());
|
|
141
|
+
await userEvent.type(minInput(), '1');
|
|
142
|
+
|
|
143
|
+
await waitFor(() => expect(rareSubstitution()).toBeInTheDocument());
|
|
144
|
+
await waitFor(() => expect(frequentSubstitution()).toBeVisible());
|
|
145
|
+
|
|
146
|
+
await userEvent.clear(maxInput());
|
|
147
|
+
await userEvent.type(maxInput(), '50');
|
|
148
|
+
|
|
149
|
+
await waitFor(() => expect(rareSubstitution()).toBeInTheDocument());
|
|
150
|
+
await waitFor(() => expect(frequentSubstitution()).not.toBeInTheDocument());
|
|
151
|
+
},
|
|
152
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { type Dispatch, type StateUpdater, useContext, useMemo, useState } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { getMutationComparisonTableData } from './getMutationComparisonTableData';
|
|
5
|
+
import { MutationComparisonTable } from './mutation-comparison-table';
|
|
6
|
+
import { MutationComparisonVenn } from './mutation-comparison-venn';
|
|
7
|
+
import { filterMutationData, type MutationData, queryMutationData } from './queryMutationData';
|
|
8
|
+
import { type LapisFilter, type SequenceType } from '../../types';
|
|
9
|
+
import { LapisUrlContext } from '../LapisUrlContext';
|
|
10
|
+
import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '../components/SegmentSelector';
|
|
11
|
+
import { CsvDownloadButton } from '../components/csv-download-button';
|
|
12
|
+
import { ErrorDisplay } from '../components/error-display';
|
|
13
|
+
import Headline from '../components/headline';
|
|
14
|
+
import Info from '../components/info';
|
|
15
|
+
import { LoadingDisplay } from '../components/loading-display';
|
|
16
|
+
import { type DisplayedMutationType, MutationTypeSelector } from '../components/mutation-type-selector';
|
|
17
|
+
import { NoDataDisplay } from '../components/no-data-display';
|
|
18
|
+
import { type ProportionInterval } from '../components/proportion-selector';
|
|
19
|
+
import { ProportionSelectorDropdown } from '../components/proportion-selector-dropdown';
|
|
20
|
+
import Tabs from '../components/tabs';
|
|
21
|
+
import { useQuery } from '../useQuery';
|
|
22
|
+
|
|
23
|
+
export type View = 'table' | 'venn';
|
|
24
|
+
|
|
25
|
+
export interface MutationComparisonVariant {
|
|
26
|
+
lapisFilter: LapisFilter;
|
|
27
|
+
displayName: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MutationComparisonProps {
|
|
31
|
+
variants: MutationComparisonVariant[];
|
|
32
|
+
sequenceType: SequenceType;
|
|
33
|
+
views: View[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const MutationComparison: FunctionComponent<MutationComparisonProps> = ({ variants, sequenceType, views }) => {
|
|
37
|
+
const lapis = useContext(LapisUrlContext);
|
|
38
|
+
|
|
39
|
+
const { data, error, isLoading } = useQuery(async () => {
|
|
40
|
+
return queryMutationData(variants, sequenceType, lapis);
|
|
41
|
+
}, [variants, sequenceType, lapis]);
|
|
42
|
+
|
|
43
|
+
const headline = 'Mutation comparison';
|
|
44
|
+
|
|
45
|
+
if (isLoading) {
|
|
46
|
+
return (
|
|
47
|
+
<Headline heading={headline}>
|
|
48
|
+
<LoadingDisplay />
|
|
49
|
+
</Headline>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (error !== null) {
|
|
54
|
+
return (
|
|
55
|
+
<Headline heading={headline}>
|
|
56
|
+
<ErrorDisplay error={error} />
|
|
57
|
+
</Headline>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (data === null) {
|
|
62
|
+
return (
|
|
63
|
+
<Headline heading={headline}>
|
|
64
|
+
<NoDataDisplay />
|
|
65
|
+
</Headline>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Headline heading={headline}>
|
|
71
|
+
<MutationComparisonTabs data={data.mutationData} sequenceType={sequenceType} views={views} />
|
|
72
|
+
</Headline>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type MutationComparisonTabsProps = {
|
|
77
|
+
data: MutationData[];
|
|
78
|
+
views: View[];
|
|
79
|
+
sequenceType: SequenceType;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const MutationComparisonTabs: FunctionComponent<MutationComparisonTabsProps> = ({ data, views, sequenceType }) => {
|
|
83
|
+
const [proportionInterval, setProportionInterval] = useState({ min: 0.5, max: 1 });
|
|
84
|
+
const [displayedMutationTypes, setDisplayedMutationTypes] = useState<DisplayedMutationType[]>([
|
|
85
|
+
{ label: 'Substitutions', checked: true, type: 'substitution' },
|
|
86
|
+
{ label: 'Deletions', checked: true, type: 'deletion' },
|
|
87
|
+
]);
|
|
88
|
+
const [displayedSegments, setDisplayedSegments] = useDisplayedSegments(sequenceType);
|
|
89
|
+
|
|
90
|
+
const filteredData = useMemo(
|
|
91
|
+
() => filterMutationData(data, displayedSegments, displayedMutationTypes),
|
|
92
|
+
[data, displayedSegments, displayedMutationTypes],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const getTab = (view: View) => {
|
|
96
|
+
switch (view) {
|
|
97
|
+
case 'table':
|
|
98
|
+
return {
|
|
99
|
+
title: 'Table',
|
|
100
|
+
content: (
|
|
101
|
+
<MutationComparisonTable
|
|
102
|
+
data={{ content: filteredData }}
|
|
103
|
+
proportionInterval={proportionInterval}
|
|
104
|
+
/>
|
|
105
|
+
),
|
|
106
|
+
};
|
|
107
|
+
case 'venn':
|
|
108
|
+
return {
|
|
109
|
+
title: 'Venn',
|
|
110
|
+
content: (
|
|
111
|
+
<MutationComparisonVenn
|
|
112
|
+
data={{ content: filteredData }}
|
|
113
|
+
proportionInterval={proportionInterval}
|
|
114
|
+
/>
|
|
115
|
+
),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const tabs = views.map((view) => getTab(view));
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Tabs
|
|
124
|
+
tabs={tabs}
|
|
125
|
+
toolbar={
|
|
126
|
+
<Toolbar
|
|
127
|
+
displayedSegments={displayedSegments}
|
|
128
|
+
setDisplayedSegments={setDisplayedSegments}
|
|
129
|
+
displayedMutationTypes={displayedMutationTypes}
|
|
130
|
+
setDisplayedMutationTypes={setDisplayedMutationTypes}
|
|
131
|
+
filteredData={filteredData}
|
|
132
|
+
proportionInterval={proportionInterval}
|
|
133
|
+
setProportionInterval={setProportionInterval}
|
|
134
|
+
/>
|
|
135
|
+
}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
type ToolbarProps = {
|
|
141
|
+
displayedSegments: DisplayedSegment[];
|
|
142
|
+
setDisplayedSegments: (segments: DisplayedSegment[]) => void;
|
|
143
|
+
displayedMutationTypes: DisplayedMutationType[];
|
|
144
|
+
setDisplayedMutationTypes: (types: DisplayedMutationType[]) => void;
|
|
145
|
+
filteredData: MutationData[];
|
|
146
|
+
proportionInterval: ProportionInterval;
|
|
147
|
+
setProportionInterval: Dispatch<StateUpdater<ProportionInterval>>;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
151
|
+
displayedSegments,
|
|
152
|
+
setDisplayedSegments,
|
|
153
|
+
displayedMutationTypes,
|
|
154
|
+
setDisplayedMutationTypes,
|
|
155
|
+
filteredData,
|
|
156
|
+
proportionInterval,
|
|
157
|
+
setProportionInterval,
|
|
158
|
+
}) => {
|
|
159
|
+
return (
|
|
160
|
+
<div class='flex flex-row'>
|
|
161
|
+
<ProportionSelectorDropdown
|
|
162
|
+
proportionInterval={proportionInterval}
|
|
163
|
+
setMinProportion={(min) => setProportionInterval((prev) => ({ ...prev, min }))}
|
|
164
|
+
setMaxProportion={(max) => setProportionInterval((prev) => ({ ...prev, max }))}
|
|
165
|
+
/>
|
|
166
|
+
<SegmentSelector displayedSegments={displayedSegments} setDisplayedSegments={setDisplayedSegments} />
|
|
167
|
+
<MutationTypeSelector
|
|
168
|
+
displayedMutationTypes={displayedMutationTypes}
|
|
169
|
+
setDisplayedMutationTypes={setDisplayedMutationTypes}
|
|
170
|
+
/>
|
|
171
|
+
<CsvDownloadButton
|
|
172
|
+
className='mx-1 btn btn-xs'
|
|
173
|
+
getData={() => getMutationComparisonTableData({ content: filteredData }, proportionInterval)}
|
|
174
|
+
filename='mutation_comparison.csv'
|
|
175
|
+
/>
|
|
176
|
+
<Info className='mx-1' content='Info for mutation comparison' />
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type MutationComparisonVariant } from './mutation-comparison';
|
|
2
|
+
import { querySubstitutionsOrDeletions } from '../../query/querySubstitutionsOrDeletions';
|
|
3
|
+
import { type SubstitutionOrDeletionEntry } from '../../types';
|
|
4
|
+
import { type DisplayedSegment } from '../components/SegmentSelector';
|
|
5
|
+
import { type DisplayedMutationType } from '../components/mutation-type-selector';
|
|
6
|
+
|
|
7
|
+
export type MutationData = {
|
|
8
|
+
displayName: string;
|
|
9
|
+
data: SubstitutionOrDeletionEntry[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function queryMutationData(
|
|
13
|
+
variants: MutationComparisonVariant[],
|
|
14
|
+
sequenceType: 'nucleotide' | 'amino acid',
|
|
15
|
+
lapis: string,
|
|
16
|
+
) {
|
|
17
|
+
const mutationData = await Promise.all(
|
|
18
|
+
variants.map(async (variant) => {
|
|
19
|
+
return {
|
|
20
|
+
displayName: variant.displayName,
|
|
21
|
+
data: (await querySubstitutionsOrDeletions(variant.lapisFilter, sequenceType, lapis)).content,
|
|
22
|
+
};
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
return { mutationData };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function filterMutationData(
|
|
29
|
+
data: MutationData[],
|
|
30
|
+
displayedSegments: DisplayedSegment[],
|
|
31
|
+
displayedMutationTypes: DisplayedMutationType[],
|
|
32
|
+
) {
|
|
33
|
+
const byDisplayedSegments = (mutationEntry: SubstitutionOrDeletionEntry) => {
|
|
34
|
+
if (mutationEntry.mutation.segment === undefined) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return displayedSegments.some(
|
|
38
|
+
(displayedSegment) =>
|
|
39
|
+
displayedSegment.segment === mutationEntry.mutation.segment && displayedSegment.checked,
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
const byDisplayedMutationTypes = (mutationEntry: SubstitutionOrDeletionEntry) => {
|
|
43
|
+
return displayedMutationTypes.some(
|
|
44
|
+
(displayedMutationType) =>
|
|
45
|
+
displayedMutationType.checked && displayedMutationType.type === mutationEntry.type,
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return data.map((mutationEntry) => ({
|
|
50
|
+
displayName: mutationEntry.displayName,
|
|
51
|
+
data: mutationEntry.data.filter(byDisplayedSegments).filter(byDisplayedMutationTypes),
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { withActions } from '@storybook/addon-actions/decorator';
|
|
2
|
+
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
3
|
+
import { expect, fireEvent, fn, userEvent, waitFor, within } from '@storybook/test';
|
|
4
|
+
|
|
5
|
+
import { MutationFilter, type MutationFilterProps } from './mutation-filter';
|
|
6
|
+
import { LAPIS_URL } from '../../constants';
|
|
7
|
+
import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
|
|
8
|
+
import { LapisUrlContext } from '../LapisUrlContext';
|
|
9
|
+
import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
10
|
+
|
|
11
|
+
const meta: Meta<MutationFilterProps> = {
|
|
12
|
+
title: 'Input/MutationFilter',
|
|
13
|
+
component: MutationFilter,
|
|
14
|
+
parameters: {
|
|
15
|
+
actions: {
|
|
16
|
+
handles: ['gs-mutation-filter-changed', 'gs-mutation-filter-on-blur'],
|
|
17
|
+
},
|
|
18
|
+
fetchMock: {},
|
|
19
|
+
},
|
|
20
|
+
decorators: [withActions],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
|
|
25
|
+
export const Default: StoryObj<MutationFilterProps> = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
28
|
+
<ReferenceGenomeContext.Provider value={referenceGenome}>
|
|
29
|
+
<MutationFilter />
|
|
30
|
+
</ReferenceGenomeContext.Provider>
|
|
31
|
+
</LapisUrlContext.Provider>
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const FiresFilterChangedEvents: StoryObj<MutationFilterProps> = {
|
|
36
|
+
...Default,
|
|
37
|
+
play: async ({ canvasElement, step }) => {
|
|
38
|
+
const canvas = within(canvasElement);
|
|
39
|
+
const listenerMock = fn();
|
|
40
|
+
await step('Setup event listener mock', async () => {
|
|
41
|
+
canvasElement.addEventListener('gs-mutation-filter-changed', listenerMock);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await step('wait until data is loaded', async () => {
|
|
45
|
+
await waitFor(() => {
|
|
46
|
+
return expect(inputField(canvas)).toBeEnabled();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await step('Enters an invalid mutation', async () => {
|
|
51
|
+
await submitMutation(canvas, 'notAMutation');
|
|
52
|
+
await expect(listenerMock).not.toHaveBeenCalled();
|
|
53
|
+
|
|
54
|
+
await userEvent.type(inputField(canvas), '{backspace>12/}');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await step('Enter a valid mutation', async () => {
|
|
58
|
+
await submitMutation(canvas, 'A123T');
|
|
59
|
+
|
|
60
|
+
await waitFor(() =>
|
|
61
|
+
expect(listenerMock).toHaveBeenCalledWith(
|
|
62
|
+
expect.objectContaining({
|
|
63
|
+
detail: {
|
|
64
|
+
nucleotideMutations: ['A123T'],
|
|
65
|
+
aminoAcidMutations: [],
|
|
66
|
+
nucleotideInsertions: [],
|
|
67
|
+
aminoAcidInsertions: [],
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await step('Enter a second valid nucleotide mutation', async () => {
|
|
75
|
+
await submitMutation(canvas, 'A234-');
|
|
76
|
+
|
|
77
|
+
await expect(listenerMock).toHaveBeenCalledWith(
|
|
78
|
+
expect.objectContaining({
|
|
79
|
+
detail: {
|
|
80
|
+
nucleotideMutations: ['A123T', 'A234-'],
|
|
81
|
+
aminoAcidMutations: [],
|
|
82
|
+
nucleotideInsertions: [],
|
|
83
|
+
aminoAcidInsertions: [],
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await step('Enter another valid mutation', async () => {
|
|
90
|
+
await submitMutation(canvas, 'ins_123:AA');
|
|
91
|
+
|
|
92
|
+
await expect(listenerMock).toHaveBeenCalledWith(
|
|
93
|
+
expect.objectContaining({
|
|
94
|
+
detail: {
|
|
95
|
+
nucleotideMutations: ['A123T', 'A234-'],
|
|
96
|
+
aminoAcidMutations: [],
|
|
97
|
+
nucleotideInsertions: ['ins_123:AA'],
|
|
98
|
+
aminoAcidInsertions: [],
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await step('Remove the first mutation', async () => {
|
|
105
|
+
const firstMutationDeleteButton = canvas.getAllByRole('button')[0];
|
|
106
|
+
await waitFor(() => fireEvent.click(firstMutationDeleteButton));
|
|
107
|
+
|
|
108
|
+
await expect(listenerMock).toHaveBeenCalledWith(
|
|
109
|
+
expect.objectContaining({
|
|
110
|
+
detail: {
|
|
111
|
+
nucleotideMutations: ['A234-'],
|
|
112
|
+
aminoAcidMutations: [],
|
|
113
|
+
nucleotideInsertions: ['ins_123:AA'],
|
|
114
|
+
aminoAcidInsertions: [],
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const FiresFilterOnBlurEvent: StoryObj<MutationFilterProps> = {
|
|
123
|
+
...Default,
|
|
124
|
+
play: async ({ canvasElement, step }) => {
|
|
125
|
+
const canvas = within(canvasElement);
|
|
126
|
+
|
|
127
|
+
const listenerMock = fn();
|
|
128
|
+
await step('Setup event listener mock', async () => {
|
|
129
|
+
canvasElement.addEventListener('gs-mutation-filter-on-blur', listenerMock);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await step('wait until data is loaded', async () => {
|
|
133
|
+
await waitFor(() => {
|
|
134
|
+
return expect(inputField(canvas)).toBeEnabled();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await step('Move outside of input', async () => {
|
|
139
|
+
await submitMutation(canvas, 'A234T');
|
|
140
|
+
await submitMutation(canvas, 'S:A123G');
|
|
141
|
+
await submitMutation(canvas, 'ins_123:AAA');
|
|
142
|
+
await submitMutation(canvas, 'ins_S:123:AAA');
|
|
143
|
+
await userEvent.tab();
|
|
144
|
+
|
|
145
|
+
await expect(listenerMock).toHaveBeenCalledWith(
|
|
146
|
+
expect.objectContaining({
|
|
147
|
+
detail: {
|
|
148
|
+
nucleotideMutations: ['A234T'],
|
|
149
|
+
aminoAcidMutations: ['S:A123G'],
|
|
150
|
+
nucleotideInsertions: ['ins_123:AAA'],
|
|
151
|
+
aminoAcidInsertions: ['ins_S:123:AAA'],
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const submitMutation = async (canvas: ReturnType<typeof within>, mutation: string) => {
|
|
160
|
+
await userEvent.type(inputField(canvas), mutation);
|
|
161
|
+
await waitFor(() => submitButton(canvas).click());
|
|
162
|
+
};
|
|
163
|
+
const inputField = (canvas: ReturnType<typeof within>) => canvas.getByPlaceholderText('Enter a mutation');
|
|
164
|
+
const submitButton = (canvas: ReturnType<typeof within>) => canvas.getByRole('button', { name: '+' });
|