139 vuetify, vuedraggable, + @ IE11 이슈, chart.js 3.6.4, chartjs-plugin-zoom 1.2.0

source: categories/study/vue-experiance/vue-experiance_9-99_40.md

139 vuetify, vuedraggable, + @ IE11 이슈, chart.js 3.6.4, chartjs-plugin-zoom 1.2.0

전역 컴포넌트 등록으로 해결..!!

  1. 지역 컴포넌트로 불러왔는데 순환참조 이슈 발생
  2. 여러 테스트를 거쳤으나 정확한 원인 파악 어려움
  3. 혹시 역으로 전역으로 등록해서 네임스페이스를 확고히 해야되나? 라는 생각을 함
    • 근거는 없지만 그냥 이런 생각을 함..
    • 하도 해결방법을 못찾겟어서..
    • 그런데 이 방법으로 해결이됨..


Vue.component('DraggableComponent', draggable);


chart.js 3.6.4, chartjs-plugin-zoom 1.2.0

PartStatusTransitPopupDetailCollectedDataGraph.vue



<template>
	<div class="canvas_area">
		<canvas id="myChart" ref="chartCanvas" 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',
	props: {
		noticeLimit: {
			type: Object,
		},
	},
	data() {
		return {
			myChart: null,
			graphData: [],
			chartData: {
				type: 'line',
				data: {
					labels: [],
					datasets: [
						{
							label: '수집데이터',
							data: [],
							xAxisID: 'xAxes',
							backgroundColor: '#FF2F00',
							borderColor: '#FF7F00',
							borderWidth: 1.5,
							strokeColor: '#FF7F00',
							pointBackgroundColor: [],
							pointRadius: [],
							fill: false,
							fillColor: '#FF7F00',
						},
					],
				},
				options: {
					interaction: {
						intersect: false,
					},
					scales: {
						xAxes: {
							ticks: {
								callback: {},
								maxTicksLimit: 10,
							},
						},
					},
					plugins: {
						legend: {
							display: true,
						},
						tooltip: {
							enabled: false,
							position: 'nearest',
							external: this.external,
						},
					},
				},
			},
		};
	},
	computed: {
		...mapGetters('transit', ['boardData', 'pubReportData']),
	},
	watch: {
		// 수집데이터
		boardData(val) {
			this.initChart(val);
		},
		pubReportData(val) {
			this.initChart(val);
		},
	},
	beforeDestroy() {
		// 차트 정보 삭제
		if (this.myChart) {
			this.myChart.destroy();
		}
	},
	methods: {
		initChart(val) {
			this.graphData = [...val];
			this.chartData.data.datasets[0].data = val.map(e => e.value);
			this.chartData.data.labels = val.map(e => e.collectTime);
			// 라벨 콜백 적용
			this.convertXLabelCallback(this.chartData.data.labels);
			// 이상 알림발생 포인트 적용
			this.pointAbnormalNoticeHistory(this.chartData.data.datasets[0]);
			// 임계치 설정
			this.limitBackground();

			this.createChart(this.$refs.chartCanvas, this.chartData);
		},
		createChart(ref, chartData) {
			const ctx = ref.getContext('2d');
			if (this.myChart) {
				this.myChart.destroy();
			}
			this.myChart = new Chart(ctx, {
				type: chartData.type,
				data: chartData.data,
				options: chartData.options,
			});
		},
		external(context) {
			// Tooltip Element
			let tooltipEl = document.getElementById('chartjs-tooltip');

			// 이상알림 발생된 이력만 표시
			const tooltipItems = context.tooltip.$context
				? context.tooltip.$context.tooltipItems
				: [];
			if (tooltipItems.length < 1) {
				if (tooltipEl) {
					tooltipEl.remove();
				}
				return;
			}

			const dataIndex = tooltipItems[0].dataIndex;
			if (!this.graphData[dataIndex].history) {
				if (tooltipEl) {
					tooltipEl.remove();
				}
				return;
			}

			// 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');
			}

			// Set Text
			if (tooltipModel.body) {
				const {
					sensorTypeName,
					history: {
						sensorName,
						noticeDivisionName,
						limitValueName,
						measureValueName,
						measureTime,
						content,
						location,
					},
				} = this.graphData[dataIndex];

				const innerHtml = `<div>
                <dl>
                  <div class="definition_item">
                    <dt class="tit">센서타입</dt>
                    <dd class="txt">${sensorTypeName || '-'}</dd>
                  </div>
                  <div class="definition_item">
                    <dt class="tit">센서명</dt>
                    <dd class="txt">${sensorName || '-'}</dd>
                  </div>
                  <div class="definition_item">
                    <dt class="tit">알림구분</dt>
                    <dd class="txt">${noticeDivisionName || '-'}</dd>
                  </div>
                  <div class="definition_item">
                    <dt class="tit">임계치</dt>
                    <dd class="txt">${limitValueName || '-'}</dd>
                  </div>
                  <div class="definition_item">
                    <dt class="tit">측정값</dt>
                    <dd class="txt ${measureValueName && 'is_red'}">${
					measureValueName || '-'
				}</dd>
                  </div>
                  <div class="definition_item">
                    <dt class="tit">발생일시</dt>
                    <dd class="txt">${measureTime || '-'}</dd>
                  </div>
                  <div class="definition_item">
                    <dt class="tit">알림내용</dt>
                    <dd class="txt">${content || '-'}</dd>
                  </div>
                  <div class="definition_item">
                    <dt class="tit">발생위치</dt>
                    <dd class="txt">${location || '-'}</dd>
                  </div>
                </dl>
              </div>`;

				const 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';
		},
		// 지정 라벨만 적용 (스코프)
		convertXLabelCallback(collectTimeList) {
			const group = {};
			const groupIndex = [];
			const xLabels = collectTimeList.map((e, index) => {
				const date = new Date(e);
				const label = `${date.getDate()}일${date.getHours()}시`;
				if (!group[label]) {
					group[label] = true;
					groupIndex.push(index);
				}
				return label;
			});

			// 지정 라벨만 적용 (스코프)
			this.chartData.options.scales.xAxes.ticks.callback = (_, index) => {
				if (groupIndex.includes(index)) {
					return xLabels[index];
				}
			};
		},
		// 이상 알림 발생구간만 포인트
		pointAbnormalNoticeHistory(dataset) {
			this.graphData.forEach((e, index) => {
				// 기본값
				dataset.pointBackgroundColor[index] = 'orange';
				if (e.history) {
					dataset.pointBackgroundColor[index] = 'red';
					dataset.pointRadius[index] = 3;
				} else {
					dataset.pointRadius[index] = 0;
				}
			});
		},
		// 임계치 백그라운드 설정
		limitBackground() {
			const graphDataLength = this.graphData.length;
			const upperLimit = [];
			const lowerLimit = [];
			for (let i = 0; i < graphDataLength; ++i) {
				upperLimit.push(this.noticeLimit.upperValue);
				lowerLimit.push(this.noticeLimit.lowerValue);
			}

			// 초기화
			this.chartData.data.datasets = [{ ...this.chartData.data.datasets[0] }];

			if (this.noticeLimit.upperValue) {
				this.chartData.data.datasets.push({
					label: '상한값',
					data: upperLimit,
					borderWidth: 1,
					borderColor: '#FFFFE0',
					backgroundColor: 'rgba(255,255,224,0.7)',
					fill: this.noticeLimit.lowerValue ? -1 : 'start',
				});
			}

			if (this.noticeLimit.lowerValue) {
				this.chartData.data.datasets.push({
					label: '하한값',
					data: lowerLimit,
					borderWidth: 1,
					borderColor: '#FFFFE0',
					backgroundColor: 'rgba(255,255,224,0.7)',
					fill: this.noticeLimit.upperValue ? +1 : 'end',
				});
			}
		},
	},
};
</script>

<style scoped lang="scss"></style>


DeliveryLineChart.vue



<template>
	<div class="canvas_area">
		<canvas id="myChart" ref="lineChart" width="800" height="800"></canvas>
	</div>
</template>

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

// 임계치 헬퍼 플러그인
const helperPlugin = {
	id: 'limitHelperPlugin',
	afterDraw: chart => {
		const context = chart.ctx;

		const limitConfig = chart.config.data.limitConfig;

		const limitStrokeDraw = (index, config, valueName) => {
			const value = config[valueName];

			const filteringLegendItem = chart.legend.legendItems.filter(
				legendItem => legendItem.datasetIndex === Number(index),
			)[0];
			const filteringInSuggestedMax =
				chart.options.scales.yAxes.suggestedMax >= value;
			const filteringInSuggestedMin =
				chart.options.scales.yAxes.suggestedMin <= value;

			if (
				value &&
				!filteringLegendItem.hidden &&
				filteringInSuggestedMax &&
				filteringInSuggestedMin
			) {
				const xAxis = chart.scales.x; // 기본 x 축
				const yAxis = chart.scales.yAxes;

				context.save();
				context.beginPath();
				context.moveTo(xAxis.left, yAxis.getPixelForValue(value));
				context.strokeStyle = config.color;
				context.setLineDash([8, 6]);
				context.lineTo(xAxis.right, yAxis.getPixelForValue(value));
				context.stroke();
			}
		};

		if (limitConfig && Array.isArray(limitConfig)) {
			for (const index in limitConfig) {
				const config = limitConfig[index];

				limitStrokeDraw(index, config, 'upperValue');
				limitStrokeDraw(index, config, 'lowerValue');
			}
		}

		context.restore();
	},
};

Chart.register(...registerables, helperPlugin);

export default {
	name: 'DeliveryLineChart',
	data() {
		return {
			myChart: null,
			chartData: {
				type: 'line',
				data: {
					labels: [],
					datasets: [
						{
							label: '',
							backgroundColor: 'transparent',
							pointBackgroundColor: '#fff',
							borderWidth: 2,
							borderColor: '#4E76DE',
							pointBorderColor: '#4E76DE',
							data: [],
							fill: true, // true 일 경우 색상이 채워진다.
							lineTension: 0.4, // 곡선 라인
							radius: 0.1,
						},
					],
				},
				options: {
					grid: {
						display: true,
					},
					interaction: {
						intersect: false,
					},
					scales: {
						yAxes: {
							beginAtZero: true,
							suggestedMax: 60,
							suggestedMin: -40,
							padding: 10,
						},
					},
					plugins: {
						legend: {
							display: true,
							align: 'end',
							textDirection: 'ltr',
							labels: {
								boxWidth: 12,
								boxHeight: 12,
								padding: 24,
							},
						},
						tooltip: {
							enabled: true,
							position: 'nearest',
							intersect: false,
							alignment: 'center',
							custom(tooltip) {
								if (!tooltip) {
									return;
								}
								tooltip.displayColors = false;
							},
							callbacks: {
								title(tooltipItem) {
									const { label } = tooltipItem[0];
									return label;
								},
							},
						},
					},
				},
			},
			sensor: {
				TEMP: {
					color: '#4E76DE',
					label: '온도',
				},
				HUMD: {
					color: '#E16E6E',
					label: '습도',
				},
				COLL: {
					color: '#9B71F7',
					label: '충격',
				},
				BATT: {
					color: '#29A7B3',
					label: '배터리',
				},
			},
		};
	},
	computed: {
		...mapGetters('delivery', ['deliveryChartData']),
	},
	watch: {
		deliveryChartData() {
			this.setChartData();
		},
	},
	beforeDestroy() {
		// 차트 정보 삭제
		if (this.myChart) {
			this.myChart.destroy();
		}
	},
	mounted() {},
	methods: {
		initChart(ref, chartData) {
			// 컨텍스트
			const ctx = ref.getContext('2d');

			if (this.myChart) {
				this.myChart.destroy();
			}
			this.myChart = new Chart(ctx, {
				type: chartData.type,
				data: chartData.data,
				options: chartData.options,
			});
		},
		setChartData() {
			const getRandomColorWithoutRed = () => {
				let color = '#';
				const randomHex = '0123456789ABCDEF';
				while (color.length <= 6) {
					color += randomHex[Math.floor(Math.random() * 16)] || '';
				}
				return color;
			};

			const graphColorList = [
				'#000080',
				'#50BCDF',
				'#FFFF00',
				'#FF0000',
				'#008000',
				'#81C147',
				'#8B00FF',
				'#660099',
				'#873904',
				'#FF7F00',
				'#FFC0CB',
			];

			const objChart = {
				label: '온도',
				backgroundColor: 'transparent',
				pointBackgroundColor: '#fff',
				borderWidth: 2,
				borderColor: '#4E76DE',
				pointBorderColor: '#4E76DE',
				data: [],
				fill: true, // true 일 경우 색상이 채워진다.
				lineTension: 0.4, // 곡선 라인
				radius: 0.1,
			};

			const chartData = [];
			const limitConfig = [];

			const suggestedMax = Math.max.apply(
				null,
				this.deliveryChartData.deviceCodeList
					.map(item => item.data)
					.map(item =>
						item.reduce((prev, curr) =>
							Number(prev) > Number(curr) ? Number(prev) : Number(curr),
						),
					),
			);
			const suggestedMin = Math.min.apply(
				null,
				this.deliveryChartData.deviceCodeList
					.map(item => item.data)
					.map(item =>
						item.reduce((prev, curr) =>
							Number(prev) < Number(curr) ? Number(prev) : Number(curr),
						),
					),
			);

			const optionalCategoryName = categoryName => {
				if (categoryName) {
					return '(' + categoryName + ')';
				}
				return '';
			};

			this.chartData.data.labels = this.deliveryChartData.sensorLabels;

			const applyColors = [];
			for (const index in this.deliveryChartData.deviceCodeList) {
				const item = this.deliveryChartData.deviceCodeList[index];

				let color = graphColorList[index] || getRandomColorWithoutRed();
				// let color = getRandomColorWithoutRed();
				while (applyColors.includes(color)) {
					color = getRandomColorWithoutRed();
				}

				const dataObj = {
					...objChart,
					label:
						this.sensor[item.sensorType].label +
						optionalCategoryName(item.categoryName),
					borderColor: color,
					pointBorderColor: color,
					data: item.data,
				};
				const configObj = {
					lowerValue: item.lowerValue,
					color,
					upperValue: item.upperValue,
				};
				chartData.push(dataObj);
				limitConfig.push(configObj);
				applyColors.push(color);
			}

			// console.log('chartData', chartData);
			this.chartData.data.datasets = chartData;
			this.chartData.data.limitConfig = limitConfig;

			// 그래프 최소값, 최대값 유동적으로 가져가기
			const offsetSuggestedMax = 0;
			const offsetSuggestedMin = 0;

			this.chartData.options.scales.yAxes.suggestedMax = Math.max(
				this.chartData.options.scales.yAxes.suggestedMax,
				(suggestedMax || 0) + offsetSuggestedMax,
			);
			this.chartData.options.scales.yAxes.suggestedMin = Math.min(
				this.chartData.options.scales.yAxes.suggestedMin,
				(suggestedMin || 0) - offsetSuggestedMin,
			);

			this.initChart(this.$refs.lineChart, this.chartData);
		},
	},
};
</script>


