Skip to content

Commit

Permalink
feat(components): number of sequences over time: compute data
Browse files Browse the repository at this point in the history
closes #327
  • Loading branch information
fengelniederhammer committed Jul 12, 2024
1 parent cc3fb57 commit 814b9c9
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,23 +1,54 @@
import { type StoryObj } from '@storybook/preact';

import { NumberSequencesOverTime, type NumberSequencesOverTimeProps } from './number-sequences-over-time';
import { LAPIS_URL } from '../../constants';
import { LapisUrlContext } from '../LapisUrlContext';

export default {
title: 'Visualization/NumberSequencesOverTime',
component: NumberSequencesOverTime,
parameters: {
fetchMock: {},
},
argTypes: {
granularity: {
options: ['day', 'week', 'month', 'year'],
control: { type: 'radio' },
},
views: {
options: ['bar', 'line', 'table'],
control: { type: 'check' },
},
},
};

const Template = {
render: (args: NumberSequencesOverTimeProps) => (
<NumberSequencesOverTime
lapisFilter={args.lapisFilter}
views={args.views}
width={args.width}
height={args.height}
headline={args.headline}
/>
const Template: StoryObj<NumberSequencesOverTimeProps> = {
render: (args) => (
<LapisUrlContext.Provider value={LAPIS_URL}>
<NumberSequencesOverTime
lapisFilter={args.lapisFilter}
lapisDateField={args.lapisDateField}
views={args.views}
width={args.width}
height={args.height}
headline={args.headline}
granularity={args.granularity}
smoothingWindow={args.smoothingWindow}
/>
</LapisUrlContext.Provider>
),
args: {
views: ['bar', 'line', 'table'],
lapisFilter: [
{ displayName: 'EG', lapisFilter: { country: 'USA', pangoLineage: 'EG*', dateFrom: '2023-01-01' } },
],
lapisDateField: 'date',
width: '100%',
height: '700px',
headline: 'Prevalence over time',
smoothingWindow: 0,
granularity: 'month',
},
};

export const Table = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { type NamedLapisFilter } from '../../types';
import { useContext } from 'preact/hooks';

import { queryNumberOfSequencesOverTime } from '../../query/queryNumberOfSequencesOverTime';
import type { NamedLapisFilter, TemporalGranularity } from '../../types';
import { LapisUrlContext } from '../LapisUrlContext';
import { ErrorBoundary } from '../components/error-boundary';
import { ErrorDisplay } from '../components/error-display';
import Headline from '../components/headline';
import { LoadingDisplay } from '../components/loading-display';
import { NoDataDisplay } from '../components/no-data-display';
import { ResizeContainer } from '../components/resize-container';
import { useQuery } from '../useQuery';

export interface NumberSequencesOverTimeProps extends NumberSequencesOverTimeInnerProps {
width: string;
Expand All @@ -11,7 +19,10 @@ export interface NumberSequencesOverTimeProps extends NumberSequencesOverTimeInn

interface NumberSequencesOverTimeInnerProps {
lapisFilter: NamedLapisFilter | NamedLapisFilter[];
lapisDateField: string;
views: ('bar' | 'line' | 'table')[];
granularity: TemporalGranularity;
smoothingWindow: number;
}

export function NumberSequencesOverTime({ width, height, headline, ...innerProps }: NumberSequencesOverTimeProps) {
Expand All @@ -28,6 +39,29 @@ export function NumberSequencesOverTime({ width, height, headline, ...innerProps
);
}

const NumberSequencesOverTimeInner = ({}: NumberSequencesOverTimeInnerProps) => {
return <div>Inner</div>;
const NumberSequencesOverTimeInner = ({
lapisFilter,
granularity,
smoothingWindow,
lapisDateField,
}: NumberSequencesOverTimeInnerProps) => {
const lapis = useContext(LapisUrlContext);

const { data, error, isLoading } = useQuery(() =>
queryNumberOfSequencesOverTime(lapis, lapisFilter, lapisDateField, granularity, smoothingWindow),
);

if (isLoading) {
return <LoadingDisplay />;
}

if (error !== null) {
return <ErrorDisplay error={error} />;
}

if (data === null) {
return <NoDataDisplay />;
}

return <div>{JSON.stringify(data)}</div>;
};
203 changes: 203 additions & 0 deletions components/src/query/queryNumberOfSequencesOverTime.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { describe, expect, test } from 'vitest';

import { queryNumberOfSequencesOverTime } from './queryNumberOfSequencesOverTime';
import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../vitest.setup';
import { TemporalCache, YearMonth, YearMonthDay } from '../utils/temporal';

const lapisDateField = 'dateField';
const lapisFilter = { field1: 'value1', field2: 'value2' };

describe('queryNumberOfSequencesOverTime', () => {
test('should fetch data for a single filter', async () => {
lapisRequestMocks.aggregated(
{ ...lapisFilter, fields: [lapisDateField] },
{
data: [
{ count: 1, [lapisDateField]: '2023-01-01' },
{ count: 2, [lapisDateField]: '2023-01-02' },
],
},
);

const result = await queryNumberOfSequencesOverTime(
DUMMY_LAPIS_URL,
{ displayName: 'displayName', lapisFilter },
lapisDateField,
'day',
0,
);

expect(result).to.deep.equal([
{
displayName: 'displayName',
content: [
{ count: 1, dateRange: yearMonthDay('2023-01-01') },
{ count: 2, dateRange: yearMonthDay('2023-01-02') },
],
},
]);
});

test('should fill missing dates with count 0', async () => {
lapisRequestMocks.aggregated(
{ ...lapisFilter, fields: [lapisDateField] },
{
data: [
{ count: 1, [lapisDateField]: '2023-01-01' },
{ count: 2, [lapisDateField]: '2023-01-04' },
],
},
);

const result = await queryNumberOfSequencesOverTime(
DUMMY_LAPIS_URL,
{ displayName: 'displayName', lapisFilter },
lapisDateField,
'day',
0,
);

expect(result).to.deep.equal([
{
displayName: 'displayName',
content: [
{ count: 1, dateRange: yearMonthDay('2023-01-01') },
{ count: 0, dateRange: yearMonthDay('2023-01-02') },
{ count: 0, dateRange: yearMonthDay('2023-01-03') },
{ count: 2, dateRange: yearMonthDay('2023-01-04') },
],
},
]);
});

test('should smooth the data', async () => {
lapisRequestMocks.aggregated(
{ ...lapisFilter, fields: [lapisDateField] },
{
data: [
{ count: 3, [lapisDateField]: '2023-01-01' },
{ count: 0, [lapisDateField]: '2023-01-02' },
{ count: 3, [lapisDateField]: '2023-01-03' },
{ count: 0, [lapisDateField]: '2023-01-04' },
{ count: 6, [lapisDateField]: '2023-01-05' },
{ count: 0, [lapisDateField]: '2023-01-06' },
],
},
);

const result = await queryNumberOfSequencesOverTime(
DUMMY_LAPIS_URL,
{ displayName: 'displayName', lapisFilter },
lapisDateField,
'day',
3,
);

expect(result).to.deep.equal([
{
displayName: 'displayName',
content: [
{ count: 2, dateRange: yearMonthDay('2023-01-02') },
{ count: 1, dateRange: yearMonthDay('2023-01-03') },
{ count: 3, dateRange: yearMonthDay('2023-01-04') },
{ count: 2, dateRange: yearMonthDay('2023-01-05') },
],
},
]);
});

test('should aggregate by month', async () => {
lapisRequestMocks.aggregated(
{ ...lapisFilter, fields: [lapisDateField] },
{
data: [
{ count: 1, [lapisDateField]: '2023-01-01' },
{ count: 2, [lapisDateField]: '2023-01-02' },
{ count: 3, [lapisDateField]: '2023-02-05' },
{ count: 4, [lapisDateField]: '2023-02-06' },
{ count: 5, [lapisDateField]: '2023-03-06' },
],
},
);

const result = await queryNumberOfSequencesOverTime(
DUMMY_LAPIS_URL,
{ displayName: 'displayName', lapisFilter },
lapisDateField,
'month',
0,
);

expect(result).to.deep.equal([
{
displayName: 'displayName',
content: [
{ count: 3, dateRange: yearMonth('2023-01') },
{ count: 7, dateRange: yearMonth('2023-02') },
{ count: 5, dateRange: yearMonth('2023-03') },
],
},
]);
});

test('should fetch data for multiple filters', async () => {
const lapisFilter1 = { field1: 'value1', field2: 'value2' };
const lapisFilter2 = { field3: 'value3', field4: 'value4' };
lapisRequestMocks.multipleAggregated([
{
body: { ...lapisFilter1, fields: [lapisDateField] },
response: {
data: [
{ count: 1, [lapisDateField]: '2023-01-01' },
{ count: 2, [lapisDateField]: '2023-01-02' },
],
},
},
{
body: { ...lapisFilter2, fields: [lapisDateField] },
response: {
data: [
{ count: 3, [lapisDateField]: '2023-01-02' },
{ count: 4, [lapisDateField]: '2023-01-03' },
],
},
},
]);

const result = await queryNumberOfSequencesOverTime(
DUMMY_LAPIS_URL,
[
{ displayName: 'displayName1', lapisFilter: lapisFilter1 },
{ displayName: 'displayName2', lapisFilter: lapisFilter2 },
],
lapisDateField,
'day',
0,
);

expect(result).to.deep.equal([
{
displayName: 'displayName1',
content: [
{ count: 1, dateRange: yearMonthDay('2023-01-01') },
{ count: 2, dateRange: yearMonthDay('2023-01-02') },
],
},
{
displayName: 'displayName2',
content: [
{ count: 3, dateRange: yearMonthDay('2023-01-02') },
{ count: 4, dateRange: yearMonthDay('2023-01-03') },
],
},
]);
});
});

function yearMonthDay(date: string) {
return YearMonthDay.parse(date, TemporalCache.getInstance());
}

function yearMonth(date: string) {
return YearMonth.parse(date, TemporalCache.getInstance());
}
32 changes: 32 additions & 0 deletions components/src/query/queryNumberOfSequencesOverTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { queryAggregatedDataOverTime } from './queryAggregatedDataOverTime';
import { type NamedLapisFilter, type TemporalGranularity } from '../types';
import { sortNullToBeginningThenByDate } from '../utils/sort';
import { makeArray } from '../utils/utils';

export type NumberOfSequencesDatasets = Awaited<ReturnType<typeof queryNumberOfSequencesOverTime>>;

export async function queryNumberOfSequencesOverTime(
lapis: string,
lapisFilter: NamedLapisFilter | NamedLapisFilter[],
lapisDateField: string,
granularity: TemporalGranularity,
smoothingWindow: number,
) {
const lapisFilters = makeArray(lapisFilter);

const queries = lapisFilters.map(async ({ displayName, lapisFilter }) => {
const { content } = await queryAggregatedDataOverTime(
lapisFilter,
granularity,
smoothingWindow,
lapisDateField,
).evaluate(lapis);

return {
displayName,
content: content.sort(sortNullToBeginningThenByDate),
};
});

return Promise.all(queries);
}
Loading

0 comments on commit 814b9c9

Please sign in to comment.