@genspectrum/dashboard-components 0.16.2 → 0.16.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.
Files changed (48) hide show
  1. package/custom-elements.json +72 -7
  2. package/dist/assets/mutationOverTimeWorker-CPfQDLe6.js.map +1 -0
  3. package/dist/components.d.ts +60 -21
  4. package/dist/components.js +804 -608
  5. package/dist/components.js.map +1 -1
  6. package/dist/style.css +21 -0
  7. package/dist/util.d.ts +69 -25
  8. package/package.json +5 -2
  9. package/src/preact/MutationAnnotationsContext.spec.tsx +58 -0
  10. package/src/preact/MutationAnnotationsContext.tsx +72 -0
  11. package/src/preact/components/annotated-mutation.stories.tsx +164 -0
  12. package/src/preact/components/annotated-mutation.tsx +84 -0
  13. package/src/preact/components/error-display.tsx +9 -9
  14. package/src/preact/components/info.tsx +6 -13
  15. package/src/preact/components/modal.stories.tsx +7 -19
  16. package/src/preact/components/modal.tsx +35 -4
  17. package/src/preact/mutationComparison/mutation-comparison-table.tsx +14 -1
  18. package/src/preact/mutationComparison/mutation-comparison-venn.tsx +39 -8
  19. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +36 -12
  20. package/src/preact/mutationComparison/mutation-comparison.tsx +2 -0
  21. package/src/preact/mutations/mutations-table.tsx +14 -2
  22. package/src/preact/mutations/mutations.stories.tsx +33 -1
  23. package/src/preact/mutations/mutations.tsx +1 -0
  24. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +19 -8
  25. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +29 -5
  26. package/src/preact/mutationsOverTime/mutations-over-time.tsx +13 -1
  27. package/src/preact/sequencesByLocation/sequences-by-location-map.tsx +28 -30
  28. package/src/preact/shared/stories/expectMutationAnnotation.ts +13 -0
  29. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +6 -1
  30. package/src/utilEntrypoint.ts +2 -0
  31. package/src/web-components/gs-app.spec-d.ts +10 -0
  32. package/src/web-components/gs-app.stories.ts +24 -6
  33. package/src/web-components/gs-app.ts +17 -0
  34. package/src/web-components/mutation-annotations-context.ts +16 -0
  35. package/src/web-components/visualization/gs-mutation-comparison.stories.ts +18 -1
  36. package/src/web-components/visualization/gs-mutation-comparison.tsx +19 -8
  37. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +19 -1
  38. package/src/web-components/visualization/gs-mutations-over-time.tsx +22 -11
  39. package/src/web-components/visualization/gs-mutations.stories.ts +19 -1
  40. package/src/web-components/visualization/gs-mutations.tsx +20 -9
  41. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +11 -1
  42. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +18 -7
  43. package/standalone-bundle/assets/mutationOverTimeWorker-CERZSdcA.js.map +1 -0
  44. package/standalone-bundle/dashboard-components.js +12555 -11853
  45. package/standalone-bundle/dashboard-components.js.map +1 -1
  46. package/standalone-bundle/style.css +1 -1
  47. package/dist/assets/mutationOverTimeWorker-BL50C-yi.js.map +0 -1
  48. package/standalone-bundle/assets/mutationOverTimeWorker-CFB5-Mdk.js.map +0 -1
