@genspectrum/dashboard-components 0.16.3 → 0.17.0

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 (61) hide show
  1. package/custom-elements.json +86 -61
  2. package/dist/{LineageFilterChangedEvent-COWV-Y0k.js → LineageFilterChangedEvent-DkvWdq_G.js} +2 -2
  3. package/dist/LineageFilterChangedEvent-DkvWdq_G.js.map +1 -0
  4. package/dist/assets/{mutationOverTimeWorker-DJcZmEH9.js.map → mutationOverTimeWorker-CPfQDLe6.js.map} +1 -1
  5. package/dist/components.d.ts +64 -51
  6. package/dist/components.js +1134 -937
  7. package/dist/components.js.map +1 -1
  8. package/dist/style.css +81 -9
  9. package/dist/util.d.ts +76 -34
  10. package/dist/util.js +1 -1
  11. package/package.json +2 -1
  12. package/src/preact/components/annotated-mutation.stories.tsx +2 -1
  13. package/src/preact/components/annotated-mutation.tsx +6 -2
  14. package/src/preact/components/clearable-select.stories.tsx +75 -0
  15. package/src/preact/components/clearable-select.tsx +76 -0
  16. package/src/preact/components/downshift-combobox.tsx +9 -7
  17. package/src/preact/dateRangeFilter/computeInitialValues.spec.ts +31 -33
  18. package/src/preact/dateRangeFilter/computeInitialValues.ts +2 -15
  19. package/src/preact/dateRangeFilter/date-picker.tsx +66 -0
  20. package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +69 -31
  21. package/src/preact/dateRangeFilter/date-range-filter.tsx +136 -139
  22. package/src/preact/dateRangeFilter/dateRangeOption.ts +11 -11
  23. package/src/preact/mutationComparison/mutation-comparison-table.tsx +14 -1
  24. package/src/preact/mutationComparison/mutation-comparison-venn.tsx +39 -8
  25. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +36 -12
  26. package/src/preact/mutationComparison/mutation-comparison.tsx +2 -0
  27. package/src/preact/mutations/mutations.stories.tsx +3 -9
  28. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +3 -8
  29. package/src/preact/shared/WithClassName/WithClassName.ts +1 -0
  30. package/src/preact/shared/icons/DeleteIcon.tsx +3 -0
  31. package/src/preact/shared/stories/expectMutationAnnotation.ts +13 -0
  32. package/src/preact/shared/stories/expectOptionSelected.tsx +7 -0
  33. package/src/utilEntrypoint.ts +3 -1
  34. package/src/web-components/MutationAnnotations.mdx +33 -0
  35. package/src/web-components/ResizeContainer.mdx +1 -1
  36. package/src/web-components/errorHandling.mdx +1 -1
  37. package/src/web-components/gs-app.ts +2 -2
  38. package/src/web-components/input/gs-date-range-filter.stories.ts +38 -32
  39. package/src/web-components/input/gs-date-range-filter.tsx +8 -2
  40. package/src/web-components/input/gs-lineage-filter.tsx +1 -1
  41. package/src/web-components/input/gs-location-filter.tsx +1 -1
  42. package/src/web-components/input/gs-mutation-filter.tsx +1 -1
  43. package/src/web-components/input/gs-text-filter.tsx +1 -1
  44. package/src/web-components/visualization/gs-aggregate.tsx +2 -2
  45. package/src/web-components/visualization/gs-mutation-comparison.stories.ts +18 -1
  46. package/src/web-components/visualization/gs-mutation-comparison.tsx +24 -10
  47. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +2 -1
  48. package/src/web-components/visualization/gs-mutations-over-time.tsx +5 -2
  49. package/src/web-components/visualization/gs-mutations.stories.ts +2 -1
  50. package/src/web-components/visualization/gs-mutations.tsx +5 -2
  51. package/src/web-components/visualization/gs-number-sequences-over-time.tsx +2 -2
  52. package/src/web-components/visualization/gs-prevalence-over-time.tsx +2 -2
  53. package/src/web-components/visualization/gs-relative-growth-advantage.tsx +2 -2
  54. package/src/web-components/visualization/gs-sequences-by-location.tsx +2 -2
  55. package/src/web-components/visualization/gs-statistics.tsx +2 -2
  56. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +2 -2
  57. package/standalone-bundle/assets/mutationOverTimeWorker-CERZSdcA.js.map +1 -1
  58. package/standalone-bundle/dashboard-components.js +13293 -12635
  59. package/standalone-bundle/dashboard-components.js.map +1 -1
  60. package/standalone-bundle/style.css +1 -1
  61. package/dist/LineageFilterChangedEvent-COWV-Y0k.js.map +0 -1
