2017/01/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/01/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

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

2017/01/09

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

D3.js 在 2016/7/28 釋出 v4.0.0 版,現在已經更新到 v4.4.1,大部分的書本還是以 v3 為主,因此我們嘗試測試將書本的範例調整為 v4 版本。

D3.js 可以生成及處理資料,處理過程經歷以下的步驟:

  1. 把資料載入到瀏覽器的 memory
  2. 把資料綁定到 DOM 的元素,根據需要建立新元素
  3. 解析每個元素的範圍資料 (bound datum),並為其設置相應的可視化屬性,實現元素的轉換 (transforming)
  4. 套用使用者輸入的,實現元素狀態的動態過渡 (transitioning)

D3 不適合產生探索型的視覺圖形,擅長產生解釋型的視覺圖形,探索型的視圖工具可以根據相同的資料,產生多個視圖。

D3 擅長處理 SVG 及 GeoJSON,不處理類似 google map 的地圖貼片。

所有的數據資料都必須傳送到客戶端的瀏覽器,如果數據資料有分享的疑慮,就不應該使用 D3.js。

以下的範例都是由 數據可視化實戰:使用D3設計交互式圖表 這本書取得的。

資料處理

  • 產生網頁 DOM 元素
d3  // 引用 D3 物件
.select("body")  // 取得 body 元素
.append("p")    // 產生 p
.text("New paragraph!")     // 放入文字到 p 元素中
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <script type="text/javascript" src="../d3/d3-4.2.2.min.js"></script>
    </head>
    <body>
        <script type="text/javascript">
            d3.select("body")
                .append("p")
                .text("New paragraph!");
        </script>
    </body>
</html>

大部分的 d3 method 會傳回正在操作的 DOM 元素,所以可以連續呼叫 method。

  • 載入 csv 資料

d3.csv 是非同步的 method,後面需要一個 callback function 處理接收的資料。如果前面多了一個 error 參數,則是在處理下載 csv 失敗時的狀況。

d3.csv("food.csv", function(data) {
        console.log(data);
    });
            
var dataset;
d3.csv("food.csv", function(error, data) {
    if (error) {
        console.log(error); // 輸出錯誤
    } else {
        console.log(data); // 輸出資料
        dataset = data;
    }
});
  • D3 的常見問題:如何使用還不存在的元素
var dataset = [ 5, 10, 15, 20, 25 ];

d3.select("body")
    .selectAll("p") // 取得 body 裡面所有的 <p>,如果還不存在,就建立一個新的 <p>
    .data(dataset)  // 根據 dataset 資料的 5 個元素,後面的城市,會執行 5 次
    .enter()    // 分析目前的<p> 及 dataset,如果資料比 DOM 元素多,就建立一個新的元素,傳給下面的 method
    .append("p")    // 將 enter 產生的空元素,加入一個 <p>
    .text("New paragraph!");    // 在 <p> 裡面加入 text

console.log(d3.selectAll("p")) 查詢所有的段落,並找到剛剛的 dataset

  • 調整 dataset 的方法

修改最後一行,可以知道如何在 callback function 中使用 dataset 裡面的元素。

d3.select("body").selectAll("p")
    .data(dataset)
    .enter()
    .append("p")
    .text(function(d) { return d; });

.style("color","red") 把段落文字變成紅色

d3.select("body").selectAll("p")
    .data(dataset)
    .enter()
    .append("p")
    .text(function(d) {
        return "I can count up to " + d;
    })
    .style("color", "red");

在 style 的處理中,也可以根據 dataset 的原始資料,進行條件判斷,產生不同的文字顏色

var dataset = [ 5, 10, 15, 20, 25 ];

d3.select("body").selectAll("p")
    .data(dataset)
    .enter()
    .append("p")
    .text(function(d) {
        return "I can count up to " + d;
    })
    .style("color", function(d) {
        if (d > 15) {
            return "red";
        } else {
            return "black";
        }
    });

根據資料繪製圖形

