145

目前在 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]) ?

4

11 回答 11

177

我的答案与 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);

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

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

例如, bl.ocks.org/ 4707858

投影到边界框

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

例如, bl.ocks.org/ 4699541

缩放到边界框

于 2013-02-04T17:11:52.703 回答
138

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

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

在代码中:

  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")
    });
于 2013-02-01T21:02:20.663 回答
64

使用 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");

欣赏,干杯

于 2016-12-02T20:02:38.617 回答
54

我是 d3 的新手 - 会尝试解释我是如何理解它的,但我不确定我是否做对了。

秘密在于知道某些方法将在制图空间(纬度、经度)上运行,而另一些方法将在笛卡尔空间(屏幕上的 x、y)上运行。制图空间(我们的星球)(几乎)是球形的,笛卡尔空间(屏幕)是平坦的——为了将一个映射到另一个,您需要一种称为投影的算法。这个空间太短了,无法深入研究令人着迷的投影主题以及它们如何扭曲地理特征以将球形变成平面;有些是为了保存角度而设计的,有些是为了保存距离等等——总有一种妥协(Mike Bostock 有大量的例子)。

在此处输入图像描述

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

投影中心([位置])

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

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

投影.翻译([点])

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

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

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

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

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

boundsfrom 方法返回以像素为单位path边界框。使用它来找到正确的比例,将像素大小与地图单位大小进行比较(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% 的边距。

于 2013-06-12T13:54:56.363 回答
4

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

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

于 2013-01-29T21:31:08.167 回答
3

除了在给定 geoJSON 对象的情况下在 d3 中居中地图之外,请注意,如果要在对象边界周围指定填充fitExtent(),您可能更喜欢。自动将此填充设置为 0。fitSize()fitSize()

于 2018-04-01T12:07:15.293 回答
2

我在互联网上四处寻找一种让我的地图居中的轻松方式,并受到 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);

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

于 2013-06-10T08:24:42.723 回答
1

要平移/缩放地图,您应该查看在 Leaflet 上覆盖 SVG。这将比转换 SVG 容易得多。请参阅此示例http://bost.ocks.org/mike/leaflet/然后如何更改传单中的地图中心

于 2013-01-31T22:25:45.343 回答
0

我如何将 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)
于 2016-03-31T21:18:44.260 回答
0

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

  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")
    });
于 2016-02-13T12:01:49.967 回答
0

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

  1. 在墨卡托中建立一个简单的投影:

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

  2. 创建路径:

    路径 = 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);
}

我希望这有帮助。

于 2015-11-03T15:36:03.877 回答