@kanaries/graphic-walker 0.4.0 → 0.4.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.
Files changed (73) hide show
  1. package/dist/App.d.ts +5 -1
  2. package/dist/assets/buildMetricTable.worker-5555966a.js.map +1 -0
  3. package/dist/components/dataTable/index.d.ts +2 -1
  4. package/dist/components/leafletRenderer/ChoroplethRenderer.d.ts +21 -0
  5. package/dist/components/leafletRenderer/POIRenderer.d.ts +19 -0
  6. package/dist/components/leafletRenderer/encodings.d.ts +7 -0
  7. package/dist/components/leafletRenderer/geoConfigPanel.d.ts +3 -0
  8. package/dist/components/leafletRenderer/index.d.ts +14 -0
  9. package/dist/components/leafletRenderer/tooltip.d.ts +9 -0
  10. package/dist/components/leafletRenderer/utils.d.ts +2 -0
  11. package/dist/components/pivotTable/index.d.ts +2 -1
  12. package/dist/components/pivotTable/inteface.d.ts +6 -2
  13. package/dist/components/pivotTable/leftTree.d.ts +1 -0
  14. package/dist/components/pivotTable/topTree.d.ts +2 -0
  15. package/dist/components/pivotTable/utils.d.ts +1 -2
  16. package/dist/config.d.ts +3 -2
  17. package/dist/graphic-walker.es.js +37802 -30402
  18. package/dist/graphic-walker.es.js.map +1 -1
  19. package/dist/graphic-walker.umd.js +145 -137
  20. package/dist/graphic-walker.umd.js.map +1 -1
  21. package/dist/hooks/index.d.ts +1 -0
  22. package/dist/interfaces.d.ts +27 -0
  23. package/dist/renderer/specRenderer.d.ts +2 -1
  24. package/dist/services.d.ts +7 -1
  25. package/dist/store/commonStore.d.ts +6 -0
  26. package/dist/store/visualSpecStore.d.ts +180 -4
  27. package/dist/utils/save.d.ts +1 -0
  28. package/dist/workers/buildPivotTable.d.ts +7 -0
  29. package/package.json +14 -2
  30. package/src/App.tsx +18 -4
  31. package/src/components/askViz/index.tsx +2 -1
  32. package/src/components/dataTable/index.tsx +7 -5
  33. package/src/components/leafletRenderer/ChoroplethRenderer.tsx +293 -0
  34. package/src/components/leafletRenderer/POIRenderer.tsx +170 -0
  35. package/src/components/leafletRenderer/encodings.ts +194 -0
  36. package/src/components/leafletRenderer/geoConfigPanel.tsx +197 -0
  37. package/src/components/leafletRenderer/index.tsx +67 -0
  38. package/src/components/leafletRenderer/tooltip.tsx +24 -0
  39. package/src/components/leafletRenderer/utils.ts +52 -0
  40. package/src/components/limitSetting.tsx +8 -6
  41. package/src/components/pivotTable/index.tsx +171 -67
  42. package/src/components/pivotTable/inteface.ts +6 -2
  43. package/src/components/pivotTable/leftTree.tsx +24 -11
  44. package/src/components/pivotTable/metricTable.tsx +15 -10
  45. package/src/components/pivotTable/topTree.tsx +50 -17
  46. package/src/components/pivotTable/utils.ts +70 -11
  47. package/src/components/sizeSetting.tsx +9 -7
  48. package/src/components/visualConfig/index.tsx +17 -1
  49. package/src/computation/serverComputation.ts +8 -3
  50. package/src/config.ts +27 -16
  51. package/src/dataSource/table.tsx +9 -9
  52. package/src/fields/aestheticFields.tsx +4 -0
  53. package/src/fields/fieldsContext.tsx +3 -0
  54. package/src/fields/posFields/index.tsx +8 -2
  55. package/src/global.d.ts +4 -4
  56. package/src/hooks/index.ts +8 -0
  57. package/src/index.tsx +11 -9
  58. package/src/interfaces.ts +34 -0
  59. package/src/locales/en-US.json +27 -2
  60. package/src/locales/ja-JP.json +27 -2
  61. package/src/locales/zh-CN.json +27 -2
  62. package/src/renderer/hooks.ts +2 -48
  63. package/src/renderer/index.tsx +24 -1
  64. package/src/renderer/pureRenderer.tsx +26 -13
  65. package/src/renderer/specRenderer.tsx +45 -30
  66. package/src/services.ts +32 -23
  67. package/src/shadow-dom.tsx +7 -0
  68. package/src/store/commonStore.ts +29 -1
  69. package/src/store/visualSpecStore.ts +40 -24
  70. package/src/utils/save.ts +28 -1
  71. package/src/visualSettings/index.tsx +58 -7
  72. package/src/workers/buildMetricTable.worker.js +27 -0
  73. package/src/workers/buildPivotTable.ts +27 -0
