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

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

2017年1月9日

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

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

2016年12月26日

Scala Play Application for Production

開發時 activator run 啟動的是 DEV mode,會一直檢查程式有沒有修改過,如果有修改,就會自動編譯並 reload,但這個功能會增加 overhead,在 PROD mode 就不需要了。另外 PROD 環境產生的 error page,也不需要像 DEV mode 一樣有太多錯誤的細節。

Production 相關設定

Play 需要一個 secret key 用來對 session cookie 簽章,還有內建的加密 utilities。

application.conf

play.crypto.secret = "newsecret"

該密碼預設為 "changeme",如果沒有修改這個密碼,在 PROD mode 就會發生錯誤,但在 DEV mode 就沒有檢查。


如果想要在 DEV 跟 PROD mode 使用不同的設定檔,可以在 conf 目錄中增加一個 prod-application.conf 新的設定檔,在啟動 PROD mode 時,加上設定檔的附加參數。

準備一個新的 prod-application.conf,內容為

include "application.conf"

play.crypto.secret = "newsecret"

另外同時準備一個 prod-logback.xml,新的 logback 設定檔,將 slick db sql statement 的 log 隱藏掉。


修改 build.sbt ,增加 JavaServerAppPackaging plugin,以及一些 production package 的設定

lazy val root = (project in file(".")).enablePlugins(PlayScala, JavaServerAppPackaging)

// production settings
maintainer := "charley <charley@maxkit.com.tw>"
packageSummary := "Play Slick Sample"
packageDescription := """A fun package description of our software,
  with multiple lines."""

// RPM SETTINGS
rpmVendor := "maxkit"
//rpmLicense := Some("BSD")
rpmChangelogFile := Some("changelog.txt")

Play 預設是使用 Netty,Netty 提供了一些參數可以調整,我們可以在 application.conf 中,調整這些參數。

play.server {

  # The server provider class name
  provider = "play.core.server.NettyServerProvider"

  netty {

    # The number of event loop threads. 0 means let Netty decide, which by default will select 2 times the number of
    # available processors.
    eventLoopThreads = 0

    # The maximum length of the initial line. This effectively restricts the maximum length of a URL that the server will
    # accept, the initial line consists of the method (3-7 characters), the URL, and the HTTP version (8 characters),
    # including typical whitespace, the maximum URL length will be this number - 18.
    maxInitialLineLength = 4096

    # The maximum length of the HTTP headers. The most common effect of this is a restriction in cookie length, including
    # number of cookies and size of cookie values.
    maxHeaderSize = 8192

    # The maximum length of body bytes that Netty will read into memory at a time.
    # This is used in many ways.  Note that this setting has no relation to HTTP chunked transfer encoding - Netty will
    # read "chunks", that is, byte buffers worth of content at a time and pass it to Play, regardless of whether the body
    # is using HTTP chunked transfer encoding.  A single HTTP chunk could span multiple Netty chunks if it exceeds this.
    # A body that is not HTTP chunked will span multiple Netty chunks if it exceeds this or if no content length is
    # specified. This only controls the maximum length of the Netty chunk byte buffers.
    maxChunkSize = 8192

    # Whether the Netty wire should be logged
    log.wire = false

    # The transport to use, either jdk or native.
    # Native socket transport has higher performance and produces less garbage but are only available on linux 
    transport = "jdk"

    # Netty options. Possible keys here are defined by:
    #
    # http://netty.io/4.0/api/io/netty/channel/ChannelOption.html
    #
    # Options that pertain to the listening server socket are defined at the top level, options for the sockets associated
    # with received client connections are prefixed with child.*
    option {

      # Set the size of the backlog of TCP connections.  The default and exact meaning of this parameter is JDK specific.
      # SO_BACKLOG = 100

      child {
        # Set whether connections should use TCP keep alive
        # SO_KEEPALIVE = false

        # Set whether the TCP no delay flag is set
        # TCP_NODELAY = false
      }

    }

  }
}

activator 常用指令

