From 5f31f8215d1ffccc2eb962d3124def1cad67c25c Mon Sep 17 00:00:00 2001 From: tombch Date: Thu, 19 Mar 2026 18:28:40 +0100 Subject: [PATCH 01/24] Reusable barplot --- website/src/components/common/BarPlot.tsx | 66 +++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 website/src/components/common/BarPlot.tsx diff --git a/website/src/components/common/BarPlot.tsx b/website/src/components/common/BarPlot.tsx new file mode 100644 index 0000000000..84efb975c7 --- /dev/null +++ b/website/src/components/common/BarPlot.tsx @@ -0,0 +1,66 @@ +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, + type ChartData, + type ChartOptions, +} from 'chart.js'; +import { type FC, useState, useEffect } from 'react'; +import { Bar } from 'react-chartjs-2'; + +type BarPlotProps = { + data: ChartData<'bar'>; + options?: ChartOptions<'bar'>; + description?: string; +}; + +export const BarPlot: FC = ({ data, options, description }) => { + const [isRegistered, setIsRegistered] = useState(false); + + useEffect(() => { + ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); + setIsRegistered(true); + }, []); + + if (!isRegistered) { + return null; + } + + return ( +
+ + {description && ( +

+ {description} +

+ )} +
+ ); +}; From 31f2dc8ee30306cb77565abfb36975e590800c71 Mon Sep 17 00:00:00 2001 From: tombch Date: Thu, 19 Mar 2026 18:29:09 +0100 Subject: [PATCH 02/24] Using BarPlot component for CitationPlot --- .../SeqSetCitations/CitationPlot.tsx | 42 ++++--------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/website/src/components/SeqSetCitations/CitationPlot.tsx b/website/src/components/SeqSetCitations/CitationPlot.tsx index dbfa6f2cd7..410a061279 100644 --- a/website/src/components/SeqSetCitations/CitationPlot.tsx +++ b/website/src/components/SeqSetCitations/CitationPlot.tsx @@ -1,25 +1,15 @@ -import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; -import { type FC, useState, useEffect } from 'react'; -import { Bar } from 'react-chartjs-2'; +import React from 'react'; import type { CitedByResult } from '../../types/seqSetCitation'; +import { BarPlot } from '../common/BarPlot'; type CitationPlotProps = { citedByData: CitedByResult; + responsive?: boolean; + description?: string; }; -export const CitationPlot: FC = ({ citedByData }) => { - const [isRegistered, setIsRegistered] = useState(false); - - useEffect(() => { - ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); - setIsRegistered(true); - }, []); - - if (!isRegistered) { - return null; - } - +export const CitationPlot: React.FC = ({ citedByData, responsive, description }) => { const emptyCitedByData = { years: [2020, 2021, 2022, 2023, 2024], citations: [0, 0, 0, 0, 0], @@ -28,7 +18,7 @@ export const CitationPlot: FC = ({ citedByData }) => { const renderData = citedByData.years.length > 0 ? citedByData : emptyCitedByData; return ( - = ({ citedByData }) => { ], }} options={{ - maintainAspectRatio: false, - responsive: false, - plugins: { - title: { - display: true, - }, - legend: { - display: false, - }, - }, - scales: { - y: { - suggestedMax: 10, - grid: { - color: 'rgba(0, 0, 0, 0)', - }, - }, - }, + responsive: responsive ?? true, }} + description={description} /> ); }; From ee8a028a8e72f234e7a2813be0cdf7255f6886b1 Mon Sep 17 00:00:00 2001 From: tombch Date: Thu, 19 Mar 2026 18:30:25 +0100 Subject: [PATCH 03/24] Updated seqset index page citations plot --- website/src/pages/seqsets/index.astro | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/website/src/pages/seqsets/index.astro b/website/src/pages/seqsets/index.astro index 36d8ee4c21..c969399d6b 100644 --- a/website/src/pages/seqsets/index.astro +++ b/website/src/pages/seqsets/index.astro @@ -96,15 +96,12 @@ const editAccountUrl = (await getUrlForKeycloakAccountPage()) + '/#/personal-inf
Cited by {/* We show an empty plot for now until we get real data. */} -
- -

- Number of times your sequences have been cited in publications -

