@genspectrum/dashboard-components 0.5.4 → 0.5.5
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 +306 -0
- package/dist/dashboard-components.js +394 -263
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +80 -2
- 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-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 +105 -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 +232 -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,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,105 @@
|
|
|
1
|
+
import { useContext } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
import { NumberSequencesOverTimeBarChart } from './number-sequences-over-time-bar-chart';
|
|
4
|
+
import { NumberSequencesOverTimeTable } from './number-sequences-over-time-table';
|
|
5
|
+
import {
|
|
6
|
+
type NumberOfSequencesDatasets,
|
|
7
|
+
queryNumberOfSequencesOverTime,
|
|
8
|
+
} from '../../query/queryNumberOfSequencesOverTime';
|
|
9
|
+
import type { NamedLapisFilter, TemporalGranularity } from '../../types';
|
|
10
|
+
import { LapisUrlContext } from '../LapisUrlContext';
|
|
11
|
+
import { ErrorBoundary } from '../components/error-boundary';
|
|
12
|
+
import { ErrorDisplay } from '../components/error-display';
|
|
13
|
+
import Headline from '../components/headline';
|
|
14
|
+
import { LoadingDisplay } from '../components/loading-display';
|
|
15
|
+
import { NoDataDisplay } from '../components/no-data-display';
|
|
16
|
+
import { ResizeContainer } from '../components/resize-container';
|
|
17
|
+
import Tabs from '../components/tabs';
|
|
18
|
+
import { useQuery } from '../useQuery';
|
|
19
|
+
|
|
20
|
+
type NumberSequencesOverTimeView = 'bar' | 'line' | 'table';
|
|
21
|
+
|
|
22
|
+
export interface NumberSequencesOverTimeProps extends NumberSequencesOverTimeInnerProps {
|
|
23
|
+
width: string;
|
|
24
|
+
height: string;
|
|
25
|
+
headline: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface NumberSequencesOverTimeInnerProps {
|
|
29
|
+
lapisFilter: NamedLapisFilter | NamedLapisFilter[];
|
|
30
|
+
lapisDateField: string;
|
|
31
|
+
views: NumberSequencesOverTimeView[];
|
|
32
|
+
granularity: TemporalGranularity;
|
|
33
|
+
smoothingWindow: number;
|
|
34
|
+
pageSize: boolean | number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const NumberSequencesOverTime = ({ width, height, headline, ...innerProps }: NumberSequencesOverTimeProps) => {
|
|
38
|
+
const size = { height, width };
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<ErrorBoundary size={size} headline={headline}>
|
|
42
|
+
<ResizeContainer size={size}>
|
|
43
|
+
<Headline heading={headline}>
|
|
44
|
+
<NumberSequencesOverTimeInner {...innerProps} />
|
|
45
|
+
</Headline>
|
|
46
|
+
</ResizeContainer>
|
|
47
|
+
</ErrorBoundary>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const NumberSequencesOverTimeInner = ({
|
|
52
|
+
lapisFilter,
|
|
53
|
+
granularity,
|
|
54
|
+
smoothingWindow,
|
|
55
|
+
lapisDateField,
|
|
56
|
+
views,
|
|
57
|
+
pageSize,
|
|
58
|
+
}: NumberSequencesOverTimeInnerProps) => {
|
|
59
|
+
const lapis = useContext(LapisUrlContext);
|
|
60
|
+
|
|
61
|
+
const { data, error, isLoading } = useQuery(() =>
|
|
62
|
+
queryNumberOfSequencesOverTime(lapis, lapisFilter, lapisDateField, granularity, smoothingWindow),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (isLoading) {
|
|
66
|
+
return <LoadingDisplay />;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (error !== null) {
|
|
70
|
+
return <ErrorDisplay error={error} />;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (data === null) {
|
|
74
|
+
return <NoDataDisplay />;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return <NumberSequencesOverTimeTabs views={views} data={data} granularity={granularity} pageSize={pageSize} />;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
interface NumberSequencesOverTimeTabsProps {
|
|
81
|
+
views: NumberSequencesOverTimeView[];
|
|
82
|
+
data: NumberOfSequencesDatasets;
|
|
83
|
+
granularity: TemporalGranularity;
|
|
84
|
+
pageSize: boolean | number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const NumberSequencesOverTimeTabs = ({ views, data, granularity, pageSize }: NumberSequencesOverTimeTabsProps) => {
|
|
88
|
+
const getTab = (view: NumberSequencesOverTimeView) => {
|
|
89
|
+
switch (view) {
|
|
90
|
+
case 'bar':
|
|
91
|
+
return { title: 'Bar', content: <NumberSequencesOverTimeBarChart data={data} /> };
|
|
92
|
+
case 'line':
|
|
93
|
+
return { title: 'Line', content: <div>not implemented, TODO #317</div> };
|
|
94
|
+
case 'table':
|
|
95
|
+
return {
|
|
96
|
+
title: 'Table',
|
|
97
|
+
content: <NumberSequencesOverTimeTable data={data} granularity={granularity} pageSize={pageSize} />,
|
|
98
|
+
};
|
|
99
|
+
default:
|
|
100
|
+
throw new Error(`Unknown view: ${view}`);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return <Tabs tabs={views.map((view) => getTab(view))} />;
|
|
105
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { queryNumberOfSequencesOverTime } from './queryNumberOfSequencesOverTime';
|
|
4
|
+
import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../vitest.setup';
|
|
5
|
+
import { yearMonth, yearMonthDay } from '../utils/temporalTestHelpers';
|
|
6
|
+
|
|
7
|
+
const lapisDateField = 'dateField';
|
|
8
|
+
const lapisFilter = { field1: 'value1', field2: 'value2' };
|
|
9
|
+
|
|
10
|
+
describe('queryNumberOfSequencesOverTime', () => {
|
|
11
|
+
test('should fetch data for a single filter', async () => {
|
|
12
|
+
lapisRequestMocks.aggregated(
|
|
13
|
+
{ ...lapisFilter, fields: [lapisDateField] },
|
|
14
|
+
{
|
|
15
|
+
data: [
|
|
16
|
+
{ count: 1, [lapisDateField]: '2023-01-01' },
|
|
17
|
+
{ count: 2, [lapisDateField]: '2023-01-02' },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const result = await queryNumberOfSequencesOverTime(
|
|
23
|
+
DUMMY_LAPIS_URL,
|
|
24
|
+
{ displayName: 'displayName', lapisFilter },
|
|
25
|
+
lapisDateField,
|
|
26
|
+
'day',
|
|
27
|
+
0,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(result).to.deep.equal([
|
|
31
|
+
{
|
|
32
|
+
displayName: 'displayName',
|
|
33
|
+
content: [
|
|
34
|
+
{ count: 1, dateRange: yearMonthDay('2023-01-01') },
|
|
35
|
+
{ count: 2, dateRange: yearMonthDay('2023-01-02') },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('should fill missing dates with count 0', async () => {
|
|
42
|
+
lapisRequestMocks.aggregated(
|
|
43
|
+
{ ...lapisFilter, fields: [lapisDateField] },
|
|
44
|
+
{
|
|
45
|
+
data: [
|
|
46
|
+
{ count: 1, [lapisDateField]: '2023-01-01' },
|
|
47
|
+
{ count: 2, [lapisDateField]: '2023-01-04' },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const result = await queryNumberOfSequencesOverTime(
|
|
53
|
+
DUMMY_LAPIS_URL,
|
|
54
|
+
{ displayName: 'displayName', lapisFilter },
|
|
55
|
+
lapisDateField,
|
|
56
|
+
'day',
|
|
57
|
+
0,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(result).to.deep.equal([
|
|
61
|
+
{
|
|
62
|
+
displayName: 'displayName',
|
|
63
|
+
content: [
|
|
64
|
+
{ count: 1, dateRange: yearMonthDay('2023-01-01') },
|
|
65
|
+
{ count: 0, dateRange: yearMonthDay('2023-01-02') },
|
|
66
|
+
{ count: 0, dateRange: yearMonthDay('2023-01-03') },
|
|
67
|
+
{ count: 2, dateRange: yearMonthDay('2023-01-04') },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('should smooth the data', async () => {
|
|
74
|
+
lapisRequestMocks.aggregated(
|
|
75
|
+
{ ...lapisFilter, fields: [lapisDateField] },
|
|
76
|
+
{
|
|
77
|
+
data: [
|
|
78
|
+
{ count: 3, [lapisDateField]: '2023-01-01' },
|
|
79
|
+
{ count: 0, [lapisDateField]: '2023-01-02' },
|
|
80
|
+
{ count: 3, [lapisDateField]: '2023-01-03' },
|
|
81
|
+
{ count: 0, [lapisDateField]: '2023-01-04' },
|
|
82
|
+
{ count: 6, [lapisDateField]: '2023-01-05' },
|
|
83
|
+
{ count: 0, [lapisDateField]: '2023-01-06' },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const result = await queryNumberOfSequencesOverTime(
|
|
89
|
+
DUMMY_LAPIS_URL,
|
|
90
|
+
{ displayName: 'displayName', lapisFilter },
|
|
91
|
+
lapisDateField,
|
|
92
|
+
'day',
|
|
93
|
+
3,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(result).to.deep.equal([
|
|
97
|
+
{
|
|
98
|
+
displayName: 'displayName',
|
|
99
|
+
content: [
|
|
100
|
+
{ count: 2, dateRange: yearMonthDay('2023-01-02') },
|
|
101
|
+
{ count: 1, dateRange: yearMonthDay('2023-01-03') },
|
|
102
|
+
{ count: 3, dateRange: yearMonthDay('2023-01-04') },
|
|
103
|
+
{ count: 2, dateRange: yearMonthDay('2023-01-05') },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('should aggregate by month', async () => {
|
|
110
|
+
lapisRequestMocks.aggregated(
|
|
111
|
+
{ ...lapisFilter, fields: [lapisDateField] },
|
|
112
|
+
{
|
|
113
|
+
data: [
|
|
114
|
+
{ count: 1, [lapisDateField]: '2023-01-01' },
|
|
115
|
+
{ count: 2, [lapisDateField]: '2023-01-02' },
|
|
116
|
+
{ count: 3, [lapisDateField]: '2023-02-05' },
|
|
117
|
+
{ count: 4, [lapisDateField]: '2023-02-06' },
|
|
118
|
+
{ count: 5, [lapisDateField]: '2023-03-06' },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const result = await queryNumberOfSequencesOverTime(
|
|
124
|
+
DUMMY_LAPIS_URL,
|
|
125
|
+
{ displayName: 'displayName', lapisFilter },
|
|
126
|
+
lapisDateField,
|
|
127
|
+
'month',
|
|
128
|
+
0,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(result).to.deep.equal([
|
|
132
|
+
{
|
|
133
|
+
displayName: 'displayName',
|
|
134
|
+
content: [
|
|
135
|
+
{ count: 3, dateRange: yearMonth('2023-01') },
|
|
136
|
+
{ count: 7, dateRange: yearMonth('2023-02') },
|
|
137
|
+
{ count: 5, dateRange: yearMonth('2023-03') },
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('should fetch data for multiple filters', async () => {
|
|
144
|
+
const lapisFilter1 = { field1: 'value1', field2: 'value2' };
|
|
145
|
+
const lapisFilter2 = { field3: 'value3', field4: 'value4' };
|
|
146
|
+
lapisRequestMocks.multipleAggregated([
|
|
147
|
+
{
|
|
148
|
+
body: { ...lapisFilter1, fields: [lapisDateField] },
|
|
149
|
+
response: {
|
|
150
|
+
data: [
|
|
151
|
+
{ count: 1, [lapisDateField]: '2023-01-01' },
|
|
152
|
+
{ count: 2, [lapisDateField]: '2023-01-02' },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
body: { ...lapisFilter2, fields: [lapisDateField] },
|
|
158
|
+
response: {
|
|
159
|
+
data: [
|
|
160
|
+
{ count: 3, [lapisDateField]: '2023-01-02' },
|
|
161
|
+
{ count: 4, [lapisDateField]: '2023-01-03' },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const result = await queryNumberOfSequencesOverTime(
|
|
168
|
+
DUMMY_LAPIS_URL,
|
|
169
|
+
[
|
|
170
|
+
{ displayName: 'displayName1', lapisFilter: lapisFilter1 },
|
|
171
|
+
{ displayName: 'displayName2', lapisFilter: lapisFilter2 },
|
|
172
|
+
],
|
|
173
|
+
lapisDateField,
|
|
174
|
+
'day',
|
|
175
|
+
0,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(result).to.deep.equal([
|
|
179
|
+
{
|
|
180
|
+
displayName: 'displayName1',
|
|
181
|
+
content: [
|
|
182
|
+
{ count: 1, dateRange: yearMonthDay('2023-01-01') },
|
|
183
|
+
{ count: 2, dateRange: yearMonthDay('2023-01-02') },
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
displayName: 'displayName2',
|
|
188
|
+
content: [
|
|
189
|
+
{ count: 3, dateRange: yearMonthDay('2023-01-02') },
|
|
190
|
+
{ count: 4, dateRange: yearMonthDay('2023-01-03') },
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
]);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { queryAggregatedDataOverTime } from './queryAggregatedDataOverTime';
|
|
2
|
+
import { type NamedLapisFilter, type TemporalGranularity } from '../types';
|
|
3
|
+
import { sortNullToBeginningThenByDate } from '../utils/sort';
|
|
4
|
+
import { makeArray } from '../utils/utils';
|
|
5
|
+
|
|
6
|
+
export type NumberOfSequencesDatasets = Awaited<ReturnType<typeof queryNumberOfSequencesOverTime>>;
|
|
7
|
+
export type NumberOfSequencesDataset = NumberOfSequencesDatasets[number];
|
|
8
|
+
|
|
9
|
+
export async function queryNumberOfSequencesOverTime(
|
|
10
|
+
lapis: string,
|
|
11
|
+
lapisFilter: NamedLapisFilter | NamedLapisFilter[],
|
|
12
|
+
lapisDateField: string,
|
|
13
|
+
granularity: TemporalGranularity,
|
|
14
|
+
smoothingWindow: number,
|
|
15
|
+
) {
|
|
16
|
+
const lapisFilters = makeArray(lapisFilter);
|
|
17
|
+
|
|
18
|
+
const queries = lapisFilters.map(async ({ displayName, lapisFilter }) => {
|
|
19
|
+
const { content } = await queryAggregatedDataOverTime(
|
|
20
|
+
lapisFilter,
|
|
21
|
+
granularity,
|
|
22
|
+
smoothingWindow,
|
|
23
|
+
lapisDateField,
|
|
24
|
+
).evaluate(lapis);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
displayName,
|
|
28
|
+
content: content.sort(sortNullToBeginningThenByDate),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return Promise.all(queries);
|
|
33
|
+
}
|