给定 geoJSON 对象,在 d3 中将地图居中

Posted

技术标签:

【中文标题】给定 geoJSON 对象,在 d3 中将地图居中【英文标题】:Center a map in d3 given a geoJSON object 【发布时间】:2013-01-07 16:10:56 【问题描述】:

目前在 d3 中,如果您有一个要绘制的 geoJSON 对象,您必须对其进行缩放和翻译,以使其达到所需的大小,并对其进行翻译以使其居中。这是一项非常繁琐的反复试验任务,我想知道是否有人知道获得这些值的更好方法?

例如,如果我有此代码

var path, vis, xy;
xy = d3.geo.mercator().scale(8500).translate([0, -1200]);

path = d3.geo.path().projection(xy);

vis = d3.select("#vis").append("svg:svg").attr("width", 960).attr("height", 600);

d3.json("../../data/ireland2.geojson", function(json) 
  return vis.append("svg:g")
    .attr("class", "tracts")
    .selectAll("path")
    .data(json.features).enter()
    .append("svg:path")
    .attr("d", path)
    .attr("fill", "#85C3C0")
    .attr("stroke", "#222");
);

我怎么能不一点一点地获得 .scale(8500) 和 .translate([0, -1200]) ?

【问题讨论】:

还有D3.geo : Frame a map given a geojson object? 【参考方案1】:

我的答案与 Jan van der Laan 的答案很接近,但您可以稍微简化一下,因为您不需要计算地理质心;你只需要边界框。而且,通过使用未缩放、未平移的单位投影,您可以简化数学运算。

代码的重要部分是这样的:

// Create a unit projection.
var projection = d3.geo.albers()
    .scale(1)
    .translate([0, 0]);

// Create a path generator.
var path = d3.geo.path()
    .projection(projection);

// Compute the bounds of a feature of interest, then derive scale & translate.
var b = path.bounds(state),
    s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
    t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];

// Update the projection to use computed scale & translate.
projection
    .scale(s)
    .translate(t);

在单位投影中计算特征的bounding box后,您可以通过比较边界框(b[1][0] - b[0][0]b[1][1] - b[0][1])的纵横比与纵横比来计算适当的比例画布(widthheight)。在这种情况下,我还将边界框缩放到画布的 95%,而不是 100%,因此边缘有一点额外空间用于笔触和周围特征或填充。

然后您可以使用边界框的中心((b[1][0] + b[0][0]) / 2(b[1][1] + b[0][1]) / 2)和画布的中心(width / 2height / 2)计算翻译。请注意,由于边界框位于单位投影的坐标中,因此必须乘以比例(s)。

例如bl.ocks.org/4707858:

有一个相关的问题是如何在不调整投影的情况下缩放到集合中的特定特征,,将投影与几何变换相结合以进行放大和缩小。这使用与上述相同的原理,但数学略有不同,因为几何变换(SVG“变换”属性)与地理投影相结合。

例如bl.ocks.org/4699541:

【讨论】:

我想指出,上面的代码中存在一些错误,特别是在边界的索引中。它应该看起来像:s = (0.95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][0] ) / 高度)) * 500, t = [(宽度 - s * (b[1][0] + b[0][0])) / 2, (高度 - s * (b[1][1] + b[0][1])) / 2]; @iros - 看起来* 500 在这里是无关紧要的......另外,b[1][1] - b[0][0] 在比例计算中应该是b[1][1] - b[0][1] 错误报告:meta.stackexchange.com/questions/184140/… 所以:b.s = b[0][1]; b.n = b[1][1]; b.w = b[0][0]; b.e = b[1][0]; b.height = Math.abs(b.n - b.s); b.width = Math.abs(b.e - b.w); s = .9 / Math.max(b.width / width, (b.height / height)); 正是因为有这样一个社区,D3 才能如此愉快地工作。太棒了!【参考方案2】:

以下内容似乎大致符合您的要求。缩放似乎没问题。将其应用于我的地图时,会有一个小的偏移量。这个小偏移可能是因为我使用 translate 命令使地图居中,而我可能应该使用 center 命令。

    创建投影和 d3.geo.path 计算当前投影的边界 使用这些边界来计算比例和平移 重新创建投影