DeviceLineChart.vue



<template>
	<div class="canvas_area">
		<canvas
			id="myChart"
			ref="lineChart"
			class="canvas"
			width="400"
			height="100"
		></canvas>
		<span class="tooltip">
			확대: 그래프 영역 클릭 후 마우스휠<br />
			x축 이동: 그래프 영역 마우스 드래그<br />
			그래프 영역 재클릭: 확대 기능 off
		</span>
	</div>
</template>

<script>
import { mapGetters } from 'vuex';
import { Chart, registerables } from 'chart.js';
import zoomPlugin from 'chartjs-plugin-zoom';
import moment from '@/plugins/moment';

Chart.register(...registerables);
Chart.register(zoomPlugin);

export default {
	name: 'DeviceChartLine',
	props: {
		period: {
			type: Number,
			required: true,
		},
		periodDt: {
			type: Object,
			required: true,
			validator(obj) {
				return (
					'startDt' in obj &&
					typeof obj.startDt === 'string' &&
					'endDt' in obj &&
					typeof obj.endDt === 'string'
				);
			},
		},
	},
	data() {
		return {
			myChart: null,
			chartData: {
				type: 'line',
				data: {
					labels: [],
					datasets: [
						// period 48시간 이내
						// {
						//   label: '데이터값',
						//   backgroundColor: 'rgba(78,118,222,0.3)',
						//   pointBackgroundColor: '#fff',
						//   borderWidth: 2,
						//   borderColor: '#4E76DE',
						//   pointBorderColor: '#4E76DE',
						//   data: [40, 85, 90, 20, 60],
						//   fill: true, // true 일 경우 색상이 채워진다.
						//   lineTension: 0.4, // 곡선 라인
						//   pointStyle: ['circle', 'circle', 'crossRot', 'circle', 'circle'],
						// },
						{
							label: '일 평균',
							backgroundColor: 'rgba(78,118,222,0.3)',
							pointBackgroundColor: '#fff',
							borderWidth: 2,
							borderColor: '#4E76DE',
							pointBorderColor: '#4E76DE',
							data: [40, 85, 90, 20, 60],
							fill: true, // true 일 경우 색상이 채워진다.
							lineTension: 0.4, // 곡선 라인
							pointStyle: ['circle', 'circle', 'crossRot', 'circle', 'circle'],
						},
						{
							label: '최소값',
							backgroundColor: 'transparent',
							borderWidth: 1,
							borderDash: [5, 5],
							borderColor: '#000',
							pointBorderColor: '#000',
							data: [10, 10, 10, 10, 10],
							pointBackgroundColor: '#fff',
							fill: false,
							pointRadius: 0,
							pointHitRadius: 5,
						},
						{
							label: '최대값',
							backgroundColor: 'rgba(78,118,222,0.3)',
							pointBackgroundColor: '#fff',
							borderWidth: 1,
							borderColor: '#C0502A',
							pointBorderColor: '#C0502A',
							data: [10, 10, 10, 10, 10],
							fill: false,
							lineTension: 0.4,
						},
					],
				},
				options: {
					grid: {
						display: true,
					},
					interaction: {
						intersect: false,
					},
					scales: {
						y: {
							// display: true,
							// suggestedMin: 0,
							// suggestedMax: 100,
							ticks: {
								stepSize: 1,
								callback(val, index, ticks) {
									const range = Math.round(
										(ticks[ticks.length - 1].value - ticks[0].value) / 4,
									);
									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;
									}
								},
							},
						},
					},
					plugins: {
						legend: {
							display: true,
						},
						tooltip: {
							enabled: true,
							position: 'nearest',
							intersect: false,
							alignment: 'center',
							custom(tooltip) {
								if (!tooltip) {
									return;
								}
								tooltip.displayColors = false;
							},
							callbacks: {
								title(tooltipItem) {
									const { label } = tooltipItem[0];
									// const { datasetIndex, label } = tooltipItem[0];
									// return datasetIndex === 0 ? label : '평균';
									return label;
								},
							},
						},
						zoom: {
							pan: {
								enabled: true,
								mode: 'x',
							},
							zoom: {
								mode: 'x',
								wheel: {
									enabled: false,
								},
								pinch: {
									enabled: false,
								},
							},
						},
					},
					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.update();
					},
				},
				plugins: [
					// 클릭했을 때 차트 테두리 설정
					{
						id: 'chartAreaBorder',
						beforeDraw(chart) {
							const {
								ctx,
								chartArea: { left, top, width, height },
							} = chart;
							if (chart.options.plugins.zoom.zoom.wheel.enabled) {
								ctx.save();
								ctx.strokeStyle = 'blue';
								ctx.lineWidth = 1;
								ctx.strokeRect(left, top, width, height);
								ctx.restore();
							}
						},
					},
				],
			},
			// 측정값 또는 일평균
			dataSetType1: {
				label: '',
				backgroundColor: 'rgba(78,118,222,0.3)',
				pointBackgroundColor: '#fff',
				borderWidth: 2,
				// borderColor: 'transparent',
				borderColor: '#4E76DE',
				// pointBorderColor: 'transparent',
				pointBorderColor: '#4E76DE',
				data: [],
				fill: true, // true 일 경우 색상이 채워진다.
				stepped: true,
				tension: 0.1,
				pointRadius: 0.1,
				pointHoverRadius: 0.1,
			},
			// 최소값
			dataSetType2: {
				label: '',
				backgroundColor: 'transparent',
				borderWidth: 1,
				borderDash: [5, 5],
				borderColor: '#000',
				pointBorderColor: '#000',
				data: [],
				pointBackgroundColor: '#fff',
				fill: false,
				stepped: true,
				tension: 0.1,
				pointRadius: 0,
				pointHoverRadius: 0,
			},
			// 최대값
			dataSetType3: {
				label: '',
				backgroundColor: 'rgba(78,118,222,0.3)',
				pointBackgroundColor: '#fff',
				borderWidth: 1,
				borderColor: '#C0502A',
				pointBorderColor: '#C0502A',
				data: [],
				fill: false,
				stepped: true,
				tension: 0.1,
				pointRadius: 0,
				pointHoverRadius: 0,
			},
			// 검색 기간이 2일 이하일 때 생성하는 배열
			lessThanTwoDaysData: {},
		};
	},
	computed: {
		...mapGetters('info', [
			'deviceChartDataList',
			'deviceChartMaxDataList',
			'deviceChartMinDataList',
		]),
	},
	watch: {
		deviceChartDataList() {
			this.setChartData();
			this.initChart(this.$refs.lineChart, this.chartData);
		},
	},
	beforeDestroy() {
		// 차트 정보 삭제
		if (this.myChart) {
			this.myChart.destroy();
		}
	},
	mounted() {
		this.setChartData();
		this.initChart(this.$refs.lineChart, this.chartData);
	},
	methods: {
		initChart(ref, chartData) {
			const ctx = ref.getContext('2d');
			const gradient = ctx.createLinearGradient(0, 0, 0, 400);

			gradient.addColorStop(0, 'rgba(78,118,222,0.3)');
			gradient.addColorStop(1, 'rgba(78,118,222,0.3)');

			this.chartData.data.datasets[0].backgroundColor = gradient;
			if (this.myChart) {
				this.myChart.destroy();
			}
			this.myChart = new Chart(ctx, {
				type: chartData.type,
				data: chartData.data,
				options: chartData.options,
				plugins: chartData.plugins,
			});
		},
		initData() {
			if (this.period === 0) {
				this.lessThanTwoDaysData = {};
				// 검색 기간이 하루일 때, 1분마다 데이터 1개씩, 1 * 60 * 24
				Array(60 * 24)
					.fill()
					.forEach((_, i) => {
						const h =
							Math.floor(i / 60) < 10
								? `0${Math.floor(i / 60)}`
								: Math.floor(i / 60);
						const m = i % 60 < 10 ? `0${i % 60}` : i % 60;
						this.$set(
							this.lessThanTwoDaysData,
							`${this.periodDt.startDt} ${h}:${m}`,
							NaN,
						);
					});
			} else if (this.period === 1) {
				this.lessThanTwoDaysData = {};
				// 검색 기간이 2틀일 때,
				Array(2 * 60 * 24)
					.fill()
					.forEach((_, i) => {
						let h =
							Math.floor(i / 60) < 10
								? `0${Math.floor(i / 60)}`
								: Math.floor(i / 60);
						h = h < 24 ? h : h - 24 < 10 ? `0${h - 24}` : h - 24;
						const m = i % 60 < 10 ? `0${i % 60}` : i % 60;
						if (i < 1440) {
							this.$set(
								this.lessThanTwoDaysData,
								`${this.periodDt.startDt} ${h}:${m}`,
								NaN,
							);
						} else {
							this.$set(
								this.lessThanTwoDaysData,
								`${this.periodDt.endDt} ${h}:${m}`,
								NaN,
							);
						}
					});
			}
		},
		setChartData() {
			this.initData();

			if (this.period < 2) {
				this.deviceChartDataList.forEach(v => {
					this.lessThanTwoDaysData[v.name] = v.value;
				});

				const sortObj = Object.entries(this.lessThanTwoDaysData).sort();
				const keys = sortObj.map(v => v[0]);
				const dataList = sortObj.map(v => v[1]);
				const pointStyle = dataList.map(item =>
					!isNaN(item) ? 'circle' : 'crossRot',
				);

				this.chartData.data.labels = keys.map(item => {
					return moment.getConvertFormat(item, 'MM/DD HH:mm');
				});
				this.$set(this.chartData.data, 'datasets', [
					{
						...this.dataSetType1,
						label: '측정값',
						pointStyle,
						data: dataList,
					},
				]);
			} else {
				this.deviceChartDataList.forEach(v => {
					this.lessThanTwoDaysData[v.name] = v.value;
				});

				const keys = this.deviceChartDataList.map(item => item.name);
				const values = this.deviceChartDataList.map(item => item.value);
				const dataList = values.map(item => (item !== '' ? item : 0));
				const maxValues = this.deviceChartMaxDataList.map(item => item.value);
				const maxDataList = maxValues.map(item => (item !== '' ? item : 0));
				const minValues = this.deviceChartMinDataList.map(item => item.value);
				const minDataList = minValues.map(item => (item !== '' ? item : 0));
				const pointStyle = values.map(item =>
					item !== '' ? 'circle' : 'crossRot',
				);

				this.chartData.data.labels = keys.map(item =>
					moment.getConvertFormat(item, 'MM/DD'),
				);
				this.$set(this.chartData.data, 'datasets', [
					{
						...this.dataSetType1,
						label: '일 평균',
						pointStyle,
						data: dataList,
					},
					{
						...this.dataSetType2,
						label: '최소값',
						data: minDataList,
					},
					{
						...this.dataSetType3,
						label: '최대값',
						data: maxDataList,
					},
				]);
			}
		},
	},
};
</script>

<style scoped lang="scss">
.canvas {
	cursor: pointer;
}

.canvas_area {
	position: relative;

	.tooltip {
		display: none;
		position: absolute;
		top: 0;
		left: 250px;
		padding: 10px;
		border-radius: 4px;
		background-color: rgba(0, 0, 0, 0.7);
		font-size: 14px;
		color: #fff;
		transform: translateY(-100%);

		&:before {
			position: absolute;
			bottom: -8px;
			left: 4px;
			width: 0;
			border-top: 8px solid rgba(0, 0, 0, 0.7);
			border-left: 8px solid transparent;
			border-right: 8px solid transparent;
			content: '';
		}
	}

	&:hover {
		.tooltip {
			display: block;
		}
	}
}
</style>


chart.js 2.9.4 다운그레이드

chartjs-plugin-zoom 0.7.7 다운그레이드

그래야 IE11 지원 가능