@genspectrum/dashboard-components 0.16.2 → 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.
Files changed (40) hide show
  1. package/custom-elements.json +72 -7
  2. package/dist/assets/mutationOverTimeWorker-DJcZmEH9.js.map +1 -0
  3. package/dist/components.d.ts +69 -31
  4. package/dist/components.js +305 -148
  5. package/dist/components.js.map +1 -1
  6. package/dist/style.css +16 -0
  7. package/dist/util.d.ts +31 -31
  8. package/package.json +4 -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 +163 -0
  12. package/src/preact/components/annotated-mutation.tsx +80 -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/mutations/mutations-table.tsx +14 -2
  18. package/src/preact/mutations/mutations.stories.tsx +40 -2
  19. package/src/preact/mutations/mutations.tsx +1 -0
  20. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +19 -8
  21. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +34 -5
  22. package/src/preact/mutationsOverTime/mutations-over-time.tsx +13 -1
  23. package/src/preact/sequencesByLocation/sequences-by-location-map.tsx +28 -30
  24. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +6 -1
  25. package/src/web-components/gs-app.spec-d.ts +10 -0
  26. package/src/web-components/gs-app.stories.ts +24 -6
  27. package/src/web-components/gs-app.ts +17 -0
  28. package/src/web-components/mutation-annotations-context.ts +16 -0
  29. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +18 -1
  30. package/src/web-components/visualization/gs-mutations-over-time.tsx +22 -11
  31. package/src/web-components/visualization/gs-mutations.stories.ts +18 -1
  32. package/src/web-components/visualization/gs-mutations.tsx +20 -9
  33. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +11 -1
  34. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +18 -7
  35. package/standalone-bundle/assets/mutationOverTimeWorker-CERZSdcA.js.map +1 -0
  36. package/standalone-bundle/dashboard-components.js +7332 -7202
  37. package/standalone-bundle/dashboard-components.js.map +1 -1
  38. package/standalone-bundle/style.css +1 -1
  39. package/dist/assets/mutationOverTimeWorker-BL50C-yi.js.map +0 -1
  40. package/standalone-bundle/assets/mutationOverTimeWorker-CFB5-Mdk.js.map +0 -1
@@ -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'>
@@ -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',
@@ -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
- <Mutations {...args} />
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
- <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;
@@ -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
- <LapisUrlContextProvider value={LAPIS_URL}>
44
- <ReferenceGenomeContext.Provider value={referenceGenome}>
45
- <MutationsOverTime {...args} />
46
- </ReferenceGenomeContext.Provider>
47
- </LapisUrlContextProvider>
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: <MutationsOverTimeGrid data={filteredData} colorScale={colorScale} />,
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, useModalRef } from '../components/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
- <button
112
- onClick={() => modalRef.current?.showModal()}
113
- className='text-sm absolute bottom-0 px-1 z-[1001] bg-white rounded border cursor-pointer tooltip'
114
- data-tip='Click for detailed information'
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
- </button>
118
- <Modal modalRef={modalRef}>
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
 
@@ -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 data={data} colorScale={colorScale} maxNumberOfGridRows={maxNumberOfGridRows} />
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
+ });
@@ -10,6 +10,7 @@ import { lapisContext } from './lapis-context';
10
10
  import { referenceGenomeContext } from './reference-genome-context';
11
11
  import { withComponentDocs } from '../../.storybook/ComponentDocsBlock';
12
12
  import { LAPIS_URL, REFERENCE_GENOME_ENDPOINT } from '../constants';
13
+ import { type MutationAnnotations, mutationAnnotationsContext } from './mutation-annotations-context';
13
14
  import type { ReferenceGenome } from '../lapisApi/ReferenceGenome';
14
15
  import referenceGenome from '../lapisApi/__mockData__/referenceGenome.json';
15
16
 
