From fc99b3dcad41246747e790cec14db16f386f72a4 Mon Sep 17 00:00:00 2001 From: Mun Seong Woo Date: Mon, 12 Sep 2022 23:34:53 +0900 Subject: [PATCH] =?UTF-8?q?[#1268]=20Chart=20>=20Brush=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20######################################=20=20-=20bru?= =?UTF-8?q?sh=EB=A1=9C=20zoom=20=EC=98=81=EC=97=AD=EC=9D=84=20=EB=84=93?= =?UTF-8?q?=ED=9E=88=EA=B1=B0=EB=82=98=20=EC=A2=81=ED=9E=88=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/views/zoomChart/example/ChartBrush.vue | 76 +----- src/components/chart/chart.core.js | 1 + src/components/chart/chartZoom.core.js | 14 +- .../chart/plugins/plugins.interaction.js | 22 ++ src/components/chart/uses.js | 10 +- src/components/chartBrush/ChartBrush.vue | 8 +- src/components/chartBrush/chartBrush.core.js | 222 ++++++++++++++---- src/components/chartBrush/uses.js | 3 +- src/components/chartGroup/ChartGroup.vue | 12 +- 9 files changed, 235 insertions(+), 133 deletions(-) diff --git a/docs/views/zoomChart/example/ChartBrush.vue b/docs/views/zoomChart/example/ChartBrush.vue index a535fccea..616db581c 100644 --- a/docs/views/zoomChart/example/ChartBrush.vue +++ b/docs/views/zoomChart/example/ChartBrush.vue @@ -18,11 +18,6 @@ :options="chartOptions2" /> - -
@@ -141,24 +136,6 @@ export default { }, }); - const chartData3 = reactive({ - series: { - series1: { name: 'series#1', fill: false, point: true }, - series2: { name: 'series#2', fill: false, point: true }, - series3: { name: 'series#3', fill: false, point: true }, - series4: { name: 'series#4', fill: false, point: true }, - series5: { name: 'series#5', fill: false, point: true }, - }, - labels: [], - data: { - series1: [], - series2: [], - series3: [], - series4: [], - series5: [], - }, - }); - const chartOptions = reactive({ type: 'line', width: '100%', @@ -215,61 +192,22 @@ export default { }, }); - const chartOptions3 = reactive({ - type: 'line', - width: '100%', - title: { - text: '그룹에 있는 차트 3', - show: true, - }, - legend: { - show: false, - position: 'right', - }, - axesX: [{ - type: 'time', - showGrid: false, - timeFormat: 'HH:mm:ss', - interval: 'second', - }], - axesY: [{ - type: 'linear', - showGrid: true, - startToZero: true, - autoScaleRatio: 0.1, - }], - maxTip: { - use: true, - showIndicator: true, - indicatorColor: '#FF0000', - tipBackground: '#000000', - tipTextColor: '#FFFFFF', - }, - }); - const brushOptions = reactive({ show: true, chartIdx: 0, - height: 90, + height: 100, }); const brushOptions2 = reactive({ show: true, chartIdx: 1, - height: 90, - }); - - const brushOptions3 = reactive({ - show: true, - chartIdx: 2, - height: 90, + height: 100, }); const addRandomChartData = () => { timeValue = dayjs(timeValue).add(1, 'second'); chartData.labels.push(dayjs(timeValue)); chartData2.labels.push(dayjs(timeValue)); - chartData3.labels.push(dayjs(timeValue)); Object.values(chartData.data).forEach((seriesData) => { seriesData.push(Math.floor(Math.random() * ((5000 - 5) + 1)) + 5); @@ -278,10 +216,6 @@ export default { Object.values(chartData2.data).forEach((seriesData) => { seriesData.push(Math.floor(Math.random() * ((5000 - 5) + 1)) + 5); }); - - Object.values(chartData3.data).forEach((seriesData) => { - seriesData.push(Math.floor(Math.random() * ((5000 - 5) + 1)) + 5); - }); }; onMounted(() => { @@ -304,7 +238,6 @@ export default { init(chartData); init(chartData2); - init(chartData3); for (let ix = 0; ix < Math.ceil(Math.random() * 100); ix++) { addRandomChartData(); @@ -318,13 +251,11 @@ export default { const onToggleBrush = () => { brushOptions.show = !brushOptions.show; brushOptions2.show = !brushOptions2.show; - brushOptions3.show = !brushOptions3.show; }; watch(isShowToggleLegend, (isShow) => { chartOptions.legend.show = isShow; chartOptions2.legend.show = isShow; - chartOptions3.legend.show = isShow; }); watch(isExpandChartArea, (isExpand) => { @@ -347,13 +278,10 @@ export default { chartGroupOptions, chartData, chartData2, - chartData3, chartOptions, chartOptions2, - chartOptions3, brushOptions, brushOptions2, - brushOptions3, isShowToggleLegend, isExpandChartArea, zoomRef, diff --git a/src/components/chart/chart.core.js b/src/components/chart/chart.core.js index b6c121a26..00444cd2e 100644 --- a/src/components/chart/chart.core.js +++ b/src/components/chart/chart.core.js @@ -57,6 +57,7 @@ class EvChart { this.bufferCtx = this.bufferCanvas.getContext('2d'); this.overlayCanvas = document.createElement('canvas'); this.overlayCanvas.setAttribute('style', 'display: block; z-index: 2;'); + this.overlayCanvas.setAttribute('class', 'overlay-canvas'); this.overlayCtx = this.overlayCanvas.getContext('2d'); this.pixelRatio = window.devicePixelRatio || 1; diff --git a/src/components/chart/chartZoom.core.js b/src/components/chart/chartZoom.core.js index f42dd167a..638235e80 100644 --- a/src/components/chart/chartZoom.core.js +++ b/src/components/chart/chartZoom.core.js @@ -105,7 +105,7 @@ export default class EvChartZoom { zoomMoveEndIdx = zoomEndIdx + 1; } - this.isExecuteZoomAtToolbar = true; + this.isExecutedByToolbar = true; this.executeZoom(zoomMoveStartIdx, zoomMoveEndIdx); this.zoomAreaMemory.current[0] = [zoomMoveStartIdx, zoomMoveEndIdx]; } @@ -117,7 +117,7 @@ export default class EvChartZoom { const [zoomStartIdx, zoomEndIdx] = this.zoomAreaMemory[direction].pop(); - this.isExecuteZoomAtToolbar = true; + this.isExecutedByToolbar = true; this.executeZoom(zoomStartIdx, zoomEndIdx); this.setZoomAreaMemory(zoomStartIdx, zoomEndIdx, direction === 'previous' ? 'latest' : 'previous'); } @@ -228,7 +228,7 @@ export default class EvChartZoom { } this.isAnimationFinish = false; - this.isExecuteZoomAtToolbar = true; + this.isExecutedByToolbar = true; this.executeDragZoomAnimation( displayCanvas, animationCtx, @@ -275,8 +275,10 @@ export default class EvChartZoom { ); } - this.brushIdx.start = zoomStartIdx; - this.brushIdx.end = zoomEndIdx; + if (!this.brushIdx.isExecutedByBrush) { + this.brushIdx.start = zoomStartIdx; + this.brushIdx.end = zoomEndIdx; + } if (this.emitFunc) { this.emitFunc.updateZoomStartIdx(zoomStartIdx); @@ -469,7 +471,7 @@ export default class EvChartZoom { const cloneLabelsLastIdx = this.cloneLabelsLastIdx; if (currentZoomStartIdx !== 0 || currentZoomEndIdx !== cloneLabelsLastIdx) { - this.isExecuteZoomAtToolbar = true; + this.isExecutedByToolbar = true; this.executeZoom(0, cloneLabelsLastIdx); this.setZoomAreaMemory(0, cloneLabelsLastIdx); } diff --git a/src/components/chart/plugins/plugins.interaction.js b/src/components/chart/plugins/plugins.interaction.js index 835a6159e..30aa19ae5 100644 --- a/src/components/chart/plugins/plugins.interaction.js +++ b/src/components/chart/plugins/plugins.interaction.js @@ -18,6 +18,28 @@ const modules = { return; } + if (this.options.brush) { + if (!this.brushCanvas) { + if (e.path[0].nextSibling.className === 'brush-canvas') { + this.brushCanvas = e.path[0].nextSibling; + } + } else { + const isCurMouseXInsideBrushBtn = xPos => + e.offsetX + this.evBrushChartPos.width >= this.evBrushChartPos[xPos] + && e.offsetX - this.evBrushChartPos.width <= this.evBrushChartPos[xPos]; + + if (isCurMouseXInsideBrushBtn('leftX')) { + this.overlayCanvas.style['z-index'] = 1; + this.brushCanvas.style['z-index'] = 2; + } + + if (isCurMouseXInsideBrushBtn('rightX')) { + this.overlayCanvas.style['z-index'] = 1; + this.brushCanvas.style['z-index'] = 2; + } + } + } + const { indicator, tooltip, type } = this.options; const offset = this.getMousePosition(e); const hitInfo = this.findHitItem(offset); diff --git a/src/components/chart/uses.js b/src/components/chart/uses.js index e3299b5b9..2d6fee45f 100644 --- a/src/components/chart/uses.js +++ b/src/components/chart/uses.js @@ -304,7 +304,7 @@ export const useZoomModel = ( const evChartToolbarRef = ref(); const evChartZoomOptions = reactive({ zoom: evChartNormalizedOptions.zoom }); - const brushIdx = reactive({ start: 0, end: 0 }); + const brushIdx = reactive({ start: 0, end: 0, isExecutedByBrush: false }); let evChartZoom = null; const evChartInfo = reactive({ @@ -329,7 +329,6 @@ export const useZoomModel = ( use: isUseZoomMode.value, getRangeInfo, }; - option.chartIdx = idx; if (isUseZoomMode.value) { option.dragSelection = { @@ -477,9 +476,9 @@ export const useZoomModel = ( }; const controlZoomIdx = (zoomStartIdx, zoomEndIdx) => { - if (evChartZoom.isExecuteZoomAtToolbar) { - evChartZoom.isExecuteZoomAtToolbar = false; - return; + if (evChartZoom.isExecutedByToolbar && !brushIdx.isExecutedByBrush) { + evChartZoom.isExecutedByToolbar = false; + return; } if (isUseZoomMode.value) { @@ -493,6 +492,7 @@ export const useZoomModel = ( evChartInfo, evChartToolbarRef, evChartClone, + isUseZoomMode, brushIdx, createEvChartZoom, diff --git a/src/components/chartBrush/ChartBrush.vue b/src/components/chartBrush/ChartBrush.vue index 39ae88fd8..ee4c52c6d 100644 --- a/src/components/chartBrush/ChartBrush.vue +++ b/src/components/chartBrush/ChartBrush.vue @@ -29,7 +29,8 @@ export default { let evChartBrush = null; const injectEvChartClone = inject('evChartClone', { data: [], options: [] }); - const injectBrushIdx = inject('brushIdx', { start: 0, end: 0 }); + const injectBrushIdx = inject('brushIdx', { start: 0, end: 0, isExecutedByBrush: false }); + const injectIsUseZoomMode = inject('isUseZoomMode', false); const { getNormalizedBrushOptions, @@ -53,8 +54,8 @@ export default { const evChartOption = computed(() => { const option = { ...(injectEvChartClone.options ?? [])[evChartBrushOptions.value.chartIdx], - chartIdx: evChartBrushOptions.value.chartIdx, brush: true, + brushButtonColor: evChartBrushOptions.value.buttonColor, height: evChartBrushOptions.value.height, title: { show: false, @@ -129,6 +130,7 @@ export default { evChart, evChartData, evChartOption, + injectIsUseZoomMode, injectBrushIdx, evChartBrushRef, ); @@ -146,7 +148,7 @@ export default { } }; - watch(injectBrushIdx, () => { + watch(() => [injectBrushIdx.start, injectBrushIdx.end], () => { if (evChartBrushRef.value) { drawChartBrush(); } diff --git a/src/components/chartBrush/chartBrush.core.js b/src/components/chartBrush/chartBrush.core.js index d87a11193..7c87203c9 100644 --- a/src/components/chartBrush/chartBrush.core.js +++ b/src/components/chartBrush/chartBrush.core.js @@ -1,86 +1,222 @@ +import { throttle } from 'lodash-es'; + export default class EvChartBrush { - constructor(evChart, evChartData, evChartOption, brushIdx, evChartBrushRef) { + constructor(evChart, evChartData, evChartOption, isUseZoomMode, brushIdx, evChartBrushRef) { this.evChart = evChart; this.evChartData = evChartData; this.evChartOption = evChartOption; + this.isUseZooMode = isUseZoomMode; this.brushIdx = brushIdx; this.evChartBrushRef = evChartBrushRef; } init(isResize) { - const { chartRect, labelOffset } = this.evChart; - - if (chartRect && labelOffset) { - if (this.brushIdx.start > this.brushIdx.end) { - return; - } - - const evChartRange = { - x1: chartRect.x1 + labelOffset.left, - x2: chartRect.x2 - labelOffset.right, - y1: chartRect.y1 + labelOffset.top, - y2: chartRect.y2 - labelOffset.bottom, - }; - - const existedBrushCanvas = this.evChartBrushRef.value.querySelector('.brush-canvas'); + if (this.brushIdx.start > this.brushIdx.end) { + return; + } - if (!existedBrushCanvas) { - const brushCanvas = document.createElement('canvas'); + const existedBrushCanvas = this.evChartBrushRef.value.querySelector('.brush-canvas'); - brushCanvas.setAttribute('class', 'brush-canvas'); - brushCanvas.setAttribute('style', 'display: block; z-index: 1;'); + if (!existedBrushCanvas) { + const brushCanvas = document.createElement('canvas'); - const evChartBrushContainer = this.evChartBrushRef.value.querySelector('.ev-chart-brush-container'); - evChartBrushContainer.appendChild(brushCanvas); + brushCanvas.setAttribute('class', 'brush-canvas'); + brushCanvas.setAttribute('style', 'display: block; z-index: 1; cursor: initial;'); - brushCanvas.style.position = 'absolute'; - brushCanvas.style.top = `${evChartRange.y1}px`; - brushCanvas.style.left = `${evChartRange.x1}px`; + const evChartBrushContainer = this.evChartBrushRef.value.querySelector('.ev-chart-brush-container'); + evChartBrushContainer.appendChild(brushCanvas); - this.drawBrushRect(brushCanvas, evChartRange); - } else { - this.drawBrushRect(existedBrushCanvas, evChartRange, isResize); - } + this.drawBrushRect(brushCanvas); + this.addEvent(brushCanvas); + } else { + this.drawBrushRect(existedBrushCanvas, isResize); } } - drawBrushRect(canvas, evChartRange, isResize) { + drawBrushRect(brushCanvas, isResize) { + const { chartRect, labelOffset } = this.evChart; + if (!chartRect && !labelOffset) { + return; + } + + const evChartRange = { + x1: chartRect.x1 + labelOffset.left, + x2: chartRect.x2 - labelOffset.right, + y1: chartRect.y1 + labelOffset.top, + y2: chartRect.y2 - labelOffset.bottom, + }; + const pixelRatio = window.devicePixelRatio || 1; - const brushCanvasWidth = evChartRange.x2 - evChartRange.x1; + const brushButtonWidth = 6; + const brushCanvasWidth = evChartRange.x2 - evChartRange.x1 + brushButtonWidth; const brushCanvasHeight = evChartRange.y2 - evChartRange.y1; - const isEqualWidth = canvas.width === Math.floor( - (brushCanvasWidth) * pixelRatio, - ); + const isEqualWidth = brushCanvas.width === Math.floor(brushCanvasWidth * pixelRatio); if (isResize && isEqualWidth) { return; } const labelEndIdx = this.evChartData.value.labels.length - 1; - const axesXInterval = brushCanvasWidth / labelEndIdx; + const axesXInterval = (evChartRange.x2 - evChartRange.x1) / labelEndIdx; const brushRectX = this.brushIdx.start * axesXInterval * pixelRatio; const brushRectWidth = ( brushCanvasWidth - (labelEndIdx - (this.brushIdx.end - this.brushIdx.start)) * axesXInterval ) * pixelRatio; + const brushRectHeight = this.evChartOption.value.height - evChartRange.y1; + const brushButtonLeftXPos = brushRectX; + const brushButtonRightXPos = brushRectX + brushRectWidth - brushButtonWidth; + + this.evBrushChartPos = { + leftX: (brushButtonLeftXPos / pixelRatio) + evChartRange.x1, + rightX: (brushButtonRightXPos / pixelRatio) + evChartRange.x1, + width: brushButtonWidth, + leftLabelX: evChartRange.x1 - (brushButtonWidth / 2), + axesXInterval, + }; + this.evChart.evBrushChartPos = this.evBrushChartPos; + + if (!brushCanvas.style.position) { + brushCanvas.style.position = 'absolute'; + brushCanvas.style.top = `${evChartRange.y1}px`; + brushCanvas.style.left = `${evChartRange.x1 - (brushButtonWidth / 2)}px`; + } if (!isEqualWidth) { - canvas.width = (brushCanvasWidth) * pixelRatio; - canvas.style.width = `${brushCanvasWidth}px`; - canvas.height = (brushCanvasHeight) * pixelRatio; - canvas.style.height = `${brushCanvasHeight}px`; + brushCanvas.width = (brushCanvasWidth * pixelRatio); + brushCanvas.style.width = `${brushCanvasWidth}px`; + brushCanvas.height = brushCanvasHeight * pixelRatio; + brushCanvas.style.height = `${brushCanvasHeight}px`; } - const ctx = canvas.getContext('2d'); + const ctx = brushCanvas.getContext('2d'); + ctx.clearRect( 0, 0, - (brushCanvasWidth) * pixelRatio, - this.evChartOption.value.height - evChartRange.y1, + brushCanvasWidth * pixelRatio, + brushCanvasHeight * pixelRatio, ); ctx.fillStyle = this.evChartOption.value.dragSelection.fillColor; ctx.globalAlpha = this.evChartOption.value.dragSelection.opacity; - ctx.fillRect(brushRectX, 0, brushRectWidth, this.evChartOption.value.height - evChartRange.y1); + ctx.fillRect(brushRectX, 0, brushRectWidth, brushRectHeight); + + ctx.globalAlpha = 1; + ctx.fillStyle = this.evChartOption.value.brushButtonColor; + ctx.fillRect(brushButtonLeftXPos, 0, brushButtonWidth, brushRectHeight); + ctx.fillRect(brushButtonRightXPos, 0, brushButtonWidth, brushRectHeight); + } + + addEvent(brushCanvas) { + if (!this.overlayCanvas) { + if (this.evChartBrushRef.value.querySelector('.overlay-canvas')) { + this.overlayCanvas = this.evChartBrushRef.value.querySelector('.overlay-canvas'); + } + } + + let isClickBrushButton = false; + let beforeMouseXPos = 0; + let curClickButtonType = null; + + const onMouseMove = (e) => { + const evBrushChartPos = this.evBrushChartPos; + + if (brushCanvas.style.cursor === 'initial' && this.isUseZooMode.value) { + brushCanvas.style.cursor = 'ew-resize'; + } + + if (isClickBrushButton) { + if (!this.isUseZooMode.value) { + return; + } + + if (!curClickButtonType) { + this.brushIdx.isExecutedByBrush = true; + const calDisToCurMouseX = xPos => Math.abs( + evBrushChartPos[xPos] - evBrushChartPos.leftLabelX - e.offsetX, + ); + + curClickButtonType = calDisToCurMouseX('rightX') > calDisToCurMouseX('leftX') ? 'leftX' : 'rightX'; + return; + } + + const brushButtonSensitivity = evBrushChartPos.axesXInterval / 3; + if (e.offsetX > beforeMouseXPos) { + const isMoveRight = e.offsetX - ( + evBrushChartPos[curClickButtonType] - evBrushChartPos.leftLabelX + ) > brushButtonSensitivity; + + if (isMoveRight && curClickButtonType === 'leftX') { + if (this.brushIdx.start < this.brushIdx.end - 1) { + this.brushIdx.start += 1; + } + } else if (isMoveRight && curClickButtonType === 'rightX') { + if (this.brushIdx.end !== this.evChartData.value.labels.length - 1) { + this.brushIdx.end += 1; + } + } + } else if (e.offsetX < beforeMouseXPos) { + const isMoveLeft = evBrushChartPos[curClickButtonType] + - evBrushChartPos.leftLabelX - e.offsetX > brushButtonSensitivity; + + if (isMoveLeft && curClickButtonType === 'leftX') { + if (this.brushIdx.start !== 0) { + this.brushIdx.start -= 1; + } + } else if (isMoveLeft && curClickButtonType === 'rightX') { + if (this.brushIdx.start < this.brushIdx.end - 1) { + this.brushIdx.end -= 1; + } + } + } + + beforeMouseXPos = e.offsetX; + } else { + const moveRight = xPos => + e.offsetX + evBrushChartPos.leftLabelX - evBrushChartPos.width > evBrushChartPos[xPos]; + const moveLeft = xPos => + e.offsetX + evBrushChartPos.leftLabelX + evBrushChartPos.width < evBrushChartPos[xPos]; + + const isCurMouseXOutsideBrush = moveLeft('leftX') || moveRight('rightX'); + const isCurMouseXInsideBrush = moveRight('leftX') && moveLeft('rightX'); + + if (isCurMouseXOutsideBrush) { + this.overlayCanvas.style['z-index'] = 2; + brushCanvas.style['z-index'] = 1; + } + + if (isCurMouseXInsideBrush) { + this.overlayCanvas.style['z-index'] = 2; + brushCanvas.style['z-index'] = 1; + } + } + }; + + const onMouseDown = (e) => { + e.preventDefault(); + isClickBrushButton = true; + }; + + const initState = () => { + brushCanvas.style.cursor = 'initial'; + this.brushIdx.isExecutedByBrush = false; + isClickBrushButton = false; + beforeMouseXPos = 0; + curClickButtonType = null; + }; + + const onMouseUp = () => { + initState(); + }; + + const onMouseLeave = () => { + initState(); + }; + + brushCanvas.addEventListener('mousemove', throttle(onMouseMove, 50)); + brushCanvas.addEventListener('mousedown', onMouseDown); + brushCanvas.addEventListener('mouseup', onMouseUp); + brushCanvas.addEventListener('mouseleave', onMouseLeave); } } diff --git a/src/components/chartBrush/uses.js b/src/components/chartBrush/uses.js index f2e1c0fff..759f0e2ea 100644 --- a/src/components/chartBrush/uses.js +++ b/src/components/chartBrush/uses.js @@ -3,7 +3,8 @@ import { defaultsDeep } from 'lodash-es'; const DEFAULT_OPTIONS = { show: true, chartIdx: 0, - height: 90, + height: 100, + buttonColor: '', }; // eslint-disable-next-line import/prefer-default-export diff --git a/src/components/chartGroup/ChartGroup.vue b/src/components/chartGroup/ChartGroup.vue index 91765501c..3c97beea8 100644 --- a/src/components/chartGroup/ChartGroup.vue +++ b/src/components/chartGroup/ChartGroup.vue @@ -62,6 +62,7 @@ export default { evChartInfo, evChartToolbarRef, evChartClone, + isUseZoomMode, brushIdx, createEvChartZoom, @@ -72,6 +73,7 @@ export default { } = useZoomModel(normalizedOptions, { wrapper: null, evChartGroupRef }); provide('evChartClone', evChartClone); + provide('isUseZoomMode', isUseZoomMode); provide('brushIdx', brushIdx); onMounted(() => { @@ -89,7 +91,15 @@ export default { }, { deep: true }); watch(() => [props.zoomStartIdx, props.zoomEndIdx], ([zoomStartIdx, zoomEndIdx]) => { - controlZoomIdx(zoomStartIdx, zoomEndIdx); + if (!brushIdx.isExecutedByBrush) { + controlZoomIdx(zoomStartIdx, zoomEndIdx); + } + }); + + watch(() => [brushIdx.start, brushIdx.end], ([brushStartIdx, brushEndIdx]) => { + if (brushIdx.isExecutedByBrush) { + controlZoomIdx(brushStartIdx, brushEndIdx); + } }); return {