2018年8月3日 星期五

D3.js 學習筆記 (一) : 基本用法

上週去母校高科大圖書館還書, 館員看到我的校友借書證 7/31 日到期, 就幫我延長一年, 同時透露學校有計畫要推出新校友卡, 採使用者付費制, 也就是免費借書時代即將過去, 所以這次書借回來就有急迫感而認真在看了.

這次借到一本 D3.js 的書 :

# D3.js 數據可視化實戰手冊 (人民郵電出版, 楊銳等譯)




此書其實是譯自 Nick Qi Zhu 寫的 "Data Visualization with D3.js Cookbook (Packt, 2013)", 原版目前已經出到第二版了 :




作者 Nick Qi Zhu 是第一個 AI 輔助購物網站 Yroo.com 的創辦人與技術長, 曾任職美國伊利諾州 ThoughtWorks 公司資深程式設計師, 也是 dc.js 框架 (基於 D3.js) 的作者. 書中範例檔案可從下列網站下載 :

https://github.com/NickQiZhu/d3-cookbook
https://github.com/NickQiZhu/d3-cookbook-v2

除了上面這本外, 事實上 D3.js 的書籍還蠻多的 (可見其重要性與受歡迎程度), 網路書店還可找到如下幾本好書 :
  1. 網頁互動式資料視覺化 : 使用 D3 (歐萊禮)
  2. Interactive Data Visualization for the Web (Oreilly)
  3. Learning D3.js 5 Mapping (Packt)
  4. D3.js in Action: Data visualization with JavaScript (Manning)
  5. Learning d3.js Data Visualization  (Packt)
  6. D3.js Cutting-edge Data Visualization (Packt)
  7. Practical D3.js (Apress)
  8. Begining JavaScript Charts (Apress)
其中 "Begining JavaScript Charts" 這本的第五章介紹 D3.js 簡易淺顯, 非常適合入門者無痛學習.

D3.js 是一個用來在網頁上動態顯示資料圖形的前端 Javascript 框架, 具有輕量級與簡單易用的特性, 它利用 HTML, CSS, 以及 SVG 技術使數據的形象以圖表方式鮮活地表現於網頁上, D3.js 稱其為數據驅動文件 (Data-Driven Documents) 技術, 故名為 D3, 意即可透過數據來操作文件.

D3.js 是目前最受歡迎的開源資料視覺化 (Data Visualization) 技術, 採用 BSD 開源授權, 它提供功能強大的可視化物件讓使用者可以用數據驅動的方式去操作 DOM. 在 ThoughtWorks 公司自 2010 年起每年發布的技術潮流走向觀察報告 "技術雷達" 中, D3.js 是瞬息萬變的技術趨勢中的長青樹.

D3.js 源自 Portovis 框架, 這是史丹福大學博士生 Mike Bostock 所開發的一套資料視覺化工具, 他在 2010 年於 IEEE InfoVis 發表了 "Declarative Language Design for Interactive Visualization" 的論文, 成為他後來於 2011 年設計 D3.js 的基礎, 參考 :

https://zh.wikipedia.org/wiki/D3.js

D3.js 目前最新版為 v5.5.0 版, 可在官網下載 d3.zip :

https://d3js.org/

解壓縮後將其中的 d3.js (約 484KB) 或壓縮版的 d3.min.js (約 232KB) 嵌入到網頁中 的 script 標籤中即可 (屬性 type="text/javascript" 不需要, 因為 Javascript 已獨霸天下), 例如 :

<script src="js/d3.js"></script>

或壓縮版 :

<script src="js/d3.min.js"></script> 

也可以使用 D3 官網提供的 CDN (注意, 檔名有版本), 例如 :

<script src="https://d3js.org/d3.v5.js"></script> 

或壓縮版 :

<script src="https://d3js.org/d3.v5.min.js"></script>    

範例網頁模板如下 :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <style>
      <!-- CSS style here -->
    </style>
  </head>
  <body>
    <!-- Web content -->
  </body>
  <script type="text/javascript">
    <!-- Script using d3 objects -->
  </script>
</html>

注意, D3.js 程式碼使用了 UTF-8 字元集, 因此在網頁中必須在 head/meta 標籤中指定 charset 為 utf-8, 否則 D3.js 的效果將不會出現甚至發生錯誤; 但若使用 d3.min.js 則不需要. 其次, D3.js 的 script 必須放在 head 內, 而執行 D3 指令的 script 則放在 body 內, 總之 D3.js 必須先下載至網頁內才行.

