PAYATHON 2026

D3 地图与经纬度点位使用同一投影,为何仍有偏移?

数据小许

结论:现有投影调用正确,偏移原因还需区分

代码中的 proj([d.lng, d.lat]) 用法正确。地图通过 d3.geoPath().projection(proj) 绘制,圆点也使用同一个 proj,两者共享投影参数。所给代码中没有看到地图和点位分别设置中心、缩放或平移。

可以确认的问题是,绘制逻辑只放在 resize 回调里,首次加载后没有主动执行。这需要修正,但无法解释已经绘制出来的点为什么与海岸线有距离。

目前没有实际截图,也不知道点位数据的来源,无法断定偏移来自经纬度错误还是地图精度。可以先核对点位代表的具体地点,再检查地图边界的细节。

坐标不需要额外转换成弧度或页面坐标

D3 投影函数接收以度为单位的 [longitude, latitude],返回投影后的 [x, y]。这里直接使用原始经纬度即可,无需手动转换成弧度或反转 y 轴。D3 投影文档

const projection = d3.geoAlbers()
  .fitSize([width, height], geomap);

const path = d3.geoPath().projection(projection);
const [x, y] = projection([-81.7811, 24.5551]);

fitSize 会根据地图范围调整投影的缩放和平移。圆点使用同一个投影,就会跟随这些调整,无需单独设置中心。D3 fitSize 文档

getBoundingClientRect() 返回的 left、top 等值属于页面布局坐标。圆点位于 SVG 内部,不应把这些页面偏移加到 cx、cy 上。

为什么点位可能看起来没有贴合海岸线?

点位的名称不能证明它是港口坐标

数据字段叫 port,但值只给出了城市名称。仅凭名称和经纬度,无法确认位置表示的是市中心、城市代表点、港区还是某个码头。

如果标注目标是城市,点落在陆地内部可能完全合理。如果要标注具体港口,则需要核对经纬度是否对应港区。不要为了让点贴近海岸线,直接给所有点加上相同的像素偏移。

地图边界经过简化

states-10m.json 的几何数据经过量化和简化,但没有预先投影。先用 topojson.feature 转换,再交给 d3.geoAlbers() 处理,方向是正确的。us-atlas 文件说明

简化后的州界不适合用来判断每个小岛、海湾或码头是否精确重合。边界简化是一个需要验证的可能原因,不能直接用它解释某个点的偏移。

不要将当前文件与 states-albers-10m.json 混用;后者已经过投影,处理方式不同。us-atlas 文件说明

修改代码:首次加载和 resize 共用绘制函数

保留原来的容器及 D3、TopoJSON 引用,将内联脚本替换为下面的代码。示例保留原始经纬度,增加首次绘制,并让地图和圆点始终使用同一个投影。

d3.json("https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json")
  .then(us => {
    const excluded = new Set([
      "02", "15", "60", "66", "69", "72", "78"
    ]);

    const states = topojson.feature(us, us.objects.states);
    const geomap = {
      type: "FeatureCollection",
      features: states.features.filter(
        d => !excluded.has(String(d.id))
      )
    };

    const portlist = [
      { port: "KEY WEST, FL", lat: 24.5551, lng: -81.7811 },
      { port: "GALVESTON, TX", lat: 29.3036, lng: -94.7975 },
      { port: "NEW YORK, NY", lat: 40.6943, lng: -73.9249 },
      { port: "BOSTON, MA", lat: 42.3118, lng: -71.0852 }
    ];

    const container = document.querySelector("#vizook");
    const svg = d3.select(container)
      .append("svg")
      .attr("class", "svgook");

    // 两个图层共用一个父组,便于统一施加变换。
    const layer = svg.append("g");

    const usmap = layer.append("path")
      .datum(geomap)
      .attr("fill", "#222222")
      .attr("stroke", "#666666");

    const portloc = layer.append("g")
      .selectAll("circle")
      .data(portlist)
      .join("circle")
      .attr("r", 5)
      .attr("fill", "#FFFFFF");

    portloc.append("title")
      .text(d => `${d.port}: ${d.lng}, ${d.lat}`);

    const proj = d3.geoAlbers();
    const path = d3.geoPath().projection(proj);

    function render() {
      const rect = container.getBoundingClientRect();
      const width = Math.max(1, rect.width);
      const height = Math.max(1, rect.height - 50);

      svg.attr("width", width)
        .attr("height", height);

      proj.fitSize([width, height], geomap);
      usmap.attr("d", path);

      portloc
        .attr("cx", d => proj([d.lng, d.lat])[0])
        .attr("cy", d => proj([d.lng, d.lat])[1]);
    }

    render();
    window.addEventListener("resize", render);
  })
  .catch(error => {
    console.error(error);
  });

示例为地图增加了可见填充,方便在黑色背景上检查位置关系。原代码中的 divw、divh 等变量没有声明,示例将它们改为函数内的局部变量。

这些修改解决了绘制时机和代码组织问题。经纬度没有改变,因此无法保证原始点位落在期望的港口位置。

按这个顺序继续排查

  1. 核对原始坐标。在可信的地图或 GIS 工具中分别定位四组经纬度,确认它们是否就是需要标注的地点,优先检查误差最明显的点。
  2. 比较更详细的边界。如果原始坐标位置正确,再用细节更丰富的海岸线或岛屿边界对照,判断州界简化是否影响了视觉结果。
  3. 缩小标记。临时将圆点半径从 5 改为 1,观察圆心与边界的关系。圆点覆盖海岸线不等于圆心投影错误。
  4. 检查实际页面的图层变换。如果完整页面还有 transform、缩放或其他布局设置,确认地图和圆点受到相同处理。所给代码没有显示两层使用了不同的变换。

核实数据和边界后,才能判断是否需要更换坐标或地图数据。单凭“沿海城市的点没有贴着海岸线”,还不足以认定 D3 投影存在错误。

备注:内容仅供参考。