@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/custom-elements.json +16 -0
- package/dist/assets/mutationOverTimeWorker-Cr-NmYEs.js.map +1 -1
- package/dist/components.d.ts +14 -14
- package/dist/components.js +324 -279
- package/dist/components.js.map +1 -1
- package/dist/style.css +2 -11
- package/dist/util.d.ts +14 -14
- package/package.json +2 -2
- package/src/preact/aggregatedData/aggregate.tsx +1 -1
- package/src/preact/components/error-display.tsx +11 -16
- package/src/preact/components/info.tsx +5 -19
- package/src/preact/components/modal.stories.tsx +44 -0
- package/src/preact/components/modal.tsx +31 -0
- package/src/preact/map/__mockData__/aggregatedGermany.json +5 -1
- package/src/preact/map/{useGeoJsonMap.tsx → loadMapSource.tsx} +7 -24
- package/src/preact/map/sequences-by-location-map.tsx +37 -132
- package/src/preact/map/sequences-by-location-table.tsx +28 -5
- package/src/preact/map/sequences-by-location.tsx +32 -13
- package/src/preact/useQuery.ts +1 -1
- package/src/query/computeMapLocationData.spec.ts +103 -0
- package/src/query/computeMapLocationData.ts +136 -0
- package/src/query/querySequencesByLocationData.ts +18 -0
- package/src/utilEntrypoint.ts +1 -1
- package/src/web-components/visualization/gs-sequences-by-location.stories.ts +17 -0
- package/standalone-bundle/assets/mutationOverTimeWorker-DIQRmxvC.js.map +1 -1
- package/standalone-bundle/dashboard-components.js +4421 -4368
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
71
|
+
<button
|
|
72
|
+
className='underline hover:text-gray-400'
|
|
73
|
+
onClick={() => modalRef.current?.showModal()}
|
|
74
|
+
>
|
|
70
75
|
Show details.
|
|
71
76
|
</button>
|
|
72
|
-
<
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
2
|
+
|
|
3
|
+
import { Modal, useModalRef } from './modal';
|
|
3
4
|
|
|
4
5
|
const Info: FunctionComponent = ({ children }) => {
|
|
5
|
-
const
|
|
6
|
+
const modalRef = useModalRef();
|
|
6
7
|
|
|
7
8
|
const toggleHelp = () => {
|
|
8
|
-
|
|
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
|
-
<
|
|
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":
|
|
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,
|
|
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,
|
|
4
|
+
import { useEffect, useRef } from 'preact/hooks';
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import { type AggregateData } from '../../query/queryAggregateData';
|
|
6
|
+
import type { EnhancedGeoJsonFeatureProperties } from '../../query/computeMapLocationData';
|
|
8
7
|
import { InfoHeadline1, InfoParagraph } from '../components/info';
|
|
9
|
-
import {
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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: '
|
|
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
|
|
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={() =>
|
|
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
|
-
<
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
{nullCount
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 '#
|
|
140
|
+
return '#DDDDDD';
|
|
236
141
|
}
|
|
237
142
|
|
|
238
143
|
const thresholds = [
|