Skip to content

Commit 9388185

Browse files
authored
feat(drag): maintainAspectRatio (#861)
* feat(drag): maintainAspectRatio * fix: some code climate issues * fix: lint
1 parent 26c248b commit 9388185

File tree

6 files changed

+206
-18
lines changed

6 files changed

+206
-18
lines changed

docs/.vuepress/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ module.exports = {
146146
children: [
147147
'drag/category',
148148
'drag/linear',
149+
'drag/linear-ratio',
149150
'drag/log',
150151
'drag/time',
151152
'drag/timeseries',

docs/guide/options.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ const chart = new Chart('id', {
8080
| [`drawTime`](#draw-time) | `string` | `beforeDatasetsDraw` | When the dragging box is drawn on the chart
8181
| `threshold` | `number` | `0` | Minimal zoom distance required before actually applying zoom
8282
| `modifierKey` | `'ctrl'`\|`'alt'`\|`'shift'`\|`'meta'` | `null` | Modifier key required for drag-to-zoom
83-
83+
| `maintainAspectRatio` | `boolean` | `undefined` | Maintain aspect ratio of the chart
8484

8585
## Draw Time
8686

docs/samples/drag/linear-ratio.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Linear Scales + maintainAspectRatio
2+
3+
Zooming is performed by clicking and selecting an area over the chart with the mouse. Pan is activated by keeping `shift` pressed.
4+
5+
```js chart-editor
6+
// <block:data:1>
7+
const NUMBER_CFG = {count: 20, min: -100, max: 100};
8+
const data = {
9+
datasets: [{
10+
label: 'My First dataset',
11+
borderColor: Utils.randomColor(0.4),
12+
backgroundColor: Utils.randomColor(0.1),
13+
pointBorderColor: Utils.randomColor(0.7),
14+
pointBackgroundColor: Utils.randomColor(0.5),
15+
pointBorderWidth: 1,
16+
data: Utils.points(NUMBER_CFG),
17+
}, {
18+
label: 'My Second dataset',
19+
borderColor: Utils.randomColor(0.4),
20+
backgroundColor: Utils.randomColor(0.1),
21+
pointBorderColor: Utils.randomColor(0.7),
22+
pointBackgroundColor: Utils.randomColor(0.5),
23+
pointBorderWidth: 1,
24+
data: Utils.points(NUMBER_CFG),
25+
}]
26+
};
27+
// </block:data>
28+
29+
// <block:scales:2>
30+
const scaleOpts = {
31+
reverse: true,
32+
grid: {
33+
borderColor: Utils.randomColor(1),
34+
color: 'rgba( 0, 0, 0, 0.1)',
35+
},
36+
title: {
37+
display: true,
38+
text: (ctx) => ctx.scale.axis + ' axis',
39+
}
40+
};
41+
const scales = {
42+
x: {
43+
position: 'top',
44+
},
45+
y: {
46+
position: 'right',
47+
},
48+
};
49+
Object.keys(scales).forEach(scale => Object.assign(scales[scale], scaleOpts));
50+
// </block:scales>
51+
52+
// <block:zoom:0>
53+
const dragColor = Utils.randomColor(0.4);
54+
const zoomOptions = {
55+
pan: {
56+
enabled: true,
57+
mode: 'xy',
58+
modifierKey: 'shift',
59+
},
60+
zoom: {
61+
mode: 'xy',
62+
drag: {
63+
enabled: true,
64+
borderColor: 'rgb(54, 162, 235)',
65+
borderWidth: 1,
66+
backgroundColor: 'rgba(54, 162, 235, 0.3)',
67+
maintainAspectRatio: true,
68+
}
69+
}
70+
};
71+
// </block:zoom>
72+
73+
const zoomStatus = () => zoomOptions.zoom.drag.enabled ? 'enabled' : 'disabled';
74+
75+
// <block:config:1>
76+
const config = {
77+
type: 'scatter',
78+
data: data,
79+
options: {
80+
scales: scales,
81+
plugins: {
82+
zoom: zoomOptions,
83+
title: {
84+
display: true,
85+
position: 'bottom',
86+
text: (ctx) => 'Zoom: ' + zoomStatus()
87+
}
88+
},
89+
}
90+
};
91+
// </block:config>
92+
93+
const actions = [
94+
{
95+
name: 'Toggle zoom',
96+
handler(chart) {
97+
zoomOptions.zoom.drag.enabled = !zoomOptions.zoom.drag.enabled;
98+
chart.update();
99+
}
100+
}, {
101+
name: 'Reset zoom',
102+
handler(chart) {
103+
chart.resetZoom();
104+
}
105+
}
106+
];
107+
108+
module.exports = {
109+
actions,
110+
config,
111+
};
112+
```

src/handlers.js

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,31 +96,59 @@ export function mouseDown(chart, event) {
9696
addHandler(chart, window.document, 'keydown', keyDown);
9797
}
9898

99-
export function computeDragRect(chart, mode, beginPointEvent, endPointEvent) {
99+
function applyAspectRatio(endPoint, beginPoint, aspectRatio) {
100+
let width = endPoint.x - beginPoint.x;
101+
let height = endPoint.y - beginPoint.y;
102+
const ratio = Math.abs(width / height);
103+
104+
if (ratio > aspectRatio) {
105+
width = Math.sign(width) * Math.abs(height * aspectRatio);
106+
} else if (ratio < aspectRatio) {
107+
height = Math.sign(height) * Math.abs(width / aspectRatio);
108+
}
109+
110+
endPoint.x = beginPoint.x + width;
111+
endPoint.y = beginPoint.y + height;
112+
}
113+
114+
function applyMinMaxProps(rect, beginPoint, endPoint, {min, max, prop}) {
115+
rect[min] = Math.max(0, Math.min(beginPoint[prop], endPoint[prop]));
116+
rect[max] = Math.max(beginPoint[prop], endPoint[prop]);
117+
}
118+
119+
function getReplativePoints(chart, points, maintainAspectRatio) {
120+
const beginPoint = getPointPosition(points.dragStart, chart);
121+
const endPoint = getPointPosition(points.dragEnd, chart);
122+
123+
if (maintainAspectRatio) {
124+
const aspectRatio = chart.chartArea.width / chart.chartArea.height;
125+
applyAspectRatio(endPoint, beginPoint, aspectRatio);
126+
}
127+
128+
return {beginPoint, endPoint};
129+
}
130+
131+
export function computeDragRect(chart, mode, points, maintainAspectRatio) {
100132
const xEnabled = directionEnabled(mode, 'x', chart);
101133
const yEnabled = directionEnabled(mode, 'y', chart);
102-
let {top, left, right, bottom, width: chartWidth, height: chartHeight} = chart.chartArea;
134+
const {top, left, right, bottom, width: chartWidth, height: chartHeight} = chart.chartArea;
135+
const rect = {top, left, right, bottom};
103136

104-
const beginPoint = getPointPosition(beginPointEvent, chart);
105-
const endPoint = getPointPosition(endPointEvent, chart);
137+
const {beginPoint, endPoint} = getReplativePoints(chart, points, maintainAspectRatio && xEnabled && yEnabled);
106138

107139
if (xEnabled) {
108-
left = Math.max(0, Math.min(beginPoint.x, endPoint.x));
109-
right = Math.min(chart.width, Math.max(beginPoint.x, endPoint.x));
140+
applyMinMaxProps(rect, beginPoint, endPoint, {min: 'left', max: 'right', prop: 'x'});
110141
}
111142

112143
if (yEnabled) {
113-
top = Math.max(0, Math.min(beginPoint.y, endPoint.y));
114-
bottom = Math.min(chart.height, Math.max(beginPoint.y, endPoint.y));
144+
applyMinMaxProps(rect, beginPoint, endPoint, {min: 'top', max: 'bottom', prop: 'y'});
115145
}
116-
const width = right - left;
117-
const height = bottom - top;
146+
147+
const width = rect.right - rect.left;
148+
const height = rect.bottom - rect.top;
118149

119150
return {
120-
left,
121-
top,
122-
right,
123-
bottom,
151+
...rect,
124152
width,
125153
height,
126154
zoomX: xEnabled && width ? 1 + ((chartWidth - width) / chartWidth) : 1,
@@ -135,8 +163,8 @@ export function mouseUp(chart, event) {
135163
}
136164

137165
removeHandler(chart, 'mousemove');
138-
const {mode, onZoomComplete, drag: {threshold = 0}} = state.options.zoom;
139-
const rect = computeDragRect(chart, mode, state.dragStart, event);
166+
const {mode, onZoomComplete, drag: {threshold = 0, maintainAspectRatio}} = state.options.zoom;
167+
const rect = computeDragRect(chart, mode, {dragStart: state.dragStart, dragEnd: event}, maintainAspectRatio);
140168
const distanceX = directionEnabled(mode, 'x', chart) ? rect.width : 0;
141169
const distanceY = directionEnabled(mode, 'y', chart) ? rect.height : 0;
142170
const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);

src/plugin.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ function draw(chart, caller, options) {
1313
if (dragOptions.drawTime !== caller || !dragEnd) {
1414
return;
1515
}
16-
const {left, top, width, height} = computeDragRect(chart, options.zoom.mode, dragStart, dragEnd);
16+
const {left, top, width, height} = computeDragRect(chart, options.zoom.mode, {dragStart, dragEnd}, dragOptions.maintainAspectRatio);
1717
const ctx = chart.ctx;
1818

1919
ctx.save();

test/specs/zoom.wheel.spec.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,53 @@ describe('zoom with wheel', function() {
309309
});
310310
});
311311

312+
it('should respect aspectRatio when mode = xy', function() {
313+
const chart = window.acquireChart({
314+
type: 'line',
315+
data,
316+
options: {
317+
scales: {
318+
x: {
319+
type: 'linear'
320+
},
321+
y: {
322+
type: 'linear'
323+
}
324+
},
325+
plugins: {
326+
legend: false,
327+
title: false,
328+
zoom: {
329+
zoom: {
330+
drag: {
331+
enabled: true,
332+
maintainAspectRatio: true,
333+
},
334+
mode: 'xy'
335+
}
336+
}
337+
}
338+
}
339+
});
340+
341+
const scaleX = chart.scales.x;
342+
const scaleY = chart.scales.y;
343+
344+
jasmine.triggerMouseEvent(chart, 'mousedown', {
345+
x: scaleX.getPixelForValue(1.5),
346+
y: scaleY.getPixelForValue(1.1)
347+
});
348+
jasmine.triggerMouseEvent(chart, 'mouseup', {
349+
x: scaleX.getPixelForValue(2.8),
350+
y: scaleY.getPixelForValue(1.7)
351+
});
352+
353+
expect(scaleX.options.min).toBeCloseTo(1.5);
354+
expect(scaleX.options.max).toBeCloseTo(2.1);
355+
expect(scaleY.options.min).toBeCloseTo(1.1);
356+
expect(scaleY.options.max).toBeCloseTo(1.7);
357+
});
358+
312359
describe('events', function() {
313360
it('should call onZoomStart', function() {
314361
const startSpy = jasmine.createSpy('started');

0 commit comments

Comments
 (0)