]> git.proxmox.com Git - extjs.git/blame - extjs/packages/charts/src/chart/interactions/PanZoom.js
bump version to 7.0.0-4
[extjs.git] / extjs / packages / charts / src / chart / interactions / PanZoom.js
CommitLineData
947f0963
TL
1/**
2 * The PanZoom interaction allows the user to navigate the data for one or more chart
3 * axes by panning and/or zooming. Navigation can be limited to particular axes. Zooming is
4 * performed by pinching on the chart or axis area; panning is performed by single-touch dragging.
5 * The interaction only works with cartesian charts/series.
6 *
7 * For devices which do not support multiple-touch events, zooming can not be done via pinch
8 * gestures; in this case the interaction will allow the user to perform both zooming and panning
9 * using the same single-touch drag gesture.
10 * {@link #modeToggleButton} provides a button to indicate and toggle between two modes.
11 *
12 * @example
13 * Ext.create({
14 * renderTo: document.body,
15 * xtype: 'cartesian',
16 * width: 600,
17 * height: 400,
18 * insetPadding: 40,
19 * interactions: [{
20 * type: 'panzoom',
21 * zoomOnPan: true
22 * }],
23 * store: {
24 * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
25 * data: [{
26 * 'name': 'metric one',
27 * 'data1': 10,
28 * 'data2': 12,
29 * 'data3': 14,
30 * 'data4': 8,
31 * 'data5': 13
32 * }, {
33 * 'name': 'metric two',
34 * 'data1': 7,
35 * 'data2': 8,
36 * 'data3': 16,
37 * 'data4': 10,
38 * 'data5': 3
39 * }, {
40 * 'name': 'metric three',
41 * 'data1': 5,
42 * 'data2': 2,
43 * 'data3': 14,
44 * 'data4': 12,
45 * 'data5': 7
46 * }, {
47 * 'name': 'metric four',
48 * 'data1': 2,
49 * 'data2': 14,
50 * 'data3': 6,
51 * 'data4': 1,
52 * 'data5': 23
53 * }, {
54 * 'name': 'metric five',
55 * 'data1': 27,
56 * 'data2': 38,
57 * 'data3': 36,
58 * 'data4': 13,
59 * 'data5': 33
60 * }]
61 * },
62 * axes: [{
63 * type: 'numeric',
64 * position: 'left',
65 * fields: ['data1'],
66 * title: {
67 * text: 'Sample Values',
68 * fontSize: 15
69 * },
70 * grid: true,
71 * minimum: 0
72 * }, {
73 * type: 'category',
74 * position: 'bottom',
75 * fields: ['name'],
76 * title: {
77 * text: 'Sample Values',
78 * fontSize: 15
79 * }
80 * }],
81 * series: [{
82 * type: 'line',
83 * highlight: {
84 * size: 7,
85 * radius: 7
86 * },
87 * style: {
88 * stroke: 'rgb(143,203,203)'
89 * },
90 * xField: 'name',
91 * yField: 'data1',
92 * marker: {
93 * type: 'path',
94 * path: ['M', - 2, 0, 0, 2, 2, 0, 0, - 2, 'Z'],
95 * stroke: 'blue',
96 * lineWidth: 0
97 * }
98 * }, {
99 * type: 'line',
100 * highlight: {
101 * size: 7,
102 * radius: 7
103 * },
104 * fill: true,
105 * xField: 'name',
106 * yField: 'data3',
107 * marker: {
108 * type: 'circle',
109 * radius: 4,
110 * lineWidth: 0
111 * }
112 * }]
113 * });
114 *
115 * The configuration object for the `panzoom` interaction type should specify which axes
116 * will be made navigable via the `axes` config. See the {@link #axes} config documentation
117 * for details on the allowed formats. If the `axes` config is not specified, it will default
118 * to making all axes navigable with the default axis options.
119 *
120 */
121Ext.define('Ext.chart.interactions.PanZoom', {
122
123 extend: 'Ext.chart.interactions.Abstract',
124
125 type: 'panzoom',
126 alias: 'interaction.panzoom',
127 requires: [
128 'Ext.draw.Animator'
129 ],
130
131 config: {
132
133 /**
134 * @cfg {Object/Array} axes
135 * Specifies which axes should be made navigable. The config value can take the following
136 * formats:
137 *
138 * - An Object with keys corresponding to the {@link Ext.chart.axis.Axis#position position}
139 * of each axis that should be made navigable. Each key's value can either be an Object
140 * with further configuration options for each axis or simply `true` for a default set
141 * of options.
142 *
143 * {
144 * type: 'panzoom',
145 * axes: {
146 * left: {
147 * maxZoom: 5,
148 * allowPan: false
149 * },
150 * bottom: true
151 * }
152 * }
153 *
154 * If using the full Object form, the following options can be specified for each axis:
155 *
156 * - minZoom (Number) A minimum zoom level for the axis. Defaults to `1` which is its
157 * natural size.
158 * - maxZoom (Number) A maximum zoom level for the axis. Defaults to `10`.
159 * - startZoom (Number) A starting zoom level for the axis. Defaults to `1`.
160 * - allowZoom (Boolean) Whether zooming is allowed for the axis. Defaults to `true`.
161 * - allowPan (Boolean) Whether panning is allowed for the axis. Defaults to `true`.
162 * - startPan (Boolean) A starting panning offset for the axis. Defaults to `0`.
163 *
164 * - An Array of strings, each one corresponding to the {@link Ext.chart.axis.Axis#position
165 * position} of an axis that should be made navigable. The default options will be used
166 * for each named axis.
167 *
168 * {
169 * type: 'panzoom',
170 * axes: ['left', 'bottom']
171 * }
172 *
173 * If the `axes` config is not specified, it will default to making all axes navigable
174 * with the default axis options.
175 */
176 axes: {
177 top: {},
178 right: {},
179 bottom: {},
180 left: {}
181 },
182
183 minZoom: null,
184
185 maxZoom: null,
186
187 /**
188 * @cfg {Boolean} showOverflowArrows
189 * If `true`, arrows will be conditionally shown at either end of each axis to indicate that
190 * the axis is overflowing and can therefore be panned in that direction. Set this
191 * to `false` to prevent the arrows from being displayed.
192 */
193 showOverflowArrows: true,
194
195 /**
196 * @cfg {Object} overflowArrowOptions
197 * A set of optional overrides for the overflow arrow sprites' options. Only relevant when
198 * {@link #showOverflowArrows} is `true`.
199 */
200
201 /**
202 * @cfg {String} panGesture
203 * Defines the gesture that initiates panning.
204 * @private
205 */
206 panGesture: 'drag',
207
208 /**
209 * @cfg {String} zoomGesture
210 * Defines the gesture that initiates zooming.
211 * @private
212 */
213 zoomGesture: 'pinch',
214
215 /**
216 * @cfg {Boolean} zoomOnPanGesture
217 * @deprecated 6.2 Please use {@link #zoomOnPan} instead.
218 * If `true`, the pan gesture will zoom the chart.
219 */
220 zoomOnPanGesture: null,
221
222 /**
223 * @cfg {Boolean} zoomOnPan
224 * If `true`, the pan gesture will zoom the chart.
225 */
226 zoomOnPan: false,
227
228 /**
229 * @cfg {Boolean} [doubleTapReset=false]
230 * If `true`, the double tap on a chart will reset the current pan/zoom to show the whole
231 * chart.
232 */
233 doubleTapReset: false,
234
235 modeToggleButton: {
236 xtype: 'segmentedbutton',
237 width: 200,
238 defaults: { ui: 'default-toolbar' },
239 cls: Ext.baseCSSPrefix + 'panzoom-toggle',
240 items: [{
241 text: 'Pan',
242 value: 'pan'
243 }, {
244 text: 'Zoom',
245 value: 'zoom'
246 }]
247 },
248
249 hideLabelInGesture: false // Ext.os.is.Android
250 },
251
252 stopAnimationBeforeSync: true,
253
254 applyAxes: function(axesConfig, oldAxesConfig) {
255 return Ext.merge(oldAxesConfig || {}, axesConfig);
256 },
257
258 updateZoomOnPan: function(zoomOnPan) {
259 var button = this.getModeToggleButton();
260
261 button.setValue(zoomOnPan ? 'zoom' : 'pan');
262 },
263
264 updateZoomOnPanGesture: function(zoomOnPanGesture) {
265 this.setZoomOnPan(zoomOnPanGesture);
266 },
267
268 getZoomOnPanGesture: function() {
269 return this.getZoomOnPan();
270 },
271
272 applyModeToggleButton: function(button, oldButton) {
273 return Ext.factory(button, 'Ext.button.Segmented', oldButton);
274 },
275
276 updateModeToggleButton: function(button) {
277 if (button) {
278 button.on('change', 'onModeToggleChange', this);
279 }
280 },
281
282 onModeToggleChange: function(segmentedButton, value) {
283 this.setZoomOnPan(value === 'zoom');
284 },
285
286 getGestures: function() {
287 var me = this,
288 gestures = {},
289 pan = me.getPanGesture(),
290 zoom = me.getZoomGesture();
291
292 gestures[zoom] = 'onZoomGestureMove';
293 gestures[zoom + 'start'] = 'onZoomGestureStart';
294 gestures[zoom + 'end'] = 'onZoomGestureEnd';
295 gestures[pan] = 'onPanGestureMove';
296 gestures[pan + 'start'] = 'onPanGestureStart';
297 gestures[pan + 'end'] = 'onPanGestureEnd';
298 gestures.doubletap = 'onDoubleTap';
299
300 return gestures;
301 },
302
303 onDoubleTap: function(e) {
304 var me = this,
305 doubleTapReset = me.getDoubleTapReset(),
306 chart, axes, axis, i, ln;
307
308 if (doubleTapReset) {
309 chart = me.getChart();
310 axes = chart.getAxes();
311
312 for (i = 0, ln = axes.length; i < ln; i++) {
313 axis = axes[i];
314 axis.setVisibleRange([0, 1]);
315 }
316
317 chart.redraw();
318 }
319 },
320
321 onPanGestureStart: function(e) {
322 var me = this,
323 chart, rect, xy;
324
325 if (!e || !e.touches || e.touches.length < 2) { // Limit drags to single touch
326 chart = me.getChart();
327 rect = chart.getInnerRect();
328 xy = chart.element.getXY();
329
330 e.claimGesture();
331 chart.suspendAnimation();
332
333 me.startX = e.getX() - xy[0] - rect[0];
334 me.startY = e.getY() - xy[1] - rect[1];
335 me.oldVisibleRanges = null;
336 me.hideLabels();
337 chart.suspendThicknessChanged();
338 me.lockEvents(me.getPanGesture());
339
340 return false;
341 }
342 },
343
344 onPanGestureMove: function(e) {
345 var me = this,
346 isMouse = e.pointerType === 'mouse',
347 isZoomOnPan = isMouse && me.getZoomOnPan(),
348 chart, rect, xy;
349
350 if (me.getLocks()[me.getPanGesture()] === me) { // Limit drags to single touch.
351 chart = me.getChart();
352 rect = chart.getInnerRect();
353 xy = chart.element.getXY();
354
355 if (isZoomOnPan) {
356 me.transformAxesBy(
357 me.getZoomableAxes(e), 0, 0,
358 (e.getX() - xy[0] - rect[0]) / me.startX,
359 me.startY / (e.getY() - xy[1] - rect[1])
360 );
361 }
362 else {
363 me.transformAxesBy(
364 me.getPannableAxes(e),
365 e.getX() - xy[0] - rect[0] - me.startX,
366 e.getY() - xy[1] - rect[1] - me.startY,
367 1, 1);
368 }
369
370 me.sync();
371
372 return false;
373 }
374 },
375
376 onPanGestureEnd: function(e) {
377 var me = this,
378 pan = me.getPanGesture(),
379 chart;
380
381 if (me.getLocks()[pan] === me) {
382 chart = me.getChart();
383 chart.resumeThicknessChanged();
384 me.showLabels();
385 me.sync();
386 me.unlockEvents(pan);
387 chart.resumeAnimation();
388
389 return false;
390 }
391 },
392
393 onZoomGestureStart: function(e) {
394 if (e.touches && e.touches.length === 2) {
395 // eslint-disable-next-line vars-on-top
396 var me = this,
397 chart = me.getChart(),
398 xy = chart.element.getXY(),
399 rect = chart.getInnerRect(),
400 x = xy[0] + rect[0],
401 y = xy[1] + rect[1],
402 newPoints = [
403 e.touches[0].point.x - x, e.touches[0].point.y - y,
404 e.touches[1].point.x - x, e.touches[1].point.y - y
405 ],
406 xDistance = Math.max(44, Math.abs(newPoints[2] - newPoints[0])),
407 yDistance = Math.max(44, Math.abs(newPoints[3] - newPoints[1]));
408
409 e.claimGesture();
410 chart.suspendAnimation();
411 chart.suspendThicknessChanged();
412 me.lastZoomDistances = [xDistance, yDistance];
413 me.lastPoints = newPoints;
414 me.oldVisibleRanges = null;
415 me.hideLabels();
416 me.lockEvents(me.getZoomGesture());
417
418 return false;
419 }
420 },
421
422 onZoomGestureMove: function(e) {
423 var me = this;
424
425 if (me.getLocks()[me.getZoomGesture()] === me) {
426 // eslint-disable-next-line vars-on-top
427 var chart = me.getChart(),
428 rect = chart.getInnerRect(),
429 xy = chart.element.getXY(),
430 x = xy[0] + rect[0],
431 y = xy[1] + rect[1],
432 abs = Math.abs,
433 lastPoints = me.lastPoints,
434 newPoints = [
435 e.touches[0].point.x - x, e.touches[0].point.y - y,
436 e.touches[1].point.x - x, e.touches[1].point.y - y
437 ],
438 xDistance = Math.max(44, abs(newPoints[2] - newPoints[0])),
439 yDistance = Math.max(44, abs(newPoints[3] - newPoints[1])),
440 lastDistances = this.lastZoomDistances || [xDistance, yDistance],
441 zoomX = xDistance / lastDistances[0],
442 zoomY = yDistance / lastDistances[1];
443
444 me.transformAxesBy(me.getZoomableAxes(e),
445 rect[2] * (zoomX - 1) / 2 + newPoints[2] - lastPoints[2] * zoomX,
446 rect[3] * (zoomY - 1) / 2 + newPoints[3] - lastPoints[3] * zoomY,
447 zoomX,
448 zoomY);
449 me.sync();
450
451 return false;
452 }
453 },
454
455 onZoomGestureEnd: function(e) {
456 var me = this,
457 zoom = me.getZoomGesture(),
458 chart;
459
460 if (me.getLocks()[zoom] === me) {
461 chart = me.getChart();
462 chart.resumeThicknessChanged();
463 me.showLabels();
464 me.sync();
465 me.unlockEvents(zoom);
466 chart.resumeAnimation();
467
468 return false;
469 }
470 },
471
472 hideLabels: function() {
473 if (this.getHideLabelInGesture()) {
474 this.eachInteractiveAxes(function(axis) {
475 axis.hideLabels();
476 });
477 }
478 },
479
480 showLabels: function() {
481 if (this.getHideLabelInGesture()) {
482 this.eachInteractiveAxes(function(axis) {
483 axis.showLabels();
484 });
485 }
486 },
487
488 isEventOnAxis: function(e, axis) {
489 // TODO: right now this uses the current event position but really we want to only
490 // use the gesture's start event. Pinch does not give that to us though.
491 var rect = axis.getSurface().getRect();
492
493 return rect[0] <= e.getX() && e.getX() <= rect[0] + rect[2] &&
494 rect[1] <= e.getY() && e.getY() <= rect[1] + rect[3];
495 },
496
497 getPannableAxes: function(e) {
498 var me = this,
499 axisConfigs = me.getAxes(),
500 axes = me.getChart().getAxes(),
501 i,
502 ln = axes.length,
503 result = [],
504 isEventOnAxis = false,
505 config;
506
507 if (e) {
508 for (i = 0; i < ln; i++) {
509 if (this.isEventOnAxis(e, axes[i])) {
510 isEventOnAxis = true;
511 break;
512 }
513 }
514 }
515
516 for (i = 0; i < ln; i++) {
517 config = axisConfigs[axes[i].getPosition()];
518
519 if (config && config.allowPan !== false &&
520 (!isEventOnAxis || this.isEventOnAxis(e, axes[i]))) {
521 result.push(axes[i]);
522 }
523 }
524
525 return result;
526 },
527
528 getZoomableAxes: function(e) {
529 var me = this,
530 axisConfigs = me.getAxes(),
531 axes = me.getChart().getAxes(),
532 result = [],
533 i,
534 ln = axes.length,
535 axis,
536 isEventOnAxis = false,
537 config;
538
539 if (e) {
540 for (i = 0; i < ln; i++) {
541 if (this.isEventOnAxis(e, axes[i])) {
542 isEventOnAxis = true;
543 break;
544 }
545 }
546 }
547
548 for (i = 0; i < ln; i++) {
549 axis = axes[i];
550 config = axisConfigs[axis.getPosition()];
551
552 if (config && config.allowZoom !== false &&
553 (!isEventOnAxis || this.isEventOnAxis(e, axis))) {
554 result.push(axis);
555 }
556 }
557
558 return result;
559 },
560
561 eachInteractiveAxes: function(fn) {
562 var me = this,
563 axisConfigs = me.getAxes(),
564 axes = me.getChart().getAxes(),
565 i;
566
567 for (i = 0; i < axes.length; i++) {
568 if (axisConfigs[axes[i].getPosition()]) {
569 if (false === fn.call(this, axes[i])) {
570 return;
571 }
572 }
573 }
574 },
575
576 transformAxesBy: function(axes, panX, panY, sx, sy) {
577 var rect = this.getChart().getInnerRect(),
578 axesCfg = this.getAxes(),
579 oldVisibleRanges = this.oldVisibleRanges,
580 result = false,
581 axisCfg, i;
582
583 if (!oldVisibleRanges) {
584 this.oldVisibleRanges = oldVisibleRanges = {};
585 this.eachInteractiveAxes(function(axis) {
586 oldVisibleRanges[axis.getId()] = axis.getVisibleRange();
587 });
588 }
589
590 if (!rect) {
591 return;
592 }
593
594 for (i = 0; i < axes.length; i++) {
595 axisCfg = axesCfg[axes[i].getPosition()];
596 result = this.transformAxisBy(
597 axes[i], oldVisibleRanges[axes[i].getId()], panX, panY, sx, sy,
598 this.minZoom || axisCfg.minZoom, this.maxZoom || axisCfg.maxZoom) || result;
599 }
600
601 return result;
602 },
603
604 transformAxisBy: function(axis, oldVisibleRange, panX, panY, sx, sy, minZoom, maxZoom) {
605 var me = this,
606 visibleLength = oldVisibleRange[1] - oldVisibleRange[0],
607 visibleRange = axis.getVisibleRange(),
608 actualMinZoom = minZoom || me.getMinZoom() || axis.config.minZoom,
609 actualMaxZoom = maxZoom || me.getMaxZoom() || axis.config.maxZoom,
610 rect = me.getChart().getInnerRect(),
611 left, right, isSide, pan, length;
612
613 if (!rect) {
614 return;
615 }
616
617 isSide = axis.isSide();
618 length = isSide ? rect[3] : rect[2];
619 pan = isSide ? -panY : panX;
620
621 visibleLength /= isSide ? sy : sx;
622
623 if (visibleLength < 0) {
624 visibleLength = -visibleLength;
625 }
626
627 if (visibleLength * actualMinZoom > 1) {
628 visibleLength = 1;
629 }
630
631 if (visibleLength * actualMaxZoom < 1) {
632 visibleLength = 1 / actualMaxZoom;
633 }
634
635 left = oldVisibleRange[0];
636 right = oldVisibleRange[1];
637
638 visibleRange = visibleRange[1] - visibleRange[0];
639
640 if (visibleLength === visibleRange && visibleRange === 1) {
641 return;
642 }
643
644 axis.setVisibleRange([
645 (oldVisibleRange[0] + oldVisibleRange[1] - visibleLength) * 0.5 -
646 pan / length * visibleLength,
647 (oldVisibleRange[0] + oldVisibleRange[1] + visibleLength) * 0.5 -
648 pan / length * visibleLength
649 ]);
650
651 return Math.abs(left - axis.getVisibleRange()[0]) > 1e-10 ||
652 Math.abs(right - axis.getVisibleRange()[1]) > 1e-10;
653 },
654
655 destroy: function() {
656 this.setModeToggleButton(null);
657 this.callParent();
658 }
659});