@genspectrum/dashboard-components 0.18.1 → 0.18.3
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 +9 -9
- package/dist/{LineageFilterChangedEvent-DkvWdq_G.js → LineageFilterChangedEvent-ixHQkq8y.js} +2 -2
- package/dist/{LineageFilterChangedEvent-DkvWdq_G.js.map → LineageFilterChangedEvent-ixHQkq8y.js.map} +1 -1
- package/dist/assets/{mutationOverTimeWorker-ChQTFL68.js.map → mutationOverTimeWorker--b8ZHlji.js.map} +1 -1
- package/dist/components.d.ts +22 -291
- package/dist/components.js +220 -124
- package/dist/components.js.map +1 -1
- package/dist/style.css +2 -2
- package/dist/util.d.ts +25 -292
- package/dist/util.js +1 -1
- package/package.json +3 -3
- package/src/preact/MutationAnnotationsContext.spec.tsx +103 -34
- package/src/preact/MutationAnnotationsContext.tsx +49 -7
- package/src/preact/components/annotated-mutation.stories.tsx +0 -5
- package/src/preact/components/annotated-mutation.tsx +6 -2
- package/src/preact/dateRangeFilter/computeInitialValues.spec.ts +2 -2
- package/src/preact/dateRangeFilter/computeInitialValues.ts +1 -1
- package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +3 -5
- package/src/preact/dateRangeFilter/date-range-filter.tsx +12 -7
- package/src/preact/dateRangeFilter/dateRangeOption.ts +1 -1
- package/src/preact/mutationComparison/mutation-comparison.stories.tsx +3 -1
- package/src/preact/mutations/mutations.stories.tsx +4 -1
- package/src/preact/mutationsOverTime/mutations-over-time-grid-tooltip.stories.tsx +108 -0
- package/src/preact/mutationsOverTime/mutations-over-time-grid-tooltip.tsx +93 -0
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +7 -78
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +3 -1
- package/src/preact/sequencesByLocation/leafletStyleModifications.css +5 -0
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +25 -0
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +65 -13
- package/src/web-components/MutationAnnotations.mdx +8 -0
- package/src/web-components/gs-app.stories.ts +2 -0
- package/src/web-components/gs-app.ts +4 -2
- package/src/web-components/input/gs-date-range-filter.stories.ts +1 -1
- package/src/web-components/input/gs-date-range-filter.tsx +7 -5
- package/src/web-components/mutation-annotations-context.ts +6 -2
- package/standalone-bundle/assets/mutationOverTimeWorker-jChgWnwp.js.map +1 -1
- package/standalone-bundle/dashboard-components.js +5778 -5707
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
|
@@ -45,7 +45,7 @@ const AnnotatedMutationWithoutContext: FunctionComponent<AnnotatedMutationWithou
|
|
|
45
45
|
annotationsProvider,
|
|
46
46
|
modalRef,
|
|
47
47
|
}) => {
|
|
48
|
-
const mutationAnnotations = annotationsProvider(mutation
|
|
48
|
+
const mutationAnnotations = annotationsProvider(mutation, sequenceType);
|
|
49
49
|
|
|
50
50
|
if (mutationAnnotations === undefined || mutationAnnotations.length === 0) {
|
|
51
51
|
return mutation.code;
|
|
@@ -67,7 +67,11 @@ const AnnotatedMutationWithoutContext: FunctionComponent<AnnotatedMutationWithou
|
|
|
67
67
|
);
|
|
68
68
|
|
|
69
69
|
return (
|
|
70
|
-
<ButtonWithModalDialog
|
|
70
|
+
<ButtonWithModalDialog
|
|
71
|
+
buttonClassName={'select-text cursor-pointer'}
|
|
72
|
+
modalContent={modalContent}
|
|
73
|
+
modalRef={modalRef}
|
|
74
|
+
>
|
|
71
75
|
{mutation.code}
|
|
72
76
|
<sup>
|
|
73
77
|
{mutationAnnotations
|
|
@@ -18,8 +18,8 @@ const dateRangeOptions = [
|
|
|
18
18
|
];
|
|
19
19
|
|
|
20
20
|
describe('computeInitialValues', () => {
|
|
21
|
-
it('should return undefined for
|
|
22
|
-
const result = computeInitialValues(
|
|
21
|
+
it('should return undefined for null value', () => {
|
|
22
|
+
const result = computeInitialValues(null, earliestDate, dateRangeOptions);
|
|
23
23
|
|
|
24
24
|
expect(result).toBeUndefined();
|
|
25
25
|
});
|
|
@@ -3,7 +3,7 @@ import { getDatesForSelectorValue, getSelectableOptions } from './selectableOpti
|
|
|
3
3
|
import { UserFacingError } from '../components/error-display';
|
|
4
4
|
|
|
5
5
|
export function computeInitialValues(value: DateRangeValue, earliestDate: string, dateRangeOptions: DateRangeOption[]) {
|
|
6
|
-
if (value ===
|
|
6
|
+
if (value === null) {
|
|
7
7
|
return undefined;
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -56,7 +56,7 @@ const meta: Meta<DateRangeFilterProps> = {
|
|
|
56
56
|
args: {
|
|
57
57
|
dateRangeOptions: [dateRangeOptionPresets.lastMonth, dateRangeOptionPresets.allTimes, customDateRange],
|
|
58
58
|
earliestDate,
|
|
59
|
-
value:
|
|
59
|
+
value: null,
|
|
60
60
|
lapisDateField: 'aDateColumn',
|
|
61
61
|
width: '100%',
|
|
62
62
|
placeholder,
|
|
@@ -194,16 +194,14 @@ export const ChangingTheValueProgrammatically: StoryObj<DateRangeFilterProps> =
|
|
|
194
194
|
...Primary,
|
|
195
195
|
render: (args) => {
|
|
196
196
|
const StatefulWrapper = () => {
|
|
197
|
-
const [value, setValue] = useState<DateRangeValue
|
|
197
|
+
const [value, setValue] = useState<DateRangeValue>('Last month');
|
|
198
198
|
const ref = useRef<HTMLDivElement>(null);
|
|
199
199
|
|
|
200
200
|
useEffect(() => {
|
|
201
201
|
ref.current?.addEventListener('gs-date-range-option-changed', (event) => {
|
|
202
|
-
|
|
203
|
-
setValue(newValue ?? undefined);
|
|
202
|
+
setValue(event.detail);
|
|
204
203
|
});
|
|
205
204
|
}, []);
|
|
206
|
-
|
|
207
205
|
return (
|
|
208
206
|
<div ref={ref}>
|
|
209
207
|
<LapisUrlContextProvider value={LAPIS_URL}>
|
|
@@ -179,13 +179,18 @@ export const DateRangeFilterInner = ({
|
|
|
179
179
|
};
|
|
180
180
|
|
|
181
181
|
const fireOptionChangedEvent = (state: DateRangeFilterState) => {
|
|
182
|
-
const eventDetail =
|
|
183
|
-
state
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
182
|
+
const eventDetail = (() => {
|
|
183
|
+
if (state === null) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (state.label === customOption) {
|
|
187
|
+
return {
|
|
188
|
+
dateFrom: state.dateFrom !== undefined ? toYYYYMMDD(state.dateFrom) : undefined,
|
|
189
|
+
dateTo: state.dateTo !== undefined ? toYYYYMMDD(state.dateTo) : undefined,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return state.label;
|
|
193
|
+
})();
|
|
189
194
|
|
|
190
195
|
divRef.current?.dispatchEvent(new DateRangeOptionChangedEvent(eventDetail));
|
|
191
196
|
};
|
|
@@ -6,6 +6,7 @@ import nucleotideMutationsSomeDataset from './__mockData__/nucleotideMutationsSo
|
|
|
6
6
|
import { MutationComparison, type MutationComparisonProps } from './mutation-comparison';
|
|
7
7
|
import { LAPIS_URL, NUCLEOTIDE_MUTATIONS_ENDPOINT } from '../../constants';
|
|
8
8
|
import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
|
|
9
|
+
import { type MutationAnnotations } from '../../web-components/mutation-annotations-context';
|
|
9
10
|
import { LapisUrlContextProvider } from '../LapisUrlContext';
|
|
10
11
|
import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
|
|
11
12
|
import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
@@ -90,8 +91,9 @@ const mutationAnnotations = [
|
|
|
90
91
|
symbol: '+',
|
|
91
92
|
nucleotideMutations: ['C3037T', 'A23403G'],
|
|
92
93
|
aminoAcidMutations: ['ORF1a:I2230T'],
|
|
94
|
+
aminoAcidPositions: ['ORF1a:3675'],
|
|
93
95
|
},
|
|
94
|
-
];
|
|
96
|
+
] satisfies MutationAnnotations;
|
|
95
97
|
|
|
96
98
|
const Template: StoryObj<MutationComparisonProps> = {
|
|
97
99
|
render: (args) => (
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
|
|
14
14
|
import baselineNucleotideMutations from '../../preact/mutations/__mockData__/baselineNucleotideMutations.json';
|
|
15
15
|
import overallVariantCount from '../../preact/mutations/__mockData__/overallVariantCount.json';
|
|
16
|
+
import { type MutationAnnotations } from '../../web-components/mutation-annotations-context';
|
|
16
17
|
import { LapisUrlContextProvider } from '../LapisUrlContext';
|
|
17
18
|
import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
|
|
18
19
|
import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
@@ -45,6 +46,7 @@ const mutationAnnotations = [
|
|
|
45
46
|
description: 'This describes what is special about these mutations.',
|
|
46
47
|
symbol: '#',
|
|
47
48
|
nucleotideMutations: ['C241T', 'C3037T'],
|
|
49
|
+
nucleotidePositions: [],
|
|
48
50
|
aminoAcidMutations: ['N:G204R', 'N:S235F'],
|
|
49
51
|
},
|
|
50
52
|
{
|
|
@@ -52,9 +54,10 @@ const mutationAnnotations = [
|
|
|
52
54
|
description: 'This describes what is special about these other mutations.',
|
|
53
55
|
symbol: '+',
|
|
54
56
|
nucleotideMutations: ['C3037T', 'C11750T'],
|
|
57
|
+
nucleotidePositions: ['14408'],
|
|
55
58
|
aminoAcidMutations: ['ORF1a:S2255F'],
|
|
56
59
|
},
|
|
57
|
-
];
|
|
60
|
+
] satisfies MutationAnnotations;
|
|
58
61
|
|
|
59
62
|
const Template = {
|
|
60
63
|
render: (args: MutationsProps) => (
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { expect, within } from '@storybook/test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
MutationsOverTimeGridTooltip,
|
|
6
|
+
type MutationsOverTimeGridTooltipProps,
|
|
7
|
+
} from './mutations-over-time-grid-tooltip';
|
|
8
|
+
|
|
9
|
+
const meta: Meta<MutationsOverTimeGridTooltipProps> = {
|
|
10
|
+
title: 'Component/Mutation over time grid tooltip',
|
|
11
|
+
component: MutationsOverTimeGridTooltip,
|
|
12
|
+
argTypes: {
|
|
13
|
+
mutation: { control: 'object' },
|
|
14
|
+
date: { control: 'object' },
|
|
15
|
+
value: { control: 'object' },
|
|
16
|
+
},
|
|
17
|
+
parameters: {
|
|
18
|
+
fetchMock: {},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
|
|
24
|
+
const Template: StoryObj<MutationsOverTimeGridTooltipProps> = {
|
|
25
|
+
render: (args: MutationsOverTimeGridTooltipProps) => <MutationsOverTimeGridTooltip {...args} />,
|
|
26
|
+
args: {
|
|
27
|
+
mutation: {
|
|
28
|
+
type: 'deletion',
|
|
29
|
+
position: 500,
|
|
30
|
+
code: 'A500-',
|
|
31
|
+
valueAtReference: 'A',
|
|
32
|
+
},
|
|
33
|
+
date: {
|
|
34
|
+
type: 'Year',
|
|
35
|
+
year: 2025,
|
|
36
|
+
dateString: '2025',
|
|
37
|
+
},
|
|
38
|
+
value: null,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const NoValue: StoryObj<MutationsOverTimeGridTooltipProps> = {
|
|
43
|
+
...Template,
|
|
44
|
+
play: async ({ canvasElement }) => {
|
|
45
|
+
const canvas = within(canvasElement);
|
|
46
|
+
|
|
47
|
+
await expect(canvas.getByText('2025', { exact: true })).toBeVisible();
|
|
48
|
+
await expect(canvas.getByText('(2025-01-01 - 2025-12-31)')).toBeVisible();
|
|
49
|
+
await expect(canvas.getByText('A500-')).toBeVisible();
|
|
50
|
+
await expect(canvas.getByText('No data')).toBeVisible();
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const WithValue: StoryObj<MutationsOverTimeGridTooltipProps> = {
|
|
55
|
+
...Template,
|
|
56
|
+
args: {
|
|
57
|
+
...Template.args,
|
|
58
|
+
value: {
|
|
59
|
+
type: 'value',
|
|
60
|
+
proportion: 0.5,
|
|
61
|
+
count: 100,
|
|
62
|
+
totalCount: 300,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
play: async ({ canvasElement }) => {
|
|
66
|
+
const canvas = within(canvasElement);
|
|
67
|
+
|
|
68
|
+
await expect(canvas.getByText('Proportion: 50.00%')).toBeVisible();
|
|
69
|
+
await expect(canvas.getByText('300 samples are in the timeframe')).toBeVisible();
|
|
70
|
+
await expect(canvas.getByText('200 have coverage, of those 100 have the mutation')).toBeVisible();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const WithValueBelowThreshold: StoryObj<MutationsOverTimeGridTooltipProps> = {
|
|
75
|
+
...Template,
|
|
76
|
+
args: {
|
|
77
|
+
...Template.args,
|
|
78
|
+
value: {
|
|
79
|
+
type: 'belowThreshold',
|
|
80
|
+
totalCount: 300,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
play: async ({ canvasElement }) => {
|
|
84
|
+
const canvas = within(canvasElement);
|
|
85
|
+
|
|
86
|
+
await expect(canvas.getByText('Proportion: <0.10%')).toBeVisible();
|
|
87
|
+
await expect(canvas.getByText('300 samples are in the timeframe')).toBeVisible();
|
|
88
|
+
await expect(canvas.getByText('none or less than 0.10% have the mutation')).toBeVisible();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const WithWastewaterValue: StoryObj<MutationsOverTimeGridTooltipProps> = {
|
|
93
|
+
...Template,
|
|
94
|
+
args: {
|
|
95
|
+
...Template.args,
|
|
96
|
+
value: {
|
|
97
|
+
type: 'wastewaterValue',
|
|
98
|
+
proportion: 0.5,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
play: async ({ canvasElement }) => {
|
|
102
|
+
const canvas = within(canvasElement);
|
|
103
|
+
|
|
104
|
+
await expect(canvas.getByText('Proportion: 50.00%')).toBeVisible();
|
|
105
|
+
await expect(canvas.queryByText('samples are in the timeframe')).not.toBeInTheDocument();
|
|
106
|
+
await expect(canvas.queryByText('have coverage')).not.toBeInTheDocument();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { FunctionComponent } from 'preact';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type MutationOverTimeMutationValue,
|
|
5
|
+
MUTATIONS_OVER_TIME_MIN_PROPORTION,
|
|
6
|
+
} from '../../query/queryMutationsOverTime';
|
|
7
|
+
import type { Deletion, Substitution } from '../../utils/mutations';
|
|
8
|
+
import { type Temporal, type TemporalClass, toTemporalClass, YearMonthDayClass } from '../../utils/temporalClass';
|
|
9
|
+
import { formatProportion } from '../shared/table/formatProportion';
|
|
10
|
+
|
|
11
|
+
export type MutationsOverTimeGridTooltipProps = {
|
|
12
|
+
mutation: Substitution | Deletion;
|
|
13
|
+
date: Temporal;
|
|
14
|
+
value: MutationOverTimeMutationValue;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const MutationsOverTimeGridTooltip: FunctionComponent<MutationsOverTimeGridTooltipProps> = ({
|
|
18
|
+
mutation,
|
|
19
|
+
date,
|
|
20
|
+
value,
|
|
21
|
+
}: MutationsOverTimeGridTooltipProps) => {
|
|
22
|
+
const dateClass = toTemporalClass(date);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
<p>
|
|
27
|
+
<span className='font-bold'>{dateClass.englishName()}</span>
|
|
28
|
+
</p>
|
|
29
|
+
<p>({timeIntervalDisplay(dateClass)})</p>
|
|
30
|
+
<p>{mutation.code}</p>
|
|
31
|
+
<TooltipValueDescription value={value} />
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const TooltipValueDescription: FunctionComponent<{ value: MutationOverTimeMutationValue }> = ({ value }) => {
|
|
37
|
+
if (value === null) {
|
|
38
|
+
return <p>No data</p>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const proportion =
|
|
42
|
+
value.type === 'belowThreshold'
|
|
43
|
+
? `<${formatProportion(MUTATIONS_OVER_TIME_MIN_PROPORTION)}`
|
|
44
|
+
: formatProportion(value.proportion);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
<p>Proportion: {proportion}</p>
|
|
49
|
+
<TooltipValueCountsDescription value={value} />
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const TooltipValueCountsDescription: FunctionComponent<{
|
|
55
|
+
value: NonNullable<MutationOverTimeMutationValue>;
|
|
56
|
+
}> = ({ value }) => {
|
|
57
|
+
switch (value.type) {
|
|
58
|
+
case 'wastewaterValue':
|
|
59
|
+
return;
|
|
60
|
+
case 'belowThreshold':
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<p>{value.totalCount} samples are in the timeframe</p>
|
|
64
|
+
<p>none or less than {formatProportion(MUTATIONS_OVER_TIME_MIN_PROPORTION)} have the mutation</p>
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
67
|
+
case 'value':
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<p>{value.totalCount} samples are in the timeframe</p>
|
|
71
|
+
<p>
|
|
72
|
+
{totalCountWithCoverage(value.count, value.proportion)} have coverage, of those {value.count}{' '}
|
|
73
|
+
have the mutation
|
|
74
|
+
</p>
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function totalCountWithCoverage(count: number, proportion: number) {
|
|
81
|
+
if (count === 0) {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
return Math.round(count / proportion);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const timeIntervalDisplay = (date: TemporalClass) => {
|
|
88
|
+
if (date instanceof YearMonthDayClass) {
|
|
89
|
+
return date.toString();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return `${date.firstDay.toString()} - ${date.lastDay.toString()}`;
|
|
93
|
+
};
|
|
@@ -3,13 +3,11 @@ import { type FunctionComponent } from 'preact';
|
|
|
3
3
|
import { useMemo, useState } from 'preact/hooks';
|
|
4
4
|
|
|
5
5
|
import { type MutationOverTimeDataMap } from './MutationOverTimeData';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
MUTATIONS_OVER_TIME_MIN_PROPORTION,
|
|
9
|
-
} from '../../query/queryMutationsOverTime';
|
|
6
|
+
import { MutationsOverTimeGridTooltip } from './mutations-over-time-grid-tooltip';
|
|
7
|
+
import { type MutationOverTimeMutationValue } from '../../query/queryMutationsOverTime';
|
|
10
8
|
import { type SequenceType } from '../../types';
|
|
11
9
|
import { type Deletion, type Substitution } from '../../utils/mutations';
|
|
12
|
-
import { type Temporal
|
|
10
|
+
import { type Temporal } from '../../utils/temporalClass';
|
|
13
11
|
import { AnnotatedMutation } from '../components/annotated-mutation';
|
|
14
12
|
import { type ColorScale, getColorWithinScale, getTextColorForScale } from '../components/color-scale-selector';
|
|
15
13
|
import Tooltip, { type TooltipPosition } from '../components/tooltip';
|
|
@@ -181,24 +179,14 @@ const ProportionCell: FunctionComponent<{
|
|
|
181
179
|
tooltipPosition: TooltipPosition;
|
|
182
180
|
colorScale: ColorScale;
|
|
183
181
|
}> = ({ value, mutation, date, tooltipPosition, colorScale }) => {
|
|
184
|
-
const dateClass = toTemporalClass(date);
|
|
185
|
-
|
|
186
|
-
const tooltipContent = (
|
|
187
|
-
<div>
|
|
188
|
-
<p>
|
|
189
|
-
<span className='font-bold'>{dateClass.englishName()}</span>
|
|
190
|
-
</p>
|
|
191
|
-
<p>({timeIntervalDisplay(dateClass)})</p>
|
|
192
|
-
<p>{mutation.code}</p>
|
|
193
|
-
<TooltipValueDescription value={value} />
|
|
194
|
-
</div>
|
|
195
|
-
);
|
|
196
|
-
|
|
197
182
|
const proportion = value?.type === 'belowThreshold' ? 0 : value?.proportion;
|
|
198
183
|
|
|
199
184
|
return (
|
|
200
185
|
<div className={'py-1 w-full h-full'}>
|
|
201
|
-
<Tooltip
|
|
186
|
+
<Tooltip
|
|
187
|
+
content={<MutationsOverTimeGridTooltip mutation={mutation} date={date} value={value} />}
|
|
188
|
+
position={tooltipPosition}
|
|
189
|
+
>
|
|
202
190
|
<div
|
|
203
191
|
style={{
|
|
204
192
|
backgroundColor: getColorWithinScale(proportion, colorScale),
|
|
@@ -217,63 +205,4 @@ const ProportionCell: FunctionComponent<{
|
|
|
217
205
|
);
|
|
218
206
|
};
|
|
219
207
|
|
|
220
|
-
const TooltipValueDescription: FunctionComponent<{ value: MutationOverTimeMutationValue }> = ({ value }) => {
|
|
221
|
-
if (value === null) {
|
|
222
|
-
return <p>No data</p>;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const proportion =
|
|
226
|
-
value.type === 'belowThreshold'
|
|
227
|
-
? `<${formatProportion(MUTATIONS_OVER_TIME_MIN_PROPORTION)}`
|
|
228
|
-
: formatProportion(value.proportion);
|
|
229
|
-
|
|
230
|
-
return (
|
|
231
|
-
<>
|
|
232
|
-
<p>Proportion: {proportion}</p>
|
|
233
|
-
<TooltipValueCountsDescription value={value} />
|
|
234
|
-
</>
|
|
235
|
-
);
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
const TooltipValueCountsDescription: FunctionComponent<{
|
|
239
|
-
value: NonNullable<MutationOverTimeMutationValue>;
|
|
240
|
-
}> = ({ value }) => {
|
|
241
|
-
switch (value.type) {
|
|
242
|
-
case 'wastewaterValue':
|
|
243
|
-
return;
|
|
244
|
-
case 'belowThreshold':
|
|
245
|
-
return (
|
|
246
|
-
<>
|
|
247
|
-
<p>{value.totalCount} samples are in the timeframe</p>
|
|
248
|
-
<p>none or less than {formatProportion(MUTATIONS_OVER_TIME_MIN_PROPORTION)} have the mutation</p>
|
|
249
|
-
</>
|
|
250
|
-
);
|
|
251
|
-
case 'value':
|
|
252
|
-
return (
|
|
253
|
-
<>
|
|
254
|
-
<p>{value.totalCount} samples are in the timeframe</p>
|
|
255
|
-
<p>
|
|
256
|
-
{totalCountWithCoverage(value.count, value.proportion)} have coverage, of those {value.count}{' '}
|
|
257
|
-
have the mutation
|
|
258
|
-
</p>
|
|
259
|
-
</>
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
function totalCountWithCoverage(count: number, proportion: number) {
|
|
265
|
-
if (count === 0) {
|
|
266
|
-
return 0;
|
|
267
|
-
}
|
|
268
|
-
return Math.round(count / proportion);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const timeIntervalDisplay = (date: TemporalClass) => {
|
|
272
|
-
if (date instanceof YearMonthDayClass) {
|
|
273
|
-
return date.toString();
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return `${date.firstDay.toString()} - ${date.lastDay.toString()}`;
|
|
277
|
-
};
|
|
278
|
-
|
|
279
208
|
export default MutationsOverTimeGrid;
|
|
@@ -5,6 +5,7 @@ import { type Canvas } from '@storybook/types';
|
|
|
5
5
|
import { MutationsOverTime, type MutationsOverTimeProps } from './mutations-over-time';
|
|
6
6
|
import { LAPIS_URL } from '../../constants';
|
|
7
7
|
import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
|
|
8
|
+
import { type MutationAnnotations } from '../../web-components/mutation-annotations-context';
|
|
8
9
|
import { LapisUrlContextProvider } from '../LapisUrlContext';
|
|
9
10
|
import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
|
|
10
11
|
import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
@@ -49,6 +50,7 @@ const mutationAnnotations = [
|
|
|
49
50
|
symbol: '#',
|
|
50
51
|
nucleotideMutations: ['C44T', 'C774T', 'G24872T', 'T23011-'],
|
|
51
52
|
aminoAcidMutations: ['S:501Y', 'S:S31-', 'ORF1a:S4286C'],
|
|
53
|
+
nucleotidePositions: ['17334'],
|
|
52
54
|
},
|
|
53
55
|
{
|
|
54
56
|
name: 'I am another mutation annotation!',
|
|
@@ -57,7 +59,7 @@ const mutationAnnotations = [
|
|
|
57
59
|
nucleotideMutations: ['C44T', 'A13121T'],
|
|
58
60
|
aminoAcidMutations: ['S:501Y', 'S:S31-', 'ORF1a:S4286C'],
|
|
59
61
|
},
|
|
60
|
-
];
|
|
62
|
+
] satisfies MutationAnnotations;
|
|
61
63
|
|
|
62
64
|
export const Default: StoryObj<MutationsOverTimeProps> = {
|
|
63
65
|
render: (args: MutationsOverTimeProps) => (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { expect } from '@storybook/test';
|
|
2
3
|
|
|
3
4
|
import { WastewaterMutationsOverTime, type WastewaterMutationsOverTimeProps } from './wastewater-mutations-over-time';
|
|
4
5
|
import { WISE_DETAILS_ENDPOINT, WISE_LAPIS_URL } from '../../../constants';
|
|
@@ -65,3 +66,27 @@ export const Default: StoryObj<WastewaterMutationsOverTimeProps> = {
|
|
|
65
66
|
},
|
|
66
67
|
},
|
|
67
68
|
};
|
|
69
|
+
|
|
70
|
+
export const AminoAcids: StoryObj<WastewaterMutationsOverTimeProps> = {
|
|
71
|
+
...Default,
|
|
72
|
+
args: {
|
|
73
|
+
...Default.args,
|
|
74
|
+
sequenceType: 'amino acid',
|
|
75
|
+
},
|
|
76
|
+
play: async ({ canvas, step }) => {
|
|
77
|
+
await step('Wait for component to render', async () => {
|
|
78
|
+
await canvas.findByText('All segments');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await step("Click 'All segments' button", async () => {
|
|
82
|
+
canvas.getByRole('button', { name: 'All segments' }).click();
|
|
83
|
+
await expect(canvas.getByText('Select none')).toBeInTheDocument();
|
|
84
|
+
canvas.getByRole('button', { name: 'Select none' }).click();
|
|
85
|
+
await canvas.findAllByText('No data available for your filters.');
|
|
86
|
+
canvas.getByRole('checkbox', { name: 'S' }).click();
|
|
87
|
+
await canvas.findAllByText('S:Q493E');
|
|
88
|
+
const element = canvas.queryByText(/ORF1a:/);
|
|
89
|
+
await expect(element).not.toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
|
-
import { type Dispatch, type StateUpdater, useState } from 'preact/hooks';
|
|
2
|
+
import { type Dispatch, type StateUpdater, useMemo, useState } from 'preact/hooks';
|
|
3
3
|
import z from 'zod';
|
|
4
4
|
|
|
5
5
|
import { computeWastewaterMutationsOverTimeDataPerLocation } from './computeWastewaterMutationsOverTimeDataPerLocation';
|
|
6
6
|
import { lapisFilterSchema, sequenceTypeSchema } from '../../../types';
|
|
7
|
+
import { Map2dView } from '../../../utils/map2d';
|
|
7
8
|
import { useLapisUrl } from '../../LapisUrlContext';
|
|
8
9
|
import { type ColorScale } from '../../components/color-scale-selector';
|
|
9
10
|
import { ColorScaleSelectorDropdown } from '../../components/color-scale-selector-dropdown';
|
|
@@ -13,6 +14,7 @@ import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../../com
|
|
|
13
14
|
import { LoadingDisplay } from '../../components/loading-display';
|
|
14
15
|
import { NoDataDisplay } from '../../components/no-data-display';
|
|
15
16
|
import { ResizeContainer } from '../../components/resize-container';
|
|
17
|
+
import { type DisplayedSegment, SegmentSelector } from '../../components/segment-selector';
|
|
16
18
|
import Tabs from '../../components/tabs';
|
|
17
19
|
import { type MutationOverTimeDataMap } from '../../mutationsOverTime/MutationOverTimeData';
|
|
18
20
|
import MutationsOverTimeGrid from '../../mutationsOverTime/mutations-over-time-grid';
|
|
@@ -86,28 +88,67 @@ type MutationOverTimeDataPerLocation = {
|
|
|
86
88
|
data: MutationOverTimeDataMap;
|
|
87
89
|
}[];
|
|
88
90
|
|
|
91
|
+
function useDisplayedSegments(mutations: MutationOverTimeDataPerLocation) {
|
|
92
|
+
const displayedSegments = useMemo(() => {
|
|
93
|
+
const unique = [
|
|
94
|
+
...new Set(
|
|
95
|
+
mutations.flatMap(({ data }) => data.getFirstAxisKeys().map((mutation) => mutation.segment || '')),
|
|
96
|
+
),
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
return unique.map((segment) => ({ segment, label: segment, checked: true }));
|
|
100
|
+
}, [mutations]);
|
|
101
|
+
|
|
102
|
+
return useState<DisplayedSegment[]>(displayedSegments);
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
type MutationOverTimeTabsProps = {
|
|
90
106
|
mutationOverTimeDataPerLocation: MutationOverTimeDataPerLocation;
|
|
91
107
|
originalComponentProps: WastewaterMutationsOverTimeProps;
|
|
92
108
|
};
|
|
93
109
|
|
|
110
|
+
function getFilteredMutationOverTimeData({
|
|
111
|
+
data,
|
|
112
|
+
displayedSegments,
|
|
113
|
+
}: {
|
|
114
|
+
data: MutationOverTimeDataMap;
|
|
115
|
+
displayedSegments: DisplayedSegment[];
|
|
116
|
+
}): MutationOverTimeDataMap {
|
|
117
|
+
const filteredData = new Map2dView(data);
|
|
118
|
+
|
|
119
|
+
const mutationsToFilterOut = data.getFirstAxisKeys().filter((entry) => {
|
|
120
|
+
return displayedSegments.some((segment) => segment.segment === entry.segment && !segment.checked);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
mutationsToFilterOut.forEach((entry) => {
|
|
124
|
+
filteredData.deleteRow(entry);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return filteredData;
|
|
128
|
+
}
|
|
129
|
+
|
|
94
130
|
const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
95
131
|
mutationOverTimeDataPerLocation,
|
|
96
132
|
originalComponentProps,
|
|
97
133
|
}) => {
|
|
98
134
|
const [colorScale, setColorScale] = useState<ColorScale>({ min: 0, max: 1, color: 'indigo' });
|
|
135
|
+
const [displayedSegments, setDisplayedSegments] = useDisplayedSegments(mutationOverTimeDataPerLocation);
|
|
99
136
|
|
|
100
|
-
const tabs =
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
137
|
+
const tabs = useMemo(
|
|
138
|
+
() =>
|
|
139
|
+
mutationOverTimeDataPerLocation.map(({ location, data }) => ({
|
|
140
|
+
title: location,
|
|
141
|
+
content: (
|
|
142
|
+
<MutationsOverTimeGrid
|
|
143
|
+
data={getFilteredMutationOverTimeData({ data, displayedSegments })}
|
|
144
|
+
colorScale={colorScale}
|
|
145
|
+
pageSizes={originalComponentProps.pageSizes}
|
|
146
|
+
sequenceType={originalComponentProps.sequenceType}
|
|
147
|
+
/>
|
|
148
|
+
),
|
|
149
|
+
})),
|
|
150
|
+
[mutationOverTimeDataPerLocation, displayedSegments, colorScale, originalComponentProps],
|
|
151
|
+
);
|
|
111
152
|
|
|
112
153
|
const toolbar = (
|
|
113
154
|
<Toolbar
|
|
@@ -115,6 +156,8 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
|
115
156
|
setColorScale={setColorScale}
|
|
116
157
|
originalComponentProps={originalComponentProps}
|
|
117
158
|
data={mutationOverTimeDataPerLocation}
|
|
159
|
+
displayedSegments={displayedSegments}
|
|
160
|
+
setDisplayedSegments={setDisplayedSegments}
|
|
118
161
|
/>
|
|
119
162
|
);
|
|
120
163
|
|
|
@@ -126,12 +169,21 @@ type ToolbarProps = {
|
|
|
126
169
|
setColorScale: Dispatch<StateUpdater<ColorScale>>;
|
|
127
170
|
originalComponentProps: WastewaterMutationsOverTimeProps;
|
|
128
171
|
data: MutationOverTimeDataPerLocation;
|
|
172
|
+
displayedSegments: DisplayedSegment[];
|
|
173
|
+
setDisplayedSegments: (segments: DisplayedSegment[]) => void;
|
|
129
174
|
};
|
|
130
175
|
|
|
131
|
-
const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
176
|
+
const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
177
|
+
colorScale,
|
|
178
|
+
setColorScale,
|
|
179
|
+
originalComponentProps,
|
|
180
|
+
displayedSegments,
|
|
181
|
+
setDisplayedSegments,
|
|
182
|
+
}) => {
|
|
132
183
|
return (
|
|
133
184
|
<>
|
|
134
185
|
<ColorScaleSelectorDropdown colorScale={colorScale} setColorScale={setColorScale} />
|
|
186
|
+
<SegmentSelector displayedSegments={displayedSegments} setDisplayedSegments={setDisplayedSegments} />
|
|
135
187
|
<WastewaterMutationsOverTimeInfo originalComponentProps={originalComponentProps} />
|
|
136
188
|
<Fullscreen />
|
|
137
189
|
</>
|