1314 lines
40 KiB
JavaScript
1314 lines
40 KiB
JavaScript
/**
|
|
* The Ext.chart package provides the capability to visualize data.
|
|
* Each chart binds directly to an {@link Ext.data.Store} enabling automatic updates of the chart.
|
|
* A chart configuration object has some overall styling options as well as an array of axes
|
|
* and series. A chart instance example could look like:
|
|
*
|
|
* new Ext.chart.CartesianChart({
|
|
* width: 800,
|
|
* height: 600,
|
|
* animate: true,
|
|
* store: store1,
|
|
* legend: {
|
|
* position: 'right'
|
|
* },
|
|
* axes: [
|
|
* // ...some axes options...
|
|
* ],
|
|
* series: [
|
|
* // ...some series options...
|
|
* ]
|
|
* });
|
|
*
|
|
* In this example we set the `width` and `height` of a cartesian chart; We decide whether our series are
|
|
* animated or not and we select a store to be bound to the chart; We also set the legend to the right part of the
|
|
* chart.
|
|
*
|
|
* You can register certain interactions such as {@link Ext.chart.interactions.PanZoom} on the chart by specify an
|
|
* array of names or more specific config objects. All the events will be wired automatically.
|
|
*
|
|
* You can also listen to `itemXXX` events directly on charts. That case all the contained series will relay this event to the
|
|
* chart.
|
|
*
|
|
* For more information about the axes and series configurations please check the documentation of
|
|
* each series (Line, Bar, Pie, etc).
|
|
*
|
|
*/
|
|
|
|
Ext.define('Ext.chart.AbstractChart', {
|
|
|
|
extend: 'Ext.draw.Component',
|
|
|
|
requires: [
|
|
'Ext.chart.series.Series',
|
|
'Ext.chart.interactions.Abstract',
|
|
'Ext.chart.axis.Axis',
|
|
'Ext.data.StoreManager',
|
|
'Ext.chart.Legend',
|
|
'Ext.data.Store',
|
|
'Ext.draw.engine.SvgExporter'
|
|
],
|
|
/**
|
|
* @event beforerefresh
|
|
* Fires before a refresh to the chart data is called. If the `beforerefresh` handler returns
|
|
* `false` the {@link #refresh} action will be canceled.
|
|
* @param {Ext.chart.AbstractChart} this
|
|
*/
|
|
|
|
/**
|
|
* @event refresh
|
|
* Fires after the chart data has been refreshed.
|
|
* @param {Ext.chart.AbstractChart} this
|
|
*/
|
|
|
|
/**
|
|
* @event redraw
|
|
* Fires after the chart is redrawn.
|
|
* @param {Ext.chart.AbstractChart} this
|
|
*/
|
|
|
|
/**
|
|
* @event itemmousemove
|
|
* Fires when the mouse is moved on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemmouseup
|
|
* Fires when a mouseup event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemmousedown
|
|
* Fires when a mousedown event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemmouseover
|
|
* Fires when the mouse enters a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemmouseout
|
|
* Fires when the mouse exits a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemclick
|
|
* Fires when a click event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemdoubleclick
|
|
* Fires when a doubleclick event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemtap
|
|
* Fires when a tap event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemtapstart
|
|
* Fires when a tapstart event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemtapend
|
|
* Fires when a tapend event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemtapcancel
|
|
* Fires when a tapcancel event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemtaphold
|
|
* Fires when a taphold event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemdoubletap
|
|
* Fires when a doubletap event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemsingletap
|
|
* Fires when a singletap event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemtouchstart
|
|
* Fires when a touchstart event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemtouchmove
|
|
* Fires when a touchmove event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemtouchend
|
|
* Fires when a touchend event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemdragstart
|
|
* Fires when a dragstart event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemdrag
|
|
* Fires when a drag event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemdragend
|
|
* Fires when a dragend event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itempinchstart
|
|
* Fires when a pinchstart event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itempinch
|
|
* Fires when a pinch event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itempinchend
|
|
* Fires when a pinchend event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
/**
|
|
* @event itemswipe
|
|
* Fires when a swipe event occurs on a series item.
|
|
* @param {Ext.chart.series.Series} series
|
|
* @param {Object} item
|
|
* @param {Event} event
|
|
*/
|
|
|
|
/**
|
|
* @property version Current Version of Touch Charts
|
|
* @type {String}
|
|
*/
|
|
version: '2.0.0',
|
|
|
|
// @ignore
|
|
viewBox: false,
|
|
|
|
delegationRegex: /^item([a-z]+)$/i,
|
|
|
|
domEvents: /click|focus|blur|paste|input|mousemove|mousedown|mouseup|mouseover|mouseout|keyup|keydown|keypress|submit|pinch|pinchstart|pinchend|touchstart|touchend|rotate|rotatestart|rotateend|drag|dragstart|dragend|tap|doubletap|singletap/,
|
|
|
|
config: {
|
|
|
|
/**
|
|
* @cfg {Ext.data.Store} store
|
|
* The store that supplies data to this chart.
|
|
*/
|
|
store: null,
|
|
|
|
/**
|
|
* @cfg {Boolean/Object} shadow (optional) `true` for the default shadow configuration `{shadowOffsetX: 2, shadowOffsetY: 2, shadowBlur: 3, shadowColor: '#444'}`
|
|
* or a standard shadow config object to be used for default chart shadows.
|
|
*/
|
|
shadow: false,
|
|
|
|
/**
|
|
* @cfg {Boolean/Object} animate (optional) `true` for the default animation (easing: 'ease' and duration: 500)
|
|
* or a standard animation config object to be used for default chart animations.
|
|
*/
|
|
animate: true,
|
|
|
|
/**
|
|
* @cfg {Ext.chart.series.Series/Array} series
|
|
* Array of {@link Ext.chart.series.Series Series} instances or config objects. For example:
|
|
*
|
|
* series: [{
|
|
* type: 'column',
|
|
* axis: 'left',
|
|
* listeners: {
|
|
* 'afterrender': function() {
|
|
* console.log('afterrender');
|
|
* }
|
|
* },
|
|
* xField: 'category',
|
|
* yField: 'data1'
|
|
* }]
|
|
*/
|
|
series: [],
|
|
|
|
/**
|
|
* @cfg {Ext.chart.axis.Axis/Array/Object} axes
|
|
* Array of {@link Ext.chart.axis.Axis Axis} instances or config objects. For example:
|
|
*
|
|
* axes: [{
|
|
* type: 'numeric',
|
|
* position: 'left',
|
|
* title: 'Number of Hits',
|
|
* minimum: 0
|
|
* }, {
|
|
* type: 'category',
|
|
* position: 'bottom',
|
|
* title: 'Month of the Year'
|
|
* }]
|
|
*/
|
|
axes: [],
|
|
|
|
/**
|
|
* @cfg {Ext.chart.Legend/Object} legend
|
|
*/
|
|
legend: null,
|
|
|
|
/**
|
|
* @cfg {Boolean/Array} colors Array of colors/gradients to override the color of items and legends.
|
|
*/
|
|
colors: null,
|
|
|
|
/**
|
|
* @cfg {Object|Number} insetPadding The amount of inset padding in pixels for the chart. Inset padding is
|
|
* the padding from the boundary of the chart to any of its contents.
|
|
* @cfg {Number} insetPadding.top
|
|
*/
|
|
insetPadding: {
|
|
top: 10,
|
|
left: 10,
|
|
right: 10,
|
|
bottom: 10
|
|
},
|
|
|
|
/**
|
|
* @cfg {Object} innerPadding The amount of inner padding in pixel. Inner padding is the padding from
|
|
* axis to the series.
|
|
*/
|
|
innerPadding: {
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0
|
|
},
|
|
|
|
/**
|
|
* @cfg {Object} background Set the chart background. This can be a gradient object, image, or color.
|
|
*
|
|
* For example, if `background` were to be a color we could set the object as
|
|
*
|
|
* background: {
|
|
* //color string
|
|
* fill: '#ccc'
|
|
* }
|
|
*
|
|
* You can specify an image by using:
|
|
*
|
|
* background: {
|
|
* image: 'http://path.to.image/'
|
|
* }
|
|
*
|
|
* Also you can specify a gradient by using the gradient object syntax:
|
|
*
|
|
* background: {
|
|
* gradient: {
|
|
* type: 'linear',
|
|
* angle: 45,
|
|
* stops: {
|
|
* 0: {
|
|
* color: '#555'
|
|
* },
|
|
* 100: {
|
|
* color: '#ddd'
|
|
* }
|
|
* }
|
|
* }
|
|
* }
|
|
*/
|
|
background: null,
|
|
|
|
/**
|
|
* @cfg {Array} interactions
|
|
* Interactions are optional modules that can be plugged in to a chart to allow the user to interact
|
|
* with the chart and its data in special ways. The `interactions` config takes an Array of Object
|
|
* configurations, each one corresponding to a particular interaction class identified by a `type` property:
|
|
*
|
|
* new Ext.chart.AbstractChart({
|
|
* renderTo: Ext.getBody(),
|
|
* width: 800,
|
|
* height: 600,
|
|
* store: store1,
|
|
* axes: [
|
|
* // ...some axes options...
|
|
* ],
|
|
* series: [
|
|
* // ...some series options...
|
|
* ],
|
|
* interactions: [{
|
|
* type: 'interactiontype'
|
|
* // ...additional configs for the interaction...
|
|
* }]
|
|
* });
|
|
*
|
|
* When adding an interaction which uses only its default configuration (no extra properties other than `type`),
|
|
* you can alternately specify only the type as a String rather than the full Object:
|
|
*
|
|
* interactions: ['reset', 'rotate']
|
|
*
|
|
* The current supported interaction types include:
|
|
*
|
|
* - {@link Ext.chart.interactions.PanZoom panzoom} - allows pan and zoom of axes
|
|
* - {@link Ext.chart.interactions.ItemHighlight itemhighlight} - allows highlighting of series data points
|
|
* - {@link Ext.chart.interactions.ItemInfo iteminfo} - allows displaying details of a data point in a popup panel
|
|
* - {@link Ext.chart.interactions.Rotate rotate} - allows rotation of pie and radar series
|
|
*
|
|
* See the documentation for each of those interaction classes to see how they can be configured.
|
|
*
|
|
* Additional custom interactions can be registered using `'interactions.'` alias prefix.
|
|
*/
|
|
interactions: [],
|
|
|
|
/**
|
|
* @private
|
|
* The main region of the chart.
|
|
*/
|
|
mainRegion: null,
|
|
|
|
/**
|
|
* @private
|
|
* Override value
|
|
*/
|
|
autoSize: false,
|
|
|
|
/**
|
|
* @private
|
|
* Override value
|
|
*/
|
|
viewBox: false,
|
|
|
|
/**
|
|
* @private
|
|
* Override value
|
|
*/
|
|
fitSurface: false,
|
|
|
|
/**
|
|
* @private
|
|
* Override value
|
|
*/
|
|
resizeHandler: null,
|
|
|
|
/**
|
|
* @readonly
|
|
* @cfg {Object} highlightItem
|
|
* The current highlight item in the chart.
|
|
* The object must be the one that you get from item events.
|
|
*
|
|
* Note that series can also own highlight items.
|
|
* This notion is separate from this one and should not be used at the same time.
|
|
*/
|
|
highlightItem: null
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
resizing: 0,
|
|
|
|
/**
|
|
* Toggle for chart interactions that require animation to be suspended.
|
|
* @private
|
|
*/
|
|
animationSuspended: 0,
|
|
|
|
/**
|
|
* @private The z-indexes to use for the various surfaces
|
|
*/
|
|
surfaceZIndexes: {
|
|
main: 0,
|
|
grid: 1,
|
|
series: 2,
|
|
axis: 3,
|
|
overlay: 4,
|
|
events: 5
|
|
},
|
|
|
|
animating: 0,
|
|
|
|
layoutSuspended: 0,
|
|
|
|
applyAnimate: function (newAnimate, oldAnimate) {
|
|
if (!newAnimate) {
|
|
newAnimate = {
|
|
duration: 0
|
|
};
|
|
} else if (newAnimate === true) {
|
|
newAnimate = {
|
|
easing: 'easeInOut',
|
|
duration: 500
|
|
};
|
|
}
|
|
if (!oldAnimate) {
|
|
return newAnimate;
|
|
} else {
|
|
oldAnimate = Ext.apply({}, newAnimate, oldAnimate);
|
|
}
|
|
return oldAnimate;
|
|
},
|
|
|
|
applyInsetPadding: function (padding, oldPadding) {
|
|
if (Ext.isNumber(padding)) {
|
|
return {
|
|
top: padding,
|
|
left: padding,
|
|
right: padding,
|
|
bottom: padding
|
|
};
|
|
} else if (!oldPadding) {
|
|
return padding;
|
|
} else {
|
|
return Ext.apply(oldPadding, padding);
|
|
}
|
|
},
|
|
|
|
applyInnerPadding: function (padding, oldPadding) {
|
|
if (Ext.isNumber(padding)) {
|
|
return {
|
|
top: padding,
|
|
left: padding,
|
|
right: padding,
|
|
bottom: padding
|
|
};
|
|
} else if (!oldPadding) {
|
|
return padding;
|
|
} else {
|
|
return Ext.apply(oldPadding, padding);
|
|
}
|
|
},
|
|
|
|
suspendAnimation: function () {
|
|
this.animationSuspended++;
|
|
if (this.animationSuspended === 1) {
|
|
var series = this.getSeries(), i = -1, n = series.length;
|
|
while (++i < n) {
|
|
//update animation config to not animate
|
|
series[i].setAnimate(this.getAnimate());
|
|
}
|
|
}
|
|
},
|
|
|
|
resumeAnimation: function () {
|
|
this.animationSuspended--;
|
|
if (this.animationSuspended === 0) {
|
|
var series = this.getSeries(), i = -1, n = series.length;
|
|
while (++i < n) {
|
|
//update animation config to animate
|
|
series[i].setAnimate(this.getAnimate());
|
|
}
|
|
}
|
|
},
|
|
|
|
suspendLayout: function () {
|
|
this.layoutSuspended++;
|
|
if (this.layoutSuspended === 1) {
|
|
if (this.scheduledLayoutId) {
|
|
this.layoutInSuspension = true;
|
|
this.cancelLayout();
|
|
} else {
|
|
this.layoutInSuspension = false;
|
|
}
|
|
}
|
|
},
|
|
|
|
resumeLayout: function () {
|
|
this.layoutSuspended--;
|
|
if (this.layoutSuspended === 0) {
|
|
if (this.layoutInSuspension) {
|
|
this.scheduleLayout();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Cancel a scheduled layout.
|
|
*/
|
|
cancelLayout: function () {
|
|
if (this.scheduledLayoutId) {
|
|
Ext.draw.Animator.cancel(this.scheduledLayoutId);
|
|
this.scheduledLayoutId = null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Schedule a layout at next frame.
|
|
*/
|
|
scheduleLayout: function () {
|
|
if (!this.scheduledLayoutId) {
|
|
this.scheduledLayoutId = Ext.draw.Animator.schedule('doScheduleLayout', this);
|
|
}
|
|
},
|
|
|
|
doScheduleLayout: function () {
|
|
if (this.layoutSuspended) {
|
|
this.layoutInSuspension = true;
|
|
} else {
|
|
this.performLayout();
|
|
}
|
|
},
|
|
|
|
getAnimate: function () {
|
|
if (this.resizing || this.animationSuspended) {
|
|
return {
|
|
duration: 0
|
|
};
|
|
} else {
|
|
return this._animate;
|
|
}
|
|
},
|
|
|
|
constructor: function () {
|
|
var me = this;
|
|
me.itemListeners = {};
|
|
me.surfaceMap = {};
|
|
me.legendStore = new Ext.data.Store({
|
|
storeId: this.getId() + '-legendStore',
|
|
autoDestroy: true,
|
|
fields: [
|
|
'id', 'name', 'mark', 'disabled', 'series', 'index'
|
|
]
|
|
});
|
|
me.suspendLayout();
|
|
me.callSuper(arguments);
|
|
me.refreshLegendStore();
|
|
me.getLegendStore().on('updaterecord', 'onUpdateLegendStore', me);
|
|
me.resumeLayout();
|
|
},
|
|
|
|
/**
|
|
* Return the legend store that contains all the legend information. These
|
|
* information are collected from all the series.
|
|
* @return {Ext.data.Store}
|
|
*/
|
|
getLegendStore: function () {
|
|
return this.legendStore;
|
|
},
|
|
|
|
refreshLegendStore: function () {
|
|
if (this.getLegendStore()) {
|
|
var i, ln,
|
|
series = this.getSeries(), seriesItem,
|
|
legendData = [];
|
|
if (series) {
|
|
for (i = 0, ln = series.length; i < ln; i++) {
|
|
seriesItem = series[i];
|
|
if (seriesItem.getShowInLegend()) {
|
|
seriesItem.provideLegendInfo(legendData);
|
|
}
|
|
}
|
|
}
|
|
this.getLegendStore().setData(legendData);
|
|
}
|
|
},
|
|
|
|
resetLegendStore: function () {
|
|
if (this.getLegendStore()) {
|
|
var data = this.getLegendStore().getData().items,
|
|
i, ln = data.length,
|
|
record;
|
|
for (i = 0; i < ln; i++) {
|
|
record = data[i];
|
|
record.beginEdit();
|
|
record.set('disabled', false);
|
|
record.commit();
|
|
}
|
|
}
|
|
},
|
|
|
|
onUpdateLegendStore: function (store, record) {
|
|
var series = this.getSeries(), seriesItem;
|
|
if (record && series) {
|
|
seriesItem = series.map[record.get('series')];
|
|
if (seriesItem) {
|
|
seriesItem.setHiddenByIndex(record.get('index'), record.get('disabled'));
|
|
this.redraw();
|
|
}
|
|
}
|
|
},
|
|
|
|
initialize: function () {
|
|
var me = this;
|
|
me.callSuper();
|
|
me.getSurface('main');
|
|
me.getSurface('overlay-surface', 'overlay').waitFor(me.getSurface('series-surface', 'series'));
|
|
},
|
|
|
|
resizeHandler: function (size) {
|
|
var me = this;
|
|
me.scheduleLayout();
|
|
return false;
|
|
},
|
|
|
|
applyMainRegion: function (newRegion, region) {
|
|
if (!region) {
|
|
return newRegion;
|
|
}
|
|
this.getSeries();
|
|
this.getAxes();
|
|
if (newRegion[0] === region[0] &&
|
|
newRegion[1] === region[1] &&
|
|
newRegion[2] === region[2] &&
|
|
newRegion[3] === region[3]) {
|
|
return region;
|
|
} else {
|
|
return newRegion;
|
|
}
|
|
},
|
|
|
|
getSurface: function (name, type) {
|
|
name = name || 'main';
|
|
type = type || name;
|
|
var me = this,
|
|
surface = this.callSuper([name]),
|
|
zIndexes = me.surfaceZIndexes;
|
|
if (type in zIndexes) {
|
|
surface.element.setStyle('zIndex', zIndexes[type]);
|
|
}
|
|
if (!me.surfaceMap[type]) {
|
|
me.surfaceMap[type] = [];
|
|
}
|
|
surface.type = type;
|
|
me.surfaceMap[type].push(surface);
|
|
return surface;
|
|
},
|
|
|
|
updateColors: function (colors) {
|
|
var series = this.getSeries(),
|
|
seriesCount = series && series.length,
|
|
seriesItem;
|
|
for (var i = 0; i < seriesCount; i++) {
|
|
seriesItem = series[i];
|
|
if (!seriesItem.getColors()) {
|
|
seriesItem.updateColors(colors);
|
|
}
|
|
}
|
|
},
|
|
|
|
applyAxes: function (newAxes, oldAxes) {
|
|
this.resizing++;
|
|
try {
|
|
if (!oldAxes) {
|
|
oldAxes = [];
|
|
oldAxes.map = {};
|
|
}
|
|
var result = [], i, ln, axis, oldAxis, oldMap = oldAxes.map;
|
|
result.map = {};
|
|
newAxes = Ext.Array.from(newAxes, true);
|
|
for (i = 0, ln = newAxes.length; i < ln; i++) {
|
|
axis = newAxes[i];
|
|
if (!axis) {
|
|
continue;
|
|
}
|
|
axis = Ext.factory(axis, null, oldAxis = oldMap[axis.getId && axis.getId() || axis.id], 'axis');
|
|
if (axis) {
|
|
axis.setChart(this);
|
|
result.push(axis);
|
|
result.map[axis.getId()] = axis;
|
|
if (!oldAxis) {
|
|
axis.on('animationstart', 'onAnimationStart', this);
|
|
axis.on('animationend', 'onAnimationEnd', this);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i in oldMap) {
|
|
if (!result.map[i]) {
|
|
oldMap[i].destroy();
|
|
}
|
|
}
|
|
return result;
|
|
} finally {
|
|
this.resizing--;
|
|
}
|
|
},
|
|
|
|
updateAxes: function (newAxes) {
|
|
this.scheduleLayout();
|
|
},
|
|
|
|
applySeries: function (newSeries, oldSeries) {
|
|
this.resizing++;
|
|
try {
|
|
this.getAxes();
|
|
if (!oldSeries) {
|
|
oldSeries = [];
|
|
oldSeries.map = {};
|
|
}
|
|
var me = this,
|
|
result = [],
|
|
i, ln, series, oldMap = oldSeries.map, oldSeriesItem;
|
|
result.map = {};
|
|
newSeries = Ext.Array.from(newSeries, true);
|
|
for (i = 0, ln = newSeries.length; i < ln; i++) {
|
|
series = newSeries[i];
|
|
if (!series) {
|
|
continue;
|
|
}
|
|
oldSeriesItem = oldSeries.map[series.getId && series.getId() || series.id];
|
|
if (series instanceof Ext.chart.series.Series) {
|
|
if (oldSeriesItem !== series) {
|
|
// Replacing
|
|
if (oldSeriesItem) {
|
|
oldSeriesItem.destroy();
|
|
}
|
|
me.addItemListenersToSeries(series);
|
|
}
|
|
series.setChart(this);
|
|
} else if (Ext.isObject(series)) {
|
|
if (oldSeriesItem) {
|
|
// Update
|
|
oldSeriesItem.setConfig(series);
|
|
series = oldSeriesItem;
|
|
} else {
|
|
// Create a series.
|
|
if (Ext.isString(series)) {
|
|
series = Ext.create(series.xclass || ("series." + series), {chart: this});
|
|
} else {
|
|
series.chart = this;
|
|
series = Ext.create(series.xclass || ("series." + series.type), series);
|
|
}
|
|
series.on('animationstart', 'onAnimationStart', this);
|
|
series.on('animationend', 'onAnimationEnd', this);
|
|
me.addItemListenersToSeries(series);
|
|
}
|
|
}
|
|
|
|
result.push(series);
|
|
result.map[series.getId()] = series;
|
|
}
|
|
|
|
for (i in oldMap) {
|
|
if (!result.map[oldMap[i].getId()]) {
|
|
oldMap[i].destroy();
|
|
}
|
|
}
|
|
return result;
|
|
} finally {
|
|
this.resizing--;
|
|
}
|
|
},
|
|
|
|
applyLegend: function (newLegend, oldLegend) {
|
|
return Ext.factory(newLegend, Ext.chart.Legend, oldLegend);
|
|
},
|
|
|
|
updateLegend: function (legend) {
|
|
if (legend) {
|
|
// On create
|
|
legend.setStore(this.getLegendStore());
|
|
if (!legend.getDocked()) {
|
|
legend.setDocked('bottom');
|
|
}
|
|
if (this.getParent()) {
|
|
this.getParent().add(legend);
|
|
}
|
|
}
|
|
},
|
|
|
|
setParent: function (parent) {
|
|
this.callSuper(arguments);
|
|
if (parent && this.getLegend()) {
|
|
parent.add(this.getLegend());
|
|
}
|
|
},
|
|
|
|
updateSeries: function (newSeries, oldSeries) {
|
|
this.resizing++;
|
|
try {
|
|
this.fireEvent('serieschanged', this, newSeries, oldSeries);
|
|
this.refreshLegendStore();
|
|
this.scheduleLayout();
|
|
} finally {
|
|
this.resizing--;
|
|
}
|
|
},
|
|
|
|
applyInteractions: function (interactions, oldInteractions) {
|
|
if (!oldInteractions) {
|
|
oldInteractions = [];
|
|
oldInteractions.map = {};
|
|
}
|
|
var me = this,
|
|
result = [], oldMap = oldInteractions.map,
|
|
i, ln, interaction;
|
|
result.map = {};
|
|
interactions = Ext.Array.from(interactions, true);
|
|
for (i = 0, ln = interactions.length; i < ln; i++) {
|
|
interaction = interactions[i];
|
|
if (!interaction) {
|
|
continue;
|
|
}
|
|
interaction = Ext.factory(interaction, null, oldMap[interaction.getId && interaction.getId() || interaction.id], 'interaction');
|
|
if (interaction) {
|
|
interaction.setChart(me);
|
|
result.push(interaction);
|
|
result.map[interaction.getId()] = interaction;
|
|
}
|
|
}
|
|
|
|
for (i in oldMap) {
|
|
if (!result.map[oldMap[i]]) {
|
|
oldMap[i].destroy();
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
applyStore: function (store) {
|
|
return Ext.StoreManager.lookup(store);
|
|
},
|
|
|
|
updateStore: function (newStore, oldStore) {
|
|
var me = this;
|
|
if (oldStore) {
|
|
oldStore.un('refresh', 'onRefresh', me, null, 'after');
|
|
if (oldStore.autoDestroy) {
|
|
oldStore.destroy();
|
|
}
|
|
}
|
|
if (newStore) {
|
|
newStore.on('refresh', 'onRefresh', me, null, 'after');
|
|
}
|
|
|
|
me.fireEvent('storechanged', newStore, oldStore);
|
|
me.onRefresh();
|
|
},
|
|
|
|
/**
|
|
* Redraw the chart. If animations are set this will animate the chart too.
|
|
*/
|
|
redraw: function () {
|
|
this.fireEvent('redraw', this);
|
|
},
|
|
|
|
performLayout: function () {
|
|
this.cancelLayout();
|
|
},
|
|
|
|
getEventXY: function (e) {
|
|
e = (e.changedTouches && e.changedTouches[0]) || e.event || e.browserEvent || e;
|
|
var me = this,
|
|
xy = me.element.getXY(),
|
|
region = me.getMainRegion();
|
|
return [e.pageX - xy[0] - region[0], e.pageY - xy[1] - region[1]];
|
|
},
|
|
|
|
/**
|
|
* Given an x/y point relative to the chart, find and return the first series item that
|
|
* matches that point.
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @return {Object} An object with `series` and `item` properties, or `false` if no item found.
|
|
*/
|
|
getItemForPoint: function (x, y) {
|
|
var me = this,
|
|
i = 0,
|
|
items = me.getSeries(),
|
|
l = items.length,
|
|
series, item;
|
|
|
|
for (; i < l; i++) {
|
|
series = items[i];
|
|
item = series.getItemForPoint(x, y);
|
|
if (item) {
|
|
return item;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Given an x/y point relative to the chart, find and return all series items that match that point.
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @return {Array} An array of objects with `series` and `item` properties.
|
|
*/
|
|
getItemsForPoint: function (x, y) {
|
|
var me = this,
|
|
series = me.getSeries(),
|
|
seriesItem,
|
|
items = [];
|
|
|
|
for (var i = 0; i < series.length; i++) {
|
|
seriesItem = series[i];
|
|
var item = seriesItem.getItemForPoint(x, y);
|
|
if (item) {
|
|
items.push(item);
|
|
}
|
|
}
|
|
|
|
return items;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
delayThicknessChanged: 0,
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
thicknessChanged: false,
|
|
|
|
/**
|
|
* Suspend the layout initialized by thickness change
|
|
*/
|
|
suspendThicknessChanged: function () {
|
|
this.delayThicknessChanged++;
|
|
},
|
|
|
|
/**
|
|
* Resume the layout initialized by thickness change
|
|
*/
|
|
resumeThicknessChanged: function () {
|
|
this.delayThicknessChanged--;
|
|
if (this.delayThicknessChanged === 0 && this.thicknessChanged) {
|
|
this.onThicknessChanged();
|
|
}
|
|
},
|
|
|
|
onAnimationStart: function () {
|
|
this.fireEvent('animationstart', this);
|
|
},
|
|
|
|
onAnimationEnd: function () {
|
|
this.fireEvent('animationend', this);
|
|
},
|
|
|
|
onThicknessChanged: function () {
|
|
if (this.delayThicknessChanged === 0) {
|
|
this.thicknessChanged = false;
|
|
this.performLayout();
|
|
} else {
|
|
this.thicknessChanged = true;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
onRefresh: function () {
|
|
var region = this.getMainRegion(),
|
|
axes = this.getAxes(),
|
|
store = this.getStore(),
|
|
series = this.getSeries();
|
|
if (!store || !axes || !series || !region) {
|
|
return;
|
|
}
|
|
this.redraw();
|
|
},
|
|
|
|
/**
|
|
* Changes the data store bound to this chart and refreshes it.
|
|
* @param {Ext.data.Store} store The store to bind to this chart.
|
|
*/
|
|
bindStore: function (store) {
|
|
this.setStore(store);
|
|
},
|
|
|
|
applyHighlightItem: function (newHighlightItem, oldHighlightItem) {
|
|
if (newHighlightItem === oldHighlightItem) {
|
|
return;
|
|
}
|
|
if (Ext.isObject(newHighlightItem) && Ext.isObject(oldHighlightItem)) {
|
|
if (newHighlightItem.sprite === oldHighlightItem.sprite &&
|
|
newHighlightItem.index === oldHighlightItem.index
|
|
) {
|
|
return;
|
|
}
|
|
}
|
|
return newHighlightItem;
|
|
},
|
|
|
|
updateHighlightItem: function (newHighlightItem, oldHighlightItem) {
|
|
if (oldHighlightItem) {
|
|
oldHighlightItem.series.setAttributesForItem(oldHighlightItem, {highlighted: false});
|
|
}
|
|
if (newHighlightItem) {
|
|
newHighlightItem.series.setAttributesForItem(newHighlightItem, {highlighted: true});
|
|
}
|
|
},
|
|
|
|
addItemListenersToSeries: function (series) {
|
|
for (var name in this.itemListeners) {
|
|
var listenerMap = this.itemListeners[name], i, ln;
|
|
for (i = 0, ln = listenerMap.length; i < ln; i++) {
|
|
series.addListener.apply(series, listenerMap[i]);
|
|
}
|
|
}
|
|
},
|
|
|
|
addItemListener: function (name, fn, scope, options, order) {
|
|
var listenerMap = this.itemListeners[name] || (this.itemListeners[name] = []),
|
|
series = this.getSeries(), seriesItem,
|
|
i, ln;
|
|
listenerMap.push([name, fn, scope, options, order]);
|
|
if (series) {
|
|
for (i = 0, ln = series.length; i < ln; i++) {
|
|
seriesItem = series[i];
|
|
seriesItem.addListener(name, fn, scope, options, order);
|
|
}
|
|
}
|
|
},
|
|
|
|
remoteItemListener: function (name, fn, scope, options, order) {
|
|
var listenerMap = this.itemListeners[name],
|
|
series = this.getSeries(), seriesItem,
|
|
i, ln;
|
|
if (listenerMap) {
|
|
for (i = 0, ln = listenerMap.length; i < ln; i++) {
|
|
if (listenerMap[i].fn === fn) {
|
|
listenerMap.splice(i, 1);
|
|
if (series) {
|
|
for (i = 0, ln = series.length; i < ln; i++) {
|
|
seriesItem = series[i];
|
|
seriesItem.removeListener(name, fn, scope, options, order);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
doAddListener: function (name, fn, scope, options, order) {
|
|
if (name.match(this.delegationRegex)) {
|
|
return this.addItemListener(name, fn, scope || this, options, order);
|
|
} else if (name.match(this.domEvents)) {
|
|
return this.element.doAddListener.apply(this.element, arguments);
|
|
} else {
|
|
return this.callSuper(arguments);
|
|
}
|
|
},
|
|
|
|
doRemoveListener: function (name, fn, scope, options, order) {
|
|
if (name.match(this.delegationRegex)) {
|
|
return this.remoteItemListener(name, fn, scope || this, options, order);
|
|
} else if (name.match(this.domEvents)) {
|
|
return this.element.doRemoveListener.apply(this.element, arguments);
|
|
} else {
|
|
return this.callSuper(arguments);
|
|
}
|
|
},
|
|
|
|
onItemRemove: function (item) {
|
|
this.callSuper(arguments);
|
|
if (this.surfaceMap) {
|
|
Ext.Array.remove(this.surfaceMap[item.type], item);
|
|
if (this.surfaceMap[item.type].length === 0) {
|
|
delete this.surfaceMap[item.type];
|
|
}
|
|
}
|
|
},
|
|
|
|
// @private remove gently.
|
|
destroy: function () {
|
|
var me = this,
|
|
emptyArray = [],
|
|
legend = me.getLegend(),
|
|
legendStore = me.getLegendStore();
|
|
me.surfaceMap = null;
|
|
me.setHighlightItem(null);
|
|
me.setSeries(emptyArray);
|
|
me.setAxes(emptyArray);
|
|
me.setInteractions(emptyArray);
|
|
if (legendStore) {
|
|
legendStore.destroy();
|
|
me.legendStore = null;
|
|
}
|
|
if (legend) {
|
|
legend.destroy();
|
|
me.setLegend(null);
|
|
}
|
|
me.setStore(null);
|
|
Ext.Viewport.un('orientationchange', me.redraw, me);
|
|
me.cancelLayout();
|
|
this.callSuper(arguments);
|
|
},
|
|
|
|
/* ---------------------------------
|
|
Methods needed for ComponentQuery
|
|
----------------------------------*/
|
|
|
|
/**
|
|
* @private
|
|
* @param {Boolean} deep
|
|
* @return {Array}
|
|
*/
|
|
getRefItems: function (deep) {
|
|
var me = this,
|
|
series = me.getSeries(),
|
|
axes = me.getAxes(),
|
|
interaction = me.getInteractions(),
|
|
ans = [], i, ln;
|
|
|
|
for (i = 0, ln = series.length; i < ln; i++) {
|
|
ans.push(series[i]);
|
|
if (series[i].getRefItems) {
|
|
ans.push.apply(ans, series[i].getRefItems(deep));
|
|
}
|
|
}
|
|
|
|
for (i = 0, ln = axes.length; i < ln; i++) {
|
|
ans.push(axes[i]);
|
|
if (axes[i].getRefItems) {
|
|
ans.push.apply(ans, axes[i].getRefItems(deep));
|
|
}
|
|
}
|
|
|
|
for (i = 0, ln = interaction.length; i < ln; i++) {
|
|
ans.push(interaction[i]);
|
|
if (interaction[i].getRefItems) {
|
|
ans.push.apply(ans, interaction[i].getRefItems(deep));
|
|
}
|
|
}
|
|
|
|
return ans;
|
|
},
|
|
|
|
/**
|
|
* Flattens the given chart surfaces into a single image.
|
|
* Note: Surfaces whose class name is different from chart's engine will be omitted.
|
|
* @param {Array} surfaces A list of chart's surfaces to flatten.
|
|
* @param {String} format If set to 'image', the method will return an Image object. Otherwise, the dataURL
|
|
* of the flattened image will be returned.
|
|
* @returns {String|Image} An Image DOM element containing the flattened image or its dataURL.
|
|
*/
|
|
flatten: function (surfaces, format) {
|
|
var me = this,
|
|
size = me.element.getSize(),
|
|
svg, canvas, ctx,
|
|
i, surface, region,
|
|
img, dataURL;
|
|
|
|
switch (me.engine) {
|
|
case 'Ext.draw.engine.Canvas':
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = size.width;
|
|
canvas.height = size.height;
|
|
ctx = canvas.getContext('2d');
|
|
for (i = 0; i < surfaces.length; i++) {
|
|
surface = surfaces[i];
|
|
if (Ext.getClassName(surface) !== me.engine) {
|
|
continue;
|
|
}
|
|
region = surface.getRegion();
|
|
ctx.drawImage(surface.canvases[0].dom, region[0], region[1]);
|
|
}
|
|
dataURL = canvas.toDataURL();
|
|
break;
|
|
case 'Ext.draw.engine.Svg':
|
|
svg = '<?xml version="1.0" standalone="yes"?>';
|
|
svg += '<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg"' +
|
|
' width="' + size.width + '"' +
|
|
' height="' + size.height + '">';
|
|
for (i = 0; i < surfaces.length - 1; i++) {
|
|
surface = surfaces[i];
|
|
if (Ext.getClassName(surface) !== me.engine) {
|
|
continue;
|
|
}
|
|
region = surface.getRegion();
|
|
svg += '<g transform="translate(' + region[0] + ',' + region[1] + ')">';
|
|
svg += Ext.dom.Element.serializeNode(surface.svgElement.dom);
|
|
svg += '</g>';
|
|
}
|
|
svg += '</svg>';
|
|
dataURL = 'data:image/svg+xml;utf8,' + svg;
|
|
break;
|
|
}
|
|
if (format === 'image') {
|
|
img = new Image();
|
|
img.src = dataURL;
|
|
return img;
|
|
}
|
|
if (format === 'stream') {
|
|
return dataURL.replace(/^data:image\/[^;]+/, 'data:application/octet-stream');
|
|
}
|
|
return dataURL;
|
|
},
|
|
|
|
save: function (download) {
|
|
if (download) {
|
|
// TODO: no control over filename, we are at the browser's mercy
|
|
// TODO: unfortunatelly, a.download attribute remains a novelty on mobile: http://caniuse.com/#feat=download
|
|
window.open(this.flatten(this.items.items, 'stream'));
|
|
} else {
|
|
Ext.Viewport.add({
|
|
xtype: 'panel',
|
|
layout: 'fit',
|
|
modal: true,
|
|
width: '90%',
|
|
height: '90%',
|
|
hideOnMaskTap: true,
|
|
centered: true,
|
|
scrollable: false,
|
|
items: {
|
|
xtype: 'image',
|
|
mode: 'img',
|
|
src: this.flatten(this.items.items)
|
|
},
|
|
listeners: {
|
|
hide: function () {
|
|
Ext.Viewport.remove(this);
|
|
}
|
|
}
|
|
}).show();
|
|
}
|
|
}
|
|
});
|