梦入琼楼寒有月,行过石树冻无烟

D3 Geo


在D3中除了常用的学术图表之外,还支持地图的数据展示,主要分为地理路径、地理投影,D3支持少数的组件来显示和操作数据。一般地图数据会保存为JSON格式,而D3常用的有GeoJson and TopoJSON,其中GeoJson主要描述地理信息的一种基本格式。而TopoJSON则是由D3作者Mike Bostock制定的格式,他们都符合了JSON的规范。

D3支持少数的组建来显示和操作数据,这些组件会使用GeoJSON格式,他也是JavaScript中标准的地理特征表示方法。而由Mike Bostock所制定的格式TopoJSON主要是GeoJSON的扩展格式,前者表示的更加紧密。

如果要将文件转换为GeoJSON格式,需要通过使用GDAL``包中的 ogr2ogr来进行转换,当让也有从 ogr2ogr 衍生出的 ogr2gui 但本文主要通过使用 ogr2ogr来进行转换,通常 D3 Geo 常用 API 如下:

ID DA FA
d3.geo.mercator() 莫卡托投影,是正轴与圆柱投影,由荷兰地图学家莫卡托(mercator)与1569年创立
center(one[,two]) 地图中心位置,通常one是经度、two是维度
d3.geo.path() 创建一个新的地理路径生成器
projection 应用投影
d3.set() 创建集合
.has() 用于检测数据是否在集合中

数据的来源

DataV

DataV是阿里巴巴数据可视化团队,旨在让更多人看到数据可视化的魅力,本文主要使用 DATAV所提供的地理地图集(DATAV.GeoAtlas),通过使用DATAV我们可以直接得到相应地图的 JSON数据,从而避免 使用 ogr2ogr 进行二次转换。

数据可以直接通过访问http://datav.aliyun.com/tools/atlas/#&lat=30.316551722910077&lng=102.21432656555675&zoom=3.5得到,除此之外,我们也可以根据 DATAV 所提供的 API 直接进行绘制:

ID DA
https://geo.datav.aliyun.com/areas_v2/bound/100000.json JSON API
https://geo.datav.aliyun.com/areas/bound/geojson?code=100000 GeoJSON API
https://geo.datav.aliyun.com/areas_v2/bound/100000_full.json JSON API (包含子域)
https://geo.datav.aliyun.com/areas/bound/geojson?code=100000_full GeoJSON API (包含子域)

Natural Earth

Natural Earth 提供的部分 GeoJSON 存在中国地图部分不完整或存在缺陷等问题而 Github 上部分的 china.json / china.geojson 等数据可能存在通过使用 -where "ADM0_A3 ('CHN','TWN')" 进行拼合而成,因此,也可能存在中国地图中的钓鱼岛、赤尾屿、台湾岛、南海诸岛、藏南地区国界线、阿克赛钦地区国界线等错误。

因此我们强烈建议使用阿里巴巴DataV与高德开放平台所提供的在线数据提供平台: http://datav.aliyun.com/tools/atlas/#&lat=30.332329214580188&lng=106.72278672066881&zoom=3.5 内进行下载geojson或svg数据。本文虽然不采用 Natural Earth 数据,但提供使用以通过 ogr2ogr 转换的步骤和数据的简化操作。

如果我们不使用中国地图的话,可以通过Natural Earth这个带有美国特色双标组织渠道来下载所有地理数据,他主要提供了三种尺寸供我们进行下载:

ID FA DA
大规模数据 1:10m,1:10,000,000,1英寸= 158英里,1厘米= 100公里 最详细且使用与制作国家和地区的放大地图
中等规模数据 1:50m,1:50,000,000,1英寸= 790英里,1厘米= 500公里 适用于制作国家和地区的缩小地图
小规模数据 1:110m,1:110,000,000,1’’= 1,736英里,1厘米= 1,100公里 小型的示意图或小型的定位器的地球仪

当下载的时候,他主要会分为Cultural、Physical、Raster三个选项:

ID DA FA
Cultural 包含具有文化性的地理信息 具有国家和地区的边界划分地图、按照行政省划分地图、包含有飞机场、港口和地图等
Physical 包含具有物理性的地理信息 含有该国家的海岸线、陆地、海洋、河流、港口等
Raster 包含栅格地图