ex: activator clean

  • clean: 刪除 target 目錄中的編譯結果
  • update: 下載的 libraries
  • compile: 編譯專案,產生到 target 目錄中
  • eclipse: 產生 eclipse 專案 project file,使用前要先執行 compile
  • run: 啟動 project,也可以用 activator "run 8888" 啟動到 TCP Port 8888
  • publish: 產生專案的 jar,發佈到 build.sbt 中設定的 repository
  • publish-local: 發佈到本機的 repository/local 目錄
  • stage: 產生 production 專案的 script,通常放在 target/universal/stage 目錄中

產生 stage project

activator stage

完整地產生新的 stage project 要執行

activator clean update compile stage

產生 production project tar.gz file

activator universal:packageZipTarball

PROD mode

以下兩種方式可以啟動 Production Mode

-Dconfig.resource 的部分,會在 class path 尋找設定檔 -Dconfig.file 則是依照 file system 尋找設定檔

-J-Xms512M -J-Xmx1G -J-server 這幾個是啟動 application 的 JVM 參數

target/universal/stage/bin/test6 -Dconfig.resource=prod-application.conf -Dlogger.resource=prod-logback.xml -Dhttp.port=9000 -J-Xms512M -J-Xmx1G -J-server

target/universal/stage/bin/test6 -Dconfig.file=conf/prod-application.conf -Dlogger.file=conf/prod-logback.xml -Dhttp.port=9000 -J-Xms512M -J-Xmx1G -J-server

啟動後,會在 target/universal/stage/RUNNING_PID 產生 process ID 的檔案

如果希望在背景執行 application,就要搭配 nohup 啟動 server

nohup target/universal/stage/bin/test6 -Dconfig.resource=prod-application.conf -Dlogger.resource=prod-logback.xml -Dhttp.port=9000 -J-Xms512M -J-Xmx1G -J-server < /dev/null > /dev/null 2>&1 &

可以用 kill 的方式關掉背景執行的 server

kill -SIGTERM `cat target/universal/stage/RUNNING_PID`

kill -SIGKILL `cat target/universal/stage/RUNNING_PID`

start.sh stop.sh scripts

為了方便,我們把上面的啟動方式,做成一個 script,放在 conf 目錄中,這樣就會在 activator stage 時,同時複製到 target/universal/stage

conf/start.sh

#!/bin/bash

#export JAVA_OPTS=""
#export JAVA_OPTS="$JAVA_OPTS -Xms512M -Xmx1G -server"

console() {
    target/universal/stage/bin/test6 -Dconfig.resource=prod-application.conf -Dlogger.resource=prod-logback.xml -Dhttp.port=9000 -J-Xms512M -J-Xmx1G -J-server
}

server() {
    nohup target/universal/stage/bin/test6 -Dconfig.resource=prod-application.conf -Dlogger.resource=prod-logback.xml -Dhttp.port=9000 -J-Xms512M -J-Xmx1G -J-server < /dev/null > /dev/null 2>&1 &
}

case "$1" in

console)
    console
;;
server)
    server
;;
*)
    echo "Usage: $0 {console|server}"
;;
esac

exit 0

conf/stop.sh

#!/bin/bash

kill -SIGTERM `cat target/universal/stage/RUNNING_PID`

# kill -SIGKILL `cat target/universal/stage/RUNNING_PID`

然後就能用這樣的方式,啟動或停止 server

target/universal/stage/conf/start.sh server
target/universal/stage/conf/stop.sh

SSL

如果要讓 server 可以支援 HTTPS,我們需要做以下設定

Property Purpose Default Value
https.port https port number
https.keyStore 儲存 private key 及 certificate 的檔案路徑
https.keyStoreType key store type JavaKeyStore(JKS)
https.keyStorePassword password blank password
https.keyStoreAlgorithm key store algorithm platform's default algorithm

也可以自己實作 SSLEngine,參考 Configuring HTTPS,製作新的 class CustomSSLEngineProvider(appProvider: ApplicationProvider) extends SSLEngineProvider 。

References

Mastering Play Framework for Scala

The Application Secret

第三章 : Play建置部署與常用指令

overriding-configuration

Start and stop a Scala application in production