@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.
- package/custom-elements.json +86 -61
- package/dist/{LineageFilterChangedEvent-COWV-Y0k.js → LineageFilterChangedEvent-DkvWdq_G.js} +2 -2
- package/dist/LineageFilterChangedEvent-DkvWdq_G.js.map +1 -0
- package/dist/assets/{mutationOverTimeWorker-DJcZmEH9.js.map → mutationOverTimeWorker-CPfQDLe6.js.map} +1 -1
- package/dist/components.d.ts +64 -51
- package/dist/components.js +1134 -937
- package/dist/components.js.map +1 -1
- package/dist/style.css +81 -9
- package/dist/util.d.ts +76 -34
- package/dist/util.js +1 -1
- package/package.json +2 -1
- package/src/preact/components/annotated-mutation.stories.tsx +2 -1
- package/src/preact/components/annotated-mutation.tsx +6 -2
- package/src/preact/components/clearable-select.stories.tsx +75 -0
- package/src/preact/components/clearable-select.tsx +76 -0
- package/src/preact/components/downshift-combobox.tsx +9 -7
- package/src/preact/dateRangeFilter/computeInitialValues.spec.ts +31 -33
- package/src/preact/dateRangeFilter/computeInitialValues.ts +2 -15
- package/src/preact/dateRangeFilter/date-picker.tsx +66 -0
- package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +69 -31
- package/src/preact/dateRangeFilter/date-range-filter.tsx +136 -139
- package/src/preact/dateRangeFilter/dateRangeOption.ts +11 -11
- package/src/preact/mutationComparison/mutation-comparison-table.tsx +14 -1
- package/src/preact/mutationComparison/mutation-comparison-venn.tsx +39 -8
- package/src/preact/mutationComparison/mutation-comparison.stories.tsx +36 -12
- package/src/preact/mutationComparison/mutation-comparison.tsx +2 -0
- package/src/preact/mutations/mutations.stories.tsx +3 -9
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +3 -8
- package/src/preact/shared/WithClassName/WithClassName.ts +1 -0
- package/src/preact/shared/icons/DeleteIcon.tsx +3 -0
- package/src/preact/shared/stories/expectMutationAnnotation.ts +13 -0
- package/src/preact/shared/stories/expectOptionSelected.tsx +7 -0
- package/src/utilEntrypoint.ts +3 -1
- package/src/web-components/MutationAnnotations.mdx +33 -0
- package/src/web-components/ResizeContainer.mdx +1 -1
- package/src/web-components/errorHandling.mdx +1 -1
- package/src/web-components/gs-app.ts +2 -2
- package/src/web-components/input/gs-date-range-filter.stories.ts +38 -32
- package/src/web-components/input/gs-date-range-filter.tsx +8 -2
- package/src/web-components/input/gs-lineage-filter.tsx +1 -1
- package/src/web-components/input/gs-location-filter.tsx +1 -1
- package/src/web-components/input/gs-mutation-filter.tsx +1 -1
- package/src/web-components/input/gs-text-filter.tsx +1 -1
- package/src/web-components/visualization/gs-aggregate.tsx +2 -2
- package/src/web-components/visualization/gs-mutation-comparison.stories.ts +18 -1
- package/src/web-components/visualization/gs-mutation-comparison.tsx +24 -10
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +2 -1
- package/src/web-components/visualization/gs-mutations-over-time.tsx +5 -2
- package/src/web-components/visualization/gs-mutations.stories.ts +2 -1
- package/src/web-components/visualization/gs-mutations.tsx +5 -2
- package/src/web-components/visualization/gs-number-sequences-over-time.tsx +2 -2
- package/src/web-components/visualization/gs-prevalence-over-time.tsx +2 -2
- package/src/web-components/visualization/gs-relative-growth-advantage.tsx +2 -2
- package/src/web-components/visualization/gs-sequences-by-location.tsx +2 -2
- package/src/web-components/visualization/gs-statistics.tsx +2 -2
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +2 -2
- package/standalone-bundle/assets/mutationOverTimeWorker-CERZSdcA.js.map +1 -1
- package/standalone-bundle/dashboard-components.js +13293 -12635
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
- package/dist/LineageFilterChangedEvent-COWV-Y0k.js.map +0 -1
|
@@ -1,28 +1,26 @@
|
|
|
1
|
-
import
|
|
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 {
|
|
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
|
|
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
|
|
66
|
-
initialValues
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
dateTo:
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
};
|
|
129
|
+
return new Date(option.dateTo);
|
|
130
|
+
}
|
|
135
131
|
|
|
136
|
-
const onChangeDateFrom = () => {
|
|
137
|
-
if (
|
|
132
|
+
const onChangeDateFrom = (date: Date | undefined) => {
|
|
133
|
+
if (date?.toDateString() === state?.dateFrom?.toDateString()) {
|
|
138
134
|
return;
|
|
139
135
|
}
|
|
140
136
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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 (
|
|
145
|
+
const onChangeDateTo = (date: Date | undefined) => {
|
|
146
|
+
if (date?.toDateString() === state?.dateTo?.toDateString()) {
|
|
157
147
|
return;
|
|
158
148
|
}
|
|
159
149
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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) =>
|
|
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'>
|
|
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
|
-
|
|
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
|
|
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
|
-
<
|
|
80
|
-
<
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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,
|
|
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
|
-
|
|
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 ({
|
|
79
|
-
await
|
|
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 };
|