为什么 D3 不能正确呈现 TopoJSON?

Why does D3 not render TopoJSON correctly?

我正在尝试使用 D3.jsTopoJSON 渲染瑞士地图。底层 JSON 可以在 here. First I tried to follow this tutorial and after I couldn't render anything looking remotely like a map, I found 中找到,其中 link 是一个工作示例。从我获取此代码的位置我目前正在使用:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
    <script src="http://d3js.org/topojson.v1.min.js"></script>
    <style>
    path {
        fill: #ccc;
    }
    </style>
</head>

<body>
    <h1>topojson simplified Switzerland</h1>
    <script>
    var width = window.innerWidth,
        height = window.innerHeight;

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

    var projection = d3.geo.mercator()
        .scale(500)
        .translate([-900, 0]);

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

    d3.json("ch-cantons.json", function(error, topology) {
        if (error) throw error;

        svg.selectAll("path")
         .data(topojson.feature(topology, topology.objects.cantons).features).enter().append("path").attr("d", path);
    });
    </script>
</body>

</html>

因为 JSON 看起来没问题(在 copy/pasting 之后正确呈现它的一些在线平台上进行了检查)并且只有很少的代码可能出错,我假设错误在投影中参数。摆弄了一下无法使其工作。因此,我们将不胜感激任何帮助!

你是对的,错误在投影中。 但是,错误取决于您的数据是投影还是未投影(经纬度对)。

未投影的数据

如果您有 WGS84 格式的数据 - 也就是说经纬度对,那么您会遇到这个问题:

使用你的投影,但只改变数据源我得到这样的东西(我刮掉了右边的空海):

要使墨卡托正确居中,您需要知道感兴趣区域的中心坐标。这通常可以相当一般,对于瑞士我可能会尝试 47N 8.25E。

一旦你有了这个坐标,你需要把它放在中间。一种方法是在 x 轴上旋转并以 y 轴为中心:

var projection = d3.geo.mercator()
    .scale(3500)
    .rotate([-8.25,0])
    .center([0,47])
    .translate([width/2,height/2])

请注意,x 轴旋转是负的,您正在投影下方旋转地球。

另一种选择是在 x 和 y 轴上旋转,当您接近两极时,这可能是首选选项 - 因为墨卡托失真在高纬度地区变得不可行。这种方法看起来像:

var projection = d3.geo.mercator()
    .scale(4000)
    .rotate([-8.25,-47])
    .center([0,0])
    .translate([width/2,height/2])

再次注意负旋转值。第二个选项需要更高的比例值,因为这种方法基本上将瑞士视为零,在墨卡托投影中为零 - 并且赤道沿线的土地面积被最小化。

使用这些预测中的第二个,我得到:

因此您必须稍微调整比例,但现在您应该能够看到您的数据(假设您的数据是正确的经纬度对)。

预计数据

根据下面的评论,其中包括链接的 json 文件,我们可以看出这是您的问题。

对此有两种可能的解决方案:

  1. 将数据转换为经纬对
  2. 使用地理变换

选项一是最简单的,您将取消投影数据 - 这需要知道当前的投影。在 GIS 软件中,这通常会将其投影为 WGS84,这可以说不是真正的投影而是基准面。一旦你有了经纬度对,你就可以按照上面的步骤处理未投影的数据。

选项二完全跳过 d3.geoProjection。相反,我们将创建一个转换函数,将投影坐标转换​​为所需的 SVG 坐标。

地理投影看起来像:

function scale (scaleFactor) {
    return d3.geo.transform({
        point: function(x, y) {
            this.stream.point(x * scaleFactor, (y * scaleFactor);
        }
    });
}

并且像投影一样使用:

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

它只是采用已经是笛卡尔坐标的 x,y 坐标流,并以指定的方式转换它们。

要平移地图使其居中,您需要知道中心坐标。您可以通过 path.bounds:

找到它
var bounds = path.bounds(features);
var centerX = (bounds[0][0] + bounds[1][0])/2;
var centerY = (bounds[0][1] + bounds[1][1])/2;

Path.bounds returns 要素的左上角和右下角。这是关键部分,你可以做一个自动缩放功能,那里有很多例子,但我经常喜欢手动缩放。如果地图居中,这很容易。对于您的地图,geoTransform 可能如下所示:

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

此处 cxcy 指的是特征的中间,宽度和高度指的是 svg 元素的宽度和高度 - 我们不希望特征聚集在 SVG 点[0,0].

总而言之,这给了我们类似的东西(比例因子为 0.002):

这是更新后的 JSbin:http://jsbin.com/wolamuzeze/edit?html,output

请记住,比例取决于 window 尺寸,因为您的 width/height 是相对于 window 尺寸的。这可能最好通过自动设置缩放级别来解决,但如果您有标签(例如),这可能会产生问题。

这个答案也可能有帮助: