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 等变量没有声明,示例将它们改为函数内的局部变量。
这些修改解决了绘制时机和代码组织问题。经纬度没有改变,因此无法保证原始点位落在期望的港口位置。
按这个顺序继续排查
- 核对原始坐标。在可信的地图或 GIS 工具中分别定位四组经纬度,确认它们是否就是需要标注的地点,优先检查误差最明显的点。
- 比较更详细的边界。如果原始坐标位置正确,再用细节更丰富的海岸线或岛屿边界对照,判断州界简化是否影响了视觉结果。
- 缩小标记。临时将圆点半径从
5改为1,观察圆心与边界的关系。圆点覆盖海岸线不等于圆心投影错误。 - 检查实际页面的图层变换。如果完整页面还有
transform、缩放或其他布局设置,确认地图和圆点受到相同处理。所给代码没有显示两层使用了不同的变换。
核实数据和边界后,才能判断是否需要更换坐标或地图数据。单凭“沿海城市的点没有贴着海岸线”,还不足以认定 D3 投影存在错误。
备注:内容仅供参考。
