@genspectrum/dashboard-components 0.11.1 → 0.11.3

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
@@ -2957,6 +2957,9 @@ input.tab:checked + .tab-content,
2957
2957
  .-top-3 {
2958
2958
  top: -0.75rem;
2959
2959
  }
2960
+ .bottom-0 {
2961
+ bottom: 0px;
2962
+ }
2960
2963
  .bottom-full {
2961
2964
  bottom: 100%;
2962
2965
  }
@@ -2993,6 +2996,9 @@ input.tab:checked + .tab-content,
2993
2996
  .z-10 {
2994
2997
  z-index: 10;
2995
2998
  }
2999
+ .z-\[1001\] {
3000
+ z-index: 1001;
3001
+ }
2996
3002
  .float-right {
2997
3003
  float: right;
2998
3004
  }
@@ -3199,6 +3205,9 @@ input.tab:checked + .tab-content,
3199
3205
  .break-words {
3200
3206
  overflow-wrap: break-word;
3201
3207
  }
3208
+ .rounded {
3209
+ border-radius: 0.25rem;
3210
+ }
3202
3211
  .rounded-lg {
3203
3212
  border-radius: 0.5rem;
3204
3213
  }
@@ -3272,6 +3281,10 @@ input.tab:checked + .tab-content,
3272
3281
  .p-4 {
3273
3282
  padding: 1rem;
3274
3283
  }
3284
+ .px-1 {
3285
+ padding-left: 0.25rem;
3286
+ padding-right: 0.25rem;
3287
+ }
3275
3288
  .px-4 {
3276
3289
  padding-left: 1rem;
3277
3290
  padding-right: 1rem;
@@ -3288,10 +3301,6 @@ input.tab:checked + .tab-content,
3288
3301
  padding-top: 0.5rem;
3289
3302
  padding-bottom: 0.5rem;
3290
3303
  }
3291
- .py-4 {
3292
- padding-top: 1rem;
3293
- padding-bottom: 1rem;
3294
- }
3295
3304
  .pl-2 {
3296
3305
  padding-left: 0.5rem;
3297
3306
  }
