/** * @class Ext.chart.interactions.CrossZoom * @extends Ext.chart.interactions.Abstract * * The CrossZoom interaction allows the user to zoom in on a selected area of the chart. * * @example * Ext.create({ * xtype: 'cartesian', * renderTo: Ext.getBody(), * width: 600, * height: 400, * insetPadding: 40, * interactions: 'crosszoom', * store: { * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'], * data: [{ * 'name': 'metric one', * 'data1': 10, * 'data2': 12, * 'data3': 14, * 'data4': 8, * 'data5': 13 * }, { * 'name': 'metric two', * 'data1': 7, * 'data2': 8, * 'data3': 16, * 'data4': 10, * 'data5': 3 * }, { * 'name': 'metric three', * 'data1': 5, * 'data2': 2, * 'data3': 14, * 'data4': 12, * 'data5': 7 * }, { * 'name': 'metric four', * 'data1': 2, * 'data2': 14, * 'data3': 6, * 'data4': 1, * 'data5': 23 * }, { * 'name': 'metric five', * 'data1': 27, * 'data2': 38, * 'data3': 36, * 'data4': 13, * 'data5': 33 * }] * }, * axes: [{ * type: 'numeric', * position: 'left', * fields: ['data1'], * title: { * text: 'Sample Values', * fontSize: 15 * }, * grid: true, * minimum: 0 * }, { * type: 'category', * position: 'bottom', * fields: ['name'], * title: { * text: 'Sample Values', * fontSize: 15 * } * }], * series: [{ * type: 'line', * highlight: { * size: 7, * radius: 7 * }, * style: { * stroke: 'rgb(143,203,203)' * }, * xField: 'name', * yField: 'data1', * marker: { * type: 'path', * path: ['M', - 2, 0, 0, 2, 2, 0, 0, - 2, 'Z'], * stroke: 'blue', * lineWidth: 0 * } * }, { * type: 'line', * highlight: { * size: 7, * radius: 7 * }, * fill: true, * xField: 'name', * yField: 'data3', * marker: { * type: 'circle', * radius: 4, * lineWidth: 0 * } * }] * }); */Ext.define('Ext.chart.interactions.CrossZoom', { extend: 'Ext.chart.interactions.Abstract', type: 'crosszoom', alias: 'interaction.crosszoom', isCrossZoom: true, config: { /** * @cfg {Object/Array} axes * Specifies which axes should be made navigable. The config value can take the following formats: * * - An Object whose keys correspond to the {@link Ext.chart.axis.Axis#position position} of each * axis that should be made navigable. Each key's value can either be an Object with further * configuration options for each axis or simply `true` for a default set of options. * { * type: 'crosszoom', * axes: { * left: { * maxZoom: 5, * allowPan: false * }, * bottom: true * } * } * * If using the full Object form, the following options can be specified for each axis: * * - minZoom (Number) A minimum zoom level for the axis. Defaults to `1` which is its natural size. * - maxZoom (Number) A maximum zoom level for the axis. Defaults to `10`. * - startZoom (Number) A starting zoom level for the axis. Defaults to `1`. * - allowZoom (Boolean) Whether zooming is allowed for the axis. Defaults to `true`. * - allowPan (Boolean) Whether panning is allowed for the axis. Defaults to `true`. * - startPan (Boolean) A starting panning offset for the axis. Defaults to `0`. * * - An Array of strings, each one corresponding to the {@link Ext.chart.axis.Axis#position position} * of an axis that should be made navigable. The default options will be used for each named axis. * * { * type: 'crosszoom', * axes: ['left', 'bottom'] * } * * If the `axes` config is not specified, it will default to making all axes navigable with the * default axis options. */ axes: true, gestures: { dragstart: 'onGestureStart', drag: 'onGesture', dragend: 'onGestureEnd', dblclick: 'onDoubleTap' }, undoButton: {} }, stopAnimationBeforeSync: false, zoomAnimationInProgress: false, constructor: function () { this.callParent(arguments); this.zoomHistory = []; }, applyAxes: function (axesConfig) { var result = {}; if (axesConfig === true) { return { top: {}, right: {}, bottom: {}, left: {} }; } else if (Ext.isArray(axesConfig)) { // array of axis names - translate to full object form result = {}; Ext.each(axesConfig, function (axis) { result[axis] = {}; }); } else if (Ext.isObject(axesConfig)) { Ext.iterate(axesConfig, function (key, val) { // axis name with `true` value -> translate to object if (val === true) { result[key] = {}; } else if (val !== false) { result[key] = val; } }); } return result; }, applyUndoButton: function (button, oldButton) { var me = this; if (oldButton) { oldButton.destroy(); } if (button) { return Ext.create('Ext.Button', Ext.apply({ cls: [], text: 'Undo Zoom', disabled: true, handler: function () { me.undoZoom(); } }, button)); } }, getSurface: function () { return this.getChart() && this.getChart().getSurface('main'); }, setSeriesOpacity: function (opacity) { var surface = this.getChart() && this.getChart().getSurface('series'); if (surface) { surface.element.setStyle('opacity', opacity); } }, onGestureStart: function (e) { var me = this, chart = me.getChart(), surface = me.getSurface(), rect = chart.getInnerRect(), innerPadding = chart.getInnerPadding(), minX = innerPadding.left, maxX = minX + rect[2], minY = innerPadding.top, maxY = minY + rect[3], xy = chart.getEventXY(e), x = xy[0], y = xy[1]; e.claimGesture(); if (me.zoomAnimationInProgress) { return; } if (x > minX && x < maxX && y > minY && y < maxY) { me.gestureEvent = 'drag'; me.lockEvents(me.gestureEvent); me.startX = x; me.startY = y; me.selectionRect = surface.add({ type: 'rect', globalAlpha: 0.5, fillStyle: 'rgba(80,80,140,0.5)', strokeStyle: 'rgba(80,80,140,1)', lineWidth: 2, x: x, y: y, width: 0, height: 0, zIndex: 10000 }); me.setSeriesOpacity(0.8); return false; } }, onGesture: function (e) { var me = this; if (me.zoomAnimationInProgress) { return; } if (me.getLocks()[me.gestureEvent] === me) { var chart = me.getChart(), surface = me.getSurface(), rect = chart.getInnerRect(), innerPadding = chart.getInnerPadding(), minX = innerPadding.left, maxX = minX + rect[2], minY = innerPadding.top, maxY = minY + rect[3], xy = chart.getEventXY(e), x = xy[0], y = xy[1]; if (x < minX) { x = minX; } else if (x > maxX) { x = maxX; } if (y < minY) { y = minY; } else if (y > maxY) { y = maxY; } me.selectionRect.setAttributes({ width: x - me.startX, height: y - me.startY }); if (Math.abs(me.startX - x) < 11 || Math.abs(me.startY - y) < 11) { me.selectionRect.setAttributes({globalAlpha: 0.5}); } else { me.selectionRect.setAttributes({globalAlpha: 1}); } surface.renderFrame(); return false; } }, onGestureEnd: function (e) { var me = this; if (me.zoomAnimationInProgress) { return; } if (me.getLocks()[me.gestureEvent] === me) { var chart = me.getChart(), surface = me.getSurface(), rect = chart.getInnerRect(), innerPadding = chart.getInnerPadding(), minX = innerPadding.left, maxX = minX + rect[2], minY = innerPadding.top, maxY = minY + rect[3], rectWidth = rect[2], rectHeight = rect[3], xy = chart.getEventXY(e), x = xy[0], y = xy[1]; if (x < minX) { x = minX; } else if (x > maxX) { x = maxX; } if (y < minY) { y = minY; } else if (y > maxY) { y = maxY; } if (Math.abs(me.startX - x) < 11 || Math.abs(me.startY - y) < 11) { surface.remove(me.selectionRect); } else { me.zoomBy([ Math.min(me.startX, x) / rectWidth, 1 - Math.max(me.startY, y) / rectHeight, Math.max(me.startX, x) / rectWidth, 1 - Math.min(me.startY, y) / rectHeight ]); me.selectionRect.setAttributes({ x: Math.min(me.startX, x), y: Math.min(me.startY, y), width: Math.abs(me.startX - x), height: Math.abs(me.startY - y) }); me.selectionRect.setAnimation(chart.getAnimation() || {duration: 0}); me.selectionRect.setAttributes({ globalAlpha: 0, x: 0, y: 0, width: rectWidth, height: rectHeight }); me.zoomAnimationInProgress = true; chart.suspendThicknessChanged(); me.selectionRect.fx.on('animationend', function () { chart.resumeThicknessChanged(); surface.remove(me.selectionRect); me.selectionRect = null; me.zoomAnimationInProgress = false; }); } surface.renderFrame(); me.sync(); me.unlockEvents(me.gestureEvent); me.setSeriesOpacity(1.0); if (!me.zoomAnimationInProgress) { surface.remove(me.selectionRect); me.selectionRect = null; } } }, zoomBy: function (rect) { var me = this, axisConfigs = me.getAxes(), chart = me.getChart(), axes = chart.getAxes(), isRtl = chart.getInherited().rtl, config, zoomMap = {}, x1, x2; if (isRtl) { rect = rect.slice(); x1 = 1 - rect[0]; x2 = 1 - rect[2]; rect[0] = Math.min(x1, x2); rect[2] = Math.max(x1, x2); } for (var i = 0; i < axes.length; i++) { var axis = axes[i]; config = axisConfigs[axis.getPosition()]; if (config && config.allowZoom !== false) { var isSide = axis.isSide(), oldRange = axis.getVisibleRange(); zoomMap[axis.getId()] = oldRange.slice(0); if (!isSide) { axis.setVisibleRange([ (oldRange[1] - oldRange[0]) * rect[0] + oldRange[0], (oldRange[1] - oldRange[0]) * rect[2] + oldRange[0] ]); } else { axis.setVisibleRange([ (oldRange[1] - oldRange[0]) * rect[1] + oldRange[0], (oldRange[1] - oldRange[0]) * rect[3] + oldRange[0] ]); } } } me.zoomHistory.push(zoomMap); me.getUndoButton().setDisabled(false); }, undoZoom: function () { var zoomMap = this.zoomHistory.pop(), axes = this.getChart().getAxes(); if (zoomMap) { for (var i = 0; i < axes.length; i++) { var axis = axes[i]; if (zoomMap[axis.getId()]) { axis.setVisibleRange(zoomMap[axis.getId()]); } } } this.getUndoButton().setDisabled(this.zoomHistory.length === 0); this.sync(); }, onDoubleTap: function (e) { this.undoZoom(); }, destroy: function () { this.setUndoButton(null); this.callParent(); } });