diff --git a/website/src/components/SequenceDetailsPage/getTableData.spec.ts b/website/src/components/SequenceDetailsPage/getTableData.spec.ts index 3af7c0f542..80254b6933 100644 --- a/website/src/components/SequenceDetailsPage/getTableData.spec.ts +++ b/website/src/components/SequenceDetailsPage/getTableData.spec.ts @@ -156,22 +156,22 @@ describe('getTableData', () => { expect(data).toContainEqual({ label: 'Nucleotide substitutions', name: 'nucleotideSubstitutions', - value: 'nucleotideMutation1, nucleotideMutation2', + value: 'T10A, C30G', }); expect(data).toContainEqual({ label: 'Nucleotide deletions', name: 'nucleotideDeletions', - value: 'nucleotideDeletion1-, nucleotideDeletion2-', + value: '20, 21, 40-42', }); expect(data).toContainEqual({ label: 'Amino acid substitutions', name: 'aminoAcidSubstitutions', - value: 'aminoAcidMutation1, aminoAcidMutation2', + value: 'gene1:N10Y, gene1:T30N', }); expect(data).toContainEqual({ label: 'Amino acid deletions', name: 'aminoAcidDeletions', - value: 'aminoAcidDeletion1-, aminoAcidDeletion2-', + value: 'gene1:20-23, gene1:40', }); }); @@ -209,16 +209,22 @@ describe('getTableData', () => { }); const nucleotideMutations = [ - { count: 0, proportion: 0, mutation: 'nucleotideMutation1' }, - { count: 0, proportion: 0, mutation: 'nucleotideDeletion1-' }, - { count: 0, proportion: 0, mutation: 'nucleotideMutation2' }, - { count: 0, proportion: 0, mutation: 'nucleotideDeletion2-' }, + { count: 0, proportion: 0, mutation: 'T10A' }, + { count: 0, proportion: 0, mutation: 'A20-' }, + { count: 0, proportion: 0, mutation: 'A21-' }, + { count: 0, proportion: 0, mutation: 'C30G' }, + { count: 0, proportion: 0, mutation: 'G40-' }, + { count: 0, proportion: 0, mutation: 'C41-' }, + { count: 0, proportion: 0, mutation: 'T42-' }, ]; const aminoAcidMutations = [ - { count: 0, proportion: 0, mutation: 'aminoAcidMutation1' }, - { count: 0, proportion: 0, mutation: 'aminoAcidDeletion1-' }, - { count: 0, proportion: 0, mutation: 'aminoAcidMutation2' }, - { count: 0, proportion: 0, mutation: 'aminoAcidDeletion2-' }, + { count: 0, proportion: 0, mutation: 'gene1:N10Y' }, + { count: 0, proportion: 0, mutation: 'gene1:R20-' }, + { count: 0, proportion: 0, mutation: 'gene1:R21-' }, + { count: 0, proportion: 0, mutation: 'gene1:N22-' }, + { count: 0, proportion: 0, mutation: 'gene1:P23-' }, + { count: 0, proportion: 0, mutation: 'gene1:T30N' }, + { count: 0, proportion: 0, mutation: 'gene1:F40-' }, ]; const nucleotideInsertions = [ diff --git a/website/src/components/SequenceDetailsPage/getTableData.ts b/website/src/components/SequenceDetailsPage/getTableData.ts index 9a8c81c750..e3b1f69c41 100644 --- a/website/src/components/SequenceDetailsPage/getTableData.ts +++ b/website/src/components/SequenceDetailsPage/getTableData.ts @@ -106,12 +106,12 @@ function toTableData(config: Schema) { { label: 'Nucleotide substitutions', name: 'nucleotideSubstitutions', - value: mutationsToCommaSeparatedString(nucleotideMutations, (m) => !m.endsWith('-')), + value: substitutionsToCommaSeparatedString(nucleotideMutations), }, { label: 'Nucleotide deletions', name: 'nucleotideDeletions', - value: mutationsToCommaSeparatedString(nucleotideMutations, (m) => m.endsWith('-')), + value: deletionsToCommaSeparatedString(nucleotideMutations), }, { label: 'Nucleotide insertions', @@ -121,12 +121,12 @@ function toTableData(config: Schema) { { label: 'Amino acid substitutions', name: 'aminoAcidSubstitutions', - value: mutationsToCommaSeparatedString(aminoAcidMutations, (m) => !m.endsWith('-')), + value: substitutionsToCommaSeparatedString(aminoAcidMutations), }, { label: 'Amino acid deletions', name: 'aminoAcidDeletions', - value: mutationsToCommaSeparatedString(aminoAcidMutations, (m) => m.endsWith('-')), + value: deletionsToCommaSeparatedString(aminoAcidMutations), }, { label: 'Amino acid insertions', @@ -151,13 +151,62 @@ function mapValueToDisplayedValue(value: undefined | null | string | number, met return value; } -function mutationsToCommaSeparatedString( - mutationData: MutationProportionCount[], - filter: (mutation: string) => boolean, -) { +function substitutionsToCommaSeparatedString(mutationData: MutationProportionCount[]) { return mutationData .map((m) => m.mutation) - .filter(filter) + .filter((m) => !m.endsWith('-')) + .join(', '); +} + +function deletionsToCommaSeparatedString(mutationData: MutationProportionCount[]) { + const segmentPositions = new Map(); + mutationData + .filter((m) => m.mutation.endsWith('-')) + .forEach((m) => { + const parts = m.mutation.split(':'); + const [segment, mutation] = parts.length === 1 ? ([undefined, parts[0]] as const) : parts; + const position = Number.parseInt(mutation.slice(1, -1), 10); + if (!segmentPositions.has(segment)) { + segmentPositions.set(segment, []); + } + segmentPositions.get(segment)!.push(position); + }); + const segmentRanges = [...segmentPositions.entries()].map(([segment, positions]) => { + const sortedPositions = positions.sort(); + const ranges = []; + let rangeStart: number | null = null; + for (let i = 0; i < sortedPositions.length; i++) { + const current = sortedPositions[i]; + const next = sortedPositions[i + 1] as number | undefined; + if (rangeStart === null) { + rangeStart = current; + } + if (next === undefined || next !== current + 1) { + if (current - rangeStart >= 2) { + ranges.push(`${rangeStart}-${current}`); + } else { + ranges.push(rangeStart.toString()); + if (current !== rangeStart) { + ranges.push(current.toString()); + } + } + rangeStart = null; + } + } + return { segment, ranges }; + }); + segmentRanges.sort((a, b) => { + const safeA = a.segment ?? ''; + const safeB = b.segment ?? ''; + if (safeA <= safeB) { + return -1; + } else { + return 1; + } + }); + return segmentRanges + .map(({ segment, ranges }) => ranges.map((range) => `${segment !== undefined ? segment + ':' : ''}${range}`)) + .flat() .join(', '); }