做本地商家调研时,我拿到过一份 200 行的表格:商家名、评分、地址、排名。表格里看半天,看不出「哪片区域密、哪片区域空、我的店被谁围着」。后来把这些点画到地图上,问题十秒内一目了然------这就是地图可视化的价值:坐标数据放在表里是数字,放在地图上是格局。这篇用 Leaflet 把地图搜索端点的商家数据画成分布图。
数据从哪来
地图搜索端点返回的每个商家都带坐标字段:
latitude/longitude:纬度经度(可选字段,谷歌有时不给,要过滤)name、rating、address、position(本地排名)website_domain:用来标出「你的店」
技术选型:Leaflet
对比过几套方案后选了 Leaflet:体积小(几十 KB)、API 直白、社区插件多(聚合、热力都有)。底图用 OpenStreetMap 瓦片起步,不用申请 Key,开发阶段零门槛。
完整实现(React)
tsx
"use client";
import { useEffect, useRef } from "react";
interface Place {
name: string;
position?: number;
address?: string;
latitude?: number;
longitude?: number;
}
export function PlacesMap({ places }: { places: Place[] }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
let map: any;
(async () => {
// Leaflet 依赖 window,必须动态引入(SSR 环境不能顶层 import)
const L = (await import("leaflet")).default;
await import("leaflet/dist/leaflet.css");
await import("leaflet.markercluster");
if (!ref.current) return;
map = L.map(ref.current);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap contributors",
}).addTo(map);
const cluster = L.markerClusterGroup(); // 点多了自动聚合
const points: [number, number][] = [];
for (const p of places) {
// 坐标是可选字段:没有就跳过,别画到 (0,0) 去
if (p.latitude == null || p.longitude == null) continue;
const color = (p.position ?? 99) <= 3 ? "#16a34a" : "#f59e0b"; // 前3名绿色
const marker = L.circleMarker([p.latitude, p.longitude], {
radius: 8,
color,
fillOpacity: 0.8,
});
marker.bindPopup(
`<b>#${p.position ?? "-"} ${p.name}</b><br/>${p.address ?? ""}`
);
cluster.addLayer(marker);
points.push([p.latitude, p.longitude]);
}
map.addLayer(cluster);
if (points.length) map.fitBounds(points, { padding: [30, 30] });
})();
return () => map?.remove(); // 卸载时销毁,防重复初始化
}, [places]);
return <div ref={ref} style={{ height: 480, borderRadius: 8 }} />;
}
三个设计点:动态引入 (SSR 安全)、坐标过滤 (可选字段)、前 3 名绿色(一眼分清你和竞品)。
三个进阶效果
1. 标记自己的店。 从数据里找 website_domain 等于自己域名的条目,换成大号红色标记 + 常开 tooltip:
tsx
const isMine = p.website_domain === "your-site.com";
const marker = L.circleMarker([p.latitude!, p.longitude!], {
radius: isMine ? 12 : 8,
color: isMine ? "#dc2626" : color,
});
2. 聚合开关。 上百个点时用 markerClusterGroup;想数密度时改用热力图插件(leaflet.heat),把 [lat, lng] 数组直接喂进去。
3. 按评分着色。 把 color 从「排名」换成「评分」档位(<4.0 红、4.0-4.5 黄、>4.5 绿),就是一张口碑分布图。
踩坑记录
坑 1:顶层 import L from "leaflet" 报 window is not defined。 Leaflet 依赖浏览器环境,Next.js 里要么 dynamic(() => import(...), { ssr: false }),要么像我上面一样在 useEffect 里动态 import。
坑 2:忘了引 leaflet.css。 结果瓦片错位、缩放按钮散架。Leaflet 的样式是必需的,不是可选装饰。
坑 3:没过滤空坐标。 latitude 缺失时传 undefined,marker 画到 (0,0)------几内亚湾上漂着一堆中国商家。过滤是硬要求。
坑 4:组件卸载没 map.remove()。 React 严格模式双执行下地图初始化两次,控制台警告 + 内存泄漏。清理函数里必须 remove。
坑 5:几百个点不开聚合。 页面卡顿、点叠成一坨。markerClusterGroup 一行接入。
坑 6:生产环境直接用 OSM 瓦片。 OSM 的公共瓦片有使用政策(限流量、要署名),上线前换商业瓦片服务或自建,别把 demo 配置带上生产。
工程清单
- Leaflet 动态引入,
leaflet.css必引 - 过滤空坐标,坐标字段是可选字段
- 用 circleMarker 按排名/评分上色,自己的店高亮
- 多点场景加 markercluster(或热力图)
- 卸载时
map.remove(),StrictMode 下别重复初始化 - 底图瓦片:开发用 OSM,生产换商业服务
地图可视化的收益,不在技术复杂度,而在「把数据变成决策」------同样一份商家数据,表格让人逐行读,地图让人一眼判断。做本地 SEO、渠道分析、门店选址,这张图值得花一个下午搭出来。
商家数据的坐标、排名、评分字段说明见 SerpBase 官方文档。你们的本地数据是怎么可视化的?表格、地图还是 Dashboard?评论区聊聊。