Skip to content

Commit

Permalink
Add onPanStart and onZoomStart callbacks (#487)
Browse files Browse the repository at this point in the history
* Add onPanStart and onZoomStart callbacks

* Split out preconditions from wheel

* Add mouseup to cancelled events

* cancel === false
  • Loading branch information
kurkle authored May 1, 2021
1 parent e6d15ca commit e8bdf4e
Show file tree
Hide file tree
Showing 10 changed files with 381 additions and 39 deletions.
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ module.exports = {
'drag',
'api',
'click-zoom',
'pan-region',
],
}
}
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const chart = new Chart('id', {
| `onPan` | `{chart}` | Called while the chart is being panned
| `onPanComplete` | `{chart}` | Called once panning is completed
| `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
| `onPanStart` | `{chart,event,point}` | Called when panning is about to start. If this callback returns false, panning is aborted and `onPanRejected` is invoked

## Zoom

Expand All @@ -67,6 +68,7 @@ const chart = new Chart('id', {
| `onZoom` | `{chart}` | Called while the chart is being zoomed
| `onZoomComplete` | `{chart}` | Called once zooming is completed
| `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
| `onZoomStart` | `{chart,event,point}` | Called when zooming is about to start. If this callback returns false, zooming is aborted and `onZoomRejected` is invoked

## Limits

Expand Down
2 changes: 1 addition & 1 deletion docs/samples/click-zoom.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const borderPlugin = {
}
}
};
// </block>
// </block:border>

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

Expand Down
110 changes: 110 additions & 0 deletions docs/samples/pan-region.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Pan Region

In this example pan is only accepted at the middle region (50%) of the chart. This region is highlighted by a red border.

