@genspectrum/dashboard-components 0.1.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.
Files changed (186) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +109 -0
  3. package/custom-elements.json +1587 -0
  4. package/dist/dashboard-components.js +7322 -0
  5. package/dist/dashboard-components.js.map +1 -0
  6. package/dist/genspectrum-components.d.ts +298 -0
  7. package/dist/style.css +2930 -0
  8. package/package.json +109 -0
  9. package/src/constants.ts +6 -0
  10. package/src/index.ts +1 -0
  11. package/src/lapisApi/ReferenceGenome.ts +30 -0
  12. package/src/lapisApi/__mockData__/referenceGenome.json +58 -0
  13. package/src/lapisApi/lapisApi.ts +99 -0
  14. package/src/lapisApi/lapisTypes.ts +51 -0
  15. package/src/operator/Dataset.ts +3 -0
  16. package/src/operator/DivisionOperator.spec.ts +27 -0
  17. package/src/operator/DivisionOperator.ts +60 -0
  18. package/src/operator/FetchAggregatedOperator.ts +44 -0
  19. package/src/operator/FetchInsertionsOperator.ts +24 -0
  20. package/src/operator/FetchSubstitutionsOrDeletionsOperator.ts +49 -0
  21. package/src/operator/FillMissingOperator.spec.ts +26 -0
  22. package/src/operator/FillMissingOperator.ts +30 -0
  23. package/src/operator/GroupByAndSumOperator.spec.ts +26 -0
  24. package/src/operator/GroupByAndSumOperator.ts +26 -0
  25. package/src/operator/GroupByOperator.spec.ts +43 -0
  26. package/src/operator/GroupByOperator.ts +32 -0
  27. package/src/operator/MapOperator.spec.ts +13 -0
  28. package/src/operator/MapOperator.ts +16 -0
  29. package/src/operator/MockOperator.spec.ts +11 -0
  30. package/src/operator/MockOperator.ts +12 -0
  31. package/src/operator/Operator.ts +5 -0
  32. package/src/operator/SlidingOperator.spec.ts +52 -0
  33. package/src/operator/SlidingOperator.ts +23 -0
  34. package/src/operator/SortOperator.spec.ts +13 -0
  35. package/src/operator/SortOperator.ts +16 -0
  36. package/src/preact/LapisUrlContext.ts +3 -0
  37. package/src/preact/ReferenceGenomeContext.ts +5 -0
  38. package/src/preact/components/SegmentSelector.tsx +62 -0
  39. package/src/preact/components/chart.stories.tsx +42 -0
  40. package/src/preact/components/chart.tsx +32 -0
  41. package/src/preact/components/checkbox-selector.stories.tsx +56 -0
  42. package/src/preact/components/checkbox-selector.tsx +46 -0
  43. package/src/preact/components/confidence-interval-selector.tsx +45 -0
  44. package/src/preact/components/csv-download-button.stories.tsx +25 -0
  45. package/src/preact/components/csv-download-button.tsx +51 -0
  46. package/src/preact/components/error-display.stories.tsx +22 -0
  47. package/src/preact/components/error-display.tsx +5 -0
  48. package/src/preact/components/headline.stories.tsx +29 -0
  49. package/src/preact/components/headline.tsx +16 -0
  50. package/src/preact/components/info.stories.tsx +22 -0
  51. package/src/preact/components/info.tsx +16 -0
  52. package/src/preact/components/loading-display.stories.tsx +20 -0
  53. package/src/preact/components/loading-display.tsx +5 -0
  54. package/src/preact/components/min-max-percent-slider.css +40 -0
  55. package/src/preact/components/min-max-range-slider.tsx +95 -0
  56. package/src/preact/components/mutation-type-selector.tsx +30 -0
  57. package/src/preact/components/no-data-display.stories.tsx +20 -0
  58. package/src/preact/components/no-data-display.tsx +5 -0
  59. package/src/preact/components/percent-intput.tsx +49 -0
  60. package/src/preact/components/proportion-selector-dropdown.stories.tsx +66 -0
  61. package/src/preact/components/proportion-selector-dropdown.tsx +33 -0
  62. package/src/preact/components/proportion-selector.stories.tsx +81 -0
  63. package/src/preact/components/proportion-selector.tsx +43 -0
  64. package/src/preact/components/scaling-selector.stories.tsx +25 -0
  65. package/src/preact/components/scaling-selector.tsx +36 -0
  66. package/src/preact/components/select.stories.tsx +42 -0
  67. package/src/preact/components/select.tsx +21 -0
  68. package/src/preact/components/table.stories.tsx +24 -0
  69. package/src/preact/components/table.tsx +51 -0
  70. package/src/preact/components/tabs.stories.tsx +60 -0
  71. package/src/preact/components/tabs.tsx +49 -0
  72. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +32 -0
  73. package/src/preact/dateRangeSelector/date-range-selector.tsx +228 -0
  74. package/src/preact/dateRangeSelector/dateConversion.ts +8 -0
  75. package/src/preact/locationFilter/__mockData__/aggregated.json +775 -0
  76. package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +36 -0
  77. package/src/preact/locationFilter/fetchAutocompletionList.ts +43 -0
  78. package/src/preact/locationFilter/location-filter.stories.tsx +50 -0
  79. package/src/preact/locationFilter/location-filter.tsx +112 -0
  80. package/src/preact/mutationComparison/__mockData__/nucleotideMutationsOtherVariant.json +295 -0
  81. package/src/preact/mutationComparison/__mockData__/nucleotideMutationsSomeVariant.json +304 -0
  82. package/src/preact/mutationComparison/fetchMutationData.spec.ts +118 -0
  83. package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +125 -0
  84. package/src/preact/mutationComparison/getMutationComparisonTableData.ts +40 -0
  85. package/src/preact/mutationComparison/mutation-comparison-table.tsx +43 -0
  86. package/src/preact/mutationComparison/mutation-comparison-venn.tsx +122 -0
  87. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +152 -0
  88. package/src/preact/mutationComparison/mutation-comparison.tsx +179 -0
  89. package/src/preact/mutationComparison/queryMutationData.ts +53 -0
  90. package/src/preact/mutationFilter/mutation-filter.stories.tsx +164 -0
  91. package/src/preact/mutationFilter/mutation-filter.tsx +268 -0
  92. package/src/preact/mutationFilter/parseAndValidateMutation.ts +54 -0
  93. package/src/preact/mutationFilter/parseMutation.spec.ts +150 -0
  94. package/src/preact/mutationFilter/sequenceTypeFromSegment.spec.ts +66 -0
  95. package/src/preact/mutationFilter/sequenceTypeFromSegment.ts +20 -0
  96. package/src/preact/mutations/__mockData__/nucleotideInsertions.json +252 -0
  97. package/src/preact/mutations/__mockData__/nucleotideMutations.json +880 -0
  98. package/src/preact/mutations/getInsertionsTableData.spec.ts +36 -0
  99. package/src/preact/mutations/getInsertionsTableData.ts +10 -0
  100. package/src/preact/mutations/getMutationsGridData.spec.ts +135 -0
  101. package/src/preact/mutations/getMutationsGridData.ts +92 -0
  102. package/src/preact/mutations/getMutationsTableData.spec.ts +94 -0
  103. package/src/preact/mutations/getMutationsTableData.ts +17 -0
  104. package/src/preact/mutations/mutations-grid.tsx +84 -0
  105. package/src/preact/mutations/mutations-insertions-table.tsx +33 -0
  106. package/src/preact/mutations/mutations-table.tsx +47 -0
  107. package/src/preact/mutations/mutations.stories.tsx +95 -0
  108. package/src/preact/mutations/mutations.tsx +192 -0
  109. package/src/preact/mutations/queryMutations.ts +55 -0
  110. package/src/preact/prevalenceOverTime/__mockData__/denominator.json +1700 -0
  111. package/src/preact/prevalenceOverTime/__mockData__/denominatorOneVariant.json +608 -0
  112. package/src/preact/prevalenceOverTime/__mockData__/numeratorEG.json +1560 -0
  113. package/src/preact/prevalenceOverTime/__mockData__/numeratorJN1.json +592 -0
  114. package/src/preact/prevalenceOverTime/__mockData__/numeratorOneVariant.json +604 -0
  115. package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.spec.ts +67 -0
  116. package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.ts +18 -0
  117. package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +105 -0
  118. package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +86 -0
  119. package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +141 -0
  120. package/src/preact/prevalenceOverTime/prevalence-over-time-table.tsx +46 -0
  121. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +165 -0
  122. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +202 -0
  123. package/src/preact/relativeGrowthAdvantage/__mockData__/denominator.json +376 -0
  124. package/src/preact/relativeGrowthAdvantage/__mockData__/numerator.json +332 -0
  125. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +138 -0
  126. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +71 -0
  127. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +136 -0
  128. package/src/preact/shared/charts/LogitScale.ts +48 -0
  129. package/src/preact/shared/charts/colors.ts +26 -0
  130. package/src/preact/shared/charts/confideceInterval.ts +29 -0
  131. package/src/preact/shared/charts/getYAxisScale.ts +16 -0
  132. package/src/preact/shared/charts/scales.ts +16 -0
  133. package/src/preact/shared/icons/DeleteIcon.tsx +17 -0
  134. package/src/preact/shared/sort/sortInsertions.spec.ts +47 -0
  135. package/src/preact/shared/sort/sortInsertions.ts +21 -0
  136. package/src/preact/shared/sort/sortMutationPositions.spec.ts +31 -0
  137. package/src/preact/shared/sort/sortMutationPositions.ts +14 -0
  138. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +47 -0
  139. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +17 -0
  140. package/src/preact/shared/table/formatProportion.ts +3 -0
  141. package/src/preact/textInput/__mockData__/aggregated_hosts.json +24 -0
  142. package/src/preact/textInput/fetchAutocompleteList.ts +9 -0
  143. package/src/preact/textInput/text-input.stories.tsx +49 -0
  144. package/src/preact/textInput/text-input.tsx +73 -0
  145. package/src/preact/useQuery.ts +27 -0
  146. package/src/query/queryInsertions.ts +14 -0
  147. package/src/query/queryPrevalenceOverTime.ts +126 -0
  148. package/src/query/queryRelativeGrowthAdvantage.ts +131 -0
  149. package/src/query/querySubstitutionsOrDeletions.ts +19 -0
  150. package/src/styles/tailwind.css +3 -0
  151. package/src/styles/tailwind.d.ts +3 -0
  152. package/src/types.ts +23 -0
  153. package/src/utils/mutations.spec.ts +64 -0
  154. package/src/utils/mutations.ts +165 -0
  155. package/src/utils/temporal.spec.ts +97 -0
  156. package/src/utils/temporal.ts +348 -0
  157. package/src/utils/test-utils.ts +5 -0
  158. package/src/utils/type-utils.ts +15 -0
  159. package/src/utils/utils.spec.ts +16 -0
  160. package/src/utils/utils.ts +38 -0
  161. package/src/web-components/PreactLitAdapter.tsx +62 -0
  162. package/src/web-components/PreactLitAdapterWithGridJsStyles.tsx +12 -0
  163. package/src/web-components/app.ts +51 -0
  164. package/src/web-components/display/index.ts +4 -0
  165. package/src/web-components/display/mutation-comparison-component.stories.ts +138 -0
  166. package/src/web-components/display/mutation-comparison-component.tsx +31 -0
  167. package/src/web-components/display/mutations-component.stories.ts +107 -0
  168. package/src/web-components/display/mutations-component.tsx +27 -0
  169. package/src/web-components/display/prevalence-over-time-component.stories.ts +205 -0
  170. package/src/web-components/display/prevalence-over-time-component.tsx +46 -0
  171. package/src/web-components/display/relative-growth-advantage-component.stories.ts +89 -0
  172. package/src/web-components/display/relative-growth-advantage-component.tsx +37 -0
  173. package/src/web-components/index.ts +3 -0
  174. package/src/web-components/input/date-range-selector-component.stories.ts +53 -0
  175. package/src/web-components/input/date-range-selector-component.tsx +33 -0
  176. package/src/web-components/input/index.ts +4 -0
  177. package/src/web-components/input/location-filter-component.stories.ts +184 -0
  178. package/src/web-components/input/location-filter-component.tsx +68 -0
  179. package/src/web-components/input/location-filter.mdx +25 -0
  180. package/src/web-components/input/mutation-filter-component.stories.ts +97 -0
  181. package/src/web-components/input/mutation-filter-component.tsx +27 -0
  182. package/src/web-components/input/text-input-component.stories.ts +92 -0
  183. package/src/web-components/input/text-input-component.tsx +30 -0
  184. package/src/web-components/lapis-context.ts +3 -0
  185. package/src/web-components/reference-genome-context.ts +5 -0
  186. package/src/web-components/withinShadowRoot.story.ts +34 -0
