@genspectrum/dashboard-components 0.3.1 → 0.4.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 -29
- package/dist/dashboard-components.js +391 -207
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +37 -12
- package/dist/style.css +58 -50
- package/package.json +2 -1
- package/src/operator/RenameFieldOperator.spec.ts +28 -0
- package/src/operator/RenameFieldOperator.ts +19 -0
- package/src/preact/aggregatedData/aggregate.tsx +1 -1
- package/src/preact/components/info.stories.tsx +8 -8
- package/src/preact/components/info.tsx +113 -20
- 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 +27 -88
- package/src/preact/dateRangeSelector/selectableOptions.ts +79 -0
- package/src/preact/locationFilter/location-filter.tsx +1 -1
- package/src/preact/mutationComparison/mutation-comparison.tsx +1 -1
- 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 +1 -1
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +3 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +7 -3
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +2 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +6 -2
- package/src/query/queryPrevalenceOverTime.ts +13 -7
- package/src/query/queryRelativeGrowthAdvantage.ts +11 -7
- 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.stories.ts +6 -2
- package/src/web-components/visualization/gs-prevalence-over-time.tsx +11 -0
- package/src/web-components/visualization/gs-relative-growth-advantage.stories.ts +3 -0
- package/src/web-components/visualization/gs-relative-growth-advantage.tsx +11 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { computeInitialValues } from './computeInitialValues';
|
|
4
|
+
import { PRESET_VALUE_CUSTOM, PRESET_VALUE_LAST_3_MONTHS, PRESET_VALUE_LAST_6_MONTHS } from './selectableOptions';
|
|
5
|
+
|
|
6
|
+
const today = new Date();
|
|
7
|
+
const earliestDate = '1900-01-01';
|
|
8
|
+
|
|
9
|
+
describe('computeInitialValues', () => {
|
|
10
|
+
it('should compute for initial value if initial "from" and "to" are unset', () => {
|
|
11
|
+
const result = computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, undefined, undefined, earliestDate, []);
|
|
12
|
+
|
|
13
|
+
const expectedFrom = new Date();
|
|
14
|
+
expectedFrom.setMonth(today.getMonth() - 3);
|
|
15
|
+
|
|
16
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_LAST_3_MONTHS);
|
|
17
|
+
expectDateMatches(result.initialSelectedDateFrom, expectedFrom);
|
|
18
|
+
expectDateMatches(result.initialSelectedDateTo, today);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should fall back to default when initial value is unknown', () => {
|
|
22
|
+
const result = computeInitialValues('not a known value', undefined, undefined, earliestDate, []);
|
|
23
|
+
|
|
24
|
+
const expectedFrom = new Date();
|
|
25
|
+
expectedFrom.setMonth(today.getMonth() - 6);
|
|
26
|
+
|
|
27
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_LAST_6_MONTHS);
|
|
28
|
+
expectDateMatches(result.initialSelectedDateFrom, expectedFrom);
|
|
29
|
+
expectDateMatches(result.initialSelectedDateTo, today);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should overwrite initial value if initial "from" is set', () => {
|
|
33
|
+
const initialDateFrom = '2020-01-01';
|
|
34
|
+
const result = computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, initialDateFrom, undefined, earliestDate, []);
|
|
35
|
+
|
|
36
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
|
|
37
|
+
expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
|
|
38
|
+
expectDateMatches(result.initialSelectedDateTo, today);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should overwrite initial value if initial "to" is set', () => {
|
|
42
|
+
const initialDateTo = '2020-01-01';
|
|
43
|
+
const result = computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, undefined, initialDateTo, earliestDate, []);
|
|
44
|
+
|
|
45
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
|
|
46
|
+
expectDateMatches(result.initialSelectedDateFrom, new Date(earliestDate));
|
|
47
|
+
expectDateMatches(result.initialSelectedDateTo, new Date(initialDateTo));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should overwrite initial value if initial "to" and "from" are set', () => {
|
|
51
|
+
const initialDateFrom = '2020-01-01';
|
|
52
|
+
const initialDateTo = '2022-01-01';
|
|
53
|
+
const result = computeInitialValues(
|
|
54
|
+
PRESET_VALUE_LAST_3_MONTHS,
|
|
55
|
+
initialDateFrom,
|
|
56
|
+
initialDateTo,
|
|
57
|
+
earliestDate,
|
|
58
|
+
[],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
|
|
62
|
+
expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
|
|
63
|
+
expectDateMatches(result.initialSelectedDateTo, new Date(initialDateTo));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should set initial "to" to "from" if "from" is after "to"', () => {
|
|
67
|
+
const initialDateFrom = '2020-01-01';
|
|
68
|
+
const initialDateTo = '1900-01-01';
|
|
69
|
+
const result = computeInitialValues(
|
|
70
|
+
PRESET_VALUE_LAST_3_MONTHS,
|
|
71
|
+
initialDateFrom,
|
|
72
|
+
initialDateTo,
|
|
73
|
+
earliestDate,
|
|
74
|
+
[],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
|
|
78
|
+
expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
|
|
79
|
+
expectDateMatches(result.initialSelectedDateTo, new Date(initialDateFrom));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should throw if initial "from" is not a valid date', () => {
|
|
83
|
+
expect(() =>
|
|
84
|
+
computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, 'not a date', undefined, earliestDate, []),
|
|
85
|
+
).toThrowError('Invalid initialDateFrom');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should throw if initial "to" is not a valid date', () => {
|
|
89
|
+
expect(() =>
|
|
90
|
+
computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, undefined, 'not a date', earliestDate, []),
|
|
91
|
+
).toThrowError('Invalid initialDateTo');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function expectDateMatches(actual: Date, expected: Date) {
|
|
95
|
+
expect(actual.getFullYear()).toEqual(expected.getFullYear());
|
|
96
|
+
expect(actual.getMonth()).toEqual(expected.getMonth());
|
|
97
|
+
expect(actual.getDate()).toEqual(expected.getDate());
|
|
98
|
+
}
|
|
99
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CustomSelectOption,
|
|
3
|
+
getDatesForSelectorValue,
|
|
4
|
+
getSelectableOptions,
|
|
5
|
+
PRESET_VALUE_CUSTOM,
|
|
6
|
+
PRESET_VALUE_LAST_6_MONTHS,
|
|
7
|
+
type PresetOptionValues,
|
|
8
|
+
} from './selectableOptions';
|
|
9
|
+
import { UserFacingError } from '../components/error-display';
|
|
10
|
+
|
|
11
|
+
export function computeInitialValues<CustomLabel extends string>(
|
|
12
|
+
initialValue: PresetOptionValues | CustomLabel | undefined,
|
|
13
|
+
initialDateFrom: string | undefined,
|
|
14
|
+
initialDateTo: string | undefined,
|
|
15
|
+
earliestDate: string,
|
|
16
|
+
customSelectOptions: CustomSelectOption<CustomLabel>[],
|
|
17
|
+
): {
|
|
18
|
+
initialSelectedDateRange: CustomLabel | PresetOptionValues;
|
|
19
|
+
initialSelectedDateFrom: Date;
|
|
20
|
+
initialSelectedDateTo: Date;
|
|
21
|
+
} {
|
|
22
|
+
if (isUndefinedOrEmpty(initialDateFrom) && isUndefinedOrEmpty(initialDateTo)) {
|
|
23
|
+
const selectableOptions = getSelectableOptions(customSelectOptions);
|
|
24
|
+
const initialSelectedDateRange =
|
|
25
|
+
initialValue !== undefined && selectableOptions.some((option) => option.value === initialValue)
|
|
26
|
+
? initialValue
|
|
27
|
+
: PRESET_VALUE_LAST_6_MONTHS;
|
|
28
|
+
|
|
29
|
+
const { dateFrom, dateTo } = getDatesForSelectorValue(
|
|
30
|
+
initialSelectedDateRange,
|
|
31
|
+
customSelectOptions,
|
|
32
|
+
earliestDate,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
initialSelectedDateRange,
|
|
37
|
+
initialSelectedDateFrom: dateFrom,
|
|
38
|
+
initialSelectedDateTo: dateTo,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const initialSelectedDateFrom = isUndefinedOrEmpty(initialDateFrom)
|
|
43
|
+
? new Date(earliestDate)
|
|
44
|
+
: new Date(initialDateFrom);
|
|
45
|
+
let initialSelectedDateTo = isUndefinedOrEmpty(initialDateTo) ? new Date() : new Date(initialDateTo);
|
|
46
|
+
|
|
47
|
+
if (isNaN(initialSelectedDateFrom.getTime())) {
|
|
48
|
+
throw new UserFacingError(
|
|
49
|
+
'Invalid initialDateFrom',
|
|
50
|
+
`Invalid initialDateFrom "${initialDateFrom}", It must be of the format YYYY-MM-DD`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (isNaN(initialSelectedDateTo.getTime())) {
|
|
54
|
+
throw new UserFacingError(
|
|
55
|
+
'Invalid initialDateTo',
|
|
56
|
+
`Invalid initialDateTo "${initialDateTo}", It must be of the format YYYY-MM-DD`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (initialSelectedDateFrom > initialSelectedDateTo) {
|
|
61
|
+
initialSelectedDateTo = initialSelectedDateFrom;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
initialSelectedDateRange: PRESET_VALUE_CUSTOM,
|
|
66
|
+
initialSelectedDateFrom,
|
|
67
|
+
initialSelectedDateTo,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isUndefinedOrEmpty(value: string | undefined): value is undefined | '' {
|
|
72
|
+
return value === undefined || value === '';
|
|
73
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { withActions } from '@storybook/addon-actions/decorator';
|
|
2
2
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
3
|
+
import { expect, waitFor, within } from '@storybook/test';
|
|
4
|
+
import dayjs from 'dayjs/esm';
|
|
3
5
|
|
|
6
|
+
import { DateRangeSelector, type DateRangeSelectorProps } from './date-range-selector';
|
|
4
7
|
import {
|
|
5
|
-
DateRangeSelector,
|
|
6
|
-
type DateRangeSelectorProps,
|
|
7
8
|
PRESET_VALUE_ALL_TIMES,
|
|
8
9
|
PRESET_VALUE_CUSTOM,
|
|
9
10
|
PRESET_VALUE_LAST_2_MONTHS,
|
|
@@ -11,10 +12,12 @@ import {
|
|
|
11
12
|
PRESET_VALUE_LAST_3_MONTHS,
|
|
12
13
|
PRESET_VALUE_LAST_6_MONTHS,
|
|
13
14
|
PRESET_VALUE_LAST_MONTH,
|
|
14
|
-
} from './
|
|
15
|
+
} from './selectableOptions';
|
|
15
16
|
import { LAPIS_URL } from '../../constants';
|
|
16
17
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
17
18
|
|
|
19
|
+
const earliestDate = '1970-01-01';
|
|
20
|
+
|
|
18
21
|
const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
19
22
|
title: 'Input/DateRangeSelector',
|
|
20
23
|
component: DateRangeSelector,
|
|
@@ -58,10 +61,12 @@ const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
|
58
61
|
},
|
|
59
62
|
args: {
|
|
60
63
|
customSelectOptions: [{ label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' }],
|
|
61
|
-
earliestDate
|
|
64
|
+
earliestDate,
|
|
62
65
|
initialValue: PRESET_VALUE_LAST_3_MONTHS,
|
|
63
66
|
dateColumn: 'aDateColumn',
|
|
64
67
|
width: '100%',
|
|
68
|
+
initialDateFrom: '',
|
|
69
|
+
initialDateTo: '',
|
|
65
70
|
},
|
|
66
71
|
decorators: [withActions],
|
|
67
72
|
};
|
|
@@ -75,9 +80,93 @@ export const Primary: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
|
75
80
|
customSelectOptions={args.customSelectOptions}
|
|
76
81
|
earliestDate={args.earliestDate}
|
|
77
82
|
initialValue={args.initialValue}
|
|
83
|
+
initialDateFrom={args.initialDateFrom}
|
|
84
|
+
initialDateTo={args.initialDateTo}
|
|
78
85
|
width={args.width}
|
|
79
86
|
dateColumn={args.dateColumn}
|
|
80
87
|
/>
|
|
81
88
|
</LapisUrlContext.Provider>
|
|
82
89
|
),
|
|
83
90
|
};
|
|
91
|
+
|
|
92
|
+
export const SetCorrectInitialValues: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
93
|
+
...Primary,
|
|
94
|
+
args: {
|
|
95
|
+
...Primary.args,
|
|
96
|
+
initialValue: 'CustomDateRange',
|
|
97
|
+
},
|
|
98
|
+
play: async ({ canvasElement }) => {
|
|
99
|
+
const canvas = within(canvasElement);
|
|
100
|
+
|
|
101
|
+
const dateFrom = () => canvas.getByPlaceholderText('Date from');
|
|
102
|
+
const dateTo = () => canvas.getByPlaceholderText('Date to');
|
|
103
|
+
const selectField = () => canvas.getByRole('combobox');
|
|
104
|
+
|
|
105
|
+
await waitFor(() => {
|
|
106
|
+
expect(selectField()).toHaveValue('CustomDateRange');
|
|
107
|
+
expect(dateFrom()).toHaveValue('2021-01-01');
|
|
108
|
+
expect(dateTo()).toHaveValue('2021-12-31');
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const initialDateFrom = '2000-01-01';
|
|
114
|
+
|
|
115
|
+
export const SetCorrectInitialDateFrom: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
116
|
+
...Primary,
|
|
117
|
+
args: {
|
|
118
|
+
...Primary.args,
|
|
119
|
+
initialDateFrom,
|
|
120
|
+
},
|
|
121
|
+
play: async ({ canvasElement }) => {
|
|
122
|
+
const canvas = within(canvasElement);
|
|
123
|
+
|
|
124
|
+
const dateFrom = () => canvas.getByPlaceholderText('Date from');
|
|
125
|
+
const dateTo = () => canvas.getByPlaceholderText('Date to');
|
|
126
|
+
const selectField = () => canvas.getByRole('combobox');
|
|
127
|
+
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(selectField()).toHaveValue(PRESET_VALUE_CUSTOM);
|
|
130
|
+
expect(dateFrom()).toHaveValue(initialDateFrom);
|
|
131
|
+
expect(dateTo()).toHaveValue(dayjs().format('YYYY-MM-DD'));
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const initialDateTo = '2000-01-01';
|
|
137
|
+
|
|
138
|
+
export const SetCorrectInitialDateTo: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
139
|
+
...Primary,
|
|
140
|
+
args: {
|
|
141
|
+
...Primary.args,
|
|
142
|
+
initialDateTo,
|
|
143
|
+
},
|
|
144
|
+
play: async ({ canvasElement }) => {
|
|
145
|
+
const canvas = within(canvasElement);
|
|
146
|
+
|
|
147
|
+
const dateFrom = () => canvas.getByPlaceholderText('Date from');
|
|
148
|
+
const dateTo = () => canvas.getByPlaceholderText('Date to');
|
|
149
|
+
const selectField = () => canvas.getByRole('combobox');
|
|
150
|
+
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
expect(selectField()).toHaveValue(PRESET_VALUE_CUSTOM);
|
|
153
|
+
expect(dateFrom()).toHaveValue(earliestDate);
|
|
154
|
+
expect(dateTo()).toHaveValue(initialDateTo);
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const HandlesInvalidInitialDateFrom: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
160
|
+
...Primary,
|
|
161
|
+
args: {
|
|
162
|
+
...Primary.args,
|
|
163
|
+
initialDateFrom: 'not a date',
|
|
164
|
+
},
|
|
165
|
+
play: async ({ canvasElement }) => {
|
|
166
|
+
const canvas = within(canvasElement);
|
|
167
|
+
|
|
168
|
+
await waitFor(() => {
|
|
169
|
+
expect(canvas.getByText('Oops! Something went wrong.')).toBeVisible();
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
};
|
|
@@ -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(() => {
|
|
@@ -184,7 +181,7 @@ export const DateRangeSelectorInner = <CustomLabel extends string>({
|
|
|
184
181
|
return (
|
|
185
182
|
<div class='join w-full' ref={divRef}>
|
|
186
183
|
<Select
|
|
187
|
-
items={
|
|
184
|
+
items={getSelectableOptions(customSelectOptions)}
|
|
188
185
|
selected={selectedDateRange}
|
|
189
186
|
selectStyle='select-bordered rounded-none join-item grow'
|
|
190
187
|
onChange={(event: Event) => {
|
|
@@ -213,61 +210,3 @@ export const DateRangeSelectorInner = <CustomLabel extends string>({
|
|
|
213
210
|
</div>
|
|
214
211
|
);
|
|
215
212
|
};
|
|
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>) => {
|
|
@@ -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>
|
|
186
|
+
<Info height={'100px'}>Info for mutation comparison</Info>
|
|
187
187
|
</div>
|
|
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);
|