準備一個長條矩形的 css style,將 div 變成長條圖

        <style type="text/css">
        
            div.bar {
                display: inline-block;
                width: 20px;
                height: 75px;   /* 會被 d3 的 style 覆寫 */
                margin-right: 2px;
                background-color: teal;
            }
        
        </style>
        <script type="text/javascript">
        
            var dataset = [ 25, 7, 5, 26, 11 ];
            
            d3.select("body").selectAll("div")
                .data(dataset)
                .enter()
                .append("div")
                .attr("class", "bar")
                .style("height", function(d) {
                    var barHeight = d * 5;
                    return barHeight + "px";
                });
            
        </script>

以亂數的方式產生 dataset

        <script type="text/javascript">

            var dataset = [];                        //Initialize empty array
            for (var i = 0; i < 25; i++) {           //Loop 25 times
                //var newNumber = Math.random() * 30;  //New random number (0-30)
                var newNumber = Math.floor(Math.random() * 30);  //New random integer (0-29)
                dataset.push(newNumber);             //Add new number to array
            }

            d3.select("body").selectAll("div")
                .data(dataset)
                .enter()
                .append("div")
                .attr("class", "bar")
                .style("height", function(d) {
                    var barHeight = d * 5;
                    return barHeight + "px";
                });

        </script>

繪製 SVG 圖形

  • 類似剛剛的 dataset 產生長條圖的方法,用同樣的方式產生 svg
        <script type="text/javascript">
            //Width and height
            var w = 500;
            var h = 50;
            
            //Data
            var dataset = [ 5, 10, 15, 20, 25 ];
            
            // 產生 svg 區塊
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            // 在 svg 中產生 circle
            var circles = svg.selectAll("circle")
                .data(dataset)
                .enter()
                .append("circle");

            // 為每個 circles 設定屬性
            circles.attr("cx", function(d, i) {
                        return (i * 50) + 25;
                    })
                   .attr("cy", h/2)
                   .attr("r", function(d) {
                        return d;
                   })
                   .attr("fill", "yellow")
                   .attr("stroke", "orange")
                   .attr("stroke-width", function(d) {
                        return d/2;
                   });
        </script>
  • 以 svg 的方式產生長條圖
<script type="text/javascript">

            //Width and height
            var w = 500;
            var h = 100;
            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 ];
            
            //Create SVG element
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            // 產生 rect 方塊
            svg.selectAll("rect")
               .data(dataset)
               .enter()
               .append("rect")
               // 方塊的 x 位置,以 svg 圖片的寬度,跟 dataset 數量計算
               .attr("x", function(d, i) {
                    return i * (w / dataset.length);
               })
               // 方塊的 y 位置,(x,y) 座標點的計算是由左上角開始的,因此 y 的位置要由 svg 高度 扣掉資料的數值來決定, 資料的 4 倍可以讓圖形的相對差距變大,讓資料的差異在圖形的表現上更大
               .attr("y", function(d) {
                    return h - (d * 4);
               })
               // 矩形的寬度由 svg 寬度跟 dataset 數量決定,badPadding 是不同矩形之間的空白
               .attr("width", w / dataset.length - barPadding)
               // 高度由 dataset 數值放大 4 倍決定
               .attr("height", function(d) {
                    return d * 4;
               })
               // 以 rgb 動態將舉行填滿不同的顏色
               .attr("fill", function(d) {
                    return "rgb(0, 0, " + (d * 10) + ")";
               });

            // 在 svg 中產生 text 文字區塊,變成長條圖上面的文字標籤 
            svg.selectAll("text")
               .data(dataset)
               .enter()
               .append("text")
               .text(function(d) {
                    return d;
               })
               // 讓文字對齊中間
               .attr("text-anchor", "middle")
               // 設定 text 的 x 座標位置
               .attr("x", function(d, i) {
                    return i * (w / dataset.length) + (w / dataset.length - barPadding) / 2;
               })
               // 設定 text 的 y 座標位置
               .attr("y", function(d) {
                    return h - (d * 4) + 14;
               })
               // 因為字看不清楚
               .attr("font-family", "sans-serif")
               .attr("font-size", "11px")
               .attr("fill", "white");
        </script>

attr 可以只設定一個屬性或是將多個屬性組合在一起

svg.select("circle")
    .attr("cx", 0)
    .attr("cy", 0)
    .attr("fill", "red");
    
svg.select("circle")
    .attr({
        cx: 0,
        cy: 0,
        fill: "red"
    });

在多個屬性中,同時指定 callback functions

svg.selectAll("rect")
    .data(dataset)
    .enter()
    .append("rect")
    .attr({
        x: function(d, i) { return i * (w / dataset.length); },
        y: function(d) { return h - (d * 4); },
        width: w / dataset.length - barPadding,
        height: function(d) { return d * 4; },
        fill: function(d) { return "rgb(0, 0, " + (d * 10) + ")";}
    });

繪製散點圖 Scatter Plot

二維的資料,一般就先繪製 scatter plot

        <script type="text/javascript">

            //Width and height
            var w = 500;
            var h = 100;
            
            var dataset = [
                            [5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
                            [410, 12], [475, 44], [25, 67], [85, 21], [220, 88]
                          ];
    
            //Create SVG element
            var svg = d3.select("body")
                        .append("svg")
                        .attr("width", w)
                        .attr("height", h);

            // 產生 circle
            svg.selectAll("circle")
               .data(dataset)
               .enter()
               .append("circle")
               // 圓心的位置
               .attr("cx", function(d) {
                    return d[0];
               })
               .attr("cy", function(d) {
                    return d[1];
               })
               // 圓的半徑
               .attr("r", function(d) {
                    return Math.sqrt(h - d[1]);
               });

            // 加上標籤
            svg.selectAll("text")
               .data(dataset)
               .enter()
               .append("text")
               .text(function(d) {
                    return d[0] + "," + d[1];
               })
               // 標籤的位置放在圓心的地方
               .attr("x", function(d) {
                    return d[0];
               })
               .attr("y", function(d) {
                    return d[1];
               })
               // 設定標籤的文字 style
               .attr("font-family", "sans-serif")
               .attr("font-size", "11px")
               .attr("fill", "red");
            
        </script>

比例尺 scale

scale 是將輸入資料映射為另一組輸出範圍的函數。

一般來說,原始的資料數據不可能會剛好是圖表中的像素,就像是剛剛的例子一樣,長條圖的高度,是由原始資料計算出來的。這個資料轉換的過程, 就是 scale。

對於線性比例尺來說,概念就像是 normalization 一樣。D3 可以先將原始資料映射到 scale.domain([100, 500]) 的值域,也就是根據值域進行 normalization,然後以 scale.range([10, 350]) 將 normalized 之後的值,對應到 range 的輸出範圍。

在 D3 v3 版是 d3.scale.linear() ,在 v4 版是 d3.scaleLinear()

var scale = d3.scaleLinear().domain([100, 500]).range([10, 350]);

scale(100); // -> 10
scale(300); // -> 180
scale(500); // -> 350
// [100, 300, 500] -> [10, 180, 350]
<script type="text/javascript">

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

            var dataset = [
                            [5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
                            [410, 12], [475, 44], [25, 67], [85, 21], [220, 88]
                          ];

            //Create scale functions
            var xScale = d3.scaleLinear()
                            // 以 d3.max 取得 dataset d[0] 的 max, normalized 為 0 ~ max(d[0])
                             .domain([0, d3.max(dataset, function(d) { return d[0]; })])
                            // 映射到 0 ~ w
                             .range([0, w]);

            // 映射的範圍是相反的 (h,0)
            var yScale = d3.scaleLinear()
                                 .domain([0, d3.max(dataset, function(d) { return d[1]; })])
                                 .range([h, 0]);

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

            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", function(d) {
                    return Math.sqrt(h - d[1]);
               });

            svg.selectAll("text")
               .data(dataset)
               .enter()
               .append("text")
               .text(function(d) {
                    return d[0] + "," + d[1];
               })
               .attr("x", function(d) {
                    return xScale(d[0]);
               })
               .attr("y", function(d) {
                    return yScale(d[1]);
               })
               .attr("font-family", "sans-serif")
               .attr("font-size", "11px")
               .attr("fill", "red");
        </script>

