55 chart.js
source: categories/study/vue-experiance/vue-experiance_9-55.md
- chartjs
- chartjs-plugin-zoom
- 예시 chart.js 3.6.2, chartjs-plugin-zoom 1.2.0
import { Chart, registerables } from 'chart.js';
import zoomPlugin from 'chartjs-plugin-zoom';
Chart.register(...registerables);
Chart.register(zoomPlugin);
- 예시 chart.js 2.9.4, chartjs-plugin-zoom 0.7.7
- 예시 chart.js 2.9.4, chartjs-plugin-zoom 0.7.7 - docs
- 예시 chart.js 2.9.4, chartjs-plugin-zoom 0.7.7 - docs2
- 예시 chart.js 2.9.4, chartjs-plugin-zoom 0.7.7 - docs3
- 이 5번째 예시처럼 IE11 호환 맞추면될듯?
- 이제 툴팁만 해보자, 툴팁도 해결!
- 아 그래프 색깔도.., 색깔도 해결!
zoom
기능도 추가함!wheel zoom
기능 구현- 그런데 위 코드에선 잘 작동하는데, 비동기로 데이터가져오고 chartjs 그리는 실제 서비스코드는
onPan
이벤트 감지시 처음 2번은xAex ticks
의min
,max
값이undefined
로 되어버린다. - 그래서 좀 이상.. 이를 대비해 예외처리코드 넣긴했는데.. 이상하네.. 정확한 원인을 모르겠음
- 그런데 위 코드에선 잘 작동하는데, 비동기로 데이터가져오고 chartjs 그리는 실제 서비스코드는
- 예시 chart.js 2.9.4, chartjs-plugin-zoom 0.7.7 - docs4
pinch zoom
기능 추가하고싶은데 어렵다..- 오예 추가성공! 더 테스트해봐야됨
- 아… 다운그레이드하려니까 어렵네… 계속 리서치해야겠다.
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>
발생했던 에러
[Vue warn]: Error in mounted hook: “Error: “linear” is not a registered scale. “found in
일단 Javascript 에서 사용하는 방법과 달리, Bundler(Webpack)
을 사용한 프로젝트에서는 chart.js
에서 Chart
에 사용할 모듈들을 import
하고, Chart
에 등록하는 작업이 필요했습니다.
왜 별도로 처리해야하는지 좀 알아봤습니다.webpack
은 죽은 코드를 제거합니다. (참고: webpack tree-shaking) 이걸 tree-shaking 이라 부릅니다.
ES2015 내장 스펙입니다.
chart.js 3
은 webpack
에 의해서 tree-shaking될 수 있다고 합니다. (참고: Chart.js - bundler)
그래서 bundler
를 사용한다면, 개발자가 chart.js
에서 사용하려는 controller
, elements
, scales
그리고 plugins
을 직접 import
하고 register
해줘야 하는 겁니다.
chart
의 모듈을 import
와 register
하는 방법은 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,