Repository: chartjs/chartjs-plugin-zoom
Pull request #888 (2024-11-17 22:56:41)

fix: incorrect zoom out factor when using using wheel
fixes: #767

github-actions[bot]: **Size Change:** +397 B (+1.97%) 

**Total Size:** 20.5 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `dist/chartjs-plugin-zoom.esm.js` | 7.43 kB | +148 B (+2.03%) |
| `dist/chartjs-plugin-zoom.js` | 7.58 kB | +142 B (+1.91%) |
| `dist/chartjs-plugin-zoom.min.js` | 5.5 kB | +107 B (+1.98%) |

<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>

Patch:
diff --git a/docs/samples/api.md b/docs/samples/api.md
index bc1c574..8c49600 100644
--- a/docs/samples/api.md
+++ b/docs/samples/api.md
@@ -71,7 +71,7 @@ const actions = [
   }, {
     name: 'Zoom -10%',
     handler(chart) {
-      chart.zoom(0.9);
+      chart.zoom(2 - 1 / 0.9);
     },
   }, {
     name: 'Zoom x +10%',
@@ -81,7 +81,7 @@ const actions = [
   }, {
     name: 'Zoom x -10%',
     handler(chart) {
-      chart.zoom({x: 0.9});
+      chart.zoom({x: 2 - 1 / 0.9});
     },
   }, {
     name: 'Pan x 100px (anim)',
diff --git a/docs/samples/wheel/log.md b/docs/samples/wheel/log.md
index 9e9af4e..2c3a072 100644
--- a/docs/samples/wheel/log.md
+++ b/docs/samples/wheel/log.md
@@ -126,6 +126,12 @@ const scales = {
       display: true,
       text: 'Frequency',
     },
+  },
+  y: {
+    // constant width for the scale
+    afterFit: (scale) => {
+      scale.width = 50;
+    },
   }
 };
 // </block:scales>
@@ -157,7 +163,7 @@ const config = {
         },
       }
     },
-  }
+  },
 };
 // </block:config>
 
diff --git a/karma.conf.js b/karma.conf.js
index b81198e..066cef8 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -33,7 +33,8 @@ module.exports = function(karma) {
 
     client: {
       jasmine: {
-        stopOnSpecFailure: !!karma.autoWatch
+        stopOnSpecFailure: !!karma.autoWatch,
+        timeoutInterval: 10000,
       }
     },
 
diff --git a/src/handlers.js b/src/handlers.js
index e21a9fc..7225695 100644
--- a/src/handlers.js
+++ b/src/handlers.js
@@ -216,10 +216,11 @@ export function wheel(chart, event) {
   }
 
   const rect = event.target.getBoundingClientRect();
-  const speed = 1 + (event.deltaY >= 0 ? -zoomOptions.wheel.speed : zoomOptions.wheel.speed);
+  const speed = zoomOptions.wheel.speed;
+  const percentage = event.deltaY >= 0 ? 2 - 1 / (1 - speed) : 1 + speed;
   const amount = {
-    x: speed,
-    y: speed,
+    x: percentage,
+    y: percentage,
     focalPoint: {
       x: event.clientX - rect.left,
       y: event.clientY - rect.top
diff --git a/src/scale.types.js b/src/scale.types.js
index cc9b6c1..c964b58 100644
--- a/src/scale.types.js
+++ b/src/scale.types.js
@@ -1,4 +1,4 @@
-import {valueOrDefault} from 'chart.js/helpers';
+import {almostEquals, valueOrDefault} from 'chart.js/helpers';
 import {getState} from './state';
 
 /**
@@ -112,6 +112,40 @@ function linearRange(scale, pixel0, pixel1) {
   };
 }
 
+/**
+ * @param {number} range
+ * @param {{ min: number; max: number; minLimit: number; maxLimit: number; }} options
+ * @param {{ min: { scale?: number; options?: number; }; max: { scale?: number; options?: number; }}} [originalLimits]
+ */
+function fixRange(range, {min, max, minLimit, maxLimit}, originalLimits) {
+  const offset = (range - max + min) / 2;
+  min -= offset;
+  max += offset;
+
+  // In case the values are really close to the original values, use the original values.
+  const origMin = originalLimits.min.options ?? originalLimits.min.scale;
+  const origMax = originalLimits.max.options ?? originalLimits.max.scale;
+
+  const epsilon = range / 1e6;
+  if (almostEquals(min, origMin, epsilon)) {
+    min = origMin;
+  }
+  if (almostEquals(max, origMax, epsilon)) {
+    max = origMax;
+  }
+
+  // Apply limits
+  if (min < minLimit) {
+    min = minLimit;
+    max = Math.min(minLimit + range, maxLimit);
+  } else if (max > maxLimit) {
+    max = maxLimit;
+    min = Math.max(maxLimit - range, minLimit);
+  }
+
+  return {min, max};
+}
+
 /**
  * @param {Scale} scale
  * @param {ScaleRange} minMax
@@ -141,24 +175,15 @@ export function updateRange(scale, {min, max}, limits, zoom = false) {
     return true;
   }
 
-  const offset = (range - max + min) / 2;
-  min -= offset;
-  max += offset;
+  const newRange = fixRange(range, {min, max, minLimit, maxLimit}, state.originalScaleLimits[scale.id]);
 
-  if (min < minLimit) {
-    min = minLimit;
-    max = Math.min(minLimit + range, maxLimit);
-  } else if (max > maxLimit) {
-    max = maxLimit;
-    min = Math.max(maxLimit - range, minLimit);
-  }
-  scaleOpts.min = min;
-  scaleOpts.max = max;
+  scaleOpts.min = newRange.min;
+  scaleOpts.max = newRange.max;
 
-  state.updatedScaleLimits[scale.id] = {min, max};
+  state.updatedScaleLimits[scale.id] = newRange;
 
   // return true if the scale range is changed
-  return scale.parse(min) !== scale.min || scale.parse(max) !== scale.max;
+  return scale.parse(newRange.min) !== scale.min || scale.parse(newRange.max) !== scale.max;
 }
 
 function zoomNumericalScale(scale, zoom, center, limits) {


Test patch:
diff --git a/test/specs/zoom.wheel.spec.js b/test/specs/zoom.wheel.spec.js
index 0abad4d..a886305 100644
--- a/test/specs/zoom.wheel.spec.js
+++ b/test/specs/zoom.wheel.spec.js
@@ -278,34 +278,50 @@ describe('zoom with wheel', function() {
       const chart = window.acquireChart(config);
       const scaleY = chart.scales.y;
 
-      const wheelEv = {
+      const zoomIn = {
         x: Math.round(scaleY.left + (scaleY.right - scaleY.left) / 2),
         y: Math.round(scaleY.top + (scaleY.bottom - scaleY.top) / 2),
         deltaY: -1
       };
 
+      const zoomOut = {
+        ...zoomIn,
+        deltaY: 1
+      };
+
       expect(scaleY.min).toBe(1);
       expect(scaleY.max).toBe(10000);
 
-      jasmine.triggerWheelEvent(chart, wheelEv);
+      jasmine.triggerWheelEvent(chart, zoomIn);
 
       expect(scaleY.min).toBeCloseTo(1.6, 1);
       expect(scaleY.max).toBeCloseTo(6310, -1);
 
-      jasmine.triggerWheelEvent(chart, wheelEv);
+      jasmine.triggerWheelEvent(chart, zoomIn);
 
       expect(scaleY.min).toBeCloseTo(2.4, 1);
       expect(scaleY.max).toBeCloseTo(4170, -1);
 
+      jasmine.triggerWheelEvent(chart, zoomOut);
+      jasmine.triggerWheelEvent(chart, zoomOut);
+
+      expect(scaleY.min).toBe(1);
+      expect(scaleY.max).toBe(10000);
+
       chart.resetZoom();
 
       expect(scaleY.min).toBe(1);
       expect(scaleY.max).toBe(10000);
 
-      jasmine.triggerWheelEvent(chart, {...wheelEv, deltaY: 1});
+      jasmine.triggerWheelEvent(chart, zoomOut);
+
+      expect(scaleY.min).toBeCloseTo(0.6, 1);
+      expect(scaleY.max).toBeCloseTo(16700, -2);
+
+      jasmine.triggerWheelEvent(chart, zoomIn);
 
-      expect(scaleY.min).toBe(0.6);
-      expect(scaleY.max).toBeCloseTo(15800, -2);
+      expect(scaleY.min).toBeCloseTo(1);
+      expect(scaleY.max).toBeCloseTo(10000);
     });
   });