Skip to content

Commit

Permalink
feat(website): show organism statistics on landing page
Browse files Browse the repository at this point in the history
  • Loading branch information
chaoran-chen committed May 20, 2024
1 parent 5cf57c5 commit d2e8465
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 20 deletions.
27 changes: 27 additions & 0 deletions website/src/components/IndexPage/OrganismCard.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
import type { OrganismStatistics } from './getOrganismStatistics';
import { routes } from '../../routes/routes';
interface Props {
key: string;
image: string | undefined;
displayName: string;
organismStatistics: OrganismStatistics;
numberDaysAgoStatistics: number;
}
const { key, image, displayName, organismStatistics, numberDaysAgoStatistics } = Astro.props;
---

<a
href={routes.organismStartPage(key)}
class='block rounded border border-gray-300 p-4 m-2 w-64 text-center hover:bg-gray-100'
>
{image !== undefined && <img src={image} class='h-32 mx-auto mb-4' alt={displayName} />}
<h3 class='font-semibold text-gray-700'>{displayName}</h3>
<p class='text-gray-700 text-sm'>
{organismStatistics.totalSequences} sequences<br />
(+{organismStatistics.recentSequences} in last {numberDaysAgoStatistics} days)<br />
{organismStatistics.lastUpdatedAt && <>Last updated {organismStatistics.lastUpdatedAt.toRelative()}</>}
</p>
</a>
71 changes: 71 additions & 0 deletions website/src/components/IndexPage/getOrganismStatistics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { DateTime, FixedOffsetZone } from 'luxon';

import { LapisClient } from '../../services/lapisClient.ts';
import { RELEASED_AT_FIELD } from '../../settings.ts';

export type OrganismStatistics = {
totalSequences: number;
recentSequences: number;
lastUpdatedAt: DateTime | undefined;
};
type OrganismStatisticsMap = Map<string, OrganismStatistics>;

export const getOrganismStatisticsMap = async (
organismNames: string[],
numberDaysAgo: number,
): Promise<OrganismStatisticsMap> => {
const statistics = await Promise.all(
organismNames.map((organism) => getOrganismStatistics(organism, numberDaysAgo)),
);
const result = new Map<string, OrganismStatistics>();
for (let i = 0; i < organismNames.length; i++) {
result.set(organismNames[i], statistics[i]);
}
return result;
};

const getOrganismStatistics = async (organism: string, numberDaysAgo: number): Promise<OrganismStatistics> => {
const [{ total, lastUpdatedAt }, recent] = await Promise.all([
getTotalAndLastUpdatedAt(organism),
getRecent(organism, numberDaysAgo),
]);
return {
totalSequences: total,
recentSequences: recent,
lastUpdatedAt,
};
};

const getTotalAndLastUpdatedAt = async (
organism: string,
): Promise<{ total: number; lastUpdatedAt: DateTime | undefined }> => {
const client = LapisClient.createForOrganism(organism);
return (
await client.call('aggregated', {
version: 1,
})
)
.map((x) => ({
total: x.data[0].count,
lastUpdatedAt: DateTime.fromSeconds(Number.parseInt(x.info.dataVersion, 10), {
zone: FixedOffsetZone.utcInstance,
}),
}))
.unwrapOr({
total: 0,
lastUpdatedAt: undefined,
});
};

const getRecent = async (organism: string, numberDaysAgo: number): Promise<number> => {
const recentTimestamp = Math.floor(Date.now() / 1000 - numberDaysAgo * 24 * 60 * 60);
const client = LapisClient.createForOrganism(organism);
return (
await client.call('aggregated', {
[`${RELEASED_AT_FIELD}From`]: recentTimestamp,
version: 1,
})
)
.map((x) => x.data[0].count)
.unwrapOr(0);
};
26 changes: 16 additions & 10 deletions website/src/components/SequenceDetailsPage/getTableData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,21 @@ const dummyError = {
},
};

const info = {
dataVersion: '1704063600',
};

const accessionVersion = 'accession';

const lapisClient = LapisClient.create(testConfig.serverSide.lapisUrls.dummy, schema);