GeoJSON 转换

我们通过在 Natural Earth 下载的 Admin 0 – Countries1:110m 的地图数据,在此之前,我们需要安装GDAL包下的ogr2ogr程序,如果你是 Linux debian 系用户可以通过使用:

1
sudo apt-get install gdal-bin

当安装成功之后,使用ogr2ogr -version来进行查询是否安装成功,当正确输出版本信息后。我们可以将刚刚下载到的ne_110m_admin_0_countries.zip进行解压,解压后进入该目录执行:

1
ogr2ogr -f "GeoJSON" china.geojson ne_110m_admin_0_countries.shp -overwrite -where "ADM0_A3 IN ('CHN')"

也可使用 -where "ADM0_A3 IN ('CHN','USER')" 来提取多个国家数据,ISO 3166-1 alpha-3即“ADM0_A3”标准来通过三个拉丁字母表示国家名称,通常这三个字母就是该国家的代码,如CHN就是中华人民共和国的国家代码。

TopoJSON 转换

TopoJSON 是由 Mike Bostock 提供的一种 GeoJSON 扩展,通过拓扑后的扩展形式,主要使用点、弧来表示图形。一般情况点、线地理实体分别通过坐标和弧索引表示而多边形也通过弧索引来实现。

使用 TopoJSON 的特点在于其大小是 GeoJSON 大小的 80%,原因在于边界线只记录一次(边界线接壤、共用),而地理坐标都使用整数即 **共享边(ARCS)**。

因此 TopoJSON 通过明显的优点深受喜爱,除此之外,还可以通过使用 TopoJSON 内 共享边(ARCS) 的特性来完成一些较为舒服用户交互或特殊标注

TopoJSON 提供了解析 TopoJSON 格式的方法,为了保证 TopoJSON 的拓扑保留简化和过滤以及更小的文件、更快的渲染,因此 TopoJSON 还特地除了个简化步骤。除此还分为客户端和服务器,两者分别是将 GeoJSON 转换为 TopoJSON,以及将 TopoJSON 转换为 GeoJSON :

ID DA FA TO
服务器 TopoJSON.Topology 支持将 GeoJSON 转换为 TopoJSON https://github.com/topojson/topojson-server/blob/master/README.md#topology
Geo2topo 将 GeoJSON 转换为 TopoJSON https://github.com/topojson/topojson-server/blob/master/README.md#geo2topo
简化 TopoJSON.presimplify 准备用于简化的 TopoJSON https://github.com/topojson/topojson-simplify/blob/master/README.md#presimplify
TopoJSON.simplify 删除坐标来简化 https://github.com/topojson/topojson-simplify/blob/master/README.md#simplify
TopoJSON.quantile 计算简化的阀值 https://github.com/topojson/topojson-simplify/blob/master/README.md#quantile
TopoSimplify 简化 TopoJSON 删除坐标 https://github.com/topojson/topojson-simplify/blob/master/README.md#toposimplify
客户端 topoJSON.feature 将 TopoJSON 转换为 GeoJSON https://github.com/topojson/topojson-client/blob/master/README.md#feature
…… https://github.com/topojson/topojson
GeoJSON > topoJSON

通过使用 TopoJSON Server 所提供的 topojson.topology 可以通过将GeoJSON转换为TopoJSON文件,可以使用sudo npm install -g topojson-server进行安装, tpology 自带了geo2topo

我们可以通过执行 geo2topo china.json -o china_topo.json 将 geoJSON 文件转换为 TopoJSON 格式文件 china_topo.json

当然你也可以通过使用其官方所提供的 <script src="https://unpkg.com/topojson-server@3"></script> 在浏览器中进行引入,这是由官方所提供的3.0.1版本

TopoJSON > GeoJSON

可以使用 topoJSON.feature来进行转换,feature 主要分为客户端和浏览器引入两种方法。通过使用cnmp install -g topojson-client来进行全局安装,当然如果没有cnmp也可以通过nmp install -g topojson-client进行安装,但速度会比较慢。

如果通过浏览器引入我们可以通过使用 <script src="https://unpkg.com/topojson-client@3"></script> 来进行引入,这是由官方所提供的3.0.2版本。

简单绘制

GeoJSON

GeoJSON 是一个用于描述地理空间信息的数据格式,因此他的语法是符合JSON规范,只是和JSON不同之处在于对名称进行了规范,名称须为字符串,而值可以是字符串、数字、布尔、对象、数组、NULL等,通常他的外部对象格式是:

2015年,互联网工程任务组(IETF)与最初的的规范作者共同组成了GeoJSON WG,以标准化GeoJSON。RFC 7946 于2016年8月发布,他是GeoJSON格式的新规范,取代了2008年的GeoJSON 规范:

1
2
3
4
5
6
7
8
9
10
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [125.6, 10.1]
},
"properties": {
"name": "Dinagat Islands"
}
}


通常上述 code 主要分为Fature(特征)以及geometry(几何体)来表示出最外层的单独对象,在特征集合(FeatureCollection)中,最外层的GenJSON会包含很多的子对象,对于每一个GeoJSON的每个type属性值必须是下面之一:

ID DA
Point
MultiPoint 多点
LineString 线
MultiLineString 多线
Polygon
MultiPolygon 多面
GeometryCollection 几何体集合
Feature 特征
FeatureCollection 特征集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Point",
"coordinates": [
114.345703125,
23.96617587126503
]
}
}
]
}

绘制

由于我们使用的是DATAV所提供的数据,因此我们你可以通过使用d3,geo.mercator()进行投影,然后使用center()来设置出地图的中心位置。select ad translate分别设置缩放量和平移量,当定义好投影之后,通过使用d3.geo.path()来进行使用投影进行绘制地图路径。

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
26
27
28
29
30
31
32
33
34
35
36
37
var padding = {
width: 1000,
height: 1000
}

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

var projection = d3.geo.mercator()
.center([107,31])
.scale(600)
.translate([padding.width/2,padding.height/2])

var color = d3.scale.category20b()

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

d3.json("https://geo.datav.aliyun.com/areas/bound/geojson?code=100000_full", function (error, root) {
if (error)
return console.error(error)
console.log(root)

var groups = svg.append("g")

groups.selectAll("path")
.data(root.features)
.enter()
.append("path")
.attr("class","province")
.style("fill", function (d,i) {
return color[i]
})
.attr("d",path)
})

CSS

1
2
3
4
5
.province {
fill: white;
stroke: #25595a;
stroke-width: 1px;
}

TopoJSON

TopoJSON 是由 Mike Bostock 提供的一种 GeoJSON 扩展,通过拓扑后的扩展形式,主要使用点、弧来表示图形。一般情况点、线地理实体分别通过坐标和弧索引表示而多边形也通过弧索引来实现。

使用 TopoJSON 的特点在于其大小是 GeoJSON 大小的 80%,原因在于边界线只记录一次(边界线接壤、共用),而地理坐标都使用整数共享边(ARCS)

因此 TopoJSON 通过明显的优点深受喜爱,除此之外,还可以通过使用 TopoJSON 内 共享边(ARCS) 的特性来完成一些较为舒服用户交互或特殊标注

TopoJSON 提供了解析 TopoJSON 格式的方法,为了保证 TopoJSON 的拓扑保留简化和过滤以及更小的文件、更快的渲染,因此 TopoJSON 还特地除了个简化步骤。除此还分为客户端和服务器,两者分别是将 GeoJSON 转换为 TopoJSON,以及将 TopoJSON 转换为 GeoJSON :

ID DA FA
topojson.feature 将 TopoJSON 转换为 GeoJSON,为返回指定的对象 GeoJSON
topojson.merge 合并 Topojson 集合,并分配个目标对象
topojson.mesh 与 topojson.mesh 等效,但返回的是 TopoJSON,可以用于共享坐标
topojson.neighbors 计算出相邻数据

绘制

在下述的 code 中主要通过使用 geojson 格式转换为 topojson 格式,之后在通过使用topojson.feature将此再次使用<script src="https://unpkg.com/topojson@3.0.2/dist/topojson.js"></script> API 转换为 geojson 格式文件。因源文件是 topojson 所以转换后的数据自然是 geojson 文件的 80% 大小,这也是使用 topojson 的好处之一。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
var padding = {
width: 1000,
height: 1000
}

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