-
+
) From f9dd6456a58c92210cdf8479c0beef4dffc4149e Mon Sep 17 00:00:00 2001 From: tombch Date: Thu, 19 Mar 2026 18:32:19 +0100 Subject: [PATCH 04/24] Added seqset id to tab title, moved author component into SeqSetItem --- .../pages/seqsets/[seqSetId].[version].astro | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/website/src/pages/seqsets/[seqSetId].[version].astro b/website/src/pages/seqsets/[seqSetId].[version].astro index 99064fca7f..e45fb529b1 100644 --- a/website/src/pages/seqsets/[seqSetId].[version].astro +++ b/website/src/pages/seqsets/[seqSetId].[version].astro @@ -1,6 +1,5 @@ --- import { ErrorFeedback } from '../../components/ErrorFeedback'; -import { AuthorDetails } from '../../components/SeqSetCitations/AuthorDetails'; import { SeqSetItem } from '../../components/SeqSetCitations/SeqSetItem'; import { SeqSetItemActions } from '../../components/SeqSetCitations/SeqSetItemActions'; import { getRuntimeConfig, seqSetsAreEnabled, getWebsiteConfig } from '../../config'; @@ -51,45 +50,48 @@ const getSeqSetByVersion = (seqSetVersions: SeqSet[], version: string) => { }; const seqSet = seqSetResponse.isOk() ? getSeqSetByVersion(seqSetResponse.value, version) : undefined; +const seqSetAccessionVersion = `${seqSetId}.${version}`; const authorResponse = seqSet !== undefined ? await seqSetClient.getAuthor(seqSet.createdBy) : undefined; + +const assignAuthorName = (seqSet: SeqSet) => { + return { + ...seqSet, + createdBy: authorResponse?.isOk() + ? [authorResponse.value?.firstName, authorResponse.value?.lastName] + .filter((name) => name !== null) + .join(' ') + : seqSet.createdBy, + }; +}; --- - + + { + !authorResponse?.isOk() && ( + + ) + }
{ seqSet !== undefined ? ( -
-
- {authorResponse?.isOk() ? ( - - ) : ( - - )} -
-
+
+
{seqSetRecordsResponse.isOk() ? ( name !== null) - .join(' ') - : seqSet.createdBy, - }} + seqSet={assignAuthorName(seqSet)} seqSetRecords={seqSetRecordsResponse.value} isAdminView={seqSet.createdBy === username} databaseName={websiteConfig.name} @@ -108,7 +110,8 @@ const authorResponse = seqSet !== undefined ? await seqSetClient.getAuthor(seqSe Date: Thu, 19 Mar 2026 18:33:44 +0100 Subject: [PATCH 05/24] Initial updates to SeqSet details page - added empty graphs, moved details and citations into separate sections, moved action buttons --- .../components/SeqSetCitations/SeqSetItem.tsx | 162 +++++++++++------- .../SeqSetCitations/SeqSetItemActions.tsx | 20 ++- 2 files changed, 113 insertions(+), 69 deletions(-) diff --git a/website/src/components/SeqSetCitations/SeqSetItem.tsx b/website/src/components/SeqSetCitations/SeqSetItem.tsx index 9f6cbefac8..270cd6a1d8 100644 --- a/website/src/components/SeqSetCitations/SeqSetItem.tsx +++ b/website/src/components/SeqSetCitations/SeqSetItem.tsx @@ -3,6 +3,7 @@ import { AxiosError } from 'axios'; import { type FC, useState } from 'react'; import { toast } from 'react-toastify'; +import { AuthorDetails } from './AuthorDetails.tsx'; import { CitationPlot } from './CitationPlot'; import { SeqSetRecordsTableWithMetadata } from './SeqSetRecordsTableWithMetadata'; import { getClientLogger } from '../../clientLogger'; @@ -12,13 +13,32 @@ import type { ClientConfig } from '../../types/runtimeConfig'; import { type CitedByResult, type SeqSet, type SeqSetRecord } from '../../types/seqSetCitation'; import { createAuthorizationHeader } from '../../utils/createAuthorizationHeader'; import { displayConfirmationDialog } from '../ConfirmationDialog.tsx'; +import { BarPlot } from '../common/BarPlot.tsx'; import { withQueryProvider } from '../common/withQueryProvider.tsx'; const logger = getClientLogger('SeqSetItem'); +const SeqSetSectionTitle: FC<{ title: string }> = ({ title }) => ( +

{title}

+); + +const SeqSetDetailsTitle: FC<{ title: string }> = ({ title }) => ( +
+

{title}

+
+); + +const SeqSetDetailsEntry: FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => ( +
+

{label}

+

{value}

+
+); + type SeqSetItemProps = { clientConfig: ClientConfig; accessToken: string; + seqSetAccessionVersion: string; seqSet: SeqSet; seqSetRecords: SeqSetRecord[]; citedByData: CitedByResult; @@ -30,6 +50,7 @@ type SeqSetItemProps = { const SeqSetItemInner: FC = ({ clientConfig, accessToken, + seqSetAccessionVersion, seqSet, seqSetRecords, citedByData, @@ -97,74 +118,89 @@ const SeqSetItemInner: FC = ({ }; return ( -
-
-
-

Description

-

{seqSet.description ?? 'N/A'}

-
-
-

Version

-

{seqSet.seqSetVersion}

-
-
-

Created date

-

{formatDate(seqSet.createdAt)}

-
-
-

Size

-

{`${seqSetRecords.length} sequence${seqSetRecords.length === 1 ? '' : 's'}`}

-
-
-

DOI

- {renderDOI()} -
-
-

Total citations

- {seqSet.seqSetDOI === undefined || seqSet.seqSetDOI === null ? ( -

Cited by 0

- ) : ( - - Cited by 0 - - )} +
+
+
+ + + + + } + /> + +
-
-

-
- -

- Number of times this SeqSet has been cited by a publication -

-
+
+ + + Cited by 0

+ ) : ( + + Cited by 0 + + ) + } + /> + + } + />
-
-

Sequences

- +
+ + + - {getMaxPages() > 1 ? ( - { - setPage(newPage); - }} - /> - ) : null}
+ + + {getMaxPages() > 1 ? ( + { + setPage(newPage); + }} + /> + ) : null}
); }; diff --git a/website/src/components/SeqSetCitations/SeqSetItemActions.tsx b/website/src/components/SeqSetCitations/SeqSetItemActions.tsx index 6e82f02a48..9d8692afdb 100644 --- a/website/src/components/SeqSetCitations/SeqSetItemActions.tsx +++ b/website/src/components/SeqSetCitations/SeqSetItemActions.tsx @@ -8,6 +8,7 @@ import { seqSetCitationClientHooks } from '../../services/serviceHooks'; import type { ClientConfig } from '../../types/runtimeConfig'; import type { SeqSetRecord, SeqSet } from '../../types/seqSetCitation'; import { createAuthorizationHeader } from '../../utils/createAuthorizationHeader'; +import { getAccessionVersionString } from '../../utils/extractAccessionVersion.ts'; import { displayConfirmationDialog } from '../ConfirmationDialog.tsx'; import { Button } from '../common/Button'; import Modal from '../common/Modal'; @@ -35,6 +36,11 @@ const SeqSetItemActionsInner: FC = ({ isAdminView = false, databaseName, }) => { + const seqSetAccessionVersion = getAccessionVersionString({ + accession: seqSet.seqSetId, + version: seqSet.seqSetVersion, + }); + const [editModalVisible, setEditModalVisible] = useState(false); const [exportModalVisible, setExportModalVisible] = useState(false); @@ -51,16 +57,18 @@ const SeqSetItemActionsInner: FC = ({ }; return ( -
-

{seqSet.name}

-
+
+
+

{seqSetAccessionVersion}

+
+
{isAdminView ? ( ) : null} {isAdminView && (seqSet.seqSetDOI === null || seqSet.seqSetDOI === undefined) ? ( @@ -82,7 +90,7 @@ const SeqSetItemActionsInner: FC = ({ } > - Delete + Delete ) : null}
From 88798ad4f50b19ea5a06c70348e667974c7ac455 Mon Sep 17 00:00:00 2001 From: tombch Date: Fri, 20 Mar 2026 12:06:18 +0100 Subject: [PATCH 06/24] Added graphs for collection date, countries and use terms --- .../components/SeqSetCitations/SeqSetItem.tsx | 127 +++++++++++------- .../SeqSetCitations/getSeqSetStatistics.ts | 62 +++++++++ .../pages/seqsets/[seqSetId].[version].astro | 51 +++---- 3 files changed, 166 insertions(+), 74 deletions(-) create mode 100644 website/src/components/SeqSetCitations/getSeqSetStatistics.ts diff --git a/website/src/components/SeqSetCitations/SeqSetItem.tsx b/website/src/components/SeqSetCitations/SeqSetItem.tsx index 270cd6a1d8..baf991cdd7 100644 --- a/website/src/components/SeqSetCitations/SeqSetItem.tsx +++ b/website/src/components/SeqSetCitations/SeqSetItem.tsx @@ -6,11 +6,12 @@ import { toast } from 'react-toastify'; import { AuthorDetails } from './AuthorDetails.tsx'; import { CitationPlot } from './CitationPlot'; import { SeqSetRecordsTableWithMetadata } from './SeqSetRecordsTableWithMetadata'; +import type { AggregateRow } from './getSeqSetStatistics.ts'; import { getClientLogger } from '../../clientLogger'; import { seqSetCitationClientHooks } from '../../services/serviceHooks'; import type { ProblemDetail } from '../../types/backend.ts'; import type { ClientConfig } from '../../types/runtimeConfig'; -import { type CitedByResult, type SeqSet, type SeqSetRecord } from '../../types/seqSetCitation'; +import { type AuthorProfile, type CitedByResult, type SeqSet, type SeqSetRecord } from '../../types/seqSetCitation'; import { createAuthorizationHeader } from '../../utils/createAuthorizationHeader'; import { displayConfirmationDialog } from '../ConfirmationDialog.tsx'; import { BarPlot } from '../common/BarPlot.tsx'; @@ -18,20 +19,26 @@ import { withQueryProvider } from '../common/withQueryProvider.tsx'; const logger = getClientLogger('SeqSetItem'); -const SeqSetSectionTitle: FC<{ title: string }> = ({ title }) => ( -

{title}

+const SeqSetSection: FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => ( +
+

{title}

+ {children} +
); -const SeqSetDetailsTitle: FC<{ title: string }> = ({ title }) => ( -
-

{title}

+const SeqSetDetails: FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => ( +
+
+

{title}

+
+ {children}
); const SeqSetDetailsEntry: FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => (
-

{label}

-

{value}

+
{label}
+
{value}
); @@ -40,8 +47,12 @@ type SeqSetItemProps = { accessToken: string; seqSetAccessionVersion: string; seqSet: SeqSet; + seqSetAuthor?: AuthorProfile; seqSetRecords: SeqSetRecord[]; citedByData: CitedByResult; + collectionDatesData: AggregateRow[]; + collectionCountriesData: AggregateRow[]; + dataUseTermsData: AggregateRow[]; isAdminView?: boolean; fieldsToDisplay?: { field: string; displayName: string }[]; organismDisplayNames?: Record; @@ -52,8 +63,12 @@ const SeqSetItemInner: FC = ({ accessToken, seqSetAccessionVersion, seqSet, + seqSetAuthor, seqSetRecords, citedByData, + collectionDatesData, + collectionCountriesData, + dataUseTermsData, isAdminView = false, fieldsToDisplay, organismDisplayNames, @@ -117,26 +132,36 @@ const SeqSetItemInner: FC = ({ return seqSetRecords.slice((page - 1) * sequencesPerPage, page * sequencesPerPage); }; + const graphColour = '#88a1d2'; + const getGraphData = (data: AggregateRow[]) => ({ + labels: data.map((item) => item.value ?? 'Unknown'), + datasets: [{ data: data.map((item) => item.count), backgroundColor: graphColour }], + }); + return (
-
-
- +
+ } + value={ + + } /> -
-
- + + = ({ /> } /> -
+
- -
- - - -
- - - {getMaxPages() > 1 ? ( - { - setPage(newPage); - }} + +
+ + + +
+
+ + - ) : null} + {getMaxPages() > 1 ? ( + { + setPage(newPage); + }} + /> + ) : null} +
); }; diff --git a/website/src/components/SeqSetCitations/getSeqSetStatistics.ts b/website/src/components/SeqSetCitations/getSeqSetStatistics.ts new file mode 100644 index 0000000000..4a17ea3e15 --- /dev/null +++ b/website/src/components/SeqSetCitations/getSeqSetStatistics.ts @@ -0,0 +1,62 @@ +import { ok } from 'neverthrow'; +import type { Result } from 'neverthrow'; + +import { getConfiguredOrganisms, getSchema } from '../../config.ts'; +import { LapisClient } from '../../services/lapisClient.ts'; +import { ACCESSION_VERSION_FIELD } from '../../settings.ts'; +import type { ProblemDetail } from '../../types/backend.ts'; +import { type Schema } from '../../types/config.ts'; + +type AggregateValue = string | number | boolean | null; +export type AggregateRow = { value: AggregateValue; count: number }; + +const getAggregate = async ( + client: LapisClient, + schema: Schema, + accessions: string[], + field: string, +): Promise> => { + if (accessions.length === 0 || !schema.metadata.some((f) => f.name === field)) { + return ok([]); + } + const result = await client.call('aggregated', { + [ACCESSION_VERSION_FIELD]: accessions, + fields: [field], + }); + + return result.map(({ data }) => + data.map((item) => ({ + value: item[field], + count: item.count, + })), + ); +}; + +export const getSeqSetStatistics = async ( + accessions: string[], + field: string, +): Promise> => { + if (accessions.length === 0) { + return ok([]); + } + + const organisms = getConfiguredOrganisms(); + const aggregates = await Promise.all( + organisms.map((organism) => { + const client = LapisClient.createForOrganism(organism.key); + const schema = getSchema(organism.key); + return getAggregate(client, schema, accessions, field); + }), + ); + + const crossAggregate = new Map(); + for (const aggregate of aggregates) { + if (aggregate.isErr()) continue; + + for (const item of aggregate.value) { + crossAggregate.set(item.value, (crossAggregate.get(item.value) ?? 0) + item.count); + } + } + + return ok(Array.from(crossAggregate.entries()).map(([value, count]) => ({ value, count }))); +}; diff --git a/website/src/pages/seqsets/[seqSetId].[version].astro b/website/src/pages/seqsets/[seqSetId].[version].astro index e45fb529b1..9328b8459a 100644 --- a/website/src/pages/seqsets/[seqSetId].[version].astro +++ b/website/src/pages/seqsets/[seqSetId].[version].astro @@ -2,9 +2,11 @@ import { ErrorFeedback } from '../../components/ErrorFeedback'; import { SeqSetItem } from '../../components/SeqSetCitations/SeqSetItem'; import { SeqSetItemActions } from '../../components/SeqSetCitations/SeqSetItemActions'; +import { getSeqSetStatistics } from '../../components/SeqSetCitations/getSeqSetStatistics.ts'; import { getRuntimeConfig, seqSetsAreEnabled, getWebsiteConfig } from '../../config'; import BaseLayout from '../../layouts/BaseLayout.astro'; import { SeqSetCitationClient } from '../../services/seqSetCitationClient.ts'; +import { DATA_USE_TERMS_FIELD } from '../../settings'; import type { SeqSet } from '../../types/seqSetCitation'; import { getAccessToken } from '../../utils/getAccessToken'; @@ -27,17 +29,20 @@ if (!seqSetsAreEnabled()) { const seqSetClient = SeqSetCitationClient.create(); -const seqSetResponse = await seqSetClient.call('getSeqSet', { - params: { seqSetId, version }, -}); +const [seqSetResponse, seqSetRecordsResponse, seqSetCitedByResponse] = await Promise.all([ + seqSetClient.call('getSeqSet', { params: { seqSetId, version } }), + seqSetClient.call('getSeqSetRecords', { params: { seqSetId, version } }), + seqSetClient.call('getSeqSetCitedBy', { params: { seqSetId, version } }), +]); -const seqSetRecordsResponse = await seqSetClient.call('getSeqSetRecords', { - params: { seqSetId, version }, -}); +const seqSetAccessions = seqSetRecordsResponse.isOk() ? seqSetRecordsResponse.value.map((r) => r.accession) : []; -const seqSetCitedByResponse = await seqSetClient.call('getSeqSetCitedBy', { - params: { seqSetId, version }, -}); +const [seqSetCollectionDatesResponse, seqSetCollectionCountriesResponse, seqSetDataUseTermsResponse] = + await Promise.all([ + getSeqSetStatistics(seqSetAccessions, 'date'), + getSeqSetStatistics(seqSetAccessions, 'country'), + getSeqSetStatistics(seqSetAccessions, DATA_USE_TERMS_FIELD), + ]); const getSeqSetByVersion = (seqSetVersions: SeqSet[], version: string) => { const matchedVersion = seqSetVersions.find((obj) => { @@ -53,17 +58,7 @@ const seqSet = seqSetResponse.isOk() ? getSeqSetByVersion(seqSetResponse.value, const seqSetAccessionVersion = `${seqSetId}.${version}`; const authorResponse = seqSet !== undefined ? await seqSetClient.getAuthor(seqSet.createdBy) : undefined; - -const assignAuthorName = (seqSet: SeqSet) => { - return { - ...seqSet, - createdBy: authorResponse?.isOk() - ? [authorResponse.value?.firstName, authorResponse.value?.lastName] - .filter((name) => name !== null) - .join(' ') - : seqSet.createdBy, - }; -}; +const author = authorResponse?.isOk() ? authorResponse.value : undefined; --- { ) : ( { client:only='react' /> )} - {seqSetRecordsResponse.isOk() && seqSetCitedByResponse.isOk() ? ( + {seqSetRecordsResponse.isOk() && + seqSetCitedByResponse.isOk() && + seqSetCollectionDatesResponse.isOk() && + seqSetCollectionCountriesResponse.isOk() && + seqSetDataUseTermsResponse.isOk() ? ( Date: Fri, 20 Mar 2026 13:20:56 +0100 Subject: [PATCH 07/24] Graph and seqset details updates --- website/src/components/SeqSetCitations/CitationPlot.tsx | 8 ++++++++ website/src/components/SeqSetCitations/SeqSetItem.tsx | 4 ++-- website/src/components/common/BarPlot.tsx | 1 - 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/website/src/components/SeqSetCitations/CitationPlot.tsx b/website/src/components/SeqSetCitations/CitationPlot.tsx index 410a061279..97d267fd83 100644 --- a/website/src/components/SeqSetCitations/CitationPlot.tsx +++ b/website/src/components/SeqSetCitations/CitationPlot.tsx @@ -31,6 +31,14 @@ export const CitationPlot: React.FC = ({ citedByData, respons }} options={{ responsive: responsive ?? true, + scales: { + y: { + suggestedMax: 10, + grid: { + color: 'rgba(0, 0, 0, 0)', + }, + }, + }, }} description={description} /> diff --git a/website/src/components/SeqSetCitations/SeqSetItem.tsx b/website/src/components/SeqSetCitations/SeqSetItem.tsx index baf991cdd7..66a4e75eb9 100644 --- a/website/src/components/SeqSetCitations/SeqSetItem.tsx +++ b/website/src/components/SeqSetCitations/SeqSetItem.tsx @@ -38,7 +38,7 @@ const SeqSetDetails: FC<{ title: string; children: React.ReactNode }> = ({ title const SeqSetDetailsEntry: FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => (
{label}
-
{value}
+
{value}
); @@ -135,7 +135,7 @@ const SeqSetItemInner: FC = ({ const graphColour = '#88a1d2'; const getGraphData = (data: AggregateRow[]) => ({ labels: data.map((item) => item.value ?? 'Unknown'), - datasets: [{ data: data.map((item) => item.count), backgroundColor: graphColour }], + datasets: [{ data: data.map((item) => item.count), backgroundColor: graphColour, maxBarThickness: 30 }], }); return ( diff --git a/website/src/components/common/BarPlot.tsx b/website/src/components/common/BarPlot.tsx index 84efb975c7..b15cdad22d 100644 --- a/website/src/components/common/BarPlot.tsx +++ b/website/src/components/common/BarPlot.tsx @@ -45,7 +45,6 @@ export const BarPlot: FC = ({ data, options, description }) => { }, scales: { y: { - suggestedMax: 10, grid: { color: 'rgba(0, 0, 0, 0)', }, From e69d2ac2b110178c9077f1f9f4c3efb24db47f39 Mon Sep 17 00:00:00 2001 From: tombch Date: Fri, 20 Mar 2026 13:56:25 +0100 Subject: [PATCH 08/24] SeqSet records table css updates --- .../SeqSetRecordsTableWithMetadata.tsx | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/website/src/components/SeqSetCitations/SeqSetRecordsTableWithMetadata.tsx b/website/src/components/SeqSetCitations/SeqSetRecordsTableWithMetadata.tsx index 048958d289..5b9fc2bd64 100644 --- a/website/src/components/SeqSetCitations/SeqSetRecordsTableWithMetadata.tsx +++ b/website/src/components/SeqSetCitations/SeqSetRecordsTableWithMetadata.tsx @@ -116,6 +116,16 @@ const fetchRecordsMetadata = async ( return metadataMap; }; +const SeqSetRecordsTableHeader: FC<{ title: string }> = ({ title }) => ( + {title} +); + +const SeqSetRecordsTableCell: FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => ( + + {children} + +); + export const SeqSetRecordsTableWithMetadata: FC = ({ seqSetRecords, clientConfig, @@ -150,20 +160,18 @@ export const SeqSetRecordsTableWithMetadata: FC + - - - - + + + + {fieldsToDisplay.map((fieldConfig) => ( - + ))} - + {sortedSeqRecords.map((seqSetRecord, index) => { const metadata = metadataMap?.get(seqSetRecord.accession); const handleRowClick = () => { @@ -172,14 +180,13 @@ export const SeqSetRecordsTableWithMetadata: FC - - + {fieldsToDisplay.map((fieldConfig) => ( - ); From d08dacfed846baaf77b20faafcaf4e00ec9d96ea Mon Sep 17 00:00:00 2001 From: tombch Date: Fri, 20 Mar 2026 14:20:20 +0100 Subject: [PATCH 09/24] Support multiple field options for the aggregate --- .../components/SeqSetCitations/getSeqSetStatistics.ts | 11 ++++------- website/src/pages/seqsets/[seqSetId].[version].astro | 6 +++--- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/website/src/components/SeqSetCitations/getSeqSetStatistics.ts b/website/src/components/SeqSetCitations/getSeqSetStatistics.ts index 4a17ea3e15..092e9796c4 100644 --- a/website/src/components/SeqSetCitations/getSeqSetStatistics.ts +++ b/website/src/components/SeqSetCitations/getSeqSetStatistics.ts @@ -5,20 +5,15 @@ import { getConfiguredOrganisms, getSchema } from '../../config.ts'; import { LapisClient } from '../../services/lapisClient.ts'; import { ACCESSION_VERSION_FIELD } from '../../settings.ts'; import type { ProblemDetail } from '../../types/backend.ts'; -import { type Schema } from '../../types/config.ts'; type AggregateValue = string | number | boolean | null; export type AggregateRow = { value: AggregateValue; count: number }; const getAggregate = async ( client: LapisClient, - schema: Schema, accessions: string[], field: string, ): Promise> => { - if (accessions.length === 0 || !schema.metadata.some((f) => f.name === field)) { - return ok([]); - } const result = await client.call('aggregated', { [ACCESSION_VERSION_FIELD]: accessions, fields: [field], @@ -34,7 +29,7 @@ const getAggregate = async ( export const getSeqSetStatistics = async ( accessions: string[], - field: string, + fieldOptions: string[], ): Promise> => { if (accessions.length === 0) { return ok([]); @@ -45,7 +40,9 @@ export const getSeqSetStatistics = async ( organisms.map((organism) => { const client = LapisClient.createForOrganism(organism.key); const schema = getSchema(organism.key); - return getAggregate(client, schema, accessions, field); + const field = fieldOptions.find((option) => schema.metadata.some((f) => f.name === option)); + if (!field) return Promise.resolve(ok([])); + return getAggregate(client, accessions, field); }), ); diff --git a/website/src/pages/seqsets/[seqSetId].[version].astro b/website/src/pages/seqsets/[seqSetId].[version].astro index 9328b8459a..fbd8659dd3 100644 --- a/website/src/pages/seqsets/[seqSetId].[version].astro +++ b/website/src/pages/seqsets/[seqSetId].[version].astro @@ -39,9 +39,9 @@ const seqSetAccessions = seqSetRecordsResponse.isOk() ? seqSetRecordsResponse.va const [seqSetCollectionDatesResponse, seqSetCollectionCountriesResponse, seqSetDataUseTermsResponse] = await Promise.all([ - getSeqSetStatistics(seqSetAccessions, 'date'), - getSeqSetStatistics(seqSetAccessions, 'country'), - getSeqSetStatistics(seqSetAccessions, DATA_USE_TERMS_FIELD), + getSeqSetStatistics(seqSetAccessions, ['sampleCollectionDate', 'date']), + getSeqSetStatistics(seqSetAccessions, ['geoLocCountry', 'country']), + getSeqSetStatistics(seqSetAccessions, [DATA_USE_TERMS_FIELD]), ]); const getSeqSetByVersion = (seqSetVersions: SeqSet[], version: string) => { From 440346e8509458cbfd15a0831183fe22f9f3fcb0 Mon Sep 17 00:00:00 2001 From: tombch Date: Tue, 24 Mar 2026 11:31:02 +0100 Subject: [PATCH 10/24] Added chartjs date adapter --- website/package-lock.json | 54 +++++++++++++++++++++++++++++++++++++++ website/package.json | 1 + 2 files changed, 55 insertions(+) diff --git a/website/package-lock.json b/website/package-lock.json index 2e98753eca..f0de177dad 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -24,6 +24,7 @@ "axios": "^1.13.6", "change-case": "~5.3.0", "chart.js": "^4.5.1", + "chartjs-adapter-date-fns": "^3.0.0", "cookie": "^1.1.1", "fflate": "^0.8.2", "flowbite-react": "^0.10.2", @@ -1305,6 +1306,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1337,6 +1339,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1369,6 +1372,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2834,6 +2838,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2873,6 +2878,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2893,6 +2899,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2913,6 +2920,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2933,6 +2941,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2953,6 +2962,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2973,6 +2983,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2993,6 +3004,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3013,6 +3025,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3033,6 +3046,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3053,6 +3067,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3073,6 +3088,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3093,6 +3109,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3113,6 +3130,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3130,6 +3148,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, "license": "MIT", "optional": true }, @@ -6316,6 +6335,16 @@ "pnpm": ">=8" } }, + "node_modules/chartjs-adapter-date-fns": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz", + "integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=2.8.0", + "date-fns": ">=2.0.0" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -6995,6 +7024,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, "license": "Apache-2.0", "optional": true, "bin": { @@ -15922,6 +15952,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15938,6 +15969,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15954,6 +15986,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15970,6 +16003,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15986,6 +16020,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16002,6 +16037,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16018,6 +16054,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16034,6 +16071,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16050,6 +16088,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16066,6 +16105,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16082,6 +16122,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16098,6 +16139,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16114,6 +16156,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16130,6 +16173,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16146,6 +16190,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16162,6 +16207,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16178,6 +16224,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16194,6 +16241,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16210,6 +16258,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16226,6 +16275,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16242,6 +16292,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16258,6 +16309,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16274,6 +16326,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16329,6 +16382,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/website/package.json b/website/package.json index 937032bf6f..568f5b0748 100644 --- a/website/package.json +++ b/website/package.json @@ -34,6 +34,7 @@ "axios": "^1.13.6", "change-case": "~5.3.0", "chart.js": "^4.5.1", + "chartjs-adapter-date-fns": "^3.0.0", "cookie": "^1.1.1", "fflate": "^0.8.2", "flowbite-react": "^0.10.2", From 055f1f8b27498bd2e6a71f08aacd30ea144e7496 Mon Sep 17 00:00:00 2001 From: tombch Date: Tue, 24 Mar 2026 11:43:50 +0100 Subject: [PATCH 11/24] Cleaned up plots for dates, countries and use terms --- .../SeqSetCitations/SeqSetPlots.tsx | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 website/src/components/SeqSetCitations/SeqSetPlots.tsx diff --git a/website/src/components/SeqSetCitations/SeqSetPlots.tsx b/website/src/components/SeqSetCitations/SeqSetPlots.tsx new file mode 100644 index 0000000000..14403c5842 --- /dev/null +++ b/website/src/components/SeqSetCitations/SeqSetPlots.tsx @@ -0,0 +1,115 @@ +import { DateTime } from 'luxon'; +import React from 'react'; + +import type { AggregateRow } from './getSeqSetStatistics'; +import { SEQSET_GRAPHS_COLOUR } from '../../types/seqSetCitation'; +import { BarPlot } from '../common/BarPlot'; + +type SeqSetPlotProps = { + data: AggregateRow[]; + description?: string; +}; + +type GraphTimeProperties = { + unit: 'day' | 'month' | 'year'; + displayFormats: { day?: string; month?: string; year?: string }; + tooltipFormat: string; +}; + +/** Transform data into the format required by the graph component. */ +const getGraphData = (data: AggregateRow[]) => ({ + labels: data.map((item) => item.value ?? 'Unknown'), + datasets: [{ data: data.map((item) => item.count), backgroundColor: SEQSET_GRAPHS_COLOUR, maxBarThickness: 30 }], +}); + +export const DatePlot: React.FC = ({ data, description }) => { + /** Get the appropriate date format for the graph based on the range of dates in the data. */ + const getDateFormatFromData = (data: AggregateRow[]): string => { + const dateValues = data + .map((row) => (typeof row.value === 'string' ? DateTime.fromISO(row.value) : null)) + .filter((date) => date !== null) + .filter((date) => date.isValid); + const minDate = DateTime.min(...dateValues); + const maxDate = DateTime.max(...dateValues); + const diff = minDate && maxDate ? maxDate.diff(minDate, 'days').days : 0; + + let format; + if (diff <= 60) format = 'yyyy-MM-dd'; + else if (diff <= 365) format = 'yyyy-MM'; + else format = 'yyyy'; + + return format; + }; + + /** Group the data by the specified date format */ + const groupByDateFormat = (data: AggregateRow[], format: string): AggregateRow[] => { + const yearMonths = new Map(); + + data.forEach((row) => { + if (typeof row.value !== 'string') return; + + const dateValue = DateTime.fromISO(row.value); + if (!dateValue.isValid) return; + + const yearMonth = dateValue.toFormat(format); + yearMonths.set(yearMonth, (yearMonths.get(yearMonth) ?? 0) + row.count); + }); + + return Array.from(yearMonths.entries()).map(([value, count]) => ({ value, count })); + }; + + /** Get the corresponding graph properties for the specified date format. */ + const getGraphTimeProperties = (format: string): GraphTimeProperties => { + if (format === 'yyyy-MM-dd') + return { unit: 'day', displayFormats: { day: 'dd MMM yyyy' }, tooltipFormat: 'yyyy-MM-dd' }; + + if (format === 'yyyy-MM') + return { unit: 'month', displayFormats: { month: 'MMM yyyy' }, tooltipFormat: 'yyyy-MM' }; + + return { unit: 'year', displayFormats: { year: 'yyyy' }, tooltipFormat: 'yyyy' }; + }; + + const dateFormat = getDateFormatFromData(data); + const groupedData = groupByDateFormat(data, dateFormat); + const graphData = getGraphData(groupedData); + const graphTimeProperties = getGraphTimeProperties(dateFormat); + + return ( + + ); +}; + +export const CountriesPlot: React.FC = ({ data, description }) => { + /** Group the data by the specified cutoff, grouping remaining points into an "Others" category. */ + const groupRemainingPoints = (data: AggregateRow[], cutoff: number): AggregateRow[] => { + const sortedData = data.sort((a, b) => b.count - a.count); + const topData = sortedData.slice(0, cutoff); + const otherData = sortedData.slice(cutoff); + const otherCount = otherData.reduce((sum, row) => sum + row.count, 0); + return otherCount > 0 ? [...topData, { value: `Others (${otherData.length})`, count: otherCount }] : topData; + }; + + const groupedData = groupRemainingPoints(data, 10); + const graphData = getGraphData(groupedData); + + return ; +}; + +export const UseTermsPlot: React.FC = ({ data, description }) => { + const graphData = getGraphData(data); + return ; +}; From 6d4f5e647f78243513af92523c96c77fa4afca61 Mon Sep 17 00:00:00 2001 From: tombch Date: Tue, 24 Mar 2026 11:45:22 +0100 Subject: [PATCH 12/24] Updating seqset plots --- .../SeqSetCitations/CitationPlot.tsx | 14 ++++---- .../components/SeqSetCitations/SeqSetItem.tsx | 33 +++++++++---------- website/src/components/common/BarPlot.tsx | 23 ++++--------- website/src/types/seqSetCitation.ts | 2 ++ 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/website/src/components/SeqSetCitations/CitationPlot.tsx b/website/src/components/SeqSetCitations/CitationPlot.tsx index 97d267fd83..ab0eb8749c 100644 --- a/website/src/components/SeqSetCitations/CitationPlot.tsx +++ b/website/src/components/SeqSetCitations/CitationPlot.tsx @@ -1,15 +1,14 @@ import React from 'react'; -import type { CitedByResult } from '../../types/seqSetCitation'; +import { SEQSET_GRAPHS_COLOUR, type CitedByResult } from '../../types/seqSetCitation'; import { BarPlot } from '../common/BarPlot'; type CitationPlotProps = { citedByData: CitedByResult; - responsive?: boolean; description?: string; }; -export const CitationPlot: React.FC = ({ citedByData, responsive, description }) => { +export const CitationPlot: React.FC = ({ citedByData, description }) => { const emptyCitedByData = { years: [2020, 2021, 2022, 2023, 2024], citations: [0, 0, 0, 0, 0], @@ -25,19 +24,20 @@ export const CitationPlot: React.FC = ({ citedByData, respons { data: renderData.citations, label: 'Citation count', - backgroundColor: '#54858c', + backgroundColor: SEQSET_GRAPHS_COLOUR, }, ], }} options={{ - responsive: responsive ?? true, scales: { - y: { - suggestedMax: 10, + x: { grid: { color: 'rgba(0, 0, 0, 0)', }, }, + y: { + suggestedMax: 10, + }, }, }} description={description} diff --git a/website/src/components/SeqSetCitations/SeqSetItem.tsx b/website/src/components/SeqSetCitations/SeqSetItem.tsx index 66a4e75eb9..fdf7b4f93a 100644 --- a/website/src/components/SeqSetCitations/SeqSetItem.tsx +++ b/website/src/components/SeqSetCitations/SeqSetItem.tsx @@ -5,6 +5,7 @@ import { toast } from 'react-toastify'; import { AuthorDetails } from './AuthorDetails.tsx'; import { CitationPlot } from './CitationPlot'; +import { DatePlot, CountriesPlot, UseTermsPlot } from './SeqSetPlots.tsx'; import { SeqSetRecordsTableWithMetadata } from './SeqSetRecordsTableWithMetadata'; import type { AggregateRow } from './getSeqSetStatistics.ts'; import { getClientLogger } from '../../clientLogger'; @@ -14,18 +15,21 @@ import type { ClientConfig } from '../../types/runtimeConfig'; import { type AuthorProfile, type CitedByResult, type SeqSet, type SeqSetRecord } from '../../types/seqSetCitation'; import { createAuthorizationHeader } from '../../utils/createAuthorizationHeader'; import { displayConfirmationDialog } from '../ConfirmationDialog.tsx'; -import { BarPlot } from '../common/BarPlot.tsx'; import { withQueryProvider } from '../common/withQueryProvider.tsx'; const logger = getClientLogger('SeqSetItem'); const SeqSetSection: FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => (
-

{title}

+
+

{title}

+
{children}
); +const SeqSetSectionSeparator: FC = () =>
; + const SeqSetDetails: FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => (
@@ -132,12 +136,6 @@ const SeqSetItemInner: FC = ({ return seqSetRecords.slice((page - 1) * sequencesPerPage, page * sequencesPerPage); }; - const graphColour = '#88a1d2'; - const getGraphData = (data: AggregateRow[]) => ({ - labels: data.map((item) => item.value ?? 'Unknown'), - datasets: [{ data: data.map((item) => item.count), backgroundColor: graphColour, maxBarThickness: 30 }], - }); - return (
@@ -184,29 +182,30 @@ const SeqSetItemInner: FC = ({ value={ } />
+
- - -
+ = ({ data, options, description }) => { const [isRegistered, setIsRegistered] = useState(false); useEffect(() => { - ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); + ChartJS.register(CategoryScale, LinearScale, TimeScale, BarElement, Tooltip, Legend); setIsRegistered(true); }, []); - if (!isRegistered) { - return null; - } + if (!isRegistered) return null; return (
= ({ data, options, description }) => { ...options, }} /> - {description && ( -

- {description} -

- )} + {description &&

{description}

}
); }; diff --git a/website/src/types/seqSetCitation.ts b/website/src/types/seqSetCitation.ts index dc5447ccc7..691237928e 100644 --- a/website/src/types/seqSetCitation.ts +++ b/website/src/types/seqSetCitation.ts @@ -38,3 +38,5 @@ export const authorProfile = z.object({ university: z.string().nullish(), }); export type AuthorProfile = z.infer; + +export const SEQSET_GRAPHS_COLOUR = '#88a1d2'; From 920981a894f000ac4b95d20d426eebb85faee5c8 Mon Sep 17 00:00:00 2001 From: tombch Date: Tue, 24 Mar 2026 11:47:47 +0100 Subject: [PATCH 13/24] Removed responsive prop --- website/src/pages/seqsets/index.astro | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/pages/seqsets/index.astro b/website/src/pages/seqsets/index.astro index c969399d6b..69a9f2265b 100644 --- a/website/src/pages/seqsets/index.astro +++ b/website/src/pages/seqsets/index.astro @@ -98,7 +98,6 @@ const editAccountUrl = (await getUrlForKeycloakAccountPage()) + '/#/personal-inf {/* We show an empty plot for now until we get real data. */} From 361861c45b9a73f227d71cc1076a4a4e99910581 Mon Sep 17 00:00:00 2001 From: tombch Date: Tue, 24 Mar 2026 12:09:10 +0100 Subject: [PATCH 14/24] Fixing esbuild error --- website/package-lock.json | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index f0de177dad..4bf85ced7d 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -15952,7 +15952,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15969,7 +15968,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -15986,7 +15984,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16003,7 +16000,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16020,7 +16016,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16037,7 +16032,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16054,7 +16048,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16071,7 +16064,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16088,7 +16080,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16105,7 +16096,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16122,7 +16112,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16139,7 +16128,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16156,7 +16144,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16173,7 +16160,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16190,7 +16176,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16207,7 +16192,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16224,7 +16208,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16241,7 +16224,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16258,7 +16240,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16275,7 +16256,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16292,7 +16272,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16309,7 +16288,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16326,7 +16304,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -16382,7 +16359,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -17374,4 +17350,4 @@ } } } -} +} \ No newline at end of file From e6e4923f06ccd9955ec2ca215672da6e39d25c32 Mon Sep 17 00:00:00 2001 From: tombch Date: Tue, 24 Mar 2026 12:12:31 +0100 Subject: [PATCH 15/24] Fixing formatting --- website/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/package-lock.json b/website/package-lock.json index 4bf85ced7d..f01bae8ad7 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -17350,4 +17350,4 @@ } } } -} \ No newline at end of file +} From f5179ea9cef84ee7bc7fabee83e96a554bc137d3 Mon Sep 17 00:00:00 2001 From: tombch Date: Tue, 24 Mar 2026 18:37:39 +0100 Subject: [PATCH 16/24] Unit testing for seq set plot utils --- .../SeqSetCitations/SeqSetPlots.spec.tsx | 197 ++++++++++++++++++ .../SeqSetCitations/SeqSetPlots.tsx | 93 ++++----- 2 files changed, 243 insertions(+), 47 deletions(-) create mode 100644 website/src/components/SeqSetCitations/SeqSetPlots.spec.tsx diff --git a/website/src/components/SeqSetCitations/SeqSetPlots.spec.tsx b/website/src/components/SeqSetCitations/SeqSetPlots.spec.tsx new file mode 100644 index 0000000000..8c203c987c --- /dev/null +++ b/website/src/components/SeqSetCitations/SeqSetPlots.spec.tsx @@ -0,0 +1,197 @@ +import { describe, expect, it } from 'vitest'; + +import { + getDateFormatFromData, + getGraphData, + getGraphTimeProperties, + groupByDateFormat, + groupRemainingPoints, +} from './SeqSetPlots'; +import { SEQSET_GRAPHS_COLOUR } from '../../types/seqSetCitation'; + +describe('getGraphData', () => { + it('maps values to labels and counts to dataset data', () => { + const input = [ + { value: 'USA', count: 10 }, + { value: 'Germany', count: 5 }, + ]; + const result = getGraphData(input); + expect(result.labels).toEqual(['USA', 'Germany']); + expect(result.datasets[0].data).toEqual([10, 5]); + }); + + it('uses SEQSET_GRAPHS_COLOUR as backgroundColor', () => { + const result = getGraphData([{ value: 'USA', count: 1 }]); + expect(result.datasets[0].backgroundColor).toBe(SEQSET_GRAPHS_COLOUR); + }); + + it('uses null value as "Unknown" label', () => { + const result = getGraphData([{ value: null, count: 3 }]); + expect(result.labels).toEqual(['Unknown']); + }); + + it('returns empty labels and data for empty input', () => { + const result = getGraphData([]); + expect(result.labels).toEqual([]); + expect(result.datasets[0].data).toEqual([]); + }); +}); + +describe('getDateFormatFromData', () => { + it('returns yyyy-MM-dd for dates within 60 days of each other', () => { + const data = [ + { value: '2024-01-01', count: 1 }, + { value: '2024-02-01', count: 1 }, + ]; + expect(getDateFormatFromData(data)).toBe('yyyy-MM-dd'); + }); + + it('returns yyyy-MM for dates between 60 and 365 days apart', () => { + const data = [ + { value: '2024-01-01', count: 1 }, + { value: '2024-06-01', count: 1 }, + ]; + expect(getDateFormatFromData(data)).toBe('yyyy-MM'); + }); + + it('returns yyyy for dates more than 365 days apart', () => { + const data = [ + { value: '2022-01-01', count: 1 }, + { value: '2024-01-01', count: 1 }, + ]; + expect(getDateFormatFromData(data)).toBe('yyyy'); + }); + + it('returns yyyy-MM-dd for a single date', () => { + expect(getDateFormatFromData([{ value: '2024-03-15', count: 1 }])).toBe('yyyy-MM-dd'); + }); + + it('ignores rows with non-string or invalid date values', () => { + const data = [ + { value: null, count: 1 }, + { value: 'not-a-date', count: 1 }, + { value: '2024-01-01', count: 1 }, + ]; + expect(getDateFormatFromData(data)).toBe('yyyy-MM-dd'); + }); +}); + +describe('groupByDateFormat', () => { + it('groups dates by month when format is yyyy-MM', () => { + const data = [ + { value: '2024-01-01', count: 3 }, + { value: '2024-01-15', count: 2 }, + { value: '2024-02-01', count: 5 }, + ]; + const result = groupByDateFormat(data, 'yyyy-MM'); + expect(result).toEqual([ + { value: '2024-01', count: 5 }, + { value: '2024-02', count: 5 }, + ]); + }); + + it('groups dates by year when format is yyyy', () => { + const data = [ + { value: '2022-06-01', count: 4 }, + { value: '2022-12-01', count: 6 }, + { value: '2023-03-01', count: 2 }, + ]; + const result = groupByDateFormat(data, 'yyyy'); + expect(result).toEqual([ + { value: '2022', count: 10 }, + { value: '2023', count: 2 }, + ]); + }); + + it('skips rows with non-string or invalid date values', () => { + const data = [ + { value: null, count: 99 }, + { value: 'not-a-date', count: 99 }, + { value: '2024-03-01', count: 1 }, + ]; + const result = groupByDateFormat(data, 'yyyy-MM'); + expect(result).toEqual([{ value: '2024-03', count: 1 }]); + }); + + it('returns empty array for empty input', () => { + expect(groupByDateFormat([], 'yyyy-MM')).toEqual([]); + }); +}); + +describe('getGraphTimeProperties', () => { + it('returns day properties for yyyy-MM-dd', () => { + expect(getGraphTimeProperties('yyyy-MM-dd')).toEqual({ + unit: 'day', + displayFormats: { day: 'dd MMM yyyy' }, + tooltipFormat: 'yyyy-MM-dd', + }); + }); + + it('returns month properties for yyyy-MM', () => { + expect(getGraphTimeProperties('yyyy-MM')).toEqual({ + unit: 'month', + displayFormats: { month: 'MMM yyyy' }, + tooltipFormat: 'yyyy-MM', + }); + }); + + it('returns year properties for yyyy', () => { + expect(getGraphTimeProperties('yyyy')).toEqual({ + unit: 'year', + displayFormats: { year: 'yyyy' }, + tooltipFormat: 'yyyy', + }); + }); +}); + +describe('groupRemainingPoints', () => { + it('returns all rows unchanged when count is within cutoff', () => { + const data = [ + { value: 'USA', count: 10 }, + { value: 'Germany', count: 5 }, + ]; + expect(groupRemainingPoints(data, 10)).toEqual([ + { value: 'USA', count: 10 }, + { value: 'Germany', count: 5 }, + ]); + }); + + it('groups rows beyond cutoff into an Others entry', () => { + const data = [ + { value: 'USA', count: 10 }, + { value: 'Germany', count: 5 }, + { value: 'France', count: 3 }, + { value: 'Spain', count: 2 }, + ]; + const result = groupRemainingPoints(data, 2); + expect(result).toEqual([ + { value: 'USA', count: 10 }, + { value: 'Germany', count: 5 }, + { value: 'Others (2)', count: 5 }, + ]); + }); + + it('sorts by count descending before applying cutoff', () => { + const data = [ + { value: 'Spain', count: 2 }, + { value: 'USA', count: 10 }, + { value: 'Germany', count: 5 }, + ]; + const [first, second] = groupRemainingPoints(data, 2); + expect(first.value).toBe('USA'); + expect(second.value).toBe('Germany'); + }); + + it('omits Others entry when all rows fit within cutoff', () => { + const data = [ + { value: 'USA', count: 10 }, + { value: 'Germany', count: 5 }, + ]; + const result = groupRemainingPoints(data, 2); + expect(result.find((r) => String(r.value).startsWith('Others'))).toBeUndefined(); + }); + + it('returns empty array for empty input', () => { + expect(groupRemainingPoints([], 10)).toEqual([]); + }); +}); diff --git a/website/src/components/SeqSetCitations/SeqSetPlots.tsx b/website/src/components/SeqSetCitations/SeqSetPlots.tsx index 14403c5842..b5896c1049 100644 --- a/website/src/components/SeqSetCitations/SeqSetPlots.tsx +++ b/website/src/components/SeqSetCitations/SeqSetPlots.tsx @@ -17,58 +17,66 @@ type GraphTimeProperties = { }; /** Transform data into the format required by the graph component. */ -const getGraphData = (data: AggregateRow[]) => ({ +export const getGraphData = (data: AggregateRow[]) => ({ labels: data.map((item) => item.value ?? 'Unknown'), datasets: [{ data: data.map((item) => item.count), backgroundColor: SEQSET_GRAPHS_COLOUR, maxBarThickness: 30 }], }); -export const DatePlot: React.FC = ({ data, description }) => { - /** Get the appropriate date format for the graph based on the range of dates in the data. */ - const getDateFormatFromData = (data: AggregateRow[]): string => { - const dateValues = data - .map((row) => (typeof row.value === 'string' ? DateTime.fromISO(row.value) : null)) - .filter((date) => date !== null) - .filter((date) => date.isValid); - const minDate = DateTime.min(...dateValues); - const maxDate = DateTime.max(...dateValues); - const diff = minDate && maxDate ? maxDate.diff(minDate, 'days').days : 0; - - let format; - if (diff <= 60) format = 'yyyy-MM-dd'; - else if (diff <= 365) format = 'yyyy-MM'; - else format = 'yyyy'; +/** Get the appropriate date format for the graph based on the range of dates in the data. */ +export const getDateFormatFromData = (data: AggregateRow[]): string => { + const dateValues = data + .map((row) => (typeof row.value === 'string' ? DateTime.fromISO(row.value) : null)) + .filter((date) => date !== null) + .filter((date) => date.isValid); + const minDate = DateTime.min(...dateValues); + const maxDate = DateTime.max(...dateValues); + const diff = minDate && maxDate ? maxDate.diff(minDate, 'days').days : 0; + + let format; + if (diff <= 60) format = 'yyyy-MM-dd'; + else if (diff <= 365) format = 'yyyy-MM'; + else format = 'yyyy'; + + return format; +}; - return format; - }; +/** Group the data by the specified date format */ +export const groupByDateFormat = (data: AggregateRow[], format: string): AggregateRow[] => { + const yearMonths = new Map(); - /** Group the data by the specified date format */ - const groupByDateFormat = (data: AggregateRow[], format: string): AggregateRow[] => { - const yearMonths = new Map(); + data.forEach((row) => { + if (typeof row.value !== 'string') return; - data.forEach((row) => { - if (typeof row.value !== 'string') return; + const dateValue = DateTime.fromISO(row.value); + if (!dateValue.isValid) return; - const dateValue = DateTime.fromISO(row.value); - if (!dateValue.isValid) return; + const yearMonth = dateValue.toFormat(format); + yearMonths.set(yearMonth, (yearMonths.get(yearMonth) ?? 0) + row.count); + }); - const yearMonth = dateValue.toFormat(format); - yearMonths.set(yearMonth, (yearMonths.get(yearMonth) ?? 0) + row.count); - }); + return Array.from(yearMonths.entries()).map(([value, count]) => ({ value, count })); +}; - return Array.from(yearMonths.entries()).map(([value, count]) => ({ value, count })); - }; +/** Get the corresponding graph properties for the specified date format. */ +export const getGraphTimeProperties = (format: string): GraphTimeProperties => { + if (format === 'yyyy-MM-dd') + return { unit: 'day', displayFormats: { day: 'dd MMM yyyy' }, tooltipFormat: 'yyyy-MM-dd' }; - /** Get the corresponding graph properties for the specified date format. */ - const getGraphTimeProperties = (format: string): GraphTimeProperties => { - if (format === 'yyyy-MM-dd') - return { unit: 'day', displayFormats: { day: 'dd MMM yyyy' }, tooltipFormat: 'yyyy-MM-dd' }; + if (format === 'yyyy-MM') return { unit: 'month', displayFormats: { month: 'MMM yyyy' }, tooltipFormat: 'yyyy-MM' }; - if (format === 'yyyy-MM') - return { unit: 'month', displayFormats: { month: 'MMM yyyy' }, tooltipFormat: 'yyyy-MM' }; + return { unit: 'year', displayFormats: { year: 'yyyy' }, tooltipFormat: 'yyyy' }; +}; - return { unit: 'year', displayFormats: { year: 'yyyy' }, tooltipFormat: 'yyyy' }; - }; +/** Group the data by the specified cutoff, grouping remaining points into an "Others" category. */ +export const groupRemainingPoints = (data: AggregateRow[], cutoff: number): AggregateRow[] => { + const sortedData = data.sort((a, b) => b.count - a.count); + const topData = sortedData.slice(0, cutoff); + const otherData = sortedData.slice(cutoff); + const otherCount = otherData.reduce((sum, row) => sum + row.count, 0); + return otherCount > 0 ? [...topData, { value: `Others (${otherData.length})`, count: otherCount }] : topData; +}; +export const DatePlot: React.FC = ({ data, description }) => { const dateFormat = getDateFormatFromData(data); const groupedData = groupByDateFormat(data, dateFormat); const graphData = getGraphData(groupedData); @@ -94,15 +102,6 @@ export const DatePlot: React.FC = ({ data, description }) => { }; export const CountriesPlot: React.FC = ({ data, description }) => { - /** Group the data by the specified cutoff, grouping remaining points into an "Others" category. */ - const groupRemainingPoints = (data: AggregateRow[], cutoff: number): AggregateRow[] => { - const sortedData = data.sort((a, b) => b.count - a.count); - const topData = sortedData.slice(0, cutoff); - const otherData = sortedData.slice(cutoff); - const otherCount = otherData.reduce((sum, row) => sum + row.count, 0); - return otherCount > 0 ? [...topData, { value: `Others (${otherData.length})`, count: otherCount }] : topData; - }; - const groupedData = groupRemainingPoints(data, 10); const graphData = getGraphData(groupedData); From 2c0a4604981fd55796b89974ae590eb1b0dec8d6 Mon Sep 17 00:00:00 2001 From: tombch Date: Thu, 26 Mar 2026 10:42:33 +0100 Subject: [PATCH 17/24] Removed hardcoded graph colours as these change between loculus instances --- website/src/colors.ts | 8 + .../SeqSetCitations/CitationPlot.tsx | 7 +- .../components/SeqSetCitations/SeqSetItem.tsx | 6 + .../SeqSetCitations/SeqSetPlots.spec.tsx | 6 - .../SeqSetCitations/SeqSetPlots.tsx | 18 +-- .../pages/seqsets/[seqSetId].[version].astro | 143 +++++++++--------- website/src/types/seqSetCitation.ts | 2 - 7 files changed, 97 insertions(+), 93 deletions(-) create mode 100644 website/src/colors.ts diff --git a/website/src/colors.ts b/website/src/colors.ts new file mode 100644 index 0000000000..07e89c3828 --- /dev/null +++ b/website/src/colors.ts @@ -0,0 +1,8 @@ +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); +const { mainTailwindColor } = require('../colors.cjs') as { + mainTailwindColor: Record; +}; + +export { mainTailwindColor }; diff --git a/website/src/components/SeqSetCitations/CitationPlot.tsx b/website/src/components/SeqSetCitations/CitationPlot.tsx index ab0eb8749c..6d80cd1817 100644 --- a/website/src/components/SeqSetCitations/CitationPlot.tsx +++ b/website/src/components/SeqSetCitations/CitationPlot.tsx @@ -1,14 +1,15 @@ import React from 'react'; -import { SEQSET_GRAPHS_COLOUR, type CitedByResult } from '../../types/seqSetCitation'; +import { type CitedByResult } from '../../types/seqSetCitation'; import { BarPlot } from '../common/BarPlot'; type CitationPlotProps = { citedByData: CitedByResult; description?: string; + barColor?: string; }; -export const CitationPlot: React.FC = ({ citedByData, description }) => { +export const CitationPlot: React.FC = ({ citedByData, description, barColor }) => { const emptyCitedByData = { years: [2020, 2021, 2022, 2023, 2024], citations: [0, 0, 0, 0, 0], @@ -24,7 +25,7 @@ export const CitationPlot: React.FC = ({ citedByData, descrip { data: renderData.citations, label: 'Citation count', - backgroundColor: SEQSET_GRAPHS_COLOUR, + backgroundColor: barColor, }, ], }} diff --git a/website/src/components/SeqSetCitations/SeqSetItem.tsx b/website/src/components/SeqSetCitations/SeqSetItem.tsx index fdf7b4f93a..5076316cc9 100644 --- a/website/src/components/SeqSetCitations/SeqSetItem.tsx +++ b/website/src/components/SeqSetCitations/SeqSetItem.tsx @@ -60,6 +60,7 @@ type SeqSetItemProps = { isAdminView?: boolean; fieldsToDisplay?: { field: string; displayName: string }[]; organismDisplayNames?: Record; + barGraphColor?: string; }; const SeqSetItemInner: FC = ({ @@ -76,6 +77,7 @@ const SeqSetItemInner: FC = ({ isAdminView = false, fieldsToDisplay, organismDisplayNames, + barGraphColor, }) => { const [page, setPage] = useState(1); const sequencesPerPage = 10; @@ -183,6 +185,7 @@ const SeqSetItemInner: FC = ({ } /> @@ -194,14 +197,17 @@ const SeqSetItemInner: FC = ({
diff --git a/website/src/components/SeqSetCitations/SeqSetPlots.spec.tsx b/website/src/components/SeqSetCitations/SeqSetPlots.spec.tsx index 8c203c987c..ca22372885 100644 --- a/website/src/components/SeqSetCitations/SeqSetPlots.spec.tsx +++ b/website/src/components/SeqSetCitations/SeqSetPlots.spec.tsx @@ -7,7 +7,6 @@ import { groupByDateFormat, groupRemainingPoints, } from './SeqSetPlots'; -import { SEQSET_GRAPHS_COLOUR } from '../../types/seqSetCitation'; describe('getGraphData', () => { it('maps values to labels and counts to dataset data', () => { @@ -20,11 +19,6 @@ describe('getGraphData', () => { expect(result.datasets[0].data).toEqual([10, 5]); }); - it('uses SEQSET_GRAPHS_COLOUR as backgroundColor', () => { - const result = getGraphData([{ value: 'USA', count: 1 }]); - expect(result.datasets[0].backgroundColor).toBe(SEQSET_GRAPHS_COLOUR); - }); - it('uses null value as "Unknown" label', () => { const result = getGraphData([{ value: null, count: 3 }]); expect(result.labels).toEqual(['Unknown']); diff --git a/website/src/components/SeqSetCitations/SeqSetPlots.tsx b/website/src/components/SeqSetCitations/SeqSetPlots.tsx index b5896c1049..75ce8ac888 100644 --- a/website/src/components/SeqSetCitations/SeqSetPlots.tsx +++ b/website/src/components/SeqSetCitations/SeqSetPlots.tsx @@ -2,12 +2,12 @@ import { DateTime } from 'luxon'; import React from 'react'; import type { AggregateRow } from './getSeqSetStatistics'; -import { SEQSET_GRAPHS_COLOUR } from '../../types/seqSetCitation'; import { BarPlot } from '../common/BarPlot'; type SeqSetPlotProps = { data: AggregateRow[]; description?: string; + barColor?: string; }; type GraphTimeProperties = { @@ -17,9 +17,9 @@ type GraphTimeProperties = { }; /** Transform data into the format required by the graph component. */ -export const getGraphData = (data: AggregateRow[]) => ({ +export const getGraphData = (data: AggregateRow[], barColor?: string) => ({ labels: data.map((item) => item.value ?? 'Unknown'), - datasets: [{ data: data.map((item) => item.count), backgroundColor: SEQSET_GRAPHS_COLOUR, maxBarThickness: 30 }], + datasets: [{ data: data.map((item) => item.count), backgroundColor: barColor, maxBarThickness: 30 }], }); /** Get the appropriate date format for the graph based on the range of dates in the data. */ @@ -76,10 +76,10 @@ export const groupRemainingPoints = (data: AggregateRow[], cutoff: number): Aggr return otherCount > 0 ? [...topData, { value: `Others (${otherData.length})`, count: otherCount }] : topData; }; -export const DatePlot: React.FC = ({ data, description }) => { +export const DatePlot: React.FC = ({ data, description, barColor }) => { const dateFormat = getDateFormatFromData(data); const groupedData = groupByDateFormat(data, dateFormat); - const graphData = getGraphData(groupedData); + const graphData = getGraphData(groupedData, barColor); const graphTimeProperties = getGraphTimeProperties(dateFormat); return ( @@ -101,14 +101,14 @@ export const DatePlot: React.FC = ({ data, description }) => { ); }; -export const CountriesPlot: React.FC = ({ data, description }) => { +export const CountriesPlot: React.FC = ({ data, description, barColor }) => { const groupedData = groupRemainingPoints(data, 10); - const graphData = getGraphData(groupedData); + const graphData = getGraphData(groupedData, barColor); return ; }; -export const UseTermsPlot: React.FC = ({ data, description }) => { - const graphData = getGraphData(data); +export const UseTermsPlot: React.FC = ({ data, description, barColor }) => { + const graphData = getGraphData(data, barColor); return ; }; diff --git a/website/src/pages/seqsets/[seqSetId].[version].astro b/website/src/pages/seqsets/[seqSetId].[version].astro index fbd8659dd3..95a54345c0 100644 --- a/website/src/pages/seqsets/[seqSetId].[version].astro +++ b/website/src/pages/seqsets/[seqSetId].[version].astro @@ -1,4 +1,5 @@ --- +import { mainTailwindColor } from '../../colors'; import { ErrorFeedback } from '../../components/ErrorFeedback'; import { SeqSetItem } from '../../components/SeqSetCitations/SeqSetItem'; import { SeqSetItemActions } from '../../components/SeqSetCitations/SeqSetItemActions'; @@ -59,6 +60,8 @@ const seqSetAccessionVersion = `${seqSetId}.${version}`; const authorResponse = seqSet !== undefined ? await seqSetClient.getAuthor(seqSet.createdBy) : undefined; const author = authorResponse?.isOk() ? authorResponse.value : undefined; + +const barGraphColor = mainTailwindColor[500]; --- ) } -
- { - seqSet !== undefined ? ( -
-
- {seqSetRecordsResponse.isOk() ? ( - - ) : ( - - )} - {seqSetRecordsResponse.isOk() && - seqSetCitedByResponse.isOk() && - seqSetCollectionDatesResponse.isOk() && - seqSetCollectionCountriesResponse.isOk() && - seqSetDataUseTermsResponse.isOk() ? ( - - ) : ( - - )} -
-
- ) : ( - - ) - } -
+ { + seqSet !== undefined ? ( +
+ {seqSetRecordsResponse.isOk() ? ( + + ) : ( + + )} + {seqSetRecordsResponse.isOk() && + seqSetCitedByResponse.isOk() && + seqSetCollectionDatesResponse.isOk() && + seqSetCollectionCountriesResponse.isOk() && + seqSetDataUseTermsResponse.isOk() ? ( + + ) : ( + + )} +
+ ) : ( + + ) + }
diff --git a/website/src/types/seqSetCitation.ts b/website/src/types/seqSetCitation.ts index 691237928e..dc5447ccc7 100644 --- a/website/src/types/seqSetCitation.ts +++ b/website/src/types/seqSetCitation.ts @@ -38,5 +38,3 @@ export const authorProfile = z.object({ university: z.string().nullish(), }); export type AuthorProfile = z.infer; - -export const SEQSET_GRAPHS_COLOUR = '#88a1d2'; From dbb6f949a45bfa93f6263264a5ab7328c1af53b9 Mon Sep 17 00:00:00 2001 From: tombch Date: Thu, 26 Mar 2026 12:07:51 +0100 Subject: [PATCH 18/24] fixed import of colors.ts by inlining the colors.cjs values at build time --- website/astro.config.mjs | 15 ++++++++++++++- website/src/colors.ts | 7 +------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 55d1c15744..73a3837b1d 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -4,6 +4,7 @@ import tailwind from '@astrojs/tailwind'; import Icons from 'unplugin-icons/vite'; import react from '@astrojs/react'; import mdx from '@astrojs/mdx'; +import { createRequire } from 'module'; // https://astro.build/config export default defineConfig({ @@ -20,6 +21,18 @@ export default defineConfig({ optimizeDeps: { exclude: ['fsevents', 'msw/node', 'msw', 'chromium-bidi'], }, - plugins: [Icons({ compiler: 'jsx', jsx: 'react' })], + plugins: [ + Icons({ compiler: 'jsx', jsx: 'react' }), + { + name: 'inline-colors-cjs', + load(id) { + if (id.endsWith('colors.cjs')) { + const require = createRequire(import.meta.url); + const { mainTailwindColor } = require('./colors.cjs'); + return `export const mainTailwindColor = ${JSON.stringify(mainTailwindColor)};`; + } + }, + }, + ], }, }); diff --git a/website/src/colors.ts b/website/src/colors.ts index 07e89c3828..9f9d468072 100644 --- a/website/src/colors.ts +++ b/website/src/colors.ts @@ -1,8 +1,3 @@ -import { createRequire } from 'module'; - -const require = createRequire(import.meta.url); -const { mainTailwindColor } = require('../colors.cjs') as { - mainTailwindColor: Record; -}; +import { mainTailwindColor } from '../colors.cjs'; export { mainTailwindColor }; From 701736f055cd900765620b3b56cc5ea4024eaa02 Mon Sep 17 00:00:00 2001 From: tombch Date: Thu, 26 Mar 2026 13:58:50 +0100 Subject: [PATCH 19/24] Updating integration tests --- integration-tests/tests/pages/seqset.page.ts | 26 ++++++++++++++----- .../specs/features/seqsets.dependent.spec.ts | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/integration-tests/tests/pages/seqset.page.ts b/integration-tests/tests/pages/seqset.page.ts index f8f8e06d4f..0ad061e30e 100644 --- a/integration-tests/tests/pages/seqset.page.ts +++ b/integration-tests/tests/pages/seqset.page.ts @@ -35,7 +35,7 @@ export class SeqSetPage { async createSeqSet(input: SeqSetFormInput) { await this.openCreateDialog(); await this.fillSeqSetForm(input); - await this.submitSeqSetForm(input.name); + await this.submitSeqSetForm(); } async fillSeqSetForm(input: SeqSetFormInput) { @@ -58,18 +58,23 @@ export class SeqSetPage { } } - async submitSeqSetForm(expectedName: string) { + async submitSeqSetForm() { await this.page.getByRole('button', { name: 'Save' }).click(); - await expect(this.getHeading(expectedName)).toBeVisible(); } - async expectDetailLayout(name: string) { - await expect(this.getHeading(name)).toBeVisible(); + async expectDetailLayout(name: string, description: string) { + await this.expectAccessionMatchesUrl(); await expect(this.page.getByRole('button', { name: 'Export' })).toBeVisible(); await expect(this.page.getByRole('button', { name: 'Edit' })).toBeVisible(); await expect(this.page.getByRole('button', { name: 'Delete' })).toBeVisible(); - await expect(this.page.getByText('Created date')).toBeVisible(); + await expect(this.page.getByText('Name', { exact: true })).toBeVisible(); + await expect(this.page.getByText(name, { exact: true })).toBeVisible(); + await expect(this.page.getByText('Description', { exact: true })).toBeVisible(); + await expect(this.page.getByText(description, { exact: true })).toBeVisible(); await expect(this.page.getByText('Version', { exact: true })).toBeVisible(); + await expect(this.page.getByText('Created by', { exact: true })).toBeVisible(); + await expect(this.page.getByText('Created date', { exact: true })).toBeVisible(); + await expect(this.page.getByText('Size', { exact: true })).toBeVisible(); await expect(this.page.getByText('Accession', { exact: true })).toBeVisible(); } @@ -111,12 +116,19 @@ export class SeqSetPage { await this.page.waitForLoadState('networkidle'); } + async expectAccessionMatchesUrl() { + const url = this.page.url(); + const accession = url.split('/seqsets/')[1]; + await expect(this.getHeading(accession)).toBeVisible(); + } + async editSeqSetName(newName: string) { await this.page.getByRole('button', { name: 'Edit' }).click(); const nameField = this.page.locator('#seqSet-name'); await nameField.waitFor({ state: 'visible' }); await nameField.fill(newName); await this.page.getByRole('button', { name: 'Save' }).click(); - await expect(this.getHeading(newName)).toBeVisible(); + await this.expectAccessionMatchesUrl(); + await expect(this.page.getByText(newName, { exact: true })).toBeVisible(); } } diff --git a/integration-tests/tests/specs/features/seqsets.dependent.spec.ts b/integration-tests/tests/specs/features/seqsets.dependent.spec.ts index 21196207aa..f4a9b4c7bd 100644 --- a/integration-tests/tests/specs/features/seqsets.dependent.spec.ts +++ b/integration-tests/tests/specs/features/seqsets.dependent.spec.ts @@ -61,7 +61,7 @@ test.describe('SeqSet management', () => { backgroundAccessions: [backgroundAccession], }); - await seqSetPage.expectDetailLayout(seqSetName); + await seqSetPage.expectDetailLayout(seqSetName, seqSetDescription); const jsonDownload = await seqSetPage.exportSeqSet('json'); expect(jsonDownload.suggestedFilename()).toContain(seqSetName); From 60609512cac6a87c09e1d6ed7daace3851becdac Mon Sep 17 00:00:00 2001 From: tombch Date: Thu, 26 Mar 2026 14:24:34 +0100 Subject: [PATCH 20/24] Fixing integration tests --- integration-tests/tests/pages/seqset.page.ts | 2 +- website/src/components/SeqSetCitations/SeqSetItem.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/integration-tests/tests/pages/seqset.page.ts b/integration-tests/tests/pages/seqset.page.ts index 0ad061e30e..42696a184e 100644 --- a/integration-tests/tests/pages/seqset.page.ts +++ b/integration-tests/tests/pages/seqset.page.ts @@ -22,7 +22,7 @@ export class SeqSetPage { } getHeading(name: string) { - return this.page.getByRole('heading', { name }); + return this.page.getByRole('heading').getByText(name, { exact: true }); } async openCreateDialog() { diff --git a/website/src/components/SeqSetCitations/SeqSetItem.tsx b/website/src/components/SeqSetCitations/SeqSetItem.tsx index 5076316cc9..5b3e55c687 100644 --- a/website/src/components/SeqSetCitations/SeqSetItem.tsx +++ b/website/src/components/SeqSetCitations/SeqSetItem.tsx @@ -22,7 +22,7 @@ const logger = getClientLogger('SeqSetItem'); const SeqSetSection: FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => (
-

{title}

+

{title}

{children}
@@ -33,7 +33,7 @@ const SeqSetSectionSeparator: FC = () =>
-

{title}

+

{title}

{children}
From c4e1d693609636412f2ad8b0d0807a529314c654 Mon Sep 17 00:00:00 2001 From: theosanderson-agent Date: Thu, 26 Mar 2026 16:24:29 +0000 Subject: [PATCH 21/24] Use colors.json instead of CJS+Vite plugin for color imports (#6199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Replaces `colors.cjs` with `colors.json`, which is natively importable by both `require()` (Tailwind config) and ESM `import` (Astro/Vite) - Removes the custom `inline-colors-cjs` Vite plugin and `createRequire` workaround from `astro.config.mjs` - Removes the `src/colors.ts` re-export wrapper (no longer needed) - Updates `.dockerignore` allowlist accordingly Net result: -20 lines of bridging code, same behavior. ## Test plan - [ ] Verify Tailwind classes using `primary-*` colors still work (the JSON is the same palette) - [ ] Verify the SeqSet detail page bar charts still pick up the correct color - [ ] Verify Docker build still includes the color config 🤖 Generated with [Claude Code](https://claude.com/claude-code) 🚀 Preview: Add `preview` label to enable Co-authored-by: theosanderson-agent Co-authored-by: Claude Opus 4.6 (1M context) --- website/.dockerignore | 2 +- website/astro.config.mjs | 15 +-------------- website/colors.cjs | 18 ------------------ website/colors.json | 16 ++++++++++++++++ website/src/colors.ts | 3 --- .../pages/seqsets/[seqSetId].[version].astro | 2 +- website/tailwind.config.cjs | 4 +--- 7 files changed, 20 insertions(+), 40 deletions(-) delete mode 100644 website/colors.cjs create mode 100644 website/colors.json delete mode 100644 website/src/colors.ts diff --git a/website/.dockerignore b/website/.dockerignore index 46923f3cb4..0dbc2d9919 100644 --- a/website/.dockerignore +++ b/website/.dockerignore @@ -6,6 +6,6 @@ src/**/*.spec.ts* !package-lock.json !astro.config.mjs !tailwind.config.cjs -!colors.cjs +!colors.json !tsconfig.json !.env.docker diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 73a3837b1d..55d1c15744 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -4,7 +4,6 @@ import tailwind from '@astrojs/tailwind'; import Icons from 'unplugin-icons/vite'; import react from '@astrojs/react'; import mdx from '@astrojs/mdx'; -import { createRequire } from 'module'; // https://astro.build/config export default defineConfig({ @@ -21,18 +20,6 @@ export default defineConfig({ optimizeDeps: { exclude: ['fsevents', 'msw/node', 'msw', 'chromium-bidi'], }, - plugins: [ - Icons({ compiler: 'jsx', jsx: 'react' }), - { - name: 'inline-colors-cjs', - load(id) { - if (id.endsWith('colors.cjs')) { - const require = createRequire(import.meta.url); - const { mainTailwindColor } = require('./colors.cjs'); - return `export const mainTailwindColor = ${JSON.stringify(mainTailwindColor)};`; - } - }, - }, - ], + plugins: [Icons({ compiler: 'jsx', jsx: 'react' })], }, }); diff --git a/website/colors.cjs b/website/colors.cjs deleted file mode 100644 index f1afb1d865..0000000000 --- a/website/colors.cjs +++ /dev/null @@ -1,18 +0,0 @@ -const mainTailwindColor = { - 50: '#f3f6fb', - 100: '#e4e9f5', - 200: '#cfdaee', - 300: '#aec1e2', - 400: '#88a1d2', - 500: '#6b84c6', - 600: '#586bb8', - 700: '#4d5ba8', - 800: '#3e467e', - 900: '#3a416e', - 950: '#272b44', - 1500: '#25396e', -}; - -module.exports = { - mainTailwindColor, -}; diff --git a/website/colors.json b/website/colors.json new file mode 100644 index 0000000000..26fad9f25e --- /dev/null +++ b/website/colors.json @@ -0,0 +1,16 @@ +{ + "mainTailwindColor": { + "50": "#f3f6fb", + "100": "#e4e9f5", + "200": "#cfdaee", + "300": "#aec1e2", + "400": "#88a1d2", + "500": "#6b84c6", + "600": "#586bb8", + "700": "#4d5ba8", + "800": "#3e467e", + "900": "#3a416e", + "950": "#272b44", + "1500": "#25396e" + } +} diff --git a/website/src/colors.ts b/website/src/colors.ts deleted file mode 100644 index 9f9d468072..0000000000 --- a/website/src/colors.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { mainTailwindColor } from '../colors.cjs'; - -export { mainTailwindColor }; diff --git a/website/src/pages/seqsets/[seqSetId].[version].astro b/website/src/pages/seqsets/[seqSetId].[version].astro index 95a54345c0..44970475db 100644 --- a/website/src/pages/seqsets/[seqSetId].[version].astro +++ b/website/src/pages/seqsets/[seqSetId].[version].astro @@ -1,5 +1,5 @@ --- -import { mainTailwindColor } from '../../colors'; +import { mainTailwindColor } from '../../../colors.json'; import { ErrorFeedback } from '../../components/ErrorFeedback'; import { SeqSetItem } from '../../components/SeqSetCitations/SeqSetItem'; import { SeqSetItemActions } from '../../components/SeqSetCitations/SeqSetItemActions'; diff --git a/website/tailwind.config.cjs b/website/tailwind.config.cjs index f8b47c9e58..0103c8aa9d 100644 --- a/website/tailwind.config.cjs +++ b/website/tailwind.config.cjs @@ -1,10 +1,8 @@ /** @type {import('tailwindcss').Config} */ -const colors = require('./colors.cjs'); +const { mainTailwindColor } = require('./colors.json'); const flowbite = require('flowbite-react/tailwind'); -const mainTailwindColor = colors.mainTailwindColor; - module.exports = { content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}', flowbite.content()], theme: { From 2a9db6f6366313329a520eb50bf308afd8868cba Mon Sep 17 00:00:00 2001 From: tombch Date: Thu, 26 Mar 2026 18:09:15 +0100 Subject: [PATCH 22/24] Loading fallback for barplot --- website/src/components/common/BarPlot.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/website/src/components/common/BarPlot.tsx b/website/src/components/common/BarPlot.tsx index 329e08b841..36ce4900a2 100644 --- a/website/src/components/common/BarPlot.tsx +++ b/website/src/components/common/BarPlot.tsx @@ -27,7 +27,12 @@ export const BarPlot: FC = ({ data, options, description }) => { setIsRegistered(true); }, []); - if (!isRegistered) return null; + if (!isRegistered) + return ( +
+
+
+ ); return (
From 464fda7b1b0fd1dd98d8061bb27cdb489ec96d04 Mon Sep 17 00:00:00 2001 From: tombch Date: Thu, 26 Mar 2026 18:32:03 +0100 Subject: [PATCH 23/24] Fixing up error handling --- .../components/SeqSetCitations/SeqSetItem.tsx | 16 +- .../SeqSetCitations/getSeqSetStatistics.ts | 10 +- .../pages/seqsets/[seqSetId].[version].astro | 159 +++++++++--------- 3 files changed, 90 insertions(+), 95 deletions(-) diff --git a/website/src/components/SeqSetCitations/SeqSetItem.tsx b/website/src/components/SeqSetCitations/SeqSetItem.tsx index 5b3e55c687..a541ce3939 100644 --- a/website/src/components/SeqSetCitations/SeqSetItem.tsx +++ b/website/src/components/SeqSetCitations/SeqSetItem.tsx @@ -33,7 +33,7 @@ const SeqSetSectionSeparator: FC = () =>
-

{title}

+

{title}

{children}
@@ -148,11 +148,15 @@ const SeqSetItemInner: FC = ({ + seqSetAuthor ? ( + + ) : ( + 'Unknown' + ) } /> diff --git a/website/src/components/SeqSetCitations/getSeqSetStatistics.ts b/website/src/components/SeqSetCitations/getSeqSetStatistics.ts index 092e9796c4..3b3ef24909 100644 --- a/website/src/components/SeqSetCitations/getSeqSetStatistics.ts +++ b/website/src/components/SeqSetCitations/getSeqSetStatistics.ts @@ -36,7 +36,7 @@ export const getSeqSetStatistics = async ( } const organisms = getConfiguredOrganisms(); - const aggregates = await Promise.all( + const aggregateResponses = await Promise.all( organisms.map((organism) => { const client = LapisClient.createForOrganism(organism.key); const schema = getSchema(organism.key); @@ -47,11 +47,11 @@ export const getSeqSetStatistics = async ( ); const crossAggregate = new Map(); - for (const aggregate of aggregates) { - if (aggregate.isErr()) continue; + for (const aggregateResponse of aggregateResponses) { + if (aggregateResponse.isErr()) return aggregateResponse; - for (const item of aggregate.value) { - crossAggregate.set(item.value, (crossAggregate.get(item.value) ?? 0) + item.count); + for (const aggregateRow of aggregateResponse.value) { + crossAggregate.set(aggregateRow.value, (crossAggregate.get(aggregateRow.value) ?? 0) + aggregateRow.count); } } diff --git a/website/src/pages/seqsets/[seqSetId].[version].astro b/website/src/pages/seqsets/[seqSetId].[version].astro index 44970475db..610edba55b 100644 --- a/website/src/pages/seqsets/[seqSetId].[version].astro +++ b/website/src/pages/seqsets/[seqSetId].[version].astro @@ -1,4 +1,6 @@ --- +import { err, type Result } from 'neverthrow'; + import { mainTailwindColor } from '../../../colors.json'; import { ErrorFeedback } from '../../components/ErrorFeedback'; import { SeqSetItem } from '../../components/SeqSetCitations/SeqSetItem'; @@ -8,6 +10,7 @@ import { getRuntimeConfig, seqSetsAreEnabled, getWebsiteConfig } from '../../con import BaseLayout from '../../layouts/BaseLayout.astro'; import { SeqSetCitationClient } from '../../services/seqSetCitationClient.ts'; import { DATA_USE_TERMS_FIELD } from '../../settings'; +import type { ProblemDetail } from '../../types/backend'; import type { SeqSet } from '../../types/seqSetCitation'; import { getAccessToken } from '../../utils/getAccessToken'; @@ -30,20 +33,9 @@ if (!seqSetsAreEnabled()) { const seqSetClient = SeqSetCitationClient.create(); -const [seqSetResponse, seqSetRecordsResponse, seqSetCitedByResponse] = await Promise.all([ - seqSetClient.call('getSeqSet', { params: { seqSetId, version } }), - seqSetClient.call('getSeqSetRecords', { params: { seqSetId, version } }), - seqSetClient.call('getSeqSetCitedBy', { params: { seqSetId, version } }), -]); - -const seqSetAccessions = seqSetRecordsResponse.isOk() ? seqSetRecordsResponse.value.map((r) => r.accession) : []; - -const [seqSetCollectionDatesResponse, seqSetCollectionCountriesResponse, seqSetDataUseTermsResponse] = - await Promise.all([ - getSeqSetStatistics(seqSetAccessions, ['sampleCollectionDate', 'date']), - getSeqSetStatistics(seqSetAccessions, ['geoLocCountry', 'country']), - getSeqSetStatistics(seqSetAccessions, [DATA_USE_TERMS_FIELD]), - ]); +const seqSetVersionsResponse = await seqSetClient.call('getSeqSet', { params: { seqSetId, version } }); +const seqSetRecordsResponse = await seqSetClient.call('getSeqSetRecords', { params: { seqSetId, version } }); +const seqSetCitedByResponse = await seqSetClient.call('getSeqSetCitedBy', { params: { seqSetId, version } }); const getSeqSetByVersion = (seqSetVersions: SeqSet[], version: string) => { const matchedVersion = seqSetVersions.find((obj) => { @@ -55,13 +47,41 @@ const getSeqSetByVersion = (seqSetVersions: SeqSet[], version: string) => { return matchedVersion; }; -const seqSet = seqSetResponse.isOk() ? getSeqSetByVersion(seqSetResponse.value, version) : undefined; -const seqSetAccessionVersion = `${seqSetId}.${version}`; +const seqSetResponse = seqSetVersionsResponse.map((seqSetVersions) => getSeqSetByVersion(seqSetVersions, version)); +const seqSetAuthorResponse = seqSetResponse.isOk() + ? await seqSetClient.getAuthor(seqSetResponse.value.createdBy) + : err(); -const authorResponse = seqSet !== undefined ? await seqSetClient.getAuthor(seqSet.createdBy) : undefined; -const author = authorResponse?.isOk() ? authorResponse.value : undefined; +const seqSetAccessions = seqSetRecordsResponse.unwrapOr([]).map((record) => record.accession); +const seqSetCollectionDatesResponse = seqSetRecordsResponse.isOk() + ? await getSeqSetStatistics(seqSetAccessions, ['sampleCollectionDate', 'date']) + : err(); +const seqSetCollectionCountriesResponse = seqSetRecordsResponse.isOk() + ? await getSeqSetStatistics(seqSetAccessions, ['geoLocCountry', 'country']) + : err(); +const seqSetDataUseTermsResponse = seqSetRecordsResponse.isOk() + ? await getSeqSetStatistics(seqSetAccessions, [DATA_USE_TERMS_FIELD]) + : err(); +const seqSetAuthor = seqSetAuthorResponse.unwrapOr(undefined); +const citedByData = seqSetCitedByResponse.unwrapOr({ years: [], citations: [] }); + +const collectionDatesData = seqSetCollectionDatesResponse.unwrapOr([]); +const collectionCountriesData = seqSetCollectionCountriesResponse.unwrapOr([]); +const dataUseTermsData = seqSetDataUseTermsResponse.unwrapOr([]); + +const seqSetAccessionVersion = `${seqSetId}.${version}`; const barGraphColor = mainTailwindColor[500]; + +const secondaryErrors = ( + [ + [seqSetAuthorResponse, 'Error while fetching author profile'], + [seqSetCitedByResponse, 'Error while fetching seqSet citations'], + [seqSetCollectionCountriesResponse, 'Error while fetching seqSet statistics'], + [seqSetCollectionDatesResponse, 'Error while fetching seqSet statistics'], + [seqSetDataUseTermsResponse, 'Error while fetching seqSet statistics'], + ] as [Result, string][] +).flatMap(([response, message]) => (response.isErr() ? [`${message}: ${JSON.stringify(response.error)}`] : [])); --- { - !authorResponse?.isOk() && ( - - ) - } - { - seqSet !== undefined ? ( + seqSetResponse.isOk() && seqSetRecordsResponse.isOk() ? (
- {seqSetRecordsResponse.isOk() ? ( - - ) : ( - - )} - {seqSetRecordsResponse.isOk() && - seqSetCitedByResponse.isOk() && - seqSetCollectionDatesResponse.isOk() && - seqSetCollectionCountriesResponse.isOk() && - seqSetDataUseTermsResponse.isOk() ? ( - - ) : ( - + {secondaryErrors.length > 0 && ( + )} + +
) : ( From a49e07fe31a8e8ab5070877d1df2b51f8769efac Mon Sep 17 00:00:00 2001 From: tombch Date: Thu, 26 Mar 2026 18:34:55 +0100 Subject: [PATCH 24/24] Fixing integration tests --- integration-tests/tests/pages/seqset.page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/tests/pages/seqset.page.ts b/integration-tests/tests/pages/seqset.page.ts index 42696a184e..9c915b015f 100644 --- a/integration-tests/tests/pages/seqset.page.ts +++ b/integration-tests/tests/pages/seqset.page.ts @@ -22,7 +22,7 @@ export class SeqSetPage { } getHeading(name: string) { - return this.page.getByRole('heading').getByText(name, { exact: true }); + return this.page.getByRole('heading').filter({ hasText: name }); } async openCreateDialog() {
AccessionOrganismContext
- {fieldConfig.displayName} -
+ {seqSetRecord.accession} - + - + + {seqSetRecord.isFocal ? 'Focal' : 'Background'} - + ))}