@genspectrum/dashboard-components 0.13.0 → 0.13.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.
Files changed (39) hide show
  1. package/custom-elements.json +178 -0
  2. package/dist/assets/mutationOverTimeWorker-B1-WrM4b.js.map +1 -0
  3. package/dist/components.d.ts +108 -43
  4. package/dist/components.js +564 -295
  5. package/dist/components.js.map +1 -1
  6. package/dist/style.css +3 -0
  7. package/dist/util.d.ts +59 -43
  8. package/package.json +2 -2
  9. package/src/constants.ts +6 -0
  10. package/src/lapisApi/__mockData__/wiseReferenceGenome.json +9 -0
  11. package/src/lapisApi/lapisApi.ts +17 -0
  12. package/src/lapisApi/lapisTypes.ts +7 -1
  13. package/src/operator/FetchDetailsOperator.ts +28 -0
  14. package/src/preact/components/downshift-combobox.tsx +18 -20
  15. package/src/preact/components/tabs.tsx +1 -1
  16. package/src/preact/mutationsOverTime/MutationOverTimeData.ts +9 -5
  17. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +5 -3
  18. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +4 -7
  19. package/src/preact/textInput/fetchStringAutocompleteList.spec.ts +34 -0
  20. package/src/preact/textInput/fetchStringAutocompleteList.ts +16 -2
  21. package/src/preact/textInput/text-input.tsx +22 -8
  22. package/src/preact/wastewater/mutationsOverTime/__mockData__/details.json +88 -0
  23. package/src/preact/wastewater/mutationsOverTime/computeWastewaterMutationsOverTimeDataPerLocation.spec.ts +159 -0
  24. package/src/preact/wastewater/mutationsOverTime/computeWastewaterMutationsOverTimeDataPerLocation.ts +51 -0
  25. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +71 -0
  26. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +151 -0
  27. package/src/query/queryMutationsOverTime.ts +6 -14
  28. package/src/query/queryWastewaterMutationsOverTime.spec.ts +94 -0
  29. package/src/query/queryWastewaterMutationsOverTime.ts +55 -0
  30. package/src/utils/map2d.ts +39 -0
  31. package/src/web-components/index.ts +1 -0
  32. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +82 -0
  33. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +112 -0
  34. package/src/web-components/wastewaterVisualization/index.ts +1 -0
  35. package/standalone-bundle/assets/{mutationOverTimeWorker-DEybsZ5r.js.map → mutationOverTimeWorker-Cls1J0cl.js.map} +1 -1
  36. package/standalone-bundle/dashboard-components.js +6228 -6008
  37. package/standalone-bundle/dashboard-components.js.map +1 -1
  38. package/standalone-bundle/style.css +1 -1
  39. package/dist/assets/mutationOverTimeWorker-DTv93Ere.js.map +0 -1
package/dist/style.css CHANGED
@@ -3199,6 +3199,9 @@ input.tab:checked + .tab-content,
3199
3199
  .grid {
3200
3200
  display: grid;
3201
3201
  }
3202
+ .contents {
3203
+ display: contents;
3204
+ }
3202
3205
  .hidden {
3203
3206
  display: none;
3204
3207
  }