describe('getTableData', () => {
beforeEach(() => {
mockRequest.lapis.details(200, { data: [toLapisEntry({ dummyField: 'dummyValue' })] });
mockRequest.lapis.nucleotideMutations(200, { data: [] });
mockRequest.lapis.aminoAcidMutations(200, { data: [] });
mockRequest.lapis.nucleotideInsertions(200, { data: [] });
mockRequest.lapis.aminoAcidInsertions(200, { data: [] });
mockRequest.lapis.details(200, { info, data: [toLapisEntry({ dummyField: 'dummyValue' })] });
mockRequest.lapis.nucleotideMutations(200, { info, data: [] });
mockRequest.lapis.aminoAcidMutations(200, { info, data: [] });
mockRequest.lapis.nucleotideInsertions(200, { info, data: [] });
mockRequest.lapis.aminoAcidInsertions(200, { info, data: [] });
});

test('should return an error when getSequenceDetails fails', async () => {
Expand Down Expand Up @@ -108,6 +112,7 @@ describe('getTableData', () => {
const value2 = 'value 2';

mockRequest.lapis.details(200, {
info,
data: [
{
metadataField1: value1,
Expand Down Expand Up @@ -136,8 +141,8 @@ describe('getTableData', () => {
});

test('should return data of mutations', async () => {
mockRequest.lapis.nucleotideMutations(200, { data: nucleotideMutations });
mockRequest.lapis.aminoAcidMutations(200, { data: aminoAcidMutations });
mockRequest.lapis.nucleotideMutations(200, { info, data: nucleotideMutations });
mockRequest.lapis.aminoAcidMutations(200, { info, data: aminoAcidMutations });

const result = await getTableData('accession', schema, lapisClient);

Expand Down Expand Up @@ -229,8 +234,8 @@ describe('getTableData', () => {
});

test('should return data of insertions', async () => {
mockRequest.lapis.nucleotideInsertions(200, { data: nucleotideInsertions });
mockRequest.lapis.aminoAcidInsertions(200, { data: aminoAcidInsertions });
mockRequest.lapis.nucleotideInsertions(200, { info, data: nucleotideInsertions });
mockRequest.lapis.aminoAcidInsertions(200, { info, data: aminoAcidInsertions });

const result = await getTableData('accession', schema, lapisClient);

Expand All @@ -252,7 +257,7 @@ describe('getTableData', () => {
});

test('should map timestamps to human readable dates', async () => {
mockRequest.lapis.details(200, { data: [{ timestampField: 1706194761 }] });
mockRequest.lapis.details(200, { info, data: [{ timestampField: 1706194761 }] });

const result = await getTableData('accession', schema, lapisClient);

Expand All @@ -269,6 +274,7 @@ describe('getTableData', () => {
test('should correctly determine revocation entry', async () => {
for (const expectedIsRevocation of [true, false]) {
mockRequest.lapis.details(200, {
info,
data: [toLapisEntry({}, expectedIsRevocation)],
});
const result = await getTableData('accession', schema, lapisClient);
Expand Down
26 changes: 16 additions & 10 deletions website/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
---
import Faq from '../components/IndexPage/FAQ.mdx';
import OrganismCard from '../components/IndexPage/OrganismCard.astro';
import WelcomeMessage from '../components/IndexPage/WelcomeMessage.astro';
import { getOrganismStatisticsMap } from '../components/IndexPage/getOrganismStatistics';
import { getConfiguredOrganisms, getWebsiteConfig } from '../config';
import BaseLayout from '../layouts/BaseLayout.astro';
import { routes } from '../routes/routes';
const websiteConfig = getWebsiteConfig();
const { name: websiteName } = websiteConfig;
import '../styles/mdcontainer.scss';
const numberDaysAgoStatistics = 30;
const organismStatisticsMap = await getOrganismStatisticsMap(
getConfiguredOrganisms().map((organism) => organism.key),
numberDaysAgoStatistics,
);
---

<BaseLayout title='Home'>
Expand All @@ -17,15 +24,14 @@ import '../styles/mdcontainer.scss';

<div class='flex flex-wrap'>
{
getConfiguredOrganisms().map(({ key, displayName, image, description }) => (
<a
href={routes.organismStartPage(key)}
class='block rounded border border-gray-300 p-4 m-2 w-64 text-center hover:bg-gray-100'
>
{image !== undefined && <img src={image} class='h-32 mx-auto mb-4' alt={displayName} />}
<h3 class='font-semibold text-gray-700'>{displayName}</h3>
<p class='text-gray-700 text-sm'>{description}</p>
</a>
getConfiguredOrganisms().map(({ key, displayName, image }) => (
<OrganismCard
key={key}
image={image}
displayName={displayName}
organismStatistics={organismStatisticsMap.get(key)!}
numberDaysAgoStatistics={numberDaysAgoStatistics}
/>
))
}
</div>
Expand Down
3 changes: 3 additions & 0 deletions website/src/types/lapis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export const aggregatedResponse = makeLapisResponse(z.array(aggregatedItem));
function makeLapisResponse<T extends ZodTypeAny>(data: T) {
return z.object({
data,
info: z.object({
dataVersion: z.string(),
}),
});
}

Expand Down

0 comments on commit d2e8465

Please sign in to comment.