因為接近外框的圓形被切掉了一部分,加上 padding=20,讓 svg 保留一部分外框。

            //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]);

將圓形的半徑也使用 scale 進行映射到 [2,5]

            var rScale = d3.scaleLinear()
                                 .domain([0, d3.max(dataset, function(d) { return d[1]; })])
                                 .range([2, 5]);

所有的數值都是動態計算的,如果把 svg 放大,也可以讓 dataset 依照同樣的方式,利用到 svg 的整個範圍。

<script type="text/javascript">

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

            var dataset = [
                            [5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
                            [410, 12], [475, 44], [25, 67], [85, 21], [220, 88],
                            [600, 150]
                          ];

            //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]);

            var rScale = d3.scaleLinear()
                                 .domain([0, d3.max(dataset, function(d) { return d[1]; })])
                                 .range([2, 5]);

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

            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", function(d) {
                    return rScale(d[1]);
               });

            svg.selectAll("text")
               .data(dataset)
               .enter()
               .append("text")
               .text(function(d) {
                    return d[0] + "," + d[1];
               })
               .attr("x", function(d) {
                    return xScale(d[0]);
               })
               .attr("y", function(d) {
                    return yScale(d[1]);
               })
               .attr("font-family", "sans-serif")
               .attr("font-size", "11px")
               .attr("fill", "red");

        </script>

d3.scaleLinear() 的其他常用 methods

  1. nice() : 在映射到 range() 時,把兩端的值擴展到最接近的短數值,ex: [0.20147987687960267, 0.996679553296417] -> [0.2, 1]

  2. rangeRound() : 取代 range,會將比例尺輸出為最接近的整數

  3. clamp() : scale 預設可以傳回超過範圍之外的值,如果加上 clamp(true),會讓超過範圍的數值,變成範圍的最高或最低值

其他的比例尺

  1. sqrt : 平方根比例尺

  2. pow : 冪比例尺,指數變化的 dataset

  3. log : 對數比例尺

  4. quantize : 輸出範圍為獨立的值的線性比例尺,適合把資料分類的情形

  5. ordinal : 使用非定量值(ex: 類別名稱) 作為輸出的序數比例尺,非常適合比較蘋果和橘子

  6. d3.scale.category10() d3.scale.category20() d3.scale.category20b() d3.scale.category20c() : 能夠輸出10到20種類別顏色的預設序數比例尺

  7. d3.time.scale() : 針對日期和時間值的一個比例尺方法,可以對日期刻度進行特殊處理

軸 x & y axis

呼叫 axis method 不會有回傳值,而是產生與 axis 相關的可見元素,例如 軸線、標籤與刻度。axis method 只適用於 svg 圖形。

  • axis 在 v3 跟 v4 的差異
// v3: Define X axis
var xAxis = d3.svg.axis()
                  .scale(xScale)
                  .orient("bottom");

// v4: Define X axis
var xAxis = d3.axisBottom()
                  .scale(xScale);
  • 產生 x axis 的方法,先以 xScale 比例尺產生 xAxis,然後在 svg 後面加上 g 元素,並呼叫 call(xAxis)。 g 是個 grouping 元素,有兩種用途 (1) 包含其他元素 (2) 對整個分組應用進行變換
            //Define X axis
            var xAxis = d3.axisBottom()
                              .scale(xScale);
                              
            //Create X axis
            svg.append("g")
                .call(xAxis);

也可以合併成一行

            //Create X axis
            svg.append("g")
                .call(d3.axisBottom().scale(xScale));

