From 069629182ba35decc5eb051fcd322ccbe3204580 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Thu, 7 May 2026 14:32:39 +0100 Subject: [PATCH 1/2] fix(components): show aliases as autocomplete options in lineage filter Previously, aliases were only used to generate prefix wildcard items (e.g. BA.3.2*) when the alias happened to be a prefix of another lineage name in the tree. This meant aliases that are not prefixes of anything (e.g. BA.3.2 when it has no children named BA.3.2.x) were silently omitted. The fix simplifies the logic: aliases are now always shown as direct autocomplete options (both exact and wildcard), deduplicated case-insensitively to avoid showing noise from lowercase variants. This also makes the previous findMissingPrefixMappings function redundant and removes it entirely. Co-Authored-By: Claude Sonnet 4.6 --- .../fetchLineageAutocompleteList.spec.ts | 74 ++++++++++++++++++- .../fetchLineageAutocompleteList.ts | 73 ++++++------------ 2 files changed, 96 insertions(+), 51 deletions(-) diff --git a/components/src/preact/lineageFilter/fetchLineageAutocompleteList.spec.ts b/components/src/preact/lineageFilter/fetchLineageAutocompleteList.spec.ts index 6dc82212..bef9696c 100644 --- a/components/src/preact/lineageFilter/fetchLineageAutocompleteList.spec.ts +++ b/components/src/preact/lineageFilter/fetchLineageAutocompleteList.spec.ts @@ -371,9 +371,31 @@ describe('fetchLineageAutocompleteList', () => { lineage: 'B.1.1.529.3.2*', count: 3, }, + // Long-form aliases of BA.3.2.1 and BA.3.2.2 + { + lineage: 'B.1.1.529.3.2.1', + count: 0, + }, + { + lineage: 'B.1.1.529.3.2.1*', + count: 1, + }, + { + lineage: 'B.1.1.529.3.2.2', + count: 0, + }, + { + lineage: 'B.1.1.529.3.2.2*', + count: 2, + }, + // BA.3.2 is the alias of B.1.1.529.3.2, shown as both exact and wildcard + { + lineage: 'BA.3.2', + count: 0, + }, { lineage: 'BA.3.2*', - count: 3, // Same as B.1.1.529.3.2* (includes .3.2 and .3.2.1) + count: 3, }, { lineage: 'BA.3.2.1', @@ -393,4 +415,54 @@ describe('fetchLineageAutocompleteList', () => { }, ]); }); + + test('should include alias as direct option when it is not a prefix of any other lineage', async () => { + lapisRequestMocks.aggregated( + { fields: [lineageField], ...lapisFilter }, + { + data: [ + { + [lineageField]: 'B.1.1.529.3.2', + count: 5, + }, + ], + }, + ); + + lapisRequestMocks.lineageDefinition( + { + 'B.1.1.529.3.2': { + aliases: ['b.1.1.529.3.2', 'BA.3.2', 'bA.3.2', 'Ba.3.2', 'ba.3.2'], + }, + }, + lineageField, + ); + + const result = await fetchLineageAutocompleteList({ + lapisUrl: DUMMY_LAPIS_URL, + lapisField: lineageField, + lapisFilter, + }); + + expect(result).to.deep.equal([ + { + lineage: 'B.1.1.529.3.2', + count: 5, + }, + { + lineage: 'B.1.1.529.3.2*', + count: 5, + }, + // BA.3.2 is shown as a direct alias option (case-insensitively deduplicated, + // so the lowercase variants like ba.3.2, Ba.3.2 etc. are not shown separately) + { + lineage: 'BA.3.2', + count: 0, + }, + { + lineage: 'BA.3.2*', + count: 5, + }, + ]); + }); }); diff --git a/components/src/preact/lineageFilter/fetchLineageAutocompleteList.ts b/components/src/preact/lineageFilter/fetchLineageAutocompleteList.ts index eba5b2ad..760a7fa5 100644 --- a/components/src/preact/lineageFilter/fetchLineageAutocompleteList.ts +++ b/components/src/preact/lineageFilter/fetchLineageAutocompleteList.ts @@ -5,8 +5,10 @@ import type { LapisFilter } from '../../types'; export type LineageItem = { lineage: string; count: number }; /** - * Generates the autocomplete list for lineage search. It includes lineages with wild cards - * (i.e. "BA.3.2.1" and "BA.3.2.1*") as well as all prefixes of lineages with an asterisk ("BA.3.2*"). + * Generates the autocomplete list for lineage search. It includes: + * - Each lineage from the definition, with and without a wildcard ("BA.3.2.1" and "BA.3.2.1*") + * - Each alias as a direct option with and without a wildcard ("BA.3.2" and "BA.3.2*"), + * deduplicated case-insensitively to avoid showing noise from lowercase variants */ export async function fetchLineageAutocompleteList({ lapisUrl, @@ -29,8 +31,6 @@ export async function fetchLineageAutocompleteList({ getLineageTreeAndAliases({ lapisUrl, lapisField, signal }), ]); - const prefixToLineage = findMissingPrefixMappings(lineageTree, aliasMapping); - // Combine actual lineages with their wildcard versions const actualLineageItems = Array.from(lineageTree.keys()).flatMap((lineage) => [ { @@ -43,14 +43,27 @@ export async function fetchLineageAutocompleteList({ }, ]); - // Add prefix alias items with wildcard and their counts - const prefixAliasItems = Array.from(prefixToLineage.entries()).map(([prefix, actualLineage]) => ({ - lineage: `${prefix}*`, - count: getCountsIncludingSublineages(actualLineage, lineageTree, countsByLineage), - })); + // Add alias items (exact and wildcard) for aliases that are meaningfully different from their + // canonical lineage. Deduplicated case-insensitively to avoid noise from case variants. + const seenAliasesUpper = new Set(Array.from(lineageTree.keys()).map((k) => k.toUpperCase())); + const aliasItems = Array.from(aliasMapping.entries()).flatMap(([canonicalLineage, aliases]) => + aliases.flatMap((alias): LineageItem[] => { + const aliasUpper = alias.toUpperCase(); + if (seenAliasesUpper.has(aliasUpper)) { + return []; + } + seenAliasesUpper.add(aliasUpper); + + const wildcardCount = getCountsIncludingSublineages(canonicalLineage, lineageTree, countsByLineage); + return [ + { lineage: alias, count: countsByLineage.get(alias) ?? 0 }, + { lineage: `${alias}*`, count: wildcardCount }, + ]; + }), + ); // Combine and sort all items (asterisk before period for same prefix) - return [...actualLineageItems, ...prefixAliasItems].sort((a, b) => { + return [...actualLineageItems, ...aliasItems].sort((a, b) => { // Replace * with a character that sorts before . in ASCII const aKey = a.lineage.replace(/\*/g, ' '); const bKey = b.lineage.replace(/\*/g, ' '); @@ -137,43 +150,3 @@ function getAllDescendants(lineage: string, lineageTree: Map Array.from(child))]); } -/** - * This function finds prefixes (i.e. "BA.3.2" for "BA.3.2.1") that are not in the lineageTree, - * but do appear as an alias. It returns a reverse mapping for those prefixes, back to a lineage - * that can be found in the lineageTree (i.e. "BA.3.2" -> "B.1.1.529.3.2"). - */ -function findMissingPrefixMappings( - lineageTree: Map, - aliasMapping: Map, -): Map { - const lineages = Array.from(lineageTree.keys()); - const lineagesSet = new Set(lineages); - - // Generate all prefixes for each lineage (e.g., "A.B.1" -> ["A", "A.B", "A.B.1"]) - const allPrefixes = lineages.flatMap((lineage) => { - const parts = lineage.split('.'); - return parts.map((_, i) => parts.slice(0, i + 1).join('.')); - }); - - // Find prefixes that are NOT in the actual lineages list - const missingPrefixes = new Set(allPrefixes.filter((prefix) => !lineagesSet.has(prefix))); - - // Create reverse alias mapping: alias -> original lineage - const reverseAliasMapping = new Map(); - aliasMapping.forEach((aliases, lineage) => { - aliases.forEach((alias) => { - reverseAliasMapping.set(alias, lineage); - }); - }); - - // Map missing prefixes to their actual lineage names via reverse alias lookup - const prefixToLineage = new Map(); - missingPrefixes.forEach((prefix) => { - const actualLineage = reverseAliasMapping.get(prefix); - if (actualLineage) { - prefixToLineage.set(prefix, actualLineage); - } - }); - - return prefixToLineage; -} From 0200fd8f790fbebcc70a7e71c96df88488cce0b3 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Thu, 7 May 2026 14:43:10 +0100 Subject: [PATCH 2/2] format --- .../src/preact/lineageFilter/fetchLineageAutocompleteList.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/components/src/preact/lineageFilter/fetchLineageAutocompleteList.ts b/components/src/preact/lineageFilter/fetchLineageAutocompleteList.ts index 760a7fa5..aa04a5a1 100644 --- a/components/src/preact/lineageFilter/fetchLineageAutocompleteList.ts +++ b/components/src/preact/lineageFilter/fetchLineageAutocompleteList.ts @@ -149,4 +149,3 @@ function getAllDescendants(lineage: string, lineageTree: Map Array.from(child))]); } -