Developer's Development
[HTML] Chart.js stacked bar chart 생성 본문
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: '두번쨰 차트 제목'
}
}
});
}
원하는 기능이 대부분 구현 가능한 라이브러리인 것 같다.
다음에 시간이 더 충분한 상황에서 차트 생성 업무를 하게 되면 더 다양한 기능을 접근해보고 싶기도
'개발 > HTML' 카테고리의 다른 글
[HTML] smarteditor2 텍스트에디터 (jQuery, thymeleaf) (0) | 2024.08.16 |
---|---|
[HTML] Luckysheet.js 엑셀 미리보기 (1) | 2024.04.15 |
[HTML] docx-preview.js 워드 미리보기 (0) | 2024.03.13 |
[HTML] Sheet.js & xspreadsheet.js 엑셀 미리보기 (0) | 2024.02.05 |
[HTML] selectbox control (jQuery) (3) | 2023.11.22 |
Comments