var projection = d3.geo.mercator()
.center([107,31])
.scale(600)
.translate([padding.width/2,padding.height/2])

var color = d3.scale.category20b()

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

d3.json("./china_topo.json", function (error, toporoot) {
if (error)
return console.error(error)
console.log(toporoot)

/*
* (1)
* 将 TopoJSON 转换为 GeoJSON 并保存在 Georoot 中。
* Feature 主要用于返回 GeoJSON 的特征(Feature)或特征集合(FeatureCollection)
* feature(topology, object) 第一额参数表示 TopJSON 文件对象,第二个参数来表示出几何体的对象。
* (2) feature
* console 分别会输出 topjson 以及 geojson 的结果。
* 所以在定义 feature() 参数是会考虑到:
* TopoJSON 对象中,有数组 arcs(共享边)、objects(存储几何对象)、 之后对象中有一个 china 对象,所以保存中国地图的几何体,因此:
* topojson.feature(toporoot,toporoot.objects.china)
*/
var georoot = topojson.feature(toporoot,toporoot.objects.china);
console.log(georoot)

var groups = svg.append("g")
groups.selectAll("path")
.data(georoot.features)
.enter()
.append("path")
.attr("class","province")
.style("fill", function (d,i) {
return color[i]
})
.attr("d",path)
})
set and merge


通过 d3.set 创建集合,southeast 包含了各省名称之后通过使用 southeast.has() 来检查传入字符串是否在集合中,合并后保存并输出。 我们将 southeast 内设置为黑色,之后使用 datum()来绑定数据通过 svg.path 进行绘制。

使用 topojson.merge() 来进行合并,每一项保存一个省的几何信息,之后通过对象的 filter() 函数,只有在集合中的省份保存至 mergedPolygon 中。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
var padding = {
width: 1000,
height: 1000
}

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

var projection = d3.geo.mercator()
.center([107,19])
.scale(600)
.translate([padding.width/2,padding.height/2])

var color = d3.scale.category20b()

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

d3.json("./china_topo.json", function (error, toporoot) {
if (error)
return console.error(error)
console.log(toporoot)
/*
* (1)
* 将 TopoJSON 转换为 GeoJSON 并保存在 Georoot 中。
* Feature 主要用于返回 GeoJSON 的特征(Feature)或特征集合(FeatureCollection)
* feature(topology, object) 第一额参数表示 TopJSON 文件对象,第二个参数来表示出几何体的对象。
* (2) feature
* console 分别会输出 topjson 以及 geojson 的结果。
* 所以在定义 feature() 参数是会考虑到:
* TopoJSON 对象中,有数组 arcs(共享边)、objects(存储几何对象)、 之后对象中有一个 china 对象,所以保存中国地图的几何体,因此:
* topojson.feature(toporoot,toporoot.objects.china)
*/
var georoot = topojson.feature(toporoot,toporoot.objects.china);
console.log(georoot)

/*
* (1) 创建集合
* 通过 d3.set 创建集合,southeast 包含了各省名称之后通过使用 southeast.has() 来检查传入字符串是否在集合中,合并后保存并输出。
* (2) 数据绘制
* 将 southeast 内设置为黑色,之后使用 datum()来绑定数据通过 svg.path 进行绘制。
* (3) 数据合并
* 之后通过使用 topojson.merge() 来进行合并,每一项保存一个省的几何信息,之后通过对象的 filter() 函数
* 只有在集合中的省份保存至 mergedPolygon 中。
*/
var southeast = d3.set([
"广东省","海南省","台湾省","北京市","吉林省","黑龙江省"
])
var mergedPolygon = topojson.merge(toporoot,
toporoot.objects.china.geometries.filter(function (d) {
return southeast.has(d.properties.name)
}))
console.log(mergedPolygon)

var groups = svg.append("g")

groups.selectAll("path")
.data(georoot.features.filter(function (d) {
return !southeast.has(d.properties.name)
}))
.enter()
.append("path")
.attr("class","province")
.attr("d",path)

svg.append("path")
.datum(mergedPolygon)
.attr("class","province")
.style("fill","#c6c6c6")
.attr("d",path)
})
mesh


topojson.mesh 主要提供了 网格花 TopoJSON 几何形状以形成多边形, 分别提取了内蒙古、黑龙江省的边界线,边界线最终存储在 boundary 中。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
var padding = {
width: 1000,
height: 1000
}

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

var projection = d3.geo.mercator()
.center([107,19])
.scale(600)
.translate([padding.width/2,padding.height/2])

var color = d3.scale.category20b()

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

d3.json("./china_topo.json", function (error, toporoot) {
if (error)
return console.error(error)
console.log(toporoot)
/*
* (1)
* 将 TopoJSON 转换为 GeoJSON 并保存在 Georoot 中。
* Feature 主要用于返回 GeoJSON 的特征(Feature)或特征集合(FeatureCollection)
* feature(topology, object) 第一额参数表示 TopJSON 文件对象,第二个参数来表示出几何体的对象。
* (2) feature
* console 分别会输出 topjson 以及 geojson 的结果。
* 所以在定义 feature() 参数是会考虑到:
* TopoJSON 对象中,有数组 arcs(共享边)、objects(存储几何对象)、 之后对象中有一个 china 对象,所以保存中国地图的几何体,因此:
* topojson.feature(toporoot,toporoot.objects.china)
*/
var georoot = topojson.feature(toporoot,toporoot.objects.china);
console.log(georoot)

/*
* (mesh)
* topojson.mesh 主要提供了 网格花 TopoJSON 几何形状以形成多边形。
* 分别提取了内蒙古、黑龙江省的边界线,边界线最终存储在 boundary 中。
*/
var boundary = topojson.mesh(toporoot ,toporoot.objects.china, function (a,b) {
return a.properties.name === "内蒙古自治区" && b.properties.name === "黑龙江省"
})
console.log(boundary)
var groups = svg.append("g")

groups.selectAll("path")
.data(georoot.features)
.enter()
.append("path")
.attr("class","province")
.attr("d",path)

svg.append("path")
.datum(boundary)
.attr("class","boundary")
.style("fill","none")
.attr("d",path)
})
neighbors


通过使用 neighbors 来计算出相邻数据,并通过为没一个元素添加相邻省份的选择集,为每个选择时添加 behavior 事件,持续时间分别为 2000ms。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
var padding = {
width: 1000,
height: 1000
}

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

var projection = d3.geo.mercator()
.center([107,19])
.scale(600)
.translate([padding.width/2,padding.height/2])

var color = d3.scale.category20b()

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

d3.json("./china_topo.json", function (error, toporoot) {
if (error)
return console.error(error)
console.log(toporoot)
/*
* (1)
* 将 TopoJSON 转换为 GeoJSON 并保存在 Georoot 中。
* Feature 主要用于返回 GeoJSON 的特征(Feature)或特征集合(FeatureCollection)
* feature(topology, object) 第一额参数表示 TopJSON 文件对象,第二个参数来表示出几何体的对象。
* (2) feature
* console 分别会输出 topjson 以及 geojson 的结果。
* 所以在定义 feature() 参数是会考虑到:
* TopoJSON 对象中,有数组 arcs(共享边)、objects(存储几何对象)、 之后对象中有一个 china 对象,所以保存中国地图的几何体,因此:
* topojson.feature(toporoot,toporoot.objects.china)
*/
var georoot = topojson.feature(toporoot,toporoot.objects.china);
console.log(georoot)

var neighbors = topojson.neighbors(toporoot.objects.china.geometries)
console.log(neighbors)

var groups = svg.append("g")

var paths = groups.selectAll("path")
.data(georoot.features)
.enter()
.append("path")
.attr("class","province")
.attr("d",path)

/*
* 通过使用 neighbors 来计算出相邻数据
* 并通过为没一个元素添加相邻省份的选择集
* 为每个选择时添加 behavior 事件,持续时间分别为 2000ms
*/
paths.each(function (d,i) {
d.neighbors = d3.selectAll(
neighbors[i].map(function (j) {
return paths[0][j]
})
)
})
.on("mouseover", function (d,i) {
d3.select(this)
.transition()
.duration(2000)
.style("fill","#9f9d9d")
d.neighbors
.transition()
.duration(2000)
.style("fill","#d8d8d8")

})
.on("mouseout",function (d,i) {
d.neighbors.style("fill","white")
.transition()
.duration(2000)
})

})
⬅️ Go back