package/dist/util.d.ts CHANGED
@@ -838,7 +838,11 @@ declare global {
838
838
 
839
839
  declare global {
840
840
  interface HTMLElementTagNameMap {
841
- 'gs-mutation-comparison-component': MutationComparisonComponent;
841
+ 'gs-date-range-selector': DateRangeSelectorComponent;
842
+ }
843
+ interface HTMLElementEventMap {
844
+ 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
845
+ 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
842
846
  }
843
847
  }
844
848
 
@@ -846,7 +850,7 @@ declare global {
846
850
  declare global {
847
851
  namespace JSX {
848
852
  interface IntrinsicElements {
849
- 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
853
+ 'gs-date-range-selector': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
850
854
  }
851
855
  }
852
856
  }
@@ -854,7 +858,10 @@ declare global {
854
858
 
855
859
  declare global {
856
860
  interface HTMLElementTagNameMap {
857
- 'gs-mutations-component': MutationsComponent;
861
+ 'gs-location-filter': LocationFilterComponent;
862
+ }
863
+ interface HTMLElementEventMap {
864
+ 'gs-location-changed': LocationChangedEvent;
858
865
  }
859
866
  }
860
867
 
@@ -862,7 +869,7 @@ declare global {
862
869
  declare global {
863
870
  namespace JSX {
864
871
  interface IntrinsicElements {
865
- 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
872
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
866
873
  }
867
874
  }
868
875
  }
@@ -870,7 +877,10 @@ declare global {
870
877
 
871
878
  declare global {
872
879
  interface HTMLElementTagNameMap {
873
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
880
+ 'gs-text-input': TextInputComponent;
881
+ }
882
+ interface HTMLElementEventMap {
883
+ 'gs-text-input-changed': TextInputChangedEvent;
874
884
  }
875
885
  }
876
886
 
@@ -878,7 +888,7 @@ declare global {
878
888
  declare global {
879
889
  namespace JSX {
880
890
  interface IntrinsicElements {
881
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
891
+ 'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
882
892
  }
883
893
  }
884
894
  }
@@ -886,7 +896,10 @@ declare global {
886
896
 
887
897
  declare global {
888
898
  interface HTMLElementTagNameMap {
889
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
899
+ 'gs-mutation-filter': MutationFilterComponent;
900
+ }
901
+ interface HTMLElementEventMap {
902
+ 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
890
903
  }
891
904
  }
892
905
 
@@ -894,7 +907,7 @@ declare global {
894
907
  declare global {
895
908
  namespace JSX {
896
909
  interface IntrinsicElements {
897
- 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
910
+ 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
898
911
  }
899
912
  }
900
913
  }
@@ -902,7 +915,10 @@ declare global {
902
915
 
903
916
  declare global {
904
917
  interface HTMLElementTagNameMap {
905
- 'gs-aggregate': AggregateComponent;
918
+ 'gs-lineage-filter': LineageFilterComponent;
919
+ }
920
+ interface HTMLElementEventMap {
921
+ 'gs-lineage-filter-changed': LineageFilterChangedEvent;
906
922
  }
907
923
  }
908
924
 
@@ -910,7 +926,7 @@ declare global {
910
926
  declare global {
911
927
  namespace JSX {
912
928
  interface IntrinsicElements {
913
- 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
929
+ 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
914
930
  }
915
931
  }
916
932
  }
@@ -918,7 +934,7 @@ declare global {
918
934
 
919
935
  declare global {
920
936
  interface HTMLElementTagNameMap {
921
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
937
+ 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
922
938
  }
923
939
  }
924
940
 
@@ -926,7 +942,7 @@ declare global {
926
942
  declare global {
927
943
  namespace JSX {
928
944
  interface IntrinsicElements {
929
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
945
+ 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
930
946
  }
931
947
  }
932
948
  }
@@ -934,7 +950,7 @@ declare global {
934
950
 
935
951
  declare global {
936
952
  interface HTMLElementTagNameMap {
937
- 'gs-mutations-over-time': MutationsOverTimeComponent;
953
+ 'gs-mutation-comparison-component': MutationComparisonComponent;
938
954
  }
939
955
  }
940
956
 
@@ -942,7 +958,7 @@ declare global {
942
958
  declare global {
943
959
  namespace JSX {
944
960
  interface IntrinsicElements {
945
- 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
961
+ 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
946
962
  }
947
963
  }
948
964
  }
@@ -950,7 +966,7 @@ declare global {
950
966
 
951
967
  declare global {
952
968
  interface HTMLElementTagNameMap {
953
- 'gs-sequences-by-location': SequencesByLocationComponent;
969
+ 'gs-mutations-component': MutationsComponent;
954
970
  }
955
971
  }
956
972
 
@@ -958,7 +974,7 @@ declare global {
958
974
  declare global {
959
975
  namespace JSX {
960
976
  interface IntrinsicElements {
961
- 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
977
+ 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
962
978
  }
963
979
  }
964
980
  }
@@ -966,7 +982,7 @@ declare global {
966
982
 
967
983
  declare global {
968
984
  interface HTMLElementTagNameMap {
969
- 'gs-statistics': StatisticsComponent;
985
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
970
986
  }
971
987
  }
972
988
 
@@ -974,7 +990,7 @@ declare global {
974
990
  declare global {
975
991
  namespace JSX {
976
992
  interface IntrinsicElements {
977
- 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
993
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
978
994
  }
979
995
  }
980
996
  }
@@ -982,11 +998,7 @@ declare global {
982
998
 
983
999
  declare global {
984
1000
  interface HTMLElementTagNameMap {
985
- 'gs-date-range-selector': DateRangeSelectorComponent;
986
- }
987
- interface HTMLElementEventMap {
988
- 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
989
- 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
1001
+ 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
990
1002
  }
991
1003
  }
992
1004
 
@@ -994,7 +1006,7 @@ declare global {
994
1006
  declare global {
995
1007
  namespace JSX {
996
1008
  interface IntrinsicElements {
997
- 'gs-date-range-selector': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1009
+ 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
998
1010
  }
999
1011
  }
1000
1012
  }
@@ -1002,10 +1014,7 @@ declare global {
1002
1014
 
1003
1015
  declare global {
1004
1016
  interface HTMLElementTagNameMap {
1005
- 'gs-location-filter': LocationFilterComponent;
1006
- }
1007
- interface HTMLElementEventMap {
1008
- 'gs-location-changed': LocationChangedEvent;
1017
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1009
1018
  }
1010
1019
  }
1011
1020
 
@@ -1013,7 +1022,7 @@ declare global {
1013
1022
  declare global {
1014
1023
  namespace JSX {
1015
1024
  interface IntrinsicElements {
1016
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1025
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1017
1026
  }
1018
1027
  }
1019
1028
  }
@@ -1021,10 +1030,7 @@ declare global {
1021
1030
 
1022
1031
  declare global {
1023
1032
  interface HTMLElementTagNameMap {
1024
- 'gs-text-input': TextInputComponent;
1025
- }
1026
- interface HTMLElementEventMap {
1027
- 'gs-text-input-changed': TextInputChangedEvent;
1033
+ 'gs-aggregate': AggregateComponent;
1028
1034
  }
1029
1035
  }
1030
1036
 
@@ -1032,7 +1038,7 @@ declare global {
1032
1038
  declare global {
1033
1039
  namespace JSX {
1034
1040
  interface IntrinsicElements {
1035
- 'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1041
+ 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1036
1042
  }
1037
1043
  }
1038
1044
  }
@@ -1040,10 +1046,7 @@ declare global {
1040
1046
 
1041
1047
  declare global {
1042
1048
  interface HTMLElementTagNameMap {
1043
- 'gs-mutation-filter': MutationFilterComponent;
1044
- }
1045
- interface HTMLElementEventMap {
1046
- 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
1049
+ 'gs-mutations-over-time': MutationsOverTimeComponent;
1047
1050
  }
1048
1051
  }
1049
1052
 
@@ -1051,7 +1054,7 @@ declare global {
1051
1054
  declare global {
1052
1055
  namespace JSX {
1053
1056
  interface IntrinsicElements {
1054
- 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1057
+ 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1055
1058
  }
1056
1059
  }
1057
1060
  }
@@ -1059,10 +1062,23 @@ declare global {
1059
1062
 
1060
1063
  declare global {
1061
1064
  interface HTMLElementTagNameMap {
1062
- 'gs-lineage-filter': LineageFilterComponent;
1065
+ 'gs-sequences-by-location': SequencesByLocationComponent;
1063
1066
  }
1064
- interface HTMLElementEventMap {
1065
- 'gs-lineage-filter-changed': LineageFilterChangedEvent;
1067
+ }
1068
+
1069
+
1070
+ declare global {
1071
+ namespace JSX {
1072
+ interface IntrinsicElements {
1073
+ 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+
1079
+ declare global {
1080
+ interface HTMLElementTagNameMap {
1081
+ 'gs-statistics': StatisticsComponent;
1066
1082
  }
1067
1083
  }
1068
1084
 
@@ -1070,7 +1086,7 @@ declare global {
1070
1086
  declare global {
1071
1087
  namespace JSX {
1072
1088
  interface IntrinsicElements {
1073
- 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1089
+ 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1074
1090
  }
1075
1091
  }
1076
1092
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -142,6 +142,6 @@
142
142
  "typescript": "~5.7.2",
143
143
  "vite": "^6.0.3",
144
144
  "vite-plugin-dts": "^4.0.3",
145
- "vitest": "^2.0.1"
145
+ "vitest": "^3.0.2"
146
146
  }
147
147
  }
package/src/constants.ts CHANGED
@@ -5,3 +5,9 @@ export const NUCLEOTIDE_MUTATIONS_ENDPOINT = `${LAPIS_URL}/sample/nucleotideMuta
5
5
  export const AMINO_ACID_MUTATIONS_ENDPOINT = `${LAPIS_URL}/sample/aminoAcidMutations`;
6
6
  export const NUCLEOTIDE_INSERTIONS_ENDPOINT = `${LAPIS_URL}/sample/nucleotideInsertions`;
7
7
  export const REFERENCE_GENOME_ENDPOINT = `${LAPIS_URL}/sample/referenceGenome`;
8
+
9
+ // WISE Wastewater
10
+ // This is a special instance for storing Swiss wastewater data generated by the WISE consortium
11
+ export const WISE_LAPIS_URL = 'https://api.wise-loculus.genspectrum.org/rsv';
12
+ export const WISE_DETAILS_ENDPOINT = `${WISE_LAPIS_URL}/sample/details`;
13
+ export const WISE_REFERENCE_GENOME_ENDPOINT = `${WISE_LAPIS_URL}/sample/referenceGenome`;
@@ -0,0 +1,9 @@
1
+ {
2
+ "nucleotideSequences": [
3
+ {
4
+ "name": "main",
5
+ "sequence": "NNN"
6
+ }
7
+ ],
8
+ "genes": []
9
+ }
@@ -1,6 +1,7 @@
1
1
  import { referenceGenomeResponse } from './ReferenceGenome';
2
2
  import {
3
3
  aggregatedResponse,
4
+ detailsResponse,
4
5
  insertionsResponse,
5
6
  type LapisBaseRequest,
6
7
  lapisError,
@@ -51,6 +52,21 @@ export async function fetchAggregated(lapisUrl: string, body: LapisBaseRequest,
51
52
  return aggregatedResponse.parse(await response.json());
52
53
  }
53
54
 
55
+ export async function fetchDetails(lapisUrl: string, body: LapisBaseRequest, signal?: AbortSignal) {
56
+ const response = await fetch(detailsEndpoint(lapisUrl), {
57
+ method: 'POST',
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ body: JSON.stringify(body),
62
+ signal,
63
+ });
64
+
65
+ await handleErrors(response, 'aggregated data');
66
+
67
+ return detailsResponse.parse(await response.json());
68
+ }
69
+
54
70
  export async function fetchInsertions(
55
71
  lapisUrl: string,
56
72
  body: LapisBaseRequest,
@@ -163,6 +179,7 @@ const handleErrors = async (response: Response, requestedData: string) => {
163
179
  };
164
180
 
165
181
  export const aggregatedEndpoint = (lapisUrl: string) => `${lapisUrl}/sample/aggregated`;
182
+ export const detailsEndpoint = (lapisUrl: string) => `${lapisUrl}/sample/details`;
166
183
  export const insertionsEndpoint = (lapisUrl: string, sequenceType: SequenceType) => {
167
184
  return sequenceType === 'amino acid'
168
185
  ? `${lapisUrl}/sample/aminoAcidInsertions`
@@ -41,10 +41,16 @@ const insertionCount = z.object({
41
41
  });
42
42
  export const insertionsResponse = makeLapisResponse(z.array(insertionCount));
43
43
 
44
- export const aggregatedItem = z.object({ count: z.number() }).catchall(z.union([z.string(), z.number(), z.null()]));
44
+ const baseResponseValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
45
+
46
+ export const aggregatedItem = z.object({ count: z.number() }).catchall(baseResponseValueSchema);
45
47
  export const aggregatedResponse = makeLapisResponse(z.array(aggregatedItem));
46
48
  export type AggregatedItem = z.infer<typeof aggregatedItem>;
47
49
 
50
+ export const detailsItem = z.object({}).catchall(baseResponseValueSchema);
51
+ export const detailsResponse = makeLapisResponse(z.array(detailsItem));
52
+ export type DetailsItem = z.infer<typeof detailsItem>;
53
+
48
54
  function makeLapisResponse<T extends ZodTypeAny>(data: T) {
49
55
  return z.object({
50
56
  data,
@@ -0,0 +1,28 @@
1
+ import { type Dataset } from './Dataset';
2
+ import { type Operator } from './Operator';
3
+ import { fetchDetails } from '../lapisApi/lapisApi';
4
+ import { type LapisFilter } from '../types';
5
+
6
+ type Details<Fields extends string> = { [field in Fields]: string | number | boolean | null };
7
+
8
+ export class FetchDetailsOperator<Fields extends string> implements Operator<Details<Fields>> {
9
+ constructor(
10
+ private filter: LapisFilter,
11
+ private fields: Fields[] = [],
12
+ ) {}
13
+
14
+ async evaluate(lapisUrl: string, signal?: AbortSignal): Promise<Dataset<Details<Fields>>> {
15
+ const detailsResponse = (
16
+ await fetchDetails(
17
+ lapisUrl,
18
+ {
19
+ ...this.filter,
20
+ fields: this.fields,
21
+ },
22
+ signal,
23
+ )
24
+ ).data;
25
+
26
+ return { content: detailsResponse as Details<Fields>[] };
27
+ }
28
+ }
@@ -120,26 +120,24 @@ export function DownshiftCombobox<Item>({
120
120
  </button>
121
121
  </div>
122
122
  </div>
123
- {isOpen && (
124
- <ul
125
- className='absolute bg-white mt-1 shadow-md max-h-80 overflow-scroll z-10 w-full min-w-32'
126
- {...getMenuProps()}
127
- >
128
- {items.length > 0 ? (
129
- items.map((item, index) => (
130
- <li
131
- className={`${highlightedIndex === index ? 'bg-blue-300' : ''} ${selectedItem !== null && itemToString(selectedItem) === itemToString(item) ? 'font-bold' : ''} py-2 px-3 shadow-sm flex flex-col`}
132
- key={itemToString(item)}
133
- {...getItemProps({ item, index })}
134
- >
135
- {formatItemInList(item)}
136
- </li>
137
- ))
138
- ) : (
139
- <li className='py-2 px-3 shadow-sm flex flex-col'>No elements to select.</li>
140
- )}
141
- </ul>
142
- )}
123
+ <ul
124
+ className={`absolute bg-white mt-1 shadow-md max-h-80 overflow-scroll z-10 w-full min-w-32 ${isOpen ? '' : 'hidden'}`}
125
+ {...getMenuProps()}
126
+ >
127
+ {items.length > 0 ? (
128
+ items.map((item, index) => (
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`}
131
+ key={itemToString(item)}
132
+ {...getItemProps({ item, index })}
133
+ >
134
+ {formatItemInList(item)}
135
+ </li>
136
+ ))
137
+ ) : (
138
+ <li className='py-2 px-3 shadow-sm flex flex-col'>No elements to select.</li>
139
+ )}
140
+ </ul>
143
141
  </div>
144
142
  );
145
143
  }
@@ -16,7 +16,7 @@ const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
16
16
  const [activeTab, setActiveTab] = useState(tabs[0]?.title);
17
17
 
18
18
  const tabElements = (
19
- <div className='flex flex-row'>
19
+ <div className='flex flex-row flex-wrap'>
20
20
  {tabs.map((tab) => {
21
21
  return (
22
22
  <button
@@ -5,16 +5,20 @@ import {
5
5
  } from '../../query/queryMutationsOverTime';
6
6
  import { type Map2d, Map2dBase, type Map2DContents } from '../../utils/map2d';
7
7
  import type { Deletion, Substitution } from '../../utils/mutations';
8
- import type { Temporal } from '../../utils/temporalClass';
8
+ import type { Temporal, TemporalClass } from '../../utils/temporalClass';
9
9
 
10
- export type MutationOverTimeDataMap = Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>;
10
+ export type MutationOverTimeDataMap<T extends Temporal | TemporalClass = Temporal> = Map2d<
11
+ Substitution | Deletion,
12
+ T,
13
+ MutationOverTimeMutationValue
14
+ >;
11
15
 
12
- export class BaseMutationOverTimeDataMap extends Map2dBase<
16
+ export class BaseMutationOverTimeDataMap<T extends Temporal | TemporalClass = Temporal> extends Map2dBase<
13
17
  Substitution | Deletion,
14
- Temporal,
18
+ T,
15
19
  MutationOverTimeMutationValue
16
20
  > {
17
- constructor(initialContent?: Map2DContents<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>) {
21
+ constructor(initialContent?: Map2DContents<Substitution | Deletion, T, MutationOverTimeMutationValue>) {
18
22
  super(serializeSubstitutionOrDeletion, serializeTemporal, initialContent);
19
23
  }
20
24
  }
@@ -111,9 +111,11 @@ const ProportionCell: FunctionComponent<{
111
111
  ) : (
112
112
  <>
113
113
  <p>Proportion: {formatProportion(value.proportion)}</p>
114
- <p>
115
- Count: {value.count} / {value.totalCount} total
116
- </p>
114
+ {value.count !== null && value.totalCount !== null && (
115
+ <p>
116
+ Count: {value.count} / {value.totalCount} total
117
+ </p>
118
+ )}
117
119
  </>
118
120
  )}
119
121
  </div>
@@ -1,9 +1,6 @@
1
- import { DeletionClass, type SubstitutionClass } from '../../../utils/mutations';
1
+ import type { Deletion, Substitution } from '../../../utils/mutations';
2
2
 
3
- export const sortSubstitutionsAndDeletions = (
4
- a: SubstitutionClass | DeletionClass,
5
- b: SubstitutionClass | DeletionClass,
6
- ) => {
3
+ export const sortSubstitutionsAndDeletions = (a: Substitution | Deletion, b: Substitution | Deletion) => {
7
4
  if (a.segment !== b.segment) {
8
5
  return compareSegments(a.segment, b.segment);
9
6
  }
@@ -12,8 +9,8 @@ export const sortSubstitutionsAndDeletions = (
12
9
  return comparePositions(a.position, b.position);
13
10
  }
14
11
 
15
- const aIsDeletion = a instanceof DeletionClass;
16
- const bIsDeletion = b instanceof DeletionClass;
12
+ const aIsDeletion = a.type === 'deletion';
13
+ const bIsDeletion = b.type === 'deletion';
17
14
 
18
15
  if (aIsDeletion !== bIsDeletion) {
19
16
  return aIsDeletion ? 1 : -1;
@@ -0,0 +1,34 @@
1
+ import { describe, expect, test } from 'vitest';
2
+
3
+ import { fetchStringAutocompleteList } from './fetchStringAutocompleteList';
4
+ import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../../vitest.setup';
5
+
6
+ describe('fetchStringAutocompleteList', () => {
7
+ test('should fetch autocompletion list and sort by field value', async () => {
8
+ const field = 'host';
9
+
10
+ lapisRequestMocks.aggregated(
11
+ { fields: [field], country: 'Germany' },
12
+ {
13
+ data: [
14
+ { count: 1, host: 'host_c' },
15
+ { count: 2, host: 'host_b' },
16
+ { count: 3, host: null },
17
+ { count: 4, host: 'host_a' },
18
+ ],
19
+ },
20
+ );
21
+
22
+ const result = await fetchStringAutocompleteList({
23
+ field,
24
+ lapis: DUMMY_LAPIS_URL,
25
+ lapisFilter: { country: 'Germany' },
26
+ });
27
+
28
+ expect(result).to.deep.equal([
29
+ { count: 4, value: 'host_a' },
30
+ { count: 2, value: 'host_b' },
31
+ { count: 1, value: 'host_c' },
32
+ ]);
33
+ });
34
+ });
@@ -12,9 +12,23 @@ export async function fetchStringAutocompleteList({
12
12
  lapisFilter?: LapisFilter;
13
13
  signal?: AbortSignal;
14
14
  }) {
15
- const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string>>(lapisFilter ?? {}, [field]);
15
+ const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string | null>>(lapisFilter ?? {}, [
16
+ field,
17
+ ]);
16
18
 
17
19
  const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
18
20
 
19
- return data.map((item) => item[field]).sort();
21
+ return data
22
+ .map((item) => ({ count: item.count, value: item[field] }))
23
+ .filter((item): item is { count: number; value: string } => item.value !== null)
24
+ .sort((a, b) => {
25
+ if (a.value === null) {
26
+ return 1;
27
+ }
28
+ if (b.value === null) {
29
+ return -1;
30
+ }
31
+
32
+ return a.value.localeCompare(b.value);
33
+ });
20
34
  }
@@ -68,32 +68,46 @@ const TextInputInner: FunctionComponent<TextInputInnerProps> = ({
68
68
  return <TextSelector lapisField={lapisField} value={value} placeholderText={placeholderText} data={data} />;
69
69
  };
70
70
 
71
+ type SelectItem = {
72
+ count: number;
73
+ value: string;
74
+ };
75
+
71
76
  const TextSelector = ({
72
77
  lapisField,
73
78
  value,
74
79
  placeholderText,
75
80
  data,
76
81
  }: TextSelectorProps & {
77
- data: string[];
82
+ data: SelectItem[];
78
83
  }) => {
84
+ const initialSelectedItem = data.find((candidate) => candidate.value == value);
85
+
79
86
  return (
80
87
  <DownshiftCombobox
81
88
  allItems={data}
82
- value={value}
89
+ value={initialSelectedItem}
83
90
  filterItemsByInputValue={filterByInputValue}
84
- createEvent={(item: string | null) => new TextInputChangedEvent({ [lapisField]: item ?? undefined })}
85
- itemToString={(item: string | undefined | null) => item ?? ''}
91
+ createEvent={(item: SelectItem | null) =>
92
+ new TextInputChangedEvent({ [lapisField]: item?.value ?? undefined })
93
+ }
94
+ itemToString={(item: SelectItem | undefined | null) => item?.value ?? ''}
86
95
  placeholderText={placeholderText}
87
- formatItemInList={(item: string) => {
88
- return <span>{item}</span>;
96
+ formatItemInList={(item: SelectItem) => {
97
+ return (
98
+ <p>
99
+ <span>{item.value}</span>
100
+ <span className='ml-2 text-gray-500'>({item.count})</span>
101
+ </p>
102
+ );
89
103
  }}
90
104
  />
91
105
  );
92
106
  };
93
107
 
94
- function filterByInputValue(item: string, inputValue: string | undefined | null) {
108
+ function filterByInputValue(item: SelectItem, inputValue: string | undefined | null) {
95
109
  if (inputValue === undefined || inputValue === null || inputValue === '') {
96
110
  return true;
97
111
  }
98
- return item?.toLowerCase().includes(inputValue?.toLowerCase() || '');
112
+ return item.value?.toLowerCase().includes(inputValue?.toLowerCase() || '');
99
113
  }