@@ -34,18 +35,29 @@ const meta: Meta = {
34
35
 
35
36
  export default meta;
36
37
 
37
- const Template: StoryObj<{ lapis: string }> = {
38
+ type StoryProps = { lapis: string; mutationAnnotations: MutationAnnotations };
39
+
40
+ const Template: StoryObj<StoryProps> = {
38
41
  render: (args) => {
39
- return html` <gs-app lapis="${args.lapis}">
42
+ return html` <gs-app lapis="${args.lapis}" .mutationAnnotations="${args.mutationAnnotations}">
40
43
  <gs-app-display></gs-app-display>
41
44
  </gs-app>`;
42
45
  },
43
46
  args: {
44
47
  lapis: LAPIS_URL,
48
+ mutationAnnotations: [
49
+ {
50
+ name: 'I am an annotation!',
51
+ description: 'This describes what is special about these mutations.',
52
+ symbol: '*',
53
+ nucleotideMutations: ['C44T', 'C774T', 'G24872T', 'T23011-'],
54
+ aminoAcidMutations: ['S:501Y', 'S:S31-', 'ORF1a:S4286C'],
55
+ },
56
+ ],
45
57
  },
46
58
  };
47
59
 
48
- export const Default: StoryObj<{ lapis: string }> = {
60
+ export const Default: StoryObj<StoryProps> = {
49
61
  ...Template,
50
62
  play: async ({ canvasElement }) => {
51
63
  const canvas = within(canvasElement);
@@ -53,11 +65,12 @@ export const Default: StoryObj<{ lapis: string }> = {
53
65
  await waitFor(async () => {
54
66
  await expect(canvas.getByText(LAPIS_URL)).toBeVisible();
55
67
  await expect(canvas.getByText('"name": "ORF1a",', { exact: false })).toBeVisible();
68
+ await expect(canvas.getByText('I am an annotation!', { exact: false })).toBeVisible();
56
69
  });
57
70
  },
58
71
  };
59
72
 
60
- export const WithNoLapisUrl: StoryObj<{ lapis: string }> = {
73
+ export const WithNoLapisUrl: StoryObj<StoryProps> = {
61
74
  ...Default,
62
75
  args: {
63
76
  ...Default.args,
@@ -72,7 +85,7 @@ export const WithNoLapisUrl: StoryObj<{ lapis: string }> = {
72
85
  },
73
86
  };
74
87
 
75
- export const DelayFetchingReferenceGenome: StoryObj<{ lapis: string }> = {
88
+ export const DelayFetchingReferenceGenome: StoryObj<StoryProps> = {
76
89
  ...Template,
77
90
  parameters: {
78
91
  fetchMock: {
@@ -95,7 +108,7 @@ export const DelayFetchingReferenceGenome: StoryObj<{ lapis: string }> = {
95
108
  },
96
109
  };
97
110
 
98
- export const FailsToFetchReferenceGenome: StoryObj<{ lapis: string }> = {
111
+ export const FailsToFetchReferenceGenome: StoryObj<StoryProps> = {
99
112
  ...Template,
100
113
  args: {
101
114
  lapis: 'https://url.to.lapis-definitely-not-a-valid-url',
@@ -121,6 +134,9 @@ class AppDisplay extends LitElement {
121
134
  genes: [],
122
135
  };
123
136
 
137
+ @consume({ context: mutationAnnotationsContext, subscribe: true })
138
+ mutationAnnotations: MutationAnnotations = [];
139
+
124
140
  override render() {
125
141
  return html`
126
142
  <h1 class="text-xl font-bold">Dummy component</h1>
@@ -132,6 +148,8 @@ class AppDisplay extends LitElement {
132
148
  <p>${this.lapis}</p>
133
149
  <h2 class="text-lg font-bold">Reference genomes</h2>
134
150
  <pre><code>${JSON.stringify(this.referenceGenome, null, 2)}</code></pre>
151
+ <h2 class="text-lg font-bold">Mutation annotations</h2>
152
+ <pre><code>${JSON.stringify(this.mutationAnnotations, null, 2)}</code></pre>
135
153
  `;
136
154
  }
137
155
 
@@ -6,6 +6,7 @@ import type { DetailedHTMLProps, HTMLAttributes } from 'react';
6
6
  import z from 'zod';
7
7
 
8
8
  import { lapisContext } from './lapis-context';
9
+ import { mutationAnnotationsContext } from './mutation-annotations-context';
9
10
  import { referenceGenomeContext } from './reference-genome-context';
10
11
  import { type ReferenceGenome } from '../lapisApi/ReferenceGenome';
11
12
  import { fetchReferenceGenome } from '../lapisApi/lapisApi';
@@ -21,6 +22,7 @@ const lapisUrlSchema = z.string().url();
21
22
  * It makes use of the [Lit Context](https://lit.dev/docs/data/context/) to
22
23
  * - provide the URL to the LAPIS instance to all its children
23
24
  * - fetch the reference genomes from LAPIS and provide it to all its children
25
+ * - distribute the mutation annotations config to its children
24
26
  *
25
27
  * This will show an error message if the reference genome cannot be fetched
26
28
  * (e.g., due to an invalid LAPIS URL).
@@ -40,6 +42,21 @@ export class AppComponent extends LitElement {
40
42
  @property()
41
43
  lapis: string = '';
42
44
 
45
+ /**
46
+ * Supply lists of mutations that are especially relevant for the current organism.
47
+ * Whenever other components display mutations, matching mutations will be highlighted by appending the `symbol`.
48
+ * On hover, a tooltip with the `name` and `description` will be shown.
49
+ */
50
+ @provide({ context: mutationAnnotationsContext })
51
+ @property({ type: Array })
52
+ mutationAnnotations: {
53
+ name: string;
54
+ description: string;
55
+ symbol: string;
56
+ nucleotideMutations: string[];
57
+ aminoAcidMutations: string[];
58
+ }[] = [];
59
+
43
60
  /**
44
61
  * @internal
45
62
  */
@@ -0,0 +1,16 @@
1
+ import { createContext } from '@lit/context';
2
+ import z from 'zod';
3
+
4
+ const mutationAnnotationSchema = z.object({
5
+ name: z.string(),
6
+ description: z.string(),
7
+ symbol: z.string(),
8
+ nucleotideMutations: z.array(z.string()),
9
+ aminoAcidMutations: z.array(z.string()),
10
+ });
11
+ export type MutationAnnotation = z.infer<typeof mutationAnnotationSchema>;
12
+
13
+ export const mutationAnnotationsSchema = z.array(mutationAnnotationSchema);
14
+ export type MutationAnnotations = z.infer<typeof mutationAnnotationsSchema>;
15
+
16
+ export const mutationAnnotationsContext = createContext<MutationAnnotations>(Symbol('mutation-annotations-context'));
@@ -64,9 +64,26 @@ const meta: Meta<Required<MutationsOverTimeProps>> = {
64
64
 
65
65
  export default meta;
66
66
 
67
+ const mutationAnnotations = [
68
+ {
69
+ name: 'I am a mutation annotation!',
70
+ description: 'This describes what is special about these mutations.',
71
+ symbol: '#',
72
+ nucleotideMutations: ['C44T', 'C774T', 'G24872T', 'T23011-'],
73
+ aminoAcidMutations: ['S:501Y', 'S:S31-', 'ORF1a:S4286C'],
74
+ },
75
+ {
76
+ name: 'I am another mutation annotation!',
77
+ description: 'This describes what is special about these other mutations.',
78
+ symbol: '+',
79
+ nucleotideMutations: ['C44T', 'A13121T'],
80
+ aminoAcidMutations: ['S:501Y', 'S:S31-', 'ORF1a:S4286C'],
81
+ },
82
+ ];
83
+
67
84
  const Template: StoryObj<Required<MutationsOverTimeProps>> = {
68
85
  render: (args) => html`
69
- <gs-app lapis="${LAPIS_URL}">
86
+ <gs-app lapis="${LAPIS_URL}" .mutationAnnotations=${mutationAnnotations}>
70
87
  <gs-mutations-over-time
71
88
  .lapisFilter=${args.lapisFilter}
72
89
  .sequenceType=${args.sequenceType}