55 chart.js

source: categories/study/vue-experiance/vue-experiance_9-55.md

55. chart.js

vue-chartjs는 쓰지말자.
업뎃이 이미 1년 이상 안된 라이브러리이다.
chart.js 2점대 버전을 사용하기 때문에 api도 일치하지 않고 찾기도 힘들다.

그냥 chart.js 라이브러리를 사용하자.

관제 그래프 (시계열 그래프)

  • 시계열 그래프에 적합한 그래프를 찾아라
  • 그래프는 한 눈에 보이도록
  • y축은 짧게
  • 스크롤은 지양해라!

Chart 만들기

아래와 같이 작성하면 됩니다.



<template>
  <div class="canvas_area">
    <canvas ref="chartCanvas" id="myChart" width="400" height="400"></canvas>
  </div>
</template>

<script>
import { Chart, registerables } from 'chart.js';
import { mapGetters } from 'vuex';

Chart.register(...registerables);

export default {
  name:'PartStatusTransitPopupDetailCollectedDataGraph',
  data() {
    return {
      chartData: {
        type: 'line',
        data: {
          labels: [],
          datasets: [
            {
              // label: '# of Votes',
              data: [],
            }
          ]
        },
        options: {
          interaction: {
            intersect: false,
          },
          plugins: {
            legend: {
              display: false,
            },
            tooltip: {
              enabled: false,
              external: this.external,
            }
          }
        },
      }
    }
  },
  // created() {
  //   this.chartData.data.datasets[0].data = this.boardData.map(e => e.value);
  //   this.chartData.data.labels = this.convertXLabel(this.boardData.map(e => e.collectTime));
  // },
  mounted() {
    // this.createChart(this.$refs.chartCanvas, this.chartData);
  },
  computed: {
    ...mapGetters('transit', [
      'boardData',
    ]),
  },
  watch: {
    // 수집데이터
    boardData(val) {
      console.log(val)
      this.chartData.data.datasets[0].data = val.map(e => e.value);
      this.chartData.data.labels = this.convertXLabel(val.map(e => e.collectTime));
      this.createChart(this.$refs.chartCanvas, this.chartData);

      // this.chartData.labels = this.convertXLabel(val.map(e => e.collectTime));
      // this.chartData.datasets[0].data = val.map(e => e.value);
      // this.chartData.datasets[0].xGraphData = [...this.graphData];
      // this.renderChart(this.chartData, this.chartOption);
    },
  },
  methods: {
    createChart(ref, chartData) {
      const ctx = ref.getContext('2d');
      const myChart = new Chart(ctx, {
        type: chartData.type,
        data: chartData.data,
        options: chartData.options,
      })
    },
    external(context) {
      // Tooltip Element
      let tooltipEl = document.getElementById('chartjs-tooltip');

      // Create element on first render
      if (!tooltipEl) {
        tooltipEl = document.createElement('div');
        tooltipEl.id = 'chartjs-tooltip';
        tooltipEl.innerHTML = '<div class="tooltip_area"></div>';
        document.body.appendChild(tooltipEl);
      }

      // Hide if no tooltip
      const tooltipModel = context.tooltip;

      if (tooltipModel.opacity === 0) {
        tooltipEl.style.opacity = 0;
        tooltipEl.style.transition = 'opacity .3s';
        return;
      }

      // Set caret Position
      tooltipEl.classList.remove('above', 'below', 'no-transform');
      if (tooltipModel.yAlign) {
        tooltipEl.classList.add(tooltipModel.yAlign);
      } else {
        tooltipEl.classList.add('no-transform');
      }

      function getBody(bodyItem) {
        return bodyItem.lines;
      }

      // Set Text
      if (tooltipModel.body) {
        const titleLines = tooltipModel.title || [];
        const bodyLines = tooltipModel.body.map(getBody);
        const bodyLinesSplit = bodyLines[0][0].split(':');


        let innerHtml = `<div>
                <dl>
                  <div class="definition_item">
                    <dt class="tit">label</dt>
                    <dd class="txt">${titleLines[0]}</dd>
                  </div>
                  <div class="definition_item">
                    <dt class="tit">data</dt>
                    <dd class="txt">${bodyLinesSplit[0].trim()}</dd>
                  </div>
                </dl>
              </div>`

        let tableRoot = tooltipEl.querySelector('.tooltip_area');
        tableRoot.innerHTML = innerHtml;
      }

      const targetCanvas = context.chart.canvas;
      const position = targetCanvas.getBoundingClientRect();
      const positionLeft = position.left + scrollX + tooltipModel.caretX;

      tooltipEl.style.opacity = 1;
      tooltipEl.style.position = 'absolute';
      tooltipEl.style.zIndex = 200;
      tooltipEl.style.left = positionLeft + 'px';
      tooltipEl.style.top = position.top + scrollY + tooltipModel.caretY + 'px';
      tooltipEl.style.pointerEvents = 'none';
    },
    convertXLabel(collectTimeList) {
      const group = {};
      // this.chartOption.scales.xAxes[0].ticks.maxTicksLimit = Object.keys(group).length;
      return collectTimeList.map(e => {
        const date = new Date(e);
        const label = `${date.getDate()}일${date.getHours()}시`;
        group[label] = label;
        return label;
      });
    },
  }
}
</script>

