@genspectrum/dashboard-components 0.13.5 → 0.13.7

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.
Files changed (46) hide show
  1. package/custom-elements.json +29 -29
  2. package/dist/components.d.ts +15 -17
  3. package/dist/components.js +462 -285
  4. package/dist/components.js.map +1 -1
  5. package/dist/style.css +20 -5
  6. package/dist/util.d.ts +14 -14
  7. package/package.json +1 -1
  8. package/src/preact/components/downshift-combobox.tsx +2 -2
  9. package/src/preact/components/mutation-info.tsx +36 -0
  10. package/src/preact/components/tabs.tsx +3 -5
  11. package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +13 -13
  12. package/src/preact/locationFilter/fetchAutocompletionList.ts +55 -19
  13. package/src/preact/locationFilter/location-filter.stories.tsx +1 -1
  14. package/src/preact/locationFilter/location-filter.tsx +18 -12
  15. package/src/preact/mutationComparison/mutation-comparison.tsx +26 -2
  16. package/src/preact/mutationFilter/ExampleMutation.tsx +68 -0
  17. package/src/preact/mutationFilter/mutation-filter-info.tsx +179 -112
  18. package/src/preact/mutationFilter/mutation-filter.tsx +10 -5
  19. package/src/preact/mutations/mutations.tsx +5 -23
  20. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +26 -4
  21. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +2 -6
  22. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +1 -1
  23. package/src/query/queryWastewaterMutationsOverTime.spec.ts +29 -1
  24. package/src/query/queryWastewaterMutationsOverTime.ts +30 -16
  25. package/src/web-components/{app.stories.ts → gs-app.stories.ts} +1 -1
  26. package/src/web-components/{app.ts → gs-app.ts} +2 -2
  27. package/src/web-components/index.ts +1 -1
  28. package/src/web-components/input/gs-date-range-selector.stories.ts +1 -1
  29. package/src/web-components/input/gs-lineage-filter.stories.ts +1 -1
  30. package/src/web-components/input/gs-location-filter.stories.ts +2 -2
  31. package/src/web-components/input/gs-mutation-filter.stories.ts +2 -2
  32. package/src/web-components/input/gs-text-input.stories.ts +1 -1
  33. package/src/web-components/visualization/gs-aggregate.stories.ts +1 -1
  34. package/src/web-components/visualization/gs-mutation-comparison.stories.ts +1 -1
  35. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +1 -1
  36. package/src/web-components/visualization/gs-mutations.stories.ts +1 -1
  37. package/src/web-components/visualization/gs-number-sequences-over-time.stories.ts +1 -1
  38. package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +1 -1
  39. package/src/web-components/visualization/gs-relative-growth-advantage.stories.ts +1 -1
  40. package/src/web-components/visualization/gs-sequences-by-location.stories.ts +1 -1
  41. package/src/web-components/visualization/gs-statistics.stories.ts +1 -1
  42. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +4 -1
  43. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +6 -2
  44. package/standalone-bundle/dashboard-components.js +5561 -5445
  45. package/standalone-bundle/dashboard-components.js.map +1 -1
  46. package/standalone-bundle/style.css +1 -1
package/dist/style.css CHANGED
@@ -3319,6 +3319,9 @@ input.tab:checked + .tab-content,
3319
3319
  .gap-y-1 {
3320
3320
  row-gap: 0.25rem;
3321
3321
  }
3322
+ .overflow-visible {
3323
+ overflow: visible;
3324
+ }
3322
3325
  .overflow-scroll {
3323
3326
  overflow: scroll;
3324
3327
  }
@@ -3328,6 +3331,9 @@ input.tab:checked + .tab-content,
3328
3331
  .whitespace-nowrap {
3329
3332
  white-space: nowrap;
3330
3333
  }
3334
+ .text-nowrap {
3335
+ text-wrap: nowrap;
3336
+ }
3331
3337
  .break-words {
3332
3338
  overflow-wrap: break-word;
3333
3339
  }
@@ -3347,9 +3353,6 @@ input.tab:checked + .tab-content,
3347
3353
  border-bottom-right-radius: 0.375rem;
3348
3354
  border-bottom-left-radius: 0.375rem;
3349
3355
  }
3350
- .rounded-tl-md {
3351
- border-top-left-radius: 0.375rem;
3352
- }
3353
3356
  .rounded-tr-md {
3354
3357
  border-top-right-radius: 0.375rem;
3355
3358
  }
@@ -3359,6 +3362,9 @@ input.tab:checked + .tab-content,
3359
3362
  .border-2 {
3360
3363
  border-width: 2px;
3361
3364
  }
3365
+ .border-b {
3366
+ border-bottom-width: 1px;
3367
+ }
3362
3368
  .border-b-2 {
3363
3369
  border-bottom-width: 2px;
3364
3370
  }
@@ -3374,9 +3380,9 @@ input.tab:checked + .tab-content,
3374
3380
  --tw-border-opacity: 1;
3375
3381
  border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
3376
3382
  }
3377
- .border-gray-400 {
3383
+ .border-gray-500 {
3378
3384
  --tw-border-opacity: 1;
3379
- border-color: rgb(156 163 175 / var(--tw-border-opacity, 1));
3385
+ border-color: rgb(107 114 128 / var(--tw-border-opacity, 1));
3380
3386
  }
3381
3387
  .border-slate-500 {
3382
3388
  --tw-border-opacity: 1;
@@ -3477,6 +3483,9 @@ input.tab:checked + .tab-content,
3477
3483
  .font-medium {
3478
3484
  font-weight: 500;
3479
3485
  }
3486
+ .font-semibold {
3487
+ font-weight: 600;
3488
+ }
3480
3489
  .leading-5 {
3481
3490
  line-height: 1.25rem;
3482
3491
  }
@@ -3623,6 +3632,12 @@ input.tab:checked + .tab-content,
3623
3632
  visibility: visible;
3624
3633
  }
3625
3634
  }
