@genspectrum/dashboard-components 0.16.1 → 0.16.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 +72 -7
- package/dist/assets/mutationOverTimeWorker-DJcZmEH9.js.map +1 -0
- package/dist/components.d.ts +63 -25
- package/dist/components.js +310 -151
- package/dist/components.js.map +1 -1
- package/dist/style.css +16 -0
- package/dist/util.d.ts +25 -25
- package/package.json +4 -2
- package/src/preact/MutationAnnotationsContext.spec.tsx +58 -0
- package/src/preact/MutationAnnotationsContext.tsx +72 -0
- package/src/preact/components/annotated-mutation.stories.tsx +163 -0
- package/src/preact/components/annotated-mutation.tsx +80 -0
- package/src/preact/components/downshift-combobox.tsx +6 -4
- package/src/preact/components/error-display.tsx +9 -9
- package/src/preact/components/info.tsx +6 -13
- package/src/preact/components/modal.stories.tsx +7 -19
- package/src/preact/components/modal.tsx +35 -4
- package/src/preact/mutations/mutations-table.tsx +14 -2
- package/src/preact/mutations/mutations.stories.tsx +40 -2
- package/src/preact/mutations/mutations.tsx +1 -0
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +19 -8
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +34 -5
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +13 -1
- package/src/preact/sequencesByLocation/sequences-by-location-map.tsx +28 -30
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +7 -2
- package/src/web-components/gs-app.spec-d.ts +10 -0
- package/src/web-components/gs-app.stories.ts +24 -6
- package/src/web-components/gs-app.ts +17 -0
- package/src/web-components/mutation-annotations-context.ts +16 -0
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +18 -1
- package/src/web-components/visualization/gs-mutations-over-time.tsx +22 -11
- package/src/web-components/visualization/gs-mutations.stories.ts +18 -1
- package/src/web-components/visualization/gs-mutations.tsx +20 -9
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +11 -1
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +18 -7
- package/standalone-bundle/assets/mutationOverTimeWorker-CERZSdcA.js.map +1 -0
- package/standalone-bundle/dashboard-components.js +8094 -7963
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
- package/dist/assets/mutationOverTimeWorker-BL50C-yi.js.map +0 -1
- package/standalone-bundle/assets/mutationOverTimeWorker-CFB5-Mdk.js.map +0 -1
|
@@ -3,7 +3,7 @@ import { useEffect, useRef } from 'preact/hooks';
|
|
|
3
3
|
import { type ZodError } from 'zod';
|
|
4
4
|
|
|
5
5
|
import { InfoHeadline1, InfoParagraph } from './info';
|
|
6
|
-
import { Modal
|
|
6
|
+
import { Modal } from './modal';
|
|
7
7
|
import { LapisError, UnknownLapisError } from '../../lapisApi/lapisApi';
|
|
8
8
|
|
|
9
9
|
export const GS_ERROR_EVENT_TYPE = 'gs-error';
|
|
@@ -48,7 +48,6 @@ export const ErrorDisplay: FunctionComponent<ErrorDisplayProps> = ({ error, rese
|
|
|
48
48
|
console.error(error);
|
|
49
49
|
|
|
50
50
|
const containerRef = useRef<HTMLInputElement>(null);
|
|
51
|
-
const modalRef = useModalRef();
|
|
52
51
|
|
|
53
52
|
useEffect(() => {
|
|
54
53
|
containerRef.current?.dispatchEvent(new ErrorEvent(error));
|
|
@@ -68,15 +67,16 @@ export const ErrorDisplay: FunctionComponent<ErrorDisplayProps> = ({ error, rese
|
|
|
68
67
|
{details !== undefined && (
|
|
69
68
|
<>
|
|
70
69
|
{' '}
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
<Modal
|
|
71
|
+
buttonClassName='underline hover:text-gray-400'
|
|
72
|
+
modalContent={
|
|
73
|
+
<>
|
|
74
|
+
<InfoHeadline1>{details.headline}</InfoHeadline1>
|
|
75
|
+
<InfoParagraph>{details.message}</InfoParagraph>
|
|
76
|
+
</>
|
|
77
|
+
}
|
|
74
78
|
>
|
|
75
79
|
Show details.
|
|
76
|
-
</button>
|
|
77
|
-
<Modal modalRef={modalRef}>
|
|
78
|
-
<InfoHeadline1>{details.headline}</InfoHeadline1>
|
|
79
|
-
<InfoParagraph>{details.message}</InfoParagraph>
|
|
80
80
|
</Modal>
|
|
81
81
|
</>
|
|
82
82
|
)}
|
|
@@ -1,34 +1,27 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
|
|
3
|
-
import { Modal
|
|
3
|
+
import { Modal } from './modal';
|
|
4
4
|
|
|
5
5
|
const Info: FunctionComponent = ({ children }) => {
|
|
6
|
-
const modalRef = useModalRef();
|
|
7
|
-
|
|
8
|
-
const toggleHelp = () => {
|
|
9
|
-
modalRef.current?.showModal();
|
|
10
|
-
};
|
|
11
|
-
|
|
12
6
|
return (
|
|
13
7
|
<div className='relative'>
|
|
14
|
-
<
|
|
8
|
+
<Modal buttonClassName='btn btn-xs' modalContent={children}>
|
|
15
9
|
?
|
|
16
|
-
</
|
|
17
|
-
<Modal modalRef={modalRef}>{children}</Modal>
|
|
10
|
+
</Modal>
|
|
18
11
|
</div>
|
|
19
12
|
);
|
|
20
13
|
};
|
|
21
14
|
|
|
22
15
|
export const InfoHeadline1: FunctionComponent = ({ children }) => {
|
|
23
|
-
return <h1 className='text-lg font-bold'>{children}</h1>;
|
|
16
|
+
return <h1 className='text-justify text-lg font-bold'>{children}</h1>;
|
|
24
17
|
};
|
|
25
18
|
|
|
26
19
|
export const InfoHeadline2: FunctionComponent = ({ children }) => {
|
|
27
|
-
return <h2 className='text-base font-bold mt-4'>{children}</h2>;
|
|
20
|
+
return <h2 className='text-justify text-base font-bold mt-4'>{children}</h2>;
|
|
28
21
|
};
|
|
29
22
|
|
|
30
23
|
export const InfoParagraph: FunctionComponent = ({ children }) => {
|
|
31
|
-
return <p className='text-justify my-1'>{children}</p>;
|
|
24
|
+
return <p className='text-justify text-base font-normal my-1'>{children}</p>;
|
|
32
25
|
};
|
|
33
26
|
|
|
34
27
|
export const InfoLink: FunctionComponent<{ href: string }> = ({ children, href }) => {
|
|
@@ -1,35 +1,23 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
2
|
import { expect, waitFor, within } from '@storybook/test';
|
|
3
|
-
import { type FunctionComponent } from 'preact';
|
|
4
3
|
|
|
5
|
-
import { Modal, type ModalProps
|
|
4
|
+
import { Modal, ModalDialog, type ModalProps } from './modal';
|
|
6
5
|
|
|
7
6
|
const meta: Meta<ModalProps> = {
|
|
8
7
|
title: 'Component/Modal',
|
|
9
|
-
component:
|
|
8
|
+
component: ModalDialog,
|
|
10
9
|
parameters: { fetchMock: {} },
|
|
11
10
|
};
|
|
12
11
|
|
|
13
12
|
export default meta;
|
|
14
13
|
|
|
15
|
-
const WrapperWithButtonThatOpensTheModal: FunctionComponent = () => {
|
|
16
|
-
const modalRef = useModalRef();
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<div>
|
|
20
|
-
<button className='btn' onClick={() => modalRef.current?.showModal()}>
|
|
21
|
-
Open modal
|
|
22
|
-
</button>
|
|
23
|
-
<Modal modalRef={modalRef}>
|
|
24
|
-
<h1>Modal content</h1>
|
|
25
|
-
</Modal>
|
|
26
|
-
</div>
|
|
27
|
-
);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
14
|
export const ModalStory: StoryObj<ModalProps> = {
|
|
31
15
|
render: () => {
|
|
32
|
-
return
|
|
16
|
+
return (
|
|
17
|
+
<Modal buttonClassName='btn' modalContent={<h1>Modal content</h1>}>
|
|
18
|
+
Open modal
|
|
19
|
+
</Modal>
|
|
20
|
+
);
|
|
33
21
|
},
|
|
34
22
|
play: async ({ canvasElement, step }) => {
|
|
35
23
|
const canvas = within(canvasElement);
|
|
@@ -1,15 +1,46 @@
|
|
|
1
|
-
import { type FunctionComponent, type Ref } from 'preact';
|
|
1
|
+
import { type ComponentChildren, type FunctionComponent, type Ref, type RefObject } from 'preact';
|
|
2
2
|
import { useRef } from 'preact/hooks';
|
|
3
3
|
|
|
4
|
-
export type
|
|
5
|
-
|
|
4
|
+
export type ModalButtonProps = {
|
|
5
|
+
buttonClassName?: string;
|
|
6
|
+
modalContent: ComponentChildren;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const Modal: FunctionComponent<ModalButtonProps> = (props) => {
|
|
10
|
+
const modalRef = useModalRef();
|
|
11
|
+
|
|
12
|
+
return <ButtonWithModalDialog {...props} modalRef={modalRef} />;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ButtonWithModalDialogProps = ModalButtonProps & {
|
|
16
|
+
modalRef: RefObject<HTMLDialogElement>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const ButtonWithModalDialog: FunctionComponent<ButtonWithModalDialogProps> = ({
|
|
20
|
+
children,
|
|
21
|
+
buttonClassName,
|
|
22
|
+
modalContent,
|
|
23
|
+
modalRef,
|
|
24
|
+
}) => {
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<button type='button' className={buttonClassName} onClick={() => modalRef.current?.showModal()}>
|
|
28
|
+
{children}
|
|
29
|
+
</button>
|
|
30
|
+
<ModalDialog modalRef={modalRef}>{modalContent}</ModalDialog>
|
|
31
|
+
</>
|
|
32
|
+
);
|
|
6
33
|
};
|
|
7
34
|
|
|
8
35
|
export function useModalRef() {
|
|
9
36
|
return useRef<HTMLDialogElement>(null);
|
|
10
37
|
}
|
|
11
38
|
|
|
12
|
-
export
|
|
39
|
+
export type ModalProps = {
|
|
40
|
+
modalRef: Ref<HTMLDialogElement>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const ModalDialog: FunctionComponent<ModalProps> = ({ children, modalRef }) => {
|
|
13
44
|
return (
|
|
14
45
|
<dialog ref={modalRef} className={'modal modal-bottom sm:modal-middle'}>
|
|
15
46
|
<div className='modal-box sm:max-w-5xl'>
|
|
@@ -2,8 +2,10 @@ import { type FunctionComponent } from 'preact';
|
|
|
2
2
|
import { useMemo } from 'preact/hooks';
|
|
3
3
|
|
|
4
4
|
import { getMutationsTableData } from './getMutationsTableData';
|
|
5
|
-
import { type SubstitutionOrDeletionEntry } from '../../types';
|
|
5
|
+
import { type SequenceType, type SubstitutionOrDeletionEntry } from '../../types';
|
|
6
6
|
import { type DeletionClass, type SubstitutionClass } from '../../utils/mutations';
|
|
7
|
+
import { useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
|
|
8
|
+
import { GridJsAnnotatedMutation } from '../components/annotated-mutation';
|
|
7
9
|
import type { ProportionInterval } from '../components/proportion-selector';
|
|
8
10
|
import { Table } from '../components/table';
|
|
9
11
|
import { sortSubstitutionsAndDeletions } from '../shared/sort/sortSubstitutionsAndDeletions';
|
|
@@ -15,6 +17,7 @@ export interface MutationsTableProps {
|
|
|
15
17
|
overallVariantCount: number;
|
|
16
18
|
proportionInterval: ProportionInterval;
|
|
17
19
|
pageSize: boolean | number;
|
|
20
|
+
sequenceType: SequenceType;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
const MutationsTable: FunctionComponent<MutationsTableProps> = ({
|
|
@@ -23,7 +26,10 @@ const MutationsTable: FunctionComponent<MutationsTableProps> = ({
|
|
|
23
26
|
overallVariantCount,
|
|
24
27
|
proportionInterval,
|
|
25
28
|
pageSize,
|
|
29
|
+
sequenceType,
|
|
26
30
|
}) => {
|
|
31
|
+
const annotationsProvider = useMutationAnnotationsProvider();
|
|
32
|
+
|
|
27
33
|
const headers = [
|
|
28
34
|
{
|
|
29
35
|
name: 'Mutation',
|
|
@@ -32,7 +38,13 @@ const MutationsTable: FunctionComponent<MutationsTableProps> = ({
|
|
|
32
38
|
return sortSubstitutionsAndDeletions(a, b);
|
|
33
39
|
},
|
|
34
40
|
},
|
|
35
|
-
formatter: (cell: SubstitutionClass | DeletionClass) =>
|
|
41
|
+
formatter: (cell: SubstitutionClass | DeletionClass) => (
|
|
42
|
+
<GridJsAnnotatedMutation
|
|
43
|
+
mutation={cell}
|
|
44
|
+
sequenceType={sequenceType}
|
|
45
|
+
annotationsProvider={annotationsProvider}
|
|
46
|
+
/>
|
|
47
|
+
),
|
|
36
48
|
},
|
|
37
49
|
{
|
|
38
50
|
name: 'Type',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
-
import { expect, waitFor, within } from '@storybook/test';
|
|
2
|
+
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
|
3
3
|
|
|
4
4
|
import nucleotideInsertions from './__mockData__/nucleotideInsertions.json';
|
|
5
5
|
import nucleotideMutations from './__mockData__/nucleotideMutations.json';
|
|
@@ -14,6 +14,7 @@ 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
16
|
import { LapisUrlContextProvider } from '../LapisUrlContext';
|
|
17
|
+
import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
|
|
17
18
|
import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
18
19
|
|
|
19
20
|
const meta: Meta<MutationsProps> = {
|
|
@@ -37,11 +38,30 @@ const meta: Meta<MutationsProps> = {
|
|
|
37
38
|
|
|
38
39
|
export default meta;
|
|
39
40
|
|
|
41
|
+
const mutationAnnotations = [
|
|
42
|
+
{
|
|
43
|
+
name: 'I am a mutation annotation!',
|
|
44
|
+
description: 'This describes what is special about these mutations.',
|
|
45
|
+
symbol: '#',
|
|
46
|
+
nucleotideMutations: ['C241T', 'C3037T'],
|
|
47
|
+
aminoAcidMutations: ['N:G204R', 'N:S235F'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'I am another mutation annotation!',
|
|
51
|
+
description: 'This describes what is special about these other mutations.',
|
|
52
|
+
symbol: '+',
|
|
53
|
+
nucleotideMutations: ['C3037T', 'C11750T'],
|
|
54
|
+
aminoAcidMutations: ['ORF1a:S2255F'],
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
40
58
|
const Template = {
|
|
41
59
|
render: (args: MutationsProps) => (
|
|
42
60
|
<LapisUrlContextProvider value={LAPIS_URL}>
|
|
43
61
|
<ReferenceGenomeContext.Provider value={referenceGenome}>
|
|
44
|
-
<
|
|
62
|
+
<MutationAnnotationsContextProvider value={mutationAnnotations}>
|
|
63
|
+
<Mutations {...args} />
|
|
64
|
+
</MutationAnnotationsContextProvider>
|
|
45
65
|
</ReferenceGenomeContext.Provider>
|
|
46
66
|
</LapisUrlContextProvider>
|
|
47
67
|
),
|
|
@@ -137,3 +157,21 @@ export const GridTab: StoryObj<MutationsProps> = {
|
|
|
137
157
|
});
|
|
138
158
|
},
|
|
139
159
|
};
|
|
160
|
+
|
|
161
|
+
export const TableTab: StoryObj<MutationsProps> = {
|
|
162
|
+
...Default,
|
|
163
|
+
args: {
|
|
164
|
+
...Default.args,
|
|
165
|
+
views: ['table'],
|
|
166
|
+
},
|
|
167
|
+
play: async ({ canvasElement }) => {
|
|
168
|
+
const canvas = within(canvasElement);
|
|
169
|
+
|
|
170
|
+
await waitFor(async () => {
|
|
171
|
+
const annotatedMutation = canvas.getByText('C241T');
|
|
172
|
+
await expect(annotatedMutation).toBeVisible();
|
|
173
|
+
await userEvent.click(annotatedMutation);
|
|
174
|
+
});
|
|
175
|
+
await waitFor(() => expect(canvas.getByText('Annotations for C241T')).toBeVisible());
|
|
176
|
+
},
|
|
177
|
+
};
|
|
@@ -109,6 +109,7 @@ const MutationsTabs: FunctionComponent<MutationTabsProps> = ({ mutationsData, or
|
|
|
109
109
|
overallVariantCount={mutationsData.overallVariantCount}
|
|
110
110
|
proportionInterval={proportionInterval}
|
|
111
111
|
pageSize={originalComponentProps.pageSize}
|
|
112
|
+
sequenceType={originalComponentProps.sequenceType}
|
|
112
113
|
/>
|
|
113
114
|
),
|
|
114
115
|
};
|
|
@@ -3,8 +3,10 @@ import { useRef } from 'preact/hooks';
|
|
|
3
3
|
|
|
4
4
|
import { type MutationOverTimeDataMap } from './MutationOverTimeData';
|
|
5
5
|
import { type MutationOverTimeMutationValue } from '../../query/queryMutationsOverTime';
|
|
6
|
+
import { type SequenceType } from '../../types';
|
|
6
7
|
import { type Deletion, type Substitution } from '../../utils/mutations';
|
|
7
8
|
import { type Temporal, type TemporalClass, toTemporalClass, YearMonthDayClass } from '../../utils/temporalClass';
|
|
9
|
+
import { AnnotatedMutation } from '../components/annotated-mutation';
|
|
8
10
|
import { type ColorScale, getColorWithingScale, getTextColorForScale } from '../components/color-scale-selector';
|
|
9
11
|
import Tooltip, { type TooltipPosition } from '../components/tooltip';
|
|
10
12
|
import { formatProportion } from '../shared/table/formatProportion';
|
|
@@ -13,6 +15,7 @@ export interface MutationsOverTimeGridProps {
|
|
|
13
15
|
data: MutationOverTimeDataMap;
|
|
14
16
|
colorScale: ColorScale;
|
|
15
17
|
maxNumberOfGridRows?: number;
|
|
18
|
+
sequenceType: SequenceType;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
const MAX_NUMBER_OF_GRID_ROWS = 100;
|
|
@@ -22,6 +25,7 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
|
|
|
22
25
|
data,
|
|
23
26
|
colorScale,
|
|
24
27
|
maxNumberOfGridRows,
|
|
28
|
+
sequenceType,
|
|
25
29
|
}) => {
|
|
26
30
|
const currentMaxNumberOfGridRows = maxNumberOfGridRows ?? MAX_NUMBER_OF_GRID_ROWS;
|
|
27
31
|
const allMutations = data.getFirstAxisKeys();
|
|
@@ -66,8 +70,9 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
|
|
|
66
70
|
<div
|
|
67
71
|
key={`mutation-${mutation.code}`}
|
|
68
72
|
style={{ gridRowStart: rowIndex + 2, gridColumnStart: 1 }}
|
|
73
|
+
className='flex items-center justify-center'
|
|
69
74
|
>
|
|
70
|
-
<
|
|
75
|
+
<AnnotatedMutation mutation={mutation} sequenceType={sequenceType} />
|
|
71
76
|
</div>
|
|
72
77
|
{dates.map((date, columnIndex) => {
|
|
73
78
|
const value = data.get(mutation, date) ?? null;
|
|
@@ -141,9 +146,12 @@ const ProportionCell: FunctionComponent<{
|
|
|
141
146
|
<>
|
|
142
147
|
<p>Proportion: {formatProportion(value.proportion)}</p>
|
|
143
148
|
{value.count !== null && value.totalCount !== null && (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
<>
|
|
150
|
+
<p>
|
|
151
|
+
{value.count} / {totalCountWithCoverage(value.count, value.proportion)} with coverage
|
|
152
|
+
</p>
|
|
153
|
+
<p>{value.totalCount} in timeframe</p>
|
|
154
|
+
</>
|
|
147
155
|
)}
|
|
148
156
|
</>
|
|
149
157
|
)}
|
|
@@ -169,6 +177,13 @@ const ProportionCell: FunctionComponent<{
|
|
|
169
177
|
);
|
|
170
178
|
};
|
|
171
179
|
|
|
180
|
+
function totalCountWithCoverage(count: number, proportion: number) {
|
|
181
|
+
if (count === 0) {
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
return Math.round(count / proportion);
|
|
185
|
+
}
|
|
186
|
+
|
|
172
187
|
const timeIntervalDisplay = (date: TemporalClass) => {
|
|
173
188
|
if (date instanceof YearMonthDayClass) {
|
|
174
189
|
return date.toString();
|
|
@@ -177,8 +192,4 @@ const timeIntervalDisplay = (date: TemporalClass) => {
|
|
|
177
192
|
return `${date.firstDay.toString()} - ${date.lastDay.toString()}`;
|
|
178
193
|
};
|
|
179
194
|
|
|
180
|
-
const MutationCell: FunctionComponent<{ mutation: Substitution | Deletion }> = ({ mutation }) => {
|
|
181
|
-
return <div>{mutation.code}</div>;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
195
|
export default MutationsOverTimeGrid;
|
|
@@ -5,6 +5,7 @@ import { MutationsOverTime, type MutationsOverTimeProps } from './mutations-over
|
|
|
5
5
|
import { LAPIS_URL } from '../../constants';
|
|
6
6
|
import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
|
|
7
7
|
import { LapisUrlContextProvider } from '../LapisUrlContext';
|
|
8
|
+
import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
|
|
8
9
|
import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
9
10
|
import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectErrorMessage';
|
|
10
11
|
|
|
@@ -38,13 +39,32 @@ const meta: Meta<MutationsOverTimeProps> = {
|
|
|
38
39
|
|
|
39
40
|
export default meta;
|
|
40
41
|
|
|
42
|
+
const mutationAnnotations = [
|
|
43
|
+
{
|
|
44
|
+
name: 'I am a mutation annotation!',
|
|
45
|
+
description: 'This describes what is special about these mutations.',
|
|
46
|
+
symbol: '#',
|
|
47
|
+
nucleotideMutations: ['C44T', 'C774T', 'G24872T', 'T23011-'],
|
|
48
|
+
aminoAcidMutations: ['S:501Y', 'S:S31-', 'ORF1a:S4286C'],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'I am another mutation annotation!',
|
|
52
|
+
description: 'This describes what is special about these other mutations.',
|
|
53
|
+
symbol: '+',
|
|
54
|
+
nucleotideMutations: ['C44T', 'A13121T'],
|
|
55
|
+
aminoAcidMutations: ['S:501Y', 'S:S31-', 'ORF1a:S4286C'],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
41
59
|
export const Default: StoryObj<MutationsOverTimeProps> = {
|
|
42
60
|
render: (args: MutationsOverTimeProps) => (
|
|
43
|
-
<
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
<MutationAnnotationsContextProvider value={mutationAnnotations}>
|
|
62
|
+
<LapisUrlContextProvider value={LAPIS_URL}>
|
|
63
|
+
<ReferenceGenomeContext.Provider value={referenceGenome}>
|
|
64
|
+
<MutationsOverTime {...args} />
|
|
65
|
+
</ReferenceGenomeContext.Provider>
|
|
66
|
+
</LapisUrlContextProvider>
|
|
67
|
+
</MutationAnnotationsContextProvider>
|
|
48
68
|
),
|
|
49
69
|
args: {
|
|
50
70
|
lapisFilter: { pangoLineage: 'JN.1*', dateFrom: '2024-01-15', dateTo: '2024-07-10' },
|
|
@@ -55,6 +75,15 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
|
|
|
55
75
|
lapisDateField: 'date',
|
|
56
76
|
initialMeanProportionInterval: { min: 0.05, max: 0.9 },
|
|
57
77
|
},
|
|
78
|
+
play: async ({ canvas }) => {
|
|
79
|
+
await waitFor(async () => {
|
|
80
|
+
const annotatedMutation = canvas.getAllByText('C44T')[0];
|
|
81
|
+
await expect(annotatedMutation).toBeVisible();
|
|
82
|
+
await userEvent.click(annotatedMutation);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await waitFor(() => expect(canvas.getByText('Annotations for C44T')).toBeVisible());
|
|
86
|
+
},
|
|
58
87
|
};
|
|
59
88
|
|
|
60
89
|
// This test uses mock data: showMessagWhenTooManyMutations.ts (through mutationOverTimeWorker.mock.ts)
|
|
@@ -161,7 +161,13 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
|
161
161
|
case 'grid':
|
|
162
162
|
return {
|
|
163
163
|
title: 'Grid',
|
|
164
|
-
content:
|
|
164
|
+
content: (
|
|
165
|
+
<MutationsOverTimeGrid
|
|
166
|
+
data={filteredData}
|
|
167
|
+
colorScale={colorScale}
|
|
168
|
+
sequenceType={originalComponentProps.sequenceType}
|
|
169
|
+
/>
|
|
170
|
+
),
|
|
165
171
|
};
|
|
166
172
|
}
|
|
167
173
|
};
|
|
@@ -257,6 +263,12 @@ const MutationsOverTimeInfo: FunctionComponent<MutationsOverTimeInfoProps> = ({
|
|
|
257
263
|
organism has multiple segments/genes), and applying a filter based on the proportion of the mutation's
|
|
258
264
|
occurrence over the entire time range.
|
|
259
265
|
</InfoParagraph>
|
|
266
|
+
<InfoParagraph>
|
|
267
|
+
The grid cells have a tooltip that will show more detailed information. It shows the count of samples
|
|
268
|
+
that have the mutation and the count of samples with coverage (i.e. a non-ambiguous read) in this
|
|
269
|
+
timeframe. Ambiguous reads are excluded when calculating the proportion. It also shows the total count
|
|
270
|
+
of samples in this timeframe.
|
|
271
|
+
</InfoParagraph>
|
|
260
272
|
<InfoComponentCode componentName='mutations-over-time' params={originalComponentProps} lapisUrl={lapis} />
|
|
261
273
|
</Info>
|
|
262
274
|
);
|
|
@@ -5,7 +5,7 @@ import { useEffect, useRef } from 'preact/hooks';
|
|
|
5
5
|
|
|
6
6
|
import type { EnhancedGeoJsonFeatureProperties } from '../../query/computeMapLocationData';
|
|
7
7
|
import { InfoHeadline1, InfoParagraph } from '../components/info';
|
|
8
|
-
import { Modal
|
|
8
|
+
import { Modal } from '../components/modal';
|
|
9
9
|
import { AspectRatio } from '../shared/aspectRatio/AspectRatio';
|
|
10
10
|
import { formatProportion } from '../shared/table/formatProportion';
|
|
11
11
|
|
|
@@ -102,39 +102,37 @@ const DataMatchInformation: FunctionComponent<DataMatchInformationProps> = ({
|
|
|
102
102
|
nullCount,
|
|
103
103
|
hasTableView,
|
|
104
104
|
}) => {
|
|
105
|
-
const modalRef = useModalRef();
|
|
106
|
-
|
|
107
105
|
const proportion = formatProportion(countOfMatchedLocationData / totalCount);
|
|
108
106
|
|
|
109
107
|
return (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
108
|
+
<Modal
|
|
109
|
+
buttonClassName='text-sm absolute bottom-0 px-1 z-[1001] bg-white rounded border'
|
|
110
|
+
modalContent={
|
|
111
|
+
<>
|
|
112
|
+
<InfoHeadline1>Sequences By Location - Map View</InfoHeadline1>
|
|
113
|
+
<InfoParagraph>
|
|
114
|
+
The current filter has matched {totalCount.toLocaleString('en-us')} sequences. From these
|
|
115
|
+
sequences, we were able to match {countOfMatchedLocationData.toLocaleString('en-us')} (
|
|
116
|
+
{proportion}) on locations on the map.
|
|
117
|
+
</InfoParagraph>
|
|
118
|
+
<InfoParagraph>
|
|
119
|
+
{unmatchedLocations.length > 0 && (
|
|
120
|
+
<>
|
|
121
|
+
The following locations from the data could not be matched on the map:{' '}
|
|
122
|
+
{unmatchedLocations.map((it) => `"${it}"`).join(', ')}.{' '}
|
|
123
|
+
</>
|
|
124
|
+
)}
|
|
125
|
+
{nullCount > 0 &&
|
|
126
|
+
`${nullCount.toLocaleString('en-us')} matching sequences have no location information. `}
|
|
127
|
+
{hasTableView && 'You can check the table view for more detailed information.'}
|
|
128
|
+
</InfoParagraph>
|
|
129
|
+
</>
|
|
130
|
+
}
|
|
131
|
+
>
|
|
132
|
+
<p className='tooltip' data-tip='Click for detailed information'>
|
|
116
133
|
This map shows {proportion} of the data.
|
|
117
|
-
</
|
|
118
|
-
|
|
119
|
-
<InfoHeadline1>Sequences By Location - Map View</InfoHeadline1>
|
|
120
|
-
<InfoParagraph>
|
|
121
|
-
The current filter has matched {totalCount.toLocaleString('en-us')} sequences. From these sequences,
|
|
122
|
-
we were able to match {countOfMatchedLocationData.toLocaleString('en-us')} ({proportion}) on
|
|
123
|
-
locations on the map.
|
|
124
|
-
</InfoParagraph>
|
|
125
|
-
<InfoParagraph>
|
|
126
|
-
{unmatchedLocations.length > 0 && (
|
|
127
|
-
<>
|
|
128
|
-
The following locations from the data could not be matched on the map:{' '}
|
|
129
|
-
{unmatchedLocations.map((it) => `"${it}"`).join(', ')}.{' '}
|
|
130
|
-
</>
|
|
131
|
-
)}
|
|
132
|
-
{nullCount > 0 &&
|
|
133
|
-
`${nullCount.toLocaleString('en-us')} matching sequences have no location information. `}
|
|
134
|
-
{hasTableView && 'You can check the table view for more detailed information.'}
|
|
135
|
-
</InfoParagraph>
|
|
136
|
-
</Modal>
|
|
137
|
-
</>
|
|
134
|
+
</p>
|
|
135
|
+
</Modal>
|
|
138
136
|
);
|
|
139
137
|
};
|
|
140
138
|
|
|
@@ -57,7 +57,7 @@ export const WastewaterMutationsOverTimeInner: FunctionComponent<WastewaterMutat
|
|
|
57
57
|
componentProps.lapisFilter,
|
|
58
58
|
componentProps.sequenceType,
|
|
59
59
|
),
|
|
60
|
-
[],
|
|
60
|
+
[componentProps.lapisFilter, componentProps.sequenceType],
|
|
61
61
|
);
|
|
62
62
|
|
|
63
63
|
if (isLoading) {
|
|
@@ -102,7 +102,12 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
|
102
102
|
const tabs = mutationOverTimeDataPerLocation.map(({ location, data }) => ({
|
|
103
103
|
title: location,
|
|
104
104
|
content: (
|
|
105
|
-
<MutationsOverTimeGrid
|
|
105
|
+
<MutationsOverTimeGrid
|
|
106
|
+
data={data}
|
|
107
|
+
colorScale={colorScale}
|
|
108
|
+
maxNumberOfGridRows={maxNumberOfGridRows}
|
|
109
|
+
sequenceType={originalComponentProps.sequenceType}
|
|
110
|
+
/>
|
|
106
111
|
),
|
|
107
112
|
}));
|
|
108
113
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expectTypeOf, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { AppComponent } from './gs-app';
|
|
4
|
+
import { type MutationAnnotations } from './mutation-annotations-context';
|
|
5
|
+
|
|
6
|
+
describe('gs-app types', () => {
|
|
7
|
+
test('mutationAnnotations type should match', ({}) => {
|
|
8
|
+
expectTypeOf(AppComponent.prototype).toHaveProperty('mutationAnnotations').toEqualTypeOf<MutationAnnotations>();
|
|
9
|
+
});
|
|
10
|
+
});
|