在代码中:

  var width  = 300;
  var height = 400;

  var vis = d3.select("#vis").append("svg")
      .attr("width", width).attr("height", height)

  d3.json("nld.json", function(json) 
      // create a first guess for the projection
      var center = d3.geo.centroid(json)
      var scale  = 150;
      var offset = [width/2, height/2];
      var projection = d3.geo.mercator().scale(scale).center(center)
          .translate(offset);

      // create the path
      var path = d3.geo.path().projection(projection);

      // using the path determine the bounds of the current map and use 
      // these to determine better values for the scale and translation
      var bounds  = path.bounds(json);
      var hscale  = scale*width  / (bounds[1][0] - bounds[0][0]);
      var vscale  = scale*height / (bounds[1][1] - bounds[0][1]);
      var scale   = (hscale < vscale) ? hscale : vscale;
      var offset  = [width - (bounds[0][0] + bounds[1][0])/2,
                        height - (bounds[0][1] + bounds[1][1])/2];

      // new projection
      projection = d3.geo.mercator().center(center)
        .scale(scale).translate(offset);
      path = path.projection(projection);

      // add a rectangle to see the bound of the svg
      vis.append("rect").attr('width', width).attr('height', height)
        .style('stroke', 'black').style('fill', 'none');

      vis.selectAll("path").data(json.features).enter().append("path")
        .attr("d", path)
        .style("fill", "red")
        .style("stroke-width", "1")
        .style("stroke", "black")
    );

【讨论】:

嗨 Jan van der Laan 我从来没有感谢过你的回复。顺便说一句,如果我能分出我想要的赏金,这是一个非常好的回应。谢谢! 如果我应用这个,我会得到 bounds = infinity。关于如何解决这个问题的任何想法? @SimkeNys 这可能与这里提到的问题相同***.com/questions/23953366/…尝试那里提到的解决方案。 您好 Jan,感谢您的代码。我用一些 GeoJson 数据尝试了你的例子,但没有奏效。你能告诉我我做错了什么吗? :) 我上传了 GeoJson 数据:onedrive.live.com/… 在 D3 v4 中,投影拟合是一种内置方法:projection.fitSize([width, height], geojson) (API docs) - 请参阅下面的 @dnltsk 答案。【参考方案3】:

使用 d3 v4 或 v5 变得更容易!

var projection = d3.geoMercator().fitSize([width, height], geojson);
var path = d3.geoPath().projection(projection);

最后

g.selectAll('path')
  .data(geojson.features)
  .enter()
  .append('path')
  .attr('d', path)
  .style("fill", "red")
  .style("stroke-width", "1")
  .style("stroke", "black");

享受,干杯

【讨论】:

我希望这个答案得到更多的支持。与d3v4 合作了一段时间,才发现这种方法。 g 来自哪里?那是 svg 容器吗? Tschallacka g 应该是 &lt;g&gt;&lt;/g&gt; 标签 遗憾的是,在 2 个高质量的答案之后,这已经落后了。很容易错过这一点,而且显然比其他答案要简单得多。 谢谢。也适用于 v5!【参考方案4】:

我是 d3 的新手 - 将尝试解释我是如何理解它的,但我不确定我是否一切正确。

秘密在于知道某些方法将在制图空间(纬度,经度)上运行,而另一些则在笛卡尔空间(屏幕上的 x,y)上运行。制图空间(我们的星球)(几乎)是球形的,笛卡尔空间(屏幕)是平面的——为了将一个映射到另一个,您需要一种算法,称为projection。这个空间太短了,无法深入研究令人着迷的投影主题以及它们如何扭曲地理特征以将球形变成平面; some are designed to conserve angles, others conserve distances 等等 - 总有妥协(迈克博斯托克有一个 huge collection of examples)。

在 d3 中,投影对象有一个中心属性/设置器,以地图单位给出:

projection.center([位置])

如果指定了中心,则将投影的中心设置为指定位置,以度为单位的经度和纬度的二元素数组并返回投影。如果未指定中心,则返回当前中心,默认为⟨0°,0°⟩。

还有翻译,以像素为单位 - 投影中心相对于画布的位置:

projection.translate([点])

如果指定了点,则将投影的平移偏移量设置为指定的二元素数组 [x, y] 并返回投影。如果未指定点,则返回当前平移偏移量,默认为 [480, 250]。平移偏移确定投影中心的像素坐标。默认平移偏移将 ⟨0°,0°⟩ 放置在 960×500 区域的中心。