<style scoped lang="scss">

</style>


발생했던 에러

Warning

[Vue warn]: Error in mounted hook: “Error: “linear” is not a registered scale. “found in

일단 Javascript 에서 사용하는 방법과 달리, Bundler(Webpack) 을 사용한 프로젝트에서는 chart.js에서 Chart에 사용할 모듈들을 import하고, Chart에 등록하는 작업이 필요했습니다.

Note

왜 별도로 처리해야하는지 좀 알아봤습니다.
webpack은 죽은 코드를 제거합니다. (참고: webpack tree-shaking) 이걸 tree-shaking 이라 부릅니다.
ES2015 내장 스펙입니다.

chart.js 3webpack에 의해서 tree-shaking될 수 있다고 합니다. (참고: Chart.js - bundler)
그래서 bundler를 사용한다면, 개발자가 chart.js에서 사용하려는 controller, elements, scales 그리고 plugins 을 직접 import 하고 register 해줘야 하는 겁니다.

chart의 모듈을 importregister하는 방법은 chart.js 문서에 잘 나와있습니다.
가이드에서는 위 링크처럼 사용했습니다.

위에처럼 많이 등록하고 사용할거면, 그냥 chart.js의 모듈을 모두 등록하는 방법도 알려주고 있습니다.



import { Chart, registerables } from 'chart.js';
Chart.register(...registerables);


필요한 부분만 불러서 사용하는 것이 좋을 듯~~!!!!

예시

chart.js y축 ticks.length 고정 못하나?

  • 못하는것 같다.
  • maxTicksLimit 옵션으로 최대값 지정만 가능..

오오 해결함!!!

  • ticks.stepSize: 1
  • ticks.callback: function(){}

위 두가지 활용해서 해결!
stepSize를 1로두어 모든 y축 값이 ticks.callback 함수의 인자값으로 들어오게하고,
그 인자값의 최소, 최대 사이를 4개로 쪼개서
5개 y축 값 할당

이런식으로 해결함!!!!



<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>text</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            border: 0;
            box-sizing: border-box;
        }
        .tooltip_area {
            width: 226px;
            padding: 16px;
            border-radius: 6px;
            box-shadow: 0 2px 8px 0 rgba(0, 0, 0, .2);
            background-color: #fff;
        }

        .definition_item {
            display: flex;
        }

        .tooltip_area .tit, .tooltip_area .txt {
            font-size: 12px;
            line-height: 18px;
        }

        .tooltip_area .tit {
            color: #A1A1A1;
        }

        .tooltip_area .txt {
            color: #555;
        }
    </style>
</head>
<body>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
<div class="canvas_area">
    <canvas id="myChart" width="400" height="100"></canvas>
</div>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
스크롤 <br/>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.6.2/dist/chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/1.2.0/chartjs-plugin-zoom.min.js"></script>
<script>
    const ctx = document.getElementById('myChart').getContext('2d');
    const myChart = new Chart(ctx, {
        type: 'line',
        data: {
            labels: Array(2880).fill().map((_, i) => i),
            datasets: [
                {
                    label: '# of Votes',
                    pointStyle: 'crossRot',
                    pointRadius: 0,
                    pointHoverRadius: 0,
                    fill: true,
                    stepped: true,
                    tension: 0.1,
                    data: Array(2880).fill().map(_ => {
                        const random = Math.floor(Math.random() * 10);
                        if (random === 4 || random === 8) {
                            return NaN;
                        } else {
                            return Math.floor(Math.random() * 80);
                        }
                    }),
                    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
                }
            ]
        },
        options: {
            responsive: true, // false로 변경해주면 원하는 크기의 chart를 만들어줄 수 있다.
            scales: {
                y: {
                    // display: true,
                    // suggestedMin: -100000,
                    // suggestedMax: 100,
                    // min: 0,
                    // max: 100,
                    ticks: {
                        // stepSize: forces step size to be 50 units
                        // stepSize: 1,
                        // sampleSize: 얼마나 많은 레이블을 쓸지 결정할 때 쓰는 옵션이다.
                        // sampleSize: 2,
                        stepSize: 1,
                        // maxTicksLimit: 5,
                        // callback: 레이블 반환값을 커스텀할 때 쓰는 옵션이다.
                        callback: function (val, index, ticks) {
                            const range = Math.round((ticks[ticks.length - 1].value - ticks[0].value) / 4)

                            // return val

                            if (index === 0) {
                                return val;
                            }
                            if (ticks[index].value % range === 0) {
                                return val;
                            }
                            // 확대시 y 축 겹침 방지 조건문
                            if (index === ticks.length - 1 && ticks[index].value % range > 5) {
                                return val;
                            }

                            // if (val % Math.round(ticks[0].value / 10) === 0) {
                            //     return val;
                            // }
                            // else if (val===0) {
                            //     return val;
                            // }

                            // return value
                            // if (index % 2 === 0) {
                            //     return ticks[0].value + ((ticks[ticks.length - 1].value - ticks[0].value) / 4) * index;
                            // } else {
                            //     return '';
                            // }
                        }
                    },
                }
            },
            interaction: {
                intersect: false,
            },
            plugins: {
                zoom: {
                    pan: { // 마우스 커서로 잡아 끌기 옵션
                        enabled: true, // 가능함
                        mode: 'x', // 'xy' // 현재는 x축 이동만 가능한 모드
                        // modifierKey: 'shift', // 쉬프트 키를 누른채 마우스커서로 찍어서 드래그해야 그래프가 움직임
                    },
                    zoom: {
                        // drag: { // 드래그로 줌 기능 설정하기
                        //     enabled: false, // 지금은 비활성화
                        // },
                        mode: 'x', // x축으로만 zoom 되게해놓음
                        wheel: { // 마우스 휠로도 zoom 기능 설정하기
                            enabled: false, // 지금은 비활성화
                        },
                        pinch: { // 핀치(두손가락)로 zoom 기능 설정하기
                            enabled: false, // 지금은 비활성화
                        },
                        // mode: 'xy',
                        // onZoomComplete({chart}) { // 이벤트 등록
                        //     // This update is needed to display up to date zoom level in the title.
                        //     // 이 업데이트는 제목에 최신 확대/축소 수준을 표시하는 데 필요합니다.
                        //     // Without this, previous zoom level is displayed.
                        //     // 이것이 없으면 이전 확대/축소 수준이 표시됩니다.
                        //     // The reason is: title uses the same beforeUpdate hook, and is evaluated before zoom.
                        //     // 이유: title은 동일한 beforeUpdate 후크를 사용하고 확대/축소 전에 평가됩니다.
                        //     chart.update('none');
                        // }
                    }
                },
                title: {
                    display: true,
                    text: (ctx) => 'Point Style: ' + ctx.chart.data.datasets[0].pointStyle,
                },
                legend: {
                    display: false,
                },
              //   tooltip: {
              //       enabled: false,
              //       external: function(context) {
              //           // Tooltip Element
              //           let tooltipEl = document.getElementById('chartjs-tooltip');
              //
              //           // Create element on first render
              //           if (!tooltipEl) {
              //               tooltipEl = document.createElement('div');
              //               tooltipEl.id = 'chartjs-tooltip';
              //               tooltipEl.innerHTML = '<div class="tooltip_area"></div>';
              //               document.body.appendChild(tooltipEl);
              //           }
              //
              //           // Hide if no tooltip
              //           const tooltipModel = context.tooltip;
              //           if (tooltipModel.opacity === 0) {
              //               tooltipEl.style.opacity = 0;
              //               return;
              //           }
              //
              //           // Set caret Position
              //           tooltipEl.classList.remove('above', 'below', 'no-transform');
              //           if (tooltipModel.yAlign) {
              //               tooltipEl.classList.add(tooltipModel.yAlign);
              //           } else {
              //               tooltipEl.classList.add('no-transform');
              //           }
              //
              //           function getBody(bodyItem) {
              //               return bodyItem.lines;
              //           }
              //
              //           // Set Text
              //           if (tooltipModel.body) {
              //               let innerHtml = `<div>
              //   <dl>
              //     <div class="definition_item">
              //       <dt class="tit">측정값</dt>
              //       <dd class="txt">11</dd>
              //     </div>
              //     <div class="definition_item">
              //       <dt class="tit">발생일시</dt>
              //       <dd class="txt">2021-08-12 10:10</dd>
              //     </div>
              //     <div class="definition_item">
              //       <dt class="tit">알림내용</dt>
              //       <dd class="txt">임계치 설정 대비 측정값 초과</dd>
              //     </div>
              //     <div class="definition_item">
              //       <dt class="tit">발생위치</dt>
              //       <dd class="txt">경기도 성남시 분당구 대왕판교로 644번길 49</dd>
              //     </div>
              //   </dl>
              // </div>`
              //
              //               let tableRoot = tooltipEl.querySelector('.tooltip_area');
              //               tableRoot.innerHTML = innerHtml;
              //           }
              //
              //           const targetCanvas = context.chart.canvas;
              //           const position = targetCanvas.getBoundingClientRect();
              //           const targetCanvasParentWidth = targetCanvas.parentElement.getBoundingClientRect().width;
              //           const positionLeft = position.left + scrollX + tooltipModel.caretX;
              //
              //           // const bodyFont = Chart.helpers.toFont(tooltipModel.options.bodyFont);
              //           // Display, position, and set styles for font
              //           tooltipEl.style.opacity = 1;
              //           tooltipEl.style.position = 'absolute';
              //           if (targetCanvasParentWidth < tooltipEl.getBoundingClientRect().width + positionLeft) {
              //               tooltipEl.style.left = targetCanvasParentWidth - tooltipEl.getBoundingClientRect().width + 'px';
              //           } else {
              //               tooltipEl.style.left = positionLeft + 'px';
              //           }
              //           tooltipEl.style.top = position.top + scrollY + tooltipModel.caretY + 'px';
              //           tooltipEl.style.pointerEvents = 'none';
              //       }
              //   }
            },


            onClick(e) { // 차트 클릭 이벤트 등록
                const chart = e.chart;
                chart.options.plugins.zoom.zoom.wheel.enabled = !chart.options.plugins.zoom.zoom.wheel.enabled;
                chart.options.plugins.zoom.zoom.pinch.enabled = !chart.options.plugins.zoom.zoom.pinch.enabled;
                // chart.options.plugins.zoom.zoom.drag.enabled = !chart.options.plugins.zoom.zoom.drag.enabled;
                chart.update();
            }
        },


        plugins: [ // 클릭했을 때 차트 테두리 설정
            {
                id: 'chartAreaBorder',
                beforeDraw(chart, args, options) {
                    const {ctx, chartArea: {left, top, width, height}} = chart;
                    if (chart.options.plugins.zoom.zoom.wheel.enabled) {
                        ctx.save();
                        ctx.strokeStyle = 'red';
                        ctx.lineWidth = 1;
                        ctx.strokeRect(left, top, width, height);
                        ctx.restore();
                    }
                }
            }
        ]
    })
</script>
</body>
</html>


관제 그래프 데이터

NaN, 1, NaN, 1, NaN, 1, NaN, 1… 이런식으로 데이터가 날라온다면,, 1분마다,,
관제데이터 그래프 특성상 NaN일 땐 그래프가 안 그려져야된다.
그런데 위와 같이오면 그래프가 연결될수가 없기 때문에 그래프를보면 그래프가 안그려진 것 처럼 보인다.
점이 찍혔다라고 생각하면 될듯.

0, 1 데이터가 반복되면 선으로 이어짐.
하지만 NaN 값이면 1, NaN, 1, NaN, 1, … 이러면 연결되지 않음.

그래서 점에 width값을 줌

  • pointRadius: 0.1,
  • pointHoverRadius: 0.1,

남은 과제 IE11 맞추기. 3점대 버전에서 2점대 버전으로 다시 다운그레이드해야됨