diff --git a/.eslintrc.yml b/.eslintrc.yml index f72034e8..4494fb97 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -20,3 +20,5 @@ plugins: ['html', 'es'] rules: complexity: ["warn", 10] max-statements: ["warn", 30] + no-var: "warn" + prefer-const: ["warn", {"destructuring": "all"}] diff --git a/samples/.eslintrc.yml b/samples/.eslintrc.yml index 618932b7..b0631976 100644 --- a/samples/.eslintrc.yml +++ b/samples/.eslintrc.yml @@ -9,3 +9,4 @@ globals: rules: indent: ["error", "tab", {flatTernaryExpressions: true}] no-new: "off" + no-var: "off" diff --git a/src/core.js b/src/core.js new file mode 100644 index 00000000..e576a3c4 --- /dev/null +++ b/src/core.js @@ -0,0 +1,115 @@ +import {each, callback as call} from 'chart.js/helpers'; +import {panFunctions, zoomFunctions} from './scale.types'; +import {directionEnabled, getEnabledScalesByPoint} from './utils'; + +function storeOriginalOptions(chart) { + const originalOptions = chart.$zoom._originalOptions; + each(chart.scales, function(scale) { + if (!originalOptions[scale.id]) { + originalOptions[scale.id] = {min: scale.options.min, max: scale.options.max}; + } + }); + each(originalOptions, function(opt, key) { + if (!chart.scales[key]) { + delete originalOptions[key]; + } + }); +} + +function zoomScale(scale, zoom, center, zoomOptions) { + call(zoomFunctions[scale.type], [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 {object} zoomOptions The zoom options + * @param {string} [whichAxes] `xy`, 'x', or 'y' + * @param {boolean} [useTransition] Whether to use `zoom` transition + */ +export function doZoom(chart, percentZoomX, percentZoomY, focalPoint, zoomOptions, whichAxes, useTransition) { + const ca = chart.chartArea; + if (!focalPoint) { + focalPoint = { + x: (ca.left + ca.right) / 2, + y: (ca.top + ca.bottom) / 2, + }; + } + + if (zoomOptions.enabled) { + storeOriginalOptions(chart); + // Do the zoom here + const zoomMode = typeof zoomOptions.mode === 'function' ? zoomOptions.mode({chart: chart}) : zoomOptions.mode; + + // Which axes should be modified when fingers were used. + let _whichAxes; + if (zoomMode === 'xy' && whichAxes !== undefined) { + // based on fingers positions + _whichAxes = whichAxes; + } else { + // no effect + _whichAxes = 'xy'; + } + + const enabledScales = getEnabledScalesByPoint(zoomOptions, focalPoint.x, focalPoint.y, chart); + each(enabledScales || 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); + } + }); + + chart.update(useTransition ? 'zoom' : 'none'); + + call(zoomOptions.onZoom, [chart]); + } +} + +export function resetZoom(chart) { + storeOriginalOptions(chart); + const originalOptions = chart.$zoom._originalOptions; + each(chart.scales, function(scale) { + + const scaleOptions = scale.options; + if (originalOptions[scale.id]) { + scaleOptions.min = originalOptions[scale.id].min; + scaleOptions.max = originalOptions[scale.id].max; + } else { + delete scaleOptions.min; + delete scaleOptions.max; + } + }); + chart.update(); +} + +function panScale(scale, delta, panOptions) { + call(panFunctions[scale.type], [scale, delta, panOptions]); +} + +export function doPan(chart, deltaX, deltaY, panOptions, panningScales) { + storeOriginalOptions(chart); + if (panOptions.enabled) { + const panMode = typeof panOptions.mode === 'function' ? panOptions.mode({chart}) : panOptions.mode; + + each(panningScales || chart.scales, function(scale) { + if (scale.isHorizontal() && directionEnabled(panMode, 'x', chart) && deltaX !== 0) { + panOptions.scaleAxes = 'x'; + panScale(scale, deltaX, panOptions); + } else if (!scale.isHorizontal() && directionEnabled(panMode, 'y', chart) && deltaY !== 0) { + panOptions.scaleAxes = 'y'; + panScale(scale, deltaY, panOptions); + } + }); + + chart.update('none'); + + call(panOptions.onPan, [chart]); + } +} + diff --git a/src/hammer.js b/src/hammer.js new file mode 100644 index 00000000..a1f11001 --- /dev/null +++ b/src/hammer.js @@ -0,0 +1,153 @@ +import Hammer from 'hammerjs'; +import {doPan, doZoom} from './core'; +import {getEnabledScalesByPoint} from './utils'; + +function createEnabler(chart, panOptions) { + return function(recognizer, event) { + if (!panOptions || !panOptions.enabled) { + return false; + } + if (!event || !event.srcEvent) { // Sometimes Hammer queries this with a null event. + return true; + } + const requireModifier = panOptions.modifierKey + && (event.pointerType === 'mouse'); + if (requireModifier && !event.srcEvent[panOptions.modifierKey + 'Key']) { + if (typeof panOptions.onPanRejected === 'function') { + panOptions.onPanRejected({ + chart: chart, + event: event + }); + } + return false; + } + return true; + }; +} + +export function startHammer(chart, options) { + const node = chart.canvas; + const {pan: panOptions, zoom: zoomOptions} = options; + + const mc = new Hammer.Manager(node); + if (zoomOptions && zoomOptions.enabled) { + mc.add(new Hammer.Pinch()); + } + if (panOptions && panOptions.enabled) { + mc.add(new Hammer.Pan({ + threshold: panOptions.threshold, + enable: createEnabler(chart, panOptions) + })); + } + + // Hammer reports the total scaling. We need the incremental amount + let currentPinchScaling; + const handlePinch = function(e) { + const diff = 1 / (currentPinchScaling) * e.scale; + const rect = e.target.getBoundingClientRect(); + const offsetX = e.center.x - rect.left; + const offsetY = e.center.y - rect.top; + const center = { + x: offsetX, + y: offsetY + }; + + // fingers position difference + const x = Math.abs(e.pointers[0].clientX - e.pointers[1].clientX); + const y = Math.abs(e.pointers[0].clientY - e.pointers[1].clientY); + + // diagonal fingers will change both (xy) axes + const p = x / y; + let xy; + if (p > 0.3 && p < 1.7) { + xy = 'xy'; + } else if (x > y) { + xy = 'x'; // x axis + } else { + xy = 'y'; // y axis + } + + doZoom(chart, diff, diff, center, zoomOptions, xy); + + if (typeof zoomOptions.onZoom === 'function') { + zoomOptions.onZoom({chart: chart}); + } + + // 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 + if (typeof zoomOptions.onZoomComplete === 'function') { + zoomOptions.onZoomComplete({chart: chart}); + } + }); + + let currentDeltaX = null; + let currentDeltaY = null; + let panning = false; + let panningScales = null; + const handlePan = function(e) { + if (currentDeltaX !== null && currentDeltaY !== null) { + panning = true; + const deltaX = e.deltaX - currentDeltaX; + const deltaY = e.deltaY - currentDeltaY; + currentDeltaX = e.deltaX; + currentDeltaY = e.deltaY; + doPan(chart, deltaX, deltaY, panOptions, panningScales); + } + }; + + mc.on('panstart', function(e) { + if (panOptions.enabled) { + const rect = e.target.getBoundingClientRect(); + const x = e.center.x - rect.left; + const y = e.center.y - rect.top; + panningScales = getEnabledScalesByPoint(panOptions, x, y, chart); + } + + currentDeltaX = 0; + currentDeltaY = 0; + handlePan(e); + }); + mc.on('panmove', handlePan); + mc.on('panend', function() { + currentDeltaX = null; + currentDeltaY = null; + setTimeout(function() { + panning = false; + }, 500); + if (typeof panOptions.onPanComplete === 'function') { + panOptions.onPanComplete({chart: chart}); + } + }); + + chart.$zoom._ghostClickHandler = function(e) { + if (panning && e.cancelable) { + e.stopImmediatePropagation(); + e.preventDefault(); + } + }; + node.addEventListener('click', chart.$zoom._ghostClickHandler); + + chart._mc = mc; +} + +export function stopHammer(chart) { + const mc = chart._mc; + if (mc) { + mc.remove('pinchstart'); + mc.remove('pinch'); + mc.remove('pinchend'); + mc.remove('panstart'); + mc.remove('pan'); + mc.remove('panend'); + mc.destroy(); + } +} diff --git a/src/handlers.js b/src/handlers.js new file mode 100644 index 00000000..4bbe6af2 --- /dev/null +++ b/src/handlers.js @@ -0,0 +1,165 @@ +import {directionEnabled, debounce} from './utils'; +import {doZoom} from './core'; +import {callback as call} from 'chart.js/helpers'; + +function removeHandler(target, type, chart) { + const props = chart.$zoom; + const handlers = props._handlers || (props._handlers = {}); + const handler = handlers[type]; + if (handler) { + target.removeEventListener(type, handler); + delete handlers[type]; + } +} + +function addHandler(target, type, handler, {chart, options}) { + const props = chart.$zoom; + const handlers = props._handlers || (props._handlers = {}); + removeHandler(target, type, chart); + handlers[type] = (event) => handler(chart, event, options); + target.addEventListener(type, handlers[type]); +} + +export function mouseMove(chart, event) { + if (chart.$zoom._dragZoomStart) { + chart.$zoom._dragZoomEnd = event; + chart.update('none'); + } +} + +export function mouseDown(chart, event, options) { + addHandler(chart.canvas, 'mousemove', mouseMove, {chart, options}); + chart.$zoom._dragZoomStart = event; +} + +export function computeDragRect(chart, mode, beginPoint, endPoint) { + const {left: offsetX, top: offsetY} = beginPoint.target.getBoundingClientRect(); + const xEnabled = directionEnabled(mode, 'x', chart); + const yEnabled = directionEnabled(mode, 'y', chart); + let {top, left, right, bottom, width: chartWidth, height: chartHeight} = chart.chartArea; + + if (xEnabled) { + left = Math.min(beginPoint.clientX, endPoint.clientX) - offsetX; + right = Math.max(beginPoint.clientX, endPoint.clientX) - offsetX; + } + + if (yEnabled) { + top = Math.min(beginPoint.clientY, endPoint.clientY) - offsetY; + bottom = Math.max(beginPoint.clientY, endPoint.clientY) - offsetY; + } + const width = right - left; + const height = bottom - top; + + return { + left, + top, + right, + bottom, + width, + height, + zoomX: xEnabled && width ? 1 + ((chartWidth - width) / chartWidth) : 1, + zoomY: yEnabled && height ? 1 + ((chartHeight - height) / chartHeight) : 1 + }; +} + +export function mouseUp(chart, event, options) { + if (!chart.$zoom || !chart.$zoom._dragZoomStart) { + return; + } + + const zoomOptions = options.zoom; + removeHandler(chart.canvas, 'mousemove', chart); + + const props = chart.$zoom; + const beginPoint = props._dragZoomStart; + const rect = computeDragRect(chart, zoomOptions.mode, beginPoint, event); + const {width: dragDistanceX, height: dragDistanceY} = rect; + + // Remove drag start and end before chart update to stop drawing selected area + props._dragZoomStart = null; + props._dragZoomEnd = null; + + const zoomThreshold = zoomOptions.threshold || 0; + if (dragDistanceX <= zoomThreshold && dragDistanceY <= zoomThreshold) { + return; + } + + const {top, left, width, height} = chart.chartArea; + const focalPoint = { + x: (rect.left - left) / (1 - dragDistanceX / width) + left, + y: (rect.top - top) / (1 - dragDistanceY / height) + top + }; + doZoom(chart, rect.zoomX, rect.zoomY, focalPoint, zoomOptions, undefined, true); + + call(zoomOptions.onZoomComplete, [chart]); +} + +export function wheel(chart, event, options) { + const zoomOptions = options.zoom; + const {wheelModifierKey, onZoomRejected, onZoomComplete} = zoomOptions; + + // Before preventDefault, check if the modifier key required and pressed + if (wheelModifierKey && !event[wheelModifierKey + 'Key']) { + return call(onZoomRejected, [{chart, 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; + } + + const rect = event.target.getBoundingClientRect(); + const speed = 1 + (event.deltaY >= 0 ? -zoomOptions.speed : zoomOptions.speed); + const center = { + x: event.clientX - rect.left, + y: event.clientY - rect.top + }; + + doZoom(chart, speed, speed, center, zoomOptions); + + if (onZoomComplete) { + debounce(() => call(onZoomComplete, [{chart}]), 250); + } +} + +export function addListeners(chart, options) { + const props = chart.$zoom; + const canvas = chart.canvas; + + // 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 + const zoomEnabled = options.zoom && options.zoom.enabled; + const dragEnabled = options.zoom.drag; + if (zoomEnabled && !dragEnabled) { + addHandler(canvas, 'wheel', wheel, {chart, options}); + } else if (props._wheelHandler) { + removeHandler(canvas, 'wheel', chart); + } + if (zoomEnabled && dragEnabled) { + addHandler(canvas, 'mousedown', mouseDown, {chart, options}); + addHandler(canvas.ownerDocument, 'mouseup', mouseUp, {chart, options}); + } else { + removeHandler(canvas, 'mousedown', chart); + removeHandler(canvas, 'mousemove', chart); + removeHandler(canvas.ownerDocument, 'mouseup', chart); + } +} + +export function removeListeners(chart) { + const {canvas, $zoom: props} = chart; + if (!canvas || !props) { + return; + } + removeHandler(canvas, 'mousedown', chart); + removeHandler(canvas, 'mousemove', chart); + removeHandler(canvas.ownerDocument, 'mouseup', chart); + removeHandler(canvas, 'wheel', chart); + removeHandler(canvas, 'click', chart); +} diff --git a/src/plugin.js b/src/plugin.js index 0061c45c..475e0553 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -1,351 +1,9 @@ -import {clone, each, isNullOrUndef} from 'chart.js/helpers'; import Hammer from 'hammerjs'; +import {addListeners, computeDragRect, removeListeners} from './handlers'; +import {startHammer, stopHammer} from './hammer'; +import {resetZoom} from './core'; -// Zoom namespace (kept under Chart prior to Chart.js 3) -var zoomNS = {}; - -// Where we store functions to handle different scale types -var zoomFunctions = zoomNS.zoomFunctions = zoomNS.zoomFunctions || {}; -var panFunctions = zoomNS.panFunctions = zoomNS.panFunctions || {}; - -function resolveOptions(chart, options) { - var props = chart.$zoom; - props._options = options; - - // 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; - each(chart.scales, function(scale) { - if (!originalOptions[scale.id]) { - originalOptions[scale.id] = clone(scale.options); - } - }); - each(originalOptions, function(opt, key) { - if (!chart.scales[key]) { - delete originalOptions[key]; - } - }); -} - -/** - * @param {string|function} [mode] can be 'x', 'y' or 'xy' - * @param {string} [dir] can be 'x' or 'y' - * @param {import('chart.js').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; -} - -/** This function use for check what axis now under mouse cursor. - * @param {number} [x] X position - * @param {number} [y] Y position - * @param {import('chart.js').Chart} [chart] instance of the chart in question - */ -function getScaleUnderPoint(x, y, chart) { - var scales = chart.scales; - var scaleIds = Object.keys(scales); - for (var i = 0; i < scaleIds.length; i++) { - var scale = scales[scaleIds[i]]; - if (y >= scale.top && y <= scale.bottom && x >= scale.left && x <= scale.right) { - return scale; - } - } - return null; -} - -/** This function return only one scale whose position is under mouse cursor and which direction is enabled. - * If under mouse hasn't scale, then return all other scales which 'mode' is diffrent with overScaleMode. - * So 'overScaleMode' works as a limiter to scale the user-selected scale (in 'mode') only when the cursor is under the scale, - * and other directions in 'mode' works as before. - * Example: mode = 'xy', overScaleMode = 'y' -> it's means 'x' - works as before, and 'y' only works for one scale when cursor is under it. - * options.overScaleMode can be a function if user want zoom only one scale of many for example. - * @param {any} [options] pan or zoom options - * @param {number} [x] X position - * @param {number} [y] Y position - * @param {import('chart.js').Chart} [chart] instance of the chart in question - */ -function getEnabledScalesByPoint(options, x, y, chart) { - if (options.enabled && options.overScaleMode) { - var scale = getScaleUnderPoint(x, y, chart); - var mode = typeof options.overScaleMode === 'function' ? options.overScaleMode({chart: chart}, scale) : options.overScaleMode; - - if (scale && directionEnabled(mode, scale.axis, chart)) { - return [scale]; - } - - var enabledScales = []; - each(chart.scales, function(scaleItem) { - if (!directionEnabled(mode, scaleItem.axis, chart)) { - enabledScales.push(scaleItem); - } - }); - return enabledScales; - } - return null; -} - -function rangeMaxLimiter(zoomPanOptions, newMax) { - if (zoomPanOptions.scaleAxes && zoomPanOptions.rangeMax && - !isNullOrUndef(zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes])) { - const rangeMax = zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes]; - if (newMax > rangeMax) { - newMax = rangeMax; - } - } - return newMax; -} - -function rangeMinLimiter(zoomPanOptions, newMin) { - if (zoomPanOptions.scaleAxes && zoomPanOptions.rangeMin && - !isNullOrUndef(zoomPanOptions.rangeMin[zoomPanOptions.scaleAxes])) { - const rangeMin = zoomPanOptions.rangeMin[zoomPanOptions.scaleAxes]; - if (newMin < rangeMin) { - newMin = rangeMin; - } - } - return newMin; -} - -function zoomDelta(scale, zoom, center) { - const range = scale.max - scale.min; - const newDiff = range * (zoom - 1); - - const centerPoint = scale.isHorizontal() ? center.x : center.y; - const minPercent = (scale.getValueForPixel(centerPoint) - scale.min) / range; - const maxPercent = 1 - minPercent; - - return { - min: newDiff * minPercent, - max: newDiff * maxPercent - }; -} - -function zoomNumericalScale(scale, zoom, center, zoomOptions) { - const delta = zoomDelta(scale, zoom, center); - scale.options.min = rangeMinLimiter(zoomOptions, scale.min + delta.min); - scale.options.max = rangeMaxLimiter(zoomOptions, scale.max - delta.max); -} - -const integerChange = (v) => v === 0 || isNaN(v) ? 0 : v < 0 ? Math.min(Math.round(v), -1) : Math.max(Math.round(v), 1); - -function zoomCategoryScale(scale, zoom, center, zoomOptions) { - const labels = scale.getLabels(); - const maxIndex = labels.length - 1; - if (scale.min === scale.max && zoom < 1) { - if (scale.min > 0) { - scale.min--; - } else if (scale.max < maxIndex) { - scale.max++; - } - } - const delta = zoomDelta(scale, zoom, center); - scale.options.min = labels[Math.max(0, rangeMinLimiter(zoomOptions, scale.min + integerChange(delta.min)))]; - scale.options.max = labels[Math.min(maxIndex, rangeMaxLimiter(zoomOptions, scale.max - integerChange(delta.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 {boolean} [useTransition] Whether to use `zoom` transition - */ -function doZoom(chart, percentZoomX, percentZoomY, focalPoint, whichAxes, useTransition) { - 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 axes should be modified when fingers were used. - var _whichAxes; - if (zoomMode === 'xy' && whichAxes !== undefined) { - // based on fingers positions - _whichAxes = whichAxes; - } else { - // no effect - _whichAxes = 'xy'; - } - - var enabledScales = getEnabledScalesByPoint(zoomOptions, focalPoint.x, focalPoint.y, chart); - - each(enabledScales || 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); - } - }); - - chart.update(useTransition ? 'zoom' : 'none'); - - if (typeof zoomOptions.onZoom === 'function') { - zoomOptions.onZoom({chart: chart}); - } - } -} - -function panCategoryScale(scale, delta, panOptions) { - const labels = scale.getLabels(); - const lastLabelIndex = labels.length - 1; - const offsetAmt = Math.max(scale.ticks.length, 1); - const panSpeed = panOptions.speed; - const step = Math.round(scale.width / (offsetAmt * panSpeed)); - let minIndex = scale.min; - let 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.min ? 0 : zoomNS.panCumulativeDelta; - - maxIndex = Math.min(lastLabelIndex, minIndex + offsetAmt - 1); - - scale.options.min = rangeMinLimiter(panOptions, labels[minIndex]); - scale.options.max = rangeMaxLimiter(panOptions, labels[maxIndex]); -} - -function panNumericalScale(scale, delta, panOptions) { - const scaleOpts = scale.options; - const prevStart = scale.min; - const prevEnd = scale.max; - const newMin = scale.getValueForPixel(scale.getPixelForValue(prevStart) - delta); - const newMax = scale.getValueForPixel(scale.getPixelForValue(prevEnd) - delta); - const rangeMin = rangeMinLimiter(panOptions, newMin); - const rangeMax = rangeMaxLimiter(panOptions, newMax); - let diff; - - if (newMin >= rangeMin && newMax <= rangeMax) { - scaleOpts.min = newMin; - scaleOpts.max = newMax; - } else if (newMin < rangeMin) { - diff = prevStart - rangeMin; - scaleOpts.min = rangeMin; - scaleOpts.max = prevEnd - diff; - } else if (newMax > rangeMax) { - diff = rangeMax - prevEnd; - scaleOpts.max = rangeMax; - scaleOpts.min = prevStart + diff; - } -} - -function panScale(scale, delta, panOptions) { - const fn = panFunctions[scale.type]; - if (fn) { - fn(scale, delta, panOptions); - } -} - -function doPan(chartInstance, deltaX, deltaY, panningScales) { - storeOriginalOptions(chartInstance); - var panOptions = chartInstance.$zoom._options.pan; - if (panOptions.enabled) { - var panMode = typeof panOptions.mode === 'function' ? panOptions.mode({chart: chartInstance}) : panOptions.mode; - - each(panningScales || 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('none'); - - 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 = zoomNumericalScale; -zoomNS.zoomFunctions.linear = zoomNumericalScale; -zoomNS.zoomFunctions.logarithmic = zoomNumericalScale; -zoomNS.panFunctions.category = panCategoryScale; -zoomNS.panFunctions.time = panNumericalScale; -zoomNS.panFunctions.linear = panNumericalScale; -zoomNS.panFunctions.logarithmic = panNumericalScale; -// Globals for category pan -zoomNS.panCumulativeDelta = 0; - -// Chartjs Zoom Plugin -var zoomPlugin = { +export default { id: 'zoom', defaults: { @@ -365,359 +23,56 @@ var zoomPlugin = { } }, - start: function(chartInstance, args, pluginOptions) { - chartInstance.$zoom = { + start: function(chart, args, options) { + chart.$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('none'); - } - }; - - 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, true); - - if (typeof zoomOptions.onZoomComplete === 'function') { - zoomOptions.onZoomComplete({chart: chartInstance}); - } - }; - - var _scrollTimeout = null; - chartInstance.$zoom._wheelHandler = function(event) { - var zoomOptions = chartInstance.$zoom._options.zoom; - - // Before preventDefault, check if the modifier key required and pressed - if (zoomOptions - && zoomOptions.wheelModifierKey - && !event[zoomOptions.wheelModifierKey + 'Key']) { - if (typeof zoomOptions.onZoomRejected === 'function') { - zoomOptions.onZoomRejected({ - chart: chartInstance, - event: event - }); - } - return; - } - - // 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 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); - }; + addListeners(chart, options); if (Hammer) { - var panEnabler = function(recognizer, event) { - const panOptions = chartInstance.$zoom._options.pan; - if (!panOptions || !panOptions.enabled) { - return false; - } - if (!event || !event.srcEvent) { // Sometimes Hammer queries this with a null event. - return true; - } - const requireModifier = panOptions.modifierKey - && (event.pointerType === 'mouse'); - if (requireModifier && !event.srcEvent[panOptions.modifierKey + 'Key']) { - if (typeof panOptions.onPanRejected === 'function') { - panOptions.onPanRejected({ - chart: chartInstance, - event: event - }); - } - return false; - } - return true; - }; - - var zoomOptions = chartInstance.$zoom._options.zoom; - var panOptions = chartInstance.$zoom._options.pan; - var mc = new Hammer.Manager(node); - if (zoomOptions && zoomOptions.enabled) { - mc.add(new Hammer.Pinch()); - } - if (panOptions && panOptions.enabled) { - mc.add(new Hammer.Pan({ - threshold: panThreshold, - enable: panEnabler - })); - } - - // 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); - - if (typeof zoomOptions.onZoom === 'function') { - zoomOptions.onZoom({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 - if (typeof zoomOptions.onZoomComplete === 'function') { - zoomOptions.onZoomComplete({chart: chartInstance}); - } - }); - - var currentDeltaX = null; - var currentDeltaY = null; - var panning = false; - var panningScales = null; - 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, panningScales); - } - }; - - mc.on('panstart', function(e) { - if (panOptions.enabled) { - var rect = e.target.getBoundingClientRect(); - var x = e.center.x - rect.left; - var y = e.center.y - rect.top; - panningScales = getEnabledScalesByPoint(panOptions, x, y, chartInstance); - } - - currentDeltaX = 0; - currentDeltaY = 0; - handlePan(e); - }); - mc.on('panmove', handlePan); - mc.on('panend', function() { - panningScales = null; - currentDeltaX = null; - currentDeltaY = null; - setTimeout(function() { - panning = false; - }, 500); - 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; + startHammer(chart, options); } - - chartInstance.resetZoom = function() { - storeOriginalOptions(chartInstance); - var originalOptions = chartInstance.$zoom._originalOptions; - each(chartInstance.scales, function(scale) { - - var scaleOptions = scale.options; - if (originalOptions[scale.id]) { - scaleOptions.min = originalOptions[scale.id].min; - scaleOptions.max = originalOptions[scale.id].max; - } else { - delete scaleOptions.min; - delete scaleOptions.max; - } - }); - - chartInstance.update('none'); - }; + chart.resetZoom = () => resetZoom(chart); }, beforeUpdate: function(chart, args, options) { - resolveOptions(chart, options); + addListeners(chart, options); }, - 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; - } + beforeDatasetsDraw: function(chart, args, options) { + const {$zoom: props = {}, ctx} = chart; + const {_dragZoomStart: beginPoint, _dragZoomEnd: endPoint} = props; - 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; - } + if (endPoint) { + const {left, top, width, height} = computeDragRect(chart, options.zoom.mode, beginPoint, endPoint); - var rectWidth = endX - startX; - var rectHeight = endY - startY; - var dragOptions = chartInstance.$zoom._options.zoom.drag; + const dragOptions = options.zoom.drag; ctx.save(); ctx.beginPath(); ctx.fillStyle = dragOptions.backgroundColor || 'rgba(225,225,225,0.3)'; - ctx.fillRect(startX, startY, rectWidth, rectHeight); + ctx.fillRect(left, top, width, height); if (dragOptions.borderWidth > 0) { ctx.lineWidth = dragOptions.borderWidth; ctx.strokeStyle = dragOptions.borderColor || 'rgba(225,225,225)'; - ctx.strokeRect(startX, startY, rectWidth, rectHeight); + ctx.strokeRect(left, top, width, height); } ctx.restore(); } }, - stop: function(chartInstance) { - if (!chartInstance.$zoom) { + stop: function(chart) { + if (!chart.$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); + removeListeners(chart); - delete chartInstance.$zoom; + delete chart.$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(); + if (Hammer) { + stopHammer(chart); } } }; - -export default zoomPlugin; diff --git a/src/scale.types.js b/src/scale.types.js new file mode 100644 index 00000000..62e9a687 --- /dev/null +++ b/src/scale.types.js @@ -0,0 +1,119 @@ +import {isNullOrUndef} from 'chart.js/helpers'; + +function rangeMaxLimiter(zoomPanOptions, newMax) { + const {scaleAxes, rangeMax} = zoomPanOptions; + if (scaleAxes && rangeMax && !isNullOrUndef(rangeMax[scaleAxes])) { + const limit = rangeMax[scaleAxes]; + if (newMax > limit) { + newMax = limit; + } + } + return newMax; +} + +function rangeMinLimiter(zoomPanOptions, newMin) { + const {scaleAxes, rangeMin} = zoomPanOptions; + if (scaleAxes && rangeMin && !isNullOrUndef(rangeMin[scaleAxes])) { + const limit = rangeMin[scaleAxes]; + if (newMin < limit) { + newMin = limit; + } + } + return newMin; +} + +function zoomDelta(scale, zoom, center) { + const range = scale.max - scale.min; + const newDiff = range * (zoom - 1); + + const centerPoint = scale.isHorizontal() ? center.x : center.y; + const minPercent = (scale.getValueForPixel(centerPoint) - scale.min) / range; + const maxPercent = 1 - minPercent; + + return { + min: newDiff * minPercent, + max: newDiff * maxPercent + }; +} + +function zoomNumericalScale(scale, zoom, center, zoomOptions) { + const delta = zoomDelta(scale, zoom, center); + scale.options.min = rangeMinLimiter(zoomOptions, scale.min + delta.min); + scale.options.max = rangeMaxLimiter(zoomOptions, scale.max - delta.max); +} + +const integerChange = (v) => v === 0 || isNaN(v) ? 0 : v < 0 ? Math.min(Math.round(v), -1) : Math.max(Math.round(v), 1); + +function zoomCategoryScale(scale, zoom, center, zoomOptions) { + const labels = scale.getLabels(); + const maxIndex = labels.length - 1; + if (scale.min === scale.max && zoom < 1) { + if (scale.min > 0) { + scale.min--; + } else if (scale.max < maxIndex) { + scale.max++; + } + } + const delta = zoomDelta(scale, zoom, center); + scale.options.min = labels[Math.max(0, rangeMinLimiter(zoomOptions, scale.min + integerChange(delta.min)))]; + scale.options.max = labels[Math.min(maxIndex, rangeMaxLimiter(zoomOptions, scale.max - integerChange(delta.max)))]; +} + +const categoryDelta = new WeakMap(); +function panCategoryScale(scale, delta, panOptions) { + const labels = scale.getLabels(); + const lastLabelIndex = labels.length - 1; + const offsetAmt = Math.max(scale.ticks.length, 1); + const panSpeed = panOptions.speed; + const step = Math.round(scale.width / (offsetAmt * panSpeed)); + const cumDelta = (categoryDelta.get(scale) || 0) + delta; + const scaleMin = scale.min; + const minIndex = cumDelta > step ? Math.max(0, scaleMin - 1) + : cumDelta < -step ? Math.min(lastLabelIndex - offsetAmt + 1, scaleMin + 1) + : scaleMin; + const maxIndex = Math.min(lastLabelIndex, minIndex + offsetAmt - 1); + + categoryDelta.set(scale, minIndex !== scaleMin ? 0 : cumDelta); + + + scale.options.min = rangeMinLimiter(panOptions, labels[minIndex]); + scale.options.max = rangeMaxLimiter(panOptions, labels[maxIndex]); +} + +function panNumericalScale(scale, delta, panOptions) { + const scaleOpts = scale.options; + const prevStart = scale.min; + const prevEnd = scale.max; + const newMin = scale.getValueForPixel(scale.getPixelForValue(prevStart) - delta); + const newMax = scale.getValueForPixel(scale.getPixelForValue(prevEnd) - delta); + const rangeMin = rangeMinLimiter(panOptions, newMin); + const rangeMax = rangeMaxLimiter(panOptions, newMax); + let diff; + + if (newMin >= rangeMin && newMax <= rangeMax) { + scaleOpts.min = newMin; + scaleOpts.max = newMax; + } else if (newMin < rangeMin) { + diff = prevStart - rangeMin; + scaleOpts.min = rangeMin; + scaleOpts.max = prevEnd - diff; + } else if (newMax > rangeMax) { + diff = rangeMax - prevEnd; + scaleOpts.max = rangeMax; + scaleOpts.min = prevStart + diff; + } +} + +export const zoomFunctions = { + category: zoomCategoryScale, + time: zoomNumericalScale, + linear: zoomNumericalScale, + logarithmic: zoomNumericalScale, +}; + +export const panFunctions = { + category: panCategoryScale, + time: panNumericalScale, + linear: panNumericalScale, + logarithmic: panNumericalScale, +}; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 00000000..526ae2fb --- /dev/null +++ b/src/utils.js @@ -0,0 +1,110 @@ +import {each} from 'chart.js/helpers'; + +/** + * @param {string|function} mode can be 'x', 'y' or 'xy' + * @param {string} dir can be 'x' or 'y' + * @param {import('chart.js').Chart} chart instance of the chart in question + * @returns {boolean} + */ +export 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}).indexOf(dir) !== -1; + } + + return false; +} + +export function getXAxis(chartInstance) { + const scales = chartInstance.scales; + const scaleIds = Object.keys(scales); + for (let i = 0; i < scaleIds.length; i++) { + const scale = scales[scaleIds[i]]; + + if (scale.isHorizontal()) { + return scale; + } + } +} + +export function getYAxis(chartInstance) { + const scales = chartInstance.scales; + const scaleIds = Object.keys(scales); + for (let i = 0; i < scaleIds.length; i++) { + const scale = scales[scaleIds[i]]; + + if (!scale.isHorizontal()) { + return scale; + } + } +} + +/** + * Debounces calling `fn` for `delay` ms + * @param {function} fn - Function to call. No arguments are passed. + * @param {number} delay - Delay in ms. 0 = immediate invocation. + * @returns {function} + */ +export function debounce(fn, delay) { + let timeout; + return function() { + if (delay) { + clearTimeout(timeout); + timeout = setTimeout(fn, delay); + } else { + fn(); + } + return delay; + }; +} + +/** This function use for check what axis now under mouse cursor. + * @param {number} [x] X position + * @param {number} [y] Y position + * @param {import('chart.js').Chart} [chart] instance of the chart in question + */ +function getScaleUnderPoint(x, y, chart) { + const scales = chart.scales; + const scaleIds = Object.keys(scales); + for (let i = 0; i < scaleIds.length; i++) { + const scale = scales[scaleIds[i]]; + if (y >= scale.top && y <= scale.bottom && x >= scale.left && x <= scale.right) { + return scale; + } + } + return null; +} + +/** This function return only one scale whose position is under mouse cursor and which direction is enabled. + * If under mouse hasn't scale, then return all other scales which 'mode' is diffrent with overScaleMode. + * So 'overScaleMode' works as a limiter to scale the user-selected scale (in 'mode') only when the cursor is under the scale, + * and other directions in 'mode' works as before. + * Example: mode = 'xy', overScaleMode = 'y' -> it's means 'x' - works as before, and 'y' only works for one scale when cursor is under it. + * options.overScaleMode can be a function if user want zoom only one scale of many for example. + * @param {any} [options] pan or zoom options + * @param {number} [x] X position + * @param {number} [y] Y position + * @param {import('chart.js').Chart} [chart] instance of the chart in question + */ +export function getEnabledScalesByPoint(options, x, y, chart) { + if (options.enabled && options.overScaleMode) { + const scale = getScaleUnderPoint(x, y, chart); + const mode = typeof options.overScaleMode === 'function' ? options.overScaleMode({chart: chart}, scale) : options.overScaleMode; + + if (scale && directionEnabled(mode, scale.axis, chart)) { + return [scale]; + } + + const enabledScales = []; + each(chart.scales, function(scaleItem) { + if (!directionEnabled(mode, scaleItem.axis, chart)) { + enabledScales.push(scaleItem); + } + }); + return enabledScales; + } + return null; +} diff --git a/test/fixtures/pan/category-x.js b/test/fixtures/pan/category-x.js index 6c97d8be..f1e660dd 100644 --- a/test/fixtures/pan/category-x.js +++ b/test/fixtures/pan/category-x.js @@ -11,6 +11,7 @@ canvas.height = 512; const ctx = canvas.getContext('2d'); module.exports = { + tolerance: 0.02, config: { type: 'bar', data: { diff --git a/test/fixtures/pan/category-x.png b/test/fixtures/pan/category-x.png index 2e33292e..3ee78c56 100644 Binary files a/test/fixtures/pan/category-x.png and b/test/fixtures/pan/category-x.png differ diff --git a/test/index.js b/test/index.js index 69f1fa86..b96aada8 100644 --- a/test/index.js +++ b/test/index.js @@ -13,9 +13,9 @@ jasmine.fixture = { jasmine.triggerMouseEvent = triggerMouseEvent; jasmine.triggerWheelEvent = function(chart, init = {}) { - var node = chart.canvas; - var rect = node.getBoundingClientRect(); - var event = new WheelEvent('wheel', Object.assign({}, init, { + const node = chart.canvas; + const rect = node.getBoundingClientRect(); + const event = new WheelEvent('wheel', Object.assign({}, init, { clientX: rect.left + init.x, clientY: rect.top + init.y, cancelable: true, diff --git a/test/specs/module.spec.js b/test/specs/module.spec.js index 85faa28d..cace4637 100644 --- a/test/specs/module.spec.js +++ b/test/specs/module.spec.js @@ -8,7 +8,7 @@ describe('module', function() { }); it ('should be globally registered', function() { - var plugin = Chart.registry.getPlugin('zoom'); + const plugin = Chart.registry.getPlugin('zoom'); expect(plugin).toBe(window.ChartZoom); }); }); diff --git a/test/specs/zoom.spec.js b/test/specs/zoom.spec.js index f470aab3..433b526c 100644 --- a/test/specs/zoom.spec.js +++ b/test/specs/zoom.spec.js @@ -316,7 +316,7 @@ describe('zoom', function() { }); describe('with overScaleMode = y and mode = xy', function() { - let config = { + const config = { type: 'line', data, options: {