@@ -1,28 +1,26 @@
1
- import flatpickr from 'flatpickr';
2
- import 'flatpickr/dist/flatpickr.min.css';
3
- import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
4
2
  import z from 'zod';
5
3
 
6
4
  import { computeInitialValues } from './computeInitialValues';
5
+ import { DatePicker } from './date-picker';
7
6
  import { toYYYYMMDD } from './dateConversion';
8
7
  import {
8
+ type DateRangeOption,
9
9
  DateRangeOptionChangedEvent,
10
10
  dateRangeOptionSchema,
11
- type DateRangeSelectOption,
12
11
  dateRangeValueSchema,
13
12
  } from './dateRangeOption';
14
- import { getDatesForSelectorValue, getSelectableOptions } from './selectableOptions';
13
+ import { ClearableSelect } from '../components/clearable-select';
15
14
  import { ErrorBoundary } from '../components/error-boundary';
16
- import { Select } from '../components/select';
17
- import type { ScaleType } from '../shared/charts/getYAxisScale';
18
15
 
19
16
  const customOption = 'Custom';
20
17
 
21
18
  const dateRangeFilterInnerPropsSchema = z.object({
22
19
  dateRangeOptions: z.array(dateRangeOptionSchema),
23
20
  earliestDate: z.string().date(),
24
- value: dateRangeValueSchema.optional(),
21
+ value: dateRangeValueSchema,
25
22
  lapisDateField: z.string().min(1),
23
+ placeholder: z.string().optional(),
26
24
  });
27
25
 
