D3.js Part 2

讀取外部檔案

D3.js 提供了多個讀取外部檔案的函式。

D3 Fetches (github.com)

d3.csv

可以使用 d3.csv 讀取 .csv 檔案,以以下csv內容為例

csv:example.csv
Year,Grade,Gender,Height
1998,Gray,male,180
1999,May,female,170

需注意新舊版本的讀檔方式不同

以下的寫法為舊版寫法,在新版不會一次讀入完整的檔案

d3.csv('example.csv', (data) => {
    console.log(data)
})

新版的 d3.js 應使用以下方式讀檔,才能讀入完整檔案的內容

d3.csv('example.csv').then((data) => {
    console.log(data)
})

讀檔的結果如下

[
    {
        "Year": "1998",
        "Grade": "Gray",
        "Gender": "male",
        "Height": "180"
    },
    {
        "Year": "1999",
        "Grade": "May",
        "Gender": "female",
        "Height": "170"
    }
]

可參考附檔 load_file.html

D3讀檔為非同步函式

D3的讀檔為非同步函式,因此建議所有需要用到讀入的資料的程式碼都寫在 then() 裡面

讀取多個檔案

D3 沒有提供讀入多個檔案的函式,因此若要一次讀入多個檔案,需要使用 JavaScript 的內建函式 Promise.all()

Promise.all([
    d3.csv("example.csv"),
    d3.tsv("example.tsv")
]).then(function(data) {
    //do something...
});
In [ ]:
%%html
<script src="https://d3js.org/d3.v6.min.js"></script>

增加與減少資料

上次介紹的 enter()exit() 可以用於增加與減少資料

D3 Joining Data (github.com)

In [ ]:
%%html
<script>
    let data = [1535, 3081, 2494, 9078, 9843, 6856, 234, 529, 6729, 2321]
    let data2 = [1535, 3081, 2494, 9078, 9843]
</script>
In [ ]:
%%html

<div id='canvas'></div>

<script>
    let svg = d3.select('#canvas').append('svg')
        .style('height', '220') //將高度設為200

    let rects = svg.selectAll("rect")
        .data(data)
        .enter()
        .append("rect")
        .attr("x", (d, i) => {
            return i * 10 + 10
        })
        .attr("y", 10)
        .attr('height', 20)
        .attr('width', 5)
        .style("fill", "red")

    let xScale = d3.scaleLinear()
        .domain([0, data.length])
        .range([0, 200])
    let yScale = d3.scaleLinear()
        .domain([0, 10000])
        .range([100, 0])

    rects.attr("x", (d, i) => xScale(i) + 50)
        .attr("y", (d, i) => yScale(d) + 100)
        .attr('height', (d, i) => 100 - yScale(d))
        .attr('width', 15)

    let xAxis = d3.axisBottom(xScale)
    let yAxis = d3.axisLeft(yScale)

    svg.append('g').attr('class', 'axis')
        .attr('transform', 'translate(50, 100)')
        .call(yAxis)

    svg.append('g').attr('class', 'axis')
        .attr('transform', 'translate(50, 200)')
        .call(xAxis)
</script>

接著我們用 data() 讀入新的資料,並用 exit()remove() 刪除多的資料

In [ ]:
%%html
<script>
    rects.data(data2)
        .exit()
        .remove()
</script>

接著我們可以再用 data() 讀入原本的資料,並將資料增加到原本的數量

In [ ]:
%%html
<script>
    let newRects = svg.selectAll("rect").data(data)
        .enter()
        .append('rect')
        .attr("x", (d, i) => xScale(i) + 50)
        .attr("y", (d, i) => yScale(d) + 100)
        .attr('height', (d, i) => 100 - yScale(d))
        .attr('width', 15)
        .style("fill", "blue")
</script>

如果我們要將新舊資料一併處理,可以使用 merge()

In [ ]:
%%html
<script>
    newRects.merge(rects)
        .style('fill', 'green')
</script>

Mouse Events

D3 提供了多個 mouse event 的函式以方便製作互動圖表,透過 d3.on() 函式可以在物件中加入 mouse event

D3 Handling Events (github.com)

d3.on()

透過 d3.on(event, callback) 可以在物件 (d3.selection) 中加入 mouse event,其中常見的 event 有下列幾種

'mousedown', 'mouseup', 'click', 'dblclick'

以上三種 event 分別定義滑鼠按下、鬆開、按一下以及按兩下的動作。見以下範例:

In [ ]:
%%html
<div id='canvas2'></div>

<script>
let svg1 = d3.select('#canvas2').append('svg')
            .style('width', '500')
            .style('height', '130')

svg1.append("rect")
        .attr("x", 10)
        .attr("y", 10)
        .attr('width', 100).attr('height', 100)
        .style("fill", "blue")
        .on('click', function(){
            if(d3.select(this).style('fill') == 'red'){
                d3.select(this).style('fill', 'blue')
            }else{
                d3.select(this).style('fill', 'red')
            }
        })

svg1.append("rect")
        .attr("x", 120)
        .attr("y", 10)
        .attr('width', 100).attr('height', 100)
        .style("fill", "yellow")
        .on('mousedown', function(){
            d3.select(this).style('fill', 'green')
        })
        .on('mouseup', function(){
            d3.select(this).style('fill', 'yellow')
        })
</script>

JavaScript 的函式

JavaScript 的函式 (function) 有分兩種:

  • function(){}
  • () => {}

其中 function(){} 的匿名函式中的 this 指向呼叫該函式的物件 (每個函式有自己的 this),
() => {} 則沒有自己的 this

The Difference Between Regular Functions and Arrow Functions (betterprogramming.pub)

'mouseover', 'mousemove', 'mouseout'

以上三種 event 分別定義滑鼠進入,在物件中移動及離開物件,見以下範例:

In [ ]:
%%html
<div id='canvas3'></div>

<script>
let svg2 = d3.select('#canvas3').append('svg')
            .style('width', '500')
            .style('height', '130')

svg2.append("rect")
        .attr("x", 10)
        .attr("y", 10)
        .attr('width', 100).attr('height', 100)
        .style("fill", "yellow")
        .on('mouseover', function(){
            d3.select(this).style('fill', 'green')
        })
        .on('mouseout', function(){
            d3.select(this).style('fill', 'yellow')
        })
</script>

過場視覺效果

d3.transition

d3-transition (github)
使用 d3.transition() 可以將物件的變換過程加上視覺效果
配合 d3.duration(value) 以及 d3.delay(value) 可以調整過場的時間和開始時間(單位為毫秒),見以下範例

In [ ]:
%%html
<div id='canvas4'></div>

<script>
let svg3 = d3.select('#canvas4').append('svg')
            .style('width', '500')
            .style('height', '360')

svg3.append("rect")
        .attr("x", 10)
        .attr("y", 10)
        .attr('width', 100).attr('height', 100)
        .style("fill", "green")
        .on('mouseover', function(){
            d3.select(this).transition().attr('width', 300)
        })
        .on('mouseout', function(){
            d3.select(this).transition().attr('width', 100)
        })

        
svg3.append("rect")
        .attr("x", 10)
        .attr("y", 120)
        .attr('width', 100).attr('height', 100)
        .style("fill", "blue")
        .on('mouseover', function(){
            d3.select(this).transition()
                .duration(2000)
                .attr('width', 400)
                .style('fill', 'red')
        })
        .on('mouseout', function(){
            d3.select(this).transition()
                .duration(2000)
                .attr('width', 100)
                .style('fill', 'blue')
        })

svg3.append("rect")
        .attr("x", 10)
        .attr("y", 230)
        .attr('width', 100).attr('height', 100)
        .style("fill", "blue")
        .on('mouseover', function(){
            d3.select(this).transition()
                .delay(1000)
                .duration(100)
                .attr('width', 200)
                .style('fill', 'red')
        })
        .on('mouseout', function(){
            d3.select(this).transition()
                .duration(100)
                .attr('width', 100)
                .style('fill', 'blue')
        })
</script>

製作互動圖表

結合以上的幾個功能,就能作出富有互動性的圖表

In [ ]:
%%html

<button id='changeData'>Change Data</button>
<div id='canvas5'></div>

<script>
let svg4 = d3.select('#canvas5').append('svg')
    .style('height', '220') //將高度設為200

let xScale2 = d3.scaleLinear()
    .domain([0, data.length])
    .range([0, 200])
let yScale2 = d3.scaleLinear()
    .domain([0, 10000])
    .range([100, 0])    

let rects2 = svg4.selectAll("rect")
    .data(data)
    .enter()
    .append("rect")
    .attr("x", (d, i) => xScale2(i) + 50)
    .attr("y", (d, i) => yScale2(d) + 100)
    .attr('height', (d, i) => 100 - yScale2(d))
    .attr('width', 15)
    .style("fill", "red")

let xAxis2 = d3.axisBottom(xScale2)
let yAxis2 = d3.axisLeft(yScale2)

