Repository: chartjs/chartjs-plugin-zoom
Pull request #861 (2024-11-16 12:22:01)

feat(drag): maintainAspectRatio
Add optional maintainAspectRatio boolean to drag options, which forces the drag rectangle to same aspect ration with chartArea.

closed: https://github.com/chartjs/chartjs-plugin-zoom/issues/115

github-actions[bot]: **Size Change:** +732 B (+3.39%) 

**Total Size:** 22.4 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `dist/chartjs-plugin-zoom.esm.js` | 8.52 kB | +263 B (+3.18%) |
| `dist/chartjs-plugin-zoom.js` | 8.68 kB | +263 B (+3.13%) |
| `dist/chartjs-plugin-zoom.min.js` | 5.15 kB | +206 B (+4.16%) |

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

Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..782a0ad
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+  - package-ecosystem: "npm"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fd22827..193d1ec 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,8 +7,7 @@ on:
       - master
 
 jobs:
-  build:
-
+  ci:
     runs-on: ${{ matrix.os }}
 
     strategy:
@@ -17,20 +16,30 @@ jobs:
       fail-fast: false
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v4
     - name: Use Node.js
-      uses: actions/setup-node@v2
+      uses: actions/setup-node@v4
       with:
-        node-version: 16
-    - run: |
-        npm ci
-        npm run build
+        node-version: 20
+
+    - name: install
+      run: npm ci
+
+    - name: lint
+      run: npm run lint
+
+    - name: build
+      run: npm run build
+
+    - name: test
+      run: |
         if [ "${{ runner.os }}" == "Windows" ]; then
           npm test
         else
           xvfb-run --auto-servernum npm test
         fi
       shell: bash
+
     - name: Coveralls Parallel - Chrome
       uses: coverallsapp/github-action@master
       with:
@@ -38,6 +47,7 @@ jobs:
         path-to-lcov: './coverage/chrome/lcov.info'
         flag-name: ${{ matrix.os }}-chrome
         parallel: true
+
     - name: Coveralls Parallel - Firefox
       uses: coverallsapp/github-action@master
       with:
@@ -47,7 +57,7 @@ jobs:
         parallel: true
 
   finish:
-    needs: build
+    needs: ci
     runs-on: ubuntu-latest
     steps:
     - name: Coveralls Finished
diff --git a/.github/workflows/compressed-size.yml b/.github/workflows/compressed-size.yml
index ed9f872..468519a 100644
--- a/.github/workflows/compressed-size.yml
+++ b/.github/workflows/compressed-size.yml
@@ -8,7 +8,7 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v4
     - uses: preactjs/compressed-size-action@v2
       with:
         repo-token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
index 7595624..2a0a3d7 100644
--- a/.github/workflows/documentation.yml
+++ b/.github/workflows/documentation.yml
@@ -11,10 +11,10 @@ jobs:
     if: github.event_name == 'pull_request'
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-node@v2
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
         with:
-          node-version: 16
+          node-version: 20
       - name: Test Build
         run: |
           npm ci
@@ -24,17 +24,17 @@ jobs:
     if: github.event_name == 'push'
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-node@v2
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
         with:
-          node-version: 16
+          node-version: 20
       - name: Build
         run: |
           npm ci
           ./scripts/docs-config.sh master
           npm run docs
       - name: Release to GitHub Pages
-        uses: peaceiris/actions-gh-pages@v3
+        uses: peaceiris/actions-gh-pages@v4
         with:
           github_token: ${{ secrets.GH_AUTH_TOKEN }}
           publish_dir: dist/docs
diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml
index 903ad69..684e58e 100644
--- a/.github/workflows/publish-npm.yml
+++ b/.github/workflows/publish-npm.yml
@@ -18,11 +18,11 @@ jobs:
   test:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v4
     - name: Use Node.js
-      uses: actions/setup-node@v2
+      uses: actions/setup-node@v4
       with:
-        node-version: 16
+        node-version: 20
     - name: Test
       run: |
         npm ci
@@ -32,10 +32,10 @@ jobs:
     needs: [test, setup]
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-node@v2
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
         with:
-          node-version: 16
+          node-version: 20
           registry-url: https://registry.npmjs.org/
       - name: Setup and build
         run: |
@@ -45,13 +45,13 @@ jobs:
 
       - name: Publish @next
         run: npm publish --tag next
-        if: "github.event.release.prerelease"
+        if: github.event.release.prerelease == true
         env:
           NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}
 
       - name: Publish @latest
         run: npm publish --tag latest
-        if: "!github.event.release.prerelease"
+        if: github.event.release.prerelease == false
         env:
           NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}
 
@@ -93,7 +93,7 @@ jobs:
           VERSION: ${{ needs.setup.outputs.version }}
 
       - name: Release to GitHub Pages (version)
-        uses: peaceiris/actions-gh-pages@v3
+        uses: peaceiris/actions-gh-pages@v4
         with:
           github_token: ${{ secrets.GH_AUTH_TOKEN }}
           publish_dir: dist/docs
@@ -103,10 +103,10 @@ jobs:
     needs: [test, setup]
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-node@v2
+      - uses: actions/checkout@v4
+      - uses: actions/setup-node@v4
         with:
-          node-version: 16
+          node-version: 20
           registry-url: https://registry.npmjs.org/
 
       - name: Build docs (latest/next)
@@ -119,16 +119,16 @@ jobs:
           VERSION: ${{ needs.setup.outputs.version }}
 
       - name: Release to GitHub Pages (latest)
-        if: "!github.event.release.prerelease"
-        uses: peaceiris/actions-gh-pages@v3
+        if: github.event.release.prerelease == false
+        uses: peaceiris/actions-gh-pages@v4
         with:
           github_token: ${{ secrets.GH_AUTH_TOKEN }}
           publish_dir: dist/docs
           destination_dir: latest
 
       - name: Release to GitHub Pages (next)
-        if: "github.event.release.prerelease"
-        uses: peaceiris/actions-gh-pages@v3
+        if: github.event.release.prerelease == true
+        uses: peaceiris/actions-gh-pages@v4
         with:
           github_token: ${{ secrets.GH_AUTH_TOKEN }}
           publish_dir: dist/docs
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
index 064af11..3c34363 100644
--- a/.github/workflows/release-drafter.yml
+++ b/.github/workflows/release-drafter.yml
@@ -17,6 +17,6 @@ jobs:
     needs: correct_repository
     runs-on: ubuntu-latest
     steps:
