@genspectrum/dashboard-components 0.11.2 → 0.11.4

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/style.css CHANGED
@@ -482,7 +482,7 @@ input[type="range"] {
482
482
  --tw-contain-paint: ;
483
483
  --tw-contain-style: ;
484
484
  }/*
485
- ! tailwindcss v3.4.16 | MIT License | https://tailwindcss.com
485
+ ! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
486
486
  *//*
487
487
  1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
488
488
  2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
@@ -1154,6 +1154,7 @@ html {
1154
1154
  display: grid;
1155
1155
  width: 100%;
1156
1156
  overflow: hidden;
1157
+ direction: ltr;
1157
1158
  container-type: inline-size;
1158
1159
  grid-template-columns: auto 1fr;
1159
1160
  }
@@ -2684,9 +2685,6 @@ input.tab:checked + .tab-content,
2684
2685
  border-end-end-radius: inherit;
2685
2686
  border-start-end-radius: inherit;
2686
2687
  }
2687
- .modal-middle {
2688
- place-items: center;
2689
- }
2690
2688
  .modal-bottom {
2691
2689
  place-items: end;
2692
2690
  }
@@ -3131,9 +3129,6 @@ input.tab:checked + .tab-content,
3131
3129
  .min-w-\[7\.5rem\] {
3132
3130
  min-width: 7.5rem;
3133
3131
  }
3134
- .max-w-3xl {
3135
- max-width: 48rem;
3136
- }
3137
3132
  .max-w-screen-lg {
3138
3133
  max-width: 1024px;
3139
3134
  }
@@ -3307,10 +3302,6 @@ input.tab:checked + .tab-content,
3307
3302
  padding-top: 0.5rem;
3308
3303
  padding-bottom: 0.5rem;
3309
3304
  }
3310
- .py-4 {
3311
- padding-top: 1rem;
3312
- padding-bottom: 1rem;
3313
- }
3314
3305
  .pl-2 {
3315
3306
  padding-left: 0.5rem;
3316
3307
  }
package/dist/util.d.ts CHANGED
@@ -787,7 +787,7 @@ declare global {
787
787
 
788
788
  declare global {
789
789
  interface HTMLElementTagNameMap {
790
- 'gs-mutation-comparison-component': MutationComparisonComponent;
790
+ 'gs-mutations-component': MutationsComponent;
791
791
  }
792
792
  }
793
793
 
@@ -795,7 +795,7 @@ declare global {
795
795
  declare global {
796
796
  namespace JSX {
797
797
  interface IntrinsicElements {
798
- 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
798
+ 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
799
799
  }
800
800
  }
801
801
  }
@@ -803,7 +803,7 @@ declare global {
803
803
 
804
804
  declare global {
805
805
  interface HTMLElementTagNameMap {
806
- 'gs-mutations-component': MutationsComponent;
806
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
807
807
  }
808
808
  }
809
809
 
@@ -811,7 +811,7 @@ declare global {
811
811
  declare global {
812
812
  namespace JSX {
813
813
  interface IntrinsicElements {
814
- 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
814
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
815
815
  }
816
816
  }
817
817
  }
@@ -819,7 +819,7 @@ declare global {
819
819
 
820
820
  declare global {
821
821
  interface HTMLElementTagNameMap {
822
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
822
+ 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
823
823
  }
824
824
  }
825
825
 
@@ -827,7 +827,7 @@ declare global {
827
827
  declare global {
828
828
  namespace JSX {
829
829
  interface IntrinsicElements {
830
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
830
+ 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
831
831
  }
832
832
  }
833
833
  }
@@ -835,7 +835,7 @@ declare global {
835
835
 
836
836
  declare global {
837
837
  interface HTMLElementTagNameMap {
838
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
838
+ 'gs-aggregate': AggregateComponent;
839
839
  }
840
840
  }
841
841
 
@@ -843,7 +843,7 @@ declare global {
843
843
  declare global {
844
844
  namespace JSX {
845
845
  interface IntrinsicElements {
846
- 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
846
+ 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
847
847
  }
848
848
  }
849
849
  }
@@ -851,7 +851,7 @@ declare global {
851
851
 
852
852
  declare global {
853
853
  interface HTMLElementTagNameMap {
854
- 'gs-aggregate': AggregateComponent;
854
+ 'gs-mutation-comparison-component': MutationComparisonComponent;
855
855
  }
856
856
  }
857
857
 
@@ -859,7 +859,7 @@ declare global {
859
859
  declare global {
860
860
  namespace JSX {
861
861
  interface IntrinsicElements {
862
- 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
862
+ 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
863
863
  }
864
864
  }
865
865
  }
@@ -883,7 +883,7 @@ declare global {
883
883
 
884
884
  declare global {
885
885
  interface HTMLElementTagNameMap {
886
- 'gs-mutations-over-time': MutationsOverTimeComponent;
886
+ 'gs-sequences-by-location': SequencesByLocationComponent;
887
887
  }
888
888
  }
889
889
 
@@ -891,7 +891,7 @@ declare global {
891
891
  declare global {
892
892
  namespace JSX {
893
893
  interface IntrinsicElements {
894
- 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
894
+ 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
895
895
  }
896
896
  }
897
897
  }
@@ -899,7 +899,7 @@ declare global {
899
899
 
900
900
  declare global {
901
901
  interface HTMLElementTagNameMap {
902
- 'gs-sequences-by-location': SequencesByLocationComponent;
902
+ 'gs-mutations-over-time': MutationsOverTimeComponent;
903
903
  }
904
904
  }
905
905
 
@@ -907,7 +907,7 @@ declare global {
907
907
  declare global {
908
908
  namespace JSX {
909
909
  interface IntrinsicElements {
910
- 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
910
+ 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
911
911
  }
912
912
  }
913
913
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -109,7 +109,7 @@
109
109
  "@storybook/preact": "^8.0.9",
110
110
  "@storybook/preact-vite": "^8.0.9",
111
111
  "@storybook/test": "^8.0.0",
112
- "@storybook/test-runner": "^0.20.1",
112
+ "@storybook/test-runner": "^0.21.0",
113
113
  "@storybook/types": "^8.0.9",
114
114
  "@storybook/web-components": "^8.0.9",
115
115
  "@storybook/web-components-vite": "^8.0.9",
@@ -53,7 +53,7 @@ export const AggregateInner: FunctionComponent<AggregateProps> = (componentProps
53
53
  field: initialSortField,
54
54
  direction: initialSortDirection,
55
55
  });
56
- }, [lapisFilter, fields, lapis]);
56
+ }, [lapisFilter, fields, lapis, initialSortField, initialSortDirection]);
57
57
 
58
58
  if (isLoading) {
59
59
  return <LoadingDisplay />;
@@ -2,6 +2,8 @@ import { type FunctionComponent } from 'preact';
2
2
  import { useEffect, useRef } from 'preact/hooks';
3
3
  import { type ZodError } from 'zod';
4
4
 
5
+ import { InfoHeadline1, InfoParagraph } from './info';
6
+ import { Modal, useModalRef } from './modal';
5
7
  import { LapisError, UnknownLapisError } from '../../lapisApi/lapisApi';
6
8
 
7
9
  export const GS_ERROR_EVENT_TYPE = 'gs-error';
@@ -46,7 +48,7 @@ export const ErrorDisplay: FunctionComponent<ErrorDisplayProps> = ({ error, rese
46
48
  console.error(error);
47
49
 
48
50
  const containerRef = useRef<HTMLInputElement>(null);
49
- const ref = useRef<HTMLDialogElement>(null);
51
+ const modalRef = useModalRef();
50
52
 
51
53
  useEffect(() => {
52
54
  containerRef.current?.dispatchEvent(new ErrorEvent(error));
@@ -66,23 +68,16 @@ export const ErrorDisplay: FunctionComponent<ErrorDisplayProps> = ({ error, rese
66
68
  {details !== undefined && (
67
69
  <>
68
70
  {' '}
69
- <button className='underline hover:text-gray-400' onClick={() => ref.current?.showModal()}>
71
+ <button
72
+ className='underline hover:text-gray-400'
73
+ onClick={() => modalRef.current?.showModal()}
74
+ >
70
75
  Show details.
71
76
  </button>
72
- <dialog ref={ref} class='modal'>
73
- <div class='modal-box'>
74
- <form method='dialog'>
75
- <button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'>
76
-
77
- </button>
78
- </form>
79
- <h1 class='text-lg'>{details.headline}</h1>
80
- <div class='py-4'>{details.message}</div>
81
- </div>
82
- <form method='dialog' class='modal-backdrop'>
83
- <button>close</button>
84
- </form>
85
- </dialog>
77
+ <Modal modalRef={modalRef}>
78
+ <InfoHeadline1>{details.headline}</InfoHeadline1>
79
+ <InfoParagraph>{details.message}</InfoParagraph>
80
+ </Modal>
86
81
  </>
87
82
  )}
88
83
  </div>
@@ -1,11 +1,12 @@
1
1
  import { type FunctionComponent } from 'preact';
2
- import { useRef } from 'preact/hooks';
2
+
3
+ import { Modal, useModalRef } from './modal';
3
4
 
4
5
  const Info: FunctionComponent = ({ children }) => {
5
- const dialogRef = useRef<HTMLDialogElement>(null);
6
+ const modalRef = useModalRef();
6
7
 
7
8
  const toggleHelp = () => {
8
- dialogRef.current?.showModal();
9
+ modalRef.current?.showModal();
9
10
  };
10
11
 
11
12
  return (
@@ -13,22 +14,7 @@ const Info: FunctionComponent = ({ children }) => {
13
14
  <button type='button' className='btn btn-xs' onClick={toggleHelp}>
14
15
  ?
15
16
  </button>
16
- <dialog ref={dialogRef} className={'modal modal-bottom sm:modal-middle'}>
17
- <div className='modal-box sm:max-w-5xl'>
18
- <form method='dialog'>
19
- <button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'>✕</button>
20
- </form>
21
- <div className={'flex flex-col'}>{children}</div>
22
- <div className='modal-action'>
23
- <form method='dialog'>
24
- <button className={'float-right underline text-sm hover:text-blue-700 mr-2'}>Close</button>
25
- </form>
26
- </div>
27
- </div>
28
- <form method='dialog' className='modal-backdrop'>
29
- <button>Helper to close when clicked outside</button>
30
- </form>
31
- </dialog>
17
+ <Modal modalRef={modalRef}>{children}</Modal>
32
18
  </div>
33
19
  );
34
20
  };
@@ -0,0 +1,44 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, waitFor, within } from '@storybook/test';
3
+ import { type FunctionComponent } from 'preact';
4
+
5
+ import { Modal, type ModalProps, useModalRef } from './modal';
6
+
7
+ const meta: Meta<ModalProps> = {
8
+ title: 'Component/Modal',
9
+ component: Modal,
10
+ parameters: { fetchMock: {} },
11
+ };
12
+
13
+ export default meta;
14
+
15
+ const WrapperWithButtonThatOpensTheModal: FunctionComponent = () => {
16
+ const modalRef = useModalRef();
17
+
18
+ return (
19
+ <div>
20
+ <button className='btn' onClick={() => modalRef.current?.showModal()}>
21
+ Open modal
22
+ </button>
23
+ <Modal modalRef={modalRef}>
24
+ <h1>Modal content</h1>
25
+ </Modal>
26
+ </div>
27
+ );
28
+ };
29
+
30
+ export const ModalStory: StoryObj<ModalProps> = {
31
+ render: () => {
32
+ return <WrapperWithButtonThatOpensTheModal />;
33
+ },
34
+ play: async ({ canvasElement, step }) => {
35
+ const canvas = within(canvasElement);
36
+
37
+ await step('Open the modal', async () => {
38
+ const button = canvas.getByText('Open modal');
39
+ button.click();
40
+
41
+ await waitFor(() => expect(canvas.getByText('Modal content')).toBeVisible());
42
+ });
43
+ },
44
+ };
@@ -0,0 +1,31 @@
1
+ import { type FunctionComponent, type Ref } from 'preact';
2
+ import { useRef } from 'preact/hooks';
3
+
4
+ export type ModalProps = {
5
+ modalRef: Ref<HTMLDialogElement>;
6
+ };
7
+
8
+ export function useModalRef() {
9
+ return useRef<HTMLDialogElement>(null);
10
+ }
11
+
12
+ export const Modal: FunctionComponent<ModalProps> = ({ children, modalRef }) => {
13
+ return (
14
+ <dialog ref={modalRef} className={'modal modal-bottom sm:modal-middle'}>
15
+ <div className='modal-box sm:max-w-5xl'>
16
+ <form method='dialog'>
17
+ <button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'>✕</button>
18
+ </form>
19
+ <div className={'flex flex-col'}>{children}</div>
20
+ <div className='modal-action'>
21
+ <form method='dialog'>
22
+ <button className={'float-right underline text-sm hover:text-blue-700 mr-2'}>Close</button>
23
+ </form>
24
+ </div>
25
+ </div>
26
+ <form method='dialog' className='modal-backdrop'>
27
+ <button>Helper to close when clicked outside</button>
28
+ </form>
29
+ </dialog>
30
+ );
31
+ };
@@ -45,7 +45,7 @@
45
45
  "division": "Saxony-Anhalt"
46
46
  },
47
47
  {
48
- "count": 109800,
48
+ "count": 109799,
49
49
  "division": null
50
50
  },
51
51
  {
@@ -56,6 +56,10 @@
56
56
  "count": 50650,
57
57
  "division": "Baden-Wuerttemberg"
58
58
  },
59
+ {
60
+ "count": 1,
61
+ "division": "Baden-Württemberg"
62
+ },
59
63
  {
60
64
  "count": 50102,
61
65
  "division": "North Rhine Westphalia"
@@ -4,7 +4,6 @@ import type { GeometryCollection, Topology } from 'topojson-specification';
4
4
  import z from 'zod';
5
5
 
6
6
  import { UserFacingError } from '../components/error-display';
7
- import { useQuery } from '../useQuery';
8
7
 
9
8
  export const mapSourceSchema = z.object({
10
9
  type: z.literal('topojson'),
@@ -13,33 +12,17 @@ export const mapSourceSchema = z.object({
13
12
  });
14
13
  export type MapSource = z.infer<typeof mapSourceSchema>;
15
14
 
16
- export function useGeoJsonMap(mapSource: MapSource) {
17
- const {
18
- data: geojsonData,
19
- error,
20
- isLoading,
21
- } = useQuery(async () => {
22
- switch (mapSource.type) {
23
- case 'topojson':
24
- return await loadTopojsonMap(mapSource);
25
- }
26
- }, [mapSource]);
27
-
28
- if (isLoading) {
29
- return { isLoading };
30
- }
31
-
32
- if (error) {
33
- throw error;
34
- }
35
-
36
- return { geojsonData, isLoading: false as const };
37
- }
38
-
39
15
  export type GeoJsonFeatureProperties = {
40
16
  name: string;
41
17
  };
42
18
 
19
+ export async function loadMapSource(mapSource: MapSource) {
20
+ switch (mapSource.type) {
21
+ case 'topojson':
22
+ return await loadTopojsonMap(mapSource);
23
+ }
24
+ }
25
+
43
26
  async function loadTopojsonMap(
44
27
  mapSource: MapSource,
45
28
  ): Promise<FeatureCollection<GeometryObject, GeoJsonFeatureProperties>> {
@@ -1,23 +1,19 @@
1
- import type { Feature, FeatureCollection, GeometryObject } from 'geojson';
1
+ import type { Feature, Geometry, GeometryObject } from 'geojson';
2
2
  import Leaflet, { type Layer, type LayerGroup } from 'leaflet';
3
3
  import type { FunctionComponent } from 'preact';
4
- import { useEffect, useMemo, useRef } from 'preact/hooks';
4
+ import { useEffect, useRef } from 'preact/hooks';
5
5
 
6
- import { type GeoJsonFeatureProperties, type MapSource, useGeoJsonMap } from './useGeoJsonMap';
7
- import { type AggregateData } from '../../query/queryAggregateData';
6
+ import type { EnhancedGeoJsonFeatureProperties } from '../../query/computeMapLocationData';
8
7
  import { InfoHeadline1, InfoParagraph } from '../components/info';
9
- import { LoadingDisplay } from '../components/loading-display';
8
+ import { Modal, useModalRef } from '../components/modal';
10
9
  import { formatProportion } from '../shared/table/formatProportion';
11
10
 
12
- type FeatureData = { proportion: number; count: number };
13
-
14
- type EnhancedGeoJsonFeatureProperties = GeoJsonFeatureProperties & {
15
- data: FeatureData | null;
16
- };
17
-
18
11
  type SequencesByLocationMapProps = {
19
- mapSource: MapSource;
20
- locationData: AggregateData;
12
+ locations: Feature<Geometry, EnhancedGeoJsonFeatureProperties>[];
13
+ totalCount: number;
14
+ countOfMatchedLocationData: number;
15
+ nullCount: number;
16
+ unmatchedLocations: string[];
21
17
  enableMapNavigation: boolean;
22
18
  lapisLocationField: string;
23
19
  zoom: number;
@@ -27,25 +23,11 @@ type SequencesByLocationMapProps = {
27
23
  };
28
24
 
29
25
  export const SequencesByLocationMap: FunctionComponent<SequencesByLocationMapProps> = ({
30
- mapSource,
31
- ...otherProps
32
- }) => {
33
- const { isLoading: isLoadingMap, geojsonData } = useGeoJsonMap(mapSource);
34
-
35
- if (isLoadingMap) {
36
- return <LoadingDisplay />;
37
- }
38
-
39
- return <SequencesByLocationMapInner geojsonData={geojsonData} {...otherProps} />;
40
- };
41
-
42
- type SequencesByLocationMapInnerProps = Omit<SequencesByLocationMapProps, 'mapSource'> & {
43
- geojsonData: FeatureCollection<GeometryObject, GeoJsonFeatureProperties>;
44
- };
45
-
46
- export const SequencesByLocationMapInner: FunctionComponent<SequencesByLocationMapInnerProps> = ({
47
- geojsonData,
48
- locationData,
26
+ locations,
27
+ totalCount,
28
+ countOfMatchedLocationData,
29
+ nullCount,
30
+ unmatchedLocations,
49
31
  enableMapNavigation,
50
32
  lapisLocationField,
51
33
  zoom,
@@ -55,22 +37,6 @@ export const SequencesByLocationMapInner: FunctionComponent<SequencesByLocationM
55
37
  }) => {
56
38
  const ref = useRef<HTMLDivElement>(null);
57
39
 
58
- const { locations, totalCount, countOfMatchedLocationData, unmatchedLocations } = useMemo(() => {
59
- const countAndProportionByCountry = buildLookupByLocationField(locationData, lapisLocationField);
60
- const { locations, unmatchedLocations } = matchLocationDataAndGeoJsonFeatures(
61
- geojsonData,
62
- countAndProportionByCountry,
63
- lapisLocationField,
64
- );
65
-
66
- const totalCount = locationData.map((value) => value.count).reduce((sum, b) => sum + b, 0);
67
- const countOfMatchedLocationData = locations
68
- .map((location) => location.properties.data?.count ?? 0)
69
- .reduce((sum, b) => sum + b, 0);
70
-
71
- return { locations, totalCount, countOfMatchedLocationData, unmatchedLocations };
72
- }, [geojsonData, locationData, lapisLocationField]);
73
-
74
40
  useEffect(() => {
75
41
  if (!ref.current) {
76
42
  return;
@@ -90,7 +56,7 @@ export const SequencesByLocationMapInner: FunctionComponent<SequencesByLocationM
90
56
  style: (feature: Feature<GeometryObject, EnhancedGeoJsonFeatureProperties> | undefined) => ({
91
57
  fillColor: getColor(feature?.properties.data?.proportion),
92
58
  fillOpacity: 1,
93
- color: 'grey',
59
+ color: '#666666',
94
60
  weight: 1,
95
61
  }),
96
62
  })
@@ -102,8 +68,6 @@ export const SequencesByLocationMapInner: FunctionComponent<SequencesByLocationM
102
68
  };
103
69
  }, [ref, locations, enableMapNavigation, lapisLocationField, zoom, offsetX, offsetY]);
104
70
 
105
- const nullCount = locationData.find((row) => row[lapisLocationField] === null)?.count ?? 0;
106
-
107
71
  return (
108
72
  <div className='h-full'>
109
73
  <div ref={ref} className='h-full' />
@@ -135,104 +99,45 @@ const DataMatchInformation: FunctionComponent<DataMatchInformationProps> = ({
135
99
  nullCount,
136
100
  hasTableView,
137
101
  }) => {
138
- const dialogRef = useRef<HTMLDialogElement>(null);
102
+ const modalRef = useModalRef();
139
103
 
140
104
  const proportion = formatProportion(countOfMatchedLocationData / totalCount);
141
105
 
142
106
  return (
143
107
  <>
144
108
  <button
145
- onClick={() => dialogRef.current?.showModal()}
109
+ onClick={() => modalRef.current?.showModal()}
146
110
  className='text-sm absolute bottom-0 px-1 z-[1001] bg-white rounded border cursor-pointer tooltip'
147
111
  data-tip='Click for detailed information'
148
112
  >
149
113
  This map shows {proportion} of the data.
150
114
  </button>
151
- <dialog ref={dialogRef} className={'modal modal-middle'}>
152
- <div className='modal-box max-w-3xl'>
153
- <InfoHeadline1>Sequences By Location - Map View</InfoHeadline1>
154
- <InfoParagraph>
155
- The current filter has matched {totalCount.toLocaleString('en-us')} sequences. From these
156
- sequences, we were able to match {countOfMatchedLocationData.toLocaleString('en-us')} (
157
- {proportion}) on locations on the map.
158
- </InfoParagraph>
159
- <InfoParagraph>
160
- {unmatchedLocations.length > 0 && (
161
- <>
162
- The following locations from the data could not be matched on the map:{' '}
163
- {unmatchedLocations.map((it) => `"${it}"`).join(', ')}.{' '}
164
- </>
165
- )}
166
- {nullCount > 0 &&
167
- `${nullCount.toLocaleString('en-us')} matching sequences have no location information. `}
168
- {hasTableView && 'You can check the table view for more detailed information.'}
169
- </InfoParagraph>
170
- <div className='modal-action'>
171
- <form method='dialog'>
172
- <button className={'float-right underline text-sm hover:text-blue-700 mr-2'}>Close</button>
173
- </form>
174
- </div>
175
- </div>
176
- <form method='dialog' className='modal-backdrop'>
177
- <button>Helper to close when clicked outside</button>
178
- </form>
179
- </dialog>
115
+ <Modal modalRef={modalRef}>
116
+ <InfoHeadline1>Sequences By Location - Map View</InfoHeadline1>
117
+ <InfoParagraph>
118
+ The current filter has matched {totalCount.toLocaleString('en-us')} sequences. From these sequences,
119
+ we were able to match {countOfMatchedLocationData.toLocaleString('en-us')} ({proportion}) on
120
+ locations on the map.
121
+ </InfoParagraph>
122
+ <InfoParagraph>
123
+ {unmatchedLocations.length > 0 && (
124
+ <>
125
+ The following locations from the data could not be matched on the map:{' '}
126
+ {unmatchedLocations.map((it) => `"${it}"`).join(', ')}.{' '}
127
+ </>
128
+ )}
129
+ {nullCount > 0 &&
130
+ `${nullCount.toLocaleString('en-us')} matching sequences have no location information. `}
131
+ {hasTableView && 'You can check the table view for more detailed information.'}
132
+ </InfoParagraph>
133
+ </Modal>
180
134
  </>
181
135
  );
182
136
  };
183
137
 
184
- function buildLookupByLocationField(locationData: AggregateData, lapisLocationField: string) {
185
- return new Map<string, FeatureData>(
186
- locationData
187
- .filter((row) => typeof row[lapisLocationField] === 'string')
188
- .map((row) => [row[lapisLocationField] as string, row]),
189
- );
190
- }
191
-
192
- function matchLocationDataAndGeoJsonFeatures(
193
- geojsonData: FeatureCollection<GeometryObject, GeoJsonFeatureProperties>,
194
- countAndProportionByCountry: Map<string, FeatureData>,
195
- lapisLocationField: string,
196
- ) {
197
- const matchedLocations: string[] = [];
198
-
199
- const locations: Feature<GeometryObject, EnhancedGeoJsonFeatureProperties>[] = geojsonData.features.map(
200
- (feature) => {
201
- const name = feature?.properties?.name;
202
- if (typeof name !== 'string') {
203
- throw new Error(
204
- `GeoJSON feature with id '${feature.id}' does not have 'properties.name' of type string, was: '${name}'`,
205
- );
206
- }
207
-
208
- const data = countAndProportionByCountry.get(name) ?? null;
209
- if (data !== null) {
210
- matchedLocations.push(name);
211
- }
212
- return {
213
- ...feature,
214
- properties: {
215
- ...feature.properties,
216
- data,
217
- },
218
- };
219
- },
220
- );
221
-
222
- const unmatchedLocations = [...countAndProportionByCountry.keys()].filter(
223
- (name) => !matchedLocations.includes(name),
224
- );
225
- if (unmatchedLocations.length > 0) {
226
- const unmatchedLocationsWarning = `gs-map: Found location data from LAPIS (aggregated by "${lapisLocationField}") that could not be matched on locations on the given map. Unmatched location names are: ${unmatchedLocations.map((it) => `"${it}"`).join(', ')}`;
227
- console.warn(unmatchedLocationsWarning); // eslint-disable-line no-console -- We should give some feedback about unmatched location data.
228
- }
229
-
230
- return { locations, unmatchedLocations };
231
- }
232
-
233
138
  function getColor(value: number | undefined): string {
234
139
  if (value === undefined) {
235
- return '#888888';
140
+ return '#DDDDDD';
236
141
  }
237
142
 
238
143
  const thresholds = [