Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit767da72

Browse files
replaced google-maps with pigeon-map in app usage logs
1 parentff8bc10 commit767da72

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-
importReactEChartsfrom"echarts-for-react";
3-
import'echarts-extension-gmap';
4-
import{findIndex}from"lodash";
5-
6-
constgoogleMapsApiUrl="https://maps.googleapis.com/maps/api/js";
7-
8-
functionloadGoogleMapsScript(apiKey:string){
9-
constmapsUrl=`${googleMapsApiUrl}?key=${apiKey}`;
10-
constscripts=document.getElementsByTagName('script');
11-
// is script already loaded
12-
letscriptIndex=findIndex(scripts,(script)=>script.src.endsWith(mapsUrl));
13-
if(scriptIndex>-1){
14-
returnscripts[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+
importSupercluster,{PointFeature}from'supercluster';
4+
importstyledfrom'styled-components';
215

22-
constscript=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+
functiongetClusterSize(count:number):number{
7+
// Logarithmic scaling for better visualization of large numbers
8+
constminSize=30;
9+
constmaxSize=60;
10+
constscale=Math.log10(count+1);
11+
returnMath.min(maxSize,Math.max(minSize,minSize+(scale*10)));
12+
}
2813

29-
returnscript;
14+
functiongetClusterColor(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-
interfaceProps{
33-
data:Array<any>;
21+
interfaceClusterProperties{
22+
id:string;
23+
count:number;
24+
cluster:boolean;
25+
point_count_abbreviated?:string;
3426
}
3527

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

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

46-
constisMapScriptLoaded=useMemo(()=>{
47-
returnmapScriptLoaded||(windowasany)?.google;
48-
},[mapScriptLoaded])
49-
50-
consthandleOnMapScriptLoad=()=>{
51-
setMapScriptLoaded(true);
41+
interfaceProps{
42+
data:Array<any>;
43+
}
44+
45+
constClusterMarker=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+
constTooltipContainer=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-
constgMapScript=loadGoogleMapsScript('');
56-
if(isMapScriptLoaded){
57-
handleOnMapScriptLoad();
58-
return;
59-
}
60-
gMapScript.addEventListener('load',handleOnMapScriptLoad);
61-
return()=>{
62-
gMapScript.removeEventListener('load',handleOnMapScriptLoad);
63-
}
64-
},[])
79+
constMapContainer=styled.div`
80+
height: 400px;
81+
width: 100%;
82+
position: relative;
83+
`;
84+
85+
constUserEngagementByRegionChart=({ 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
constgeoPoints=useMemo(()=>{
6791
returndata.reduce((acc,log)=>{
6892
constregion=log?.geolocationDataJsonb?.city?.names?.en||'Unknown';// assuming `region` is added to each event
69-
letregionData={
93+
letregionData: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
returnacc;
83-
},{}asRecord<string,number>);
108+
},{}asRecord<string,GeoPoint>);
84109
},[data]);
85110

86-
constseries=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[])=>{return8+((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+
constcluster=useMemo(()=>{
112+
constsc=newSupercluster<ClusterProperties>({
113+
radius:300,
114+
maxZoom:20,
115+
});
116+
117+
constgeojsonPoints:PointFeature<ClusterProperties>[]=(Object.values(geoPoints)asGeoPoint[]).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+
returnsc;
111128
},[geoPoints]);
112129

130+
constclusters=useMemo(()=>{
131+
if(!bounds?.ne||!bounds?.sw)return[];
132+
133+
constwestLng=bounds.sw[1];
134+
constsouthLat=bounds.sw[0];
135+
consteastLng=bounds.ne[1];
136+
constnorthLat=bounds.ne[0];
137+
138+
returncluster.getClusters([westLng,southLat,eastLng,northLat],zoom);
139+
},[cluster,bounds,zoom]);
140+
141+
consthandleBoundsChanged=useCallback(({ zoom, bounds}:{zoom:number;bounds:Bounds})=>{
142+
setZoom(zoom);
143+
setBounds(bounds);
144+
},[]);
145+
146+
consthandleMarkerMouseOver=useCallback((lat:number,lng:number,id:string,count:number)=>{
147+
setTooltip({ lat, lng,text:`${id}:${count}`});
148+
},[]);
149+
150+
consthandleMarkerMouseLeave=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+
constisCluster=!!c.properties.cluster;
165+
166+
if(isCluster){
167+
constcount=c.properties.count;
168+
constsize=getClusterSize(count);
169+
constcolor=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+
<Overlayanchor={[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
exportdefaultUserEngagementByRegionChart;

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp