zzxworld

使用 D3.js(v7) 绘制地图

D3.js 的第六篇文章,今天来画地图。这是我连续几天学习 D3 碰到的最有难度的一关。其中涉及的相关概念太多,没有专业理论知识一时半会儿也很难弄明白,所以这图画的颇为曲折。

折戟 geoJSON

和之前画的图相比,画地图的难点主要有两点:

  1. 数据集格式更复杂。
  2. 地理坐标的相关概念。

前面画的各种图形,数据我都还能根据情况编造,但要我徒手编画地图要的数据集还真没这个能力。来看看地图数据集的示例:

{
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      properties: {
        name: "City Name",
      },
      geometry: {
        type: "MultiPolygon",
        coordinates: [...]
      }
    }
  ],
  ...
}

就这复杂度完全要依靠工具才行。国内推荐较多的主要是来自阿里云的 DataV.GeoAtlas 地理工具

DataV.GeoAtlas地理小工具

通过这个工具,可以直接远程调用或是下载画地图时需要的绘图数据。这个数据叫 GeoJSON。这个数据格式比较特殊,所以无法像之前那样直接拿来就往 data 方法中塞。它有一个球面转平面的投影(Projection)概念。好消息是 D3 的 Geo 模块提供了一系列的方法可以直接拿来使用,但问题是它们的使用效果好像不太正常。

来看个实际的例子。

通过上面的工具保存 GeoJSON 数据到本地,命名为 map.json,然后输入下面的绘图代码:

const width = 1000
const height = 600

const svg = d3.select('svg')
    .attr('width', width)
    .attr('height', height)
    .attr('viewBox', [0, 0, width, height])

const g = svg.append('g')
const projection = d3.geoMercator()
const path = d3.geoPath().projection(projection)

d3.json("map.json").then(data => {
    g.selectAll('path')
        .data(data.features)
        .join('path')
        .attr('d', path)
})

然后我满心期待的以为会展现出想要的地图,实际结果却如下:

Screenshot 1

只能看到一块黑色区域。我试着去掉了填充色,并只显示边框,也就是把加载数据后的绘图代码调整为如下:

d3.json("map.json").then(data => {
    g.selectAll('path')
        .data(data.features)
        .join('path')
        .attr('d', path)
        .attr('fill', 'none')
        .attr('stroke', 'blue')
})

发现还是绘制出了地图的。

Screenshot 2

大小和位置不是问题,因为有参数可以调整。但是最外面的边框,以及不能设置背景色就让我毫无办法了。而且我发现从另一个标签切换到地图标签的时候,切换的显示过程有明显的迟滞感。找了很多相关的文章并反复调整代码,都无法解决我碰到的问题。于是我尝试去调整阿里云地图工具生成的数据。我发现去掉子区域后,地图的黑块和标签切换迟滞问题就消失了。

Screenshot 3

但又出来了反白撕裂的效果。看到数据生成工具还有个 3 和 2 的版本切换功能,于是下载了版本 2 的,只是反白的区域变了。

至此我只能怀疑 V7 版本的 D3 还无法和现有的一些 GeoJSON 数据相兼容。因为网上很多相关的文章都是使用的 V6 甚至 V4 版本的 D3。所以如果选择低版本的 D3,应该是可以直接使用阿里云的 GeoJSON 数据。但我不想用低版本的 D3,所以只能去官网看看有没有突破点。

圆梦 TopoJSON

通过查看 D3 官方网站的地图示例代码,我发现在塞入数据时都会用到一个叫 topojson 的对象,于是了解了一下。它是 GeoJSON 的扩展格式,但对其格式进行了一些优化,所以数据体积更小。我决定尝试一下这个 TopoJSON。

GeoJSON 数据可以直接转换为 TopoJSON,网上有各种现成的线上工具。我上面在阿里云下载的 GeoJSON 转换前大小为 569KB,转换后只有 157KB。这个体积的优化的确很不错。

体积优化虽然不错,但图画不出来也是白搭。所以上面准备好数据后,接着就是去下载 TopoJSON 组件。

去上面的地址下载 TopoJSON 后引入到 HTML 页面。然后输入下面的绘图代码:

const width = 1000
const height = 600

const svg = d3.select('svg')
    .attr('width', width)
    .attr('height', height)
    .attr('viewBox', [0, 0, width, height])

const g = svg.append('g')
const projection = d3.geoMercator()
const path = d3.geoPath().projection(projection)

d3.json("map1.json").then(data => {
     g.selectAll('path')
        .data(topojson.feature(data, data.objects.map).features)
        .join('path')
        .attr('d', path)
})

对比一下上面使用 GeoJSON 的代码,除了 data 函数部分,其他都没有区别。来看看效果:

Screenshot 4

这看起来就很正常了。尝试调整了一下绘图参数:

//...
const projection = d3.geoMercator()
    .scale(width*1.7/Math.PI)
    .center([105, 32])
    .translate([width/2, height/2])
//...

d3.json("/map1.json").then(data => {
     g.selectAll('path')
        .data(topojson.feature(data, data.objects.map).features)
        .join('path')
        .attr('d', path)
        .attr('fill', '#903')
})

上面只给出了修改行的代码。主要是放大比例,设置了地图中心点以及位置。然后自定义了一个红色背景。来看看效果:

Screenshot 5

非常不错,感谢 TopoJSON。