@genspectrum/dashboard-components 0.13.4 → 0.13.6
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/custom-elements.json +20 -1
- package/dist/components.d.ts +27 -21
- package/dist/components.js +136 -63
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +21 -21
- package/package.json +1 -1
- package/src/preact/components/downshift-combobox.tsx +2 -2
- package/src/preact/components/mutation-info.tsx +36 -0
- package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +13 -13
- package/src/preact/locationFilter/fetchAutocompletionList.ts +55 -19
- package/src/preact/locationFilter/location-filter.stories.tsx +1 -1
- package/src/preact/locationFilter/location-filter.tsx +18 -12
- package/src/preact/mutationComparison/mutation-comparison.tsx +26 -2
- package/src/preact/mutations/mutations.tsx +5 -23
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +11 -5
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +7 -1
- package/src/query/queryWastewaterMutationsOverTime.spec.ts +29 -1
- package/src/query/queryWastewaterMutationsOverTime.ts +30 -16
- package/src/web-components/input/gs-location-filter.stories.ts +1 -1
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +9 -0
- package/standalone-bundle/dashboard-components.js +4463 -4395
- package/standalone-bundle/dashboard-components.js.map +1 -1
package/dist/util.d.ts
CHANGED
|
@@ -870,7 +870,7 @@ declare global {
|
|
|
870
870
|
|
|
871
871
|
declare global {
|
|
872
872
|
interface HTMLElementTagNameMap {
|
|
873
|
-
'gs-
|
|
873
|
+
'gs-prevalence-over-time': PrevalenceOverTimeComponent;
|
|
874
874
|
}
|
|
875
875
|
}
|
|
876
876
|
|
|
@@ -878,7 +878,7 @@ declare global {
|
|
|
878
878
|
declare global {
|
|
879
879
|
namespace JSX {
|
|
880
880
|
interface IntrinsicElements {
|
|
881
|
-
'gs-
|
|
881
|
+
'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
882
882
|
}
|
|
883
883
|
}
|
|
884
884
|
}
|
|
@@ -886,7 +886,7 @@ declare global {
|
|
|
886
886
|
|
|
887
887
|
declare global {
|
|
888
888
|
interface HTMLElementTagNameMap {
|
|
889
|
-
'gs-
|
|
889
|
+
'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
|
|
890
890
|
}
|
|
891
891
|
}
|
|
892
892
|
|
|
@@ -894,7 +894,7 @@ declare global {
|
|
|
894
894
|
declare global {
|
|
895
895
|
namespace JSX {
|
|
896
896
|
interface IntrinsicElements {
|
|
897
|
-
'gs-
|
|
897
|
+
'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
898
898
|
}
|
|
899
899
|
}
|
|
900
900
|
}
|
|
@@ -902,7 +902,7 @@ declare global {
|
|
|
902
902
|
|
|
903
903
|
declare global {
|
|
904
904
|
interface HTMLElementTagNameMap {
|
|
905
|
-
'gs-
|
|
905
|
+
'gs-aggregate': AggregateComponent;
|
|
906
906
|
}
|
|
907
907
|
}
|
|
908
908
|
|
|
@@ -910,7 +910,7 @@ declare global {
|
|
|
910
910
|
declare global {
|
|
911
911
|
namespace JSX {
|
|
912
912
|
interface IntrinsicElements {
|
|
913
|
-
'gs-
|
|
913
|
+
'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
914
914
|
}
|
|
915
915
|
}
|
|
916
916
|
}
|
|
@@ -918,7 +918,7 @@ declare global {
|
|
|
918
918
|
|
|
919
919
|
declare global {
|
|
920
920
|
interface HTMLElementTagNameMap {
|
|
921
|
-
'gs-
|
|
921
|
+
'gs-mutations-over-time': MutationsOverTimeComponent;
|
|
922
922
|
}
|
|
923
923
|
}
|
|
924
924
|
|
|
@@ -926,7 +926,7 @@ declare global {
|
|
|
926
926
|
declare global {
|
|
927
927
|
namespace JSX {
|
|
928
928
|
interface IntrinsicElements {
|
|
929
|
-
'gs-
|
|
929
|
+
'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
930
930
|
}
|
|
931
931
|
}
|
|
932
932
|
}
|
|
@@ -934,7 +934,7 @@ declare global {
|
|
|
934
934
|
|
|
935
935
|
declare global {
|
|
936
936
|
interface HTMLElementTagNameMap {
|
|
937
|
-
'gs-
|
|
937
|
+
'gs-sequences-by-location': SequencesByLocationComponent;
|
|
938
938
|
}
|
|
939
939
|
}
|
|
940
940
|
|
|
@@ -942,7 +942,7 @@ declare global {
|
|
|
942
942
|
declare global {
|
|
943
943
|
namespace JSX {
|
|
944
944
|
interface IntrinsicElements {
|
|
945
|
-
'gs-
|
|
945
|
+
'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
946
946
|
}
|
|
947
947
|
}
|
|
948
948
|
}
|
|
@@ -950,7 +950,7 @@ declare global {
|
|
|
950
950
|
|
|
951
951
|
declare global {
|
|
952
952
|
interface HTMLElementTagNameMap {
|
|
953
|
-
'gs-sequences-
|
|
953
|
+
'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
|
|
954
954
|
}
|
|
955
955
|
}
|
|
956
956
|
|
|
@@ -958,7 +958,7 @@ declare global {
|
|
|
958
958
|
declare global {
|
|
959
959
|
namespace JSX {
|
|
960
960
|
interface IntrinsicElements {
|
|
961
|
-
'gs-sequences-
|
|
961
|
+
'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
962
962
|
}
|
|
963
963
|
}
|
|
964
964
|
}
|
|
@@ -1018,10 +1018,10 @@ declare global {
|
|
|
1018
1018
|
|
|
1019
1019
|
declare global {
|
|
1020
1020
|
interface HTMLElementTagNameMap {
|
|
1021
|
-
'gs-
|
|
1021
|
+
'gs-location-filter': LocationFilterComponent;
|
|
1022
1022
|
}
|
|
1023
1023
|
interface HTMLElementEventMap {
|
|
1024
|
-
'gs-
|
|
1024
|
+
'gs-location-changed': LocationChangedEvent;
|
|
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-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1033
1033
|
}
|
|
1034
1034
|
}
|
|
1035
1035
|
}
|
|
@@ -1037,10 +1037,10 @@ declare global {
|
|
|
1037
1037
|
|
|
1038
1038
|
declare global {
|
|
1039
1039
|
interface HTMLElementTagNameMap {
|
|
1040
|
-
'gs-
|
|
1040
|
+
'gs-text-input': TextInputComponent;
|
|
1041
1041
|
}
|
|
1042
1042
|
interface HTMLElementEventMap {
|
|
1043
|
-
'gs-
|
|
1043
|
+
'gs-text-input-changed': TextInputChangedEvent;
|
|
1044
1044
|
}
|
|
1045
1045
|
}
|
|
1046
1046
|
|
|
@@ -1048,7 +1048,7 @@ declare global {
|
|
|
1048
1048
|
declare global {
|
|
1049
1049
|
namespace JSX {
|
|
1050
1050
|
interface IntrinsicElements {
|
|
1051
|
-
'gs-
|
|
1051
|
+
'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1052
1052
|
}
|
|
1053
1053
|
}
|
|
1054
1054
|
}
|
|
@@ -1075,10 +1075,10 @@ declare global {
|
|
|
1075
1075
|
|
|
1076
1076
|
declare global {
|
|
1077
1077
|
interface HTMLElementTagNameMap {
|
|
1078
|
-
'gs-
|
|
1078
|
+
'gs-mutation-filter': MutationFilterComponent;
|
|
1079
1079
|
}
|
|
1080
1080
|
interface HTMLElementEventMap {
|
|
1081
|
-
'gs-
|
|
1081
|
+
'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
|
|
1082
1082
|
}
|
|
1083
1083
|
}
|
|
1084
1084
|
|
|
@@ -1086,7 +1086,7 @@ declare global {
|
|
|
1086
1086
|
declare global {
|
|
1087
1087
|
namespace JSX {
|
|
1088
1088
|
interface IntrinsicElements {
|
|
1089
|
-
'gs-
|
|
1089
|
+
'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1090
1090
|
}
|
|
1091
1091
|
}
|
|
1092
1092
|
}
|
package/package.json
CHANGED
|
@@ -127,7 +127,7 @@ export function DownshiftCombobox<Item>({
|
|
|
127
127
|
{items.length > 0 ? (
|
|
128
128
|
items.map((item, index) => (
|
|
129
129
|
<li
|
|
130
|
-
className={`${highlightedIndex === index ? 'bg-blue-300' : ''} ${selectedItem !== null && itemToString(selectedItem) === itemToString(item) ? 'font-bold' : ''} py-2 px-3 shadow-sm
|
|
130
|
+
className={`${highlightedIndex === index ? 'bg-blue-300' : ''} ${selectedItem !== null && itemToString(selectedItem) === itemToString(item) ? 'font-bold' : ''} py-2 px-3 shadow-sm`}
|
|
131
131
|
key={itemToString(item)}
|
|
132
132
|
{...getItemProps({ item, index })}
|
|
133
133
|
>
|
|
@@ -135,7 +135,7 @@ export function DownshiftCombobox<Item>({
|
|
|
135
135
|
</li>
|
|
136
136
|
))
|
|
137
137
|
) : (
|
|
138
|
-
<li className='py-2 px-3 shadow-sm
|
|
138
|
+
<li className='py-2 px-3 shadow-sm'>No elements to select.</li>
|
|
139
139
|
)}
|
|
140
140
|
</ul>
|
|
141
141
|
</div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { InfoHeadline2, InfoLink, InfoParagraph } from './info';
|
|
2
|
+
|
|
3
|
+
export const SubstitutionsLink = () => (
|
|
4
|
+
<InfoLink href='https://www.genome.gov/genetics-glossary/Substitution'>substitutions</InfoLink>
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
export const InsertionsLink = () => (
|
|
8
|
+
<InfoLink href='https://www.genome.gov/genetics-glossary/Insertion'>insertions</InfoLink>
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export const DeletionsLink = () => (
|
|
12
|
+
<InfoLink href='https://www.genome.gov/genetics-glossary/Deletion'>deletions</InfoLink>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const ProportionExplanation = () => (
|
|
16
|
+
<>
|
|
17
|
+
<InfoHeadline2>Proportion calculation</InfoHeadline2>
|
|
18
|
+
<InfoParagraph>
|
|
19
|
+
The proportion of a mutation is calculated by dividing the number of sequences with the mutation by the
|
|
20
|
+
total number of sequences with a non-ambiguous symbol at the position.
|
|
21
|
+
</InfoParagraph>
|
|
22
|
+
<InfoParagraph>
|
|
23
|
+
<b>Example:</b> Assume we look at nucleotide mutations at position 5 where the reference has a T and assume
|
|
24
|
+
there are 10 sequences in total:
|
|
25
|
+
<ul className='list-disc list-inside ml-2'>
|
|
26
|
+
<li>3 sequences have a C,</li>
|
|
27
|
+
<li>2 sequences have a T,</li>
|
|
28
|
+
<li>1 sequence has a G,</li>
|
|
29
|
+
<li>3 sequences have an N,</li>
|
|
30
|
+
<li>1 sequence has a Y (which means T or C),</li>
|
|
31
|
+
</ul>
|
|
32
|
+
then the proportion of the T5C mutation is 50%. The 4 sequences that have an N or Y are excluded from the
|
|
33
|
+
calculation.
|
|
34
|
+
</InfoParagraph>
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
@@ -11,11 +11,11 @@ describe('fetchAutocompletionList', () => {
|
|
|
11
11
|
{ fields, country: 'Germany' },
|
|
12
12
|
{
|
|
13
13
|
data: [
|
|
14
|
-
{ count:
|
|
15
|
-
{ count:
|
|
16
|
-
{ count:
|
|
17
|
-
{ count:
|
|
18
|
-
{ count:
|
|
14
|
+
{ count: 1, region: 'region1', country: 'country1_1', division: 'division1_1_1' },
|
|
15
|
+
{ count: 2, region: 'region1', country: 'country1_1', division: 'division1_1_2' },
|
|
16
|
+
{ count: 3, region: 'region1', country: 'country1_1', division: null },
|
|
17
|
+
{ count: 4, region: 'region1', country: 'country1_2', division: 'division1_2_1' },
|
|
18
|
+
{ count: 5, region: 'region2', country: 'country2_1', division: null },
|
|
19
19
|
],
|
|
20
20
|
},
|
|
21
21
|
);
|
|
@@ -27,14 +27,14 @@ describe('fetchAutocompletionList', () => {
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
expect(result).to.deep.equal([
|
|
30
|
-
{ region: 'region1', country: undefined, division: undefined },
|
|
31
|
-
{ region: 'region1', country: 'country1_1', division: undefined },
|
|
32
|
-
{ region: 'region1', country: 'country1_1', division: 'division1_1_1' },
|
|
33
|
-
{ region: 'region1', country: 'country1_1', division: 'division1_1_2' },
|
|
34
|
-
{ region: 'region1', country: 'country1_2', division: undefined },
|
|
35
|
-
{ region: 'region1', country: 'country1_2', division: 'division1_2_1' },
|
|
36
|
-
{ region: 'region2', country: undefined, division: undefined },
|
|
37
|
-
{ region: 'region2', country: 'country2_1', division: undefined },
|
|
30
|
+
{ value: { region: 'region1', country: undefined, division: undefined }, count: 10 },
|
|
31
|
+
{ value: { region: 'region1', country: 'country1_1', division: undefined }, count: 6 },
|
|
32
|
+
{ value: { region: 'region1', country: 'country1_1', division: 'division1_1_1' }, count: 1 },
|
|
33
|
+
{ value: { region: 'region1', country: 'country1_1', division: 'division1_1_2' }, count: 2 },
|
|
34
|
+
{ value: { region: 'region1', country: 'country1_2', division: undefined }, count: 4 },
|
|
35
|
+
{ value: { region: 'region1', country: 'country1_2', division: 'division1_2_1' }, count: 4 },
|
|
36
|
+
{ value: { region: 'region2', country: undefined, division: undefined }, count: 5 },
|
|
37
|
+
{ value: { region: 'region2', country: 'country2_1', division: undefined }, count: 5 },
|
|
38
38
|
]);
|
|
39
39
|
});
|
|
40
40
|
});
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
|
|
2
|
-
import type { LapisFilter } from '../../types';
|
|
2
|
+
import type { LapisFilter, LapisLocationFilter } from '../../types';
|
|
3
|
+
|
|
4
|
+
export type LocationEntry = {
|
|
5
|
+
value: LapisLocationFilter;
|
|
6
|
+
count: number;
|
|
7
|
+
};
|
|
3
8
|
|
|
4
9
|
export async function fetchAutocompletionList({
|
|
5
10
|
fields,
|
|
@@ -11,11 +16,10 @@ export async function fetchAutocompletionList({
|
|
|
11
16
|
lapis: string;
|
|
12
17
|
lapisFilter?: LapisFilter;
|
|
13
18
|
signal?: AbortSignal;
|
|
14
|
-
}): Promise<
|
|
15
|
-
const
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
.map((i) => fields.slice(i).reduce((acc, field) => ({ ...acc, [field]: null }), {}));
|
|
19
|
+
}): Promise<LocationEntry[]> {
|
|
20
|
+
const helpersThatOverwriteAValueToItsAncestor = fields.map((_, i) =>
|
|
21
|
+
fields.slice(i + 1).reduce((acc, field) => ({ ...acc, [field]: null }), {}),
|
|
22
|
+
);
|
|
19
23
|
|
|
20
24
|
const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string | null>>(
|
|
21
25
|
lapisFilter ?? {},
|
|
@@ -25,26 +29,58 @@ export async function fetchAutocompletionList({
|
|
|
25
29
|
const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
|
|
26
30
|
|
|
27
31
|
const locationValues = data
|
|
28
|
-
.map((entry) =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
.map((entry) => ({
|
|
33
|
+
value: fields.reduce((acc, field) => ({ ...acc, [field]: entry[field] }), {}),
|
|
34
|
+
count: entry.count,
|
|
35
|
+
}))
|
|
36
|
+
.reduce((mapOfAllHierarchiesAndCounts, entry) => {
|
|
37
|
+
return addValueAndAllAncestorsToMap(
|
|
38
|
+
entry,
|
|
39
|
+
helpersThatOverwriteAValueToItsAncestor,
|
|
40
|
+
mapOfAllHierarchiesAndCounts,
|
|
41
|
+
);
|
|
42
|
+
}, new Map<string, number>());
|
|
36
43
|
|
|
37
44
|
return [...locationValues]
|
|
38
|
-
.map((json) =>
|
|
45
|
+
.map<EntryWithNullValues>(([json, count]) => ({
|
|
46
|
+
value: JSON.parse(json),
|
|
47
|
+
count,
|
|
48
|
+
}))
|
|
39
49
|
.sort(compareLocationEntries(fields))
|
|
40
|
-
.map((
|
|
50
|
+
.map(({ value, count }) => ({
|
|
51
|
+
value: fields.reduce((acc, field) => ({ ...acc, [field]: value[field] ?? undefined }), {}),
|
|
52
|
+
count,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function addValueAndAllAncestorsToMap(
|
|
57
|
+
{ value, count }: LocationEntry,
|
|
58
|
+
helpersThatOverwriteAValueToItsAncestor: Record<string, null>[],
|
|
59
|
+
mapOfAllHierarchiesAndCounts: Map<string, number>,
|
|
60
|
+
) {
|
|
61
|
+
const keysOfAllHierarchyLevels = new Set(
|
|
62
|
+
helpersThatOverwriteAValueToItsAncestor
|
|
63
|
+
.map((overwriteValues) => ({ ...value, ...overwriteValues }))
|
|
64
|
+
.map((value) => JSON.stringify(value)),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
for (const key of keysOfAllHierarchyLevels) {
|
|
68
|
+
mapOfAllHierarchiesAndCounts.set(key, (mapOfAllHierarchiesAndCounts.get(key) ?? 0) + count);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return mapOfAllHierarchiesAndCounts;
|
|
41
72
|
}
|
|
42
73
|
|
|
74
|
+
type EntryWithNullValues = {
|
|
75
|
+
value: Record<string, string | null>;
|
|
76
|
+
count: number;
|
|
77
|
+
};
|
|
78
|
+
|
|
43
79
|
function compareLocationEntries(fields: string[]) {
|
|
44
|
-
return (a:
|
|
80
|
+
return (a: EntryWithNullValues, b: EntryWithNullValues) => {
|
|
45
81
|
for (const field of fields) {
|
|
46
|
-
const valueA = a[field];
|
|
47
|
-
const valueB = b[field];
|
|
82
|
+
const valueA = a.value[field];
|
|
83
|
+
const valueB = b.value[field];
|
|
48
84
|
if (valueA === valueB) {
|
|
49
85
|
continue;
|
|
50
86
|
}
|
|
@@ -88,7 +88,7 @@ export const Primary: StoryObj<LocationFilterProps> = {
|
|
|
88
88
|
const input = await inputField(canvas);
|
|
89
89
|
await userEvent.clear(input);
|
|
90
90
|
await userEvent.type(input, 'Germany');
|
|
91
|
-
await userEvent.click(canvas.getByRole('option', { name: 'Germany Europe / Germany' }));
|
|
91
|
+
await userEvent.click(canvas.getByRole('option', { name: 'Germany(42) Europe / Germany' }));
|
|
92
92
|
|
|
93
93
|
await waitFor(() => {
|
|
94
94
|
return expect(locationChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
@@ -2,7 +2,7 @@ import { type FunctionComponent } from 'preact';
|
|
|
2
2
|
import { useContext, useMemo } from 'preact/hooks';
|
|
3
3
|
import z from 'zod';
|
|
4
4
|
|
|
5
|
-
import { fetchAutocompletionList } from './fetchAutocompletionList';
|
|
5
|
+
import { fetchAutocompletionList, type LocationEntry } from './fetchAutocompletionList';
|
|
6
6
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
7
7
|
import { LocationChangedEvent } from './LocationChangedEvent';
|
|
8
8
|
import { lapisFilterSchema, type LapisLocationFilter, lapisLocationFilterSchema } from '../../types';
|
|
@@ -61,6 +61,7 @@ type SelectItem = {
|
|
|
61
61
|
lapisFilter: LapisLocationFilter;
|
|
62
62
|
label: string | null | undefined;
|
|
63
63
|
description: string;
|
|
64
|
+
count: number;
|
|
64
65
|
};
|
|
65
66
|
|
|
66
67
|
const LocationSelector = ({
|
|
@@ -69,7 +70,7 @@ const LocationSelector = ({
|
|
|
69
70
|
placeholderText,
|
|
70
71
|
locationData,
|
|
71
72
|
}: LocationSelectorProps & {
|
|
72
|
-
locationData:
|
|
73
|
+
locationData: LocationEntry[];
|
|
73
74
|
}) => {
|
|
74
75
|
const allItems = useMemo(() => {
|
|
75
76
|
return locationData
|
|
@@ -78,8 +79,10 @@ const LocationSelector = ({
|
|
|
78
79
|
}, [fields, locationData]);
|
|
79
80
|
|
|
80
81
|
const selectedItem = useMemo(() => {
|
|
81
|
-
return value !== undefined
|
|
82
|
-
|
|
82
|
+
return value !== undefined
|
|
83
|
+
? allItems.find((item) => item.description == concatenateLocation(value, fields))
|
|
84
|
+
: undefined;
|
|
85
|
+
}, [fields, value, allItems]);
|
|
83
86
|
|
|
84
87
|
return (
|
|
85
88
|
<DownshiftCombobox
|
|
@@ -91,14 +94,15 @@ const LocationSelector = ({
|
|
|
91
94
|
}
|
|
92
95
|
itemToString={(item: SelectItem | undefined | null) => item?.label ?? ''}
|
|
93
96
|
placeholderText={placeholderText}
|
|
94
|
-
formatItemInList={(item: SelectItem) =>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
formatItemInList={(item: SelectItem) => (
|
|
98
|
+
<>
|
|
99
|
+
<p>
|
|
97
100
|
<span>{item.label}</span>
|
|
98
|
-
<span className='
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
<span className='ml-2 text-gray-500'>({item.count})</span>
|
|
102
|
+
</p>
|
|
103
|
+
<span className='text-sm text-gray-500'>{item.description}</span>
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
102
106
|
/>
|
|
103
107
|
);
|
|
104
108
|
};
|
|
@@ -113,7 +117,8 @@ function filterByInputValue(item: SelectItem, inputValue: string | undefined | n
|
|
|
113
117
|
);
|
|
114
118
|
}
|
|
115
119
|
|
|
116
|
-
function toSelectItem(
|
|
120
|
+
function toSelectItem(locationEntry: LocationEntry, fields: string[]): SelectItem | undefined {
|
|
121
|
+
const locationFilter = locationEntry.value;
|
|
117
122
|
const concatenatedLocation = concatenateLocation(locationFilter, fields);
|
|
118
123
|
|
|
119
124
|
const lastNonUndefinedField = [...fields]
|
|
@@ -128,6 +133,7 @@ function toSelectItem(locationFilter: LapisLocationFilter, fields: string[]): Se
|
|
|
128
133
|
lapisFilter: locationFilter,
|
|
129
134
|
label: locationFilter[lastNonUndefinedField],
|
|
130
135
|
description: concatenatedLocation,
|
|
136
|
+
count: locationEntry.count,
|
|
131
137
|
};
|
|
132
138
|
}
|
|
133
139
|
|
|
@@ -11,8 +11,9 @@ import { LapisUrlContext } from '../LapisUrlContext';
|
|
|
11
11
|
import { CsvDownloadButton } from '../components/csv-download-button';
|
|
12
12
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
13
13
|
import { Fullscreen } from '../components/fullscreen';
|
|
14
|
-
import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../components/info';
|
|
14
|
+
import Info, { InfoComponentCode, InfoHeadline1, InfoHeadline2, InfoParagraph } from '../components/info';
|
|
15
15
|
import { LoadingDisplay } from '../components/loading-display';
|
|
16
|
+
import { DeletionsLink, ProportionExplanation, SubstitutionsLink } from '../components/mutation-info';
|
|
16
17
|
import { type DisplayedMutationType, MutationTypeSelector } from '../components/mutation-type-selector';
|
|
17
18
|
import { NoDataDisplay } from '../components/no-data-display';
|
|
18
19
|
import { type ProportionInterval } from '../components/proportion-selector';
|
|
@@ -190,7 +191,30 @@ const MutationComparisonInfo: FunctionComponent<MutationComparisonInfoProps> = (
|
|
|
190
191
|
return (
|
|
191
192
|
<Info>
|
|
192
193
|
<InfoHeadline1>Info for mutation comparison</InfoHeadline1>
|
|
193
|
-
<InfoParagraph>
|
|
194
|
+
<InfoParagraph>
|
|
195
|
+
This displays <SubstitutionsLink /> and <DeletionsLink /> of several variants. It shows mutations where
|
|
196
|
+
the proportion for any given variant falls within the range you can select in the component's toolbar.
|
|
197
|
+
</InfoParagraph>
|
|
198
|
+
<ProportionExplanation />
|
|
199
|
+
{originalComponentProps.views.includes(views.table) && (
|
|
200
|
+
<>
|
|
201
|
+
<InfoHeadline2>Table View</InfoHeadline2>
|
|
202
|
+
<InfoParagraph>
|
|
203
|
+
The table view displays the proportion of mutations that appear in any of the variants.
|
|
204
|
+
</InfoParagraph>
|
|
205
|
+
</>
|
|
206
|
+
)}
|
|
207
|
+
{originalComponentProps.views.includes(views.venn) && (
|
|
208
|
+
<>
|
|
209
|
+
<InfoHeadline2>Venn Diagram View</InfoHeadline2>
|
|
210
|
+
<InfoParagraph>
|
|
211
|
+
The Venn diagram view illustrates which mutations overlap between the variants and which are
|
|
212
|
+
exclusive to specific variants. Mutations overlap if their proportion falls within the selected
|
|
213
|
+
range for two variants. If the proportion of a mutation is within the selected range for one
|
|
214
|
+
variant but not for the other, the mutation is considered exclusive to that variant.
|
|
215
|
+
</InfoParagraph>
|
|
216
|
+
</>
|
|
217
|
+
)}
|
|
194
218
|
<InfoComponentCode componentName='mutation-comparison' params={originalComponentProps} lapisUrl={lapis} />
|
|
195
219
|
</Info>
|
|
196
220
|
);
|
|
@@ -19,8 +19,9 @@ import { LapisUrlContext } from '../LapisUrlContext';
|
|
|
19
19
|
import { CsvDownloadButton } from '../components/csv-download-button';
|
|
20
20
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
21
21
|
import { Fullscreen } from '../components/fullscreen';
|
|
22
|
-
import Info, { InfoComponentCode, InfoHeadline1,
|
|
22
|
+
import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../components/info';
|
|
23
23
|
import { LoadingDisplay } from '../components/loading-display';
|
|
24
|
+
import { DeletionsLink, InsertionsLink, ProportionExplanation, SubstitutionsLink } from '../components/mutation-info';
|
|
24
25
|
import { type DisplayedMutationType, MutationTypeSelector } from '../components/mutation-type-selector';
|
|
25
26
|
import { NoDataDisplay } from '../components/no-data-display';
|
|
26
27
|
import type { ProportionInterval } from '../components/proportion-selector';
|
|
@@ -242,29 +243,10 @@ const MutationsInfo: FunctionComponent<MutationsInfoProps> = ({ originalComponen
|
|
|
242
243
|
<Info>
|
|
243
244
|
<InfoHeadline1>Mutations</InfoHeadline1>
|
|
244
245
|
<InfoParagraph>
|
|
245
|
-
This shows mutations of a variant. There are three types of mutations:{' '}
|
|
246
|
-
<
|
|
247
|
-
<InfoLink href='https://www.genome.gov/genetics-glossary/Deletion'>deletions</InfoLink> and{' '}
|
|
248
|
-
<InfoLink href='https://www.genome.gov/genetics-glossary/Insertion'>insertions</InfoLink>.
|
|
249
|
-
</InfoParagraph>
|
|
250
|
-
<InfoHeadline2>Proportion calculation</InfoHeadline2>
|
|
251
|
-
<InfoParagraph>
|
|
252
|
-
The proportion of a mutation is calculated by dividing the number of sequences with the mutation by the
|
|
253
|
-
total number of sequences with a non-ambiguous symbol at the position.
|
|
254
|
-
</InfoParagraph>
|
|
255
|
-
<InfoParagraph>
|
|
256
|
-
<b>Example:</b> Assume we look at nucleotide mutations at position 5 where the reference has a T and
|
|
257
|
-
assume there are 10 sequences in total:
|
|
258
|
-
<ul className='list-disc list-inside ml-2'>
|
|
259
|
-
<li>3 sequences have a C,</li>
|
|
260
|
-
<li>2 sequences have a T,</li>
|
|
261
|
-
<li>1 sequence has a G,</li>
|
|
262
|
-
<li>3 sequences have an N,</li>
|
|
263
|
-
<li>1 sequence has a Y (which means T or C),</li>
|
|
264
|
-
</ul>
|
|
265
|
-
then the proportion of the T5C mutation is 50%. The 4 sequences that have an N or Y are excluded from
|
|
266
|
-
the calculation.
|
|
246
|
+
This shows mutations of a variant. There are three types of mutations: <SubstitutionsLink />,{' '}
|
|
247
|
+
<DeletionsLink /> and <InsertionsLink />.
|
|
267
248
|
</InfoParagraph>
|
|
249
|
+
<ProportionExplanation />
|
|
268
250
|
<InfoComponentCode componentName='mutations' params={originalComponentProps} lapisUrl={lapis} />
|
|
269
251
|
</Info>
|
|
270
252
|
);
|
|
@@ -12,14 +12,20 @@ import { formatProportion } from '../shared/table/formatProportion';
|
|
|
12
12
|
export interface MutationsOverTimeGridProps {
|
|
13
13
|
data: MutationOverTimeDataMap;
|
|
14
14
|
colorScale: ColorScale;
|
|
15
|
+
maxNumberOfGridRows?: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
const MAX_NUMBER_OF_GRID_ROWS = 100;
|
|
18
19
|
const MUTATION_CELL_WIDTH_REM = 8;
|
|
19
20
|
|
|
20
|
-
const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
|
|
21
|
+
const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
|
|
22
|
+
data,
|
|
23
|
+
colorScale,
|
|
24
|
+
maxNumberOfGridRows,
|
|
25
|
+
}) => {
|
|
26
|
+
const currentMaxNumberOfGridRows = maxNumberOfGridRows ?? MAX_NUMBER_OF_GRID_ROWS;
|
|
21
27
|
const allMutations = data.getFirstAxisKeys();
|
|
22
|
-
const shownMutations = allMutations.slice(0,
|
|
28
|
+
const shownMutations = allMutations.slice(0, currentMaxNumberOfGridRows);
|
|
23
29
|
|
|
24
30
|
const dates = data.getSecondAxisKeys();
|
|
25
31
|
|
|
@@ -27,10 +33,10 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
|
|
|
27
33
|
|
|
28
34
|
return (
|
|
29
35
|
<>
|
|
30
|
-
{allMutations.length >
|
|
36
|
+
{allMutations.length > currentMaxNumberOfGridRows && (
|
|
31
37
|
<div className='pl-2'>
|
|
32
|
-
Showing {
|
|
33
|
-
reduce the number of mutations.
|
|
38
|
+
Showing {currentMaxNumberOfGridRows} of {allMutations.length} mutations. You can narrow the filter
|
|
39
|
+
to reduce the number of mutations.
|
|
34
40
|
</div>
|
|
35
41
|
)}
|
|
36
42
|
{allMutations.length === 0 && (
|
|
@@ -23,6 +23,7 @@ const wastewaterMutationOverTimeSchema = z.object({
|
|
|
23
23
|
sequenceType: sequenceTypeSchema,
|
|
24
24
|
width: z.string(),
|
|
25
25
|
height: z.string(),
|
|
26
|
+
maxNumberOfGridRows: z.number().optional(),
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
export type WastewaterMutationsOverTimeProps = z.infer<typeof wastewaterMutationOverTimeSchema>;
|
|
@@ -75,6 +76,7 @@ export const WastewaterMutationsOverTimeInner: FunctionComponent<WastewaterMutat
|
|
|
75
76
|
<MutationsOverTimeTabs
|
|
76
77
|
mutationOverTimeDataPerLocation={mutationOverTimeDataPerLocation}
|
|
77
78
|
originalComponentProps={componentProps}
|
|
79
|
+
maxNumberOfGridRows={componentProps.maxNumberOfGridRows}
|
|
78
80
|
/>
|
|
79
81
|
);
|
|
80
82
|
};
|
|
@@ -87,17 +89,21 @@ type MutationOverTimeDataPerLocation = {
|
|
|
87
89
|
type MutationOverTimeTabsProps = {
|
|
88
90
|
mutationOverTimeDataPerLocation: MutationOverTimeDataPerLocation;
|
|
89
91
|
originalComponentProps: WastewaterMutationsOverTimeProps;
|
|
92
|
+
maxNumberOfGridRows?: number;
|
|
90
93
|
};
|
|
91
94
|
|
|
92
95
|
const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
93
96
|
mutationOverTimeDataPerLocation,
|
|
94
97
|
originalComponentProps,
|
|
98
|
+
maxNumberOfGridRows,
|
|
95
99
|
}) => {
|
|
96
100
|
const [colorScale, setColorScale] = useState<ColorScale>({ min: 0, max: 1, color: 'indigo' });
|
|
97
101
|
|
|
98
102
|
const tabs = mutationOverTimeDataPerLocation.map(({ location, data }) => ({
|
|
99
103
|
title: location,
|
|
100
|
-
content:
|
|
104
|
+
content: (
|
|
105
|
+
<MutationsOverTimeGrid data={data} colorScale={colorScale} maxNumberOfGridRows={maxNumberOfGridRows} />
|
|
106
|
+
),
|
|
101
107
|
}));
|
|
102
108
|
|
|
103
109
|
const toolbar = (
|