2017/2/20

Spark tutorial


要使用 Spark 之前,一般會先遇到 scala 這個語言的熟悉度的問題,當有了一定的語言程度後,再來就是 scala IDE 的選擇,目前的狀況,還是IDEA 會比 scala IDE for Eclipse 好用。接下來就是下載跟安裝 spark,然後進行 WordCount 的範例練習,以下記錄怎麼安裝與設定 stand alone 的 spark 開發環境。


要下載哪一個 spark 套件


當我們連結到 Download Apache Spark 時,首先遇到的問題,就是要下載哪一個 spark release 套件。


基本的原則如下:


如果要直接下載已經編譯好的 binary 套件,我們可以根據 Hadoop 的版本決定要下載哪一個,但如果像我們一樣,不打算安裝 Hadoop 就直接測試,就直接選最新版的 spark-1.6.1-bin-hadoop2.6.tgz 就好了,下載後解壓縮,馬上就可以使用 spark-shell,或直接取得 all-in-one 的 spark-assembly-1.6.1-hadoop2.6.0.jar 套件。


如果我們要編譯 source code,就下載預設的 1.6.1(Mar 09 2016) spark release,Package type 選擇 Source Code:spark-1.6.1.tgz


由於目前 spark 預設是使用 scala 2.10 版,使用預先編譯的 spark 就必須要使用 scala 2.10 版,如果像要改成 2.11,就一定要自己重新編譯 spark,目前 spark 的 JDBC component 還不支援 scala 2.11。


Building for Scala 2.11 有兩行指令說明如何將 spark 由 2.10 調整為 2.11,我們同時把 hadoop 版本改為 2.6。


./dev/change-scala-version.sh 2.11
mvn -Pyarn -Phadoop-2.6 -Dscala-2.11 -DskipTests clean package

編譯 spark 要花的時間很久,以我現在的環境花了 40 分鐘。


