@genspectrum/dashboard-components 0.1.2 → 0.1.4
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/README.md +1 -1
- package/custom-elements.json +453 -67
- package/dist/dashboard-components.js +778 -488
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +277 -50
- package/dist/style.css +132 -139
- package/package.json +9 -5
- package/src/lapisApi/lapisApi.ts +1 -1
- package/src/preact/aggregatedData/__mockData__/aggregated.json +585 -0
- package/src/preact/aggregatedData/aggregate-table.tsx +32 -0
- package/src/preact/aggregatedData/aggregate.stories.tsx +53 -0
- package/src/preact/aggregatedData/aggregate.tsx +102 -0
- package/src/preact/components/ReferenceGenomesAwaiter.tsx +25 -0
- package/src/preact/components/csv-download-button.tsx +8 -2
- package/src/preact/components/headline.tsx +16 -4
- package/src/preact/components/min-max-range-slider.tsx +4 -4
- package/src/preact/components/percent-intput.tsx +2 -3
- package/src/preact/components/resize-container.tsx +23 -0
- package/src/preact/components/table.tsx +1 -0
- package/src/preact/components/tabs.stories.tsx +2 -2
- package/src/preact/components/tabs.tsx +47 -24
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +36 -4
- package/src/preact/dateRangeSelector/date-range-selector.tsx +57 -43
- package/src/preact/locationFilter/location-filter.tsx +2 -2
- package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +5 -5
- package/src/preact/mutationComparison/getMutationComparisonTableData.ts +45 -10
- package/src/preact/mutationComparison/mutation-comparison-table.tsx +20 -22
- package/src/preact/mutationComparison/mutation-comparison-venn.tsx +6 -3
- package/src/preact/mutationComparison/mutation-comparison.stories.tsx +8 -1
- package/src/preact/mutationComparison/mutation-comparison.tsx +13 -4
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +70 -31
- package/src/preact/mutationFilter/mutation-filter.tsx +62 -14
- package/src/preact/mutations/getInsertionsTableData.spec.ts +6 -4
- package/src/preact/mutations/getInsertionsTableData.ts +1 -1
- package/src/preact/mutations/getMutationsTableData.spec.ts +9 -19
- package/src/preact/mutations/getMutationsTableData.ts +1 -1
- package/src/preact/mutations/mutations-insertions-table.tsx +3 -1
- package/src/preact/mutations/mutations-table.tsx +3 -1
- package/src/preact/mutations/mutations.stories.tsx +8 -1
- package/src/preact/mutations/mutations.tsx +16 -5
- package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +1 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +1 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +1 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +4 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +17 -9
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +8 -5
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +12 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +13 -8
- package/src/preact/shared/sort/sortInsertions.spec.ts +11 -10
- package/src/preact/shared/sort/sortInsertions.ts +10 -17
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +19 -10
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +45 -12
- package/src/preact/textInput/text-input.stories.tsx +22 -1
- package/src/preact/textInput/text-input.tsx +3 -1
- package/src/query/queryAggregateData.spec.ts +32 -0
- package/src/query/queryAggregateData.ts +25 -0
- package/src/utils/typeAssertions.spec.ts +31 -0
- package/src/utils/typeAssertions.ts +16 -0
- package/src/web-components/PreactLitAdapter.tsx +0 -1
- package/src/web-components/app.stories.ts +129 -0
- package/src/web-components/app.ts +27 -6
- package/src/web-components/display/aggregate-component.stories.ts +73 -0
- package/src/web-components/display/aggregate-component.tsx +58 -0
- package/src/web-components/display/index.ts +1 -0
- package/src/web-components/display/mutation-comparison-component.stories.ts +29 -11
- package/src/web-components/display/mutation-comparison-component.tsx +72 -4
- package/src/web-components/display/mutations-component.stories.ts +14 -13
- package/src/web-components/display/mutations-component.tsx +14 -1
- package/src/web-components/display/prevalence-over-time-component.stories.ts +20 -18
- package/src/web-components/display/prevalence-over-time-component.tsx +12 -0
- package/src/web-components/display/relative-growth-advantage-component.stories.ts +11 -10
- package/src/web-components/display/relative-growth-advantage-component.tsx +12 -0
- package/src/web-components/input/date-range-selector-component.stories.ts +35 -8
- package/src/web-components/input/date-range-selector-component.tsx +18 -5
- package/src/web-components/input/location-filter-component.stories.ts +17 -8
- package/src/web-components/input/location-filter-component.tsx +2 -6
- package/src/web-components/input/mutation-filter-component.stories.ts +20 -9
- package/src/web-components/input/mutation-filter-component.tsx +10 -2
- package/src/web-components/input/text-input-component.stories.ts +13 -4
- package/src/web-components/input/text-input-component.tsx +11 -2
- package/src/web-components/input/location-filter.mdx +0 -25
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { useContext } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { AggregateTable } from './aggregate-table';
|
|
5
|
+
import { type AggregateData, queryAggregateData } from '../../query/queryAggregateData';
|
|
6
|
+
import { type LapisFilter } from '../../types';
|
|
7
|
+
import { LapisUrlContext } from '../LapisUrlContext';
|
|
8
|
+
import { CsvDownloadButton } from '../components/csv-download-button';
|
|
9
|
+
import { ErrorDisplay } from '../components/error-display';
|
|
10
|
+
import Headline from '../components/headline';
|
|
11
|
+
import Info from '../components/info';
|
|
12
|
+
import { LoadingDisplay } from '../components/loading-display';
|
|
13
|
+
import { NoDataDisplay } from '../components/no-data-display';
|
|
14
|
+
import { ResizeContainer, type Size } from '../components/resize-container';
|
|
15
|
+
import Tabs from '../components/tabs';
|
|
16
|
+
import { useQuery } from '../useQuery';
|
|
17
|
+
|
|
18
|
+
export type View = 'table';
|
|
19
|
+
|
|
20
|
+
export interface AggregateProps {
|
|
21
|
+
filter: LapisFilter;
|
|
22
|
+
fields: string[];
|
|
23
|
+
views: View[];
|
|
24
|
+
size?: Size;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Aggregate: FunctionComponent<AggregateProps> = ({ fields, views, filter, size }) => {
|
|
28
|
+
const lapis = useContext(LapisUrlContext);
|
|
29
|
+
|
|
30
|
+
const { data, error, isLoading } = useQuery(async () => {
|
|
31
|
+
return queryAggregateData(filter, fields, lapis);
|
|
32
|
+
}, [filter, fields, lapis]);
|
|
33
|
+
|
|
34
|
+
const headline = 'Aggregate';
|
|
35
|
+
|
|
36
|
+
if (isLoading) {
|
|
37
|
+
return (
|
|
38
|
+
<Headline heading={headline}>
|
|
39
|
+
<LoadingDisplay />
|
|
40
|
+
</Headline>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (error !== null) {
|
|
45
|
+
return (
|
|
46
|
+
<Headline heading={headline}>
|
|
47
|
+
<ErrorDisplay error={error} />
|
|
48
|
+
</Headline>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (data === null) {
|
|
53
|
+
return (
|
|
54
|
+
<Headline heading={headline}>
|
|
55
|
+
<NoDataDisplay />
|
|
56
|
+
</Headline>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<ResizeContainer size={size} defaultSize={{ height: '700px', width: '100%' }}>
|
|
62
|
+
<Headline heading={headline}>
|
|
63
|
+
<AggregatedDataTabs data={data} views={views} fields={fields} />
|
|
64
|
+
</Headline>
|
|
65
|
+
</ResizeContainer>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type AggregatedDataTabsProps = {
|
|
70
|
+
data: AggregateData;
|
|
71
|
+
fields: string[];
|
|
72
|
+
views: View[];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const AggregatedDataTabs: FunctionComponent<AggregatedDataTabsProps> = ({ data, views, fields }) => {
|
|
76
|
+
const getTab = (view: View) => {
|
|
77
|
+
switch (view) {
|
|
78
|
+
case 'table':
|
|
79
|
+
return {
|
|
80
|
+
title: 'Table',
|
|
81
|
+
content: <AggregateTable data={data} fields={fields} />,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const tabs = views.map((view) => getTab(view));
|
|
87
|
+
|
|
88
|
+
return <Tabs tabs={tabs} toolbar={<Toolbar data={data} />} />;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
type ToolbarProps = {
|
|
92
|
+
data: AggregateData;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const Toolbar: FunctionComponent<ToolbarProps> = ({ data }) => {
|
|
96
|
+
return (
|
|
97
|
+
<div class='flex flex-row'>
|
|
98
|
+
<CsvDownloadButton className='mx-1 btn btn-xs' getData={() => data} filename='aggregate.csv' />
|
|
99
|
+
<Info className='mx-1' content='Info for aggregate' />
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ComponentChildren, type FunctionalComponent } from 'preact';
|
|
2
|
+
import { useContext } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { type ReferenceGenome } from '../../lapisApi/ReferenceGenome';
|
|
5
|
+
import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sometimes the reference genome is not immediately available.
|
|
9
|
+
* This component will display a loading spinner until the reference genome is available.
|
|
10
|
+
* Child components can assume that the reference genome is available on the first render,
|
|
11
|
+
* which e.g. matters for initial values of `useState`.
|
|
12
|
+
*/
|
|
13
|
+
export const ReferenceGenomesAwaiter: FunctionalComponent<{ children: ComponentChildren }> = ({ children }) => {
|
|
14
|
+
const referenceGenome = useContext(ReferenceGenomeContext);
|
|
15
|
+
|
|
16
|
+
if (isNotInitialized(referenceGenome)) {
|
|
17
|
+
return <div className='laoding loading-spinner loading-md'>Loading...</div>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return <>{children}</>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function isNotInitialized(referenceGenome: ReferenceGenome) {
|
|
24
|
+
return referenceGenome.nucleotideSequences.length === 0 && referenceGenome.genes.length === 0;
|
|
25
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
|
|
3
|
+
type ToStringable = {
|
|
4
|
+
toString: () => string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type DataValue = string | number | boolean | null | ToStringable;
|
|
8
|
+
|
|
3
9
|
export interface CsvDownloadButtonProps {
|
|
4
10
|
label?: string;
|
|
5
11
|
filename?: string;
|
|
6
|
-
getData: () => Record<string,
|
|
12
|
+
getData: () => Record<string, DataValue>[];
|
|
7
13
|
className?: string;
|
|
8
14
|
}
|
|
9
15
|
|
|
@@ -32,7 +38,7 @@ export const CsvDownloadButton: FunctionComponent<CsvDownloadButtonProps> = ({
|
|
|
32
38
|
return header + rows;
|
|
33
39
|
};
|
|
34
40
|
|
|
35
|
-
const getDataKeys = (data: Record<string,
|
|
41
|
+
const getDataKeys = (data: Record<string, DataValue>[]) => {
|
|
36
42
|
const keysSet = data
|
|
37
43
|
.map((row) => Object.keys(row))
|
|
38
44
|
.reduce((accumulatedKeys, keys) => {
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
3
|
|
|
3
4
|
export interface HeadlineProps {
|
|
4
5
|
heading: string;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
const Headline: FunctionComponent<HeadlineProps> = ({ heading, children }) => {
|
|
9
|
+
const ref = useRef<HTMLHeadingElement>(null);
|
|
10
|
+
|
|
11
|
+
const [h1Height, setH1Height] = useState('2rem');
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (ref.current) {
|
|
15
|
+
const h1Height = ref.current.getBoundingClientRect().height;
|
|
16
|
+
setH1Height(`${h1Height}px`);
|
|
17
|
+
}
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
8
20
|
return (
|
|
9
|
-
|
|
10
|
-
<h1>{heading}</h1>
|
|
11
|
-
{children}
|
|
12
|
-
|
|
21
|
+
<div className='h-full w-full'>
|
|
22
|
+
<h1 ref={ref}>{heading}</h1>
|
|
23
|
+
<div style={{ height: `calc(100% - ${h1Height})` }}>{children}</div>
|
|
24
|
+
</div>
|
|
13
25
|
);
|
|
14
26
|
};
|
|
15
27
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type FunctionComponent } from 'preact';
|
|
1
|
+
import { type FunctionComponent, type JSX } from 'preact';
|
|
2
2
|
import { useState } from 'preact/hooks';
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
import './min-max-percent-slider.css';
|
|
5
5
|
|
|
6
6
|
export interface MinMaxPercentSliderProps {
|
|
@@ -27,7 +27,7 @@ export const MinMaxRangeSlider: FunctionComponent<MinMaxPercentSliderProps> = ({
|
|
|
27
27
|
|
|
28
28
|
const [zIndexTo, setZIndexTo] = useState(0);
|
|
29
29
|
|
|
30
|
-
const onMinChange = (event:
|
|
30
|
+
const onMinChange = (event: JSX.TargetedInputEvent<HTMLInputElement>) => {
|
|
31
31
|
const input = event.target as HTMLInputElement;
|
|
32
32
|
const minValue = Number(input.value);
|
|
33
33
|
|
|
@@ -39,7 +39,7 @@ export const MinMaxRangeSlider: FunctionComponent<MinMaxPercentSliderProps> = ({
|
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
const onMaxChange = (event:
|
|
42
|
+
const onMaxChange = (event: JSX.TargetedInputEvent<HTMLInputElement>) => {
|
|
43
43
|
const input = event.target as HTMLInputElement;
|
|
44
44
|
const maxValue = Number(input.value);
|
|
45
45
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { type FunctionComponent } from 'preact';
|
|
1
|
+
import { type FunctionComponent, type JSX } from 'preact';
|
|
2
2
|
import { useEffect, useState } from 'preact/hooks';
|
|
3
|
-
import { type ChangeEvent } from 'react';
|
|
4
3
|
|
|
5
4
|
export type PercentInputProps = {
|
|
6
5
|
percentage: number;
|
|
@@ -18,7 +17,7 @@ export const PercentInput: FunctionComponent<PercentInputProps> = ({ percentage,
|
|
|
18
17
|
setInternalPercentage(percentage);
|
|
19
18
|
}, [percentage]);
|
|
20
19
|
|
|
21
|
-
const handleInputChange = (event:
|
|
20
|
+
const handleInputChange = (event: JSX.TargetedInputEvent<HTMLInputElement>) => {
|
|
22
21
|
const input = event.target as HTMLInputElement;
|
|
23
22
|
const value = Number(input.value);
|
|
24
23
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'preact';
|
|
2
|
+
|
|
3
|
+
export type Size = {
|
|
4
|
+
width?: string;
|
|
5
|
+
height?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export interface ResizeContainerProps {
|
|
9
|
+
size?: Size;
|
|
10
|
+
defaultSize: Size;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ResizeContainer: FunctionComponent<ResizeContainerProps> = ({ children, size, defaultSize }) => {
|
|
14
|
+
return <div style={extendByDefault(size, defaultSize)}>{children}</div>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const extendByDefault = (size: Size | undefined, defaultSize: Size) => {
|
|
18
|
+
if (size === undefined) {
|
|
19
|
+
return defaultSize;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { ...defaultSize, ...size };
|
|
23
|
+
};
|
|
@@ -3,6 +3,7 @@ import { type OneDArray, type TColumn, type TData } from 'gridjs/dist/src/types'
|
|
|
3
3
|
import { type PaginationConfig } from 'gridjs/dist/src/view/plugin/pagination';
|
|
4
4
|
import { type ComponentChild } from 'preact';
|
|
5
5
|
import { useEffect, useRef } from 'preact/hooks';
|
|
6
|
+
|
|
6
7
|
import 'gridjs/dist/theme/mermaid.css';
|
|
7
8
|
|
|
8
9
|
export const tableStyle = {
|
|
@@ -51,10 +51,10 @@ export const TabsWithToolbarOnlyShowingOnSecondTab: StoryObj = {
|
|
|
51
51
|
play: async ({ canvasElement }) => {
|
|
52
52
|
const canvas = within(canvasElement);
|
|
53
53
|
|
|
54
|
-
await waitFor(() => expect(canvas.
|
|
54
|
+
await waitFor(() => expect(canvas.getByRole('button', { name: 'SecondTab' })).toBeVisible());
|
|
55
55
|
await expect(canvas.queryByText('Toolbar')).not.toBeInTheDocument();
|
|
56
56
|
|
|
57
|
-
await fireEvent.click(canvas.
|
|
57
|
+
await fireEvent.click(canvas.getByRole('button', { name: 'SecondTab' }));
|
|
58
58
|
await waitFor(() => expect(canvas.getByText('Toolbar')).toBeVisible());
|
|
59
59
|
},
|
|
60
60
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment, type FunctionComponent } from 'preact';
|
|
2
|
-
import { useState } from 'preact/hooks';
|
|
2
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
3
3
|
import { type JSXInternal } from 'preact/src/jsx';
|
|
4
4
|
|
|
5
5
|
type Tab = {
|
|
@@ -14,34 +14,57 @@ interface ComponentTabsProps {
|
|
|
14
14
|
|
|
15
15
|
const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
|
|
16
16
|
const [activeTab, setActiveTab] = useState(tabs[0].title);
|
|
17
|
+
const [heightOfTabs, setHeightOfTabs] = useState('3rem');
|
|
18
|
+
const tabRef = useRef<HTMLDivElement>(null);
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (tabRef.current) {
|
|
22
|
+
const heightOfTabs = tabRef.current.getBoundingClientRect().height;
|
|
23
|
+
setHeightOfTabs(`${heightOfTabs}px`);
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const tabElements = (
|
|
28
|
+
<div className='flex flex-row'>
|
|
29
|
+
{tabs.map((tab) => {
|
|
30
|
+
return (
|
|
31
|
+
<Fragment key={tab.title}>
|
|
32
|
+
<button
|
|
33
|
+
className={`px-4 py-2 text-sm font-medium leading-5 transition-colors duration-150 ${
|
|
34
|
+
activeTab === tab.title
|
|
35
|
+
? 'border-b-2 border-gray-400'
|
|
36
|
+
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-700'
|
|
37
|
+
}`}
|
|
38
|
+
onClick={() => {
|
|
39
|
+
setActiveTab(tab.title);
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
{tab.title}
|
|
43
|
+
</button>
|
|
44
|
+
</Fragment>
|
|
45
|
+
);
|
|
46
|
+
})}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
38
49
|
|
|
39
50
|
const toolbarElement = typeof toolbar === 'function' ? toolbar(activeTab) : toolbar;
|
|
40
51
|
|
|
41
52
|
return (
|
|
42
|
-
<div
|
|
43
|
-
{
|
|
44
|
-
|
|
53
|
+
<div className='h-full w-full'>
|
|
54
|
+
<div ref={tabRef} className='flex flex-row justify-between'>
|
|
55
|
+
{tabElements}
|
|
56
|
+
{toolbar && <div className='py-2'>{toolbarElement}</div>}
|
|
57
|
+
</div>
|
|
58
|
+
<div
|
|
59
|
+
className={`p-2 border-2 border-gray-100 rounded-b-md rounded-tr-md ${activeTab === tabs[0].title ? '' : 'rounded-tl-md'}`}
|
|
60
|
+
style={{ height: `calc(100% - ${heightOfTabs})` }}
|
|
61
|
+
>
|
|
62
|
+
{tabs.map((tab) => (
|
|
63
|
+
<div className='h-full overflow-auto' key={tab.title} hidden={activeTab !== tab.title}>
|
|
64
|
+
{tab.content}
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
45
68
|
</div>
|
|
46
69
|
);
|
|
47
70
|
};
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { withActions } from '@storybook/addon-actions/decorator';
|
|
2
2
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
DateRangeSelector,
|
|
6
|
+
type DateRangeSelectorProps,
|
|
7
|
+
PRESET_VALUE_ALL_TIMES,
|
|
8
|
+
PRESET_VALUE_CUSTOM,
|
|
9
|
+
PRESET_VALUE_LAST_2_MONTHS,
|
|
10
|
+
PRESET_VALUE_LAST_2_WEEKS,
|
|
11
|
+
PRESET_VALUE_LAST_3_MONTHS,
|
|
12
|
+
PRESET_VALUE_LAST_6_MONTHS,
|
|
13
|
+
PRESET_VALUE_LAST_MONTH,
|
|
14
|
+
} from './date-range-selector';
|
|
5
15
|
import { LAPIS_URL } from '../../constants';
|
|
6
16
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
7
17
|
|
|
8
|
-
const meta: Meta<DateRangeSelectorProps
|
|
18
|
+
const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
9
19
|
title: 'Input/DateRangeSelector',
|
|
10
20
|
component: DateRangeSelector,
|
|
11
21
|
parameters: {
|
|
@@ -14,19 +24,41 @@ const meta: Meta<DateRangeSelectorProps> = {
|
|
|
14
24
|
},
|
|
15
25
|
fetchMock: {},
|
|
16
26
|
},
|
|
27
|
+
argTypes: {
|
|
28
|
+
initialValue: {
|
|
29
|
+
control: {
|
|
30
|
+
type: 'select',
|
|
31
|
+
},
|
|
32
|
+
options: [
|
|
33
|
+
PRESET_VALUE_CUSTOM,
|
|
34
|
+
PRESET_VALUE_ALL_TIMES,
|
|
35
|
+
PRESET_VALUE_LAST_2_WEEKS,
|
|
36
|
+
PRESET_VALUE_LAST_MONTH,
|
|
37
|
+
PRESET_VALUE_LAST_2_MONTHS,
|
|
38
|
+
PRESET_VALUE_LAST_3_MONTHS,
|
|
39
|
+
PRESET_VALUE_LAST_6_MONTHS,
|
|
40
|
+
'CustomDateRange',
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
17
44
|
args: {
|
|
18
45
|
customSelectOptions: [{ label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' }],
|
|
19
46
|
earliestDate: '1970-01-01',
|
|
47
|
+
initialValue: PRESET_VALUE_LAST_3_MONTHS,
|
|
20
48
|
},
|
|
21
49
|
decorators: [withActions],
|
|
22
50
|
};
|
|
23
51
|
|
|
24
52
|
export default meta;
|
|
25
53
|
|
|
26
|
-
export const Primary: StoryObj<DateRangeSelectorProps
|
|
54
|
+
export const Primary: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
27
55
|
render: (args) => (
|
|
28
56
|
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
29
|
-
<DateRangeSelector
|
|
57
|
+
<DateRangeSelector
|
|
58
|
+
customSelectOptions={args.customSelectOptions}
|
|
59
|
+
earliestDate={args.earliestDate}
|
|
60
|
+
initialValue={args.initialValue}
|
|
61
|
+
/>
|
|
30
62
|
</LapisUrlContext.Provider>
|
|
31
63
|
),
|
|
32
64
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import flatpickr from 'flatpickr';
|
|
2
|
-
import { type FunctionComponent } from 'preact';
|
|
3
2
|
import 'flatpickr/dist/flatpickr.min.css';
|
|
4
3
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
5
4
|
|
|
@@ -7,24 +6,53 @@ import { toYYYYMMDD } from './dateConversion';
|
|
|
7
6
|
import { Select } from '../components/select';
|
|
8
7
|
import type { ScaleType } from '../shared/charts/getYAxisScale';
|
|
9
8
|
|
|
10
|
-
export type CustomSelectOption = { label:
|
|
9
|
+
export type CustomSelectOption<CustomLabel extends string> = { label: CustomLabel; dateFrom: string; dateTo: string };
|
|
11
10
|
|
|
12
|
-
export interface DateRangeSelectorProps {
|
|
13
|
-
customSelectOptions: CustomSelectOption[];
|
|
11
|
+
export interface DateRangeSelectorProps<CustomLabel extends string> {
|
|
12
|
+
customSelectOptions: CustomSelectOption<CustomLabel>[];
|
|
14
13
|
earliestDate?: string;
|
|
14
|
+
initialValue?: PresetOptionValues | CustomLabel;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export const
|
|
17
|
+
export const PRESET_VALUE_CUSTOM = 'custom';
|
|
18
|
+
export const PRESET_VALUE_ALL_TIMES = 'allTimes';
|
|
19
|
+
export const PRESET_VALUE_LAST_2_WEEKS = 'last2Weeks';
|
|
20
|
+
export const PRESET_VALUE_LAST_MONTH = 'lastMonth';
|
|
21
|
+
export const PRESET_VALUE_LAST_2_MONTHS = 'last2Months';
|
|
22
|
+
export const PRESET_VALUE_LAST_3_MONTHS = 'last3Months';
|
|
23
|
+
export const PRESET_VALUE_LAST_6_MONTHS = 'last6Months';
|
|
24
|
+
|
|
25
|
+
export const presets = {
|
|
26
|
+
[PRESET_VALUE_CUSTOM]: { label: 'Custom' },
|
|
27
|
+
[PRESET_VALUE_ALL_TIMES]: { label: 'All times' },
|
|
28
|
+
[PRESET_VALUE_LAST_2_WEEKS]: { label: 'Last 2 weeks' },
|
|
29
|
+
[PRESET_VALUE_LAST_MONTH]: { label: 'Last month' },
|
|
30
|
+
[PRESET_VALUE_LAST_2_MONTHS]: { label: 'Last 2 months' },
|
|
31
|
+
[PRESET_VALUE_LAST_3_MONTHS]: { label: 'Last 3 months' },
|
|
32
|
+
[PRESET_VALUE_LAST_6_MONTHS]: { label: 'Last 6 months' },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type PresetOptionValues = keyof typeof presets;
|
|
36
|
+
|
|
37
|
+
export const DateRangeSelector = <CustomLabel extends string>({
|
|
18
38
|
customSelectOptions,
|
|
19
39
|
earliestDate = '1900-01-01',
|
|
20
|
-
|
|
40
|
+
initialValue,
|
|
41
|
+
}: DateRangeSelectorProps<CustomLabel>) => {
|
|
21
42
|
const datePickerRef = useRef<HTMLInputElement>(null);
|
|
22
43
|
const endDatePickerRef = useRef<HTMLInputElement>(null);
|
|
23
44
|
const divRef = useRef<HTMLDivElement>(null);
|
|
24
45
|
const [dateFromPicker, setDateFromPicker] = useState<flatpickr.Instance | null>(null);
|
|
25
46
|
const [dateToPicker, setDateToPicker] = useState<flatpickr.Instance | null>(null);
|
|
26
47
|
|
|
27
|
-
const
|
|
48
|
+
const selectableOptions = getSelectableOptions(customSelectOptions);
|
|
49
|
+
|
|
50
|
+
const [selectedDateRange, setSelectedDateRange] = useState<CustomLabel | PresetOptionValues>(
|
|
51
|
+
initialValue !== undefined && selectableOptions.some((option) => option.value === initialValue)
|
|
52
|
+
? initialValue
|
|
53
|
+
: PRESET_VALUE_LAST_6_MONTHS,
|
|
54
|
+
);
|
|
55
|
+
|
|
28
56
|
const [selectedDates, setSelectedDates] = useState<{ dateFrom: Date; dateTo: Date }>({
|
|
29
57
|
dateFrom: getDatesForSelectorValue('last6Months', customSelectOptions, earliestDate).dateFrom,
|
|
30
58
|
dateTo: getDatesForSelectorValue('last6Months', customSelectOptions, earliestDate).dateTo,
|
|
@@ -61,27 +89,7 @@ export const DateRangeSelector: FunctionComponent<DateRangeSelectorProps> = ({
|
|
|
61
89
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
62
90
|
}, [datePickerRef, endDatePickerRef]);
|
|
63
91
|
|
|
64
|
-
const
|
|
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) => {
|
|
92
|
+
const onSelectChange = (value: CustomLabel | PresetOptionValues) => {
|
|
85
93
|
setSelectedDateRange(value);
|
|
86
94
|
|
|
87
95
|
const dateRange = getDatesForSelectorValue(value, customSelectOptions, earliestDate);
|
|
@@ -145,14 +153,14 @@ export const DateRangeSelector: FunctionComponent<DateRangeSelectorProps> = ({
|
|
|
145
153
|
return (
|
|
146
154
|
<div class='join' ref={divRef}>
|
|
147
155
|
<Select
|
|
148
|
-
items={selectableOptions
|
|
156
|
+
items={selectableOptions}
|
|
149
157
|
selected={selectedDateRange}
|
|
150
158
|
selectStyle='select-bordered rounded-none join-item'
|
|
151
159
|
onChange={(event: Event) => {
|
|
152
160
|
event.preventDefault();
|
|
153
161
|
const select = event.target as HTMLSelectElement;
|
|
154
162
|
const value = select.value as ScaleType;
|
|
155
|
-
onSelectChange(value);
|
|
163
|
+
onSelectChange(value as CustomLabel | PresetOptionValues);
|
|
156
164
|
}}
|
|
157
165
|
/>
|
|
158
166
|
<input
|
|
@@ -175,51 +183,57 @@ export const DateRangeSelector: FunctionComponent<DateRangeSelectorProps> = ({
|
|
|
175
183
|
);
|
|
176
184
|
};
|
|
177
185
|
|
|
178
|
-
const
|
|
179
|
-
|
|
186
|
+
const getSelectableOptions = <Label extends string>(customSelectOptions: CustomSelectOption<Label>[]) => {
|
|
187
|
+
const presetOptions = Object.entries(presets).map(([key, value]) => {
|
|
188
|
+
return { label: value.label, value: key };
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const customOptions = customSelectOptions.map((customSelectOption) => {
|
|
192
|
+
return { label: customSelectOption.label, value: customSelectOption.label };
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return [...presetOptions, ...customOptions];
|
|
180
196
|
};
|
|
181
197
|
|
|
182
|
-
const getDatesForSelectorValue = (
|
|
198
|
+
const getDatesForSelectorValue = <Label extends string>(
|
|
183
199
|
selectorValue: string,
|
|
184
|
-
customSelectOptions: CustomSelectOption[],
|
|
200
|
+
customSelectOptions: CustomSelectOption<Label>[],
|
|
185
201
|
earliestDate: string,
|
|
186
202
|
) => {
|
|
187
203
|
const today = new Date();
|
|
188
204
|
|
|
189
|
-
const customSelectOption = customSelectOptions.find(
|
|
190
|
-
(option) => customLabelToOptionValue(option.label) === selectorValue,
|
|
191
|
-
);
|
|
205
|
+
const customSelectOption = customSelectOptions.find((option) => option.label === selectorValue);
|
|
192
206
|
if (customSelectOption) {
|
|
193
207
|
return { dateFrom: new Date(customSelectOption.dateFrom), dateTo: new Date(customSelectOption.dateTo) };
|
|
194
208
|
}
|
|
195
209
|
|
|
196
210
|
switch (selectorValue) {
|
|
197
|
-
case
|
|
211
|
+
case PRESET_VALUE_LAST_2_WEEKS: {
|
|
198
212
|
const twoWeeksAgo = new Date(today);
|
|
199
213
|
twoWeeksAgo.setDate(today.getDate() - 14);
|
|
200
214
|
return { dateFrom: twoWeeksAgo, dateTo: today };
|
|
201
215
|
}
|
|
202
|
-
case
|
|
216
|
+
case PRESET_VALUE_LAST_MONTH: {
|
|
203
217
|
const lastMonth = new Date(today);
|
|
204
218
|
lastMonth.setMonth(today.getMonth() - 1);
|
|
205
219
|
return { dateFrom: lastMonth, dateTo: today };
|
|
206
220
|
}
|
|
207
|
-
case
|
|
221
|
+
case PRESET_VALUE_LAST_2_MONTHS: {
|
|
208
222
|
const twoMonthsAgo = new Date(today);
|
|
209
223
|
twoMonthsAgo.setMonth(today.getMonth() - 2);
|
|
210
224
|
return { dateFrom: twoMonthsAgo, dateTo: today };
|
|
211
225
|
}
|
|
212
|
-
case
|
|
226
|
+
case PRESET_VALUE_LAST_3_MONTHS: {
|
|
213
227
|
const threeMonthsAgo = new Date(today);
|
|
214
228
|
threeMonthsAgo.setMonth(today.getMonth() - 3);
|
|
215
229
|
return { dateFrom: threeMonthsAgo, dateTo: today };
|
|
216
230
|
}
|
|
217
|
-
case
|
|
231
|
+
case PRESET_VALUE_LAST_6_MONTHS: {
|
|
218
232
|
const sixMonthsAgo = new Date(today);
|
|
219
233
|
sixMonthsAgo.setMonth(today.getMonth() - 6);
|
|
220
234
|
return { dateFrom: sixMonthsAgo, dateTo: today };
|
|
221
235
|
}
|
|
222
|
-
case
|
|
236
|
+
case PRESET_VALUE_ALL_TIMES: {
|
|
223
237
|
return { dateFrom: new Date(earliestDate), dateTo: today };
|
|
224
238
|
}
|
|
225
239
|
default:
|
|
@@ -5,11 +5,11 @@ import { LapisUrlContext } from '../LapisUrlContext';
|
|
|
5
5
|
import { useQuery } from '../useQuery';
|
|
6
6
|
|
|
7
7
|
export type LocationFilterProps = {
|
|
8
|
-
|
|
8
|
+
initialValue?: string;
|
|
9
9
|
fields: string[];
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export const LocationFilter = ({
|
|
12
|
+
export const LocationFilter = ({ initialValue, fields }: LocationFilterProps) => {
|
|
13
13
|
const lapis = useContext(LapisUrlContext);
|
|
14
14
|
|
|
15
15
|
const [value, setValue] = useState(initialValue ?? '');
|
|
@@ -51,12 +51,12 @@ describe('getPrevalenceOverTimeTableData', () => {
|
|
|
51
51
|
|
|
52
52
|
expect(result).toEqual([
|
|
53
53
|
{
|
|
54
|
-
mutation: '
|
|
54
|
+
mutation: new Substitution(undefined, 'A', 'T', 123),
|
|
55
55
|
'Test 1 prevalence': 0.123,
|
|
56
56
|
'Test 2 prevalence': 0.345,
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
|
-
mutation: '
|
|
59
|
+
mutation: new Substitution(undefined, 'G', 'A', 234),
|
|
60
60
|
'Test 1 prevalence': 0.567,
|
|
61
61
|
'Test 2 prevalence': 0.789,
|
|
62
62
|
},
|
|
@@ -106,17 +106,17 @@ describe('getPrevalenceOverTimeTableData', () => {
|
|
|
106
106
|
|
|
107
107
|
expect(result).toEqual([
|
|
108
108
|
{
|
|
109
|
-
mutation: '
|
|
109
|
+
mutation: new Substitution(undefined, 'A', 'T', 200),
|
|
110
110
|
'Test 1 prevalence': inRange,
|
|
111
111
|
'Test 2 prevalence': belowRange,
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
|
-
mutation: '
|
|
114
|
+
mutation: new Substitution(undefined, 'A', 'T', 300),
|
|
115
115
|
'Test 1 prevalence': inRange,
|
|
116
116
|
'Test 2 prevalence': inRange,
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
|
-
mutation: '
|
|
119
|
+
mutation: new Substitution(undefined, 'A', 'T', 400),
|
|
120
120
|
'Test 1 prevalence': inRange,
|
|
121
121
|
'Test 2 prevalence': aboveRange,
|
|
122
122
|
},
|