UNPKG

apexcharts

Version:

A JavaScript Chart Library

628 lines (542 loc) 18.4 kB
import Graphics from './Graphics' import Utils from './../utils/Utils' import Toolbar from './Toolbar' /** * ApexCharts Zoom Class for handling zooming and panning on axes based charts. * * @module ZoomPanSelection **/ export default class ZoomPanSelection extends Toolbar { constructor (ctx) { super(ctx) this.ctx = ctx this.w = ctx.w this.dragged = false this.graphics = new Graphics(this.ctx) this.eventList = [ 'mousedown', 'mousemove', 'touchstart', 'touchmove', 'mouseup', 'touchend' ] this.clientX = 0 this.clientY = 0 this.startX = 0 this.endX = 0 this.dragX = 0 this.startY = 0 this.endY = 0 this.dragY = 0 } init ({ xyRatios }) { let w = this.w var me = this this.xyRatios = xyRatios this.zoomRect = this.graphics.drawRect(0, 0, 0, 0) this.selectionRect = this.graphics.drawRect(0, 0, 0, 0) this.gridRect = w.globals.dom.baseEl.querySelector('.apexcharts-grid') this.zoomRect.node.classList.add('apexcharts-zoom-rect') this.selectionRect.node.classList.add('apexcharts-selection-rect') w.globals.dom.elGraphical.add(this.zoomRect) w.globals.dom.elGraphical.add(this.selectionRect) if (w.config.chart.selection.type === 'x') { this.slDraggableRect = this.selectionRect.draggable({ minX: 0, minY: 0, maxX: w.globals.gridWidth, maxY: w.globals.gridHeight }).on('dragmove', this.selectionDragging.bind(this, 'dragging')) } else if (w.config.chart.selection.type === 'y') { this.slDraggableRect = this.selectionRect.draggable({ minX: 0, maxX: w.globals.gridWidth }).on('dragmove', this.selectionDragging.bind(this, 'dragging')) } else { this.slDraggableRect = this.selectionRect.draggable().on('dragmove', this.selectionDragging.bind(this, 'dragging')) } this.preselectedSelection() this.hoverArea = w.globals.dom.baseEl.querySelector(w.globals.chartClass) this.hoverArea.classList.add('zoomable') for (let event of this.eventList) { this.hoverArea.addEventListener( event, me.svgMouseEvents.bind(me, xyRatios), {capture: false, passive: true} ) } } // remove the event listeners which were previously added on hover area destroy () { const me = this for (let event of this.eventList) { if (this.hoverArea) { this.hoverArea.removeEventListener( event, me.svgMouseEvents.bind(me, me.xyRatios), {capture: false, passive: true} ) } } if (this.slDraggableRect) { this.slDraggableRect.draggable(false) this.slDraggableRect.off() this.selectionRect.off() } this.selectionRect = null this.zoomRect = null this.gridRect = null } svgMouseEvents (xyRatios, e) { let w = this.w let me = this const toolbar = this.ctx.toolbar let zoomtype = w.globals.zoomEnabled ? w.config.chart.zoom.type : w.config.chart.selection.type if (e.shiftKey) { this.shiftWasPressed = true toolbar.enablePanning() } else { if (this.shiftWasPressed) { toolbar.enableZooming() this.shiftWasPressed = false } } const falsePositives = e.target.classList.contains('apexcharts-selection-rect') || e.target.parentNode.classList.contains('apexcharts-toolbar') if (falsePositives) return me.clientX = e.type === 'touchmove' || e.type === 'touchstart' ? e.touches[0].clientX : (e.type === 'touchend' ? e.changedTouches[0].clientX : e.clientX) me.clientY = e.type === 'touchmove' || e.type === 'touchstart' ? e.touches[0].clientY : (e.type === 'touchend' ? e.changedTouches[0].clientY : e.clientY) if ((e.type === 'mousedown' && e.which === 1)) { let gridRectDim = me.gridRect.getBoundingClientRect() me.startX = me.clientX - gridRectDim.left me.startY = me.clientY - gridRectDim.top me.dragged = false me.w.globals.mousedown = true } if ((e.type === 'mousemove' && e.which === 1) || (e.type === 'touchmove')) { me.dragged = true if (w.globals.panEnabled) { w.globals.selection = null if (me.w.globals.mousedown) { me.panDragging({ context: me, zoomtype, xyRatios }) } } else { if ((me.w.globals.mousedown && w.globals.zoomEnabled) || (me.w.globals.mousedown && w.globals.selectionEnabled)) { me.selection = me.selectionDrawing({ context: me, zoomtype }) } } } if ((e.type === 'mouseup') || e.type === 'touchend') { // we will be calling getBoundingClientRect on each mousedown/mousemove/mouseup let gridRectDim = me.gridRect.getBoundingClientRect() if (me.w.globals.mousedown) { // user released the drag, now do all the calculations me.endX = me.clientX - gridRectDim.left me.endY = me.clientY - gridRectDim.top me.dragX = Math.abs(me.endX - me.startX) me.dragY = Math.abs(me.endY - me.startY) if (w.globals.zoomEnabled || w.globals.selectionEnabled) { me.selectionDrawn({ context: me, zoomtype }) } } if (w.globals.zoomEnabled) { me.hideSelectionRect(this.selectionRect) } me.dragged = false me.w.globals.mousedown = false } this.makeSelectionRectDraggable() } makeSelectionRectDraggable () { const w = this.w if (!this.selectionRect) return const rectDim = this.selectionRect.node.getBoundingClientRect() if (rectDim.width > 0 && rectDim.height > 0) { this.slDraggableRect.selectize().resize({ constraint: { minX: 0, minY: 0, maxX: w.globals.gridWidth, maxY: w.globals.gridHeight } }).on('resizing', this.selectionDragging.bind(this, 'resizing')) } } preselectedSelection () { const w = this.w const xyRatios = this.xyRatios if (!w.globals.zoomEnabled) { if (typeof w.globals.selection !== 'undefined' && w.globals.selection !== null) { this.drawSelectionRect(w.globals.selection) } else { if (w.config.chart.selection.xaxis.min !== undefined && w.config.chart.selection.xaxis.max !== undefined) { const x = (w.config.chart.selection.xaxis.min - w.globals.minX) / xyRatios.xRatio const width = w.globals.gridWidth - ((w.globals.maxX - w.config.chart.selection.xaxis.max) / xyRatios.xRatio) - x let selectionRect = { x, y: 0, width, height: w.globals.gridHeight, translateX: 0, translateY: 0, selectionEnabled: true } this.drawSelectionRect(selectionRect) this.makeSelectionRectDraggable() if (typeof w.config.chart.events.selection === 'function') { w.config.chart.events.selection(this.ctx, { xaxis: { min: w.config.chart.selection.xaxis.min, max: w.config.chart.selection.xaxis.max }, yaxis: {} }) } } } } } drawSelectionRect ({ x, y, width, height, translateX, translateY }) { const w = this.w const zoomRect = this.zoomRect const selectionRect = this.selectionRect if (this.dragged || w.globals.selection !== null) { let scalingAttrs = { transform: 'translate(' + translateX + ', ' + translateY + ')' } // change styles based on zoom or selection // zoom is Enabled and user has dragged, so draw blue rect if (w.globals.zoomEnabled && this.dragged) { zoomRect.attr({ x, y, width, height, fill: w.config.chart.zoom.zoomedArea.fill.color, 'fill-opacity': w.config.chart.zoom.zoomedArea.fill.opacity, stroke: w.config.chart.zoom.zoomedArea.stroke.color, 'stroke-width': w.config.chart.zoom.zoomedArea.stroke.width, 'stroke-opacity': w.config.chart.zoom.zoomedArea.stroke.opacity }) Graphics.setAttrs(zoomRect.node, scalingAttrs) } // selection is enabled if (w.globals.selectionEnabled) { selectionRect.attr({ x, y, width: width > 0 ? width : 0, height: height > 0 ? height : 0, fill: w.config.chart.selection.fill.color, 'fill-opacity': w.config.chart.selection.fill.opacity, stroke: w.config.chart.selection.stroke.color, 'stroke-width': w.config.chart.selection.stroke.width, 'stroke-dasharray': w.config.chart.selection.stroke.dashArray, 'stroke-opacity': w.config.chart.selection.stroke.opacity }) Graphics.setAttrs(selectionRect.node, scalingAttrs) } } } hideSelectionRect (rect) { if (rect) { rect.attr({ x: 0, y: 0, width: 0, height: 0 }) } } selectionDrawing ({ context, zoomtype }) { const w = this.w let me = context let gridRectDim = this.gridRect.getBoundingClientRect() let startX = me.startX - 1 let startY = me.startY let selectionWidth = me.clientX - gridRectDim.left - startX let selectionHeight = me.clientY - gridRectDim.top - startY let translateX = 0 let translateY = 0 let selectionRect = {} if (Math.abs(selectionWidth + startX) > w.globals.gridWidth || me.clientX - gridRectDim.left < 0) { // user dragged the mouse outside drawing area // TODO: test the selectionRect and make sure it doesn't crosses drawing area me.hideSelectionRect(this.zoomRect) me.dragged = false me.w.globals.mousedown = false } // inverse selection X if (startX > me.clientX - gridRectDim.left) { selectionWidth = Math.abs(selectionWidth) translateX = -selectionWidth } // inverse selection Y if (startY > me.clientY - gridRectDim.top) { selectionHeight = Math.abs(selectionHeight) translateY = -selectionHeight } if (zoomtype === 'x') { selectionRect = { x: startX, y: 0, width: selectionWidth, height: w.globals.gridHeight, translateX, translateY: 0 } } else if (zoomtype === 'y') { selectionRect = { x: 0, y: startY, width: w.globals.gridWidth, height: selectionHeight, translateX: 0, translateY } } else { selectionRect = { x: startX, y: startY, width: selectionWidth, height: selectionHeight, translateX, translateY } } me.drawSelectionRect(selectionRect) return selectionRect } selectionDragging (type, e) { const w = this.w const xyRatios = this.xyRatios const selRect = this.selectionRect let timerInterval = 0 if (type === 'resizing') { timerInterval = 30 } if (typeof w.config.chart.events.selection === 'function') { // a small debouncer is required when resizing to avoid freezing the chart clearTimeout(this.w.globals.selectionResizeTimer) this.w.globals.selectionResizeTimer = window.setTimeout(() => { const gridRectDim = this.gridRect.getBoundingClientRect() const selectionRect = selRect.node.getBoundingClientRect() const minX = w.globals.xAxisScale.niceMin + (selectionRect.left - gridRectDim.left) * xyRatios.xRatio const maxX = w.globals.xAxisScale.niceMin + (selectionRect.right - gridRectDim.left) * xyRatios.xRatio const minY = w.globals.yAxisScale[0].niceMin + (gridRectDim.bottom - selectionRect.bottom) * xyRatios.yRatio[0] const maxY = w.globals.yAxisScale[0].niceMax - (selectionRect.top - gridRectDim.top) * xyRatios.yRatio[0] w.config.chart.events.selection(this.ctx, { xaxis: { min: minX, max: maxX }, yaxis: { min: minY, max: maxY } }) }, timerInterval) } } selectionDrawn ({ context, zoomtype }) { const w = this.w const me = context const xyRatios = this.xyRatios const toolbar = this.ctx.toolbar if (me.startX > me.endX) { let tempX = me.startX me.startX = me.endX me.endX = tempX } if (me.startY > me.endY) { let tempY = me.startY me.startY = me.endY me.endY = tempY } let xLowestValue = w.globals.xAxisScale.niceMin + me.startX * xyRatios.xRatio let xHighestValue = w.globals.xAxisScale.niceMin + me.endX * xyRatios.xRatio // TODO: we will consider the 1st y axis values here for getting highest and lowest y let yHighestValue = [] let yLowestValue = [] w.config.yaxis.forEach((yaxe, index) => { yHighestValue.push(Math.floor( w.globals.yAxisScale[index].niceMax - xyRatios.yRatio[index] * me.startY )) yLowestValue.push(Math.floor( w.globals.yAxisScale[index].niceMax - xyRatios.yRatio[index] * me.endY )) }) if ( me.dragged && (me.dragX > 10 || me.dragY > 10) && xLowestValue !== xHighestValue ) { if (w.globals.zoomEnabled) { let yaxis = Utils.clone(w.config.yaxis) // before zooming in/out, store the last yaxis and xaxis range, so that when user hits the RESET button, we get the original range // also - make sure user is not already zoomed in/out - otherwise we will store zoomed values in lastAxis if (!w.globals.zoomed) { w.globals.lastXAxis = Utils.clone(w.config.xaxis) w.globals.lastYAxis = Utils.clone(w.config.yaxis) } let xaxis = { min: xLowestValue, max: xHighestValue } if (zoomtype === 'xy' || zoomtype === 'y') { yaxis.forEach((yaxe, index) => { yaxis[index].min = yLowestValue[index] yaxis[index].max = yHighestValue[index] }) } if (toolbar) { let beforeZoomRange = toolbar.getBeforeZoomRange(xaxis, yaxis) if (beforeZoomRange) { xaxis = beforeZoomRange.xaxis ? beforeZoomRange.xaxis : xaxis yaxis = beforeZoomRange.yaxis ? beforeZoomRange.yaxe : yaxis } } if (zoomtype === 'x') { me.ctx._updateOptions({ xaxis }, false, me.w.config.chart.animations.dynamicAnimation.enabled ) } else if (zoomtype === 'y') { me.ctx._updateOptions({ yaxis }, false, me.w.config.chart.animations.dynamicAnimation.enabled ) } else { me.ctx._updateOptions({ xaxis, yaxis }, false, me.w.config.chart.animations.dynamicAnimation.enabled ) } if (typeof w.config.chart.events.zoomed === 'function') { toolbar.zoomCallback(xaxis, yaxis) } w.globals.zoomed = true } else if (w.globals.selectionEnabled) { let yaxis = null; let xaxis = null xaxis = { min: xLowestValue, max: xHighestValue } if (zoomtype === 'xy' || zoomtype === 'y') { yaxis = Utils.clone(w.config.yaxis) yaxis.forEach((yaxe, index) => { yaxis[index].min = yLowestValue[index] yaxis[index].max = yHighestValue[index] }) } w.globals.selection = me.selection if (typeof w.config.chart.events.selection === 'function') { w.config.chart.events.selection(me.ctx, { xaxis, yaxis }) } } } } panDragging ({ context, zoomtype }) { const w = this.w let me = context let moveDirection // check to make sure there is data to compare against if (typeof (w.globals.lastClientPosition.x) !== 'undefined') { // get the change from last position to this position const deltaX = w.globals.lastClientPosition.x - me.clientX const deltaY = w.globals.lastClientPosition.y - me.clientY // check which direction had the highest amplitude and then figure out direction by checking if the value is greater or less than zero if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX > 0) { moveDirection = 'left' } else if (Math.abs(deltaX) > Math.abs(deltaY) && deltaX < 0) { moveDirection = 'right' } else if (Math.abs(deltaY) > Math.abs(deltaX) && deltaY > 0) { moveDirection = 'up' } else if (Math.abs(deltaY) > Math.abs(deltaX) && deltaY < 0) { moveDirection = 'down' } } // set the new last position to the current for next time (to get the position of drag) w.globals.lastClientPosition = { x: me.clientX, y: me.clientY } let xLowestValue = w.globals.minX let xHighestValue = w.globals.maxX this.panScrolled(moveDirection, xLowestValue, xHighestValue) } panScrolled (moveDirection, xLowestValue, xHighestValue) { const w = this.w const xyRatios = this.xyRatios if (moveDirection === 'left') { xLowestValue = w.globals.minX + ((w.globals.gridWidth / 15) * xyRatios.xRatio) xHighestValue = w.globals.maxX + ((w.globals.gridWidth / 15) * xyRatios.xRatio) } else if (moveDirection === 'right') { xLowestValue = w.globals.minX - ((w.globals.gridWidth / 15) * xyRatios.xRatio) xHighestValue = w.globals.maxX - ((w.globals.gridWidth / 15) * xyRatios.xRatio) } if (xLowestValue < w.globals.initialminX || xHighestValue > w.globals.initialmaxX) { xLowestValue = w.globals.minX xHighestValue = w.globals.maxX } this.ctx._updateOptions({ xaxis: { min: xLowestValue, max: xHighestValue } }, false, false ) if (typeof w.config.chart.events.scrolled === 'function') { w.config.chart.events.scrolled(this.ctx, { xaxis: { min: xLowestValue, max: xHighestValue } }) } } }