Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(drag): maintainAspectRatio #860

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ module.exports = {
children: [
'drag/category',
'drag/linear',
'drag/linear-ratio',
'drag/log',
'drag/time',
'drag/timeseries',
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
112 changes: 112 additions & 0 deletions docs/samples/drag/linear-ratio.md
Original file line number Diff line number Diff line change
@@ -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,
};
```
14 changes: 7 additions & 7 deletions src/core.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {each, callback as call, sign, valueOrDefault} from 'chart.js/helpers';
import {panFunctions, updateRange, zoomFunctions, zoomRectFunctions} from './scale.types';
import {getState} from './state';
import {directionEnabled, getEnabledScalesByPoint} from './utils';
import {directionEnabled, getEnabledScalesByPoint, mathMax, mathMin, objectKeys, mathRound} from './utils';

function shouldUpdateScaleLimits(scale, originalScaleLimits, updatedScaleLimits) {
const {id, options: {min, max}} = scale;
Expand Down Expand Up @@ -149,9 +149,9 @@ export function getZoomLevel(chart) {
each(chart.scales, function(scale) {
const origRange = getOriginalRange(state, scale.id);
if (origRange) {
const level = Math.round(origRange / (scale.max - scale.min) * 100) / 100;
min = Math.min(min, level);
max = Math.max(max, level);
const level = mathRound(origRange / (scale.max - scale.min) * 100) / 100;
min = mathMin(min, level);
max = mathMax(max, level);
}
});
return min < 1 ? min : max;
Expand Down Expand Up @@ -202,7 +202,7 @@ export function getInitialScaleBounds(chart) {
const state = getState(chart);
storeOriginalScaleLimits(chart, state);
const scaleBounds = {};
for (const scaleId of Object.keys(chart.scales)) {
for (const scaleId of objectKeys(chart.scales)) {
const {min, max} = state.originalScaleLimits[scaleId] || {min: {}, max: {}};
scaleBounds[scaleId] = {min: min.scale, max: max.scale};
}
Expand All @@ -213,7 +213,7 @@ export function getInitialScaleBounds(chart) {
export function getZoomedScaleBounds(chart) {
const state = getState(chart);
const scaleBounds = {};
for (const scaleId of Object.keys(chart.scales)) {
for (const scaleId of objectKeys(chart.scales)) {
scaleBounds[scaleId] = state.updatedScaleLimits[scaleId];
}

Expand All @@ -222,7 +222,7 @@ export function getZoomedScaleBounds(chart) {

export function isZoomedOrPanned(chart) {
const scaleBounds = getInitialScaleBounds(chart);
for (const scaleId of Object.keys(chart.scales)) {
for (const scaleId of objectKeys(chart.scales)) {
const {min: originalMin, max: originalMax} = scaleBounds[scaleId];

if (originalMin !== undefined && chart.scales[scaleId].min !== originalMin) {
Expand Down
6 changes: 3 additions & 3 deletions src/hammer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {callback as call} from 'chart.js/helpers';
import Hammer from 'hammerjs';
import {pan, zoom} from './core';
import {getState} from './state';
import {directionEnabled, getEnabledScalesByPoint, getModifierKey, keyNotPressed, keyPressed} from './utils';
import {mathAbs, directionEnabled, getEnabledScalesByPoint, getModifierKey, keyNotPressed, keyPressed} from './utils';

function createEnabler(chart, state) {
return function(recognizer, event) {
Expand All @@ -26,8 +26,8 @@ function createEnabler(chart, state) {

function pinchAxes(p0, p1) {
// fingers position difference
const pinchX = Math.abs(p0.clientX - p1.clientX);
const pinchY = Math.abs(p0.clientY - p1.clientY);
const pinchX = mathAbs(p0.clientX - p1.clientX);
const pinchY = mathAbs(p0.clientY - p1.clientY);

// diagonal fingers will change both (xy) axes
const p = pinchX / pinchY;
Expand Down
64 changes: 46 additions & 18 deletions src/handlers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {directionEnabled, debounce, keyNotPressed, getModifierKey, keyPressed} from './utils';
import {directionEnabled, debounce, keyNotPressed, getModifierKey, keyPressed, mathMin, mathMax, mathAbs} from './utils';
import {zoom, zoomRect} from './core';
import {callback as call, getRelativePosition, _isPointInArea} from 'chart.js/helpers';
import {callback as call, getRelativePosition, _isPointInArea, sign} from 'chart.js/helpers';
import {getState} from './state';

function removeHandler(chart, type) {
Expand Down Expand Up @@ -86,31 +86,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 = mathAbs(width / height);

if (ratio > aspectRatio) {
width = sign(width) * mathAbs(height * aspectRatio);
} else if (ratio < aspectRatio) {
height = sign(height) * mathAbs(width / aspectRatio);
}

endPoint.x = beginPoint.x + width;
endPoint.y = beginPoint.y + height;
}

function applyMinMaxProps(rect, beginPoint, endPoint, {min, max, prop}) {
rect[min] = mathMin(beginPoint[prop], endPoint[prop]);
rect[max] = mathMax(beginPoint[prop], endPoint[prop]);
}

function getReplativePoints(chart, points, maintainAspectRatio) {
const beginPoint = getRelativePosition(points.dragStart, chart);
const endPoint = getRelativePosition(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 = getRelativePosition(beginPointEvent, chart);
const endPoint = getRelativePosition(endPointEvent, chart);
const {beginPoint, endPoint} = getReplativePoints(chart, points, maintainAspectRatio && xEnabled && yEnabled);

if (xEnabled) {
left = Math.min(beginPoint.x, endPoint.x);
right = Math.max(beginPoint.x, endPoint.x);
applyMinMaxProps(rect, beginPoint, endPoint, {min: 'left', max: 'right', prop: 'x'});
}

if (yEnabled) {
top = Math.min(beginPoint.y, endPoint.y);
bottom = 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,
Expand All @@ -125,8 +153,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);
Expand Down
10 changes: 6 additions & 4 deletions src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import {panFunctions, zoomFunctions, zoomRectFunctions} from './scale.types';
import {getState, removeState} from './state';
import {version} from '../package.json';

const hasOwnProp = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);

function draw(chart, caller, options) {
const dragOptions = options.zoom.drag;
const {dragStart, dragEnd} = getState(chart);

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();
Expand Down Expand Up @@ -63,11 +65,11 @@ export default {
const state = getState(chart);
state.options = options;

if (Object.prototype.hasOwnProperty.call(options.zoom, 'enabled')) {
if (hasOwnProp(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')) {
if (hasOwnProp(options.zoom, 'overScaleMode')
|| hasOwnProp(options.pan, 'overScaleMode')) {
console.warn('The option `overScaleMode` is deprecated. Please use `scaleMode` instead (and update `mode` as desired).');
}

Expand Down
Loading
Loading