From 64837d13b875e3133c0c506b927468183b5fdabb Mon Sep 17 00:00:00 2001 From: "Chart.js" Date: Thu, 7 May 2020 17:13:09 +0000 Subject: [PATCH] Release 0.7.7 --- bower.json | 7 + dist/chartjs-plugin-zoom.js | 740 ++++++++++++++++++++++++++++++++ dist/chartjs-plugin-zoom.min.js | 11 + 3 files changed, 758 insertions(+) create mode 100644 bower.json create mode 100644 dist/chartjs-plugin-zoom.js create mode 100644 dist/chartjs-plugin-zoom.min.js diff --git a/bower.json b/bower.json new file mode 100644 index 000000000..c4d49e49d --- /dev/null +++ b/bower.json @@ -0,0 +1,7 @@ +{ + "name": "chartjs-plugin-zoom", + "description": "Plugin that enables zoom and pan functionality in Chart.js charts.", + "license": "MIT", + "version": "0.7.7", + "main": "./dist/chartjs-plugin-zoom.js" +} \ No newline at end of file diff --git a/dist/chartjs-plugin-zoom.js b/dist/chartjs-plugin-zoom.js new file mode 100644 index 000000000..b51d3418d --- /dev/null +++ b/dist/chartjs-plugin-zoom.js @@ -0,0 +1,740 @@ +/*! + * @license + * chartjs-plugin-zoom + * http://chartjs.org/ + * Version: 0.7.7 + * + * Copyright 2020 Chart.js Contributors + * Released under the MIT license + * https://github.com/chartjs/chartjs-plugin-zoom/blob/master/LICENSE.md + */ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('chart.js'), require('hammerjs')) : +typeof define === 'function' && define.amd ? define(['chart.js', 'hammerjs'], factory) : +(global = global || self, global.ChartZoom = factory(global.Chart, global.Hammer)); +}(this, (function (Chart, Hammer) { 'use strict'; + +Chart = Chart && Object.prototype.hasOwnProperty.call(Chart, 'default') ? Chart['default'] : Chart; +Hammer = Hammer && Object.prototype.hasOwnProperty.call(Hammer, 'default') ? Hammer['default'] : Hammer; + +var helpers = Chart.helpers; + +// Take the zoom namespace of Chart +var zoomNS = Chart.Zoom = Chart.Zoom || {}; + +// Where we store functions to handle different scale types +var zoomFunctions = zoomNS.zoomFunctions = zoomNS.zoomFunctions || {}; +var panFunctions = zoomNS.panFunctions = zoomNS.panFunctions || {}; + +Chart.Zoom.defaults = Chart.defaults.global.plugins.zoom = { + pan: { + enabled: false, + mode: 'xy', + speed: 20, + threshold: 10 + }, + zoom: { + enabled: false, + mode: 'xy', + sensitivity: 3, + speed: 0.1 + } +}; + +function resolveOptions(chart, options) { + var deprecatedOptions = {}; + if (typeof chart.options.pan !== 'undefined') { + deprecatedOptions.pan = chart.options.pan; + } + if (typeof chart.options.zoom !== 'undefined') { + deprecatedOptions.zoom = chart.options.zoom; + } + var props = chart.$zoom; + options = props._options = helpers.merge({}, [options, deprecatedOptions]); + + // Install listeners. Do this dynamically based on options so that we can turn zoom on and off + // We also want to make sure listeners aren't always on. E.g. if you're scrolling down a page + // and the mouse goes over a chart you don't want it intercepted unless the plugin is enabled + var node = props._node; + var zoomEnabled = options.zoom && options.zoom.enabled; + var dragEnabled = options.zoom.drag; + if (zoomEnabled && !dragEnabled) { + node.addEventListener('wheel', props._wheelHandler); + } else { + node.removeEventListener('wheel', props._wheelHandler); + } + if (zoomEnabled && dragEnabled) { + node.addEventListener('mousedown', props._mouseDownHandler); + node.ownerDocument.addEventListener('mouseup', props._mouseUpHandler); + } else { + node.removeEventListener('mousedown', props._mouseDownHandler); + node.removeEventListener('mousemove', props._mouseMoveHandler); + node.ownerDocument.removeEventListener('mouseup', props._mouseUpHandler); + } +} + +function storeOriginalOptions(chart) { + var originalOptions = chart.$zoom._originalOptions; + helpers.each(chart.scales, function(scale) { + if (!originalOptions[scale.id]) { + originalOptions[scale.id] = helpers.clone(scale.options); + } + }); + helpers.each(originalOptions, function(opt, key) { + if (!chart.scales[key]) { + delete originalOptions[key]; + } + }); +} + +/** + * @param {string} mode can be 'x', 'y' or 'xy' + * @param {string} dir can be 'x' or 'y' + * @param {Chart} chart instance of the chart in question + */ +function directionEnabled(mode, dir, chart) { + if (mode === undefined) { + return true; + } else if (typeof mode === 'string') { + return mode.indexOf(dir) !== -1; + } else if (typeof mode === 'function') { + return mode({chart: chart}).indexOf(dir) !== -1; + } + + return false; +} + +function rangeMaxLimiter(zoomPanOptions, newMax) { + if (zoomPanOptions.scaleAxes && zoomPanOptions.rangeMax && + !helpers.isNullOrUndef(zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes])) { + var rangeMax = zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes]; + if (newMax > rangeMax) { + newMax = rangeMax; + } + } + return newMax; +} + +function rangeMinLimiter(zoomPanOptions, newMin) { + if (zoomPanOptions.scaleAxes && zoomPanOptions.rangeMin && + !helpers.isNullOrUndef(zoomPanOptions.rangeMin[zoomPanOptions.scaleAxes])) { + var rangeMin = zoomPanOptions.rangeMin[zoomPanOptions.scaleAxes]; + if (newMin < rangeMin) { + newMin = rangeMin; + } + } + return newMin; +} + +function zoomCategoryScale(scale, zoom, center, zoomOptions) { + var labels = scale.chart.data.labels; + var minIndex = scale.minIndex; + var lastLabelIndex = labels.length - 1; + var maxIndex = scale.maxIndex; + var sensitivity = zoomOptions.sensitivity; + var chartCenter = scale.isHorizontal() ? scale.left + (scale.width / 2) : scale.top + (scale.height / 2); + var centerPointer = scale.isHorizontal() ? center.x : center.y; + + zoomNS.zoomCumulativeDelta = zoom > 1 ? zoomNS.zoomCumulativeDelta + 1 : zoomNS.zoomCumulativeDelta - 1; + + if (Math.abs(zoomNS.zoomCumulativeDelta) > sensitivity) { + if (zoomNS.zoomCumulativeDelta < 0) { + if (centerPointer >= chartCenter) { + if (minIndex <= 0) { + maxIndex = Math.min(lastLabelIndex, maxIndex + 1); + } else { + minIndex = Math.max(0, minIndex - 1); + } + } else if (centerPointer < chartCenter) { + if (maxIndex >= lastLabelIndex) { + minIndex = Math.max(0, minIndex - 1); + } else { + maxIndex = Math.min(lastLabelIndex, maxIndex + 1); + } + } + zoomNS.zoomCumulativeDelta = 0; + } else if (zoomNS.zoomCumulativeDelta > 0) { + if (centerPointer >= chartCenter) { + minIndex = minIndex < maxIndex ? minIndex = Math.min(maxIndex, minIndex + 1) : minIndex; + } else if (centerPointer < chartCenter) { + maxIndex = maxIndex > minIndex ? maxIndex = Math.max(minIndex, maxIndex - 1) : maxIndex; + } + zoomNS.zoomCumulativeDelta = 0; + } + scale.options.ticks.min = rangeMinLimiter(zoomOptions, labels[minIndex]); + scale.options.ticks.max = rangeMaxLimiter(zoomOptions, labels[maxIndex]); + } +} + +function zoomNumericalScale(scale, zoom, center, zoomOptions) { + var range = scale.max - scale.min; + var newDiff = range * (zoom - 1); + + var centerPoint = scale.isHorizontal() ? center.x : center.y; + var minPercent = (scale.getValueForPixel(centerPoint) - scale.min) / range; + var maxPercent = 1 - minPercent; + + var minDelta = newDiff * minPercent; + var maxDelta = newDiff * maxPercent; + + scale.options.ticks.min = rangeMinLimiter(zoomOptions, scale.min + minDelta); + scale.options.ticks.max = rangeMaxLimiter(zoomOptions, scale.max - maxDelta); +} + +function zoomTimeScale(scale, zoom, center, zoomOptions) { + zoomNumericalScale(scale, zoom, center, zoomOptions); + + var options = scale.options; + if (options.time) { + if (options.time.min) { + options.time.min = options.ticks.min; + } + if (options.time.max) { + options.time.max = options.ticks.max; + } + } +} + +function zoomScale(scale, zoom, center, zoomOptions) { + var fn = zoomFunctions[scale.type]; + if (fn) { + fn(scale, zoom, center, zoomOptions); + } +} + +/** + * @param chart The chart instance + * @param {number} percentZoomX The zoom percentage in the x direction + * @param {number} percentZoomY The zoom percentage in the y direction + * @param {{x: number, y: number}} focalPoint The x and y coordinates of zoom focal point. The point which doesn't change while zooming. E.g. the location of the mouse cursor when "drag: false" + * @param {string} whichAxes `xy`, 'x', or 'y' + * @param {number} animationDuration Duration of the animation of the redraw in milliseconds + */ +function doZoom(chart, percentZoomX, percentZoomY, focalPoint, whichAxes, animationDuration) { + var ca = chart.chartArea; + if (!focalPoint) { + focalPoint = { + x: (ca.left + ca.right) / 2, + y: (ca.top + ca.bottom) / 2, + }; + } + + var zoomOptions = chart.$zoom._options.zoom; + + if (zoomOptions.enabled) { + storeOriginalOptions(chart); + // Do the zoom here + var zoomMode = typeof zoomOptions.mode === 'function' ? zoomOptions.mode({chart: chart}) : zoomOptions.mode; + + // Which axe should be modified when figers were used. + var _whichAxes; + if (zoomMode === 'xy' && whichAxes !== undefined) { + // based on fingers positions + _whichAxes = whichAxes; + } else { + // no effect + _whichAxes = 'xy'; + } + + helpers.each(chart.scales, function(scale) { + if (scale.isHorizontal() && directionEnabled(zoomMode, 'x', chart) && directionEnabled(_whichAxes, 'x', chart)) { + zoomOptions.scaleAxes = 'x'; + zoomScale(scale, percentZoomX, focalPoint, zoomOptions); + } else if (!scale.isHorizontal() && directionEnabled(zoomMode, 'y', chart) && directionEnabled(_whichAxes, 'y', chart)) { + // Do Y zoom + zoomOptions.scaleAxes = 'y'; + zoomScale(scale, percentZoomY, focalPoint, zoomOptions); + } + }); + + if (animationDuration) { + chart.update({ + duration: animationDuration, + easing: 'easeOutQuad', + }); + } else { + chart.update(0); + } + + if (typeof zoomOptions.onZoom === 'function') { + zoomOptions.onZoom({chart: chart}); + } + } +} + +function panCategoryScale(scale, delta, panOptions) { + var labels = scale.chart.data.labels; + var lastLabelIndex = labels.length - 1; + var offsetAmt = Math.max(scale.ticks.length, 1); + var panSpeed = panOptions.speed; + var minIndex = scale.minIndex; + var step = Math.round(scale.width / (offsetAmt * panSpeed)); + var maxIndex; + + zoomNS.panCumulativeDelta += delta; + + minIndex = zoomNS.panCumulativeDelta > step ? Math.max(0, minIndex - 1) : zoomNS.panCumulativeDelta < -step ? Math.min(lastLabelIndex - offsetAmt + 1, minIndex + 1) : minIndex; + zoomNS.panCumulativeDelta = minIndex !== scale.minIndex ? 0 : zoomNS.panCumulativeDelta; + + maxIndex = Math.min(lastLabelIndex, minIndex + offsetAmt - 1); + + scale.options.ticks.min = rangeMinLimiter(panOptions, labels[minIndex]); + scale.options.ticks.max = rangeMaxLimiter(panOptions, labels[maxIndex]); +} + +function panNumericalScale(scale, delta, panOptions) { + var tickOpts = scale.options.ticks; + var prevStart = scale.min; + var prevEnd = scale.max; + var newMin = scale.getValueForPixel(scale.getPixelForValue(prevStart) - delta); + var newMax = scale.getValueForPixel(scale.getPixelForValue(prevEnd) - delta); + // The time scale returns date objects so convert to numbers. Can remove at Chart.js v3 + newMin = newMin.valueOf ? newMin.valueOf() : newMin; + newMax = newMax.valueOf ? newMax.valueOf() : newMax; + var rangeMin = newMin; + var rangeMax = newMax; + var diff; + + if (panOptions.scaleAxes && panOptions.rangeMin && + !helpers.isNullOrUndef(panOptions.rangeMin[panOptions.scaleAxes])) { + rangeMin = panOptions.rangeMin[panOptions.scaleAxes]; + } + if (panOptions.scaleAxes && panOptions.rangeMax && + !helpers.isNullOrUndef(panOptions.rangeMax[panOptions.scaleAxes])) { + rangeMax = panOptions.rangeMax[panOptions.scaleAxes]; + } + + if (newMin >= rangeMin && newMax <= rangeMax) { + tickOpts.min = newMin; + tickOpts.max = newMax; + } else if (newMin < rangeMin) { + diff = prevStart - rangeMin; + tickOpts.min = rangeMin; + tickOpts.max = prevEnd - diff; + } else if (newMax > rangeMax) { + diff = rangeMax - prevEnd; + tickOpts.max = rangeMax; + tickOpts.min = prevStart + diff; + } +} + +function panTimeScale(scale, delta, panOptions) { + panNumericalScale(scale, delta, panOptions); + + var options = scale.options; + if (options.time) { + if (options.time.min) { + options.time.min = options.ticks.min; + } + if (options.time.max) { + options.time.max = options.ticks.max; + } + } +} + +function panScale(scale, delta, panOptions) { + var fn = panFunctions[scale.type]; + if (fn) { + fn(scale, delta, panOptions); + } +} + +function doPan(chartInstance, deltaX, deltaY) { + storeOriginalOptions(chartInstance); + var panOptions = chartInstance.$zoom._options.pan; + if (panOptions.enabled) { + var panMode = typeof panOptions.mode === 'function' ? panOptions.mode({chart: chartInstance}) : panOptions.mode; + + helpers.each(chartInstance.scales, function(scale) { + if (scale.isHorizontal() && directionEnabled(panMode, 'x', chartInstance) && deltaX !== 0) { + panOptions.scaleAxes = 'x'; + panScale(scale, deltaX, panOptions); + } else if (!scale.isHorizontal() && directionEnabled(panMode, 'y', chartInstance) && deltaY !== 0) { + panOptions.scaleAxes = 'y'; + panScale(scale, deltaY, panOptions); + } + }); + + chartInstance.update(0); + + if (typeof panOptions.onPan === 'function') { + panOptions.onPan({chart: chartInstance}); + } + } +} + +function getXAxis(chartInstance) { + var scales = chartInstance.scales; + var scaleIds = Object.keys(scales); + for (var i = 0; i < scaleIds.length; i++) { + var scale = scales[scaleIds[i]]; + + if (scale.isHorizontal()) { + return scale; + } + } +} + +function getYAxis(chartInstance) { + var scales = chartInstance.scales; + var scaleIds = Object.keys(scales); + for (var i = 0; i < scaleIds.length; i++) { + var scale = scales[scaleIds[i]]; + + if (!scale.isHorizontal()) { + return scale; + } + } +} + +// Store these for later +zoomNS.zoomFunctions.category = zoomCategoryScale; +zoomNS.zoomFunctions.time = zoomTimeScale; +zoomNS.zoomFunctions.linear = zoomNumericalScale; +zoomNS.zoomFunctions.logarithmic = zoomNumericalScale; +zoomNS.panFunctions.category = panCategoryScale; +zoomNS.panFunctions.time = panTimeScale; +zoomNS.panFunctions.linear = panNumericalScale; +zoomNS.panFunctions.logarithmic = panNumericalScale; +// Globals for category pan and zoom +zoomNS.panCumulativeDelta = 0; +zoomNS.zoomCumulativeDelta = 0; + +// Chartjs Zoom Plugin +var zoomPlugin = { + id: 'zoom', + + afterInit: function(chartInstance) { + + chartInstance.resetZoom = function() { + storeOriginalOptions(chartInstance); + var originalOptions = chartInstance.$zoom._originalOptions; + helpers.each(chartInstance.scales, function(scale) { + + var timeOptions = scale.options.time; + var tickOptions = scale.options.ticks; + + if (originalOptions[scale.id]) { + + if (timeOptions) { + timeOptions.min = originalOptions[scale.id].time.min; + timeOptions.max = originalOptions[scale.id].time.max; + } + + if (tickOptions) { + tickOptions.min = originalOptions[scale.id].ticks.min; + tickOptions.max = originalOptions[scale.id].ticks.max; + } + } else { + + if (timeOptions) { + delete timeOptions.min; + delete timeOptions.max; + } + + if (tickOptions) { + delete tickOptions.min; + delete tickOptions.max; + } + } + + + }); + + chartInstance.update(); + }; + + }, + + beforeUpdate: function(chart, options) { + resolveOptions(chart, options); + }, + + beforeInit: function(chartInstance, pluginOptions) { + chartInstance.$zoom = { + _originalOptions: {} + }; + var node = chartInstance.$zoom._node = chartInstance.ctx.canvas; + resolveOptions(chartInstance, pluginOptions); + + var options = chartInstance.$zoom._options; + var panThreshold = options.pan && options.pan.threshold; + + chartInstance.$zoom._mouseDownHandler = function(event) { + node.addEventListener('mousemove', chartInstance.$zoom._mouseMoveHandler); + chartInstance.$zoom._dragZoomStart = event; + }; + + chartInstance.$zoom._mouseMoveHandler = function(event) { + if (chartInstance.$zoom._dragZoomStart) { + chartInstance.$zoom._dragZoomEnd = event; + chartInstance.update(0); + } + }; + + chartInstance.$zoom._mouseUpHandler = function(event) { + if (!chartInstance.$zoom._dragZoomStart) { + return; + } + + node.removeEventListener('mousemove', chartInstance.$zoom._mouseMoveHandler); + + var beginPoint = chartInstance.$zoom._dragZoomStart; + + var offsetX = beginPoint.target.getBoundingClientRect().left; + var startX = Math.min(beginPoint.clientX, event.clientX) - offsetX; + var endX = Math.max(beginPoint.clientX, event.clientX) - offsetX; + + var offsetY = beginPoint.target.getBoundingClientRect().top; + var startY = Math.min(beginPoint.clientY, event.clientY) - offsetY; + var endY = Math.max(beginPoint.clientY, event.clientY) - offsetY; + + var dragDistanceX = endX - startX; + var dragDistanceY = endY - startY; + + // Remove drag start and end before chart update to stop drawing selected area + chartInstance.$zoom._dragZoomStart = null; + chartInstance.$zoom._dragZoomEnd = null; + + var zoomThreshold = options.zoom && options.zoom.threshold || 0; + if (dragDistanceX <= zoomThreshold && dragDistanceY <= zoomThreshold) { + return; + } + + var chartArea = chartInstance.chartArea; + + var zoomOptions = chartInstance.$zoom._options.zoom; + var chartDistanceX = chartArea.right - chartArea.left; + var xEnabled = directionEnabled(zoomOptions.mode, 'x', chartInstance); + var zoomX = xEnabled && dragDistanceX ? 1 + ((chartDistanceX - dragDistanceX) / chartDistanceX) : 1; + + var chartDistanceY = chartArea.bottom - chartArea.top; + var yEnabled = directionEnabled(zoomOptions.mode, 'y', chartInstance); + var zoomY = yEnabled && dragDistanceY ? 1 + ((chartDistanceY - dragDistanceY) / chartDistanceY) : 1; + + doZoom(chartInstance, zoomX, zoomY, { + x: (startX - chartArea.left) / (1 - dragDistanceX / chartDistanceX) + chartArea.left, + y: (startY - chartArea.top) / (1 - dragDistanceY / chartDistanceY) + chartArea.top + }, undefined, zoomOptions.drag.animationDuration); + + if (typeof zoomOptions.onZoomComplete === 'function') { + zoomOptions.onZoomComplete({chart: chartInstance}); + } + }; + + var _scrollTimeout = null; + chartInstance.$zoom._wheelHandler = function(event) { + // Prevent the event from triggering the default behavior (eg. Content scrolling). + if (event.cancelable) { + event.preventDefault(); + } + + // Firefox always fires the wheel event twice: + // First without the delta and right after that once with the delta properties. + if (typeof event.deltaY === 'undefined') { + return; + } + + var rect = event.target.getBoundingClientRect(); + var offsetX = event.clientX - rect.left; + var offsetY = event.clientY - rect.top; + + var center = { + x: offsetX, + y: offsetY + }; + + var zoomOptions = chartInstance.$zoom._options.zoom; + var speedPercent = zoomOptions.speed; + + if (event.deltaY >= 0) { + speedPercent = -speedPercent; + } + doZoom(chartInstance, 1 + speedPercent, 1 + speedPercent, center); + + clearTimeout(_scrollTimeout); + _scrollTimeout = setTimeout(function() { + if (typeof zoomOptions.onZoomComplete === 'function') { + zoomOptions.onZoomComplete({chart: chartInstance}); + } + }, 250); + }; + + if (Hammer) { + var mc = new Hammer.Manager(node); + mc.add(new Hammer.Pinch()); + mc.add(new Hammer.Pan({ + threshold: panThreshold + })); + + // Hammer reports the total scaling. We need the incremental amount + var currentPinchScaling; + var handlePinch = function(e) { + var diff = 1 / (currentPinchScaling) * e.scale; + var rect = e.target.getBoundingClientRect(); + var offsetX = e.center.x - rect.left; + var offsetY = e.center.y - rect.top; + var center = { + x: offsetX, + y: offsetY + }; + + // fingers position difference + var x = Math.abs(e.pointers[0].clientX - e.pointers[1].clientX); + var y = Math.abs(e.pointers[0].clientY - e.pointers[1].clientY); + + // diagonal fingers will change both (xy) axes + var p = x / y; + var xy; + if (p > 0.3 && p < 1.7) { + xy = 'xy'; + } else if (x > y) { + xy = 'x'; // x axis + } else { + xy = 'y'; // y axis + } + + doZoom(chartInstance, diff, diff, center, xy); + + var zoomOptions = chartInstance.$zoom._options.zoom; + if (typeof zoomOptions.onZoomComplete === 'function') { + zoomOptions.onZoomComplete({chart: chartInstance}); + } + + // Keep track of overall scale + currentPinchScaling = e.scale; + }; + + mc.on('pinchstart', function() { + currentPinchScaling = 1; // reset tracker + }); + mc.on('pinch', handlePinch); + mc.on('pinchend', function(e) { + handlePinch(e); + currentPinchScaling = null; // reset + zoomNS.zoomCumulativeDelta = 0; + }); + + var currentDeltaX = null; + var currentDeltaY = null; + var panning = false; + var handlePan = function(e) { + if (currentDeltaX !== null && currentDeltaY !== null) { + panning = true; + var deltaX = e.deltaX - currentDeltaX; + var deltaY = e.deltaY - currentDeltaY; + currentDeltaX = e.deltaX; + currentDeltaY = e.deltaY; + doPan(chartInstance, deltaX, deltaY); + } + }; + + mc.on('panstart', function(e) { + currentDeltaX = 0; + currentDeltaY = 0; + handlePan(e); + }); + mc.on('panmove', handlePan); + mc.on('panend', function() { + currentDeltaX = null; + currentDeltaY = null; + zoomNS.panCumulativeDelta = 0; + setTimeout(function() { + panning = false; + }, 500); + + var panOptions = chartInstance.$zoom._options.pan; + if (typeof panOptions.onPanComplete === 'function') { + panOptions.onPanComplete({chart: chartInstance}); + } + }); + + chartInstance.$zoom._ghostClickHandler = function(e) { + if (panning && e.cancelable) { + e.stopImmediatePropagation(); + e.preventDefault(); + } + }; + node.addEventListener('click', chartInstance.$zoom._ghostClickHandler); + + chartInstance._mc = mc; + } + }, + + beforeDatasetsDraw: function(chartInstance) { + var ctx = chartInstance.ctx; + + if (chartInstance.$zoom._dragZoomEnd) { + var xAxis = getXAxis(chartInstance); + var yAxis = getYAxis(chartInstance); + var beginPoint = chartInstance.$zoom._dragZoomStart; + var endPoint = chartInstance.$zoom._dragZoomEnd; + + var startX = xAxis.left; + var endX = xAxis.right; + var startY = yAxis.top; + var endY = yAxis.bottom; + + if (directionEnabled(chartInstance.$zoom._options.zoom.mode, 'x', chartInstance)) { + var offsetX = beginPoint.target.getBoundingClientRect().left; + startX = Math.min(beginPoint.clientX, endPoint.clientX) - offsetX; + endX = Math.max(beginPoint.clientX, endPoint.clientX) - offsetX; + } + + if (directionEnabled(chartInstance.$zoom._options.zoom.mode, 'y', chartInstance)) { + var offsetY = beginPoint.target.getBoundingClientRect().top; + startY = Math.min(beginPoint.clientY, endPoint.clientY) - offsetY; + endY = Math.max(beginPoint.clientY, endPoint.clientY) - offsetY; + } + + var rectWidth = endX - startX; + var rectHeight = endY - startY; + var dragOptions = chartInstance.$zoom._options.zoom.drag; + + ctx.save(); + ctx.beginPath(); + ctx.fillStyle = dragOptions.backgroundColor || 'rgba(225,225,225,0.3)'; + ctx.fillRect(startX, startY, rectWidth, rectHeight); + + if (dragOptions.borderWidth > 0) { + ctx.lineWidth = dragOptions.borderWidth; + ctx.strokeStyle = dragOptions.borderColor || 'rgba(225,225,225)'; + ctx.strokeRect(startX, startY, rectWidth, rectHeight); + } + ctx.restore(); + } + }, + + destroy: function(chartInstance) { + if (!chartInstance.$zoom) { + return; + } + var props = chartInstance.$zoom; + var node = props._node; + + node.removeEventListener('mousedown', props._mouseDownHandler); + node.removeEventListener('mousemove', props._mouseMoveHandler); + node.ownerDocument.removeEventListener('mouseup', props._mouseUpHandler); + node.removeEventListener('wheel', props._wheelHandler); + node.removeEventListener('click', props._ghostClickHandler); + + delete chartInstance.$zoom; + + var mc = chartInstance._mc; + if (mc) { + mc.remove('pinchstart'); + mc.remove('pinch'); + mc.remove('pinchend'); + mc.remove('panstart'); + mc.remove('pan'); + mc.remove('panend'); + mc.destroy(); + } + } +}; + +Chart.plugins.register(zoomPlugin); + +return zoomPlugin; + +}))); diff --git a/dist/chartjs-plugin-zoom.min.js b/dist/chartjs-plugin-zoom.min.js new file mode 100644 index 000000000..0db855755 --- /dev/null +++ b/dist/chartjs-plugin-zoom.min.js @@ -0,0 +1,11 @@ +/*! + * @license + * chartjs-plugin-zoom + * http://chartjs.org/ + * Version: 0.7.7 + * + * Copyright 2020 Chart.js Contributors + * Released under the MIT license + * https://github.com/chartjs/chartjs-plugin-zoom/blob/master/LICENSE.md + */ +!function(e,o){"object"==typeof exports&&"undefined"!=typeof module?module.exports=o(require("chart.js"),require("hammerjs")):"function"==typeof define&&define.amd?define(["chart.js","hammerjs"],o):(e=e||self).ChartZoom=o(e.Chart,e.Hammer)}(this,(function(e,o){"use strict";e=e&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e,o=o&&Object.prototype.hasOwnProperty.call(o,"default")?o.default:o;var t=e.helpers,n=e.Zoom=e.Zoom||{},a=n.zoomFunctions=n.zoomFunctions||{},i=n.panFunctions=n.panFunctions||{};function m(e,o){var n={};void 0!==e.options.pan&&(n.pan=e.options.pan),void 0!==e.options.zoom&&(n.zoom=e.options.zoom);var a=e.$zoom;o=a._options=t.merge({},[o,n]);var i=a._node,m=o.zoom&&o.zoom.enabled,r=o.zoom.drag;m&&!r?i.addEventListener("wheel",a._wheelHandler):i.removeEventListener("wheel",a._wheelHandler),m&&r?(i.addEventListener("mousedown",a._mouseDownHandler),i.ownerDocument.addEventListener("mouseup",a._mouseUpHandler)):(i.removeEventListener("mousedown",a._mouseDownHandler),i.removeEventListener("mousemove",a._mouseMoveHandler),i.ownerDocument.removeEventListener("mouseup",a._mouseUpHandler))}function r(e){var o=e.$zoom._originalOptions;t.each(e.scales,(function(e){o[e.id]||(o[e.id]=t.clone(e.options))})),t.each(o,(function(t,n){e.scales[n]||delete o[n]}))}function l(e,o,t){return void 0===e||("string"==typeof e?-1!==e.indexOf(o):"function"==typeof e&&-1!==e({chart:t}).indexOf(o))}function s(e,o){if(e.scaleAxes&&e.rangeMax&&!t.isNullOrUndef(e.rangeMax[e.scaleAxes])){var n=e.rangeMax[e.scaleAxes];o>n&&(o=n)}return o}function c(e,o){if(e.scaleAxes&&e.rangeMin&&!t.isNullOrUndef(e.rangeMin[e.scaleAxes])){var n=e.rangeMin[e.scaleAxes];o=c&&s<=u?(i.min=l,i.max=s):lu&&(a=u-r,i.max=u,i.min=m+a)}function v(e,o,t){var n=i[e.type];n&&n(e,o,t)}e.Zoom.defaults=e.defaults.global.plugins.zoom={pan:{enabled:!1,mode:"xy",speed:20,threshold:10},zoom:{enabled:!1,mode:"xy",sensitivity:3,speed:.1}},n.zoomFunctions.category=function(e,o,t,a){var i=e.chart.data.labels,m=e.minIndex,r=i.length-1,l=e.maxIndex,u=a.sensitivity,d=e.isHorizontal()?e.left+e.width/2:e.top+e.height/2,p=e.isHorizontal()?t.x:t.y;n.zoomCumulativeDelta=o>1?n.zoomCumulativeDelta+1:n.zoomCumulativeDelta-1,Math.abs(n.zoomCumulativeDelta)>u&&(n.zoomCumulativeDelta<0?(p>=d?m<=0?l=Math.min(r,l+1):m=Math.max(0,m-1):p=r?m=Math.max(0,m-1):l=Math.min(r,l+1)),n.zoomCumulativeDelta=0):n.zoomCumulativeDelta>0&&(p>=d?m=mm?l=Math.max(m,l-1):l),n.zoomCumulativeDelta=0),e.options.ticks.min=c(a,i[m]),e.options.ticks.max=s(a,i[l]))},n.zoomFunctions.time=function(e,o,t,n){u(e,o,t,n);var a=e.options;a.time&&(a.time.min&&(a.time.min=a.ticks.min),a.time.max&&(a.time.max=a.ticks.max))},n.zoomFunctions.linear=u,n.zoomFunctions.logarithmic=u,n.panFunctions.category=function(e,o,t){var a,i=e.chart.data.labels,m=i.length-1,r=Math.max(e.ticks.length,1),l=t.speed,u=e.minIndex,d=Math.round(e.width/(r*l));n.panCumulativeDelta+=o,u=n.panCumulativeDelta>d?Math.max(0,u-1):n.panCumulativeDelta<-d?Math.min(m-r+1,u+1):u,n.panCumulativeDelta=u!==e.minIndex?0:n.panCumulativeDelta,a=Math.min(m,u+r-1),e.options.ticks.min=c(t,i[u]),e.options.ticks.max=s(t,i[a])},n.panFunctions.time=function(e,o,t){f(e,o,t);var n=e.options;n.time&&(n.time.min&&(n.time.min=n.ticks.min),n.time.max&&(n.time.max=n.ticks.max))},n.panFunctions.linear=f,n.panFunctions.logarithmic=f,n.panCumulativeDelta=0,n.zoomCumulativeDelta=0;var h={id:"zoom",afterInit:function(e){e.resetZoom=function(){r(e);var o=e.$zoom._originalOptions;t.each(e.scales,(function(e){var t=e.options.time,n=e.options.ticks;o[e.id]?(t&&(t.min=o[e.id].time.min,t.max=o[e.id].time.max),n&&(n.min=o[e.id].ticks.min,n.max=o[e.id].ticks.max)):(t&&(delete t.min,delete t.max),n&&(delete n.min,delete n.max))})),e.update()}},beforeUpdate:function(e,o){m(e,o)},beforeInit:function(e,a){e.$zoom={_originalOptions:{}};var i=e.$zoom._node=e.ctx.canvas;m(e,a);var s=e.$zoom._options,c=s.pan&&s.pan.threshold;e.$zoom._mouseDownHandler=function(o){i.addEventListener("mousemove",e.$zoom._mouseMoveHandler),e.$zoom._dragZoomStart=o},e.$zoom._mouseMoveHandler=function(o){e.$zoom._dragZoomStart&&(e.$zoom._dragZoomEnd=o,e.update(0))},e.$zoom._mouseUpHandler=function(o){if(e.$zoom._dragZoomStart){i.removeEventListener("mousemove",e.$zoom._mouseMoveHandler);var t=e.$zoom._dragZoomStart,n=t.target.getBoundingClientRect().left,a=Math.min(t.clientX,o.clientX)-n,m=Math.max(t.clientX,o.clientX)-n,r=t.target.getBoundingClientRect().top,c=Math.min(t.clientY,o.clientY)-r,u=m-a,d=Math.max(t.clientY,o.clientY)-r-c;e.$zoom._dragZoomStart=null,e.$zoom._dragZoomEnd=null;var f=s.zoom&&s.zoom.threshold||0;if(!(u<=f&&d<=f)){var v=e.chartArea,h=e.$zoom._options.zoom,x=v.right-v.left,g=l(h.mode,"x",e)&&u?1+(x-u)/x:1,z=v.bottom-v.top,y=l(h.mode,"y",e);p(e,g,y&&d?1+(z-d)/z:1,{x:(a-v.left)/(1-u/x)+v.left,y:(c-v.top)/(1-d/z)+v.top},void 0,h.drag.animationDuration),"function"==typeof h.onZoomComplete&&h.onZoomComplete({chart:e})}}};var u=null;if(e.$zoom._wheelHandler=function(o){if(o.cancelable&&o.preventDefault(),void 0!==o.deltaY){var t=o.target.getBoundingClientRect(),n={x:o.clientX-t.left,y:o.clientY-t.top},a=e.$zoom._options.zoom,i=a.speed;o.deltaY>=0&&(i=-i),p(e,1+i,1+i,n),clearTimeout(u),u=setTimeout((function(){"function"==typeof a.onZoomComplete&&a.onZoomComplete({chart:e})}),250)}},o){var d,f=new o.Manager(i);f.add(new o.Pinch),f.add(new o.Pan({threshold:c}));var h=function(o){var t=1/d*o.scale,n=o.target.getBoundingClientRect(),a={x:o.center.x-n.left,y:o.center.y-n.top},i=Math.abs(o.pointers[0].clientX-o.pointers[1].clientX),m=Math.abs(o.pointers[0].clientY-o.pointers[1].clientY),r=i/m;p(e,t,t,a,r>.3&&r<1.7?"xy":i>m?"x":"y");var l=e.$zoom._options.zoom;"function"==typeof l.onZoomComplete&&l.onZoomComplete({chart:e}),d=o.scale};f.on("pinchstart",(function(){d=1})),f.on("pinch",h),f.on("pinchend",(function(e){h(e),d=null,n.zoomCumulativeDelta=0}));var x=null,g=null,z=!1,y=function(o){if(null!==x&&null!==g){z=!0;var n=o.deltaX-x,a=o.deltaY-g;x=o.deltaX,g=o.deltaY,function(e,o,n){r(e);var a=e.$zoom._options.pan;if(a.enabled){var i="function"==typeof a.mode?a.mode({chart:e}):a.mode;t.each(e.scales,(function(t){t.isHorizontal()&&l(i,"x",e)&&0!==o?(a.scaleAxes="x",v(t,o,a)):!t.isHorizontal()&&l(i,"y",e)&&0!==n&&(a.scaleAxes="y",v(t,n,a))})),e.update(0),"function"==typeof a.onPan&&a.onPan({chart:e})}}(e,n,a)}};f.on("panstart",(function(e){x=0,g=0,y(e)})),f.on("panmove",y),f.on("panend",(function(){x=null,g=null,n.panCumulativeDelta=0,setTimeout((function(){z=!1}),500);var o=e.$zoom._options.pan;"function"==typeof o.onPanComplete&&o.onPanComplete({chart:e})})),e.$zoom._ghostClickHandler=function(e){z&&e.cancelable&&(e.stopImmediatePropagation(),e.preventDefault())},i.addEventListener("click",e.$zoom._ghostClickHandler),e._mc=f}},beforeDatasetsDraw:function(e){var o=e.ctx;if(e.$zoom._dragZoomEnd){var t=function(e){for(var o=e.scales,t=Object.keys(o),n=0;n0&&(o.lineWidth=v.borderWidth,o.strokeStyle=v.borderColor||"rgba(225,225,225)",o.strokeRect(m,s,p,f)),o.restore()}},destroy:function(e){if(e.$zoom){var o=e.$zoom,t=o._node;t.removeEventListener("mousedown",o._mouseDownHandler),t.removeEventListener("mousemove",o._mouseMoveHandler),t.ownerDocument.removeEventListener("mouseup",o._mouseUpHandler),t.removeEventListener("wheel",o._wheelHandler),t.removeEventListener("click",o._ghostClickHandler),delete e.$zoom;var n=e._mc;n&&(n.remove("pinchstart"),n.remove("pinch"),n.remove("pinchend"),n.remove("panstart"),n.remove("pan"),n.remove("panend"),n.destroy())}}};return e.plugins.register(h),h}));