UNPKG

@nativescript-community/ui-chart

Version:

A powerful chart / graph plugin, supporting line, bar, pie, radar, bubble, and candlestick charts as well as scaling, panning and animations.

544 lines (543 loc) 23 kB
import { GestureHandlerStateEvent, GestureHandlerTouchEvent, GestureState, HandlerType, Manager } from '@nativescript-community/gesturehandler'; import { Matrix } from '@nativescript-community/ui-canvas'; import { Trace } from '@nativescript/core'; import { CLog, CLogTypes } from '../utils/Utils'; import { ChartGesture, ChartTouchListener } from './ChartTouchListener'; const LOG_TAG = 'BarLineChartTouchListener'; let TAP_HANDLER_TAG = 12450000; let DOUBLE_TAP_HANDLER_TAG = 12451000; let PINCH_HANDLER_TAG = 12452000; let PAN_HANDLER_TAG = 12453000; /** * TouchListener for Bar-, Line-, Scatter- and CandleStickChart with handles all * touch interaction. Longpress === Zoom out. Double-Tap === Zoom in. * */ export class BarLineChartTouchListener extends ChartTouchListener { constructor(chart, touchMatrix, dragTriggerDistance) { super(chart); /** * the original touch-matrix from the chart */ this.mMatrix = new Matrix(); /** * matrix for saving the original matrix state */ this.mSavedMatrix = new Matrix(); /** * center between two pointers (fingers on the display) */ this.mTouchPointCenter = { x: 0, y: 0 }; this.mSavedXDist = 1; this.mSavedYDist = 1; this.mDecelerationVelocity = { x: 0, y: 0 }; this.mMatrix = touchMatrix; this.TAP_HANDLER_TAG = TAP_HANDLER_TAG++; this.DOUBLE_TAP_HANDLER_TAG = DOUBLE_TAP_HANDLER_TAG++; this.PINCH_HANDLER_TAG = PINCH_HANDLER_TAG++; this.PAN_HANDLER_TAG = PAN_HANDLER_TAG++; } getPanGestureOptions() { return { gestureTag: this.PAN_HANDLER_TAG, ...(this.chart.panGestureOptions || {}) }; } getPinchGestureOptions() { return { gestureTag: this.PINCH_HANDLER_TAG, ...(this.chart.pinchGestureOptions || {}) }; } getTapGestureOptions() { return { gestureTag: this.TAP_HANDLER_TAG, ...(this.chart.tapGestureOptions || {}) }; } getDoubleTapGestureOptions() { return { gestureTag: this.DOUBLE_TAP_HANDLER_TAG, ...(this.chart.doubleTapGestureOptions || {}) }; } getOrCreateDoubleTapGestureHandler() { if (!this.doubleTapGestureHandler) { const manager = Manager.getInstance(); if (Trace.isEnabled()) { CLog(CLogTypes.log, LOG_TAG, 'creating double tap gesture'); } const options = this.getDoubleTapGestureOptions(); this.doubleTapGestureHandler = manager.createGestureHandler(HandlerType.TAP, options.gestureTag, { numberOfTaps: 2, ...options }); this.doubleTapGestureHandler.on(GestureHandlerStateEvent, this.onDoubleTapGesture, this); } return this.doubleTapGestureHandler; } getOrCreateTapGestureHandler() { if (!this.tapGestureHandler) { if (Trace.isEnabled()) { CLog(CLogTypes.log, LOG_TAG, 'creating tap gesture'); } const manager = Manager.getInstance(); const options = this.getTapGestureOptions(); const doubleTapOptions = this.getDoubleTapGestureOptions(); this.tapGestureHandler = manager .createGestureHandler(HandlerType.TAP, options.gestureTag, { waitFor: [doubleTapOptions.gestureTag], ...options }) .on(GestureHandlerStateEvent, this.onTapGesture, this); } return this.tapGestureHandler; } getOrCreatePinchGestureHandler() { if (!this.pinchGestureHandler) { const manager = Manager.getInstance(); if (Trace.isEnabled()) { CLog(CLogTypes.log, LOG_TAG, 'creating pinch gesture'); } const panOptions = this.getPanGestureOptions(); const pinchOptions = this.getPinchGestureOptions(); this.pinchGestureHandler = manager .createGestureHandler(HandlerType.PINCH, pinchOptions.gestureTag, { minSpan: 20, simultaneousHandlers: [panOptions.gestureTag], shouldCancelWhenOutside: false, ...pinchOptions }) .on(GestureHandlerStateEvent, this.onPinchGestureState, this) .on(GestureHandlerTouchEvent, this.onPinchGestureTouch, this); } return this.pinchGestureHandler; } getOrCreatePanGestureHandler() { if (!this.panGestureHandler) { const manager = Manager.getInstance(); if (Trace.isEnabled()) { CLog(CLogTypes.log, LOG_TAG, 'creating pan gestures'); } const panOptions = this.getPanGestureOptions(); const pinchOptions = this.getPinchGestureOptions(); this.panGestureHandler = manager .createGestureHandler(HandlerType.PAN, panOptions.gestureTag, { simultaneousHandlers: [pinchOptions.gestureTag], minPointers: 1, maxPointers: 2, shouldCancelWhenOutside: false, ...panOptions }) .on(GestureHandlerStateEvent, this.onPanGestureState, this) .on(GestureHandlerTouchEvent, this.onPanGestureTouch, this); } return this.panGestureHandler; } setDoubleTap(enabled) { const chart = this.chart; if (enabled) { this.getOrCreateDoubleTapGestureHandler().attachToView(chart); if (Trace.isEnabled()) { CLog(CLogTypes.log, LOG_TAG, 'enabling double tap gestures'); } } else if (this.doubleTapGestureHandler) { if (Trace.isEnabled()) { CLog(CLogTypes.log, LOG_TAG, 'disabling double tap gestures'); } this.doubleTapGestureHandler.detachFromView(chart); } } setTap(enabled) { const chart = this.chart; if (enabled) { this.getOrCreateTapGestureHandler().attachToView(chart); if (Trace.isEnabled()) { CLog(CLogTypes.log, LOG_TAG, 'enabling tap gestures'); } } else if (this.tapGestureHandler) { if (Trace.isEnabled()) { CLog(CLogTypes.log, LOG_TAG, 'disabling tap gestures'); } this.tapGestureHandler.detachFromView(chart); } } setPinch(enabled) { const chart = this.chart; if (enabled) { this.getOrCreatePinchGestureHandler().attachToView(chart); if (Trace.isEnabled()) { CLog(CLogTypes.log, LOG_TAG, 'enabling pinch gestures'); } } else if (this.pinchGestureHandler) { if (Trace.isEnabled()) { CLog(CLogTypes.log, LOG_TAG, 'disabling pinch gestures'); } this.pinchGestureHandler.detachFromView(chart); } } setPan(enabled) { const chart = this.chart; if (enabled) { this.getOrCreatePanGestureHandler().attachToView(chart); if (Trace.isEnabled()) { CLog(CLogTypes.log, LOG_TAG, 'enabling pan gestures'); } } else if (this.panGestureHandler) { if (Trace.isEnabled()) { CLog(CLogTypes.log, LOG_TAG, 'disabling pan gestures'); } this.panGestureHandler.detachFromView(chart); } } dispose() { super.dispose(); const chart = this.chart; this.panGestureHandler && this.panGestureHandler.detachFromView(chart); this.pinchGestureHandler && this.pinchGestureHandler.detachFromView(chart); this.tapGestureHandler && this.tapGestureHandler.detachFromView(chart); this.doubleTapGestureHandler && this.doubleTapGestureHandler.detachFromView(chart); this.panGestureHandler = null; this.pinchGestureHandler = null; this.tapGestureHandler = null; this.doubleTapGestureHandler = null; } init() { super.init(); const chart = this.chart; if (chart.doubleTapToZoomEnabled) { this.setDoubleTap(true); } if (chart.highlightPerTapEnabled) { this.setTap(true); } if (chart.highlightPerDragEnabled || chart.dragEnabled) { this.setPan(true); } if (chart.pinchZoomEnabled) { this.setPinch(true); } } onPanGestureState(event) { const chart = this.chart; if (!chart.highlightPerDragEnabled && !chart.dragEnabled) return; const state = event.data.state; chart.noComputeAutoScaleOnNextDraw = true; switch (state) { // break; case GestureState.ACTIVE: this.stopDeceleration(); this.saveTouchStart(event); break; case GestureState.END: case GestureState.CANCELLED: if (this.mTouchMode === ChartTouchListener.DRAG) { this.mTouchMode = ChartTouchListener.NONE; this.stopDeceleration(); this.mDecelerationVelocity.x = event.data.extraData.velocityX; this.mDecelerationVelocity.y = event.data.extraData.velocityY; chart.invalidate(); } break; } } onPanGestureTouch(event) { const chart = this.chart; if (!chart.highlightPerDragEnabled && !chart.dragEnabled) return; const data = event.data; if (data.state !== GestureState.ACTIVE) { return; } if (this.mTouchMode === ChartTouchListener.DRAG) { if (chart.hasListeners('pan')) { chart.notify({ eventName: 'pan', data: event.data, object: chart }); } chart.disableScroll(); const x = event.data.extraData.translationX; const y = event.data.extraData.translationY; this.performDrag(event, x, y); this.mMatrix = chart.viewPortHandler.refresh(this.mMatrix, chart, true); } else if (this.mTouchMode === ChartTouchListener.NONE) { const shouldPan = (!chart.isFullyZoomedOut() || !chart.hasNoDragOffset()) && (!chart.zoomedPanWith2Pointers || event.data.extraData.numberOfPointers === 2); if (shouldPan) { // Disable dragging in a direction that's disallowed if (chart.dragXEnabled && chart.dragYEnabled) { chart.disableScroll(); this.mLastGesture = ChartGesture.DRAG; this.mTouchMode = ChartTouchListener.DRAG; } } else { if (chart.highlightPerDragEnabled) { chart.disableScroll(); this.mLastGesture = ChartGesture.DRAG; this.performHighlightDrag(event); } } } } onPinchGestureState(event) { const chart = this.chart; if (!chart.scaleXEnabled && !chart.scaleYEnabled) return; chart.noComputeAutoScaleOnNextDraw = true; const state = event.data.state; switch (state) { case GestureState.ACTIVE: this.saveTouchStart(event); // get the distance between the pointers on the x-axis this.mSavedXDist = BarLineChartTouchListener.getXDist(event); chart.disableScroll(); // get the distance between the pointers on the y-axis this.mSavedYDist = BarLineChartTouchListener.getYDist(event); if (chart.pinchZoomEnabled) { this.mTouchMode = ChartTouchListener.PINCH_ZOOM; } else { if (chart.scaleXEnabled !== chart.scaleYEnabled) { this.mTouchMode = chart.scaleXEnabled ? ChartTouchListener.X_ZOOM : ChartTouchListener.Y_ZOOM; } else { this.mTouchMode = this.mSavedXDist > this.mSavedYDist ? ChartTouchListener.X_ZOOM : ChartTouchListener.Y_ZOOM; } } // determine the touch-pointer center this.mTouchPointCenter.x = event.data.extraData.focalX; this.mTouchPointCenter.y = event.data.extraData.focalY; break; case GestureState.END: case GestureState.CANCELLED: if (this.mTouchMode === ChartTouchListener.X_ZOOM || this.mTouchMode === ChartTouchListener.Y_ZOOM || this.mTouchMode === ChartTouchListener.PINCH_ZOOM || this.mTouchMode === ChartTouchListener.POST_ZOOM) { this.mTouchMode = ChartTouchListener.NONE; // Range might have changed, which means that Y-axis labels // could have changed in size, affecting Y-axis size. // So we need to recalculate offsets. chart.calculateOffsets(); chart.invalidate(); } chart.enableScroll(); break; } } onPinchGestureTouch(event) { const chart = this.chart; if (!chart.scaleXEnabled && !chart.scaleYEnabled) return; if (event.data.state !== GestureState.ACTIVE) { return; } if (chart.hasListeners('pinch')) { chart.notify({ eventName: 'pinch', data: event.data, object: chart }); } if (this.mTouchMode === ChartTouchListener.X_ZOOM || this.mTouchMode === ChartTouchListener.Y_ZOOM || this.mTouchMode === ChartTouchListener.PINCH_ZOOM) { if (this.mTouchMode === ChartTouchListener.PINCH_ZOOM) { this.mLastGesture = ChartGesture.PINCH_ZOOM; const t = this.getTrans(this.mTouchPointCenter.x, this.mTouchPointCenter.y); //focalPoint const scale = event.data.extraData.scale; const isZoomingOut = scale < 1; const h = chart.viewPortHandler; const canZoomMoreX = isZoomingOut ? h.canZoomOutMoreX() : h.canZoomInMoreX(); const canZoomMoreY = isZoomingOut ? h.canZoomOutMoreY() : h.canZoomInMoreY(); const scaleX = chart.scaleXEnabled ? scale : 1; const scaleY = chart.scaleYEnabled ? scale : 1; if (canZoomMoreY || canZoomMoreX) { this.mMatrix.set(this.mSavedMatrix); this.mMatrix.postScale(scaleX, scaleY, t.x, t.y); } if (chart.hasListeners('zoom')) { chart.notify({ eventName: 'zoom', scaleX, scaleY, ...t }); } } else if (this.mTouchMode === ChartTouchListener.X_ZOOM && chart.scaleXEnabled) { this.mLastGesture = ChartGesture.X_ZOOM; const t = this.getTrans(this.mTouchPointCenter.x, this.mTouchPointCenter.y); const scaleX = event.data.extraData.scale; const h = chart.viewPortHandler; const isZoomingOut = scaleX < 1; const canZoomMoreX = isZoomingOut ? h.canZoomOutMoreX() : h.canZoomInMoreX(); if (canZoomMoreX) { this.mMatrix.set(this.mSavedMatrix); this.mMatrix.postScale(scaleX, 1, t.x, t.y); } if (chart.hasListeners('zoom')) { chart.notify({ eventName: 'zoom', scaleX, scaleY: 1, ...t }); } } else if (this.mTouchMode === ChartTouchListener.Y_ZOOM && chart.scaleYEnabled) { this.mLastGesture = ChartGesture.Y_ZOOM; const t = this.getTrans(this.mTouchPointCenter.x, this.mTouchPointCenter.y); const scaleY = event.data.extraData.scale; const h = chart.viewPortHandler; const isZoomingOut = scaleY < 1; const canZoomMoreY = isZoomingOut ? h.canZoomOutMoreY() : h.canZoomInMoreY(); if (canZoomMoreY) { this.mMatrix.set(this.mSavedMatrix); this.mMatrix.postScale(1, scaleY, t.x, t.y); } if (chart.hasListeners('zoom')) { chart.notify({ eventName: 'zoom', scaleX: 1, scaleY, ...t }); } } this.mMatrix = chart.viewPortHandler.refresh(this.mMatrix, chart, true); chart.noComputeAutoScaleOnNextDraw = true; // } } } /** * ################ ################ ################ ################ */ /** BELOW CODE PERFORMS THE ACTUAL TOUCH ACTIONS */ /** * Saves the current Matrix state and the touch-start point. * * @param event */ saveTouchStart(event) { this.mSavedMatrix.set(this.mMatrix); } /** * Performs all necessary operations needed for dragging. * * @param event */ performDrag(event, distanceX, distanceY) { this.mLastGesture = ChartTouchListener.DRAG; this.mMatrix.set(this.mSavedMatrix); // check if axis is inverted if (this.inverted()) { distanceY = -distanceY; } this.chart.noComputeAutoScaleOnNextDraw = true; this.mMatrix.postTranslate(distanceX, distanceY); if (this.chart.hasListeners('translate')) { this.chart.notify({ eventName: 'translate', object: this.chart, distanceX, distanceY }); } } /** * Highlights upon dragging, generates callbacks for the selection-listener. * * @param e */ performHighlightDrag(event) { // const highlights = this.chart.getHighlightsByTouchPoint(event.data.extraData.x, event.data.extraData.y); // const h = highlights[0]; // if (h && h !== this.lastHighlighted) { // this.chart.highlights(highlights, true); // } this.handleTouchHighlight(event, true); } /** * Highlights upon dragging, generates callbacks for the selection-listener. * * @param e */ handleTouchHighlight(event, highlightInChart, checkForLast = true, eventName) { const highlights = this.chart.getHighlightsByTouchPoint(event.data.extraData.x, event.data.extraData.y); const h = highlights[0]; if (h) { if (highlightInChart && (!checkForLast || h !== this.lastHighlighted)) { this.chart.highlight(highlights, true); } } if (eventName) { this.chart.notify({ eventName, data: event.data, highlight: h, highlights }); } } /** * calculates the distance on the x-axis between two pointers (fingers on * the display) * * @param e * @return */ static getXDist(e) { const x = Math.abs(e.data.extraData.positions[0] - e.data.extraData.positions[2]); return x; } /** * calculates the distance on the y-axis between two pointers (fingers on * the display) * * @param e * @return */ static getYDist(e) { const y = Math.abs(e.data.extraData.positions[1] - e.data.extraData.positions[3]); return y; } /** * Returns a recyclable MPPointF instance. * returns the correct translation depending on the provided x and y touch * points * * @param x * @param y * @return */ getTrans(x, y) { const vph = this.chart.viewPortHandler; const xTrans = x - vph.offsetLeft; let yTrans = 0; // check if axis is inverted if (this.inverted()) { yTrans = -(y - vph.offsetTop); } else { yTrans = -(vph.chartHeight - y - vph.offsetBottom); } return { x: xTrans, y: yTrans }; } /** * Returns true if the current touch situation should be interpreted as inverted, false if not. */ inverted() { return (!this.mClosestDataSetToTouch && this.chart.anyAxisInverted) || (this.mClosestDataSetToTouch && this.chart.isInverted(this.mClosestDataSetToTouch.axisDependency)); } /** * ################ ################ ################ ################ */ /** GETTERS AND GESTURE RECOGNITION BELOW */ /** * returns the matrix object the listener holds */ getMatrix() { return this.mMatrix; } onDoubleTapGesture(event) { if (event.data.state === GestureState.END && event.data.prevState === GestureState.ACTIVE) { const chart = this.chart; // check if double-tap zooming is enabled if (chart.hasListeners('doubleTap')) { this.handleTouchHighlight(event, false, false, 'doubleTap'); // const h = chart.getHighlightByTouchPoint(event.data.extraData.x, event.data.extraData.y); // chart.notify({ eventName: 'doubleTap', data: event.data, object: chart, highlight: h }); } if (chart.doubleTapToZoomEnabled && chart.data?.entryCount > 0) { const trans = this.getTrans(event.data.extraData.x, event.data.extraData.y); chart.zoom(chart.scaleXEnabled ? 1.4 : 1, chart.scaleYEnabled ? 1.4 : 1, trans.x, trans.y); if (Trace.isEnabled()) { CLog(CLogTypes.info, LOG_TAG, 'Double-Tap, Zooming In, x: ' + trans.x + ', y: ' + trans.y); } } } } onTapGesture(event) { if (Trace.isEnabled()) { CLog(CLogTypes.log, LOG_TAG, 'onTapGesture', event.data.state, event.data.prevState); } if (event.data.state === GestureState.END && event.data.prevState === GestureState.ACTIVE) { this.mLastGesture = ChartGesture.SINGLE_TAP; const chart = this.chart; const hasListener = chart.hasListeners('tap'); const isHighlightPerTapEnabled = chart.highlightPerTapEnabled; if (!hasListener && !isHighlightPerTapEnabled) { return; } this.handleTouchHighlight(event, isHighlightPerTapEnabled, false, hasListener ? 'tap' : undefined); // const h = chart.getHighlightByTouchPoint(event.data.extraData.x, event.data.extraData.y); // if (hasListener) { // chart.notify({ eventName: 'tap', data: event.data, object: chart, highlight: h }); // } // if (isHighlightPerTapEnabled) { // this.performHighlight(h); // } } } stopDeceleration() { this.mDecelerationVelocity.x = 0; this.mDecelerationVelocity.y = 0; } } //# sourceMappingURL=BarLineChartTouchListener.js.map