@genspectrum/dashboard-components 1.9.0 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components.d.ts +18 -18
- package/dist/components.js +54 -19
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +18 -18
- package/package.json +1 -1
- package/src/preact/components/mutations-over-time-mutations-filter.stories.tsx +73 -14
- package/src/preact/components/mutations-over-time-mutations-filter.tsx +30 -28
- package/src/preact/lineageFilter/fetchLineageAutocompleteList.spec.ts +72 -0
- package/src/preact/lineageFilter/fetchLineageAutocompleteList.ts +81 -19
- package/standalone-bundle/dashboard-components.js +2581 -2559
- package/standalone-bundle/dashboard-components.js.map +1 -1
package/dist/util.d.ts
CHANGED
|
@@ -1005,7 +1005,7 @@ declare global {
|
|
|
1005
1005
|
|
|
1006
1006
|
declare global {
|
|
1007
1007
|
interface HTMLElementTagNameMap {
|
|
1008
|
-
'gs-
|
|
1008
|
+
'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
|
|
1009
1009
|
}
|
|
1010
1010
|
}
|
|
1011
1011
|
|
|
@@ -1013,7 +1013,7 @@ declare global {
|
|
|
1013
1013
|
declare global {
|
|
1014
1014
|
namespace JSX {
|
|
1015
1015
|
interface IntrinsicElements {
|
|
1016
|
-
'gs-
|
|
1016
|
+
'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1017
1017
|
}
|
|
1018
1018
|
}
|
|
1019
1019
|
}
|
|
@@ -1021,7 +1021,7 @@ declare global {
|
|
|
1021
1021
|
|
|
1022
1022
|
declare global {
|
|
1023
1023
|
interface HTMLElementTagNameMap {
|
|
1024
|
-
'gs-
|
|
1024
|
+
'gs-aggregate': AggregateComponent;
|
|
1025
1025
|
}
|
|
1026
1026
|
}
|
|
1027
1027
|
|
|
@@ -1029,7 +1029,7 @@ declare global {
|
|
|
1029
1029
|
declare global {
|
|
1030
1030
|
namespace JSX {
|
|
1031
1031
|
interface IntrinsicElements {
|
|
1032
|
-
'gs-
|
|
1032
|
+
'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1033
1033
|
}
|
|
1034
1034
|
}
|
|
1035
1035
|
}
|
|
@@ -1037,7 +1037,7 @@ declare global {
|
|
|
1037
1037
|
|
|
1038
1038
|
declare global {
|
|
1039
1039
|
interface HTMLElementTagNameMap {
|
|
1040
|
-
'gs-
|
|
1040
|
+
'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
|
|
1041
1041
|
}
|
|
1042
1042
|
}
|
|
1043
1043
|
|
|
@@ -1045,7 +1045,7 @@ declare global {
|
|
|
1045
1045
|
declare global {
|
|
1046
1046
|
namespace JSX {
|
|
1047
1047
|
interface IntrinsicElements {
|
|
1048
|
-
'gs-
|
|
1048
|
+
'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1049
1049
|
}
|
|
1050
1050
|
}
|
|
1051
1051
|
}
|
|
@@ -1053,7 +1053,7 @@ declare global {
|
|
|
1053
1053
|
|
|
1054
1054
|
declare global {
|
|
1055
1055
|
interface HTMLElementTagNameMap {
|
|
1056
|
-
'gs-
|
|
1056
|
+
'gs-mutations-over-time': MutationsOverTimeComponent;
|
|
1057
1057
|
}
|
|
1058
1058
|
}
|
|
1059
1059
|
|
|
@@ -1061,7 +1061,7 @@ declare global {
|
|
|
1061
1061
|
declare global {
|
|
1062
1062
|
namespace JSX {
|
|
1063
1063
|
interface IntrinsicElements {
|
|
1064
|
-
'gs-
|
|
1064
|
+
'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1065
1065
|
}
|
|
1066
1066
|
}
|
|
1067
1067
|
}
|
|
@@ -1069,7 +1069,7 @@ declare global {
|
|
|
1069
1069
|
|
|
1070
1070
|
declare global {
|
|
1071
1071
|
interface HTMLElementTagNameMap {
|
|
1072
|
-
'gs-
|
|
1072
|
+
'gs-sequences-by-location': SequencesByLocationComponent;
|
|
1073
1073
|
}
|
|
1074
1074
|
}
|
|
1075
1075
|
|
|
@@ -1077,7 +1077,7 @@ declare global {
|
|
|
1077
1077
|
declare global {
|
|
1078
1078
|
namespace JSX {
|
|
1079
1079
|
interface IntrinsicElements {
|
|
1080
|
-
'gs-
|
|
1080
|
+
'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1081
1081
|
}
|
|
1082
1082
|
}
|
|
1083
1083
|
}
|
|
@@ -1101,11 +1101,7 @@ declare global {
|
|
|
1101
1101
|
|
|
1102
1102
|
declare global {
|
|
1103
1103
|
interface HTMLElementTagNameMap {
|
|
1104
|
-
'gs-
|
|
1105
|
-
}
|
|
1106
|
-
interface HTMLElementEventMap {
|
|
1107
|
-
[gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
|
|
1108
|
-
[gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
|
|
1104
|
+
'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
|
|
1109
1105
|
}
|
|
1110
1106
|
}
|
|
1111
1107
|
|
|
@@ -1113,7 +1109,7 @@ declare global {
|
|
|
1113
1109
|
declare global {
|
|
1114
1110
|
namespace JSX {
|
|
1115
1111
|
interface IntrinsicElements {
|
|
1116
|
-
'gs-
|
|
1112
|
+
'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1117
1113
|
}
|
|
1118
1114
|
}
|
|
1119
1115
|
}
|
|
@@ -1121,7 +1117,11 @@ declare global {
|
|
|
1121
1117
|
|
|
1122
1118
|
declare global {
|
|
1123
1119
|
interface HTMLElementTagNameMap {
|
|
1124
|
-
'gs-
|
|
1120
|
+
'gs-date-range-filter': DateRangeFilterComponent;
|
|
1121
|
+
}
|
|
1122
|
+
interface HTMLElementEventMap {
|
|
1123
|
+
[gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
|
|
1124
|
+
[gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
|
|
1125
1125
|
}
|
|
1126
1126
|
}
|
|
1127
1127
|
|
|
@@ -1129,7 +1129,7 @@ declare global {
|
|
|
1129
1129
|
declare global {
|
|
1130
1130
|
namespace JSX {
|
|
1131
1131
|
interface IntrinsicElements {
|
|
1132
|
-
'gs-
|
|
1132
|
+
'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1133
1133
|
}
|
|
1134
1134
|
}
|
|
1135
1135
|
}
|
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
MutationsOverTimeMutationsFilter,
|
|
8
8
|
type MutationsOverTimeMutationsFilterProps,
|
|
9
9
|
} from './mutations-over-time-mutations-filter';
|
|
10
|
+
import { type MutationAnnotations } from '../../web-components/mutation-annotations-context';
|
|
10
11
|
import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
|
|
11
12
|
import { type MutationFilter } from '../mutationsOverTime/getFilteredMutationsOverTimeData';
|
|
12
13
|
|
|
@@ -18,30 +19,38 @@ const meta: Meta = {
|
|
|
18
19
|
|
|
19
20
|
export default meta;
|
|
20
21
|
|
|
22
|
+
const manyMutationAnnotations = Array.from({ length: 300 }, (_, i) => ({
|
|
23
|
+
name: `Annotation ${i + 1}`,
|
|
24
|
+
description: `This is test annotation number ${i + 1} for testing many annotations.`,
|
|
25
|
+
symbol: String.fromCharCode(33 + (i % 94)), // Cycle through printable ASCII characters
|
|
26
|
+
nucleotideMutations: ['A23G'],
|
|
27
|
+
aminoAcidMutations: [],
|
|
28
|
+
})) satisfies MutationAnnotations;
|
|
29
|
+
|
|
21
30
|
const WrapperWithState = ({
|
|
22
31
|
setFilterValue,
|
|
23
32
|
value,
|
|
33
|
+
annotations = [
|
|
34
|
+
{
|
|
35
|
+
name: 'Test Annotation 1',
|
|
36
|
+
description: 'Test Annotation 1',
|
|
37
|
+
symbol: '#',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Test Annotation 2',
|
|
41
|
+
description: 'Test Annotation 2',
|
|
42
|
+
symbol: '+',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
24
45
|
}: {
|
|
25
46
|
setFilterValue: Dispatch<StateUpdater<MutationFilter>>;
|
|
26
47
|
value: MutationFilter;
|
|
48
|
+
annotations?: MutationAnnotations;
|
|
27
49
|
}) => {
|
|
28
50
|
const [state, setState] = useState(value);
|
|
29
51
|
|
|
30
52
|
return (
|
|
31
|
-
<MutationAnnotationsContextProvider
|
|
32
|
-
value={[
|
|
33
|
-
{
|
|
34
|
-
name: 'Test Annotation 1',
|
|
35
|
-
description: 'Test Annotation 1',
|
|
36
|
-
symbol: '#',
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: 'Test Annotation 2',
|
|
40
|
-
description: 'Test Annotation 2',
|
|
41
|
-
symbol: '+',
|
|
42
|
-
},
|
|
43
|
-
]}
|
|
44
|
-
>
|
|
53
|
+
<MutationAnnotationsContextProvider value={annotations}>
|
|
45
54
|
<MutationsOverTimeMutationsFilter
|
|
46
55
|
setFilterValue={(value) => {
|
|
47
56
|
setFilterValue(value);
|
|
@@ -107,3 +116,53 @@ export const FilterByAnnotation: StoryObj<MutationsOverTimeMutationsFilterProps>
|
|
|
107
116
|
});
|
|
108
117
|
},
|
|
109
118
|
};
|
|
119
|
+
|
|
120
|
+
export const WithManyMutationAnnotations: StoryObj<MutationsOverTimeMutationsFilterProps> = {
|
|
121
|
+
render: (args) => {
|
|
122
|
+
return (
|
|
123
|
+
<WrapperWithState
|
|
124
|
+
setFilterValue={args.setFilterValue}
|
|
125
|
+
value={args.value}
|
|
126
|
+
annotations={manyMutationAnnotations}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
args: {
|
|
131
|
+
setFilterValue: fn(),
|
|
132
|
+
value: { textFilter: '', annotationNameFilter: new Set() },
|
|
133
|
+
},
|
|
134
|
+
play: async ({ canvasElement, step }) => {
|
|
135
|
+
const canvas = within(canvasElement);
|
|
136
|
+
|
|
137
|
+
await step('Open filter dropdown', async () => {
|
|
138
|
+
const filterButton = canvas.getByRole('button', { name: 'Filter mutations' });
|
|
139
|
+
await userEvent.click(filterButton);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await step('Verify scroll container is scrollable', () => {
|
|
143
|
+
const scrollContainer = canvas
|
|
144
|
+
.getByText('Filter by annotations')
|
|
145
|
+
.parentElement!.querySelector('.overflow-scroll')!;
|
|
146
|
+
void expect(scrollContainer).toBeInTheDocument();
|
|
147
|
+
|
|
148
|
+
// Verify the container has scrollable content
|
|
149
|
+
void expect(scrollContainer.scrollHeight).toBeGreaterThan(scrollContainer.clientHeight);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await step('Scroll to bottom and verify we can scroll', async () => {
|
|
153
|
+
const scrollContainer = canvas
|
|
154
|
+
.getByText('Filter by annotations')
|
|
155
|
+
.parentElement!.querySelector('.overflow-scroll')!;
|
|
156
|
+
|
|
157
|
+
const initialScrollTop = scrollContainer.scrollTop;
|
|
158
|
+
|
|
159
|
+
// Scroll to the bottom
|
|
160
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
161
|
+
|
|
162
|
+
await waitFor(async () => {
|
|
163
|
+
// Verify that scrollTop actually changed
|
|
164
|
+
await expect(scrollContainer.scrollTop).toBeGreaterThan(initialScrollTop);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
};
|
|
@@ -105,34 +105,36 @@ const AnnotationCheckboxes: FunctionComponent<MutationsOverTimeMutationsFilterPr
|
|
|
105
105
|
<div className='divider mt-0.5 mb-0' />
|
|
106
106
|
<div className='text-sm'>
|
|
107
107
|
<div className='font-bold mb-1'>Filter by annotations</div>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
108
|
+
<div className='max-h-72 overflow-scroll'>
|
|
109
|
+
{mutationAnnotations.map((annotation, index) => (
|
|
110
|
+
<li className='flex flex-row items-center' key={annotation.name}>
|
|
111
|
+
<label>
|
|
112
|
+
<input
|
|
113
|
+
className={'mr-2'}
|
|
114
|
+
type='checkbox'
|
|
115
|
+
id={`item-${index}`}
|
|
116
|
+
checked={value.annotationNameFilter.has(annotation.name)}
|
|
117
|
+
onChange={() => {
|
|
118
|
+
setFilterValue((previousFilter) => {
|
|
119
|
+
const newAnnotationFilter = previousFilter.annotationNameFilter.has(
|
|
120
|
+
annotation.name,
|
|
121
|
+
)
|
|
122
|
+
? [...previousFilter.annotationNameFilter].filter(
|
|
123
|
+
(name) => name !== annotation.name,
|
|
124
|
+
)
|
|
125
|
+
: [...previousFilter.annotationNameFilter, annotation.name];
|
|
126
|
+
return {
|
|
127
|
+
...previousFilter,
|
|
128
|
+
annotationNameFilter: new Set(newAnnotationFilter),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
{annotation.name} (<span className='text-red-600'>{annotation.symbol}</span>)
|
|
134
|
+
</label>
|
|
135
|
+
</li>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
136
138
|
</div>
|
|
137
139
|
</>
|
|
138
140
|
);
|
|
@@ -321,4 +321,76 @@ describe('fetchLineageAutocompleteList', () => {
|
|
|
321
321
|
},
|
|
322
322
|
]);
|
|
323
323
|
});
|
|
324
|
+
|
|
325
|
+
test('should include prefix aliases that are missing from lineage tree', async () => {
|
|
326
|
+
lapisRequestMocks.aggregated(
|
|
327
|
+
{ fields: [lineageField], ...lapisFilter },
|
|
328
|
+
{
|
|
329
|
+
data: [
|
|
330
|
+
{
|
|
331
|
+
[lineageField]: 'BA.3.2.1',
|
|
332
|
+
count: 1,
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
[lineageField]: 'BA.3.2.2',
|
|
336
|
+
count: 2,
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
lapisRequestMocks.lineageDefinition(
|
|
343
|
+
{
|
|
344
|
+
'B.1.1.529.3.2': {
|
|
345
|
+
aliases: ['BA.3.2'],
|
|
346
|
+
},
|
|
347
|
+
'BA.3.2.1': {
|
|
348
|
+
parents: ['B.1.1.529.3.2'],
|
|
349
|
+
aliases: ['B.1.1.529.3.2.1'],
|
|
350
|
+
},
|
|
351
|
+
'BA.3.2.2': {
|
|
352
|
+
parents: ['B.1.1.529.3.2'],
|
|
353
|
+
aliases: ['B.1.1.529.3.2.2'],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
lineageField,
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const result = await fetchLineageAutocompleteList({
|
|
360
|
+
lapisUrl: DUMMY_LAPIS_URL,
|
|
361
|
+
lapisField: lineageField,
|
|
362
|
+
lapisFilter,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
expect(result).to.deep.equal([
|
|
366
|
+
{
|
|
367
|
+
lineage: 'B.1.1.529.3.2',
|
|
368
|
+
count: 0,
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
lineage: 'B.1.1.529.3.2*',
|
|
372
|
+
count: 3,
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
lineage: 'BA.3.2*',
|
|
376
|
+
count: 3, // Same as B.1.1.529.3.2* (includes .3.2 and .3.2.1)
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
lineage: 'BA.3.2.1',
|
|
380
|
+
count: 1,
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
lineage: 'BA.3.2.1*',
|
|
384
|
+
count: 1,
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
lineage: 'BA.3.2.2',
|
|
388
|
+
count: 2,
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
lineage: 'BA.3.2.2*',
|
|
392
|
+
count: 2,
|
|
393
|
+
},
|
|
394
|
+
]);
|
|
395
|
+
});
|
|
324
396
|
});
|
|
@@ -2,6 +2,10 @@ import { fetchLineageDefinition } from '../../lapisApi/lapisApi';
|
|
|
2
2
|
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
|
|
3
3
|
import type { LapisFilter } from '../../types';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Generates the autocomplete list for lineage search. It includes lineages with wild cards
|
|
7
|
+
* (i.e. "BA.3.2.1" and "BA.3.2.1*") as well as all prefixes of lineages with an asterisk ("BA.3.2*").
|
|
8
|
+
*/
|
|
5
9
|
export async function fetchLineageAutocompleteList({
|
|
6
10
|
lapisUrl,
|
|
7
11
|
lapisField,
|
|
@@ -13,31 +17,43 @@ export async function fetchLineageAutocompleteList({
|
|
|
13
17
|
lapisFilter?: LapisFilter;
|
|
14
18
|
signal?: AbortSignal;
|
|
15
19
|
}): Promise<LineageItem[]> {
|
|
16
|
-
const [countsByLineage, lineageTree] = await Promise.all([
|
|
20
|
+
const [countsByLineage, { lineageTree, aliasMapping }] = await Promise.all([
|
|
17
21
|
getCountsByLineage({
|
|
18
22
|
lapisUrl,
|
|
19
23
|
lapisField,
|
|
20
24
|
lapisFilter,
|
|
21
25
|
signal,
|
|
22
26
|
}),
|
|
23
|
-
|
|
27
|
+
getLineageTreeAndAliases({ lapisUrl, lapisField, signal }),
|
|
24
28
|
]);
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
const prefixToLineage = findMissingPrefixMappings(lineageTree, aliasMapping);
|
|
31
|
+
|
|
32
|
+
// Combine actual lineages with their wildcard versions
|
|
33
|
+
const actualLineageItems = Array.from(lineageTree.keys()).flatMap((lineage) => [
|
|
34
|
+
{
|
|
35
|
+
lineage,
|
|
36
|
+
count: countsByLineage.get(lineage) ?? 0,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
lineage: `${lineage}*`,
|
|
40
|
+
count: getCountsIncludingSublineages(lineage, lineageTree, countsByLineage),
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// Add prefix alias items with wildcard and their counts
|
|
45
|
+
const prefixAliasItems = Array.from(prefixToLineage.entries()).map(([prefix, actualLineage]) => ({
|
|
46
|
+
lineage: `${prefix}*`,
|
|
47
|
+
count: getCountsIncludingSublineages(actualLineage, lineageTree, countsByLineage),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
// Combine and sort all items (asterisk before period for same prefix)
|
|
51
|
+
return [...actualLineageItems, ...prefixAliasItems].sort((a, b) => {
|
|
52
|
+
// Replace * with a character that sorts before . in ASCII
|
|
53
|
+
const aKey = a.lineage.replace(/\*/g, ' ');
|
|
54
|
+
const bKey = b.lineage.replace(/\*/g, ' ');
|
|
55
|
+
return aKey.localeCompare(bKey);
|
|
56
|
+
});
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
export type LineageItem = { lineage: string; count: number };
|
|
@@ -61,7 +77,7 @@ async function getCountsByLineage({
|
|
|
61
77
|
return new Map<string, number>(countsByLineageArray.map((value) => [value[lapisField], value.count]));
|
|
62
78
|
}
|
|
63
79
|
|
|
64
|
-
async function
|
|
80
|
+
async function getLineageTreeAndAliases({
|
|
65
81
|
lapisUrl,
|
|
66
82
|
lapisField,
|
|
67
83
|
signal,
|
|
@@ -73,12 +89,17 @@ async function getLineageTree({
|
|
|
73
89
|
const lineageDefinitions = await fetchLineageDefinition({ lapisUrl, lapisField, signal });
|
|
74
90
|
|
|
75
91
|
const lineageTree = new Map<string, { children: string[] }>();
|
|
92
|
+
const aliasMapping = new Map<string, string[]>();
|
|
76
93
|
|
|
77
94
|
Object.entries(lineageDefinitions).forEach(([lineage, definition]) => {
|
|
78
95
|
if (!lineageTree.has(lineage)) {
|
|
79
96
|
lineageTree.set(lineage, { children: [] });
|
|
80
97
|
}
|
|
81
98
|
|
|
99
|
+
if (definition.aliases && definition.aliases.length > 0) {
|
|
100
|
+
aliasMapping.set(lineage, definition.aliases);
|
|
101
|
+
}
|
|
102
|
+
|
|
82
103
|
definition.parents?.forEach((parent) => {
|
|
83
104
|
const parentChildren = lineageTree.get(parent)?.children;
|
|
84
105
|
|
|
@@ -88,7 +109,7 @@ async function getLineageTree({
|
|
|
88
109
|
});
|
|
89
110
|
});
|
|
90
111
|
|
|
91
|
-
return lineageTree;
|
|
112
|
+
return { lineageTree, aliasMapping };
|
|
92
113
|
}
|
|
93
114
|
|
|
94
115
|
function getCountsIncludingSublineages(
|
|
@@ -115,3 +136,44 @@ function getAllDescendants(lineage: string, lineageTree: Map<string, { children:
|
|
|
115
136
|
|
|
116
137
|
return new Set([...children, ...childrenOfChildren.flatMap((child) => Array.from(child))]);
|
|
117
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* This function finds prefixes (i.e. "BA.3.2" for "BA.3.2.1") that are not in the lineageTree,
|
|
142
|
+
* but do appear as an alias. It returns a reverse mapping for those prefixes, back to a lineage
|
|
143
|
+
* that can be found in the lineageTree (i.e. "BA.3.2" -> "B.1.1.529.3.2").
|
|
144
|
+
*/
|
|
145
|
+
function findMissingPrefixMappings(
|
|
146
|
+
lineageTree: Map<string, { children: string[] }>,
|
|
147
|
+
aliasMapping: Map<string, string[]>,
|
|
148
|
+
): Map<string, string> {
|
|
149
|
+
const lineages = Array.from(lineageTree.keys());
|
|
150
|
+
const lineagesSet = new Set(lineages);
|
|
151
|
+
|
|
152
|
+
// Generate all prefixes for each lineage (e.g., "A.B.1" -> ["A", "A.B", "A.B.1"])
|
|
153
|
+
const allPrefixes = lineages.flatMap((lineage) => {
|
|
154
|
+
const parts = lineage.split('.');
|
|
155
|
+
return parts.map((_, i) => parts.slice(0, i + 1).join('.'));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Find prefixes that are NOT in the actual lineages list
|
|
159
|
+
const missingPrefixes = new Set(allPrefixes.filter((prefix) => !lineagesSet.has(prefix)));
|
|
160
|
+
|
|
161
|
+
// Create reverse alias mapping: alias -> original lineage
|
|
162
|
+
const reverseAliasMapping = new Map<string, string>();
|
|
163
|
+
aliasMapping.forEach((aliases, lineage) => {
|
|
164
|
+
aliases.forEach((alias) => {
|
|
165
|
+
reverseAliasMapping.set(alias, lineage);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Map missing prefixes to their actual lineage names via reverse alias lookup
|
|
170
|
+
const prefixToLineage = new Map<string, string>();
|
|
171
|
+
missingPrefixes.forEach((prefix) => {
|
|
172
|
+
const actualLineage = reverseAliasMapping.get(prefix);
|
|
173
|
+
if (actualLineage) {
|
|
174
|
+
prefixToLineage.set(prefix, actualLineage);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return prefixToLineage;
|
|
179
|
+
}
|