@genspectrum/dashboard-components 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/custom-elements.json +453 -67
- package/dist/dashboard-components.js +778 -488
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +277 -50
- package/dist/style.css +132 -139
- package/package.json +9 -5
- package/src/lapisApi/lapisApi.ts +1 -1
- package/src/preact/aggregatedData/__mockData__/aggregated.json +585 -0
- package/src/preact/aggregatedData/aggregate-table.tsx +32 -0
- package/src/preact/aggregatedData/aggregate.stories.tsx +53 -0
- package/src/preact/aggregatedData/aggregate.tsx +102 -0
- package/src/preact/components/ReferenceGenomesAwaiter.tsx +25 -0
- package/src/preact/components/csv-download-button.tsx +8 -2
- package/src/preact/components/headline.tsx +16 -4
- 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 +57 -43
- 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 +8 -1
- package/src/preact/mutationComparison/mutation-comparison.tsx +13 -4
- 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 +8 -1
- package/src/preact/mutations/mutations.tsx +16 -5
- 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 +4 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +17 -9
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +8 -5
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +12 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +13 -8
- 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/query/queryAggregateData.spec.ts +32 -0
- package/src/query/queryAggregateData.ts +25 -0
- 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 +73 -0
- package/src/web-components/display/aggregate-component.tsx +58 -0
- package/src/web-components/display/index.ts +1 -0
- package/src/web-components/display/mutation-comparison-component.stories.ts +29 -11
- package/src/web-components/display/mutation-comparison-component.tsx +72 -4
- package/src/web-components/display/mutations-component.stories.ts +14 -13
- package/src/web-components/display/mutations-component.tsx +14 -1
- package/src/web-components/display/prevalence-over-time-component.stories.ts +20 -18
- package/src/web-components/display/prevalence-over-time-component.tsx +12 -0
- package/src/web-components/display/relative-growth-advantage-component.stories.ts +11 -10
- package/src/web-components/display/relative-growth-advantage-component.tsx +12 -0
- package/src/web-components/input/date-range-selector-component.stories.ts +35 -8
- package/src/web-components/input/date-range-selector-component.tsx +18 -5
- package/src/web-components/input/location-filter-component.stories.ts +17 -8
- package/src/web-components/input/location-filter-component.tsx +2 -6
- package/src/web-components/input/mutation-filter-component.stories.ts +20 -9
- package/src/web-components/input/mutation-filter-component.tsx +10 -2
- package/src/web-components/input/text-input-component.stories.ts +13 -4
- package/src/web-components/input/text-input-component.tsx +11 -2
- package/src/web-components/input/location-filter.mdx +0 -25
|
@@ -1,35 +1,53 @@
|
|
|
1
1
|
import { type MutationData } from './queryMutationData';
|
|
2
2
|
import { type Dataset } from '../../operator/Dataset';
|
|
3
|
+
import { type Deletion, type Substitution } from '../../utils/mutations';
|
|
3
4
|
import { type ProportionInterval } from '../components/proportion-selector';
|
|
4
5
|
|
|
5
6
|
type Proportions = {
|
|
6
7
|
[displayName: string]: number;
|
|
7
8
|
};
|
|
8
9
|
|
|
10
|
+
type MutationComparisonRow = {
|
|
11
|
+
mutation: Substitution | Deletion;
|
|
12
|
+
proportions: Proportions;
|
|
13
|
+
};
|
|
14
|
+
|
|
9
15
|
export function getMutationComparisonTableData(data: Dataset<MutationData>, proportionInterval: ProportionInterval) {
|
|
10
|
-
const mutationsToProportions = new Map<string,
|
|
16
|
+
const mutationsToProportions = new Map<string, MutationComparisonRow>();
|
|
11
17
|
|
|
12
18
|
for (const mutationData of data.content) {
|
|
13
19
|
for (const mutationEntry of mutationData.data) {
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
const mutationKey = mutationEntry.mutation.toString();
|
|
21
|
+
const existingRow = mutationsToProportions.get(mutationKey);
|
|
22
|
+
|
|
23
|
+
if (!existingRow) {
|
|
24
|
+
mutationsToProportions.set(
|
|
25
|
+
mutationKey,
|
|
26
|
+
initializeMutationRow(mutationEntry.mutation, mutationData.displayName, mutationEntry.proportion),
|
|
27
|
+
);
|
|
28
|
+
} else {
|
|
29
|
+
existingRow.proportions = updateProportions(
|
|
30
|
+
existingRow.proportions,
|
|
31
|
+
mutationData.displayName,
|
|
32
|
+
mutationEntry.proportion,
|
|
33
|
+
);
|
|
34
|
+
mutationsToProportions.set(mutationKey, existingRow);
|
|
35
|
+
}
|
|
18
36
|
}
|
|
19
37
|
}
|
|
20
38
|
|
|
21
|
-
return [...mutationsToProportions.
|
|
22
|
-
.map((
|
|
39
|
+
return [...mutationsToProportions.values()]
|
|
40
|
+
.map((row) => {
|
|
23
41
|
return {
|
|
24
|
-
mutation,
|
|
42
|
+
mutation: row.mutation,
|
|
25
43
|
...data.content
|
|
26
44
|
.map((mutationData) => {
|
|
27
45
|
return {
|
|
28
|
-
[`${mutationData.displayName} prevalence`]: proportions[mutationData.displayName] || 0,
|
|
46
|
+
[`${mutationData.displayName} prevalence`]: row.proportions[mutationData.displayName] || 0,
|
|
29
47
|
};
|
|
30
48
|
})
|
|
31
49
|
.reduce((acc, val) => ({ ...acc, ...val }), {}),
|
|
32
|
-
} as { mutation:
|
|
50
|
+
} as { mutation: Substitution | Deletion } & Proportions;
|
|
33
51
|
})
|
|
34
52
|
.filter((row) =>
|
|
35
53
|
Object.values(row).some(
|
|
@@ -38,3 +56,20 @@ export function getMutationComparisonTableData(data: Dataset<MutationData>, prop
|
|
|
38
56
|
),
|
|
39
57
|
);
|
|
40
58
|
}
|
|
59
|
+
|
|
60
|
+
function initializeMutationRow(
|
|
61
|
+
mutation: Substitution | Deletion,
|
|
62
|
+
displayName: string,
|
|
63
|
+
proportion: number,
|
|
64
|
+
): MutationComparisonRow {
|
|
65
|
+
return {
|
|
66
|
+
mutation,
|
|
67
|
+
proportions: {
|
|
68
|
+
[displayName]: proportion,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function updateProportions(proportions: Proportions, displayName: string, proportion: number): Proportions {
|
|
74
|
+
return { ...proportions, [displayName]: proportion };
|
|
75
|
+
}
|
|
@@ -3,6 +3,7 @@ import { type FunctionComponent } from 'preact';
|
|
|
3
3
|
import { getMutationComparisonTableData } from './getMutationComparisonTableData';
|
|
4
4
|
import { type MutationData } from './queryMutationData';
|
|
5
5
|
import { type Dataset } from '../../operator/Dataset';
|
|
6
|
+
import { type Deletion, type Substitution } from '../../utils/mutations';
|
|
6
7
|
import { type ProportionInterval } from '../components/proportion-selector';
|
|
7
8
|
import { Table } from '../components/table';
|
|
8
9
|
import { sortSubstitutionsAndDeletions } from '../shared/sort/sortSubstitutionsAndDeletions';
|
|
@@ -14,30 +15,27 @@ export interface MutationsTableProps {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export const MutationComparisonTable: FunctionComponent<MutationsTableProps> = ({ data, proportionInterval }) => {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
compare: (a: string, b: string) => {
|
|
23
|
-
return sortSubstitutionsAndDeletions(a, b);
|
|
24
|
-
},
|
|
25
|
-
},
|
|
18
|
+
const headers = [
|
|
19
|
+
{
|
|
20
|
+
name: 'Mutation',
|
|
21
|
+
sort: {
|
|
22
|
+
compare: sortSubstitutionsAndDeletions,
|
|
26
23
|
},
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
formatter: (cell: Substitution | Deletion) => cell.toString(),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'Prevalence',
|
|
28
|
+
columns: data.content.map((mutationData) => {
|
|
29
|
+
return {
|
|
30
|
+
name: mutationData.displayName,
|
|
31
|
+
sort: true,
|
|
32
|
+
formatter: (cell: number) => formatProportion(cell),
|
|
33
|
+
};
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
];
|
|
39
37
|
|
|
40
38
|
const tableData = getMutationComparisonTableData(data, proportionInterval).map((row) => Object.values(row));
|
|
41
39
|
|
|
42
|
-
return <Table data={tableData} columns={
|
|
40
|
+
return <Table data={tableData} columns={headers} pagination={true} />;
|
|
43
41
|
};
|
|
@@ -64,6 +64,7 @@ export const MutationComparisonVenn: FunctionComponent<MutationComparisonVennPro
|
|
|
64
64
|
type: 'venn',
|
|
65
65
|
data: sets,
|
|
66
66
|
options: {
|
|
67
|
+
maintainAspectRatio: false,
|
|
67
68
|
scales: {
|
|
68
69
|
x: {
|
|
69
70
|
ticks: {
|
|
@@ -114,9 +115,11 @@ export const MutationComparisonVenn: FunctionComponent<MutationComparisonVennPro
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
return (
|
|
117
|
-
|
|
118
|
-
<
|
|
118
|
+
<div className='h-full flex flex-col'>
|
|
119
|
+
<div className='flex-1'>
|
|
120
|
+
<GsChart configuration={config} />
|
|
121
|
+
</div>
|
|
119
122
|
<div class='flex flex-wrap break-words m-2' ref={divRef} />
|
|
120
|
-
|
|
123
|
+
</div>
|
|
121
124
|
);
|
|
122
125
|
};
|
|
@@ -27,6 +27,7 @@ const meta: Meta<MutationComparisonProps> = {
|
|
|
27
27
|
options: ['table', 'venn'],
|
|
28
28
|
control: { type: 'check' },
|
|
29
29
|
},
|
|
30
|
+
size: [{ control: 'object' }],
|
|
30
31
|
},
|
|
31
32
|
parameters: {
|
|
32
33
|
fetchMock: {
|
|
@@ -75,7 +76,12 @@ const Template: StoryObj<MutationComparisonProps> = {
|
|
|
75
76
|
render: (args) => (
|
|
76
77
|
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
77
78
|
<ReferenceGenomeContext.Provider value={referenceGenome}>
|
|
78
|
-
<MutationComparison
|
|
79
|
+
<MutationComparison
|
|
80
|
+
variants={args.variants}
|
|
81
|
+
sequenceType={args.sequenceType}
|
|
82
|
+
views={args.views}
|
|
83
|
+
size={args.size}
|
|
84
|
+
/>
|
|
79
85
|
</ReferenceGenomeContext.Provider>
|
|
80
86
|
</LapisUrlContext.Provider>
|
|
81
87
|
),
|
|
@@ -101,6 +107,7 @@ export const TwoVariants: StoryObj<MutationComparisonProps> = {
|
|
|
101
107
|
],
|
|
102
108
|
sequenceType: 'nucleotide',
|
|
103
109
|
views: ['table', 'venn'],
|
|
110
|
+
size: { width: '100%', height: '700px' },
|
|
104
111
|
},
|
|
105
112
|
};
|
|
106
113
|
|
|
@@ -17,6 +17,7 @@ import { type DisplayedMutationType, MutationTypeSelector } from '../components/
|
|
|
17
17
|
import { NoDataDisplay } from '../components/no-data-display';
|
|
18
18
|
import { type ProportionInterval } from '../components/proportion-selector';
|
|
19
19
|
import { ProportionSelectorDropdown } from '../components/proportion-selector-dropdown';
|
|
20
|
+
import { ResizeContainer, type Size } from '../components/resize-container';
|
|
20
21
|
import Tabs from '../components/tabs';
|
|
21
22
|
import { useQuery } from '../useQuery';
|
|
22
23
|
|
|
@@ -31,9 +32,15 @@ export interface MutationComparisonProps {
|
|
|
31
32
|
variants: MutationComparisonVariant[];
|
|
32
33
|
sequenceType: SequenceType;
|
|
33
34
|
views: View[];
|
|
35
|
+
size?: Size;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
export const MutationComparison: FunctionComponent<MutationComparisonProps> = ({
|
|
38
|
+
export const MutationComparison: FunctionComponent<MutationComparisonProps> = ({
|
|
39
|
+
variants,
|
|
40
|
+
sequenceType,
|
|
41
|
+
views,
|
|
42
|
+
size,
|
|
43
|
+
}) => {
|
|
37
44
|
const lapis = useContext(LapisUrlContext);
|
|
38
45
|
|
|
39
46
|
const { data, error, isLoading } = useQuery(async () => {
|
|
@@ -67,9 +74,11 @@ export const MutationComparison: FunctionComponent<MutationComparisonProps> = ({
|
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
return (
|
|
70
|
-
<
|
|
71
|
-
<
|
|
72
|
-
|
|
77
|
+
<ResizeContainer size={size} defaultSize={{ height: '700px', width: '100%' }}>
|
|
78
|
+
<Headline heading={headline}>
|
|
79
|
+
<MutationComparisonTabs data={data.mutationData} sequenceType={sequenceType} views={views} />
|
|
80
|
+
</Headline>
|
|
81
|
+
</ResizeContainer>
|
|
73
82
|
);
|
|
74
83
|
};
|
|
75
84
|
|
|
@@ -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 className='mx-1' content='Info for mutation filter' />
|
|
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',
|