@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/custom-elements.json +3 -3
- package/dist/assets/mutationOverTimeWorker-Cr-NmYEs.js.map +1 -0
- package/dist/components.d.ts +44 -44
- package/dist/components.js +222 -129
- package/dist/components.js.map +1 -1
- package/dist/style.css +13 -4
- package/dist/util.d.ts +44 -44
- package/package.json +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/sequences-by-location-map.tsx +91 -12
- package/src/preact/map/sequences-by-location.tsx +1 -0
- package/src/web-components/visualization/gs-sequences-by-location.stories.ts +4 -4
- package/src/web-components/visualization/gs-sequences-by-location.tsx +1 -1
- package/standalone-bundle/assets/mutationOverTimeWorker-DIQRmxvC.js.map +1 -0
- package/standalone-bundle/dashboard-components.js +4313 -4229
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
- package/dist/assets/mutationOverTimeWorker-BjjkMGzd.js.map +0 -1
- package/standalone-bundle/assets/mutationOverTimeWorker-DoUBht2e.js.map +0 -1
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
1022
|
+
'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1023
1023
|
}
|
|
1024
1024
|
}
|
|
1025
1025
|
}
|
package/package.json
CHANGED
|
@@ -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
|
+
};
|
|
@@ -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
|
-
|
|
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: '
|
|
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,
|
|
104
|
+
}, [ref, locations, enableMapNavigation, lapisLocationField, zoom, offsetX, offsetY]);
|
|
92
105
|
|
|
93
|
-
|
|
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 '#
|
|
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
|
};
|