折線圖 line chart

基本折線圖

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();
          
        
進階折線圖-line.defined() 過濾資料折線圖
          
            // 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();