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的讀檔為非同步函式,因此建議所有需要用到讀入的資料的程式碼都寫在 then()
裡面
D3 沒有提供讀入多個檔案的函式,因此若要一次讀入多個檔案,需要使用 JavaScript 的內建函式 Promise.all()
Promise.all([
d3.csv("example.csv"),
d3.tsv("example.tsv")
]).then(function(data) {
//do something...
});
%%html
<script src="https://d3js.org/d3.v6.min.js"></script>
%%html
<script>
let data = [1535, 3081, 2494, 9078, 9843, 6856, 234, 529, 6729, 2321]
let data2 = [1535, 3081, 2494, 9078, 9843]
</script>
%%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()
刪除多的資料
%%html
<script>
rects.data(data2)
.exit()
.remove()
</script>
接著我們可以再用 data()
讀入原本的資料,並將資料增加到原本的數量
%%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()
%%html
<script>
newRects.merge(rects)
.style('fill', 'green')
</script>
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 分別定義滑鼠按下、鬆開、按一下以及按兩下的動作。見以下範例:
%%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 的函式 (function) 有分兩種:
function(){}
() => {}
其中 function(){}
的匿名函式中的 this
指向呼叫該函式的物件 (每個函式有自己的 this),
而 () => {}
則沒有自己的 this
The Difference Between Regular Functions and Arrow Functions (betterprogramming.pub)
'mouseover'
, 'mousemove'
, 'mouseout'
¶以上三種 event 分別定義滑鼠進入,在物件中移動及離開物件,見以下範例:
%%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)
可以調整過場的時間和開始時間(單位為毫秒),見以下範例
%%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>
結合以上的幾個功能,就能作出富有互動性的圖表
%%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] 之間
%%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
%%html
<script>
let yScaleNew = d3.scaleLinear()
.domain([0, 20000])
.range([100, 0])
let yAxisNew = d3.axisLeft(yScaleNew)
</script>
接著把新的資料套用到圖表中
需注意使用 transition()
之後不能直接使用 on()
%%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 可以取用,能夠省去爬取資料的步驟
該資料集以 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" 兩個欄位,這是我們這次需要的資料,接下來我們就可以利用這兩個資料繪製圖表
%%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>