Developer's Development

[HTML] Chart.js stacked bar chart 생성 본문

개발/HTML

[HTML] Chart.js stacked bar chart 생성

mylee99 2024. 5. 30. 15:48
Case

검색한 날짜 조건별(년, 월, 일) 데이터셋의 값을 누적하여 차트를 쌓인 형태로 표시하는 stacked bar chart 생성
데이터셋은 3개씩 2개의 차트로 표시

 

Reference

https://www.chartjs.org/docs/latest/

 

Chart.js | Chart.js

Chart.js Welcome to Chart.js! Why Chart.js Among many charting libraries (opens new window) for JavaScript application developers, Chart.js is currently the most popular one according to GitHub stars (opens new window) (~60,000) and npm downloads (opens ne

www.chartjs.org

 

HTML
<div id="canvasArea" class="canvasArea" th:if="${paging.listCnt > 0}">
  <canvas id="status1" style="width:40vw; height:40vh; margin-top:20px; display: inline-block;"></canvas>
  <canvas id="status2" style="width:40vw; height:40vh; margin-top:20px; display: inline-block;"></canvas>
</div>

 

Mapper(xml)

검색 날짜 조건(년, 월, 일)에 따라 GROUP BY 할 DATE_FORMAT을 다르게 설정

<sql id="searchWhere">
        <where>
                <if test='searchType == "year"'>
                        AND FIND_DTIME <![CDATA[>=]]> STR_TO_DATE( CONCAT( #{searchStartYear}, '-01' ), '%Y-%m' )
                        AND FIND_DTIME <![CDATA[<]]> DATE_ADD( LAST_DAY( STR_TO_DATE( CONCAT( #{searchEndYear}, '-12' ), '%Y-%m' ) ), INTERVAL 1 DAY )
                </if>
                <if test='searchType == "month"'>
                        AND FIND_DTIME <![CDATA[>=]]> STR_TO_DATE( #{searchStartMonth}, '%Y-%m' )
                        AND FIND_DTIME <![CDATA[<]]> DATE_ADD( LAST_DAY( STR_TO_DATE( #{searchEndMonth}, '%Y-%m' ) ), INTERVAL 1 DAY )
                </if>
                <if test='searchType == "date"'>
                        AND FIND_DTIME <![CDATA[>=]]> STR_TO_DATE( #{searchStartDate}, '%Y-%m-%d' )
                        AND FIND_DTIME <![CDATA[<]]> DATE_ADD( STR_TO_DATE( #{searchEndDate}, '%Y-%m-%d' ), INTERVAL 1 DAY )
                </if>
        </where>
</sql>

SELECT
    <if test='searchType == "year"'>
        DATE_FORMAT(FIND_DTIME, '%Y') AS cDate
    </if>
    <if test='searchType == "month"'>
        DATE_FORMAT(FIND_DTIME, '%Y-%m') AS cDate
    </if>
    <if test='searchType == "date"'>
        DATE_FORMAT(FIND_DTIME, '%m-%d') AS cDate
    </if>
    , SUM(FIND_CNT) AS cSum
    , SUM(CASE WHEN RIGHT(FIND_CODE, 1) = '1' THEN FIND_CNT ELSE 0 END) AS cdata1
    , SUM(CASE WHEN RIGHT(FIND_CODE, 1) = '2' THEN FIND_CNT ELSE 0 END) AS cdata2
    , SUM(CASE WHEN RIGHT(FIND_CODE, 1) = '3' THEN FIND_CNT ELSE 0 END) AS cdata3
    , SUM(CASE WHEN RIGHT(FIND_CODE, 1) = '4' THEN FIND_CNT ELSE 0 END) AS cdata4
    , SUM(CASE WHEN RIGHT(FIND_CODE, 1) = '5' THEN FIND_CNT ELSE 0 END) AS cdata5
    , SUM(CASE WHEN RIGHT(FIND_CODE, 1) = '6' THEN FIND_CNT ELSE 0 END) AS cdata6
FROM FIND_TABLE
<include refid="searchWhere"/>
GROUP BY cDate
ORDER BY 1

 

JavaScript
// 차트데이터 생성
let listCnt = "[[${paging.listCnt}]]";
if( listCnt > 0 ) {
  const chartObj = [[${chart}]];        //백엔드에서 받은 데이터. [0:{'cdate': '05-20', 'cdata1': '10',...}, 1:{'cdate': '05-21', 'cdata1': '30',...}] 와 같은 형식으로 넘어옴.
  const chartJSON = JSON.stringify(chartObj, ['cdate', 'cdata1', 'cdata2', 'cdata3', 'cdata4', 'cdata5', 'cdata6']);
  const chartParse = JSON.parse(chartJSON);

  // 레이블 및 데이터 배열 초기화
  let dateString;
  let chartLabels = [];
  let cdata1 = [];
  let cdata2 = [];
  let cdata3 = [];
  let cdata4 = [];
  let cdata5 = [];
  let cdata6 = [];

  setData = function () {
    // 해당하는 날짜의 DB 데이터가 있으면 그 값, 없으면 0으로 값을 차트 데이터 배열에 넣음
    const foundIndex = chartParse.findIndex(item => item.cdate === dateString);
    if( foundIndex !== -1 ) {
      const foundData = chartParse[foundIndex];
      cdata1.push(parseInt(foundData.cdata1));
      cdata2.push(parseInt(foundData.cdata2));
      cdata3.push(parseInt(foundData.cdata3));
      cdata4.push(parseInt(foundData.cdata4));
      cdata5.push(parseInt(foundData.cdata5));
      cdata6.push(parseInt(foundData.cdata6));
    } else {
      cdata1.push(0);
      cdata2.push(0);
      cdata3.push(0);
      cdata4.push(0);
      cdata5.push(0);
      cdata6.push(0);
    }
  }

  //연도별 데이터
  if ( $dataForm.find("input[name='searchType']").val() == 'year' ) {
        const startYear = parseInt($dataForm.find("input[name='searchStartYear']").val());
        const endYear = parseInt($dataForm.find("input[name='searchEndYear']").val());

        for ( let year=startYear; year<=endYear; year++ ) {
                dateString = year.toString();
                chartLabels.push(dateString);
                setData();
        }
  }
  //월별 데이터
 else if( $dataForm.find("input[name='searchType']").val() == 'month') {
        const [startYear, startMonth] = $dataForm.find("input[name='searchStartMonth']").val().split('-').map(str => parseInt(str));
        const [endYear, endMonth] = $dataForm.find("input[name='searchEndMonth']").val().split('-').map(str => parseInt(str));

        for ( let year=startYear; year<=endYear; year++ ) {
                const startM = (year === startYear) ? startMonth : 1;
                const endM = (year === endYear) ? endMonth : 12;

                for ( let month=startM; month<=endM; month++ ) {
                        dateString = `${year}-${('0' + month).slice(-2)}`;
                        if ( month === 1 ) {    //연도가 바뀔 때(1월일 때) 해당 연도 표시
                                chartLabels.push([('0' + month).slice(-2), year + '년']);
                        } else {
                                chartLabels.push(('0' + month).slice(-2));
                        }
                        setData();
                }
        }
  }
  //일별 데이터
  else {
        const [startYear, startMonth, startDay] = $dataForm.find("input[name='searchStartDate']").val().split('-').map(str => parseInt(str));
        const [endYear, endMonth, endDay] = $dataForm.find("input[name='searchEndDate']").val().split('-').map(str => parseInt(str));

        let currentDate = new Date(startYear, startMonth - 1, startDay);
        const endDate = new Date(endYear, endMonth - 1, endDay);

        while ( currentDate <= endDate ) {
                const month = ('0' + (currentDate.getMonth() + 1)).slice(-2);
                const day = ('0' + currentDate.getDate()).slice(-2);

                dateString = `${month}-${day}`;
                if ( day === '01' ) {   //월이 바뀔 때(1일일 때) 해당 월 표시
                        chartLabels.push([day, month + '월']);
                } else {
                        chartLabels.push(day);
                }
                setData();

                currentDate.setDate(currentDate.getDate() + 1);
        }
  }

  const chart1 = $('#status1');
  const chart2 = $('#status2');

  // 차트 옵션 생성
  const option = {
    title: {
        display: true,
        fontSize: 15,
        fontFamily: 'Noto Sans KR'
    },
    responsive: false,
    scales: {
      xAxes: [{
        stacked: true,
        barThickness: $dataForm.find("input[name='searchType']").val() == 'date' ? 4 : 30,      //그래프가 많을 땐(일별 데이터일 땐) 두께 좁게
        gridLines: {
                display: false          //x축 grid 숨기기
        },
        ticks: {
                fontSize: 10,
                fontFamily: 'Noto Sans KR'
        }
      }],
      yAxes: [{
        stacked: true,
        scaleLabel: {
                 display: false 
        },
        ticks: {
          // 축 눈금에 천단위 쉼표 추가
          callback: function(value) {
            return value.toLocaleString();
          },
          fontSize: 10,
          fontFamily: 'Noto Sans KR'
        }
      }]
    },
    legend: {
      position: 'bottom',
      labels: {
        fontFamily: 'Noto Sans KR',
        fontSize: 12
      }
    },
    tooltips: {
      mode: 'index',            //같은 인덱스의 툴팁 한번에 표시
      intersect: false,
      callbacks: {
        // 숫자 데이터 천단위 표시 (데이터가 있을 때만)
        label: function(tooltipItem, data) {
          if (tooltipItem.yLabel > 0) {
            let label = data.datasets[tooltipItem.datasetIndex].label || '';
            if (label) {
              label += ': ';
            }
            label += tooltipItem.yLabel.toLocaleString();
            return label;
          }
        },
        // 합계 표시
        footer: function(data) {
          let total = 0;
          for (let i=0; i<data.length; i++) {
            total += data[i].yLabel;
          }
          return "합계: " + total.toLocaleString();
        }
      }
    }
  };

  // 차트 생성
  const result1 = new Chart(chart1, {
    type: 'bar',
    data: {
      labels: chartLabels,
      datasets: [
        {
          label: '데이터1',
          data: cdata1,
          borderColor: '#FFBF2E',
          backgroundColor: '#FFBF2E'
        },
        {
          label: '데이터2',
          data: cdata2,
          borderColor: '#FF9E28',
          backgroundColor: '#FF9E28'
        },
        {
          label: '데이터3',
          data: cdata3,
          borderColor: '#FE5A1C',
          backgroundColor: '#FE5A1C'
        }
      ]
    }, options : {      //두 차트의 모든 옵션을 동일하게 설정하고, 차트 제목만 다르게 설정
        ...option,
        title: {
                ...option.title,
                text: '첫번째 차트 제목'
        }
    }
  });

  const result2 = new Chart(chart2, {
    type: 'bar',
    data: {
      labels: chartLabels,
      datasets: [
        {
          label: '데이터4',
          data: cdata4,
          borderColor: '#d39d9d',
          backgroundColor: '#d39d9d'
        },
        {
          label: '데이터5',
          data: cdata5,
          borderColor: '#d77276',
          backgroundColor: '#d77276'
        },
        {
          label: '데이터6',
          data: cdata6,
          borderColor: '#d43d51',
          backgroundColor: '#d43d51'
        },
      ]
    }, options : {
        ...option,
        title: {
                ...option.title,
                text: '두번쨰 차트 제목'
        }
    }
  });
}



원하는 기능이 대부분 구현 가능한 라이브러리인 것 같다.
다음에 시간이 더 충분한 상황에서 차트 생성 업무를 하게 되면 더 다양한 기능을 접근해보고 싶기도

 

 

Comments