@genspectrum/dashboard-components 0.19.9 → 0.21.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.
@@ -14,11 +14,10 @@ import { gsEventNames } from '../../utils/gsEventNames';
14
14
  import { ClearableSelect } from '../components/clearable-select';
15
15
  import { ErrorBoundary } from '../components/error-boundary';
16
16
 
17
- const customOption = 'Custom';
17
+ const CUSTOM_OPTION = 'Custom';
18
18
 
19
19
  const dateRangeFilterInnerPropsSchema = z.object({
20
20
  dateRangeOptions: z.array(dateRangeOptionSchema),
21
- earliestDate: z.string().date(),
22
21
  value: dateRangeValueSchema,
23
22
  lapisDateField: z.string().min(1),
24
23
  placeholder: z.string().optional(),
@@ -52,15 +51,11 @@ export const DateRangeFilter = (props: DateRangeFilterProps) => {
52
51
 
53
52
  export const DateRangeFilterInner = ({
54
53
  dateRangeOptions,
55
- earliestDate = '1900-01-01',
56
54
  value,
57
55
  lapisDateField,
58
56
  placeholder,
59
57
  }: DateRangeFilterInnerProps) => {
60
- const initialValues = useMemo(
61
- () => computeInitialValues(value, earliestDate, dateRangeOptions),
62
- [value, earliestDate, dateRangeOptions],
63
- );
58
+ const initialValues = useMemo(() => computeInitialValues(value, dateRangeOptions), [value, dateRangeOptions]);
64
59
 
65
60
  const divRef = useRef<HTMLDivElement>(null);
66
61
 
@@ -75,15 +70,15 @@ export const DateRangeFilterInner = ({
75
70
  dateTo: initialValues.initialSelectedDateTo,
76
71
  }
77
72
  : {
78
- label: customOption,
73
+ label: CUSTOM_OPTION,
79
74
  dateFrom: initialValues.initialSelectedDateFrom,
80
75
  dateTo: initialValues.initialSelectedDateTo,
81
76
  };
82
77
  }, [initialValues]);
83
78
 
84
- const customComboboxValue = { label: customOption };
79
+ const customComboboxValue = { label: CUSTOM_OPTION };
85
80
  const [options, setOptions] = useState(
86
- getInitialState()?.label === customOption ? [...dateRangeOptions, customComboboxValue] : [...dateRangeOptions],
81
+ getInitialState()?.label === CUSTOM_OPTION ? [...dateRangeOptions, customComboboxValue] : [...dateRangeOptions],
87
82
  );
88
83
  const [state, setState] = useState<DateRangeFilterState>(getInitialState());
89
84
 
@@ -101,42 +96,24 @@ export const DateRangeFilterInner = ({
101
96
  updateState(
102
97
  option !== null
103
98
  ? {
104
- label: option?.label,
105
- dateFrom: getFromDate(option, earliestDate),
106
- dateTo: getToDate(option),
99
+ label: option.label,
100
+ dateFrom: toMaybeDate(option.dateFrom),
101
+ dateTo: toMaybeDate(option.dateTo),
107
102
  }
108
103
  : null,
109
104
  );
110
- if (option?.label !== customOption) {
105
+ if (option?.label !== CUSTOM_OPTION) {
111
106
  setOptions([...dateRangeOptions]);
112
107
  }
113
108
  };
114
109
 
115
- function getFromDate(option: DateRangeOption | null, earliestDate: string) {
116
- if (!option || option.label === customOption) {
117
- return undefined;
118
- }
119
- return new Date(option?.dateFrom ?? earliestDate);
120
- }
121
-
122
- function getToDate(option: DateRangeOption | null) {
123
- if (!option || option.label === customOption) {
124
- return undefined;
125
- }
126
- if (!option.dateTo) {
127
- return new Date();
128
- }
129
-
130
- return new Date(option.dateTo);
131
- }
132
-
133
110
  const onChangeDateFrom = (date: Date | undefined) => {
134
111
  if (date?.toDateString() === state?.dateFrom?.toDateString()) {
135
112
  return;
136
113
  }
137
114
 
138
115
  updateState({
139
- label: customOption,
116
+ label: CUSTOM_OPTION,
140
117
  dateFrom: date,
141
118
  dateTo: state?.dateTo,
142
119
  });
@@ -149,7 +126,7 @@ export const DateRangeFilterInner = ({
149
126
  }
150
127
 
151
128
  updateState({
152
- label: customOption,
129
+ label: CUSTOM_OPTION,
153
130
  dateFrom: state?.dateFrom,
154
131
  dateTo: date,
155
132
  });
@@ -184,7 +161,7 @@ export const DateRangeFilterInner = ({
184
161
  if (state === null) {
185
162
  return null;
186
163
  }
187
- if (state.label === customOption) {
164
+ if (state.label === CUSTOM_OPTION) {
188
165
  return {
189
166
  dateFrom: state.dateFrom !== undefined ? toYYYYMMDD(state.dateFrom) : undefined,
190
167
  dateTo: state.dateTo !== undefined ? toYYYYMMDD(state.dateTo) : undefined,
@@ -233,3 +210,7 @@ export const DateRangeFilterInner = ({
233
210
  </div>
234
211
  );
235
212
  };
213
+
214
+ function toMaybeDate(dateString: string | undefined) {
215
+ return dateString ? new Date(dateString) : undefined;
216
+ }
@@ -11,12 +11,10 @@ export const dateRangeOptionSchema = z.object({
11
11
  label: z.string(),
12
12
  /**
13
13
  * The start date of the date range in the format `YYYY-MM-DD`.
14
- * If not set, the date range selector will default to the `earliestDate` property.
15
14
  */
16
15
  dateFrom: z.string().date().optional(),
17
16
  /**
18
17
  * The end date of the date range in the format `YYYY-MM-DD`.
19
- * If not set, the date range selector will default to the current date.
20
18
  */
21
19
  dateTo: z.string().date().optional(),
22
20
  });
@@ -45,55 +43,77 @@ export class DateRangeOptionChangedEvent extends CustomEvent<DateRangeValue> {
45
43
  }
46
44
  }
47
45
 
48
- const today = new Date();
46
+ type DateRangeOptionPresets = {
47
+ last2Weeks: DateRangeOption;
48
+ lastMonth: DateRangeOption;
49
+ last2Months: DateRangeOption;
50
+ last3Months: DateRangeOption;
51
+ last6Months: DateRangeOption;
52
+ lastYear: DateRangeOption;
53
+ };
49
54
 
50
- const twoWeeksAgo = new Date();
51
- twoWeeksAgo.setDate(today.getDate() - 14);
55
+ let dateRangeOptionsPresetsCacheDate: string | null = null;
56
+ let dateRangeOptionPresetsCache: DateRangeOptionPresets | null = null;
52
57
 
53
- const lastMonth = new Date(today);
54
- lastMonth.setMonth(today.getMonth() - 1);
58
+ /**
59
+ * Presets for the `gs-date-range-filter` component that can be used as `dateRangeOptions`.
60
+ */
61
+ export const dateRangeOptionPresets = (): DateRangeOptionPresets => {
62
+ const today = new Date();
63
+ const todayString = new Date().toISOString().slice(0, 10);
55
64
 
56
- const last2Months = new Date(today);
57
- last2Months.setMonth(today.getMonth() - 2);
65
+ if (
66
+ dateRangeOptionPresetsCache === null ||
67
+ dateRangeOptionsPresetsCacheDate === null ||
68
+ dateRangeOptionsPresetsCacheDate !== todayString
69
+ ) {
70
+ dateRangeOptionsPresetsCacheDate = todayString;
58
71
 
59
- const last3Months = new Date(today);
60
- last3Months.setMonth(today.getMonth() - 3);
72
+ const twoWeeksAgo = new Date();
73
+ twoWeeksAgo.setDate(today.getDate() - 14);
61
74
 
62
- const last6Months = new Date(today);
63
- last6Months.setMonth(today.getMonth() - 6);
75
+ const lastMonth = new Date(today);
76
+ lastMonth.setMonth(today.getMonth() - 1);
64
77
 
65
- const lastYear = new Date(today);
66
- lastYear.setFullYear(today.getFullYear() - 1);
78
+ const last2Months = new Date(today);
79
+ last2Months.setMonth(today.getMonth() - 2);
67
80
 
68
- /**
69
- * Presets for the `gs-date-range-filter` component that can be used as `dateRangeOptions`.
70
- */
71
- export const dateRangeOptionPresets = {
72
- last2Weeks: {
73
- label: 'Last 2 weeks',
74
- dateFrom: toYYYYMMDD(twoWeeksAgo),
75
- },
76
- lastMonth: {
77
- label: 'Last month',
78
- dateFrom: toYYYYMMDD(lastMonth),
79
- },
80
- last2Months: {
81
- label: 'Last 2 months',
82
- dateFrom: toYYYYMMDD(last2Months),
83
- },
84
- last3Months: {
85
- label: 'Last 3 months',
86
- dateFrom: toYYYYMMDD(last3Months),
87
- },
88
- last6Months: {
89
- label: 'Last 6 months',
90
- dateFrom: toYYYYMMDD(last6Months),
91
- },
92
- lastYear: {
93
- label: 'Last year',
94
- dateFrom: toYYYYMMDD(lastYear),
95
- },
96
- allTimes: {
97
- label: 'All times',
98
- },
99
- } satisfies Record<string, DateRangeOption>;
81
+ const last3Months = new Date(today);
82
+ last3Months.setMonth(today.getMonth() - 3);
83
+
84
+ const last6Months = new Date(today);
85
+ last6Months.setMonth(today.getMonth() - 6);
86
+
87
+ const lastYear = new Date(today);
88
+ lastYear.setFullYear(today.getFullYear() - 1);
89
+
90
+ dateRangeOptionPresetsCache = {
91
+ last2Weeks: {
92
+ label: 'Last 2 weeks',
93
+ dateFrom: toYYYYMMDD(twoWeeksAgo),
94
+ },
95
+ lastMonth: {
96
+ label: 'Last month',
97
+ dateFrom: toYYYYMMDD(lastMonth),
98
+ },
99
+ last2Months: {
100
+ label: 'Last 2 months',
101
+ dateFrom: toYYYYMMDD(last2Months),
102
+ },
103
+ last3Months: {
104
+ label: 'Last 3 months',
105
+ dateFrom: toYYYYMMDD(last3Months),
106
+ },
107
+ last6Months: {
108
+ label: 'Last 6 months',
109
+ dateFrom: toYYYYMMDD(last6Months),
110
+ },
111
+ lastYear: {
112
+ label: 'Last year',
113
+ dateFrom: toYYYYMMDD(lastYear),
114
+ },
115
+ };
116
+ }
117
+
118
+ return dateRangeOptionPresetsCache;
119
+ };
@@ -5,26 +5,3 @@ export const getSelectableOptions = (dateRangeOptions: DateRangeOption[]) => {
5
5
  return { label: customSelectOption.label, value: customSelectOption.label };
6
6
  });
7
7
  };
8
-
9
- export const getDatesForSelectorValue = (
10
- initialSelectedDateRange: string | undefined,
11
- dateRangeOptions: DateRangeOption[],
12
- earliestDate: string,
13
- ) => {
14
- const today = new Date();
15
- const defaultDates = { dateFrom: new Date(earliestDate), dateTo: today };
16
-
17
- if (initialSelectedDateRange === undefined) {
18
- return defaultDates;
19
- }
20
-
21
- const dateRangeOption = dateRangeOptions.find((option) => option.label === initialSelectedDateRange);
22
- if (dateRangeOption) {
23
- return {
24
- dateFrom: new Date(dateRangeOption.dateFrom ?? earliestDate),
25
- dateTo: new Date(dateRangeOption.dateTo ?? today),
26
- };
27
- }
28
-
29
- return defaultDates;
30
- };
@@ -80,6 +80,73 @@ export const Default: StoryObj<StatisticsProps> = {
80
80
  },
81
81
  };
82
82
 
83
+ export const ValuesAre0: StoryObj<StatisticsProps> = {
84
+ ...Default,
85
+ parameters: {
86
+ fetchMock: {
87
+ mocks: [
88
+ {
89
+ matcher: {
90
+ name: 'denominatorData',
91
+ url: AGGREGATED_ENDPOINT,
92
+ body: {
93
+ fields: [],
94
+ country: 'USA',
95
+ division: 'Alabama',
96
+ },
97
+ },
98
+ response: {
99
+ status: 200,
100
+ body: {
101
+ data: [
102
+ {
103
+ count: 0,
104
+ },
105
+ ],
106
+ info: {
107
+ dataVersion: '1712315293',
108
+ requestId: '4603a85e-ae5e-495d-aee5-778a3af862c1',
109
+ },
110
+ },
111
+ },
112
+ },
113
+ {
114
+ matcher: {
115
+ name: 'numeratorData',
116
+ url: AGGREGATED_ENDPOINT,
117
+ body: {
118
+ fields: [],
119
+ country: 'USA',
120
+ },
121
+ },
122
+ response: {
123
+ status: 200,
124
+ body: {
125
+ data: [
126
+ {
127
+ count: 0,
128
+ },
129
+ ],
130
+ info: {
131
+ dataVersion: '1712315293',
132
+ requestId: '4603a85e-ae5e-495d-aee5-778a3af862c1',
133
+ },
134
+ },
135
+ },
136
+ },
137
+ ],
138
+ },
139
+ },
140
+ play: async ({ canvasElement }) => {
141
+ const canvas = within(canvasElement);
142
+
143
+ await waitFor(async () => {
144
+ await expect(canvas.getByText('0')).toBeInTheDocument();
145
+ await expect(canvas.getByText('-%')).toBeInTheDocument();
146
+ });
147
+ },
148
+ };
149
+
83
150
  export const FiresFinishedLoadingEvent: StoryObj<StatisticsProps> = {
84
151
  ...Default,
85
152
  play: playThatExpectsFinishedLoadingEvent(),
@@ -74,7 +74,9 @@ const MetricDataTabs: FunctionComponent<MetricDataTabsProps> = ({ data }) => {
74
74
 
75
75
  <div className='stat'>
76
76
  <div className='stat-title'>Overall proportion</div>
77
- <div className='stat-value text-2xl sm:text-4xl'>{formatProportion(proportion)}</div>
77
+ <div className='stat-value text-2xl sm:text-4xl'>
78
+ {Number.isFinite(proportion) ? formatProportion(proportion) : '-%'}
79
+ </div>
78
80
  <div className='stat-desc text-wrap'>The proportion among all sequenced samples</div>
79
81
  </div>
80
82
  </div>
@@ -8,7 +8,6 @@ import { LAPIS_URL } from '../../constants';
8
8
  import { type DateRangeFilterProps } from '../../preact/dateRangeFilter/date-range-filter';
9
9
  import './gs-date-range-filter';
10
10
  import '../gs-app';
11
- import { toYYYYMMDD } from '../../preact/dateRangeFilter/dateConversion';
12
11
  import { dateRangeOptionPresets } from '../../preact/dateRangeFilter/dateRangeOption';
13
12
  import { gsEventNames } from '../../utils/gsEventNames';
14
13
  import { withinShadowRoot } from '../withinShadowRoot.story';
@@ -16,7 +15,6 @@ import { withinShadowRoot } from '../withinShadowRoot.story';
16
15
  const codeExample = String.raw`
17
16
  <gs-date-range-filter
18
17
  dateRangeOptions='[{ "label": "Year 2021", "dateFrom": "2021-01-01", "dateTo": "2021-12-31" }]'
19
- earliestDate="1970-01-01"
20
18
  value="Year 2021"
21
19
  width="100%"
22
20
  lapisDateField="myDateColumn"
@@ -47,9 +45,6 @@ const meta: Meta<Required<DateRangeFilterProps>> = {
47
45
  dateRangeOptions: {
48
46
  control: { type: 'object' },
49
47
  },
50
- earliestDate: {
51
- control: { type: 'text' },
52
- },
53
48
  width: {
54
49
  control: { type: 'text' },
55
50
  },
@@ -59,14 +54,12 @@ const meta: Meta<Required<DateRangeFilterProps>> = {
59
54
  },
60
55
  args: {
61
56
  dateRangeOptions: [
62
- dateRangeOptionPresets.lastMonth,
63
- dateRangeOptionPresets.last3Months,
64
- dateRangeOptionPresets.allTimes,
57
+ dateRangeOptionPresets().lastMonth,
58
+ dateRangeOptionPresets().last3Months,
65
59
  { label: '2021', dateFrom: '2021-01-01', dateTo: '2021-12-31' },
66
60
  customDateRange,
67
61
  ],
68
- earliestDate: '1970-01-01',
69
- value: dateRangeOptionPresets.lastMonth.label,
62
+ value: dateRangeOptionPresets().lastMonth.label,
70
63
  lapisDateField: 'aDateColumn',
71
64
  width: '100%',
72
65
  placeholder: 'Date range',
@@ -82,7 +75,6 @@ export const Default: StoryObj<Required<DateRangeFilterProps>> = {
82
75
  <div class="max-w-(--breakpoint-lg)">
83
76
  <gs-date-range-filter
84
77
  .dateRangeOptions=${args.dateRangeOptions}
85
- .earliestDate=${args.earliestDate}
86
78
  .value=${args.value}
87
79
  .width=${args.width}
88
80
  .lapisDateField=${args.lapisDateField}
@@ -98,7 +90,6 @@ export const TestRenderAttributesInHtmlInsteadOfUsingPropertyExpression: StoryOb
98
90
  <div class="max-w-(--breakpoint-lg)">
99
91
  <gs-date-range-filter
100
92
  .dateRangeOptions=${args.dateRangeOptions}
101
- earliestDate="${args.earliestDate}"
102
93
  value="${args.value ?? 'null'}"
103
94
  width="${args.width}"
104
95
  lapisDateField="${args.lapisDateField}"
@@ -155,7 +146,7 @@ export const FiresEvents: StoryObj<Required<DateRangeFilterProps>> = {
155
146
  await expect(placeholderOption).toHaveTextContent('Last month');
156
147
  });
157
148
  await waitFor(async () => {
158
- await expect(dateToPicker(canvas)).toHaveValue(toYYYYMMDD(new Date()));
149
+ await expect(dateToPicker(canvas)).toHaveValue('');
159
150
  });
160
151
  });
161
152
 
@@ -64,24 +64,18 @@ export class DateRangeFilterComponent extends PreactLitAdapter {
64
64
  * The `label` will be shown to the user, and it will be available as `value`.
65
65
  * The dates must be in the format `YYYY-MM-DD`.
66
66
  *
67
- * If dateFrom or dateTo is not set, the component will default to the `earliestDate` or the current date.
67
+ * If dateFrom or dateTo is not set, the component will leave the corresponding input field empty.
68
68
  *
69
69
  * We provide some options in `dateRangeOptionPresets` for convenience.
70
70
  */
71
71
  @property({ type: Array })
72
72
  dateRangeOptions: { label: string; dateFrom?: string; dateTo?: string }[] = [];
73
73
 
74
- /**
75
- * The `dateFrom` value to use in the `allTimes` preset in the format `YYYY-MM-DD`.
76
- */
77
- @property({ type: String })
78
- earliestDate: string = '1900-01-01';
79
-
80
74
  /**
81
75
  * The value to use for this date range selector.
82
76
  * - If it is a string, then it must be a valid label from the `dateRangeOptions`.
83
77
  * - If it is an object, then it accepts dates in the format `YYYY-MM-DD` for the keys `dateFrom` and `dateTo`.
84
- * Keys that are not set will default to the `earliestDate` or the current date respectively.
78
+ * Keys that are not set will leave the corresponding input field empty.
85
79
  *
86
80
  * The `detail` of the `gs-date-range-option-changed` event can be used for this attribute,
87
81
  * if you want to control this component in your JS application.
@@ -128,7 +122,6 @@ export class DateRangeFilterComponent extends PreactLitAdapter {
128
122
  return (
129
123
  <DateRangeFilter
130
124
  dateRangeOptions={this.dateRangeOptions}
131
- earliestDate={this.earliestDate}
132
125
  value={this.value}
133
126
  lapisDateField={this.lapisDateField}
134
127
  width={this.width}
@@ -162,9 +155,6 @@ declare global {
162
155
  type CustomSelectOptionsMatches = Expect<
163
156
  Equals<typeof DateRangeFilterComponent.prototype.dateRangeOptions, DateRangeFilterProps['dateRangeOptions']>
164
157
  >;
165
- type EarliestDateMatches = Expect<
166
- Equals<typeof DateRangeFilterComponent.prototype.earliestDate, DateRangeFilterProps['earliestDate']>
167
- >;
168
158
  type ValueMatches = Expect<Equals<typeof DateRangeFilterComponent.prototype.value, DateRangeFilterProps['value']>>;
169
159
  type WidthMatches = Expect<Equals<typeof DateRangeFilterComponent.prototype.width, DateRangeFilterProps['width']>>;
170
160
  type DateColumnMatches = Expect<