Taiwan Map Cover

在台灣地圖上做視覺化常常讓人困擾,因為很多工具雖然支援地圖上的資料視覺化,但在台灣往往卻看不到比縣市還更細緻的區塊。沒問題,今天我們就來實戰一下,看看利用開源套件我們如何在台灣地圖上製作資料視覺化。

前陣子我們看過了如何用 D3.js 繪製地球儀,相信大家對 GeoJSON 與 TopoJSON 都稍微有了一點概念。基本上,他們是一種用來描述地圖的資料格式,而 D3.js 有足夠的支援讓我們可以很快的把他們畫出來。

我們可以利用類似地球儀的方法來描繪台灣地圖的 TopoJSON ,接著再根據我們的需求著色以製作面量圖 ( Choropleth Map ) 或是統計地圖 ( Cartogram ) 等等的圖表。 問題是,台灣地圖的電子檔哪裡來?

近年由於 OpenData.tw 、 零時政府 G0V 等等組織的推動,政府也逐漸的擁抱開放資料的潮流,甚至出現了政府官方的開放資料平台 data.gov.tw ;而行政邊界圖自然也成為開放資料的其中一項。零時政府有整理好的開源台灣行政區域圖, 網頁上甚至有 Leaflet 、 WebGL 與 Dorling Cartogram 的範例,但並沒有技術細節的說明,今天就讓小編來為大家介紹一下吧!

轉換行政區域圖資料格式

說到行政區域圖,政府釋出了三個等級的資料:

  1. 縣(市)行政區界線
  2. 鄉(鎮、市、區)行政區域界線
  3. 全國村里界圖

因為政府內部所使用系統的關係,釋出的格式均為 SHP 檔格式。感謝 D3.js 的作者 Mike ,剛好他最近在 Github 上開了一個新專案「shapefile」,讓我們可以讀取 SHP 檔並輸出成 GeoJSON 格式;事實上, Mike 已經將 shapefile 整進了他的另一個專案「topojson」,讓我們可以直接由 SHP 檔產生 TopoJSON 檔。

由於 topojson 是 NodeJS 的模組,所以需要先安裝 NodeJS ;NodeJS 的安裝在另一篇文章「資料爬蟲實戰-使用 NodeJS」有說明過,在 Window 下可以到 https://nodejs.org/download/ 下載安裝檔,而在 Linux 或 Mac 環境下則可以參考這個網頁提供的作法來安裝。

確認 NodeJS 安裝完成後,我們解壓縮政府提供的下載檔並使用 topojson 讀取( 以縣市邊界圖為例 ):

  npm install -g topojson
  topojson -s 0.0000001 -o county.json -p --shapefile-encoding big5 county.shp

產生了一個檔案 county.json 即為我們需要的 TopoJSON 檔。

我們也可以使用 API 的方式程式化處理,但事情會變得比較複雜,這裡先略過不談。特別要注意的是原始資料採 Big5 格式,所以我們要用「–shapefile-encoding 」參數提醒 shapefile 使用正確的編碼來轉換。

TopoJSON 處理與繪製

產生 TopoJSON 檔後,我們便利用 topojson 模組提供的函式讀取該檔案:

  <svg width="800px" height="600px" viewBox="0 0 800 600"></svg>
  <script type="text/javascript" src="http://d3js.org/topojson.v1.min.js"></script>
  <script>
    d3.json("county.json", function(topodata) {
      var features = topojson.feature(topodata, topodata.objects["county"]).features;
      // 這裡要注意的是 topodata.objects["county"] 中的 "county" 為原本 shp 的檔名
    });
  </script>

然後利用 d3.geo.path 搭配 d3.geo.mercator 來繪圖:

  var path = d3.geo.path().projection( // 路徑產生器
    d3.geo.mercator().center([121,24]).scale(6000) // 座標變換函式
  );
  d3.select("svg").selectAll("path").data(features)
    .enter().append("path").attr("d",path);

繪製的結果如下:

Taiwan TopoJSON

色階對應製作面量圖

一旦將行政區塊變成資料對應到 SVG 區塊,接下來就與一般視覺化沒有什麼不同了。假設我們想製作人口密度的視覺化,並有類似下表這樣的人口密度資料 ( 資料來源: wiki ):

  var density = {"臺北市": 9952.60, "嘉義市": 4512.66, "新竹市": 4151.27, ...};

我們想要將密度資訊插入到行政區塊的物件中,但我們要怎麼知道哪個區塊是臺北市、哪個區塊嘉義市呢?政府提供的 SHP 檔中行政區塊都帶有名字,經過 topojson 轉換後則存在物件的 properties.C_Name 屬性中。只要知道這一點,一個簡單的迴圈就可以整理好資料。

  for(i=features.length - 1; i >= 0; i-- ) {
    features[i].properties.density = density[features[i].properties.C_Name];
  }

在上例中,我們搭配人口密度資料來更新行政區塊的物件。每個行政區塊物件都帶有一個 properties 物件,裡面會包含各個區塊的額外資訊;這裡的資訊各筆資料都不太一樣,以此處的例子來說, properties.C_Name 會存放區塊對應的縣市名稱。

接著區塊著色也就不是難事了,利用 d3.scale.linear 函式把密度 ( 0 ~ 10000 ) 投射到顏色空間 ( #090 ~ #f00 ):

  var color = d3.scale.linear().domain([0,10000]).range(["#090","#f00"]);
  d3.select("svg").selectAll("path").data(features).attr({
    d: path,
    fill: function(d) {
      return color(d.properties.density);
    }
  });

執行結果如下:

Taiwan Map Visualize (2)

處理圖資的難題

做到這邊,大致上都還算簡單,但是其實這類型的視覺化有一些潛在的問題。台灣開放資料尚在起步,除了最基本的「五星開放資料」標準尚無法達成外,歷史資料追朔、全面萬國碼、名稱標準化等諸多良好資料具有的要素都還辦法達成;行政區塊是個很典型的例子,比方說:

  • 舊資料無法正確在新地圖上呈現:
    • 村里會增減、縣市有改制,例如台北縣更名成新北市後,很多資料都需要重新對應
    • 這有待政府提供詳細的邊界變化、名稱修正的歷史紀錄才有辦法很好的處理
  • 文字編碼問題
    • 有些地方的名字非常特別,Big5 無法呈現,最後只能用造字的手法來處理。這在萬國碼轉換時會產生亂碼
    • 有時是異體字的問題,比方說「台北市」與「臺北市」、「五峰鄉」與「五峯鄉」造成的問題
    • 所幸政府提供的 SHP 檔中有附上地名對應的 ISO 3316-2 ID,可以讓我們透過代碼做比較
  • 顯示範圍問題
    • 台澎金馬涵蓋範圍頗廣,若要納入同一份地圖,海會佔很大一片面積
    • 因此有必要利用客製化投影轉換函式把離島拉到台灣本島附近,但也帶來了額外實作負擔
  • 資料檔太大:
    • 村里級的 TopoJSON 即使簡化過仍耗費 1MB 以上的空間
    • 另外,上千筆的資料建立 SVG 物件,效能會變成一個很重要的問題

實際上在運用地理區塊做視覺化時,多少都還是需要手動做一些修正。即使如此,能夠自己使用 D3.js 畫出行政區塊也是很不錯的開始;接著我們就有機會嘗試 Cartogram 、 Google Maps Overlay 、 Dorling Transformation 等等的變化,對地圖做更好的視覺化運用,比方說下面這個例子在滑鼠移過地圖時,地圖區塊會有膨脹的效果出現:

上例的程式碼可以在這裡找到。

相關的主題我們會陸續在這裡與大家分享,如果有興趣的話不妨定時關注我們的動態,以免漏掉了最新的地圖視覺化教學喔!

技術問題補充 ( 2016/01/19 )

有些朋友在使用 d3.json 時碰到了困難,這是因為 d3.json 使用 AJAX ( XMLHttpRequest ) 向網頁伺服器要求資料,但當我們直接用瀏覽器開啟本機電腦網頁時,使用的是「file://」Protocol ,這時我們的電腦上並沒有網頁伺服器可以提供回應,因此就會失敗。

如果上面這一長串看不懂的讀者,可以試著自己改用下面的方式來讀取 json 檔。首先,開啟我們的 geojson 檔 ( 以上例來說,就是 county.json ) ,原始碼如下:

{"type":"Topology","objects":{"county":{"type":"GeometryCollection","bbox":[116.7062 ..... }

在最前面加上一個變數宣告,並在結尾加上一個「;」分號:

  var topodata = {"type":"Topology","objects":{"county":{"type":"GeometryCol ... };

接著,把檔名改為 county.js 後,在我們的 html 之中引入該檔:

  ...
  <svg width="800px" height="600px" viewBox="0 0 800 600"></svg>
  <script type="text/javascript" src="http://d3js.org/topojson.v1.min.js"></script>
  <script type="text/javascript" src="county.js"></script>
  <script>
  ...

這時我們便已經將地理資料 json 存在「topodata」,在網頁中可以直接利用,原本使用 d3.json 讀取資料的程式碼片段便不需要了:

  <script>
    /* d3.json 不再需要了 */
    /*d3.json("county.json", function(topodata) {*/
    var features = topojson.feature(topodata, topodata.objects["county"]).features;
    // 這裡要注意的是 topodata.objects["county"] 中的 "county" 為原本 shp 的檔名
    /* }); */
  </script>

這是所謂的「jsonp」技巧,如果只是需要做開發測試用途,還算是方便。當然,建議如果開發的程式會應用到 Production 環境,還是要考量到是否在自己的電腦架設網頁伺服器,以避免開發與實際運作環境需要兩份程式碼喔!


Written by infographics.tw

11 Comments

RexLi

很棒的教學 ,範例的程式法好像有點錯。


var features = topojson(topodata, topodata.objects["county"]).features;

var features = topojson.feature(topodata, topodata.objects["county"]).features;

Reply
Andy

打擾了~~
我剛有留言說範例打不開
自己找到原因了~~
IIS沒有添加JSON的MIME檔案類型

Reply
Andy

你好~~
想請問一下
目前這個地圖描繪出來縣市邊界的顏色是白色
有沒有設定選項或參數可以修改邊界的顏色呢?

Reply
infographics.tw

您好,可以透過 stroke 與 stroke-width 屬性來改變邊界的樣式;例如:

d3.select(“svg").selectAll(“path").data(features).attr({
stroke: “red",
strokeWidth: “2″
});

會設定邊界寬度為 2 以及顏色為紅色。

Reply

發表迴響

你的電子郵件位址並不會被公開。 必要欄位標記為 *