@@ -1,7 +1,7 @@
1
1
  import React, { useMemo, useState, useRef, useEffect } from 'react';
2
2
  import styled from 'styled-components';
3
3
  import { observer } from 'mobx-react-lite';
4
- import type { IMutField, IRow, DataSet } from '../../interfaces';
4
+ import type { IMutField, IRow, DataSet, IComputationFunction } from '../../interfaces';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import LoadingLayer from "../loadingLayer";
7
7
  import { useComputationFunc } from "../../renderer/hooks";
@@ -16,6 +16,7 @@ interface DataTableProps {
16
16
  /** total count of rows */
17
17
  total: number;
18
18
  dataset: DataSet;
19
+ computation?: IComputationFunction;
19
20
  onMetaChange: (fid: string, fIndex: number, meta: Partial<IMutField>) => void;
20
21
  loading?: boolean;
21
22
  }
@@ -117,10 +118,11 @@ const getHeaderKey = (f: wrapMutField) => {
117
118
  };
118
119
 
119
120
  const DataTable: React.FC<DataTableProps> = (props) => {
120
- const { size = 10, onMetaChange, dataset, total, loading: statLoading } = props;
121
+ const { size = 10, onMetaChange, dataset, computation, total, loading: statLoading } = props;
121
122
  const [pageIndex, setPageIndex] = useState(0);
122
123
  const { t } = useTranslation();
123
- const computationFuction = useComputationFunc();
124
+ const defaultComputation = useComputationFunc();
125
+ const computationFunction = computation ?? defaultComputation;
124
126
 
125
127
  const analyticTypeList = useMemo<{ value: string; label: string }[]>(() => {
126
128
  return ANALYTIC_TYPE_LIST.map((at) => ({
@@ -149,7 +151,7 @@ const DataTable: React.FC<DataTableProps> = (props) => {
149
151
  }
150
152
  setDataLoading(true);
151
153
  const taskId = ++taskIdRef.current;
152
- dataReadRawServer(computationFuction, size, pageIndex).then(data => {
154
+ dataReadRawServer(computationFunction, size, pageIndex).then(data => {
153
155
  if (taskId === taskIdRef.current) {
154
156
  setDataLoading(false);
155
157
  setRows(data);
@@ -164,7 +166,7 @@ const DataTable: React.FC<DataTableProps> = (props) => {
164
166
  return () => {
165
167
  taskIdRef.current++;
166
168
  };
167
- }, [computationFuction, pageIndex, size]);
169
+ }, [computationFunction, pageIndex, size]);
168
170
 
169
171
  const loading = statLoading || dataLoading;
170
172
 
@@ -0,0 +1,293 @@
1
+ import React, { Fragment, forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from "react";
2
+ import { CircleMarker, MapContainer, Polygon, Marker, TileLayer, Tooltip } from "react-leaflet";
3
+ import { type Map, divIcon } from "leaflet";
4
+ import type { DeepReadonly, IRow, IViewField, VegaGlobalConfig } from "../../interfaces";
5
+ import type { FeatureCollection, Geometry } from "geojson";
6
+ import { getMeaAggKey } from "../../utils";
7
+ import { useColorScale, useOpacityScale } from "./encodings";
8
+ import { isValidLatLng } from "./POIRenderer";
9
+ import { TooltipContent } from "./tooltip";
10
+
11
+
12
+ export interface IChoroplethRendererProps {
13
+ data: IRow[];
14
+ allFields: DeepReadonly<IViewField[]>;
15
+ features: FeatureCollection | undefined;
16
+ geoKey: string;
17
+ defaultAggregated: boolean;
18
+ geoId: DeepReadonly<IViewField>;
19
+ color: DeepReadonly<IViewField> | undefined;
20
+ opacity: DeepReadonly<IViewField> | undefined;
21
+ text: DeepReadonly<IViewField> | undefined;
22
+ details: readonly DeepReadonly<IViewField>[];
23
+ vegaConfig: VegaGlobalConfig;
24
+ scaleIncludeUnmatchedChoropleth: boolean;
25
+ }
26
+
27
+ export interface IChoroplethRendererRef {}
28
+
29
+ const resolveCoords = (featureGeom: Geometry): [lat: number, lng: number][][] => {
30
+ switch (featureGeom.type) {
31
+ case 'Polygon': {
32
+ const coords = featureGeom.coordinates[0];
33
+ return [coords.map<[lat: number, lng: number]>(c => [c[1], c[0]])];
34
+ }
35
+ case 'Point': {
36
+ const coords = featureGeom.coordinates;
37
+ return [[[coords[1], coords[0]]]];
38
+ }
39
+ case 'GeometryCollection': {
40
+ const coords = featureGeom.geometries.map<[lat: number, lng: number][][]>(resolveCoords);
41
+ return coords.flat();
42
+ }
43
+ case 'LineString': {
44
+ const coords = featureGeom.coordinates;
45
+ return [coords.map<[lat: number, lng: number]>(c => [c[1], c[0]])];
46
+ }
47
+ case 'MultiLineString': {
48
+ const coords = featureGeom.coordinates;
49
+ return coords.map<[lat: number, lng: number][]>(c => c.map<[lat: number, lng: number]>(c => [c[1], c[0]]));
50
+ }
51
+ case 'MultiPoint': {
52
+ const coords = featureGeom.coordinates;
53
+ return [coords.map<[lat: number, lng: number]>(c => [c[1], c[0]])];
54
+ }
55
+ case 'MultiPolygon': {
56
+ const coords = featureGeom.coordinates;
57
+ return coords.map<[lat: number, lng: number][]>(c => c[0].map<[lat: number, lng: number]>(c => [c[1], c[0]]));
58
+ }
59
+ default: {
60
+ return [];
61
+ }
62
+ }
63
+ };
64
+
65
+ const resolveCenter = (coordinates: [lat: number, lng: number][]): [lng: number, lat: number] => {
66
+ let area = 0;
67
+ let centroid: [lat: number, lng: number] = [0, 0];
68
+
69
+ for (let i = 0; i < coordinates.length - 1; i++) {
70
+ let [x1, y1] = coordinates[i];
71
+ let [x2, y2] = coordinates[i + 1];
72
+
73
+ let tempArea = x1 * y2 - x2 * y1;
74
+ area += tempArea;
75
+
76
+ centroid[0] += (x1 + x2) * tempArea;
77
+ centroid[1] += (y1 + y2) * tempArea;
78
+ }
79
+
80
+ area /= 2;
81
+
82
+ centroid[0] /= 6 * area;
83
+ centroid[1] /= 6 * area;
84
+
85
+ return centroid;
86
+ };
87
+
88
+ const ChoroplethRenderer = forwardRef<IChoroplethRendererRef, IChoroplethRendererProps>(function ChoroplethRenderer (props, ref) {
89
+ const { data, allFields, features, geoKey, defaultAggregated, geoId, color, opacity, text, details, vegaConfig, scaleIncludeUnmatchedChoropleth } = props;
90
+
91
+ useImperativeHandle(ref, () => ({}));
92
+
93
+ const geoIndices = useMemo(() => {
94
+ if (geoId) {
95
+ return data.map(row => row[geoId.fid]);
96
+ }
97
+ return [];
98
+ }, [geoId, data]);
99
+
100
+ const [indices, geoShapes] = useMemo<[indices: number[], geoShapes: (FeatureCollection['features'][number] | undefined)[]]>(() => {
101
+ if (geoIndices.length && geoKey && features) {
102
+ const indices: number[] = [];
103
+ const shapes = geoIndices.map((id, i) => {
104
+ const feature = id ? features.features.find(f => f.properties?.[geoKey] === id) : undefined;
105
+ if (feature) {
106
+ indices.push(i);
107
+ }
108
+ return feature;
109
+ });
110
+ return [indices, shapes];
111
+ }
112
+ return [[], []];
113
+ }, [geoIndices, features, geoKey]);
114
+
115
+ useEffect(() => {
116
+ if (geoShapes.length > 0) {
117
+ const notMatched = geoShapes.filter(f => !f);
118
+ if (notMatched.length) {
119
+ console.warn(`Failed to render ${notMatched.length.toLocaleString()} items of ${data.length.toLocaleString()} rows due to missing geojson feature.`);
120
+ }
121
+ }
122
+ }, [geoShapes]);
123
+
124
+ const lngLat = useMemo<[lat: number, lng: number][][][]>(() => {
125
+ if (geoShapes.length > 0) {
126
+ return geoShapes.map<[lat: number, lng: number][][]>(feature => {
127
+ if (feature) {
128
+ return resolveCoords(feature.geometry);
129
+ }
130
+ return [];
131
+ }, []);
132
+ }
133
+ return [];
134
+ }, [geoShapes]);
135
+
136
+ const [bounds, center] = useMemo<[bounds: [[n: number, w: number], [s: number, e: number]], center: [lng: number, lat: number]]>(() => {
137
+ const allLngLat = lngLat.flat(2);
138
+ if (allLngLat.length > 0) {
139
+ const [bounds, coords] = allLngLat.reduce<[bounds: [[w: number, n: number], [e: number, s: number]], center: [lat: number, lng: number]]>(([bounds, acc], [lat, lng]) => {
140
+ if (lng < bounds[0][0]) {
141
+ bounds[0][0] = lng;
142
+ }
143
+ if (lng > bounds[1][0]) {
144
+ bounds[1][0] = lng;
145
+ }
146
+ if (lat < bounds[0][1]) {
147
+ bounds[0][1] = lat;
148
+ }
149
+ if (lat > bounds[1][1]) {
150
+ bounds[1][1] = lat;
151
+ }
152
+ return [bounds, [acc[0] + lng, acc[1] + lat]];
153
+ }, [[[-180, -90], [180, 90]], [0, 0]]);
154
+ return [bounds, [coords[0] / lngLat.length, coords[1] / lngLat.length] as [number, number]];
155
+ }
156
+
157
+ return [[[-180, -90], [180, 90]], [0, 0]];
158
+ }, [lngLat]);
159
+
160
+ const distribution = useMemo(() => {
161
+ if (scaleIncludeUnmatchedChoropleth) {
162
+ return data;
163
+ }
164
+ return indices.map(i => data[i]);
165
+ }, [data, indices, scaleIncludeUnmatchedChoropleth]);
166
+
167
+ const opacityScale = useOpacityScale(distribution, opacity, defaultAggregated);
168
+ const colorScale = useColorScale(distribution, color, defaultAggregated, vegaConfig);
169
+
170
+ const tooltipFields = useMemo(() => {
171
+ return details.concat(
172
+ [color!, opacity!].filter(Boolean)
173
+ ).map(f => ({
174
+ ...f,
175
+ key: defaultAggregated && f.analyticType === 'measure' && f.aggName ? getMeaAggKey(f.fid, f.aggName) : f.fid,
176
+ }));
177
+ }, [defaultAggregated, details, color, opacity]);
178
+
179
+ const mapRef = useRef<Map>(null);
180
+
181
+ useEffect(() => {
182
+ const container = mapRef.current?.getContainer();
183
+ if (container) {
184
+ const ro = new ResizeObserver(() => {
185
+ mapRef.current?.invalidateSize();
186
+ });
187
+ ro.observe(container);
188
+ return () => {
189
+ ro.unobserve(container);
190
+ };
191
+ }
192
+ });
193
+
194
+ useEffect(() => {
195
+ mapRef.current?.flyToBounds(bounds);
196
+ }, [`${bounds[0][0]},${bounds[0][1]},${bounds[1][0]},${bounds[1][1]}`]);
197
+
198
+ return (
199
+ <MapContainer center={center} ref={mapRef} zoom={5} bounds={bounds} style={{ width: '100%', height: '100%', zIndex: 1 }}>
200
+ <TileLayer
201
+ attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
202
+ url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
203
+ />
204
+ {lngLat.length > 0 && data.map((row, i) => {
205
+ const coords = lngLat[i];
206
+ const opacity = opacityScale(row);
207
+ const color = colorScale(row);
208
+ return (
209
+ <Fragment key={`${i}-${opacity}-${color}`}>
210
+ {coords.map((coord, j) => {
211
+ if (coord.length === 0) {
212
+ return null;
213
+ }
214
+ if (coord.length === 1) {
215
+ return (
216
+ <CircleMarker
217
+ key={j}
218
+ center={coord[0]}
219
+ radius={3}
220
+ opacity={0.8}
221
+ fillOpacity={opacity}
222
+ fillColor={color}
223
+ color="#0004"
224
+ weight={1}
225
+ stroke
226
+ fill
227
+ >
228
+ {tooltipFields.length > 0 && (
229
+ <Tooltip>
230
+ <header>{data[i][geoId.fid]}</header>
231
+ {tooltipFields.map((f, j) => (
232
+ <TooltipContent
233
+ key={j}
234
+ allFields={allFields}
235
+ vegaConfig={vegaConfig}
236
+ field={f}
237
+ value={row[f.key]}
238
+ />
239
+ ))}
240
+ </Tooltip>
241
+ )}
242
+ </CircleMarker>
243
+ )
244
+ }
245
+ const center: [lat: number, lng: number] = text && coord.length >= 3 ? resolveCenter(coord) : [NaN, NaN];
246
+ return (
247
+ <Fragment key={j}>
248
+ <Polygon
249
+ positions={coord}
250
+ pathOptions={{
251
+ fillOpacity: opacity * 0.8,
252
+ fillColor: color,
253
+ color: "#0004",
254
+ weight: 1,
255
+ stroke: true,
256
+ fill: true,
257
+ }}
258
+ >
259
+ <Tooltip>
260
+ <header>{data[i][geoId.fid]}</header>
261
+ {tooltipFields.map((f, j) => (
262
+ <TooltipContent
263
+ key={j}
264
+ allFields={allFields}
265
+ vegaConfig={vegaConfig}
266
+ field={f}
267
+ value={row[f.key]}
268
+ />
269
+ ))}
270
+ </Tooltip>
271
+ </Polygon>
272
+ {text && data[i][text.fid] && isValidLatLng(center[0], center[1]) && (
273
+ <Marker
274
+ position={center}
275
+ interactive={false}
276
+ icon={divIcon({
277
+ className: '!bg-transparent !border-none',
278
+ html: `<div style="font-size: 11px; transform: translate(-50%, -50%); opacity: 0.8;">${data[i][text.fid]}</div>`,
279
+ })}
280
+ />
281
+ )}
282
+ </Fragment>
283
+ );
284
+ })}
285
+ </Fragment>
286
+ );
287
+ })}
288
+ </MapContainer>
289
+ );
290
+ });
291
+
292
+
293
+ export default ChoroplethRenderer;
@@ -0,0 +1,170 @@
1
+ import React, { forwardRef, useEffect, useMemo, useRef } from "react";
2
+ import { MapContainer, TileLayer, Tooltip, CircleMarker } from "react-leaflet";
3
+ import type { Map } from "leaflet";
4
+ import type { DeepReadonly, IRow, IViewField, VegaGlobalConfig } from "../../interfaces";
5
+ import { getMeaAggKey } from "../../utils";
6
+ import { useColorScale, useOpacityScale, useSizeScale } from "./encodings";
7
+ import { TooltipContent } from "./tooltip";
8
+
9
+
10
+ export interface IPOIRendererProps {
11
+ data: IRow[];
12
+ allFields: DeepReadonly<IViewField[]>;
13
+ defaultAggregated: boolean;
14
+ latitude: DeepReadonly<IViewField> | undefined;
15
+ longitude: DeepReadonly<IViewField> | undefined;
16
+ color: DeepReadonly<IViewField> | undefined;
17
+ opacity: DeepReadonly<IViewField> | undefined;
18
+ size: DeepReadonly<IViewField> | undefined;
19
+ details: readonly DeepReadonly<IViewField>[];
20
+ vegaConfig: VegaGlobalConfig;
21
+ }
22
+
23
+ export interface IPOIRendererRef {}
24
+
25
+ export const isValidLatLng = (latRaw: unknown, lngRaw: unknown) => {
26
+ const lat = Number(latRaw);
27
+ const lng = Number(lngRaw);
28
+ return lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;
29
+ };
30
+
31
+ const formatCoerceLatLng = (latRaw: unknown, lngRaw: unknown) => {
32
+ return `${
33
+ typeof latRaw === 'number' ? latRaw : JSON.stringify(latRaw)
34
+ }, ${
35
+ typeof lngRaw === 'number' ? lngRaw : JSON.stringify(lngRaw)
36
+ }`;
37
+ };
38
+
39
+ const debugMaxLen = 20;
40
+
41
+ const POIRenderer = forwardRef<IPOIRendererRef, IPOIRendererProps>(function POIRenderer (props, ref) {
42
+ const { data, allFields, latitude, longitude, color, opacity, size, details, defaultAggregated, vegaConfig } = props;
43
+
44
+ const lngLat = useMemo<[lat: number, lng: number][]>(() => {
45
+ if (longitude && latitude) {
46
+ return data.map<[lat: number, lng: number]>(row => [Number(row[latitude.fid]), Number(row[longitude.fid])]).filter(v => isValidLatLng(v[0], v[1]));
47
+ }
48
+ return [];
49
+ }, [longitude, latitude, data]);
50
+
51
+ const [bounds, center] = useMemo<[bounds: [[n: number, w: number], [s: number, e: number]], center: [lng: number, lat: number]]>(() => {
52
+ if (lngLat.length > 0) {
53
+ const [bounds, coords] = lngLat.reduce<[bounds: [[w: number, n: number], [e: number, s: number]], center: [lat: number, lng: number]]>(([bounds, acc], [lat, lng]) => {
54
+ if (lng < bounds[0][0]) {
55
+ bounds[0][0] = lng;
56
+ }
57
+ if (lng > bounds[1][0]) {
58
+ bounds[1][0] = lng;
59
+ }
60
+ if (lat < bounds[0][1]) {
61
+ bounds[0][1] = lat;
62
+ }
63
+ if (lat > bounds[1][1]) {
64
+ bounds[1][1] = lat;
65
+ }
66
+ return [bounds, [acc[0] + lng, acc[1] + lat]];
67
+ }, [[[-180, -90], [180, 90]], [0, 0]]);
68
+ return [bounds, [coords[0] / lngLat.length, coords[1] / lngLat.length] as [number, number]];
69
+ }
70
+
71
+ return [[[-180, -90], [180, 90]], [0, 0]];
72
+ }, [lngLat]);
73
+
74
+ const failedLatLngListRef = useRef<[index: number, lng: unknown, lat: unknown][]>([]);
75
+ failedLatLngListRef.current = [];
76
+
77
+ useEffect(() => {
78
+ if (failedLatLngListRef.current.length > 0) {
79
+ console.warn(`Failed to render ${failedLatLngListRef.current.length.toLocaleString()} markers of ${data.length.toLocaleString()} rows due to invalid lat/lng.\n--------\n${
80
+ `${failedLatLngListRef.current.slice(0, debugMaxLen).map(([idx, lng, lat]) =>
81
+ `[${idx + 1}] ${formatCoerceLatLng(lat, lng)}`
82
+ ).join('\n')}`
83
+ + (failedLatLngListRef.current.length > debugMaxLen ? `\n\t... and ${(failedLatLngListRef.current.length - debugMaxLen).toLocaleString()} more` : '')
84
+ }\n`);
85
+ }
86
+ });
87
+
88
+ const mapRef = useRef<Map>(null);
89
+
90
+ useEffect(() => {
91
+ const container = mapRef.current?.getContainer();
92
+ if (container) {
93
+ const ro = new ResizeObserver(() => {
94
+ mapRef.current?.invalidateSize();
95
+ });
96
+ ro.observe(container);
97
+ return () => {
98
+ ro.unobserve(container);
99
+ };
100
+ }
101
+ });
102
+
103
+ useEffect(() => {
104
+ mapRef.current?.flyToBounds(bounds);
105
+ }, [`${bounds[0][0]},${bounds[0][1]},${bounds[1][0]},${bounds[1][1]}`]);
106
+
107
+ const sizeScale = useSizeScale(data, size, defaultAggregated);
108
+ const opacityScale = useOpacityScale(data, opacity, defaultAggregated);
109
+ const colorScale = useColorScale(data, color, defaultAggregated, vegaConfig);
110
+
111
+ const tooltipFields = useMemo(() => {
112
+ return details.concat(
113
+ [size!, color!, opacity!].filter(Boolean)
114
+ ).map(f => ({
115
+ ...f,
116
+ key: defaultAggregated && f.analyticType === 'measure' && f.aggName ? getMeaAggKey(f.fid, f.aggName) : f.fid,
117
+ }));
118
+ }, [defaultAggregated, details, size, color, opacity]);
119
+
120
+ return (
121
+ <MapContainer center={center} ref={mapRef} zoom={5} bounds={bounds} style={{ width: '100%', height: '100%', zIndex: 1 }}>
122
+ <TileLayer
123
+ attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
124
+ url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
125
+ />
126
+ {Boolean(latitude && longitude) && data.map((row, i) => {
127
+ const lat = row[latitude!.fid];
128
+ const lng = row[longitude!.fid];
129
+ if (!isValidLatLng(lat, lng)) {
130
+ failedLatLngListRef.current.push([i, lat, lng]);
131
+ return null;
132
+ }
133
+ const radius = sizeScale(row);
134
+ const opacity = opacityScale(row);
135
+ const color = colorScale(row);
136
+ return (
137
+ <CircleMarker
138
+ key={`${i}-${radius}-${opacity}-${color}`}
139
+ center={[Number(lat), Number(lng)]}
140
+ radius={radius}
141
+ opacity={0.8}
142
+ fillOpacity={opacity}
143
+ fillColor={color}
144
+ color="#0004"
145
+ weight={1}
146
+ stroke
147
+ fill
148
+ >
149
+ {tooltipFields.length > 0 && (
150
+ <Tooltip>
151
+ {tooltipFields.map((f, j) => (
152
+ <TooltipContent
153
+ key={j}
154
+ allFields={allFields}
155
+ vegaConfig={vegaConfig}
156
+ field={f}
157
+ value={row[f.key]}
158
+ />
159
+ ))}
160
+ </Tooltip>
161
+ )}
162
+ </CircleMarker>
163
+ );
164
+ })}
165
+ </MapContainer>
166
+ );
167
+ });
168
+
169
+
170
+ export default POIRenderer;