@genspectrum/dashboard-components 0.5.4 → 0.5.6
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 +322 -0
- package/dist/dashboard-components.js +436 -263
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +78 -0
- package/package.json +1 -1
- package/src/preact/aggregatedData/aggregate.tsx +2 -14
- package/src/preact/dateRangeSelector/date-range-selector.tsx +2 -14
- package/src/preact/locationFilter/location-filter.tsx +2 -7
- package/src/preact/mutationComparison/mutation-comparison.tsx +2 -10
- package/src/preact/mutations/mutations.tsx +2 -10
- package/src/preact/numberSequencesOverTime/__mockData__/oneVariantEG.json +1702 -0
- package/src/preact/numberSequencesOverTime/__mockData__/twoVariantsEG.json +1710 -0
- package/src/preact/numberSequencesOverTime/__mockData__/twoVariantsJN1.json +1070 -0
- package/src/preact/numberSequencesOverTime/__mockData__/twoVariantsXBB.json +506 -0
- package/src/preact/numberSequencesOverTime/getNumberOfSequencesOverTimeTableData.spec.ts +75 -0
- package/src/preact/numberSequencesOverTime/getNumberOfSequencesOverTimeTableData.ts +39 -0
- package/src/preact/numberSequencesOverTime/number-sequences-over-time-bar-chart.tsx +58 -0
- package/src/preact/numberSequencesOverTime/number-sequences-over-time-line-chart.tsx +59 -0
- package/src/preact/numberSequencesOverTime/number-sequences-over-time-table.tsx +32 -0
- package/src/preact/numberSequencesOverTime/number-sequences-over-time.stories.tsx +133 -0
- package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +106 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +11 -20
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +2 -20
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +2 -14
- package/src/preact/textInput/text-input.tsx +2 -2
- package/src/query/queryAggregatedDataOverTime.ts +78 -0
- package/src/query/queryNumberOfSequencesOverTime.spec.ts +195 -0
- package/src/query/queryNumberOfSequencesOverTime.ts +33 -0
- package/src/query/queryPrevalenceOverTime.ts +10 -86
- package/src/utils/sort.ts +9 -0
- package/src/utils/temporalTestHelpers.ts +9 -0
- package/src/utils/utils.ts +7 -0
- package/src/web-components/visualization/gs-number-sequences-over-time.stories.ts +243 -0
- package/src/web-components/visualization/gs-number-sequences-over-time.tsx +140 -0
- package/src/web-components/visualization/index.ts +1 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Chart, type ChartConfiguration, type ChartDataset, registerables } from 'chart.js';
|
|
2
|
+
import { useMemo } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { getNumberOfSequencesOverTimeTableData } from './getNumberOfSequencesOverTimeTableData';
|
|
5
|
+
import { type NumberOfSequencesDatasets } from '../../query/queryNumberOfSequencesOverTime';
|
|
6
|
+
import GsChart from '../components/chart';
|
|
7
|
+
import { singleGraphColorRGBAById } from '../shared/charts/colors';
|
|
8
|
+
|
|
9
|
+
interface NumberSequencesOverBarChartProps {
|
|
10
|
+
data: NumberOfSequencesDatasets;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
Chart.register(...registerables);
|
|
14
|
+
|
|
15
|
+
export const NumberSequencesOverTimeLineChart = ({ data }: NumberSequencesOverBarChartProps) => {
|
|
16
|
+
const config: ChartConfiguration = useMemo(
|
|
17
|
+
() => ({
|
|
18
|
+
type: 'line',
|
|
19
|
+
data: {
|
|
20
|
+
datasets: getDatasets(data),
|
|
21
|
+
},
|
|
22
|
+
options: {
|
|
23
|
+
maintainAspectRatio: false,
|
|
24
|
+
animation: false,
|
|
25
|
+
plugins: {
|
|
26
|
+
legend: {
|
|
27
|
+
display: false,
|
|
28
|
+
},
|
|
29
|
+
tooltip: {
|
|
30
|
+
mode: 'index',
|
|
31
|
+
intersect: false,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
[data],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return <GsChart configuration={config} />;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const getDatasets = (data: NumberOfSequencesDatasets) => {
|
|
43
|
+
const tableData = getNumberOfSequencesOverTimeTableData(data, 'date');
|
|
44
|
+
|
|
45
|
+
return data.map(
|
|
46
|
+
({ displayName }, index) =>
|
|
47
|
+
({
|
|
48
|
+
borderWidth: 1,
|
|
49
|
+
pointRadius: 0,
|
|
50
|
+
label: displayName,
|
|
51
|
+
backgroundColor: singleGraphColorRGBAById(index, 0.3),
|
|
52
|
+
borderColor: singleGraphColorRGBAById(index),
|
|
53
|
+
data: tableData.map((row) => ({
|
|
54
|
+
x: row.date,
|
|
55
|
+
y: row[displayName],
|
|
56
|
+
})),
|
|
57
|
+
}) as ChartDataset<'line', { x: string; y: number }[]>,
|
|
58
|
+
);
|
|
59
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useMemo } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
import { getNumberOfSequencesOverTimeTableData } from './getNumberOfSequencesOverTimeTableData';
|
|
4
|
+
import { type NumberOfSequencesDatasets } from '../../query/queryNumberOfSequencesOverTime';
|
|
5
|
+
import { type TemporalGranularity } from '../../types';
|
|
6
|
+
import { Table } from '../components/table';
|
|
7
|
+
|
|
8
|
+
interface NumberSequencesOverTimeTableProps {
|
|
9
|
+
data: NumberOfSequencesDatasets;
|
|
10
|
+
granularity: TemporalGranularity;
|
|
11
|
+
pageSize: boolean | number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const NumberSequencesOverTimeTable = ({ data, granularity, pageSize }: NumberSequencesOverTimeTableProps) => {
|
|
15
|
+
const columns = [
|
|
16
|
+
{
|
|
17
|
+
name: granularity,
|
|
18
|
+
sort: true,
|
|
19
|
+
},
|
|
20
|
+
...data.map((dataset) => ({
|
|
21
|
+
name: dataset.displayName,
|
|
22
|
+
sort: true,
|
|
23
|
+
})),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const flatTableData = useMemo(() => {
|
|
27
|
+
const tableData = getNumberOfSequencesOverTimeTableData(data, granularity);
|
|
28
|
+
return Object.values(tableData).map((row) => Object.values(row));
|
|
29
|
+
}, [data, granularity]);
|
|
30
|
+
|
|
31
|
+
return <Table data={flatTableData} columns={columns} pageSize={pageSize} />;
|
|
32
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { type StoryObj } from '@storybook/preact';
|
|
2
|
+
|
|
3
|
+
import { NumberSequencesOverTime, type NumberSequencesOverTimeProps } from './number-sequences-over-time';
|
|
4
|
+
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
5
|
+
import oneVariantEG from '../../preact/numberSequencesOverTime/__mockData__/oneVariantEG.json';
|
|
6
|
+
import twoVariantsEG from '../../preact/numberSequencesOverTime/__mockData__/twoVariantsEG.json';
|
|
7
|
+
import twoVariantsJN1 from '../../preact/numberSequencesOverTime/__mockData__/twoVariantsJN1.json';
|
|
8
|
+
import { LapisUrlContext } from '../LapisUrlContext';
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
title: 'Visualization/NumberSequencesOverTime',
|
|
12
|
+
component: NumberSequencesOverTime,
|
|
13
|
+
parameters: {
|
|
14
|
+
fetchMock: {},
|
|
15
|
+
},
|
|
16
|
+
argTypes: {
|
|
17
|
+
granularity: {
|
|
18
|
+
options: ['day', 'week', 'month', 'year'],
|
|
19
|
+
control: { type: 'radio' },
|
|
20
|
+
},
|
|
21
|
+
views: {
|
|
22
|
+
options: ['bar', 'line', 'table'],
|
|
23
|
+
control: { type: 'check' },
|
|
24
|
+
},
|
|
25
|
+
pageSize: { control: 'object' },
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const Template: StoryObj<NumberSequencesOverTimeProps> = {
|
|
30
|
+
render: (args) => (
|
|
31
|
+
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
32
|
+
<NumberSequencesOverTime
|
|
33
|
+
lapisFilter={args.lapisFilter}
|
|
34
|
+
lapisDateField={args.lapisDateField}
|
|
35
|
+
views={args.views}
|
|
36
|
+
width={args.width}
|
|
37
|
+
height={args.height}
|
|
38
|
+
headline={args.headline}
|
|
39
|
+
granularity={args.granularity}
|
|
40
|
+
smoothingWindow={args.smoothingWindow}
|
|
41
|
+
pageSize={args.pageSize}
|
|
42
|
+
/>
|
|
43
|
+
</LapisUrlContext.Provider>
|
|
44
|
+
),
|
|
45
|
+
args: {
|
|
46
|
+
views: ['bar', 'line', 'table'],
|
|
47
|
+
lapisFilter: [
|
|
48
|
+
{ displayName: 'EG', lapisFilter: { country: 'USA', pangoLineage: 'EG*', dateFrom: '2022-12-01' } },
|
|
49
|
+
],
|
|
50
|
+
lapisDateField: 'date',
|
|
51
|
+
width: '100%',
|
|
52
|
+
height: '700px',
|
|
53
|
+
headline: 'Number of sequences over time',
|
|
54
|
+
smoothingWindow: 0,
|
|
55
|
+
granularity: 'month',
|
|
56
|
+
pageSize: 10,
|
|
57
|
+
},
|
|
58
|
+
parameters: {
|
|
59
|
+
fetchMock: {
|
|
60
|
+
mocks: [
|
|
61
|
+
{
|
|
62
|
+
matcher: {
|
|
63
|
+
name: 'aggregated',
|
|
64
|
+
url: AGGREGATED_ENDPOINT,
|
|
65
|
+
body: {
|
|
66
|
+
country: 'USA',
|
|
67
|
+
pangoLineage: 'EG*',
|
|
68
|
+
dateFrom: '2022-12-01',
|
|
69
|
+
fields: ['date'],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
response: {
|
|
73
|
+
status: 200,
|
|
74
|
+
body: oneVariantEG,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const Table = {
|
|
83
|
+
...Template,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const TwoVariants = {
|
|
87
|
+
...Template,
|
|
88
|
+
args: {
|
|
89
|
+
...Template.args,
|
|
90
|
+
lapisFilter: [
|
|
91
|
+
{ displayName: 'EG', lapisFilter: { country: 'USA', pangoLineage: 'EG*', dateTo: '2023-06-30' } },
|
|
92
|
+
{ displayName: 'JN.1', lapisFilter: { country: 'USA', pangoLineage: 'JN.1*', dateFrom: '2023-01-01' } },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
parameters: {
|
|
96
|
+
fetchMock: {
|
|
97
|
+
mocks: [
|
|
98
|
+
{
|
|
99
|
+
matcher: {
|
|
100
|
+
name: 'aggregatedEG',
|
|
101
|
+
url: AGGREGATED_ENDPOINT,
|
|
102
|
+
body: {
|
|
103
|
+
country: 'USA',
|
|
104
|
+
pangoLineage: 'EG*',
|
|
105
|
+
dateTo: '2023-06-30',
|
|
106
|
+
fields: ['date'],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
response: {
|
|
110
|
+
status: 200,
|
|
111
|
+
body: twoVariantsEG,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
matcher: {
|
|
116
|
+
name: 'aggregatedJN.1',
|
|
117
|
+
url: AGGREGATED_ENDPOINT,
|
|
118
|
+
body: {
|
|
119
|
+
country: 'USA',
|
|
120
|
+
pangoLineage: 'JN.1*',
|
|
121
|
+
dateFrom: '2023-01-01',
|
|
122
|
+
fields: ['date'],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
response: {
|
|
126
|
+
status: 200,
|
|
127
|
+
body: twoVariantsJN1,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useContext } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
import { NumberSequencesOverTimeBarChart } from './number-sequences-over-time-bar-chart';
|
|
4
|
+
import { NumberSequencesOverTimeLineChart } from './number-sequences-over-time-line-chart';
|
|
5
|
+
import { NumberSequencesOverTimeTable } from './number-sequences-over-time-table';
|
|
6
|
+
import {
|
|
7
|
+
type NumberOfSequencesDatasets,
|
|
8
|
+
queryNumberOfSequencesOverTime,
|
|
9
|
+
} from '../../query/queryNumberOfSequencesOverTime';
|
|
10
|
+
import type { NamedLapisFilter, TemporalGranularity } from '../../types';
|
|
11
|
+
import { LapisUrlContext } from '../LapisUrlContext';
|
|
12
|
+
import { ErrorBoundary } from '../components/error-boundary';
|
|
13
|
+
import { ErrorDisplay } from '../components/error-display';
|
|
14
|
+
import Headline from '../components/headline';
|
|
15
|
+
import { LoadingDisplay } from '../components/loading-display';
|
|
16
|
+
import { NoDataDisplay } from '../components/no-data-display';
|
|
17
|
+
import { ResizeContainer } from '../components/resize-container';
|
|
18
|
+
import Tabs from '../components/tabs';
|
|
19
|
+
import { useQuery } from '../useQuery';
|
|
20
|
+
|
|
21
|
+
type NumberSequencesOverTimeView = 'bar' | 'line' | 'table';
|
|
22
|
+
|
|
23
|
+
export interface NumberSequencesOverTimeProps extends NumberSequencesOverTimeInnerProps {
|
|
24
|
+
width: string;
|
|
25
|
+
height: string;
|
|
26
|
+
headline: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface NumberSequencesOverTimeInnerProps {
|
|
30
|
+
lapisFilter: NamedLapisFilter | NamedLapisFilter[];
|
|
31
|
+
lapisDateField: string;
|
|
32
|
+
views: NumberSequencesOverTimeView[];
|
|
33
|
+
granularity: TemporalGranularity;
|
|
34
|
+
smoothingWindow: number;
|
|
35
|
+
pageSize: boolean | number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const NumberSequencesOverTime = ({ width, height, headline, ...innerProps }: NumberSequencesOverTimeProps) => {
|
|
39
|
+
const size = { height, width };
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<ErrorBoundary size={size} headline={headline}>
|
|
43
|
+
<ResizeContainer size={size}>
|
|
44
|
+
<Headline heading={headline}>
|
|
45
|
+
<NumberSequencesOverTimeInner {...innerProps} />
|
|
46
|
+
</Headline>
|
|
47
|
+
</ResizeContainer>
|
|
48
|
+
</ErrorBoundary>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const NumberSequencesOverTimeInner = ({
|
|
53
|
+
lapisFilter,
|
|
54
|
+
granularity,
|
|
55
|
+
smoothingWindow,
|
|
56
|
+
lapisDateField,
|
|
57
|
+
views,
|
|
58
|
+
pageSize,
|
|
59
|
+
}: NumberSequencesOverTimeInnerProps) => {
|
|
60
|
+
const lapis = useContext(LapisUrlContext);
|
|
61
|
+
|
|
62
|
+
const { data, error, isLoading } = useQuery(() =>
|
|
63
|
+
queryNumberOfSequencesOverTime(lapis, lapisFilter, lapisDateField, granularity, smoothingWindow),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (isLoading) {
|
|
67
|
+
return <LoadingDisplay />;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (error !== null) {
|
|
71
|
+
return <ErrorDisplay error={error} />;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (data === null) {
|
|
75
|
+
return <NoDataDisplay />;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return <NumberSequencesOverTimeTabs views={views} data={data} granularity={granularity} pageSize={pageSize} />;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
interface NumberSequencesOverTimeTabsProps {
|
|
82
|
+
views: NumberSequencesOverTimeView[];
|
|
83
|
+
data: NumberOfSequencesDatasets;
|
|
84
|
+
granularity: TemporalGranularity;
|
|
85
|
+
pageSize: boolean | number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const NumberSequencesOverTimeTabs = ({ views, data, granularity, pageSize }: NumberSequencesOverTimeTabsProps) => {
|
|
89
|
+
const getTab = (view: NumberSequencesOverTimeView) => {
|
|
90
|
+
switch (view) {
|
|
91
|
+
case 'bar':
|
|
92
|
+
return { title: 'Bar', content: <NumberSequencesOverTimeBarChart data={data} /> };
|
|
93
|
+
case 'line':
|
|
94
|
+
return { title: 'Line', content: <NumberSequencesOverTimeLineChart data={data} /> };
|
|
95
|
+
case 'table':
|
|
96
|
+
return {
|
|
97
|
+
title: 'Table',
|
|
98
|
+
content: <NumberSequencesOverTimeTable data={data} granularity={granularity} pageSize={pageSize} />,
|
|
99
|
+
};
|
|
100
|
+
default:
|
|
101
|
+
throw new Error(`Unknown view: ${view}`);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return <Tabs tabs={views.map((view) => getTab(view))} />;
|
|
106
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Chart, type ChartConfiguration, registerables, type TooltipItem } from 'chart.js';
|
|
1
|
+
import { Chart, type ChartConfiguration, type ChartDataset, registerables, type TooltipItem } from 'chart.js';
|
|
2
2
|
import { BarWithErrorBar, BarWithErrorBarsController } from 'chartjs-chart-error-bars';
|
|
3
3
|
|
|
4
4
|
import { maxInData } from './prevalence-over-time';
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type PrevalenceOverTimeVariantData,
|
|
8
8
|
type PrevalenceOverTimeVariantDataPoint,
|
|
9
9
|
} from '../../query/queryPrevalenceOverTime';
|
|
10
|
-
import
|
|
10
|
+
import { sortNullToBeginningThenByDate } from '../../utils/sort';
|
|
11
11
|
import GsChart from '../components/chart';
|
|
12
12
|
import { LogitScale } from '../shared/charts/LogitScale';
|
|
13
13
|
import { singleGraphColorRGBAById } from '../shared/charts/colors';
|
|
@@ -67,30 +67,21 @@ const PrevalenceOverTimeBarChart = ({
|
|
|
67
67
|
return <GsChart configuration={config} />;
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
function sortNullToBeginningThenByDate(
|
|
71
|
-
a: { count: number; prevalence: number; total: number; dateRange: Temporal | null },
|
|
72
|
-
b: {
|
|
73
|
-
count: number;
|
|
74
|
-
prevalence: number;
|
|
75
|
-
total: number;
|
|
76
|
-
dateRange: Temporal | null;
|
|
77
|
-
},
|
|
78
|
-
) {
|
|
79
|
-
return a.dateRange === null
|
|
80
|
-
? -1
|
|
81
|
-
: b.dateRange === null
|
|
82
|
-
? 1
|
|
83
|
-
: a.dateRange.toString().localeCompare(b.dateRange.toString());
|
|
84
|
-
}
|
|
85
|
-
|
|
86
70
|
const getDataset = (
|
|
87
71
|
prevalenceOverTimeVariant: PrevalenceOverTimeVariantData,
|
|
88
72
|
index: number,
|
|
89
73
|
confidenceIntervalMethod: ConfidenceIntervalMethod,
|
|
90
|
-
)
|
|
74
|
+
): ChartDataset<
|
|
75
|
+
typeof BarWithErrorBarsController.id,
|
|
76
|
+
{
|
|
77
|
+
x: string;
|
|
78
|
+
yMin: number | undefined;
|
|
79
|
+
yMax: number | undefined;
|
|
80
|
+
y: number;
|
|
81
|
+
}[]
|
|
82
|
+
> => {
|
|
91
83
|
return {
|
|
92
84
|
borderWidth: 1,
|
|
93
|
-
pointRadius: 0,
|
|
94
85
|
label: prevalenceOverTimeVariant.displayName,
|
|
95
86
|
backgroundColor: singleGraphColorRGBAById(index, 0.3),
|
|
96
87
|
borderColor: singleGraphColorRGBAById(index),
|
|
@@ -46,18 +46,10 @@ export interface PrevalenceOverTimeInnerProps {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export const PrevalenceOverTime: FunctionComponent<PrevalenceOverTimeProps> = ({
|
|
49
|
-
numeratorFilter,
|
|
50
|
-
denominatorFilter,
|
|
51
|
-
granularity,
|
|
52
|
-
smoothingWindow,
|
|
53
|
-
views,
|
|
54
|
-
confidenceIntervalMethods,
|
|
55
49
|
width,
|
|
56
50
|
height,
|
|
57
51
|
headline = 'Prevalence over time',
|
|
58
|
-
|
|
59
|
-
pageSize,
|
|
60
|
-
yAxisMaxConfig,
|
|
52
|
+
...innerProps
|
|
61
53
|
}) => {
|
|
62
54
|
const size = { height, width };
|
|
63
55
|
|
|
@@ -65,17 +57,7 @@ export const PrevalenceOverTime: FunctionComponent<PrevalenceOverTimeProps> = ({
|
|
|
65
57
|
<ErrorBoundary size={size} headline={headline}>
|
|
66
58
|
<ResizeContainer size={size}>
|
|
67
59
|
<Headline heading={headline}>
|
|
68
|
-
<PrevalenceOverTimeInner
|
|
69
|
-
numeratorFilter={numeratorFilter}
|
|
70
|
-
denominatorFilter={denominatorFilter}
|
|
71
|
-
granularity={granularity}
|
|
72
|
-
smoothingWindow={smoothingWindow}
|
|
73
|
-
views={views}
|
|
74
|
-
confidenceIntervalMethods={confidenceIntervalMethods}
|
|
75
|
-
lapisDateField={lapisDateField}
|
|
76
|
-
pageSize={pageSize}
|
|
77
|
-
yAxisMaxConfig={yAxisMaxConfig}
|
|
78
|
-
/>
|
|
60
|
+
<PrevalenceOverTimeInner {...innerProps} />
|
|
79
61
|
</Headline>
|
|
80
62
|
</ResizeContainer>
|
|
81
63
|
</ErrorBoundary>
|
|
@@ -39,15 +39,10 @@ export interface RelativeGrowthAdvantagePropsInner {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export const RelativeGrowthAdvantage: FunctionComponent<RelativeGrowthAdvantageProps> = ({
|
|
42
|
-
views,
|
|
43
42
|
width,
|
|
44
43
|
height,
|
|
45
|
-
numeratorFilter,
|
|
46
|
-
denominatorFilter,
|
|
47
|
-
generationTime,
|
|
48
44
|
headline = 'Relative growth advantage',
|
|
49
|
-
|
|
50
|
-
yAxisMaxConfig,
|
|
45
|
+
...innerProps
|
|
51
46
|
}) => {
|
|
52
47
|
const size = { height, width };
|
|
53
48
|
|
|
@@ -55,14 +50,7 @@ export const RelativeGrowthAdvantage: FunctionComponent<RelativeGrowthAdvantageP
|
|
|
55
50
|
<ErrorBoundary size={size} headline={headline}>
|
|
56
51
|
<ResizeContainer size={size}>
|
|
57
52
|
<Headline heading={headline}>
|
|
58
|
-
<RelativeGrowthAdvantageInner
|
|
59
|
-
views={views}
|
|
60
|
-
numeratorFilter={numeratorFilter}
|
|
61
|
-
denominatorFilter={denominatorFilter}
|
|
62
|
-
generationTime={generationTime}
|
|
63
|
-
lapisDateField={lapisDateField}
|
|
64
|
-
yAxisMaxConfig={yAxisMaxConfig}
|
|
65
|
-
/>
|
|
53
|
+
<RelativeGrowthAdvantageInner {...innerProps} />
|
|
66
54
|
</Headline>
|
|
67
55
|
</ResizeContainer>
|
|
68
56
|
</ErrorBoundary>
|
|
@@ -20,13 +20,13 @@ export interface TextInputProps extends TextInputInnerProps {
|
|
|
20
20
|
width: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export const TextInput: FunctionComponent<TextInputProps> = ({ width,
|
|
23
|
+
export const TextInput: FunctionComponent<TextInputProps> = ({ width, ...innerProps }) => {
|
|
24
24
|
const size = { width, height: '3rem' };
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
27
|
<ErrorBoundary size={size}>
|
|
28
28
|
<ResizeContainer size={size}>
|
|
29
|
-
<TextInputInner
|
|
29
|
+
<TextInputInner {...innerProps} />
|
|
30
30
|
</ResizeContainer>
|
|
31
31
|
</ErrorBoundary>
|
|
32
32
|
);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { FetchAggregatedOperator } from '../operator/FetchAggregatedOperator';
|
|
2
|
+
import { FillMissingOperator } from '../operator/FillMissingOperator';
|
|
3
|
+
import { GroupByAndSumOperator } from '../operator/GroupByAndSumOperator';
|
|
4
|
+
import { MapOperator } from '../operator/MapOperator';
|
|
5
|
+
import { RenameFieldOperator } from '../operator/RenameFieldOperator';
|
|
6
|
+
import { SlidingOperator } from '../operator/SlidingOperator';
|
|
7
|
+
import { SortOperator } from '../operator/SortOperator';
|
|
8
|
+
import type { LapisFilter, TemporalGranularity } from '../types';
|
|
9
|
+
import {
|
|
10
|
+
compareTemporal,
|
|
11
|
+
generateAllInRange,
|
|
12
|
+
getMinMaxTemporal,
|
|
13
|
+
type Temporal,
|
|
14
|
+
TemporalCache,
|
|
15
|
+
} from '../utils/temporal';
|
|
16
|
+
|
|
17
|
+
export function queryAggregatedDataOverTime<LapisDateField extends string>(
|
|
18
|
+
filter: LapisFilter,
|
|
19
|
+
granularity: TemporalGranularity,
|
|
20
|
+
smoothingWindow: number,
|
|
21
|
+
lapisDateField: LapisDateField,
|
|
22
|
+
) {
|
|
23
|
+
const fetchData = new FetchAggregatedOperator<{ [key in LapisDateField]: string | null }>(filter, [lapisDateField]);
|
|
24
|
+
const dataWithFixedDateKey = new RenameFieldOperator(fetchData, lapisDateField, 'date');
|
|
25
|
+
const mapData = new MapOperator(dataWithFixedDateKey, (d) => mapDateToGranularityRange(d, granularity));
|
|
26
|
+
const groupByData = new GroupByAndSumOperator(mapData, 'dateRange', 'count');
|
|
27
|
+
const fillData = new FillMissingOperator(
|
|
28
|
+
groupByData,
|
|
29
|
+
'dateRange',
|
|
30
|
+
getMinMaxTemporal,
|
|
31
|
+
generateAllInRange,
|
|
32
|
+
(key) => ({ dateRange: key, count: 0 }),
|
|
33
|
+
);
|
|
34
|
+
const sortData = new SortOperator(fillData, dateRangeCompare);
|
|
35
|
+
|
|
36
|
+
return smoothingWindow >= 1 ? new SlidingOperator(sortData, smoothingWindow, averageSmoothing) : sortData;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mapDateToGranularityRange(d: { date: string | null; count: number }, granularity: TemporalGranularity) {
|
|
40
|
+
let dateRange: Temporal | null = null;
|
|
41
|
+
if (d.date !== null) {
|
|
42
|
+
const date = TemporalCache.getInstance().getYearMonthDay(d.date);
|
|
43
|
+
switch (granularity) {
|
|
44
|
+
case 'day':
|
|
45
|
+
dateRange = date;
|
|
46
|
+
break;
|
|
47
|
+
case 'week':
|
|
48
|
+
dateRange = date.week;
|
|
49
|
+
break;
|
|
50
|
+
case 'month':
|
|
51
|
+
dateRange = date.month;
|
|
52
|
+
break;
|
|
53
|
+
case 'year':
|
|
54
|
+
dateRange = date.year;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
dateRange,
|
|
60
|
+
count: d.count,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function dateRangeCompare(a: { dateRange: Temporal | null }, b: { dateRange: Temporal | null }) {
|
|
65
|
+
if (a.dateRange === null) {
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
if (b.dateRange === null) {
|
|
69
|
+
return -1;
|
|
70
|
+
}
|
|
71
|
+
return compareTemporal(a.dateRange, b.dateRange);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function averageSmoothing(slidingWindow: { dateRange: Temporal | null; count: number }[]) {
|
|
75
|
+
const average = slidingWindow.reduce((acc, curr) => acc + curr.count, 0) / slidingWindow.length;
|
|
76
|
+
const centerIndex = Math.floor(slidingWindow.length / 2);
|
|
77
|
+
return { dateRange: slidingWindow[centerIndex].dateRange, count: average };
|
|
78
|
+
}
|