world atlas globe cover

看過地球儀嗎?有沒有想過要怎麼做類似的視覺化?感覺起來好像很難,但是用 D3.js 的話,短短十幾行程式就做得到喔!這次 D3js 實戰我們要來看如何從無到有生出一個可以轉動的地球儀視覺化,看完你會發現原來這麼簡單!

前陣子我們分享了的 D3.js 簡介 中提到,除了資料綁定外,D3.js 也提供了一整組相當強大的視覺化函式庫,包含向量繪圖、座標轉換等等;裡面也包含了一組地理圖形相關的函式可以幫我們讀取地形向量檔、繪製行政區塊並且做各種投影,如下圖:

d3js-global projections

使用 D3.js 畫地圖並不複雜,我們讀取現有的地圖圖檔內各個行政區,透過 data() 函式逐一與 SVG 的 path 標籤綁定,做法跟製作長條圖、圓餅圖的做法差異並不大;然而接著我們馬上會碰到兩個問題:

  1. 地圖的資料格式?要怎麼讀取?
  2. 讀入的地圖要怎麼轉換成 <path> 標籤?

很幸運的是這兩個問題 D3js 都有處理,我們只要利用強大的 D3js 函式庫,一切問題都能迎刃而解。

地圖資料格式

D3.js 支援 GeoJSON 的地圖格式,它是一種將地圖以點、線、多邊形包裝而成的 JSON 資料格式。由於在 GeoJSON 中各個行政區塊是各別描述的,重疊的邊界會讓檔案變得比較大,所以出現了另一種格式 TopoJSON ,它把共用的邊集合在一起,再用這些邊組成地理區塊,可以省下將近 80% 的檔案大小。

雖然 D3.js 無法直接讀取 TopoJSON,D3.js 作者 Mike 有提供了一個函式庫幫我們讀取 TopoJSON 並轉換成 GeoJSON 的格式。同時, Mike 也利用 Natural Earth 的地球資訊製作了一份世界地圖的 TopoJSON 檔,轉換程式與結果都有釋出;我們也可以利用該程式自己做一個 TopoJSON 出來,而 Natural Earth 的資料授權為 CC0 ( Public Domain ) ,所以不用擔心授權的問題。

取得世界地圖 world.json 後利用 TopoJSON 讀取:

  <svg width="800px" height="600px"></svg>
  <script type="text/javascript" src="http://d3js.org/topojson.v1.min.js"></script>
  <script>
    d3.json("world.json", function(world) {
      var countries = topojson.feature(world, world.objects.countries).features;
    });

這裡的 countries 變數已經取得每個國家的地理區塊,接著我們就利用這個資料來繪圖。

地圖資料繪製

繪製地圖牽涉到兩個問題,D3.js 也提供了兩組便利函式各別來處理:

  1. 球形的地球如何投影到平面的地圖上?
  2. 投影後的資料如何變成 SVG Path 標籤?

投影函式

將球面投影到平面有各種投影法,麥卡托、等角圓柱 … 等等,我們現在要做的是把我們所看到的球面直接投影在平面上,可以使用 d3.geo.orthographic 函式:

  var globalProjection = d3.geo.orthographic().scale(245).translate([400,300]).clipAngle(90);

簡單的說明一下:

  • scale(245): 投影後的地圖大小
  • translate([400,300]): 投影後的中心位置,我們將之移動到一個長寬為 800 x 600 的 SVG 元素中央,所以是 [400, 300]
  • clipAngel(90): 投影時,地球另一面的區塊略過不畫

SVG 轉換

有了投影函式,我們接著利用 d3.geo.path 函式幫我們建立資料轉換成 SVG Path 標籤的轉換函式:

  var pathRenderer = d3.geo.path().projection(globalProjection);

這裡利用 projection 函式設定繪製時的投影法為剛剛我們建立的投影。再來把資料扔到 D3 裡用,讓 pathRenderer 幫我們畫 <Path> 的參數,就完成了:

  d3.select("svg").selectAll("path").data(countries).attr("d", pathRenderer);

結果如下:

first attempt of earth rendering

到目前為止只寫了三行程式碼,感謝 D3.js 的強大函式庫。也可以利用 D3.js 的顏色函式 category20 替他上點色:

  var color = d3.scale.category20();
  d3.select("svg").selectAll("path").data(countries).attr({
    "d": path, 
    "fill": function(d) { return color(d.id); }
  });
結果如下:
second attempt of earth rendering

 

轉動吧!地球

光是這樣當然不夠,地球儀要可以轉才對。我們可以利用滑鼠拖動來轉動地球,這時 D3.js 的另一組函式 d3.behavior.drag 就可以派上用場了。 d3.behavior.drag 並不複雜,我們需要設定兩件事:

  • 利用 drag.origin 設定拖動的起點
  • 利用 drag.on(“drag") 設定拖動時的處理函式

D3.js 的滑鼠事件會將滑鼠位置等資訊存在 d3.event 物件之中,我們可以使用 d3.event.x 與 d3.event.y 取得滑鼠的位置。至於地球轉動, orthographic 投影提供了 rotate 函式,讓我們可以設定投影時球體轉動的角度 (一共有三個值,分別對應到 X、Y、Z 三個軸 ) 。

簡單的說,在拖曳事件發生時,我們該做的是把 d3.event.x 跟 d3.event.y 拿來更新 globalProjection.rotate 函式,程式碼如下:

  d3.select("svg").call( /* 拖動事件在 SVG 元素上發生 */
    d3.behavior.drag()
      .origin(function() {
        var r = globalProjection.rotate(); /* 目前轉動的角度 ... */
        return {x: r[0], y: -r[1]}; /* ... 做為這次拖動的起點 */
      })
      .on("drag", function() {
        var r = globalProjection.rotate();
        /* 更新投影的角度 */
        globalProjection.rotate([d3.event.x, -d3.event.y, r[2]]);
        /* 更新完投影後必須要重畫一遍地圖 */
        d3.select("svg").selectAll("path").attr("d",path);
      })
  );

這邊有點小複雜,可以參考相對應的函式說明 ( projection.rotate / d3.behavior.drag ) 。最後的結果如下,別忘了用拖動看看,這是會轉動的地球喔:

程式碼:

  <svg width="800px" height="600px"></svg>
  <script type="text/javascript" src="http://d3js.org/topojson.v1.min.js"></script>
  <script>
  d3.json("world.json", function(world) {
    var countries = topojson.feature(world, world.objects.countries).features;
    var globalPorjection = d3.geo.orthographic()
      .scale(245).translate([400,300]).clipAngle(90);
    var pathRenderer = d3.geo.path().projection(globalProjection);
    var color = d3.scale.category20();
    d3.select("svg").selectAll("path").data(countries).attr({
      "d": path,
      "fill": function(d) {return color(d.id);
    }});

    d3.select("svg").call(
      d3.behavior.drag()
        .origin(function() {
          var r = globalProjection.rotate();
          return {x: r[0], y: -r[1]};
        })
        .on("drag", function() {
          var r = globalProjection.rotate();
          globalProjection.rotate([d3.event.x, -d3.event.y, r[2]);
          d3.select("svg").selectAll("path").attr("d",path);
        })
    );
  });
  </script>

最後結果的程式碼可以在 gist.github.com 找到,你可以把他 fork 下來玩玩看。

結語

我們一步一步的從讀資料開始,實作到可以拖動的地球儀,有沒有覺得很酷呢?這次我們利用了這些 D3js 函式:

  • d3.json
  • d3.geo.orthographic
  • d3.geo.path
  • d3.scale.category20
  • d3.behavior.drag
  • d3.call
  • d3.event

有趣的是只要把 orthographic 函式換成其它投影,我們立刻就可以得到完全不一樣的世界地圖,而由於每一塊國家區塊都是一個 <path> 標籤,我們可以對他做上色、畫邊、變形等等的各種動作。想玩玩看嗎?就從這邊文章裡的程式碼出發,自己動手改改看吧!


Written by infographics.tw

5 Comments

andy

程式碼內錯誤:
第6行 var globalPorjection = d3.geo.orthographic();
的global"Por"jection 應該是 globalProjection

第11行 “d": path
path未宣告
應該是"d": pathRenderer

倒數第6行globalProjection.rotate([d3.event.x, -d3.event.y, r[2]);
r[2]的右邊少一個中括號

Reply
Dylan Yang

Hi, seems lost some code like enter & append path. Really thanks for sharing this article.

Reply
infographics.tw

該圖是利用 photoshop 做出來的,實際上要用 d3js 做出類似效果的話,可以試著用兩個 orthographic projection 帶入不同的 scale, 一塊做影子、另一塊做陸塊即可.

Reply

發表迴響

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