Web/JS

[JS] Chart.js 원형 차트, 사용자 지정 범례 그리기!(pie chart, custom legend)

메바동 2020. 7. 13. 20:36
728x90

Chart.js를 이용해서 원형 차트를 그리고 사용자 지정 범례를 만들어야 했다.

 

근데 저는 Chart.js를 처음 들어봅니다...

한 번도 사용한 적이 없어 사용 방법을 찾아보니 그리 어려운 라이브러리는 아니었다.

 

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">

        <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js"></script>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.css">
    </head>

    <body>
        <div class="chart-div">
            <canvas id="pieChartCanvas" width="300px" height="300px"></canvas>
        </div>

        <script src="script.js"></script>
    </body>
</html>
window.onload = function () {
    pieChartDraw();
}

let pieChartData = {
    labels: ['foo', 'bar', 'baz', 'fie', 'foe', 'fee'],
    datasets: [{
        data: [95, 12, 13, 7, 13, 10],
        backgroundColor: ['rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(54, 162, 235)', 'rgb(153, 102, 255)']
    }] 
};

let pieChartDraw = function () {
    let ctx = document.getElementById('pieChartCanvas').getContext('2d');
    
    window.pieChart = new Chart(ctx, {
        type: 'pie',
        data: pieChartData,
        options: {
            responsive: false
        }
    });
};

위와 같이 HTML에 canvas를 만들어주고 Data 객체를 만든 다음 그리면 됐다.

 

별거 없지만 뭔가 있어보여...!

하지만 내가 할 일은 단지 차트만 그리는 일이 아니었다. custom legend를 만들어야 했다. 

우선 처음 다루는 일이니 기존에 있던 legend를 display: false로 해서 가려준 뒤 legendCallback을 이용해서 ul태그를 이용한 범례를 만들어주면 됐다.

 

지금 말은 쉽게 하지만 이해력이 좋지 않아 사용하는데 시간이 꽤 걸렸다.

 

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">

        <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js"></script>
        <link rel="stylesheet" href="style.css">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.css">
    </head>

    <body>
        <div class="chart-div">
            <canvas id="pieChartCanvas" width="300px" height="300px"></canvas>
            <div id='legend-div' class="legend-div"></div>
        </div>

        <script src="script.js"></script>
    </body>
</html>
window.onload = function () {
    pieChartDraw();
    document.getElementById('legend-div').innerHTML = window.pieChart.generateLegend();
}

let pieChartData = {
    labels: ['foo', 'bar', 'baz', 'fie', 'foe', 'fee'],
    datasets: [{
        data: [95, 12, 13, 7, 13, 10],
        backgroundColor: ['rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(54, 162, 235)', 'rgb(153, 102, 255)']
    }] 
};

let pieChartDraw = function () {
    let ctx = document.getElementById('pieChartCanvas').getContext('2d');
    
    window.pieChart = new Chart(ctx, {
        type: 'pie',
        data: pieChartData,
        options: {
            responsive: false,
            legend: {
                display: false
            },
            legendCallback: customLegend
        }
    });
};

let customLegend = function (chart) {
    let ul = document.createElement('ul');
    let color = chart.data.datasets[0].backgroundColor;

    chart.data.labels.forEach(function (label, index) {
        ul.innerHTML += `<li><span style="background-color: ${color[index]}; display: inline-block; width: 30px; height: 10px;"></span> ${label}</li>`;
    });

    return ul.outerHTML;
};

기존 HTML에서 변한 점은 canvas 밑에 custom legend가 들어갈 div를 하나 더 만들어주었고 모양을 가다듬기 위해 Reset CSS를 추가해 주었다.

 

그리고 custom legend를 이용하기 위해서는 만들어 준 div의 innerHTML에 차트의 generateLegend()를 이용해 legendCallback을 호출하여 넣어주어야 한다.

왜 더 못생겨진거죠????

기존에 있던 범례보다 못생겨진 사용자 지정 범례를 예쁘게 만들어야 했다.

 

li 태그 안에 span 태그를 쓰기 싫어 가상 요소인 ::before를 이용해 넣어주려 했다. 대충 li 태그 안에 color를 담고 있는 데이터 속성을 주어 그 값을 읽어와 동그란 색상 라벨을 주려고 했는데...

 

왜 아무런 브라우저도 지원을 하지 않는거죠...?

보는 것과 같이 아무런 브라우저도 해당 기능을 지원하지 않았다.

 

결국 span 태그를 주어서 만들었다.

 

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">

        <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js"></script>
        <link rel="stylesheet" href="style.css">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.css">

<style>
    .legend-div {
        position: absolute;
        top: 62px;
        left: 320px;
    }

    .legend-div ul li {
        margin: 10px 0;
        color: #666;
        font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif
    }

    .legend-div ul li span {
        display: inline-block;
        width: 20px;
        height: 20px;
        border-radius: 50%;
        margin-right: 5px;
        vertical-align: middle;
    }
</style>

    </head>

    <body>
        <div class="chart-div">
            <canvas id="pieChartCanvas" width="300px" height="300px"></canvas>
            <div id='legend-div' class="legend-div"></div>
        </div>

        <script src="script.js"></script>
    </body>
</html>
window.onload = function () {
    pieChartDraw();
    document.getElementById('legend-div').innerHTML = window.pieChart.generateLegend();
}

let pieChartData = {
    labels: ['foo', 'bar', 'baz', 'fie', 'foe', 'fee'],
    datasets: [{
        data: [95, 12, 13, 7, 13, 10],
        backgroundColor: ['rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(54, 162, 235)', 'rgb(153, 102, 255)']
    }] 
};

let pieChartDraw = function () {
    let ctx = document.getElementById('pieChartCanvas').getContext('2d');
    
    window.pieChart = new Chart(ctx, {
        type: 'pie',
        data: pieChartData,
        options: {
            responsive: false,
            legend: {
                display: false
            },
            legendCallback: customLegend
        }
    });
};

let customLegend = function (chart) {
    let ul = document.createElement('ul');
    let color = chart.data.datasets[0].backgroundColor;

    chart.data.labels.forEach(function (label, index) {
        ul.innerHTML += `<li><span style="background-color: ${color[index]}"></span>${label}</li>`;
    });

    return ul.outerHTML;
};

 

차이를 못 느낄 수도 있지만 내 눈에는 훨씬 세련된 사용자 지정 범례가 만들어졌다.

 

이제 저 범례를 클릭하면 차트에서 사라지던 기존 기능을 넣어주어야 한다. 라이브러리를 열어본 뒤 onClick을 보면 어떻게 만들어야 할지 대충 감이 온다.

 

window.onload = function () {
    pieChartDraw();
    document.getElementById('legend-div').innerHTML = window.pieChart.generateLegend();
    setLegendOnClick();
}

let pieChartData = {
    labels: ['foo', 'bar', 'baz', 'fie', 'foe', 'fee'],
    datasets: [{
        data: [95, 12, 13, 7, 13, 10],
        backgroundColor: ['rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(54, 162, 235)', 'rgb(153, 102, 255)']
    }] 
};

let pieChartDraw = function () {
    let ctx = document.getElementById('pieChartCanvas').getContext('2d');
    
    window.pieChart = new Chart(ctx, {
        type: 'pie',
        data: pieChartData,
        options: {
            responsive: false,
            legend: {
                display: false
            },
            legendCallback: customLegend
        }
    });
};

let customLegend = function (chart) {
    let ul = document.createElement('ul');
    let color = chart.data.datasets[0].backgroundColor;

    chart.data.labels.forEach(function (label, index) {
        ul.innerHTML += `<li data-index="${index}"><span style="background-color: ${color[index]}"></span>${label}</li>`;
    });

    return ul.outerHTML;
};

let setLegendOnClick = function () {
    let liList = document.querySelectorAll('#legend-div ul li');

    for (let element of liList) {
        element.onclick = function () {
            updateChart(event, this.dataset.index, "pieChart");
            
            if (this.style.textDecoration.indexOf("line-through") < 0) {
                this.style.textDecoration = "line-through";
            } else {
                this.style.textDecoration = "";
            }
        }
    }
};

let updateChart = function (e, datasetIndex, chartId) {
  let index = datasetIndex;
  let chart = e.view[chartId];
  let i, ilen, meta;
  
  for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) {
      meta = chart.getDatasetMeta(i);

      if (meta.data[index]) {
          meta.data[index].hidden = !meta.data[index].hidden;
      }
  }

  chart.update();
};

대충 이렇게 각각 li에 onclick을 주면 해당 기능을 구현할 수 있다. 

 

누르면 실선이 그어지고 차트에서 사라진다.

이제 끝! 이 아니라 최종 목표는 사실 저 데이터 값을 담은 범례를 만드는 것이 아니라 '성공', '실패' 라벨을 가진 범례를 만들어 성공을 누를 경우 실패한 데이터만 차트에 그려주고 '실패'를 누를 경우 성공한 데이터만 차트에 그려줘야 한다.

 

window.onload = function () {
    pieChartDraw();
    document.getElementById('legend-div').innerHTML = window.pieChart.generateLegend();
    setLegendOnClick();
}

let pieChartData = {
    labels: ['개발 성공', '개발 실패', '테스트 성공', '테스트 실패', '배포 성공', '배포 실패'],
    datasets: [{
        data: [95, 12, 13, 7, 13, 10],
        backgroundColor: ['rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(54, 162, 235)', 'rgb(153, 102, 255)']
    }] 
};

let pieChartDraw = function () {
    let ctx = document.getElementById('pieChartCanvas').getContext('2d');
    
    window.pieChart = new Chart(ctx, {
        type: 'pie',
        data: pieChartData,
        options: {
            responsive: false,
            legend: {
                display: false
            },
            legendCallback: customLegend
        }
    });
};

let customLegend = function (chart) {
    let ul = document.createElement('ul');
    let color = chart.data.datasets[0].backgroundColor;

    // chart.data.labels.forEach(function (label, index) {
    //     ul.innerHTML += `<li data-index="${index}"><span style="background-color: ${color[index]}"></span>${label}</li>`;
    // });

    ul.innerHTML += "<li data-index='0'><span style='background-color: rgb(54, 162, 235)';></span>성공</ul>";
    ul.innerHTML += "<li data-index='1'><span style='background-color: rgb(255, 99, 132)';></span>실패</ul>";


    return ul.outerHTML;
};

let setLegendOnClick = function () {
    let liList = document.querySelectorAll('#legend-div ul li');

    for (let element of liList) {
        element.onclick = function () {
            updateChart(event, this.dataset.index, "pieChart");
            
            if (this.style.textDecoration.indexOf("line-through") < 0) {
                this.style.textDecoration = "line-through";
            } else {
                this.style.textDecoration = "";
            }
        }
    }
};

let updateChart = function (e, datasetIndex, chartId) {
  let index = datasetIndex;
  let chart = e.view[chartId];
  let i, ilen, meta;
  
  for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) {
      meta = chart.getDatasetMeta(i);

    //   if (meta.data[index]) {
    //       meta.data[index].hidden = !meta.data[index].hidden;
    //   }

      for (var j = 0; j < meta.data.length; j++) {
          if (j % 2 == index) {
              meta.data[j].hidden = !meta.data[j].hidden;
          }
      }
  }

  chart.update();
};

처음에는 dataset에 성공은 0을 포함한 짝수, 실패는 홀수로 되어있어 각 데이터의 인덱스를 2로 나눈 나머지를 통해 홀수, 짝수를 구별하고 가려주는 식으로 만들었었다. 

 

하지만 그렇게 구현하게 되면 항상 홀짝 패턴을 맞춰 데이터를 넣어주어야 하는 문제가 있어 데이터의 label에 '성공', '실패'가 들어가는지 검사를 하는 방법으로 수정하였다.

 

window.onload = function () {
    pieChartDraw();
    document.getElementById('legend-div').innerHTML = window.pieChart.generateLegend();
    setLegendOnClick();
}

let pieChartData = {
    labels: ['개발 성공', '개발 실패', '테스트 성공', '테스트 실패', '배포 성공', '배포 실패'],
    datasets: [{
        data: [95, 12, 13, 7, 13, 10],
        backgroundColor: ['rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(54, 162, 235)', 'rgb(153, 102, 255)']
    }] 
};

let pieChartDraw = function () {
    let ctx = document.getElementById('pieChartCanvas').getContext('2d');
    
    window.pieChart = new Chart(ctx, {
        type: 'pie',
        data: pieChartData,
        options: {
            responsive: false,
            legend: {
                display: false
            },
            legendCallback: customLegend
        }
    });
};

let customLegend = function (chart) {
    let ul = document.createElement('ul');
    let color = chart.data.datasets[0].backgroundColor;

    // chart.data.labels.forEach(function (label, index) {
    //     ul.innerHTML += `<li data-index="${index}"><span style="background-color: ${color[index]}"></span>${label}</li>`;
    // });

    ul.innerHTML += "<li data-target='성공'><span style='background-color: rgb(54, 162, 235)';></span>성공</ul>";
    ul.innerHTML += "<li data-target='실패'><span style='background-color: rgb(255, 99, 132)';></span>실패</ul>";


    return ul.outerHTML;
};

let setLegendOnClick = function () {
    let liList = document.querySelectorAll('#legend-div ul li');

    for (let element of liList) {
        element.onclick = function () {
            updateChart(event, this.dataset.target, "pieChart");
            
            if (this.style.textDecoration.indexOf("line-through") < 0) {
                this.style.textDecoration = "line-through";
            } else {
                this.style.textDecoration = "";
            }
        }
    }
};

let updateChart = function (e, target, chartId) {
  let chart = e.view[chartId];
  let i, ilen, meta;
  
  for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) {
      meta = chart.getDatasetMeta(i);

    //   if (meta.data[index]) {
    //       meta.data[index].hidden = !meta.data[index].hidden;
    //   }

      for (var j = 0; j < meta.data.length; j++) {
          if (meta.data[j]._view.label.includes(target)) {
              meta.data[j].hidden = !meta.data[j].hidden;
          }
      }
  }

  chart.update();
};

data의 label에 해당 문자열이 포함되는지 검사한 후 포함이 되면 가려주게 된다. 

 

최종 목표 달성~

 

- 포스팅 마침 -

728x90