D3.js 的最上層物件名為 d3, 其用法跟 jQuery 類似, 先呼叫 select() 或 selectAll() 方法選取網頁中的元素後, 再利用一連串的修飾函數 (modifier function) 去操作元素, 例如添加或修飾內容與屬性等. 總之, 選擇元素 (Selection) 在 D3.js 中是最核心的操作, 主要是透過下列的 CSS 選擇器選取方式 :
  1. 元素名稱 (Tag name)
  2. 元素 id 屬性
  3. 元素 class 屬性
  4. 偽類選擇器 (Pseudo-selector)
D3.js 用來選擇網頁元素的函數有兩個 :
  1. d3.select() : 選取第一個指定的元素
  2. d3.selectAll() : 選取所有指定的元素
這兩個函數的傳入參數是字串, 可以傳入 tag name (如 a, p, div 等), id (以 # 開頭), 或 class (以 . 開頭) 等參數來選取網頁元素, 例如傳入 "p" 表示選取 p 元素, 傳入 "#p1" 表示選取 id 為 p1 的元素; 傳入 ".class1" 表示選取樣式類別為 class1 的元素. 也可以將多個標的以 AND 或 OR 方式選取, 例如 ".class1.class2" 表示選取具有 class1 與 class2 兩種樣式類別之元素 (AND); 而 ".class1, .class2" 則是選取具有 class1 或 class2 任一種樣式類別之元素.

選取函數的傳回值為物件 (object), 包含一個或多個元素之陣列. 注意, 即使網頁中有多個符合選取條件之元素, 函數 select() 只會傳回其中的第一個 (只有一個元素的陣列), 而 selectAll() 則傳回全部符合之元素物件之陣列, 例如 :

 測試 1  :  https://tony1966.000webhostapp.com/test/D3/d3_select.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
    <p name='p1'>這是 p1</p>
    <p name='p2'>這是 p2</p>
  </body>
  <script>
    var sel=d3.select('p');
    console.log(sel);
  </script>
</html>

在 Chrome 瀏覽器中按 F12, 選取 Console 頁籤可看到在 _group 的 0 屬性值是一個只有一個 p 元素的陣列 :




但是如果改用 selectAll() 的話, 就變成有兩個 p 元素的陣列了, 例如 :

測試 2  :  https://tony1966.000webhostapp.com/test/D3/d3_selectAll.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
    <p name='p1'>這是 p1</p>
    <p name='p2'>這是 p2</p>
  </body>
  <script>
    var sel=d3.selectAll('p');
    console.log(sel);
  </script>
</html>

結果如下 :




呼叫 select() 或 selectAll() 取得元素物件後, 可以用 D3.js 所提供的操作函數 (operator) 來操作元素, D3.js 提供如下操作函數 :
  1. text() : 操作元素的文本內容 (=innerText)
  2. html() : 操作元素的網頁內容 (=innerHTML)
  3. attr() : 操作元素屬性
  4. style() : 操作元素的樣式
  5. append() : 在目前元素的最後一個子元素後面新增元素
  6. insert() : 在目前元素內插入新元素
注意, append() 與 insert() 只是新增空的標籤而已, 必須再呼叫 text() 或 html() 加入內容才看得到. 下面的測試利用 text() 修改 p 元素的文本內容, 在網頁上曬出 "Hello World!" 招呼語 :

測試 3  :  https://tony1966.000webhostapp.com/test/D3/d3_hello_world.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
    <p id="hello"></p>
  </body>
  <script>
    d3.select('#hello')
      .text('Hello World!');
  </script>
</html>

此範例先在網頁中放置一個 id 為 hello 的無內容 p 段落標籤, 然後呼叫 d3.select() 取得此元素物件, 再呼叫其 text() 方法設定 p 元素的文字內容為 'Hello World!'. 這種連續呼叫物件函數的方式跟 jQuery 的做法是一樣的, 稱為 function chaining.

操作函數 text() 只能修改元素的純文字內容, 如果要修改內部 HTML 內容須呼叫 html() 函數, 例如 :

測試 4  :  https://tony1966.000webhostapp.com/test/D3/d3_html.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
    <p name='p1'>這是 p1</p>
    <p name='p2'>這是 p2</p>
  </body>
  <script>
    var sel=d3.select('p');
    sel.html("<i>Hello World!</i>");
  </script>
</html>

此網頁中的兩個 p 元素用 select() 選取時只會傳回第一個 p 元素物件, 呼叫 html() 將其 innerHTML 內容改為斜體之 "Hello World!", 執行結果如下 :

Hello World!
這是 p2

元素 p2 內容不受影響, 可見 select() 只會傳回第一個符合之元素, 因此 呼叫 html() 的效果只作用於 p1 元素上. 如果要將改變套用到全部符合之元素必須用 selectAll() 選取, 例如 :

測試 5  :  https://tony1966.000webhostapp.com/test/D3/d3_html_selectAll.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
    <p name='p1'>這是 p1</p>
    <p name='p2'>這是 p2</p>
  </body>
  <script>
    var sel=d3.selectAll('p');
    sel.html("<i>Hello World!</i>");
  </script>
</html>

執行結果如下 :

Hello World!
Hello World!

可見兩個 p 元素原本的文本內容 "這是 px" 都被改成 "Hello World!" 了!

上面範例中 D3.js 元素物件的 text() 或 html() 方法只有一個可能的參數, 有傳參數進去時為 setter 方法; 沒有傳參數為 getter 方法. 但在 attr() 這種有兩個可能參數的方法來說, 則是傳一個參數為 getter; 傳兩個參數為 setter. D3.js 的 attr() 方法用來設定或取得元素的屬性, 例如 : 

測試 6  :  https://tony1966.000webhostapp.com/test/D3/d3_attr.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
    <p id='hello'>Hello World!</p>
  </body>
  <script>
    var p=d3.select('#hello');
    p.attr('style', 'font-size:40px;');
    alert(p.attr('style'));
  </script>
</html>

此例透過 id 取得 p 元素物件後呼叫 attr() 之 setter 方法設定 style 屬性, 然後呼叫 attr() 的 getter 方法讀取 style 屬性. 執行結果如下 :


不只是樣式, 方法 attr() 可設定元素的各種屬性. 事實上除了使用 attr() 外, D3.js 物件還有 style() 方法專門用來設定標籤物件之屬性, 此方法也是有兩個可能參數, 只傳一個時為 getter, 傳兩個時為 setter, 例如 :

測試 7  :  https://tony1966.000webhostapp.com/test/D3/d3_style.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
    <p id='hello'>Hello World!</p>
  </body>
  <script>
    var p=d3.select('#hello');
    p.style('font-size', '40px');
    alert(p.style('font-size'));
  </script>
</html>

可見效果與用 attr() 是相同的. 但注意, 第二個參數末尾不可以加 ";", 必須是像 "40px" 這樣的格式, 不能只有數字, 一定要有 px, 且中間不可有空格, 否則會無效果.

樣式設定還有一個 claaed() 方法可用來設定或取得元素的樣式類別, 此方法若只傳一個參數 (class 名稱) 是檢查元素是否有此樣式類別, 傳回值為 true 或 false; 傳入兩個參數時, 若第二參數為 true, 表示為此元素添加該樣式類別, 例如 :

測試 8  :  https://tony1966.000webhostapp.com/test/D3/d3_classed_1.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <style>
      .bgyellow{background-color: yellow;}
      .bgred{background-color: red;}
    </style>
  </head>
  <body>
    <p id='p1'>這是 p1</p>
    <p id='p2' class='bgred'>這是 p2</p>
  </body>
  <script>
    var p1=d3.select('#p1');
    p1.classed("bgyellow", true);
    alert("p1樣式類別=bgyellow:" + p1.classed('bgyellow'));
    var p2=d3.select('#p2'); 
    alert("p2樣式類別=bgred:" + p2.classed('bgred'));
    alert("p2樣式類別=yellow:" + p2.classed('bgyellow'));
  </script>
</html>

此範例網頁有兩個定義在 style 標籤中的樣式類別 bgyellow 與 bgred, 以及兩個 p 元素 : p1 與 p2, 其中元素 p1 預設沒有添加樣式類別, 元素 p2 則預載 bgred 樣式類別. 網頁載入後會呼叫 classed() 為 p1 添加樣式類別 bgyellow, 因此查詢其是否有 bgyellow 類別結果為 true, 接著查詢 p2 是否有 bgred 類別結果也是 true (因為預載), 而查詢 p2 是否有 bgyellow 類別結果為 false, 執行結果如下 :




如果要取消指定之樣式類別, 則需在呼叫 classed() 時第二參數傳入 false 或是一個傳回 return false 的函數, 例如 :

測試 9  :  https://tony1966.000webhostapp.com/test/D3/d3_classed_2.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <style>
      .bgyellow{background-color: yellow;}
      .bgred{background-color: red;}
    </style>
  </head>
  <body>
    <p id='p1' class='bgyellow'>這是 p1</p>
    <p id='p2' class='bgred'>這是 p2</p>
  </body>
  <script>
    var p1=d3.select('#p1');
    alert("p1樣式類別=bgyellow:" + p1.classed('bgyellow'));
    var p2=d3.select('#p2'); 
    alert("p2樣式類別=bgred:" + p2.classed('bgred'));
    p1.classed("bgyellow", false);
    p2.classed("bgred", function(){return false;});
  </script>
</html>

可見不論是直接傳入 false 或一個會傳回 false 的函數, 其效果都是一樣會將指定的樣式類別自元素中移除, 上例的 p1 與 p2 都失去背景顏色了.

除了操控既有的網頁元素外, 還可以呼叫 append() 與 insert() 方法並傳入標籤名稱來添加新的元素, 這兩個方法在只傳入一個參數時效果完全一樣, append() 只能傳入一個標籤參數, 功能是在目前操作之元素的所有子元素後面添加一個新的子元素; 而 insert() 可傳入一個或兩個參數, 當只傳入一個參數時, 其功能與 append() 完全相同, 例如 :

測試 10  :  https://tony1966.000webhostapp.com/test/D3/d3_append.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
    <ul>
      <li>食品</li>
      <li>生技</li>
      <li>金融</li>
    </ul>
  </body>
  <script>
    d3.select("ul")
      .append("li")
      .text("鋼鐵");
  </script>
</html>

此程式會在既有的三個清單項目後面添加新的項目 "鋼鐵", 執行結果如下 :
  • 食品
  • 生技
  • 金融
  • 鋼鐵
如果將上面程式中的的 append() 改為 insert() 結果是一樣的, 例如 :

測試 11  :  https://tony1966.000webhostapp.com/test/D3/d3_insert.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
    <ul>
      <li>食品</li>
      <li>生技</li>
      <li>金融</li>
    </ul>
  </body>
  <script>
    d3.select("ul")
      .insert("li")
      .text("鋼鐵");
  </script>
</html>

函數 insert(tag1, tag2) 在傳入兩個參數時功能就與 append() 不同了, 其中 tag1 是要添加的子元素標籤, 而 tag2 則是添加位置所在處後面的那個元素, 易即 tag1 會被添加在子元素 tag2 之前, 例如 :

測試 12  :  https://tony1966.000webhostapp.com/test/D3/d3_insert_2.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
    <ul>
      <li>食品</li>
      <li>生技</li>
      <li>金融</li>
    </ul>
  </body>
  <script>
    d3.select("ul")
      .insert("li","li")
      .text("鋼鐵");
  </script>
</html>

此例中的 insert("li","li") 表示要在被選取元素 ul 的子元素 li (參數 2) 前面添加一個 li 子元素 (參數 2), 但 ul 有三個 li 子元素, 這時會添加在第一個子元素前面, 執行結果如下 :
  • 鋼鐵
  • 食品
  • 生技
  • 金融
可見 insert() 會將新子元素添加在全部原有子元素的最前面, 而 append() 則是在最後面. 如果要指定插在哪一個子元素前面, 則需使用偽類選擇器 (Pseudo-selector) nth-child(i), 例如 :

測試 13  :  https://tony1966.000webhostapp.com/test/D3/d3_insert_3.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
    <ul>
      <li>食品</li>
      <li>生技</li>
      <li>金融</li>
    </ul>
  </body>
  <script>
    d3.select("ul")
      .insert("li","li:nth-child(2)")
      .text("鋼鐵");
  </script>
</html>

此例之 insert() 要在 ul 的第 2 個  li 子元素前面插入一個內容為 "鋼鐵" 的子元素, 因此 "鋼鐵" 變成新的第二個子元素, 結果如下 :
  • 食品
  • 鋼鐵
  • 生技
  • 金融

最後來看看 D3.js 繪圖所使用的 SVG 技術, 繪圖首先須在網頁中利用 append() 或 insert() 新增一個 SVG 畫布元素, 例如 :

測試 14  :  https://tony1966.000webhostapp.com/test/D3/d3_append_svg.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
  </body>
  <script>
    d3.select("body")
      .append("svg")
      .attr("width","250px")
      .attr("height","150px")
      .style("background","green");
  </script>
</html>

此網頁之 body 內原本空空如也, 利用 select() 選取 body 元素後呼叫其 append() 方法添加一個 svg 元素, 並接著呼叫其 attr() 方法設定寬高屬性, 最後呼叫 style() 方法設定背景顏色為綠色. 注意, 此處 attr() 不會傳回新的元素物件, 因 style() 仍然是作用於 append() 所傳回的 svg 元素上. 注意, 若將 append() 改為 insert() 效果相同, 執行結果如下 :


SVG 元素是一個可以在上面繪圖的畫布, 只要呼叫 SVG 元素的 append() 方法即可在畫布上添加圖形, 例如傳入 "rect" 即可繪製矩形, 但需透過呼叫 attr() 來修飾其四個屬性 :
  1. width : 矩形寬度
  2. height : 矩形高度
  3. x : 與畫布左緣之距離 (padding)
  4. y : 與畫布上緣之距離 (padding)
例如 :

測試 15  :  https://tony1966.000webhostapp.com/test/D3/d3_draw_rectangle.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
  </body>
  <script>
    var svg=d3.select("body")
              .append("svg")
              .attr("width","250px")
              .attr("height","150px")
              .style("background","yellow");
    svg.append("rect")
       .attr("width","150px")
       .attr("height","50px")
       .attr("x","50px")
       .attr("y","50px")
       .style("fill","blue");
  </script>
</html>

此範例先在 body 元素內添加一個 svg 元素, 然後呼叫此 svg 物件之 append() 函數並用 attr() 來修飾矩形的諸元. 執行結果如下 :


如果在呼叫 svg 物件的 append() 時傳入 "circle" 則可在 svg 畫布上繪製圓形, 同樣需呼叫 attr() 修飾下列圓的三個屬性 :
  1. r : 半徑
  2. cx : 圓心與畫布左緣距離 (padding)
  3. cy : 圓心與畫布上緣距離 (padding)
例如 :

測試 16  :  https://tony1966.000webhostapp.com/test/D3/d3_draw_circle.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
  </body>
  <script>
    var svg=d3.select("body")
              .append("svg")
              .attr("width","250px")
              .attr("height","150px")
              .style("background","purple");
    svg.append("circle")
       .attr("r","50px")
       .attr("cx","125px")
       .attr("cy","75px")
       .style("fill","red");
  </script>
</html>

執行結果如下 :



以上是 D3.js 的基本用法. 但 D3.js 的威力不只是這樣, 還可以利用 SVG 繪圖. 下面的範例取自 "Practical D3.js" 的第五章加以修改, 其作用是在一塊 200*80 的 SVG 畫布上繪製四個紅色圓圈 :

測試 17  :  https://tony1966.000webhostapp.com/test/D3/d3_svg4circles.htm

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>The Power of D3.js</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
    <svg id='svg1' width='200px' height='80px' style='background:yellow'></svg> 
  </body>
  <script>
    d3.select("#svg1")
      .selectAll("circle")
      .data([40, 80, 120, 160])
      .enter()
      .append("circle")
      .attr("cy", "40")
      .attr("cx", function(d) {return d;})
      .attr("r", "15")
      .style("fill", "red")
      .style("stroke", "black")
      .style("stroke-width", 2);
  </script>
</html>

執行結果如下 :


自從前年學完 jQuery 後就沒再學習新的前端技術了, D3.js 我很早以前就有興趣, 市圖找到的 "網頁互動式資料視覺化 : 使用 D3" 這本之前借來幾次都沒時間看, 現在終於踏出第一步了.
參考 :

SVG D3.js - 淺談 D3.js 的資料處理
CSS 語法簡介
https://github.com/PacktPublishing/Learning-D3js-4-Mapping-Second-Edition

沒有留言 :