28
26
  const dateRangeFilterPropsSchema = dateRangeFilterInnerPropsSchema.extend({
@@ -32,6 +30,12 @@ const dateRangeFilterPropsSchema = dateRangeFilterInnerPropsSchema.extend({
32
30
  export type DateRangeFilterProps = z.infer<typeof dateRangeFilterPropsSchema>;
33
31
  export type DateRangeFilterInnerProps = z.infer<typeof dateRangeFilterInnerPropsSchema>;
34
32
 
33
+ type DateRangeFilterState = {
34
+ label: string;
35
+ dateFrom?: Date;
36
+ dateTo?: Date;
37
+ } | null;
38
+
35
39
  export const DateRangeFilter = (props: DateRangeFilterProps) => {
36
40
  const { width, ...innerProps } = props;
37
41
  const size = { width, height: '3rem' };
@@ -50,135 +54,116 @@ export const DateRangeFilterInner = ({
50
54
  earliestDate = '1900-01-01',
51
55
  value,
52
56
  lapisDateField,
57
+ placeholder,
53
58
  }: DateRangeFilterInnerProps) => {
54
59
  const initialValues = useMemo(
55
60
  () => computeInitialValues(value, earliestDate, dateRangeOptions),
56
61
  [value, earliestDate, dateRangeOptions],
57
62
  );
58
63
 
59
- const fromDatePickerRef = useRef<HTMLInputElement>(null);
60
- const toDatePickerRef = useRef<HTMLInputElement>(null);
61
64
  const divRef = useRef<HTMLDivElement>(null);
62
- const [dateFromPicker, setDateFromPicker] = useState<flatpickr.Instance | null>(null);
63
- const [dateToPicker, setDateToPicker] = useState<flatpickr.Instance | null>(null);
64
65
 
65
- const [selectedDateRange, setSelectedDateRange] = useState<string | undefined>(
66
- initialValues.initialSelectedDateRange,
66
+ const getInitialState = useCallback(() => {
67
+ if (!initialValues) {
68
+ return null;
69
+ }
70
+ return initialValues.initialSelectedDateRange
71
+ ? {
72
+ label: initialValues.initialSelectedDateRange,
73
+ dateFrom: initialValues.initialSelectedDateFrom,
74
+ dateTo: initialValues.initialSelectedDateTo,
75
+ }
76
+ : {
77
+ label: customOption,
78
+ dateFrom: initialValues.initialSelectedDateFrom,
79
+ dateTo: initialValues.initialSelectedDateTo,
80
+ };
81
+ }, [initialValues]);
82
+
83
+ const customComboboxValue = { label: customOption };
84
+ const [options, setOptions] = useState(
85
+ getInitialState()?.label === customOption ? [...dateRangeOptions, customComboboxValue] : [...dateRangeOptions],
67
86
  );
87
+ const [state, setState] = useState<DateRangeFilterState>(getInitialState());
68
88
 
69
- const [selectedDates, setSelectedDates] = useState<{ dateFrom: Date; dateTo: Date }>({
70
- dateFrom: initialValues.initialSelectedDateFrom,
71
- dateTo: initialValues.initialSelectedDateTo,
72
- });
89
+ function updateState(newState: DateRangeFilterState) {
90
+ setState(newState);
91
+ fireFilterChangedEvent({ dateFrom: newState?.dateFrom, dateTo: newState?.dateTo, lapisDateField });
92
+ fireOptionChangedEvent(newState);
93
+ }
73
94
 
74
95
  useEffect(() => {
75
- setSelectedDateRange(initialValues.initialSelectedDateRange);
76
- setSelectedDates({
77
- dateFrom: initialValues.initialSelectedDateFrom,
78
- dateTo: initialValues.initialSelectedDateTo,
79
- });
80
-
81
- const commonConfig = {
82
- allowInput: true,
83
- dateFormat: 'Y-m-d',
84
- };
85
-
86
- if (fromDatePickerRef.current) {
87
- setDateFromPicker(
88
- flatpickr(fromDatePickerRef.current, {
89
- ...commonConfig,
90
- defaultDate: initialValues.initialSelectedDateFrom,
91
- }),
92
- );
96
+ setState(getInitialState());
97
+ }, [getInitialState]);
98
+
99
+ const onSelectChange = (option: DateRangeOption | null) => {
100
+ updateState(
101
+ option !== null
102
+ ? {
103
+ label: option?.label,
104
+ dateFrom: getFromDate(option, earliestDate),
105
+ dateTo: getToDate(option),
106
+ }
107
+ : null,
108
+ );
109
+ if (option?.label !== customOption) {
110
+ setOptions([...dateRangeOptions]);
93
111
  }
112
+ };
94
113
 
95
- if (toDatePickerRef.current) {
96
- setDateToPicker(
97
- flatpickr(toDatePickerRef.current, {
98
- ...commonConfig,
99
- defaultDate: initialValues.initialSelectedDateTo,
100
- }),
101
- );
114
+ function getFromDate(option: DateRangeOption | null, earliestDate: string) {
115
+ if (!option || option.label === customOption) {
116
+ return undefined;
102
117
  }
118
+ return new Date(option?.dateFrom ?? earliestDate);
119
+ }
103
120
 
104
- return () => {
105
- setDateFromPicker((prev) => {
106
- prev?.destroy();
107
- return null;
108
- });
109
- setDateToPicker((prev) => {
110
- prev?.destroy();
111
- return null;
112
- });
113
- };
114
- }, [fromDatePickerRef, toDatePickerRef, initialValues]);
115
-
116
- const onSelectChange = (value: string) => {
117
- setSelectedDateRange(value);
118
-
119
- const dateRange = getDatesForSelectorValue(value, dateRangeOptions, earliestDate);
120
-
121
- dateToPicker?.set('minDate', dateRange.dateFrom);
122
- dateFromPicker?.set('maxDate', dateRange.dateTo);
123
-
124
- dateFromPicker?.setDate(dateRange.dateFrom);
125
- dateToPicker?.setDate(dateRange.dateTo);
126
-
127
- setSelectedDates({
128
- dateFrom: dateRange.dateFrom,
129
- dateTo: dateRange.dateTo,
130
- });
121
+ function getToDate(option: DateRangeOption | null) {
122
+ if (!option || option.label === customOption) {
123
+ return undefined;
124
+ }
125
+ if (!option.dateTo) {
126
+ return new Date();
127
+ }
131
128
 
132
- fireFilterChangedEvent();
133
- fireOptionChangedEvent(value);
134
- };
129
+ return new Date(option.dateTo);
130
+ }
135
131
 
136
- const onChangeDateFrom = () => {
137
- if (selectedDates.dateFrom.toDateString() === dateFromPicker?.selectedDates[0].toDateString()) {
132
+ const onChangeDateFrom = (date: Date | undefined) => {
133
+ if (date?.toDateString() === state?.dateFrom?.toDateString()) {
138
134
  return;
139
135
  }
140
136
 
141
- const dateTo = dateToPicker?.selectedDates[0];
142
- const dateFrom = dateFromPicker?.selectedDates[0];
143
-
144
- selectedDates.dateFrom = dateFrom || new Date();
145
- dateToPicker?.set('minDate', dateFrom);
146
- setSelectedDateRange(customOption);
147
-
148
- fireFilterChangedEvent();
149
- fireOptionChangedEvent({
150
- dateFrom: dateFrom !== undefined ? toYYYYMMDD(dateFrom) : earliestDate,
151
- dateTo: toYYYYMMDD(dateTo || new Date()),
137
+ updateState({
138
+ label: customOption,
139
+ dateFrom: date,
140
+ dateTo: state?.dateTo,
152
141
  });
142
+ setOptions([...dateRangeOptions, customComboboxValue]);
153
143
  };
154
144
 
155
- const onChangeDateTo = () => {
156
- if (selectedDates.dateTo.toDateString() === dateToPicker?.selectedDates[0].toDateString()) {
145
+ const onChangeDateTo = (date: Date | undefined) => {
146
+ if (date?.toDateString() === state?.dateTo?.toDateString()) {
157
147
  return;
158
148
  }
159
149
 
160
- const dateTo = dateToPicker?.selectedDates[0];
161
- const dateFrom = dateFromPicker?.selectedDates[0];
162
-
163
- selectedDates.dateTo = dateTo || new Date();
164
- dateFromPicker?.set('maxDate', dateTo);
165
- setSelectedDateRange(customOption);
166
-
167
- fireFilterChangedEvent();
168
- fireOptionChangedEvent({
169
- dateFrom: dateFrom !== undefined ? toYYYYMMDD(dateFrom) : earliestDate,
170
- dateTo: toYYYYMMDD(dateTo || new Date()),
150
+ updateState({
151
+ label: customOption,
152
+ dateFrom: state?.dateFrom,
153
+ dateTo: date,
171
154
  });
155
+ setOptions([...dateRangeOptions, customComboboxValue]);
172
156
  };
173
157
 
174
- const fireOptionChangedEvent = (option: DateRangeSelectOption) => {
175
- divRef.current?.dispatchEvent(new DateRangeOptionChangedEvent(option));
176
- };
177
-
178
- const fireFilterChangedEvent = () => {
179
- const dateFrom = dateFromPicker?.selectedDates[0];
180
- const dateTo = dateToPicker?.selectedDates[0];
181
-
158
+ const fireFilterChangedEvent = ({
159
+ dateFrom,
160
+ dateTo,
161
+ lapisDateField,
162
+ }: {
163
+ dateFrom: Date | undefined;
164
+ dateTo: Date | undefined;
165
+ lapisDateField: string;
166
+ }) => {
182
167
  const detail = {
183
168
  ...(dateFrom !== undefined && { [`${lapisDateField}From`]: toYYYYMMDD(dateFrom) }),
184
169
  ...(dateTo !== undefined && { [`${lapisDateField}To`]: toYYYYMMDD(dateTo) }),
@@ -193,39 +178,51 @@ export const DateRangeFilterInner = ({
193
178
  );
194
179
  };
195
180
 
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;
189
+
190
+ divRef.current?.dispatchEvent(new DateRangeOptionChangedEvent(eventDetail));
191
+ };
192
+
196
193
  return (
197
- <div class='flex flex-wrap' ref={divRef}>
198
- <Select
199
- items={[
200
- ...getSelectableOptions(dateRangeOptions),
201
- { label: customOption, value: customOption, disabled: true },
202
- ]}
203
- selected={selectedDateRange ?? customOption}
204
- selectStyle='select-bordered rounded-none flex-grow min-w-[7.5rem]'
205
- onChange={(event: Event) => {
206
- event.preventDefault();
207
- const select = event.target as HTMLSelectElement;
208
- const value = select.value as ScaleType;
209
- onSelectChange(value);
210
- }}
211
- />
212
- <div className={'flex flex-wrap flex-grow'}>
213
- <input
214
- class='input input-bordered rounded-none flex-grow w-[7.5rem]'
215
- type='text'
216
- placeholder='Date from'
217
- ref={fromDatePickerRef}
218
- onChange={onChangeDateFrom}
219
- onBlur={onChangeDateFrom}
220
- />
221
- <input
222
- class='input input-bordered rounded-none flex-grow w-[7.5rem]'
223
- type='text'
224
- placeholder='Date to'
225
- ref={toDatePickerRef}
226
- onChange={onChangeDateTo}
227
- onBlur={onChangeDateTo}
228
- />
194
+ <div className={'@container'} ref={divRef}>
195
+ <div className='flex min-w-[7.5rem] flex-col @md:flex-row'>
196
+ <div className='flex-grow'>
197
+ <ClearableSelect
198
+ items={options.map((item) => item.label)}
199
+ placeholderText={placeholder}
200
+ onChange={(value) => {
201
+ const dateRangeOption = options.find((item) => item.label === value);
202
+ onSelectChange(dateRangeOption ?? null);
203
+ }}
204
+ value={state?.label ?? null}
205
+ selectClassName={'rounded-t-md rounded-b-none @md:rounded-l-md @md:rounded-r-none'}
206
+ />
207
+ </div>
208
+ <div className={'flex flex-grow flex-col @4xs:flex-row'}>
209
+ <DatePicker
210
+ className={'flex-grow min-w-[7.5rem] @4xs:rounded-bl-md @md:rounded-l-none rounded-none'}
211
+ value={state?.dateFrom}
212
+ onChange={onChangeDateFrom}
213
+ maxDate={state?.dateTo}
214
+ placeholderText={'Date from'}
215
+ />
216
+ <DatePicker
217
+ className={
218
+ 'flex-grow min-w-[7.5rem] rounded-b-md rounded-t-none @4xs:rounded-tr-none @4xs:rounded-l-none @md:rounded-r-md '
219
+ }
220
+ value={state?.dateTo}
221
+ onChange={onChangeDateTo}
222
+ minDate={state?.dateFrom}
223
+ placeholderText={'Date to'}
224
+ />
225
+ </div>
229
226
  </div>
230
227
  </div>
231
228
  );
@@ -22,20 +22,20 @@ export const dateRangeOptionSchema = z.object({
22
22
 
23
23
  export type DateRangeOption = z.infer<typeof dateRangeOptionSchema>;
24
24
 
25
- export const dateRangeValueSchema = z.union([
26
- z.string(),
27
- z.object({
28
- dateFrom: z.string().date().optional(),
29
- dateTo: z.string().date().optional(),
30
- }),
31
- ]);
25
+ export const dateRangeValueSchema = z
26
+ .union([
27
+ z.string(),
28
+ z.object({
29
+ dateFrom: z.string().date().optional(),
30
+ dateTo: z.string().date().optional(),
31
+ }),
32
+ ])
33
+ .optional();
32
34
 
33
35
  export type DateRangeValue = z.infer<typeof dateRangeValueSchema>;
34
36
 
35
- export type DateRangeSelectOption = Required<DateRangeValue>;
36
-
37
- export class DateRangeOptionChangedEvent extends CustomEvent<DateRangeSelectOption> {
38
- constructor(detail: DateRangeSelectOption) {
37
+ export class DateRangeOptionChangedEvent extends CustomEvent<DateRangeValue> {
38
+ constructor(detail: DateRangeValue) {
39
39
  super('gs-date-range-option-changed', {
40
40
  detail,
41
41
  bubbles: true,
@@ -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
  };
@@ -1,5 +1,5 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
- import { expect, userEvent, waitFor, within } from '@storybook/test';
2
+ import { expect, waitFor, within } from '@storybook/test';
3
3
 
4
4
  import nucleotideInsertions from './__mockData__/nucleotideInsertions.json';
5
5
  import nucleotideMutations from './__mockData__/nucleotideMutations.json';
@@ -16,6 +16,7 @@ import overallVariantCount from '../../preact/mutations/__mockData__/overallVari
16
16
  import { LapisUrlContextProvider } from '../LapisUrlContext';
17
17
  import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
18
18
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
19
+ import { expectMutationAnnotation } from '../shared/stories/expectMutationAnnotation';
19
20
 
20
21
  const meta: Meta<MutationsProps> = {
21
22
  title: 'Visualization/Mutations',
@@ -165,13 +166,6 @@ export const TableTab: StoryObj<MutationsProps> = {
165
166
  views: ['table'],
166
167
  },
167
168
  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());
169
+ await expectMutationAnnotation(canvasElement, 'C241T');
176
170
  },
177
171
  };
@@ -8,6 +8,7 @@ import { LapisUrlContextProvider } from '../LapisUrlContext';
8
8
  import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
9
9
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
10
10
  import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectErrorMessage';
11
+ import { expectMutationAnnotation } from '../shared/stories/expectMutationAnnotation';
11
12
 
12
13
  const meta: Meta<MutationsOverTimeProps> = {
13
14
  title: 'Visualization/Mutation over time',
@@ -75,14 +76,8 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
75
76
  lapisDateField: 'date',
76
77
  initialMeanProportionInterval: { min: 0.05, max: 0.9 },
77
78
  },
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());
79
+ play: async ({ canvasElement }) => {
80
+ await expectMutationAnnotation(canvasElement, 'C44T');
86
81
  },
87
82
  };
88
83
 
@@ -0,0 +1 @@
1
+ export type WithClassName<T = object> = T & { className?: string };
@@ -0,0 +1,3 @@
1
+ export function DeleteIcon() {
2
+ return <>×</>;
3
+ }