当我想在画布中居中一个特征时,我喜欢将投影中心设置为特征边界框的中心 - 这对我使用 mercator(WGS 84,用于谷歌地图)时适用国家(巴西),从未使用其他投影和半球进行测试。您可能需要针对其他情况进行调整,但如果您掌握了这些基本原则,您就可以了。

例如,给定一个投影和路径:

var projection = d3.geo.mercator()
    .scale(1);

var path = d3.geo.path()
    .projection(projection);

来自pathbounds 方法返回以像素为单位的边界框。使用它来找到正确的比例,将像素大小与地图单位大小进行比较(0.95 为您提供比最适合宽度或高度的 5% 的边距)。这里的基本几何,计算给定对角对角的矩形宽度/高度:

var b = path.bounds(feature),
    s = 0.9 / Math.max(
                   (b[1][0] - b[0][0]) / width, 
                   (b[1][1] - b[0][1]) / height
               );
projection.scale(s); 

使用d3.geo.bounds 方法以地图单位查找边界框:

b = d3.geo.bounds(feature);

将投影的中心设置为边界框的中心:

projection.center([(b[1][0]+b[0][0])/2, (b[1][1]+b[0][1])/2]);

使用translate方法将地图中心移动到画布中心:

projection.translate([width/2, height/2]);

现在您应该已将地图中心的要素缩放了 5% 的边距。

【讨论】:

某处有块吗? 对不起,没有 bl.ocks 或 gist,你想做什么?是不是类似于点击缩放?发布它,我可以看看你的代码。 Bostock's answer and images 提供了 bl.ocks.org 示例的链接,让我可以复制整个代码。任务完成。 +1 并感谢您的精彩插图!【参考方案5】:

您可以使用center() 方法来接受纬度/经度对。

据我了解, translate() 仅用于从字面上移动地图的像素。我不确定如何确定规模。

【讨论】:

如果您正在使用 TopoJSON 并且想要使整个地图居中,您可以使用 --bbox 运行 topojson 以在 JSON 对象中包含一个 bbox 属性。中心的纬度/经度坐标应为 [(b[0]+b[2])/2, (b[1]+b[3])/2](其中 b 是 bbox 值)。跨度> 【参考方案6】:

除了Center a map in d3 given a geoJSON object,请注意,如果要在对象边界周围指定填充,您可能更喜欢fitExtent() 而不是fitSize()fitSize() 自动将此填充设置为 0。

【讨论】:

【参考方案7】:

我在 Internet 上四处寻找一种让我的地图居中的简单方法,并受到 Jan van der Laan 和 mbostock 的回答的启发。如果您使用 svg 的容器,这是使用 jQuery 的更简单方法。我为填充/边框等创建了 95% 的边框。

var width = $("#container").width() * 0.95,
    height = $("#container").width() * 0.95 / 1.9 //using height() doesn't work since there's nothing inside

var projection = d3.geo.mercator().translate([width / 2, height / 2]).scale(width);
var path = d3.geo.path().projection(projection);

var svg = d3.select("#container").append("svg").attr("width", width).attr("height", height);

如果您正在寻找精确的缩放比例,这个答案对您不起作用。但是如果像我一样,您希望显示一个集中在容器中的地图,这应该足够了。我尝试展示墨卡托地图,发现这种方法对我的地图集中很有用,而且我可以很容易地切断南极部分,因为我不需要它。

【讨论】:

【参考方案8】:

要平移/缩放地图,您应该查看在 Leaflet 上覆盖 SVG。这将比转换 SVG 容易得多。看这个例子http://bost.ocks.org/mike/leaflet/ 然后How to change the map center in leaflet

【讨论】:

如果担心添加另一个依赖,PAN和ZOOM可以在纯d3中轻松完成,见***.com/questions/17093614/… 这个答案并没有真正处理 d3。您也可以在 d3 中平移/缩放地图,不需要传单。 (刚刚意识到这是一个旧帖子,只是浏览答案)【参考方案9】:

有了 mbostocks 的回答和 Herb Caudill 的评论,我开始遇到阿拉斯加问题,因为我使用的是墨卡托投影。我应该指出,出于我自己的目的,我正在尝试投射和集中美国各州。我发现我必须将这两个答案与 Jan van der Laan 答案结合起来,但对于与半球重叠的多边形(最终的东西绝对值大于 1 的多边形)有以下例外:

    在墨卡托中设置一个简单的投影:

    projection = d3.geo.mercator().scale(1).translate([0,0]);

    创建路径:

    path = d3.geo.path().projection(projection);

3.设置我的界限:

var bounds = path.bounds(topoJson),
  dx = Math.abs(bounds[1][0] - bounds[0][0]),
  dy = Math.abs(bounds[1][1] - bounds[0][1]),
  x = (bounds[1][0] + bounds[0][0]),
  y = (bounds[1][1] + bounds[0][1]);

4.为阿拉斯加和与半球重叠的州添加例外:

if(dx > 1)
var center = d3.geo.centroid(topojson.feature(json, json.objects[topoObj]));
scale = height / dy * 0.85;
console.log(scale);
projection = projection
    .scale(scale)
    .center(center)
    .translate([ width/2, height/2]);
else
scale = 0.85 / Math.max( dx / width, dy / height );
offset = [ (width - scale * x)/2 , (height - scale * y)/2];

// new projection
projection = projection                     
    .scale(scale)
    .translate(offset);

我希望这会有所帮助。

【讨论】:

【参考方案10】:

对于想要调整垂直和水平的人,这里是解决方案:

  var width  = 300;
  var height = 400;

  var vis = d3.select("#vis").append("svg")
      .attr("width", width).attr("height", height)

  d3.json("nld.json", function(json) 
      // create a first guess for the projection
      var center = d3.geo.centroid(json)
      var scale  = 150;
      var offset = [width/2, height/2];
      var projection = d3.geo.mercator().scale(scale).center(center)
          .translate(offset);

      // create the path
      var path = d3.geo.path().projection(projection);

      // using the path determine the bounds of the current map and use 
      // these to determine better values for the scale and translation
      var bounds  = path.bounds(json);
      var hscale  = scale*width  / (bounds[1][0] - bounds[0][0]);
      var vscale  = scale*height / (bounds[1][1] - bounds[0][1]);
      var scale   = (hscale < vscale) ? hscale : vscale;
      var offset  = [width - (bounds[0][0] + bounds[1][0])/2,
                        height - (bounds[0][1] + bounds[1][1])/2];

      // new projection
      projection = d3.geo.mercator().center(center)
        .scale(scale).translate(offset);
      path = path.projection(projection);

      // adjust projection
      var bounds  = path.bounds(json);
      offset[0] = offset[0] + (width - bounds[1][0] - bounds[0][0]) / 2;
      offset[1] = offset[1] + (height - bounds[1][1] - bounds[0][1]) / 2;

      projection = d3.geo.mercator().center(center)
        .scale(scale).translate(offset);
      path = path.projection(projection);

      // add a rectangle to see the bound of the svg
      vis.append("rect").attr('width', width).attr('height', height)
        .style('stroke', 'black').style('fill', 'none');

      vis.selectAll("path").data(json.features).enter().append("path")
        .attr("d", path)
        .style("fill", "red")
        .style("stroke-width", "1")
        .style("stroke", "black")
    );

【讨论】:

【参考方案11】:

我如何将一个 Topojson 居中,我需要在其中提取功能:

      var projection = d3.geo.albersUsa();

      var path = d3.geo.path()
        .projection(projection);


      var tracts = topojson.feature(mapdata, mapdata.objects.tx_counties);

      projection
          .scale(1)
          .translate([0, 0]);

      var b = path.bounds(tracts),
          s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
          t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];

      projection
          .scale(s)
          .translate(t);

        svg.append("path")
            .datum(topojson.feature(mapdata, mapdata.objects.tx_counties))
            .attr("d", path)

【讨论】:

以上是关于给定 geoJSON 对象,在 d3 中将地图居中的主要内容,如果未能解决你的问题,请参考以下文章

d3.js 地图:使用 geojson 文件和 CSV 数据

使用D3 Geo模块画澳大利亚地图

班加罗尔geojson地图与d3 js

单击geojson多边形到传单地图时添加的更新d3图表

d3.js地图图表绘制矩形

使用D3.js绘制地图并打点