上面這個產生的 x 軸,是出現在圖形的上方,如果要換到下面,要利用平移 transform 移動

            //Define X axis
            var xAxis = d3.axisBottom()
                              .scale(xScale);
                              
           svg.append("g")
                .attr("class", "axis")
                .attr("transform", "translate(0," + (h - padding) + ")")
                .call(xAxis);

可以為 axis 利用 css 進行視覺調整

        <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>
  • tick 刻度

D3 會自動根據 scale 計算 ticks,也可以呼叫 ticks(5) 改為 5 個刻度線,但實際上 tick 只是個參考值,不是絕對值,D3 會自動調整為最適當的又接近 tick 數值的刻度數量。

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

為了讓 y 放在圖形的左邊,並產生 padding,還是需要 transform 進行平移

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

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

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

            //Create Y axis
            svg.append("g")
                .attr("class", "axis")
                .attr("transform", "translate(" + padding + ",0)")
                .call(yAxis);
  • 格式化軸刻度

".1%" 就是將 200 顯示為 200.0%

            var formatAsPercentage = d3.format(".1%");

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

            //Define Y axis
            var yAxis = d3.axisLeft()
                              .scale(yScale)
                              .ticks(5)
                              .tickFormat(formatAsPercentage);
  • 完整的軸線處理程式碼
        <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>
        
        <script type="text/javascript">

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

            /*
            //Static dataset
            var dataset = [
                            [5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
                            [410, 12], [475, 44], [25, 67], [85, 21], [220, 88],
                            [600, 150]
                          ];
            */

            //Dynamic, random dataset
            var dataset = [];                   //Initialize empty array
            var numDataPoints = 50;             //Number of dummy data points to create
            var xRange = Math.random() * 1000;  //Max range of new x values
            var yRange = Math.random() * 1000;  //Max range of new y values
            for (var i = 0; i < numDataPoints; i++) {                   //Loop numDataPoints times
                var newNumber1 = Math.floor(Math.random() * xRange);    //New random integer
                var newNumber2 = Math.floor(Math.random() * yRange);    //New random integer
                dataset.push([newNumber1, newNumber2]);                 //Add new number to array
            }

            //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]);

            var rScale = d3.scaleLinear()
                                 .domain([0, d3.max(dataset, function(d) { return d[1]; })])
                                 .range([2, 5]);

            //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", function(d) {
                    return rScale(d[1]);
               });

            /*
            //Create labels
            svg.selectAll("text")
               .data(dataset)
               .enter()
               .append("text")
               .text(function(d) {
                    return d[0] + "," + d[1];
               })
               .attr("x", function(d) {
                    return xScale(d[0]);
               })
               .attr("y", function(d) {
                    return yScale(d[1]);
               })
               .attr("font-family", "sans-serif")
               .attr("font-size", "11px")
               .attr("fill", "red");
            */

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

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

        </script>

序數比例尺

序數就是有固定順序的一個數列,ex: 週一、週二、週三..,新生、大二、大三、大四

scaleBand 跟 scaleLinear 不同,使用的是離散的數據資料。

domain 用來指定輸入的值域

.domain([" 新生 ", " 大二 ", " 大三 ", " 大四 "])

.domain([0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
    10, 11, 12, 13, 14, 15, 16, 17, 18, 19])

.domain(d3.range(20))

.domain(d3.range(dataset.length))

range 用來做映射,前面是映射的範圍,第二個參數指定間距

.range([0,w], 0.2)

D3 的序數比例尺 ordinal,在 v3 跟 v4 有些 API 呼叫的差異。

// v3
var xScale = d3.scale.ordinal()
                .domain(d3.range(dataset.length))
                .rangeRoundBands([0, w], 0.05);
                
// v4
var xScale = d3.scaleBand()
                .domain(d3.range(dataset.length))
                .range([0, w], 0.05);
// v3
xScale.rangeBand()

// v4
xScale.bandwidth()
        <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.scale.ordinal()
            //              .domain(d3.range(dataset.length))
            //              .rangeRoundBands([0, w], 0.05);

            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) {
                    // xScale(i) 是傳回 index 為 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>

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

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