package/dist/util.d.ts CHANGED
@@ -787,7 +787,10 @@ declare global {
787
787
 
788
788
  declare global {
789
789
  interface HTMLElementTagNameMap {
790
- 'gs-mutation-comparison-component': MutationComparisonComponent;
790
+ 'gs-location-filter': LocationFilterComponent;
791
+ }
792
+ interface HTMLElementEventMap {
793
+ 'gs-location-changed': CustomEvent<Record<string, string>>;
791
794
  }
792
795
  }
793
796
 
@@ -795,7 +798,7 @@ declare global {
795
798
  declare global {
796
799
  namespace JSX {
797
800
  interface IntrinsicElements {
798
- 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
801
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
799
802
  }
800
803
  }
801
804
  }
@@ -803,7 +806,11 @@ declare global {
803
806
 
804
807
  declare global {
805
808
  interface HTMLElementTagNameMap {
806
- 'gs-mutations-component': MutationsComponent;
809
+ 'gs-date-range-selector': DateRangeSelectorComponent;
810
+ }
811
+ interface HTMLElementEventMap {
812
+ 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
813
+ 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
807
814
  }
808
815
  }
809
816
 
@@ -811,7 +818,7 @@ declare global {
811
818
  declare global {
812
819
  namespace JSX {
813
820
  interface IntrinsicElements {
814
- 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
821
+ 'gs-date-range-selector': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
815
822
  }
816
823
  }
817
824
  }
@@ -819,7 +826,10 @@ declare global {
819
826
 
820
827
  declare global {
821
828
  interface HTMLElementTagNameMap {
822
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
829
+ 'gs-text-input': TextInputComponent;
830
+ }
831
+ interface HTMLElementEventMap {
832
+ 'gs-text-input-changed': CustomEvent<Record<string, string>>;
823
833
  }
824
834
  }
825
835
 
@@ -827,7 +837,7 @@ declare global {
827
837
  declare global {
828
838
  namespace JSX {
829
839
  interface IntrinsicElements {
830
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
840
+ 'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
831
841
  }
832
842
  }
833
843
  }
@@ -835,7 +845,10 @@ declare global {
835
845
 
836
846
  declare global {
837
847
  interface HTMLElementTagNameMap {
838
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
848
+ 'gs-mutation-filter': MutationFilterComponent;
849
+ }
850
+ interface HTMLElementEventMap {
851
+ 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
839
852
  }
840
853
  }
841
854
 
@@ -843,7 +856,7 @@ declare global {
843
856
  declare global {
844
857
  namespace JSX {
845
858
  interface IntrinsicElements {
846
- 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
859
+ 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
847
860
  }
848
861
  }
849
862
  }
@@ -851,7 +864,10 @@ declare global {
851
864
 
852
865
  declare global {
853
866
  interface HTMLElementTagNameMap {
854
- 'gs-aggregate': AggregateComponent;
867
+ 'gs-lineage-filter': LineageFilterComponent;
868
+ }
869
+ interface HTMLElementEventMap {
870
+ 'gs-lineage-filter-changed': CustomEvent<Record<string, string>>;
855
871
  }
856
872
  }
857
873
 
@@ -859,7 +875,7 @@ declare global {
859
875
  declare global {
860
876
  namespace JSX {
861
877
  interface IntrinsicElements {
862
- 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
878
+ 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
863
879
  }
864
880
  }
865
881
  }
@@ -867,7 +883,7 @@ declare global {
867
883
 
868
884
  declare global {
869
885
  interface HTMLElementTagNameMap {
870
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
886
+ 'gs-mutation-comparison-component': MutationComparisonComponent;
871
887
  }
872
888
  }
873
889
 
@@ -875,7 +891,7 @@ declare global {
875
891
  declare global {
876
892
  namespace JSX {
877
893
  interface IntrinsicElements {
878
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
894
+ 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
879
895
  }
880
896
  }
881
897
  }
@@ -883,7 +899,7 @@ declare global {
883
899
 
884
900
  declare global {
885
901
  interface HTMLElementTagNameMap {
886
- 'gs-sequences-by-location': SequencesByLocationComponent;
902
+ 'gs-mutations-component': MutationsComponent;
887
903
  }
888
904
  }
889
905
 
@@ -891,7 +907,7 @@ declare global {
891
907
  declare global {
892
908
  namespace JSX {
893
909
  interface IntrinsicElements {
894
- 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
910
+ 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
895
911
  }
896
912
  }
897
913
  }
@@ -899,7 +915,7 @@ declare global {
899
915
 
900
916
  declare global {
901
917
  interface HTMLElementTagNameMap {
902
- 'gs-mutations-over-time': MutationsOverTimeComponent;
918
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
903
919
  }
904
920
  }
905
921
 
@@ -907,7 +923,7 @@ declare global {
907
923
  declare global {
908
924
  namespace JSX {
909
925
  interface IntrinsicElements {
910
- 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
926
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
911
927
  }
912
928
  }
913
929
  }
@@ -915,7 +931,7 @@ declare global {
915
931
 
916
932
  declare global {
917
933
  interface HTMLElementTagNameMap {
918
- 'gs-statistics': StatisticsComponent;
934
+ 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
919
935
  }
920
936
  }
921
937
 
@@ -923,7 +939,7 @@ declare global {
923
939
  declare global {
924
940
  namespace JSX {
925
941
  interface IntrinsicElements {
926
- 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
942
+ 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
927
943
  }
928
944
  }
929
945
  }
@@ -931,10 +947,7 @@ declare global {
931
947
 
932
948
  declare global {
933
949
  interface HTMLElementTagNameMap {
934
- 'gs-location-filter': LocationFilterComponent;
935
- }
936
- interface HTMLElementEventMap {
937
- 'gs-location-changed': CustomEvent<Record<string, string>>;
950
+ 'gs-aggregate': AggregateComponent;
938
951
  }
939
952
  }
940
953
 
@@ -942,7 +955,7 @@ declare global {
942
955
  declare global {
943
956
  namespace JSX {
944
957
  interface IntrinsicElements {
945
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
958
+ 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
946
959
  }
947
960
  }
948
961
  }
@@ -950,11 +963,7 @@ declare global {
950
963
 
951
964
  declare global {
952
965
  interface HTMLElementTagNameMap {
953
- 'gs-date-range-selector': DateRangeSelectorComponent;
954
- }
955
- interface HTMLElementEventMap {
956
- 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
957
- 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
966
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
958
967
  }
959
968
  }
960
969
 
@@ -962,7 +971,7 @@ declare global {
962
971
  declare global {
963
972
  namespace JSX {
964
973
  interface IntrinsicElements {
965
- 'gs-date-range-selector': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
974
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
966
975
  }
967
976
  }
968
977
  }
@@ -970,10 +979,7 @@ declare global {
970
979
 
971
980
  declare global {
972
981
  interface HTMLElementTagNameMap {
973
- 'gs-text-input': TextInputComponent;
974
- }
975
- interface HTMLElementEventMap {
976
- 'gs-text-input-changed': CustomEvent<Record<string, string>>;
982
+ 'gs-mutations-over-time': MutationsOverTimeComponent;
977
983
  }
978
984
  }
979
985
 
@@ -981,7 +987,7 @@ declare global {
981
987
  declare global {
982
988
  namespace JSX {
983
989
  interface IntrinsicElements {
984
- 'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
990
+ 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
985
991
  }
986
992
  }
987
993
  }
@@ -989,10 +995,7 @@ declare global {
989
995
 
990
996
  declare global {
991
997
  interface HTMLElementTagNameMap {
992
- 'gs-mutation-filter': MutationFilterComponent;
993
- }
994
- interface HTMLElementEventMap {
995
- 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
998
+ 'gs-sequences-by-location': SequencesByLocationComponent;
996
999
  }
997
1000
  }
998
1001
 
@@ -1000,7 +1003,7 @@ declare global {
1000
1003
  declare global {
1001
1004
  namespace JSX {
1002
1005
  interface IntrinsicElements {
1003
- 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1006
+ 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1004
1007
  }
1005
1008
  }
1006
1009
  }
@@ -1008,10 +1011,7 @@ declare global {
1008
1011
 
1009
1012
  declare global {
1010
1013
  interface HTMLElementTagNameMap {
1011
- 'gs-lineage-filter': LineageFilterComponent;
1012
- }
1013
- interface HTMLElementEventMap {
1014
- 'gs-lineage-filter-changed': CustomEvent<Record<string, string>>;
1014
+ 'gs-statistics': StatisticsComponent;
1015
1015
  }
1016
1016
  }
1017
1017
 
@@ -1019,7 +1019,7 @@ declare global {
1019
1019
  declare global {
1020
1020
  namespace JSX {
1021
1021
  interface IntrinsicElements {
1022
- 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1022
+ 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1023
1023
  }
1024
1024
  }
1025
1025
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -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
+ };
@@ -1,11 +1,13 @@
1
1
  import type { Feature, FeatureCollection, GeometryObject } from 'geojson';
2
2
  import Leaflet, { type Layer, type LayerGroup } from 'leaflet';
3
3
  import type { FunctionComponent } from 'preact';
4
- import { useEffect, useRef } from 'preact/hooks';
4
+ import { useEffect, useMemo, useRef } from 'preact/hooks';
5
5
 
6
6
  import { type GeoJsonFeatureProperties, type MapSource, useGeoJsonMap } from './useGeoJsonMap';
7
7
  import { type AggregateData } from '../../query/queryAggregateData';
8
+ import { InfoHeadline1, InfoParagraph } from '../components/info';
8
9
  import { LoadingDisplay } from '../components/loading-display';
10
+ import { Modal, useModalRef } from '../components/modal';
9
11
  import { formatProportion } from '../shared/table/formatProportion';
10
12
 
11
13
  type FeatureData = { proportion: number; count: number };
@@ -22,6 +24,7 @@ type SequencesByLocationMapProps = {
22
24
  zoom: number;
23
25
  offsetX: number;
24
26
  offsetY: number;
27
+ hasTableView: boolean;
25
28
  };
26
29
 
27
30
  export const SequencesByLocationMap: FunctionComponent<SequencesByLocationMapProps> = ({
@@ -49,21 +52,31 @@ export const SequencesByLocationMapInner: FunctionComponent<SequencesByLocationM
49
52
  zoom,
50
53
  offsetX,
51
54
  offsetY,
55
+ hasTableView,
52
56
  }) => {
53
57
  const ref = useRef<HTMLDivElement>(null);
54
58
 
55
- useEffect(() => {
56
- if (!ref.current || geojsonData === undefined || locationData === undefined) {
57
- return;
58
- }
59
-
59
+ const { locations, totalCount, countOfMatchedLocationData, unmatchedLocations } = useMemo(() => {
60
60
  const countAndProportionByCountry = buildLookupByLocationField(locationData, lapisLocationField);
61
- const locations = matchLocationDataAndGeoJsonFeatures(
61
+ const { locations, unmatchedLocations } = matchLocationDataAndGeoJsonFeatures(
62
62
  geojsonData,
63
63
  countAndProportionByCountry,
64
64
  lapisLocationField,
65
65
  );
66
66
 
67
+ const totalCount = locationData.map((value) => value.count).reduce((sum, b) => sum + b, 0);
68
+ const countOfMatchedLocationData = locations
69
+ .map((location) => location.properties.data?.count ?? 0)
70
+ .reduce((sum, b) => sum + b, 0);
71
+
72
+ return { locations, totalCount, countOfMatchedLocationData, unmatchedLocations };
73
+ }, [geojsonData, locationData, lapisLocationField]);
74
+
75
+ useEffect(() => {
76
+ if (!ref.current) {
77
+ return;
78
+ }
79
+
67
80
  const leafletMap = Leaflet.map(ref.current, {
68
81
  scrollWheelZoom: enableMapNavigation,
69
82
  zoomControl: enableMapNavigation,
@@ -78,7 +91,7 @@ export const SequencesByLocationMapInner: FunctionComponent<SequencesByLocationM
78
91
  style: (feature: Feature<GeometryObject, EnhancedGeoJsonFeatureProperties> | undefined) => ({
79
92
  fillColor: getColor(feature?.properties.data?.proportion),
80
93
  fillOpacity: 1,
81
- color: 'grey',
94
+ color: '#666666',
82
95
  weight: 1,
83
96
  }),
84
97
  })
@@ -88,9 +101,75 @@ export const SequencesByLocationMapInner: FunctionComponent<SequencesByLocationM
88
101
  return () => {
89
102
  leafletMap.remove();
90
103
  };
91
- }, [ref, locationData, geojsonData, enableMapNavigation, lapisLocationField, zoom, offsetX, offsetY]);
104
+ }, [ref, locations, enableMapNavigation, lapisLocationField, zoom, offsetX, offsetY]);
92
105
 
93
- return <div ref={ref} className='h-full' />;
106
+ const nullCount = locationData.find((row) => row[lapisLocationField] === null)?.count ?? 0;
107
+
108
+ return (
109
+ <div className='h-full'>
110
+ <div ref={ref} className='h-full' />
111
+ <div className='relative'>
112
+ <DataMatchInformation
113
+ totalCount={totalCount}
114
+ countOfMatchedLocationData={countOfMatchedLocationData}
115
+ unmatchedLocations={unmatchedLocations}
116
+ nullCount={nullCount}
117
+ hasTableView={hasTableView}
118
+ />
119
+ </div>
120
+ </div>
121
+ );
122
+ };
123
+
124
+ type DataMatchInformationProps = {
125
+ totalCount: number;
126
+ countOfMatchedLocationData: number;
127
+ unmatchedLocations: string[];
128
+ nullCount: number;
129
+ hasTableView: boolean;
130
+ };
131
+
132
+ const DataMatchInformation: FunctionComponent<DataMatchInformationProps> = ({
133
+ totalCount,
134
+ countOfMatchedLocationData,
135
+ unmatchedLocations,
136
+ nullCount,
137
+ hasTableView,
138
+ }) => {
139
+ const modalRef = useModalRef();
140
+
141
+ const proportion = formatProportion(countOfMatchedLocationData / totalCount);
142
+
143
+ return (
144
+ <>
145
+ <button
146
+ onClick={() => modalRef.current?.showModal()}
147
+ className='text-sm absolute bottom-0 px-1 z-[1001] bg-white rounded border cursor-pointer tooltip'
148
+ data-tip='Click for detailed information'
149
+ >
150
+ This map shows {proportion} of the data.
151
+ </button>
152
+ <Modal modalRef={modalRef}>
153
+ <InfoHeadline1>Sequences By Location - Map View</InfoHeadline1>
154
+ <InfoParagraph>
155
+ The current filter has matched {totalCount.toLocaleString('en-us')} sequences. From these sequences,
156
+ we were able to match {countOfMatchedLocationData.toLocaleString('en-us')} ({proportion}) on
157
+ 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
+ </Modal>
171
+ </>
172
+ );
94
173
  };
95
174
 
96
175
  function buildLookupByLocationField(locationData: AggregateData, lapisLocationField: string) {
@@ -139,12 +218,12 @@ function matchLocationDataAndGeoJsonFeatures(
139
218
  console.warn(unmatchedLocationsWarning); // eslint-disable-line no-console -- We should give some feedback about unmatched location data.
140
219
  }
141
220
 
142
- return locations;
221
+ return { locations, unmatchedLocations };
143
222
  }
144
223
 
145
224
  function getColor(value: number | undefined): string {
146
225
  if (value === undefined) {
147
- return '#888888';
226
+ return '#DDDDDD';
148
227
  }
149
228
 
150
229
  const thresholds = [
@@ -98,6 +98,7 @@ const SequencesByLocationMapTabs: FunctionComponent<SequencesByLocationMapTabsPr
98
98
  zoom={originalComponentProps.zoom}
99
99
  offsetX={originalComponentProps.offsetX}
100
100
  offsetY={originalComponentProps.offsetY}
101
+ hasTableView={originalComponentProps.views.includes(views.table)}
101
102
  />
102
103
  ),
103
104
  };