3635
+ @container (min-width: 6rem) {
3636
+
3637
+ .\@\[6rem\]\:visible {
3638
+ visibility: visible;
3639
+ }
3640
+ }
3626
3641
  @media (min-width: 640px) {
3627
3642
 
3628
3643
  .sm\:max-w-5xl {
package/dist/util.d.ts CHANGED
@@ -822,7 +822,7 @@ declare global {
822
822
 
823
823
  declare global {
824
824
  interface HTMLElementTagNameMap {
825
- 'gs-app': App;
825
+ 'gs-app': AppComponent;
826
826
  }
827
827
  }
828
828
 
@@ -950,7 +950,7 @@ declare global {
950
950
 
951
951
  declare global {
952
952
  interface HTMLElementTagNameMap {
953
- 'gs-sequences-by-location': SequencesByLocationComponent;
953
+ 'gs-statistics': StatisticsComponent;
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-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
961
+ 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
962
962
  }
963
963
  }
964
964
  }
@@ -966,7 +966,7 @@ declare global {
966
966
 
967
967
  declare global {
968
968
  interface HTMLElementTagNameMap {
969
- 'gs-statistics': StatisticsComponent;
969
+ 'gs-sequences-by-location': SequencesByLocationComponent;
970
970
  }
971
971
  }
972
972
 
@@ -974,7 +974,7 @@ declare global {
974
974
  declare global {
975
975
  namespace JSX {
976
976
  interface IntrinsicElements {
977
- 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
977
+ 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
978
978
  }
979
979
  }
980
980
  }
@@ -1018,10 +1018,10 @@ declare global {
1018
1018
 
1019
1019
  declare global {
1020
1020
  interface HTMLElementTagNameMap {
1021
- 'gs-text-input': TextInputComponent;
1021
+ 'gs-location-filter': LocationFilterComponent;
1022
1022
  }
1023
1023
  interface HTMLElementEventMap {
1024
- 'gs-text-input-changed': TextInputChangedEvent;
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-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
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-location-filter': LocationFilterComponent;
1040
+ 'gs-mutation-filter': MutationFilterComponent;
1041
1041
  }
1042
1042
  interface HTMLElementEventMap {
1043
- 'gs-location-changed': LocationChangedEvent;
1043
+ 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
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-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1051
+ 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1052
1052
  }
1053
1053
  }
1054
1054
  }
@@ -1056,10 +1056,10 @@ declare global {
1056
1056
 
1057
1057
  declare global {
1058
1058
  interface HTMLElementTagNameMap {
1059
- 'gs-mutation-filter': MutationFilterComponent;
1059
+ 'gs-text-input': TextInputComponent;
1060
1060
  }
1061
1061
  interface HTMLElementEventMap {
1062
- 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
1062
+ 'gs-text-input-changed': TextInputChangedEvent;
1063
1063
  }
1064
1064
  }
1065
1065
 
@@ -1067,7 +1067,7 @@ declare global {
1067
1067
  declare global {
1068
1068
  namespace JSX {
1069
1069
  interface IntrinsicElements {
1070
- 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1070
+ 'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1071
1071
  }
1072
1072
  }
1073
1073
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.13.5",
3
+ "version": "0.13.7",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -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 flex flex-col`}
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 flex flex-col'>No elements to select.</li>
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
+ );
@@ -23,8 +23,8 @@ const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
23
23
  key={tab.title}
24
24
  className={`px-4 py-2 text-sm font-medium leading-5 transition-colors duration-150 ${
25
25
  activeTab === tab.title
26
- ? 'border-b-2 border-gray-400'
27
- : 'text-gray-600 hover:bg-gray-100 hover:text-gray-700'
26
+ ? 'border-b-2 border-gray-500'
27
+ : 'border-b border-gray-300 text-gray-600 hover:bg-gray-100 hover:text-gray-700'
28
28
  }`}
29
29
  onClick={() => {
30
30
  setActiveTab(tab.title);
@@ -45,9 +45,7 @@ const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
45
45
  {tabElements}
46
46
  {toolbar && <div className='py-2 flex flex-wrap gap-y-1'>{toolbarElement}</div>}
47
47
  </div>
48
- <div
49
- className={`p-2 flex-grow overflow-scroll border-2 border-gray-100 rounded-b-md rounded-tr-md ${activeTab === tabs[0]?.title ? '' : 'rounded-tl-md'}`}
50
- >
48
+ <div className={`p-2 flex-grow overflow-scroll border-2 border-gray-100 rounded-b-md rounded-tr-md`}>
51
49
  {tabs.map((tab) => (
52
50
  <div className='h-full' key={tab.title} hidden={activeTab !== tab.title}>
53
51
  {tab.content}
@@ -11,11 +11,11 @@ describe('fetchAutocompletionList', () => {
11
11
  { fields, country: 'Germany' },
12
12
  {
13
13
  data: [
14
- { count: 0, region: 'region1', country: 'country1_1', division: 'division1_1_1' },
15
- { count: 0, region: 'region1', country: 'country1_1', division: 'division1_1_2' },
16
- { count: 0, region: 'region1', country: 'country1_1', division: null },
17
- { count: 0, region: 'region1', country: 'country1_2', division: 'division1_2_1' },
18
- { count: 0, region: 'region2', country: 'country2_1', division: null },
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<Record<string, string | undefined>[]> {
15
- const toAncestorInHierarchyOverwriteValues = Array(fields.length - 1)
16
- .fill(0)
17
- .map((_, i) => i + 1)
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) => fields.reduce((acc, field) => ({ ...acc, [field]: entry[field] }), {}))
29
- .reduce<Set<string>>((setOfAllHierarchies, entry) => {
30
- setOfAllHierarchies.add(JSON.stringify(entry));
31
- toAncestorInHierarchyOverwriteValues.forEach((overwriteValues) => {
32
- setOfAllHierarchies.add(JSON.stringify({ ...entry, ...overwriteValues }));
33
- });
34
- return setOfAllHierarchies;
35
- }, new Set());
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) => JSON.parse(json))
45
+ .map<EntryWithNullValues>(([json, count]) => ({
46
+ value: JSON.parse(json),
47
+ count,
48
+ }))
39
49
  .sort(compareLocationEntries(fields))
40
- .map((entry) => fields.reduce((acc, field) => ({ ...acc, [field]: entry[field] ?? undefined }), {}));
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: Record<string, string | null>, b: Record<string, string | null>) => {
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: LapisLocationFilter[];
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 ? toSelectItem(value, fields) : undefined;
82
- }, [fields, value]);
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
- return (
96
- <>
97
+ formatItemInList={(item: SelectItem) => (
98
+ <>
99
+ <p>
97
100
  <span>{item.label}</span>
98
- <span className='text-sm text-gray-500'>{item.description}</span>
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(locationFilter: LapisLocationFilter, fields: string[]): SelectItem | undefined {
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>TODO: https://github.com/GenSpectrum/dashboard-components/issues/465</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
  );
@@ -0,0 +1,68 @@
1
+ import { useContext } from 'preact/hooks';
2
+ import type { FC } from 'react';
3
+
4
+ import { type ReferenceGenome } from '../../lapisApi/ReferenceGenome';
5
+ import type { SequenceType } from '../../types';
6
+ import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
7
+
8
+ type ExampleMutationProps = {
9
+ sequenceType: SequenceType;
10
+ mutationType: 'substitution' | 'insertion';
11
+ };
12
+
13
+ export const ExampleMutation: FC<ExampleMutationProps> = ({ sequenceType, mutationType }) => {
14
+ const referenceGenome = useContext(ReferenceGenomeContext);
15
+
16
+ return <b>{getExampleMutation(referenceGenome, sequenceType, mutationType)}</b>;
17
+ };
18
+
19
+ export function getExampleMutation(
20
+ referenceGenome: ReferenceGenome,
21
+ sequenceType: SequenceType,
22
+ mutationType: 'substitution' | 'insertion',
23
+ ) {
24
+ switch (sequenceType) {
25
+ case 'amino acid': {
26
+ if (referenceGenome.genes.length === 0) {
27
+ return '';
28
+ }
29
+
30
+ const firstGene = referenceGenome.genes[0].name;
31
+
32
+ switch (mutationType) {
33
+ case 'substitution':
34
+ return `${firstGene}:57Q`;
35
+ case 'insertion':
36
+ return `ins_${firstGene}:31:N`;
37
+ }
38
+ }
39
+ // Issue of linter https://github.com/typescript-eslint/typescript-eslint/issues/3455
40
+ // eslint-disable-next-line no-fallthrough
41
+ case 'nucleotide': {
42
+ switch (referenceGenome.nucleotideSequences.length) {
43
+ case 0: {
44
+ return '';
45
+ }
46
+ case 1: {
47
+ switch (mutationType) {
48
+ case 'substitution':
49
+ return '23T';
50
+ case 'insertion':
51
+ return 'ins_1046:A';
52
+ }
53
+ }
54
+ // Issue of linter https://github.com/typescript-eslint/typescript-eslint/issues/3455
55
+ // eslint-disable-next-line no-fallthrough
56
+ default: {
57
+ const firstSegment = referenceGenome.nucleotideSequences[0].name;
58
+ switch (mutationType) {
59
+ case 'substitution':
60
+ return `${firstSegment}:23T`;
61
+ case 'insertion':
62
+ return `ins_${firstSegment}:10462:A`;
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }