diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index a55fa36d..bb57688e 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 74c6cd93..74afe03b 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 00000000..54094b36 --- /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 +// +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), + }] +}; +// + +// +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)); +// + +// +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, + } + } +}; +// + +const zoomStatus = () => zoomOptions.zoom.drag.enabled ? 'enabled' : 'disabled'; + +// +const config = { + type: 'scatter', + data: data, + options: { + scales: scales, + plugins: { + zoom: zoomOptions, + title: { + display: true, + position: 'bottom', + text: (ctx) => 'Zoom: ' + zoomStatus() + } + }, + } +}; +// + +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/src/handlers.js b/src/handlers.js index 2d659350..e21a9fc2 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 e0ae2a5d..012e322e 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(); diff --git a/test/specs/zoom.wheel.spec.js b/test/specs/zoom.wheel.spec.js index d0be2745..0abad4dc 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');