Skip to content

Commit 767da72

Browse files
replaced google-maps with pigeon-map in app usage logs
1 parent ff8bc10 commit 767da72

File tree

3 files changed

+211
-110
lines changed

3 files changed

+211
-110
lines changed

client/packages/lowcoder/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
"copy-to-clipboard": "^3.3.3",
4747
"core-js": "^3.25.2",
4848
"echarts": "^5.4.3",
49-
"echarts-extension-gmap": "^1.7.0",
5049
"echarts-for-react": "^3.0.2",
5150
"echarts-wordcloud": "^2.1.0",
5251
"eslint4b-prebuilt-2": "^7.32.0",
@@ -102,6 +101,7 @@
102101
"sql-formatter": "^8.2.0",
103102
"styled-components": "^6.1.8",
104103
"stylis": "^4.1.1",
104+
"supercluster": "^8.0.1",
105105
"tern": "^0.24.3",
106106
"typescript-collections": "^1.3.3",
107107
"ua-parser-js": "^1.0.33",
@@ -123,6 +123,7 @@
123123
"@types/react": "18",
124124
"@types/react-dom": "18",
125125
"@types/regenerator-runtime": "^0.13.1",
126+
"@types/supercluster": "^7.1.3",
126127
"@types/uuid": "^8.3.4",
127128
"@vitejs/plugin-react": "^2.2.0",
128129
"dotenv": "^16.0.3",
Lines changed: 174 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,100 @@
1-
import { useEffect, useMemo, useRef, useState } from "react";
2-
import ReactECharts from "echarts-for-react";
3-
import 'echarts-extension-gmap';
4-
import { findIndex } from "lodash";
5-
6-
const googleMapsApiUrl = "https://maps.googleapis.com/maps/api/js";
7-
8-
function loadGoogleMapsScript(apiKey: string) {
9-
const mapsUrl = `${googleMapsApiUrl}?key=${apiKey}`;
10-
const scripts = document.getElementsByTagName('script');
11-
// is script already loaded
12-
let scriptIndex = findIndex(scripts, (script) => script.src.endsWith(mapsUrl));
13-
if(scriptIndex > -1) {
14-
return scripts[scriptIndex];
15-
}
16-
// is script loaded with diff api_key, remove the script and load again
17-
scriptIndex = findIndex(scripts, (script) => script.src.startsWith(googleMapsApiUrl));
18-
if(scriptIndex > -1) {
19-
scripts[scriptIndex].remove();
20-
}
1+
import { useMemo, useState, useCallback } from "react";
2+
import { Map, Marker, Overlay, Bounds } from 'pigeon-maps';
3+
import Supercluster, { PointFeature } from 'supercluster';
4+
import styled from 'styled-components';
215

22-
const script = document.createElement("script");
23-
script.type = "text/javascript";
24-
script.src = mapsUrl;
25-
script.async = true;
26-
script.defer = true;
27-
window.document.body.appendChild(script);
6+
function getClusterSize(count: number): number {
7+
// Logarithmic scaling for better visualization of large numbers
8+
const minSize = 30;
9+
const maxSize = 60;
10+
const scale = Math.log10(count + 1);
11+
return Math.min(maxSize, Math.max(minSize, minSize + (scale * 10)));
12+
}
2813

29-
return script;
14+
function getClusterColor(count: number): string {
15+
if (count > 1000) return '#d32f2f'; // red for very high density
16+
if (count > 500) return '#f57c00'; // orange for high density
17+
if (count > 100) return '#f9a825'; // yellow for medium density
18+
return '#1976d2'; // blue for low density
3019
}
3120

32-
interface Props {
33-
data: Array<any>;
21+
interface ClusterProperties {
22+
id: string;
23+
count: number;
24+
cluster: boolean;
25+
point_count_abbreviated?: string;
3426
}
3527

36-
function getRandomLatLng(minLat: number, maxLat: number, minLng: number, maxLng: number) {
37-
const lat = Math.random() * (maxLat - minLat) + minLat
38-
const lng = Math.random() * (maxLng - minLng) + minLng
39-
return [lat, lng]
28+
interface GeoPoint {
29+
latitude: number;
30+
longitude: number;
31+
count: number;
32+
id: string;
4033
}
4134

42-
const UserEngagementByRegionChart = ({ data }: Props) => {
43-
const chartRef = useRef<any>(null);
44-
const [mapScriptLoaded, setMapScriptLoaded] = useState(false);
35+
interface TooltipState {
36+
lat: number;
37+
lng: number;
38+
text: string;
39+
}
4540

46-
const isMapScriptLoaded = useMemo(() => {
47-
return mapScriptLoaded || (window as any)?.google;
48-
}, [mapScriptLoaded])
49-
50-
const handleOnMapScriptLoad = () => {
51-
setMapScriptLoaded(true);
41+
interface Props {
42+
data: Array<any>;
43+
}
44+
45+
const ClusterMarker = styled.div<{ size: number; color: string }>`
46+
background: ${props => props.color};
47+
width: ${props => props.size}px;
48+
height: ${props => props.size}px;
49+
border-radius: 50%;
50+
color: #fff;
51+
display: flex;
52+
align-items: center;
53+
justify-content: center;
54+
font-weight: bold;
55+
font-size: ${props => props.size / 3}px;
56+
border: 2px solid white;
57+
box-shadow: 0 0 6px rgba(0,0,0,0.3);
58+
cursor: pointer;
59+
pointer-events: auto;
60+
opacity: 0.5;
61+
transition: opacity 0.2s ease;
62+
63+
&:hover {
64+
opacity: 1;
5265
}
66+
`;
67+
68+
const TooltipContainer = styled.div`
69+
background: white;
70+
border: 1px solid #ccc;
71+
padding: 5px 10px;
72+
border-radius: 4px;
73+
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
74+
pointer-events: none;
75+
transform: translateY(-120px);
76+
white-space: nowrap;
77+
`;
5378

54-
useEffect(() => {
55-
const gMapScript = loadGoogleMapsScript('');
56-
if(isMapScriptLoaded) {
57-
handleOnMapScriptLoad();
58-
return;
59-
}
60-
gMapScript.addEventListener('load', handleOnMapScriptLoad);
61-
return () => {
62-
gMapScript.removeEventListener('load', handleOnMapScriptLoad);
63-
}
64-
}, [])
79+
const MapContainer = styled.div`
80+
height: 400px;
81+
width: 100%;
82+
position: relative;
83+
`;
84+
85+
const UserEngagementByRegionChart = ({ data }: Props) => {
86+
const [zoom, setZoom] = useState(3);
87+
const [bounds, setBounds] = useState<Bounds | null>(null);
88+
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
6589

6690
const geoPoints = useMemo(() => {
6791
return data.reduce((acc, log) => {
6892
const region = log?.geolocationDataJsonb?.city?.names?.en || 'Unknown'; // assuming `region` is added to each event
69-
let regionData = {
93+
let regionData: GeoPoint = {
7094
latitude: log?.geolocationDataJsonb?.location?.latitude ?? 55,
7195
longitude: log?.geolocationDataJsonb?.location?.longitude ?? 15,
7296
count: 0,
97+
id: region,
7398
};
7499
if (acc[region]) {
75100
acc[region] = {
@@ -80,63 +105,105 @@ const UserEngagementByRegionChart = ({ data }: Props) => {
80105
acc[region] = regionData;
81106
}
82107
return acc;
83-
}, {} as Record<string, number>);
108+
}, {} as Record<string, GeoPoint>);
84109
}, [data]);
85110

86-
const series = useMemo(() => {
87-
return [
88-
{
89-
"name": "Users/Region",
90-
"type": "scatter",
91-
"coordinateSystem": "gmap",
92-
"itemStyle": {
93-
"color": "#ff00ff"
94-
},
95-
"data": Object.keys(geoPoints).map(key => ({
96-
name: key,
97-
value: [
98-
geoPoints[key].longitude,
99-
geoPoints[key].latitude,
100-
geoPoints[key].count,
101-
]
102-
})),
103-
"symbolSize": (val: number[]) => { return 8 + ((Math.log(val[2]) - Math.log(2)) / (Math.log(40) - Math.log(2))) * (40 - 8) },
104-
"encode": {
105-
"value": 2,
106-
"lng": 0,
107-
"lat": 1
108-
}
109-
}
110-
]
111+
const cluster = useMemo(() => {
112+
const sc = new Supercluster<ClusterProperties>({
113+
radius: 300,
114+
maxZoom: 20,
115+
});
116+
117+
const geojsonPoints: PointFeature<ClusterProperties>[] = (Object.values(geoPoints) as GeoPoint[]).map(({ id, latitude, longitude, count }) => ({
118+
type: 'Feature',
119+
properties: { id, count, cluster: true },
120+
geometry: {
121+
type: 'Point',
122+
coordinates: [longitude, latitude],
123+
},
124+
}));
125+
126+
sc.load(geojsonPoints);
127+
return sc;
111128
}, [geoPoints]);
112129

130+
const clusters = useMemo(() => {
131+
if (!bounds?.ne || !bounds?.sw) return [];
132+
133+
const westLng = bounds.sw[1];
134+
const southLat = bounds.sw[0];
135+
const eastLng = bounds.ne[1];
136+
const northLat = bounds.ne[0];
137+
138+
return cluster.getClusters([westLng, southLat, eastLng, northLat], zoom);
139+
}, [cluster, bounds, zoom]);
140+
141+
const handleBoundsChanged = useCallback(({ zoom, bounds }: { zoom: number; bounds: Bounds }) => {
142+
setZoom(zoom);
143+
setBounds(bounds);
144+
}, []);
145+
146+
const handleMarkerMouseOver = useCallback((lat: number, lng: number, id: string, count: number) => {
147+
setTooltip({ lat, lng, text: `${id}: ${count}` });
148+
}, []);
149+
150+
const handleMarkerMouseLeave = useCallback(() => {
151+
setTooltip(null);
152+
}, []);
153+
113154
return (
114-
<>
115-
{isMapScriptLoaded && (
116-
<ReactECharts
117-
ref={chartRef}
118-
option={{
119-
gmap: {
120-
center: [15, 55],
121-
zoom: 3,
122-
renderOnMoving: true,
123-
echartsLayerZIndex: 2019,
124-
roam: true
125-
},
126-
tooltip: {
127-
trigger: "item",
128-
formatter: (params: { data: { name: string; value: any[]; }; }) => {
129-
return `${params.data.name}: ${params.data.value[2]}`;
130-
}
131-
},
132-
animation: true,
133-
series: series,
134-
}}
135-
style={{ height: "400px" }}
136-
/>
137-
)}
138-
</>
139-
)
140-
}
155+
<MapContainer>
156+
<Map
157+
height={400}
158+
defaultCenter={[55, 15]}
159+
defaultZoom={5}
160+
onBoundsChanged={handleBoundsChanged}
161+
>
162+
{clusters.map((c, i) => {
163+
const [lng, lat] = c.geometry.coordinates;
164+
const isCluster = !!c.properties.cluster;
165+
166+
if (isCluster) {
167+
const count = c.properties.count;
168+
const size = getClusterSize(count);
169+
const color = getClusterColor(count);
170+
return (
171+
<Marker
172+
key={`cluster-${i}`}
173+
anchor={[lat, lng]}
174+
>
175+
<ClusterMarker
176+
size={size}
177+
color={color}
178+
onMouseEnter={() => handleMarkerMouseOver(lat, lng, c.properties.id, c.properties.count)}
179+
onMouseLeave={handleMarkerMouseLeave}
180+
>
181+
{c.properties.point_count_abbreviated}
182+
</ClusterMarker>
183+
</Marker>
184+
);
185+
}
186+
187+
return (
188+
<Marker
189+
key={`marker-${i}`}
190+
anchor={[lat, lng]}
191+
onMouseOver={() => handleMarkerMouseOver(lat, lng, c.properties.id, c.properties.count)}
192+
onMouseOut={handleMarkerMouseLeave}
193+
/>
194+
);
195+
})}
196+
197+
{tooltip && (
198+
<Overlay anchor={[tooltip.lat, tooltip.lng]} offset={[0, -40]}>
199+
<TooltipContainer>
200+
{tooltip.text}
201+
</TooltipContainer>
202+
</Overlay>
203+
)}
204+
</Map>
205+
</MapContainer>
206+
);
207+
};
141208

142209
export default UserEngagementByRegionChart;

0 commit comments

Comments
 (0)