把地图数据画出来:Leaflet + 搜索 API 做商家分布图

做本地商家调研时,我拿到过一份 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 配置带上生产。

工程清单

  1. Leaflet 动态引入,leaflet.css 必引
  2. 过滤空坐标,坐标字段是可选字段
  3. 用 circleMarker 按排名/评分上色,自己的店高亮
  4. 多点场景加 markercluster(或热力图)
  5. 卸载时 map.remove(),StrictMode 下别重复初始化
  6. 底图瓦片:开发用 OSM,生产换商业服务

地图可视化的收益,不在技术复杂度,而在「把数据变成决策」------同样一份商家数据,表格让人逐行读,地图让人一眼判断。做本地 SEO、渠道分析、门店选址,这张图值得花一个下午搭出来。

商家数据的坐标、排名、评分字段说明见 SerpBase 官方文档。你们的本地数据是怎么可视化的?表格、地图还是 Dashboard?评论区聊聊。

相关推荐
超兔一体云1 小时前
Vue3组合式API实战——从Options API迁移
api
用户298698530141 小时前
Python 文档格式转换实战:RTF 转 PDF 与 HTML
python·html·api
用户7783366132111 小时前
前端开发别拿生产 Key 刷数据:用 MSW 给搜索接口做 Mock
前端·api
用户341852229221 小时前
你买的中转站真的是 GPT-6 吗?我写了个不问“你是什么模型”的检测工具
api
VIP_CQCRE6 天前
Coze 接入自定义模型更简单:用 Ace Data Cloud 打通 OpenAI Responses API
openai·api·ai agent·coze·acedatacloud
Patrick在香港6 天前
Claude 工具调用返回空:8 次失败里只有 1 次状态码不对,其余全带 200
爬虫·python·api·claude·香港
todoitbo6 天前
本地图库语义搜索实战:接上蓝耘元生代,让“傍晚的海边“能搜到图
人工智能·ai·api·工具实战
用户7783366132116 天前
搜索页的 URL 状态管理:可分享、可回退、可刷新
python·api