缩放 d3 v4 地图以适合 SVG(或根本不适合)

Scaling d3 v4 map to fit SVG (or at all)

我正在尝试缩小这张美国地图的比例。要么到我的 SVG,要么手动。

这是我最简单的代码,来自:

function initializeMapDifferent(){
    var svg = d3.select("#map").append("svg")
        .attr("width", 1000)
        .attr("height", 500);



    d3.json("https://d3js.org/us-10m.v1.json", function (error, us){

        svg.append("g")
            .attr("class", "states")
            .selectAll("path")
            .data(topojson.feature(us, us.objects.states).features)
            .enter().append("path")
            .attr("fill", "gray")
            .attr("d", d3.geoPath());
    });
}

我试过类似的东西:

  var path = d3.geoPath()
  .projection(d3.geoConicConformal()
      .parallels([33, 45])
      .rotate([96, -39])
      .fitSize([width, height], conus));

但每次我向我的路径变量添加任何内容时,我都会从 D3 的内部部分收到 NAN 错误。感谢您的帮助!

为什么数据不能正确投影

关键问题是您的数据已经 投影。 D3 geoProjections 使用未投影的数据或经纬度对数据。 WGS84 数据中的数据。本质上,d3 geoProjection 采用球坐标并将它们转换为平面笛卡尔 x、y 坐标。

您的数据不符合此要求 - 它已经是平面的。你可以看到最明显的是因为阿拉斯加不在它应该在的地方(除非有人改变了阿拉斯加的经纬度对,这是不太可能的)。已预测数据的其他迹象和症状可能是覆盖整个地球的特征和 NaN 错误。

这是一个复合投影,因此很难取消投影,但您可以在 d3.js 中显示已经投影的数据。

"Projecting" 已投影数据

空投影:

最简单的是,您可以将投影定义为空:

var path = d3.geoPath(null);

这将从 geojson 几何图形中获取 x,y 数据并将其显示为 x,y 数据。但是,如果您的 x,y 坐标超过 svg 的宽度和高度,地图将不会包含在您的 svg 中(正如您在 .attr("d", d3.geoPath()); 示例中所发现的那样)。

此问题中的特定文件已预先投影以适合 960x600 地图,因此这是空投影的理想选择 - 它的设计考虑了尺寸。它的单位是像素,所有坐标都在所需的尺寸范围内。但是,大多数投影几何图形都使用以米为单位的坐标系,因此要素坐标的边界框可能跨越数百万个单位。在这些情况下,空投影将不起作用 - 它会将地图单位值转换为没有缩放的像素值。

对于 d3,空投影通常与 geojson/topojson 一起使用,它使用 d3 投影预先投影以适应指定的视口。见 command line cartography for an example (the example uses unprojected source files - the same issues that arise from using a d3 projection on projected data apply in both browser and command line). The primary advantage of preprojecting a file for use with a null projection is .

geoIdentity

如果您只需要对要素进行缩放和居中,则可以使用 geoIdentity。这是实现一个 geoTransform,但使用标准投影方法,例如 scaletranslate,最重要的是 - fitSize/fitExtent。因此,我们可以将投影设置为 geoIdentity:

var projection = d3.geoIdentity();

这目前与上面使用的空投影相同,它从 geojson 几何体中获取 x,y 数据并将其显示为 x,y 数据而不进行转换 - 将 geojson 中的每个坐标视为像素坐标。但是,我们可以将 fitSize 应用于此(或 fitExtent),它将自动缩放并将数据转换为指定的边界框:

var projection = d3.geoIdentity()
  .fitSize([width,height],geojsonObject);

var projection = d3.geoIdentity()
  .fitExtent([[left,top],[right,bottom]], geojsonObject);

请记住,大多数投影数据使用地理惯例,y=0 位于底部,y 值随着向北移动而增加。在svg/canvas坐标space中,y=0在顶部,y值随着向下移动而增加。所以,我们会经常需要翻转y轴:

var projection = d3.geoIdentity()
 .fitExtent([width,height],geojsonObject)
 .reflectY(true);

这个特定的数据集:https://d3js.org/us-10m.v1.json 是用 d3 投影投影的,所以它的 y 轴已经被翻转,因为 d3 投影投影到 svg 或 canvas 坐标 space.

geoIdentity 演示

var width = 600;
var height = 300;

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



