@genspectrum/dashboard-components 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +3 -3
- package/dist/assets/mutationOverTimeWorker-Cr-NmYEs.js.map +1 -0
- package/dist/components.d.ts +11 -11
- package/dist/components.js +112 -14
- package/dist/components.js.map +1 -1
- package/dist/style.css +19 -0
- package/dist/util.d.ts +11 -11
- package/package.json +1 -1
- package/src/preact/map/sequences-by-location-map.tsx +98 -10
- 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 +2234 -2153
- 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
|
@@ -2684,6 +2684,9 @@ input.tab:checked + .tab-content,
|
|
|
2684
2684
|
border-end-end-radius: inherit;
|
|
2685
2685
|
border-start-end-radius: inherit;
|
|
2686
2686
|
}
|
|
2687
|
+
.modal-middle {
|
|
2688
|
+
place-items: center;
|
|
2689
|
+
}
|
|
2687
2690
|
.modal-bottom {
|
|
2688
2691
|
place-items: end;
|
|
2689
2692
|
}
|
|
@@ -2957,6 +2960,9 @@ input.tab:checked + .tab-content,
|
|
|
2957
2960
|
.-top-3 {
|
|
2958
2961
|
top: -0.75rem;
|
|
2959
2962
|
}
|
|
2963
|
+
.bottom-0 {
|
|
2964
|
+
bottom: 0px;
|
|
2965
|
+
}
|
|
2960
2966
|
.bottom-full {
|
|
2961
2967
|
bottom: 100%;
|
|
2962
2968
|
}
|
|
@@ -2993,6 +2999,9 @@ input.tab:checked + .tab-content,
|
|
|
2993
2999
|
.z-10 {
|
|
2994
3000
|
z-index: 10;
|
|
2995
3001
|
}
|
|
3002
|
+
.z-\[1001\] {
|
|
3003
|
+
z-index: 1001;
|
|
3004
|
+
}
|
|
2996
3005
|
.float-right {
|
|
2997
3006
|
float: right;
|
|
2998
3007
|
}
|
|
@@ -3122,6 +3131,9 @@ input.tab:checked + .tab-content,
|
|
|
3122
3131
|
.min-w-\[7\.5rem\] {
|
|
3123
3132
|
min-width: 7.5rem;
|
|
3124
3133
|
}
|
|
3134
|
+
.max-w-3xl {
|
|
3135
|
+
max-width: 48rem;
|
|
3136
|
+
}
|
|
3125
3137
|
.max-w-screen-lg {
|
|
3126
3138
|
max-width: 1024px;
|
|
3127
3139
|
}
|
|
@@ -3199,6 +3211,9 @@ input.tab:checked + .tab-content,
|
|
|
3199
3211
|
.break-words {
|
|
3200
3212
|
overflow-wrap: break-word;
|
|
3201
3213
|
}
|
|
3214
|
+
.rounded {
|
|
3215
|
+
border-radius: 0.25rem;
|
|
3216
|
+
}
|
|
3202
3217
|
.rounded-lg {
|
|
3203
3218
|
border-radius: 0.5rem;
|
|
3204
3219
|
}
|
|
@@ -3272,6 +3287,10 @@ input.tab:checked + .tab-content,
|
|
|
3272
3287
|
.p-4 {
|
|
3273
3288
|
padding: 1rem;
|
|
3274
3289
|
}
|
|
3290
|
+
.px-1 {
|
|
3291
|
+
padding-left: 0.25rem;
|
|
3292
|
+
padding-right: 0.25rem;
|
|
3293
|
+
}
|
|
3275
3294
|
.px-4 {
|
|
3276
3295
|
padding-left: 1rem;
|
|
3277
3296
|
padding-right: 1rem;
|
package/dist/util.d.ts
CHANGED
|
@@ -883,7 +883,7 @@ declare global {
|
|
|
883
883
|
|
|
884
884
|
declare global {
|
|
885
885
|
interface HTMLElementTagNameMap {
|
|
886
|
-
'gs-
|
|
886
|
+
'gs-mutations-over-time': MutationsOverTimeComponent;
|
|
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-mutations-over-time': 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-sequences-by-location': SequencesByLocationComponent;
|
|
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-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
911
911
|
}
|
|
912
912
|
}
|
|
913
913
|
}
|
|
@@ -931,10 +931,11 @@ declare global {
|
|
|
931
931
|
|
|
932
932
|
declare global {
|
|
933
933
|
interface HTMLElementTagNameMap {
|
|
934
|
-
'gs-
|
|
934
|
+
'gs-date-range-selector': DateRangeSelectorComponent;
|
|
935
935
|
}
|
|
936
936
|
interface HTMLElementEventMap {
|
|
937
|
-
'gs-
|
|
937
|
+
'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
|
|
938
|
+
'gs-date-range-option-changed': DateRangeOptionChangedEvent;
|
|
938
939
|
}
|
|
939
940
|
}
|
|
940
941
|
|
|
@@ -942,7 +943,7 @@ declare global {
|
|
|
942
943
|
declare global {
|
|
943
944
|
namespace JSX {
|
|
944
945
|
interface IntrinsicElements {
|
|
945
|
-
'gs-
|
|
946
|
+
'gs-date-range-selector': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
946
947
|
}
|
|
947
948
|
}
|
|
948
949
|
}
|
|
@@ -950,11 +951,10 @@ declare global {
|
|
|
950
951
|
|
|
951
952
|
declare global {
|
|
952
953
|
interface HTMLElementTagNameMap {
|
|
953
|
-
'gs-
|
|
954
|
+
'gs-location-filter': LocationFilterComponent;
|
|
954
955
|
}
|
|
955
956
|
interface HTMLElementEventMap {
|
|
956
|
-
'gs-
|
|
957
|
-
'gs-date-range-option-changed': DateRangeOptionChangedEvent;
|
|
957
|
+
'gs-location-changed': CustomEvent<Record<string, string>>;
|
|
958
958
|
}
|
|
959
959
|
}
|
|
960
960
|
|
|
@@ -962,7 +962,7 @@ declare global {
|
|
|
962
962
|
declare global {
|
|
963
963
|
namespace JSX {
|
|
964
964
|
interface IntrinsicElements {
|
|
965
|
-
'gs-
|
|
965
|
+
'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
966
966
|
}
|
|
967
967
|
}
|
|
968
968
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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';
|
|
9
10
|
import { formatProportion } from '../shared/table/formatProportion';
|
|
10
11
|
|
|
@@ -22,6 +23,7 @@ type SequencesByLocationMapProps = {
|
|
|
22
23
|
zoom: number;
|
|
23
24
|
offsetX: number;
|
|
24
25
|
offsetY: number;
|
|
26
|
+
hasTableView: boolean;
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
export const SequencesByLocationMap: FunctionComponent<SequencesByLocationMapProps> = ({
|
|
@@ -49,21 +51,31 @@ export const SequencesByLocationMapInner: FunctionComponent<SequencesByLocationM
|
|
|
49
51
|
zoom,
|
|
50
52
|
offsetX,
|
|
51
53
|
offsetY,
|
|
54
|
+
hasTableView,
|
|
52
55
|
}) => {
|
|
53
56
|
const ref = useRef<HTMLDivElement>(null);
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
if (!ref.current || geojsonData === undefined || locationData === undefined) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
58
|
+
const { locations, totalCount, countOfMatchedLocationData, unmatchedLocations } = useMemo(() => {
|
|
60
59
|
const countAndProportionByCountry = buildLookupByLocationField(locationData, lapisLocationField);
|
|
61
|
-
const locations = matchLocationDataAndGeoJsonFeatures(
|
|
60
|
+
const { locations, unmatchedLocations } = matchLocationDataAndGeoJsonFeatures(
|
|
62
61
|
geojsonData,
|
|
63
62
|
countAndProportionByCountry,
|
|
64
63
|
lapisLocationField,
|
|
65
64
|
);
|
|
66
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
|
+
useEffect(() => {
|
|
75
|
+
if (!ref.current) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
const leafletMap = Leaflet.map(ref.current, {
|
|
68
80
|
scrollWheelZoom: enableMapNavigation,
|
|
69
81
|
zoomControl: enableMapNavigation,
|
|
@@ -88,9 +100,85 @@ export const SequencesByLocationMapInner: FunctionComponent<SequencesByLocationM
|
|
|
88
100
|
return () => {
|
|
89
101
|
leafletMap.remove();
|
|
90
102
|
};
|
|
91
|
-
}, [ref,
|
|
103
|
+
}, [ref, locations, enableMapNavigation, lapisLocationField, zoom, offsetX, offsetY]);
|
|
92
104
|
|
|
93
|
-
|
|
105
|
+
const nullCount = locationData.find((row) => row[lapisLocationField] === null)?.count ?? 0;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className='h-full'>
|
|
109
|
+
<div ref={ref} className='h-full' />
|
|
110
|
+
<div className='relative'>
|
|
111
|
+
<DataMatchInformation
|
|
112
|
+
totalCount={totalCount}
|
|
113
|
+
countOfMatchedLocationData={countOfMatchedLocationData}
|
|
114
|
+
unmatchedLocations={unmatchedLocations}
|
|
115
|
+
nullCount={nullCount}
|
|
116
|
+
hasTableView={hasTableView}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
type DataMatchInformationProps = {
|
|
124
|
+
totalCount: number;
|
|
125
|
+
countOfMatchedLocationData: number;
|
|
126
|
+
unmatchedLocations: string[];
|
|
127
|
+
nullCount: number;
|
|
128
|
+
hasTableView: boolean;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const DataMatchInformation: FunctionComponent<DataMatchInformationProps> = ({
|
|
132
|
+
totalCount,
|
|
133
|
+
countOfMatchedLocationData,
|
|
134
|
+
unmatchedLocations,
|
|
135
|
+
nullCount,
|
|
136
|
+
hasTableView,
|
|
137
|
+
}) => {
|
|
138
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
139
|
+
|
|
140
|
+
const proportion = formatProportion(countOfMatchedLocationData / totalCount);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<>
|
|
144
|
+
<button
|
|
145
|
+
onClick={() => dialogRef.current?.showModal()}
|
|
146
|
+
className='text-sm absolute bottom-0 px-1 z-[1001] bg-white rounded border cursor-pointer tooltip'
|
|
147
|
+
data-tip='Click for detailed information'
|
|
148
|
+
>
|
|
149
|
+
This map shows {proportion} of the data.
|
|
150
|
+
</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>
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
94
182
|
};
|
|
95
183
|
|
|
96
184
|
function buildLookupByLocationField(locationData: AggregateData, lapisLocationField: string) {
|
|
@@ -139,7 +227,7 @@ function matchLocationDataAndGeoJsonFeatures(
|
|
|
139
227
|
console.warn(unmatchedLocationsWarning); // eslint-disable-line no-console -- We should give some feedback about unmatched location data.
|
|
140
228
|
}
|
|
141
229
|
|
|
142
|
-
return locations;
|
|
230
|
+
return { locations, unmatchedLocations };
|
|
143
231
|
}
|
|
144
232
|
|
|
145
233
|
function getColor(value: number | undefined): string {
|
|
@@ -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
|
};
|
|
@@ -17,7 +17,7 @@ const codeExample = `<gs-sequences-by-location
|
|
|
17
17
|
lapisFilter='{"dateFrom":"2022-01-01","dateTo":"2022-04-01"}'
|
|
18
18
|
lapisLocationField='country'
|
|
19
19
|
mapSource='{"type":"topojson","url":"https://mock.map.data/topo.json","topologyObjectsKey":"countries"}'
|
|
20
|
-
enableMapNavigation
|
|
20
|
+
enableMapNavigation
|
|
21
21
|
width='1100px'
|
|
22
22
|
height='800px'
|
|
23
23
|
views='["map"]'
|
|
@@ -25,7 +25,7 @@ const codeExample = `<gs-sequences-by-location
|
|
|
25
25
|
offsetX='0'
|
|
26
26
|
offsetY='10'
|
|
27
27
|
pageSize='5'
|
|
28
|
-
|
|
28
|
+
></gs-sequences-by-location>`;
|
|
29
29
|
|
|
30
30
|
const meta: Meta<Required<SequencesByLocationProps>> = {
|
|
31
31
|
title: 'Visualization/Sequences by location',
|
|
@@ -84,7 +84,7 @@ const Template: StoryObj<SequencesByLocationProps> = {
|
|
|
84
84
|
args: {
|
|
85
85
|
enableMapNavigation: false,
|
|
86
86
|
width: '1100px',
|
|
87
|
-
height: '
|
|
87
|
+
height: '700px',
|
|
88
88
|
views: ['map', 'table'],
|
|
89
89
|
pageSize: 10,
|
|
90
90
|
},
|
|
@@ -150,7 +150,7 @@ export const Germany: StoryObj<SequencesByLocationProps> = {
|
|
|
150
150
|
topologyObjectsKey: 'deu',
|
|
151
151
|
},
|
|
152
152
|
views: ['map', 'table'],
|
|
153
|
-
zoom: 6
|
|
153
|
+
zoom: 6,
|
|
154
154
|
offsetX: 10,
|
|
155
155
|
offsetY: 51.4,
|
|
156
156
|
},
|
|
@@ -130,7 +130,7 @@ export class SequencesByLocationComponent extends PreactLitAdapterWithGridJsStyl
|
|
|
130
130
|
* Enable map navigation (dragging, keyboard navigation, zooming).
|
|
131
131
|
*/
|
|
132
132
|
@property({ type: Boolean })
|
|
133
|
-
enableMapNavigation: boolean =
|
|
133
|
+
enableMapNavigation: boolean = false;
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
136
|
* The width of the component.
|