-      - uses: release-drafter/release-drafter@v5
+      - uses: release-drafter/release-drafter@v6
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index a55fa36..bb57688 100755
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -146,6 +146,7 @@ module.exports = {
           children: [
             'drag/category',
             'drag/linear',
+            'drag/linear-ratio',
             'drag/log',
             'drag/time',
             'drag/timeseries',
diff --git a/docs/guide/options.md b/docs/guide/options.md
index 74c6cd9..74afe03 100644
--- a/docs/guide/options.md
+++ b/docs/guide/options.md
@@ -80,7 +80,7 @@ const chart = new Chart('id', {
 | [`drawTime`](#draw-time) | `string` | `beforeDatasetsDraw` | When the dragging box is drawn on the chart
 | `threshold` | `number` | `0` | Minimal zoom distance required before actually applying zoom
 | `modifierKey` | `'ctrl'`\|`'alt'`\|`'shift'`\|`'meta'` | `null` |  Modifier key required for drag-to-zoom
-
+| `maintainAspectRatio` | `boolean` | `undefined` | Maintain aspect ratio of the chart
 
 ## Draw Time
 
diff --git a/docs/samples/drag/linear-ratio.md b/docs/samples/drag/linear-ratio.md
new file mode 100644
index 0000000..54094b3
--- /dev/null
+++ b/docs/samples/drag/linear-ratio.md
@@ -0,0 +1,112 @@
+# Linear Scales + maintainAspectRatio
+
+Zooming is performed by clicking and selecting an area over the chart with the mouse. Pan is activated by keeping `shift` pressed.
+
+```js chart-editor
+// <block:data:1>
+const NUMBER_CFG = {count: 20, min: -100, max: 100};
+const data = {
+  datasets: [{
+    label: 'My First dataset',
+    borderColor: Utils.randomColor(0.4),
+    backgroundColor: Utils.randomColor(0.1),
+    pointBorderColor: Utils.randomColor(0.7),
+    pointBackgroundColor: Utils.randomColor(0.5),
+    pointBorderWidth: 1,
+    data: Utils.points(NUMBER_CFG),
+  }, {
+    label: 'My Second dataset',
+    borderColor: Utils.randomColor(0.4),
+    backgroundColor: Utils.randomColor(0.1),
+    pointBorderColor: Utils.randomColor(0.7),
+    pointBackgroundColor: Utils.randomColor(0.5),
+    pointBorderWidth: 1,
+    data: Utils.points(NUMBER_CFG),
+  }]
+};
+// </block:data>
+
+// <block:scales:2>
+const scaleOpts = {
+  reverse: true,
+  grid: {
+    borderColor: Utils.randomColor(1),
+    color: 'rgba( 0, 0, 0, 0.1)',
+  },
+  title: {
+    display: true,
+    text: (ctx) => ctx.scale.axis + ' axis',
+  }
+};
+const scales = {
+  x: {
+    position: 'top',
+  },
+  y: {
+    position: 'right',
+  },
+};
+Object.keys(scales).forEach(scale => Object.assign(scales[scale], scaleOpts));
+// </block:scales>
+
+// <block:zoom:0>
+const dragColor = Utils.randomColor(0.4);
+const zoomOptions = {
+  pan: {
+    enabled: true,
+    mode: 'xy',
+    modifierKey: 'shift',
+  },
+  zoom: {
+    mode: 'xy',
+    drag: {
+      enabled: true,
+      borderColor: 'rgb(54, 162, 235)',
+      borderWidth: 1,
+      backgroundColor: 'rgba(54, 162, 235, 0.3)',
+      maintainAspectRatio: true,
+    }
+  }
+};
+// </block:zoom>
+
+const zoomStatus = () => zoomOptions.zoom.drag.enabled ? 'enabled' : 'disabled';
+
+// <block:config:1>
+const config = {
+  type: 'scatter',
+  data: data,
+  options: {
+    scales: scales,
+    plugins: {
+      zoom: zoomOptions,
+      title: {
+        display: true,
+        position: 'bottom',
+        text: (ctx) => 'Zoom: ' + zoomStatus()
+      }
+    },
+  }
+};
+// </block:config>
+
+const actions = [
+  {
+    name: 'Toggle zoom',
+    handler(chart) {
+      zoomOptions.zoom.drag.enabled = !zoomOptions.zoom.drag.enabled;
+      chart.update();
+    }
+  }, {
+    name: 'Reset zoom',
+    handler(chart) {
+      chart.resetZoom();
+    }
+  }
+];
+
+module.exports = {
+  actions,
+  config,
+};
+```
diff --git a/package-lock.json b/package-lock.json
index dc62137..7875992 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,7 @@
       "devDependencies": {
         "@babel/core": "^7.20.2",
         "@babel/preset-env": "^7.20.2",
-        "@rollup/plugin-commonjs": "^25.0.7",
+        "@rollup/plugin-commonjs": "^28.0.1",
         "@rollup/plugin-json": "^6.1.0",
         "@rollup/plugin-node-resolve": "^15.2.3",
         "@rollup/plugin-terser": "^0.4.4",
@@ -25,7 +25,7 @@
         "@typescript-eslint/parser": "^5.4.0",
         "babel-loader": "^8.3.0",
         "chart.js": "^4.3.2",
-        "chartjs-adapter-date-fns": "^2.0.1",
+        "chartjs-adapter-date-fns": "^3.0.0",
         "chartjs-test-utils": "^0.3.0",
         "concurrently": "^6.0.2",
         "coveralls": "^3.1.0",
@@ -44,7 +44,7 @@
         "karma-jasmine": "^4.0.2",
         "karma-jasmine-html-reporter": "^1.5.4",
         "karma-rollup-preprocessor": "^7.0.7",
-        "karma-spec-reporter": "0.0.32",
+        "karma-spec-reporter": "0.0.36",
         "ng-hammerjs": "^2.0.8",
         "pixelmatch": "^5.2.1",
         "rollup": "^4.12.1",
@@ -1879,20 +1879,21 @@
       }
     },
     "node_modules/@rollup/plugin-commonjs": {
-      "version": "25.0.8",
-      "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz",
-      "integrity": "sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==",
+      "version": "28.0.1",
+      "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz",
+      "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==",
       "dev": true,
       "dependencies": {
         "@rollup/pluginutils": "^5.0.1",
         "commondir": "^1.0.1",
         "estree-walker": "^2.0.2",
-        "glob": "^8.0.3",
+        "fdir": "^6.2.0",
         "is-reference": "1.2.1",
-        "magic-string": "^0.30.3"
+        "magic-string": "^0.30.3",
+        "picomatch": "^4.0.2"
       },
       "engines": {
-        "node": ">=14.0.0"
+        "node": ">=16.0.0 || 14 >= 14.17"
       },
       "peerDependencies": {
         "rollup": "^2.68.0||^3.0.0||^4.0.0"
@@ -6097,12 +6098,13 @@
       }
     },
     "node_modules/chartjs-adapter-date-fns": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.1.tgz",
-      "integrity": "sha512-v3WV9rdnQ05ce3A0ZCjzUekJCAbfm6+3HqSoeY2BIkdMYZoYr/4T+ril1tZyDl869lz6xdNVMXejUFT9YKpw4A==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
+      "integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
       "dev": true,
       "peerDependencies": {
-        "chart.js": ">=2.8.0"
+        "chart.js": ">=2.8.0",
+        "date-fns": ">=2.0.0"
       }
     },
     "node_modules/chartjs-test-utils": {
@@ -9720,6 +9722,20 @@
         "node": ">=0.8.0"
       }
     },
+    "node_modules/fdir": {
+      "version": "6.4.2",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz",
+      "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==",
+      "dev": true,
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/figgy-pudding": {
       "version": "3.5.2",
       "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
@@ -10232,26 +10248,6 @@
         "assert-plus": "^1.0.0"
       }
     },
-    "node_modules/glob": {
-      "version": "8.1.0",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
-      "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
-      "deprecated": "Glob versions prior to v9 are no longer supported",
-      "dev": true,
-      "dependencies": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^5.0.1",
-        "once": "^1.3.0"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
     "node_modules/glob-parent": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -10270,27 +10266,6 @@
       "integrity": "sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==",
       "dev": true
     },
-    "node_modules/glob/node_modules/brace-expansion": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-      "dev": true,
-      "dependencies": {
-        "balanced-match": "^1.0.0"
-      }
-    },
-    "node_modules/glob/node_modules/minimatch": {
-      "version": "5.1.6",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
-      "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
-      "dev": true,
-      "dependencies": {
-        "brace-expansion": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/global": {
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
@@ -12387,12 +12362,12 @@
       }
     },
     "node_modules/karma-spec-reporter": {
-      "version": "0.0.32",
-      "resolved": "https://registry.npmjs.org/karma-spec-reporter/-/karma-spec-reporter-0.0.32.tgz",
-      "integrity": "sha512-ZXsYERZJMTNRR2F3QN11OWF5kgnT/K2dzhM+oY3CDyMrDI3TjIWqYGG7c15rR9wjmy9lvdC+CCshqn3YZqnNrA==",
+      "version": "0.0.36",
+      "resolved": "https://registry.npmjs.org/karma-spec-reporter/-/karma-spec-reporter-0.0.36.tgz",
+      "integrity": "sha512-11bvOl1x6ryKZph7kmbmMpbi8vsngEGxGOoeTlIcDaH3ab3j8aPJnZ+r+K/SS0sBSGy5VGkGYO2+hLct7hw/6w==",
       "dev": true,
       "dependencies": {
-        "colors": "^1.1.2"
+        "colors": "1.4.0"
       },
       "peerDependencies": {
         "karma": ">=0.9"
diff --git a/package.json b/package.json
index 5370b9f..97b952e 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,6 @@
     "lint-types": "eslint \"types/**/*.ts\" && tsc -p types/tests/",
     "lint": "concurrently \"npm:lint-*\"",
     "test": "cross-env NODE_ENV=test concurrently \"npm:test-*\"",
-    "test-lint": "npm run lint",
     "test-karma": "karma start --auto-watch --single-run --coverage",
     "test-types": "tsc -p types/tests/"
   },
@@ -37,7 +36,7 @@
   "devDependencies": {
     "@babel/core": "^7.20.2",
     "@babel/preset-env": "^7.20.2",
-    "@rollup/plugin-commonjs": "^25.0.7",
+    "@rollup/plugin-commonjs": "^28.0.1",
     "@rollup/plugin-json": "^6.1.0",
     "@rollup/plugin-node-resolve": "^15.2.3",
     "@rollup/plugin-terser": "^0.4.4",
@@ -47,7 +46,7 @@
     "@typescript-eslint/parser": "^5.4.0",
     "babel-loader": "^8.3.0",
     "chart.js": "^4.3.2",
-    "chartjs-adapter-date-fns": "^2.0.1",
+    "chartjs-adapter-date-fns": "^3.0.0",
     "chartjs-test-utils": "^0.3.0",
     "concurrently": "^6.0.2",
     "coveralls": "^3.1.0",
@@ -66,7 +65,7 @@
     "karma-jasmine": "^4.0.2",
     "karma-jasmine-html-reporter": "^1.5.4",
     "karma-rollup-preprocessor": "^7.0.7",
-    "karma-spec-reporter": "0.0.32",
+    "karma-spec-reporter": "0.0.36",
     "ng-hammerjs": "^2.0.8",
     "pixelmatch": "^5.2.1",
     "rollup": "^4.12.1",
diff --git a/src/handlers.js b/src/handlers.js
index 2d65935..e21a9fc 100644
--- a/src/handlers.js
+++ b/src/handlers.js
@@ -96,31 +96,59 @@ export function mouseDown(chart, event) {
   addHandler(chart, window.document, 'keydown', keyDown);
 }
 
-export function computeDragRect(chart, mode, beginPointEvent, endPointEvent) {
+function applyAspectRatio(endPoint, beginPoint, aspectRatio) {
+  let width = endPoint.x - beginPoint.x;
+  let height = endPoint.y - beginPoint.y;
+  const ratio = Math.abs(width / height);
+
+  if (ratio > aspectRatio) {
+    width = Math.sign(width) * Math.abs(height * aspectRatio);
+  } else if (ratio < aspectRatio) {
+    height = Math.sign(height) * Math.abs(width / aspectRatio);
+  }
+
+  endPoint.x = beginPoint.x + width;
+  endPoint.y = beginPoint.y + height;
+}
+
+function applyMinMaxProps(rect, beginPoint, endPoint, {min, max, prop}) {
+  rect[min] = Math.max(0, Math.min(beginPoint[prop], endPoint[prop]));
+  rect[max] = Math.max(beginPoint[prop], endPoint[prop]);
+}
+
+function getReplativePoints(chart, points, maintainAspectRatio) {
+  const beginPoint = getPointPosition(points.dragStart, chart);
+  const endPoint = getPointPosition(points.dragEnd, chart);
+
+  if (maintainAspectRatio) {
+    const aspectRatio = chart.chartArea.width / chart.chartArea.height;
+    applyAspectRatio(endPoint, beginPoint, aspectRatio);
+  }
+
+  return {beginPoint, endPoint};
+}
+
+export function computeDragRect(chart, mode, points, maintainAspectRatio) {
   const xEnabled = directionEnabled(mode, 'x', chart);
   const yEnabled = directionEnabled(mode, 'y', chart);
-  let {top, left, right, bottom, width: chartWidth, height: chartHeight} = chart.chartArea;
+  const {top, left, right, bottom, width: chartWidth, height: chartHeight} = chart.chartArea;
+  const rect = {top, left, right, bottom};
 
-  const beginPoint = getPointPosition(beginPointEvent, chart);
-  const endPoint = getPointPosition(endPointEvent, chart);
+  const {beginPoint, endPoint} = getReplativePoints(chart, points, maintainAspectRatio && xEnabled && yEnabled);
 
   if (xEnabled) {
-    left = Math.max(0, Math.min(beginPoint.x, endPoint.x));
-    right = Math.min(chart.width, Math.max(beginPoint.x, endPoint.x));
+    applyMinMaxProps(rect, beginPoint, endPoint, {min: 'left', max: 'right', prop: 'x'});
   }
 
   if (yEnabled) {
-    top = Math.max(0, Math.min(beginPoint.y, endPoint.y));
-    bottom = Math.min(chart.height, Math.max(beginPoint.y, endPoint.y));
+    applyMinMaxProps(rect, beginPoint, endPoint, {min: 'top', max: 'bottom', prop: 'y'});
   }
-  const width = right - left;
-  const height = bottom - top;
+
+  const width = rect.right - rect.left;
+  const height = rect.bottom - rect.top;
 
   return {
-    left,
-    top,
-    right,
-    bottom,
+    ...rect,
     width,
     height,
     zoomX: xEnabled && width ? 1 + ((chartWidth - width) / chartWidth) : 1,
@@ -135,8 +163,8 @@ export function mouseUp(chart, event) {
   }
 
   removeHandler(chart, 'mousemove');
-  const {mode, onZoomComplete, drag: {threshold = 0}} = state.options.zoom;
-  const rect = computeDragRect(chart, mode, state.dragStart, event);
+  const {mode, onZoomComplete, drag: {threshold = 0, maintainAspectRatio}} = state.options.zoom;
+  const rect = computeDragRect(chart, mode, {dragStart: state.dragStart, dragEnd: event}, maintainAspectRatio);
   const distanceX = directionEnabled(mode, 'x', chart) ? rect.width : 0;
   const distanceY = directionEnabled(mode, 'y', chart) ? rect.height : 0;
   const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
diff --git a/src/plugin.js b/src/plugin.js
index e0ae2a5..012e322 100644
--- a/src/plugin.js
+++ b/src/plugin.js
@@ -13,7 +13,7 @@ function draw(chart, caller, options) {
   if (dragOptions.drawTime !== caller || !dragEnd) {
     return;
   }
-  const {left, top, width, height} = computeDragRect(chart, options.zoom.mode, dragStart, dragEnd);
+  const {left, top, width, height} = computeDragRect(chart, options.zoom.mode, {dragStart, dragEnd}, dragOptions.maintainAspectRatio);
   const ctx = chart.ctx;
 
   ctx.save();


Test patch:
diff --git a/test/specs/zoom.wheel.spec.js b/test/specs/zoom.wheel.spec.js
index d0be274..0abad4d 100644
--- a/test/specs/zoom.wheel.spec.js
+++ b/test/specs/zoom.wheel.spec.js
@@ -309,6 +309,53 @@ describe('zoom with wheel', function() {
     });
   });
 
+  it('should respect aspectRatio when mode = xy', function() {
+    const chart = window.acquireChart({
+      type: 'line',
+      data,
+      options: {
+        scales: {
+          x: {
+            type: 'linear'
+          },
+          y: {
+            type: 'linear'
+          }
+        },
+        plugins: {
+          legend: false,
+          title: false,
+          zoom: {
+            zoom: {
+              drag: {
+                enabled: true,
+                maintainAspectRatio: true,
+              },
+              mode: 'xy'
+            }
+          }
+        }
+      }
+    });
+
+    const scaleX = chart.scales.x;
+    const scaleY = chart.scales.y;
+
+    jasmine.triggerMouseEvent(chart, 'mousedown', {
+      x: scaleX.getPixelForValue(1.5),
+      y: scaleY.getPixelForValue(1.1)
+    });
+    jasmine.triggerMouseEvent(chart, 'mouseup', {
+      x: scaleX.getPixelForValue(2.8),
+      y: scaleY.getPixelForValue(1.7)
+    });
+
+    expect(scaleX.options.min).toBeCloseTo(1.5);
+    expect(scaleX.options.max).toBeCloseTo(2.1);
+    expect(scaleY.options.min).toBeCloseTo(1.1);
+    expect(scaleY.options.max).toBeCloseTo(1.7);
+  });
+
   describe('events', function() {
     it('should call onZoomStart', function() {
       const startSpy = jasmine.createSpy('started');