@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.
Files changed (39) hide show
  1. package/custom-elements.json +9 -9
  2. package/dist/{LineageFilterChangedEvent-DkvWdq_G.js → LineageFilterChangedEvent-ixHQkq8y.js} +2 -2
  3. package/dist/{LineageFilterChangedEvent-DkvWdq_G.js.map → LineageFilterChangedEvent-ixHQkq8y.js.map} +1 -1
  4. package/dist/assets/{mutationOverTimeWorker-ChQTFL68.js.map → mutationOverTimeWorker--b8ZHlji.js.map} +1 -1
  5. package/dist/components.d.ts +22 -291
  6. package/dist/components.js +220 -124
  7. package/dist/components.js.map +1 -1
  8. package/dist/style.css +2 -2
  9. package/dist/util.d.ts +25 -292
  10. package/dist/util.js +1 -1
  11. package/package.json +3 -3
  12. package/src/preact/MutationAnnotationsContext.spec.tsx +103 -34
  13. package/src/preact/MutationAnnotationsContext.tsx +49 -7
  14. package/src/preact/components/annotated-mutation.stories.tsx +0 -5
  15. package/src/preact/components/annotated-mutation.tsx +6 -2
  16. package/src/preact/dateRangeFilter/computeInitialValues.spec.ts +2 -2
  17. package/src/preact/dateRangeFilter/computeInitialValues.ts +1 -1
  18. package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +3 -5
  19. package/src/preact/dateRangeFilter/date-range-filter.tsx +12 -7
  20. package/src/preact/dateRangeFilter/dateRangeOption.ts +1 -1
  21. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +3 -1
  22. package/src/preact/mutations/mutations.stories.tsx +4 -1
  23. package/src/preact/mutationsOverTime/mutations-over-time-grid-tooltip.stories.tsx +108 -0
  24. package/src/preact/mutationsOverTime/mutations-over-time-grid-tooltip.tsx +93 -0
  25. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +7 -78
  26. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +3 -1
  27. package/src/preact/sequencesByLocation/leafletStyleModifications.css +5 -0
  28. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +25 -0
  29. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +65 -13
  30. package/src/web-components/MutationAnnotations.mdx +8 -0
  31. package/src/web-components/gs-app.stories.ts +2 -0
  32. package/src/web-components/gs-app.ts +4 -2
  33. package/src/web-components/input/gs-date-range-filter.stories.ts +1 -1
  34. package/src/web-components/input/gs-date-range-filter.tsx +7 -5
  35. package/src/web-components/mutation-annotations-context.ts +6 -2
  36. package/standalone-bundle/assets/mutationOverTimeWorker-jChgWnwp.js.map +1 -1
  37. package/standalone-bundle/dashboard-components.js +5778 -5707
  38. package/standalone-bundle/dashboard-components.js.map +1 -1
  39. 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.code, sequenceType);
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 buttonClassName={'select-text'} modalContent={modalContent} modalRef={modalRef}>
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 unedfined value', () => {
22
- const result = computeInitialValues(undefined, earliestDate, dateRangeOptions);
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 === undefined) {
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: undefined,
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 | undefined>('Last month');
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
- const newValue = (event as CustomEvent).detail;
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?.label === customOption
184
- ? {
185
- dateFrom: state.dateFrom !== undefined ? toYYYYMMDD(state.dateFrom) : undefined,
186
- dateTo: state.dateTo !== undefined ? toYYYYMMDD(state.dateTo) : undefined,
187
- }
188
- : state?.label;
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
  };
@@ -30,7 +30,7 @@ export const dateRangeValueSchema = z
30
30
  dateTo: z.string().date().optional(),
31
31
  }),
32
32
  ])
33
- .optional();
33
+ .nullable();
34
34
 
35
35
  export type DateRangeValue = z.infer<typeof dateRangeValueSchema>;
36
36
 
@@ -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
- type MutationOverTimeMutationValue,
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, type TemporalClass, toTemporalClass, YearMonthDayClass } from '../../utils/temporalClass';
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 content={tooltipContent} position={tooltipPosition}>
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,3 +1,8 @@
1
1
  .leaflet-container {
2
2
  background: transparent;
3
3
  }
4
+
5
+ .leaflet-interactive {
6
+ outline: none;
7
+ cursor: pointer;
8
+ }
@@ -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 = mutationOverTimeDataPerLocation.map(({ location, data }) => ({
101
- title: location,
102
- content: (
103
- <MutationsOverTimeGrid
104
- data={data}
105
- colorScale={colorScale}
106
- pageSizes={originalComponentProps.pageSizes}
107
- sequenceType={originalComponentProps.sequenceType}
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> = ({ colorScale, setColorScale, originalComponentProps }) => {
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
  </>