@@ -0,0 +1,84 @@
1
+ import DOMPurify from 'dompurify';
2
+ import { useRef } from 'gridjs';
3
+ import { Fragment, type FunctionComponent, type RefObject } from 'preact';
4
+
5
+ import type { SequenceType } from '../../types';
6
+ import type { Deletion, Substitution } from '../../utils/mutations';
7
+ import { useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
8
+ import { InfoHeadline1, InfoHeadline2, InfoParagraph } from './info';
9
+ import { ButtonWithModalDialog, useModalRef } from './modal';
10
+
11
+ export type AnnotatedMutationProps = {
12
+ mutation: Substitution | Deletion;
13
+ sequenceType: SequenceType;
14
+ };
15
+
16
+ export const AnnotatedMutation: FunctionComponent<AnnotatedMutationProps> = (props) => {
17
+ const annotationsProvider = useMutationAnnotationsProvider();
18
+ const modalRef = useModalRef();
19
+
20
+ return <AnnotatedMutationWithoutContext {...props} annotationsProvider={annotationsProvider} modalRef={modalRef} />;
21
+ };
22
+
23
+ type GridJsAnnotatedMutationProps = AnnotatedMutationProps & {
24
+ annotationsProvider: ReturnType<typeof useMutationAnnotationsProvider>;
25
+ };
26
+
27
+ /**
28
+ * GridJS internally also uses Preact, but it uses its own Preact instance:
29
+ * - Our Preact contexts are not available in GridJS. We need to inject context content as long as we're in our Preact instance.
30
+ * - We must use the GridJS re-exports of the Preact hooks. I'm not sure why.
31
+ */
32
+ export const GridJsAnnotatedMutation: FunctionComponent<GridJsAnnotatedMutationProps> = (props) => {
33
+ const modalRef = useRef<HTMLDialogElement>(null);
34
+
35
+ return <AnnotatedMutationWithoutContext {...props} modalRef={modalRef} />;
36
+ };
37
+
38
+ type AnnotatedMutationWithoutContextProps = GridJsAnnotatedMutationProps & {
39
+ modalRef: RefObject<HTMLDialogElement>;
40
+ };
41
+
42
+ const AnnotatedMutationWithoutContext: FunctionComponent<AnnotatedMutationWithoutContextProps> = ({
43
+ mutation,
44
+ sequenceType,
45
+ annotationsProvider,
46
+ modalRef,
47
+ }) => {
48
+ const mutationAnnotations = annotationsProvider(mutation.code, sequenceType);
49
+
50
+ if (mutationAnnotations === undefined || mutationAnnotations.length === 0) {
51
+ return mutation.code;
52
+ }
53
+
54
+ const modalContent = (
55
+ <div className='block'>
56
+ <InfoHeadline1>Annotations for {mutation.code}</InfoHeadline1>
57
+ {mutationAnnotations.map((annotation) => (
58
+ <Fragment key={annotation.name}>
59
+ <InfoHeadline2>{annotation.name}</InfoHeadline2>
60
+ <InfoParagraph>
61
+ {/* eslint-disable-next-line react/no-danger */}
62
+ <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(annotation.description) }} />
63
+ </InfoParagraph>
64
+ </Fragment>
65
+ ))}
66
+ </div>
67
+ );
68
+
69
+ return (
70
+ <ButtonWithModalDialog buttonClassName={'select-text'} modalContent={modalContent} modalRef={modalRef}>
71
+ {mutation.code}
72
+ <sup>
73
+ {mutationAnnotations
74
+ .map((annotation) => annotation.symbol)
75
+ .map((symbol, index) => (
76
+ <Fragment key={symbol}>
77
+ <span className='text-red-600'>{symbol}</span>
78
+ {index !== mutationAnnotations.length - 1 && ','}
79
+ </Fragment>
80
+ ))}
81
+ </sup>
82
+ </ButtonWithModalDialog>
83
+ );
84
+ };
@@ -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, useModalRef } from './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
- <button
72
- className='underline hover:text-gray-400'
73
- onClick={() => modalRef.current?.showModal()}
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, useModalRef } from './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
- <button type='button' className='btn btn-xs' onClick={toggleHelp}>
8
+ <Modal buttonClassName='btn btn-xs' modalContent={children}>
15
9
  ?
16
- </button>
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, useModalRef } from './modal';
4
+ import { Modal, ModalDialog, type ModalProps } from './modal';
6
5
 
