@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/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-sequences-by-location': SequencesByLocationComponent;
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-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
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-mutations-over-time': MutationsOverTimeComponent;
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-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
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-location-filter': LocationFilterComponent;
934
+ 'gs-date-range-selector': DateRangeSelectorComponent;
935
935
  }
936
936
  interface HTMLElementEventMap {
937
- 'gs-location-changed': CustomEvent<Record<string, string>>;
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-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
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-date-range-selector': DateRangeSelectorComponent;
954
+ 'gs-location-filter': LocationFilterComponent;
954
955
  }
955
956
  interface HTMLElementEventMap {
956
- 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
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-date-range-selector': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
965
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
966
966
  }
967
967
  }
968
968
  }
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.2",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -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
- useEffect(() => {
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, locationData, geojsonData, enableMapNavigation, lapisLocationField, zoom, offsetX, offsetY]);
103
+ }, [ref, locations, enableMapNavigation, lapisLocationField, zoom, offsetX, offsetY]);
92
104
 
93
- return <div ref={ref} className='h-full' />;
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='false'
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: '800px',
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.3,
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 = true;
133
+ enableMapNavigation: boolean = false;
134
134
 
135
135
  /**
136
136
  * The width of the component.