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