7
6
  const meta: Meta<ModalProps> = {
8
7
  title: 'Component/Modal',
9
- component: Modal,
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 <WrapperWithButtonThatOpensTheModal />;
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 ModalProps = {
5
- modalRef: Ref<HTMLDialogElement>;
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 const Modal: FunctionComponent<ModalProps> = ({ children, modalRef }) => {
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'>
@@ -3,7 +3,10 @@ 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 { SequenceType } from '../../types';
6
7
  import { type DeletionClass, type SubstitutionClass } from '../../utils/mutations';
8
+ import { useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
9
+ import { GridJsAnnotatedMutation } from '../components/annotated-mutation';
7
10
  import { type ProportionInterval } from '../components/proportion-selector';
8
11
  import { Table } from '../components/table';
9
12
  import { sortSubstitutionsAndDeletions } from '../shared/sort/sortSubstitutionsAndDeletions';
@@ -13,20 +16,30 @@ export interface MutationsTableProps {
13
16
  data: Dataset<MutationData>;
14
17
  proportionInterval: ProportionInterval;
15
18
  pageSize: boolean | number;
19
+ sequenceType: SequenceType;
16
20
  }
17
21
 
18
22
  export const MutationComparisonTable: FunctionComponent<MutationsTableProps> = ({
19
23
  data,
20
24
  proportionInterval,
21
25
  pageSize,
26
+ sequenceType,
22
27
  }) => {
28
+ const annotationsProvider = useMutationAnnotationsProvider();
29
+
23
30
  const headers = [
24
31
  {
25
32
  name: 'Mutation',
26
33
  sort: {
27
34
  compare: sortSubstitutionsAndDeletions,
28
35
  },
29
- formatter: (cell: SubstitutionClass | DeletionClass) => cell.toString(),
36
+ formatter: (cell: SubstitutionClass | DeletionClass) => (
37
+ <GridJsAnnotatedMutation
38
+ mutation={cell}
39
+ sequenceType={sequenceType}
40
+ annotationsProvider={annotationsProvider}
41
+ />
42
+ ),
30
43
  },
31
44
  {
32
45
  name: 'Prevalence',
@@ -1,10 +1,13 @@
1
1
  import { type ActiveElement, Chart, type ChartConfiguration, type ChartEvent, registerables } from 'chart.js';
2
2
  import { ArcSlice, extractSets, VennDiagramController } from 'chartjs-chart-venn';
3
- import { type FunctionComponent } from 'preact';
3
+ import { Fragment, type FunctionComponent } from 'preact';
4
4
  import { useMemo, useState } from 'preact/hooks';
5
5
 
6
6
  import { type MutationData } from './queryMutationData';
7
7
  import { type Dataset } from '../../operator/Dataset';
8
+ import { type SequenceType } from '../../types';
9
+ import { DeletionClass, SubstitutionClass } from '../../utils/mutations';
10
+ import { AnnotatedMutation } from '../components/annotated-mutation';
8
11
  import GsChart from '../components/chart';
9
12
  import { type ProportionInterval } from '../components/proportion-selector';
10
13
 
@@ -14,12 +17,14 @@ export interface MutationComparisonVennProps {
14
17
  data: Dataset<MutationData>;
15
18
  proportionInterval: ProportionInterval;
16
19
  maintainAspectRatio: boolean;
20
+ sequenceType: SequenceType;
17
21
  }
18
22
 
19
23
  export const MutationComparisonVenn: FunctionComponent<MutationComparisonVennProps> = ({
20
24
  data,
21
25
  proportionInterval,
22
26
  maintainAspectRatio,
27
+ sequenceType,
23
28
  }) => {
24
29
  const [selectedDatasetIndex, setSelectedDatasetIndex] = useState<null | number>(null);
25
30
 
@@ -105,22 +110,48 @@ export const MutationComparisonVenn: FunctionComponent<MutationComparisonVennPro
105
110
  <div className='flex-1'>
106
111
  <GsChart configuration={config} />
107
112
  </div>
108
- <p class='flex flex-wrap break-words m-2'>{getSelectedMutationsDescription(selectedDatasetIndex, sets)}</p>
113
+ <p class='flex flex-wrap break-words m-2'>
114
+ <SelectedMutationsDescription
115
+ selectedDatasetIndex={selectedDatasetIndex}
116
+ sets={sets}
117
+ sequenceType={sequenceType}
118
+ />
119
+ </p>
109
120
  </div>
110
121
  );
111
122
  };
112
123
 
113
124
  const noElementSelectedMessage = 'You have no elements selected. Click in the venn diagram to select.';
114
125
 
115
- function getSelectedMutationsDescription(
116
- selectedDatasetIndex: number | null,
117
- sets: ReturnType<typeof extractSets<string>>,
118
- ) {
126
+ type SelectedMutationsDescriptionProps = {
127
+ selectedDatasetIndex: number | null;
128
+ sets: ReturnType<typeof extractSets<string>>;
129
+ sequenceType: SequenceType;
130
+ };
131
+
132
+ const SelectedMutationsDescription: FunctionComponent<SelectedMutationsDescriptionProps> = ({
133
+ selectedDatasetIndex,
134
+ sets,
135
+ sequenceType,
136
+ }) => {
119
137
  if (selectedDatasetIndex === null) {
120
138
  return noElementSelectedMessage;
121
139
  }
122
140
 
123
141
  const values = sets.datasets[0].data[selectedDatasetIndex].values;
124
142
  const label = sets.datasets[0].data[selectedDatasetIndex].label;
125
- return `${label}: ${values.join(', ')}` || '';
126
- }
143
+ return (
144
+ <span>
145
+ {`${label}: `}
146
+ {values
147
+ .map((value) => SubstitutionClass.parse(value) ?? DeletionClass.parse(value))
148
+ .filter((value) => value !== null)
149
+ .map((value, index) => (
150
+ <Fragment key={value}>
151
+ {index > 0 && ', '}
152
+ <AnnotatedMutation mutation={value} sequenceType={sequenceType} />
153
+ </Fragment>
154
+ ))}
155
+ </span>
156
+ );
157
+ };
@@ -7,7 +7,9 @@ import { MutationComparison, type MutationComparisonProps } from './mutation-com
7
7
  import { LAPIS_URL, NUCLEOTIDE_MUTATIONS_ENDPOINT } from '../../constants';
8
8
  import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
9
9
  import { LapisUrlContextProvider } from '../LapisUrlContext';
10
+ import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
10
11
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
12
+ import { expectMutationAnnotation } from '../shared/stories/expectMutationAnnotation';
11
13
 
12
14
  const dateToSomeDataset = '2022-01-01';
13
15
 
@@ -74,20 +76,39 @@ const meta: Meta<MutationComparisonProps> = {
74
76
 
75
77
  export default meta;
76
78
 
79
+ const mutationAnnotations = [
80
+ {
81
+ name: 'I am a mutation annotation!',
82
+ description: 'This describes what is special about these mutations.',
83
+ symbol: '#',
84
+ nucleotideMutations: ['G199-', 'C3037T'],
85
+ aminoAcidMutations: ['N:G204R'],
86
+ },
87
+ {
88
+ name: 'I am another mutation annotation!',
89
+ description: 'This describes what is special about these other mutations.',
90
+ symbol: '+',
91
+ nucleotideMutations: ['C3037T', 'A23403G'],
92
+ aminoAcidMutations: ['ORF1a:I2230T'],
93
+ },
94
+ ];
95
+
77
96
  const Template: StoryObj<MutationComparisonProps> = {
78
97
  render: (args) => (
79
- <LapisUrlContextProvider value={LAPIS_URL}>
80
- <ReferenceGenomeContext.Provider value={referenceGenome}>
81
- <MutationComparison
82
- lapisFilters={args.lapisFilters}
83
- sequenceType={args.sequenceType}
84
- views={args.views}
85
- width={args.width}
86
- height={args.height}
87
- pageSize={args.pageSize}
88
- />
89
- </ReferenceGenomeContext.Provider>
90
- </LapisUrlContextProvider>
98
+ <MutationAnnotationsContextProvider value={mutationAnnotations}>
99
+ <LapisUrlContextProvider value={LAPIS_URL}>
100
+ <ReferenceGenomeContext.Provider value={referenceGenome}>
101
+ <MutationComparison
102
+ lapisFilters={args.lapisFilters}
103
+ sequenceType={args.sequenceType}
104
+ views={args.views}
105
+ width={args.width}
106
+ height={args.height}
107
+ pageSize={args.pageSize}
108
+ />
109
+ </ReferenceGenomeContext.Provider>
110
+ </LapisUrlContextProvider>
111
+ </MutationAnnotationsContextProvider>
91
112
  ),
92
113
  };
93
114
 
@@ -114,6 +135,9 @@ export const TwoVariants: StoryObj<MutationComparisonProps> = {
114
135
  width: '100%',
115
136
  pageSize: 10,
116
137
  },
138
+ play: async ({ canvasElement }) => {
139
+ await expectMutationAnnotation(canvasElement, 'C3037T');
140
+ },
117
141
  };
118
142
 
119
143
  export const FilterForOnlyDeletions: StoryObj<MutationComparisonProps> = {
@@ -104,6 +104,7 @@ const MutationComparisonTabs: FunctionComponent<MutationComparisonTabsProps> = (
104
104
  data={{ content: filteredData }}
105
105
  proportionInterval={proportionInterval}
106
106
  pageSize={originalComponentProps.pageSize}
107
+ sequenceType={originalComponentProps.sequenceType}
107
108
  />
108
109
  ),
109
110
  };
@@ -115,6 +116,7 @@ const MutationComparisonTabs: FunctionComponent<MutationComparisonTabsProps> = (
115
116
  data={{ content: filteredData }}
116
117
  proportionInterval={proportionInterval}
117
118
  maintainAspectRatio={maintainAspectRatio}
119
+ sequenceType={originalComponentProps.sequenceType}
118
120
  />
119
121
  ),
120
122
  };
@@ -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) => cell.toString(),
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',
@@ -14,7 +14,9 @@ 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';
19
+ import { expectMutationAnnotation } from '../shared/stories/expectMutationAnnotation';
18
20
 
19
21
  const meta: Meta<MutationsProps> = {
20
22
  title: 'Visualization/Mutations',
@@ -37,11 +39,30 @@ const meta: Meta<MutationsProps> = {
37
39
 
38
40
  export default meta;
39
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: ['C241T', 'C3037T'],
48
+ aminoAcidMutations: ['N:G204R', 'N:S235F'],
49
+ },
50
+ {
51
+ name: 'I am another mutation annotation!',
52
+ description: 'This describes what is special about these other mutations.',
53
+ symbol: '+',
54
+ nucleotideMutations: ['C3037T', 'C11750T'],
55
+ aminoAcidMutations: ['ORF1a:S2255F'],
56
+ },
57
+ ];
58
+
40
59
  const Template = {
41
60
  render: (args: MutationsProps) => (
42
61
  <LapisUrlContextProvider value={LAPIS_URL}>
43
62
  <ReferenceGenomeContext.Provider value={referenceGenome}>
44
- <Mutations {...args} />
63
+ <MutationAnnotationsContextProvider value={mutationAnnotations}>
64
+ <Mutations {...args} />
65
+ </MutationAnnotationsContextProvider>
45
66
  </ReferenceGenomeContext.Provider>
46
67
  </LapisUrlContextProvider>
47
68
  ),
@@ -137,3 +158,14 @@ export const GridTab: StoryObj<MutationsProps> = {
137
158
  });
138
159
  },
139
160
  };
161
+
162
+ export const TableTab: StoryObj<MutationsProps> = {
163
+ ...Default,
164
+ args: {
165
+ ...Default.args,
166
+ views: ['table'],
167
+ },
168
+ play: async ({ canvasElement }) => {
169
+ await expectMutationAnnotation(canvasElement, 'C241T');
170
+ },
171
+ };
@@ -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
- <MutationCell mutation={mutation} />
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
- <p>
145
- Count: {value.count} / {value.totalCount} total
146
- </p>
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;