利用d3画地图

其实这篇文章主要借鉴了文章Let’s make a map来画地图,就当是看一个翻译文章吧,不过我感觉会比原文简单很多!

首先明确几个问题:

  • 这里画的是新加坡的地图以及新加坡所有的公交站点,原因是新加坡的数据在网上是公开的,易获取。后面会附上所有数据来源。
  • 国内的Echarts其实在地图方面已经做的很领先了,提供了很多酷炫组件。但是如上一条,除了Echarts本身提供了地图数据,要再在地图的基础上做一些其他的展示就比较困难,因为真实数据很难获取到。很多时候个人想拿数据做点研究工作,或者练手工作都苦于没有数据

OK,这就开始画吧!

Step1: 创建一个html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>your title</title>
<script type="text/javascript" src="//d3js.org/d3.v4.min.js"></script>
<script type="text/javascript" src="//d3js.org/topojson.v2.min.js"></script>
</head>

<body>
<script>
//...
</script>

</body>

引入了两个文件,一个是d3,一个是topojson。其中topojson是对GeoJson的一种扩展。而eoJson是一种对地理数据的编码格式。

Step2: 画地图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

var width = 960;
var height = 600;
var svg = d3.select("svg");

//1: make sg in the center (103.8°E 1.3°N)
var projection = d3.geoMercator()
.center([103.85, 1.3])
.scale(100000)
.translate([width / 2, height / 2]);

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

//2: draw
d3.json("sg.json", function(error, sg) {

svg.append("g")
.selectAll("path")
.data(topojson.feature(sg, sg.objects.sg).features)
.enter().append("path")
.attr("class","map")
.attr("d", path);

});

上面的代码有两处需要解释的。
第一处:首先地球是球体,是立体的,当我们要在二维平面上描述地图的时候,就需要做一个投影把立体的地图投影到二维平面上。投影很多种方法,得到的地图看到会不太一样。互联网上常见的地图都是做的墨卡托投影(mercator projection)。如上面代码所示,第一处代码的作用就是把新加坡放到中心,首先是做一个投影d3.geoMercator()(注意此函数是d3 v4版本的,v3版本的是d3.geo.mercator());因为新加坡的经纬度大概是是东经103.8度,北纬1.3度,所以设置中心点center();然后放大,是的,因为新加坡太小啦,不放大的话,根本看不到它的样子,scale();最后移动到页面上svg的中心translate();

第二处:画地图,其中sg.json是经过处理的新加坡地图数据(处理成topoJson格式)。原始数据是的GeoJson格式的,后面会附上所有数据来源。要处理GeoJson数据也是简单npm安装topoJson包,然后调用命令geo2topo

1

npm install topoJson
geo2topo old.json > new.json

如果调用命令的时候,显示没有geo2topo命令,那npm的时候全局安装就好了。
如下图,我们的到了想要的地图。

新加坡地图,不包含水域部分

Step3: 增加区域之间的边界

我们的数据中不是只有整个地图的边界数据,我们拿到的其实包括了新加坡每个区域的数据。就类似中国有好多个省组成,每个省之间有边界,我们也可以画出新加坡每个区域之间的边界。

1
<style>
.area-borders{
    fill: none;
    stroke: #999;
    stroke-dasharray: 2,2;
    stroke-linejoin: round;
}
</style>

<script>
svg.append("path")
        .attr("class", "area-borders")
        .attr("d", path(topojson.mesh(sg, sg.objects.sg, function(a, b) { return a !== b; })));
</script>

如下图,得到带边界的地图

带区域边界

Step4: 增加bus stop的打点

画出地图以后,总想在上面做点什么。结果查到了新加坡的公交数据,所以准备在地图上显示新加坡所有的公交站点的情况。看一下分布情况。新加坡总共有4853个公交,数据来源见下面。通过api,然后简单处理,得到如下这样的json数据

1
2

{"value":[{"BusStopCode":"01012","RoadName":"Victoria St","Description":"Hotel Grand Pacific","Latitude":1.29684825487647,"Longitude":103.85253591654006},...]}

然后我们就可以用经纬度在地图上打点了。

1
2
3
4
5
6
7
8
9
10
11
12
13

d3.json("bus-stop-grab.json",function(error,busstop){
svg.selectAll("circle")
.data(busstop.value).enter().append("circle")
.attr("class","pin")
.attr("r", 1)
.attr("transform", function(d) {
return "translate(" + projection([
d.Longitude,
d.Latitude
]) + ")";
});
})

得到如下的地图:

带公交站点的地图

从新加坡的公交站点分布也能看出,在居民区密集分布,没有公交的地方恰好就是机场,蓄水区,军演区,化工区,附带一张新加坡重建局官网的2013城市规划建设图。

城市规划建设图

结论:新加坡这个城市,各种数据公开,太适合拿来做一些研究了。

数据来源