d3.json("https://d3js.org/us-10m.v1.json", function (error, us){
  var featureCollection = topojson.feature(us, us.objects.states);
  
  var projection = d3.geoIdentity()
  .fitExtent([[50,50],[600-50,300-50]], featureCollection)

  var path = d3.geoPath().projection(projection)
  
  svg.append("g")
    .attr("class", "states")
    .selectAll("path")
    .data(featureCollection.features)
    .enter().append("path")
    .attr("fill", "gray")
    .attr("d", path);
  
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.6.0/d3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/2.2.0/topojson.js"></script>

地理变换

如果您希望更好地控制数据的显示方式,可以使用 geoTransform

来自 Mike Bostock

But what if your geometry is already planar? That is, what if you just want to take projected geometry, but still translate or scale it to fit the viewport?

You can implement a custom geometry transform to gain complete control over the projection process.

假设您不想更改投影类型,使用geoTransform 相对简单。例如,如果您想缩放数据,您可以使用 geoTransform:

实现一个用于缩放的短函数
function scale (scaleFactor) {
    return d3.geoTransform({
        point: function(x, y) {
            this.stream.point(x * scaleFactor, y  * scaleFactor);
        }
    });
}

var path = d3.geoPath().projection(scale(0.2));

不过,这会在您缩小时将所有内容缩放到左上角。为了让事情居中,您可以添加一些代码来使投影居中:

function scale (scaleFactor,width,height) {
    return d3.geoTransform({
        point: function(x, y) {
            this.stream.point( (x - width/2) * scaleFactor + width/2 , (y - height/2) * scaleFactor + height/2);
        }
    });
    }

var path = d3.geoPath().projection(scale(0.2,width,height))

geoTransform 演示:

这是一个使用您的文件和 geoTransform 的示例:

var width = 600;
var height = 300;

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


function scale (scaleFactor,width,height) {
  return d3.geoTransform({
    point: function(x, y) {
      this.stream.point( (x - width/2) * scaleFactor + width/2 , (y - height/2) * scaleFactor + height/2);
    }
  });
}
  
d3.json("https://d3js.org/us-10m.v1.json", function (error, us){
  var path = d3.geoPath().projection(scale(0.2,width,height))
 
  svg.append("g")
    .attr("class", "states")
    .selectAll("path")
    .data(topojson.feature(us, us.objects.states).features)
    .enter().append("path")
    .attr("fill", "gray")
    .attr("d", path);
  
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.6.0/d3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/2.2.0/topojson.js"></script>

取消投影数据

这种方法在某些情况下很有用。但它要求您知道用于创建数据的投影。使用 QGIS/ArcGIS 甚至 mapshaper,您可以更改数据的投影,使其 "projected" 成为 WGS84(又名 EPSG 4326)。转换后,您有未投影的数据。

在 Mapshaper 中,使用 shapefile 非常容易,将 shapefile 的 .dbf、.shp 和 .prj 文件拖入 window。在 mapshaper 中打开控制台并输入 proj wgs84.

如果您不知道用于创建数据的投影,则无法取消投影 - 您不知道应用了哪些转换以及使用了哪些参数。

一旦未投影,您就可以正常使用常规 d3 投影,因为坐标位于正确的坐标 space:经纬度对。

如果您还有未投影的数据并且想在同一张地图中混合使用,则取消投影很有用。或者,您可以投影未投影的数据,以便两者使用相同的坐标系。将地图中不匹配的坐标系与 d3 结合起来并不容易,d3 可能不是正确的工具。如果你真的想用 d3 复制一个特定的投影来匹配已经投影的特征和未投影的特征,那么这个 可能会有用。

如何判断您的数据是否已投影?

您可以检查一下要素的几何形状是否符合纬度和经度的限制。例如,如果您要记录:

d3.json("https://d3js.org/us-10m.v1.json", function (error, us){
   console.log(topojson.feature(us, us.objects.states).features);
});

您很快就会看到值超过 +/- 90 度 N/S 和 +/- 180 度 E/W。不太可能是经纬对。

或者,您可以将数据导入 mapshaper.org 等在线服务,然后与您知道未投影的另一个 topojson/geojson(或使用 WGS84 的 'projected')进行比较。

如果处理geojson,你可能幸运地看到定义投影的属性,例如:"name": "urn:ogc:def:crs:OGC:1.3:CRS84"(CRS代表坐标参考系统)或EPSG编号:EPSG:4326(EPSG代表欧洲石油调查组)。

此外,如果您的数据投影使用空投影而不是标准投影(scaled/zoomed 输出以确保您没有看错区域),您可能正在处理投影数据。同样,如果您的视口完全被一个功能覆盖(并且您没有放大)。 NaN 坐标也是一个潜在的指标。然而,这些预测数据的最后指标也可能意味着其他问题。

最后,数据源也可能表明数据已经在元数据中投影或如何使用:看看这个 block,我们可以看到当 geoPath 已定义。