@@ -0,0 +1,43 @@
1
+ import { type FunctionComponent } from 'preact';
2
+
3
+ import { MinMaxRangeSlider } from './min-max-range-slider';
4
+ import { PercentInput } from './percent-intput';
5
+
6
+ export type ProportionInterval = { min: number; max: number };
7
+
8
+ export interface ProportionSelectorProps {
9
+ proportionInterval: ProportionInterval;
10
+ setMinProportion: (minProportion: number) => void;
11
+ setMaxProportion: (maxProportion: number) => void;
12
+ }
13
+
14
+ export const ProportionSelector: FunctionComponent<ProportionSelectorProps> = ({
15
+ proportionInterval,
16
+ setMinProportion,
17
+ setMaxProportion,
18
+ }) => {
19
+ const { min: minProportion, max: maxProportion } = proportionInterval;
20
+ return (
21
+ <div class='flex flex-col w-64 mb-2'>
22
+ <div class='flex items-center '>
23
+ <PercentInput
24
+ percentage={minProportion * 100}
25
+ setPercentage={(percentage) => setMinProportion(percentage / 100)}
26
+ />
27
+ <div class='m-2'>-</div>
28
+ <PercentInput
29
+ percentage={maxProportion * 100}
30
+ setPercentage={(percentage) => setMaxProportion(percentage / 100)}
31
+ />
32
+ </div>
33
+ <div class='my-1'>
34
+ <MinMaxRangeSlider
35
+ min={minProportion * 100}
36
+ max={maxProportion * 100}
37
+ setMin={(percentage) => setMinProportion(percentage / 100)}
38
+ setMax={(percentage) => setMaxProportion(percentage / 100)}
39
+ />
40
+ </div>
41
+ </div>
42
+ );
43
+ };
@@ -0,0 +1,25 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
3
+
4
+ import { ScalingSelector, type ScalingSelectorProps } from './scaling-selector';
5
+ import { type SelectProps } from './select';
6
+
7
+ const meta: Meta<SelectProps> = {
8
+ title: 'Component/Scaling selector',
9
+ component: ScalingSelector,
10
+ parameters: { fetchMock: {} },
11
+ };
12
+
13
+ export default meta;
14
+
15
+ export const ScalingSelectorStory: StoryObj<ScalingSelectorProps> = {
16
+ args: {
17
+ yAxisScaleType: 'linear',
18
+ setYAxisScaleType: fn(),
19
+ },
20
+ play: async ({ canvasElement, args }) => {
21
+ const canvas = within(canvasElement);
22
+ await userEvent.selectOptions(canvas.getByRole('combobox'), 'logarithmic');
23
+ await waitFor(() => expect(args.setYAxisScaleType).toHaveBeenCalledWith('logarithmic'));
24
+ },
25
+ };
@@ -0,0 +1,36 @@
1
+ import { type FunctionComponent } from 'preact';
2
+
3
+ import { Select } from './select';
4
+ import type { ScaleType } from '../shared/charts/getYAxisScale';
5
+
6
+ export type ScalingSelectorProps = {
7
+ yAxisScaleType: ScaleType;
8
+ setYAxisScaleType: (scaleType: ScaleType) => void;
9
+ className?: string;
10
+ };
11
+
12
+ export const ScalingSelector: FunctionComponent<ScalingSelectorProps> = ({
13
+ yAxisScaleType,
14
+ setYAxisScaleType,
15
+ className,
16
+ }) => {
17
+ const items = [
18
+ { label: 'y axis scaling type', value: 'none', disabled: true },
19
+ { label: 'Linear', value: 'linear' },
20
+ { label: 'Logarithmic', value: 'logarithmic' },
21
+ { label: 'Logit', value: 'logit' },
22
+ ];
23
+
24
+ return (
25
+ <Select
26
+ items={items}
27
+ selected={yAxisScaleType}
28
+ onChange={(event: Event) => {
29
+ const select = event.target as HTMLSelectElement;
30
+ const value = select.value as ScaleType;
31
+ setYAxisScaleType(value);
32
+ }}
33
+ selectStyle={`${className} select-xs select-bordered`}
34
+ />
35
+ );
36
+ };
@@ -0,0 +1,42 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
3
+
4
+ import { Select, type SelectProps } from './select';
5
+
6
+ const meta: Meta<SelectProps> = {
7
+ title: 'Component/Select',
8
+ component: Select,
9
+ argTypes: {
10
+ onChange: { action: true },
11
+ },
12
+ parameters: { fetchMock: {} },
13
+ };
14
+
15
+ export default meta;
16
+
17
+ export const SelectStory: StoryObj<SelectProps> = {
18
+ args: {
19
+ items: [
20
+ { label: 'Disabled first element', disabled: true, value: 'does not matter' },
21
+ { label: 'First Option', value: 'firstOption' },
22
+ { label: 'Second Option', value: 'secondOption' },
23
+ ],
24
+ selected: 'firstOption',
25
+ selectStyle: '',
26
+ onChange: fn(),
27
+ },
28
+ play: async ({ canvasElement, args }) => {
29
+ const canvas = within(canvasElement);
30
+ await userEvent.selectOptions(canvas.getByRole('combobox'), 'secondOption');
31
+ await waitFor(() =>
32
+ expect(args.onChange).toHaveBeenCalledWith(
33
+ expect.objectContaining({
34
+ type: 'change',
35
+ target: expect.objectContaining({
36
+ value: 'secondOption',
37
+ }),
38
+ }),
39
+ ),
40
+ );
41
+ },
42
+ };
@@ -0,0 +1,21 @@
1
+ import { type FunctionComponent } from 'preact';
2
+ import { type JSXInternal } from 'preact/src/jsx';
3
+
4
+ export interface SelectProps {
5
+ items: { label: string; value: string; disabled?: boolean }[];
6
+ selected: string;
7
+ onChange: JSXInternal.GenericEventHandler<HTMLSelectElement>;
8
+ selectStyle?: string;
9
+ }
10
+
11
+ export const Select: FunctionComponent<SelectProps> = ({ items, selected, onChange, selectStyle }) => {
12
+ return (
13
+ <select class={`select select-bordered ${selectStyle}`} value={selected} onChange={onChange}>
14
+ {items.map((item) => (
15
+ <option key={item.value} value={item.value} disabled={item.disabled}>
16
+ {item.label}
17
+ </option>
18
+ ))}
19
+ </select>
20
+ );
21
+ };
@@ -0,0 +1,24 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+
3
+ import { Table } from './table';
4
+
5
+ const meta: Meta = {
6
+ title: 'Component/Table',
7
+ component: Table,
8
+ parameters: { fetchMock: {} },
9
+ };
10
+
11
+ export default meta;
12
+
13
+ export const TableStory: StoryObj = {
14
+ render: (args) => {
15
+ return <Table data={args.data} columns={args.columns} pagination={false} />;
16
+ },
17
+ args: {
18
+ data: [
19
+ ['John Do', 'john@example.com', '123-456-7890'],
20
+ ['Jane Doe', 'jane@example.com', '098-765-4321'],
21
+ ],
22
+ columns: [{ name: 'Name' }, { name: 'Email', sort: true }, { name: 'Phone Number' }],
23
+ },
24
+ };
@@ -0,0 +1,51 @@
1
+ import { Grid } from 'gridjs';
2
+ import { type OneDArray, type TColumn, type TData } from 'gridjs/dist/src/types';
3
+ import { type PaginationConfig } from 'gridjs/dist/src/view/plugin/pagination';
4
+ import { type ComponentChild } from 'preact';
5
+ import { useEffect, useRef } from 'preact/hooks';
6
+ import 'gridjs/dist/theme/mermaid.css';
7
+
8
+ export const tableStyle = {
9
+ table: {
10
+ fontSize: '12px',
11
+ },
12
+ th: {
13
+ padding: '4px',
14
+ textAlign: 'center',
15
+ },
16
+ td: {
17
+ textAlign: 'center',
18
+ padding: '8px',
19
+ },
20
+ footer: {
21
+ fontSize: '12px',
22
+ },
23
+ };
24
+
25
+ interface TableProps {
26
+ data: TData;
27
+ columns: OneDArray<TColumn | string | ComponentChild>;
28
+ pagination: PaginationConfig | boolean;
29
+ }
30
+
31
+ export const Table = ({ data, columns, pagination }: TableProps) => {
32
+ const wrapper = useRef(null);
33
+
34
+ useEffect(() => {
35
+ if (wrapper.current === null) {
36
+ return;
37
+ }
38
+ const grid = new Grid({
39
+ columns,
40
+ data,
41
+ style: tableStyle,
42
+ pagination,
43
+ }).render(wrapper.current);
44
+
45
+ return () => {
46
+ grid.destroy();
47
+ };
48
+ });
49
+
50
+ return <div ref={wrapper} />;
51
+ };
@@ -0,0 +1,60 @@
1
+ import { type StoryObj } from '@storybook/preact';
2
+ import { expect, fireEvent, waitFor, within } from '@storybook/test';
3
+ import { type Meta } from '@storybook/web-components';
4
+
5
+ import Tabs from './tabs';
6
+
7
+ const meta: Meta = {
8
+ title: 'Component/Tabs',
9
+ component: 'ComponentTabs',
10
+ parameters: { fetchMock: {} },
11
+ };
12
+
13
+ export default meta;
14
+
15
+ export const TabsStory: StoryObj = {
16
+ render: () => {
17
+ const firstTab = {
18
+ title: 'Tab 1',
19
+ content: <p>Tab 1 Content</p>,
20
+ };
21
+
22
+ const secondTab = {
23
+ title: 'Tab 2',
24
+ content: <p>Tab 2 Content</p>,
25
+ };
26
+
27
+ const toolbar = <p>Toolbar</p>;
28
+
29
+ return <Tabs tabs={[firstTab, secondTab]} toolbar={toolbar} />;
30
+ },
31
+ };
32
+
33
+ export const TabsWithToolbarOnlyShowingOnSecondTab: StoryObj = {
34
+ render: () => {
35
+ const firstTab = {
36
+ title: 'FirstTab',
37
+ content: <p>Tab 1 Content</p>,
38
+ };
39
+
40
+ const secondTab = {
41
+ title: 'SecondTab',
42
+ content: <p>Tab 2 Content</p>,
43
+ };
44
+
45
+ const toolbar = (activeTab: string) => {
46
+ return <>{activeTab === 'SecondTab' && <p>Toolbar</p>}</>;
47
+ };
48
+
49
+ return <Tabs tabs={[firstTab, secondTab]} toolbar={toolbar} />;
50
+ },
51
+ play: async ({ canvasElement }) => {
52
+ const canvas = within(canvasElement);
53
+
54
+ await waitFor(() => expect(canvas.getByLabelText('FirstTab', { selector: 'input' })).toBeVisible());
55
+ await expect(canvas.queryByText('Toolbar')).not.toBeInTheDocument();
56
+
57
+ await fireEvent.click(canvas.getByLabelText('SecondTab', { selector: 'input' }));
58
+ await waitFor(() => expect(canvas.getByText('Toolbar')).toBeVisible());
59
+ },
60
+ };
@@ -0,0 +1,49 @@
1
+ import { Fragment, type FunctionComponent } from 'preact';
2
+ import { useState } from 'preact/hooks';
3
+ import { type JSXInternal } from 'preact/src/jsx';
4
+
5
+ type Tab = {
6
+ title: string;
7
+ content: JSXInternal.Element;
8
+ };
9
+
10
+ interface ComponentTabsProps {
11
+ tabs: Tab[];
12
+ toolbar?: JSXInternal.Element | ((activeTab: string) => JSXInternal.Element);
13
+ }
14
+
15
+ const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
16
+ const [activeTab, setActiveTab] = useState(tabs[0].title);
17
+
18
+ const tabNames = tabs.map((tab) => tab.title).join(', ');
19
+
20
+ const tabElements = tabs.map((tab) => {
21
+ return (
22
+ <Fragment key={tab.title}>
23
+ <input
24
+ type='radio'
25
+ name={tabNames}
26
+ role='tab'
27
+ className='tab'
28
+ aria-label={tab.title}
29
+ checked={activeTab === tab.title}
30
+ onChange={() => setActiveTab(tab.title)}
31
+ />
32
+ <div role='tabpanel' className='tab-content bg-base-100 border-base-300 rounded-box p-1'>
33
+ {tab.content}
34
+ </div>
35
+ </Fragment>
36
+ );
37
+ });
38
+
39
+ const toolbarElement = typeof toolbar === 'function' ? toolbar(activeTab) : toolbar;
40
+
41
+ return (
42
+ <div role='tablist' className='tabs tabs-lifted'>
43
+ {tabElements}
44
+ {toolbar && <div className='m-1 col-[9999]'>{toolbarElement}</div>}
45
+ </div>
46
+ );
47
+ };
48
+
49
+ export default Tabs;
@@ -0,0 +1,32 @@
1
+ import { withActions } from '@storybook/addon-actions/decorator';
2
+ import { type Meta, type StoryObj } from '@storybook/preact';
3
+
4
+ import { DateRangeSelector, type DateRangeSelectorProps } from './date-range-selector';
5
+ import { LAPIS_URL } from '../../constants';
6
+ import { LapisUrlContext } from '../LapisUrlContext';
7
+
8
+ const meta: Meta<DateRangeSelectorProps> = {
9
+ title: 'Input/DateRangeSelector',
10
+ component: DateRangeSelector,
11
+ parameters: {
12
+ actions: {
13
+ handles: ['gs-date-range-changed'],
14
+ },
15
+ fetchMock: {},
16
+ },
17
+ args: {
18
+ customSelectOptions: [{ label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' }],
19
+ earliestDate: '1970-01-01',
20
+ },
21
+ decorators: [withActions],
22
+ };
23
+
24
+ export default meta;
25
+
26
+ export const Primary: StoryObj<DateRangeSelectorProps> = {
27
+ render: (args) => (
28
+ <LapisUrlContext.Provider value={LAPIS_URL}>
29
+ <DateRangeSelector customSelectOptions={args.customSelectOptions} earliestDate={args.earliestDate} />
30
+ </LapisUrlContext.Provider>
31
+ ),
32
+ };
@@ -0,0 +1,228 @@
1
+ import flatpickr from 'flatpickr';
2
+ import { type FunctionComponent } from 'preact';
3
+ import 'flatpickr/dist/flatpickr.min.css';
4
+ import { useEffect, useRef, useState } from 'preact/hooks';
5
+
6
+ import { toYYYYMMDD } from './dateConversion';
7
+ import { Select } from '../components/select';
8
+ import type { ScaleType } from '../shared/charts/getYAxisScale';
9
+
10
+ export type CustomSelectOption = { label: string; dateFrom: string; dateTo: string };
11
+
12
+ export interface DateRangeSelectorProps {
13
+ customSelectOptions: CustomSelectOption[];
14
+ earliestDate?: string;
15
+ }
16
+
17
+ export const DateRangeSelector: FunctionComponent<DateRangeSelectorProps> = ({
18
+ customSelectOptions,
19
+ earliestDate = '1900-01-01',
20
+ }) => {
21
+ const datePickerRef = useRef<HTMLInputElement>(null);
22
+ const endDatePickerRef = useRef<HTMLInputElement>(null);
23
+ const divRef = useRef<HTMLDivElement>(null);
24
+ const [dateFromPicker, setDateFromPicker] = useState<flatpickr.Instance | null>(null);
25
+ const [dateToPicker, setDateToPicker] = useState<flatpickr.Instance | null>(null);
26
+
27
+ const [selectedDateRange, setSelectedDateRange] = useState('last6Months');
28
+ const [selectedDates, setSelectedDates] = useState<{ dateFrom: Date; dateTo: Date }>({
29
+ dateFrom: getDatesForSelectorValue('last6Months', customSelectOptions, earliestDate).dateFrom,
30
+ dateTo: getDatesForSelectorValue('last6Months', customSelectOptions, earliestDate).dateTo,
31
+ });
32
+
33
+ useEffect(() => {
34
+ const commonConfig = {
35
+ allowInput: true,
36
+ dateFormat: 'Y-m-d',
37
+ };
38
+
39
+ if (datePickerRef.current) {
40
+ setDateFromPicker(
41
+ flatpickr(datePickerRef.current, {
42
+ ...commonConfig,
43
+ defaultDate: selectedDates.dateFrom,
44
+ }),
45
+ );
46
+ }
47
+
48
+ if (endDatePickerRef.current) {
49
+ setDateToPicker(
50
+ flatpickr(endDatePickerRef.current, {
51
+ ...commonConfig,
52
+ defaultDate: selectedDates.dateTo,
53
+ }),
54
+ );
55
+ }
56
+
57
+ return () => {
58
+ dateFromPicker?.destroy();
59
+ dateToPicker?.destroy();
60
+ };
61
+ // eslint-disable-next-line react-hooks/exhaustive-deps
62
+ }, [datePickerRef, endDatePickerRef]);
63
+
64
+ const selectableOptions = () => {
65
+ const presetOptions = [
66
+ { label: 'Custom', value: 'custom' },
67
+ { label: 'All times', value: 'allTimes' },
68
+ { label: 'Last 2 weeks', value: 'last2Weeks' },
69
+ { label: 'Last month', value: 'lastMonth' },
70
+ { label: 'Last 2 weeks', value: 'last2Weeks' },
71
+ { label: 'Last month', value: 'lastMonth' },
72
+ { label: 'Last 2 months', value: 'last2Months' },
73
+ { label: 'Last 3 months', value: 'last3Months' },
74
+ { label: 'Last 6 months', value: 'last6Months' },
75
+ ];
76
+
77
+ const customOptions = customSelectOptions.map((customSelectOption) => {
78
+ return { label: customSelectOption.label, value: customLabelToOptionValue(customSelectOption.label) };
79
+ });
80
+
81
+ return [...presetOptions, ...customOptions];
82
+ };
83
+
84
+ const onSelectChange = (value: string) => {
85
+ setSelectedDateRange(value);
86
+
87
+ const dateRange = getDatesForSelectorValue(value, customSelectOptions, earliestDate);
88
+
89
+ dateToPicker?.set('minDate', dateRange.dateFrom);
90
+ dateFromPicker?.set('maxDate', dateRange.dateTo);
91
+
92
+ dateFromPicker?.setDate(dateRange.dateFrom);
93
+ dateToPicker?.setDate(dateRange.dateTo);
94
+
95
+ setSelectedDates({
96
+ dateFrom: dateRange.dateFrom,
97
+ dateTo: dateRange.dateTo,
98
+ });
99
+
100
+ submit();
101
+ };
102
+
103
+ const onChangeDateFrom = () => {
104
+ if (selectedDates.dateFrom.toDateString() === dateFromPicker?.selectedDates[0].toDateString()) {
105
+ return;
106
+ }
107
+
108
+ selectedDates.dateFrom = dateFromPicker?.selectedDates[0] || new Date();
109
+ dateToPicker?.set('minDate', dateFromPicker?.selectedDates[0]);
110
+ setSelectedDateRange('custom');
111
+
112
+ submit();
113
+ };
114
+
115
+ const onChangeDateTo = () => {
116
+ if (selectedDates.dateTo.toDateString() === dateToPicker?.selectedDates[0].toDateString()) {
117
+ return;
118
+ }
119
+
120
+ selectedDates.dateTo = dateToPicker?.selectedDates[0] || new Date();
121
+ dateFromPicker?.set('maxDate', dateToPicker?.selectedDates[0]);
122
+ setSelectedDateRange('custom');
123
+
124
+ submit();
125
+ };
126
+
127
+ const submit = () => {
128
+ const dateFrom = toYYYYMMDD(dateFromPicker?.selectedDates[0]);
129
+ const dateTo = toYYYYMMDD(dateToPicker?.selectedDates[0]);
130
+
131
+ const detail = {
132
+ ...(dateFrom !== undefined && { dateFrom }),
133
+ ...(dateTo !== undefined && { dateTo }),
134
+ };
135
+
136
+ divRef.current?.dispatchEvent(
137
+ new CustomEvent('gs-date-range-changed', {
138
+ detail,
139
+ bubbles: true,
140
+ composed: true,
141
+ }),
142
+ );
143
+ };
144
+
145
+ return (
146
+ <div class='join' ref={divRef}>
147
+ <Select
148
+ items={selectableOptions()}
149
+ selected={selectedDateRange}
150
+ selectStyle='select-bordered rounded-none join-item'
151
+ onChange={(event: Event) => {
152
+ event.preventDefault();
153
+ const select = event.target as HTMLSelectElement;
154
+ const value = select.value as ScaleType;
155
+ onSelectChange(value);
156
+ }}
157
+ />
158
+ <input
159
+ class='input input-bordered rounded-none join-item'
160
+ type='text'
161
+ placeholder='Date from'
162
+ ref={datePickerRef}
163
+ onChange={onChangeDateFrom}
164
+ onBlur={onChangeDateFrom}
165
+ />
166
+ <input
167
+ class='input input-bordered rounded-none join-item'
168
+ type='text'
169
+ placeholder='Date to'
170
+ ref={endDatePickerRef}
171
+ onChange={onChangeDateTo}
172
+ onBlur={onChangeDateFrom}
173
+ />
174
+ </div>
175
+ );
176
+ };
177
+
178
+ const customLabelToOptionValue = (customLabel: string) => {
179
+ return `${customLabel}customLabel`;
180
+ };
181
+
182
+ const getDatesForSelectorValue = (
183
+ selectorValue: string,
184
+ customSelectOptions: CustomSelectOption[],
185
+ earliestDate: string,
186
+ ) => {
187
+ const today = new Date();
188
+
189
+ const customSelectOption = customSelectOptions.find(
190
+ (option) => customLabelToOptionValue(option.label) === selectorValue,
191
+ );
192
+ if (customSelectOption) {
193
+ return { dateFrom: new Date(customSelectOption.dateFrom), dateTo: new Date(customSelectOption.dateTo) };
194
+ }
195
+
196
+ switch (selectorValue) {
197
+ case 'last2Weeks': {
198
+ const twoWeeksAgo = new Date(today);
199
+ twoWeeksAgo.setDate(today.getDate() - 14);
200
+ return { dateFrom: twoWeeksAgo, dateTo: today };
201
+ }
202
+ case 'lastMonth': {
203
+ const lastMonth = new Date(today);
204
+ lastMonth.setMonth(today.getMonth() - 1);
205
+ return { dateFrom: lastMonth, dateTo: today };
206
+ }
207
+ case 'last2Months': {
208
+ const twoMonthsAgo = new Date(today);
209
+ twoMonthsAgo.setMonth(today.getMonth() - 2);
210
+ return { dateFrom: twoMonthsAgo, dateTo: today };
211
+ }
212
+ case 'last3Months': {
213
+ const threeMonthsAgo = new Date(today);
214
+ threeMonthsAgo.setMonth(today.getMonth() - 3);
215
+ return { dateFrom: threeMonthsAgo, dateTo: today };
216
+ }
217
+ case 'last6Months': {
218
+ const sixMonthsAgo = new Date(today);
219
+ sixMonthsAgo.setMonth(today.getMonth() - 6);
220
+ return { dateFrom: sixMonthsAgo, dateTo: today };
221
+ }
222
+ case 'allTimes': {
223
+ return { dateFrom: new Date(earliestDate), dateTo: today };
224
+ }
225
+ default:
226
+ return { dateFrom: today, dateTo: today };
227
+ }
228
+ };
@@ -0,0 +1,8 @@
1
+ export const toYYYYMMDD = (date?: Date) => {
2
+ if (!date) {
3
+ return undefined;
4
+ }
5
+
6
+ const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit' };
7
+ return date.toLocaleDateString('en-CA', options);
8
+ };