```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 = {
ticks: {
callback: (val, index, ticks) => index === 0 || index === ticks.length - 1 ? null : val,
},
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 zoomOptions = {
limits: {
x: {min: -200, max: 200, minRange: 50},
y: {min: -200, max: 200, minRange: 50}
},
pan: {
enabled: true,
onPanStart({chart, point}) {
const area = chart.chartArea;
const w25 = area.width * 0.25;
const h25 = area.height * 0.25;
if (point.x < area.left + w25 || point.x > area.right - w25
|| point.y < area.top + h25 || point.y > area.bottom - h25) {
return false; // abort
}
},
mode: 'xy',
},
zoom: {
enabled: false,
}
};
// </block:zoom>

// <block:border:3>
const borderPlugin = {
id: 'panAreaBorder',
beforeDraw(chart, args, options) {
const {ctx, chartArea: {left, top, width, height}} = chart;
ctx.save();
ctx.strokeStyle = 'rgba(255, 0, 0, 0.3)';
ctx.lineWidth = 1;
ctx.strokeRect(left + width * 0.25, top + height * 0.25, width / 2, height / 2);
ctx.restore();
}
};
// </block:border>

// <block:config:1>
const config = {
type: 'scatter',
data: data,
options: {
scales: scales,
plugins: {
zoom: zoomOptions,
},
},
plugins: [borderPlugin]
};
// </block:config>

module.exports = {
config,
};
```
18 changes: 11 additions & 7 deletions src/hammer.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,28 +82,32 @@ function endPinch(chart, state, e) {

function handlePan(chart, state, e) {
const delta = state.delta;
if (delta !== null) {
if (delta) {
state.panning = true;
pan(chart, {x: e.deltaX - delta.x, y: e.deltaY - delta.y}, state.panScales);
state.delta = {x: e.deltaX, y: e.deltaY};
}
}

function startPan(chart, state, e) {
const {enabled, overScaleMode} = state.options.pan;
function startPan(chart, state, event) {
const {enabled, overScaleMode, onPanStart, onPanRejected} = state.options.pan;
if (!enabled) {
return;
}
const rect = e.target.getBoundingClientRect();
const rect = event.target.getBoundingClientRect();
const point = {
x: e.center.x - rect.left,
y: e.center.y - rect.top
x: event.center.x - rect.left,
y: event.center.y - rect.top
};

if (call(onPanStart, [{chart, event, point}]) === false) {
return call(onPanRejected, [{chart, event}]);
}

state.panScales = overScaleMode && getEnabledScalesByPoint(overScaleMode, point, chart);
state.delta = {x: 0, y: 0};
clearTimeout(state.panEndTimeout);
handlePan(chart, state, e);
handlePan(chart, state, event);
}

function endPan(chart, state) {
Expand Down
39 changes: 35 additions & 4 deletions src/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,32 @@ export function mouseMove(chart, event) {
}
}

function zoomStart(chart, event, zoomOptions) {
const {onZoomStart, onZoomRejected} = zoomOptions;
if (onZoomStart) {
const {left: offsetX, top: offsetY} = event.target.getBoundingClientRect();
const point = {
x: event.clientX - offsetX,
y: event.clientY - offsetY
};
if (call(onZoomStart, [{chart, event, point}]) === false) {
call(onZoomRejected, [{chart, event}]);
return false;
}
}
}

export function mouseDown(chart, event) {
const state = getState(chart);
const {pan: panOptions, zoom: zoomOptions} = state.options;
const panKey = panOptions && panOptions.modifierKey;
if (panKey && event[panKey + 'Key']) {
return call(zoomOptions.onZoomRejected, [{chart, event}]);
}

if (zoomStart(chart, event, zoomOptions) === false) {
return;
}
state.dragStart = event;

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

export function wheel(chart, event) {
const {handlers: {onZoomComplete}, options: {zoom: zoomOptions}} = getState(chart);
function wheelPreconditions(chart, event, zoomOptions) {
const {wheelModifierKey, onZoomRejected} = zoomOptions;

// Before preventDefault, check if the modifier key required and pressed
if (wheelModifierKey && !event[wheelModifierKey + 'Key']) {
return call(onZoomRejected, [{chart, event}]);
call(onZoomRejected, [{chart, event}]);
return;
}

if (zoomStart(chart, event, zoomOptions) === false) {
return;
}

// Prevent the event from triggering the default behavior (eg. Content scrolling).
Expand All @@ -123,6 +145,15 @@ export function wheel(chart, event) {
if (event.deltaY === undefined) {
return;
}
return true;
}

export function wheel(chart, event) {
const {handlers: {onZoomComplete}, options: {zoom: zoomOptions}} = getState(chart);

if (!wheelPreconditions(chart, event, zoomOptions)) {
return;
}

const rect = event.target.getBoundingClientRect();
const speed = 1 + (event.deltaY >= 0 ? -zoomOptions.speed : zoomOptions.speed);
Expand Down
5 changes: 3 additions & 2 deletions src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ export default {

beforeEvent(chart, args) {
const state = getState(chart);
if (args.event.type === 'click' && (state.panning || state.dragging)) {
// cancel the click event at pan/zoom end
const type = args.event.type;
if ((type === 'click' || type === 'mouseup') && (state.panning || state.dragging)) {
// cancel the click/mouseup event at pan/zoom end
return false;
}
},
Expand Down
90 changes: 90 additions & 0 deletions test/specs/pan.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,93 @@
describe('pan', function() {
describe('auto', jasmine.fixture.specs('pan'));

const data = {
datasets: [{
data: [{
x: 1,
y: 3
}, {
x: 2,
y: 2
}, {
x: 3,
y: 1
}]
}]
};

describe('events', function() {
it('should call onPanStart', function(done) {
const startSpy = jasmine.createSpy('started');
const chart = window.acquireChart({
type: 'scatter',
data,
options: {
plugins: {
zoom: {
pan: {
enabled: true,
mode: 'xy',
onPanStart: startSpy
}
}
}
}
});

Simulator.gestures.pan(chart.canvas, {deltaX: -350, deltaY: 0, duration: 50}, function() {
expect(startSpy).toHaveBeenCalled();
expect(chart.scales.x.min).not.toBe(1);
done();
});
});

it('should call onPanRejected when onStartPan returns false', function(done) {
const rejectSpy = jasmine.createSpy('rejected');
const chart = window.acquireChart({
type: 'scatter',
data,
options: {
plugins: {
zoom: {
pan: {
enabled: true,
mode: 'xy',
onPanStart: () => false,
onPanRejected: rejectSpy
}
}
}
}
});

Simulator.gestures.pan(chart.canvas, {deltaX: -350, deltaY: 0, duration: 50}, function() {
expect(rejectSpy).toHaveBeenCalled();
expect(chart.scales.x.min).toBe(1);
done();
});
});

it('should call onPanComplete', function(done) {
const chart = window.acquireChart({
type: 'scatter',
data,
options: {
plugins: {
zoom: {
pan: {
enabled: true,
mode: 'xy',
onPanComplete(ctx) {
expect(ctx.chart.scales.x.min).not.toBe(1);
done();
}
}
}
}
}
});
Simulator.gestures.pan(chart.canvas, {deltaX: -350, deltaY: 0, duration: 50});
});
});
});
Loading

0 comments on commit e8bdf4e

Please sign in to comment.