2006-2024 台灣房屋成交價格
// html
<div class="housePriceLineChart"></div>
// js
引入day.js函式庫
<script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.7/dayjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.11.7/locale/zh-tw.min.js"></script>
// 中華年份改西元
const TWDateToADDate = (date) => {
// 年份轉換
date = date.replace(/\d{3}/, (match) => String(+match + 1911));
// 季度換為每季第一天
const seasonDates = {
Q1: "-01-01",
Q2: "-04-01",
Q3: "-07-01",
Q4: "-10-01",
};
const season = date.match(/Q\d/)[0];
date = date.replace(season, seasonDates[season]);
return new Date(date);
};
const housePriceLineChart = async () => {
// 設定 SVG
const width = parseInt(
d3.select(".housePriceLineChart").style("width")
),
height = 500,
margin = { top:20, bottom:90, right:20, left:60 };
const svg = d3
.select(".housePriceLineChart")
.append("svg")
.attr("width", width)
.attr("height", height);
// 取資料
const res = await d3.csv("./data/U96年-113年房價統計資訊整合結果.csv");
console.log(res)
const data = res.map((i) => {
i["時間"] = TWDateToADDate(i["時間"]);
return i;
});
console.log(data)
// map 資料集
const xData = data.map((i) => i["時間"]);
const yData = data.map((i) => +i["買賣契約價格平均總價(不分建物類別)"]);
// Time Scale
// 設定要給 X 軸用的 scale 跟 axis
const xScale = d3
.scaleTime()
.domain(d3.extent(xData))
.range([margin.left, width - margin.right])
.nice();
// X軸
let tickNumber = window.innerWidth > 900 ? xData.length / 3 : 10;
const xAxis = d3
.axisBottom(xScale)
.ticks(tickNumber)
.tickFormat((d) => dayjs(d).format("YYYY/MM/DD"));
// 呼叫繪製x軸、調整x軸位置
const xAxisGroup = svg
.append("g")
.call(xAxis)
.style('font-size', '16px')
.attr("transform", `translate(0,${height - margin.bottom})`);
// X軸刻度位置調整
xAxisGroup.call((g) =>
g.selectAll(".tick text")
.style("transform", "rotate(-48deg)")
.attr("x", -50)
.attr("y", 6)
);
// Y 軸
const yScale = d3
.scaleLinear()
.domain(d3.extent(yData))
.range([height - margin.bottom, margin.top])
.nice();
const yAxis = d3.axisLeft(yScale).tickFormat((d) => `${d}萬`);
// 呼叫繪製y軸、調整y軸位置
const yAxisGroup = svg
.append("g")
.call(yAxis)
.style('font-size', '16px')
.attr("transform", `translate(${margin.left},0)`);
// 設定 path 的 d
const lineChart = d3
.line()
.x((d) => xScale(d["時間"]))
.y((d) => yScale(+d["買賣契約價格平均總價(不分建物類別)"]));
// 建立折線圖
svg.append("path")
.data(data)
.attr("d", lineChart(data))
.attr("fill", "none")
.attr("stroke", "#f68b47")
.attr("stroke-width", 1.5);
};
housePriceLineChart();
COVID 19 病例資料
// html
<div class="interactLineChart"></div>
// js
const interactLineChart = async () => {
const width = parseInt(
d3.select('.interactLineChart').style('width')
),
height = 500,
margin = 80;
const svg = d3.select('.interactLineChart')
.append('svg')
.attr('width', width)
.attr('height', height);
const res = await d3.csv('./data/2022-2023-covid19.csv');
console.log(res);
const data = res.filter(i=>i['發病年週']<'202301')
// map 資料集
xData = data.map((i) => parseInt(i['發病年週'].substring(4,6)));
yData = data.map((i) => parseInt(i['確定病例數']));
// 設定要給 X 軸用的 scale 跟 axis
const xScale = d3.scaleLinear()
.domain(d3.extent(xData))
.range([margin, width - margin])
.nice();
const xAxis = d3.axisBottom(xScale)
.tickFormat(d=>d+'週')
// 呼叫繪製x軸、調整x軸位置
const xAxisGroup = svg.append("g")
.call(xAxis)
.style('font-size', '16px')
.attr("transform", `translate(0,${height - margin})`)
// 設定要給 Y 軸用的 scale 跟 axis
const yScale = d3.scaleLinear()
.domain([0, d3.max(yData)])
.range([height - margin, margin])
.nice()
const yAxis = d3.axisLeft(yScale).ticks(5)
// 呼叫繪製y軸、調整y軸位置
const yAxisGroup = svg
.append("g")
.call(yAxis)
.style('font-size', '16px')
.attr("transform", `translate(${margin},0)`)
// 開始建立折線圖
// 設定折線圖相關資料
const lineChart = d3.line()
.x((d) => xScale(parseInt(d['發病年週'].substring(4,6))))
.y((d) => yScale(parseInt(d['確定病例數'])))
svg.append('path')
.data(data)
.attr("d", lineChart(data))
.attr("fill", "none")
.attr("stroke", "#f68b47")
.attr("stroke-width", 1.5)
// 建立一個覆蓋svg的方形
svg.append('rect')
.style("fill", "transparent")
.style("pointer-events", "all")
.attr('width', width - margin)
.attr('height', height - margin)
.style('cursor', 'pointer')
.on('mouseover', mouseover)
.on('mousemove', mousemove)
.on('mouseout', mouseout);
// 建立沿著折線移動的圓點點
const focusDot = svg.append('g')
.append('circle')
.style("fill", "black")
.attr("stroke", "black")
.attr('r', 3)
.style("opacity", 0)
// 建立移動的資料標籤
const focusText = svg.append('g')
.append('text')
.style("opacity", 0)
.attr("text-anchor", "left")
.attr("alignment-baseline", "middle")
// 使用 d3.bisector() 找到滑鼠的 X 軸 index 值
const bisect = d3.bisector(d=>d['發病年週']).left;
// 設定滑鼠事件
function mouseover(){
focusDot.style("opacity", 1)
focusText.style("opacity",1)
}
function mousemove(){
// 把目前X的位置用xScale去換算
const x0 = xScale.invert(d3.pointer(event, this)[0])
// 由於我的X軸資料是擷取過的,這邊要整理並補零
const fixedX0 = parseInt(x0).toString().padStart(2,'0')
// 接著把擷取掉的2022補回來,因為data是帶入原本的資料
let i = bisect(data, '2022'+ fixedX0)
selectedData = data[i]
// 圓點
focusDot
// 換算到X軸位置時,一樣使用擷取過的資料,才能準確換算到正確位置
.attr("cx", xScale(selectedData['發病年週'].substring(4,6)))
.attr("cy", yScale(selectedData['確定病例數']))
focusText
.html('確診人數:' + selectedData['確定病例數'])
.attr("x", xScale(selectedData['發病年週'].substring(4,6))+15)
.attr("y", yScale(selectedData['確定病例數']))
}
function mouseout(){
focusDot.style("opacity", 0)
focusText.style("opacity", 0)
}
};
interactLineChart();
// html
<div class="definedLineChart"></div>
// js
const definedLineChart = ()=>{
const width = parseInt(
d3.select(".definedLineChart").style("width")
),
height = 500,
margin = 40;
const svg = d3.select('.definedLineChart')
.append('svg')
.attr('width', width)
.attr('height', height)
// 資料結構
const data = [{x:1, y:120},{x:2, y:355},{x:3, y:0},
{x:4, y:470},{x:5, y:19},{x:6, y:90},
{x:7, y:0},{x:8, y:220}];
const xData = data.map(d=>d.x);
const yData = data.map(d=>d.y);
// X 比例尺與軸線
const xScale = d3.scaleLinear()
.domain(d3.extent(xData))
.range([margin, width - margin]);
const xAxis = d3.axisBottom(xScale)
.ticks(8)
.tickFormat(d=>d + '月')
svg.append('g')
.call(xAxis)
.attr('transform', `translate(0, ${height-margin})`)
// Y 比例尺與軸線
const yScale = d3.scaleLinear()
.domain(d3.extent(yData))
.range([height - margin, margin])
.nice()
const yAxis = d3.axisLeft(yScale)
svg.append('g')
.call(yAxis)
.attr('transform', `translate(${margin}, 0)`)
// 建立折線圖 path 的 d 數值
// 用 line.defined 過濾掉是零的數值
const lineChart = d3.line()
.x((d) => xScale(d.x))
.y((d) => yScale(d.y))
.defined((d) => d.y >0)
// 建立折線
svg.append('g')
.append('path')
.data(data)
.attr("fill", "none")
.attr("stroke", "#f68b47")
.attr("stroke-width", 1.5)
.attr('d', lineChart(data))
// 把 d.y 大於零的資料拉出來,另外用這些資料去建立連線
let filteredData = data.filter(d => d.y > 0);
//也可以用 lineChart.defined()
// 建立 dashed 折線
svg.append('g')
.append('path')
.attr("fill", "none")
.attr("stroke", "#f68b47")
.attr("stroke-width", 1.5)
.attr("stroke-dasharray", '4,4')
.attr('d',lineChart(filteredData))
// 加上 tooltip
const tooltip = d3.select('.definedLineChart')
.append('div')
.style('position', 'absolute')
.style("opacity", 0)
.style("background-color", "white")
.style("border", "1px solid black")
.style("border-radius", "5px")
.style("padding", "5px")
// 加上圓點點
svg.append('g')
.selectAll('circle')
.data(filteredData)
.join('circle')
.attr('r', '5')
.attr('cx', d => xScale(d.x))
.attr('cy', d => yScale(d.y))
.attr('fill', 'white')
.attr('stroke', "#f68b47")
.attr('stroke-width', '2')
.style('cursor', 'pointer')
.on('mouseover', dotsMouseover)
.on('mouseleave', dotsMouseleave)
function dotsMouseover(d){
const pt = d3.pointer(event, svg.node())
tooltip.style("opacity", 1)
.style('left', (pt[0]+20) + 'px')
.style('top', (pt[1]) + 'px')
.html(`月份: ${d.target.__data__.x}月
`+
`數值: ${d.target.__data__.y}`)
// 加上 X-dashed 線
svg.append('line')
.attr('class', 'dashed-X')
.attr('x1', xScale(d.target.__data__.x))
.attr('y1', margin)
.attr('x2', xScale(d.target.__data__.x))
.attr('y2', height-margin)
.style('stroke', "#f68b47")
.style('stroke-dasharray', '4' )
// 加上 Y-dashed 線
svg.append('line')
.attr('class', 'dashed-Y')
.attr('x1', margin)
.attr('y1', yScale(d.target.__data__.y))
.attr('x2', width-margin)
.attr('y2', yScale(d.target.__data__.y))
.style('stroke', "#f68b47")
.style('stroke-dasharray', '4' )
}
function dotsMouseleave(){
tooltip.style('opacity', 0)
svg.selectAll('.dashed-X').remove()
svg.selectAll('.dashed-Y').remove()
}
};
definedLineChart();