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

Add onPanStart and onZoomStart callbacks #487

Merged
merged 4 commits into from
May 1, 2021
Merged
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 @@ -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