@genspectrum/dashboard-components 0.1.3 → 0.1.5
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/custom-elements.json +488 -117
- package/dist/dashboard-components.js +904 -466
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +473 -67
- package/dist/style.css +273 -153
- package/package.json +11 -7
- package/src/preact/aggregatedData/aggregate.stories.tsx +7 -5
- package/src/preact/aggregatedData/aggregate.tsx +16 -7
- package/src/preact/components/ReferenceGenomesAwaiter.tsx +25 -0
- package/src/preact/components/csv-download-button.tsx +8 -2
- package/src/preact/components/headline.stories.tsx +19 -1
- package/src/preact/components/headline.tsx +25 -5
- package/src/preact/components/info.stories.tsx +24 -3
- package/src/preact/components/info.tsx +49 -5
- package/src/preact/components/min-max-range-slider.tsx +4 -4
- package/src/preact/components/percent-intput.tsx +2 -3
- package/src/preact/components/resize-container.tsx +23 -0
- package/src/preact/components/table.tsx +1 -0
- package/src/preact/components/tabs.stories.tsx +2 -2
- package/src/preact/components/tabs.tsx +47 -24
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +36 -4
- package/src/preact/dateRangeSelector/date-range-selector.tsx +67 -53
- package/src/preact/locationFilter/location-filter.tsx +2 -2
- package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +5 -5
- package/src/preact/mutationComparison/getMutationComparisonTableData.ts +45 -10
- package/src/preact/mutationComparison/mutation-comparison-table.tsx +20 -22
- package/src/preact/mutationComparison/mutation-comparison-venn.tsx +6 -3
- package/src/preact/mutationComparison/mutation-comparison.stories.tsx +11 -1
- package/src/preact/mutationComparison/mutation-comparison.tsx +16 -7
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +70 -31
- package/src/preact/mutationFilter/mutation-filter.tsx +62 -14
- package/src/preact/mutations/getInsertionsTableData.spec.ts +6 -4
- package/src/preact/mutations/getInsertionsTableData.ts +1 -1
- package/src/preact/mutations/getMutationsTableData.spec.ts +9 -19
- package/src/preact/mutations/getMutationsTableData.ts +1 -1
- package/src/preact/mutations/mutations-insertions-table.tsx +3 -1
- package/src/preact/mutations/mutations-table.tsx +3 -1
- package/src/preact/mutations/mutations.stories.tsx +11 -1
- package/src/preact/mutations/mutations.tsx +24 -7
- package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +1 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +1 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +1 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +8 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +31 -13
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +8 -5
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +15 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +62 -12
- package/src/preact/shared/sort/sortInsertions.spec.ts +11 -10
- package/src/preact/shared/sort/sortInsertions.ts +10 -17
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +19 -10
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +45 -12
- package/src/preact/textInput/text-input.stories.tsx +22 -1
- package/src/preact/textInput/text-input.tsx +3 -1
- package/src/utils/typeAssertions.spec.ts +31 -0
- package/src/utils/typeAssertions.ts +16 -0
- package/src/web-components/PreactLitAdapter.tsx +0 -1
- package/src/web-components/app.stories.ts +129 -0
- package/src/web-components/app.ts +27 -6
- package/src/web-components/display/aggregate-component.stories.ts +24 -11
- package/src/web-components/display/aggregate-component.tsx +26 -5
- package/src/web-components/display/mutation-comparison-component.stories.ts +32 -11
- package/src/web-components/display/mutation-comparison-component.tsx +79 -4
- package/src/web-components/display/mutations-component.stories.ts +40 -19
- package/src/web-components/display/mutations-component.tsx +71 -4
- package/src/web-components/display/prevalence-over-time-component.stories.ts +44 -18
- package/src/web-components/display/prevalence-over-time-component.tsx +105 -5
- package/src/web-components/display/relative-growth-advantage-component.stories.ts +32 -10
- package/src/web-components/display/relative-growth-advantage-component.tsx +66 -3
- package/src/web-components/input/date-range-selector-component.stories.ts +51 -9
- package/src/web-components/input/date-range-selector-component.tsx +69 -4
- package/src/web-components/input/location-filter-component.stories.ts +15 -4
- package/src/web-components/input/location-filter-component.tsx +2 -6
- package/src/web-components/input/mutation-filter-component.stories.ts +33 -12
- package/src/web-components/input/mutation-filter-component.tsx +60 -4
- package/src/web-components/input/text-input-component.stories.ts +26 -6
- package/src/web-components/input/text-input-component.tsx +34 -3
- package/src/web-components/display/aggregate-component.mdx +0 -25
- package/src/web-components/input/location-filter.mdx +0 -25
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { withActions } from '@storybook/addon-actions/decorator';
|
|
2
|
-
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { type Meta, type PreactRenderer, type StoryObj } from '@storybook/preact';
|
|
3
3
|
import { expect, fireEvent, fn, userEvent, waitFor, within } from '@storybook/test';
|
|
4
|
+
import { type StepFunction } from '@storybook/types';
|
|
4
5
|
|
|
5
6
|
import { MutationFilter, type MutationFilterProps } from './mutation-filter';
|
|
6
7
|
import { LAPIS_URL } from '../../constants';
|
|
@@ -35,21 +36,11 @@ export const Default: StoryObj<MutationFilterProps> = {
|
|
|
35
36
|
export const FiresFilterChangedEvents: StoryObj<MutationFilterProps> = {
|
|
36
37
|
...Default,
|
|
37
38
|
play: async ({ canvasElement, step }) => {
|
|
38
|
-
const canvas =
|
|
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
|
-
});
|
|
39
|
+
const { canvas, changedListenerMock } = await prepare(canvasElement, step);
|
|
49
40
|
|
|
50
41
|
await step('Enters an invalid mutation', async () => {
|
|
51
42
|
await submitMutation(canvas, 'notAMutation');
|
|
52
|
-
await expect(
|
|
43
|
+
await expect(changedListenerMock).not.toHaveBeenCalled();
|
|
53
44
|
|
|
54
45
|
await userEvent.type(inputField(canvas), '{backspace>12/}');
|
|
55
46
|
});
|
|
@@ -58,7 +49,7 @@ export const FiresFilterChangedEvents: StoryObj<MutationFilterProps> = {
|
|
|
58
49
|
await submitMutation(canvas, 'A123T');
|
|
59
50
|
|
|
60
51
|
await waitFor(() =>
|
|
61
|
-
expect(
|
|
52
|
+
expect(changedListenerMock).toHaveBeenCalledWith(
|
|
62
53
|
expect.objectContaining({
|
|
63
54
|
detail: {
|
|
64
55
|
nucleotideMutations: ['A123T'],
|
|
@@ -74,7 +65,7 @@ export const FiresFilterChangedEvents: StoryObj<MutationFilterProps> = {
|
|
|
74
65
|
await step('Enter a second valid nucleotide mutation', async () => {
|
|
75
66
|
await submitMutation(canvas, 'A234-');
|
|
76
67
|
|
|
77
|
-
await expect(
|
|
68
|
+
await expect(changedListenerMock).toHaveBeenCalledWith(
|
|
78
69
|
expect.objectContaining({
|
|
79
70
|
detail: {
|
|
80
71
|
nucleotideMutations: ['A123T', 'A234-'],
|
|
@@ -89,7 +80,7 @@ export const FiresFilterChangedEvents: StoryObj<MutationFilterProps> = {
|
|
|
89
80
|
await step('Enter another valid mutation', async () => {
|
|
90
81
|
await submitMutation(canvas, 'ins_123:AA');
|
|
91
82
|
|
|
92
|
-
await expect(
|
|
83
|
+
await expect(changedListenerMock).toHaveBeenCalledWith(
|
|
93
84
|
expect.objectContaining({
|
|
94
85
|
detail: {
|
|
95
86
|
nucleotideMutations: ['A123T', 'A234-'],
|
|
@@ -105,7 +96,7 @@ export const FiresFilterChangedEvents: StoryObj<MutationFilterProps> = {
|
|
|
105
96
|
const firstMutationDeleteButton = canvas.getAllByRole('button')[0];
|
|
106
97
|
await waitFor(() => fireEvent.click(firstMutationDeleteButton));
|
|
107
98
|
|
|
108
|
-
await expect(
|
|
99
|
+
await expect(changedListenerMock).toHaveBeenCalledWith(
|
|
109
100
|
expect.objectContaining({
|
|
110
101
|
detail: {
|
|
111
102
|
nucleotideMutations: ['A234-'],
|
|
@@ -122,18 +113,7 @@ export const FiresFilterChangedEvents: StoryObj<MutationFilterProps> = {
|
|
|
122
113
|
export const FiresFilterOnBlurEvent: StoryObj<MutationFilterProps> = {
|
|
123
114
|
...Default,
|
|
124
115
|
play: async ({ canvasElement, step }) => {
|
|
125
|
-
const canvas =
|
|
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
|
-
});
|
|
116
|
+
const { canvas, onBlurListenerMock } = await prepare(canvasElement, step);
|
|
137
117
|
|
|
138
118
|
await step('Move outside of input', async () => {
|
|
139
119
|
await submitMutation(canvas, 'A234T');
|
|
@@ -142,7 +122,7 @@ export const FiresFilterOnBlurEvent: StoryObj<MutationFilterProps> = {
|
|
|
142
122
|
await submitMutation(canvas, 'ins_S:123:AAA');
|
|
143
123
|
await userEvent.tab();
|
|
144
124
|
|
|
145
|
-
await expect(
|
|
125
|
+
await expect(onBlurListenerMock).toHaveBeenCalledWith(
|
|
146
126
|
expect.objectContaining({
|
|
147
127
|
detail: {
|
|
148
128
|
nucleotideMutations: ['A234T'],
|
|
@@ -156,9 +136,68 @@ export const FiresFilterOnBlurEvent: StoryObj<MutationFilterProps> = {
|
|
|
156
136
|
},
|
|
157
137
|
};
|
|
158
138
|
|
|
139
|
+
export const WithInitialValue: StoryObj<MutationFilterProps> = {
|
|
140
|
+
render: (args) => (
|
|
141
|
+
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
142
|
+
<ReferenceGenomeContext.Provider value={referenceGenome}>
|
|
143
|
+
<MutationFilter initialValue={args.initialValue} />
|
|
144
|
+
</ReferenceGenomeContext.Provider>
|
|
145
|
+
</LapisUrlContext.Provider>
|
|
146
|
+
),
|
|
147
|
+
args: {
|
|
148
|
+
initialValue: {
|
|
149
|
+
nucleotideMutations: ['A234T'],
|
|
150
|
+
aminoAcidMutations: ['S:A123G'],
|
|
151
|
+
nucleotideInsertions: ['ins_123:AAA'],
|
|
152
|
+
aminoAcidInsertions: ['ins_S:123:AAA'],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
play: async ({ canvasElement, step }) => {
|
|
156
|
+
const { canvas, onBlurListenerMock } = await prepare(canvasElement, step);
|
|
157
|
+
|
|
158
|
+
await step('Move outside of input', async () => {
|
|
159
|
+
await submitMutation(canvas, 'G500T');
|
|
160
|
+
await userEvent.tab();
|
|
161
|
+
|
|
162
|
+
await expect(onBlurListenerMock).toHaveBeenCalledWith(
|
|
163
|
+
expect.objectContaining({
|
|
164
|
+
detail: {
|
|
165
|
+
nucleotideMutations: ['A234T', 'G500T'],
|
|
166
|
+
aminoAcidMutations: ['S:A123G'],
|
|
167
|
+
nucleotideInsertions: ['ins_123:AAA'],
|
|
168
|
+
aminoAcidInsertions: ['ins_S:123:AAA'],
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRenderer, unknown>) {
|
|
177
|
+
const canvas = within(canvasElement);
|
|
178
|
+
|
|
179
|
+
const onBlurListenerMock = fn();
|
|
180
|
+
const changedListenerMock = fn();
|
|
181
|
+
await step('Setup event listener mock', async () => {
|
|
182
|
+
canvasElement.addEventListener('gs-mutation-filter-on-blur', onBlurListenerMock);
|
|
183
|
+
canvasElement.addEventListener('gs-mutation-filter-changed', changedListenerMock);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await step('wait until data is loaded', async () => {
|
|
187
|
+
await waitFor(() => {
|
|
188
|
+
return expect(inputField(canvas)).toBeEnabled();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return { canvas, onBlurListenerMock, changedListenerMock };
|
|
193
|
+
}
|
|
194
|
+
|
|
159
195
|
const submitMutation = async (canvas: ReturnType<typeof within>, mutation: string) => {
|
|
160
196
|
await userEvent.type(inputField(canvas), mutation);
|
|
161
197
|
await waitFor(() => submitButton(canvas).click());
|
|
162
198
|
};
|
|
163
|
-
|
|
199
|
+
|
|
200
|
+
const inputField = (canvas: ReturnType<typeof within>) =>
|
|
201
|
+
canvas.getByPlaceholderText('Enter a mutation', { exact: false });
|
|
202
|
+
|
|
164
203
|
const submitButton = (canvas: ReturnType<typeof within>) => canvas.getByRole('button', { name: '+' });
|
|
@@ -2,12 +2,16 @@ import { type FunctionComponent } from 'preact';
|
|
|
2
2
|
import { useContext, useRef, useState } from 'preact/hooks';
|
|
3
3
|
|
|
4
4
|
import { parseAndValidateMutation } from './parseAndValidateMutation';
|
|
5
|
+
import { type ReferenceGenome } from '../../lapisApi/ReferenceGenome';
|
|
5
6
|
import { type Deletion, type Insertion, type Mutation, type Substitution } from '../../utils/mutations';
|
|
6
7
|
import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
8
|
+
import Info from '../components/info';
|
|
7
9
|
import { singleGraphColorRGBByName } from '../shared/charts/colors';
|
|
8
10
|
import { DeleteIcon } from '../shared/icons/DeleteIcon';
|
|
9
11
|
|
|
10
|
-
export type MutationFilterProps = {
|
|
12
|
+
export type MutationFilterProps = {
|
|
13
|
+
initialValue?: SelectedMutationFilterStrings | string[] | undefined;
|
|
14
|
+
};
|
|
11
15
|
|
|
12
16
|
export type SelectedFilters = {
|
|
13
17
|
nucleotideMutations: (Substitution | Deletion)[];
|
|
@@ -20,14 +24,11 @@ export type SelectedMutationFilterStrings = {
|
|
|
20
24
|
[Key in keyof SelectedFilters]: string[];
|
|
21
25
|
};
|
|
22
26
|
|
|
23
|
-
export const MutationFilter: FunctionComponent<MutationFilterProps> = () => {
|
|
27
|
+
export const MutationFilter: FunctionComponent<MutationFilterProps> = ({ initialValue }) => {
|
|
24
28
|
const referenceGenome = useContext(ReferenceGenomeContext);
|
|
25
|
-
const [selectedFilters, setSelectedFilters] = useState<SelectedFilters>(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
nucleotideInsertions: [],
|
|
29
|
-
aminoAcidInsertions: [],
|
|
30
|
-
});
|
|
29
|
+
const [selectedFilters, setSelectedFilters] = useState<SelectedFilters>(
|
|
30
|
+
getInitialState(initialValue, referenceGenome),
|
|
31
|
+
);
|
|
31
32
|
const [inputValue, setInputValue] = useState('');
|
|
32
33
|
const [isError, setIsError] = useState(false);
|
|
33
34
|
const formRef = useRef<HTMLFormElement>(null);
|
|
@@ -83,11 +84,14 @@ export const MutationFilter: FunctionComponent<MutationFilterProps> = () => {
|
|
|
83
84
|
|
|
84
85
|
return (
|
|
85
86
|
<div class={`rounded-lg border border-gray-300 bg-white p-2`}>
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
<div class='flex justify-between'>
|
|
88
|
+
<SelectedMutationDisplay
|
|
89
|
+
selectedFilters={selectedFilters}
|
|
90
|
+
setSelectedFilters={setSelectedFilters}
|
|
91
|
+
fireChangeEvent={fireChangeEvent}
|
|
92
|
+
/>
|
|
93
|
+
<Info>Info for mutation filter</Info>
|
|
94
|
+
</div>
|
|
91
95
|
|
|
92
96
|
<form className='mt-2 w-full' onSubmit={handleSubmit} ref={formRef}>
|
|
93
97
|
<label className={`input flex items-center gap-2 ${isError ? 'input-error' : 'input-bordered'}`}>
|
|
@@ -96,7 +100,7 @@ export const MutationFilter: FunctionComponent<MutationFilterProps> = () => {
|
|
|
96
100
|
type='text'
|
|
97
101
|
value={inputValue}
|
|
98
102
|
onInput={handleInputChange}
|
|
99
|
-
placeholder={
|
|
103
|
+
placeholder={getPlaceholder(referenceGenome)}
|
|
100
104
|
onBlur={handleOnBlur}
|
|
101
105
|
/>
|
|
102
106
|
<button className='btn btn-sm'>+</button>
|
|
@@ -106,6 +110,50 @@ export const MutationFilter: FunctionComponent<MutationFilterProps> = () => {
|
|
|
106
110
|
);
|
|
107
111
|
};
|
|
108
112
|
|
|
113
|
+
function getInitialState(
|
|
114
|
+
initialValue: SelectedMutationFilterStrings | string[] | undefined,
|
|
115
|
+
referenceGenome: ReferenceGenome,
|
|
116
|
+
) {
|
|
117
|
+
if (initialValue === undefined) {
|
|
118
|
+
return {
|
|
119
|
+
nucleotideMutations: [],
|
|
120
|
+
aminoAcidMutations: [],
|
|
121
|
+
nucleotideInsertions: [],
|
|
122
|
+
aminoAcidInsertions: [],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const values = Array.isArray(initialValue) ? initialValue : Object.values(initialValue).flatMap((it) => it);
|
|
127
|
+
|
|
128
|
+
return values.reduce(
|
|
129
|
+
(selectedFilters, value) => {
|
|
130
|
+
const parsedMutation = parseAndValidateMutation(value, referenceGenome);
|
|
131
|
+
if (parsedMutation === null) {
|
|
132
|
+
return selectedFilters;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
...selectedFilters,
|
|
137
|
+
[parsedMutation.type]: [...selectedFilters[parsedMutation.type], parsedMutation.value],
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
nucleotideMutations: [],
|
|
142
|
+
aminoAcidMutations: [],
|
|
143
|
+
nucleotideInsertions: [],
|
|
144
|
+
aminoAcidInsertions: [],
|
|
145
|
+
} as SelectedFilters,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getPlaceholder(referenceGenome: ReferenceGenome) {
|
|
150
|
+
const segmentPrefix =
|
|
151
|
+
referenceGenome.nucleotideSequences.length > 1 ? `${referenceGenome.nucleotideSequences[0].name}:` : '';
|
|
152
|
+
const firstGene = referenceGenome.genes[0].name;
|
|
153
|
+
|
|
154
|
+
return `Enter a mutation (e.g. ${segmentPrefix}A123T, ins_${segmentPrefix}123:AT, ${firstGene}:M123E, ins_${firstGene}:123:ME)`;
|
|
155
|
+
}
|
|
156
|
+
|
|
109
157
|
const SelectedMutationDisplay: FunctionComponent<{
|
|
110
158
|
selectedFilters: SelectedFilters;
|
|
111
159
|
setSelectedFilters: (selectedFilters: SelectedFilters) => void;
|
|
@@ -5,16 +5,18 @@ import { Insertion } from '../../utils/mutations';
|
|
|
5
5
|
|
|
6
6
|
describe('getInsertionsTableData', () => {
|
|
7
7
|
test('should return the correct data', () => {
|
|
8
|
+
const insertion1 = new Insertion('segment1', 123, 'T');
|
|
9
|
+
const insertion2 = new Insertion('segment2', 234, 'AAA');
|
|
8
10
|
const data = [
|
|
9
11
|
{
|
|
10
12
|
type: 'insertion' as const,
|
|
11
|
-
mutation:
|
|
13
|
+
mutation: insertion1,
|
|
12
14
|
count: 1,
|
|
13
15
|
proportion: 0.1,
|
|
14
16
|
},
|
|
15
17
|
{
|
|
16
18
|
type: 'insertion' as const,
|
|
17
|
-
mutation:
|
|
19
|
+
mutation: insertion2,
|
|
18
20
|
count: 2,
|
|
19
21
|
proportion: 0.2,
|
|
20
22
|
},
|
|
@@ -24,11 +26,11 @@ describe('getInsertionsTableData', () => {
|
|
|
24
26
|
|
|
25
27
|
expect(result).toEqual([
|
|
26
28
|
{
|
|
27
|
-
insertion:
|
|
29
|
+
insertion: insertion1,
|
|
28
30
|
count: 1,
|
|
29
31
|
},
|
|
30
32
|
{
|
|
31
|
-
insertion:
|
|
33
|
+
insertion: insertion2,
|
|
32
34
|
count: 2,
|
|
33
35
|
},
|
|
34
36
|
]);
|
|
@@ -3,7 +3,7 @@ import { type InsertionEntry } from '../../types';
|
|
|
3
3
|
export function getInsertionsTableData(data: InsertionEntry[]) {
|
|
4
4
|
return data.map((mutationEntry) => {
|
|
5
5
|
return {
|
|
6
|
-
insertion: mutationEntry.mutation
|
|
6
|
+
insertion: mutationEntry.mutation,
|
|
7
7
|
count: mutationEntry.count,
|
|
8
8
|
};
|
|
9
9
|
});
|
|
@@ -4,7 +4,7 @@ import { getMutationsTableData } from './getMutationsTableData';
|
|
|
4
4
|
import { Deletion, Substitution } from '../../utils/mutations';
|
|
5
5
|
|
|
6
6
|
describe('getMutationsTableData', () => {
|
|
7
|
-
test('should
|
|
7
|
+
test('should not filter anything, when proportions are in interval', () => {
|
|
8
8
|
const data = [
|
|
9
9
|
{
|
|
10
10
|
type: 'substitution' as const,
|
|
@@ -24,20 +24,7 @@ describe('getMutationsTableData', () => {
|
|
|
24
24
|
|
|
25
25
|
const result = getMutationsTableData(data, proportionInterval);
|
|
26
26
|
|
|
27
|
-
expect(result).toEqual(
|
|
28
|
-
{
|
|
29
|
-
mutation: 'segment1:A123T',
|
|
30
|
-
type: 'substitution',
|
|
31
|
-
count: 1,
|
|
32
|
-
proportion: 0.1,
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
mutation: 'segment2:C123-',
|
|
36
|
-
type: 'deletion',
|
|
37
|
-
count: 2,
|
|
38
|
-
proportion: 0.2,
|
|
39
|
-
},
|
|
40
|
-
]);
|
|
27
|
+
expect(result).toEqual(data);
|
|
41
28
|
});
|
|
42
29
|
|
|
43
30
|
test('should filter out data below/above proportionInterval', () => {
|
|
@@ -45,10 +32,13 @@ describe('getMutationsTableData', () => {
|
|
|
45
32
|
const aboveInterval = 0.95;
|
|
46
33
|
const inInterval = 0.5;
|
|
47
34
|
|
|
35
|
+
const substitutionInInterval = new Substitution('segment1', 'A', 'T', 123);
|
|
36
|
+
const deletionInInterval = new Deletion('segment2', 'C', 123);
|
|
37
|
+
|
|
48
38
|
const data = [
|
|
49
39
|
{
|
|
50
40
|
type: 'substitution' as const,
|
|
51
|
-
mutation:
|
|
41
|
+
mutation: substitutionInInterval,
|
|
52
42
|
count: 1,
|
|
53
43
|
proportion: inInterval,
|
|
54
44
|
},
|
|
@@ -60,7 +50,7 @@ describe('getMutationsTableData', () => {
|
|
|
60
50
|
},
|
|
61
51
|
{
|
|
62
52
|
type: 'deletion' as const,
|
|
63
|
-
mutation:
|
|
53
|
+
mutation: deletionInInterval,
|
|
64
54
|
count: 2,
|
|
65
55
|
proportion: inInterval,
|
|
66
56
|
},
|
|
@@ -78,13 +68,13 @@ describe('getMutationsTableData', () => {
|
|
|
78
68
|
|
|
79
69
|
expect(result).toEqual([
|
|
80
70
|
{
|
|
81
|
-
mutation:
|
|
71
|
+
mutation: substitutionInInterval,
|
|
82
72
|
type: 'substitution',
|
|
83
73
|
count: 1,
|
|
84
74
|
proportion: inInterval,
|
|
85
75
|
},
|
|
86
76
|
{
|
|
87
|
-
mutation:
|
|
77
|
+
mutation: deletionInInterval,
|
|
88
78
|
type: 'deletion',
|
|
89
79
|
count: 2,
|
|
90
80
|
proportion: inInterval,
|
|
@@ -8,7 +8,7 @@ export function getMutationsTableData(data: SubstitutionOrDeletionEntry[], propo
|
|
|
8
8
|
|
|
9
9
|
return data.filter(byProportion).map((mutationEntry) => {
|
|
10
10
|
return {
|
|
11
|
-
mutation: mutationEntry.mutation
|
|
11
|
+
mutation: mutationEntry.mutation,
|
|
12
12
|
type: mutationEntry.type,
|
|
13
13
|
count: mutationEntry.count,
|
|
14
14
|
proportion: mutationEntry.proportion,
|
|
@@ -2,6 +2,7 @@ import { type FunctionComponent } from 'preact';
|
|
|
2
2
|
|
|
3
3
|
import { getInsertionsTableData } from './getInsertionsTableData';
|
|
4
4
|
import { type InsertionEntry } from '../../types';
|
|
5
|
+
import { type Insertion } from '../../utils/mutations';
|
|
5
6
|
import { Table } from '../components/table';
|
|
6
7
|
import { sortInsertions } from '../shared/sort/sortInsertions';
|
|
7
8
|
|
|
@@ -15,10 +16,11 @@ export const InsertionsTable: FunctionComponent<InsertionsTableProps> = ({ data
|
|
|
15
16
|
{
|
|
16
17
|
name: 'Insertion',
|
|
17
18
|
sort: {
|
|
18
|
-
compare: (a:
|
|
19
|
+
compare: (a: Insertion, b: Insertion) => {
|
|
19
20
|
return sortInsertions(a, b);
|
|
20
21
|
},
|
|
21
22
|
},
|
|
23
|
+
formatter: (cell: Insertion) => cell.toString(),
|
|
22
24
|
},
|
|
23
25
|
{
|
|
24
26
|
name: 'Count',
|
|
@@ -2,6 +2,7 @@ import { type FunctionComponent } from 'preact';
|
|
|
2
2
|
|
|
3
3
|
import { getMutationsTableData } from './getMutationsTableData';
|
|
4
4
|
import { type SubstitutionOrDeletionEntry } from '../../types';
|
|
5
|
+
import { type Deletion, type Substitution } from '../../utils/mutations';
|
|
5
6
|
import type { ProportionInterval } from '../components/proportion-selector';
|
|
6
7
|
import { Table } from '../components/table';
|
|
7
8
|
import { sortSubstitutionsAndDeletions } from '../shared/sort/sortSubstitutionsAndDeletions';
|
|
@@ -18,10 +19,11 @@ const MutationsTable: FunctionComponent<MutationsTableProps> = ({ data, proporti
|
|
|
18
19
|
{
|
|
19
20
|
name: 'Mutation',
|
|
20
21
|
sort: {
|
|
21
|
-
compare: (a:
|
|
22
|
+
compare: (a: Substitution | Deletion, b: Substitution | Deletion) => {
|
|
22
23
|
return sortSubstitutionsAndDeletions(a, b);
|
|
23
24
|
},
|
|
24
25
|
},
|
|
26
|
+
formatter: (cell: Substitution | Deletion) => cell.toString(),
|
|
25
27
|
},
|
|
26
28
|
{
|
|
27
29
|
name: 'Type',
|
|
@@ -22,6 +22,8 @@ const meta: Meta<MutationsProps> = {
|
|
|
22
22
|
options: ['table', 'grid', 'insertions'],
|
|
23
23
|
control: { type: 'check' },
|
|
24
24
|
},
|
|
25
|
+
size: [{ control: 'object' }],
|
|
26
|
+
headline: { control: 'text' },
|
|
25
27
|
},
|
|
26
28
|
};
|
|
27
29
|
|
|
@@ -31,7 +33,13 @@ const Template = {
|
|
|
31
33
|
render: (args: MutationsProps) => (
|
|
32
34
|
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
33
35
|
<ReferenceGenomeContext.Provider value={referenceGenome}>
|
|
34
|
-
<Mutations
|
|
36
|
+
<Mutations
|
|
37
|
+
variant={args.variant}
|
|
38
|
+
sequenceType={args.sequenceType}
|
|
39
|
+
views={args.views}
|
|
40
|
+
size={args.size}
|
|
41
|
+
headline={args.headline}
|
|
42
|
+
/>
|
|
35
43
|
</ReferenceGenomeContext.Provider>
|
|
36
44
|
</LapisUrlContext.Provider>
|
|
37
45
|
),
|
|
@@ -43,6 +51,8 @@ export const Default: StoryObj<MutationsProps> = {
|
|
|
43
51
|
variant: { country: 'Switzerland', pangoLineage: 'B.1.1.7', dateTo: '2022-01-01' },
|
|
44
52
|
sequenceType: 'nucleotide',
|
|
45
53
|
views: ['grid', 'table', 'insertions'],
|
|
54
|
+
size: { width: '100%', height: '700px' },
|
|
55
|
+
headline: 'Mutations',
|
|
46
56
|
},
|
|
47
57
|
parameters: {
|
|
48
58
|
fetchMock: {
|
|
@@ -24,6 +24,7 @@ import { type DisplayedMutationType, MutationTypeSelector } from '../components/
|
|
|
24
24
|
import { NoDataDisplay } from '../components/no-data-display';
|
|
25
25
|
import type { ProportionInterval } from '../components/proportion-selector';
|
|
26
26
|
import { ProportionSelectorDropdown } from '../components/proportion-selector-dropdown';
|
|
27
|
+
import { ResizeContainer, type Size } from '../components/resize-container';
|
|
27
28
|
import Tabs from '../components/tabs';
|
|
28
29
|
import { useQuery } from '../useQuery';
|
|
29
30
|
|
|
@@ -33,15 +34,22 @@ export interface MutationsProps {
|
|
|
33
34
|
variant: LapisFilter;
|
|
34
35
|
sequenceType: SequenceType;
|
|
35
36
|
views: View[];
|
|
37
|
+
size?: Size;
|
|
38
|
+
headline?: string;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
export const Mutations: FunctionComponent<MutationsProps> = ({
|
|
41
|
+
export const Mutations: FunctionComponent<MutationsProps> = ({
|
|
42
|
+
variant,
|
|
43
|
+
sequenceType,
|
|
44
|
+
views,
|
|
45
|
+
size,
|
|
46
|
+
headline = 'Mutations',
|
|
47
|
+
}) => {
|
|
39
48
|
const lapis = useContext(LapisUrlContext);
|
|
40
49
|
const { data, error, isLoading } = useQuery(async () => {
|
|
41
50
|
return queryMutationsData(variant, sequenceType, lapis);
|
|
42
51
|
}, [variant, sequenceType, lapis]);
|
|
43
52
|
|
|
44
|
-
const headline = 'Mutations';
|
|
45
53
|
if (isLoading) {
|
|
46
54
|
return (
|
|
47
55
|
<Headline heading={headline}>
|
|
@@ -67,9 +75,11 @@ export const Mutations: FunctionComponent<MutationsProps> = ({ variant, sequence
|
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
return (
|
|
70
|
-
<
|
|
71
|
-
<
|
|
72
|
-
|
|
78
|
+
<ResizeContainer size={size} defaultSize={{ height: '700px', width: '100%' }}>
|
|
79
|
+
<Headline heading={headline}>
|
|
80
|
+
<MutationsTabs mutationsData={data} sequenceType={sequenceType} views={views} />
|
|
81
|
+
</Headline>
|
|
82
|
+
</ResizeContainer>
|
|
73
83
|
);
|
|
74
84
|
};
|
|
75
85
|
|
|
@@ -182,11 +192,18 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
182
192
|
{activeTab === 'Insertions' && (
|
|
183
193
|
<CsvDownloadButton
|
|
184
194
|
className='mx-1 btn btn-xs'
|
|
185
|
-
getData={() =>
|
|
195
|
+
getData={() =>
|
|
196
|
+
getInsertionsTableData(filteredData.insertions).map((row) => {
|
|
197
|
+
return {
|
|
198
|
+
insertion: row.insertion.toString(),
|
|
199
|
+
count: row.count,
|
|
200
|
+
};
|
|
201
|
+
})
|
|
202
|
+
}
|
|
186
203
|
filename='insertions.csv'
|
|
187
204
|
/>
|
|
188
205
|
)}
|
|
189
|
-
<Info
|
|
206
|
+
<Info>Info for mutations</Info>
|
|
190
207
|
</div>
|
|
191
208
|
);
|
|
192
209
|
};
|
|
@@ -29,6 +29,8 @@ export default {
|
|
|
29
29
|
options: ['wilson'],
|
|
30
30
|
control: { type: 'check' },
|
|
31
31
|
},
|
|
32
|
+
size: [{ control: 'object' }],
|
|
33
|
+
headline: { control: 'text' },
|
|
32
34
|
},
|
|
33
35
|
};
|
|
34
36
|
|
|
@@ -42,6 +44,8 @@ const Template = {
|
|
|
42
44
|
smoothingWindow={args.smoothingWindow}
|
|
43
45
|
views={args.views}
|
|
44
46
|
confidenceIntervalMethods={args.confidenceIntervalMethods}
|
|
47
|
+
size={args.size}
|
|
48
|
+
headline={args.headline}
|
|
45
49
|
/>
|
|
46
50
|
</LapisUrlContext.Provider>
|
|
47
51
|
),
|
|
@@ -59,6 +63,8 @@ export const TwoVariants = {
|
|
|
59
63
|
smoothingWindow: 0,
|
|
60
64
|
views: ['bar', 'line', 'bubble', 'table'],
|
|
61
65
|
confidenceIntervalMethods: ['wilson'],
|
|
66
|
+
size: { width: '100%', height: '700px' },
|
|
67
|
+
headline: 'Prevalence over time',
|
|
62
68
|
},
|
|
63
69
|
parameters: {
|
|
64
70
|
fetchMock: {
|
|
@@ -124,6 +130,8 @@ export const OneVariant = {
|
|
|
124
130
|
smoothingWindow: 7,
|
|
125
131
|
views: ['bar', 'line', 'bubble', 'table'],
|
|
126
132
|
confidenceIntervalMethods: ['wilson'],
|
|
133
|
+
size: { width: '100%', height: '700px' },
|
|
134
|
+
headline: 'Prevalence over time',
|
|
127
135
|
},
|
|
128
136
|
parameters: {
|
|
129
137
|
fetchMock: {
|