Skip to content

Commit e8bdf4e

Browse files
authored
Add onPanStart and onZoomStart callbacks (#487)
* Add onPanStart and onZoomStart callbacks * Split out preconditions from wheel * Add mouseup to cancelled events * cancel === false
1 parent e6d15ca commit e8bdf4e

File tree

10 files changed

+381
-39
lines changed

10 files changed

+381
-39
lines changed

docs/.vuepress/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ module.exports = {
8585
'drag',
8686
'api',
8787
'click-zoom',
88+
'pan-region',
8889
],
8990
}
9091
}

docs/guide/options.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const chart = new Chart('id', {
4545
| `onPan` | `{chart}` | Called while the chart is being panned
4646
| `onPanComplete` | `{chart}` | Called once panning is completed
4747
| `onPanRejected` | `{chart,event}` | Called when panning is rejected due to missing modifier key. `event` is the a [hammer event](https://hammerjs.github.io/api#event-object) that failed
48+
| `onPanStart` | `{chart,event,point}` | Called when panning is about to start. If this callback returns false, panning is aborted and `onPanRejected` is invoked
4849

4950
## Zoom
5051

@@ -67,6 +68,7 @@ const chart = new Chart('id', {
6768
| `onZoom` | `{chart}` | Called while the chart is being zoomed
6869
| `onZoomComplete` | `{chart}` | Called once zooming is completed
6970
| `onZoomRejected` | `{chart,event}` | Called when zoom is rejected due to missing modifier key. `event` is the a [hammer event](https://hammerjs.github.io/api#event-object) that failed
71+
| `onZoomStart` | `{chart,event,point}` | Called when zooming is about to start. If this callback returns false, zooming is aborted and `onZoomRejected` is invoked
7072

7173
## Limits
7274

docs/samples/click-zoom.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ const borderPlugin = {
7070
}
7171
}
7272
};
73-
// </block>
73+
// </block:border>
7474

7575
const zoomStatus = () => 'Zoom: ' + (zoomOptions.zoom.enabled ? 'enabled' : 'disabled');
7676

docs/samples/pan-region.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Pan Region
2+
3+
In this example pan is only accepted at the middle region (50%) of the chart. This region is highlighted by a red border.
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+
ticks: {
32+
callback: (val, index, ticks) => index === 0 || index === ticks.length - 1 ? null : val,
33+
},
34+
grid: {
35+
borderColor: Utils.randomColor(1),
36+
color: 'rgba( 0, 0, 0, 0.1)',
37+
},
38+
title: {
39+
display: true,
40+
text: (ctx) => ctx.scale.axis + ' axis',
41+
}
42+
};
43+
const scales = {
44+
x: {
45+
position: 'top',
46+
},
47+
y: {
48+
position: 'right',
49+
},
50+
};
51+
Object.keys(scales).forEach(scale => Object.assign(scales[scale], scaleOpts));
52+
// </block:scales>
53+
54+
// <block:zoom:0>
55+
const zoomOptions = {
56+
limits: {
57+
x: {min: -200, max: 200, minRange: 50},
58+
y: {min: -200, max: 200, minRange: 50}
59+
},
60+
pan: {
61+
enabled: true,
62+
onPanStart({chart, point}) {
63+
const area = chart.chartArea;
64+
const w25 = area.width * 0.25;
65+
const h25 = area.height * 0.25;
66+
if (point.x < area.left + w25 || point.x > area.right - w25
67+
|| point.y < area.top + h25 || point.y > area.bottom - h25) {
68+
return false; // abort
69+
}
70+
},
71+
mode: 'xy',
72+
},
73+
zoom: {
74+
enabled: false,
75+
}
76+
};
77+
// </block:zoom>
78+
79+
// <block:border:3>
80+
const borderPlugin = {
81+
id: 'panAreaBorder',
82+
beforeDraw(chart, args, options) {
83+
const {ctx, chartArea: {left, top, width, height}} = chart;
84+
ctx.save();
85+
ctx.strokeStyle = 'rgba(255, 0, 0, 0.3)';
86+
ctx.lineWidth = 1;
87+
ctx.strokeRect(left + width * 0.25, top + height * 0.25, width / 2, height / 2);
88+
ctx.restore();
89+
}
90+
};
91+
// </block:border>
92+
93+
// <block:config:1>
94+
const config = {
95+
type: 'scatter',
96+
data: data,
97+
options: {
98+
scales: scales,
99+
plugins: {
100+
zoom: zoomOptions,
101+
},
102+
},
103+
plugins: [borderPlugin]
104+
};
105+
// </block:config>
106+
107+
module.exports = {
108+
config,
109+
};
110+
```

src/hammer.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,28 +82,32 @@ function endPinch(chart, state, e) {
8282

8383
function handlePan(chart, state, e) {
8484
const delta = state.delta;
85-
if (delta !== null) {
85+
if (delta) {
8686
state.panning = true;
8787
pan(chart, {x: e.deltaX - delta.x, y: e.deltaY - delta.y}, state.panScales);
8888
state.delta = {x: e.deltaX, y: e.deltaY};
8989
}
9090
}
9191

92-
function startPan(chart, state, e) {
93-
const {enabled, overScaleMode} = state.options.pan;
92+
function startPan(chart, state, event) {
93+
const {enabled, overScaleMode, onPanStart, onPanRejected} = state.options.pan;
9494
if (!enabled) {
9595
return;
9696
}
97-
const rect = e.target.getBoundingClientRect();
97+
const rect = event.target.getBoundingClientRect();
9898
const point = {
99-
x: e.center.x - rect.left,
100-
y: e.center.y - rect.top
99+
x: event.center.x - rect.left,
100+
y: event.center.y - rect.top
101101
};
102102

103+
if (call(onPanStart, [{chart, event, point}]) === false) {
104+
return call(onPanRejected, [{chart, event}]);
105+
}
106+
103107
state.panScales = overScaleMode && getEnabledScalesByPoint(overScaleMode, point, chart);
104108
state.delta = {x: 0, y: 0};
105109
clearTimeout(state.panEndTimeout);
106-
handlePan(chart, state, e);
110+
handlePan(chart, state, event);
107111
}
108112

109113
function endPan(chart, state) {

src/handlers.js

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,32 @@ export function mouseMove(chart, event) {
2828
}
2929
}
3030

31+
function zoomStart(chart, event, zoomOptions) {
32+
const {onZoomStart, onZoomRejected} = zoomOptions;
33+
if (onZoomStart) {
34+
const {left: offsetX, top: offsetY} = event.target.getBoundingClientRect();
35+
const point = {
36+
x: event.clientX - offsetX,
37+
y: event.clientY - offsetY
38+
};
39+
if (call(onZoomStart, [{chart, event, point}]) === false) {
40+
call(onZoomRejected, [{chart, event}]);
41+
return false;
42+
}
43+
}
44+
}
45+
3146
export function mouseDown(chart, event) {
3247
const state = getState(chart);
3348
const {pan: panOptions, zoom: zoomOptions} = state.options;
3449
const panKey = panOptions && panOptions.modifierKey;
3550
if (panKey && event[panKey + 'Key']) {
3651
return call(zoomOptions.onZoomRejected, [{chart, event}]);
3752
}
53+
54+
if (zoomStart(chart, event, zoomOptions) === false) {
55+
return;
56+
}
3857
state.dragStart = event;
3958

4059
addHandler(chart, chart.canvas, 'mousemove', mouseMove);
@@ -104,13 +123,16 @@ export function mouseUp(chart, event) {
104123
call(zoomOptions.onZoomComplete, [chart]);
105124
}
106125

107-
export function wheel(chart, event) {
108-
const {handlers: {onZoomComplete}, options: {zoom: zoomOptions}} = getState(chart);
126+
function wheelPreconditions(chart, event, zoomOptions) {
109127
const {wheelModifierKey, onZoomRejected} = zoomOptions;
110-
111128
// Before preventDefault, check if the modifier key required and pressed
112129
if (wheelModifierKey && !event[wheelModifierKey + 'Key']) {
113-
return call(onZoomRejected, [{chart, event}]);
130+
call(onZoomRejected, [{chart, event}]);
131+
return;
132+
}
133+
134+
if (zoomStart(chart, event, zoomOptions) === false) {
135+
return;
114136
}
115137

116138
// Prevent the event from triggering the default behavior (eg. Content scrolling).
@@ -123,6 +145,15 @@ export function wheel(chart, event) {
123145
if (event.deltaY === undefined) {
124146
return;
125147
}
148+
return true;
149+
}
150+
151+
export function wheel(chart, event) {
152+
const {handlers: {onZoomComplete}, options: {zoom: zoomOptions}} = getState(chart);
153+
154+
if (!wheelPreconditions(chart, event, zoomOptions)) {
155+
return;
156+
}
126157

127158
const rect = event.target.getBoundingClientRect();
128159
const speed = 1 + (event.deltaY >= 0 ? -zoomOptions.speed : zoomOptions.speed);

src/plugin.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ export default {
4242

4343
beforeEvent(chart, args) {
4444
const state = getState(chart);
45-
if (args.event.type === 'click' && (state.panning || state.dragging)) {
46-
// cancel the click event at pan/zoom end
45+
const type = args.event.type;
46+
if ((type === 'click' || type === 'mouseup') && (state.panning || state.dragging)) {
47+
// cancel the click/mouseup event at pan/zoom end
4748
return false;
4849
}
4950
},

test/specs/pan.spec.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,93 @@
11
describe('pan', function() {
22
describe('auto', jasmine.fixture.specs('pan'));
3+
4+
const data = {
5+
datasets: [{
6+
data: [{
7+
x: 1,
8+
y: 3
9+
}, {
10+
x: 2,
11+
y: 2
12+
}, {
13+
x: 3,
14+
y: 1
15+
}]
16+
}]
17+
};
18+
19+
describe('events', function() {
20+
it('should call onPanStart', function(done) {
21+
const startSpy = jasmine.createSpy('started');
22+
const chart = window.acquireChart({
23+
type: 'scatter',
24+
data,
25+
options: {
26+
plugins: {
27+
zoom: {
28+
pan: {
29+
enabled: true,
30+
mode: 'xy',
31+
onPanStart: startSpy
32+
}
33+
}
34+
}
35+
}
36+
});
37+
38+
Simulator.gestures.pan(chart.canvas, {deltaX: -350, deltaY: 0, duration: 50}, function() {
39+
expect(startSpy).toHaveBeenCalled();
40+
expect(chart.scales.x.min).not.toBe(1);
41+
done();
42+
});
43+
});
44+
45+
it('should call onPanRejected when onStartPan returns false', function(done) {
46+
const rejectSpy = jasmine.createSpy('rejected');
47+
const chart = window.acquireChart({
48+
type: 'scatter',
49+
data,
50+
options: {
51+
plugins: {
52+
zoom: {
53+
pan: {
54+
enabled: true,
55+
mode: 'xy',
56+
onPanStart: () => false,
57+
onPanRejected: rejectSpy
58+
}
59+
}
60+
}
61+
}
62+
});
63+
64+
Simulator.gestures.pan(chart.canvas, {deltaX: -350, deltaY: 0, duration: 50}, function() {
65+
expect(rejectSpy).toHaveBeenCalled();
66+
expect(chart.scales.x.min).toBe(1);
67+
done();
68+
});
69+
});
70+
71+
it('should call onPanComplete', function(done) {
72+
const chart = window.acquireChart({
73+
type: 'scatter',
74+
data,
75+
options: {
76+
plugins: {
77+
zoom: {
78+
pan: {
79+
enabled: true,
80+
mode: 'xy',
81+
onPanComplete(ctx) {
82+
expect(ctx.chart.scales.x.min).not.toBe(1);
83+
done();
84+
}
85+
}
86+
}
87+
}
88+
}
89+
});
90+
Simulator.gestures.pan(chart.canvas, {deltaX: -350, deltaY: 0, duration: 50});
91+
});
92+
});
393
});

0 commit comments

Comments
 (0)