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 );
});
效果如下:
總結
地圖會成為我畢設后續代碼編寫的一塊主要內容,通過這幾天的學習初步掌握了地圖繪制的方式,在以后的代碼編寫中再來鞏固。
轉載于:https://www.cnblogs.com/xiaoYu3328/p/6536036.html
總結
- 上一篇: 怎么将图片格式转换成JPG?学会这两种方
- 下一篇: 【每日一题】4月9日题目精讲 Runni