본문으로 바로가기

차트를 표현해주는 라이브러리 중 Chart.js 사용에 대해 정리하는 글이다.
최종목표는 차트를 이미지화 하여 엑셀로 다운로드까지 하는 것이다.

Chart.js

Chart.js(Chart.js | Open source)는 오픈 소스이며 데이터를 다양한 차트로 시각화 해주는 Javascript 라이브러리 중 하나이다.
Sample Page를 살펴보면 차트의 기본인 Line, Pie, Doughnut, Bar 등이 있다. Chart.js Sample Page


Sample

Chart.js를 사용하려면 CDN이나 JS를 다운받아 Import 하면된다. Chart.js CDN by jsDelivr

<style>
    body {
        width: 400px;
        height: 400px;
        margin: auto;
        margin-top: 100px;
    }
</style>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.26.0/polyfill.js"></script>
<select onchange="changeChart(this.value)">
  <option value="line">라인</option>
  <option value="pie">파이</option>
  <option value="doughnut">도넛</option>
  <option value="bar">막대(세로)</option>
  <option value="horizontalBar">막대(가로)</option>
</select>
<canvas id="myChart" width="100" height="100"></canvas>
<script>
  let data = {
    labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
    datasets: [{
      label: '# of Votes',
      data: [12, 19, 3, 5, 2, 3],
      backgroundColor: [
        'rgba(255, 99, 132, 0.2)',
        'rgba(54, 162, 235, 0.2)',
        'rgba(255, 206, 86, 0.2)',
        'rgba(75, 192, 192, 0.2)',
        'rgba(153, 102, 255, 0.2)',
        'rgba(255, 159, 64, 0.2)'
      ],
      borderColor: [
        'rgba(255, 99, 132, 1)',
        'rgba(54, 162, 235, 1)',
        'rgba(255, 206, 86, 1)',
        'rgba(75, 192, 192, 1)',
        'rgba(153, 102, 255, 1)',
        'rgba(255, 159, 64, 1)'
      ],
      borderWidth: 1
    }]
  };

  let options = {
    scales: {
      yAxes: [{
        ticks: {
          beginAtZero: true
        }
      }]
    }
  };

  function changeChart(value) {
    myChart.destroy();

    myChart = new Chart(ctx, {
      type: value,
      data: data,
      options: options,
    });
  };
  const ctx = document.getElementById('myChart'); // getContext('2d') 를 하는 경우가 있는데 없어야 엑셀다운가능
  let myChart = new Chart(ctx, {
    type: 'line',
    data: data,
    options: options
  });
</script>

위 예제 소스를 실행하면 다음과 같이 Bar 형태 차트가 생성된다.

이 상태로 type 속성만 변경하면 다양하게 적용이 가능하다.

options 에 대해서는 Documents를 참고하여 적용한다.

legend: {
    display: false, // label 숨기기
},
title: {
    display: true, // title 표시
    text: '# title', // title 명
},
maintainAspectRatio: false, // 비율유지
showAllTooltips: false, // tooltip 항상표시
tooltips: {
    enabled: true, // tooltip 표시(기본은 마우스 hover)
},
plugins: {
    legend: true, // legend 표시
    outlabels: {} // pie, doughnut 때문에 외부 라이브러리 사용(tooltip을 항상 표시 하는 경우 겹쳐서)
},
layout: {
    padding: {
        left: 25,
        right: 25,
        top: 0,
        bottom: 0
    }
}

위는 사용해본 Options 목록이다.

만약 Line 사용 시 아래에 자동으로 채워지는 옵션을 끄고싶다면 다음을 추가한다.

datasets: [{
    fill: false,
    ...
}]

datasetsfill 옵션을 false로 수정하면된다.


Doughnut 이나 Pie를 사용해서 Tooltips를 항상 표시했더니 다음과 같은 현상이 생겼다.

