D3学习之地图
D3学习之地图
(2017.03.09-03.11)
地图的意义
在可视化领域中,将数据点投影和关联到地理区域上,是一个非常关键的内容(体现了可视化中利用读者自身知识常识从而加速吸收信息的原则)。
GeoJSON and TopoJSON
GeoJSON是用于描述地图空间信息的数据格式。GeoJSON不是一种新的格式,其语法规范是符合JSON格式的,只不过对其名称进行了规范,专门用于表示地理信息。GeoJSON里的对象也是由名称/值对的集合构成,名称总是字符串,值可以是字符串、数字、布尔值、对象、数组、null。内部结构的话,每一个GeoJSON都必有一个type属性,表示对象的类型,如Point(点)、LineString(线)、Feature(特征)等。
上图展示的就是一个标准GeoJSON文件的内容,可以看到type值为FeatureCollection(特征集合),则该对象必须要一个名称为features的成员(就在type下一行),features是一个数组,数组每一项都是一个特征对象,如果是中国地图的话,则每一项描述一个省的地理信息。
TopoJSON是GeoJSON按拓补学编码后的扩展形式,TopoJSON中的每一个几何体都是通过将共享边整合后组成的,从而消除了部分冗余数据,同时地理坐标使用整数,因此文件大小较GeoJSON缩小了80%左右。同时,TopoJSON的语法规范也是符合JSON格式的。
获取地图数据
首先介绍一个非常好的地理数据网站Natural Earth,其中提供了大量免费的地理数据下载内容,包括中国地图,那么在网站上下载了相应的zip包后,进行解压:
其中的shp文件,我们需要从中提取出需要的地理信息,并保存为中文形式,标准的工具室ogr2ogr,但是工具需要使用命令行操作,而且需要VS进行编译,较为麻烦,因此我们使用一个基于ogr2ogr开发的图形化软件:ogr2gui,下载地址为ogr2gui,下载完成后打开工具,进行下图操作:(切记!!导入的文件和导出的文件路径中一定不能出现汉字,不然会导致导出失败并且没有任何错误提示!)
完成后生成china.geojson文件,那么我们通过在线工具来看看我们获取的数据绘制成地图后长什么样,浏览http://mapshaper.org,导入我们的geojson文件:
地图绘制得不错,然而不能接受的是地图里没有台湾省。。。。所以最后我在网上下载了一个更合适的地图:
mapshaper这个网站还有一个很重要的功能就是简化地图的边界,原始地图数据通常非常大,因为其中包含了大量地图的细微边界变化数据,而其中一些我们并不需要,因此可以进行简化,下图就是简化后的效果:
可以发现适当的简化并不影响地图的整体效果(并且有时候,舍弃基本的地理信息可以让我们展示更真实的数据,见我的博客《数据可视化之美阅读》)。
使用D3绘制地图
那么我们最终的目的当然是使用D3来绘制地图,GeoJSON和TopoJSON格式都可以绘制地图,然而TopoJSON具有文件大小更小的优势,所以尽可能都是用TopoJSON,但TopoJSON的缺点在于它标准是由D3作者制定,目前还不是世界范围内承认的标准。下面的内容,我们来分别使用两种方式来绘制地图。
GeoJSON
首选无论使用GeoJSON还是TopoJSON,都需要先定义地图的投影和地理路径生成器,具体代码和注释如下:
var projection = d3.geo.mercator()
.center([107, 31]) //设置地图中心位置,前经度后纬度
.scale(850) //设置缩放量
.translate([width/2, height/2]); //设置平移量
//定义地理路径生成器
var path = d3.geo.path() //应用上面生成的投影,每一个坐标都会先调用此投影函数,然后才产生路径值
.projection(projection);
然后,通过d3.json请求文件china.geojson,并添加足够数量的path(svg的path,svg是d3的基础),每一个path用于绘制一个省的路径。
//颜色比例尺
var color = d3.scale.category20();
//请求china.geojson,把文件的json内容传递给root对象
d3.json("../geojson/china.geojson", function(error, root) {
if (error)
return console.error(error);
console.log(root.features);
svg.selectAll("path")
.data( root.features )
.enter()
.append("path")
.attr("stroke","#aaa") //svg边线属性定义,这里是颜色
.attr("stroke-width",1) //这里是宽度
.attr("stroke-dasharray",10,10) //svg stroke虚线
.attr("fill", function(d,i){ //每一块的颜色填充
return color(i);
})
.attr("d", path )
.on("mouseover",function(d,i){ //两个交互,鼠标放置和鼠标移开
d3.select(this)
.attr("fill","yellow");
})
.on("mouseout",function(d,i){
d3.select(this)
.attr("fill",color(i));
});
});
至此,地图绘制成功,步骤非常简洁,并且带有部分交互效果,我们来看看效果:
如上图所示,鼠标箭头停留处对应的省份颜色会变成黄色,实现了一定程度的交互效果(虚拟机截图关系,看不到鼠标箭头。。)。
查看网页源代码,看看地图的HTML格式:
每一个path对应一个省份,并且都在svg元素内。
TopoJSON
使用上文提到的mapshaper网站,可以将GeoJSON文件转为TopoJSON文件后导出。首先需要明确的一点是,我们使用D3虽然导入的是topojson文件,但D3通过将TopoJSON对象转换为GeoJSON再绘制地图,所以实质还是使用GeoJSON对象绘制地图,和上面的操作并不多少不同。我们主要来看看对象转换过程:
d3.json("../geojson/china.topojson",function(error,toporoot){
if(error)
return console.error(error);
//输出china.topojson的对象
console.log(toporoot);
//将topoJSON对象转换为GeoJSON,保存在georoot中
//然而需要注意的是,实际上在绘制地图时,还是使用了GeoJSON对象。
//feature方法返回GeoJSON的特征(Feature)或特征集合(FeatureColleciton)
var georoot = topojson.feature(toporoot,toporoot.objects.china);
console.log(georoot);
后面绘制过程和使用GeoJSON并不差别,所以不贴代码了。
那么为什么我们要使用TopoJSON呢,除了它文件相比GeoJSON会小很多外,它还能实现一些有趣的功能。
①合并地区
举个例子,要将东南各省合并在一起用一个颜色表示,那么就可以使用topojson.merge( )方法来返回一个合并后的几何体对象,并且其中只保存着我们所需要的几个省的几何信息。代码如下:
var southeast = d3.set([
"广东", "海南", "福建", "浙江", "江西",
"江苏", "台湾", "上海", "香港", "澳门"
]);
//所有省份
var georoot = topojson.feature(toporoot,toporoot.objects.china);
//合并东南各省
var mergePolygon = topojson.merge(toporoot, toporoot.objects.china.geometries
.filter(function (d) {
return southeast.has(d.properties.name); //只有集合中名字相称的省份才会留下,其他会被filter过滤
}
));
在绘制的时候我们分两步来绘制,一、绘制除东南各省外的其他省份;二、绘制东南各省,代码如下:
//先不绘制选中的那几块
svg.selectAll("path")
.data(georoot.features.filter(function(d){
return !southeast.has(d.properties.name); //筛选掉东南各省,不绘制
}))
.enter()
.append("path")
.attr("class","province")
.style("fill","#ccc")
.attr("d",path);
//绘制东南各省
svg.append("path")
.datum(mergePolygon)
.attr("class","province")
.style("fill","blue") //用蓝色标注
.attr("d",path);
绘制后,看看结果:
效果不错~
②绘制边界线
假设我们现在需要用蓝色标注新疆的西藏的边界,使其更加显眼,那该怎么做呢,下面代码展示了如何使用topojson做到边界线的绘制:
d3.json("../geojson/china.topojson",function(error,toporoot){
if(error)
return console.log(error);
//获取西藏和新疆的边界线
var boundary = topojson.mesh(toporoot,toporoot.objects.china,function (a,b) {
//经尝试发现a和b的取值存在顺序关系,参数相反的话无法识别,所以正反条件都加上了
return (a.properties.name ==="西藏" && b.properties.name ==="新疆")
or (b.properties.name ==="西藏" && a.properties.name ==="新疆");
});
console.log(boundary);
var georoot = topojson.feature(toporoot,toporoot.objects.china);
//绘制整体地图
svg.selectAll("path")
.data(georoot.features)
.enter()
.append("path")
.attr("class","province")
.style("fill","#ccc")
.attr("d",path);
//绘制特殊边界线
svg.append("path")
.datum(boundary) //boundary为topojson.mesh方法生成的几何对象
.attr("class","boundary")
.style("fill","none") //path如果不设置fill为none的话会自带黑色填充,导致无法呈现为一条线
.style("stroke","blue")
.style("stroke-width",3)
.attr("d",path);
});
效果如图:
③查找相邻区域
TopoJSON除了可获取两省份的边界线之外,还可以计算与一个省份相邻的省份,需要用到topojson.neighbors( )方法,代码如下:
d3.json("../geojson/china.topojson",function(error,toporoot){
//通过topojson.neighbors计算所有省份的相邻省份,保存在数组neighbors里
//数组neighbors保存有各省份的邻省序号
var neighbors = topojson.neighbors(toporoot.objects.china.geometries);
var georoot = topojson.feature(toporoot,toporoot.objects.china);
paths = svg.selectAll("path")
.data(georoot.features)
.enter()
.append("path")
.style("fill","#ccc")
.attr("class","province")
.attr("d",path);
console.log(paths);
svg.selectAll("path").each(function (d,i) {
//为每一个元素添加相邻省份的选择集
d.neighbors = d3.selectAll(
neighbors[i].map(function(j){ //使用map方法通过序号返回邻省的path对象
return paths[0][j];
})
);
}).on("mouseover",function (d,i) {
//鼠标移入后,变色
d3.select(this).style("fill","red");
d.neighbors.style("fill","steelblue");
}).on("mouseout",function(d,i){
//鼠标移出后,恢复原来的颜色
d3.select(this).style("fill","#ccc");
d.neighbors.style("fill","#ccc");
});
});
效果如图:
网格生成器
作为地图,有时候需要我们添加经纬线,我们可以使用网格生成器来绘制:
var width = 1000, height = 1000;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(0,0)");
var eps = 1e-4; //防止网格没有边界线,不过因为我们是对中国区域画网格,并不影响
//创建一个经纬度网格生成器,设置经度和纬度范围以及步长
var graticule = d3.geo.graticule()
.extent([[71,16],[137+eps,54]])
.step([3,3]);
//生成网格数据
var grid = graticule();
var projection = d3.geo.mercator()
.center([107,31])
.scale(800)
.translate([width/2,height/2]);
var path = d3.geo.path().projection(projection);
d3.json("../geojson/china.topojson",function(error,toporoot){
if(error)
return console.error(error);
var georoot = topojson.feature(toporoot,toporoot.objects.china);
svg.append("path")
.datum(grid)
.attr("class","graticule")
.style("stroke","steelblue")
.style("stroke-width","2")
.attr("d",path);
svg.selectAll("path.province")
.data(georoot.features)
.enter()
.append("path")
.attr("class","province")
.attr("fill", "#ccc")
.style("stroke","steelblue")
.attr("d", path );
});
效果如下:
总结
地图会成为我毕设后续代码编写的一块主要内容,通过这几天的学习初步掌握了地图绘制的方式,在以后的代码编写中再来巩固。