@genspectrum/dashboard-components 0.3.2 → 0.4.1
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 +45 -26
- package/dist/dashboard-components.js +518 -434
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +21 -12
- package/dist/style.css +150 -229
- package/package.json +3 -1
- package/src/preact/aggregatedData/aggregate.tsx +1 -1
- package/src/preact/components/SegmentSelector.tsx +0 -1
- package/src/preact/components/checkbox-selector.tsx +7 -9
- package/src/preact/components/dropdown.tsx +40 -0
- package/src/preact/components/info.stories.tsx +8 -8
- package/src/preact/components/info.tsx +38 -19
- package/src/preact/components/mutation-type-selector.tsx +0 -1
- package/src/preact/components/proportion-selector-dropdown.tsx +9 -18
- package/src/preact/components/tabs.tsx +12 -3
- package/src/preact/dateRangeSelector/computeInitialValues.spec.ts +99 -0
- package/src/preact/dateRangeSelector/computeInitialValues.ts +73 -0
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +93 -4
- package/src/preact/dateRangeSelector/date-range-selector.tsx +49 -106
- package/src/preact/dateRangeSelector/selectableOptions.ts +79 -0
- package/src/preact/locationFilter/location-filter.tsx +1 -1
- package/src/preact/mutationComparison/mutation-comparison.tsx +3 -3
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +3 -6
- package/src/preact/mutationFilter/mutation-filter.tsx +48 -54
- package/src/preact/mutations/mutations.tsx +3 -4
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +3 -5
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +3 -3
- package/src/preact/shared/floating-ui/hooks.ts +83 -0
- package/src/web-components/input/gs-date-range-selector.stories.ts +11 -5
- package/src/web-components/input/gs-date-range-selector.tsx +22 -5
- package/src/web-components/input/gs-location-filter.stories.ts +6 -7
- package/src/web-components/input/gs-location-filter.tsx +3 -2
- package/src/web-components/input/gs-mutation-filter.stories.ts +1 -8
- package/src/web-components/input/gs-mutation-filter.tsx +1 -9
- package/src/web-components/visualization/gs-prevalence-over-time.tsx +1 -1
|
@@ -2,14 +2,19 @@ import flatpickr from 'flatpickr';
|
|
|
2
2
|
import 'flatpickr/dist/flatpickr.min.css';
|
|
3
3
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
4
4
|
|
|
5
|
+
import { computeInitialValues } from './computeInitialValues';
|
|
5
6
|
import { toYYYYMMDD } from './dateConversion';
|
|
7
|
+
import {
|
|
8
|
+
type CustomSelectOption,
|
|
9
|
+
getDatesForSelectorValue,
|
|
10
|
+
getSelectableOptions,
|
|
11
|
+
type PresetOptionValues,
|
|
12
|
+
} from './selectableOptions';
|
|
6
13
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
7
14
|
import { ResizeContainer } from '../components/resize-container';
|
|
8
15
|
import { Select } from '../components/select';
|
|
9
16
|
import type { ScaleType } from '../shared/charts/getYAxisScale';
|
|
10
17
|
|
|
11
|
-
export type CustomSelectOption<CustomLabel extends string> = { label: CustomLabel; dateFrom: string; dateTo: string };
|
|
12
|
-
|
|
13
18
|
export interface DateRangeSelectorProps<CustomLabel extends string> extends DateRangeSelectorPropsInner<CustomLabel> {
|
|
14
19
|
width: string;
|
|
15
20
|
}
|
|
@@ -18,35 +23,19 @@ export interface DateRangeSelectorPropsInner<CustomLabel extends string> {
|
|
|
18
23
|
customSelectOptions: CustomSelectOption<CustomLabel>[];
|
|
19
24
|
earliestDate?: string;
|
|
20
25
|
initialValue?: PresetOptionValues | CustomLabel;
|
|
26
|
+
initialDateFrom?: string;
|
|
27
|
+
initialDateTo?: string;
|
|
21
28
|
dateColumn: string;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
export const PRESET_VALUE_CUSTOM = 'custom';
|
|
25
|
-
export const PRESET_VALUE_ALL_TIMES = 'allTimes';
|
|
26
|
-
export const PRESET_VALUE_LAST_2_WEEKS = 'last2Weeks';
|
|
27
|
-
export const PRESET_VALUE_LAST_MONTH = 'lastMonth';
|
|
28
|
-
export const PRESET_VALUE_LAST_2_MONTHS = 'last2Months';
|
|
29
|
-
export const PRESET_VALUE_LAST_3_MONTHS = 'last3Months';
|
|
30
|
-
export const PRESET_VALUE_LAST_6_MONTHS = 'last6Months';
|
|
31
|
-
|
|
32
|
-
export const presets = {
|
|
33
|
-
[PRESET_VALUE_CUSTOM]: { label: 'Custom' },
|
|
34
|
-
[PRESET_VALUE_ALL_TIMES]: { label: 'All times' },
|
|
35
|
-
[PRESET_VALUE_LAST_2_WEEKS]: { label: 'Last 2 weeks' },
|
|
36
|
-
[PRESET_VALUE_LAST_MONTH]: { label: 'Last month' },
|
|
37
|
-
[PRESET_VALUE_LAST_2_MONTHS]: { label: 'Last 2 months' },
|
|
38
|
-
[PRESET_VALUE_LAST_3_MONTHS]: { label: 'Last 3 months' },
|
|
39
|
-
[PRESET_VALUE_LAST_6_MONTHS]: { label: 'Last 6 months' },
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type PresetOptionValues = keyof typeof presets;
|
|
43
|
-
|
|
44
31
|
export const DateRangeSelector = <CustomLabel extends string>({
|
|
45
32
|
customSelectOptions,
|
|
46
33
|
earliestDate = '1900-01-01',
|
|
47
34
|
initialValue,
|
|
48
35
|
width,
|
|
49
36
|
dateColumn,
|
|
37
|
+
initialDateFrom,
|
|
38
|
+
initialDateTo,
|
|
50
39
|
}: DateRangeSelectorProps<CustomLabel>) => {
|
|
51
40
|
const size = { width, height: '3rem' };
|
|
52
41
|
|
|
@@ -58,6 +47,8 @@ export const DateRangeSelector = <CustomLabel extends string>({
|
|
|
58
47
|
earliestDate={earliestDate}
|
|
59
48
|
initialValue={initialValue}
|
|
60
49
|
dateColumn={dateColumn}
|
|
50
|
+
initialDateFrom={initialDateFrom}
|
|
51
|
+
initialDateTo={initialDateTo}
|
|
61
52
|
/>
|
|
62
53
|
</ResizeContainer>
|
|
63
54
|
</ErrorBoundary>
|
|
@@ -69,24 +60,30 @@ export const DateRangeSelectorInner = <CustomLabel extends string>({
|
|
|
69
60
|
earliestDate = '1900-01-01',
|
|
70
61
|
initialValue,
|
|
71
62
|
dateColumn,
|
|
63
|
+
initialDateFrom,
|
|
64
|
+
initialDateTo,
|
|
72
65
|
}: DateRangeSelectorPropsInner<CustomLabel>) => {
|
|
66
|
+
const initialValues = computeInitialValues(
|
|
67
|
+
initialValue,
|
|
68
|
+
initialDateFrom,
|
|
69
|
+
initialDateTo,
|
|
70
|
+
earliestDate,
|
|
71
|
+
customSelectOptions,
|
|
72
|
+
);
|
|
73
|
+
|
|
73
74
|
const fromDatePickerRef = useRef<HTMLInputElement>(null);
|
|
74
75
|
const toDatePickerRef = useRef<HTMLInputElement>(null);
|
|
75
76
|
const divRef = useRef<HTMLDivElement>(null);
|
|
76
77
|
const [dateFromPicker, setDateFromPicker] = useState<flatpickr.Instance | null>(null);
|
|
77
78
|
const [dateToPicker, setDateToPicker] = useState<flatpickr.Instance | null>(null);
|
|
78
79
|
|
|
79
|
-
const selectableOptions = getSelectableOptions(customSelectOptions);
|
|
80
|
-
|
|
81
80
|
const [selectedDateRange, setSelectedDateRange] = useState<CustomLabel | PresetOptionValues>(
|
|
82
|
-
|
|
83
|
-
? initialValue
|
|
84
|
-
: PRESET_VALUE_LAST_6_MONTHS,
|
|
81
|
+
initialValues.initialSelectedDateRange,
|
|
85
82
|
);
|
|
86
83
|
|
|
87
84
|
const [selectedDates, setSelectedDates] = useState<{ dateFrom: Date; dateTo: Date }>({
|
|
88
|
-
dateFrom:
|
|
89
|
-
dateTo:
|
|
85
|
+
dateFrom: initialValues.initialSelectedDateFrom,
|
|
86
|
+
dateTo: initialValues.initialSelectedDateTo,
|
|
90
87
|
});
|
|
91
88
|
|
|
92
89
|
useEffect(() => {
|
|
@@ -182,11 +179,11 @@ export const DateRangeSelectorInner = <CustomLabel extends string>({
|
|
|
182
179
|
};
|
|
183
180
|
|
|
184
181
|
return (
|
|
185
|
-
<div class='
|
|
182
|
+
<div class='flex flex-wrap' ref={divRef}>
|
|
186
183
|
<Select
|
|
187
|
-
items={
|
|
184
|
+
items={getSelectableOptions(customSelectOptions)}
|
|
188
185
|
selected={selectedDateRange}
|
|
189
|
-
selectStyle='select-bordered rounded-none
|
|
186
|
+
selectStyle='select-bordered rounded-none flex-grow w-40'
|
|
190
187
|
onChange={(event: Event) => {
|
|
191
188
|
event.preventDefault();
|
|
192
189
|
const select = event.target as HTMLSelectElement;
|
|
@@ -194,80 +191,26 @@ export const DateRangeSelectorInner = <CustomLabel extends string>({
|
|
|
194
191
|
onSelectChange(value as CustomLabel | PresetOptionValues);
|
|
195
192
|
}}
|
|
196
193
|
/>
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
194
|
+
<div className={'flex flex-wrap flex-grow'}>
|
|
195
|
+
<input
|
|
196
|
+
class='input input-bordered rounded-none flex-grow min-w-40'
|
|
197
|
+
type='text'
|
|
198
|
+
size={10}
|
|
199
|
+
placeholder='Date from'
|
|
200
|
+
ref={fromDatePickerRef}
|
|
201
|
+
onChange={onChangeDateFrom}
|
|
202
|
+
onBlur={onChangeDateFrom}
|
|
203
|
+
/>
|
|
204
|
+
<input
|
|
205
|
+
class='input input-bordered rounded-none flex-grow min-w-40'
|
|
206
|
+
type='text'
|
|
207
|
+
size={10}
|
|
208
|
+
placeholder='Date to'
|
|
209
|
+
ref={toDatePickerRef}
|
|
210
|
+
onChange={onChangeDateTo}
|
|
211
|
+
onBlur={onChangeDateTo}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
213
214
|
</div>
|
|
214
215
|
);
|
|
215
216
|
};
|
|
216
|
-
|
|
217
|
-
const getSelectableOptions = <Label extends string>(customSelectOptions: CustomSelectOption<Label>[]) => {
|
|
218
|
-
const presetOptions = Object.entries(presets).map(([key, value]) => {
|
|
219
|
-
return { label: value.label, value: key };
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const customOptions = customSelectOptions.map((customSelectOption) => {
|
|
223
|
-
return { label: customSelectOption.label, value: customSelectOption.label };
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
return [...presetOptions, ...customOptions];
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
const getDatesForSelectorValue = <Label extends string>(
|
|
230
|
-
selectorValue: string,
|
|
231
|
-
customSelectOptions: CustomSelectOption<Label>[],
|
|
232
|
-
earliestDate: string,
|
|
233
|
-
) => {
|
|
234
|
-
const today = new Date();
|
|
235
|
-
|
|
236
|
-
const customSelectOption = customSelectOptions.find((option) => option.label === selectorValue);
|
|
237
|
-
if (customSelectOption) {
|
|
238
|
-
return { dateFrom: new Date(customSelectOption.dateFrom), dateTo: new Date(customSelectOption.dateTo) };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
switch (selectorValue) {
|
|
242
|
-
case PRESET_VALUE_LAST_2_WEEKS: {
|
|
243
|
-
const twoWeeksAgo = new Date(today);
|
|
244
|
-
twoWeeksAgo.setDate(today.getDate() - 14);
|
|
245
|
-
return { dateFrom: twoWeeksAgo, dateTo: today };
|
|
246
|
-
}
|
|
247
|
-
case PRESET_VALUE_LAST_MONTH: {
|
|
248
|
-
const lastMonth = new Date(today);
|
|
249
|
-
lastMonth.setMonth(today.getMonth() - 1);
|
|
250
|
-
return { dateFrom: lastMonth, dateTo: today };
|
|
251
|
-
}
|
|
252
|
-
case PRESET_VALUE_LAST_2_MONTHS: {
|
|
253
|
-
const twoMonthsAgo = new Date(today);
|
|
254
|
-
twoMonthsAgo.setMonth(today.getMonth() - 2);
|
|
255
|
-
return { dateFrom: twoMonthsAgo, dateTo: today };
|
|
256
|
-
}
|
|
257
|
-
case PRESET_VALUE_LAST_3_MONTHS: {
|
|
258
|
-
const threeMonthsAgo = new Date(today);
|
|
259
|
-
threeMonthsAgo.setMonth(today.getMonth() - 3);
|
|
260
|
-
return { dateFrom: threeMonthsAgo, dateTo: today };
|
|
261
|
-
}
|
|
262
|
-
case PRESET_VALUE_LAST_6_MONTHS: {
|
|
263
|
-
const sixMonthsAgo = new Date(today);
|
|
264
|
-
sixMonthsAgo.setMonth(today.getMonth() - 6);
|
|
265
|
-
return { dateFrom: sixMonthsAgo, dateTo: today };
|
|
266
|
-
}
|
|
267
|
-
case PRESET_VALUE_ALL_TIMES: {
|
|
268
|
-
return { dateFrom: new Date(earliestDate), dateTo: today };
|
|
269
|
-
}
|
|
270
|
-
default:
|
|
271
|
-
return { dateFrom: today, dateTo: today };
|
|
272
|
-
}
|
|
273
|
-
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const PRESET_VALUE_CUSTOM = 'custom';
|
|
2
|
+
export const PRESET_VALUE_ALL_TIMES = 'allTimes';
|
|
3
|
+
export const PRESET_VALUE_LAST_2_WEEKS = 'last2Weeks';
|
|
4
|
+
export const PRESET_VALUE_LAST_MONTH = 'lastMonth';
|
|
5
|
+
export const PRESET_VALUE_LAST_2_MONTHS = 'last2Months';
|
|
6
|
+
export const PRESET_VALUE_LAST_3_MONTHS = 'last3Months';
|
|
7
|
+
export const PRESET_VALUE_LAST_6_MONTHS = 'last6Months';
|
|
8
|
+
|
|
9
|
+
export const presets = {
|
|
10
|
+
[PRESET_VALUE_CUSTOM]: { label: 'Custom' },
|
|
11
|
+
[PRESET_VALUE_ALL_TIMES]: { label: 'All times' },
|
|
12
|
+
[PRESET_VALUE_LAST_2_WEEKS]: { label: 'Last 2 weeks' },
|
|
13
|
+
[PRESET_VALUE_LAST_MONTH]: { label: 'Last month' },
|
|
14
|
+
[PRESET_VALUE_LAST_2_MONTHS]: { label: 'Last 2 months' },
|
|
15
|
+
[PRESET_VALUE_LAST_3_MONTHS]: { label: 'Last 3 months' },
|
|
16
|
+
[PRESET_VALUE_LAST_6_MONTHS]: { label: 'Last 6 months' },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type PresetOptionValues = keyof typeof presets;
|
|
20
|
+
|
|
21
|
+
export type CustomSelectOption<CustomLabel extends string> = { label: CustomLabel; dateFrom: string; dateTo: string };
|
|
22
|
+
|
|
23
|
+
export const getSelectableOptions = <Label extends string>(customSelectOptions: CustomSelectOption<Label>[]) => {
|
|
24
|
+
const presetOptions = Object.entries(presets).map(([key, value]) => {
|
|
25
|
+
return { label: value.label, value: key };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const customOptions = customSelectOptions.map((customSelectOption) => {
|
|
29
|
+
return { label: customSelectOption.label, value: customSelectOption.label };
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return [...presetOptions, ...customOptions];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const getDatesForSelectorValue = <Label extends string>(
|
|
36
|
+
selectorValue: string,
|
|
37
|
+
customSelectOptions: CustomSelectOption<Label>[],
|
|
38
|
+
earliestDate: string,
|
|
39
|
+
) => {
|
|
40
|
+
const today = new Date();
|
|
41
|
+
|
|
42
|
+
const customSelectOption = customSelectOptions.find((option) => option.label === selectorValue);
|
|
43
|
+
if (customSelectOption) {
|
|
44
|
+
return { dateFrom: new Date(customSelectOption.dateFrom), dateTo: new Date(customSelectOption.dateTo) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
switch (selectorValue) {
|
|
48
|
+
case PRESET_VALUE_LAST_2_WEEKS: {
|
|
49
|
+
const twoWeeksAgo = new Date(today);
|
|
50
|
+
twoWeeksAgo.setDate(today.getDate() - 14);
|
|
51
|
+
return { dateFrom: twoWeeksAgo, dateTo: today };
|
|
52
|
+
}
|
|
53
|
+
case PRESET_VALUE_LAST_MONTH: {
|
|
54
|
+
const lastMonth = new Date(today);
|
|
55
|
+
lastMonth.setMonth(today.getMonth() - 1);
|
|
56
|
+
return { dateFrom: lastMonth, dateTo: today };
|
|
57
|
+
}
|
|
58
|
+
case PRESET_VALUE_LAST_2_MONTHS: {
|
|
59
|
+
const twoMonthsAgo = new Date(today);
|
|
60
|
+
twoMonthsAgo.setMonth(today.getMonth() - 2);
|
|
61
|
+
return { dateFrom: twoMonthsAgo, dateTo: today };
|
|
62
|
+
}
|
|
63
|
+
case PRESET_VALUE_LAST_3_MONTHS: {
|
|
64
|
+
const threeMonthsAgo = new Date(today);
|
|
65
|
+
threeMonthsAgo.setMonth(today.getMonth() - 3);
|
|
66
|
+
return { dateFrom: threeMonthsAgo, dateTo: today };
|
|
67
|
+
}
|
|
68
|
+
case PRESET_VALUE_LAST_6_MONTHS: {
|
|
69
|
+
const sixMonthsAgo = new Date(today);
|
|
70
|
+
sixMonthsAgo.setMonth(today.getMonth() - 6);
|
|
71
|
+
return { dateFrom: sixMonthsAgo, dateTo: today };
|
|
72
|
+
}
|
|
73
|
+
case PRESET_VALUE_ALL_TIMES: {
|
|
74
|
+
return { dateFrom: new Date(earliestDate), dateTo: today };
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
return { dateFrom: today, dateTo: today };
|
|
78
|
+
}
|
|
79
|
+
};
|
|
@@ -93,7 +93,7 @@ export const LocationFilterInner = ({ initialValue, fields }: LocationFilterInne
|
|
|
93
93
|
|
|
94
94
|
const parseLocation = (location: string, fields: string[]) => {
|
|
95
95
|
const fieldValues = location.split('/').map((part) => part.trim());
|
|
96
|
-
return
|
|
96
|
+
return fields.reduce((acc, field, i) => ({ ...acc, [field]: fieldValues[i] }), {});
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
const hasMatchingEntry = (data: Record<string, string>[] | null, eventDetail: Record<string, string>) => {
|
|
@@ -167,7 +167,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
167
167
|
setProportionInterval,
|
|
168
168
|
}) => {
|
|
169
169
|
return (
|
|
170
|
-
|
|
170
|
+
<>
|
|
171
171
|
<ProportionSelectorDropdown
|
|
172
172
|
proportionInterval={proportionInterval}
|
|
173
173
|
setMinProportion={(min) => setProportionInterval((prev) => ({ ...prev, min }))}
|
|
@@ -183,7 +183,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
183
183
|
getData={() => getMutationComparisonTableData({ content: filteredData }, proportionInterval)}
|
|
184
184
|
filename='mutation_comparison.csv'
|
|
185
185
|
/>
|
|
186
|
-
<Info>Info for mutation comparison</Info>
|
|
187
|
-
|
|
186
|
+
<Info height={'100px'}>Info for mutation comparison</Info>
|
|
187
|
+
</>
|
|
188
188
|
);
|
|
189
189
|
};
|
|
@@ -20,7 +20,6 @@ const meta: Meta<MutationFilterProps> = {
|
|
|
20
20
|
},
|
|
21
21
|
argTypes: {
|
|
22
22
|
width: { control: 'text' },
|
|
23
|
-
height: { control: 'text' },
|
|
24
23
|
initialValue: {
|
|
25
24
|
control: {
|
|
26
25
|
type: 'object',
|
|
@@ -36,13 +35,12 @@ export const Default: StoryObj<MutationFilterProps> = {
|
|
|
36
35
|
render: (args) => (
|
|
37
36
|
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
38
37
|
<ReferenceGenomeContext.Provider value={referenceGenome}>
|
|
39
|
-
<MutationFilter width={args.width}
|
|
38
|
+
<MutationFilter width={args.width} initialValue={args.initialValue} />
|
|
40
39
|
</ReferenceGenomeContext.Provider>
|
|
41
40
|
</LapisUrlContext.Provider>
|
|
42
41
|
),
|
|
43
42
|
args: {
|
|
44
43
|
width: '100%',
|
|
45
|
-
height: '700px',
|
|
46
44
|
},
|
|
47
45
|
};
|
|
48
46
|
|
|
@@ -106,7 +104,7 @@ export const FiresFilterChangedEvents: StoryObj<MutationFilterProps> = {
|
|
|
106
104
|
});
|
|
107
105
|
|
|
108
106
|
await step('Remove the first mutation', async () => {
|
|
109
|
-
const firstMutationDeleteButton = canvas.getAllByRole('button')[
|
|
107
|
+
const firstMutationDeleteButton = canvas.getAllByRole('button')[1];
|
|
110
108
|
await waitFor(() => fireEvent.click(firstMutationDeleteButton));
|
|
111
109
|
|
|
112
110
|
await expect(changedListenerMock).toHaveBeenCalledWith(
|
|
@@ -153,7 +151,7 @@ export const WithInitialValue: StoryObj<MutationFilterProps> = {
|
|
|
153
151
|
render: (args) => (
|
|
154
152
|
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
155
153
|
<ReferenceGenomeContext.Provider value={referenceGenome}>
|
|
156
|
-
<MutationFilter initialValue={args.initialValue} width={args.width}
|
|
154
|
+
<MutationFilter initialValue={args.initialValue} width={args.width} />
|
|
157
155
|
</ReferenceGenomeContext.Provider>
|
|
158
156
|
</LapisUrlContext.Provider>
|
|
159
157
|
),
|
|
@@ -165,7 +163,6 @@ export const WithInitialValue: StoryObj<MutationFilterProps> = {
|
|
|
165
163
|
aminoAcidInsertions: ['ins_S:123:AAA'],
|
|
166
164
|
},
|
|
167
165
|
width: '100%',
|
|
168
|
-
height: '700px',
|
|
169
166
|
},
|
|
170
167
|
play: async ({ canvasElement, step }) => {
|
|
171
168
|
const { canvas, onBlurListenerMock } = await prepare(canvasElement, step);
|
|
@@ -7,7 +7,6 @@ import { type Deletion, type Insertion, type Mutation, type Substitution } from
|
|
|
7
7
|
import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
8
8
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
9
9
|
import Info from '../components/info';
|
|
10
|
-
import { ResizeContainer } from '../components/resize-container';
|
|
11
10
|
import { singleGraphColorRGBByName } from '../shared/charts/colors';
|
|
12
11
|
import { DeleteIcon } from '../shared/icons/DeleteIcon';
|
|
13
12
|
|
|
@@ -17,7 +16,6 @@ export interface MutationFilterInnerProps {
|
|
|
17
16
|
|
|
18
17
|
export interface MutationFilterProps extends MutationFilterInnerProps {
|
|
19
18
|
width: string;
|
|
20
|
-
height: string;
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
export type SelectedFilters = {
|
|
@@ -31,14 +29,12 @@ export type SelectedMutationFilterStrings = {
|
|
|
31
29
|
[Key in keyof SelectedFilters]: string[];
|
|
32
30
|
};
|
|
33
31
|
|
|
34
|
-
export const MutationFilter: FunctionComponent<MutationFilterProps> = ({ initialValue, width
|
|
35
|
-
const size = { height, width };
|
|
36
|
-
|
|
32
|
+
export const MutationFilter: FunctionComponent<MutationFilterProps> = ({ initialValue, width }) => {
|
|
37
33
|
return (
|
|
38
|
-
<ErrorBoundary size={
|
|
39
|
-
<
|
|
34
|
+
<ErrorBoundary size={{ height: '3.375rem', width }}>
|
|
35
|
+
<div style={width}>
|
|
40
36
|
<MutationFilterInner initialValue={initialValue} />
|
|
41
|
-
</
|
|
37
|
+
</div>
|
|
42
38
|
</ErrorBoundary>
|
|
43
39
|
);
|
|
44
40
|
};
|
|
@@ -54,6 +50,9 @@ export const MutationFilterInner: FunctionComponent<MutationFilterInnerProps> =
|
|
|
54
50
|
|
|
55
51
|
const handleSubmit = (event: Event) => {
|
|
56
52
|
event.preventDefault();
|
|
53
|
+
if (inputValue === '') {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
57
56
|
|
|
58
57
|
const parsedMutation = parseAndValidateMutation(inputValue, referenceGenome);
|
|
59
58
|
|
|
@@ -102,30 +101,33 @@ export const MutationFilterInner: FunctionComponent<MutationFilterInnerProps> =
|
|
|
102
101
|
};
|
|
103
102
|
|
|
104
103
|
return (
|
|
105
|
-
<
|
|
106
|
-
<div
|
|
104
|
+
<form className='w-full border boder-gray-300 rounded-md relative' onSubmit={handleSubmit} ref={formRef}>
|
|
105
|
+
<div className='absolute -top-3 -right-3'>
|
|
106
|
+
<Info height={'100px'}>Info for mutation filter</Info>
|
|
107
|
+
</div>
|
|
108
|
+
<div className='w-full flex p-2 flex-wrap items-center'>
|
|
107
109
|
<SelectedMutationDisplay
|
|
108
110
|
selectedFilters={selectedFilters}
|
|
109
111
|
setSelectedFilters={setSelectedFilters}
|
|
110
112
|
fireChangeEvent={fireChangeEvent}
|
|
111
113
|
/>
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<form className='mt-2 w-full' onSubmit={handleSubmit} ref={formRef}>
|
|
116
|
-
<label className={`input flex items-center gap-2 ${isError ? 'input-error' : 'input-bordered'}`}>
|
|
114
|
+
<div
|
|
115
|
+
className={`w-full flex border ${isError ? 'border-red-500' : 'border-gray-300'} border-solid m-2 text-sm focus-within:border-gray-400 `}
|
|
116
|
+
>
|
|
117
117
|
<input
|
|
118
|
-
className='grow
|
|
118
|
+
className='grow flex-1 p-1 border-none focus:outline-none focus:ring-0'
|
|
119
119
|
type='text'
|
|
120
120
|
value={inputValue}
|
|
121
121
|
onInput={handleInputChange}
|
|
122
122
|
placeholder={getPlaceholder(referenceGenome)}
|
|
123
123
|
onBlur={handleOnBlur}
|
|
124
124
|
/>
|
|
125
|
-
<button className='btn btn-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
<button type='submit' className='btn btn-xs m-1'>
|
|
126
|
+
+
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</form>
|
|
129
131
|
);
|
|
130
132
|
};
|
|
131
133
|
|
|
@@ -193,44 +195,36 @@ const SelectedMutationDisplay: FunctionComponent<{
|
|
|
193
195
|
};
|
|
194
196
|
|
|
195
197
|
return (
|
|
196
|
-
|
|
198
|
+
<>
|
|
197
199
|
{selectedFilters.nucleotideMutations.map((mutation) => (
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
/>
|
|
205
|
-
</li>
|
|
200
|
+
<SelectedNucleotideMutation
|
|
201
|
+
key={mutation.toString()}
|
|
202
|
+
mutation={mutation}
|
|
203
|
+
onDelete={(mutation: Substitution | Deletion) => onSelectedRemoved(mutation, 'nucleotideMutations')}
|
|
204
|
+
/>
|
|
206
205
|
))}
|
|
207
206
|
{selectedFilters.aminoAcidMutations.map((mutation) => (
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
/>
|
|
215
|
-
</li>
|
|
207
|
+
<SelectedAminoAcidMutation
|
|
208
|
+
key={mutation.toString()}
|
|
209
|
+
mutation={mutation}
|
|
210
|
+
onDelete={(mutation: Substitution | Deletion) => onSelectedRemoved(mutation, 'aminoAcidMutations')}
|
|
211
|
+
/>
|
|
216
212
|
))}
|
|
217
213
|
{selectedFilters.nucleotideInsertions.map((insertion) => (
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
</li>
|
|
214
|
+
<SelectedNucleotideInsertion
|
|
215
|
+
key={insertion.toString()}
|
|
216
|
+
insertion={insertion}
|
|
217
|
+
onDelete={(insertion) => onSelectedRemoved(insertion, 'nucleotideInsertions')}
|
|
218
|
+
/>
|
|
224
219
|
))}
|
|
225
220
|
{selectedFilters.aminoAcidInsertions.map((insertion) => (
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
</li>
|
|
221
|
+
<SelectedAminoAcidInsertion
|
|
222
|
+
key={insertion.toString()}
|
|
223
|
+
insertion={insertion}
|
|
224
|
+
onDelete={(insertion: Insertion) => onSelectedRemoved(insertion, 'aminoAcidInsertions')}
|
|
225
|
+
/>
|
|
232
226
|
))}
|
|
233
|
-
|
|
227
|
+
</>
|
|
234
228
|
);
|
|
235
229
|
};
|
|
236
230
|
|
|
@@ -313,15 +307,15 @@ const SelectedFilter = <MutationType extends Mutation>({
|
|
|
313
307
|
textColor,
|
|
314
308
|
}: SelectedFilterProps<MutationType>) => {
|
|
315
309
|
return (
|
|
316
|
-
<
|
|
317
|
-
class='
|
|
310
|
+
<span
|
|
311
|
+
class='inline-block mx-1 px-2 py-1 font-medium text-xs rounded-full'
|
|
318
312
|
style={{ backgroundColor, color: textColor }}
|
|
319
313
|
>
|
|
320
|
-
|
|
314
|
+
{mutation.toString()}
|
|
321
315
|
<button type='button' onClick={() => onDelete(mutation)}>
|
|
322
316
|
<DeleteIcon />
|
|
323
317
|
</button>
|
|
324
|
-
</
|
|
318
|
+
</span>
|
|
325
319
|
);
|
|
326
320
|
};
|
|
327
321
|
|
|
@@ -168,7 +168,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
168
168
|
setProportionInterval,
|
|
169
169
|
}) => {
|
|
170
170
|
return (
|
|
171
|
-
|
|
171
|
+
<>
|
|
172
172
|
<SegmentSelector displayedSegments={displayedSegments} setDisplayedSegments={setDisplayedSegments} />
|
|
173
173
|
{activeTab === 'Table' && (
|
|
174
174
|
<MutationTypeSelector
|
|
@@ -182,7 +182,6 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
182
182
|
proportionInterval={proportionInterval}
|
|
183
183
|
setMinProportion={(min) => setProportionInterval((prev) => ({ ...prev, min }))}
|
|
184
184
|
setMaxProportion={(max) => setProportionInterval((prev) => ({ ...prev, max }))}
|
|
185
|
-
openDirection={'left'}
|
|
186
185
|
/>
|
|
187
186
|
<CsvDownloadButton
|
|
188
187
|
className='mx-1 btn btn-xs'
|
|
@@ -205,7 +204,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
205
204
|
filename='insertions.csv'
|
|
206
205
|
/>
|
|
207
206
|
)}
|
|
208
|
-
<Info>Info for mutations</Info>
|
|
209
|
-
|
|
207
|
+
<Info height={'100px'}>Info for mutations</Info>
|
|
208
|
+
</>
|
|
210
209
|
);
|
|
211
210
|
};
|
|
@@ -208,7 +208,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
208
208
|
granularity,
|
|
209
209
|
}) => {
|
|
210
210
|
return (
|
|
211
|
-
|
|
211
|
+
<>
|
|
212
212
|
{activeTab !== 'Table' && (
|
|
213
213
|
<ScalingSelector yAxisScaleType={yAxisScaleType} setYAxisScaleType={setYAxisScaleType} />
|
|
214
214
|
)}
|
|
@@ -226,17 +226,15 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
226
226
|
/>
|
|
227
227
|
|
|
228
228
|
<PrevalenceOverTimeInfo />
|
|
229
|
-
|
|
229
|
+
</>
|
|
230
230
|
);
|
|
231
231
|
};
|
|
232
232
|
|
|
233
233
|
const PrevalenceOverTimeInfo: FunctionComponent = () => {
|
|
234
234
|
return (
|
|
235
|
-
<Info
|
|
235
|
+
<Info height={'100px'}>
|
|
236
236
|
<InfoHeadline1>Prevalence over time</InfoHeadline1>
|
|
237
237
|
<InfoParagraph>Prevalence over time info.</InfoParagraph>
|
|
238
238
|
</Info>
|
|
239
239
|
);
|
|
240
240
|
};
|
|
241
|
-
|
|
242
|
-
export default PrevalenceOverTime;
|
|
@@ -161,16 +161,16 @@ const RelativeGrowthAdvantageToolbar: FunctionComponent<RelativeGrowthAdvantageT
|
|
|
161
161
|
generationTime,
|
|
162
162
|
}) => {
|
|
163
163
|
return (
|
|
164
|
-
|
|
164
|
+
<>
|
|
165
165
|
<ScalingSelector yAxisScaleType={yAxisScaleType} setYAxisScaleType={setYAxisScaleType} />
|
|
166
166
|
<RelativeGrowthAdvantageInfo generationTime={generationTime} />
|
|
167
|
-
|
|
167
|
+
</>
|
|
168
168
|
);
|
|
169
169
|
};
|
|
170
170
|
|
|
171
171
|
const RelativeGrowthAdvantageInfo: FunctionComponent<{ generationTime: number }> = ({ generationTime }) => {
|
|
172
172
|
return (
|
|
173
|
-
<Info
|
|
173
|
+
<Info>
|
|
174
174
|
<InfoHeadline1>Relative growth advantage</InfoHeadline1>
|
|
175
175
|
<InfoParagraph>
|
|
176
176
|
If variants spread pre-dominantly by local transmission across demographic groups, this estimate
|