// tooltip을 렌더링 이후 바로 표시하기 위해서는 다음 소스가 추가되어야함
Chart.plugins.register({
    beforeRender: function(chart) {
        if (chart.config.options.showAllTooltips) {
            // create an array of tooltips
            // we can't use the chart tooltip because there is only one tooltip
            // per chart
            chart.pluginTooltips = [];
            chart.config.data.datasets.forEach(function(dataset, i) {
                chart.getDatasetMeta(i).data.forEach(function(sector, j) {
                    chart.pluginTooltips.push(new Chart.Tooltip({
                        _chart: chart.chart,
                        _chartInstance: chart,
                        _data: chart.data,
                        _options: chart.options.tooltips,
                        _active: [sector]
                    }, chart));
                });
            });

            // turn off normal tooltips
            chart.options.tooltips.enabled = false;
        }
    },
    afterDraw: function(chart, easing) {
        if (chart.config.options.showAllTooltips) {
            // we don't want the permanent tooltips to animate, so don't do
            // anything till the animation runs atleast once
            if (!chart.allTooltipsOnce) {
                if (easing !== 1)
                    return;
                chart.allTooltipsOnce = true;
            }

            // turn on tooltips
            chart.options.tooltips.enabled = true;
            Chart.helpers.each(chart.pluginTooltips, function(tooltip) {
                tooltip.initialize();
                tooltip.update();
                // we don't actually need this since we are not animating
                // tooltips
                tooltip.pivot();
                tooltip.transition(easing).draw();
            });
            chart.options.tooltips.enabled = false;
        }
    }
});

아까 지정한 옵션들 중 showAllTooltipstrue로 수정하고 차트를 보면 다음과 같이 나온다.

물론 도넛이나 파이가 아니더라도 라인도 겹치게 표시된다. 여기서는 도넛이나 파이에서만 적용해보려고 한다.
라이브러리 중에 라벨만 외부로 표시해주는 플러그인을 추가한다.

<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-piechart-outlabels"></script>

그리고 아까 지정한 옵션들 중 pluginsoutlabels 안에 다음을 추가한다.

outlabels: {
    text: '%l \n %v',
    borderRadius: 15,
    color: 'black',
    stretch: 30,
    font: {
        resizable: true,
        minSize: 12,
        maxSize: 18
    },
    textAlign: "center"
}

해당 라이브러리의 문서는 링크를 참고하기 바란다.chartjs-plugin-outlabels

text의 기능은 다음과 같다.

  • %l: 라벨 데이터를 표시
  • %p: 비율을 표시
  • %v: 을 표시
  • \n: 개행을 표시

결과는 다음과 같다.

이제 파이나 도넛인 경우에 옵션 중 scales, showAllTooltips를 미사용하면 된다. scales는 그래프의 기본선을 말한다.

function chartUpdate(type, options, title) {
    if(type == "doughnut" || type == "pie") {
        delete options.scales;
        options.title.text = '';
        options.plugins.outlabels = {
                text: '%l \n %v',
                borderRadius: 15,
                color: 'black',
                stretch: 30,
                font: {
                    resizable: true,
                    minSize: 12,
                    maxSize: 18
                },
                textAlign: "center"
        };
        options.layout.padding.top = 50;
        options.layout.padding.bottom = 50;
    } else {
        options.title.text = title;
        options.scales = {
            yAxes: [{
                ticks: {
                    beginAtZero: true
                }
            }]
        }
        delete options.plugins.outlabels;
        options.layout.padding.top = 50;
        options.layout.padding.bottom = 50;
    }
}

위는 일부만 사용한 소스이며 changeChart 함수에서 다음을 추가하면 된다.

chartUpdate(value, options, '# title');

위, 아래의 간격은 padding 옵션을 통해 적절히 조절한다.
(파이나 도넛의 경우 타이틀을 표시하는 경우 겹칠 수 있으므로 사용하지 않음)


Export to Excel

위의 예제를 통해 만들어진 차트를 엑셀로 다운로드 해보자.
그러기 위해서는 또 라이브러리를 추가해야한다. exceljs 라는 라이브러리 이다.Excel.js Github / Excel.js Npm

js를 다운받기 위해서는 다음 링크에서 cdn이나 직접 다운로드한다.Excel.js CDN

그리고 파일을 다운로드하는데 도와주기 위한 FileSaver.js 라이브러리를 또 추가한다.FileSaver.js Github


<script src="https://cdnjs.cloudflare.com/ajax/libs/exceljs/4.2.0/exceljs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js"></script>
<style>
    #btn_chart_excel_download {
        border: 0; background-color: #007f1b; color: white; font-weight: bold; padding: 5px; border-radius: 10px;
    }
</style>
<input type="button" value="엑셀 다운로드" id="btn_chart_excel_download" class="btn_excel"/>
$(function() {
    $("#btn_chart_excel_download").click(function() {
        // 캔버스에 그려진 이미지를 data url로 변환
        let base64Image = ctx.toDataURL(1.0);

        // excel js 객체 생성
        let workbook = new ExcelJS.Workbook();

        // 워크시트 생성
        let worksheet =  workbook.addWorksheet('Sheet');

        // 흰 배경을 만들기 위해 셀 병합
        worksheet.mergeCells('A1:H25');

        // 가상의 파일 읽기
        workbook.xlsx.readFile("chartExample.xlsx");

        // 이미지 등록
        let imageId = workbook.addImage({
            base64: base64Image,
            extension: 'png',
        });

        // 병합했던 셀에 이미지 추가 (엑셀 파일 열면 위치 이동가능)
        worksheet.addImage(imageId, 'A1:H25');

        // 파일 다운로드
        workbook.xlsx.writeBuffer().then(function (data) {
            let blob = new Blob([data], {type: "application/vnd.ms-excel;charset=utf-8"});
            saveAs(blob, "chartReal.xlsx");
        });
    });
});

엑셀 다운로드 버튼을 만들고 클릭이벤트를 추가한 후 위의 JS 스크립트를 붙여서 실행하면 다음처럼 다운로드가 된다.

다운로드된 엑셀 파일을 열어보면 미리 병합했던 셀에 이미지가 붙여진걸 확인할 수 있다.
미리 값을 정해진 위치에 지정한다면 보고서 형식으로도 만들 수 있을 것같다.


Sample Source

