Repository: chartjs/chartjs-plugin-zoom
Pull request #671 (2022-09-01 03:30:04)

Only drag zoom on left mouse button
This makes drag act the same as pan (which is implemented by Hammer.js, which - as far as I can tell - automatically only listens to the left / primary mouse button).

By default, right-click is intercepted by the browser to display a context menu, so this isn't noticeable,  However, if `oncontextmenu` is used to disable that (e.g., to use JS to display a menu instead), then right-click can trigger zoom, which feels strange.


Patch:
diff --git a/docs/guide/developers.md b/docs/guide/developers.md
index 80f4811..41b9d05 100644
--- a/docs/guide/developers.md
+++ b/docs/guide/developers.md
@@ -44,7 +44,7 @@ Returns whether the chart has been zoomed or panned - i.e. whether the initial s
 
 ## Custom Scales
 
-You can extend chartjs-plugin-zoom with support for [custom scales](https://www.chartjs.org/docs/latest/developers/axes.html) by using the zoom plugin's `zoomFunctions` and `panFunctions` members. These objects are indexed by scale types (scales' `id` members) and give optional handlers for zoom and pan functionality.
+You can extend chartjs-plugin-zoom with support for [custom scales](https://www.chartjs.org/docs/latest/developers/axes.html) by using the zoom plugin's `zoomFunctions`, `zoomRectFunctions`, and `panFunctions` members. These objects are indexed by scale types (scales' `id` members) and give optional handlers for zoom and pan functionality.
 
 ```js
 import {Scale} from 'chart.js';
@@ -57,17 +57,22 @@ MyScale.id = 'myScale';
 MyScale.defaults = defaultConfigObject;
 
 zoomPlugin.zoomFunctions.myScale = (scale, zoom, center, limits) => false;
+zoomPlugin.zoomRectFunctions.myScale = (scale, from, to, limits) => false;
 zoomPlugin.panFunctions.myScale = (scale, delta, limits) => false;
+// zoomRectFunctions can normally be omitted, since zooming by specific pixel
+// coordinates rarely needs special handling.
 ```
 
-The zoom and pan functions take the following arguments:
+The zoom, zoomRect, and pan functions take the following arguments:
 
 | Name | Type | For | Description
 | ---- | ---- | --- | ----------
 | `scale` | `Scale` | Zoom, Pan | The custom scale instance (usually derived from `Chart.Scale`)
 | `zoom` | `number` | Zoom | The zoom fraction; 1.0 is unzoomed, 0.5 means zoomed in to 50% of the original area, etc.
 | `center` | `{x, y}` | Zoom | Pixel coordinates of the center of the zoom operation. `{x: 0, y: 0}` is the upper left corner of the chart's canvas.
+| `from` | `number` | ZoomRect | Pixel coordinate of the start of the zoomRect operation.
+| `to` | `number` | ZoomRect | Pixel coordinate of the end of the zoomRect operation.
 | `delta` | `number` | Pan | Pixel amount to pan by
 | `limits` | [Limits](./options#limits) | Zoom, Pan | Zoom and pan limits (from chart options)
 
-For examples, see chartjs-plugin-zoom's [default zoomFunctions and panFunctions handling for standard Chart.js axes](https://github.com/chartjs/chartjs-plugin-zoom/blob/v1.0.1/src/scale.types.js#L128).
+For examples, see chartjs-plugin-zoom's [default zoomFunctions, zoomRectFunctions, and panFunctions handling for standard Chart.js axes](https://github.com/chartjs/chartjs-plugin-zoom/blob/v1.0.1/src/scale.types.js#L128).
diff --git a/docs/guide/options.md b/docs/guide/options.md
index f45a50e..2eedf5c 100644
--- a/docs/guide/options.md
+++ b/docs/guide/options.md
@@ -35,7 +35,8 @@ const chart = new Chart('id', {
 | `enabled` | `boolean` | `false` | Enable panning
 | `mode` | `'x'`\|`'y'`\|`'xy'` | `'xy'` | Allowed panning directions
 | `modifierKey` | `'ctrl'`\|`'alt'`\|`'shift'`\|`'meta'` | `null` |  Modifier key required for panning with mouse
-| `overScaleMode` | `'x'`\|`'y'`\|`'xy'` | `undefined` | Which of the enabled panning directions should only be available when the mouse cursor is over a scale for that axis
+| `scaleMode` | `'x'`\|`'y'`\|`'xy'` | `undefined` | Enable panning over a scale for that axis (regardless of mode)
+| `overScaleMode` | `'x'`\|`'y'`\|`'xy'` | `undefined` | Enable panning over a scale for that axis (but only if mode is also enabled), and disables panning along that axis otherwise. Deprecated.
 | `threshold` | `number` | `10` | Minimal pan distance required before actually applying pan
 
 ### Pan Events
@@ -57,7 +58,8 @@ const chart = new Chart('id', {
 | `drag` | [`DragOptions`](#drag-options) | | Options of the drag-to-zoom behavior
 | `pinch` | [`PinchOptions`](#pinch-options) | | Options of the pinch behavior
 | `mode` | `'x'`\|`'y'`\|`'xy'` | `'xy'` | Allowed zoom directions
-| `overScaleMode` | `'x'`\|`'y'`\|`'xy'` | `undefined` | Which of the enabled zooming directions should only be available when the mouse cursor is over a scale for that axis
+| `scaleMode` | `'x'`\|`'y'`\|`'xy'` | `undefined` | Which of the enabled zooming directions should only be available when the mouse cursor is over a scale for that axis
+| `overScaleMode` | `'x'`\|`'y'`\|`'xy'` | `undefined` | Allowed zoom directions when the mouse cursor is over a scale for that axis (but only if mode is also enabled), and disables zooming along that axis otherwise. Deprecated; use `scaleMode` instead.
 
 #### Wheel options
 
diff --git a/docs/samples/wheel/over-scale-mode.md b/docs/samples/wheel/over-scale-mode.md
index d3b707d..685791d 100644
--- a/docs/samples/wheel/over-scale-mode.md
+++ b/docs/samples/wheel/over-scale-mode.md
@@ -62,12 +62,12 @@ const zoomOptions = {
       enabled: true,
     },
     mode: 'xy',
-    overScaleMode: 'xy',
+    scaleMode: 'xy',
   },
   pan: {
     enabled: true,
     mode: 'xy',
-    overScaleMode: 'xy',
+    scaleMode: 'xy',
   }
 };
 // </block>
diff --git a/samples/zoom-separately.html b/samples/zoom-separately.html
index 84e2ca0..d5d20dc 100644
--- a/samples/zoom-separately.html
+++ b/samples/zoom-separately.html
@@ -100,8 +100,8 @@
 						zoom: {
 							pan: {
 								enabled: true,
-								mode: 'xy',
-								overScaleMode: 'y'
+								mode: 'x',
+								scaleMode: 'y'
 							},
 							zoom: {
 								wheel: {
@@ -110,8 +110,8 @@
 								pinch: {
 									enabled: true,
 								},
-								mode: 'xy',
-								overScaleMode: 'y'
+								mode: 'x',
+								scaleMode: 'y'
 							}
 						}
 					}
diff --git a/src/core.js b/src/core.js
index 79b4ad4..2011676 100644
--- a/src/core.js
+++ b/src/core.js
@@ -1,5 +1,5 @@
 import {each, callback as call, sign, valueOrDefault} from 'chart.js/helpers';
-import {panFunctions, updateRange, zoomFunctions} from './scale.types';
+import {panFunctions, updateRange, zoomFunctions, zoomRectFunctions} from './scale.types';
 import {getState} from './state';
 import {directionEnabled, getEnabledScalesByPoint} from './utils';
 
@@ -43,6 +43,11 @@ function doZoom(scale, amount, center, limits) {
   call(fn, [scale, amount, center, limits]);
 }
 
+function doZoomRect(scale, amount, from, to, limits) {
+  const fn = zoomRectFunctions[scale.type] || zoomRectFunctions.default;
+  call(fn, [scale, amount, from, to, limits]);
+}
+
 function getCenter(chart) {
   const ca = chart.chartArea;
   return {
@@ -60,13 +65,12 @@ export function zoom(chart, amount, transition = 'none') {
   const {x = 1, y = 1, focalPoint = getCenter(chart)} = typeof amount === 'number' ? {x: amount, y: amount} : amount;
   const state = getState(chart);
   const {options: {limits, zoom: zoomOptions}} = state;
-  const {mode = 'xy', overScaleMode} = zoomOptions || {};
 
   storeOriginalScaleLimits(chart, state);
 
-  const xEnabled = x !== 1 && directionEnabled(mode, 'x', chart);
-  const yEnabled = y !== 1 && directionEnabled(mode, 'y', chart);
-  const enabledScales = overScaleMode && getEnabledScalesByPoint(overScaleMode, focalPoint, chart);
+  const xEnabled = x !== 1;
+  const yEnabled = y !== 1;
+  const enabledScales = getEnabledScalesByPoint(zoomOptions, focalPoint, chart);
 
   each(enabledScales || chart.scales, function(scale) {
     if (scale.isHorizontal() && xEnabled) {
@@ -81,15 +85,6 @@ export function zoom(chart, amount, transition = 'none') {
   call(zoomOptions.onZoom, [{chart}]);
 }
 
-function getRange(scale, pixel0, pixel1) {
-  const v0 = scale.getValueForPixel(pixel0);
-  const v1 = scale.getValueForPixel(pixel1);
-  return {
-    min: Math.min(v0, v1),
-    max: Math.max(v0, v1)
-  };
-}
-
 export function zoomRect(chart, p0, p1, transition = 'none') {
   const state = getState(chart);
   const {options: {limits, zoom: zoomOptions}} = state;
@@ -101,9 +96,9 @@ export function zoomRect(chart, p0, p1, transition = 'none') {
 
   each(chart.scales, function(scale) {
     if (scale.isHorizontal() && xEnabled) {
-      updateRange(scale, getRange(scale, p0.x, p1.x), limits, true);
+      doZoomRect(scale, p0.x, p1.x, limits);
     } else if (!scale.isHorizontal() && yEnabled) {
-      updateRange(scale, getRange(scale, p0.y, p1.y), limits, true);
+      doZoomRect(scale, p0.y, p1.y, limits);
     }
   });
 
@@ -182,12 +177,12 @@ export function pan(chart, delta, enabledScales, transition = 'none') {
   const {x = 0, y = 0} = typeof delta === 'number' ? {x: delta, y: delta} : delta;
   const state = getState(chart);
   const {options: {pan: panOptions, limits}} = state;
-  const {mode = 'xy', onPan} = panOptions || {};
+  const {onPan} = panOptions || {};
 
   storeOriginalScaleLimits(chart, state);
 
-  const xEnabled = x !== 0 && directionEnabled(mode, 'x', chart);
-  const yEnabled = y !== 0 && directionEnabled(mode, 'y', chart);
+  const xEnabled = x !== 0;
+  const yEnabled = y !== 0;
 
   each(enabledScales || chart.scales, function(scale) {
     if (scale.isHorizontal() && xEnabled) {
diff --git a/src/hammer.js b/src/hammer.js
index 273f000..91b0c6a 100644
--- a/src/hammer.js
+++ b/src/hammer.js
@@ -90,7 +90,7 @@ function handlePan(chart, state, e) {
 }
 
 function startPan(chart, state, event) {
-  const {enabled, overScaleMode, onPanStart, onPanRejected} = state.options.pan;
+  const {enabled, onPanStart, onPanRejected} = state.options.pan;
   if (!enabled) {
     return;
   }
@@ -104,7 +104,7 @@ function startPan(chart, state, event) {
     return call(onPanRejected, [{chart, event}]);
   }
 
-  state.panScales = overScaleMode && getEnabledScalesByPoint(overScaleMode, point, chart);
+  state.panScales = getEnabledScalesByPoint(state.options.pan, point, chart);
   state.delta = {x: 0, y: 0};
   clearTimeout(state.panEndTimeout);
   handlePan(chart, state, event);
diff --git a/src/handlers.js b/src/handlers.js
index 00a9045..85b36ac 100644
--- a/src/handlers.js
+++ b/src/handlers.js
@@ -52,7 +52,11 @@ function zoomStart(chart, event, zoomOptions) {
 export function mouseDown(chart, event) {
   const state = getState(chart);
   const {pan: panOptions, zoom: zoomOptions = {}} = state.options;
-  if (keyPressed(getModifierKey(panOptions), event) || keyNotPressed(getModifierKey(zoomOptions.drag), event)) {
+  if (
+    event.button !== 0 ||
+    keyPressed(getModifierKey(panOptions), event) ||
+    keyNotPressed(getModifierKey(zoomOptions.drag), event)
+  ) {
     return call(zoomOptions.onZoomRejected, [{chart, event}]);
   }
 
@@ -133,7 +137,7 @@ function wheelPreconditions(chart, event, zoomOptions) {
     return;
   }
 
-  // Prevent the event from triggering the default behavior (eg. Content scrolling).
+  // Prevent the event from triggering the default behavior (e.g. content scrolling).
   if (event.cancelable) {
     event.preventDefault();
   }
diff --git a/src/index.esm.js b/src/index.esm.js
index f75af00..3ff1210 100644
--- a/src/index.esm.js
+++ b/src/index.esm.js
@@ -1,4 +1,4 @@
 import plugin from './plugin';
 
 export default plugin;
-export {pan, zoom, zoomScale, resetZoom} from './core';
+export {pan, zoom, zoomRect, zoomScale, resetZoom} from './core';
diff --git a/src/plugin.js b/src/plugin.js
index 26b13ab..786a13d 100644
--- a/src/plugin.js
+++ b/src/plugin.js
@@ -1,8 +1,8 @@
 import Hammer from 'hammerjs';
 import {addListeners, computeDragRect, removeListeners} from './handlers';
 import {startHammer, stopHammer} from './hammer';
-import {pan, zoom, resetZoom, zoomScale, getZoomLevel, getInitialScaleBounds, isZoomedOrPanned} from './core';
-import {panFunctions, zoomFunctions} from './scale.types';
+import {pan, zoom, resetZoom, zoomScale, getZoomLevel, getInitialScaleBounds, isZoomedOrPanned, zoomRect} from './core';
+import {panFunctions, zoomFunctions, zoomRectFunctions} from './scale.types';
 import {getState, removeState} from './state';
 import {version} from '../package.json';
 
@@ -42,6 +42,10 @@ export default {
     if (Object.prototype.hasOwnProperty.call(options.zoom, 'enabled')) {
       console.warn('The option `zoom.enabled` is no longer supported. Please use `zoom.wheel.enabled`, `zoom.drag.enabled`, or `zoom.pinch.enabled`.');
     }
+    if (Object.prototype.hasOwnProperty.call(options.zoom, 'overScaleMode')
+      || Object.prototype.hasOwnProperty.call(options.pan, 'overScaleMode')) {
+      console.warn('The option `overScaleMode` is deprecated. Please use `scaleMode` instead (and update `mode` as desired).');
+    }
 
     if (Hammer) {
       startHammer(chart, options);
@@ -49,6 +53,7 @@ export default {
 
     chart.pan = (delta, panScales, transition) => pan(chart, delta, panScales, transition);
     chart.zoom = (args, transition) => zoom(chart, args, transition);
+    chart.zoomRect = (p0, p1, transition) => zoomRect(chart, p0, p1, transition);
     chart.zoomScale = (id, range, transition) => zoomScale(chart, id, range, transition);
     chart.resetZoom = (transition) => resetZoom(chart, transition);
     chart.getZoomLevel = () => getZoomLevel(chart);
@@ -103,6 +108,6 @@ export default {
   },
 
   panFunctions,
-
-  zoomFunctions
+  zoomFunctions,
+  zoomRectFunctions,
 };
diff --git a/src/scale.types.js b/src/scale.types.js
index 97b14e0..18f68cf 100644
--- a/src/scale.types.js
+++ b/src/scale.types.js
@@ -29,6 +29,15 @@ function getLimit(state, scale, scaleLimits, prop, fallback) {
   return valueOrDefault(limit, fallback);
 }
 
+function getRange(scale, pixel0, pixel1) {
+  const v0 = scale.getValueForPixel(pixel0);
+  const v1 = scale.getValueForPixel(pixel1);
+  return {
+    min: Math.min(v0, v1),
+    max: Math.max(v0, v1)
+  };
+}
+
 export function updateRange(scale, {min, max}, limits, zoom = false) {
   const state = getState(scale.chart);
   const {id, axis, options: scaleOpts} = scale;
@@ -72,6 +81,10 @@ function zoomNumericalScale(scale, zoom, center, limits) {
   return updateRange(scale, newRange, limits, true);
 }
 
+function zoomRectNumericalScale(scale, from, to, limits) {
+  updateRange(scale, getRange(scale, from, to), limits, true);
+}
+
 const integerChange = (v) => v === 0 || isNaN(v) ? 0 : v < 0 ? Math.min(Math.round(v), -1) : Math.max(Math.round(v), 1);
 
 function existCategoryFromMaxZoom(scale) {
@@ -158,6 +171,10 @@ export const zoomFunctions = {
   default: zoomNumericalScale,
 };
 
+export const zoomRectFunctions = {
+  default: zoomRectNumericalScale,
+};
+
 export const panFunctions = {
   category: panCategoryScale,
   default: panNumericalScale,
diff --git a/src/utils.js b/src/utils.js
index cce065f..3d16880 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -22,6 +22,17 @@ export function directionEnabled(mode, dir, chart) {
   return false;
 }
 
+function directionsEnabled(mode, chart) {
+  if (typeof mode === 'function') {
+    mode = mode({chart});
+  }
+  if (typeof mode === 'string') {
+    return {x: mode.indexOf('x') !== -1, y: mode.indexOf('y') !== -1};
+  }
+
+  return {x: false, y: false};
+}
+
 /**
  * Debounces calling `fn` for `delay` ms
  * @param {function} fn - Function to call. No arguments are passed.
@@ -37,7 +48,8 @@ export function debounce(fn, delay) {
   };
 }
 
-/** This function use for check what axis now under mouse cursor.
+/**
+ * Checks which axis is under the mouse cursor.
  * @param {{x: number, y: number}} point - the mouse location
  * @param {import('chart.js').Chart} [chart] instance of the chart in question
  * @return {import('chart.js').Scale}
@@ -54,27 +66,40 @@ function getScaleUnderPoint({x, y}, chart) {
   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.
+/**
+ * Evaluate the chart's mode, scaleMode, and overScaleMode properties to
+ * determine which axes are eligible for scaling.
  * options.overScaleMode can be a function if user want zoom only one scale of many for example.
- * @param {string} mode - 'xy', 'x' or 'y'
+ * @param options - Zoom or pan options
  * @param {{x: number, y: number}} point - the mouse location
  * @param {import('chart.js').Chart} [chart] instance of the chart in question
  * @return {import('chart.js').Scale[]}
  */
-export function getEnabledScalesByPoint(mode, point, chart) {
+export function getEnabledScalesByPoint(options, point, chart) {
+  const {mode = 'xy', scaleMode, overScaleMode} = options || {};
   const scale = getScaleUnderPoint(point, chart);
 
-  if (scale && directionEnabled(mode, scale.axis, chart)) {
+  const enabled = directionsEnabled(mode, chart);
+  const scaleEnabled = directionsEnabled(scaleMode, chart);
+
+  // Convert deprecated overScaleEnabled to new scaleEnabled.
+  if (overScaleMode) {
+    const overScaleEnabled = directionsEnabled(overScaleMode, chart);
+    for (const axis of ['x', 'y']) {
+      if (overScaleEnabled[axis]) {
+        scaleEnabled[axis] = enabled[axis];
+        enabled[axis] = false;
+      }
+    }
+  }
+
+  if (scale && scaleEnabled[scale.axis]) {
     return [scale];
   }
 
   const enabledScales = [];
   each(chart.scales, function(scaleItem) {
-    if (!directionEnabled(mode, scaleItem.axis, chart)) {
+    if (enabled[scaleItem.axis]) {
       enabledScales.push(scaleItem);
     }
   });
diff --git a/types/index.d.ts b/types/index.d.ts
index 0ed7c65..147f650 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -21,6 +21,7 @@ declare module 'chart.js' {
   interface Chart<TType extends keyof ChartTypeRegistry = keyof ChartTypeRegistry, TData = DistributiveArray<ChartTypeRegistry[TType]['defaultDataPoint']>, TLabel = unknown> {
     pan(pan: PanAmount, scales?: Scale[], mode?: UpdateMode): void;
     zoom(zoom: ZoomAmount, mode?: UpdateMode): void;
+    zoomRect(p0: Point, p1: Point, mode?: UpdateMode): void;
     zoomScale(id: string, range: ScaleRange, mode?: UpdateMode): void;
     resetZoom(mode?: UpdateMode): void;
     getZoomLevel(): number;
@@ -30,6 +31,7 @@ declare module 'chart.js' {
 }
 
 export type ZoomFunction = (scale: Scale, zoom: number, center: Point, limits: LimitOptions) => boolean;
+export type ZoomRectFunction = (scale: Scale, from: number, to: number, limits: LimitOptions) => boolean;
 export type PanFunction = (scale: Scale, delta: number, limits: LimitOptions) => boolean;
 
 type ScaleFunctions<T> = {
@@ -40,6 +42,7 @@ type ScaleFunctions<T> = {
 
 declare const Zoom: Plugin & {
   zoomFunctions: ScaleFunctions<ZoomFunction>;
+  zoomRectFunctions: ScaleFunctions<ZoomRectFunction>;
   panFunctions: ScaleFunctions<PanFunction>;
 };
 
@@ -47,6 +50,7 @@ export default Zoom;
 
 export function pan(chart: Chart, amount: PanAmount, scales?: Scale[], mode?: UpdateMode): void;
 export function zoom(chart: Chart, amount: ZoomAmount, mode?: UpdateMode): void;
+export function zoomRect(chart: Chart, p0: Point, p1: Point, mode?: UpdateMode): void;
 export function zoomScale(chart: Chart, scaleId: string, range: ScaleRange, mode?: UpdateMode): void;
 export function resetZoom(chart: Chart, mode?: UpdateMode): void;
 export function getZoomLevel(chart: Chart): number;
diff --git a/types/options.d.ts b/types/options.d.ts
index d276e41..6b1f2eb 100644
--- a/types/options.d.ts
+++ b/types/options.d.ts
@@ -67,7 +67,7 @@ export interface PinchOptions {
 export interface ZoomOptions {
   /**
    * Zooming directions. Remove the appropriate direction to disable
-   * Eg. 'y' would only allow zooming in the y direction
+   * E.g. 'y' would only allow zooming in the y direction
    * A function that is called as the user is zooming and returns the
    * available directions can also be used:
    *    mode: function({ chart }) {
@@ -91,6 +91,8 @@ export interface ZoomOptions {
    */
   pinch?: PinchOptions;
 
+  scaleMode?: Mode | { (chart: Chart): Mode };
+  /** @deprecated Use scaleMode instead */
   overScaleMode?: Mode | { (chart: Chart): Mode };
 
   /**
@@ -122,7 +124,7 @@ export interface PanOptions {
 
   /**
    * Panning directions. Remove the appropriate direction to disable
-   * Eg. 'y' would only allow panning in the y direction
+   * E.g. 'y' would only allow panning in the y direction
    * A function that is called as the user is panning and returns the
    * available directions can also be used:
    *   mode: function({ chart }) {
@@ -136,7 +138,9 @@ export interface PanOptions {
    */
   modifierKey?: Key;
 
-  overScaleMode?: Mode | { (char: Chart): Mode };
+  scaleMode?: Mode | { (chart: Chart): Mode };
+  /** @deprecated Use scaleMode instead */
+  overScaleMode?: Mode | { (chart: Chart): Mode };
 
   /**
    * Minimal pan distance required before actually applying pan


Test patch:
diff --git a/test/specs/api.spec.js b/test/specs/api.spec.js
index 46f1672..b11ef5e 100644
--- a/test/specs/api.spec.js
+++ b/test/specs/api.spec.js
@@ -5,6 +5,7 @@ describe('api', function() {
     expect(typeof chart.pan).toBe('function');
     expect(typeof chart.zoom).toBe('function');
     expect(typeof chart.zoomScale).toBe('function');
+    expect(typeof chart.zoomRect).toBe('function');
     expect(typeof chart.resetZoom).toBe('function');
     expect(typeof chart.getZoomLevel).toBe('function');
     expect(typeof chart.getInitialScaleBounds).toBe('function');
diff --git a/test/specs/module.spec.js b/test/specs/module.spec.js
index 456939c..ff992f7 100644
--- a/test/specs/module.spec.js
+++ b/test/specs/module.spec.js
@@ -7,8 +7,9 @@ describe('module', function() {
     expect(window.ChartZoom.id).toBe('zoom');
   });
 
-  it ('should expose zoomFunctions and panFunctions', function() {
+  it ('should expose zoomFunctions, zoomRectFunctions, and panFunctions', function() {
     expect(window.ChartZoom.zoomFunctions instanceof Object).toBe(true);
+    expect(window.ChartZoom.zoomRectFunctions instanceof Object).toBe(true);
     expect(window.ChartZoom.panFunctions instanceof Object).toBe(true);
   });
 
diff --git a/test/specs/zoom.spec.js b/test/specs/zoom.spec.js
index 5c62469..bb76e5c 100644
--- a/test/specs/zoom.spec.js
+++ b/test/specs/zoom.spec.js
@@ -550,6 +550,90 @@ describe('zoom', function() {
     });
   });
 
+  describe('with scaleMode = y and mode = xy', function() {
+    const config = {
+      type: 'line',
+      data,
+      options: {
+        scales: {
+          x: {
+            type: 'linear',
+            min: 1,
+            max: 10
+          },
+          y: {
+            type: 'linear'
+          }
+        },
+        plugins: {
+          zoom: {
+            zoom: {
+              wheel: {
+                enabled: true,
+              },
+              mode: 'xy',
+              scaleMode: 'y'
+            }
+          }
+        }
+      }
+    };
+
+    describe('Wheel under Y scale', function() {
+      it('should be applied on Y, but not on X scales.', async function() {
+        const chart = window.acquireChart(config);
+
+        const scaleX = chart.scales.x;
+        const scaleY = chart.scales.y;
+
+        const oldMinX = scaleX.options.min;
+        const oldMaxX = scaleX.options.max;
+        const oldMinY = scaleY.options.min;
+        const oldMaxY = scaleY.options.max;
+
+        const wheelEv = {
+          x: scaleY.left + (scaleY.right - scaleY.left) / 2,
+          y: scaleY.top + (scaleY.bottom - scaleY.top) / 2,
+          deltaY: 1
+        };
+
+        await jasmine.triggerWheelEvent(chart, wheelEv);
+
+        expect(scaleX.options.min).toEqual(oldMinX);
+        expect(scaleX.options.max).toEqual(oldMaxX);
+        expect(scaleY.options.min).not.toEqual(oldMinY);
+        expect(scaleY.options.max).not.toEqual(oldMaxY);
+      });
+    });
+
+    describe('Wheel not under Y scale', function() {
+      it('should be applied on X and Y scales.', async function() {
+        const chart = window.acquireChart(config);
+
+        const scaleX = chart.scales.x;
+        const scaleY = chart.scales.y;
+
+        const oldMinX = scaleX.options.min;
+        const oldMaxX = scaleX.options.max;
+        const oldMinY = scaleY.options.min;
+        const oldMaxY = scaleY.options.max;
+
+        const wheelEv = {
+          x: scaleX.getPixelForValue(1.5),
+          y: scaleY.getPixelForValue(1.1),
+          deltaY: 1
+        };
+
+        await jasmine.triggerWheelEvent(chart, wheelEv);
+
+        expect(scaleX.options.min).not.toEqual(oldMinX);
+        expect(scaleX.options.max).not.toEqual(oldMaxX);
+        expect(scaleY.options.min).not.toEqual(oldMinY);
+        expect(scaleY.options.max).not.toEqual(oldMaxY);
+      });
+    });
+  });
+
   describe('events', function() {
     describe('wheel', function() {
       it('should call onZoomStart', function() {