@genspectrum/dashboard-components 0.11.0 → 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
@@ -157,6 +157,22 @@ declare const lapisFilterSchema: default_2.ZodIntersection<default_2.ZodRecord<d
157
157
  aminoAcidInsertions?: string[] | undefined;
158
158
  }>>;
159
159
 
160
+ export declare type MapSource = default_2.infer<typeof mapSourceSchema>;
161
+
162
+ declare const mapSourceSchema: default_2.ZodObject<{
163
+ type: default_2.ZodLiteral<"topojson">;
164
+ url: default_2.ZodString;
165
+ topologyObjectsKey: default_2.ZodString;
166
+ }, "strip", default_2.ZodTypeAny, {
167
+ type: "topojson";
168
+ url: string;
169
+ topologyObjectsKey: string;
170
+ }, {
171
+ type: "topojson";
172
+ url: string;
173
+ topologyObjectsKey: string;
174
+ }>;
175
+
160
176
  export declare type MutationComparisonProps = default_2.infer<typeof mutationComparisonPropsSchema>;
161
177
 
162
178
  declare const mutationComparisonPropsSchema: default_2.ZodObject<{
@@ -835,7 +851,7 @@ declare global {
835
851
 
836
852
  declare global {
837
853
  interface HTMLElementTagNameMap {
838
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
854
+ 'gs-aggregate': AggregateComponent;
839
855
  }
840
856
  }
841
857
 
@@ -843,7 +859,7 @@ declare global {
843
859
  declare global {
844
860
  namespace JSX {
845
861
  interface IntrinsicElements {
846
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
862
+ 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
847
863
  }
848
864
  }
849
865
  }
@@ -851,7 +867,7 @@ declare global {
851
867
 
852
868
  declare global {
853
869
  interface HTMLElementTagNameMap {
854
- 'gs-aggregate': AggregateComponent;
870
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
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-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
863
879
  }
864
880
  }
865
881
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.11.0",
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
  };
@@ -27,6 +27,7 @@ export type {
27
27
  RelativeGrowthAdvantageProps,
28
28
  } from './preact/relativeGrowthAdvantage/relative-growth-advantage';
29
29
  export type { StatisticsProps } from './preact/statistic/statistics';
30
+ export type { MapSource } from './preact/map/useGeoJsonMap';
30
31
 
31
32
  export type { ConfidenceIntervalMethod } from './preact/shared/charts/confideceInterval';
32
33
 
@@ -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.