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

Posted

技术标签:

【中文标题】缩放 d3 v4 地图以适合 SVG(或根本不适合)【英文标题】:Scaling d3 v4 map to fit SVG (or at all) 【发布时间】:2020-01-19 14:55:27 【问题描述】:

我正在尝试缩小这张美国地图的比例。要么是我的 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 错误。感谢您的帮助!

【问题讨论】:

【参考方案1】:

为什么数据不能正确投影

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

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

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

“投影”已经投影的数据

空投影:

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

var path = d3.geoPath(null);

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

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

对于 d3,空投影通常与 geojson/topojson 一起使用,它使用 d3 投影预投影以适合指定的视口。有关示例,请参见 command line cartography(该示例使用未投影的源文件 - 对投影数据使用 d3 投影产生的相同问题适用于浏览器和命令行)。预投影文件以用于空投影的主要优点是performance。

地理身份

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

var projection = d3.geoIdentity();

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

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

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

请注意,大多数预测数据使用地理约定,y=0 位于底部,y 值随着向北移动而增加。在 svg/canvas 坐标空间中,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 或画布坐标空间。

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:

但是,如果您的几何图形已经是平面的怎么办?也就是说,如果你只是 想要采用投影几何,但仍将其转换或缩放到 适合视口吗?

您可以实现自定义几何变换以获得完全控制 在投影过程中。

假设您不想更改投影类型,使用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,您可以更改数据的投影,使其“投影”为 WGS84(又名 EPSG 4326)。转换后,您将拥有未投影的数据。

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

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

一旦未投影,您可以正常使用常规 d3 投影,因为您在正确的坐标空间中拥有坐标:经纬度对。

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

如何判断您的数据是否已经被预测?

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

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。不太可能是lat long pair。

或者,您可以将数据导入到在线服务,例如 mapshaper.org,并与您知道未投影(或使用 WGS84 进行“投影”)的另一个 topojson/geojson 进行比较。

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

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

最后,数据源还可能表明数据已经在元数据中投影或如何使用:查看此block,我们可以看到在定义geoPath 时没有使用投影。

【讨论】:

非常感谢您的帮助。我将仔细研究细节,但这提供了一个解决方案,我非常感谢! 很高兴,如果您确实有美国的未投影 geojson 或 topojson,但您不想单独关闭阿拉斯加和夏威夷,您可以使用 geoAlbersUsa 投影,它是一个复合投影这实质上是在这个问题中重新创建文件的布局。信息在:github.com/d3/d3-geo/blob/master/README.md#geoAlbersUsa

以上是关于缩放 d3 v4 地图以适合 SVG(或根本不适合)的主要内容,如果未能解决你的问题,请参考以下文章

缩放以适合非标准尺寸地图的所有注释

缩放以适合 LeafletJS 地图上的所有标记

裁剪以适合 svg 模式

svg学习之旅

D3 V4 缩放以使用流式投影在画布中显示

d3js 画布 概念