Repository: chartjs/chartjs-plugin-zoom
Pull request #485 (2021-04-30 20:22:09)

Fix calling of onZoomComplete from wheel event


Patch:
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index f887c1c..6e921fc 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -84,6 +84,7 @@ module.exports = {
         'time',
         'drag',
         'api',
+        'click-zoom',
       ],
     }
   }
diff --git a/docs/samples/click-zoom.md b/docs/samples/click-zoom.md
new file mode 100644
index 0000000..859ee13
--- /dev/null
+++ b/docs/samples/click-zoom.md
@@ -0,0 +1,144 @@
+# Click to Zoom
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Fringilla ut morbi tincidunt augue interdum velit euismod. Elit pellentesque habitant morbi tristique senectus et netus. Consectetur adipiscing elit pellentesque habitant morbi. Id faucibus nisl tincidunt eget nullam non nisi est sit. Blandit turpis cursus in hac habitasse. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel. Ornare massa eget egestas purus. A diam sollicitudin tempor id eu nisl nunc. Augue mauris augue neque gravida in fermentum et sollicitudin. Dolor purus non enim praesent elementum facilisis leo vel fringilla. Habitant morbi tristique senectus et netus et malesuada. Nulla pharetra diam sit amet nisl suscipit adipiscing bibendum est. Gravida dictum fusce ut placerat orci nulla pellentesque.
+
+Semper viverra nam libero justo laoreet sit. Pretium vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae. Risus ultricies tristique nulla aliquet enim tortor at auctor urna. Consequat semper viverra nam libero justo laoreet sit amet. Magna ac placerat vestibulum lectus mauris ultrices eros. Dolor purus non enim praesent elementum facilisis leo vel. Enim eu turpis egestas pretium aenean pharetra. Vitae purus faucibus ornare suspendisse sed nisi lacus. Senectus et netus et malesuada fames. Nec feugiat in fermentum posuere urna nec tincidunt praesent. Accumsan lacus vel facilisis volutpat. Lectus quam id leo in vitae turpis massa. Cras sed felis eget velit aliquet. Volutpat maecenas volutpat blandit aliquam.
+
+Volutpat lacus laoreet non curabitur. Diam donec adipiscing tristique risus. Fusce id velit ut tortor pretium viverra suspendisse. Dui ut ornare lectus sit amet est. Pharetra sit amet aliquam id. Porttitor massa id neque aliquam vestibulum morbi blandit. Viverra adipiscing at in tellus integer feugiat scelerisque varius morbi. Quam elementum pulvinar etiam non quam. Congue quisque egestas diam in arcu cursus euismod quis. Dictum fusce ut placerat orci nulla pellentesque. Ut placerat orci nulla pellentesque dignissim enim sit amet. Sagittis id consectetur purus ut faucibus pulvinar elementum integer. Dui ut ornare lectus sit amet est.
+
+Ut tortor pretium viverra suspendisse potenti nullam ac tortor. Mauris a diam maecenas sed enim. Tellus in hac habitasse platea dictumst vestibulum rhoncus. Cras sed felis eget velit aliquet. Purus viverra accumsan in nisl nisi. Sed risus ultricies tristique nulla aliquet enim tortor at. Integer quis auctor elit sed vulputate mi sit amet mauris. Adipiscing elit duis tristique sollicitudin nibh sit amet commodo. Risus feugiat in ante metus dictum at. Posuere urna nec tincidunt praesent semper. Auctor elit sed vulputate mi sit amet mauris commodo. Senectus et netus et malesuada fames ac turpis egestas integer. Varius morbi enim nunc faucibus a pellentesque. Sed felis eget velit aliquet sagittis id. Ac auctor augue mauris augue neque gravida. Etiam erat velit scelerisque in dictum non consectetur a erat.
+
+Tortor condimentum lacinia quis vel eros donec ac. Phasellus vestibulum lorem sed risus ultricies tristique. Vitae tortor condimentum lacinia quis vel eros donec. Morbi tempus iaculis urna id volutpat lacus laoreet non curabitur. Ut pharetra sit amet aliquam id diam. Eu non diam phasellus vestibulum lorem. Pharetra pharetra massa massa ultricies mi. Donec ultrices tincidunt arcu non. Sagittis orci a scelerisque purus semper eget duis. In iaculis nunc sed augue lacus viverra. Vitae proin sagittis nisl rhoncus mattis rhoncus urna neque. Consequat mauris nunc congue nisi vitae suscipit tellus mauris a. Massa placerat duis ultricies lacus sed turpis tincidunt id. Sit amet tellus cras adipiscing enim eu turpis. Amet porttitor eget dolor morbi non arcu risus quis varius. Potenti nullam ac tortor vitae purus.
+
+```js chart-editor
+// <block:data:1>
+const DATA_COUNT = 70;
+const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100};
+
+const labels = Utils.months({count: DATA_COUNT});
+const data = {
+  labels: labels,
+  datasets: [
+    {
+      label: 'Dataset 1',
+      data: Utils.numbers(NUMBER_CFG),
+      borderColor: Utils.randomColor(0.4),
+      backgroundColor: Utils.randomColor(0.1),
+      stack: 'combined',
+      type: 'bar'
+    },
+    {
+      label: 'Dataset 2',
+      data: Utils.numbers(NUMBER_CFG),
+      borderColor: Utils.randomColor(0.4),
+      backgroundColor: Utils.randomColor(0.1),
+      pointBorderColor: Utils.randomColor(0.7),
+      pointBackgroundColor: Utils.randomColor(0.5),
+      stack: 'combined'
+    }
+  ]
+};
+// </block:data>
+
+// <block:zoom:0>
+const zoomOptions = {
+  limits: {
+    y: {min: 0, max: 200, minRange: 50}
+  },
+  pan: {
+    enabled: true,
+    mode: 'xy',
+  },
+  zoom: {
+    enabled: false,
+    mode: 'xy',
+  }
+};
+// </block:zoom>
+
+// <block:border:3>
+const borderPlugin = {
+  id: 'chartAreaBorder',
+  beforeDraw(chart, args, options) {
+    const {ctx, chartArea: {left, top, width, height}} = chart;
+    if (chart.options.plugins.zoom.zoom.enabled) {
+      ctx.save();
+      ctx.strokeStyle = 'red';
+      ctx.lineWidth = 1;
+      ctx.strokeRect(left, top, width, height);
+      ctx.restore();
+    }
+  }
+};
+// </block>
+
+const zoomStatus = () => 'Zoom: ' + (zoomOptions.zoom.enabled ? 'enabled' : 'disabled');
+
+// <block:config:1>
+const config = {
+  type: 'line',
+  data: data,
+  options: {
+    scales: {y: {stacked: true, min: 0}},
+    plugins: {
+      zoom: zoomOptions,
+      title: {
+        display: true,
+        position: 'bottom',
+        text: zoomStatus
+      }
+    },
+    onClick(e) {
+      const chart = e.chart;
+      chart.options.plugins.zoom.zoom.enabled = !chart.options.plugins.zoom.zoom.enabled;
+      chart.update();
+    }
+  },
+  plugins: [borderPlugin]
+};
+// </block:config>
+
+const actions = [
+  {
+    name: 'Randomize',
+    handler(chart) {
+      chart.data.datasets.forEach(dataset => {
+        dataset.data = Utils.numbers(NUMBER_CFG);
+      });
+      chart.update();
+    }
+  }, {
+    name: 'Toggle zoom',
+    handler(chart) {
+      zoomOptions.zoom.enabled = !zoomOptions.zoom.enabled;
+      chart.update();
+    }
+  }, {
+    name: 'Toggle pan',
+    handler(chart) {
+      zoomOptions.pan.enabled = !zoomOptions.pan.enabled;
+      chart.update();
+    },
+  }, {
+    name: 'Reset zoom',
+    handler(chart) {
+      chart.resetZoom();
+    }
+  }
+];
+
+module.exports = {
+  actions,
+  config,
+};
+```
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Fringilla ut morbi tincidunt augue interdum velit euismod. Elit pellentesque habitant morbi tristique senectus et netus. Consectetur adipiscing elit pellentesque habitant morbi. Id faucibus nisl tincidunt eget nullam non nisi est sit. Blandit turpis cursus in hac habitasse. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel. Ornare massa eget egestas purus. A diam sollicitudin tempor id eu nisl nunc. Augue mauris augue neque gravida in fermentum et sollicitudin. Dolor purus non enim praesent elementum facilisis leo vel fringilla. Habitant morbi tristique senectus et netus et malesuada. Nulla pharetra diam sit amet nisl suscipit adipiscing bibendum est. Gravida dictum fusce ut placerat orci nulla pellentesque.
+
+Semper viverra nam libero justo laoreet sit. Pretium vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae. Risus ultricies tristique nulla aliquet enim tortor at auctor urna. Consequat semper viverra nam libero justo laoreet sit amet. Magna ac placerat vestibulum lectus mauris ultrices eros. Dolor purus non enim praesent elementum facilisis leo vel. Enim eu turpis egestas pretium aenean pharetra. Vitae purus faucibus ornare suspendisse sed nisi lacus. Senectus et netus et malesuada fames. Nec feugiat in fermentum posuere urna nec tincidunt praesent. Accumsan lacus vel facilisis volutpat. Lectus quam id leo in vitae turpis massa. Cras sed felis eget velit aliquet. Volutpat maecenas volutpat blandit aliquam.
+
+Volutpat lacus laoreet non curabitur. Diam donec adipiscing tristique risus. Fusce id velit ut tortor pretium viverra suspendisse. Dui ut ornare lectus sit amet est. Pharetra sit amet aliquam id. Porttitor massa id neque aliquam vestibulum morbi blandit. Viverra adipiscing at in tellus integer feugiat scelerisque varius morbi. Quam elementum pulvinar etiam non quam. Congue quisque egestas diam in arcu cursus euismod quis. Dictum fusce ut placerat orci nulla pellentesque. Ut placerat orci nulla pellentesque dignissim enim sit amet. Sagittis id consectetur purus ut faucibus pulvinar elementum integer. Dui ut ornare lectus sit amet est.
+
+Ut tortor pretium viverra suspendisse potenti nullam ac tortor. Mauris a diam maecenas sed enim. Tellus in hac habitasse platea dictumst vestibulum rhoncus. Cras sed felis eget velit aliquet. Purus viverra accumsan in nisl nisi. Sed risus ultricies tristique nulla aliquet enim tortor at. Integer quis auctor elit sed vulputate mi sit amet mauris. Adipiscing elit duis tristique sollicitudin nibh sit amet commodo. Risus feugiat in ante metus dictum at. Posuere urna nec tincidunt praesent semper. Auctor elit sed vulputate mi sit amet mauris commodo. Senectus et netus et malesuada fames ac turpis egestas integer. Varius morbi enim nunc faucibus a pellentesque. Sed felis eget velit aliquet sagittis id. Ac auctor augue mauris augue neque gravida. Etiam erat velit scelerisque in dictum non consectetur a erat.
+
+Tortor condimentum lacinia quis vel eros donec ac. Phasellus vestibulum lorem sed risus ultricies tristique. Vitae tortor condimentum lacinia quis vel eros donec. Morbi tempus iaculis urna id volutpat lacus laoreet non curabitur. Ut pharetra sit amet aliquam id diam. Eu non diam phasellus vestibulum lorem. Pharetra pharetra massa massa ultricies mi. Donec ultrices tincidunt arcu non. Sagittis orci a scelerisque purus semper eget duis. In iaculis nunc sed augue lacus viverra. Vitae proin sagittis nisl rhoncus mattis rhoncus urna neque. Consequat mauris nunc congue nisi vitae suscipit tellus mauris a. Massa placerat duis ultricies lacus sed turpis tincidunt id. Sit amet tellus cras adipiscing enim eu turpis. Amet porttitor eget dolor morbi non arcu risus quis varius. Potenti nullam ac tortor vitae purus.
diff --git a/src/hammer.js b/src/hammer.js
index 078e0e9..504bec7 100644
--- a/src/hammer.js
+++ b/src/hammer.js
@@ -102,13 +102,14 @@ function startPan(chart, state, e) {
 
   state.panScales = overScaleMode && getEnabledScalesByPoint(overScaleMode, point, chart);
   state.delta = {x: 0, y: 0};
+  clearTimeout(state.panEndTimeout);
   handlePan(chart, state, e);
 }
 
 function endPan(chart, state) {
   state.delta = null;
   if (state.panning) {
-    setTimeout(() => (state.panning = false), 500);
+    state.panEndTimeout = setTimeout(() => (state.panning = false), 500);
     call(state.options.pan.onPanComplete, [{chart}]);
   }
 }
diff --git a/src/handlers.js b/src/handlers.js
index e9ef411..b1c41a4 100644
--- a/src/handlers.js
+++ b/src/handlers.js
@@ -99,8 +99,8 @@ export function mouseUp(chart, event) {
 }
 
 export function wheel(chart, event) {
-  const {options: {zoom: zoomOptions}} = getState(chart);
-  const {wheelModifierKey, onZoomRejected, onZoomComplete} = zoomOptions;
+  const {handlers: {onZoomComplete}, options: {zoom: zoomOptions}} = getState(chart);
+  const {wheelModifierKey, onZoomRejected} = zoomOptions;
 
   // Before preventDefault, check if the modifier key required and pressed
   if (wheelModifierKey && !event[wheelModifierKey + 'Key']) {
@@ -114,7 +114,7 @@ export function wheel(chart, event) {
 
   // 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') {
+  if (event.deltaY === undefined) {
     return;
   }
 
@@ -132,20 +132,26 @@ export function wheel(chart, event) {
   zoom(chart, amount);
 
   if (onZoomComplete) {
-    debounce(() => call(onZoomComplete, [{chart}]), 250);
+    onZoomComplete();
+  }
+}
+
+function addDebouncedHandler(chart, name, handler, delay) {
+  if (handler) {
+    getState(chart).handlers[name] = debounce(() => call(handler, [{chart}]), delay);
   }
 }
 
 export function addListeners(chart, options) {
   const canvas = chart.canvas;
+  const {enabled: zoomEnabled, drag: dragEnabled, onZoomComplete} = options.zoom;
 
   // 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(chart, canvas, 'wheel', wheel);
+    addDebouncedHandler(chart, 'onZoomComplete', onZoomComplete, 250);
   } else {
     removeHandler(chart, canvas, 'wheel');
   }
diff --git a/src/utils.js b/src/utils.js
index 5d3a4c0..ce4eb03 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -27,12 +27,8 @@ export function directionEnabled(mode, dir, chart) {
 export function debounce(fn, delay) {
   let timeout;
   return function() {
-    if (delay) {
-      clearTimeout(timeout);
-      timeout = setTimeout(fn, delay);
-    } else {
-      fn();
-    }
+    clearTimeout(timeout);
+    timeout = setTimeout(fn, delay);
     return delay;
   };
 }


Test patch:
diff --git a/test/specs/zoom.spec.js b/test/specs/zoom.spec.js
index 433b526..51f49f2 100644
--- a/test/specs/zoom.spec.js
+++ b/test/specs/zoom.spec.js
@@ -396,4 +396,30 @@ describe('zoom', function() {
       });
     });
   });
+
+  describe('onZoomComplete', function() {
+    it('should be called', function(done) {
+      const chart = window.acquireChart({
+        type: 'scatter',
+        data,
+        options: {
+          plugins: {
+            zoom: {
+              zoom: {
+                enabled: true,
+                mode: 'xy',
+                onZoomComplete: done
+              }
+            }
+          }
+        }
+      });
+      const wheelEv = {
+        x: chart.scales.x.getPixelForValue(1.5),
+        y: chart.scales.y.getPixelForValue(1.1),
+        deltaY: 1
+      };
+      jasmine.triggerWheelEvent(chart, wheelEv);
+    });
+  });
 });