<!DOCTYPE html>
<html lang="ko">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        body {
            width: 400px;
            height: 400px;
            margin: auto;
            margin-top: 100px;
        }

        #btn_chart_excel_download {
            border: 0; background-color: #007f1b; color: white; font-weight: bold; padding: 5px; border-radius: 10px;
        }
    </style>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.26.0/polyfill.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-piechart-outlabels"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/exceljs/4.2.0/exceljs.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js"></script>
    <select onchange="changeChart(this.value)">
        <option value="line">라인</option>
        <option value="pie">파이</option>
        <option value="doughnut">도넛</option>
        <option value="bar">막대(세로)</option>
        <option value="horizontalBar">막대(가로)</option>
    </select>
    <input type="button" value="엑셀 다운로드" id="btn_chart_excel_download" class="btn_excel" />
    <canvas id="myChart" width="100" height="100"></canvas>
    <script>
    // tooltip을 렌더링 이후 바로 표시하기 위해서는 다음 소스가 추가되어야함
    Chart.plugins.register({
        beforeRender: function(chart) {
            if (chart.config.options.showAllTooltips) {
                // create an array of tooltips
                // we can't use the chart tooltip because there is only one tooltip
                // per chart
                chart.pluginTooltips = [];
                chart.config.data.datasets.forEach(function(dataset, i) {
                    chart.getDatasetMeta(i).data.forEach(function(sector, j) {
                        chart.pluginTooltips.push(new Chart.Tooltip({
                            _chart: chart.chart,
                            _chartInstance: chart,
                            _data: chart.data,
                            _options: chart.options.tooltips,
                            _active: [sector]
                        }, chart));
                    });
                });

                // turn off normal tooltips
                chart.options.tooltips.enabled = false;
            }
        },
        afterDraw: function(chart, easing) {
            if (chart.config.options.showAllTooltips) {
                // we don't want the permanent tooltips to animate, so don't do
                // anything till the animation runs atleast once
                if (!chart.allTooltipsOnce) {
                    if (easing !== 1)
                        return;
                    chart.allTooltipsOnce = true;
                }

                // turn on tooltips
                chart.options.tooltips.enabled = true;
                Chart.helpers.each(chart.pluginTooltips, function(tooltip) {
                    tooltip.initialize();
                    tooltip.update();
                    // we don't actually need this since we are not animating
                    // tooltips
                    tooltip.pivot();
                    tooltip.transition(easing).draw();
                });
                chart.options.tooltips.enabled = false;
            }
        }
    });
    </script>
    <script>
    let data = {
        labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
        datasets: [{
            fill: false,
            label: '# of Votes',
            data: [12, 19, 3, 5, 2, 3],
            backgroundColor: [
                'rgba(255, 99, 132, 0.2)',
                'rgba(54, 162, 235, 0.2)',
                'rgba(255, 206, 86, 0.2)',
                'rgba(75, 192, 192, 0.2)',
                'rgba(153, 102, 255, 0.2)',
                'rgba(255, 159, 64, 0.2)'
            ],
            borderColor: [
                'rgba(255, 99, 132, 1)',
                'rgba(54, 162, 235, 1)',
                'rgba(255, 206, 86, 1)',
                'rgba(75, 192, 192, 1)',
                'rgba(153, 102, 255, 1)',
                'rgba(255, 159, 64, 1)'
            ],
            borderWidth: 1
        }]
    };

    let options = {
        scales: {
            yAxes: [{
                ticks: {
                    beginAtZero: true
                }
            }]
        },
        legend: {
            display: false, // label 숨기기
        },
        title: {
            display: true, // title 표시
            text: '# title', // title 명
        },
        maintainAspectRatio: false, // 비율유지
        showAllTooltips: false, // tooltip 항상표시
        tooltips: {
            enabled: true, // tooltip 표시(기본은 마우스 hover)
        },
        plugins: {
            legend: true, // legend 표시
            outlabels: {
                text: '%l \n %v',
                borderRadius: 15,
                color: 'black',
                stretch: 30,
                font: {
                    resizable: true,
                    minSize: 12,
                    maxSize: 18
                },
                textAlign: "center"
            } // pie, doughnut 때문에 외부 라이브러리 사용(tooltip을 항상 표시 하는 경우 겹쳐서)
        },
        layout: {
            padding: {
                left: 25,
                right: 25,
                top: 0,
                bottom: 0
            }
        }
    };

    function chartUpdate(type, options, title) {
        if (type == "doughnut" || type == "pie") {
            delete options.scales;
            options.title.text = '';
            options.plugins.outlabels = {
                text: '%l \n %v',
                borderRadius: 15,
                color: 'black',
                stretch: 30,
                font: {
                    resizable: true,
                    minSize: 12,
                    maxSize: 18
                },
                textAlign: "center"
            };
            options.layout.padding.top = 50;
            options.layout.padding.bottom = 50;
        } else {
            options.title.text = title;
            options.scales = {
                yAxes: [{
                    ticks: {
                        beginAtZero: true
                    }
                }]
            }
            delete options.plugins.outlabels;
            options.layout.padding.top = 50;
            options.layout.padding.bottom = 50;
        }
    }

    function changeChart(value) {
        myChart.destroy();

        chartUpdate(value, options, '# title');

        myChart = new Chart(ctx, {
            type: value,
            data: data,
            options: options,
        });
    };
    const ctx = document.getElementById('myChart'); // getContext('2d') 를 하는 경우가 있는데 없어야 엑셀다운가능
    let myChart = new Chart(ctx, {
        type: 'line',
        data: data,
        options: options
    });

    $(function() {
        $("#btn_chart_excel_download").click(function() {
            // 캔버스에 그려진 이미지를 data url로 변환
            let base64Image = ctx.toDataURL(1.0);

            // excel js 객체 생성
            let workbook = new ExcelJS.Workbook();

            // 워크시트 생성
            let worksheet = workbook.addWorksheet('Sheet');

            // 흰 배경을 만들기 위해 셀 병합
            worksheet.mergeCells('A1:H25');

            // 가상의 파일 읽기
            workbook.xlsx.readFile("chartExample.xlsx");

            // 이미지 등록
            let imageId = workbook.addImage({
                base64: base64Image,
                extension: 'png',
            });

            // 병합했던 셀에 이미지 추가 (엑셀 파일 열면 위치 이동가능)
            worksheet.addImage(imageId, 'A1:H25');

            // 파일 다운로드
            workbook.xlsx.writeBuffer().then(function(data) {
                let blob = new Blob([data], { type: "application/vnd.ms-excel;charset=utf-8" });
                saveAs(blob, "chartReal.xlsx");
            });
        });
    });
    </script>
</head>

<body>
</body>

</html>