[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 39:33 min
[INFO] Finished at: 2016-04-29T09:23:03+08:00
[INFO] Final Memory: 452M/2703M

也可以使用 sbt 來編譯 spark,編譯後會得到 spark-assembly 的 jar。


sbt/sbt assembly

如果要修改 spark souce code,可以啟用增量編譯模式,避免每一次修改都要花很久的時間重新編譯。


export SPARK_PREPEND_CLASSES=true
sbt/sbt compile

unset SPARK_PREPEND_CLASSES

在 compile 前面加上 ~ 可以避免每一次都重開一次新的 sbt console


sbt/sbt ~ compile

可以用 sbt 或是 mvn 指令查閱 dependency map


sbt/sbt dependency-tree

mvn -DskipTests install
mvb dependency:tree

如果要設定 spark source 的開發環境,可以用以下的指令產生 IDEA project file


git clone https://github.com/apache/spark
sbt/sbt gen-idea

Spark 開發環境 in IDEA


  1. 在 IDEA 建立新的 scala project: sparktest

  2. 在 project 中建立一個 lib 目錄,把 spark-assembly-1.6.1-hadoop2.6.0.jar 放在那個目錄中

  3. 在 File -> Project Structure -> Libraries 點 "+",然後把 lib 目錄加入 project 中

  4. 取得一個文字檔的測試資料 pg5000.txt ,將檔案放在新建立的 data 目錄中

  5. 將 RunWordCount.scala 放在 src 目錄中,程式會計算 pg5000.txt 裡面每一個字出現的數量


    import org.apache.log4j.Logger
    import org.apache.log4j.Level
    import org.apache.spark.{ SparkConf, SparkContext }
    import org.apache.spark.rdd.RDD
    
    object RunWordCount {
      def main(args: Array[String]): Unit = {
    
        // 以這兩行設定不顯示 spark 內部的訊息
        Logger.getLogger("org").setLevel(Level.OFF)
        System.setProperty("spark.ui.showConsoleProgress", "false")
    
        // 清除 output folder
        FileUtils.deleteDirectory(new File("data/output"))
    
        println("執行RunWordCount")
    
        // 設定 application 提交到 MASTER 指向的 cluster 或是 local 執行的模式
        // local[4] 代表是在本地以 四核心的 CPU 執行
        val sc = new SparkContext(new SparkConf().setAppName("wordCount").setMaster("local[4]"))
    
        println("讀取文字檔...")
        val textFile = sc.textFile("data/pg5000.txt") 
    
        println("開始建立RDD...")
        // flapMap 是取出文字檔的每一行資料,並以 " " 進行 split,分成一個一個的 word
        // map 是將每一個 word 轉換成 (word, 1) 的 tuple
        // reduceByKey 會根據 word 這個 key,將後面的 1 加總起來,就會得到 (word, 數量) 的結果
        val countsRDD = textFile.flatMap(line => line.split(" "))
          .map(word => (word, 1))
          .reduceByKey(_ + _) 
    
        println("儲存結果至文字檔...")
        try {
          countsRDD.saveAsTextFile("data/output") 
          println("存檔成功")
        } catch {
          case e: Exception => println("輸出目錄已經存在,請先刪除原有目錄");
        }
    
      }
    }
  6. 我們可以直接在 IDEA 就執行這個測試程式


    執行RunWordCount
    Using Spark's default log4j profile: org/apache/spark/log4j-defaults.properties
    16/04/29 16:28:50 INFO Slf4jLogger: Slf4jLogger started
    16/04/29 16:28:50 INFO Remoting: Starting remoting
    16/04/29 16:28:50 INFO Remoting: Remoting started; listening on addresses :[akka.tcp://sparkDriverActorSystem@192.168.1.151:56205]
    讀取文字檔...
    開始建立RDD...
    儲存結果至文字檔...
    存檔成功
    
    Process finished with exit code 0
  7. 最後產生的結果有三個檔案,其中 part-00000 及 part-00001 裡面存了每一個 word 的發生次數


    _SUCCESS
    part-00000
    part-00001

會產生兩個檔案的原因是因為,spark 本身是平行運算的工具,所以會自動產生多個 partitions。


如果需要將結果整合成一個檔案,就必須使用 coalesce,在程式的最後面,用 countsRDD.coalesce(1).saveAsTextFile 將結果輸出到新目錄,也會得到一個檔案的結果。


try {
      countsRDD.coalesce(1).saveAsTextFile("data/output2")
      println("存檔成功")
    } catch {
      case e: Exception => println("輸出目錄已經存在,請先刪除原有目錄");
    }

匯出程式


  1. 在 IDEA 選擇 "File" -> "Project Structure" -> "Artifact"

  2. 點擊 "+" -> "JAR" -> "From modules with dependencies"

  3. Main Class 填成 "RunWordCount",輸出目錄的最後面改為 "out"

  4. 選擇 "Build" -> "Build Artifacts",就能在 out 目錄取得 sparktest.jar 檔

  5. 這樣就能在另一台機器執行 sparktest


java -jar sparktest.jar

References


HADOOP+SPARK大數據巨量分析與機器學習整合開發實戰


Spark大資料分析實戰

2017/2/13

scopt: command line parsing library in Scala


在 Java 要製作一個 command line 工具可以使用 Apache Commons cli,不過在 scala,有另一個更簡潔的 library: scopt,可以幫助我們製作 cli 程式。


libraryDependencies


根據 scopt github 的說明,我們應該在 build.sbt 中加上這樣的 libraryDependencies 宣告設定


libraryDependencies += "com.github.scopt" %% "scopt" % "3.5.0"

但我們使用起來覺得有點問題,搜尋了 maven Group: com.github.scopt,看起來這個 library 有針對 scala 的版本提供不同的 library,因為我們是使用 scala 2.11.8,所以就將 libraryDependencies 改成以下這樣


"com.github.scopt" % "scopt_2.11" % "3.5.0",

Config


使用 scopt 之前,要先定義一個用來存放 cli parsing 結果的 case class: Config,我們是要做一個 License File 的產生工具,所以 Config 裡面存的都是 license 需要的資料。


  case class Config(mode: String = "",
                    ver: Boolean = false,
                    getmid: Boolean = false,
                    keyfile: String ="",

                    lictype:String ="",
                    product:String ="",
                    version:String ="",
                    name:String ="",
                    company:String="",
                    copies:Int=1,
                    mid:String="",
                    validfrom:String="",
                    goodthru:String=""
                   ) {
    def copy(mode: String = mode, ver: Boolean = ver,
             getmid:Boolean = getmid,
             keyfile: String = keyfile,

             lictype: String = lictype,
             product: String = product,
             version: String = version,
             name: String = name,
             company: String = company,
             copies: Int = copies,
             mid: String = mid,
             validfrom: String = validfrom,
             goodthru: String = goodthru
            ) =
      new Config(mode, ver, getmid, keyfile, lictype, product, version, name, company,
        copies, mid, validfrom, goodthru)
  }

Parser


接下來是使用 Config 產生 OptionParser,Parser 中是以第一個參數 "mode" 作為不同指令的判斷,我們提供了四個指令:key, lic, dec, --getmid, --ver,另外還有一個基本的 --help,每一個指令都有一個縮寫。


我們可以先看 help 列印出來的結果,最前面的 Usage 是這個程式的使用方式,然後有兩個基本的 --ver 及 --getmid 方法。


接下來是 key, lic, dec 這三個獨立指令的說明,每一個指令都有相關的參數,最後一行是 --help 列印 help 頁面的部分。


[info] Running license.LicenseBuilder -h
[info] License Builder 0.1
[info] Usage: license.LicenseBuilder [key|lic|dec] [options] <args>...
[info]
[info]   -v, --ver                Prints the version number.
[info]   -i, --getmid             Prints the machine id.
[info] Command: key keyfile
[info]   generate RSA key file
[info]   keyfile                  gen key files with key filename prefix
[info] Command: lic [options]
[info]   generate license file
[info]   -k, --prikeyfile <value>
[info]                            private key file prefix
[info]   -l, --lictype <value>    Evaluation/Standard/Enterprise
[info]   -p, --product <value>    product name, ex: kokome
[info]   -e, --version <value>    product version number, ex: 3.0.0
[info]   -n, --name <value>       licensed name, ex: kokome
[info]   -o, --company <value>    licensed company name, ex: maxkit
[info]   -c, --copies <value>     licensed number of users, ex: 5
[info]   -m, --mid <value>        machine id
[info]   -v, --validfrom <value>  licensed valid from date ex: 2016/01/01
[info]   -g, --goodthru <value>   licensed good thru date ex: 2016/12/31
[info] Command: dec keyfile
[info]   decode maxkit.lic
[info]   keyfile                  decode maxkit.lic with key filename prefix
[info]   -h, --help               prints this usage text

看了 help 的說明後,再去看 OptionParser 的寫法,就比較能清楚地分辨不同指令區塊的部分。


val parser = new scopt.OptionParser[Config]("license.LicenseBuilder") {
    head("License Builder", LicenseBuilder.ver)

    //activator "runMain license.LicenseBuilder -v"
    opt[Unit]("ver").abbr("v").action( (_, c) => c.copy(ver = true)).
      text("Prints the version number.")

    //activator "runMain license.LicenseBuilder -i"
    opt[Unit]("getmid").abbr("i").action( (_, c) => c.copy(getmid = true)).
      text("Prints the machine id.")

    //activator "runMain license.LicenseBuilder key maxkit"
    cmd("key").action( (x, c) => c.copy(mode = "key")).
      children(
        arg[String]("keyfile").unbounded().required().action( (x, c) => c.copy(keyfile = x)).
          text("gen key files with key filename prefix")
      ).text("  generate RSA key file")

    //activator "runMain license.LicenseBuilder lic -k maxkit -l Enterprise -p kokome -e 3.0.0 -n kokome -o maxkit -c 10 -m 1234 -v 2016/10/01 -g 2116/01/01"
    cmd("lic").action( (_, c) => c.copy(mode = "lic")).
      children(
        opt[String]('k', "prikeyfile").required().action( (x,c) => c.copy(keyfile=x) ).
          text("private key file prefix"),

        opt[String]('l', "lictype").required().action( (x,c) => c.copy(lictype=x) ).
          text("Evaluation/Standard/Enterprise"),

        opt[String]('p', "product").required().action( (x,c) => c.copy(product=x) ).
          text("product name, ex: kokome"),

        opt[String]('e', "version").required().action( (x,c) => c.copy(version=x) ).
          text("product version number, ex: 3.0.0"),

        opt[String]('n', "name").required().action( (x,c) => c.copy(name=x) ).
          text("licensed name, ex: kokome"),

        opt[String]('o', "company").required().action( (x,c) => c.copy(company=x) ).
          text("licensed company name, ex: maxkit"),

        opt[Int]('c', "copies").required().action( (x,c) => c.copy(copies=x) ).
          text("licensed number of users, ex: 5"),

        opt[String]('m', "mid").required().action( (x,c) => c.copy(mid=x) ).
          text("machine id"),

        opt[String]('v', "validfrom").required().action( (x,c) => c.copy(validfrom=x) ).
          text("licensed valid from date ex: 2016/01/01"),

        opt[String]('g', "goodthru").required().action( (x,c) => c.copy(goodthru=x) ).
          text("licensed good thru date ex: 2016/12/31")

      ).text("  generate license file")

    //activator "runMain license.LicenseBuilder dec maxkit"
    cmd("dec").action( (x, c) => c.copy(mode = "dec")).
      children(
        arg[String]("keyfile").unbounded().required().action( (x, c) => c.copy(keyfile = x)).
          text("decode maxkit.lic with key filename prefix")
      ).text("  decode maxkit.lic")

    //activator "runMain license.LicenseBuilder --help"
    help("help").abbr("h").text("prints this usage text")
  }

  parser.parse(args, Config()) match {
    case Some(config) => {
      // gen privat/pubilic key pairs
      if (config.mode == "key") LicenseBuilder.key(config.keyfile)

      // gen license file
      if (config.mode == "lic") LicenseBuilder.lic(config.keyfile, config.lictype, config.product,
        config.version, config.name, config.company, config.copies,
        config.mid, config.validfrom, config.goodthru)

      // decode license file
      if (config.mode == "dec") LicenseBuilder.dec(config.keyfile)

      // get machine if
      if (config.getmid) LicenseBuilder.getmid

      // print LicenseBuilder version
      if (config.ver) println("LicenseBuilder Version is: " + LicenseBuilder.ver)
    }
    case None => println("Please use -h for usage")
  }

完整的程式


package license

import java.io.File
import java.text.SimpleDateFormat
import java.util.Date

import org.apache.commons.codec.binary.Base64
import org.apache.commons.io.FileUtils
import play.api.Logger
import utils.StringUtil

object LicenseBuilder extends App {
  val ver = "0.1"

  case class Config(mode: String = "",
                    ver: Boolean = false,
                    getmid: Boolean = false,
                    keyfile: String ="",

                    lictype:String ="",
                    product:String ="",
                    version:String ="",
                    name:String ="",
                    company:String="",
                    copies:Int=1,
                    mid:String="",
                    validfrom:String="",
                    goodthru:String=""
                   ) {
    def copy(mode: String = mode, ver: Boolean = ver,
             getmid:Boolean = getmid,
             keyfile: String = keyfile,

             lictype: String = lictype,
             product: String = product,
             version: String = version,
             name: String = name,
             company: String = company,
             copies: Int = copies,
             mid: String = mid,
             validfrom: String = validfrom,
             goodthru: String = goodthru
            ) =
      new Config(mode, ver, getmid, keyfile, lictype, product, version, name, company,
        copies, mid, validfrom, goodthru)
  }

  def key(keyfile: String) = {
    println(s"generate key pairs with filename prefix ${keyfile}")
  }

  def getmid() = {
    val mid = LicenseId.getLicenseId
    println(s"mid = ${mid}")
  }

  def dec(keyfile:String) = {
    println(s"decode license maxkit.lic with ${keyfile}.prikey.dat")

  }

  def lic(keyfile:String, lictype:String,
          product:String, version:String,
          name:String, company:String,
          copies:Int, mid:String,
          validfrom:String, goodthru:String) = {

    println(s"gen license with ${keyfile}.prikey.dat, lictype=${lictype}," +
      s"product=${product}, version=${version}, name=${name}, company=${company}, " +
      s"copies=${copies}, mid=${mid}, validfrom=${validfrom}, goodthru=${goodthru}")
      
  }

  val parser = new scopt.OptionParser[Config]("license.LicenseBuilder") {
    head("License Builder", LicenseBuilder.ver)

    //activator "runMain license.LicenseBuilder -v"
    opt[Unit]("ver").abbr("v").action( (_, c) => c.copy(ver = true)).
      text("Prints the version number.")

    //activator "runMain license.LicenseBuilder -i"
    opt[Unit]("getmid").abbr("i").action( (_, c) => c.copy(getmid = true)).
      text("Prints the machine id.")

    //activator "runMain license.LicenseBuilder key maxkit"
    cmd("key").action( (x, c) => c.copy(mode = "key")).
      children(
        arg[String]("keyfile").unbounded().required().action( (x, c) => c.copy(keyfile = x)).
          text("gen key files with key filename prefix")
      ).text("  generate RSA key file")

    //activator "runMain license.LicenseBuilder lic -k maxkit -l Enterprise -p kokome -e 3.0.0 -n kokome -o maxkit -c 10 -m 1234 -v 2016/10/01 -g 2116/01/01"
    cmd("lic").action( (_, c) => c.copy(mode = "lic")).
      children(
        opt[String]('k', "prikeyfile").required().action( (x,c) => c.copy(keyfile=x) ).
          text("private key file prefix"),

        opt[String]('l', "lictype").required().action( (x,c) => c.copy(lictype=x) ).
          text("Evaluation/Standard/Enterprise"),

        opt[String]('p', "product").required().action( (x,c) => c.copy(product=x) ).
          text("product name, ex: kokome"),

        opt[String]('e', "version").required().action( (x,c) => c.copy(version=x) ).
          text("product version number, ex: 3.0.0"),

        opt[String]('n', "name").required().action( (x,c) => c.copy(name=x) ).
          text("licensed name, ex: kokome"),

        opt[String]('o', "company").required().action( (x,c) => c.copy(company=x) ).
          text("licensed company name, ex: maxkit"),

        opt[Int]('c', "copies").required().action( (x,c) => c.copy(copies=x) ).
          text("licensed number of users, ex: 5"),

        opt[String]('m', "mid").required().action( (x,c) => c.copy(mid=x) ).
          text("machine id"),

        opt[String]('v', "validfrom").required().action( (x,c) => c.copy(validfrom=x) ).
          text("licensed valid from date ex: 2016/01/01"),

        opt[String]('g', "goodthru").required().action( (x,c) => c.copy(goodthru=x) ).
          text("licensed good thru date ex: 2016/12/31")

      ).text("  generate license file")

    //activator "runMain license.LicenseBuilder dec maxkit"
    cmd("dec").action( (x, c) => c.copy(mode = "dec")).
      children(
        arg[String]("keyfile").unbounded().required().action( (x, c) => c.copy(keyfile = x)).
          text("decode maxkit.lic with key filename prefix")
      ).text("  decode maxkit.lic")

    //activator "runMain license.LicenseBuilder --help"
    help("help").abbr("h").text("prints this usage text")
  }

  parser.parse(args, Config()) match {
    case Some(config) => {
      // gen privat/pubilic key pairs
      if (config.mode == "key") LicenseBuilder.key(config.keyfile)

      // gen license file
      if (config.mode == "lic") LicenseBuilder.lic(config.keyfile, config.lictype, config.product,
        config.version, config.name, config.company, config.copies,
        config.mid, config.validfrom, config.goodthru)

      // decode license file
      if (config.mode == "dec") LicenseBuilder.dec(config.keyfile)

      // get machine if
      if (config.getmid) LicenseBuilder.getmid

      // print LicenseBuilder version
      if (config.ver) println("LicenseBuilder Version is: " + LicenseBuilder.ver)
    }
    case None => println("Please use -h for usage")
  }
}

Reference


scala 命令行解析

2017/2/6

OpenJDK


Oracle JDK 長久以來並沒有被追討授權費用的問題,但因為 JDK 本來就是以 BCL 授權,並不是整個 JDK 都是免費使用的,再加上Oracle 開始追討 Java 授權費,企業客戶頭痛,所以要開始注意這個問題。Oracle 取締未經適當授權的 Java 用戶 提供了如何安全地使用 Oracle JDK 的一些 hint,不過最根本的方法就是換成 OpenJDK。


OpenJDK 是以 GPL with Classpath Exception 授權,classpath exception 就是可以在 proprietary 軟體中使用 OpenJDK 的意思。


OpenJDK 8 已經跟 Oracle JDK 沒有什麼差異,在 Linux Server 中,都已經可以很快速就將 JDK 轉換到 OpenJDK 上面,不過 windows 跟 MacOS 就麻煩了一些,但基本上後面這兩個 OS 都是開發環境,只是下載使用,沒有散佈,繼續用 Oracle JDK 應該也可以。


CentOS


ref: CentOS7 使用yum命令安装Java SDK


$ yum search java | grep -i --color JDK

java-1.8.0-openjdk.x86_64 : OpenJDK Runtime Environment
java-1.8.0-openjdk-debug.x86_64 : OpenJDK Runtime Environment with full debug on
java-1.8.0-openjdk-demo.x86_64 : OpenJDK Demos
java-1.8.0-openjdk-demo-debug.x86_64 : OpenJDK Demos with full debug on
java-1.8.0-openjdk-devel.x86_64 : OpenJDK Development Environment
java-1.8.0-openjdk-devel-debug.x86_64 : OpenJDK Development Environment with
java-1.8.0-openjdk-headless.x86_64 : OpenJDK Runtime Environment
java-1.8.0-openjdk-headless-debug.x86_64 : OpenJDK Runtime Environment with full
java-1.8.0-openjdk-javadoc.noarch : OpenJDK API Documentation
java-1.8.0-openjdk-javadoc-debug.noarch : OpenJDK API Documentation for packages
java-1.8.0-openjdk-src.x86_64 : OpenJDK Source Bundle
java-1.8.0-openjdk-src-debug.x86_64 : OpenJDK Source Bundle for packages with

openjdk 的安裝路徑 /usr/lib/jvm/


yum install java-1.8.0-openjdk  java-1.8.0-openjdk-devel

以 alternatives 調整執行檔的目標


alternatives --config java
alternatives --config javac
alternatives --config javadoc
alternatives --config javah
alternatives --config javap

設定環境變數


vi /etc/profile

export JAVA_HOME=/usr/lib/jvm/java-openjdk
export CLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin

Debian


ref: How to download and install prebuilt OpenJDK packages


apt-get update
apt-get install openjdk-8-jdk

openjdk8 的路徑是 /usr/lib/jvm/java-8-openjdk-amd64


update-alternatives --display java
update-alternatives --display javac
update-alternatives --display javadoc
update-alternatives --display javah
update-alternatives --display javap

java -version

設定環境變數


vi /etc/profile

export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
export CLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin

openjdk for windows


Redhat Develper 提供了 windows 的 openjdk binary installer OpenJDK now available for Windows


openjdk for macos


build OpenJDKs at home on Linux and OSX


How to build and package OpenJDK 8 on OSX


https://www.zhihu.com/question/40816585
安装Homebrew然后在终端sudo brew install openjdk

2017/1/23

D3.js 基本的使用方式 part 3


layout


D3 的 layout:


  1. Bundle: 把霍爾頓的分層捆綁算法應用到連線
  2. Chord: 根據矩陣關係生成弦形圖
  3. Cluster: 聚集實體生成系統樹圖
  4. Force: 根據物理模擬定位鏈接的節點
  5. Hierarchy: 派生自定義的系統佈局實現
  6. Histogram: 基於量化的分組計算數據分佈
  7. Pack: 基於遞歸原型填充產生分層佈局
  8. Partition: 遞歸細分節點數,呈射線或冰掛狀
  9. Pie: 計算一系列堆疊的條形或面積圖的基線
  10. Stack: 計算一系列堆疊的條形或面積圖的基線
  11. Tree: 計算一系列堆疊的條形或面積圖的基線
  12. Tree: 整齊的定位樹節點
  13. Treemap: 基於遞歸空間細分來顯示節點樹

pie chart


// v3
var arc = d3.svg.arc()
                .innerRadius(innerRadius)
                .outerRadius(outerRadius);

var pie = d3.layout.pie();
var color = d3.scaleCategory10();

// v4
var arc = d3.arc()
    .innerRadius(innerRadius)
    .outerRadius(outerRadius);
var pie = d3.pie();
var color = d3.scaleOrdinal(d3.schemeCategory10);

在 developer console 中,以 dataset 跟 pie(dataset) 查看資料轉換前後的差異


> dataset
[5, 10, 20, 45, 6, 25]
> pie(dataset)
[Objectdata: 5
endAngle: 6.283185307179586
index: 5
padAngle: 0
startAngle: 6.000158941991317
value: 5
__proto__: Object, Objectdata: 10endAngle: 5.660527303765393index: 3padAngle: 0startAngle: 5.094474573388854value: 10__proto__:
Object, Object, Object, Object, Object]

        <style type="text/css">

            text {
                font-family: sans-serif;
                font-size: 12px;
                fill: white;
            }

        </style>
        
        <script type="text/javascript">

            //Width and height
            var w = 300;
            var h = 300;

            var dataset = [ 5, 10, 20, 45, 6, 25 ];

            var outerRadius = w / 2;
            var innerRadius = 0;
            // 調整 innerRadius 就可以變成環狀圖
            //var innerRadius = w/3;
            var arc = d3.arc()
                            .innerRadius(innerRadius)
                            .outerRadius(outerRadius);

            var pie = d3.pie();

            //Easy colors accessible via a 10-step ordinal scale
            var color = d3.scaleOrdinal(d3.schemeCategory10);

            //Create SVG element
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            //Set up groups
            var arcs = svg.selectAll("g.arc")
                          .data(pie(dataset))
                          .enter()
                          .append("g")
                          .attr("class", "arc")
                          .attr("transform", "translate(" + outerRadius + "," + outerRadius + ")");

            //Draw arc paths
            arcs.append("path")
                .attr("fill", function(d, i) {
                    return color(i);
                })
                .attr("d", arc);

            //Labels
            arcs.append("text")
                .attr("transform", function(d) {
                    return "translate(" + arc.centroid(d) + ")";
                })
                .attr("text-anchor", "middle")
                .text(function(d) {
                    return d.value;
                });

        </script>


stack chart


        <script type="text/javascript">

            //Width and height
            var w = 500;
            var h = 300;
            var barPadding = 5;

var mydata = [
{ apples: 5, oranges: 10, grapes: 22 },
{ apples: 4, oranges: 12, grapes: 28 },
{ apples: 2, oranges: 19, grapes: 32 },
{ apples: 7, oranges: 23, grapes: 35 },
{ apples: 23, oranges: 17, grapes: 43 }
];

// 資料轉換後,才能繪製 stack chart
var mystack = d3.stack()
    .keys(["apples", "oranges", "grapes"]);
    // .order(d3.stackOrderNone)
    // .offset(d3.stackOffsetNone);

var dataset = mystack(mydata);

//資料轉換後的結果
// JSON.stringify(dataset, null, '\t');
// "[
//  [
//      [0,5],
//      [0,4],
//      [0,2],
//      [0,7],
//      [0,23]
//  ],
//  [
//      [5,15],
//      [4,16],
//      [2,21],
//      [7,30],
//      [23,40]
//  ],
//  [
//      [15,37],
//      [16,44],
//      [21,53],
//      [30,65],
//      [40,83]
//  ]
// ]"

            //Set up scales
            var xScale = d3.scaleBand()
                .domain(d3.range(dataset[0].length))
                .range([0, w], 0.05);

            var yScale = d3.scaleLinear()
                .domain([0,
                    d3.max(dataset, function(d) {
                        return d3.max(d, function(d) {
                            // 找到 d[1] 的最大值,就是圖形 y 軸的最大值

                            // console.log("d="+d);
                            // d=0,5
                            // d=0,4
                            // d=0,2
                            // d=0,7
                            // d=0,23
                            // -------
                            // d=5,15
                            // d=4,16
                            // d=2,21
                            // d=7,30
                            // d=23,40
                            // -------
                            // d=15,37
                            // d=16,44
                            // d=21,53
                            // d=30,65
                            // d=40,83
                            //return d.y0 + d.y;
                            return d[1];
                        });
                    })
                ])
                // 對應 svg 的高度
                .range([0, h]);

            //Easy colors accessible via a 10-step ordinal scale
            var colors = d3.scaleOrdinal(d3.schemeCategory10);

            //Create SVG element
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            // Add a group for each row of data
            // 建立每一行的資料
            var groups = svg.selectAll("g")
                .data(dataset)
                .enter()
                .append("g")
                .style("fill", function(d, i) {
                    return colors(i);
                });

            // Add a rect for each data value
            // 產生矩形
            var rects = groups.selectAll("rect")
                .data(function(d) { return d; })
                .enter()
                .append("rect")
                // x 軸的位置要對應到 xScale
                .attr("x", function(d, i) {
// console.log("attr x i="+i+", d[0]="+d[0]+", d[1]="+d[1]+", xScale(i)="+xScale(i));
// 03_stacked_bar.html:105 attr x i=0, d[0]=0, d[1]=5, xScale(i)=0
// 03_stacked_bar.html:105 attr x i=1, d[0]=0, d[1]=4, xScale(i)=100
// 03_stacked_bar.html:105 attr x i=2, d[0]=0, d[1]=2, xScale(i)=200
// 03_stacked_bar.html:105 attr x i=3, d[0]=0, d[1]=7, xScale(i)=300
// 03_stacked_bar.html:105 attr x i=4, d[0]=0, d[1]=23, xScale(i)=400
// 03_stacked_bar.html:105 attr x i=0, d[0]=5, d[1]=15, xScale(i)=0
// 03_stacked_bar.html:105 attr x i=1, d[0]=4, d[1]=16, xScale(i)=100
// 03_stacked_bar.html:105 attr x i=2, d[0]=2, d[1]=21, xScale(i)=200
// 03_stacked_bar.html:105 attr x i=3, d[0]=7, d[1]=30, xScale(i)=300
// 03_stacked_bar.html:105 attr x i=4, d[0]=23, d[1]=40, xScale(i)=400
// 03_stacked_bar.html:105 attr x i=0, d[0]=15, d[1]=37, xScale(i)=0
// 03_stacked_bar.html:105 attr x i=1, d[0]=16, d[1]=44, xScale(i)=100
// 03_stacked_bar.html:105 attr x i=2, d[0]=21, d[1]=53, xScale(i)=200
// 03_stacked_bar.html:105 attr x i=3, d[0]=30, d[1]=65, xScale(i)=300
// 03_stacked_bar.html:105 attr x i=4, d[0]=40, d[1]=83, xScale(i)=400
                    return xScale(i);
                })
                // y 軸的起點位置要對應到 yScale
                .attr("y", function(d) {
// console.log("attr y, d[0]="+d[0]+", d[1]="+d[1]+" yScale(d[0])="+yScale(d[0]));
// attr y, d[0]=0, d[1]=5 yScale(d[1])=18.072289156626507
// 03_stacked_bar.html:168 attr y, d[0]=0, d[1]=4 yScale(d[1])=14.457831325301205
// 03_stacked_bar.html:168 attr y, d[0]=0, d[1]=2 yScale(d[1])=7.228915662650603
// 03_stacked_bar.html:168 attr y, d[0]=0, d[1]=7 yScale(d[1])=25.301204819277107
// 03_stacked_bar.html:168 attr y, d[0]=0, d[1]=23 yScale(d[1])=83.13253012048193
// 03_stacked_bar.html:168 attr y, d[0]=5, d[1]=15 yScale(d[1])=54.21686746987952
// 03_stacked_bar.html:168 attr y, d[0]=4, d[1]=16 yScale(d[1])=57.83132530120482
// 03_stacked_bar.html:168 attr y, d[0]=2, d[1]=21 yScale(d[1])=75.90361445783132
// 03_stacked_bar.html:168 attr y, d[0]=7, d[1]=30 yScale(d[1])=108.43373493975903
// 03_stacked_bar.html:168 attr y, d[0]=23, d[1]=40 yScale(d[1])=144.57831325301206
// 03_stacked_bar.html:168 attr y, d[0]=15, d[1]=37 yScale(d[1])=133.73493975903614
// 03_stacked_bar.html:168 attr y, d[0]=16, d[1]=44 yScale(d[1])=159.03614457831327
// 03_stacked_bar.html:168 attr y, d[0]=21, d[1]=53 yScale(d[1])=191.56626506024094
// 03_stacked_bar.html:168 attr y, d[0]=30, d[1]=65 yScale(d[1])=234.93975903614458
// 03_stacked_bar.html:168 attr y, d[0]=40, d[1]=83 yScale(d[1])=300
                    return yScale(d[0]);
                })
                // 矩形的高度就是 d[1] 及 d[0] 的差異
                .attr("height", function(d) {
                    return yScale(d[1]-d[0]);
                })
                // 矩形的寬度要扣掉一點點 padding,讓長條之間留下一些空白
                .attr("width", xScale.bandwidth()-barPadding);

        </script>


force chart


<script type="text/javascript">
            //ref: http://bl.ocks.org/mbostock/4062045

            //Width and height
            var w = 500;
            var h = 300;

            //Original data
            var dataset = {
                nodes: [
                    { name: "Adam" },
                    { name: "Bob" },
                    { name: "Carrie" },
                    { name: "Donovan" },
                    { name: "Edward" },
                    { name: "Felicity" },
                    { name: "George" },
                    { name: "Hannah" },
                    { name: "Iris" },
                    { name: "Jerry" }
                ],
                // edges 描述 起點及終點的線段
                edges: [
                    // Adam -> Bob
                    { source: 0, target: 1 },
                    { source: 0, target: 2 },
                    { source: 0, target: 3 },
                    { source: 0, target: 4 },
                    { source: 1, target: 5 },
                    { source: 2, target: 5 },
                    { source: 2, target: 5 },
                    { source: 3, target: 4 },
                    { source: 5, target: 8 },
                    { source: 5, target: 9 },
                    { source: 6, target: 7 },
                    { source: 7, target: 8 },
                    { source: 8, target: 9 }
                ]
            };

            //Initialize a default force layout, using the nodes and edges in dataset
            // var force = d3.forceSimulation()
            //                   .nodes(dataset.nodes)
            //                   .links(dataset.edges)
            //                   .size([w, h])
            //                   .linkDistance([50])
            //                   .charge([-100])
            //                   .start();
            var force = d3.forceSimulation(dataset.nodes)
                            .force("link", d3.forceLink(dataset.edges))
                            // 讓端點(戴上電荷)之間分開更遠
                            .force("charge",d3.forceManyBody())
                            .force("center", d3.forceCenter(w / 2, h / 2))
                            ;

            var colors = d3.scaleOrdinal(d3.schemeCategory10);

            //Create SVG element
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            //Create edges as lines
            var edges = svg.selectAll("line")
                .data(dataset.edges)
                .enter()
                .append("line")
                .style("stroke", "#ccc")
                .style("stroke-width", 1);

            //Create nodes as circles
            var nodes = svg.selectAll("circle")
                .data(dataset.nodes)
                .enter()
                .append("circle")
                .attr("r", 10)
                // 將端點設定為不同顏色
                .style("fill", function(d, i) {
                    return colors(i);
                })
                // 在端點設定拖動的功能
                .call(d3.drag()
                      .on("start", dragstarted)
                      .on("drag", dragged)
                      .on("end", dragended));

            //Every time the simulation "ticks", this will be called
            //每次 tick , 取得每條直線和每個圓形的新 x/y 值, 在 DOM 中更新它們
            force.on("tick", function() {

                edges.attr("x1", function(d) {
                        return d.source.x;
                     })
                     .attr("y1", function(d) { return d.source.y; })
                     .attr("x2", function(d) { return d.target.x; })
                     .attr("y2", function(d) { return d.target.y; });

                nodes.attr("cx", function(d) { return d.x; })
                     .attr("cy", function(d) { return d.y; });

            });

            function dragstarted(d) {
              if (!d3.event.active) force.alphaTarget(0.3).restart();
              d.fx = d.x;
              d.fy = d.y;
            }

            function dragged(d) {
              d.fx = d3.event.x;
              d.fy = d3.event.y;
            }

            function dragended(d) {
              if (!d3.event.active) force.alphaTarget(0);
              d.fx = null;
              d.fy = null;
            }

        </script>


Map


D3 使用 GeoJSON 搭配不同的投影演算法,能夠很方便就畫出地圖。


us-states.json 是美國的 GeoJSON 資料。


{"type":"FeatureCollection","features":[

{"type":"Feature","id":"01","properties":{"name":"Alabama"},
"geometry":{"type":"Polygon","coordinates":[[[-87.359296,35.00118],[-85.606675,34.984749],[-85.431413,34.124869],[-85.184951,32.859696],[-85.069935,32.580372],[-84.960397,32.421541],[-85.004212,32.322956],[-84.889196,32.262709],[-85.058981,32.13674],[-85.053504,32.01077],[-85.141136,31.840985],[-85.042551,31.539753],[-85.113751,31.27686],[-85.004212,31.003013],[-85.497137,30.997536],[-87.600282,30.997536],[-87.633143,30.86609],[-87.408589,30.674397],[-87.446927,30.510088],[-87.37025,30.427934],[-87.518128,30.280057],[-87.655051,30.247195],[-87.90699,30.411504],[-87.934375,30.657966],[-88.011052,30.685351],[-88.10416,30.499135],[-88.137022,30.318396],[-88.394438,30.367688],[-88.471115,31.895754],[-88.241084,33.796253],[-88.098683,34.891641],[-88.202745,34.995703],[-87.359296,35.00118]]]}},
{"type":"Feature","id":"02","properties":{"name":"Alaska"},"geometry":{ ....

        <script type="text/javascript">

            //Width and height
            var w = 500;
            var h = 300;

            //Define map projection
            // Albers USA 是一種復合投影, 可以把阿拉斯加和夏威夷整合到西南地區的下方。
            var projection = d3.geoAlbersUsa()
                            // 這裡是把投影平移到了 SVG 圖形的中央
                            .translate([w/2, h/2])
                            //預設的縮放值是 1000, 比這個值小就會縮小地圖, 比這個值大就會放大地圖。
                            .scale([500]);

            //Define path generator
            var path = d3.geoPath()
                             .projection(projection);

            //Create SVG element
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            //Load in GeoJSON data
            // us-states.json 是 美國地圖的 geojson 資料,需要的是 geometry 資料
            d3.json("us-states.json", function(json) {

                //Bind data and create one path per GeoJSON feature
                svg.selectAll("path")
                   .data(json.features)
                   .enter()
                   // 根據 GeoJson 產生 Path
                   .append("path")
                   .attr("d", path)
                   // 填上 steelblue 顏色
                   .style("fill", "steelblue");

            });

        </script>


  • 等值區域地圖

選舉時常用,可以把相同數值的區域塗上一樣的顏色。


以量化的比例尺函數作為線性比例尺, 但比例尺輸出的則是離散的數值範圍。 這裡輸出的值可以是數值、顏色或是其他你需要的值。 這個比例尺適合把值分組( bucket),這裡只分了 5 個組, 實際上你想分幾個就分幾個。


var color = d3.scaleQuantize()
        .range(["rgb(237,248,233)","rgb(186,228,179)","rgb(116,196,118)","rgb(49,163,84)","rgb(0,109,44)"]);

us-ag-productivity-2004.csv


state,value
Alabama,1.1791
Arkansas,1.3705
Arizona,1.3847
California,1.7979
Colorado,1.0325
Connecticut,1.3209
Delaware,1.4345
Florida,1.6304
Georgia,1.3891
Iowa,1.5297
Idaho,1.4285
Illinois,1.5297
Indiana,1.4220
Kansas,1.0124
Kentucky,0.9403
Louisiana,0.9904
Maine,1.3877
Maryland,1.2457
Massachusetts,1.1458
Michigan,1.1058
Minnesota,1.2359
Missouri,1.0212
Mississippi,1.1306
Montana,0.8145
North Carolina,1.3554
North Dakota,1.0278
Nebraska,1.1619
New Hampshire,1.0204
New Jersey,1.2831
New Mexico,0.8925
Nevada,0.9640
New York,1.1327
Ohio,1.2075
Oklahoma,0.7693
Oregon,1.3154
Pennsylvania,1.0601
Rhode Island,1.4192
South Carolina,1.1247
South Dakota,1.0760
Tennessee,0.7648
Texas,0.8873
Utah,0.9638
Virginia,0.9660
Vermont,1.0762
Washington,1.1457
Wisconsin,1.1130
West Virginia,0.5777
Wyoming,0.5712

<script type="text/javascript">

            //Width and height
            var w = 500;
            var h = 300;

            //Define map projection
            var projection = d3.geoAlbersUsa()
                                   .translate([w/2, h/2])
                                   .scale([500]);

            //Define path generator
            var path = d3.geoPath()
                             .projection(projection);

            //Define quantize scale to sort data values into buckets of color
            var color = d3.scaleQuantize()
                                .range(["rgb(237,248,233)","rgb(186,228,179)","rgb(116,196,118)","rgb(49,163,84)","rgb(0,109,44)"]);
                                //Colors taken from colorbrewer.js, included in the D3 download

            //Create SVG element
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            //Load in agriculture data
            d3.csv("us-ag-productivity-2004.csv", function(data) {

                //Set input domain for color scale
                // 設定顏色的值域,以 csv 的 value 欄位的 max, min 來設定
                color.domain([
                    d3.min(data, function(d) { return d.value; }),
                    d3.max(data, function(d) { return d.value; })
                ]);

                //Load in GeoJSON data
                d3.json("us-states.json", function(json) {

                    //Merge the ag. data and GeoJSON
                    // loop GeoJson 的每一個 state
                    for (var i = 0; i < data.length; i++) {

                        //Grab state name 取得州的名稱
                        var dataState = data[i].state;

                        //Grab data value, and convert from string to float
                        // 取得 csv 的 對應 value
                        var dataValue = parseFloat(data[i].value);

                        //Find the corresponding state inside the GeoJSON
                        for (var j = 0; j < json.features.length; j++) {

                            var jsonState = json.features[j].properties.name;

                            if (dataState == jsonState) {

                                //Copy the data value into the JSON
                                // 把 value 複製到 GeoJSON 裡面
                                json.features[j].properties.value = dataValue;

                                //Stop looking through the JSON
                                break;

                            }
                        }
                    }

                    //Bind data and create one path per GeoJSON feature
                    svg.selectAll("path")
                       .data(json.features)
                       .enter()
                       .append("path")
                       .attr("d", path)
                       // 建立 Path 的顏色,以 properties.value 決定顏色
                       .style("fill", function(d) {
                            //Get data value
                            var value = d.properties.value;

                            if (value) {
                                //If value exists…
                                return color(value);
                            } else {
                                //缺少資料以固定的顏色設定
                                return "#ccc";
                            }
                       });

                });

            });

        </script>


  • 在地圖上標記事件點

us-cities.csv


rank,place,population,lat,lon
1,New York city,8175133,40.71455,-74.007124
2,Los Angeles city,3792621,34.05349,-118.245323
3,Chicago city,2695598,45.37399,-92.888759
4,Houston city,2099451,41.337462,-75.733627
5,Philadelphia city,1526006,37.15477,-94.486114
6,Phoenix city,1445632,32.46764,-85.000823
7,San Antonio city,1327407,37.706576,-122.440612
8,San Diego city,1307402,37.707815,-122.466624
9,Dallas city,1197816,40.636,-91.168309
...

                    //Load in cities data
                    d3.csv("us-cities.csv", function(data) {

                        svg.selectAll("circle")
                           .data(data)
                           .enter()
                           .append("circle")
                           // 城市的座標位置要經過投影才能正確繪製在地圖上
                           .attr("cx", function(d) {
                               return projection([d.lon, d.lat])[0];
                           })
                           .attr("cy", function(d) {
                               return projection([d.lon, d.lat])[1];
                           })
                           // 依照人口的數量,決定半徑的大小
                           .attr("r", function(d) {
                                return Math.sqrt(parseInt(d.population) * 0.00004);
                           })
                           .style("fill", "yellow")
                           .style("opacity", 0.75);

                    });


  • 世界地圖的海洋

更換 GeoJSON 以及 Mercator 投影法,就可以畫出世界地圖。


        <script type="text/javascript">

            //Width and height
            var w = 500;
            var h = 300;

            //Define map projection
            var projection = d3.geoMercator()
                                   .translate([w/2, h/2])
                                   .scale([100]);

            //Define path generator
            var path = d3.geoPath()
                             .projection(projection);

            //Create SVG element
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            //Load in GeoJSON data
            d3.json("oceans.json", function(json) {

                //Bind data and create one path per GeoJSON feature
                svg.selectAll("path")
                   .data(json.features)
                   .enter()
                   .append("path")
                   .attr("d", path)
                   .style("fill", "steelblue");

            });

        </script>


References


D3: Data-Driven Documents - Michael Bostock, Vadim Ogievetsky and Jeffrey Heer


《D3 API 詳解》隨書源碼 後面的 Refereces 有很多 D3.js 的網頁資源


用 D3.js v4 看 Pokemon 屬性表 D3.js v3 到 v4 的 migration 差異


Update d3.js scripts from V3 to V4


D3 Tips and Tricks v4.x


Mike Bostock’s Blocks


OUR D3.JS 數據可視化專題站


數據可視化與D3.js,數據可視化D3.js


讀書筆記 - 數據可視化實踐

2017/1/16

D3.js 基本的使用方式 part 2


update, transition


  • 當資料會隨著時間變化,就需要動態更新這些資料,視覺處理以 transition 動畫展現。

            d3.select("p")
                .on("click", function() {

                    //New values for dataset 更新資料
                    dataset = [ 11, 12, 15, 20, 18, 17, 16, 18, 23, 25,
                                5, 10, 13, 19, 21, 25, 22, 18, 15, 13 ];

                    //Update all rects  更新矩形的大小
                    svg.selectAll("rect")
                       .data(dataset)
                       .transition()    // <-- 加上這一個 method 就會有更新過程的動畫
                       .duration(5000)  // 設定動畫更新時間 5s
                       .attr("y", function(d) {
                            return h - yScale(d);
                       })
                       .attr("height", function(d) {
                            return yScale(d);
                       })
                       .attr("fill", function(d) {
                            return "rgb(0, 0, " + (d * 10) + ")";
                       });

                    //Update all labels 更新 label
                    svg.selectAll("text")
                       .data(dataset)
                       .transition()    // <-- 加上這一個 method 就會有更新過程的動畫
                       .duration(5000)  // 設定動畫更新時間 5s
                       .text(function(d) {
                            return d;
                       })
                       .attr("x", function(d, i) {
                            return xScale(i) + xScale.bandwidth() / 2;
                       })
                       .attr("y", function(d) {
                            return h - yScale(d) + 14;
                       });

                });

  • 延遲時間

dealy() 設定固定的時間,延遲幾毫秒後開始進行動畫,也可以用匿名函數動態設定延遲時間


ease() 設定動畫改變的加速模型,有 linear, circle, elastic, bounce...


// v3 的寫法
.ease("linear")

//v4
.ease(d3.easeLinear)
.ease(d3.easeCircle)
.ease(d3.easeBounce)

在更新矩形或是 label 時,加上 ease 就可以了


                //Update all rects
                    svg.selectAll("rect")
                       .data(dataset)
                       .transition()
                       .duration(2000)
                       .ease(d3.easeBounce)
                       .attr("y", function(d) {
                            return h - yScale(d);
                       })
                       .attr("height", function(d) {
                            return yScale(d);
                       })
                       .attr("fill", function(d) {
                            return "rgb(0, 0, " + (d * 10) + ")";
                       });

.delay(1000)      // 固定延遲時間


.delay(function(d, i) {
    return i * 100;     // 後面的動畫開始時間比前一個晚 100ms
})
.duration(500)          // 總時間縮短,避免動畫時間過長


// 這種方式,可以確保 dataset 不管有多少個,動畫時間都是合理的長度
.delay(function(d, i) {
    return i / dataset.length * 1000;   // 先將 i/data.length 做 normalized,然後再放大 1000 倍
})
.duration(500)

  • 套用亂數產生的 dataset

        <p>Click on this text to update the chart with new data values as many times as you like!</p>

        <script type="text/javascript">

            //Width and height
            var w = 600;
            var h = 250;
            var barPadding = 1;

            var dataset = [ 5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
                            11, 12, 15, 20, 18, 17, 16, 18, 23, 25 ];

            var xScale = d3.scaleBand()
                        .domain(d3.range(dataset.length))
                        .range([0, w], 0.05);

            var yScale = d3.scaleLinear()
                            .domain([0, d3.max(dataset)])
                            .range([0, h]);

            //Create SVG element
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            //Create bars
            svg.selectAll("rect")
               .data(dataset)
               .enter()
               .append("rect")
               .attr("x", function(d, i) {
                    return xScale(i);
               })
               .attr("y", function(d) {
                    return h - yScale(d);
               })
               .attr("width", xScale.bandwidth()-barPadding)
               .attr("height", function(d) {
                    return yScale(d);
               })
               .attr("fill", function(d) {
                    return "rgb(0, 0, " + (d * 10) + ")";
               });

            //Create labels
            svg.selectAll("text")
               .data(dataset)
               .enter()
               .append("text")
               .text(function(d) {
                    return d;
               })
               .attr("text-anchor", "middle")
               .attr("x", function(d, i) {
                    return xScale(i) + xScale.bandwidth() / 2;
               })
               .attr("y", function(d) {
                    return h - yScale(d) + 14;
               })
               .attr("font-family", "sans-serif")
               .attr("font-size", "11px")
               .attr("fill", "white");

            //On click, update with new data
            d3.select("p")
                .on("click", function() {

                    //New values for dataset
                    var numValues = dataset.length;
                    var maxValue = 100; //Highest possible new value
                    dataset = [];
                    for (var i = 0; i < numValues; i++) {
                        var newNumber = Math.floor(Math.random() * maxValue);   //New random integer (0-100)
                        dataset.push(newNumber);
                    }

                    // 因為 dataset 的資料範圍改變了, 要重新計算 yScale 的 domain
                    //Recalibrate the scale domain, given the new max value in dataset
                    yScale.domain([0, d3.max(dataset)]);

                    //Update all rects
                    svg.selectAll("rect")
                       .data(dataset)
                       .transition()
                       .delay(function(d, i) {
                           return i / dataset.length * 1000;
                       })
                       .duration(500)
                       .attr("y", function(d) {
                            return h - yScale(d);
                       })
                       .attr("height", function(d) {
                            return yScale(d);
                       })
                       .attr("fill", function(d) {
                            return "rgb(0, 0, " + (d * 10) + ")";
                       });

                    //Update all labels
                    svg.selectAll("text")
                       .data(dataset)
                       .transition()
                       .delay(function(d, i) {
                           return i / dataset.length * 1000;
                       })
                       .duration(500)
                       .text(function(d) {
                            return d;
                       })
                       .attr("x", function(d, i) {
                            return xScale(i) + xScale.bandwidth() / 2;
                       })
                       .attr("y", function(d) {
                            return h - yScale(d) + 14;
                       });

                });
        </script>


  • 換成 二維 dataset

        <style type="text/css">

            .axis path,
            .axis line {
                fill: none;
                stroke: black;
                shape-rendering: crispEdges;
            }

            .axis text {
                font-family: sans-serif;
                font-size: 11px;
            }

        </style>

        <p>Click on this text to update the chart with new data values as many times as you like!</p>

        <script type="text/javascript">

            //Width and height
            var w = 500;
            var h = 300;
            var padding = 30;

            //Dynamic, random dataset
            var dataset = [];
            var numDataPoints = 50;
            var maxRange = Math.random() * 1000;
            for (var i = 0; i < numDataPoints; i++) {
                var newNumber1 = Math.floor(Math.random() * maxRange);
                var newNumber2 = Math.floor(Math.random() * maxRange);
                dataset.push([newNumber1, newNumber2]);
            }

            //Create scale functions
            var xScale = d3.scaleLinear()
                                 .domain([0, d3.max(dataset, function(d) { return d[0]; })])
                                 .range([padding, w - padding * 2]);

            var yScale = d3.scaleLinear()
                                 .domain([0, d3.max(dataset, function(d) { return d[1]; })])
                                 .range([h - padding, padding]);

            //Define X axis
            var xAxis = d3.axisBottom()
                              .scale(xScale)
                              .ticks(5);

            //Define Y axis
            var yAxis = d3.axisLeft()
                              .scale(yScale)
                              .ticks(5);

            //Create SVG element
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            //Create circles
            svg.selectAll("circle")
               .data(dataset)
               .enter()
               .append("circle")
               .attr("cx", function(d) {
                    return xScale(d[0]);
               })
               .attr("cy", function(d) {
                    return yScale(d[1]);
               })
               .attr("r", 2);

            //Create X axis,加上 css class
            svg.append("g")
                .attr("class", "x axis")
                .attr("transform", "translate(0," + (h - padding) + ")")
                .call(xAxis);

            //Create Y axis
            svg.append("g")
                .attr("class", "y axis")
                .attr("transform", "translate(" + padding + ",0)")
                .call(yAxis);



            //On click, update with new data
            d3.select("p")
                .on("click", function() {

                    //New values for dataset
                    var numValues = dataset.length;
                    var maxRange = Math.random() * 1000;
                    dataset = [];
                    for (var i = 0; i < numValues; i++) {
                        var newNumber1 = Math.floor(Math.random() * maxRange);
                        var newNumber2 = Math.floor(Math.random() * maxRange);
                        dataset.push([newNumber1, newNumber2]);
                    }

                    //Update scale domains
                    xScale.domain([0, d3.max(dataset, function(d) { return d[0]; })]);
                    yScale.domain([0, d3.max(dataset, function(d) { return d[1]; })]);

                    //Update all circles
                    svg.selectAll("circle")
                       .data(dataset)
                       .transition()
                       .duration(1000)
                       .attr("cx", function(d) {
                            return xScale(d[0]);
                       })
                       .attr("cy", function(d) {
                            return yScale(d[1]);
                       });

                    //Update X axis
                    svg.select(".x.axis")
                        .transition()
                        .duration(1000)
                        .call(xAxis);

                    //Update Y axis
                    svg.select(".y.axis")
                        .transition()
                        .duration(1000)
                        .call(yAxis);

                });

        </script>


  • transition 開始與結束的 callback function

// v3
.each("start", function() {
   d3.select(this)
     .attr("fill", "magenta")
     .attr("r", 3);
})

// v4
.on("start", function() {
   d3.select(this)
     .attr("fill", "magenta")
     .attr("r", 3);
})

                    //Update all circles
                    svg.selectAll("circle")
                       .data(dataset)
                       .transition()
                       .duration(1000)
                       // 開始時執行
                       .on("start", function() {
                           d3.select(this)
                             .attr("fill", "magenta")
                             .attr("r", 3);
                       })
                       .attr("cx", function(d) {
                            return xScale(d[0]);
                       })
                       .attr("cy", function(d) {
                            return yScale(d[1]);
                       })
                       // 結束後執行
                       .on("end", function() {
                           d3.select(this)
                             .attr("fill", "black")
                             .attr("r", 2);
                       });

在 start 時,不能再加上其他的 transition,因為 D3 限制任何元素同一時間,只能有一個 transition,新的 transition 會覆蓋舊的,這跟 jQuery 的設計不同,jQuery 會把動畫效果排隊依序執行。


// 錯誤的用法
.each("start", function() {
    d3.select(this)
        .transition()
        .duration(250)
        .attr("fill", "magenta")
        .attr("r", 3);
})

// end 可以加上另一個 transition
.on("end", function() {
   d3.select(this)
     .transition()
     .duration(1000)
     .attr("fill", "black")
     .attr("r", 2);
});

也可以在 svg 中 transition + on + transition + on 的方式,進行連續的動畫。


                    //Update all circles
                    svg.selectAll("circle")
                       .data(dataset)
                       .transition()
                       .duration(1000)
                       .on("start", function() {
                           d3.select(this)
                             .attr("fill", "magenta")
                             .attr("r", 7);
                       })
                       .attr("cx", function(d) {
                            return xScale(d[0]);
                       })
                       .attr("cy", function(d) {
                            return yScale(d[1]);
                       })
                       .transition()
                       .duration(1000)
                       .attr("fill", "black")
                       .attr("r", 2);

  • clipPath

剛剛的轉場動畫中,因為將散點圖的圓形放大,因此在接近軸線的地方,圓形會超過軸線,超過中間的圖形區塊。


可以利用 D3 的 clipPath 製造一塊遮罩板片,遮住超過該區塊的圖形。


            //Define clipping path 產生 clip path,id 為 chart-area,矩形區塊
            svg.append("clipPath")
                .attr("id", "chart-area")
                .append("rect")
                .attr("x", padding)
                .attr("y", padding)
                .attr("width", w - padding * 3)
                .attr("height", h - padding * 2);

            //Create circles 產生的散點放在 chart-area 這個 clipPath 裡面
            svg.append("g")
               .attr("id", "circles")
               .attr("clip-path", "url(#chart-area)")
               .selectAll("circle")
               .data(dataset)
               .enter()
               .append("circle")
               .attr("cx", function(d) {
                    return xScale(d[0]);
               })
               .attr("cy", function(d) {
                    return yScale(d[1]);
               })
               .attr("r", 2);

  • 新增資料到舊的 dataset

先調整 dataset 將,新資料放進去,接下來產生新的 rect 及 text,並將初始位置訂在畫面的右邊,一開始就看不到新的 rect, text。


然後再將全部的 rect, text 以 transition 移動到新的位置。



        <p id="add">Add a new data value</p>
        <p id="remove">Remove a data value</p>

        <script type="text/javascript">

            //Width and height
            var w = 600;
            var h = 250;
            var barPadding = 1;

            // dataset 改為 key, value pair
            var dataset = [ { key: 0, value: 5 },       //dataset is now an array of objects.
                            { key: 1, value: 10 },      //Each object has a 'key' and a 'value'.
                            { key: 2, value: 13 },
                            { key: 3, value: 19 },
                            { key: 4, value: 21 },
                            { key: 5, value: 25 },
                            { key: 6, value: 22 },
                            { key: 7, value: 18 },
                            { key: 8, value: 15 },
                            { key: 9, value: 13 },
                            { key: 10, value: 11 },
                            { key: 11, value: 12 },
                            { key: 12, value: 15 },
                            { key: 13, value: 20 },
                            { key: 14, value: 18 },
                            { key: 15, value: 17 },
                            { key: 16, value: 16 },
                            { key: 17, value: 18 },
                            { key: 18, value: 23 },
                            { key: 19, value: 25 } ];

            var xScale = d3.scaleBand()
                        .domain(d3.range(dataset.length))
                        .range([0, w], 0.05);

            // d3.max 改為 使用 dataset 的 value
            var yScale = d3.scaleLinear()
                            .domain([0, d3.max(dataset, function(d) { return d.value; })])
                            .range([0, h]);

            //定義用來 bind data 的 key function
            var key = function(d) {
                return d.key;
            };

            //Create SVG element
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            //Create bars
            svg.selectAll("rect")
               .data(dataset, key)
               .enter()
               .append("rect")
               .attr("x", function(d, i) {
                    return xScale(i);
               })
               .attr("y", function(d) {
                    return h - yScale(d.value);
               })
               .attr("width", xScale.bandwidth()-barPadding)
               .attr("height", function(d) {
                    return yScale(d.value);
               })
               .attr("fill", function(d) {
                    return "rgb(0, 0, " + (d.value * 10) + ")";
               });

            //Create labels
            svg.selectAll("text")
               .data(dataset, key)
               .enter()
               .append("text")
               .text(function(d) {
                    return d.value;
               })
               .attr("text-anchor", "middle")
               .attr("x", function(d, i) {
                    return xScale(i) + xScale.bandwidth() / 2;
               })
               .attr("y", function(d) {
                    return h - yScale(d.value) + 14;
               })
               .attr("font-family", "sans-serif")
               .attr("font-size", "11px")
               .attr("fill", "white");

            //On click, update with new data
            d3.selectAll("p")
                .on("click", function() {

                    // 判斷點擊了 add / remove
                    var paragraphID = d3.select(this).attr("id");

                    //Decide what to do next
                    if (paragraphID == "add") {
                        //Add a data value
                        var maxValue = 25;
                        var newNumber = Math.floor(Math.random() * maxValue+1);
                        // 新的 key 以 dataset 最後一個元素的 key +1 來設定
                        var lastKeyValue = dataset[dataset.length - 1].key;
                        console.log(lastKeyValue);
                        dataset.push({
                            key: lastKeyValue + 1,
                            value: newNumber
                        });
                    } else {
                        //Remove a value, 移除 dataset 最前面那個元素
                        dataset.shift();
                    }

                    //Update scale domains
                    xScale.domain(d3.range(dataset.length));
                    yScale.domain([0, d3.max(dataset, function(d) { return d.value; })]);

                    //Select…
                    var bars = svg.selectAll("rect")
                        .data(dataset, key);

                    // exit 會回傳被移除的元素
                    bars.exit().remove();
                    // bars.exit()
                    //  .transition()
                    //  .duration(500)
                    //  .attr("x", -xScale.bandwidth())
                    //  .remove();

                    //Enter…
                    var newbars = bars.enter()
                        .append("rect")
                        .attr("x", w)
                        .attr("y", function(d) {
                            return h - yScale(d.value);
                        })
                        .attr("width", xScale.bandwidth()-barPadding)
                        .attr("height", function(d) {
                            return yScale(d.value);
                        })
                        .attr("fill", function(d) {
                            return "rgb(0, 0, " + (d.value * 10) + ")";
                        });

                    //Update…
                    // bars.transition()
                    svg.selectAll("rect").transition()
                        .duration(500)
                        .attr("x", function(d, i) {
                            return xScale(i);
                        })
                        .attr("y", function(d) {
                            return h - yScale(d.value);
                        })
                        .attr("width", xScale.bandwidth()-barPadding)
                        .attr("height", function(d) {
                            return yScale(d.value);
                        });


                    //Update all labels

                    //Select…
                    var labels = svg.selectAll("text")
                        .data(dataset, key);

                    labels.exit().remove();

                    //Enter…
                    labels.enter()
                        .append("text")
                        .text(function(d) {
                            return d.value;
                        })
                        .attr("text-anchor", "middle")
                        .attr("x", w)
                        .attr("y", function(d) {
                            return h - yScale(d.value) + 14;
                        })
                       .attr("font-family", "sans-serif")
                       .attr("font-size", "11px")
                       .attr("fill", "white");

                    //Update…
                    // labels.transition()
                    svg.selectAll("text").transition()
                        .duration(500)
                        .attr("x", function(d, i) {
                            return xScale(i) + xScale.bandwidth() / 2;
                        });
                });


        </script>

互動式圖表


  • event listener

以 on method 綁定 evnet listener,在 callback function 中調整畫面。


// 以 on 綁定 click 事件
.on("click", function(d) {
    console.log(d);
})

css mouse hover


// 加上 css,讓滑鼠 hover 時,改變顏色
        <style type="text/css">

            rect:hover {
                fill: orange;
            }

        </style>

mouseover, mouseout


// 以 on 綁定 mouseover 事件
.on("mouseover", function() {
    // this 就是目前這個操作的元素
    d3.select(this)
        .attr("fill", "orange");
})
// 以 on 綁定 mouseout 事件
.on("mouseout", function(d) {
   d3.select(this)
        .attr("fill", "rgb(0, 0, " + (d * 10) + ")");
})

在 mouseout 以 transition 方式改回原本的顏色,讓畫面更流暢


// 以 on 綁定 mouseover 事件
.on("mouseover", function() {
    d3.select(this)
        .attr("fill", "orange");
})
// 以 on 綁定 mouseout 事件, 以 transition 方式改回原本的顏色,讓畫面更流暢
.on("mouseout", function(d) {
   d3.select(this)
        .transition()
        .duration(250)
        .attr("fill", "rgb(0, 0, " + (d * 10) + ")");
})

完整的範例


        <script type="text/javascript">

            //Width and height
            var w = 600;
            var h = 250;

            var dataset = [ 5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
                            11, 12, 15, 20, 18, 17, 16, 18, 23, 25 ];

            var xScale = d3.scaleBand()
                            .domain(d3.range(dataset.length))
                            .range([0, w], 0.05);

            var yScale = d3.scaleLinear()
                            .domain([0, d3.max(dataset)])
                            .range([0, h]);

            //Create SVG element
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            //Create bars
            svg.selectAll("rect")
               .data(dataset)
               .enter()
               .append("rect")
               .attr("x", function(d, i) {
                    return xScale(i);
               })
               .attr("y", function(d) {
                    return h - yScale(d);
               })
               .attr("width", xScale.bandwidth())
               .attr("height", function(d) {
                    return yScale(d);
               })
               .attr("fill", function(d) {
                    return "rgb(0, 0, " + (d * 10) + ")";
               })
               // 以 on 綁定 mouseover 事件
               .on("mouseover", function() {
                    d3.select(this)
                        .attr("fill", "orange");
               })
               // 以 on 綁定 mouseout 事件, 以 transition 方式改回原本的顏色,讓畫面更流暢
               .on("mouseout", function(d) {
                   d3.select(this)
                        .transition()
                        .duration(250)
                        .attr("fill", "rgb(0, 0, " + (d * 10) + ")");
               });

            //Create labels
            svg.selectAll("text")
               .data(dataset)
               .enter()
               .append("text")
               .text(function(d) {
                    return d;
               })
               // 加上這個 css style,讓滑鼠移動到 label 時,不會變成鍵盤輸入的游標
               .style("pointer-events", "none")
               .attr("text-anchor", "middle")
               .attr("x", function(d, i) {
                    return xScale(i) + xScale.bandwidth() / 2;
               })
               .attr("y", function(d) {
                    return h - yScale(d) + 14;
               })
               .attr("font-family", "sans-serif")
               .attr("font-size", "11px")
               .attr("fill", "white");

        </script>

  • sorting

可以在 on action listener 中,呼叫 sortBars function,可針對矩形及 label 進行排序。


但上面最後一個例子中,綁定了 mouseover 及 mouseout 進行 hover 顏色變化的處理,如果在 sorting 時,移動了滑鼠,因為 D3 預設會覆蓋動畫,這會造成 mouseover 及 mouseout 的元素,會停留在滑鼠指到的地方的問題。


要解決這個問題,必須將 hover 的顏色處理,回歸讓 css 來調整。


            //Define sort function
            var sortBars = function() {

                svg.selectAll("rect")
                   .sort(function(a, b) {
                       return d3.ascending(a, b);
                    })
                   .transition()
                   .duration(1000)
                   .attr("x", function(d, i) {
                        return xScale(i);
                   });

                svg.selectAll("text")
                    .sort(function(a, b) {
                       return d3.ascending(a, b);
                    })
                    .transition()
                    .duration(1000)
                    .attr("x", function(d, i) {
                        return xScale(i) + xScale.bandwidth() / 2;
                    });
            };

以下為實例,排序會在順序及倒序兩個一直變換。


        <style type="text/css">

            rect:hover {
                fill: orange;
            }

        </style>
        
        <script type="text/javascript">

            //Width and height
            var w = 600;
            var h = 250;

            var dataset = [ 5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
                            11, 12, 15, 20, 18, 17, 16, 18, 23, 25 ];

            var xScale = d3.scaleBand()
                            .domain(d3.range(dataset.length))
                            .range([0, w], 0.05);

            var yScale = d3.scaleLinear()
                            .domain([0, d3.max(dataset)])
                            .range([0, h]);

            //Create SVG element
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            //Create bars
            svg.selectAll("rect")
               .data(dataset)
               .enter()
               .append("rect")
               .attr("x", function(d, i) {
                    return xScale(i);
               })
               .attr("y", function(d) {
                    return h - yScale(d);
               })
               .attr("width", xScale.bandwidth())
               .attr("height", function(d) {
                    return yScale(d);
               })
               .attr("fill", function(d) {
                    return "rgb(0, 0, " + (d * 10) + ")";
               })
               .on("click", function() {
                    sortBars();
               });

            //Create labels
            svg.selectAll("text")
               .data(dataset)
               .enter()
               .append("text")
               .text(function(d) {
                    return d;
               })
               .attr("text-anchor", "middle")
               .attr("x", function(d, i) {
                    return xScale(i) + xScale.bandwidth() / 2;
               })
               .attr("y", function(d) {
                    return h - yScale(d) + 14;
               })
               .attr("font-family", "sans-serif")
               .attr("font-size", "11px")
               .attr("fill", "white");

            //Define sort order flag
            var sortOrder = false;

            //Define sort function
            var sortBars = function() {

                //Flip value of sortOrder
                sortOrder = !sortOrder;

                svg.selectAll("rect")
                   .sort(function(a, b) {
                        if (sortOrder) {
                            return d3.ascending(a, b);
                        } else {
                            return d3.descending(a, b);
                        }
                    })
                   .transition()
                   // 加上 delay 會減慢變化的過程
                   //.delay(function(d, i) {
                    //   return i * 50;
                   //})
                   .duration(1000)
                   .attr("x", function(d, i) {
                        return xScale(i);
                   });

                svg.selectAll("text")
                    .sort(function(a, b) {
                       if (sortOrder) {
                            return d3.ascending(a, b);
                        } else {
                            return d3.descending(a, b);
                        }
                    })
                    .transition()
                    // 加上 delay 會減慢變化的過程
                   //.delay(function(d, i) {
                    //   return i * 50;
                   //})
                    .duration(1000)
                    .attr("x", function(d, i) {
                        return xScale(i) + xScale.bandwidth() / 2;
                    });
            };

        </script>

  • tooltip

瀏覽器標準 tooltip: 在產生矩形時以 title 及 text 產生 tooltip


            //Create bars
            svg.selectAll("rect")
               .data(dataset)
               .enter()
               .append("rect")
               .attr("x", function(d, i) {
                    return xScale(i);
               })
               .attr("y", function(d) {
                    return h - yScale(d);
               })
               .attr("width", xScale.bandwidth())
               .attr("height", function(d) {
                    return yScale(d);
               })
               .attr("fill", function(d) {
                    return "rgb(0, 0, " + (d * 10) + ")";
               })
               .on("click", function() {
                    sortBars();
               })
               // 在矩形中以 title 產生 tooltip
               .append("title")
               .text(function(d) {
                    return "This value is " + d;
               });

svg tooltip: mouseover 中,動態產生 svg 的 text 區塊,然後在 mouseout 移除


            //Create bars
            svg.selectAll("rect")
               .data(dataset)
               .enter()
               .append("rect")
               .attr("x", function(d, i) {
                    return xScale(i);
               })
               .attr("y", function(d) {
                    return h - yScale(d);
               })
               .attr("width", xScale.bandwidth())
               .attr("height", function(d) {
                    return yScale(d);
               })
               .attr("fill", function(d) {
                    return "rgb(0, 0, " + (d * 10) + ")";
               })
               .on("mouseover", function(d) {

                    //Get this bar's x/y values, then augment for the tooltip
                    var xPosition = parseFloat(d3.select(this).attr("x")) + xScale.bandwidth() / 2;
                    var yPosition = parseFloat(d3.select(this).attr("y")) + 14;

                    //Create the tooltip label
                    svg.append("text")
                       .attr("id", "tooltip")
                       .attr("x", xPosition)
                       .attr("y", yPosition)
                       .attr("text-anchor", "middle")
                       .attr("font-family", "sans-serif")
                       .attr("font-size", "11px")
                       .attr("font-weight", "bold")
                       .attr("fill", "black")
                       .text(d);

               })
               .on("mouseout", function() {

                    //Remove the tooltip
                    d3.select("#tooltip").remove();

               })
               .on("click", function() {
                    sortBars();
               });

div tooltip: 跟 svg 一樣,mouseover 中,動態產生 div 區塊,然後在 mouseout 隱藏


            //Create bars
            svg.selectAll("rect")
               .data(dataset)
               .enter()
               .append("rect")
               .attr("x", function(d, i) {
                    return xScale(i);
               })
               .attr("y", function(d) {
                    return h - yScale(d);
               })
               .attr("width", xScale.bandwidth())
               .attr("height", function(d) {
                    return yScale(d);
               })
               .attr("fill", function(d) {
                    return "rgb(0, 0, " + (d * 10) + ")";
               })
               .on("mouseover", function(d) {

                    //Get this bar's x/y values, then augment for the tooltip
                    var xPosition = parseFloat(d3.select(this).attr("x")) + xScale.bandwidth() / 2;
                    var yPosition = parseFloat(d3.select(this).attr("y")) / 2 + h / 2;

                    //Update the tooltip position and value
                    d3.select("#tooltip")
                        .style("left", xPosition + "px")
                        .style("top", yPosition + "px")
                        .select("#value")
                        .text(d);

                    //Show the tooltip
                    d3.select("#tooltip").classed("hidden", false);

               })
               .on("mouseout", function() {

                    //Hide the tooltip
                    d3.select("#tooltip").classed("hidden", true);

               })
               .on("click", function() {
                    sortBars();
               });

References


D3: Data-Driven Documents - Michael Bostock, Vadim Ogievetsky and Jeffrey Heer


《D3 API 詳解》隨書源碼 後面的 Refereces 有很多 D3.js 的網頁資源


用 D3.js v4 看 Pokemon 屬性表 D3.js v3 到 v4 的 migration 差異


Update d3.js scripts from V3 to V4


D3 Tips and Tricks v4.x


Mike Bostock’s Blocks


OUR D3.JS 數據可視化專題站


數據可視化與D3.js,數據可視化D3.js


讀書筆記 - 數據可視化實踐