Skip to content

Commit ea7087f

Browse files
committed
Merge branch 'feature/zoom-separately' into master
Added the overScaleMode option so that you can use zoom and pan separately for each scale when the mouse is over the scale.
1 parent 12c1b39 commit ea7087f

File tree

4 files changed

+278
-4
lines changed

4 files changed

+278
-4
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ plugins: {
4242
// },
4343
mode: 'xy',
4444

45+
// Which of the enabled panning directions should only be available
46+
// when the mouse cursor is over one of scale.
47+
overScaleMode: 'xy',
48+
4549
rangeMin: {
4650
// Format of min pan range depends on scale type
4751
x: null,
@@ -93,6 +97,10 @@ plugins: {
9397
// },
9498
mode: 'xy',
9599

100+
// Which of the enabled zooming directions should only be available
101+
// when the mouse cursor is over one of scale.
102+
overScaleMode: 'xy',
103+
96104
rangeMin: {
97105
// Format of min zoom range depends on scale type
98106
x: null,

samples/zoom-separately.html

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<!doctype html>
2+
<html>
3+
4+
<head>
5+
<title>Chart.js Zoom each scale separately</title>
6+
<script src="../node_modules/chart.js/dist/chart.js"></script>
7+
<script src="../node_modules/hammerjs/hammer.min.js"></script>
8+
<script src="../dist/chartjs-plugin-zoom.min.js"></script>
9+
10+
<style>
11+
canvas {
12+
-moz-user-select: none;
13+
-webkit-user-select: none;
14+
-ms-user-select: none;
15+
}
16+
</style>
17+
</head>
18+
19+
<body>
20+
<canvas id="canvas"></canvas>
21+
<script>
22+
function genData() {
23+
var analogData = [];
24+
var digitalData = [];
25+
var angle = 1980;
26+
var limit = 35;
27+
for (var x = 0; x <= angle; x += 10) {
28+
var y = Math.sin(x * Math.PI / 180) * limit;
29+
analogData.push({x, y});
30+
31+
y = x === 0 ? -limit : x === angle ? limit : y;
32+
if (y === limit || y === -limit) {
33+
digitalData.push({x, y: y === -limit});
34+
}
35+
}
36+
37+
return [analogData, digitalData];
38+
}
39+
40+
function createConfig() {
41+
var [analogData, digitalData] = genData();
42+
return {
43+
data: {
44+
datasets: [{
45+
yAxisID: 'yA',
46+
label: 'Temperature',
47+
type: 'line',
48+
fill: false,
49+
borderColor: 'rgb(54, 162, 235)',
50+
data: analogData
51+
}, {
52+
yAxisID: 'yB',
53+
label: 'Heater',
54+
type: 'line',
55+
fill: false,
56+
steppedLine: true,
57+
borderColor: 'rgb(255, 99, 132)',
58+
data: digitalData
59+
}]
60+
},
61+
options: {
62+
responsive: true,
63+
title: {
64+
display: true,
65+
text: 'Chart.js Zoom each scale separately'
66+
},
67+
scales: {
68+
x: {
69+
type: 'linear',
70+
offset: true,
71+
scaleLabel: {
72+
display: true,
73+
labelString: 'x axis'
74+
}
75+
},
76+
yA: {
77+
id: 'A',
78+
offset: true,
79+
position: 'left',
80+
scaleLabel: {
81+
display: true,
82+
labelString: 'Analog'
83+
}
84+
},
85+
yB: {
86+
id: 'B',
87+
position: 'right',
88+
scaleLabel: {
89+
display: true,
90+
labelString: 'Digital'
91+
},
92+
ticks: {
93+
max: 2,
94+
min: -1,
95+
stepSize: 1
96+
}
97+
}
98+
},
99+
plugins: {
100+
zoom: {
101+
pan: {
102+
enabled: true,
103+
mode: 'xy',
104+
overScaleMode: 'y'
105+
},
106+
zoom: {
107+
enabled: true,
108+
mode: 'xy',
109+
overScaleMode: 'y'
110+
}
111+
}
112+
}
113+
}
114+
};
115+
}
116+
117+
window.onload = function() {
118+
var ctx = document.getElementById('canvas').getContext('2d');
119+
var config = createConfig();
120+
window.myChart = new window.Chart(ctx, config);
121+
};
122+
</script>
123+
</body>
124+
125+
</html>

src/plugin.js

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,54 @@ function directionEnabled(mode, dir, chart) {
6464
return false;
6565
}
6666

67+
/** This function use for check what axis now under mouse cursor.
68+
* @param {number} [x] X position
69+
* @param {number} [y] Y position
70+
* @param {import('chart.js').Chart} [chart] instance of the chart in question
71+
*/
72+
function getScaleUnderPoint(x, y, chart) {
73+
var scales = chart.scales;
74+
var scaleIds = Object.keys(scales);
75+
for (var i = 0; i < scaleIds.length; i++) {
76+
var scale = scales[scaleIds[i]];
77+
if (y >= scale.top && y <= scale.bottom && x >= scale.left && x <= scale.right) {
78+
return scale;
79+
}
80+
}
81+
return null;
82+
}
83+
84+
/** This function return only one scale whose position is under mouse cursor and which direction is enabled.
85+
* If under mouse hasn't scale, then return all other scales which 'mode' is diffrent with overScaleMode.
86+
* So 'overScaleMode' works as a limiter to scale the user-selected scale (in 'mode') only when the cursor is under the scale,
87+
* and other directions in 'mode' works as before.
88+
* Example: mode = 'xy', overScaleMode = 'y' -> it's means 'x' - works as before, and 'y' only works for one scale when cursor is under it.
89+
* options.overScaleMode can be a function if user want zoom only one scale of many for example.
90+
* @param {any} [options] pan or zoom options
91+
* @param {number} [x] X position
92+
* @param {number} [y] Y position
93+
* @param {import('chart.js').Chart} [chart] instance of the chart in question
94+
*/
95+
function getEnabledScalesByPoint(options, x, y, chart) {
96+
if (options.enabled && options.overScaleMode) {
97+
var scale = getScaleUnderPoint(x, y, chart);
98+
var mode = typeof options.overScaleMode === 'function' ? options.overScaleMode({chart: chart}, scale) : options.overScaleMode;
99+
100+
if (scale && directionEnabled(mode, scale.axis, chart)) {
101+
return [scale];
102+
}
103+
104+
var enabledScales = [];
105+
each(chart.scales, function(scaleItem) {
106+
if (!directionEnabled(mode, scaleItem.axis, chart)) {
107+
enabledScales.push(scaleItem);
108+
}
109+
});
110+
return enabledScales;
111+
}
112+
return null;
113+
}
114+
67115
function rangeMaxLimiter(zoomPanOptions, newMax) {
68116
if (zoomPanOptions.scaleAxes && zoomPanOptions.rangeMax &&
69117
!isNullOrUndef(zoomPanOptions.rangeMax[zoomPanOptions.scaleAxes])) {
@@ -164,7 +212,9 @@ function doZoom(chart, percentZoomX, percentZoomY, focalPoint, whichAxes, animat
164212
_whichAxes = 'xy';
165213
}
166214

167-
each(chart.scales, function(scale) {
215+
var enabledScales = getEnabledScalesByPoint(zoomOptions, focalPoint.x, focalPoint.y, chart);
216+
217+
each(enabledScales || chart.scales, function(scale) {
168218
if (scale.isHorizontal() && directionEnabled(zoomMode, 'x', chart) && directionEnabled(_whichAxes, 'x', chart)) {
169219
zoomOptions.scaleAxes = 'x';
170220
zoomScale(scale, percentZoomX, focalPoint, zoomOptions);
@@ -245,13 +295,13 @@ function panScale(scale, delta, panOptions) {
245295
}
246296
}
247297

248-
function doPan(chartInstance, deltaX, deltaY) {
298+
function doPan(chartInstance, deltaX, deltaY, panningScales) {
249299
storeOriginalOptions(chartInstance);
250300
var panOptions = chartInstance.$zoom._options.pan;
251301
if (panOptions.enabled) {
252302
var panMode = typeof panOptions.mode === 'function' ? panOptions.mode({chart: chartInstance}) : panOptions.mode;
253303

254-
each(chartInstance.scales, function(scale) {
304+
each(panningScales || chartInstance.scales, function(scale) {
255305
if (scale.isHorizontal() && directionEnabled(panMode, 'x', chartInstance) && deltaX !== 0) {
256306
panOptions.scaleAxes = 'x';
257307
panScale(scale, deltaX, panOptions);
@@ -538,24 +588,33 @@ var zoomPlugin = {
538588
var currentDeltaX = null;
539589
var currentDeltaY = null;
540590
var panning = false;
591+
var panningScales = null;
541592
var handlePan = function(e) {
542593
if (currentDeltaX !== null && currentDeltaY !== null) {
543594
panning = true;
544595
var deltaX = e.deltaX - currentDeltaX;
545596
var deltaY = e.deltaY - currentDeltaY;
546597
currentDeltaX = e.deltaX;
547598
currentDeltaY = e.deltaY;
548-
doPan(chartInstance, deltaX, deltaY);
599+
doPan(chartInstance, deltaX, deltaY, panningScales);
549600
}
550601
};
551602

552603
mc.on('panstart', function(e) {
604+
if (panOptions.enabled) {
605+
var rect = e.target.getBoundingClientRect();
606+
var x = e.center.x - rect.left;
607+
var y = e.center.y - rect.top;
608+
panningScales = getEnabledScalesByPoint(panOptions, x, y, chartInstance);
609+
}
610+
553611
currentDeltaX = 0;
554612
currentDeltaY = 0;
555613
handlePan(e);
556614
});
557615
mc.on('panmove', handlePan);
558616
mc.on('panend', function() {
617+
panningScales = null;
559618
currentDeltaX = null;
560619
currentDeltaY = null;
561620
setTimeout(function() {

test/specs/zoom.spec.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,4 +314,86 @@ describe('zoom', function() {
314314
}
315315
}
316316
});
317+
318+
describe('with overScaleMode = y and mode = xy', function() {
319+
let config = {
320+
type: 'line',
321+
data,
322+
options: {
323+
scales: {
324+
x: {
325+
type: 'linear',
326+
min: 1,
327+
max: 10
328+
},
329+
y: {
330+
type: 'linear'
331+
}
332+
},
333+
plugins: {
334+
zoom: {
335+
zoom: {
336+
enabled: true,
337+
mode: 'xy',
338+
overScaleMode: 'y'
339+
}
340+
}
341+
}
342+
}
343+
};
344+
345+
describe('Wheel under Y scale', function() {
346+
it('should be applied on Y, but not on X scales.', async function() {
347+
const chart = window.acquireChart(config);
348+
349+
const scaleX = chart.scales.x;
350+
const scaleY = chart.scales.y;
351+
352+
const oldMinX = scaleX.options.min;
353+
const oldMaxX = scaleX.options.max;
354+
const oldMinY = scaleY.options.min;
355+
const oldMaxY = scaleY.options.max;
356+
357+
const wheelEv = {
358+
x: scaleY.left + (scaleY.right - scaleY.left) / 2,
359+
y: scaleY.top + (scaleY.bottom - scaleY.top) / 2,
360+
deltaY: 1
361+
};
362+
363+
await jasmine.triggerWheelEvent(chart, wheelEv);
364+
365+
expect(scaleX.options.min).toEqual(oldMinX);
366+
expect(scaleX.options.max).toEqual(oldMaxX);
367+
expect(scaleY.options.min).not.toEqual(oldMinY);
368+
expect(scaleY.options.max).not.toEqual(oldMaxY);
369+
});
370+
});
371+
372+
describe('Wheel not under Y scale', function() {
373+
it('should be applied on X, but not on Y scales.', async function() {
374+
const chart = window.acquireChart(config);
375+
376+
const scaleX = chart.scales.x;
377+
const scaleY = chart.scales.y;
378+
379+
const oldMinX = scaleX.options.min;
380+
const oldMaxX = scaleX.options.max;
381+
const oldMinY = scaleY.options.min;
382+
const oldMaxY = scaleY.options.max;
383+
384+
const wheelEv = {
385+
x: scaleX.getPixelForValue(1.5),
386+
y: scaleY.getPixelForValue(1.1),
387+
deltaY: 1
388+
};
389+
390+
await jasmine.triggerWheelEvent(chart, wheelEv);
391+
392+
expect(scaleX.options.min).not.toEqual(oldMinX);
393+
expect(scaleX.options.max).not.toEqual(oldMaxX);
394+
expect(scaleY.options.min).toEqual(oldMinY);
395+
expect(scaleY.options.max).toEqual(oldMaxY);
396+
});
397+
});
398+
});
317399
});

0 commit comments

Comments
 (0)