let yAxisSvg = svg4.append('g').attr('class', 'axis')
    .attr('transform', 'translate(50, 100)')
    .call(yAxis2)

svg4.append('g').attr('class', 'axis')
    .attr('transform', 'translate(50, 200)')
    .call(xAxis2)
</script>

生成新的資料

隨機生成一筆同樣為十筆的資料,但將數值的範圍設在 [0, 20000] 之間

In [ ]:
%%html
<p id='console'></p>
<script>

let data3 = new Array(10).fill(0).map(() => ~~(Math.random() * 20000))
d3.select('#console').html(data3.toString())

</script>

設定新的座標

接著因為 Y 軸的範圍跟原本的不同,因此我們要定義新的 scale 以及 axis

In [ ]:
%%html
<script>

let yScaleNew = d3.scaleLinear()
    .domain([0, 20000])
    .range([100, 0])

let yAxisNew = d3.axisLeft(yScaleNew)

</script>

接著把新的資料套用到圖表中

需注意使用 transition() 之後不能直接使用 on()

In [ ]:
%%html
<script>
d3.select('#changeData').on('click', function(){
    data3 = new Array(10).fill(0).map(() => ~~(Math.random() * 20000))
    
    yScaleNew = d3.scaleLinear()
        .domain([0, d3.max(data3)])
        .range([100, 0])
        .nice()
    
    let yAxisNew = d3.axisLeft(yScaleNew)
    
    rects2.data(data3).transition()
        .attr('id', 'bar')
        .attr("x", (d, i) => xScale2(i) + 50)
        .attr("y", (d, i) => yScaleNew(d) + 100)
        .attr('height', (d, i) => 100 - yScaleNew(d))
        .attr('width', 15)
        .style("fill", "blue")
    
    rects2.on('mouseover', function(d, i){
            d3.selectAll('#bar').filter(function(d, j){
                    return !(i == j)
                })
                .transition()
                .style('fill', 'black')
            d3.select(this).transition().style('fill', 'yellow')
        })
        .on('mouseout', function(){
            d3.selectAll('#bar').transition().style('fill', 'blue')
        })

    yAxisSvg.transition().call(yAxisNew)
})
</script>

使用外部 API

現在許多網站都有提供 API 可以取用,能夠省去爬取資料的步驟

以 FinMind 的台股即時資料為例

該資料集以 JSON 格式為主,因此我們使用 d3.json() 取得資料

d3.json('https://api.finmindtrade.com/api/v4/data?dataset=TaiwanStockPriceTick&data_id=2330&streaming_all_data=True')
    .then((data) => {
        console.log(data)
    })

我們可以看到在得到的資料中,有 "time" 和 "deal_price" 兩個欄位,這是我們這次需要的資料,接下來我們就可以利用這兩個資料繪製圖表

In [ ]:
%%html
<div id='canvas6'></div>
<script>

let margin = {'top':10, 'bottom': 30, 'left': 60, 'right': 30}
let width = 500 - margin.left - margin.right
let height = 400 - margin.top - margin.bottom
let count = 5000

d3.json('https://api.finmindtrade.com/api/v4/data?dataset=TaiwanStockPriceTick&data_id=2330&streaming_all_data=True')
    .then((d) => {
        let data = d.data.slice(-1 * count).map((x) => {
            let time = x.Time.split(':')
            x.time = new Date().setHours(time[0], time[1], time[2].split('.')[0])
            return x
        }).filter((x, i) => i%~~(count/100) == 0)
        console.log(data)
        let svg = d3.select('#canvas6').append('svg')
            .attr('width', 500)
            .attr('height', 400)
        let scalarX = d3.scaleTime()
            .domain(d3.extent(data, function(x){return x.time}))
            .range([0, width])
        let scalarY = d3.scaleLinear()
            .domain(d3.extent(data, function(x){return x.deal_price}))
            .range([height, 0])
            .nice()
        svg.append('g').attr("transform", "translate(" + margin.left + "," + (height + margin.top) + ")")
            .call(d3.axisBottom(scalarX))
        svg.append('g').attr('transform', 'translate('+ margin.left + ', ' + margin.top + ')').call(d3.axisLeft(scalarY))
    svg.append("path")
        .datum(data)
        .attr("fill", "none")
        .attr("stroke", "steelblue")
        .attr("stroke-width", 1.5)
        .attr("d", d3.line()
            .interpolate()
            .x(function(d) { return scalarX(d.time) + margin.left })
            .y(function(d) { return scalarY(d.deal_price) + margin.top})
        )
    })

</script>
In [ ]: