@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.
Files changed (34) hide show
  1. package/custom-elements.json +306 -0
  2. package/dist/dashboard-components.js +394 -263
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +80 -2
  5. package/package.json +1 -1
  6. package/src/preact/aggregatedData/aggregate.tsx +2 -14
  7. package/src/preact/dateRangeSelector/date-range-selector.tsx +2 -14
  8. package/src/preact/locationFilter/location-filter.tsx +2 -7
  9. package/src/preact/mutationComparison/mutation-comparison.tsx +2 -10
  10. package/src/preact/mutations/mutations.tsx +2 -10
  11. package/src/preact/numberSequencesOverTime/__mockData__/oneVariantEG.json +1702 -0
  12. package/src/preact/numberSequencesOverTime/__mockData__/twoVariantsEG.json +1710 -0
  13. package/src/preact/numberSequencesOverTime/__mockData__/twoVariantsJN1.json +1070 -0
  14. package/src/preact/numberSequencesOverTime/__mockData__/twoVariantsXBB.json +506 -0
  15. package/src/preact/numberSequencesOverTime/getNumberOfSequencesOverTimeTableData.spec.ts +75 -0
  16. package/src/preact/numberSequencesOverTime/getNumberOfSequencesOverTimeTableData.ts +39 -0
  17. package/src/preact/numberSequencesOverTime/number-sequences-over-time-bar-chart.tsx +58 -0
  18. package/src/preact/numberSequencesOverTime/number-sequences-over-time-table.tsx +32 -0
  19. package/src/preact/numberSequencesOverTime/number-sequences-over-time.stories.tsx +133 -0
  20. package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +105 -0
  21. package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +11 -20
  22. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +2 -20
  23. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +2 -14
  24. package/src/preact/textInput/text-input.tsx +2 -2
  25. package/src/query/queryAggregatedDataOverTime.ts +78 -0
  26. package/src/query/queryNumberOfSequencesOverTime.spec.ts +195 -0
  27. package/src/query/queryNumberOfSequencesOverTime.ts +33 -0
  28. package/src/query/queryPrevalenceOverTime.ts +10 -86
  29. package/src/utils/sort.ts +9 -0
  30. package/src/utils/temporalTestHelpers.ts +9 -0
  31. package/src/utils/utils.ts +7 -0
  32. package/src/web-components/visualization/gs-number-sequences-over-time.stories.ts +232 -0
  33. package/src/web-components/visualization/gs-number-sequences-over-time.tsx +140 -0
  34. 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 type { Temporal } from '../../utils/temporal';
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
- lapisDateField,
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
- lapisDateField,
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, lapisField, placeholderText, initialValue }) => {
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 lapisField={lapisField} placeholderText={placeholderText} initialValue={initialValue} />
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
+ }