outlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplace
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
19855 lines
677 KiB
19855 lines
677 KiB
/** |
|
* @class Ext.chart.Callout |
|
* A mixin providing callout functionality for Ext.chart.series.Series. |
|
*/ |
|
Ext.define('Ext.chart.Callout', { |
|
/* Begin Definitions */ |
|
/* End Definitions */ |
|
constructor: function(config) { |
|
if (config.callouts) { |
|
config.callouts.styles = Ext.applyIf(config.callouts.styles || {}, { |
|
color: "#000", |
|
font: "11px Helvetica, sans-serif" |
|
}); |
|
this.callouts = Ext.apply(this.callouts || {}, config.callouts); |
|
this.calloutsArray = []; |
|
} |
|
}, |
|
renderCallouts: function() { |
|
if (!this.callouts) { |
|
return; |
|
} |
|
var me = this, |
|
items = me.items, |
|
animate = me.chart.animate, |
|
config = me.callouts, |
|
styles = config.styles, |
|
group = me.calloutsArray, |
|
store = me.chart.getChartStore(), |
|
len = store.getCount(), |
|
ratio = items.length / len, |
|
previouslyPlacedCallouts = [], |
|
i, count, j, p, item, label, storeItem, display; |
|
for (i = 0 , count = 0; i < len; i++) { |
|
for (j = 0; j < ratio; j++) { |
|
item = items[count]; |
|
label = group[count]; |
|
storeItem = store.getAt(i); |
|
display = (!config.filter || config.filter(storeItem)); |
|
if (!display && !label) { |
|
count++; |
|
|
|
continue; |
|
} |
|
if (!label) { |
|
group[count] = label = me.onCreateCallout(storeItem, item, i, display, j, count); |
|
} |
|
for (p in label) { |
|
if (label[p] && label[p].setAttributes) { |
|
label[p].setAttributes(styles, true); |
|
} |
|
} |
|
if (!display) { |
|
for (p in label) { |
|
if (label[p]) { |
|
if (label[p].setAttributes) { |
|
label[p].setAttributes({ |
|
hidden: true |
|
}, true); |
|
} else if (label[p].setVisible) { |
|
label[p].setVisible(false); |
|
} |
|
} |
|
} |
|
} |
|
if (config && config.renderer) { |
|
config.renderer(label, storeItem); |
|
} |
|
me.onPlaceCallout(label, storeItem, item, i, display, animate, j, count, previouslyPlacedCallouts); |
|
previouslyPlacedCallouts.push(label); |
|
count++; |
|
} |
|
} |
|
this.hideCallouts(count); |
|
}, |
|
onCreateCallout: function(storeItem, item, i, display) { |
|
var me = this, |
|
group = me.calloutsGroup, |
|
config = me.callouts, |
|
styles = (config ? config.styles : undefined), |
|
width = (styles ? styles.width : 0), |
|
height = (styles ? styles.height : 0), |
|
chart = me.chart, |
|
surface = chart.surface, |
|
calloutObj = { |
|
//label: false, |
|
//box: false, |
|
lines: false |
|
}; |
|
calloutObj.lines = surface.add(Ext.apply({}, { |
|
type: 'path', |
|
path: 'M0,0', |
|
stroke: me.getLegendColor() || '#555' |
|
}, styles)); |
|
if (config.items) { |
|
calloutObj.panel = new Ext.Panel({ |
|
style: "position: absolute;", |
|
width: width, |
|
height: height, |
|
items: config.items, |
|
renderTo: chart.el |
|
}); |
|
} |
|
return calloutObj; |
|
}, |
|
hideCallouts: function(index) { |
|
var calloutsArray = this.calloutsArray, |
|
len = calloutsArray.length, |
|
co, p; |
|
while (len-- > index) { |
|
co = calloutsArray[len]; |
|
for (p in co) { |
|
if (co[p]) { |
|
co[p].hide(true); |
|
} |
|
} |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* A composite Sprite handles a group of sprites with common methods to a sprite |
|
* such as `hide`, `show`, `setAttributes`. These methods are applied to the set of sprites |
|
* added to the group. |
|
* |
|
* CompositeSprite extends {@link Ext.util.MixedCollection} so you can use the same methods |
|
* in `MixedCollection` to iterate through sprites, add and remove elements, etc. |
|
* |
|
* In order to create a CompositeSprite, one has to provide a handle to the surface where it is |
|
* rendered: |
|
* |
|
* var group = Ext.create('Ext.draw.CompositeSprite', { |
|
* surface: drawComponent.surface |
|
* }); |
|
* |
|
* Then just by using `MixedCollection` methods it's possible to add {@link Ext.draw.Sprite}s: |
|
* |
|
* group.add(sprite1); |
|
* group.add(sprite2); |
|
* group.add(sprite3); |
|
* |
|
* And then apply common Sprite methods to them: |
|
* |
|
* group.setAttributes({ |
|
* fill: '#f00' |
|
* }, true); |
|
*/ |
|
Ext.define('Ext.draw.CompositeSprite', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.util.MixedCollection', |
|
mixins: { |
|
animate: 'Ext.util.Animate' |
|
}, |
|
autoDestroy: false, |
|
/* End Definitions */ |
|
isCompositeSprite: true, |
|
/** |
|
* @event |
|
* @inheritdoc Ext.draw.Sprite#mousedown |
|
*/ |
|
/** |
|
* @event |
|
* @inheritdoc Ext.draw.Sprite#mouseup |
|
*/ |
|
/** |
|
* @event |
|
* @inheritdoc Ext.draw.Sprite#mouseover |
|
*/ |
|
/** |
|
* @event |
|
* @inheritdoc Ext.draw.Sprite#mouseout |
|
*/ |
|
/** |
|
* @event |
|
* @inheritdoc Ext.draw.Sprite#click |
|
*/ |
|
constructor: function(config) { |
|
var me = this; |
|
Ext.apply(me, config); |
|
me.id = Ext.id(null, 'ext-sprite-group-'); |
|
me.callParent(); |
|
}, |
|
// @private |
|
onClick: function(e) { |
|
this.fireEvent('click', e); |
|
}, |
|
// @private |
|
onMouseUp: function(e) { |
|
this.fireEvent('mouseup', e); |
|
}, |
|
// @private |
|
onMouseDown: function(e) { |
|
this.fireEvent('mousedown', e); |
|
}, |
|
// @private |
|
onMouseOver: function(e) { |
|
this.fireEvent('mouseover', e); |
|
}, |
|
// @private |
|
onMouseOut: function(e) { |
|
this.fireEvent('mouseout', e); |
|
}, |
|
attachEvents: function(o) { |
|
var me = this; |
|
o.on({ |
|
scope: me, |
|
mousedown: me.onMouseDown, |
|
mouseup: me.onMouseUp, |
|
mouseover: me.onMouseOver, |
|
mouseout: me.onMouseOut, |
|
click: me.onClick |
|
}); |
|
}, |
|
// Inherit docs from MixedCollection |
|
add: function(key, o) { |
|
var result = this.callParent(arguments); |
|
this.attachEvents(result); |
|
return result; |
|
}, |
|
insert: function(index, key, o) { |
|
return this.callParent(arguments); |
|
}, |
|
// Inherit docs from MixedCollection |
|
remove: function(o) { |
|
var me = this; |
|
o.un({ |
|
scope: me, |
|
mousedown: me.onMouseDown, |
|
mouseup: me.onMouseUp, |
|
mouseover: me.onMouseOver, |
|
mouseout: me.onMouseOut, |
|
click: me.onClick |
|
}); |
|
return me.callParent(arguments); |
|
}, |
|
/** |
|
* Returns the group bounding box. |
|
* Behaves like {@link Ext.draw.Sprite#getBBox} method. |
|
* @return {Object} an object with x, y, width, and height properties. |
|
*/ |
|
getBBox: function() { |
|
var i = 0, |
|
sprite, bb, |
|
items = this.items, |
|
len = this.length, |
|
infinity = Infinity, |
|
minX = infinity, |
|
maxHeight = -infinity, |
|
minY = infinity, |
|
maxWidth = -infinity, |
|
maxWidthBBox, maxHeightBBox; |
|
for (; i < len; i++) { |
|
sprite = items[i]; |
|
if (sprite.el && !sprite.bboxExcluded) { |
|
bb = sprite.getBBox(); |
|
minX = Math.min(minX, bb.x); |
|
minY = Math.min(minY, bb.y); |
|
maxHeight = Math.max(maxHeight, bb.height + bb.y); |
|
maxWidth = Math.max(maxWidth, bb.width + bb.x); |
|
} |
|
} |
|
return { |
|
x: minX, |
|
y: minY, |
|
height: maxHeight - minY, |
|
width: maxWidth - minX |
|
}; |
|
}, |
|
/** |
|
* Iterates through all sprites calling `setAttributes` on each one. For more information {@link Ext.draw.Sprite} |
|
* provides a description of the attributes that can be set with this method. |
|
* @param {Object} attrs Attributes to be changed on the sprite. |
|
* @param {Boolean} redraw Flag to immediately draw the change. |
|
* @return {Ext.draw.CompositeSprite} this |
|
*/ |
|
setAttributes: function(attrs, redraw) { |
|
var i = 0, |
|
items = this.items, |
|
len = this.length; |
|
for (; i < len; i++) { |
|
items[i].setAttributes(attrs, redraw); |
|
} |
|
return this; |
|
}, |
|
/** |
|
* Hides all sprites. If `true` is passed then a redraw will be forced for each sprite. |
|
* @param {Boolean} redraw Flag to immediately draw the change. |
|
* @return {Ext.draw.CompositeSprite} this |
|
*/ |
|
hide: function(redraw) { |
|
var i = 0, |
|
items = this.items, |
|
len = this.length; |
|
for (; i < len; i++) { |
|
items[i].hide(redraw); |
|
} |
|
return this; |
|
}, |
|
/** |
|
* Shows all sprites. If `true` is passed then a redraw will be forced for each sprite. |
|
* @param {Boolean} redraw Flag to immediately draw the change. |
|
* @return {Ext.draw.CompositeSprite} this |
|
*/ |
|
show: function(redraw) { |
|
var i = 0, |
|
items = this.items, |
|
len = this.length; |
|
for (; i < len; i++) { |
|
items[i].show(redraw); |
|
} |
|
return this; |
|
}, |
|
/** |
|
* Force redraw of all sprites. |
|
*/ |
|
redraw: function() { |
|
var me = this, |
|
i = 0, |
|
items = me.items, |
|
surface = me.getSurface(), |
|
len = me.length; |
|
if (surface) { |
|
for (; i < len; i++) { |
|
surface.renderItem(items[i]); |
|
} |
|
} |
|
return me; |
|
}, |
|
/** |
|
* Sets style for all sprites. |
|
* @param {String} style CSS Style definition. |
|
*/ |
|
setStyle: function(obj) { |
|
var i = 0, |
|
items = this.items, |
|
len = this.length, |
|
item, el; |
|
for (; i < len; i++) { |
|
item = items[i]; |
|
el = item.el; |
|
if (el) { |
|
el.setStyle(obj); |
|
} |
|
} |
|
}, |
|
/** |
|
* Adds class to all sprites. |
|
* @param {String} cls CSS class name |
|
*/ |
|
addCls: function(obj) { |
|
var i = 0, |
|
items = this.items, |
|
surface = this.getSurface(), |
|
len = this.length; |
|
if (surface) { |
|
for (; i < len; i++) { |
|
surface.addCls(items[i], obj); |
|
} |
|
} |
|
}, |
|
/** |
|
* Removes class from all sprites. |
|
* @param {String} cls CSS class name |
|
*/ |
|
removeCls: function(obj) { |
|
var i = 0, |
|
items = this.items, |
|
surface = this.getSurface(), |
|
len = this.length; |
|
if (surface) { |
|
for (; i < len; i++) { |
|
surface.removeCls(items[i], obj); |
|
} |
|
} |
|
}, |
|
/** |
|
* Grab the surface from the items |
|
* @private |
|
* @return {Ext.draw.Surface} The surface, null if not found |
|
*/ |
|
getSurface: function() { |
|
var first = this.first(); |
|
if (first) { |
|
return first.surface; |
|
} |
|
return null; |
|
}, |
|
/** |
|
* Destroys this CompositeSprite. |
|
*/ |
|
destroy: function() { |
|
var me = this, |
|
surface = me.getSurface(), |
|
destroySprites = me.autoDestroy, |
|
item; |
|
if (surface) { |
|
while (me.getCount() > 0) { |
|
item = me.first(); |
|
me.remove(item); |
|
surface.remove(item, destroySprites); |
|
} |
|
} |
|
me.clearListeners(); |
|
} |
|
}); |
|
|
|
/** |
|
* A Surface is an interface to render methods inside {@link Ext.draw.Component}. |
|
* |
|
* Most of the Surface methods are abstract and they have a concrete implementation |
|
* in {@link Ext.draw.engine.Vml VML} or {@link Ext.draw.engine.Svg SVG} engines. |
|
* |
|
* A Surface contains methods to render {@link Ext.draw.Sprite sprites}, get bounding |
|
* boxes of sprites, add sprites to the canvas, initialize other graphic components, etc. |
|
* |
|
* ## Adding sprites to surface |
|
* |
|
* One of the most used methods for this class is the {@link #add} method, to add Sprites to |
|
* the surface. For example: |
|
* |
|
* drawComponent.surface.add({ |
|
* type: 'circle', |
|
* fill: '#ffc', |
|
* radius: 100, |
|
* x: 100, |
|
* y: 100 |
|
* }); |
|
* |
|
* The configuration object passed in the `add` method is the same as described in the |
|
* {@link Ext.draw.Sprite} class documentation. |
|
* |
|
* Sprites can also be added to surface by setting their surface config at creation time: |
|
* |
|
* var sprite = Ext.create('Ext.draw.Sprite', { |
|
* type: 'circle', |
|
* fill: '#ff0', |
|
* surface: drawComponent.surface, |
|
* radius: 5 |
|
* }); |
|
* |
|
* In order to properly apply properties and render the sprite we have to |
|
* `show` the sprite setting the option `redraw` to `true`: |
|
* |
|
* sprite.show(true); |
|
* |
|
*/ |
|
Ext.define('Ext.draw.Surface', { |
|
/* Begin Definitions */ |
|
mixins: { |
|
observable: 'Ext.util.Observable' |
|
}, |
|
requires: [ |
|
'Ext.draw.CompositeSprite' |
|
], |
|
uses: [ |
|
'Ext.draw.engine.Svg', |
|
'Ext.draw.engine.Vml', |
|
'Ext.draw.engine.SvgExporter', |
|
'Ext.draw.engine.ImageExporter' |
|
], |
|
separatorRe: /[, ]+/, |
|
enginePriority: [ |
|
'Svg', |
|
'Vml' |
|
], |
|
statics: { |
|
/** |
|
* Creates and returns a new concrete Surface instance appropriate for the current environment. |
|
* @param {Object} config Initial configuration for the Surface instance |
|
* @param {String[]} enginePriority (Optional) order of implementations to use; the first one that is |
|
* available in the current environment will be used. Defaults to `['Svg', 'Vml']`. |
|
* @return {Object} The created Surface or false. |
|
* @static |
|
*/ |
|
create: function(config, enginePriority) { |
|
enginePriority = enginePriority || this.prototype.enginePriority; |
|
var i = 0, |
|
len = enginePriority.length; |
|
for (; i < len; i++) { |
|
if (Ext.supports[enginePriority[i]]) { |
|
return Ext.create('Ext.draw.engine.' + enginePriority[i], config); |
|
} |
|
} |
|
return false; |
|
}, |
|
/** |
|
* Exports a {@link Ext.draw.Surface surface} in a different format. |
|
* The surface may be exported to an SVG string, using the |
|
* {@link Ext.draw.engine.SvgExporter}. It may also be exported |
|
* as an image using the {@link Ext.draw.engine.ImageExporter ImageExporter}. |
|
* Note that this requires sending data to a remote server to process |
|
* the SVG into an image, see the {@link Ext.draw.engine.ImageExporter} for |
|
* more details. |
|
* @param {Ext.draw.Surface} surface The surface to export. |
|
* @param {Object} [config] The configuration to be passed to the exporter. |
|
* See the export method for the appropriate exporter for the relevant |
|
* configuration options |
|
* @return {Object} See the return types for the appropriate exporter |
|
* @static |
|
*/ |
|
save: function(surface, config) { |
|
config = config || {}; |
|
var exportTypes = { |
|
'image/png': 'Image', |
|
'image/jpeg': 'Image', |
|
'image/svg+xml': 'Svg' |
|
}, |
|
prefix = exportTypes[config.type] || 'Svg', |
|
exporter = Ext.draw.engine[prefix + 'Exporter']; |
|
return exporter.generate(surface, config); |
|
} |
|
}, |
|
/* End Definitions */ |
|
// @private |
|
availableAttrs: { |
|
blur: 0, |
|
"clip-rect": "0 0 1e9 1e9", |
|
cursor: "default", |
|
cx: 0, |
|
cy: 0, |
|
'dominant-baseline': 'auto', |
|
fill: "none", |
|
"fill-opacity": 1, |
|
font: '10px "Arial"', |
|
"font-family": '"Arial"', |
|
"font-size": "10", |
|
"font-style": "normal", |
|
"font-weight": 400, |
|
gradient: "", |
|
height: 0, |
|
hidden: false, |
|
href: "http://sencha.com/", |
|
opacity: 1, |
|
path: "M0,0", |
|
radius: 0, |
|
rx: 0, |
|
ry: 0, |
|
scale: "1 1", |
|
src: "", |
|
stroke: "none", |
|
"stroke-dasharray": "", |
|
"stroke-linecap": "butt", |
|
"stroke-linejoin": "butt", |
|
"stroke-miterlimit": 0, |
|
"stroke-opacity": 1, |
|
"stroke-width": 1, |
|
target: "_blank", |
|
text: "", |
|
"text-anchor": "middle", |
|
title: "Ext Draw", |
|
width: 0, |
|
x: 0, |
|
y: 0, |
|
zIndex: 0 |
|
}, |
|
/** |
|
* @cfg {Number} height |
|
* The height of this component in pixels (defaults to auto). |
|
*/ |
|
/** |
|
* @cfg {Number} width |
|
* The width of this component in pixels (defaults to auto). |
|
*/ |
|
container: undefined, |
|
height: 352, |
|
width: 512, |
|
x: 0, |
|
y: 0, |
|
/** |
|
* @cfg {Ext.draw.Sprite[]} items |
|
* Array of sprites or sprite config objects to add initially to the surface. |
|
*/ |
|
/** |
|
* @private Flag indicating that the surface implementation requires sprites to be maintained |
|
* in order of their zIndex. Impls that don't require this can set it to false. |
|
*/ |
|
orderSpritesByZIndex: true, |
|
/** |
|
* @event |
|
* Fires when a mousedown is detected within the surface. |
|
* @param {Ext.EventObject} e An object encapsulating the DOM event. |
|
*/ |
|
/** |
|
* @event |
|
* Fires when a mouseup is detected within the surface. |
|
* @param {Ext.EventObject} e An object encapsulating the DOM event. |
|
*/ |
|
/** |
|
* @event |
|
* Fires when a mouseover is detected within the surface. |
|
* @param {Ext.EventObject} e An object encapsulating the DOM event. |
|
*/ |
|
/** |
|
* @event |
|
* Fires when a mouseout is detected within the surface. |
|
* @param {Ext.EventObject} e An object encapsulating the DOM event. |
|
*/ |
|
/** |
|
* @event |
|
* Fires when a mousemove is detected within the surface. |
|
* @param {Ext.EventObject} e An object encapsulating the DOM event. |
|
*/ |
|
/** |
|
* @event |
|
* Fires when a mouseenter is detected within the surface. |
|
* @param {Ext.EventObject} e An object encapsulating the DOM event. |
|
*/ |
|
/** |
|
* @event |
|
* Fires when a mouseleave is detected within the surface. |
|
* @param {Ext.EventObject} e An object encapsulating the DOM event. |
|
*/ |
|
/** |
|
* @event |
|
* Fires when a click is detected within the surface. |
|
* @param {Ext.EventObject} e An object encapsulating the DOM event. |
|
*/ |
|
/** |
|
* @event |
|
* Fires when a dblclick is detected within the surface. |
|
* @param {Ext.EventObject} e An object encapsulating the DOM event. |
|
*/ |
|
/** |
|
* Creates new Surface. |
|
* @param {Object} config (optional) Config object. |
|
*/ |
|
constructor: function(config) { |
|
var me = this; |
|
config = config || {}; |
|
Ext.apply(me, config); |
|
me.domRef = Ext.getDoc().dom; |
|
me.customAttributes = {}; |
|
me.mixins.observable.constructor.call(me); |
|
me.getId(); |
|
me.initGradients(); |
|
me.initItems(); |
|
if (me.renderTo) { |
|
me.render(me.renderTo); |
|
delete me.renderTo; |
|
} |
|
me.initBackground(config.background); |
|
}, |
|
// @private called to initialize components in the surface |
|
// this is dependent on the underlying implementation. |
|
initSurface: Ext.emptyFn, |
|
// @private called to setup the surface to render an item |
|
//this is dependent on the underlying implementation. |
|
renderItem: Ext.emptyFn, |
|
// @private |
|
renderItems: Ext.emptyFn, |
|
// @private |
|
setViewBox: function(x, y, width, height) { |
|
if (isFinite(x) && isFinite(y) && isFinite(width) && isFinite(height)) { |
|
this.viewBox = { |
|
x: x, |
|
y: y, |
|
width: width, |
|
height: height |
|
}; |
|
this.applyViewBox(); |
|
} |
|
}, |
|
/** |
|
* Adds one or more CSS classes to the element. Duplicate classes are automatically filtered out. |
|
* |
|
* For example: |
|
* |
|
* drawComponent.surface.addCls(sprite, 'x-visible'); |
|
* |
|
* @param {Object} sprite The sprite to add the class to. |
|
* @param {String/String[]} className The CSS class to add, or an array of classes |
|
* @method |
|
*/ |
|
addCls: Ext.emptyFn, |
|
/** |
|
* Removes one or more CSS classes from the element. |
|
* |
|
* For example: |
|
* |
|
* drawComponent.surface.removeCls(sprite, 'x-visible'); |
|
* |
|
* @param {Object} sprite The sprite to remove the class from. |
|
* @param {String/String[]} className The CSS class to remove, or an array of classes |
|
* @method |
|
*/ |
|
removeCls: Ext.emptyFn, |
|
/** |
|
* Sets CSS style attributes to an element. |
|
* |
|
* For example: |
|
* |
|
* drawComponent.surface.setStyle(sprite, { |
|
* 'cursor': 'pointer' |
|
* }); |
|
* |
|
* @param {Object} sprite The sprite to add, or an array of classes to |
|
* @param {Object} styles An Object with CSS styles. |
|
* @method |
|
*/ |
|
setStyle: Ext.emptyFn, |
|
// @private |
|
initGradients: function() { |
|
if (this.hasOwnProperty('gradients')) { |
|
var gradients = this.gradients, |
|
fn = this.addGradient, |
|
g, gLen; |
|
if (gradients) { |
|
for (g = 0 , gLen = gradients.length; g < gLen; g++) { |
|
if (fn.call(this, gradients[g], g, gLen) === false) { |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
}, |
|
// @private |
|
initItems: function() { |
|
var items = this.items; |
|
this.items = new Ext.draw.CompositeSprite(); |
|
this.items.autoDestroy = true; |
|
this.groups = new Ext.draw.CompositeSprite(); |
|
if (items) { |
|
this.add(items); |
|
} |
|
}, |
|
// @private |
|
initBackground: function(config) { |
|
var me = this, |
|
width = me.width, |
|
height = me.height, |
|
gradientId, gradient; |
|
if (Ext.isString(config)) { |
|
config = { |
|
fill: config |
|
}; |
|
} |
|
if (config) { |
|
if (config.gradient) { |
|
gradient = config.gradient; |
|
gradientId = gradient.id; |
|
me.addGradient(gradient); |
|
me.background = me.add({ |
|
type: 'rect', |
|
isBackground: true, |
|
x: 0, |
|
y: 0, |
|
width: width, |
|
height: height, |
|
fill: 'url(#' + gradientId + ')', |
|
zIndex: -1 |
|
}); |
|
} else if (config.fill) { |
|
me.background = me.add({ |
|
type: 'rect', |
|
isBackground: true, |
|
x: 0, |
|
y: 0, |
|
width: width, |
|
height: height, |
|
fill: config.fill, |
|
zIndex: -1 |
|
}); |
|
} else if (config.image) { |
|
me.background = me.add({ |
|
type: 'image', |
|
isBackground: true, |
|
x: 0, |
|
y: 0, |
|
width: width, |
|
height: height, |
|
src: config.image, |
|
zIndex: -1 |
|
}); |
|
} |
|
// prevent me.background to jeopardize me.items.getBBox |
|
me.background.bboxExcluded = true; |
|
} |
|
}, |
|
/** |
|
* Sets the size of the surface. Accomodates the background (if any) to fit the new size too. |
|
* |
|
* For example: |
|
* |
|
* drawComponent.surface.setSize(500, 500); |
|
* |
|
* This method is generally called when also setting the size of the draw Component. |
|
* |
|
* @param {Number} w The new width of the canvas. |
|
* @param {Number} h The new height of the canvas. |
|
*/ |
|
setSize: function(w, h) { |
|
this.applyViewBox(); |
|
}, |
|
// @private |
|
scrubAttrs: function(sprite) { |
|
var i, |
|
attrs = {}, |
|
exclude = {}, |
|
sattr = sprite.attr; |
|
for (i in sattr) { |
|
// Narrow down attributes to the main set |
|
if (this.translateAttrs.hasOwnProperty(i)) { |
|
// Translated attr |
|
attrs[this.translateAttrs[i]] = sattr[i]; |
|
exclude[this.translateAttrs[i]] = true; |
|
} else if (this.availableAttrs.hasOwnProperty(i) && !exclude[i]) { |
|
// Passtrhough attr |
|
attrs[i] = sattr[i]; |
|
} |
|
} |
|
return attrs; |
|
}, |
|
// @private |
|
onClick: function(e) { |
|
this.processEvent('click', e); |
|
}, |
|
// @private |
|
onDblClick: function(e) { |
|
this.processEvent('dblclick', e); |
|
}, |
|
// @private |
|
onMouseUp: function(e) { |
|
this.processEvent('mouseup', e); |
|
}, |
|
// @private |
|
onMouseDown: function(e) { |
|
this.processEvent('mousedown', e); |
|
}, |
|
// @private |
|
onMouseOver: function(e) { |
|
this.processEvent('mouseover', e); |
|
}, |
|
// @private |
|
onMouseOut: function(e) { |
|
this.processEvent('mouseout', e); |
|
}, |
|
// @private |
|
onMouseMove: function(e) { |
|
this.fireEvent('mousemove', e); |
|
}, |
|
// @private |
|
onMouseEnter: Ext.emptyFn, |
|
// @private |
|
onMouseLeave: Ext.emptyFn, |
|
/** |
|
* Adds a gradient definition to the Surface. Note that in some surface engines, adding |
|
* a gradient via this method will not take effect if the surface has already been rendered. |
|
* Therefore, it is preferred to pass the gradients as an item to the surface config, rather |
|
* than calling this method, especially if the surface is rendered immediately (e.g. due to |
|
* 'renderTo' in its config). For more information on how to create gradients in the Chart |
|
* configuration object please refer to {@link Ext.chart.Chart}. |
|
* |
|
* The gradient object to be passed into this method is composed by: |
|
* |
|
* - **id** - string - The unique name of the gradient. |
|
* - **angle** - number, optional - The angle of the gradient in degrees. |
|
* - **stops** - object - An object with numbers as keys (from 0 to 100) and style objects as values. |
|
* |
|
* For example: |
|
* |
|
* drawComponent.surface.addGradient({ |
|
* id: 'gradientId', |
|
* angle: 45, |
|
* stops: { |
|
* 0: { |
|
* color: '#555' |
|
* }, |
|
* 100: { |
|
* color: '#ddd' |
|
* } |
|
* } |
|
* }); |
|
* |
|
* @param {Object} gradient A gradient config. |
|
* @method |
|
*/ |
|
addGradient: Ext.emptyFn, |
|
/** |
|
* Adds a Sprite to the surface. See {@link Ext.draw.Sprite} for the configuration object to be |
|
* passed into this method. |
|
* |
|
* For example: |
|
* |
|
* drawComponent.surface.add({ |
|
* type: 'circle', |
|
* fill: '#ffc', |
|
* radius: 100, |
|
* x: 100, |
|
* y: 100 |
|
* }); |
|
* |
|
* @param {Ext.draw.Sprite[]/Ext.draw.Sprite...} args One or more Sprite objects or configs. |
|
* @return {Ext.draw.Sprite[]/Ext.draw.Sprite} The sprites added. |
|
*/ |
|
add: function() { |
|
var args = Array.prototype.slice.call(arguments), |
|
sprite, |
|
hasMultipleArgs = args.length > 1, |
|
items, results, i, ln, item; |
|
if (hasMultipleArgs || Ext.isArray(args[0])) { |
|
items = hasMultipleArgs ? args : args[0]; |
|
results = []; |
|
for (i = 0 , ln = items.length; i < ln; i++) { |
|
item = items[i]; |
|
item = this.add(item); |
|
results.push(item); |
|
} |
|
return results; |
|
} |
|
sprite = this.prepareItems(args[0], true)[0]; |
|
this.insertByZIndex(sprite); |
|
this.onAdd(sprite); |
|
return sprite; |
|
}, |
|
/** |
|
* @private |
|
* Inserts a given sprite into the correct position in the items collection, according to |
|
* its zIndex. It will be inserted at the end of an existing series of sprites with the same or |
|
* lower zIndex. By ensuring sprites are always ordered, this allows surface subclasses to render |
|
* the sprites in the correct order for proper z-index stacking. |
|
* @param {Ext.draw.Sprite} sprite |
|
* @return {Number} the sprite's new index in the list |
|
*/ |
|
insertByZIndex: function(sprite) { |
|
var me = this, |
|
sprites = me.items.items, |
|
len = sprites.length, |
|
ceil = Math.ceil, |
|
zIndex = sprite.attr.zIndex, |
|
idx = len, |
|
high = idx - 1, |
|
low = 0, |
|
otherZIndex; |
|
if (me.orderSpritesByZIndex && len && zIndex < sprites[high].attr.zIndex) { |
|
// Find the target index via a binary search for speed |
|
while (low <= high) { |
|
idx = ceil((low + high) / 2); |
|
otherZIndex = sprites[idx].attr.zIndex; |
|
if (otherZIndex > zIndex) { |
|
high = idx - 1; |
|
} else if (otherZIndex < zIndex) { |
|
low = idx + 1; |
|
} else { |
|
break; |
|
} |
|
} |
|
// Step forward to the end of a sequence of the same or lower z-index |
|
while (idx < len && sprites[idx].attr.zIndex <= zIndex) { |
|
idx++; |
|
} |
|
} |
|
me.items.insert(idx, sprite); |
|
return idx; |
|
}, |
|
onAdd: function(sprite) { |
|
var group = sprite.group, |
|
draggable = sprite.draggable, |
|
groups, ln, i; |
|
if (group) { |
|
groups = [].concat(group); |
|
ln = groups.length; |
|
for (i = 0; i < ln; i++) { |
|
group = groups[i]; |
|
this.getGroup(group).add(sprite); |
|
} |
|
delete sprite.group; |
|
} |
|
if (draggable) { |
|
sprite.initDraggable(); |
|
} |
|
}, |
|
/** |
|
* Removes a given sprite from the surface, optionally destroying the sprite in the process. |
|
* You can also call the sprite own `remove` method. |
|
* |
|
* For example: |
|
* |
|
* drawComponent.surface.remove(sprite); |
|
* //or... |
|
* sprite.remove(); |
|
* |
|
* @param {Ext.draw.Sprite} sprite |
|
* @param {Boolean} destroySprite |
|
*/ |
|
remove: function(sprite, destroySprite) { |
|
if (sprite) { |
|
this.items.remove(sprite); |
|
var groups = [].concat(this.groups.items), |
|
gLen = groups.length, |
|
g; |
|
for (g = 0; g < gLen; g++) { |
|
groups[g].remove(sprite); |
|
} |
|
sprite.onRemove(); |
|
if (destroySprite === true) { |
|
sprite.destroy(); |
|
} |
|
} |
|
}, |
|
/** |
|
* Removes all sprites from the surface, optionally destroying the sprites in the process. |
|
* |
|
* For example: |
|
* |
|
* drawComponent.surface.removeAll(); |
|
* |
|
* @param {Boolean} destroySprites Whether to destroy all sprites when removing them. |
|
*/ |
|
removeAll: function(destroySprites) { |
|
var items = this.items.items, |
|
ln = items.length, |
|
i; |
|
for (i = ln - 1; i > -1; i--) { |
|
this.remove(items[i], destroySprites); |
|
} |
|
}, |
|
onRemove: Ext.emptyFn, |
|
onDestroy: Ext.emptyFn, |
|
/** |
|
* @private Using the current viewBox property and the surface's width and height, calculate the |
|
* appropriate viewBoxShift that will be applied as a persistent transform to all sprites. |
|
*/ |
|
applyViewBox: function() { |
|
var me = this, |
|
viewBox = me.viewBox, |
|
width = me.width || 1, |
|
// Avoid problems in division |
|
height = me.height || 1, |
|
viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight, relativeHeight, relativeWidth, size; |
|
if (viewBox && (width || height)) { |
|
viewBoxX = viewBox.x; |
|
viewBoxY = viewBox.y; |
|
viewBoxWidth = viewBox.width; |
|
viewBoxHeight = viewBox.height; |
|
relativeHeight = height / viewBoxHeight; |
|
relativeWidth = width / viewBoxWidth; |
|
size = Math.min(relativeWidth, relativeHeight); |
|
if (viewBoxWidth * size < width) { |
|
viewBoxX -= (width - viewBoxWidth * size) / 2 / size; |
|
} |
|
if (viewBoxHeight * size < height) { |
|
viewBoxY -= (height - viewBoxHeight * size) / 2 / size; |
|
} |
|
me.viewBoxShift = { |
|
dx: -viewBoxX, |
|
dy: -viewBoxY, |
|
scale: size |
|
}; |
|
if (me.background) { |
|
me.background.setAttributes(Ext.apply({}, { |
|
x: viewBoxX, |
|
y: viewBoxY, |
|
width: width / size, |
|
height: height / size |
|
}, { |
|
hidden: false |
|
}), true); |
|
} |
|
} else { |
|
if (me.background && width && height) { |
|
me.background.setAttributes(Ext.apply({ |
|
x: 0, |
|
y: 0, |
|
width: width, |
|
height: height |
|
}, { |
|
hidden: false |
|
}), true); |
|
} |
|
} |
|
}, |
|
getBBox: function(sprite, isWithoutTransform) { |
|
var realPath = this["getPath" + sprite.type](sprite); |
|
if (isWithoutTransform) { |
|
sprite.bbox.plain = sprite.bbox.plain || Ext.draw.Draw.pathDimensions(realPath); |
|
return sprite.bbox.plain; |
|
} |
|
if (sprite.dirtyTransform) { |
|
this.applyTransformations(sprite, true); |
|
} |
|
sprite.bbox.transform = sprite.bbox.transform || Ext.draw.Draw.pathDimensions(Ext.draw.Draw.mapPath(realPath, sprite.matrix)); |
|
return sprite.bbox.transform; |
|
}, |
|
transformToViewBox: function(x, y) { |
|
if (this.viewBoxShift) { |
|
var me = this, |
|
shift = me.viewBoxShift; |
|
return [ |
|
x / shift.scale - shift.dx, |
|
y / shift.scale - shift.dy |
|
]; |
|
} else { |
|
return [ |
|
x, |
|
y |
|
]; |
|
} |
|
}, |
|
// @private |
|
applyTransformations: function(sprite, onlyMatrix) { |
|
if (sprite.type == 'text') { |
|
// TODO: getTextBBox function always take matrix into account no matter whether `isWithoutTransform` is true. Fix that. |
|
sprite.bbox.transform = 0; |
|
this.transform(sprite, false); |
|
} |
|
sprite.dirtyTransform = false; |
|
var me = this, |
|
attr = sprite.attr; |
|
if (attr.translation.x != null || attr.translation.y != null) { |
|
me.translate(sprite); |
|
} |
|
if (attr.scaling.x != null || attr.scaling.y != null) { |
|
me.scale(sprite); |
|
} |
|
if (attr.rotation.degrees != null) { |
|
me.rotate(sprite); |
|
} |
|
sprite.bbox.transform = 0; |
|
this.transform(sprite, onlyMatrix); |
|
sprite.transformations = []; |
|
}, |
|
// @private |
|
rotate: function(sprite) { |
|
var bbox, |
|
deg = sprite.attr.rotation.degrees, |
|
centerX = sprite.attr.rotation.x, |
|
centerY = sprite.attr.rotation.y; |
|
if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) { |
|
bbox = this.getBBox(sprite, true); |
|
centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX; |
|
centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY; |
|
} |
|
sprite.transformations.push({ |
|
type: "rotate", |
|
degrees: deg, |
|
x: centerX, |
|
y: centerY |
|
}); |
|
}, |
|
// @private |
|
translate: function(sprite) { |
|
var x = sprite.attr.translation.x || 0, |
|
y = sprite.attr.translation.y || 0; |
|
sprite.transformations.push({ |
|
type: "translate", |
|
x: x, |
|
y: y |
|
}); |
|
}, |
|
// @private |
|
scale: function(sprite) { |
|
var bbox, |
|
x = sprite.attr.scaling.x || 1, |
|
y = sprite.attr.scaling.y || 1, |
|
centerX = sprite.attr.scaling.centerX, |
|
centerY = sprite.attr.scaling.centerY; |
|
if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) { |
|
bbox = this.getBBox(sprite, true); |
|
centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX; |
|
centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY; |
|
} |
|
sprite.transformations.push({ |
|
type: "scale", |
|
x: x, |
|
y: y, |
|
centerX: centerX, |
|
centerY: centerY |
|
}); |
|
}, |
|
// @private |
|
rectPath: function(x, y, w, h, r) { |
|
if (r) { |
|
return [ |
|
[ |
|
"M", |
|
x + r, |
|
y |
|
], |
|
[ |
|
"l", |
|
w - r * 2, |
|
0 |
|
], |
|
[ |
|
"a", |
|
r, |
|
r, |
|
0, |
|
0, |
|
1, |
|
r, |
|
r |
|
], |
|
[ |
|
"l", |
|
0, |
|
h - r * 2 |
|
], |
|
[ |
|
"a", |
|
r, |
|
r, |
|
0, |
|
0, |
|
1, |
|
-r, |
|
r |
|
], |
|
[ |
|
"l", |
|
r * 2 - w, |
|
0 |
|
], |
|
[ |
|
"a", |
|
r, |
|
r, |
|
0, |
|
0, |
|
1, |
|
-r, |
|
-r |
|
], |
|
[ |
|
"l", |
|
0, |
|
r * 2 - h |
|
], |
|
[ |
|
"a", |
|
r, |
|
r, |
|
0, |
|
0, |
|
1, |
|
r, |
|
-r |
|
], |
|
[ |
|
"z" |
|
] |
|
]; |
|
} |
|
return [ |
|
[ |
|
"M", |
|
x, |
|
y |
|
], |
|
[ |
|
"l", |
|
w, |
|
0 |
|
], |
|
[ |
|
"l", |
|
0, |
|
h |
|
], |
|
[ |
|
"l", |
|
-w, |
|
0 |
|
], |
|
[ |
|
"z" |
|
] |
|
]; |
|
}, |
|
// @private |
|
ellipsePath: function(x, y, rx, ry) { |
|
if (ry == null) { |
|
ry = rx; |
|
} |
|
return [ |
|
[ |
|
"M", |
|
x, |
|
y |
|
], |
|
[ |
|
"m", |
|
0, |
|
-ry |
|
], |
|
[ |
|
"a", |
|
rx, |
|
ry, |
|
0, |
|
1, |
|
1, |
|
0, |
|
2 * ry |
|
], |
|
[ |
|
"a", |
|
rx, |
|
ry, |
|
0, |
|
1, |
|
1, |
|
0, |
|
-2 * ry |
|
], |
|
[ |
|
"z" |
|
] |
|
]; |
|
}, |
|
// @private |
|
getPathpath: function(el) { |
|
return el.attr.path; |
|
}, |
|
// @private |
|
getPathcircle: function(el) { |
|
var a = el.attr; |
|
return this.ellipsePath(a.x, a.y, a.radius, a.radius); |
|
}, |
|
// @private |
|
getPathellipse: function(el) { |
|
var a = el.attr; |
|
return this.ellipsePath(a.x, a.y, a.radiusX || (a.width / 2) || 0, a.radiusY || (a.height / 2) || 0); |
|
}, |
|
// @private |
|
getPathrect: function(el) { |
|
var a = el.attr; |
|
return this.rectPath(a.x || 0, a.y || 0, a.width || 0, a.height || 0, a.r || 0); |
|
}, |
|
// @private |
|
getPathimage: function(el) { |
|
var a = el.attr; |
|
return this.rectPath(a.x || 0, a.y || 0, a.width, a.height); |
|
}, |
|
// @private |
|
getPathtext: function(el) { |
|
var bbox = this.getBBoxText(el); |
|
return this.rectPath(bbox.x, bbox.y, bbox.width, bbox.height); |
|
}, |
|
createGroup: function(id) { |
|
var group = this.groups.get(id); |
|
if (!group) { |
|
group = new Ext.draw.CompositeSprite({ |
|
surface: this |
|
}); |
|
group.id = id || Ext.id(null, 'ext-surface-group-'); |
|
this.groups.add(group); |
|
} |
|
return group; |
|
}, |
|
/** |
|
* Returns a new group or an existent group associated with the current surface. |
|
* The group returned is a {@link Ext.draw.CompositeSprite} group. |
|
* |
|
* For example: |
|
* |
|
* var spriteGroup = drawComponent.surface.getGroup('someGroupId'); |
|
* |
|
* @param {String} id The unique identifier of the group. |
|
* @return {Object} The {@link Ext.draw.CompositeSprite}. |
|
*/ |
|
getGroup: function(id) { |
|
var group; |
|
if (typeof id == "string") { |
|
group = this.groups.get(id); |
|
if (!group) { |
|
group = this.createGroup(id); |
|
} |
|
} else { |
|
group = id; |
|
} |
|
return group; |
|
}, |
|
// @private |
|
prepareItems: function(items, applyDefaults) { |
|
items = [].concat(items); |
|
// Make sure defaults are applied and item is initialized |
|
var item, i, ln; |
|
for (i = 0 , ln = items.length; i < ln; i++) { |
|
item = items[i]; |
|
if (!(item instanceof Ext.draw.Sprite)) { |
|
// Temporary, just take in configs... |
|
item.surface = this; |
|
items[i] = this.createItem(item); |
|
} else { |
|
item.surface = this; |
|
} |
|
} |
|
return items; |
|
}, |
|
/** |
|
* Changes the text in the sprite element. The sprite must be a `text` sprite. |
|
* This method can also be called from {@link Ext.draw.Sprite}. |
|
* |
|
* For example: |
|
* |
|
* var spriteGroup = drawComponent.surface.setText(sprite, 'my new text'); |
|
* |
|
* @param {Object} sprite The Sprite to change the text. |
|
* @param {String} text The new text to be set. |
|
* @method |
|
*/ |
|
setText: Ext.emptyFn, |
|
// @private Creates an item and appends it to the surface. Called |
|
// as an internal method when calling `add`. |
|
createItem: Ext.emptyFn, |
|
/** |
|
* Retrieves the id of this component. |
|
* Will autogenerate an id if one has not already been set. |
|
*/ |
|
getId: function() { |
|
return this.id || (this.id = Ext.id(null, 'ext-surface-')); |
|
}, |
|
/** |
|
* Destroys the surface. This is done by removing all components from it and |
|
* also removing its reference to a DOM element. |
|
* |
|
* For example: |
|
* |
|
* drawComponent.surface.destroy(); |
|
*/ |
|
destroy: function() { |
|
var me = this; |
|
delete me.domRef; |
|
if (me.background) { |
|
me.background.destroy(); |
|
} |
|
me.removeAll(true); |
|
Ext.destroy(me.groups.items); |
|
} |
|
}); |
|
|
|
/** |
|
* @private |
|
*/ |
|
Ext.define('Ext.draw.layout.Component', { |
|
/* Begin Definitions */ |
|
alias: 'layout.draw', |
|
extend: 'Ext.layout.component.Auto', |
|
setHeightInDom: true, |
|
setWidthInDom: true, |
|
/* End Definitions */ |
|
type: 'draw', |
|
measureContentWidth: function(ownerContext) { |
|
var target = ownerContext.target, |
|
paddingInfo = ownerContext.getPaddingInfo(), |
|
bbox = this.getBBox(ownerContext); |
|
if (!target.viewBox) { |
|
if (target.autoSize) { |
|
return bbox.width + paddingInfo.width; |
|
} else { |
|
return bbox.x + bbox.width + paddingInfo.width; |
|
} |
|
} else { |
|
if (ownerContext.heightModel.shrinkWrap) { |
|
return paddingInfo.width; |
|
} else { |
|
return bbox.width / bbox.height * (ownerContext.getProp('contentHeight') - paddingInfo.height) + paddingInfo.width; |
|
} |
|
} |
|
}, |
|
measureContentHeight: function(ownerContext) { |
|
var target = ownerContext.target, |
|
paddingInfo = ownerContext.getPaddingInfo(), |
|
bbox = this.getBBox(ownerContext); |
|
if (!ownerContext.target.viewBox) { |
|
if (target.autoSize) { |
|
return bbox.height + paddingInfo.height; |
|
} else { |
|
return bbox.y + bbox.height + paddingInfo.height; |
|
} |
|
} else { |
|
if (ownerContext.widthModel.shrinkWrap) { |
|
return paddingInfo.height; |
|
} else { |
|
return bbox.height / bbox.width * (ownerContext.getProp('contentWidth') - paddingInfo.width) + paddingInfo.height; |
|
} |
|
} |
|
}, |
|
getBBox: function(ownerContext) { |
|
var bbox = ownerContext.surfaceBBox; |
|
if (!bbox) { |
|
bbox = ownerContext.target.surface.items.getBBox(); |
|
// If the surface is empty, we'll get these values, normalize them |
|
if (bbox.width === -Infinity && bbox.height === -Infinity) { |
|
bbox.width = bbox.height = bbox.x = bbox.y = 0; |
|
} |
|
ownerContext.surfaceBBox = bbox; |
|
} |
|
return bbox; |
|
}, |
|
publishInnerWidth: function(ownerContext, width) { |
|
ownerContext.setContentWidth(width - ownerContext.getFrameInfo().width, true); |
|
}, |
|
publishInnerHeight: function(ownerContext, height) { |
|
ownerContext.setContentHeight(height - ownerContext.getFrameInfo().height, true); |
|
}, |
|
finishedLayout: function(ownerContext) { |
|
// TODO: Is there a better way doing this? |
|
var props = ownerContext.props, |
|
paddingInfo = ownerContext.getPaddingInfo(); |
|
// We don't want the cost of getProps, so we just use the props data... this is ok |
|
// because all the props have been calculated by this time |
|
this.owner.setSurfaceSize(props.contentWidth - paddingInfo.width, props.contentHeight - paddingInfo.height); |
|
// calls afterComponentLayout, so we want the surface to be sized before that: |
|
this.callParent(arguments); |
|
} |
|
}); |
|
|
|
/** |
|
* The Draw Component is a surface in which sprites can be rendered. The Draw Component |
|
* manages and holds an {@link Ext.draw.Surface} instance where |
|
* {@link Ext.draw.Sprite Sprites} can be appended. |
|
* |
|
* One way to create a draw component is: |
|
* |
|
* @example |
|
* var drawComponent = Ext.create('Ext.draw.Component', { |
|
* viewBox: false, |
|
* items: [{ |
|
* type: 'circle', |
|
* fill: '#79BB3F', |
|
* radius: 100, |
|
* x: 100, |
|
* y: 100 |
|
* }] |
|
* }); |
|
* |
|
* Ext.create('Ext.Window', { |
|
* width: 215, |
|
* height: 235, |
|
* layout: 'fit', |
|
* items: [drawComponent] |
|
* }).show(); |
|
* |
|
* In this case we created a draw component and added a {@link Ext.draw.Sprite sprite} to it. |
|
* The {@link Ext.draw.Sprite#type type} of the sprite is `circle` so if you run this code you'll see a yellow-ish |
|
* circle in a Window. When setting `viewBox` to `false` we are responsible for setting the object's position and |
|
* dimensions accordingly. |
|
* |
|
* You can also add sprites by using the surface's add method: |
|
* |
|
* drawComponent.surface.add({ |
|
* type: 'circle', |
|
* fill: '#79BB3F', |
|
* radius: 100, |
|
* x: 100, |
|
* y: 100 |
|
* }); |
|
* |
|
* ## Larger example |
|
* |
|
* @example |
|
* var drawComponent = Ext.create('Ext.draw.Component', { |
|
* width: 800, |
|
* height: 600, |
|
* renderTo: document.body |
|
* }), surface = drawComponent.surface; |
|
* |
|
* surface.add([{ |
|
* type: 'circle', |
|
* radius: 10, |
|
* fill: '#f00', |
|
* x: 10, |
|
* y: 10, |
|
* group: 'circles' |
|
* }, { |
|
* type: 'circle', |
|
* radius: 10, |
|
* fill: '#0f0', |
|
* x: 50, |
|
* y: 50, |
|
* group: 'circles' |
|
* }, { |
|
* type: 'circle', |
|
* radius: 10, |
|
* fill: '#00f', |
|
* x: 100, |
|
* y: 100, |
|
* group: 'circles' |
|
* }, { |
|
* type: 'rect', |
|
* width: 20, |
|
* height: 20, |
|
* fill: '#f00', |
|
* x: 10, |
|
* y: 10, |
|
* group: 'rectangles' |
|
* }, { |
|
* type: 'rect', |
|
* width: 20, |
|
* height: 20, |
|
* fill: '#0f0', |
|
* x: 50, |
|
* y: 50, |
|
* group: 'rectangles' |
|
* }, { |
|
* type: 'rect', |
|
* width: 20, |
|
* height: 20, |
|
* fill: '#00f', |
|
* x: 100, |
|
* y: 100, |
|
* group: 'rectangles' |
|
* }]); |
|
* |
|
* // Get references to my groups |
|
* circles = surface.getGroup('circles'); |
|
* rectangles = surface.getGroup('rectangles'); |
|
* |
|
* // Animate the circles down |
|
* circles.animate({ |
|
* duration: 1000, |
|
* to: { |
|
* translate: { |
|
* y: 200 |
|
* } |
|
* } |
|
* }); |
|
* |
|
* // Animate the rectangles across |
|
* rectangles.animate({ |
|
* duration: 1000, |
|
* to: { |
|
* translate: { |
|
* x: 200 |
|
* } |
|
* } |
|
* }); |
|
* |
|
* For more information on Sprites, the core elements added to a draw component's surface, |
|
* refer to the {@link Ext.draw.Sprite} documentation. |
|
*/ |
|
Ext.define('Ext.draw.Component', { |
|
/* Begin Definitions */ |
|
alias: 'widget.draw', |
|
extend: 'Ext.Component', |
|
requires: [ |
|
'Ext.draw.Surface', |
|
'Ext.draw.layout.Component' |
|
], |
|
/* End Definitions */ |
|
/** |
|
* @cfg {String[]} enginePriority |
|
* Defines the priority order for which Surface implementation to use. The first |
|
* one supported by the current environment will be used. |
|
*/ |
|
enginePriority: [ |
|
'Svg', |
|
'Vml' |
|
], |
|
baseCls: Ext.baseCSSPrefix + 'surface', |
|
componentLayout: 'draw', |
|
/** |
|
* @cfg {Boolean} viewBox |
|
* Turn on view box support which will scale and position items in the draw component to fit to the component while |
|
* maintaining aspect ratio. Note that this scaling can override other sizing settings on your items. |
|
*/ |
|
viewBox: true, |
|
shrinkWrap: 3, |
|
/** |
|
* @cfg {Boolean} autoSize |
|
* Turn on autoSize support which will set the bounding div's size to the natural size of the contents. |
|
*/ |
|
autoSize: false, |
|
/** |
|
* @cfg {Object[]} gradients (optional) Define a set of gradients that can be used as `fill` property in sprites. |
|
* The gradients array is an array of objects with the following properties: |
|
* |
|
* - `id` - string - The unique name of the gradient. |
|
* - `angle` - number, optional - The angle of the gradient in degrees. |
|
* - `stops` - object - An object with numbers as keys (from 0 to 100) and style objects as values |
|
* |
|
* ## Example |
|
* |
|
* gradients: [{ |
|
* id: 'gradientId', |
|
* angle: 45, |
|
* stops: { |
|
* 0: { |
|
* color: '#555' |
|
* }, |
|
* 100: { |
|
* color: '#ddd' |
|
* } |
|
* } |
|
* }, { |
|
* id: 'gradientId2', |
|
* angle: 0, |
|
* stops: { |
|
* 0: { |
|
* color: '#590' |
|
* }, |
|
* 20: { |
|
* color: '#599' |
|
* }, |
|
* 100: { |
|
* color: '#ddd' |
|
* } |
|
* } |
|
* }] |
|
* |
|
* Then the sprites can use `gradientId` and `gradientId2` by setting the fill attributes to those ids, for example: |
|
* |
|
* sprite.setAttributes({ |
|
* fill: 'url(#gradientId)' |
|
* }, true); |
|
*/ |
|
/** |
|
* @cfg {Ext.draw.Sprite[]} items |
|
* Array of sprites or sprite config objects to add initially to the surface. |
|
*/ |
|
/** |
|
* @property {Ext.draw.Surface} surface |
|
* The Surface instance managed by this component. |
|
*/ |
|
suspendSizing: 0, |
|
/** |
|
* @event |
|
* Event forwarded from {@link Ext.draw.Surface surface}. |
|
* @inheritdoc Ext.draw.Surface#mousedown |
|
*/ |
|
/** |
|
* @event |
|
* Event forwarded from {@link Ext.draw.Surface surface}. |
|
* @inheritdoc Ext.draw.Surface#mouseup |
|
*/ |
|
/** |
|
* @event |
|
* Event forwarded from {@link Ext.draw.Surface surface}. |
|
* @inheritdoc Ext.draw.Surface#mousemove |
|
*/ |
|
/** |
|
* @event |
|
* Event forwarded from {@link Ext.draw.Surface surface}. |
|
* @inheritdoc Ext.draw.Surface#mouseenter |
|
*/ |
|
/** |
|
* @event |
|
* Event forwarded from {@link Ext.draw.Surface surface}. |
|
* @inheritdoc Ext.draw.Surface#mouseleave |
|
*/ |
|
/** |
|
* @event |
|
* Event forwarded from {@link Ext.draw.Surface surface}. |
|
* @inheritdoc Ext.draw.Surface#click |
|
*/ |
|
/** |
|
* @event |
|
* Event forwarded from {@link Ext.draw.Surface surface}. |
|
* @inheritdoc Ext.draw.Surface#dblclick |
|
*/ |
|
/** |
|
* @private |
|
* |
|
* Create the Surface on initial render |
|
*/ |
|
onRender: function() { |
|
this.callParent(arguments); |
|
if (this.createSurface() !== false) { |
|
this.configureSurfaceSize(); |
|
} |
|
}, |
|
configureSurfaceSize: function() { |
|
var me = this, |
|
viewBox = me.viewBox, |
|
autoSize = me.autoSize, |
|
bbox; |
|
if ((viewBox || autoSize) && !me.suspendSizing) { |
|
bbox = me.surface.items.getBBox(); |
|
if (viewBox) { |
|
me.surface.setViewBox(bbox.x, bbox.y, bbox.width, bbox.height); |
|
} else { |
|
me.autoSizeSurface(bbox); |
|
} |
|
} |
|
}, |
|
// @private |
|
autoSizeSurface: function(bbox) { |
|
bbox = bbox || this.surface.items.getBBox(); |
|
this.setSurfaceSize(bbox.width, bbox.height); |
|
}, |
|
setSurfaceSize: function(width, height) { |
|
this.surface.setSize(width, height); |
|
if (this.autoSize) { |
|
var bbox = this.surface.items.getBBox(); |
|
this.surface.setViewBox(bbox.x, bbox.y - (+Ext.isOpera), width, height); |
|
} |
|
}, |
|
/** |
|
* Create the Surface instance. Resolves the correct Surface implementation to |
|
* instantiate based on the 'enginePriority' config. Once the Surface instance is |
|
* created you can use the handle to that instance to add sprites. For example: |
|
* |
|
* drawComponent.surface.add(sprite); |
|
* |
|
* @private |
|
*/ |
|
createSurface: function() { |
|
var me = this, |
|
cfg = Ext.applyIf({ |
|
renderTo: me.el, |
|
height: me.height, |
|
width: me.width, |
|
items: me.items |
|
}, me.initialConfig), |
|
surface; |
|
// ensure we remove any listeners to prevent duplicate events since we refire them below |
|
delete cfg.listeners; |
|
if (!cfg.gradients) { |
|
cfg.gradients = me.gradients; |
|
} |
|
me.initSurfaceCfg(cfg); |
|
surface = Ext.draw.Surface.create(cfg, me.enginePriority); |
|
if (!surface) { |
|
// In case we cannot create a surface, return false so we can stop |
|
return false; |
|
} |
|
me.surface = surface; |
|
surface.owner = me; |
|
function refire(eventName) { |
|
return function(e) { |
|
me.fireEvent(eventName, e); |
|
}; |
|
} |
|
surface.on({ |
|
scope: me, |
|
mouseup: refire('mouseup'), |
|
mousedown: refire('mousedown'), |
|
mousemove: refire('mousemove'), |
|
mouseenter: refire('mouseenter'), |
|
mouseleave: refire('mouseleave'), |
|
click: refire('click'), |
|
dblclick: refire('dblclick') |
|
}); |
|
}, |
|
initSurfaceCfg: Ext.emptyFn, |
|
/** |
|
* @private |
|
* |
|
* Clean up the Surface instance on component destruction |
|
*/ |
|
onDestroy: function() { |
|
Ext.destroy(this.surface); |
|
this.callParent(arguments); |
|
} |
|
}); |
|
|
|
Ext.define('Ext.rtl.draw.Component', { |
|
override: 'Ext.draw.Component', |
|
initSurfaceCfg: function(cfg) { |
|
if (this.getInherited().rtl) { |
|
cfg.isRtl = true; |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* Represents an RGB color and provides helper functions get |
|
* color components in HSL color space. |
|
*/ |
|
Ext.define('Ext.draw.Color', { |
|
isColor: true, |
|
colorToHexRe: /(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/, |
|
rgbRe: /\s*rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)\s*/, |
|
hexRe: /\s*#([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)\s*/, |
|
/** |
|
* @cfg {Number} lightnessFactor |
|
* |
|
* The default factor to compute the lighter or darker color. Defaults to 0.2. |
|
*/ |
|
lightnessFactor: 0.2, |
|
/** |
|
* Creates new Color. |
|
* @param {Number} red Red component (0..255) |
|
* @param {Number} green Green component (0..255) |
|
* @param {Number} blue Blue component (0..255) |
|
*/ |
|
constructor: function(red, green, blue) { |
|
var me = this, |
|
clamp = Ext.Number.constrain; |
|
me.r = clamp(red, 0, 255); |
|
me.g = clamp(green, 0, 255); |
|
me.b = clamp(blue, 0, 255); |
|
}, |
|
/** |
|
* Get the red component of the color, in the range 0..255. |
|
* @return {Number} |
|
*/ |
|
getRed: function() { |
|
return this.r; |
|
}, |
|
/** |
|
* Get the green component of the color, in the range 0..255. |
|
* @return {Number} |
|
*/ |
|
getGreen: function() { |
|
return this.g; |
|
}, |
|
/** |
|
* Get the blue component of the color, in the range 0..255. |
|
* @return {Number} |
|
*/ |
|
getBlue: function() { |
|
return this.b; |
|
}, |
|
/** |
|
* Get the RGB values. |
|
* @return {Number[]} |
|
*/ |
|
getRGB: function() { |
|
var me = this; |
|
return [ |
|
me.r, |
|
me.g, |
|
me.b |
|
]; |
|
}, |
|
/** |
|
* Get the equivalent HSL components of the color. |
|
* @return {Number[]} |
|
*/ |
|
getHSL: function() { |
|
var me = this, |
|
r = me.r / 255, |
|
g = me.g / 255, |
|
b = me.b / 255, |
|
max = Math.max(r, g, b), |
|
min = Math.min(r, g, b), |
|
delta = max - min, |
|
h, |
|
s = 0, |
|
l = 0.5 * (max + min); |
|
// min==max means achromatic (hue is undefined) |
|
if (min != max) { |
|
s = (l <= 0.5) ? delta / (max + min) : delta / (2 - max - min); |
|
if (r == max) { |
|
h = 60 * (g - b) / delta; |
|
} else if (g == max) { |
|
h = 120 + 60 * (b - r) / delta; |
|
} else { |
|
h = 240 + 60 * (r - g) / delta; |
|
} |
|
if (h < 0) { |
|
h += 360; |
|
} |
|
if (h >= 360) { |
|
h -= 360; |
|
} |
|
} |
|
return [ |
|
h, |
|
s, |
|
l |
|
]; |
|
}, |
|
/** |
|
* Get the equivalent HSV components of the color. |
|
* @return {Number[]} |
|
*/ |
|
getHSV: function() { |
|
var me = this, |
|
r = me.r / 255, |
|
g = me.g / 255, |
|
b = me.b / 255, |
|
max = Math.max(r, g, b), |
|
min = Math.min(r, g, b), |
|
C = max - min, |
|
h, |
|
s = 0, |
|
v = max; |
|
// min == max means achromatic (hue is undefined) |
|
if (min != max) { |
|
s = v ? C / v : 0; |
|
if (r === max) { |
|
h = 60 * (g - b) / C; |
|
} else if (g === max) { |
|
h = 60 * (b - r) / C + 120; |
|
} else { |
|
h = 60 * (r - g) / C + 240; |
|
} |
|
if (h < 0) { |
|
h += 360; |
|
} |
|
if (h >= 360) { |
|
h -= 360; |
|
} |
|
} |
|
return [ |
|
h, |
|
s, |
|
v |
|
]; |
|
}, |
|
/** |
|
* Returns a new color that is lighter than this color in the HSL color space. |
|
* @param {Number} [factor=0.2] Lighter factor (0..1). |
|
* @return {Ext.draw.Color} |
|
*/ |
|
getLighter: function(factor) { |
|
var hsl = this.getHSL(); |
|
factor = factor || this.lightnessFactor; |
|
hsl[2] = Ext.Number.constrain(hsl[2] + factor, 0, 1); |
|
return this.fromHSL(hsl[0], hsl[1], hsl[2]); |
|
}, |
|
/** |
|
* Returns a new color that is darker than this color in the HSL color space. |
|
* @param {Number} [factor=0.2] Darker factor (0..1). |
|
* @return {Ext.draw.Color} |
|
*/ |
|
getDarker: function(factor) { |
|
factor = factor || this.lightnessFactor; |
|
return this.getLighter(-factor); |
|
}, |
|
/** |
|
* Return the color in the hex format, i.e. '#rrggbb'. |
|
* @return {String} |
|
*/ |
|
toString: function() { |
|
var me = this, |
|
round = Math.round, |
|
r = round(me.r).toString(16), |
|
g = round(me.g).toString(16), |
|
b = round(me.b).toString(16); |
|
r = (r.length == 1) ? '0' + r : r; |
|
g = (g.length == 1) ? '0' + g : g; |
|
b = (b.length == 1) ? '0' + b : b; |
|
return [ |
|
'#', |
|
r, |
|
g, |
|
b |
|
].join(''); |
|
}, |
|
/** |
|
* Convert a color to hexadecimal format. |
|
* |
|
* **Note:** This method is both static and instance. |
|
* |
|
* @param {String/String[]} color The color value (i.e 'rgb(255, 255, 255)', 'color: #ffffff'). |
|
* Can also be an Array, in this case the function handles the first member. |
|
* @return {String} The color in hexadecimal format. |
|
* @static |
|
*/ |
|
toHex: function(color) { |
|
if (Ext.isArray(color)) { |
|
color = color[0]; |
|
} |
|
if (!Ext.isString(color)) { |
|
return ''; |
|
} |
|
if (color.substr(0, 1) === '#') { |
|
return color; |
|
} |
|
var digits = this.colorToHexRe.exec(color), |
|
red, green, blue, rgb; |
|
if (Ext.isArray(digits)) { |
|
red = parseInt(digits[2], 10); |
|
green = parseInt(digits[3], 10); |
|
blue = parseInt(digits[4], 10); |
|
rgb = blue | (green << 8) | (red << 16); |
|
return digits[1] + '#' + ("000000" + rgb.toString(16)).slice(-6); |
|
} else { |
|
return color; |
|
} |
|
}, |
|
/** |
|
* Parse the string and create a new color. |
|
* |
|
* Supported formats: '#rrggbb', '#rgb', and 'rgb(r,g,b)'. |
|
* |
|
* If the string is not recognized, an undefined will be returned instead. |
|
* |
|
* **Note:** This method is both static and instance. |
|
* |
|
* @param {String} str Color in string. |
|
* @return Ext.draw.Color |
|
* @static |
|
*/ |
|
fromString: function(str) { |
|
var values, r, g, b, |
|
parse = parseInt, |
|
firstChar = str.substr(0, 1), |
|
colorValue; |
|
if (firstChar != '#') { |
|
colorValue = Ext.draw.Color.cssColors[str]; |
|
if (colorValue) { |
|
str = colorValue; |
|
firstChar = str.substr(0, 1); |
|
} |
|
} |
|
if ((str.length == 4 || str.length == 7) && firstChar === '#') { |
|
values = str.match(this.hexRe); |
|
if (values) { |
|
r = parse(values[1], 16) >> 0; |
|
g = parse(values[2], 16) >> 0; |
|
b = parse(values[3], 16) >> 0; |
|
if (str.length == 4) { |
|
r += (r * 16); |
|
g += (g * 16); |
|
b += (b * 16); |
|
} |
|
} |
|
} else { |
|
values = str.match(this.rgbRe); |
|
if (values) { |
|
r = values[1]; |
|
g = values[2]; |
|
b = values[3]; |
|
} |
|
} |
|
return (typeof r == 'undefined') ? undefined : new Ext.draw.Color(r, g, b); |
|
}, |
|
/** |
|
* Returns the gray value (0 to 255) of the color. |
|
* |
|
* The gray value is calculated using the formula r*0.3 + g*0.59 + b*0.11. |
|
* |
|
* @return {Number} |
|
*/ |
|
getGrayscale: function() { |
|
// http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale |
|
return this.r * 0.3 + this.g * 0.59 + this.b * 0.11; |
|
}, |
|
/** |
|
* Create a new color based on the specified HSL values. |
|
* |
|
* **Note:** This method is both static and instance. |
|
* |
|
* @param {Number} h Hue component [0..360) |
|
* @param {Number} s Saturation component [0..1] |
|
* @param {Number} l Lightness component [0..1] |
|
* @return Ext.draw.Color |
|
* @static |
|
*/ |
|
fromHSL: function(h, s, l) { |
|
var C, X, m, |
|
rgb = [], |
|
abs = Math.abs; |
|
if (s == 0 || h == null) { |
|
// achromatic |
|
rgb = [ |
|
l, |
|
l, |
|
l |
|
]; |
|
} else { |
|
// http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL |
|
// C is the chroma |
|
// X is the second largest component |
|
// m is the lightness adjustment |
|
h /= 60; |
|
C = s * (1 - abs(2 * l - 1)); |
|
X = C * (1 - abs(h % 2 - 1)); |
|
m = l - C / 2; |
|
switch (Math.floor(h)) { |
|
case 0: |
|
rgb = [ |
|
C, |
|
X, |
|
0 |
|
]; |
|
break; |
|
case 1: |
|
rgb = [ |
|
X, |
|
C, |
|
0 |
|
]; |
|
break; |
|
case 2: |
|
rgb = [ |
|
0, |
|
C, |
|
X |
|
]; |
|
break; |
|
case 3: |
|
rgb = [ |
|
0, |
|
X, |
|
C |
|
]; |
|
break; |
|
case 4: |
|
rgb = [ |
|
X, |
|
0, |
|
C |
|
]; |
|
break; |
|
case 5: |
|
rgb = [ |
|
C, |
|
0, |
|
X |
|
]; |
|
break; |
|
} |
|
rgb = [ |
|
rgb[0] + m, |
|
rgb[1] + m, |
|
rgb[2] + m |
|
]; |
|
} |
|
return new Ext.draw.Color(rgb[0] * 255, rgb[1] * 255, rgb[2] * 255); |
|
}, |
|
/** |
|
* Create a new color based on the specified HSV values. |
|
* |
|
* **Note:** This method is both static and instance. |
|
* |
|
* @param {Number} h Hue component [0..360) |
|
* @param {Number} s Saturation component [0..1] |
|
* @param {Number} v Value component [0..1] |
|
* @return Ext.draw.Color |
|
* @static |
|
*/ |
|
fromHSV: function(h, s, v) { |
|
var C, X, m, |
|
rgb = []; |
|
if (s == 0 || h == null) { |
|
// achromatic |
|
rgb = [ |
|
v, |
|
v, |
|
v |
|
]; |
|
} else { |
|
// http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV |
|
// C is the chroma |
|
// X is the second largest component |
|
// m is the value adjustment |
|
h /= 60; |
|
C = v * s; |
|
X = C * (1 - Math.abs(h % 2 - 1)); |
|
m = v - C; |
|
switch (Math.floor(h)) { |
|
case 0: |
|
rgb = [ |
|
C, |
|
X, |
|
0 |
|
]; |
|
break; |
|
case 1: |
|
rgb = [ |
|
X, |
|
C, |
|
0 |
|
]; |
|
break; |
|
case 2: |
|
rgb = [ |
|
0, |
|
C, |
|
X |
|
]; |
|
break; |
|
case 3: |
|
rgb = [ |
|
0, |
|
X, |
|
C |
|
]; |
|
break; |
|
case 4: |
|
rgb = [ |
|
X, |
|
0, |
|
C |
|
]; |
|
break; |
|
case 5: |
|
rgb = [ |
|
C, |
|
0, |
|
X |
|
]; |
|
break; |
|
} |
|
rgb = [ |
|
rgb[0] + m, |
|
rgb[1] + m, |
|
rgb[2] + m |
|
]; |
|
} |
|
return new Ext.draw.Color(rgb[0] * 255, rgb[1] * 255, rgb[2] * 255); |
|
} |
|
}, function() { |
|
var prototype = this.prototype, |
|
// These methods are both static and instance. |
|
staticMethods = [ |
|
'fromHSL', |
|
'fromHSV', |
|
'fromString', |
|
'toHex' |
|
], |
|
statics = {}; |
|
Ext.Array.each(staticMethods, function(name) { |
|
statics[name] = function() { |
|
return prototype[name].apply(prototype, arguments); |
|
}; |
|
}); |
|
//The CSS / SVG / X11 colors |
|
statics.cssColors = { |
|
aliceblue: '#F0F8FF', |
|
antiquewhite: '#FAEBD7', |
|
aqua: '#00FFFF', |
|
aquamarine: '#7FFFD4', |
|
azure: '#F0FFFF', |
|
beige: '#F5F5DC', |
|
bisque: '#FFE4C4', |
|
black: '#000000', |
|
blanchedalmond: '#FFEBCD', |
|
blue: '#0000FF', |
|
blueviolet: '#8A2BE2', |
|
brown: '#A52A2A', |
|
burlywood: '#DEB887', |
|
cadetblue: '#5F9EA0', |
|
chartreuse: '#7FFF00', |
|
chocolate: '#D2691E', |
|
coral: '#FF7F50', |
|
cornflowerblue: '#6495ED', |
|
cornsilk: '#FFF8DC', |
|
crimson: '#DC143C', |
|
cyan: '#00FFFF', |
|
darkblue: '#00008B', |
|
darkcyan: '#008B8B', |
|
darkgoldenrod: '#B8860B', |
|
darkgray: '#A9A9A9', |
|
darkgreen: '#006400', |
|
darkgrey: '#A9A9A9', |
|
darkkhaki: '#BDB76B', |
|
darkmagenta: '#8B008B', |
|
darkolivegreen: '#556B2F', |
|
darkorange: '#FF8C00', |
|
darkorchid: '#9932CC', |
|
darkred: '#8B0000', |
|
darksalmon: '#E9967A', |
|
darkseagreen: '#8FBC8F', |
|
darkslateblue: '#483D8B', |
|
darkslategray: '#2F4F4F', |
|
darkslategrey: '#2F4F4F', |
|
darkturquoise: '#00CED1', |
|
darkviolet: '#9400D3', |
|
deeppink: '#FF1493', |
|
deepskyblue: '#00BFFF', |
|
dimgray: '#696969', |
|
dimgrey: '#696969', |
|
dodgerblue: '#1E90FF', |
|
firebrick: '#B22222', |
|
floralwhite: '#FFFAF0', |
|
forestgreen: '#228B22', |
|
fuchsia: '#FF00FF', |
|
gainsboro: '#DCDCDC', |
|
ghostwhite: '#F8F8FF', |
|
gold: '#FFD700', |
|
goldenrod: '#DAA520', |
|
gray: '#808080', |
|
grey: '#808080', |
|
green: '#008000', |
|
greenyellow: '#ADFF2F', |
|
honeydew: '#F0FFF0', |
|
hotpink: '#FF69B4', |
|
indianred: '#CD5C5C', |
|
indigo: '#4B0082', |
|
ivory: '#FFFFF0', |
|
khaki: '#F0E68C', |
|
lavender: '#E6E6FA', |
|
lavenderblush: '#FFF0F5', |
|
lawngreen: '#7CFC00', |
|
lemonchiffon: '#FFFACD', |
|
lightblue: '#ADD8E6', |
|
lightcoral: '#F08080', |
|
lightcyan: '#E0FFFF', |
|
lightgoldenrodyellow: '#FAFAD2', |
|
lightgray: '#D3D3D3', |
|
lightgreen: '#90EE90', |
|
lightgrey: '#D3D3D3', |
|
lightpink: '#FFB6C1', |
|
lightsalmon: '#FFA07A', |
|
lightseagreen: '#20B2AA', |
|
lightskyblue: '#87CEFA', |
|
lightslategray: '#778899', |
|
lightslategrey: '#778899', |
|
lightsteelblue: '#B0C4DE', |
|
lightyellow: '#FFFFE0', |
|
lime: '#00FF00', |
|
limegreen: '#32CD32', |
|
linen: '#FAF0E6', |
|
magenta: '#FF00FF', |
|
maroon: '#800000', |
|
mediumaquamarine: '#66CDAA', |
|
mediumblue: '#0000CD', |
|
mediumorchid: '#BA55D3', |
|
mediumpurple: '#9370DB', |
|
mediumseagreen: '#3CB371', |
|
mediumslateblue: '#7B68EE', |
|
mediumspringgreen: '#00FA9A', |
|
mediumturquoise: '#48D1CC', |
|
mediumvioletred: '#C71585', |
|
midnightblue: '#191970', |
|
mintcream: '#F5FFFA', |
|
mistyrose: '#FFE4E1', |
|
moccasin: '#FFE4B5', |
|
navajowhite: '#FFDEAD', |
|
navy: '#000080', |
|
oldlace: '#FDF5E6', |
|
olive: '#808000', |
|
olivedrab: '#6B8E23', |
|
orange: '#FFA500', |
|
orangered: '#FF4500', |
|
orchid: '#DA70D6', |
|
palegoldenrod: '#EEE8AA', |
|
palegreen: '#98FB98', |
|
paleturquoise: '#AFEEEE', |
|
palevioletred: '#DB7093', |
|
papayawhip: '#FFEFD5', |
|
peachpuff: '#FFDAB9', |
|
peru: '#CD853F', |
|
pink: '#FFC0CB', |
|
plum: '#DDA0DD', |
|
powderblue: '#B0E0E6', |
|
purple: '#800080', |
|
red: '#FF0000', |
|
rosybrown: '#BC8F8F', |
|
royalblue: '#4169E1', |
|
saddlebrown: '#8B4513', |
|
salmon: '#FA8072', |
|
sandybrown: '#F4A460', |
|
seagreen: '#2E8B57', |
|
seashell: '#FFF5EE', |
|
sienna: '#A0522D', |
|
silver: '#C0C0C0', |
|
skyblue: '#87CEEB', |
|
slateblue: '#6A5ACD', |
|
slategray: '#708090', |
|
slategrey: '#708090', |
|
snow: '#FFFAFA', |
|
springgreen: '#00FF7F', |
|
steelblue: '#4682B4', |
|
tan: '#D2B48C', |
|
teal: '#008080', |
|
thistle: '#D8BFD8', |
|
tomato: '#FF6347', |
|
turquoise: '#40E0D0', |
|
violet: '#EE82EE', |
|
wheat: '#F5DEB3', |
|
white: '#FFFFFF', |
|
whitesmoke: '#F5F5F5', |
|
yellow: '#FFFF00', |
|
yellowgreen: '#9ACD32' |
|
}; |
|
this.addStatics(statics); |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.theme.Theme |
|
* |
|
* Provides chart theming. |
|
* |
|
* Used as mixins by Ext.chart.Chart. |
|
*/ |
|
Ext.chart = Ext.chart || {}; |
|
Ext.define('Ext.chart.theme.Theme', (function() { |
|
/* Theme constructor: takes either a complex object with styles like: |
|
|
|
{ |
|
axis: { |
|
fill: '#000', |
|
'stroke-width': 1 |
|
}, |
|
axisLabelTop: { |
|
fill: '#000', |
|
font: '11px Arial' |
|
}, |
|
axisLabelLeft: { |
|
fill: '#000', |
|
font: '11px Arial' |
|
}, |
|
axisLabelRight: { |
|
fill: '#000', |
|
font: '11px Arial' |
|
}, |
|
axisLabelBottom: { |
|
fill: '#000', |
|
font: '11px Arial' |
|
}, |
|
axisTitleTop: { |
|
fill: '#000', |
|
font: '11px Arial' |
|
}, |
|
axisTitleLeft: { |
|
fill: '#000', |
|
font: '11px Arial' |
|
}, |
|
axisTitleRight: { |
|
fill: '#000', |
|
font: '11px Arial' |
|
}, |
|
axisTitleBottom: { |
|
fill: '#000', |
|
font: '11px Arial' |
|
}, |
|
series: { |
|
'stroke-width': 1 |
|
}, |
|
seriesLabel: { |
|
font: '12px Arial', |
|
fill: '#333' |
|
}, |
|
marker: { |
|
stroke: '#555', |
|
fill: '#000', |
|
radius: 3, |
|
size: 3 |
|
}, |
|
seriesThemes: [{ |
|
fill: '#C6DBEF' |
|
}, { |
|
fill: '#9ECAE1' |
|
}, { |
|
fill: '#6BAED6' |
|
}, { |
|
fill: '#4292C6' |
|
}, { |
|
fill: '#2171B5' |
|
}, { |
|
fill: '#084594' |
|
}], |
|
markerThemes: [{ |
|
fill: '#084594', |
|
type: 'circle' |
|
}, { |
|
fill: '#2171B5', |
|
type: 'cross' |
|
}, { |
|
fill: '#4292C6', |
|
type: 'plus' |
|
}] |
|
} |
|
|
|
...or also takes just an array of colors and creates the complex object: |
|
|
|
{ |
|
colors: ['#aaa', '#bcd', '#eee'] |
|
} |
|
|
|
...or takes just a base color and makes a theme from it |
|
|
|
{ |
|
baseColor: '#bce' |
|
} |
|
|
|
To create a new theme you may add it to the Themes object: |
|
|
|
Ext.chart.theme.MyNewTheme = Ext.extend(Object, { |
|
constructor: function(config) { |
|
Ext.chart.theme.call(this, config, { |
|
baseColor: '#mybasecolor' |
|
}); |
|
} |
|
}); |
|
|
|
//Proposal: |
|
Ext.chart.theme.MyNewTheme = Ext.chart.createTheme('#basecolor'); |
|
|
|
...and then to use it provide the name of the theme (as a lower case string) in the chart config. |
|
|
|
{ |
|
theme: 'mynewtheme' |
|
} |
|
*/ |
|
(function() { |
|
Ext.chart.theme = function(config, base) { |
|
config = config || {}; |
|
var i = 0, |
|
d = Ext.Date.now(), |
|
l, colors, color, seriesThemes, markerThemes, seriesTheme, markerTheme, key, |
|
gradients = [], |
|
midColor, midL; |
|
if (config.baseColor) { |
|
midColor = Ext.draw.Color.fromString(config.baseColor); |
|
midL = midColor.getHSL()[2]; |
|
if (midL < 0.15) { |
|
midColor = midColor.getLighter(0.3); |
|
} else if (midL < 0.3) { |
|
midColor = midColor.getLighter(0.15); |
|
} else if (midL > 0.85) { |
|
midColor = midColor.getDarker(0.3); |
|
} else if (midL > 0.7) { |
|
midColor = midColor.getDarker(0.15); |
|
} |
|
config.colors = [ |
|
midColor.getDarker(0.3).toString(), |
|
midColor.getDarker(0.15).toString(), |
|
midColor.toString(), |
|
midColor.getLighter(0.15).toString(), |
|
midColor.getLighter(0.3).toString() |
|
]; |
|
delete config.baseColor; |
|
} |
|
if (config.colors) { |
|
colors = config.colors.slice(); |
|
markerThemes = base.markerThemes; |
|
seriesThemes = base.seriesThemes; |
|
l = colors.length; |
|
base.colors = colors; |
|
for (; i < l; i++) { |
|
color = colors[i]; |
|
markerTheme = markerThemes[i] || {}; |
|
seriesTheme = seriesThemes[i] || {}; |
|
markerTheme.fill = seriesTheme.fill = markerTheme.stroke = seriesTheme.stroke = color; |
|
markerThemes[i] = markerTheme; |
|
seriesThemes[i] = seriesTheme; |
|
} |
|
base.markerThemes = markerThemes.slice(0, l); |
|
base.seriesThemes = seriesThemes.slice(0, l); |
|
} |
|
//the user is configuring something in particular (either markers, series or pie slices) |
|
for (key in base) { |
|
if (key in config) { |
|
if (Ext.isObject(config[key]) && Ext.isObject(base[key])) { |
|
Ext.apply(base[key], config[key]); |
|
} else { |
|
base[key] = config[key]; |
|
} |
|
} |
|
} |
|
if (config.useGradients) { |
|
colors = base.colors || (function() { |
|
var ans = []; |
|
for (i = 0 , seriesThemes = base.seriesThemes , l = seriesThemes.length; i < l; i++) { |
|
ans.push(seriesThemes[i].fill || seriesThemes[i].stroke); |
|
} |
|
return ans; |
|
}()); |
|
for (i = 0 , l = colors.length; i < l; i++) { |
|
midColor = Ext.draw.Color.fromString(colors[i]); |
|
if (midColor) { |
|
color = midColor.getDarker(0.1).toString(); |
|
midColor = midColor.toString(); |
|
key = 'theme-' + midColor.substr(1) + '-' + color.substr(1) + '-' + d; |
|
gradients.push({ |
|
id: key, |
|
angle: 45, |
|
stops: { |
|
0: { |
|
color: midColor.toString() |
|
}, |
|
100: { |
|
color: color.toString() |
|
} |
|
} |
|
}); |
|
colors[i] = 'url(#' + key + ')'; |
|
} |
|
} |
|
base.gradients = gradients; |
|
base.colors = colors; |
|
} |
|
/* |
|
base.axis = Ext.apply(base.axis || {}, config.axis || {}); |
|
base.axisLabel = Ext.apply(base.axisLabel || {}, config.axisLabel || {}); |
|
base.axisTitle = Ext.apply(base.axisTitle || {}, config.axisTitle || {}); |
|
*/ |
|
Ext.apply(this, base); |
|
}; |
|
}()); |
|
return { |
|
/* Begin Definitions */ |
|
requires: [ |
|
'Ext.draw.Color' |
|
], |
|
/* End Definitions */ |
|
theme: 'Base', |
|
themeAttrs: false, |
|
initTheme: function(theme) { |
|
var me = this, |
|
themes = Ext.chart.theme, |
|
key, gradients; |
|
if (theme) { |
|
theme = theme.split(':'); |
|
for (key in themes) { |
|
if (key == theme[0]) { |
|
gradients = theme[1] == 'gradients'; |
|
me.themeAttrs = new themes[key]({ |
|
useGradients: gradients |
|
}); |
|
if (gradients) { |
|
me.gradients = me.themeAttrs.gradients; |
|
} |
|
if (me.themeAttrs.background) { |
|
me.background = me.themeAttrs.background; |
|
} |
|
return; |
|
} |
|
} |
|
Ext.Error.raise('No theme found named "' + theme + '"'); |
|
} |
|
} |
|
}; |
|
})()); |
|
|
|
/** |
|
* @private |
|
*/ |
|
Ext.define('Ext.chart.MaskLayer', { |
|
extend: 'Ext.Component', |
|
constructor: function(config) { |
|
config = Ext.apply(config || {}, { |
|
style: 'position:absolute;background-color:#ff9;cursor:crosshair;opacity:0.5;border:1px solid #00f;' |
|
}); |
|
this.callParent([ |
|
config |
|
]); |
|
}, |
|
//'mousedown', |
|
//'mouseup', |
|
//'mousemove', |
|
//'mouseenter', |
|
//'mouseleave' |
|
privates: { |
|
initDraggable: function() { |
|
this.callParent(arguments); |
|
this.dd.onStart = function(e) { |
|
var me = this, |
|
comp = me.comp; |
|
// Cache the start [X, Y] array |
|
this.startPosition = comp.getPosition(true); |
|
// If client Component has a ghost method to show a lightweight version of itself |
|
// then use that as a drag proxy unless configured to liveDrag. |
|
if (comp.ghost && !comp.liveDrag) { |
|
me.proxy = comp.ghost(); |
|
me.dragTarget = me.proxy.header.el; |
|
} |
|
// Set the constrainTo Region before we start dragging. |
|
if (me.constrain || me.constrainDelegate) { |
|
me.constrainTo = me.calculateConstrainRegion(); |
|
} |
|
}; |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* Defines a mask for a chart's series. |
|
* The 'chart' member must be set prior to rendering. |
|
* |
|
* A Mask can be used to select a certain region in a chart. |
|
* When enabled, the `select` event will be triggered when a |
|
* region is selected by the mask, allowing the user to perform |
|
* other tasks like zooming on that region, etc. |
|
* |
|
* In order to use the mask one has to set the Chart `mask` option to |
|
* `true`, `vertical` or `horizontal`. Then a possible configuration for the |
|
* listener could be: |
|
* |
|
* items: { |
|
* xtype: 'chart', |
|
* animate: true, |
|
* store: store1, |
|
* mask: 'horizontal', |
|
* listeners: { |
|
* select: { |
|
* fn: function(me, selection) { |
|
* me.setZoom(selection); |
|
* me.mask.hide(); |
|
* } |
|
* } |
|
* } |
|
* } |
|
* |
|
* In this example we zoom the chart to that particular region. You can also get |
|
* a handle to a mask instance from the chart object. The `chart.mask` element is a |
|
* `Ext.Panel`. |
|
* |
|
*/ |
|
Ext.define('Ext.chart.Mask', { |
|
mixinId: 'mask', |
|
requires: [ |
|
'Ext.chart.MaskLayer' |
|
], |
|
/** |
|
* @cfg {Boolean/String} mask |
|
* Enables selecting a region on chart. True to enable any selection, |
|
* 'horizontal' or 'vertical' to restrict the selection to X or Y axis. |
|
* |
|
* The mask in itself will do nothing but fire 'select' event. |
|
* See {@link Ext.chart.Mask} for example. |
|
*/ |
|
/** |
|
* Creates new Mask. |
|
* @param {Object} [config] Config object. |
|
*/ |
|
constructor: function(config) { |
|
var me = this; |
|
if (config) { |
|
Ext.apply(me, config); |
|
} |
|
if (me.enableMask) { |
|
me.on('afterrender', function() { |
|
//create a mask layer component |
|
var comp = new Ext.chart.MaskLayer({ |
|
renderTo: me.el, |
|
hidden: true |
|
}); |
|
comp.el.on({ |
|
'mousemove': function(e) { |
|
me.onMouseMove(e); |
|
}, |
|
'mouseup': function(e) { |
|
me.onMouseUp(e); |
|
} |
|
}); |
|
comp.initDraggable(); |
|
me.maskType = me.mask; |
|
me.mask = comp; |
|
me.maskSprite = me.surface.add({ |
|
type: 'path', |
|
path: [ |
|
'M', |
|
0, |
|
0 |
|
], |
|
zIndex: 1001, |
|
opacity: 0.6, |
|
hidden: true, |
|
stroke: '#00f', |
|
cursor: 'crosshair' |
|
}); |
|
}, me, { |
|
single: true |
|
}); |
|
} |
|
}, |
|
onMouseUp: function(e) { |
|
var me = this, |
|
bbox = me.bbox || me.chartBBox, |
|
sel; |
|
me.maskMouseDown = false; |
|
me.mouseDown = false; |
|
if (me.mouseMoved) { |
|
me.handleMouseEvent(e); |
|
me.mouseMoved = false; |
|
sel = me.maskSelection; |
|
me.fireEvent('select', me, { |
|
x: sel.x - bbox.x, |
|
y: sel.y - bbox.y, |
|
width: sel.width, |
|
height: sel.height |
|
}); |
|
} |
|
}, |
|
onMouseDown: function(e) { |
|
this.handleMouseEvent(e); |
|
}, |
|
onMouseMove: function(e) { |
|
this.handleMouseEvent(e); |
|
}, |
|
handleMouseEvent: function(e) { |
|
var me = this, |
|
mask = me.maskType, |
|
bbox = me.bbox || me.chartBBox, |
|
x = bbox.x, |
|
y = bbox.y, |
|
math = Math, |
|
floor = math.floor, |
|
abs = math.abs, |
|
min = math.min, |
|
max = math.max, |
|
height = floor(y + bbox.height), |
|
width = floor(x + bbox.width), |
|
staticX = e.getPageX() - me.el.getX(), |
|
staticY = e.getPageY() - me.el.getY(), |
|
maskMouseDown = me.maskMouseDown, |
|
path; |
|
staticX = max(staticX, x); |
|
staticY = max(staticY, y); |
|
staticX = min(staticX, width); |
|
staticY = min(staticY, height); |
|
if (e.type === 'mousedown') { |
|
// remember the cursor location |
|
me.mouseDown = true; |
|
me.mouseMoved = false; |
|
me.maskMouseDown = { |
|
x: staticX, |
|
y: staticY |
|
}; |
|
} else { |
|
// mousedown or mouseup: |
|
// track the cursor to display the selection |
|
me.mouseMoved = me.mouseDown; |
|
if (maskMouseDown && me.mouseDown) { |
|
if (mask == 'horizontal') { |
|
staticY = y; |
|
maskMouseDown.y = height; |
|
} else if (mask == 'vertical') { |
|
staticX = x; |
|
maskMouseDown.x = width; |
|
} |
|
width = maskMouseDown.x - staticX; |
|
height = maskMouseDown.y - staticY; |
|
path = [ |
|
'M', |
|
staticX, |
|
staticY, |
|
'l', |
|
width, |
|
0, |
|
0, |
|
height, |
|
-width, |
|
0, |
|
'z' |
|
]; |
|
me.maskSelection = { |
|
x: (width > 0 ? staticX : staticX + width) + me.el.getX(), |
|
y: (height > 0 ? staticY : staticY + height) + me.el.getY(), |
|
width: abs(width), |
|
height: abs(height) |
|
}; |
|
me.mask.updateBox(me.maskSelection); |
|
me.mask.show(); |
|
me.maskSprite.setAttributes({ |
|
hidden: true |
|
}, true); |
|
} else { |
|
if (mask == 'horizontal') { |
|
path = [ |
|
'M', |
|
staticX, |
|
y, |
|
'L', |
|
staticX, |
|
height |
|
]; |
|
} else if (mask == 'vertical') { |
|
path = [ |
|
'M', |
|
x, |
|
staticY, |
|
'L', |
|
width, |
|
staticY |
|
]; |
|
} else { |
|
path = [ |
|
'M', |
|
staticX, |
|
y, |
|
'L', |
|
staticX, |
|
height, |
|
'M', |
|
x, |
|
staticY, |
|
'L', |
|
width, |
|
staticY |
|
]; |
|
} |
|
me.maskSprite.setAttributes({ |
|
path: path, |
|
'stroke-width': mask === true ? 1 : 1, |
|
hidden: false |
|
}, true); |
|
} |
|
} |
|
}, |
|
onMouseLeave: function(e) { |
|
var me = this; |
|
me.mouseMoved = false; |
|
me.mouseDown = false; |
|
me.maskMouseDown = false; |
|
me.mask.hide(); |
|
me.maskSprite.hide(true); |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.Navigation |
|
* |
|
* Handles panning and zooming capabilities. |
|
* |
|
* Used as mixin by Ext.chart.Chart. |
|
*/ |
|
Ext.define('Ext.chart.Navigation', { |
|
mixinId: 'navigation', |
|
/** |
|
* Zooms the chart to the specified selection range. |
|
* Can be used with a selection mask. For example: |
|
* |
|
* items: { |
|
* xtype: 'chart', |
|
* animate: true, |
|
* store: store1, |
|
* mask: 'horizontal', |
|
* listeners: { |
|
* select: { |
|
* fn: function(me, selection) { |
|
* me.setZoom(selection); |
|
* me.mask.hide(); |
|
* } |
|
* } |
|
* } |
|
* } |
|
* |
|
* @param {Object} [zoomConfig] The config to set the zoom area to. |
|
* This object is then used on the axis {@link Ext.chart.axis.Axis#minimum} and {@link Ext.chart.axis.Axis#maximum} and then the chart is redrawn. |
|
* @param {Number} zoomConfig.x The x coordinate to zoom |
|
* @param {Number} zoomConfig.y The y coordinate to zoom |
|
* @param {Number} zoomConfig.width The width of the zoom area |
|
* @param {Number} zoomConfig.height The height of the zoom area |
|
*/ |
|
setZoom: function(zoomConfig) { |
|
var me = this, |
|
axesItems = me.axes.items, |
|
i, ln, axis, |
|
bbox = me.chartBBox, |
|
xScale = bbox.width, |
|
yScale = bbox.height, |
|
zoomArea = { |
|
x: zoomConfig.x - me.el.getX(), |
|
y: zoomConfig.y - me.el.getY(), |
|
width: zoomConfig.width, |
|
height: zoomConfig.height |
|
}, |
|
zoomer, ends, from, to, store, count, step, length, horizontal; |
|
for (i = 0 , ln = axesItems.length; i < ln; i++) { |
|
axis = axesItems[i]; |
|
horizontal = (axis.position == 'bottom' || axis.position == 'top'); |
|
if (axis.type == 'Category') { |
|
if (!store) { |
|
store = me.getChartStore(); |
|
count = store.data.items.length; |
|
} |
|
zoomer = zoomArea; |
|
length = axis.length; |
|
step = Math.round(length / count); |
|
if (horizontal) { |
|
from = (zoomer.x ? Math.floor(zoomer.x / step) + 1 : 0); |
|
to = (zoomer.x + zoomer.width) / step; |
|
} else { |
|
from = (zoomer.y ? Math.floor(zoomer.y / step) + 1 : 0); |
|
to = (zoomer.y + zoomer.height) / step; |
|
} |
|
} else { |
|
zoomer = { |
|
x: zoomArea.x / xScale, |
|
y: zoomArea.y / yScale, |
|
width: zoomArea.width / xScale, |
|
height: zoomArea.height / yScale |
|
}; |
|
ends = axis.calcEnds(); |
|
if (horizontal) { |
|
from = (ends.to - ends.from) * zoomer.x + ends.from; |
|
to = (ends.to - ends.from) * zoomer.width + from; |
|
} else { |
|
to = (ends.to - ends.from) * (1 - zoomer.y) + ends.from; |
|
from = to - (ends.to - ends.from) * zoomer.height; |
|
} |
|
} |
|
axis.minimum = from; |
|
axis.maximum = to; |
|
if (horizontal) { |
|
if (axis.doConstrain && me.maskType != 'vertical') { |
|
axis.doConstrain(); |
|
} |
|
} else { |
|
if (axis.doConstrain && me.maskType != 'horizontal') { |
|
axis.doConstrain(); |
|
} |
|
} |
|
} |
|
me.redraw(false); |
|
}, |
|
/** |
|
* Restores the zoom to the original value. This can be used to reset |
|
* the previous zoom state set by `setZoom`. For example: |
|
* |
|
* myChart.restoreZoom(); |
|
*/ |
|
restoreZoom: function() { |
|
var me = this, |
|
axesItems = me.axes.items, |
|
i, ln, axis; |
|
me.setSubStore(null); |
|
for (i = 0 , ln = axesItems.length; i < ln; i++) { |
|
axis = axesItems[i]; |
|
delete axis.minimum; |
|
delete axis.maximum; |
|
} |
|
me.redraw(false); |
|
} |
|
}); |
|
|
|
/** |
|
* @private |
|
*/ |
|
Ext.define('Ext.chart.Shape', { |
|
/* Begin Definitions */ |
|
singleton: true, |
|
/* End Definitions */ |
|
circle: function(surface, opts) { |
|
return surface.add(Ext.apply({ |
|
type: 'circle', |
|
x: opts.x, |
|
y: opts.y, |
|
stroke: null, |
|
radius: opts.radius |
|
}, opts)); |
|
}, |
|
line: function(surface, opts) { |
|
return surface.add(Ext.apply({ |
|
type: 'rect', |
|
x: opts.x - opts.radius, |
|
y: opts.y - opts.radius, |
|
height: 2 * opts.radius, |
|
width: 2 * opts.radius / 5 |
|
}, opts)); |
|
}, |
|
square: function(surface, opts) { |
|
return surface.add(Ext.applyIf({ |
|
type: 'rect', |
|
x: opts.x - opts.radius, |
|
y: opts.y - opts.radius, |
|
height: 2 * opts.radius, |
|
width: 2 * opts.radius, |
|
radius: null |
|
}, opts)); |
|
}, |
|
triangle: function(surface, opts) { |
|
opts.radius *= 1.75; |
|
return surface.add(Ext.apply({ |
|
type: 'path', |
|
stroke: null, |
|
path: "M".concat(opts.x, ",", opts.y, "m0-", opts.radius * 0.58, "l", opts.radius * 0.5, ",", opts.radius * 0.87, "-", opts.radius, ",0z") |
|
}, opts)); |
|
}, |
|
diamond: function(surface, opts) { |
|
var r = opts.radius; |
|
r *= 1.5; |
|
return surface.add(Ext.apply({ |
|
type: 'path', |
|
stroke: null, |
|
path: [ |
|
"M", |
|
opts.x, |
|
opts.y - r, |
|
"l", |
|
r, |
|
r, |
|
-r, |
|
r, |
|
-r, |
|
-r, |
|
r, |
|
-r, |
|
"z" |
|
] |
|
}, opts)); |
|
}, |
|
cross: function(surface, opts) { |
|
var r = opts.radius; |
|
r = r / 1.7; |
|
return surface.add(Ext.apply({ |
|
type: 'path', |
|
stroke: null, |
|
path: "M".concat(opts.x - r, ",", opts.y, "l", [ |
|
-r, |
|
-r, |
|
r, |
|
-r, |
|
r, |
|
r, |
|
r, |
|
-r, |
|
r, |
|
r, |
|
-r, |
|
r, |
|
r, |
|
r, |
|
-r, |
|
r, |
|
-r, |
|
-r, |
|
-r, |
|
r, |
|
-r, |
|
-r, |
|
"z" |
|
]) |
|
}, opts)); |
|
}, |
|
plus: function(surface, opts) { |
|
var r = opts.radius / 1.3; |
|
return surface.add(Ext.apply({ |
|
type: 'path', |
|
stroke: null, |
|
path: "M".concat(opts.x - r / 2, ",", opts.y - r / 2, "l", [ |
|
0, |
|
-r, |
|
r, |
|
0, |
|
0, |
|
r, |
|
r, |
|
0, |
|
0, |
|
r, |
|
-r, |
|
0, |
|
0, |
|
r, |
|
-r, |
|
0, |
|
0, |
|
-r, |
|
-r, |
|
0, |
|
0, |
|
-r, |
|
"z" |
|
]) |
|
}, opts)); |
|
}, |
|
arrow: function(surface, opts) { |
|
var r = opts.radius; |
|
return surface.add(Ext.apply({ |
|
type: 'path', |
|
path: "M".concat(opts.x - r * 0.7, ",", opts.y - r * 0.4, "l", [ |
|
r * 0.6, |
|
0, |
|
0, |
|
-r * 0.4, |
|
r, |
|
r * 0.8, |
|
-r, |
|
r * 0.8, |
|
0, |
|
-r * 0.4, |
|
-r * 0.6, |
|
0 |
|
], "z") |
|
}, opts)); |
|
}, |
|
drop: function(surface, x, y, text, size, angle) { |
|
size = size || 30; |
|
angle = angle || 0; |
|
surface.add({ |
|
type: 'path', |
|
path: [ |
|
'M', |
|
x, |
|
y, |
|
'l', |
|
size, |
|
0, |
|
'A', |
|
size * 0.4, |
|
size * 0.4, |
|
0, |
|
1, |
|
0, |
|
x + size * 0.7, |
|
y - size * 0.7, |
|
'z' |
|
], |
|
fill: '#000', |
|
stroke: 'none', |
|
rotate: { |
|
degrees: 22.5 - angle, |
|
x: x, |
|
y: y |
|
} |
|
}); |
|
angle = (angle + 90) * Math.PI / 180; |
|
surface.add({ |
|
type: 'text', |
|
x: x + size * Math.sin(angle) - 10, |
|
// Shift here, Not sure why. |
|
y: y + size * Math.cos(angle) + 5, |
|
text: text, |
|
'font-size': size * 12 / 40, |
|
stroke: 'none', |
|
fill: '#fff' |
|
}); |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.LegendItem |
|
* A single item of a legend (marker plus label) |
|
*/ |
|
Ext.define('Ext.chart.LegendItem', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.draw.CompositeSprite', |
|
requires: [ |
|
'Ext.chart.Shape' |
|
], |
|
/* End Definitions */ |
|
// Controls Series visibility |
|
hiddenSeries: false, |
|
// These are cached for quick lookups |
|
label: undefined, |
|
// Position of the item, relative to the upper-left corner of the legend box |
|
x: 0, |
|
y: 0, |
|
zIndex: 500, |
|
// checks to make sure that a unit size follows the bold keyword in the font style value |
|
boldRe: /bold\s\d{1,}.*/i, |
|
constructor: function(config) { |
|
this.callParent(arguments); |
|
this.createLegend(config); |
|
}, |
|
/** |
|
* Creates all the individual sprites for this legend item |
|
*/ |
|
createLegend: function(config) { |
|
var me = this, |
|
series = me.series, |
|
index = config.yFieldIndex; |
|
me.label = me.createLabel(config); |
|
me.createSeriesMarkers(config); |
|
me.setAttributes({ |
|
hidden: false |
|
}, true); |
|
me.yFieldIndex = index; |
|
// Add event listeners |
|
me.on('mouseover', me.onMouseOver, me); |
|
me.on('mouseout', me.onMouseOut, me); |
|
me.on('mousedown', me.onMouseDown, me); |
|
if (!series.visibleInLegend(index)) { |
|
me.hiddenSeries = true; |
|
me.label.setAttributes({ |
|
opacity: 0.5 |
|
}, true); |
|
} |
|
// Relative to 0,0 at first so that the bbox is calculated correctly |
|
me.updatePosition({ |
|
x: 0, |
|
y: 0 |
|
}); |
|
}, |
|
/** |
|
* @private Retrieves text to be displayed as item label. |
|
*/ |
|
getLabelText: function() { |
|
var me = this, |
|
series = me.series, |
|
idx = me.yFieldIndex; |
|
function getSeriesProp(name) { |
|
var val = series[name]; |
|
return (Ext.isArray(val) ? val[idx] : val); |
|
} |
|
return getSeriesProp('title') || getSeriesProp('yField'); |
|
}, |
|
/** |
|
* @private Creates label sprite. |
|
*/ |
|
createLabel: function(config) { |
|
var me = this, |
|
legend = me.legend; |
|
return me.add('label', me.surface.add({ |
|
type: 'text', |
|
x: 20, |
|
y: 0, |
|
zIndex: (me.zIndex || 0) + 2, |
|
fill: legend.labelColor, |
|
font: legend.labelFont, |
|
text: me.getLabelText(), |
|
style: { |
|
cursor: 'pointer' |
|
} |
|
})); |
|
}, |
|
/** |
|
* @private Creates Series marker Sprites. |
|
*/ |
|
createSeriesMarkers: function(config) { |
|
var me = this, |
|
index = config.yFieldIndex, |
|
series = me.series, |
|
seriesType = series.type, |
|
surface = me.surface, |
|
z = me.zIndex; |
|
// Line series - display as short line with optional marker in the middle |
|
if (seriesType === 'line' || seriesType === 'scatter') { |
|
if (seriesType === 'line') { |
|
var seriesStyle = Ext.apply(series.seriesStyle, series.style); |
|
me.drawLine(0.5, 0.5, 16.5, 0.5, z, seriesStyle, index); |
|
} |
|
if (series.showMarkers || seriesType === 'scatter') { |
|
var markerConfig = Ext.apply(series.markerStyle, series.markerConfig || {}, { |
|
fill: series.getLegendColor(index) |
|
}); |
|
me.drawMarker(8.5, 0.5, z, markerConfig); |
|
} |
|
} else // All other series types - display as filled box |
|
{ |
|
me.drawFilledBox(12, 12, z, index); |
|
} |
|
}, |
|
/** |
|
* @private Creates line sprite for Line series. |
|
*/ |
|
drawLine: function(fromX, fromY, toX, toY, z, seriesStyle, index) { |
|
var me = this, |
|
surface = me.surface, |
|
series = me.series; |
|
return me.add('line', surface.add({ |
|
type: 'path', |
|
path: 'M' + fromX + ',' + fromY + 'L' + toX + ',' + toY, |
|
zIndex: (z || 0) + 2, |
|
"stroke-width": series.lineWidth, |
|
"stroke-linejoin": "round", |
|
"stroke-dasharray": series.dash, |
|
stroke: seriesStyle.stroke || series.getLegendColor(index) || '#000', |
|
style: { |
|
cursor: 'pointer' |
|
} |
|
})); |
|
}, |
|
/** |
|
* @private Creates series-shaped marker for Line and Scatter series. |
|
*/ |
|
drawMarker: function(x, y, z, markerConfig) { |
|
var me = this, |
|
surface = me.surface, |
|
series = me.series; |
|
return me.add('marker', Ext.chart.Shape[markerConfig.type](surface, { |
|
fill: markerConfig.fill, |
|
x: x, |
|
y: y, |
|
zIndex: (z || 0) + 2, |
|
radius: markerConfig.radius || markerConfig.size, |
|
style: { |
|
cursor: 'pointer' |
|
} |
|
})); |
|
}, |
|
/** |
|
* @private Creates box-shaped marker for all series but Line and Scatter. |
|
*/ |
|
drawFilledBox: function(width, height, z, index) { |
|
var me = this, |
|
surface = me.surface, |
|
series = me.series; |
|
return me.add('box', surface.add({ |
|
type: 'rect', |
|
zIndex: (z || 0) + 2, |
|
x: 0, |
|
y: 0, |
|
width: width, |
|
height: height, |
|
fill: series.getLegendColor(index), |
|
style: { |
|
cursor: 'pointer' |
|
} |
|
})); |
|
}, |
|
/** |
|
* @private Draws label in bold when mouse cursor is over the item. |
|
*/ |
|
onMouseOver: function() { |
|
var me = this; |
|
me.label.setAttributes({ |
|
'font-weight': 'bold' |
|
}, true); |
|
me.series._index = me.yFieldIndex; |
|
me.series.highlightItem(); |
|
}, |
|
/** |
|
* @private Draws label in normal when mouse cursor leaves the item. |
|
*/ |
|
onMouseOut: function() { |
|
var me = this, |
|
legend = me.legend, |
|
boldRe = me.boldRe; |
|
me.label.setAttributes({ |
|
'font-weight': legend.labelFont && boldRe.test(legend.labelFont) ? 'bold' : 'normal' |
|
}, true); |
|
me.series._index = me.yFieldIndex; |
|
me.series.unHighlightItem(); |
|
}, |
|
/** |
|
* @private Toggles Series visibility upon mouse click on the item. |
|
*/ |
|
onMouseDown: function() { |
|
var me = this, |
|
index = me.yFieldIndex; |
|
if (!me.hiddenSeries) { |
|
me.series.hideAll(index); |
|
me.label.setAttributes({ |
|
opacity: 0.5 |
|
}, true); |
|
} else { |
|
me.series.showAll(index); |
|
me.label.setAttributes({ |
|
opacity: 1 |
|
}, true); |
|
} |
|
me.hiddenSeries = !me.hiddenSeries; |
|
me.legend.chart.redraw(); |
|
}, |
|
/** |
|
* Update the positions of all this item's sprites to match the root position |
|
* of the legend box. |
|
* @param {Object} relativeTo (optional) If specified, this object's 'x' and 'y' values will be used |
|
* as the reference point for the relative positioning. Defaults to the Legend. |
|
*/ |
|
updatePosition: function(relativeTo) { |
|
var me = this, |
|
items = me.items, |
|
ln = items.length, |
|
currentX = me.x, |
|
currentY = me.y, |
|
item, i, x, y, translate, o, relativeX, relativeY; |
|
if (!relativeTo) { |
|
relativeTo = me.legend; |
|
} |
|
relativeX = relativeTo.x; |
|
relativeY = relativeTo.y; |
|
for (i = 0; i < ln; i++) { |
|
translate = true; |
|
item = items[i]; |
|
switch (item.type) { |
|
case 'text': |
|
x = 20 + relativeX + currentX; |
|
y = relativeY + currentY; |
|
translate = false; |
|
break; |
|
case 'rect': |
|
x = relativeX + currentX; |
|
y = relativeY + currentY - 6; |
|
break; |
|
default: |
|
x = relativeX + currentX; |
|
y = relativeY + currentY; |
|
} |
|
o = { |
|
x: x, |
|
y: y |
|
}; |
|
item.setAttributes(translate ? { |
|
translate: o |
|
} : o, true); |
|
} |
|
} |
|
}); |
|
|
|
Ext.define('Ext.rtl.chart.LegendItem', { |
|
override: 'Ext.chart.LegendItem', |
|
updatePosition: function(relativeTo) { |
|
var me = this, |
|
items = me.items, |
|
ln = items.length, |
|
legend = me.legend, |
|
currentX = me.x, |
|
currentY = me.y, |
|
item, i, x, y, translate, o, width, relativeX, relativeY; |
|
if (!relativeTo) { |
|
relativeTo = legend; |
|
} |
|
if (!legend.chart.getInherited().rtl || !relativeTo.width) { |
|
me.callParent(arguments); |
|
return; |
|
} |
|
relativeX = relativeTo.x; |
|
relativeY = relativeTo.y; |
|
width = relativeTo.width; |
|
for (i = 0; i < ln; i++) { |
|
translate = true; |
|
item = items[i]; |
|
switch (item.type) { |
|
case 'text': |
|
x = width + relativeX + currentX - 30 - item.getBBox().width; |
|
// -25 & -5 for a gap |
|
y = relativeY + currentY; |
|
translate = false; |
|
break; |
|
case 'rect': |
|
x = width + relativeX + currentX - 25; |
|
y = relativeY + currentY - 6; |
|
break; |
|
default: |
|
x = width + relativeX + currentX - 25; |
|
y = relativeY + currentY; |
|
} |
|
o = { |
|
x: x, |
|
y: y |
|
}; |
|
item.setAttributes(translate ? { |
|
translate: o |
|
} : o, true); |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.Legend |
|
* |
|
* Defines a legend for a chart's series. |
|
* The 'chart' member must be set prior to rendering. |
|
* The legend class displays a list of legend items each of them related with a |
|
* series being rendered. In order to render the legend item of the proper series |
|
* the series configuration object must have `showInLegend` set to true. |
|
* |
|
* The legend configuration object accepts a `position` as parameter. |
|
* The `position` parameter can be `left`, `right` |
|
* `top` or `bottom`. For example: |
|
* |
|
* legend: { |
|
* position: 'right' |
|
* }, |
|
* |
|
* ## Example |
|
* |
|
* @example |
|
* var store = Ext.create('Ext.data.JsonStore', { |
|
* 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 } |
|
* ] |
|
* }); |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* width: 500, |
|
* height: 300, |
|
* animate: true, |
|
* store: store, |
|
* shadow: true, |
|
* theme: 'Category1', |
|
* legend: { |
|
* position: 'top' |
|
* }, |
|
* axes: [ |
|
* { |
|
* type: 'Numeric', |
|
* position: 'left', |
|
* fields: ['data1', 'data2', 'data3', 'data4', 'data5'], |
|
* title: 'Sample Values', |
|
* grid: { |
|
* odd: { |
|
* opacity: 1, |
|
* fill: '#ddd', |
|
* stroke: '#bbb', |
|
* 'stroke-width': 1 |
|
* } |
|
* }, |
|
* minimum: 0, |
|
* adjustMinimumByMajorUnit: 0 |
|
* }, |
|
* { |
|
* type: 'Category', |
|
* position: 'bottom', |
|
* fields: ['name'], |
|
* title: 'Sample Metrics', |
|
* grid: true, |
|
* label: { |
|
* rotate: { |
|
* degrees: 315 |
|
* } |
|
* } |
|
* } |
|
* ], |
|
* series: [{ |
|
* type: 'area', |
|
* highlight: false, |
|
* axis: 'left', |
|
* xField: 'name', |
|
* yField: ['data1', 'data2', 'data3', 'data4', 'data5'], |
|
* style: { |
|
* opacity: 0.93 |
|
* } |
|
* }] |
|
* }); |
|
*/ |
|
Ext.define('Ext.chart.Legend', { |
|
/* Begin Definitions */ |
|
requires: [ |
|
'Ext.chart.LegendItem' |
|
], |
|
/* End Definitions */ |
|
/** |
|
* @cfg {Boolean} visible |
|
* Whether or not the legend should be displayed. |
|
*/ |
|
visible: true, |
|
/** |
|
* @cfg {Boolean} update |
|
* If set to true the legend will be refreshed when the chart is. |
|
* This is useful to update the legend items if series are |
|
* added/removed/updated from the chart. Default is true. |
|
*/ |
|
update: true, |
|
/** |
|
* @cfg {String} position |
|
* The position of the legend in relation to the chart. One of: "top", |
|
* "bottom", "left", "right", or "float". If set to "float", then the legend |
|
* box will be positioned at the point denoted by the x and y parameters. |
|
*/ |
|
position: 'bottom', |
|
/** |
|
* @cfg {Number} x |
|
* X-position of the legend box. Used directly if position is set to "float", otherwise |
|
* it will be calculated dynamically. |
|
*/ |
|
x: 0, |
|
/** |
|
* @cfg {Number} y |
|
* Y-position of the legend box. Used directly if position is set to "float", otherwise |
|
* it will be calculated dynamically. |
|
*/ |
|
y: 0, |
|
/** |
|
* @cfg {String} labelColor |
|
* Color to be used for the legend labels, eg '#000' |
|
*/ |
|
labelColor: '#000', |
|
/** |
|
* @cfg {String} labelFont |
|
* Font to be used for the legend labels, eg '12px Helvetica' |
|
*/ |
|
labelFont: '12px Helvetica, sans-serif', |
|
/** |
|
* @cfg {String} boxStroke |
|
* Style of the stroke for the legend box |
|
*/ |
|
boxStroke: '#000', |
|
/** |
|
* @cfg {Number} boxStrokeWidth |
|
* Width of the stroke for the legend box |
|
*/ |
|
boxStrokeWidth: 1, |
|
/** |
|
* @cfg {String} boxFill |
|
* Fill style for the legend box |
|
*/ |
|
boxFill: '#FFF', |
|
/** |
|
* @cfg {Number} itemSpacing |
|
* Amount of space between legend items |
|
*/ |
|
itemSpacing: 10, |
|
/** |
|
* @cfg {Number} padding |
|
* Amount of padding between the legend box's border and its items |
|
*/ |
|
padding: 5, |
|
// @private |
|
width: 0, |
|
// @private |
|
height: 0, |
|
/** |
|
* @cfg {Number} boxZIndex |
|
* Sets the z-index for the legend. Defaults to 100. |
|
*/ |
|
boxZIndex: 100, |
|
/** |
|
* Creates new Legend. |
|
* @param {Object} config (optional) Config object. |
|
*/ |
|
constructor: function(config) { |
|
var me = this; |
|
if (config) { |
|
Ext.apply(me, config); |
|
} |
|
me.items = []; |
|
/** |
|
* Whether the legend box is oriented vertically, i.e. if it is on the left or right side or floating. |
|
* @type {Boolean} |
|
*/ |
|
me.isVertical = ("left|right|float".indexOf(me.position) !== -1); |
|
// cache these here since they may get modified later on |
|
me.origX = me.x; |
|
me.origY = me.y; |
|
}, |
|
/** |
|
* @private Create all the sprites for the legend |
|
*/ |
|
create: function() { |
|
var me = this, |
|
seriesItems = me.chart.series.items, |
|
i, ln, series; |
|
me.createBox(); |
|
if (me.rebuild !== false) { |
|
me.createItems(); |
|
} |
|
if (!me.created && me.isDisplayed()) { |
|
me.created = true; |
|
// Listen for changes to series titles to trigger regeneration of the legend |
|
for (i = 0 , ln = seriesItems.length; i < ln; i++) { |
|
series = seriesItems[i]; |
|
series.on('titlechange', me.redraw, me); |
|
} |
|
} |
|
}, |
|
init: Ext.emptyFn, |
|
/** |
|
* @private Redraws the Legend |
|
*/ |
|
redraw: function() { |
|
var me = this; |
|
me.create(); |
|
me.updatePosition(); |
|
}, |
|
/** |
|
* @private Determine whether the legend should be displayed. Looks at the legend's 'visible' config, |
|
* and also the 'showInLegend' config for each of the series. |
|
*/ |
|
isDisplayed: function() { |
|
return this.visible && this.chart.series.findIndex('showInLegend', true) !== -1; |
|
}, |
|
/** |
|
* @private Create the series markers and labels |
|
*/ |
|
createItems: function() { |
|
var me = this, |
|
seriesItems = me.chart.series.items, |
|
items = me.items, |
|
fields, i, li, j, lj, series, item; |
|
//remove all legend items |
|
me.removeItems(); |
|
// Create all the item labels |
|
for (i = 0 , li = seriesItems.length; i < li; i++) { |
|
series = seriesItems[i]; |
|
if (series.showInLegend) { |
|
fields = [].concat(series.yField); |
|
for (j = 0 , lj = fields.length; j < lj; j++) { |
|
item = me.createLegendItem(series, j); |
|
items.push(item); |
|
} |
|
} |
|
} |
|
me.alignItems(); |
|
}, |
|
/** |
|
* @private Removes all legend items. |
|
*/ |
|
removeItems: function() { |
|
var me = this, |
|
items = me.items, |
|
len = items ? items.length : 0, |
|
i; |
|
if (len) { |
|
for (i = 0; i < len; i++) { |
|
items[i].destroy(); |
|
} |
|
} |
|
//empty array |
|
items.length = []; |
|
}, |
|
/** |
|
* @private |
|
* Positions all items within Legend box. |
|
*/ |
|
alignItems: function() { |
|
var me = this, |
|
padding = me.padding, |
|
vertical = me.isVertical, |
|
mfloor = Math.floor, |
|
dim, maxWidth, maxHeight, totalWidth, totalHeight; |
|
dim = me.updateItemDimensions(); |
|
maxWidth = dim.maxWidth; |
|
maxHeight = dim.maxHeight; |
|
totalWidth = dim.totalWidth; |
|
totalHeight = dim.totalHeight; |
|
// Store the collected dimensions for later |
|
// Give some extra offset for the width because we bold on hover |
|
me.width = mfloor((vertical ? maxWidth : totalWidth) + padding * 2) + 10; |
|
me.height = mfloor((vertical ? totalHeight : maxHeight) + padding * 2); |
|
}, |
|
updateItemDimensions: function() { |
|
var me = this, |
|
items = me.items, |
|
padding = me.padding, |
|
itemSpacing = me.itemSpacing, |
|
maxWidth = 0, |
|
maxHeight = 0, |
|
totalWidth = 0, |
|
totalHeight = 0, |
|
vertical = me.isVertical, |
|
mfloor = Math.floor, |
|
mmax = Math.max, |
|
spacing = 0, |
|
i, l, item, bbox, width, height; |
|
// Collect item dimensions and position each one |
|
// properly in relation to the previous item |
|
for (i = 0 , l = items.length; i < l; i++) { |
|
item = items[i]; |
|
bbox = item.getBBox(); |
|
//always measure from x=0, since not all markers go all the way to the left |
|
width = bbox.width; |
|
height = bbox.height; |
|
spacing = (i === 0 ? 0 : itemSpacing); |
|
// Set the item's position relative to the legend box |
|
item.x = padding + mfloor(vertical ? 0 : totalWidth + spacing); |
|
item.y = padding + mfloor(vertical ? totalHeight + spacing : 0) + height / 2; |
|
// Collect cumulative dimensions |
|
totalWidth += spacing + width; |
|
totalHeight += spacing + height; |
|
maxWidth = mmax(maxWidth, width); |
|
maxHeight = mmax(maxHeight, height); |
|
} |
|
return { |
|
totalWidth: totalWidth, |
|
totalHeight: totalHeight, |
|
maxWidth: maxWidth, |
|
maxHeight: maxHeight |
|
}; |
|
}, |
|
/** |
|
* @private Creates single Legend Item |
|
*/ |
|
createLegendItem: function(series, yFieldIndex) { |
|
var me = this; |
|
return new Ext.chart.LegendItem({ |
|
legend: me, |
|
series: series, |
|
surface: me.chart.surface, |
|
yFieldIndex: yFieldIndex |
|
}); |
|
}, |
|
/** |
|
* @private Get the bounds for the legend's outer box |
|
*/ |
|
getBBox: function() { |
|
var me = this; |
|
return { |
|
x: Math.round(me.x) - me.boxStrokeWidth / 2, |
|
y: Math.round(me.y) - me.boxStrokeWidth / 2, |
|
width: me.width + me.boxStrokeWidth, |
|
height: me.height + me.boxStrokeWidth |
|
}; |
|
}, |
|
/** |
|
* @private Create the box around the legend items |
|
*/ |
|
createBox: function() { |
|
var me = this, |
|
box, bbox; |
|
if (me.boxSprite) { |
|
me.boxSprite.destroy(); |
|
} |
|
bbox = me.getBBox(); |
|
//if some of the dimensions are NaN this means that we |
|
//cannot set a specific width/height for the legend |
|
//container. One possibility for this is that there are |
|
//actually no items to show in the legend, and the legend |
|
//should be hidden. |
|
if (isNaN(bbox.width) || isNaN(bbox.height)) { |
|
me.boxSprite = false; |
|
return; |
|
} |
|
box = me.boxSprite = me.chart.surface.add(Ext.apply({ |
|
type: 'rect', |
|
stroke: me.boxStroke, |
|
"stroke-width": me.boxStrokeWidth, |
|
fill: me.boxFill, |
|
zIndex: me.boxZIndex |
|
}, bbox)); |
|
box.redraw(); |
|
}, |
|
/** |
|
* @private Calculates Legend position with respect to other Chart elements. |
|
*/ |
|
calcPosition: function() { |
|
var me = this, |
|
x, y, |
|
legendWidth = me.width, |
|
legendHeight = me.height, |
|
chart = me.chart, |
|
chartBBox = chart.chartBBox, |
|
insets = chart.insetPadding, |
|
chartWidth = chartBBox.width - (insets * 2), |
|
chartHeight = chartBBox.height - (insets * 2), |
|
chartX = chartBBox.x + insets, |
|
chartY = chartBBox.y + insets, |
|
surface = chart.surface, |
|
mfloor = Math.floor; |
|
// Find the position based on the dimensions |
|
switch (me.position) { |
|
case "left": |
|
x = insets; |
|
y = mfloor(chartY + chartHeight / 2 - legendHeight / 2); |
|
break; |
|
case "right": |
|
x = mfloor(surface.width - legendWidth) - insets; |
|
y = mfloor(chartY + chartHeight / 2 - legendHeight / 2); |
|
break; |
|
case "top": |
|
x = mfloor(chartX + chartWidth / 2 - legendWidth / 2); |
|
y = insets; |
|
break; |
|
case "bottom": |
|
x = mfloor(chartX + chartWidth / 2 - legendWidth / 2); |
|
y = mfloor(surface.height - legendHeight) - insets; |
|
break; |
|
default: |
|
x = mfloor(me.origX) + insets; |
|
y = mfloor(me.origY) + insets; |
|
} |
|
return { |
|
x: x, |
|
y: y |
|
}; |
|
}, |
|
/** |
|
* @private Update the position of all the legend's sprites to match its current x/y values |
|
*/ |
|
updatePosition: function() { |
|
var me = this, |
|
items = me.items, |
|
pos, i, l, bbox; |
|
if (me.isDisplayed()) { |
|
// Find the position based on the dimensions |
|
pos = me.calcPosition(); |
|
me.x = pos.x; |
|
me.y = pos.y; |
|
// Update the position of each item |
|
for (i = 0 , l = items.length; i < l; i++) { |
|
items[i].updatePosition(); |
|
} |
|
bbox = me.getBBox(); |
|
//if some of the dimensions are NaN this means that we |
|
//cannot set a specific width/height for the legend |
|
//container. One possibility for this is that there are |
|
//actually no items to show in the legend, and the legend |
|
//should be hidden. |
|
if (isNaN(bbox.width) || isNaN(bbox.height)) { |
|
if (me.boxSprite) { |
|
me.boxSprite.hide(true); |
|
} |
|
} else { |
|
if (!me.boxSprite) { |
|
me.createBox(); |
|
} |
|
// Update the position of the outer box |
|
me.boxSprite.setAttributes(bbox, true); |
|
me.boxSprite.show(true); |
|
} |
|
} |
|
}, |
|
/** toggle |
|
* @param {Boolean} show Whether to show or hide the legend. |
|
* |
|
*/ |
|
toggle: function(show) { |
|
var me = this, |
|
i = 0, |
|
items = me.items, |
|
len = items.length; |
|
if (me.boxSprite) { |
|
if (show) { |
|
me.boxSprite.show(true); |
|
} else { |
|
me.boxSprite.hide(true); |
|
} |
|
} |
|
for (; i < len; ++i) { |
|
if (show) { |
|
items[i].show(true); |
|
} else { |
|
items[i].hide(true); |
|
} |
|
} |
|
me.visible = show; |
|
} |
|
}); |
|
|
|
Ext.define('Ext.rtl.chart.Legend', { |
|
override: 'Ext.chart.Legend', |
|
init: function() { |
|
var me = this; |
|
me.callParent(arguments); |
|
me.position = me.chart.invertPosition(me.position); |
|
me.rtl = me.chart.getInherited().rtl; |
|
}, |
|
updateItemDimensions: function() { |
|
var me = this, |
|
result = me.callParent(), |
|
padding = me.padding, |
|
spacing = me.itemSpacing, |
|
items = me.items, |
|
len = items.length, |
|
mfloor = Math.floor, |
|
width = result.totalWidth, |
|
usedWidth = 0, |
|
i, item, itemWidth; |
|
if (me.rtl && !me.isVertical) { |
|
for (i = 0; i < len; ++i) { |
|
item = items[i]; |
|
// Set the item's position relative to the legend box |
|
itemWidth = mfloor(item.getBBox().width + spacing); |
|
item.x = -usedWidth + padding; |
|
usedWidth += itemWidth; |
|
} |
|
} |
|
return result; |
|
} |
|
}); |
|
|
|
/** |
|
* Provides default colors for non-specified things. Should be sub-classed when creating new themes. |
|
* @private |
|
*/ |
|
Ext.define('Ext.chart.theme.Base', { |
|
/* Begin Definitions */ |
|
requires: [ |
|
'Ext.chart.theme.Theme' |
|
], |
|
/* End Definitions */ |
|
constructor: function(config) { |
|
var ident = Ext.identityFn; |
|
Ext.chart.theme.call(this, config, { |
|
background: false, |
|
axis: { |
|
stroke: '#444', |
|
'stroke-width': 1 |
|
}, |
|
axisLabelTop: { |
|
fill: '#444', |
|
font: '12px Arial, Helvetica, sans-serif', |
|
spacing: 2, |
|
padding: 5, |
|
renderer: ident |
|
}, |
|
axisLabelRight: { |
|
fill: '#444', |
|
font: '12px Arial, Helvetica, sans-serif', |
|
spacing: 2, |
|
padding: 5, |
|
renderer: ident |
|
}, |
|
axisLabelBottom: { |
|
fill: '#444', |
|
font: '12px Arial, Helvetica, sans-serif', |
|
spacing: 2, |
|
padding: 5, |
|
renderer: ident |
|
}, |
|
axisLabelLeft: { |
|
fill: '#444', |
|
font: '12px Arial, Helvetica, sans-serif', |
|
spacing: 2, |
|
padding: 5, |
|
renderer: ident |
|
}, |
|
axisTitleTop: { |
|
font: 'bold 18px Arial', |
|
fill: '#444' |
|
}, |
|
axisTitleRight: { |
|
font: 'bold 18px Arial', |
|
fill: '#444', |
|
rotate: { |
|
x: 0, |
|
y: 0, |
|
degrees: 270 |
|
} |
|
}, |
|
axisTitleBottom: { |
|
font: 'bold 18px Arial', |
|
fill: '#444' |
|
}, |
|
axisTitleLeft: { |
|
font: 'bold 18px Arial', |
|
fill: '#444', |
|
rotate: { |
|
x: 0, |
|
y: 0, |
|
degrees: 270 |
|
} |
|
}, |
|
series: { |
|
'stroke-width': 0 |
|
}, |
|
seriesLabel: { |
|
font: '12px Arial', |
|
fill: '#333' |
|
}, |
|
marker: { |
|
stroke: '#555', |
|
radius: 3, |
|
size: 3 |
|
}, |
|
colors: [ |
|
"#94ae0a", |
|
"#115fa6", |
|
"#a61120", |
|
"#ff8809", |
|
"#ffd13e", |
|
"#a61187", |
|
"#24ad9a", |
|
"#7c7474", |
|
"#a66111" |
|
], |
|
seriesThemes: [ |
|
{ |
|
fill: "#94ae0a" |
|
}, |
|
{ |
|
fill: "#115fa6" |
|
}, |
|
{ |
|
fill: "#a61120" |
|
}, |
|
{ |
|
fill: "#ff8809" |
|
}, |
|
{ |
|
fill: "#ffd13e" |
|
}, |
|
{ |
|
fill: "#a61187" |
|
}, |
|
{ |
|
fill: "#24ad9a" |
|
}, |
|
{ |
|
fill: "#7c7474" |
|
}, |
|
{ |
|
fill: "#115fa6" |
|
}, |
|
{ |
|
fill: "#94ae0a" |
|
}, |
|
{ |
|
fill: "#a61120" |
|
}, |
|
{ |
|
fill: "#ff8809" |
|
}, |
|
{ |
|
fill: "#ffd13e" |
|
}, |
|
{ |
|
fill: "#a61187" |
|
}, |
|
{ |
|
fill: "#24ad9a" |
|
}, |
|
{ |
|
fill: "#7c7474" |
|
}, |
|
{ |
|
fill: "#a66111" |
|
} |
|
], |
|
markerThemes: [ |
|
{ |
|
fill: "#115fa6", |
|
type: 'circle' |
|
}, |
|
{ |
|
fill: "#94ae0a", |
|
type: 'cross' |
|
}, |
|
{ |
|
fill: "#115fa6", |
|
type: 'plus' |
|
}, |
|
{ |
|
fill: "#94ae0a", |
|
type: 'circle' |
|
}, |
|
{ |
|
fill: "#a61120", |
|
type: 'cross' |
|
} |
|
] |
|
}); |
|
} |
|
}, function() { |
|
var palette = [ |
|
'#b1da5a', |
|
'#4ce0e7', |
|
'#e84b67', |
|
'#da5abd', |
|
'#4d7fe6', |
|
'#fec935' |
|
], |
|
names = [ |
|
'Green', |
|
'Sky', |
|
'Red', |
|
'Purple', |
|
'Blue', |
|
'Yellow' |
|
], |
|
i = 0, |
|
j = 0, |
|
l = palette.length, |
|
themes = Ext.chart.theme, |
|
categories = [ |
|
[ |
|
'#f0a50a', |
|
'#c20024', |
|
'#2044ba', |
|
'#810065', |
|
'#7eae29' |
|
], |
|
[ |
|
'#6d9824', |
|
'#87146e', |
|
'#2a9196', |
|
'#d39006', |
|
'#1e40ac' |
|
], |
|
[ |
|
'#fbbc29', |
|
'#ce2e4e', |
|
'#7e0062', |
|
'#158b90', |
|
'#57880e' |
|
], |
|
[ |
|
'#ef5773', |
|
'#fcbd2a', |
|
'#4f770d', |
|
'#1d3eaa', |
|
'#9b001f' |
|
], |
|
[ |
|
'#7eae29', |
|
'#fdbe2a', |
|
'#910019', |
|
'#27b4bc', |
|
'#d74dbc' |
|
], |
|
[ |
|
'#44dce1', |
|
'#0b2592', |
|
'#996e05', |
|
'#7fb325', |
|
'#b821a1' |
|
] |
|
], |
|
cats = categories.length; |
|
//Create themes from base colors |
|
for (; i < l; i++) { |
|
themes[names[i]] = (function(color) { |
|
return Ext.extend(themes.Base, { |
|
constructor: function(config) { |
|
themes.Base.prototype.constructor.call(this, Ext.apply({ |
|
baseColor: color |
|
}, config)); |
|
} |
|
}); |
|
}(palette[i])); |
|
} |
|
//Create theme from color array |
|
for (i = 0; i < cats; i++) { |
|
themes['Category' + (i + 1)] = (function(category) { |
|
return Ext.extend(themes.Base, { |
|
constructor: function(config) { |
|
themes.Base.prototype.constructor.call(this, Ext.apply({ |
|
colors: category |
|
}, config)); |
|
} |
|
}); |
|
}(categories[i])); |
|
} |
|
}); |
|
|
|
/** |
|
* Charts provide a flexible way to achieve a wide range of data visualization capablitities. |
|
* Each Chart gets its data directly from a {@link Ext.data.Store Store}, and automatically |
|
* updates its display whenever data in the Store changes. In addition, the look and feel |
|
* of a Chart can be customized using {@link Ext.chart.theme.Theme Theme}s. |
|
* |
|
* ## Creating a Simple Chart |
|
* |
|
* Every Chart has three key parts - a {@link Ext.data.Store Store} that contains the data, |
|
* an array of {@link Ext.chart.axis.Axis Axes} which define the boundaries of the Chart, |
|
* and one or more {@link Ext.chart.series.Series Series} to handle the visual rendering of the data points. |
|
* |
|
* ### 1. Creating a Store |
|
* |
|
* The first step is to create a {@link Ext.data.Model Model} that represents the type of |
|
* data that will be displayed in the Chart. For example the data for a chart that displays |
|
* a weather forecast could be represented as a series of "WeatherPoint" data points with |
|
* two fields - "temperature", and "date": |
|
* |
|
* Ext.define('WeatherPoint', { |
|
* extend: 'Ext.data.Model', |
|
* fields: ['temperature', 'date'] |
|
* }); |
|
* |
|
* Next a {@link Ext.data.Store Store} must be created. The store contains a collection of "WeatherPoint" Model instances. |
|
* The data could be loaded dynamically, but for sake of ease this example uses inline data: |
|
* |
|
* var store = Ext.create('Ext.data.Store', { |
|
* model: 'WeatherPoint', |
|
* data: [ |
|
* { temperature: 58, date: new Date(2011, 1, 1, 8) }, |
|
* { temperature: 63, date: new Date(2011, 1, 1, 9) }, |
|
* { temperature: 73, date: new Date(2011, 1, 1, 10) }, |
|
* { temperature: 78, date: new Date(2011, 1, 1, 11) }, |
|
* { temperature: 81, date: new Date(2011, 1, 1, 12) } |
|
* ] |
|
* }); |
|
* |
|
* For additional information on Models and Stores please refer to the [Data Guide](#/guide/data). |
|
* |
|
* ### 2. Creating the Chart object |
|
* |
|
* Now that a Store has been created it can be used in a Chart: |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* width: 400, |
|
* height: 300, |
|
* store: store |
|
* }); |
|
* |
|
* That's all it takes to create a Chart instance that is backed by a Store. |
|
* However, if the above code is run in a browser, a blank screen will be displayed. |
|
* This is because the two pieces that are responsible for the visual display, |
|
* the Chart's {@link #cfg-axes axes} and {@link #cfg-series series}, have not yet been defined. |
|
* |
|
* ### 3. Configuring the Axes |
|
* |
|
* {@link Ext.chart.axis.Axis Axes} are the lines that define the boundaries of the data points that a Chart can display. |
|
* This example uses one of the most common Axes configurations - a horizontal "x" axis, and a vertical "y" axis: |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* ... |
|
* axes: [ |
|
* { |
|
* title: 'Temperature', |
|
* type: 'Numeric', |
|
* position: 'left', |
|
* fields: ['temperature'], |
|
* minimum: 0, |
|
* maximum: 100 |
|
* }, |
|
* { |
|
* title: 'Time', |
|
* type: 'Time', |
|
* position: 'bottom', |
|
* fields: ['date'], |
|
* dateFormat: 'ga' |
|
* } |
|
* ] |
|
* }); |
|
* |
|
* The "Temperature" axis is a vertical {@link Ext.chart.axis.Numeric Numeric Axis} and is positioned on the left edge of the Chart. |
|
* It represents the bounds of the data contained in the "WeatherPoint" Model's "temperature" field that was |
|
* defined above. The minimum value for this axis is "0", and the maximum is "100". |
|
* |
|
* The horizontal axis is a {@link Ext.chart.axis.Time Time Axis} and is positioned on the bottom edge of the Chart. |
|
* It represents the bounds of the data contained in the "WeatherPoint" Model's "date" field. |
|
* The {@link Ext.chart.axis.Time#cfg-dateFormat dateFormat} |
|
* configuration tells the Time Axis how to format it's labels. |
|
* |
|
* Here's what the Chart looks like now that it has its Axes configured: |
|
* |
|
* {@img Ext.chart.Chart/Ext.chart.Chart1.png Chart Axes} |
|
* |
|
* ### 4. Configuring the Series |
|
* |
|
* The final step in creating a simple Chart is to configure one or more {@link Ext.chart.series.Series Series}. |
|
* Series are responsible for the visual representation of the data points contained in the Store. |
|
* This example only has one Series: |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* ... |
|
* axes: [ |
|
* ... |
|
* ], |
|
* series: [ |
|
* { |
|
* type: 'line', |
|
* xField: 'date', |
|
* yField: 'temperature' |
|
* } |
|
* ] |
|
* }); |
|
* |
|
* This Series is a {@link Ext.chart.series.Line Line Series}, and it uses the "date" and "temperature" fields |
|
* from the "WeatherPoint" Models in the Store to plot its data points: |
|
* |
|
* {@img Ext.chart.Chart/Ext.chart.Chart2.png Line Series} |
|
* |
|
* See the [Line Charts Example](#!/example/charts/Charts.html) for a live demo. |
|
* |
|
* ## Themes |
|
* |
|
* The color scheme for a Chart can be easily changed using the {@link #cfg-theme theme} configuration option: |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* ... |
|
* theme: 'Green', |
|
* ... |
|
* }); |
|
* |
|
* {@img Ext.chart.Chart/Ext.chart.Chart3.png Green Theme} |
|
* |
|
* For more information on Charts please refer to the [Charting Guide](#/guide/charting). |
|
*/ |
|
Ext.define('Ext.chart.Chart', { |
|
extend: 'Ext.draw.Component', |
|
alias: 'widget.chart', |
|
mixins: [ |
|
'Ext.chart.theme.Theme', |
|
'Ext.chart.Mask', |
|
'Ext.chart.Navigation', |
|
'Ext.util.StoreHolder', |
|
'Ext.util.Observable' |
|
], |
|
uses: [ |
|
'Ext.chart.series.Series' |
|
], |
|
requires: [ |
|
'Ext.util.MixedCollection', |
|
'Ext.data.StoreManager', |
|
'Ext.chart.Legend', |
|
'Ext.chart.theme.Base', |
|
'Ext.chart.theme.Theme', |
|
'Ext.util.DelayedTask' |
|
], |
|
/* End Definitions */ |
|
// @private |
|
viewBox: false, |
|
/** |
|
* @cfg {String} theme |
|
* The name of the theme to be used. A theme defines the colors and other visual displays of tick marks |
|
* on axis, text, title text, line colors, marker colors and styles, etc. Possible theme values are 'Base', 'Green', |
|
* 'Sky', 'Red', 'Purple', 'Blue', 'Yellow' and also six category themes 'Category1' to 'Category6'. Default value |
|
* is 'Base'. |
|
*/ |
|
/** |
|
* @cfg {Boolean/Object} animate |
|
* True for the default animation (easing: 'ease' and duration: 500) or a standard animation config |
|
* object to be used for default chart animations. Defaults to false. |
|
*/ |
|
animate: false, |
|
/** |
|
* @cfg {Boolean/Object} legend |
|
* True for the default legend display or a {@link Ext.chart.Legend} config object. |
|
*/ |
|
legend: false, |
|
/** |
|
* @cfg {Number} insetPadding |
|
* The amount of inset padding in pixels for the chart. Defaults to 10. |
|
*/ |
|
insetPadding: 10, |
|
/** |
|
* @cfg {Object/Boolean} background |
|
* The chart background. This can be a gradient object, image, or color. Defaults to false for no |
|
* background. 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: { |
|
* id: 'gradientId', |
|
* angle: 45, |
|
* stops: { |
|
* 0: { |
|
* color: '#555' |
|
* } |
|
* 100: { |
|
* color: '#ddd' |
|
* } |
|
* } |
|
* } |
|
* } |
|
*/ |
|
background: false, |
|
/** |
|
* @cfg {Object[]} gradients |
|
* Define a set of gradients that can be used as `fill` property in sprites. The gradients array is an |
|
* array of objects with the following properties: |
|
* |
|
* - **id** - string - The unique name of the gradient. |
|
* - **angle** - number, optional - The angle of the gradient in degrees. |
|
* - **stops** - object - An object with numbers as keys (from 0 to 100) and style objects as values |
|
* |
|
* For example: |
|
* |
|
* gradients: [{ |
|
* id: 'gradientId', |
|
* angle: 45, |
|
* stops: { |
|
* 0: { |
|
* color: '#555' |
|
* }, |
|
* 100: { |
|
* color: '#ddd' |
|
* } |
|
* } |
|
* }, { |
|
* id: 'gradientId2', |
|
* angle: 0, |
|
* stops: { |
|
* 0: { |
|
* color: '#590' |
|
* }, |
|
* 20: { |
|
* color: '#599' |
|
* }, |
|
* 100: { |
|
* color: '#ddd' |
|
* } |
|
* } |
|
* }] |
|
* |
|
* Then the sprites can use `gradientId` and `gradientId2` by setting the fill attributes to those ids, for example: |
|
* |
|
* sprite.setAttributes({ |
|
* fill: 'url(#gradientId)' |
|
* }, true); |
|
*/ |
|
/** |
|
* @cfg {Ext.data.Store} store |
|
* The store that supplies data to this chart. |
|
*/ |
|
/** |
|
* @cfg {Ext.chart.series.Series[]} series |
|
* Array of {@link Ext.chart.series.Series Series} instances or config objects. For example: |
|
* |
|
* series: [{ |
|
* type: 'column', |
|
* axis: 'left', |
|
* listeners: { |
|
* 'afterrender': function() { |
|
* console('afterrender'); |
|
* } |
|
* }, |
|
* xField: 'category', |
|
* yField: 'data1' |
|
* }] |
|
*/ |
|
/** |
|
* @cfg {Ext.chart.axis.Axis[]} axes |
|
* Array of {@link Ext.chart.axis.Axis Axis} instances or config objects. For example: |
|
* |
|
* axes: [{ |
|
* type: 'Numeric', |
|
* position: 'left', |
|
* fields: ['data1'], |
|
* title: 'Number of Hits', |
|
* minimum: 0, |
|
* //one minor tick between two major ticks |
|
* minorTickSteps: 1 |
|
* }, { |
|
* type: 'Category', |
|
* position: 'bottom', |
|
* fields: ['name'], |
|
* title: 'Month of the Year' |
|
* }] |
|
*/ |
|
refreshBuffer: 1, |
|
/** |
|
* @event beforerefresh |
|
* Fires before a refresh to the chart data is called. If the beforerefresh handler returns false the |
|
* {@link #event-refresh} action will be cancelled. |
|
* @param {Ext.chart.Chart} this |
|
*/ |
|
/** |
|
* @event refresh |
|
* Fires after the chart data has been refreshed. |
|
* @param {Ext.chart.Chart} this |
|
*/ |
|
constructor: function(config) { |
|
var me = this, |
|
defaultAnim; |
|
config = Ext.apply({}, config); |
|
me.initTheme(config.theme || me.theme); |
|
if (me.gradients) { |
|
Ext.apply(config, { |
|
gradients: me.gradients |
|
}); |
|
} |
|
if (me.background) { |
|
Ext.apply(config, { |
|
background: me.background |
|
}); |
|
} |
|
if (config.animate) { |
|
defaultAnim = { |
|
easing: 'ease', |
|
duration: 500 |
|
}; |
|
if (Ext.isObject(config.animate)) { |
|
config.animate = Ext.applyIf(config.animate, defaultAnim); |
|
} else { |
|
config.animate = defaultAnim; |
|
} |
|
} |
|
me.mixins.observable.constructor.call(me, config); |
|
if (config.mask) { |
|
config = Ext.apply({ |
|
enableMask: true |
|
}, config); |
|
} |
|
if (config.enableMask) { |
|
me.mixins.mask.constructor.call(me, config); |
|
} |
|
me.mixins.navigation.constructor.call(me); |
|
me.callParent([ |
|
config |
|
]); |
|
}, |
|
getChartStore: function() { |
|
return this.substore || this.store; |
|
}, |
|
initComponent: function() { |
|
var me = this, |
|
axes, series; |
|
me.callParent(); |
|
Ext.applyIf(me, { |
|
zoom: { |
|
width: 1, |
|
height: 1, |
|
x: 0, |
|
y: 0 |
|
} |
|
}); |
|
me.maxGutters = { |
|
left: 0, |
|
right: 0, |
|
bottom: 0, |
|
top: 0 |
|
}; |
|
me.store = Ext.data.StoreManager.lookup(me.store); |
|
axes = me.axes; |
|
me.axes = new Ext.util.MixedCollection(false, function(a) { |
|
return a.position; |
|
}); |
|
if (axes) { |
|
me.axes.addAll(axes); |
|
} |
|
series = me.series; |
|
me.series = new Ext.util.MixedCollection(false, function(a) { |
|
return a.seriesId || (a.seriesId = Ext.id(null, 'ext-chart-series-')); |
|
}); |
|
if (series) { |
|
me.series.addAll(series); |
|
} |
|
if (me.legend !== false) { |
|
me.legend = new Ext.chart.Legend(Ext.applyIf({ |
|
chart: me |
|
}, me.legend)); |
|
} |
|
me.on({ |
|
mousemove: me.onMouseMove, |
|
mouseleave: me.onMouseLeave, |
|
mousedown: me.onMouseDown, |
|
mouseup: me.onMouseUp, |
|
click: me.onClick, |
|
dblclick: me.onDblClick, |
|
scope: me |
|
}); |
|
}, |
|
// @private overrides the component method to set the correct dimensions to the chart. |
|
afterComponentLayout: function(width, height, oldWidth, oldHeight) { |
|
var me = this; |
|
if (Ext.isNumber(width) && Ext.isNumber(height)) { |
|
if (width !== oldWidth || height !== oldHeight) { |
|
me.curWidth = width; |
|
me.curHeight = height; |
|
me.redraw(true); |
|
me.needsRedraw = false; |
|
} else if (me.needsRedraw) { |
|
me.redraw(); |
|
me.needsRedraw = false; |
|
} |
|
} |
|
this.callParent(arguments); |
|
}, |
|
/** |
|
* Redraws the chart. If animations are set this will animate the chart too. |
|
* @param {Boolean} resize (optional) flag which changes the default origin points of the chart for animations. |
|
*/ |
|
redraw: function(resize) { |
|
var me = this, |
|
seriesItems = me.series.items, |
|
seriesLen = seriesItems.length, |
|
axesItems = me.axes.items, |
|
axesLen = axesItems.length, |
|
themeIndex = 0, |
|
i, item, |
|
chartBBox = me.chartBBox = { |
|
x: 0, |
|
y: 0, |
|
height: me.curHeight, |
|
width: me.curWidth |
|
}, |
|
legend = me.legend, |
|
series; |
|
me.surface.setSize(chartBBox.width, chartBBox.height); |
|
// Instantiate Series and Axes |
|
for (i = 0; i < seriesLen; i++) { |
|
item = seriesItems[i]; |
|
if (!item.initialized) { |
|
series = me.initializeSeries(item, i, themeIndex); |
|
} else { |
|
series = item; |
|
} |
|
// Allow the series to react to a redraw, for example, a pie series |
|
// backed by a remote data set needs to build legend labels correctly |
|
series.onRedraw(); |
|
// For things like stacked bar charts, a single series can consume |
|
// multiple colors from the index, so we compensate for it here |
|
if (Ext.isArray(item.yField)) { |
|
themeIndex += item.yField.length; |
|
} else { |
|
++themeIndex; |
|
} |
|
} |
|
for (i = 0; i < axesLen; i++) { |
|
item = axesItems[i]; |
|
if (!item.initialized) { |
|
me.initializeAxis(item); |
|
} |
|
} |
|
//process all views (aggregated data etc) on stores |
|
//before rendering. |
|
for (i = 0; i < axesLen; i++) { |
|
axesItems[i].processView(); |
|
} |
|
for (i = 0; i < axesLen; i++) { |
|
axesItems[i].drawAxis(true); |
|
} |
|
// Create legend if not already created |
|
if (legend !== false && legend.visible) { |
|
if (legend.update || !legend.created) { |
|
legend.create(); |
|
} |
|
} |
|
// Place axes properly, including influence from each other |
|
me.alignAxes(); |
|
// Reposition legend based on new axis alignment |
|
if (legend !== false && legend.visible) { |
|
legend.updatePosition(); |
|
} |
|
// Find the max gutters |
|
me.getMaxGutters(); |
|
// Draw axes and series |
|
me.resizing = !!resize; |
|
for (i = 0; i < axesLen; i++) { |
|
axesItems[i].drawAxis(); |
|
} |
|
for (i = 0; i < seriesLen; i++) { |
|
me.drawCharts(seriesItems[i]); |
|
} |
|
me.resizing = false; |
|
}, |
|
// @private set the store after rendering the chart. |
|
afterRender: function() { |
|
var me = this, |
|
legend = me.legend; |
|
me.callParent(arguments); |
|
if (me.categoryNames) { |
|
me.setCategoryNames(me.categoryNames); |
|
} |
|
if (legend) { |
|
legend.init(); |
|
} |
|
me.bindStore(me.store, true); |
|
me.refresh(); |
|
if (me.surface.engine === 'Vml') { |
|
me.on('added', me.onAddedVml, me); |
|
me.mon(Ext.GlobalEvents, 'added', me.onContainerAddedVml, me); |
|
} |
|
}, |
|
// When using a vml surface we need to redraw when this chart or one of its ancestors |
|
// is moved to a new container after render, because moving the vml chart causes the |
|
// vml elements to go haywire, some displaing incorrectly or not displaying at all. |
|
// This appears to be caused by the component being moved to the detached body element |
|
// before being added to the new container. |
|
onAddedVml: function() { |
|
this.needsRedraw = true; |
|
}, |
|
// redraw after component layout |
|
onContainerAddedVml: function(container) { |
|
if (this.isDescendantOf(container)) { |
|
this.needsRedraw = true; |
|
} |
|
}, |
|
// redraw after component layout |
|
// @private get x and y position of the mouse cursor. |
|
getEventXY: function(e) { |
|
var box = this.surface.getRegion(), |
|
pageXY = e.getXY(), |
|
x = pageXY[0] - box.left, |
|
y = pageXY[1] - box.top; |
|
return [ |
|
x, |
|
y |
|
]; |
|
}, |
|
onClick: function(e) { |
|
this.handleClick('itemclick', e); |
|
}, |
|
onDblClick: function(e) { |
|
this.handleClick('itemdblclick', e); |
|
}, |
|
// @private wrap the mouse down position to delegate the event to the series. |
|
handleClick: function(name, e) { |
|
var me = this, |
|
position = me.getEventXY(e), |
|
seriesItems = me.series.items, |
|
i, ln, series, item; |
|
// Ask each series if it has an item corresponding to (not necessarily exactly |
|
// on top of) the current mouse coords. Fire itemclick event. |
|
for (i = 0 , ln = seriesItems.length; i < ln; i++) { |
|
series = seriesItems[i]; |
|
if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) { |
|
if (series.getItemForPoint) { |
|
item = series.getItemForPoint(position[0], position[1]); |
|
if (item) { |
|
series.fireEvent(name, item); |
|
} |
|
} |
|
} |
|
} |
|
}, |
|
// @private wrap the mouse down position to delegate the event to the series. |
|
onMouseDown: function(e) { |
|
var me = this, |
|
position = me.getEventXY(e), |
|
seriesItems = me.series.items, |
|
i, ln, series, item; |
|
if (me.enableMask) { |
|
me.mixins.mask.onMouseDown.call(me, e); |
|
} |
|
// Ask each series if it has an item corresponding to (not necessarily exactly |
|
// on top of) the current mouse coords. Fire itemmousedown event. |
|
for (i = 0 , ln = seriesItems.length; i < ln; i++) { |
|
series = seriesItems[i]; |
|
if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) { |
|
if (series.getItemForPoint) { |
|
item = series.getItemForPoint(position[0], position[1]); |
|
if (item) { |
|
series.fireEvent('itemmousedown', item); |
|
} |
|
} |
|
} |
|
} |
|
}, |
|
// @private wrap the mouse up event to delegate it to the series. |
|
onMouseUp: function(e) { |
|
var me = this, |
|
position = me.getEventXY(e), |
|
seriesItems = me.series.items, |
|
i, ln, series, item; |
|
if (me.enableMask) { |
|
me.mixins.mask.onMouseUp.call(me, e); |
|
} |
|
// Ask each series if it has an item corresponding to (not necessarily exactly |
|
// on top of) the current mouse coords. Fire itemmouseup event. |
|
for (i = 0 , ln = seriesItems.length; i < ln; i++) { |
|
series = seriesItems[i]; |
|
if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) { |
|
if (series.getItemForPoint) { |
|
item = series.getItemForPoint(position[0], position[1]); |
|
if (item) { |
|
series.fireEvent('itemmouseup', item); |
|
} |
|
} |
|
} |
|
} |
|
}, |
|
// @private wrap the mouse move event so it can be delegated to the series. |
|
onMouseMove: function(e) { |
|
var me = this, |
|
position = me.getEventXY(e), |
|
seriesItems = me.series.items, |
|
i, ln, series, item, last, storeItem, storeField; |
|
if (me.enableMask) { |
|
me.mixins.mask.onMouseMove.call(me, e); |
|
} |
|
// Ask each series if it has an item corresponding to (not necessarily exactly |
|
// on top of) the current mouse coords. Fire itemmouseover/out events. |
|
for (i = 0 , ln = seriesItems.length; i < ln; i++) { |
|
series = seriesItems[i]; |
|
if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) { |
|
if (series.getItemForPoint) { |
|
item = series.getItemForPoint(position[0], position[1]); |
|
last = series._lastItemForPoint; |
|
storeItem = series._lastStoreItem; |
|
storeField = series._lastStoreField; |
|
if (item !== last || item && (item.storeItem != storeItem || item.storeField != storeField)) { |
|
if (last) { |
|
series.fireEvent('itemmouseout', last); |
|
delete series._lastItemForPoint; |
|
delete series._lastStoreField; |
|
delete series._lastStoreItem; |
|
} |
|
if (item) { |
|
series.fireEvent('itemmouseover', item); |
|
series._lastItemForPoint = item; |
|
series._lastStoreItem = item.storeItem; |
|
series._lastStoreField = item.storeField; |
|
} |
|
} |
|
} |
|
} else { |
|
last = series._lastItemForPoint; |
|
if (last) { |
|
series.fireEvent('itemmouseout', last); |
|
delete series._lastItemForPoint; |
|
delete series._lastStoreField; |
|
delete series._lastStoreItem; |
|
} |
|
} |
|
} |
|
}, |
|
// @private handle mouse leave event. |
|
onMouseLeave: function(e) { |
|
var me = this, |
|
seriesItems = me.series.items, |
|
i, ln, series; |
|
if (me.enableMask) { |
|
me.mixins.mask.onMouseLeave.call(me, e); |
|
} |
|
for (i = 0 , ln = seriesItems.length; i < ln; i++) { |
|
series = seriesItems[i]; |
|
delete series._lastItemForPoint; |
|
} |
|
}, |
|
// @private buffered refresh for when we update the store |
|
delayRefresh: function() { |
|
var me = this; |
|
if (!me.refreshTask) { |
|
me.refreshTask = new Ext.util.DelayedTask(me.refresh, me); |
|
} |
|
me.refreshTask.delay(me.refreshBuffer); |
|
}, |
|
// @private |
|
refresh: function() { |
|
var me = this; |
|
if (me.rendered && me.curWidth !== undefined && me.curHeight !== undefined) { |
|
if (!me.isVisible(true)) { |
|
if (!me.refreshPending) { |
|
me.setShowListeners('mon'); |
|
me.refreshPending = true; |
|
} |
|
return; |
|
} |
|
if (me.fireEvent('beforerefresh', me) !== false) { |
|
me.redraw(); |
|
me.fireEvent('refresh', me); |
|
} |
|
} |
|
}, |
|
onShow: function() { |
|
var me = this; |
|
me.callParent(arguments); |
|
if (me.refreshPending) { |
|
me.delayRefresh(); |
|
me.setShowListeners('mun'); |
|
} |
|
delete me.refreshPending; |
|
}, |
|
setShowListeners: function(method) { |
|
var me = this; |
|
me[method](Ext.GlobalEvents, { |
|
scope: me, |
|
single: true, |
|
show: me.forceRefresh, |
|
expand: me.forceRefresh |
|
}); |
|
}, |
|
doRefresh: function() { |
|
// Data in the main store has changed, clear the sub store |
|
this.setSubStore(null); |
|
this.refresh(); |
|
}, |
|
forceRefresh: function(container) { |
|
var me = this; |
|
if (me.isDescendantOf(container) && me.refreshPending) { |
|
// Add unbind here, because either expand/show could be fired, |
|
// so be sure to unbind the listener that didn't |
|
me.setShowListeners('mun'); |
|
me.delayRefresh(); |
|
} |
|
delete me.refreshPending; |
|
}, |
|
bindStore: function(store, initial) { |
|
var me = this; |
|
me.mixins.storeholder.bindStore.apply(me, arguments); |
|
if (me.store && !initial) { |
|
me.refresh(); |
|
} |
|
}, |
|
getStoreListeners: function() { |
|
var refresh = this.doRefresh, |
|
delayRefresh = this.delayRefresh; |
|
return { |
|
refresh: refresh, |
|
add: delayRefresh, |
|
remove: delayRefresh, |
|
update: delayRefresh, |
|
clear: refresh |
|
}; |
|
}, |
|
setSubStore: function(subStore) { |
|
this.substore = subStore; |
|
}, |
|
// @private Create Axis |
|
initializeAxis: function(axis) { |
|
var me = this, |
|
chartBBox = me.chartBBox, |
|
w = chartBBox.width, |
|
h = chartBBox.height, |
|
x = chartBBox.x, |
|
y = chartBBox.y, |
|
themeAttrs = me.themeAttrs, |
|
axes = me.axes, |
|
config = { |
|
chart: me |
|
}; |
|
if (themeAttrs) { |
|
config.axisStyle = Ext.apply({}, themeAttrs.axis); |
|
config.axisLabelLeftStyle = Ext.apply({}, themeAttrs.axisLabelLeft); |
|
config.axisLabelRightStyle = Ext.apply({}, themeAttrs.axisLabelRight); |
|
config.axisLabelTopStyle = Ext.apply({}, themeAttrs.axisLabelTop); |
|
config.axisLabelBottomStyle = Ext.apply({}, themeAttrs.axisLabelBottom); |
|
config.axisTitleLeftStyle = Ext.apply({}, themeAttrs.axisTitleLeft); |
|
config.axisTitleRightStyle = Ext.apply({}, themeAttrs.axisTitleRight); |
|
config.axisTitleTopStyle = Ext.apply({}, themeAttrs.axisTitleTop); |
|
config.axisTitleBottomStyle = Ext.apply({}, themeAttrs.axisTitleBottom); |
|
me.configureAxisStyles(config); |
|
} |
|
switch (axis.position) { |
|
case 'top': |
|
Ext.apply(config, { |
|
length: w, |
|
width: h, |
|
x: x, |
|
y: y |
|
}); |
|
break; |
|
case 'bottom': |
|
Ext.apply(config, { |
|
length: w, |
|
width: h, |
|
x: x, |
|
y: h |
|
}); |
|
break; |
|
case 'left': |
|
Ext.apply(config, { |
|
length: h, |
|
width: w, |
|
x: x, |
|
y: h |
|
}); |
|
break; |
|
case 'right': |
|
Ext.apply(config, { |
|
length: h, |
|
width: w, |
|
x: w, |
|
y: h |
|
}); |
|
break; |
|
} |
|
if (!axis.chart) { |
|
Ext.apply(config, axis); |
|
axis = Ext.createByAlias('axis.' + axis.type.toLowerCase(), config); |
|
axes.replace(axis); |
|
} else { |
|
Ext.apply(axis, config); |
|
} |
|
axis.initialized = true; |
|
}, |
|
configureAxisStyles: Ext.emptyFn, |
|
/** |
|
* @private Get initial insets; override to provide different defaults. |
|
*/ |
|
getInsets: function() { |
|
var me = this, |
|
insetPadding = me.insetPadding; |
|
return { |
|
top: insetPadding, |
|
right: insetPadding, |
|
bottom: insetPadding, |
|
left: insetPadding |
|
}; |
|
}, |
|
/** |
|
* @private Calculate insets for the Chart. |
|
*/ |
|
calculateInsets: function() { |
|
var me = this, |
|
legend = me.legend, |
|
axes = me.axes, |
|
edges = [ |
|
'top', |
|
'right', |
|
'bottom', |
|
'left' |
|
], |
|
insets, i, l, edge, isVertical, axis, bbox; |
|
function getAxis(edge) { |
|
var i = axes.findIndex('position', edge); |
|
return (i < 0) ? null : axes.getAt(i); |
|
} |
|
insets = me.getInsets(); |
|
// Find the space needed by axes and legend as a positive inset from each edge |
|
for (i = 0 , l = edges.length; i < l; i++) { |
|
edge = edges[i]; |
|
isVertical = (edge === 'left' || edge === 'right'); |
|
axis = getAxis(edge); |
|
// Add legend size if it's on this edge |
|
if (legend !== false) { |
|
if (legend.position === edge) { |
|
bbox = legend.getBBox(); |
|
insets[edge] += (isVertical ? bbox.width : bbox.height) + me.insetPadding; |
|
} |
|
} |
|
// Add axis size if there's one on this edge only if it has been |
|
//drawn before. |
|
if (axis && axis.bbox) { |
|
bbox = axis.bbox; |
|
insets[edge] += (isVertical ? bbox.width : bbox.height); |
|
} |
|
} |
|
return insets; |
|
}, |
|
/** |
|
* @private Adjust the dimensions and positions of each axis and the chart body area after accounting |
|
* for the space taken up on each side by the axes and legend. |
|
* This code is taken from Ext.chart.Chart and refactored to provide better flexibility. |
|
*/ |
|
alignAxes: function() { |
|
var me = this, |
|
axesItems = me.axes.items, |
|
insets, chartBBox, i, l, axis, pos, isVertical; |
|
insets = me.calculateInsets(); |
|
// Build the chart bbox based on the collected inset values |
|
chartBBox = { |
|
x: insets.left, |
|
y: insets.top, |
|
width: me.curWidth - insets.left - insets.right, |
|
height: me.curHeight - insets.top - insets.bottom |
|
}; |
|
me.chartBBox = chartBBox; |
|
// Go back through each axis and set its length and position based on the |
|
// corresponding edge of the chartBBox |
|
for (i = 0 , l = axesItems.length; i < l; i++) { |
|
axis = axesItems[i]; |
|
pos = axis.position; |
|
isVertical = pos === 'left' || pos === 'right'; |
|
axis.x = (pos === 'right' ? chartBBox.x + chartBBox.width : chartBBox.x); |
|
axis.y = (pos === 'top' ? chartBBox.y : chartBBox.y + chartBBox.height); |
|
axis.width = (isVertical ? chartBBox.width : chartBBox.height); |
|
axis.length = (isVertical ? chartBBox.height : chartBBox.width); |
|
} |
|
}, |
|
// @private initialize the series. |
|
initializeSeries: function(series, idx, themeIndex) { |
|
var me = this, |
|
themeAttrs = me.themeAttrs, |
|
seriesObj, markerObj, seriesThemes, st, markerThemes, |
|
colorArrayStyle = [], |
|
isInstance = (series instanceof Ext.chart.series.Series).i = 0, |
|
l, config; |
|
if (!series.initialized) { |
|
config = { |
|
chart: me, |
|
seriesId: series.seriesId |
|
}; |
|
if (themeAttrs) { |
|
seriesThemes = themeAttrs.seriesThemes; |
|
markerThemes = themeAttrs.markerThemes; |
|
seriesObj = Ext.apply({}, themeAttrs.series); |
|
markerObj = Ext.apply({}, themeAttrs.marker); |
|
config.seriesStyle = Ext.apply(seriesObj, seriesThemes[themeIndex % seriesThemes.length]); |
|
config.seriesLabelStyle = Ext.apply({}, themeAttrs.seriesLabel); |
|
config.markerStyle = Ext.apply(markerObj, markerThemes[themeIndex % markerThemes.length]); |
|
if (themeAttrs.colors) { |
|
config.colorArrayStyle = themeAttrs.colors; |
|
} else { |
|
colorArrayStyle = []; |
|
for (l = seriesThemes.length; i < l; i++) { |
|
st = seriesThemes[i]; |
|
if (st.fill || st.stroke) { |
|
colorArrayStyle.push(st.fill || st.stroke); |
|
} |
|
} |
|
if (colorArrayStyle.length) { |
|
config.colorArrayStyle = colorArrayStyle; |
|
} |
|
} |
|
config.seriesIdx = idx; |
|
config.themeIdx = themeIndex; |
|
} |
|
if (isInstance) { |
|
Ext.applyIf(series, config); |
|
} else { |
|
Ext.applyIf(config, series); |
|
series = me.series.replace(Ext.createByAlias('series.' + series.type.toLowerCase(), config)); |
|
} |
|
} |
|
series.initialize(); |
|
series.initialized = true; |
|
return series; |
|
}, |
|
// @private |
|
getMaxGutters: function() { |
|
var me = this, |
|
seriesItems = me.series.items, |
|
i, ln, series, gutters, |
|
lowerH = 0, |
|
upperH = 0, |
|
lowerV = 0, |
|
upperV = 0; |
|
for (i = 0 , ln = seriesItems.length; i < ln; i++) { |
|
gutters = seriesItems[i].getGutters(); |
|
if (gutters) { |
|
if (gutters.verticalAxis) { |
|
lowerV = Math.max(lowerV, gutters.lower); |
|
upperV = Math.max(upperV, gutters.upper); |
|
} else { |
|
lowerH = Math.max(lowerH, gutters.lower); |
|
upperH = Math.max(upperH, gutters.upper); |
|
} |
|
} |
|
} |
|
me.maxGutters = { |
|
left: lowerH, |
|
right: upperH, |
|
bottom: lowerV, |
|
top: upperV |
|
}; |
|
}, |
|
// @private draw axis. |
|
drawAxis: function(axis) { |
|
axis.drawAxis(); |
|
}, |
|
// @private draw series. |
|
drawCharts: function(series) { |
|
series.triggerafterrender = false; |
|
series.drawSeries(); |
|
if (!this.animate) { |
|
series.fireEvent('afterrender', series); |
|
} |
|
}, |
|
/** |
|
* Saves the chart by either triggering a download or returning a string containing the chart data |
|
* as SVG. The action depends on the export type specified in the passed configuration. The chart |
|
* will be exported using either the {@link Ext.draw.engine.SvgExporter} or the {@link Ext.draw.engine.ImageExporter} |
|
* classes. |
|
* |
|
* Possible export types: |
|
* |
|
* - 'image/png' |
|
* - 'image/jpeg', |
|
* - 'image/svg+xml' |
|
* |
|
* If 'image/svg+xml' is specified, the SvgExporter will be used. |
|
* If 'image/png' or 'image/jpeg' are specified, the ImageExporter will be used. This exporter |
|
* must post the SVG data to a remote server to have the data processed, see the {@link Ext.draw.engine.ImageExporter} |
|
* for more details. |
|
* |
|
* Example usage: |
|
* |
|
* chart.save({ |
|
* type: 'image/png' |
|
* }); |
|
* |
|
* **Important**: By default, chart data is sent to a server operated |
|
* by Sencha to do data processing. You may change this default by |
|
* setting the {@link Ext.draw.engine.ImageExporter#defaultUrl defaultUrl} of the {@link Ext.draw.engine.ImageExporter} class. |
|
* In addition, please note that this service only creates PNG images. |
|
* |
|
* @param {Object} [config] The configuration to be passed to the exporter. |
|
* See the export method for the appropriate exporter for the relevant |
|
* configuration options |
|
* @return {Object} See the return types for the appropriate exporter |
|
*/ |
|
save: function(config) { |
|
return Ext.draw.Surface.save(this.surface, config); |
|
}, |
|
// @private remove gently. |
|
destroy: function() { |
|
var me = this, |
|
task = me.refreshTask; |
|
if (task) { |
|
task.cancel(); |
|
me.refreshTask = null; |
|
} |
|
// We don't have to destroy the surface here because |
|
// parent Draw component will do that |
|
me.bindStore(null); |
|
me.callParent(arguments); |
|
} |
|
}); |
|
|
|
Ext.define('Ext.rtl.chart.Chart', { |
|
override: 'Ext.chart.Chart', |
|
initSurfaceCfg: function(cfg) { |
|
this.callParent(arguments); |
|
// Even in rtl mode, we still want the chart to use ltr, since |
|
// we're just reversing the axes |
|
cfg.forceLtr = true; |
|
}, |
|
configureAxisStyles: function(config) { |
|
var temp; |
|
if (this.getInherited().rtl) { |
|
temp = config.axisLabelLeftStyle; |
|
config.axisLabelLeftStyle = config.axisLabelRightStyle; |
|
config.axisLabelRightStyle = temp; |
|
temp = config.axisTitleLeftStyle; |
|
config.axisTitleLeftStyle = config.axisTitleRightStyle; |
|
config.axisTitleRightStyle = temp; |
|
} |
|
}, |
|
beforeRender: function() { |
|
// Put this here because by this point we definitely know that we've been added to a container |
|
// so we can identify the hierarchy state. Since the collection is keyed by side, we'll go ahead |
|
// and do all our modifications before everything is initialized ~and~ we know our RTL state |
|
var me = this, |
|
axes = me.axes, |
|
items, i, len, axis; |
|
if (me.getInherited().rtl) { |
|
// There are 2 cases for RTL: |
|
// The root is LTR & we are RTL, in which case we don't reverse the events |
|
// The root is RTL & we are RTL, in we which need to re-LTRify the events, since |
|
// the charts always lay out in an LTR fashion. |
|
me.rtlEvent = !me.isOppositeRootDirection(); |
|
items = axes.getRange(); |
|
axes.removeAll(); |
|
for (i = 0 , len = items.length; i < len; ++i) { |
|
axis = items[i]; |
|
axis.position = this.invertPosition(axis.position); |
|
axes.add(axis); |
|
} |
|
} |
|
me.callParent(arguments); |
|
}, |
|
invertPosition: function(pos) { |
|
if (Ext.isArray(pos)) { |
|
var out = [], |
|
len = pos.length, |
|
i; |
|
for (i = 0; i < len; ++i) { |
|
out.push(this.invertPosition(pos[i])); |
|
} |
|
return out; |
|
} |
|
if (this.getInherited().rtl) { |
|
if (pos == 'left') { |
|
pos = 'right'; |
|
} else if (pos == 'right') { |
|
pos = 'left'; |
|
} |
|
} |
|
return pos; |
|
}, |
|
getEventXY: function(e) { |
|
var box, pageXY, x, y, width; |
|
if (this.rtlEvent) { |
|
// If we're in RTL mode, the event coordinates have been reversed, |
|
// so we need to modify them to get them back to a useful |
|
// state for us! |
|
box = this.surface.getRegion(); |
|
pageXY = e.getXY(); |
|
width = box.right - box.left; |
|
x = width - (pageXY[0] - box.left); |
|
y = pageXY[1] - box.top; |
|
return [ |
|
x, |
|
y |
|
]; |
|
} else { |
|
return this.callParent(arguments); |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.Highlight |
|
* A mixin providing highlight functionality for Ext.chart.series.Series. |
|
*/ |
|
Ext.define('Ext.chart.Highlight', { |
|
/* Begin Definitions */ |
|
requires: [ |
|
'Ext.fx.Anim' |
|
], |
|
/* End Definitions */ |
|
/** |
|
* @cfg {Boolean/Object} [highlight=false] Set to `true` to enable highlighting using the {@link #highlightCfg default highlight attributes}. |
|
* |
|
* Can also be an object with style properties (i.e fill, stroke, stroke-width, radius) which are may override the {@link #highlightCfg default highlight attributes}. |
|
*/ |
|
highlight: false, |
|
/** |
|
* @property {Object} highlightCfg The default properties to apply as a highight. Value is |
|
* |
|
* { |
|
* fill: '#fdd', |
|
* "stroke-width": 5, |
|
* stroke: "#f55' |
|
* } |
|
*/ |
|
highlightCfg: { |
|
fill: '#fdd', |
|
"stroke-width": 5, |
|
stroke: '#f55' |
|
}, |
|
constructor: function(config) { |
|
// If configured with a highlight object, apply to to *a local copy of* this class's highlightCfg. Do not mutate the prototype's copy. |
|
if (config.highlight && (typeof config.highlight !== 'boolean')) { |
|
//is an object |
|
this.highlightCfg = Ext.merge({}, this.highlightCfg, config.highlight); |
|
} |
|
}, |
|
/** |
|
* Highlight the given series item. |
|
* @param {Object} item Info about the item; same format as returned by #getItemForPoint. |
|
*/ |
|
highlightItem: function(item) { |
|
if (!item) { |
|
return; |
|
} |
|
var me = this, |
|
sprite = item.sprite, |
|
opts = Ext.merge({}, me.highlightCfg, me.highlight), |
|
surface = me.chart.surface, |
|
animate = me.chart.animate, |
|
p, from, to, pi; |
|
if (!me.highlight || !sprite || sprite._highlighted) { |
|
return; |
|
} |
|
if (sprite._anim) { |
|
sprite._anim.paused = true; |
|
} |
|
sprite._highlighted = true; |
|
if (!sprite._defaults) { |
|
sprite._defaults = Ext.apply({}, sprite.attr); |
|
from = {}; |
|
to = {}; |
|
// TODO: Clean up code below. |
|
for (p in opts) { |
|
if (!(p in sprite._defaults)) { |
|
sprite._defaults[p] = surface.availableAttrs[p]; |
|
} |
|
from[p] = sprite._defaults[p]; |
|
to[p] = opts[p]; |
|
if (Ext.isObject(opts[p])) { |
|
from[p] = {}; |
|
to[p] = {}; |
|
Ext.apply(sprite._defaults[p], sprite.attr[p]); |
|
Ext.apply(from[p], sprite._defaults[p]); |
|
for (pi in sprite._defaults[p]) { |
|
if (!(pi in opts[p])) { |
|
to[p][pi] = from[p][pi]; |
|
} else { |
|
to[p][pi] = opts[p][pi]; |
|
} |
|
} |
|
for (pi in opts[p]) { |
|
if (!(pi in to[p])) { |
|
to[p][pi] = opts[p][pi]; |
|
} |
|
} |
|
} |
|
} |
|
sprite._from = from; |
|
sprite._to = to; |
|
sprite._endStyle = to; |
|
} |
|
if (animate) { |
|
sprite._anim = new Ext.fx.Anim({ |
|
target: sprite, |
|
from: sprite._from, |
|
to: sprite._to, |
|
duration: 150 |
|
}); |
|
} else { |
|
sprite.setAttributes(sprite._to, true); |
|
} |
|
}, |
|
/** |
|
* Un-highlight any existing highlights |
|
*/ |
|
unHighlightItem: function() { |
|
if (!this.highlight || !this.items) { |
|
return; |
|
} |
|
var me = this, |
|
items = me.items, |
|
len = items.length, |
|
opts = Ext.merge({}, me.highlightCfg, me.highlight), |
|
animate = me.chart.animate, |
|
i = 0, |
|
obj, p, sprite; |
|
for (; i < len; i++) { |
|
if (!items[i]) { |
|
|
|
continue; |
|
} |
|
sprite = items[i].sprite; |
|
if (sprite && sprite._highlighted) { |
|
if (sprite._anim) { |
|
sprite._anim.paused = true; |
|
} |
|
obj = {}; |
|
for (p in opts) { |
|
if (Ext.isObject(sprite._defaults[p])) { |
|
obj[p] = Ext.apply({}, sprite._defaults[p]); |
|
} else { |
|
obj[p] = sprite._defaults[p]; |
|
} |
|
} |
|
if (animate) { |
|
//sprite._to = obj; |
|
sprite._endStyle = obj; |
|
sprite._anim = new Ext.fx.Anim({ |
|
target: sprite, |
|
to: obj, |
|
duration: 150 |
|
}); |
|
} else { |
|
sprite.setAttributes(obj, true); |
|
} |
|
delete sprite._highlighted; |
|
} |
|
} |
|
}, |
|
//delete sprite._defaults; |
|
cleanHighlights: function() { |
|
if (!this.highlight) { |
|
return; |
|
} |
|
var group = this.group, |
|
markerGroup = this.markerGroup, |
|
i = 0, |
|
l; |
|
for (l = group.getCount(); i < l; i++) { |
|
delete group.getAt(i)._defaults; |
|
} |
|
if (markerGroup) { |
|
for (l = markerGroup.getCount(); i < l; i++) { |
|
delete markerGroup.getAt(i)._defaults; |
|
} |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* Labels is a mixin to the Series class. Labels methods are implemented |
|
* in each of the Series (Pie, Bar, etc) for label creation and placement. |
|
* |
|
* The 2 methods that must be implemented by the Series are: |
|
* |
|
* - {@link #onCreateLabel} |
|
* - {@link #onPlaceLabel} |
|
* |
|
* The application can override these methods to control the style and |
|
* location of the labels. For instance, to display the labels in green and |
|
* add a '+' symbol when the value of a Line series exceeds 50: |
|
* |
|
* Ext.define('Ext.chart.series.MyLine', { |
|
* extend: 'Ext.chart.series.Line', |
|
* alias: ['series.myline', 'Ext.chart.series.MyLine'], |
|
* type: 'MYLINE', |
|
* |
|
* onPlaceLabel: function(label, storeItem, item, i, display, animate){ |
|
* if (storeItem.data.y >= 50) { |
|
* label.setAttributes({ |
|
* fill: '#080', |
|
* text: "+" + storeItem.data.y |
|
* }, true); |
|
* } |
|
* return this.callParent(arguments); |
|
* } |
|
* }); |
|
* |
|
* Note that for simple effects, like the example above, it is simpler |
|
* for the application to provide a label.renderer function in the config: |
|
* |
|
* label: { |
|
* renderer: function(value, label, storeItem, item, i, display, animate, index) { |
|
* if (value >= 50) { |
|
* label.setAttributes({fill:'#080'}); |
|
* value = "+" + value; |
|
* } |
|
* return value; |
|
* } |
|
* } |
|
* |
|
* The rule of thumb is that to customize the value and modify simple visual attributes, it |
|
* is simpler to use a renderer function, while overridding `onCreateLabel` and `onPlaceLabel` |
|
* allows the application to take entire control over the labels. |
|
* |
|
*/ |
|
Ext.define('Ext.chart.Label', { |
|
/* Begin Definitions */ |
|
requires: [ |
|
'Ext.draw.Color' |
|
], |
|
/* End Definitions */ |
|
/** |
|
* @method onCreateLabel |
|
* @template |
|
* |
|
* Called each time a new label is created. |
|
* |
|
* **Note:** This method must be implemented in Series that mixes |
|
* in this Label mixin. |
|
* |
|
* @param {Ext.data.Model} storeItem The element of the store that is |
|
* related to the sprite. |
|
* @param {Object} item The item related to the sprite. |
|
* An item is an object containing the position of the shape |
|
* used to describe the visualization and also pointing to the |
|
* actual shape (circle, rectangle, path, etc). |
|
* @param {Number} i The index of the element created |
|
* (i.e the first created label, second created label, etc). |
|
* @param {String} display The label.display type. |
|
* May be `false` if the label is hidden |
|
* @return {Ext.draw.Sprite} The created sprite that will draw the label. |
|
*/ |
|
/** |
|
* @method onPlaceLabel |
|
* @template |
|
* |
|
* Called for updating the position of the label. |
|
* |
|
* **Note:** This method must be implemented in Series that mixes |
|
* in this Label mixin. |
|
* |
|
* @param {Ext.draw.Sprite} label The sprite that draws the label. |
|
* @param {Ext.data.Model} storeItem The element of the store |
|
* that is related to the sprite. |
|
* @param {Object} item The item related to the |
|
* sprite. An item is an object containing the position of |
|
* the shape used to describe the visualization and also |
|
* pointing to the actual shape (circle, rectangle, path, etc). |
|
* @param {Number} i The index of the element to be updated |
|
* (i.e. whether it is the first, second, third from the |
|
* labelGroup) |
|
* @param {String} display The label.display type. |
|
* May be `false` if the label is hidden |
|
* @param {Boolean} animate A boolean value to set or unset |
|
* animations for the labels. |
|
* @param {Number} index The series index. |
|
*/ |
|
/** |
|
* @cfg {Object} label |
|
* Object with the following properties: |
|
* |
|
* @cfg {String} label.display |
|
* |
|
* Specifies the presence and position of the labels. The possible values depend on the chart type. |
|
* For Line and Scatter charts: "under" | "over" | "rotate". |
|
* For Bar and Column charts: "insideStart" | "insideEnd" | "outside". |
|
* For Pie charts: "inside" | "outside" | "rotate". |
|
* For all charts: "none" hides the labels and "middle" is reserved for future use. |
|
* On stacked Bar and stacked Column charts, if 'stackedDisplay' is set, the values |
|
* "over" or "under" can be passed internally to {@link #onCreateLabel} and {@link #onPlaceLabel} |
|
* (however they cannot be used by the application as config values for label.display). |
|
* |
|
* Default value: 'none'. |
|
* |
|
* @cfg {String} label.stackedDisplay |
|
* |
|
* The type of label we want to display as a summary on a stacked |
|
* bar or a stacked column. If set to 'total', the total amount |
|
* of all the stacked values is displayed on top of the column. |
|
* If set to 'balances', the total amount of the positive values |
|
* is displayed on top of the column and the total amount of the |
|
* negative values is displayed at the bottom. |
|
* |
|
* Default value: 'none'. |
|
* |
|
* @cfg {String} label.color |
|
* |
|
* The color of the label text. It can be specified in hex values |
|
* (eg. '#f00' or '#ff0000'), or as a CSS color name (eg. 'red'). |
|
* |
|
* Default value: '#000' (black). |
|
* |
|
* @cfg {Boolean} label.contrast |
|
* |
|
* True to render the label in contrasting color with the backround of a column |
|
* in a Bar chart or of a slice in a Pie chart. |
|
* |
|
* Default value: false. |
|
* |
|
* @cfg {String|String[]} label.field |
|
* |
|
* The name(s) of the field(s) to be displayed in the labels. If your chart has 3 series |
|
* that correspond to the fields 'a', 'b', and 'c' of your model and you only want to |
|
* display labels for the series 'c', you must still provide an array `[null, null, 'c']`. |
|
* |
|
* Default value: 'name'. |
|
* |
|
* @cfg {Number} label.minMargin |
|
* |
|
* Specifies the minimum distance from a label to the origin of |
|
* the visualization. This parameter is useful when using |
|
* PieSeries width variable pie slice lengths. |
|
* |
|
* Default value: 50. |
|
* |
|
* @cfg {Number} label.padding |
|
* |
|
* The distance between the label and the chart when the label is displayed |
|
* outside the chart. |
|
* |
|
* Default value: 20. |
|
* |
|
* @cfg {Boolean|Object} label.calloutLine |
|
* |
|
* True to draw a line between the label and the chart with the default settings, |
|
* or an Object that defines the 'color', 'width' and 'length' properties of the line. |
|
* This config is only applicable when the label is displayed outside the chart. |
|
* |
|
* Default value: false. |
|
* |
|
* @cfg {String} label.calloutLine.color |
|
* |
|
* The color of the line. It can be specified in hex values |
|
* (eg. '#f00' or '#ff0000'), or as a CSS color name (eg. 'red'). |
|
* By default, it uses the color of the pie slice. |
|
* |
|
* @cfg {Number} label.calloutLine.width |
|
* |
|
* The width of the line. |
|
* |
|
* Default value: 2. |
|
* |
|
* @cfg {Number} label.calloutLine.length |
|
* |
|
* The length of the line. By default, the length of the line is calculated taking |
|
* into account the {@link #label.padding} and the width and height of the label. |
|
* If specified, it should be larger than {@link #label.padding} otherwise the |
|
* line may cross the label itself. |
|
* |
|
* @cfg {String} label.font |
|
* |
|
* The font used for the labels. |
|
* |
|
* Default value: `"11px Helvetica, sans-serif"`. |
|
* |
|
* @cfg {String} label.orientation |
|
* |
|
* Either "horizontal" or "vertical". |
|
* |
|
* Default value: `"horizontal"`. |
|
* |
|
* @cfg {Function} label.renderer |
|
* |
|
* Optional function for formatting the label into a displayable value. |
|
* |
|
* The arguments to the method are: |
|
* |
|
* - *`value`* The value |
|
* - *`label`*, *`storeItem`*, *`item`*, *`i`*, *`display`*, *`animate`*, *`index`* |
|
* |
|
* Same arguments as {@link #onPlaceLabel}. |
|
* |
|
* Default value: `function(v) { return v; }` |
|
*/ |
|
// @private a regex to parse url type colors. |
|
colorStringRe: /url\s*\(\s*#([^\/)]+)\s*\)/, |
|
// @private the mixin constructor. Used internally by Series. |
|
constructor: function(config) { |
|
var me = this; |
|
me.label = Ext.applyIf(me.label || {}, { |
|
display: "none", |
|
stackedDisplay: "none", |
|
color: "#000", |
|
field: "name", |
|
minMargin: 50, |
|
font: "11px Helvetica, sans-serif", |
|
orientation: "horizontal", |
|
renderer: Ext.identityFn |
|
}); |
|
if (me.label.display !== 'none') { |
|
me.labelsGroup = me.chart.surface.getGroup(me.seriesId + '-labels'); |
|
} |
|
}, |
|
// @private a method to render all labels in the labelGroup |
|
renderLabels: function() { |
|
var me = this, |
|
chart = me.chart, |
|
gradients = chart.gradients, |
|
items = me.items, |
|
animate = chart.animate, |
|
config = me.label, |
|
display = config.display, |
|
stackedDisplay = config.stackedDisplay, |
|
format = config.renderer, |
|
color = config.color, |
|
field = [].concat(config.field), |
|
group = me.labelsGroup, |
|
groupLength = (group || 0) && group.length, |
|
store = me.chart.getChartStore(), |
|
len = store.getCount(), |
|
itemLength = (items || 0) && items.length, |
|
ratio = itemLength / len, |
|
gradientsCount = (gradients || 0) && gradients.length, |
|
Color = Ext.draw.Color, |
|
hides = [], |
|
gradient, i, count, groupIndex, index, j, k, colorStopTotal, colorStopIndex, colorStop, item, label, storeItem, sprite, spriteColor, spriteBrightness, labelColor, colorString, total, totalPositive, totalNegative, topText, bottomText; |
|
if (display == 'none' || !group) { |
|
return; |
|
} |
|
// no items displayed, hide all labels |
|
if (itemLength == 0) { |
|
while (groupLength--) { |
|
hides.push(groupLength); |
|
} |
|
} else { |
|
for (i = 0 , count = 0 , groupIndex = 0; i < len; i++) { |
|
index = 0; |
|
for (j = 0; j < ratio; j++) { |
|
item = items[count]; |
|
label = group.getAt(groupIndex); |
|
storeItem = store.getAt(i); |
|
//check the excludes |
|
while (this.__excludes && this.__excludes[index]) { |
|
index++; |
|
} |
|
if (!item && label) { |
|
label.hide(true); |
|
groupIndex++; |
|
} |
|
if (item && field[j]) { |
|
if (!label) { |
|
label = me.onCreateLabel(storeItem, item, i, display); |
|
if (!label) { |
|
break; |
|
} |
|
} |
|
// set color (the app can override it in onPlaceLabel) |
|
label.setAttributes({ |
|
fill: String(color) |
|
}, true); |
|
// position the label |
|
me.onPlaceLabel(label, storeItem, item, i, display, animate, index); |
|
groupIndex++; |
|
// set contrast |
|
if (config.contrast && item.sprite) { |
|
sprite = item.sprite; |
|
//set the color string to the color to be set, only read the |
|
// _endStyle/_to if we're animating, otherwise they're not relevant |
|
if (animate && sprite._endStyle) { |
|
colorString = sprite._endStyle.fill; |
|
} else if (animate && sprite._to) { |
|
colorString = sprite._to.fill; |
|
} else { |
|
colorString = sprite.attr.fill; |
|
} |
|
colorString = colorString || sprite.attr.fill; |
|
spriteColor = Color.fromString(colorString); |
|
//color wasn't parsed property maybe because it's a gradient id |
|
if (colorString && !spriteColor) { |
|
colorString = colorString.match(me.colorStringRe)[1]; |
|
for (k = 0; k < gradientsCount; k++) { |
|
gradient = gradients[k]; |
|
if (gradient.id == colorString) { |
|
//avg color stops |
|
colorStop = 0; |
|
colorStopTotal = 0; |
|
for (colorStopIndex in gradient.stops) { |
|
colorStop++; |
|
colorStopTotal += Color.fromString(gradient.stops[colorStopIndex].color).getGrayscale(); |
|
} |
|
spriteBrightness = (colorStopTotal / colorStop) / 255; |
|
break; |
|
} |
|
} |
|
} else { |
|
spriteBrightness = spriteColor.getGrayscale() / 255; |
|
} |
|
if (label.isOutside) { |
|
spriteBrightness = 1; |
|
} |
|
labelColor = Color.fromString(label.attr.fill || label.attr.color).getHSL(); |
|
labelColor[2] = spriteBrightness > 0.5 ? 0.2 : 0.8; |
|
label.setAttributes({ |
|
fill: String(Color.fromHSL.apply({}, labelColor)) |
|
}, true); |
|
} |
|
// display totals on stacked charts |
|
if (me.stacked && stackedDisplay && (item.totalPositiveValues || item.totalNegativeValues)) { |
|
totalPositive = (item.totalPositiveValues || 0); |
|
totalNegative = (item.totalNegativeValues || 0); |
|
total = totalPositive + totalNegative; |
|
if (stackedDisplay == 'total') { |
|
topText = format(total); |
|
} else if (stackedDisplay == 'balances') { |
|
if (totalPositive == 0 && totalNegative == 0) { |
|
topText = format(0); |
|
} else { |
|
topText = format(totalPositive); |
|
bottomText = format(totalNegative); |
|
} |
|
} |
|
if (topText) { |
|
label = group.getAt(groupIndex); |
|
if (!label) { |
|
label = me.onCreateLabel(storeItem, item, i, 'over'); |
|
} |
|
labelColor = Color.fromString(label.attr.color || label.attr.fill).getHSL(); |
|
label.setAttributes({ |
|
text: topText, |
|
style: config.font, |
|
fill: String(Color.fromHSL.apply({}, labelColor)) |
|
}, true); |
|
me.onPlaceLabel(label, storeItem, item, i, 'over', animate, index); |
|
groupIndex++; |
|
} |
|
if (bottomText) { |
|
label = group.getAt(groupIndex); |
|
if (!label) { |
|
label = me.onCreateLabel(storeItem, item, i, 'under'); |
|
} |
|
labelColor = Color.fromString(label.attr.color || label.attr.fill).getHSL(); |
|
label.setAttributes({ |
|
text: bottomText, |
|
style: config.font, |
|
fill: String(Color.fromHSL.apply({}, labelColor)) |
|
}, true); |
|
me.onPlaceLabel(label, storeItem, item, i, 'under', animate, index); |
|
groupIndex++; |
|
} |
|
} |
|
} |
|
count++; |
|
index++; |
|
} |
|
} |
|
groupLength = group.length; |
|
while (groupLength > groupIndex) { |
|
hides.push(groupIndex); |
|
groupIndex++; |
|
} |
|
} |
|
me.hideLabels(hides); |
|
}, |
|
hideLabels: function(hides) { |
|
var labelsGroup = this.labelsGroup, |
|
hlen = !!hides && hides.length; |
|
if (!labelsGroup) { |
|
return; |
|
} |
|
if (hlen === false) { |
|
hlen = labelsGroup.getCount(); |
|
while (hlen--) { |
|
labelsGroup.getAt(hlen).hide(true); |
|
} |
|
} else { |
|
while (hlen--) { |
|
labelsGroup.getAt(hides[hlen]).hide(true); |
|
} |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* @private |
|
*/ |
|
Ext.define('Ext.chart.TipSurface', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.draw.Component', |
|
/* End Definitions */ |
|
spriteArray: false, |
|
renderFirst: true, |
|
constructor: function(config) { |
|
this.callParent([ |
|
config |
|
]); |
|
if (config.sprites) { |
|
this.spriteArray = [].concat(config.sprites); |
|
delete config.sprites; |
|
} |
|
}, |
|
onRender: function() { |
|
var me = this, |
|
i = 0, |
|
l = 0, |
|
sp, sprites; |
|
this.callParent(arguments); |
|
sprites = me.spriteArray; |
|
if (me.renderFirst && sprites) { |
|
me.renderFirst = false; |
|
for (l = sprites.length; i < l; i++) { |
|
sp = me.surface.add(sprites[i]); |
|
sp.setAttributes({ |
|
hidden: false |
|
}, true); |
|
} |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.Tip |
|
* Provides tips for Ext.chart.series.Series. |
|
*/ |
|
Ext.define('Ext.chart.Tip', { |
|
/* Begin Definitions */ |
|
requires: [ |
|
'Ext.tip.ToolTip', |
|
'Ext.chart.TipSurface' |
|
], |
|
/* End Definitions */ |
|
constructor: function(config) { |
|
var me = this, |
|
surface, sprites, tipSurface; |
|
if (config.tips) { |
|
me.tipTimeout = null; |
|
me.tipConfig = Ext.apply({}, config.tips, { |
|
renderer: Ext.emptyFn, |
|
constrainPosition: true, |
|
autoHide: true, |
|
shrinkWrapDock: true |
|
}); |
|
me.tooltip = new Ext.tip.ToolTip(me.tipConfig); |
|
me.chart.surface.on('mousemove', me.tooltip.onMouseMove, me.tooltip); |
|
me.chart.surface.on('mouseleave', function() { |
|
me.hideTip(); |
|
}); |
|
if (me.tipConfig.surface) { |
|
//initialize a surface |
|
surface = me.tipConfig.surface; |
|
sprites = surface.sprites; |
|
tipSurface = new Ext.chart.TipSurface({ |
|
id: 'tipSurfaceComponent', |
|
sprites: sprites |
|
}); |
|
if (surface.width && surface.height) { |
|
tipSurface.setSize(surface.width, surface.height); |
|
} |
|
me.tooltip.add(tipSurface); |
|
me.spriteTip = tipSurface; |
|
} |
|
} |
|
}, |
|
showTip: function(item) { |
|
var me = this, |
|
tooltip, spriteTip, tipConfig, trackMouse, sprite, surface, surfaceExt, pos, x, y; |
|
if (!me.tooltip) { |
|
return; |
|
} |
|
clearTimeout(me.tipTimeout); |
|
tooltip = me.tooltip; |
|
spriteTip = me.spriteTip; |
|
tipConfig = me.tipConfig; |
|
trackMouse = tooltip.trackMouse; |
|
if (!trackMouse) { |
|
tooltip.trackMouse = true; |
|
sprite = item.sprite; |
|
surface = sprite.surface; |
|
surfaceExt = Ext.get(surface.getId()); |
|
if (surfaceExt) { |
|
pos = surfaceExt.getXY(); |
|
x = pos[0] + (sprite.attr.x || 0) + (sprite.attr.translation && sprite.attr.translation.x || 0); |
|
y = pos[1] + (sprite.attr.y || 0) + (sprite.attr.translation && sprite.attr.translation.y || 0); |
|
tooltip.targetXY = [ |
|
x, |
|
y |
|
]; |
|
} |
|
} |
|
if (spriteTip) { |
|
tipConfig.renderer.call(tooltip, item.storeItem, item, spriteTip.surface); |
|
} else { |
|
tipConfig.renderer.call(tooltip, item.storeItem, item); |
|
} |
|
tooltip.delayShow(trackMouse); |
|
tooltip.trackMouse = trackMouse; |
|
}, |
|
hideTip: function(item) { |
|
var tooltip = this.tooltip; |
|
if (!tooltip) { |
|
return; |
|
} |
|
clearTimeout(this.tipTimeout); |
|
this.tipTimeout = Ext.defer(function() { |
|
tooltip.delayHide(); |
|
}, 1); |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.axis.Abstract |
|
* Base class for all axis classes. |
|
* @private |
|
*/ |
|
Ext.define('Ext.chart.axis.Abstract', { |
|
/* Begin Definitions */ |
|
requires: [ |
|
'Ext.chart.Chart' |
|
], |
|
/* End Definitions */ |
|
/** |
|
* @cfg {Ext.chart.Label} label |
|
* The config for chart label. |
|
*/ |
|
/** |
|
* @cfg {String[]} fields |
|
* The fields of model to bind to this axis. |
|
* |
|
* For example if you have a data set of lap times per car, each having the fields: |
|
* `'carName'`, `'avgSpeed'`, `'maxSpeed'`. Then you might want to show the data on chart |
|
* with `['carName']` on Name axis and `['avgSpeed', 'maxSpeed']` on Speed axis. |
|
*/ |
|
/** |
|
* Creates new Axis. |
|
* @param {Object} config (optional) Config options. |
|
*/ |
|
constructor: function(config) { |
|
config = config || {}; |
|
var me = this, |
|
pos = config.position || 'left'; |
|
pos = pos.charAt(0).toUpperCase() + pos.substring(1); |
|
//axisLabel(Top|Bottom|Right|Left)Style |
|
config.label = Ext.apply(config['axisLabel' + pos + 'Style'] || {}, config.label || {}); |
|
config.axisTitleStyle = Ext.apply(config['axisTitle' + pos + 'Style'] || {}, config.labelTitle || {}); |
|
Ext.apply(me, config); |
|
me.fields = Ext.Array.from(me.fields); |
|
this.callParent(); |
|
me.labels = []; |
|
me.getId(); |
|
me.labelGroup = me.chart.surface.getGroup(me.axisId + "-labels"); |
|
}, |
|
alignment: null, |
|
grid: false, |
|
steps: 10, |
|
x: 0, |
|
y: 0, |
|
minValue: 0, |
|
maxValue: 0, |
|
getId: function() { |
|
return this.axisId || (this.axisId = Ext.id(null, 'ext-axis-')); |
|
}, |
|
/* |
|
Called to process a view i.e to make aggregation and filtering over |
|
a store creating a substore to be used to render the axis. Since many axes |
|
may do different things on the data and we want the final result of all these |
|
operations to be rendered we need to call processView on all axes before drawing |
|
them. |
|
*/ |
|
processView: Ext.emptyFn, |
|
drawAxis: Ext.emptyFn, |
|
addDisplayAndLabels: Ext.emptyFn |
|
}); |
|
|
|
/** |
|
* @class Ext.draw.Draw |
|
* Base Drawing class. Provides base drawing functions. |
|
* @private |
|
*/ |
|
Ext.define('Ext.draw.Draw', { |
|
/* Begin Definitions */ |
|
singleton: true, |
|
requires: [ |
|
'Ext.draw.Color' |
|
], |
|
/* End Definitions */ |
|
pathToStringRE: /,?([achlmqrstvxz]),?/gi, |
|
pathCommandRE: /([achlmqstvz])[\s,]*((-?\d*\.?\d*(?:e[-+]?\d+)?\s*,?\s*)+)/ig, |
|
pathValuesRE: /(-?\d*\.?\d*(?:e[-+]?\d+)?)\s*,?\s*/ig, |
|
stopsRE: /^(\d+%?)$/, |
|
radian: Math.PI / 180, |
|
availableAnimAttrs: { |
|
along: "along", |
|
blur: null, |
|
"clip-rect": "csv", |
|
cx: null, |
|
cy: null, |
|
fill: "color", |
|
"fill-opacity": null, |
|
"font-size": null, |
|
height: null, |
|
opacity: null, |
|
path: "path", |
|
r: null, |
|
rotation: "csv", |
|
rx: null, |
|
ry: null, |
|
scale: "csv", |
|
stroke: "color", |
|
"stroke-opacity": null, |
|
"stroke-width": null, |
|
translation: "csv", |
|
width: null, |
|
x: null, |
|
y: null |
|
}, |
|
is: function(o, type) { |
|
type = String(type).toLowerCase(); |
|
return (type == "object" && o === Object(o)) || (type == "undefined" && typeof o == type) || (type == "null" && o === null) || (type == "array" && Array.isArray && Array.isArray(o)) || (Object.prototype.toString.call(o).toLowerCase().slice(8, -1)) == type; |
|
}, |
|
ellipsePath: function(sprite) { |
|
var attr = sprite.attr; |
|
return Ext.String.format("M{0},{1}A{2},{3},0,1,1,{0},{4}A{2},{3},0,1,1,{0},{1}z", attr.x, attr.y - attr.ry, attr.rx, attr.ry, attr.y + attr.ry); |
|
}, |
|
rectPath: function(sprite) { |
|
var attr = sprite.attr; |
|
if (attr.radius) { |
|
return Ext.String.format("M{0},{1}l{2},0a{3},{3},0,0,1,{3},{3}l0,{5}a{3},{3},0,0,1,{4},{3}l{6},0a{3},{3},0,0,1,{4},{4}l0,{7}a{3},{3},0,0,1,{3},{4}z", attr.x + attr.radius, attr.y, attr.width - attr.radius * 2, attr.radius, -attr.radius, attr.height - attr.radius * 2, attr.radius * 2 - attr.width, attr.radius * 2 - attr.height); |
|
} else { |
|
return Ext.String.format("M{0},{1}L{2},{1},{2},{3},{0},{3}z", attr.x, attr.y, attr.width + attr.x, attr.height + attr.y); |
|
} |
|
}, |
|
// To be deprecated, converts itself (an arrayPath) to a proper SVG path string |
|
path2string: function() { |
|
return this.join(",").replace(Ext.draw.Draw.pathToStringRE, "$1"); |
|
}, |
|
// Convert the passed arrayPath to a proper SVG path string (d attribute) |
|
pathToString: function(arrayPath) { |
|
return arrayPath.join(",").replace(Ext.draw.Draw.pathToStringRE, "$1"); |
|
}, |
|
parsePathString: function(pathString) { |
|
if (!pathString) { |
|
return null; |
|
} |
|
var paramCounts = { |
|
a: 7, |
|
c: 6, |
|
h: 1, |
|
l: 2, |
|
m: 2, |
|
q: 4, |
|
s: 4, |
|
t: 2, |
|
v: 1, |
|
z: 0 |
|
}, |
|
data = [], |
|
me = this; |
|
if (me.is(pathString, "array") && me.is(pathString[0], "array")) { |
|
// rough assumption |
|
data = me.pathClone(pathString); |
|
} |
|
if (!data.length) { |
|
String(pathString).replace(me.pathCommandRE, function(a, b, c) { |
|
var params = [], |
|
name = b.toLowerCase(); |
|
c.replace(me.pathValuesRE, function(a, b) { |
|
b && params.push(+b); |
|
}); |
|
if (name == "m" && params.length > 2) { |
|
data.push([ |
|
b |
|
].concat(Ext.Array.splice(params, 0, 2))); |
|
name = "l"; |
|
b = (b == "m") ? "l" : "L"; |
|
} |
|
while (params.length >= paramCounts[name]) { |
|
data.push([ |
|
b |
|
].concat(Ext.Array.splice(params, 0, paramCounts[name]))); |
|
if (!paramCounts[name]) { |
|
break; |
|
} |
|
} |
|
}); |
|
} |
|
data.toString = me.path2string; |
|
return data; |
|
}, |
|
mapPath: function(path, matrix) { |
|
if (!matrix) { |
|
return path; |
|
} |
|
var x, y, i, ii, j, jj, pathi; |
|
path = this.path2curve(path); |
|
for (i = 0 , ii = path.length; i < ii; i++) { |
|
pathi = path[i]; |
|
for (j = 1 , jj = pathi.length; j < jj - 1; j += 2) { |
|
x = matrix.x(pathi[j], pathi[j + 1]); |
|
y = matrix.y(pathi[j], pathi[j + 1]); |
|
pathi[j] = x; |
|
pathi[j + 1] = y; |
|
} |
|
} |
|
return path; |
|
}, |
|
pathClone: function(pathArray) { |
|
var res = [], |
|
j, jj, i, ii; |
|
if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) { |
|
// rough assumption |
|
pathArray = this.parsePathString(pathArray); |
|
} |
|
for (i = 0 , ii = pathArray.length; i < ii; i++) { |
|
res[i] = []; |
|
for (j = 0 , jj = pathArray[i].length; j < jj; j++) { |
|
res[i][j] = pathArray[i][j]; |
|
} |
|
} |
|
res.toString = this.path2string; |
|
return res; |
|
}, |
|
pathToAbsolute: function(pathArray) { |
|
if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) { |
|
// rough assumption |
|
pathArray = this.parsePathString(pathArray); |
|
} |
|
var res = [], |
|
x = 0, |
|
y = 0, |
|
mx = 0, |
|
my = 0, |
|
i = 0, |
|
ln = pathArray.length, |
|
r, pathSegment, j, ln2; |
|
// MoveTo initial x/y position |
|
if (ln && pathArray[0][0] == "M") { |
|
x = +pathArray[0][1]; |
|
y = +pathArray[0][2]; |
|
mx = x; |
|
my = y; |
|
i++; |
|
res[0] = [ |
|
"M", |
|
x, |
|
y |
|
]; |
|
} |
|
for (; i < ln; i++) { |
|
r = res[i] = []; |
|
pathSegment = pathArray[i]; |
|
if (pathSegment[0] != pathSegment[0].toUpperCase()) { |
|
r[0] = pathSegment[0].toUpperCase(); |
|
switch (r[0]) { |
|
// Elliptical Arc |
|
case "A": |
|
r[1] = pathSegment[1]; |
|
r[2] = pathSegment[2]; |
|
r[3] = pathSegment[3]; |
|
r[4] = pathSegment[4]; |
|
r[5] = pathSegment[5]; |
|
r[6] = +(pathSegment[6] + x); |
|
r[7] = +(pathSegment[7] + y); |
|
break; |
|
// Vertical LineTo |
|
case "V": |
|
r[1] = +pathSegment[1] + y; |
|
break; |
|
// Horizontal LineTo |
|
case "H": |
|
r[1] = +pathSegment[1] + x; |
|
break; |
|
case "M": |
|
// MoveTo |
|
mx = +pathSegment[1] + x; |
|
my = +pathSegment[2] + y; |
|
default: |
|
j = 1; |
|
ln2 = pathSegment.length; |
|
for (; j < ln2; j++) { |
|
r[j] = +pathSegment[j] + ((j % 2) ? x : y); |
|
}; |
|
} |
|
} else { |
|
j = 0; |
|
ln2 = pathSegment.length; |
|
for (; j < ln2; j++) { |
|
res[i][j] = pathSegment[j]; |
|
} |
|
} |
|
switch (r[0]) { |
|
// ClosePath |
|
case "Z": |
|
x = mx; |
|
y = my; |
|
break; |
|
// Horizontal LineTo |
|
case "H": |
|
x = r[1]; |
|
break; |
|
// Vertical LineTo |
|
case "V": |
|
y = r[1]; |
|
break; |
|
// MoveTo |
|
case "M": |
|
pathSegment = res[i]; |
|
ln2 = pathSegment.length; |
|
mx = pathSegment[ln2 - 2]; |
|
my = pathSegment[ln2 - 1]; |
|
default: |
|
pathSegment = res[i]; |
|
ln2 = pathSegment.length; |
|
x = pathSegment[ln2 - 2]; |
|
y = pathSegment[ln2 - 1]; |
|
} |
|
} |
|
res.toString = this.path2string; |
|
return res; |
|
}, |
|
// TO BE DEPRECATED |
|
pathToRelative: function(pathArray) { |
|
if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) { |
|
pathArray = this.parsePathString(pathArray); |
|
} |
|
var res = [], |
|
x = 0, |
|
y = 0, |
|
mx = 0, |
|
my = 0, |
|
start = 0, |
|
r, pa, i, j, k, len, ii, jj, kk; |
|
if (pathArray[0][0] == "M") { |
|
x = pathArray[0][1]; |
|
y = pathArray[0][2]; |
|
mx = x; |
|
my = y; |
|
start++; |
|
res.push([ |
|
"M", |
|
x, |
|
y |
|
]); |
|
} |
|
for (i = start , ii = pathArray.length; i < ii; i++) { |
|
r = res[i] = []; |
|
pa = pathArray[i]; |
|
if (pa[0] != pa[0].toLowerCase()) { |
|
r[0] = pa[0].toLowerCase(); |
|
switch (r[0]) { |
|
case "a": |
|
r[1] = pa[1]; |
|
r[2] = pa[2]; |
|
r[3] = pa[3]; |
|
r[4] = pa[4]; |
|
r[5] = pa[5]; |
|
r[6] = +(pa[6] - x).toFixed(3); |
|
r[7] = +(pa[7] - y).toFixed(3); |
|
break; |
|
case "v": |
|
r[1] = +(pa[1] - y).toFixed(3); |
|
break; |
|
case "m": |
|
mx = pa[1]; |
|
my = pa[2]; |
|
default: |
|
for (j = 1 , jj = pa.length; j < jj; j++) { |
|
r[j] = +(pa[j] - ((j % 2) ? x : y)).toFixed(3); |
|
}; |
|
} |
|
} else { |
|
r = res[i] = []; |
|
if (pa[0] == "m") { |
|
mx = pa[1] + x; |
|
my = pa[2] + y; |
|
} |
|
for (k = 0 , kk = pa.length; k < kk; k++) { |
|
res[i][k] = pa[k]; |
|
} |
|
} |
|
len = res[i].length; |
|
switch (res[i][0]) { |
|
case "z": |
|
x = mx; |
|
y = my; |
|
break; |
|
case "h": |
|
x += +res[i][len - 1]; |
|
break; |
|
case "v": |
|
y += +res[i][len - 1]; |
|
break; |
|
default: |
|
x += +res[i][len - 2]; |
|
y += +res[i][len - 1]; |
|
} |
|
} |
|
res.toString = this.path2string; |
|
return res; |
|
}, |
|
// Returns a path converted to a set of curveto commands |
|
path2curve: function(path) { |
|
var me = this, |
|
points = me.pathToAbsolute(path), |
|
ln = points.length, |
|
attrs = { |
|
x: 0, |
|
y: 0, |
|
bx: 0, |
|
by: 0, |
|
X: 0, |
|
Y: 0, |
|
qx: null, |
|
qy: null |
|
}, |
|
i, seg, segLn, point; |
|
for (i = 0; i < ln; i++) { |
|
points[i] = me.command2curve(points[i], attrs); |
|
if (points[i].length > 7) { |
|
points[i].shift(); |
|
point = points[i]; |
|
while (point.length) { |
|
Ext.Array.splice(points, i++, 0, [ |
|
"C" |
|
].concat(Ext.Array.splice(point, 0, 6))); |
|
} |
|
Ext.Array.erase(points, i, 1); |
|
ln = points.length; |
|
i--; |
|
} |
|
seg = points[i]; |
|
segLn = seg.length; |
|
attrs.x = seg[segLn - 2]; |
|
attrs.y = seg[segLn - 1]; |
|
attrs.bx = parseFloat(seg[segLn - 4]) || attrs.x; |
|
attrs.by = parseFloat(seg[segLn - 3]) || attrs.y; |
|
} |
|
return points; |
|
}, |
|
interpolatePaths: function(path, path2) { |
|
var me = this, |
|
p = me.pathToAbsolute(path), |
|
p2 = me.pathToAbsolute(path2), |
|
attrs = { |
|
x: 0, |
|
y: 0, |
|
bx: 0, |
|
by: 0, |
|
X: 0, |
|
Y: 0, |
|
qx: null, |
|
qy: null |
|
}, |
|
attrs2 = { |
|
x: 0, |
|
y: 0, |
|
bx: 0, |
|
by: 0, |
|
X: 0, |
|
Y: 0, |
|
qx: null, |
|
qy: null |
|
}, |
|
fixArc = function(pp, i) { |
|
if (pp[i].length > 7) { |
|
pp[i].shift(); |
|
var pi = pp[i]; |
|
while (pi.length) { |
|
Ext.Array.splice(pp, i++, 0, [ |
|
"C" |
|
].concat(Ext.Array.splice(pi, 0, 6))); |
|
} |
|
Ext.Array.erase(pp, i, 1); |
|
ii = Math.max(p.length, p2.length || 0); |
|
} |
|
}, |
|
fixM = function(path1, path2, a1, a2, i) { |
|
if (path1 && path2 && path1[i][0] == "M" && path2[i][0] != "M") { |
|
Ext.Array.splice(path2, i, 0, [ |
|
"M", |
|
a2.x, |
|
a2.y |
|
]); |
|
a1.bx = 0; |
|
a1.by = 0; |
|
a1.x = path1[i][1]; |
|
a1.y = path1[i][2]; |
|
ii = Math.max(p.length, p2.length || 0); |
|
} |
|
}, |
|
i, ii, seg, seg2, seglen, seg2len; |
|
for (i = 0 , ii = Math.max(p.length, p2.length || 0); i < ii; i++) { |
|
p[i] = me.command2curve(p[i], attrs); |
|
fixArc(p, i); |
|
(p2[i] = me.command2curve(p2[i], attrs2)); |
|
fixArc(p2, i); |
|
fixM(p, p2, attrs, attrs2, i); |
|
fixM(p2, p, attrs2, attrs, i); |
|
seg = p[i]; |
|
seg2 = p2[i]; |
|
seglen = seg.length; |
|
seg2len = seg2.length; |
|
attrs.x = seg[seglen - 2]; |
|
attrs.y = seg[seglen - 1]; |
|
attrs.bx = parseFloat(seg[seglen - 4]) || attrs.x; |
|
attrs.by = parseFloat(seg[seglen - 3]) || attrs.y; |
|
attrs2.bx = (parseFloat(seg2[seg2len - 4]) || attrs2.x); |
|
attrs2.by = (parseFloat(seg2[seg2len - 3]) || attrs2.y); |
|
attrs2.x = seg2[seg2len - 2]; |
|
attrs2.y = seg2[seg2len - 1]; |
|
} |
|
return [ |
|
p, |
|
p2 |
|
]; |
|
}, |
|
//Returns any path command as a curveto command based on the attrs passed |
|
command2curve: function(pathCommand, d) { |
|
var me = this; |
|
if (!pathCommand) { |
|
return [ |
|
"C", |
|
d.x, |
|
d.y, |
|
d.x, |
|
d.y, |
|
d.x, |
|
d.y |
|
]; |
|
} |
|
if (pathCommand[0] != "T" && pathCommand[0] != "Q") { |
|
d.qx = d.qy = null; |
|
} |
|
switch (pathCommand[0]) { |
|
case "M": |
|
d.X = pathCommand[1]; |
|
d.Y = pathCommand[2]; |
|
break; |
|
case "A": |
|
pathCommand = [ |
|
"C" |
|
].concat(me.arc2curve.apply(me, [ |
|
d.x, |
|
d.y |
|
].concat(pathCommand.slice(1)))); |
|
break; |
|
case "S": |
|
pathCommand = [ |
|
"C", |
|
d.x + (d.x - (d.bx || d.x)), |
|
d.y + (d.y - (d.by || d.y)) |
|
].concat(pathCommand.slice(1)); |
|
break; |
|
case "T": |
|
d.qx = d.x + (d.x - (d.qx || d.x)); |
|
d.qy = d.y + (d.y - (d.qy || d.y)); |
|
pathCommand = [ |
|
"C" |
|
].concat(me.quadratic2curve(d.x, d.y, d.qx, d.qy, pathCommand[1], pathCommand[2])); |
|
break; |
|
case "Q": |
|
d.qx = pathCommand[1]; |
|
d.qy = pathCommand[2]; |
|
pathCommand = [ |
|
"C" |
|
].concat(me.quadratic2curve(d.x, d.y, pathCommand[1], pathCommand[2], pathCommand[3], pathCommand[4])); |
|
break; |
|
case "L": |
|
pathCommand = [ |
|
"C" |
|
].concat(d.x, d.y, pathCommand[1], pathCommand[2], pathCommand[1], pathCommand[2]); |
|
break; |
|
case "H": |
|
pathCommand = [ |
|
"C" |
|
].concat(d.x, d.y, pathCommand[1], d.y, pathCommand[1], d.y); |
|
break; |
|
case "V": |
|
pathCommand = [ |
|
"C" |
|
].concat(d.x, d.y, d.x, pathCommand[1], d.x, pathCommand[1]); |
|
break; |
|
case "Z": |
|
pathCommand = [ |
|
"C" |
|
].concat(d.x, d.y, d.X, d.Y, d.X, d.Y); |
|
break; |
|
} |
|
return pathCommand; |
|
}, |
|
quadratic2curve: function(x1, y1, ax, ay, x2, y2) { |
|
var _13 = 1 / 3, |
|
_23 = 2 / 3; |
|
return [ |
|
_13 * x1 + _23 * ax, |
|
_13 * y1 + _23 * ay, |
|
_13 * x2 + _23 * ax, |
|
_13 * y2 + _23 * ay, |
|
x2, |
|
y2 |
|
]; |
|
}, |
|
rotate: function(x, y, rad) { |
|
var cos = Math.cos(rad), |
|
sin = Math.sin(rad), |
|
X = x * cos - y * sin, |
|
Y = x * sin + y * cos; |
|
return { |
|
x: X, |
|
y: Y |
|
}; |
|
}, |
|
arc2curve: function(x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) { |
|
// for more information of where this Math came from visit: |
|
// http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes |
|
var me = this, |
|
PI = Math.PI, |
|
radian = me.radian, |
|
_120 = PI * 120 / 180, |
|
rad = radian * (+angle || 0), |
|
res = [], |
|
math = Math, |
|
mcos = math.cos, |
|
msin = math.sin, |
|
msqrt = math.sqrt, |
|
mabs = math.abs, |
|
masin = math.asin, |
|
xy, x, y, h, rx2, ry2, k, cx, cy, f1, f2, df, c1, s1, c2, s2, t, hx, hy, m1, m2, m3, m4, newres, i, ln, f2old, x2old, y2old; |
|
if (!recursive) { |
|
xy = me.rotate(x1, y1, -rad); |
|
x1 = xy.x; |
|
y1 = xy.y; |
|
xy = me.rotate(x2, y2, -rad); |
|
x2 = xy.x; |
|
y2 = xy.y; |
|
x = (x1 - x2) / 2; |
|
y = (y1 - y2) / 2; |
|
h = (x * x) / (rx * rx) + (y * y) / (ry * ry); |
|
if (h > 1) { |
|
h = msqrt(h); |
|
rx = h * rx; |
|
ry = h * ry; |
|
} |
|
rx2 = rx * rx; |
|
ry2 = ry * ry; |
|
k = (large_arc_flag == sweep_flag ? -1 : 1) * msqrt(mabs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))); |
|
cx = k * rx * y / ry + (x1 + x2) / 2; |
|
cy = k * -ry * x / rx + (y1 + y2) / 2; |
|
f1 = masin(((y1 - cy) / ry).toFixed(7)); |
|
f2 = masin(((y2 - cy) / ry).toFixed(7)); |
|
f1 = x1 < cx ? PI - f1 : f1; |
|
f2 = x2 < cx ? PI - f2 : f2; |
|
if (f1 < 0) { |
|
f1 = PI * 2 + f1; |
|
} |
|
if (f2 < 0) { |
|
f2 = PI * 2 + f2; |
|
} |
|
if (sweep_flag && f1 > f2) { |
|
f1 = f1 - PI * 2; |
|
} |
|
if (!sweep_flag && f2 > f1) { |
|
f2 = f2 - PI * 2; |
|
} |
|
} else { |
|
f1 = recursive[0]; |
|
f2 = recursive[1]; |
|
cx = recursive[2]; |
|
cy = recursive[3]; |
|
} |
|
df = f2 - f1; |
|
if (mabs(df) > _120) { |
|
f2old = f2; |
|
x2old = x2; |
|
y2old = y2; |
|
f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1); |
|
x2 = cx + rx * mcos(f2); |
|
y2 = cy + ry * msin(f2); |
|
res = me.arc2curve(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [ |
|
f2, |
|
f2old, |
|
cx, |
|
cy |
|
]); |
|
} |
|
df = f2 - f1; |
|
c1 = mcos(f1); |
|
s1 = msin(f1); |
|
c2 = mcos(f2); |
|
s2 = msin(f2); |
|
t = math.tan(df / 4); |
|
hx = 4 / 3 * rx * t; |
|
hy = 4 / 3 * ry * t; |
|
m1 = [ |
|
x1, |
|
y1 |
|
]; |
|
m2 = [ |
|
x1 + hx * s1, |
|
y1 - hy * c1 |
|
]; |
|
m3 = [ |
|
x2 + hx * s2, |
|
y2 - hy * c2 |
|
]; |
|
m4 = [ |
|
x2, |
|
y2 |
|
]; |
|
m2[0] = 2 * m1[0] - m2[0]; |
|
m2[1] = 2 * m1[1] - m2[1]; |
|
if (recursive) { |
|
return [ |
|
m2, |
|
m3, |
|
m4 |
|
].concat(res); |
|
} else { |
|
res = [ |
|
m2, |
|
m3, |
|
m4 |
|
].concat(res).join().split(","); |
|
newres = []; |
|
ln = res.length; |
|
for (i = 0; i < ln; i++) { |
|
newres[i] = i % 2 ? me.rotate(res[i - 1], res[i], rad).y : me.rotate(res[i], res[i + 1], rad).x; |
|
} |
|
return newres; |
|
} |
|
}, |
|
// TO BE DEPRECATED |
|
rotateAndTranslatePath: function(sprite) { |
|
var alpha = sprite.rotation.degrees, |
|
cx = sprite.rotation.x, |
|
cy = sprite.rotation.y, |
|
dx = sprite.translation.x, |
|
dy = sprite.translation.y, |
|
path, i, p, xy, j, |
|
res = []; |
|
if (!alpha && !dx && !dy) { |
|
return this.pathToAbsolute(sprite.attr.path); |
|
} |
|
dx = dx || 0; |
|
dy = dy || 0; |
|
path = this.pathToAbsolute(sprite.attr.path); |
|
for (i = path.length; i--; ) { |
|
p = res[i] = path[i].slice(); |
|
if (p[0] == "A") { |
|
xy = this.rotatePoint(p[6], p[7], alpha, cx, cy); |
|
p[6] = xy.x + dx; |
|
p[7] = xy.y + dy; |
|
} else { |
|
j = 1; |
|
while (p[j + 1] != null) { |
|
xy = this.rotatePoint(p[j], p[j + 1], alpha, cx, cy); |
|
p[j] = xy.x + dx; |
|
p[j + 1] = xy.y + dy; |
|
j += 2; |
|
} |
|
} |
|
} |
|
return res; |
|
}, |
|
// TO BE DEPRECATED |
|
rotatePoint: function(x, y, alpha, cx, cy) { |
|
if (!alpha) { |
|
return { |
|
x: x, |
|
y: y |
|
}; |
|
} |
|
cx = cx || 0; |
|
cy = cy || 0; |
|
x = x - cx; |
|
y = y - cy; |
|
alpha = alpha * this.radian; |
|
var cos = Math.cos(alpha), |
|
sin = Math.sin(alpha); |
|
return { |
|
x: x * cos - y * sin + cx, |
|
y: x * sin + y * cos + cy |
|
}; |
|
}, |
|
pathDimensions: function(path) { |
|
if (!path || !(path + "")) { |
|
return { |
|
x: 0, |
|
y: 0, |
|
width: 0, |
|
height: 0 |
|
}; |
|
} |
|
path = this.path2curve(path); |
|
var x = 0, |
|
y = 0, |
|
X = [], |
|
Y = [], |
|
i = 0, |
|
ln = path.length, |
|
p, xmin, ymin, xmax, ymax, dim; |
|
for (; i < ln; i++) { |
|
p = path[i]; |
|
if (p[0] == "M") { |
|
x = p[1]; |
|
y = p[2]; |
|
X.push(x); |
|
Y.push(y); |
|
} else { |
|
dim = this.curveDim(x, y, p[1], p[2], p[3], p[4], p[5], p[6]); |
|
X = X.concat(dim.min.x, dim.max.x); |
|
Y = Y.concat(dim.min.y, dim.max.y); |
|
x = p[5]; |
|
y = p[6]; |
|
} |
|
} |
|
xmin = Math.min.apply(0, X); |
|
ymin = Math.min.apply(0, Y); |
|
xmax = Math.max.apply(0, X); |
|
ymax = Math.max.apply(0, Y); |
|
return { |
|
x: Math.round(xmin), |
|
y: Math.round(ymin), |
|
path: path, |
|
width: Math.round(xmax - xmin), |
|
height: Math.round(ymax - ymin) |
|
}; |
|
}, |
|
intersectInside: function(path, cp1, cp2) { |
|
return (cp2[0] - cp1[0]) * (path[1] - cp1[1]) > (cp2[1] - cp1[1]) * (path[0] - cp1[0]); |
|
}, |
|
intersectIntersection: function(s, e, cp1, cp2) { |
|
var p = [], |
|
dcx = cp1[0] - cp2[0], |
|
dcy = cp1[1] - cp2[1], |
|
dpx = s[0] - e[0], |
|
dpy = s[1] - e[1], |
|
n1 = cp1[0] * cp2[1] - cp1[1] * cp2[0], |
|
n2 = s[0] * e[1] - s[1] * e[0], |
|
n3 = 1 / (dcx * dpy - dcy * dpx); |
|
p[0] = (n1 * dpx - n2 * dcx) * n3; |
|
p[1] = (n1 * dpy - n2 * dcy) * n3; |
|
return p; |
|
}, |
|
intersect: function(subjectPolygon, clipPolygon) { |
|
var me = this, |
|
i = 0, |
|
ln = clipPolygon.length, |
|
cp1 = clipPolygon[ln - 1], |
|
outputList = subjectPolygon, |
|
cp2, s, e, ln2, inputList, j; |
|
for (; i < ln; ++i) { |
|
cp2 = clipPolygon[i]; |
|
inputList = outputList; |
|
outputList = []; |
|
s = inputList[inputList.length - 1]; |
|
j = 0; |
|
ln2 = inputList.length; |
|
for (; j < ln2; j++) { |
|
e = inputList[j]; |
|
if (me.intersectInside(e, cp1, cp2)) { |
|
if (!me.intersectInside(s, cp1, cp2)) { |
|
outputList.push(me.intersectIntersection(s, e, cp1, cp2)); |
|
} |
|
outputList.push(e); |
|
} else if (me.intersectInside(s, cp1, cp2)) { |
|
outputList.push(me.intersectIntersection(s, e, cp1, cp2)); |
|
} |
|
s = e; |
|
} |
|
cp1 = cp2; |
|
} |
|
return outputList; |
|
}, |
|
bezier: function(a, b, c, d, x) { |
|
if (x === 0) { |
|
return a; |
|
} else if (x === 1) { |
|
return d; |
|
} |
|
var du = 1 - x, |
|
d3 = du * du * du, |
|
r = x / du; |
|
return d3 * (a + r * (3 * b + r * (3 * c + d * r))); |
|
}, |
|
bezierDim: function(a, b, c, d) { |
|
var points = [], |
|
r, A, top, C, delta, bottom, s, min, max, i; |
|
// The min and max happens on boundary or b' == 0 |
|
if (a + 3 * c == d + 3 * b) { |
|
r = a - b; |
|
r /= 2 * (a - b - b + c); |
|
if (r < 1 && r > 0) { |
|
points.push(r); |
|
} |
|
} else { |
|
// b'(x) / -3 = (a-3b+3c-d)x^2+ (-2a+4b-2c)x + (a-b) |
|
// delta = -4 (-b^2+a c+b c-c^2-a d+b d) |
|
A = a - 3 * b + 3 * c - d; |
|
top = 2 * (a - b - b + c); |
|
C = a - b; |
|
delta = top * top - 4 * A * C; |
|
bottom = A + A; |
|
if (delta === 0) { |
|
r = top / bottom; |
|
if (r < 1 && r > 0) { |
|
points.push(r); |
|
} |
|
} else if (delta > 0) { |
|
s = Math.sqrt(delta); |
|
r = (s + top) / bottom; |
|
if (r < 1 && r > 0) { |
|
points.push(r); |
|
} |
|
r = (top - s) / bottom; |
|
if (r < 1 && r > 0) { |
|
points.push(r); |
|
} |
|
} |
|
} |
|
min = Math.min(a, d); |
|
max = Math.max(a, d); |
|
for (i = 0; i < points.length; i++) { |
|
min = Math.min(min, this.bezier(a, b, c, d, points[i])); |
|
max = Math.max(max, this.bezier(a, b, c, d, points[i])); |
|
} |
|
return [ |
|
min, |
|
max |
|
]; |
|
}, |
|
curveDim: function(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) { |
|
var x = this.bezierDim(p1x, c1x, c2x, p2x), |
|
y = this.bezierDim(p1y, c1y, c2y, p2y); |
|
return { |
|
min: { |
|
x: x[0], |
|
y: y[0] |
|
}, |
|
max: { |
|
x: x[1], |
|
y: y[1] |
|
} |
|
}; |
|
}, |
|
/** |
|
* @private |
|
* |
|
* Calculates bezier curve control anchor points for a particular point in a path, with a |
|
* smoothing curve applied. The smoothness of the curve is controlled by the 'value' parameter. |
|
* Note that this algorithm assumes that the line being smoothed is normalized going from left |
|
* to right; it makes special adjustments assuming this orientation. |
|
* |
|
* @param {Number} prevX X coordinate of the previous point in the path |
|
* @param {Number} prevY Y coordinate of the previous point in the path |
|
* @param {Number} curX X coordinate of the current point in the path |
|
* @param {Number} curY Y coordinate of the current point in the path |
|
* @param {Number} nextX X coordinate of the next point in the path |
|
* @param {Number} nextY Y coordinate of the next point in the path |
|
* @param {Number} value A value to control the smoothness of the curve; this is used to |
|
* divide the distance between points, so a value of 2 corresponds to |
|
* half the distance between points (a very smooth line) while higher values |
|
* result in less smooth curves. Defaults to 4. |
|
* @return {Object} Object containing x1, y1, x2, y2 bezier control anchor points; x1 and y1 |
|
* are the control point for the curve toward the previous path point, and |
|
* x2 and y2 are the control point for the curve toward the next path point. |
|
*/ |
|
getAnchors: function(prevX, prevY, curX, curY, nextX, nextY, value) { |
|
value = value || 4; |
|
var M = Math, |
|
PI = M.PI, |
|
halfPI = PI / 2, |
|
abs = M.abs, |
|
sin = M.sin, |
|
cos = M.cos, |
|
atan = M.atan, |
|
control1Length, control2Length, control1Angle, control2Angle, control1X, control1Y, control2X, control2Y, alpha; |
|
// Find the length of each control anchor line, by dividing the horizontal distance |
|
// between points by the value parameter. |
|
control1Length = (curX - prevX) / value; |
|
control2Length = (nextX - curX) / value; |
|
// Determine the angle of each control anchor line. If the middle point is a vertical |
|
// turnaround then we force it to a flat horizontal angle to prevent the curve from |
|
// dipping above or below the middle point. Otherwise we use an angle that points |
|
// toward the previous/next target point. |
|
if ((curY >= prevY && curY >= nextY) || (curY <= prevY && curY <= nextY)) { |
|
control1Angle = control2Angle = halfPI; |
|
} else { |
|
control1Angle = atan((curX - prevX) / abs(curY - prevY)); |
|
if (prevY < curY) { |
|
control1Angle = PI - control1Angle; |
|
} |
|
control2Angle = atan((nextX - curX) / abs(curY - nextY)); |
|
if (nextY < curY) { |
|
control2Angle = PI - control2Angle; |
|
} |
|
} |
|
// Adjust the calculated angles so they point away from each other on the same line |
|
alpha = halfPI - ((control1Angle + control2Angle) % (PI * 2)) / 2; |
|
if (alpha > halfPI) { |
|
alpha -= PI; |
|
} |
|
control1Angle += alpha; |
|
control2Angle += alpha; |
|
// Find the control anchor points from the angles and length |
|
control1X = curX - control1Length * sin(control1Angle); |
|
control1Y = curY + control1Length * cos(control1Angle); |
|
control2X = curX + control2Length * sin(control2Angle); |
|
control2Y = curY + control2Length * cos(control2Angle); |
|
// One last adjustment, make sure that no control anchor point extends vertically past |
|
// its target prev/next point, as that results in curves dipping above or below and |
|
// bending back strangely. If we find this happening we keep the control angle but |
|
// reduce the length of the control line so it stays within bounds. |
|
if ((curY > prevY && control1Y < prevY) || (curY < prevY && control1Y > prevY)) { |
|
control1X += abs(prevY - control1Y) * (control1X - curX) / (control1Y - curY); |
|
control1Y = prevY; |
|
} |
|
if ((curY > nextY && control2Y < nextY) || (curY < nextY && control2Y > nextY)) { |
|
control2X -= abs(nextY - control2Y) * (control2X - curX) / (control2Y - curY); |
|
control2Y = nextY; |
|
} |
|
return { |
|
x1: control1X, |
|
y1: control1Y, |
|
x2: control2X, |
|
y2: control2Y |
|
}; |
|
}, |
|
/* Smoothing function for a path. Converts a path into cubic beziers. Value defines the divider of the distance between points. |
|
* Defaults to a value of 4. |
|
*/ |
|
smooth: function(originalPath, value) { |
|
var path = this.path2curve(originalPath), |
|
newp = [ |
|
path[0] |
|
], |
|
x = path[0][1], |
|
y = path[0][2], |
|
j, points, |
|
i = 1, |
|
ii = path.length, |
|
beg = 1, |
|
mx = x, |
|
my = y, |
|
pathi, pathil, pathim, pathiml, pathip, pathipl, begl; |
|
for (; i < ii; i++) { |
|
pathi = path[i]; |
|
pathil = pathi.length; |
|
pathim = path[i - 1]; |
|
pathiml = pathim.length; |
|
pathip = path[i + 1]; |
|
pathipl = pathip && pathip.length; |
|
if (pathi[0] == "M") { |
|
mx = pathi[1]; |
|
my = pathi[2]; |
|
j = i + 1; |
|
while (path[j][0] != "C") { |
|
j++; |
|
} |
|
newp.push([ |
|
"M", |
|
mx, |
|
my |
|
]); |
|
beg = newp.length; |
|
x = mx; |
|
y = my; |
|
|
|
continue; |
|
} |
|
if (pathi[pathil - 2] == mx && pathi[pathil - 1] == my && (!pathip || pathip[0] == "M")) { |
|
begl = newp[beg].length; |
|
points = this.getAnchors(pathim[pathiml - 2], pathim[pathiml - 1], mx, my, newp[beg][begl - 2], newp[beg][begl - 1], value); |
|
newp[beg][1] = points.x2; |
|
newp[beg][2] = points.y2; |
|
} else if (!pathip || pathip[0] == "M") { |
|
points = { |
|
x1: pathi[pathil - 2], |
|
y1: pathi[pathil - 1] |
|
}; |
|
} else { |
|
points = this.getAnchors(pathim[pathiml - 2], pathim[pathiml - 1], pathi[pathil - 2], pathi[pathil - 1], pathip[pathipl - 2], pathip[pathipl - 1], value); |
|
} |
|
newp.push([ |
|
"C", |
|
x, |
|
y, |
|
points.x1, |
|
points.y1, |
|
pathi[pathil - 2], |
|
pathi[pathil - 1] |
|
]); |
|
x = points.x2; |
|
y = points.y2; |
|
} |
|
return newp; |
|
}, |
|
findDotAtSegment: function(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) { |
|
var t1 = 1 - t; |
|
return { |
|
x: Math.pow(t1, 3) * p1x + Math.pow(t1, 2) * 3 * t * c1x + t1 * 3 * t * t * c2x + Math.pow(t, 3) * p2x, |
|
y: Math.pow(t1, 3) * p1y + Math.pow(t1, 2) * 3 * t * c1y + t1 * 3 * t * t * c2y + Math.pow(t, 3) * p2y |
|
}; |
|
}, |
|
/** |
|
* @private |
|
*/ |
|
snapEnds: function(from, to, stepsMax, prettyNumbers) { |
|
if (Ext.isDate(from)) { |
|
return this.snapEndsByDate(from, to, stepsMax); |
|
} |
|
var step = (to - from) / stepsMax, |
|
level = Math.floor(Math.log(step) / Math.LN10) + 1, |
|
m = Math.pow(10, level), |
|
cur, floor, |
|
modulo = Math.round((step % m) * Math.pow(10, 2 - level)), |
|
interval = [ |
|
[ |
|
0, |
|
15 |
|
], |
|
[ |
|
10, |
|
1 |
|
], |
|
[ |
|
20, |
|
4 |
|
], |
|
[ |
|
25, |
|
2 |
|
], |
|
[ |
|
50, |
|
9 |
|
], |
|
[ |
|
100, |
|
15 |
|
] |
|
], |
|
stepCount = 0, |
|
value, weight, i, topValue, |
|
topWeight = 1000000000, |
|
ln = interval.length; |
|
floor = Math.floor(from / m) * m; |
|
if (from == floor && floor > 0) { |
|
floor = Math.floor((from - (m / 10)) / m) * m; |
|
} |
|
if (prettyNumbers) { |
|
for (i = 0; i < ln; i++) { |
|
value = interval[i][0]; |
|
weight = (value - modulo) < 0 ? 1000000 : (value - modulo) / interval[i][1]; |
|
if (weight < topWeight) { |
|
topValue = value; |
|
topWeight = weight; |
|
} |
|
} |
|
step = Math.floor(step * Math.pow(10, -level)) * Math.pow(10, level) + topValue * Math.pow(10, level - 2); |
|
if (from < 0 && to >= 0) { |
|
cur = 0; |
|
while (cur > from) { |
|
cur -= step; |
|
stepCount++; |
|
} |
|
from = +cur.toFixed(10); |
|
cur = 0; |
|
while (cur < to) { |
|
cur += step; |
|
stepCount++; |
|
} |
|
to = +cur.toFixed(10); |
|
} else { |
|
cur = from = floor; |
|
while (cur < to) { |
|
cur += step; |
|
stepCount++; |
|
} |
|
} |
|
to = +cur.toFixed(10); |
|
} else { |
|
from = floor; |
|
stepCount = stepsMax; |
|
} |
|
return { |
|
from: from, |
|
to: to, |
|
power: level, |
|
step: step, |
|
steps: stepCount |
|
}; |
|
}, |
|
/** |
|
* snapEndsByDate is a utility method to deduce an appropriate tick configuration for the data set of given |
|
* feature. Refer to {@link #snapEnds}. |
|
* |
|
* @param {Date} from The minimum value in the data |
|
* @param {Date} to The maximum value in the data |
|
* @param {Number} stepsMax The maximum number of ticks |
|
* @param {Boolean} lockEnds If true, the 'from' and 'to' parameters will be used as fixed end values and will not be adjusted |
|
* |
|
* @return {Object} The calculated step and ends info; properties are: |
|
* - from: The result start value, which may be lower than the original start value |
|
* - to: The result end value, which may be higher than the original end value |
|
* - step: The fixed value size of each step, or undefined if the steps are not fixed. |
|
* - steps: The number of steps if the steps are fixed, or an array of step values. |
|
|
|
* NOTE: Even when the steps have a fixed value, they may not divide the from/to range perfectly evenly; |
|
* there may be a smaller distance between the last step and the end value than between prior |
|
* steps, particularly when the `endsLocked` param is true. Therefore it is best to not use |
|
* the `steps` result when finding the axis tick points, instead use the `step`, `to`, and |
|
* `from` to find the correct point for each tick. |
|
*/ |
|
snapEndsByDate: function(from, to, stepsMax, lockEnds) { |
|
var selectedStep = false, |
|
scales = [ |
|
[ |
|
Ext.Date.MILLI, |
|
[ |
|
1, |
|
2, |
|
5, |
|
10, |
|
20, |
|
50, |
|
100, |
|
200, |
|
250, |
|
500 |
|
] |
|
], |
|
[ |
|
Ext.Date.SECOND, |
|
[ |
|
1, |
|
2, |
|
5, |
|
10, |
|
15, |
|
30 |
|
] |
|
], |
|
[ |
|
Ext.Date.MINUTE, |
|
[ |
|
1, |
|
2, |
|
5, |
|
10, |
|
15, |
|
30 |
|
] |
|
], |
|
[ |
|
Ext.Date.HOUR, |
|
[ |
|
1, |
|
2, |
|
3, |
|
4, |
|
6, |
|
12 |
|
] |
|
], |
|
[ |
|
Ext.Date.DAY, |
|
[ |
|
1, |
|
2, |
|
7, |
|
14 |
|
] |
|
], |
|
[ |
|
Ext.Date.MONTH, |
|
[ |
|
1, |
|
2, |
|
3, |
|
6 |
|
] |
|
] |
|
], |
|
sLen = scales.length, |
|
stop = false, |
|
scale, j, yearDiff, s; |
|
// Find the most desirable scale |
|
for (s = 0; s < sLen; s++) { |
|
scale = scales[s]; |
|
if (!stop) { |
|
for (j = 0; j < scale[1].length; j++) { |
|
if (to < Ext.Date.add(from, scale[0], scale[1][j] * stepsMax)) { |
|
selectedStep = [ |
|
scale[0], |
|
scale[1][j] |
|
]; |
|
stop = true; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
if (!selectedStep) { |
|
yearDiff = this.snapEnds(from.getFullYear(), to.getFullYear() + 1, stepsMax, lockEnds); |
|
selectedStep = [ |
|
Date.YEAR, |
|
Math.round(yearDiff.step) |
|
]; |
|
} |
|
return this.snapEndsByDateAndStep(from, to, selectedStep, lockEnds); |
|
}, |
|
/** |
|
* snapEndsByDateAndStep is a utility method to deduce an appropriate tick configuration for the data set of given |
|
* feature and specific step size. |
|
* |
|
* @param {Date} from The minimum value in the data |
|
* @param {Date} to The maximum value in the data |
|
* @param {Array} step An array with two components: The first is the unit of the step (day, month, year, etc). |
|
* The second is the number of units for the step (1, 2, etc.). |
|
* If the number is an integer, it represents the number of units for the step ([Ext.Date.DAY, 2] means "Every other day"). |
|
* If the number is a fraction, it represents the number of steps per unit ([Ext.Date.DAY, 1/2] means "Twice a day"). |
|
* If the unit is the month, the steps may be adjusted depending on the month. For instance [Ext.Date.MONTH, 1/3], which means "Three times a month", |
|
* generates steps on the 1st, the 10th and the 20th of every month regardless of whether a month has 28 days or 31 days. The steps are generated |
|
* as follows: |
|
* - [Ext.Date.MONTH, n]: on the current date every 'n' months, maxed to the number of days in the month. |
|
* - [Ext.Date.MONTH, 1/2]: on the 1st and 15th of every month. |
|
* - [Ext.Date.MONTH, 1/3]: on the 1st, 10th and 20th of every month. |
|
* - [Ext.Date.MONTH, 1/4]: on the 1st, 8th, 15th and 22nd of every month. |
|
* @param {Boolean} lockEnds If true, the 'from' and 'to' parameters will be used as fixed end values |
|
* and will not be adjusted |
|
* |
|
* @return {Object} The calculated step and ends info; properties are: |
|
* - from: The result start value, which may be lower than the original start value |
|
* - to: The result end value, which may be higher than the original end value |
|
* - step: The fixed value size of each step, or undefined if the steps are not fixed. |
|
* - steps: The number of steps if the steps are fixed, or an array of step values. |
|
|
|
* NOTE: Even when the steps have a fixed value, they may not divide the from/to range perfectly evenly; |
|
* there may be a smaller distance between the last step and the end value than between prior |
|
* steps, particularly when the `endsLocked` param is true. Therefore it is best to not use |
|
* the `steps` result when finding the axis tick points, instead use the `step`, `to`, and |
|
* `from` to find the correct point for each tick. For Ext.Date.MONTH and Ext.Date.YEAR step unit, |
|
* `steps` are always returned as array instead of number of steps; this is because months and years |
|
* have uneven step distribution and dividing them in even intervals does not work correctly. |
|
*/ |
|
snapEndsByDateAndStep: function(from, to, step, lockEnds) { |
|
var fromStat = [ |
|
from.getFullYear(), |
|
from.getMonth(), |
|
from.getDate(), |
|
from.getHours(), |
|
from.getMinutes(), |
|
from.getSeconds(), |
|
from.getMilliseconds() |
|
], |
|
testFrom, testTo, date, year, month, day, fractionalMonth, stepsArray, |
|
stepUnit = step[0], |
|
stepValue = step[1], |
|
steps = 0; |
|
if (lockEnds) { |
|
testFrom = from; |
|
} else { |
|
switch (stepUnit) { |
|
case Ext.Date.MILLI: |
|
testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3], fromStat[4], fromStat[5], Math.floor(fromStat[6] / stepValue) * stepValue); |
|
break; |
|
case Ext.Date.SECOND: |
|
testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3], fromStat[4], Math.floor(fromStat[5] / stepValue) * stepValue, 0); |
|
break; |
|
case Ext.Date.MINUTE: |
|
testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3], Math.floor(fromStat[4] / stepValue) * stepValue, 0, 0); |
|
break; |
|
case Ext.Date.HOUR: |
|
testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], Math.floor(fromStat[3] / stepValue) * stepValue, 0, 0, 0); |
|
break; |
|
case Ext.Date.DAY: |
|
testFrom = new Date(fromStat[0], fromStat[1], Math.floor((fromStat[2] - 1) / stepValue) * stepValue + 1, 0, 0, 0, 0); |
|
break; |
|
case Ext.Date.MONTH: |
|
testFrom = new Date(fromStat[0], Math.floor(fromStat[1] / stepValue) * stepValue, 1, 0, 0, 0, 0); |
|
steps = []; |
|
stepsArray = true; |
|
break; |
|
default: |
|
// Ext.Date.YEAR |
|
testFrom = new Date(Math.floor(fromStat[0] / stepValue) * stepValue, 0, 1, 0, 0, 0, 0); |
|
steps = []; |
|
stepsArray = true; |
|
break; |
|
} |
|
} |
|
fractionalMonth = ((stepUnit === Ext.Date.MONTH) && (stepValue == 1 / 2 || stepValue == 1 / 3 || stepValue == 1 / 4)); |
|
// TODO(zhangbei) : We can do it better somehow... |
|
testTo = new Date(testFrom); |
|
while (testTo < to) { |
|
if (fractionalMonth) { |
|
date = new Date(testTo); |
|
year = date.getFullYear(); |
|
month = date.getMonth(); |
|
day = date.getDate(); |
|
switch (stepValue) { |
|
case 1 / 2: |
|
// the 1st and 15th of every month |
|
if (day >= 15) { |
|
day = 1; |
|
if (++month > 11) { |
|
year++; |
|
} |
|
} else { |
|
day = 15; |
|
}; |
|
break; |
|
case 1 / 3: |
|
// the 1st, 10th and 20th of every month |
|
if (day >= 20) { |
|
day = 1; |
|
if (++month > 11) { |
|
year++; |
|
} |
|
} else { |
|
if (day >= 10) { |
|
day = 20; |
|
} else { |
|
day = 10; |
|
} |
|
}; |
|
break; |
|
case 1 / 4: |
|
// the 1st, 8th, 15th and 22nd of every month |
|
if (day >= 22) { |
|
day = 1; |
|
if (++month > 11) { |
|
year++; |
|
} |
|
} else { |
|
if (day >= 15) { |
|
day = 22; |
|
} else { |
|
if (day >= 8) { |
|
day = 15; |
|
} else { |
|
day = 8; |
|
} |
|
} |
|
}; |
|
break; |
|
} |
|
testTo.setYear(year); |
|
testTo.setMonth(month); |
|
testTo.setDate(day); |
|
steps.push(new Date(testTo)); |
|
} else if (stepsArray) { |
|
testTo = Ext.Date.add(testTo, stepUnit, stepValue); |
|
steps.push(new Date(testTo)); |
|
} else { |
|
testTo = Ext.Date.add(testTo, stepUnit, stepValue); |
|
steps++; |
|
} |
|
} |
|
if (lockEnds) { |
|
testTo = to; |
|
} |
|
if (stepsArray) { |
|
return { |
|
from: +testFrom, |
|
to: +testTo, |
|
steps: steps |
|
}; |
|
} else // array of steps |
|
{ |
|
return { |
|
from: +testFrom, |
|
to: +testTo, |
|
step: (testTo - testFrom) / steps, |
|
steps: steps |
|
}; |
|
} |
|
}, |
|
// number of steps |
|
sorter: function(a, b) { |
|
return a.offset - b.offset; |
|
}, |
|
rad: function(degrees) { |
|
return degrees % 360 * Math.PI / 180; |
|
}, |
|
normalizeRadians: function(radian) { |
|
var twoPi = 2 * Math.PI; |
|
if (radian >= 0) { |
|
return radian % twoPi; |
|
} |
|
return ((radian % twoPi) + twoPi) % twoPi; |
|
}, |
|
degrees: function(radian) { |
|
return radian * 180 / Math.PI % 360; |
|
}, |
|
normalizeDegrees: function(degrees) { |
|
if (degrees >= 0) { |
|
return degrees % 360; |
|
} |
|
return ((degrees % 360) + 360) % 360; |
|
}, |
|
withinBox: function(x, y, bbox) { |
|
bbox = bbox || {}; |
|
return (x >= bbox.x && x <= (bbox.x + bbox.width) && y >= bbox.y && y <= (bbox.y + bbox.height)); |
|
}, |
|
parseGradient: function(gradient) { |
|
var me = this, |
|
type = gradient.type || 'linear', |
|
angle = gradient.angle || 0, |
|
radian = me.radian, |
|
stops = gradient.stops, |
|
stopsArr = [], |
|
stop, vector, max, stopObj; |
|
if (type == 'linear') { |
|
vector = [ |
|
0, |
|
0, |
|
Math.cos(angle * radian), |
|
Math.sin(angle * radian) |
|
]; |
|
max = 1 / (Math.max(Math.abs(vector[2]), Math.abs(vector[3])) || 1); |
|
vector[2] *= max; |
|
vector[3] *= max; |
|
if (vector[2] < 0) { |
|
vector[0] = -vector[2]; |
|
vector[2] = 0; |
|
} |
|
if (vector[3] < 0) { |
|
vector[1] = -vector[3]; |
|
vector[3] = 0; |
|
} |
|
} |
|
for (stop in stops) { |
|
if (stops.hasOwnProperty(stop) && me.stopsRE.test(stop)) { |
|
stopObj = { |
|
offset: parseInt(stop, 10), |
|
color: Ext.draw.Color.toHex(stops[stop].color) || '#ffffff', |
|
opacity: stops[stop].opacity || 1 |
|
}; |
|
stopsArr.push(stopObj); |
|
} |
|
} |
|
// Sort by pct property |
|
Ext.Array.sort(stopsArr, me.sorter); |
|
if (type == 'linear') { |
|
return { |
|
id: gradient.id, |
|
type: type, |
|
vector: vector, |
|
stops: stopsArr |
|
}; |
|
} else { |
|
return { |
|
id: gradient.id, |
|
type: type, |
|
centerX: gradient.centerX, |
|
centerY: gradient.centerY, |
|
focalX: gradient.focalX, |
|
focalY: gradient.focalY, |
|
radius: gradient.radius, |
|
vector: vector, |
|
stops: stopsArr |
|
}; |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.axis.Axis |
|
* |
|
* Defines axis for charts. The axis position, type, style can be configured. |
|
* The axes are defined in an axes array of configuration objects where the type, |
|
* field, grid and other configuration options can be set. To know more about how |
|
* to create a Chart please check the Chart class documentation. Here's an example for the axes part: |
|
* An example of axis for a series (in this case for an area chart that has multiple layers of yFields) could be: |
|
* |
|
* axes: [{ |
|
* type: 'Numeric', |
|
* position: 'left', |
|
* titleAlign: 'end', // or 'start', or 'center' (default) |
|
* fields: ['data1', 'data2', 'data3'], |
|
* title: 'Number of Hits', |
|
* grid: { |
|
* odd: { |
|
* opacity: 1, |
|
* fill: '#ddd', |
|
* stroke: '#bbb', |
|
* 'stroke-width': 1 |
|
* } |
|
* }, |
|
* minimum: 0 |
|
* }, { |
|
* type: 'Category', |
|
* position: 'bottom', |
|
* fields: ['name'], |
|
* title: 'Month of the Year', |
|
* grid: true, |
|
* label: { |
|
* rotate: { |
|
* degrees: 315 |
|
* } |
|
* } |
|
* }] |
|
* |
|
* In this case we use a `Numeric` axis for displaying the values of the Area series and a `Category` axis for displaying the names of |
|
* the store elements. The numeric axis is placed on the left of the screen, while the category axis is placed at the bottom of the chart. |
|
* Both the category and numeric axes have `grid` set, which means that horizontal and vertical lines will cover the chart background. In the |
|
* category axis the labels will be rotated so they can fit the space better. |
|
*/ |
|
Ext.define('Ext.chart.axis.Axis', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.chart.axis.Abstract', |
|
alternateClassName: 'Ext.chart.Axis', |
|
requires: [ |
|
'Ext.draw.Draw' |
|
], |
|
/* End Definitions */ |
|
/** |
|
* @cfg {Boolean/Object} grid |
|
* The grid configuration enables you to set a background grid for an axis. |
|
* If set to *true* on a vertical axis, vertical lines will be drawn. |
|
* If set to *true* on a horizontal axis, horizontal lines will be drawn. |
|
* If both are set, a proper grid with horizontal and vertical lines will be drawn. |
|
* |
|
* You can set specific options for the grid configuration for odd and/or even lines/rows. |
|
* Since the rows being drawn are rectangle sprites, you can set to an odd or even property |
|
* all styles that apply to {@link Ext.draw.Sprite}. For more information on all the style |
|
* properties you can set please take a look at {@link Ext.draw.Sprite}. Some useful style |
|
* properties are `opacity`, `fill`, `stroke`, `stroke-width`, etc. |
|
* |
|
* The possible values for a grid option are then *true*, *false*, or an object with `{ odd, even }` properties |
|
* where each property contains a sprite style descriptor object that is defined in {@link Ext.draw.Sprite}. |
|
* |
|
* For example: |
|
* |
|
* axes: [{ |
|
* type: 'Numeric', |
|
* position: 'left', |
|
* fields: ['data1', 'data2', 'data3'], |
|
* title: 'Number of Hits', |
|
* grid: { |
|
* odd: { |
|
* opacity: 1, |
|
* fill: '#ddd', |
|
* stroke: '#bbb', |
|
* 'stroke-width': 1 |
|
* } |
|
* } |
|
* }, { |
|
* type: 'Category', |
|
* position: 'bottom', |
|
* fields: ['name'], |
|
* title: 'Month of the Year', |
|
* grid: true |
|
* }] |
|
* |
|
*/ |
|
/** |
|
* @cfg {Number} majorTickSteps |
|
* If `minimum` and `maximum` are specified it forces the number of major ticks to the specified value. |
|
* If a number of major ticks is forced, it wont search for pretty numbers at the ticks. |
|
*/ |
|
/** |
|
* @cfg {Number} minorTickSteps |
|
* The number of small ticks between two major ticks. Default is zero. |
|
*/ |
|
/** |
|
* @cfg {String} title |
|
* The title for the Axis |
|
*/ |
|
/** |
|
* @cfg {Boolean} hidden |
|
* `true` to hide the axis. |
|
*/ |
|
hidden: false, |
|
// @private force min/max values from store |
|
forceMinMax: false, |
|
/** |
|
* @cfg {Number} dashSize |
|
* The size of the dash marker. Default's 3. |
|
*/ |
|
dashSize: 3, |
|
/** |
|
* @cfg {String} position |
|
* Where to set the axis. Available options are `left`, `bottom`, `right`, `top`. Default's `bottom`. |
|
*/ |
|
position: 'bottom', |
|
// @private |
|
skipFirst: false, |
|
/** |
|
* @cfg {Number} length |
|
* Offset axis position. Default's 0. |
|
*/ |
|
length: 0, |
|
/** |
|
* @cfg {Number} width |
|
* Offset axis width. Default's 0. |
|
*/ |
|
width: 0, |
|
/** |
|
* @cfg {Boolean} adjustEnd |
|
* Whether to adjust the label at the end of the axis. |
|
*/ |
|
adjustEnd: true, |
|
majorTickSteps: false, |
|
nullGutters: { |
|
lower: 0, |
|
upper: 0, |
|
verticalAxis: undefined |
|
}, |
|
// @private |
|
applyData: Ext.emptyFn, |
|
getRange: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
data = store.data.items, |
|
series = chart.series.items, |
|
position = me.position, |
|
axes, |
|
seriesClasses = Ext.chart.series, |
|
aggregations = [], |
|
min = Infinity, |
|
max = -Infinity, |
|
vertical = me.position === 'left' || me.position === 'right' || me.position === 'radial', |
|
i, ln, ln2, j, k, |
|
dataLength = data.length, |
|
aggregates, |
|
countedFields = {}, |
|
allFields = {}, |
|
excludable = true, |
|
fields, fieldMap, record, field, value; |
|
fields = me.fields; |
|
for (j = 0 , ln = fields.length; j < ln; j++) { |
|
allFields[fields[j]] = true; |
|
} |
|
for (i = 0 , ln = series.length; i < ln; i++) { |
|
if (series[i].seriesIsHidden) { |
|
|
|
continue; |
|
} |
|
if (!series[i].getAxesForXAndYFields) { |
|
|
|
continue; |
|
} |
|
axes = series[i].getAxesForXAndYFields(); |
|
if (axes.xAxis && axes.xAxis !== position && axes.yAxis && axes.yAxis !== position) { |
|
// The series doesn't use this axis. |
|
|
|
continue; |
|
} |
|
if (seriesClasses.Bar && series[i] instanceof seriesClasses.Bar && !series[i].column) { |
|
// If this is a horizontal bar series, then flip xField and yField. |
|
fields = vertical ? Ext.Array.from(series[i].xField) : Ext.Array.from(series[i].yField); |
|
} else { |
|
fields = vertical ? Ext.Array.from(series[i].yField) : Ext.Array.from(series[i].xField); |
|
} |
|
if (me.fields.length) { |
|
for (j = 0 , ln2 = fields.length; j < ln2; j++) { |
|
if (allFields[fields[j]]) { |
|
break; |
|
} |
|
} |
|
if (j == ln2) { |
|
// Not matching fields, skipping this series. |
|
|
|
continue; |
|
} |
|
} |
|
if (aggregates = series[i].stacked) { |
|
// If this is a bar/column series, then it will be aggregated if it is of the same direction of the axis. |
|
if (seriesClasses.Bar && series[i] instanceof seriesClasses.Bar) { |
|
if (series[i].column != vertical) { |
|
aggregates = false; |
|
excludable = false; |
|
} |
|
} |
|
// Otherwise it is stacked vertically |
|
else if (!vertical) { |
|
aggregates = false; |
|
excludable = false; |
|
} |
|
} |
|
if (aggregates) { |
|
fieldMap = {}; |
|
for (j = 0; j < fields.length; j++) { |
|
if (excludable && series[i].__excludes && series[i].__excludes[j]) { |
|
|
|
continue; |
|
} |
|
if (!allFields[fields[j]]) { |
|
Ext.Logger.warn('Field `' + fields[j] + '` is not included in the ' + position + ' axis config.'); |
|
} |
|
allFields[fields[j]] = fieldMap[fields[j]] = true; |
|
} |
|
aggregations.push({ |
|
fields: fieldMap, |
|
positiveValue: 0, |
|
negativeValue: 0 |
|
}); |
|
} else { |
|
if (!fields || fields.length == 0) { |
|
fields = me.fields; |
|
} |
|
for (j = 0; j < fields.length; j++) { |
|
if (excludable && series[i].__excludes && series[i].__excludes[j]) { |
|
|
|
continue; |
|
} |
|
allFields[fields[j]] = countedFields[fields[j]] = true; |
|
} |
|
} |
|
} |
|
for (i = 0; i < dataLength; i++) { |
|
record = data[i]; |
|
for (k = 0; k < aggregations.length; k++) { |
|
aggregations[k].positiveValue = 0; |
|
aggregations[k].negativeValue = 0; |
|
} |
|
for (field in allFields) { |
|
value = record.get(field); |
|
if (me.type == 'Time' && typeof value == "string") { |
|
value = Date.parse(value); |
|
} |
|
if (isNaN(value)) { |
|
|
|
continue; |
|
} |
|
if (value === undefined) { |
|
value = 0; |
|
} else { |
|
value = Number(value); |
|
} |
|
if (countedFields[field]) { |
|
if (min > value) { |
|
min = value; |
|
} |
|
if (max < value) { |
|
max = value; |
|
} |
|
} |
|
for (k = 0; k < aggregations.length; k++) { |
|
if (aggregations[k].fields[field]) { |
|
if (value >= 0) { |
|
aggregations[k].positiveValue += value; |
|
if (max < aggregations[k].positiveValue) { |
|
max = aggregations[k].positiveValue; |
|
} |
|
// If any aggregation is actually hit, then the min value should be at most 0. |
|
if (min > 0) { |
|
min = 0; |
|
} |
|
} else { |
|
aggregations[k].negativeValue += value; |
|
if (min > aggregations[k].negativeValue) { |
|
min = aggregations[k].negativeValue; |
|
} |
|
// If any aggregation is actually hit, then the max value should be at least 0. |
|
if (max < 0) { |
|
max = 0; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
if (!isFinite(max)) { |
|
max = me.prevMax || 0; |
|
} |
|
if (!isFinite(min)) { |
|
min = me.prevMin || 0; |
|
} |
|
if (typeof min === 'number') { |
|
min = Ext.Number.correctFloat(min); |
|
} |
|
if (typeof max === 'number') { |
|
max = Ext.Number.correctFloat(max); |
|
} |
|
//normalize min max for snapEnds. |
|
if (min != max && (max != Math.floor(max) || min != Math.floor(min))) { |
|
min = Math.floor(min); |
|
max = Math.floor(max) + 1; |
|
} |
|
if (!isNaN(me.minimum)) { |
|
min = me.minimum; |
|
} |
|
if (!isNaN(me.maximum)) { |
|
max = me.maximum; |
|
} |
|
if (min >= max) { |
|
// snapEnds will return NaN if max >= min; |
|
min = Math.floor(min); |
|
max = min + 1; |
|
} |
|
return { |
|
min: min, |
|
max: max |
|
}; |
|
}, |
|
// @private creates a structure with start, end and step points. |
|
calcEnds: function() { |
|
var me = this, |
|
range = me.getRange(), |
|
min = range.min, |
|
max = range.max, |
|
steps, prettyNumbers, out, changedRange; |
|
steps = (Ext.isNumber(me.majorTickSteps) ? me.majorTickSteps + 1 : me.steps); |
|
prettyNumbers = !(Ext.isNumber(me.maximum) && Ext.isNumber(me.minimum) && Ext.isNumber(me.majorTickSteps) && me.majorTickSteps > 0); |
|
out = Ext.draw.Draw.snapEnds(min, max, steps, prettyNumbers); |
|
if (Ext.isNumber(me.maximum)) { |
|
out.to = me.maximum; |
|
changedRange = true; |
|
} |
|
if (Ext.isNumber(me.minimum)) { |
|
out.from = me.minimum; |
|
changedRange = true; |
|
} |
|
if (me.adjustMaximumByMajorUnit) { |
|
out.to = Math.ceil(out.to / out.step) * out.step; |
|
changedRange = true; |
|
} |
|
if (me.adjustMinimumByMajorUnit) { |
|
out.from = Math.floor(out.from / out.step) * out.step; |
|
changedRange = true; |
|
} |
|
if (changedRange) { |
|
out.steps = Math.ceil((out.to - out.from) / out.step); |
|
} |
|
me.prevMin = (min == max ? 0 : min); |
|
me.prevMax = max; |
|
return out; |
|
}, |
|
/** |
|
* Renders the axis into the screen and updates its position. |
|
*/ |
|
drawAxis: function(init) { |
|
var me = this, |
|
i, |
|
x = me.x, |
|
y = me.y, |
|
dashSize = me.dashSize, |
|
length = me.length, |
|
position = me.position, |
|
verticalAxis = (position == 'left' || position == 'right'), |
|
inflections = [], |
|
calcLabels = (me.isNumericAxis), |
|
stepCalcs = me.applyData(), |
|
step = stepCalcs.step, |
|
steps = stepCalcs.steps, |
|
stepsArray = Ext.isArray(steps), |
|
from = stepCalcs.from, |
|
to = stepCalcs.to, |
|
// If we have a single item, to - from will be 0. |
|
axisRange = (to - from) || 1, |
|
trueLength, currentX, currentY, path, |
|
subDashesX = me.minorTickSteps || 0, |
|
subDashesY = me.minorTickSteps || 0, |
|
dashesX = Math.max(subDashesX + 1, 0), |
|
dashesY = Math.max(subDashesY + 1, 0), |
|
dashDirection = (position == 'left' || position == 'top' ? -1 : 1), |
|
dashLength = dashSize * dashDirection, |
|
series = me.chart.series.items, |
|
firstSeries = series[0], |
|
gutters = Ext.clone(firstSeries ? firstSeries.nullGutters : me.nullGutters), |
|
seriesGutters, hasGutters, sameDirectionGutters, padding, subDashes, subDashValue, |
|
delta = 0, |
|
stepCount = 0, |
|
tick, axes, ln, val, begin, end; |
|
me.from = from; |
|
me.to = to; |
|
// If there is nothing to show, then leave. |
|
if (me.hidden || (from > to)) { |
|
return; |
|
} |
|
// If no steps are specified (for instance if the store is empty), then leave. |
|
if ((stepsArray && (steps.length == 0)) || (!stepsArray && isNaN(step))) { |
|
return; |
|
} |
|
if (stepsArray) { |
|
// Clean the array of steps: |
|
// First remove the steps that are out of bounds. |
|
steps = Ext.Array.filter(steps, function(elem, index, array) { |
|
return (+elem > +me.from && +elem < +me.to); |
|
}, this); |
|
// Then add bounds on each side. |
|
steps = Ext.Array.union([ |
|
me.from |
|
], steps, [ |
|
me.to |
|
]); |
|
} else { |
|
// Build the array of steps out of the fixed-value 'step'. |
|
steps = new Array(); |
|
for (val = +me.from; val < +me.to; val += step) { |
|
steps.push(val); |
|
} |
|
steps.push(+me.to); |
|
} |
|
stepCount = steps.length; |
|
// Get the gutters for the matching series |
|
for (i = 0 , ln = series.length; i < ln; i++) { |
|
if (series[i].seriesIsHidden) { |
|
|
|
continue; |
|
} |
|
if (!series[i].getAxesForXAndYFields) { |
|
|
|
continue; |
|
} |
|
axes = series[i].getAxesForXAndYFields(); |
|
if (!axes.xAxis || !axes.yAxis || (axes.xAxis === position) || (axes.yAxis === position)) { |
|
seriesGutters = Ext.clone(series[i].getGutters()); |
|
hasGutters = (seriesGutters.verticalAxis !== undefined); |
|
sameDirectionGutters = (hasGutters && (seriesGutters.verticalAxis == verticalAxis)); |
|
if (hasGutters) { |
|
if (!sameDirectionGutters) { |
|
// This series has gutters that don't apply to the direction of this axis |
|
// (for instance, gutters for Bars apply to the vertical axis while gutters |
|
// for Columns apply to the horizontal axis). Since there is no gutter, the |
|
// padding is all that is left to take into account. |
|
padding = series[i].getPadding(); |
|
if (verticalAxis) { |
|
seriesGutters = { |
|
lower: padding.bottom, |
|
upper: padding.top, |
|
verticalAxis: true |
|
}; |
|
} else { |
|
seriesGutters = { |
|
lower: padding.left, |
|
upper: padding.right, |
|
verticalAxis: false |
|
}; |
|
} |
|
} |
|
if (gutters.lower < seriesGutters.lower) { |
|
gutters.lower = seriesGutters.lower; |
|
} |
|
if (gutters.upper < seriesGutters.upper) { |
|
gutters.upper = seriesGutters.upper; |
|
} |
|
gutters.verticalAxis = verticalAxis; |
|
} |
|
} |
|
} |
|
// Draw the major ticks |
|
if (calcLabels) { |
|
me.labels = []; |
|
} |
|
if (gutters) { |
|
if (verticalAxis) { |
|
currentX = Math.floor(x); |
|
path = [ |
|
"M", |
|
currentX + 0.5, |
|
y, |
|
"l", |
|
0, |
|
-length |
|
]; |
|
trueLength = length - (gutters.lower + gutters.upper); |
|
for (tick = 0; tick < stepCount; tick++) { |
|
currentY = y - gutters.lower - (steps[tick] - steps[0]) * trueLength / axisRange; |
|
path.push("M", currentX, Math.floor(currentY) + 0.5, "l", dashLength * 2, 0); |
|
inflections.push([ |
|
currentX, |
|
Math.floor(currentY) |
|
]); |
|
if (calcLabels) { |
|
me.labels.push(steps[tick]); |
|
} |
|
} |
|
} else { |
|
currentY = Math.floor(y); |
|
path = [ |
|
"M", |
|
x, |
|
currentY + 0.5, |
|
"l", |
|
length, |
|
0 |
|
]; |
|
trueLength = length - (gutters.lower + gutters.upper); |
|
for (tick = 0; tick < stepCount; tick++) { |
|
currentX = x + gutters.lower + (steps[tick] - steps[0]) * trueLength / axisRange; |
|
path.push("M", Math.floor(currentX) + 0.5, currentY, "l", 0, dashLength * 2 + 1); |
|
inflections.push([ |
|
Math.floor(currentX), |
|
currentY |
|
]); |
|
if (calcLabels) { |
|
me.labels.push(steps[tick]); |
|
} |
|
} |
|
} |
|
} |
|
// Draw the minor ticks |
|
// If 'minorTickSteps' is... |
|
// - A number: it contains the number of minor ticks between 2 major ticks. |
|
// - An array with 2 numbers: it contains a date interval like [Ext.Date.DAY,2]. |
|
// - An array with a single number: it contains the value of a minor tick. |
|
subDashes = (verticalAxis ? subDashesY : subDashesX); |
|
if (Ext.isArray(subDashes)) { |
|
if (subDashes.length == 2) { |
|
subDashValue = +Ext.Date.add(new Date(), subDashes[0], subDashes[1]) - Date.now(); |
|
} else { |
|
subDashValue = subDashes[0]; |
|
} |
|
} else { |
|
if (Ext.isNumber(subDashes) && subDashes > 0) { |
|
subDashValue = step / (subDashes + 1); |
|
} |
|
} |
|
if (gutters && subDashValue) { |
|
for (tick = 0; tick < stepCount - 1; tick++) { |
|
begin = +steps[tick]; |
|
end = +steps[tick + 1]; |
|
if (verticalAxis) { |
|
for (value = begin + subDashValue; value < end; value += subDashValue) { |
|
currentY = y - gutters.lower - (value - steps[0]) * trueLength / axisRange; |
|
path.push("M", currentX, Math.floor(currentY) + 0.5, "l", dashLength, 0); |
|
} |
|
} else { |
|
for (value = begin + subDashValue; value < end; value += subDashValue) { |
|
currentX = x + gutters.upper + (value - steps[0]) * trueLength / axisRange; |
|
path.push("M", Math.floor(currentX) + 0.5, currentY, "l", 0, dashLength + 1); |
|
} |
|
} |
|
} |
|
} |
|
// Render |
|
if (!me.axis) { |
|
me.axis = me.chart.surface.add(Ext.apply({ |
|
type: 'path', |
|
path: path |
|
}, me.axisStyle)); |
|
} |
|
me.axis.setAttributes({ |
|
path: path |
|
}, true); |
|
me.inflections = inflections; |
|
if (!init && me.grid) { |
|
me.drawGrid(); |
|
} |
|
me.axisBBox = me.axis.getBBox(); |
|
me.drawLabel(); |
|
}, |
|
/** |
|
* Renders an horizontal and/or vertical grid into the Surface. |
|
*/ |
|
drawGrid: function() { |
|
var me = this, |
|
surface = me.chart.surface, |
|
grid = me.grid, |
|
odd = grid.odd, |
|
even = grid.even, |
|
inflections = me.inflections, |
|
ln = inflections.length - ((odd || even) ? 0 : 1), |
|
position = me.position, |
|
maxGutters = me.chart.maxGutters, |
|
width = me.width - 2, |
|
point, prevPoint, |
|
i = 1, |
|
path = [], |
|
styles, lineWidth, dlineWidth, |
|
oddPath = [], |
|
evenPath = []; |
|
if (((maxGutters.bottom !== 0 || maxGutters.top !== 0) && (position == 'left' || position == 'right')) || ((maxGutters.left !== 0 || maxGutters.right !== 0) && (position == 'top' || position == 'bottom'))) { |
|
i = 0; |
|
ln++; |
|
} |
|
for (; i < ln; i++) { |
|
point = inflections[i]; |
|
prevPoint = inflections[i - 1]; |
|
if (odd || even) { |
|
path = (i % 2) ? oddPath : evenPath; |
|
styles = ((i % 2) ? odd : even) || {}; |
|
lineWidth = (styles.lineWidth || styles['stroke-width'] || 0) / 2; |
|
dlineWidth = 2 * lineWidth; |
|
if (position == 'left') { |
|
path.push("M", prevPoint[0] + 1 + lineWidth, prevPoint[1] + 0.5 - lineWidth, "L", prevPoint[0] + 1 + width - lineWidth, prevPoint[1] + 0.5 - lineWidth, "L", point[0] + 1 + width - lineWidth, point[1] + 0.5 + lineWidth, "L", point[0] + 1 + lineWidth, point[1] + 0.5 + lineWidth, "Z"); |
|
} else if (position == 'right') { |
|
path.push("M", prevPoint[0] - lineWidth, prevPoint[1] + 0.5 - lineWidth, "L", prevPoint[0] - width + lineWidth, prevPoint[1] + 0.5 - lineWidth, "L", point[0] - width + lineWidth, point[1] + 0.5 + lineWidth, "L", point[0] - lineWidth, point[1] + 0.5 + lineWidth, "Z"); |
|
} else if (position == 'top') { |
|
path.push("M", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] + 1 + lineWidth, "L", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] + 1 + width - lineWidth, "L", point[0] + 0.5 - lineWidth, point[1] + 1 + width - lineWidth, "L", point[0] + 0.5 - lineWidth, point[1] + 1 + lineWidth, "Z"); |
|
} else { |
|
path.push("M", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] - lineWidth, "L", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] - width + lineWidth, "L", point[0] + 0.5 - lineWidth, point[1] - width + lineWidth, "L", point[0] + 0.5 - lineWidth, point[1] - lineWidth, "Z"); |
|
} |
|
} else { |
|
if (position == 'left') { |
|
path = path.concat([ |
|
"M", |
|
point[0] + 0.5, |
|
point[1] + 0.5, |
|
"l", |
|
width, |
|
0 |
|
]); |
|
} else if (position == 'right') { |
|
path = path.concat([ |
|
"M", |
|
point[0] - 0.5, |
|
point[1] + 0.5, |
|
"l", |
|
-width, |
|
0 |
|
]); |
|
} else if (position == 'top') { |
|
path = path.concat([ |
|
"M", |
|
point[0] + 0.5, |
|
point[1] + 0.5, |
|
"l", |
|
0, |
|
width |
|
]); |
|
} else { |
|
path = path.concat([ |
|
"M", |
|
point[0] + 0.5, |
|
point[1] - 0.5, |
|
"l", |
|
0, |
|
-width |
|
]); |
|
} |
|
} |
|
} |
|
if (odd || even) { |
|
if (oddPath.length) { |
|
if (!me.gridOdd && oddPath.length) { |
|
me.gridOdd = surface.add({ |
|
type: 'path', |
|
path: oddPath |
|
}); |
|
} |
|
me.gridOdd.setAttributes(Ext.apply({ |
|
path: oddPath, |
|
hidden: false |
|
}, odd || {}), true); |
|
} |
|
if (evenPath.length) { |
|
if (!me.gridEven) { |
|
me.gridEven = surface.add({ |
|
type: 'path', |
|
path: evenPath |
|
}); |
|
} |
|
me.gridEven.setAttributes(Ext.apply({ |
|
path: evenPath, |
|
hidden: false |
|
}, even || {}), true); |
|
} |
|
} else { |
|
if (path.length) { |
|
if (!me.gridLines) { |
|
me.gridLines = me.chart.surface.add({ |
|
type: 'path', |
|
path: path, |
|
"stroke-width": me.lineWidth || 1, |
|
stroke: me.gridColor || '#ccc' |
|
}); |
|
} |
|
me.gridLines.setAttributes({ |
|
hidden: false, |
|
path: path |
|
}, true); |
|
} else if (me.gridLines) { |
|
me.gridLines.hide(true); |
|
} |
|
} |
|
}, |
|
// @private |
|
getOrCreateLabel: function(i, text) { |
|
var me = this, |
|
labelGroup = me.labelGroup, |
|
textLabel = labelGroup.getAt(i), |
|
surface = me.chart.surface; |
|
if (textLabel) { |
|
if (text != textLabel.attr.text) { |
|
textLabel.setAttributes(Ext.apply({ |
|
text: text |
|
}, me.label), true); |
|
textLabel._bbox = textLabel.getBBox(); |
|
} |
|
} else { |
|
textLabel = surface.add(Ext.apply({ |
|
group: labelGroup, |
|
type: 'text', |
|
x: 0, |
|
y: 0, |
|
text: text |
|
}, me.label)); |
|
surface.renderItem(textLabel); |
|
textLabel._bbox = textLabel.getBBox(); |
|
} |
|
//get untransformed bounding box |
|
if (me.label.rotation) { |
|
textLabel.setAttributes({ |
|
rotation: { |
|
degrees: 0 |
|
} |
|
}, true); |
|
textLabel._ubbox = textLabel.getBBox(); |
|
textLabel.setAttributes(me.label, true); |
|
} else { |
|
textLabel._ubbox = textLabel._bbox; |
|
} |
|
return textLabel; |
|
}, |
|
rect2pointArray: function(sprite) { |
|
var surface = this.chart.surface, |
|
rect = surface.getBBox(sprite, true), |
|
p1 = [ |
|
rect.x, |
|
rect.y |
|
], |
|
p1p = p1.slice(), |
|
p2 = [ |
|
rect.x + rect.width, |
|
rect.y |
|
], |
|
p2p = p2.slice(), |
|
p3 = [ |
|
rect.x + rect.width, |
|
rect.y + rect.height |
|
], |
|
p3p = p3.slice(), |
|
p4 = [ |
|
rect.x, |
|
rect.y + rect.height |
|
], |
|
p4p = p4.slice(), |
|
matrix = sprite.matrix; |
|
//transform the points |
|
p1[0] = matrix.x.apply(matrix, p1p); |
|
p1[1] = matrix.y.apply(matrix, p1p); |
|
p2[0] = matrix.x.apply(matrix, p2p); |
|
p2[1] = matrix.y.apply(matrix, p2p); |
|
p3[0] = matrix.x.apply(matrix, p3p); |
|
p3[1] = matrix.y.apply(matrix, p3p); |
|
p4[0] = matrix.x.apply(matrix, p4p); |
|
p4[1] = matrix.y.apply(matrix, p4p); |
|
return [ |
|
p1, |
|
p2, |
|
p3, |
|
p4 |
|
]; |
|
}, |
|
intersect: function(l1, l2) { |
|
var r1 = this.rect2pointArray(l1), |
|
r2 = this.rect2pointArray(l2); |
|
return !!Ext.draw.Draw.intersect(r1, r2).length; |
|
}, |
|
drawHorizontalLabels: function() { |
|
var me = this, |
|
labelConf = me.label, |
|
floor = Math.floor, |
|
max = Math.max, |
|
axes = me.chart.axes, |
|
insetPadding = me.chart.insetPadding, |
|
gutters = me.chart.maxGutters, |
|
position = me.position, |
|
inflections = me.inflections, |
|
ln = inflections.length, |
|
labels = me.labels, |
|
maxHeight = 0, |
|
ratio, bbox, point, prevLabel, prevLabelId, |
|
adjustEnd = me.adjustEnd, |
|
hasLeft = axes.findIndex('position', 'left') != -1, |
|
hasRight = axes.findIndex('position', 'right') != -1, |
|
reverse = me.reverse, |
|
textLabel, text, idx, last, x, y, i, firstLabel; |
|
last = ln - 1; |
|
//get a reference to the first text label dimensions |
|
point = inflections[0]; |
|
firstLabel = me.getOrCreateLabel(0, me.label.renderer(labels[0])); |
|
ratio = Math.floor(Math.abs(Math.sin(labelConf.rotate && (labelConf.rotate.degrees * Math.PI / 180) || 0))); |
|
for (i = 0; i < ln; i++) { |
|
point = inflections[i]; |
|
idx = i; |
|
if (reverse) { |
|
idx = ln - i - 1; |
|
} |
|
text = me.label.renderer(labels[idx]); |
|
textLabel = me.getOrCreateLabel(i, text); |
|
bbox = textLabel._bbox; |
|
maxHeight = max(maxHeight, bbox.height + me.dashSize + me.label.padding); |
|
x = floor(point[0] - (ratio ? bbox.height : bbox.width) / 2); |
|
if (adjustEnd && gutters.left == 0 && gutters.right == 0) { |
|
if (i == 0 && !hasLeft) { |
|
x = point[0]; |
|
} else if (i == last && !hasRight) { |
|
x = Math.min(x, point[0] - bbox.width + insetPadding); |
|
} |
|
} |
|
if (position == 'top') { |
|
y = point[1] - (me.dashSize * 2) - me.label.padding - (bbox.height / 2); |
|
} else { |
|
y = point[1] + (me.dashSize * 2) + me.label.padding + (bbox.height / 2); |
|
} |
|
textLabel.setAttributes({ |
|
hidden: false, |
|
x: x, |
|
y: y |
|
}, true); |
|
// Skip label if there isn't available minimum space |
|
if (i != 0 && (me.intersect(textLabel, prevLabel) || me.intersect(textLabel, firstLabel))) { |
|
if (i === last && prevLabelId !== 0) { |
|
prevLabel.hide(true); |
|
} else { |
|
textLabel.hide(true); |
|
|
|
continue; |
|
} |
|
} |
|
prevLabel = textLabel; |
|
prevLabelId = i; |
|
} |
|
return maxHeight; |
|
}, |
|
drawVerticalLabels: function() { |
|
var me = this, |
|
inflections = me.inflections, |
|
position = me.position, |
|
ln = inflections.length, |
|
chart = me.chart, |
|
insetPadding = chart.insetPadding, |
|
labels = me.labels, |
|
maxWidth = 0, |
|
max = Math.max, |
|
floor = Math.floor, |
|
ceil = Math.ceil, |
|
axes = me.chart.axes, |
|
gutters = me.chart.maxGutters, |
|
bbox, point, prevLabel, prevLabelId, |
|
hasTop = axes.findIndex('position', 'top') != -1, |
|
hasBottom = axes.findIndex('position', 'bottom') != -1, |
|
adjustEnd = me.adjustEnd, |
|
textLabel, text, |
|
last = ln - 1, |
|
x, y, i; |
|
for (i = 0; i < ln; i++) { |
|
point = inflections[i]; |
|
text = me.label.renderer(labels[i]); |
|
textLabel = me.getOrCreateLabel(i, text); |
|
bbox = textLabel._bbox; |
|
maxWidth = max(maxWidth, bbox.width + me.dashSize + me.label.padding); |
|
y = point[1]; |
|
if (adjustEnd && (gutters.bottom + gutters.top) < bbox.height / 2) { |
|
if (i == last && !hasTop) { |
|
y = Math.max(y, me.y - me.length + ceil(bbox.height / 2) - insetPadding); |
|
} else if (i == 0 && !hasBottom) { |
|
y = me.y + gutters.bottom - floor(bbox.height / 2); |
|
} |
|
} |
|
if (position == 'left') { |
|
x = point[0] - bbox.width - me.dashSize - me.label.padding - 2; |
|
} else { |
|
x = point[0] + me.dashSize + me.label.padding + 2; |
|
} |
|
textLabel.setAttributes(Ext.apply({ |
|
hidden: false, |
|
x: x, |
|
y: y |
|
}, me.label), true); |
|
// Skip label if there isn't available minimum space |
|
if (i != 0 && me.intersect(textLabel, prevLabel)) { |
|
if (i === last && prevLabelId !== 0) { |
|
prevLabel.hide(true); |
|
} else { |
|
textLabel.hide(true); |
|
|
|
continue; |
|
} |
|
} |
|
prevLabel = textLabel; |
|
prevLabelId = i; |
|
} |
|
return maxWidth; |
|
}, |
|
/** |
|
* Renders the labels in the axes. |
|
*/ |
|
drawLabel: function() { |
|
var me = this, |
|
position = me.position, |
|
labelGroup = me.labelGroup, |
|
inflections = me.inflections, |
|
maxWidth = 0, |
|
maxHeight = 0, |
|
ln, i; |
|
if (position == 'left' || position == 'right') { |
|
maxWidth = me.drawVerticalLabels(); |
|
} else { |
|
maxHeight = me.drawHorizontalLabels(); |
|
} |
|
// Hide unused bars |
|
ln = labelGroup.getCount(); |
|
i = inflections.length; |
|
for (; i < ln; i++) { |
|
labelGroup.getAt(i).hide(true); |
|
} |
|
me.bbox = {}; |
|
Ext.apply(me.bbox, me.axisBBox); |
|
me.bbox.height = maxHeight; |
|
me.bbox.width = maxWidth; |
|
if (Ext.isString(me.title)) { |
|
me.drawTitle(maxWidth, maxHeight); |
|
} |
|
}, |
|
/** |
|
* Updates the {@link #title} of this axis. |
|
* @param {String} title |
|
*/ |
|
setTitle: function(title) { |
|
this.title = title; |
|
this.drawLabel(); |
|
}, |
|
// @private draws the title for the axis. |
|
drawTitle: function(maxWidth, maxHeight) { |
|
var me = this, |
|
position = me.position, |
|
titleAlign = me.titleAlign, |
|
surface = me.chart.surface, |
|
displaySprite = me.displaySprite, |
|
title = me.title, |
|
rotate = (position == 'left' || position == 'right'), |
|
x = me.x, |
|
y = me.y, |
|
base, bbox, pad; |
|
if (displaySprite) { |
|
displaySprite.setAttributes({ |
|
text: title |
|
}, true); |
|
} else { |
|
base = { |
|
type: 'text', |
|
x: 0, |
|
y: 0, |
|
text: title |
|
}; |
|
displaySprite = me.displaySprite = surface.add(Ext.apply(base, me.axisTitleStyle, me.labelTitle)); |
|
surface.renderItem(displaySprite); |
|
} |
|
bbox = displaySprite.getBBox(); |
|
pad = me.dashSize + me.label.padding; |
|
if (rotate) { |
|
if (titleAlign === 'end') { |
|
y -= me.length - bbox.height; |
|
} else if (!titleAlign || titleAlign === 'center') { |
|
y -= ((me.length / 2) - (bbox.height / 2)); |
|
} |
|
if (position == 'left') { |
|
x -= (maxWidth + pad + (bbox.width / 2)); |
|
} else { |
|
x += (maxWidth + pad + bbox.width - (bbox.width / 2)); |
|
} |
|
me.bbox.width += bbox.width + 10; |
|
} else { |
|
if (titleAlign === 'end' || (me.reverse && titleAlign === 'start')) { |
|
x += me.length - bbox.width; |
|
} else if (!titleAlign || titleAlign === 'center') { |
|
x += (me.length / 2) - (bbox.width * 0.5); |
|
} |
|
if (position == 'top') { |
|
y -= (maxHeight + pad + (bbox.height * 0.3)); |
|
} else { |
|
y += (maxHeight + pad + (bbox.height * 0.8)); |
|
} |
|
me.bbox.height += bbox.height + 10; |
|
} |
|
displaySprite.setAttributes({ |
|
translate: { |
|
x: x, |
|
y: y |
|
} |
|
}, true); |
|
} |
|
}); |
|
|
|
Ext.define('Ext.rtl.chart.axis.Axis', { |
|
override: 'Ext.chart.axis.Axis', |
|
constructor: function() { |
|
var me = this, |
|
pos; |
|
me.callParent(arguments); |
|
pos = me.position; |
|
if (me.chart.getInherited().rtl && (pos == 'top' || pos == 'bottom')) { |
|
me.reverse = true; |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.axis.Category |
|
* |
|
* A type of axis that displays items in categories. This axis is generally used to |
|
* display categorical information like names of items, month names, quarters, etc. |
|
* but no quantitative values. For that other type of information `Number` |
|
* axis are more suitable. |
|
* |
|
* As with other axis you can set the position of the axis and its title. For example: |
|
* |
|
* @example |
|
* var store = Ext.create('Ext.data.JsonStore', { |
|
* 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} |
|
* ] |
|
* }); |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* width: 500, |
|
* height: 300, |
|
* store: store, |
|
* axes: [{ |
|
* type: 'Numeric', |
|
* position: 'left', |
|
* fields: ['data1', 'data2', 'data3', 'data4', 'data5'], |
|
* title: 'Sample Values', |
|
* grid: { |
|
* odd: { |
|
* opacity: 1, |
|
* fill: '#ddd', |
|
* stroke: '#bbb', |
|
* 'stroke-width': 1 |
|
* } |
|
* }, |
|
* minimum: 0, |
|
* adjustMinimumByMajorUnit: 0 |
|
* }, { |
|
* type: 'Category', |
|
* position: 'bottom', |
|
* fields: ['name'], |
|
* title: 'Sample Metrics', |
|
* grid: true, |
|
* label: { |
|
* rotate: { |
|
* degrees: 315 |
|
* } |
|
* } |
|
* }], |
|
* series: [{ |
|
* type: 'area', |
|
* highlight: false, |
|
* axis: 'left', |
|
* xField: 'name', |
|
* yField: ['data1', 'data2', 'data3', 'data4', 'data5'], |
|
* style: { |
|
* opacity: 0.93 |
|
* } |
|
* }] |
|
* }); |
|
* |
|
* In this example with set the category axis to the bottom of the surface, bound the axis to |
|
* the `name` property and set as title _Month of the Year_. |
|
*/ |
|
Ext.define('Ext.chart.axis.Category', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.chart.axis.Axis', |
|
alternateClassName: 'Ext.chart.CategoryAxis', |
|
alias: 'axis.category', |
|
/** |
|
* Indicates whether or not the axis is Categorical |
|
*/ |
|
isCategoryAxis: true, |
|
/* End Definitions */ |
|
// @private constrains to datapoints between minimum and maximum only |
|
doConstrain: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
items = store.data.items, |
|
series = chart.series.items, |
|
seriesLength = series.length, |
|
data = [], |
|
i; |
|
for (i = 0; i < seriesLength; i++) { |
|
if (series[i].type === 'bar' && series[i].stacked) { |
|
// Do not constrain stacked bar chart. |
|
return; |
|
} |
|
} |
|
for (i = me.minimum; i < me.maximum; i++) { |
|
data.push(items[i]); |
|
} |
|
chart.setSubStore(new Ext.data.Store({ |
|
model: store.model, |
|
data: data |
|
})); |
|
}, |
|
// @private creates an array of labels to be used when rendering. |
|
setLabels: function() { |
|
var store = this.chart.getChartStore(), |
|
data = store.data.items, |
|
d, dLen, record, |
|
fields = this.fields, |
|
ln = fields.length, |
|
labels, name, i; |
|
labels = this.labels = []; |
|
for (d = 0 , dLen = data.length; d < dLen; d++) { |
|
record = data[d]; |
|
for (i = 0; i < ln; i++) { |
|
name = record.get(fields[i]); |
|
if (Ext.Array.indexOf(labels, name) > -1) { |
|
Ext.log.warn('Duplicate category in axis, ' + name); |
|
} |
|
labels.push(name); |
|
} |
|
} |
|
}, |
|
// @private calculates labels positions and marker positions for rendering. |
|
applyData: function() { |
|
this.callParent(); |
|
this.setLabels(); |
|
var count = this.chart.getChartStore().getCount(); |
|
return { |
|
from: 0, |
|
to: count - 1, |
|
power: 1, |
|
step: 1, |
|
steps: count - 1 |
|
}; |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.axis.Gauge |
|
* |
|
* Gauge Axis is the axis to be used with a Gauge series. The Gauge axis |
|
* displays numeric data from an interval defined by the `minimum`, `maximum` and |
|
* `step` configuration properties. The placement of the numeric data can be changed |
|
* by altering the `margin` option that is set to `10` by default. |
|
* |
|
* A possible configuration for this axis would look like: |
|
* |
|
* axes: [{ |
|
* type: 'gauge', |
|
* position: 'gauge', |
|
* minimum: 0, |
|
* maximum: 100, |
|
* steps: 10, |
|
* margin: 7 |
|
* }], |
|
*/ |
|
Ext.define('Ext.chart.axis.Gauge', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.chart.axis.Abstract', |
|
/* End Definitions */ |
|
/** |
|
* @cfg {Number} minimum (required) |
|
* The minimum value of the interval to be displayed in the axis. |
|
*/ |
|
/** |
|
* @cfg {Number} maximum (required) |
|
* The maximum value of the interval to be displayed in the axis. |
|
*/ |
|
/** |
|
* @cfg {Number} steps (required) |
|
* The number of steps and tick marks to add to the interval. |
|
*/ |
|
/** |
|
* @cfg {Number} [margin=10] |
|
* The offset positioning of the tick marks and labels in pixels. |
|
*/ |
|
/** |
|
* @cfg {String} title |
|
* The title for the Axis. |
|
*/ |
|
position: 'gauge', |
|
alias: 'axis.gauge', |
|
drawAxis: function(init) { |
|
var chart = this.chart, |
|
surface = chart.surface, |
|
bbox = chart.chartBBox, |
|
centerX = bbox.x + (bbox.width / 2), |
|
centerY = bbox.y + bbox.height, |
|
margin = this.margin || 10, |
|
rho = Math.min(bbox.width, 2 * bbox.height) / 2 + margin, |
|
sprites = [], |
|
sprite, |
|
steps = this.steps, |
|
i, |
|
pi = Math.PI, |
|
cos = Math.cos, |
|
sin = Math.sin; |
|
if (this.sprites && !chart.resizing) { |
|
this.drawLabel(); |
|
return; |
|
} |
|
if (this.margin >= 0) { |
|
if (!this.sprites) { |
|
//draw circles |
|
for (i = 0; i <= steps; i++) { |
|
sprite = surface.add({ |
|
type: 'path', |
|
path: [ |
|
'M', |
|
centerX + (rho - margin) * cos(i / steps * pi - pi), |
|
centerY + (rho - margin) * sin(i / steps * pi - pi), |
|
'L', |
|
centerX + rho * cos(i / steps * pi - pi), |
|
centerY + rho * sin(i / steps * pi - pi), |
|
'Z' |
|
], |
|
stroke: '#ccc' |
|
}); |
|
sprite.setAttributes({ |
|
hidden: false |
|
}, true); |
|
sprites.push(sprite); |
|
} |
|
} else { |
|
sprites = this.sprites; |
|
//draw circles |
|
for (i = 0; i <= steps; i++) { |
|
sprites[i].setAttributes({ |
|
path: [ |
|
'M', |
|
centerX + (rho - margin) * cos(i / steps * pi - pi), |
|
centerY + (rho - margin) * sin(i / steps * pi - pi), |
|
'L', |
|
centerX + rho * cos(i / steps * pi - pi), |
|
centerY + rho * sin(i / steps * pi - pi), |
|
'Z' |
|
], |
|
stroke: '#ccc' |
|
}, true); |
|
} |
|
} |
|
} |
|
this.sprites = sprites; |
|
this.drawLabel(); |
|
if (this.title) { |
|
this.drawTitle(); |
|
} |
|
}, |
|
drawTitle: function() { |
|
var me = this, |
|
chart = me.chart, |
|
surface = chart.surface, |
|
bbox = chart.chartBBox, |
|
labelSprite = me.titleSprite, |
|
labelBBox; |
|
if (!labelSprite) { |
|
me.titleSprite = labelSprite = surface.add(Ext.apply({ |
|
type: 'text', |
|
zIndex: 2 |
|
}, me.axisTitleStyle, me.labelTitle)); |
|
} |
|
labelSprite.setAttributes(Ext.apply({ |
|
text: me.title |
|
}, me.label || {}), true); |
|
labelBBox = labelSprite.getBBox(); |
|
labelSprite.setAttributes({ |
|
x: bbox.x + (bbox.width / 2) - (labelBBox.width / 2), |
|
y: bbox.y + bbox.height - (labelBBox.height / 2) - 4 |
|
}, true); |
|
}, |
|
/** |
|
* Updates the {@link #title} of this axis. |
|
* @param {String} title |
|
*/ |
|
setTitle: function(title) { |
|
this.title = title; |
|
this.drawTitle(); |
|
}, |
|
drawLabel: function() { |
|
var me = this, |
|
chart = me.chart, |
|
surface = chart.surface, |
|
bbox = chart.chartBBox, |
|
centerX = bbox.x + (bbox.width / 2), |
|
centerY = bbox.y + bbox.height, |
|
margin = me.margin || 10, |
|
rho = Math.min(bbox.width, 2 * bbox.height) / 2 + 2 * margin, |
|
round = Math.round, |
|
labelArray = [], |
|
label, |
|
maxValue = me.maximum || 0, |
|
minValue = me.minimum || 0, |
|
steps = me.steps, |
|
pi = Math.PI, |
|
cos = Math.cos, |
|
sin = Math.sin, |
|
labelConf = this.label, |
|
renderer = labelConf.renderer || Ext.identityFn, |
|
reverse = me.reverse, |
|
i, adjY, idx; |
|
if (!this.labelArray) { |
|
//draw scale |
|
for (i = 0; i <= steps; i++) { |
|
// TODO Adjust for height of text / 2 instead |
|
adjY = (i === 0 || i === steps) ? 7 : 0; |
|
idx = reverse ? steps - i : i; |
|
label = surface.add({ |
|
type: 'text', |
|
text: renderer(round(minValue + idx / steps * (maxValue - minValue))), |
|
x: centerX + rho * cos(i / steps * pi - pi), |
|
y: centerY + rho * sin(i / steps * pi - pi) - adjY, |
|
'text-anchor': 'middle', |
|
'stroke-width': 0.2, |
|
zIndex: 10, |
|
stroke: '#333' |
|
}); |
|
label.setAttributes({ |
|
hidden: false |
|
}, true); |
|
labelArray.push(label); |
|
} |
|
} else { |
|
labelArray = this.labelArray; |
|
//draw values |
|
for (i = 0; i <= steps; i++) { |
|
// TODO Adjust for height of text / 2 instead |
|
adjY = (i === 0 || i === steps) ? 7 : 0; |
|
idx = reverse ? steps - i : i; |
|
labelArray[i].setAttributes({ |
|
text: renderer(round(minValue + idx / steps * (maxValue - minValue))), |
|
x: centerX + rho * cos(i / steps * pi - pi), |
|
y: centerY + rho * sin(i / steps * pi - pi) - adjY |
|
}, true); |
|
} |
|
} |
|
this.labelArray = labelArray; |
|
} |
|
}); |
|
|
|
Ext.define('Ext.rtl.chart.axis.Gauge', { |
|
override: 'Ext.chart.axis.Gauge', |
|
constructor: function() { |
|
var me = this; |
|
me.callParent(arguments); |
|
if (me.chart.getInherited().rtl) { |
|
me.reverse = true; |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.axis.Numeric |
|
* |
|
* An axis to handle numeric values. This axis is used for quantitative data as |
|
* opposed to the category axis. You can set mininum and maximum values to the |
|
* axis so that the values are bound to that. If no values are set, then the |
|
* scale will auto-adjust to the values. |
|
* |
|
* @example |
|
* var store = Ext.create('Ext.data.JsonStore', { |
|
* 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} |
|
* ] |
|
* }); |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* width: 500, |
|
* height: 300, |
|
* store: store, |
|
* axes: [{ |
|
* type: 'Numeric', |
|
* position: 'left', |
|
* fields: ['data1', 'data2', 'data3', 'data4', 'data5'], |
|
* title: 'Sample Values', |
|
* grid: { |
|
* odd: { |
|
* opacity: 1, |
|
* fill: '#ddd', |
|
* stroke: '#bbb', |
|
* 'stroke-width': 1 |
|
* } |
|
* }, |
|
* minimum: 0, |
|
* adjustMinimumByMajorUnit: 0 |
|
* }, { |
|
* type: 'Category', |
|
* position: 'bottom', |
|
* fields: ['name'], |
|
* title: 'Sample Metrics', |
|
* grid: true, |
|
* label: { |
|
* rotate: { |
|
* degrees: 315 |
|
* } |
|
* } |
|
* }], |
|
* series: [{ |
|
* type: 'area', |
|
* highlight: false, |
|
* axis: 'left', |
|
* xField: 'name', |
|
* yField: ['data1', 'data2', 'data3', 'data4', 'data5'], |
|
* style: { |
|
* opacity: 0.93 |
|
* } |
|
* }] |
|
* }); |
|
* |
|
* In this example we create an axis of Numeric type. We set a minimum value so that |
|
* even if all series have values greater than zero, the grid starts at zero. We bind |
|
* the axis onto the left part of the surface by setting `position` to `left`. |
|
* We bind three different store fields to this axis by setting `fields` to an array. |
|
* We set the title of the axis to _Number of Hits_ by using the `title` property. |
|
* We use a `grid` configuration to set odd background rows to a certain style and even rows |
|
* to be transparent/ignored. |
|
*/ |
|
Ext.define('Ext.chart.axis.Numeric', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.chart.axis.Axis', |
|
alternateClassName: 'Ext.chart.NumericAxis', |
|
/* End Definitions */ |
|
type: 'Numeric', |
|
/** |
|
* Indicates whether or not the axis is Numeric |
|
*/ |
|
isNumericAxis: true, |
|
alias: 'axis.numeric', |
|
uses: [ |
|
'Ext.data.Store' |
|
], |
|
constructor: function(config) { |
|
var me = this, |
|
hasLabel = !!(config.label && config.label.renderer), |
|
label; |
|
me.callParent([ |
|
config |
|
]); |
|
label = me.label; |
|
if (config.constrain == null) { |
|
me.constrain = (config.minimum != null && config.maximum != null); |
|
} |
|
if (!hasLabel) { |
|
label.renderer = function(v) { |
|
return me.roundToDecimal(v, me.decimals); |
|
}; |
|
} |
|
}, |
|
roundToDecimal: function(v, dec) { |
|
var val = Math.pow(10, dec || 0); |
|
return Math.round(v * val) / val; |
|
}, |
|
/** |
|
* @cfg {Number} minimum |
|
* The minimum value drawn by the axis. If not set explicitly, the axis |
|
* minimum will be calculated automatically. It is ignored for stacked charts. |
|
*/ |
|
minimum: NaN, |
|
/** |
|
* @cfg {Number} maximum |
|
* The maximum value drawn by the axis. If not set explicitly, the axis |
|
* maximum will be calculated automatically. It is ignored for stacked charts. |
|
*/ |
|
maximum: NaN, |
|
/** |
|
* @cfg {Boolean} constrain |
|
* If true, the values of the chart will be rendered only if they belong between minimum and maximum. |
|
* If false, all values of the chart will be rendered, regardless of whether they belong between minimum and maximum or not. |
|
* Default's true if maximum and minimum is specified. It is ignored for stacked charts. |
|
*/ |
|
constrain: true, |
|
/** |
|
* @cfg {Number} decimals |
|
* The number of decimals to round the value to. |
|
*/ |
|
decimals: 2, |
|
/** |
|
* @cfg {String} scale |
|
* The scaling algorithm to use on this axis. May be "linear" or |
|
* "logarithmic". Currently only linear scale is implemented. |
|
* @private |
|
*/ |
|
scale: "linear", |
|
// @private constrains to datapoints between minimum and maximum only |
|
doConstrain: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
items = store.data.items, |
|
d, dLen, record, |
|
series = chart.series.items, |
|
fields = me.fields, |
|
ln = fields.length, |
|
range = me.calcEnds(), |
|
min = range.from, |
|
max = range.to, |
|
i, l, |
|
useAcum = false, |
|
value, |
|
data = [], |
|
addRecord; |
|
for (d = 0 , dLen = items.length; d < dLen; d++) { |
|
addRecord = true; |
|
record = items[d]; |
|
for (i = 0; i < ln; i++) { |
|
value = record.get(fields[i]); |
|
if (me.type == 'Time' && typeof value == "string") { |
|
value = Date.parse(value); |
|
} |
|
if (+value < +min) { |
|
addRecord = false; |
|
break; |
|
} |
|
if (+value > +max) { |
|
addRecord = false; |
|
break; |
|
} |
|
} |
|
if (addRecord) { |
|
data.push(record); |
|
} |
|
} |
|
chart.setSubStore(new Ext.data.Store({ |
|
model: store.model, |
|
data: data |
|
})); |
|
}, |
|
/** |
|
* @cfg {String} position |
|
* Indicates the position of the axis relative to the chart |
|
*/ |
|
position: 'left', |
|
/** |
|
* @cfg {Boolean} adjustMaximumByMajorUnit |
|
* Indicates whether to extend maximum beyond data's maximum to the nearest |
|
* majorUnit. |
|
*/ |
|
adjustMaximumByMajorUnit: false, |
|
/** |
|
* @cfg {Boolean} adjustMinimumByMajorUnit |
|
* Indicates whether to extend the minimum beyond data's minimum to the |
|
* nearest majorUnit. |
|
*/ |
|
adjustMinimumByMajorUnit: false, |
|
// applying constraint |
|
processView: function() { |
|
var me = this, |
|
chart = me.chart, |
|
series = chart.series.items, |
|
i, l; |
|
for (i = 0 , l = series.length; i < l; i++) { |
|
if (series[i].stacked) { |
|
// Do not constrain stacked charts (bar, column, or area). |
|
delete me.minimum; |
|
delete me.maximum; |
|
me.constrain = false; |
|
break; |
|
} |
|
} |
|
if (me.constrain) { |
|
me.doConstrain(); |
|
} |
|
}, |
|
// @private apply data. |
|
applyData: function() { |
|
this.callParent(); |
|
return this.calcEnds(); |
|
} |
|
}); |
|
|
|
/** |
|
* @private |
|
*/ |
|
Ext.define('Ext.chart.axis.Radial', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.chart.axis.Numeric', |
|
/* End Definitions */ |
|
position: 'radial', |
|
alias: 'axis.radial', |
|
/** |
|
* @cfg {Number} maximum |
|
* The maximum value drawn by the axis. If not set explicitly, the axis |
|
* maximum will be calculated automatically. |
|
*/ |
|
/** |
|
* @cfg {Number} minimum |
|
* The minimum value drawn by the axis. Default is 0. |
|
*/ |
|
/** |
|
* @cfg {Number} [steps=10] |
|
* The number of circles to draw outward from the center. |
|
*/ |
|
drawAxis: function(init) { |
|
var chart = this.chart, |
|
surface = chart.surface, |
|
bbox = chart.chartBBox, |
|
store = chart.getChartStore(), |
|
l = store.getCount(), |
|
centerX = bbox.x + (bbox.width / 2), |
|
centerY = bbox.y + (bbox.height / 2), |
|
rho = Math.min(bbox.width, bbox.height) / 2, |
|
sprites = [], |
|
sprite, |
|
steps = this.steps, |
|
i, j, |
|
pi2 = Math.PI * 2, |
|
cos = Math.cos, |
|
sin = Math.sin; |
|
if (this.sprites && !chart.resizing) { |
|
this.drawLabel(); |
|
return; |
|
} |
|
if (!this.sprites) { |
|
//draw circles |
|
for (i = 1; i <= steps; i++) { |
|
sprite = surface.add({ |
|
type: 'circle', |
|
x: centerX, |
|
y: centerY, |
|
radius: Math.max(rho * i / steps, 0), |
|
stroke: '#ccc' |
|
}); |
|
sprite.setAttributes({ |
|
hidden: false |
|
}, true); |
|
sprites.push(sprite); |
|
} |
|
//draw lines |
|
for (i = 0; i < l; i++) { |
|
sprite = surface.add({ |
|
type: 'path', |
|
path: [ |
|
'M', |
|
centerX, |
|
centerY, |
|
'L', |
|
centerX + rho * cos(i / l * pi2), |
|
centerY + rho * sin(i / l * pi2), |
|
'Z' |
|
], |
|
stroke: '#ccc' |
|
}); |
|
sprite.setAttributes({ |
|
hidden: false |
|
}, true); |
|
sprites.push(sprite); |
|
} |
|
} else { |
|
sprites = this.sprites; |
|
//draw circles |
|
for (i = 0; i < steps; i++) { |
|
sprites[i].setAttributes({ |
|
x: centerX, |
|
y: centerY, |
|
radius: Math.max(rho * (i + 1) / steps, 0), |
|
stroke: '#ccc' |
|
}, true); |
|
} |
|
//draw lines |
|
for (j = 0; j < l; j++) { |
|
sprites[i + j].setAttributes({ |
|
path: [ |
|
'M', |
|
centerX, |
|
centerY, |
|
'L', |
|
centerX + rho * cos(j / l * pi2), |
|
centerY + rho * sin(j / l * pi2), |
|
'Z' |
|
], |
|
stroke: '#ccc' |
|
}, true); |
|
} |
|
} |
|
this.sprites = sprites; |
|
this.drawLabel(); |
|
}, |
|
drawLabel: function() { |
|
var chart = this.chart, |
|
seriesItems = chart.series.items, |
|
series, |
|
surface = chart.surface, |
|
bbox = chart.chartBBox, |
|
store = chart.getChartStore(), |
|
data = store.data.items, |
|
ln, record, |
|
centerX = bbox.x + (bbox.width / 2), |
|
centerY = bbox.y + (bbox.height / 2), |
|
rho = Math.min(bbox.width, bbox.height) / 2, |
|
max = Math.max, |
|
round = Math.round, |
|
labelArray = [], |
|
label, |
|
fields = [], |
|
nfields, |
|
categories = [], |
|
xField, |
|
aggregate = !this.maximum, |
|
maxValue = this.maximum || 0, |
|
minValue = this.minimum || 0, |
|
steps = this.steps, |
|
i = 0, |
|
j, dx, dy, |
|
pi2 = Math.PI * 2, |
|
cos = Math.cos, |
|
sin = Math.sin, |
|
display = this.label.display, |
|
draw = display !== 'none', |
|
margin = 10; |
|
if (!draw) { |
|
return; |
|
} |
|
//get all rendered fields |
|
for (i = 0 , ln = seriesItems.length; i < ln; i++) { |
|
series = seriesItems[i]; |
|
fields.push(series.yField); |
|
xField = series.xField; |
|
} |
|
//get maxValue to interpolate |
|
for (j = 0 , ln = data.length; j < ln; j++) { |
|
record = data[j]; |
|
categories.push(record.get(xField)); |
|
if (aggregate) { |
|
for (i = 0 , nfields = fields.length; i < nfields; i++) { |
|
maxValue = max(+record.get(fields[i]), maxValue); |
|
} |
|
} |
|
} |
|
if (!this.labelArray) { |
|
if (display != 'categories') { |
|
//draw scale |
|
for (i = 1; i <= steps; i++) { |
|
label = surface.add({ |
|
type: 'text', |
|
text: round(i / steps * maxValue), |
|
x: centerX, |
|
y: centerY - rho * i / steps, |
|
'text-anchor': 'middle', |
|
'stroke-width': 0.1, |
|
stroke: '#333' |
|
}); |
|
label.setAttributes({ |
|
hidden: false |
|
}, true); |
|
labelArray.push(label); |
|
} |
|
} |
|
if (display != 'scale') { |
|
//draw text |
|
for (j = 0 , steps = categories.length; j < steps; j++) { |
|
dx = cos(j / steps * pi2) * (rho + margin); |
|
dy = sin(j / steps * pi2) * (rho + margin); |
|
label = surface.add({ |
|
type: 'text', |
|
text: categories[j], |
|
x: centerX + dx, |
|
y: centerY + dy, |
|
'text-anchor': dx * dx <= 0.001 ? 'middle' : (dx < 0 ? 'end' : 'start') |
|
}); |
|
label.setAttributes({ |
|
hidden: false |
|
}, true); |
|
labelArray.push(label); |
|
} |
|
} |
|
} else { |
|
labelArray = this.labelArray; |
|
if (display != 'categories') { |
|
//draw values |
|
for (i = 0; i < steps; i++) { |
|
labelArray[i].setAttributes({ |
|
text: round((i + 1) / steps * (maxValue - minValue) + minValue), |
|
x: centerX, |
|
y: centerY - rho * (i + 1) / steps, |
|
'text-anchor': 'middle', |
|
'stroke-width': 0.1, |
|
stroke: '#333' |
|
}, true); |
|
} |
|
} |
|
if (display != 'scale') { |
|
//draw text |
|
for (j = 0 , steps = categories.length; j < steps; j++) { |
|
dx = cos(j / steps * pi2) * (rho + margin); |
|
dy = sin(j / steps * pi2) * (rho + margin); |
|
if (labelArray[i + j]) { |
|
labelArray[i + j].setAttributes({ |
|
type: 'text', |
|
text: categories[j], |
|
x: centerX + dx, |
|
y: centerY + dy, |
|
'text-anchor': dx * dx <= 0.001 ? 'middle' : (dx < 0 ? 'end' : 'start') |
|
}, true); |
|
} |
|
} |
|
} |
|
} |
|
this.labelArray = labelArray; |
|
}, |
|
processView: function() { |
|
var me = this, |
|
seriesItems = me.chart.series.items, |
|
i, ln, series, ends, |
|
fields = []; |
|
for (i = 0 , ln = seriesItems.length; i < ln; i++) { |
|
series = seriesItems[i]; |
|
fields.push(series.yField); |
|
} |
|
me.fields = fields; |
|
ends = me.calcEnds(); |
|
me.maximum = ends.to; |
|
me.steps = ends.steps; |
|
} |
|
}); |
|
|
|
/** |
|
* A type of axis whose units are measured in time values. Use this axis |
|
* for listing dates that you will want to group or dynamically change. |
|
* If you just want to display dates as categories then use the |
|
* Category class for axis instead. |
|
* |
|
* For example: |
|
* |
|
* axes: [{ |
|
* type: 'Time', |
|
* position: 'bottom', |
|
* fields: 'date', |
|
* title: 'Day', |
|
* dateFormat: 'M d', |
|
* |
|
* constrain: true, |
|
* fromDate: new Date('1/1/11'), |
|
* toDate: new Date('1/7/11') |
|
* }] |
|
* |
|
* In this example we're creating a time axis that has as title *Day*. |
|
* The field the axis is bound to is `date`. |
|
* The date format to use to display the text for the axis labels is `M d` |
|
* which is a three letter month abbreviation followed by the day number. |
|
* The time axis will show values for dates between `fromDate` and `toDate`. |
|
* Since `constrain` is set to true all other values for other dates not between |
|
* the fromDate and toDate will not be displayed. |
|
* |
|
*/ |
|
Ext.define('Ext.chart.axis.Time', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.chart.axis.Numeric', |
|
alternateClassName: 'Ext.chart.TimeAxis', |
|
type: 'Time', |
|
alias: 'axis.time', |
|
uses: [ |
|
'Ext.data.Store' |
|
], |
|
/* End Definitions */ |
|
/** |
|
* @cfg {String/Boolean} dateFormat |
|
* Indicates the format the date will be rendered on. |
|
* For example: 'M d' will render the dates as 'Jan 30', etc. |
|
* For a list of possible format strings see {@link Ext.Date Date} |
|
*/ |
|
dateFormat: false, |
|
/** |
|
* @cfg {Date} fromDate The starting date for the time axis. |
|
*/ |
|
fromDate: false, |
|
/** |
|
* @cfg {Date} toDate The ending date for the time axis. |
|
*/ |
|
toDate: false, |
|
/** |
|
* @cfg {Array} step |
|
* An array with two components: The first is the unit of the step (day, month, year, etc). The second one is a number. |
|
* If the number is an integer, it represents the number of units for the step ([Ext.Date.DAY, 2] means "Every other day"). |
|
* If the number is a fraction, it represents the number of steps per unit ([Ext.Date.DAY, 1/2] means "Twice a day"). |
|
* If the unit is the month, the steps may be adjusted depending on the month. For instance [Ext.Date.MONTH, 1/3], which means "Three times a month", |
|
* generates steps on the 1st, the 10th and the 20th of every month regardless of whether a month has 28 days or 31 days. The steps are generated |
|
* as follows: |
|
* - [Ext.Date.MONTH, n]: on the current date every 'n' months, maxed to the number of days in the month. |
|
* - [Ext.Date.MONTH, 1/2]: on the 1st and 15th of every month. |
|
* - [Ext.Date.MONTH, 1/3]: on the 1st, 10th and 20th of every month. |
|
* - [Ext.Date.MONTH, 1/4]: on the 1st, 8th, 15th and 22nd of every month. |
|
* |
|
* Defaults to: [Ext.Date.DAY, 1]. |
|
*/ |
|
step: [ |
|
Ext.Date.DAY, |
|
1 |
|
], |
|
/** |
|
* @cfg {Boolean} constrain |
|
* If true, the values of the chart will be rendered only if they belong between the fromDate and toDate. |
|
* If false, the time axis will adapt to the new values by adding/removing steps. |
|
*/ |
|
constrain: false, |
|
constructor: function(config) { |
|
var me = this, |
|
label, f, df; |
|
me.callParent([ |
|
config |
|
]); |
|
label = me.label || {}; |
|
df = this.dateFormat; |
|
if (df) { |
|
if (label.renderer) { |
|
f = label.renderer; |
|
label.renderer = function(v) { |
|
v = f(v); |
|
return Ext.Date.format(new Date(f(v)), df); |
|
}; |
|
} else { |
|
label.renderer = function(v) { |
|
return Ext.Date.format(new Date(v >> 0), df); |
|
}; |
|
} |
|
} |
|
}, |
|
// Before rendering, set current default step count to be number of records. |
|
processView: function() { |
|
var me = this; |
|
if (me.fromDate) { |
|
me.minimum = +me.fromDate; |
|
} |
|
if (me.toDate) { |
|
me.maximum = +me.toDate; |
|
} |
|
if (me.constrain) { |
|
me.doConstrain(); |
|
} |
|
}, |
|
// @private modifies the store and creates the labels for the axes. |
|
calcEnds: function() { |
|
var me = this, |
|
range, |
|
step = me.step; |
|
if (step) { |
|
range = me.getRange(); |
|
range = Ext.draw.Draw.snapEndsByDateAndStep(new Date(range.min), new Date(range.max), Ext.isNumber(step) ? [ |
|
Date.MILLI, |
|
step |
|
] : step); |
|
if (me.minimum) { |
|
range.from = me.minimum; |
|
} |
|
if (me.maximum) { |
|
range.to = me.maximum; |
|
} |
|
return range; |
|
} else { |
|
return me.callParent(arguments); |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.series.Series |
|
* |
|
* Series is the abstract class containing the common logic to all chart series. Series includes |
|
* methods from Labels, Highlights, Tips and Callouts mixins. This class implements the logic of handling |
|
* mouse events, animating, hiding, showing all elements and returning the color of the series to be used as a legend item. |
|
* |
|
* ## Listeners |
|
* |
|
* The series class supports listeners via the Observable syntax. Some of these listeners are: |
|
* |
|
* - `itemclick` When the user interacts with a marker. |
|
* - `itemmouseup` When the user interacts with a marker. |
|
* - `itemmousedown` When the user interacts with a marker. |
|
* - `afterrender` Will be triggered when the animation ends or when the series has been rendered completely. |
|
* |
|
* For example: |
|
* |
|
* series: [{ |
|
* type: 'column', |
|
* axis: 'left', |
|
* listeners: { |
|
* 'afterrender': function() { |
|
* console('afterrender'); |
|
* } |
|
* }, |
|
* xField: 'category', |
|
* yField: 'data1' |
|
* }] |
|
*/ |
|
Ext.define('Ext.chart.series.Series', { |
|
/* Begin Definitions */ |
|
mixins: { |
|
observable: 'Ext.util.Observable', |
|
labels: 'Ext.chart.Label', |
|
highlights: 'Ext.chart.Highlight', |
|
tips: 'Ext.chart.Tip', |
|
callouts: 'Ext.chart.Callout' |
|
}, |
|
/* End Definitions */ |
|
/** |
|
* @cfg {Boolean/Object} highlight |
|
* If set to `true` it will highlight the markers or the series when hovering |
|
* with the mouse. This parameter can also be an object with the same style |
|
* properties you would apply to a {@link Ext.draw.Sprite} to apply custom |
|
* styles to markers and series. |
|
*/ |
|
/** |
|
* @cfg {Object} tips |
|
* Add tooltips to the visualization's markers. The options for the tips are the |
|
* same configuration used with {@link Ext.tip.ToolTip}. For example: |
|
* |
|
* tips: { |
|
* trackMouse: true, |
|
* renderer: function(storeItem, item) { |
|
* this.setHtml(storeItem.get('name') + ': ' + storeItem.get('data1') + ' views'); |
|
* } |
|
* }, |
|
*/ |
|
/** |
|
* @cfg {String} type |
|
* The type of series. Set in subclasses. |
|
*/ |
|
type: null, |
|
/** |
|
* @cfg {String} title |
|
* The human-readable name of the series. |
|
*/ |
|
title: null, |
|
/** |
|
* @cfg {Boolean} showInLegend |
|
* Whether to show this series in the legend. |
|
*/ |
|
showInLegend: true, |
|
/** |
|
* @cfg {Function} renderer |
|
* A function that can be overridden to set custom styling properties to each rendered element. |
|
* Passes in (sprite, record, attributes, index, store) to the function. This function **must** return |
|
* an object of attributes. By default, the renderer will return the attributes parameter. |
|
* @param {Ext.draw.Sprite} sprite The sprite being rendered. |
|
* @param {Ext.data.Model} record The record assocatied with the sprite datapoint being rendered. |
|
* @param {Object} attributes The attributes used to style the sprite. |
|
* @param {Number} index The index of the record in the store. |
|
* @param {Ext.data.Store} store The store for the chart. |
|
* @return {Object} The attributes the sprite will use to render. |
|
*/ |
|
renderer: function(sprite, record, attributes, index, store) { |
|
return attributes; |
|
}, |
|
/** |
|
* @cfg {Array} shadowAttributes |
|
* An array with shadow attributes. |
|
* |
|
* Defaults to: |
|
* |
|
* [{ |
|
* "stroke-width": 6, |
|
* "stroke-opacity": 1, |
|
* "stroke": 'rgb(200, 200, 200)', |
|
* "translate": { |
|
* "x": 1.2, |
|
* "y": 2 |
|
* } |
|
* }, |
|
* { |
|
* "stroke-width": 4, |
|
* "stroke-opacity": 1, |
|
* "stroke": 'rgb(150, 150, 150)', |
|
* "translate": { |
|
* "x": 0.9, |
|
* "y": 1.5 |
|
* } |
|
* }, |
|
* { |
|
* "stroke-width": 2, |
|
* "stroke-opacity": 1, |
|
* "stroke": 'rgb(100, 100, 100)', |
|
* "translate": { |
|
* "x": 0.6, |
|
* "y": 1 |
|
* } |
|
* }] |
|
* |
|
* Each object in the array will be applied to a sprite to make up the |
|
* underlying shadow. |
|
* |
|
* Only applicable when the chart's shadow property is `true`. |
|
* |
|
*/ |
|
shadowAttributes: null, |
|
// @private animating flag |
|
animating: false, |
|
// @private default gutters |
|
nullGutters: { |
|
lower: 0, |
|
upper: 0, |
|
verticalAxis: undefined |
|
}, |
|
// @private default padding |
|
nullPadding: { |
|
left: 0, |
|
right: 0, |
|
width: 0, |
|
bottom: 0, |
|
top: 0, |
|
height: 0 |
|
}, |
|
/** |
|
* @event itemclick |
|
* Fires when the user clicks on a marker. |
|
* @param {Object} item Target item object. See {@link #getItemFromPoint} for |
|
* description of object properties. |
|
*/ |
|
/** |
|
* @event itemdblclick |
|
* Fires when the user double clicks on a marker. |
|
* @param {Object} item Target item object. See {@link #getItemFromPoint} for |
|
* description of object properties. |
|
*/ |
|
/** |
|
* @event itemmouseover |
|
* Fires when the user hovers mouse cursor over a marker. |
|
* @param {Object} item Target item object. See {@link #getItemFromPoint} for |
|
* description of object properties. |
|
*/ |
|
/** |
|
* @event itemmouseout |
|
* Fires when the user moves mouse cursor out of marker. |
|
* @param {Object} item Target item object. See {@link #getItemFromPoint} for |
|
* description of object properties. |
|
*/ |
|
/** |
|
* @event itemmousedown |
|
* Fires when a marker receives mousedown event. |
|
* @param {Object} item Target item object. See {@link #getItemFromPoint} for |
|
* description of object properties. |
|
*/ |
|
/** |
|
* @event itemmouseup |
|
* Fires when a marker receives mouseup event. |
|
* @param {Object} item Target item object. See {@link #getItemFromPoint} for |
|
* description of object properties. |
|
*/ |
|
constructor: function(config) { |
|
var me = this; |
|
if (config) { |
|
Ext.apply(me, config); |
|
} |
|
me.shadowGroups = []; |
|
me.mixins.labels.constructor.call(me, config); |
|
me.mixins.highlights.constructor.call(me, config); |
|
me.mixins.tips.constructor.call(me, config); |
|
me.mixins.callouts.constructor.call(me, config); |
|
me.mixins.observable.constructor.call(me, config); |
|
me.on({ |
|
scope: me, |
|
itemmouseover: me.onItemMouseOver, |
|
itemmouseout: me.onItemMouseOut, |
|
mouseleave: me.onMouseLeave |
|
}); |
|
if (me.style) { |
|
Ext.apply(me.seriesStyle, me.style); |
|
} |
|
}, |
|
initialize: Ext.emptyFn, |
|
onRedraw: Ext.emptyFn, |
|
/** |
|
* Iterate over each of the records for this series. The default implementation simply iterates |
|
* through the entire data store, but individual series implementations can override this to |
|
* provide custom handling, e.g. adding/removing records. |
|
* @param {Function} fn The function to execute for each record. |
|
* @param {Object} scope Scope for the fn. |
|
*/ |
|
eachRecord: function(fn, scope) { |
|
var chart = this.chart; |
|
chart.getChartStore().each(fn, scope); |
|
}, |
|
/** |
|
* Return the number of records being displayed in this series. Defaults to the number of |
|
* records in the store; individual series implementations can override to provide custom handling. |
|
*/ |
|
getRecordCount: function() { |
|
var chart = this.chart, |
|
store = chart.getChartStore(); |
|
return store ? store.getCount() : 0; |
|
}, |
|
/** |
|
* Determines whether the series item at the given index has been excluded, i.e. toggled off in the legend. |
|
* @param index |
|
*/ |
|
isExcluded: function(index) { |
|
var excludes = this.__excludes; |
|
return !!(excludes && excludes[index]); |
|
}, |
|
// @private set the bbox and clipBox for the series |
|
setBBox: function(noGutter) { |
|
var me = this, |
|
chart = me.chart, |
|
chartBBox = chart.chartBBox, |
|
maxGutters = noGutter ? { |
|
left: 0, |
|
right: 0, |
|
bottom: 0, |
|
top: 0 |
|
} : chart.maxGutters, |
|
clipBox, bbox; |
|
clipBox = { |
|
x: chartBBox.x, |
|
y: chartBBox.y, |
|
width: chartBBox.width, |
|
height: chartBBox.height |
|
}; |
|
me.clipBox = clipBox; |
|
bbox = { |
|
x: (clipBox.x + maxGutters.left) - (chart.zoom.x * chart.zoom.width), |
|
y: (clipBox.y + maxGutters.bottom) - (chart.zoom.y * chart.zoom.height), |
|
width: (clipBox.width - (maxGutters.left + maxGutters.right)) * chart.zoom.width, |
|
height: (clipBox.height - (maxGutters.bottom + maxGutters.top)) * chart.zoom.height |
|
}; |
|
me.bbox = bbox; |
|
}, |
|
// @private set the animation for the sprite |
|
onAnimate: function(sprite, attr) { |
|
var me = this; |
|
sprite.stopAnimation(); |
|
if (me.animating) { |
|
return sprite.animate(Ext.applyIf(attr, me.chart.animate)); |
|
} else { |
|
me.animating = true; |
|
return sprite.animate(Ext.apply(Ext.applyIf(attr, me.chart.animate), { |
|
// use callback, don't overwrite listeners |
|
callback: function() { |
|
me.animating = false; |
|
me.fireEvent('afterrender', me); |
|
} |
|
})); |
|
} |
|
}, |
|
// @private return the gutters. |
|
getGutters: function() { |
|
return this.nullGutters; |
|
}, |
|
// @private return the gutters. |
|
getPadding: function() { |
|
return this.nullPadding; |
|
}, |
|
// @private wrapper for the itemmouseover event. |
|
onItemMouseOver: function(item) { |
|
var me = this; |
|
if (item.series === me) { |
|
if (me.highlight) { |
|
me.highlightItem(item); |
|
} |
|
if (me.tooltip) { |
|
me.showTip(item); |
|
} |
|
} |
|
}, |
|
// @private wrapper for the itemmouseout event. |
|
onItemMouseOut: function(item) { |
|
var me = this; |
|
if (item.series === me) { |
|
me.unHighlightItem(); |
|
if (me.tooltip) { |
|
me.hideTip(item); |
|
} |
|
} |
|
}, |
|
// @private wrapper for the mouseleave event. |
|
onMouseLeave: function() { |
|
var me = this; |
|
me.unHighlightItem(); |
|
if (me.tooltip) { |
|
me.hideTip(); |
|
} |
|
}, |
|
/** |
|
* For a given x/y point relative to the Surface, find a corresponding item from this |
|
* series, if any. |
|
* @param {Number} x |
|
* @param {Number} y |
|
* @return {Object} An object describing the item, or null if there is no matching item. |
|
* The exact contents of this object will vary by series type, but should always contain the following: |
|
* @return {Ext.chart.series.Series} return.series the Series object to which the item belongs |
|
* @return {Object} return.value the value(s) of the item's data point |
|
* @return {Array} return.point the x/y coordinates relative to the chart box of a single point |
|
* for this data item, which can be used as e.g. a tooltip anchor point. |
|
* @return {Ext.draw.Sprite} return.sprite the item's rendering Sprite. |
|
*/ |
|
getItemForPoint: function(x, y) { |
|
//if there are no items to query just return null. |
|
if (!this.items || !this.items.length || this.seriesIsHidden) { |
|
return null; |
|
} |
|
var me = this, |
|
items = me.items, |
|
bbox = me.bbox, |
|
item, i, ln; |
|
// Check bounds |
|
if (!Ext.draw.Draw.withinBox(x, y, bbox)) { |
|
return null; |
|
} |
|
for (i = 0 , ln = items.length; i < ln; i++) { |
|
if (items[i] && this.isItemInPoint(x, y, items[i], i)) { |
|
return items[i]; |
|
} |
|
} |
|
return null; |
|
}, |
|
isItemInPoint: function(x, y, item, i) { |
|
return false; |
|
}, |
|
/** |
|
* Hides all the elements in the series. |
|
*/ |
|
hideAll: function() { |
|
var me = this, |
|
items = me.items, |
|
item, len, i, j, l, sprite, shadows; |
|
me.seriesIsHidden = true; |
|
me._prevShowMarkers = me.showMarkers; |
|
me.showMarkers = false; |
|
//hide all labels |
|
me.hideLabels(0); |
|
//hide all sprites |
|
for (i = 0 , len = items.length; i < len; i++) { |
|
item = items[i]; |
|
sprite = item.sprite; |
|
if (sprite) { |
|
sprite.setAttributes({ |
|
hidden: true |
|
}, true); |
|
} |
|
if (sprite && sprite.shadows) { |
|
shadows = sprite.shadows; |
|
for (j = 0 , l = shadows.length; j < l; ++j) { |
|
shadows[j].setAttributes({ |
|
hidden: true |
|
}, true); |
|
} |
|
} |
|
} |
|
}, |
|
/** |
|
* Shows all the elements in the series. |
|
*/ |
|
showAll: function() { |
|
var me = this, |
|
prevAnimate = me.chart.animate; |
|
me.chart.animate = false; |
|
me.seriesIsHidden = false; |
|
me.showMarkers = me._prevShowMarkers; |
|
me.drawSeries(); |
|
me.chart.animate = prevAnimate; |
|
}, |
|
hide: function() { |
|
if (this.items) { |
|
var me = this, |
|
items = me.items, |
|
i, j, lsh, ln, shadows; |
|
if (items && items.length) { |
|
for (i = 0 , ln = items.length; i < ln; ++i) { |
|
if (items[i].sprite) { |
|
items[i].sprite.hide(true); |
|
shadows = items[i].shadows || items[i].sprite.shadows; |
|
if (shadows) { |
|
for (j = 0 , lsh = shadows.length; j < lsh; ++j) { |
|
shadows[j].hide(true); |
|
} |
|
} |
|
} |
|
} |
|
me.hideLabels(); |
|
} |
|
} |
|
}, |
|
/** |
|
* Returns a string with the color to be used for the series legend item. |
|
*/ |
|
getLegendColor: function(index) { |
|
var me = this, |
|
fill, stroke; |
|
if (me.seriesStyle) { |
|
fill = me.seriesStyle.fill; |
|
stroke = me.seriesStyle.stroke; |
|
if (fill && fill != 'none') { |
|
return fill; |
|
} |
|
if (stroke) { |
|
return stroke; |
|
} |
|
} |
|
return (me.colorArrayStyle) ? me.colorArrayStyle[me.themeIdx % me.colorArrayStyle.length] : '#000'; |
|
}, |
|
/** |
|
* Checks whether the data field should be visible in the legend |
|
* @private |
|
* @param {Number} index The index of the current item |
|
*/ |
|
visibleInLegend: function(index) { |
|
var excludes = this.__excludes; |
|
if (excludes) { |
|
return !excludes[index]; |
|
} |
|
return !this.seriesIsHidden; |
|
}, |
|
/** |
|
* Changes the value of the {@link #title} for the series. |
|
* Arguments can take two forms: |
|
* <ul> |
|
* <li>A single String value: this will be used as the new single title for the series (applies |
|
* to series with only one yField)</li> |
|
* <li>A numeric index and a String value: this will set the title for a single indexed yField.</li> |
|
* </ul> |
|
* @param {Number} index |
|
* @param {String} title |
|
*/ |
|
setTitle: function(index, title) { |
|
var me = this, |
|
oldTitle = me.title; |
|
if (Ext.isString(index)) { |
|
title = index; |
|
index = 0; |
|
} |
|
if (Ext.isArray(oldTitle)) { |
|
oldTitle[index] = title; |
|
} else { |
|
me.title = title; |
|
} |
|
me.fireEvent('titlechange', title, index); |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.series.Cartesian |
|
* |
|
* Common base class for series implementations which plot values using x/y coordinates. |
|
*/ |
|
Ext.define('Ext.chart.series.Cartesian', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.chart.series.Series', |
|
alternateClassName: [ |
|
'Ext.chart.CartesianSeries', |
|
'Ext.chart.CartesianChart' |
|
], |
|
/* End Definitions */ |
|
/** |
|
* @cfg {String} xField |
|
* The name of the data Model field corresponding to the x-axis value. |
|
*/ |
|
xField: null, |
|
/** |
|
* @cfg {String/String[]} yField |
|
* The name(s) of the data Model field(s) corresponding to the y-axis value(s). |
|
*/ |
|
yField: null, |
|
/** |
|
* @cfg {String/String[]} axis |
|
* The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'. |
|
* You must explicitly set this value to bind the values of the line series to the ones in the axis, otherwise a |
|
* relative scale will be used. For example, if you're using a Scatter or Line series and you'd like to have the |
|
* values in the chart relative to the bottom and left axes then `axis` should be `['left', 'bottom']`. |
|
*/ |
|
axis: 'left', |
|
getLegendLabels: function() { |
|
var me = this, |
|
labels = [], |
|
fields, i, ln, |
|
combinations = me.combinations, |
|
title, combo, label0, label1; |
|
fields = [].concat(me.yField); |
|
for (i = 0 , ln = fields.length; i < ln; i++) { |
|
title = me.title; |
|
// Use the 'title' config if present, otherwise use the raw yField name |
|
labels.push((Ext.isArray(title) ? title[i] : title) || fields[i]); |
|
} |
|
// Handle yFields combined via legend drag-drop |
|
// TODO need to check to see if this is supported in extjs 4 branch |
|
if (combinations) { |
|
combinations = Ext.Array.from(combinations); |
|
for (i = 0 , ln = combinations.length; i < ln; i++) { |
|
combo = combinations[i]; |
|
label0 = labels[combo[0]]; |
|
label1 = labels[combo[1]]; |
|
labels[combo[1]] = label0 + ' & ' + label1; |
|
labels.splice(combo[0], 1); |
|
} |
|
} |
|
return labels; |
|
}, |
|
/** |
|
* @protected Iterates over a given record's values for each of this series's yFields, |
|
* executing a given function for each value. Any yFields that have been combined |
|
* via legend drag-drop will be treated as a single value. |
|
* @param {Ext.data.Model} record |
|
* @param {Function} fn |
|
* @param {Object} scope |
|
*/ |
|
eachYValue: function(record, fn, scope) { |
|
var me = this, |
|
yValueAccessors = me.getYValueAccessors(), |
|
i, ln, accessor; |
|
for (i = 0 , ln = yValueAccessors.length; i < ln; i++) { |
|
accessor = yValueAccessors[i]; |
|
fn.call(scope, accessor(record), i); |
|
} |
|
}, |
|
/** |
|
* @protected Returns the number of yField values, taking into account fields combined |
|
* via legend drag-drop. |
|
* @return {Number} |
|
*/ |
|
getYValueCount: function() { |
|
return this.getYValueAccessors().length; |
|
}, |
|
/** |
|
* @protected Returns an array of functions, each of which returns the value of the yField |
|
* corresponding to function's index in the array, for a given record (each function takes the |
|
* record as its only argument.) If yFields have been combined by the user via legend drag-drop, |
|
* this list of accessors will be kept in sync with those combinations. |
|
* @return {Array} array of accessor functions |
|
*/ |
|
getYValueAccessors: function() { |
|
var me = this, |
|
accessors = me.yValueAccessors, |
|
yFields, i, ln; |
|
function getFieldAccessor(field) { |
|
return function(record) { |
|
return record.get(field); |
|
}; |
|
} |
|
if (!accessors) { |
|
accessors = me.yValueAccessors = []; |
|
yFields = [].concat(me.yField); |
|
for (i = 0 , ln = yFields.length; i < ln; i++) { |
|
accessors.push(getFieldAccessor(yFields[i])); |
|
} |
|
} |
|
return accessors; |
|
}, |
|
/** |
|
* Calculate the min and max values for this series's xField. |
|
* @return {Array} [min, max] |
|
*/ |
|
getMinMaxXValues: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
data = store.data.items, |
|
count = me.getRecordCount(), |
|
i, ln, record, min, max, |
|
xField = me.xField, |
|
xValue; |
|
if (count > 0) { |
|
min = Infinity; |
|
max = -min; |
|
for (i = 0 , ln = data.length; i < ln; i++) { |
|
record = data[i]; |
|
xValue = record.get(xField); |
|
if (xValue > max) { |
|
max = xValue; |
|
} |
|
if (xValue < min) { |
|
min = xValue; |
|
} |
|
} |
|
// If we made no progress, treat it like a category axis |
|
if (min == Infinity) { |
|
min = 0; |
|
} |
|
if (max == -Infinity) { |
|
max = count - 1; |
|
} |
|
} else { |
|
min = max = 0; |
|
} |
|
return [ |
|
min, |
|
max |
|
]; |
|
}, |
|
/** |
|
* Calculate the min and max values for this series's yField(s). Takes into account yField |
|
* combinations, exclusions, and stacking. |
|
* @return {Array} [min, max] |
|
*/ |
|
getMinMaxYValues: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
data = store.data.items, |
|
count = me.getRecordCount(), |
|
i, ln, record, |
|
stacked = me.stacked, |
|
min, max, positiveTotal, negativeTotal; |
|
function eachYValueStacked(yValue, i) { |
|
if (!me.isExcluded(i)) { |
|
if (yValue < 0) { |
|
negativeTotal += yValue; |
|
} else { |
|
positiveTotal += yValue; |
|
} |
|
} |
|
} |
|
function eachYValue(yValue, i) { |
|
if (!me.isExcluded(i)) { |
|
if (yValue > max) { |
|
max = yValue; |
|
} |
|
if (yValue < min) { |
|
min = yValue; |
|
} |
|
} |
|
} |
|
if (count > 0) { |
|
min = Infinity; |
|
max = -min; |
|
for (i = 0 , ln = data.length; i < ln; i++) { |
|
record = data[i]; |
|
if (stacked) { |
|
positiveTotal = 0; |
|
negativeTotal = 0; |
|
me.eachYValue(record, eachYValueStacked); |
|
if (positiveTotal > max) { |
|
max = positiveTotal; |
|
} |
|
if (negativeTotal < min) { |
|
min = negativeTotal; |
|
} |
|
} else { |
|
me.eachYValue(record, eachYValue); |
|
} |
|
} |
|
// If we made no progress, treat it like a category axis |
|
if (min == Infinity) { |
|
min = 0; |
|
} |
|
if (max == -Infinity) { |
|
max = count - 1; |
|
} |
|
} else { |
|
min = max = 0; |
|
} |
|
return [ |
|
min, |
|
max |
|
]; |
|
}, |
|
getAxesForXAndYFields: function() { |
|
var me = this, |
|
axes = me.chart.axes, |
|
reverse = me.reverse, |
|
axis = [].concat(me.axis), |
|
yFields = {}, |
|
yFieldList = [].concat(me.yField), |
|
xFields = {}, |
|
xFieldList = [].concat(me.xField), |
|
fields, xAxis, yAxis, i, ln, flipXY; |
|
flipXY = me.type === 'bar' && me.column === false; |
|
if (flipXY) { |
|
fields = yFieldList; |
|
yFieldList = xFieldList; |
|
xFieldList = fields; |
|
} |
|
if (Ext.Array.indexOf(axis, 'top') > -1) { |
|
xAxis = 'top'; |
|
} else if (Ext.Array.indexOf(axis, 'bottom') > -1) { |
|
xAxis = 'bottom'; |
|
} else { |
|
if (axes.get('top') && axes.get('bottom')) { |
|
for (i = 0 , ln = xFieldList.length; i < ln; i++) { |
|
xFields[xFieldList[i]] = true; |
|
} |
|
fields = [].concat(axes.get('bottom').fields); |
|
for (i = 0 , ln = fields.length; i < ln; i++) { |
|
if (xFields[fields[i]]) { |
|
xAxis = 'bottom'; |
|
break; |
|
} |
|
} |
|
fields = [].concat(axes.get('top').fields); |
|
for (i = 0 , ln = fields.length; i < ln; i++) { |
|
if (xFields[fields[i]]) { |
|
xAxis = 'top'; |
|
break; |
|
} |
|
} |
|
} else if (axes.get('top')) { |
|
xAxis = 'top'; |
|
} else if (axes.get('bottom')) { |
|
xAxis = 'bottom'; |
|
} |
|
} |
|
if (Ext.Array.indexOf(axis, 'left') > -1) { |
|
yAxis = flipXY ? 'right' : 'left'; |
|
} else if (Ext.Array.indexOf(axis, 'right') > -1) { |
|
yAxis = flipXY ? 'left' : 'right'; |
|
} else { |
|
if (axes.get('left') && axes.get('right')) { |
|
for (i = 0 , ln = yFieldList.length; i < ln; i++) { |
|
yFields[yFieldList[i]] = true; |
|
} |
|
fields = [].concat(axes.get('right').fields); |
|
for (i = 0 , ln = fields.length; i < ln; i++) { |
|
if (yFields[fields[i]]) { |
|
break; |
|
} |
|
} |
|
fields = [].concat(axes.get('left').fields); |
|
for (i = 0 , ln = fields.length; i < ln; i++) { |
|
if (yFields[fields[i]]) { |
|
yAxis = 'left'; |
|
break; |
|
} |
|
} |
|
} else if (axes.get('left')) { |
|
yAxis = 'left'; |
|
} else if (axes.get('right')) { |
|
yAxis = 'right'; |
|
} |
|
} |
|
return flipXY ? { |
|
xAxis: yAxis, |
|
yAxis: xAxis |
|
} : { |
|
xAxis: xAxis, |
|
yAxis: yAxis |
|
}; |
|
} |
|
}); |
|
|
|
Ext.define('Ext.rtl.chart.series.Cartesian', { |
|
override: 'Ext.chart.series.Cartesian', |
|
initialize: function() { |
|
var me = this; |
|
me.callParent(arguments); |
|
me.axis = me.chart.invertPosition(me.axis); |
|
if (me.chart.getInherited().rtl) { |
|
me.reverse = true; |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.series.Area |
|
* @extends Ext.chart.series.Cartesian |
|
* |
|
* Creates a Stacked Area Chart. The stacked area chart is useful when displaying multiple aggregated layers of information. |
|
* As with all other series, the Area Series must be appended in the *series* Chart array configuration. See the Chart |
|
* documentation for more information. A typical configuration object for the area series could be: |
|
* |
|
* @example |
|
* var store = Ext.create('Ext.data.JsonStore', { |
|
* 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 } |
|
* ] |
|
* }); |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* width: 500, |
|
* height: 300, |
|
* store: store, |
|
* axes: [ |
|
* { |
|
* type: 'Numeric', |
|
* position: 'left', |
|
* fields: ['data1', 'data2', 'data3', 'data4', 'data5'], |
|
* title: 'Sample Values', |
|
* grid: { |
|
* odd: { |
|
* opacity: 1, |
|
* fill: '#ddd', |
|
* stroke: '#bbb', |
|
* 'stroke-width': 1 |
|
* } |
|
* }, |
|
* minimum: 0, |
|
* adjustMinimumByMajorUnit: 0 |
|
* }, |
|
* { |
|
* type: 'Category', |
|
* position: 'bottom', |
|
* fields: ['name'], |
|
* title: 'Sample Metrics', |
|
* grid: true, |
|
* label: { |
|
* rotate: { |
|
* degrees: 315 |
|
* } |
|
* } |
|
* } |
|
* ], |
|
* series: [{ |
|
* type: 'area', |
|
* highlight: false, |
|
* axis: 'left', |
|
* xField: 'name', |
|
* yField: ['data1', 'data2', 'data3', 'data4', 'data5'], |
|
* style: { |
|
* opacity: 0.93 |
|
* } |
|
* }] |
|
* }); |
|
* |
|
* In this configuration we set `area` as the type for the series, set highlighting options to true for highlighting elements on hover, |
|
* take the left axis to measure the data in the area series, set as xField (x values) the name field of each element in the store, |
|
* and as yFields (aggregated layers) seven data fields from the same store. Then we override some theming styles by adding some opacity |
|
* to the style object. |
|
*/ |
|
Ext.define('Ext.chart.series.Area', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.chart.series.Cartesian', |
|
alias: 'series.area', |
|
requires: [ |
|
'Ext.chart.axis.Axis', |
|
'Ext.draw.Color', |
|
'Ext.fx.Anim' |
|
], |
|
/* End Definitions */ |
|
type: 'area', |
|
// @private Area charts are alyways stacked |
|
stacked: true, |
|
/** |
|
* @cfg {Object} style |
|
* Append styling properties to this object for it to override theme properties. |
|
*/ |
|
style: {}, |
|
constructor: function(config) { |
|
this.callParent(arguments); |
|
var me = this, |
|
surface = me.chart.surface, |
|
i, l; |
|
config.highlightCfg = Ext.Object.merge({}, { |
|
lineWidth: 3, |
|
stroke: '#55c', |
|
opacity: 0.8, |
|
color: '#f00' |
|
}, config.highlightCfg); |
|
Ext.apply(me, config, { |
|
__excludes: [] |
|
}); |
|
if (me.highlight) { |
|
me.highlightSprite = surface.add({ |
|
type: 'path', |
|
path: [ |
|
'M', |
|
0, |
|
0 |
|
], |
|
zIndex: 1000, |
|
opacity: 0.3, |
|
lineWidth: 5, |
|
hidden: true, |
|
stroke: '#444' |
|
}); |
|
} |
|
me.group = surface.getGroup(me.seriesId); |
|
}, |
|
// @private Shrinks dataSets down to a smaller size |
|
shrink: function(xValues, yValues, size) { |
|
var len = xValues.length, |
|
ratio = Math.floor(len / size), |
|
i, j, |
|
xSum = 0, |
|
yCompLen = this.areas.length, |
|
ySum = [], |
|
xRes = [], |
|
yRes = []; |
|
//initialize array |
|
for (j = 0; j < yCompLen; ++j) { |
|
ySum[j] = 0; |
|
} |
|
for (i = 0; i < len; ++i) { |
|
xSum += +xValues[i]; |
|
for (j = 0; j < yCompLen; ++j) { |
|
ySum[j] += +yValues[i][j]; |
|
} |
|
if (i % ratio == 0) { |
|
//push averages |
|
xRes.push(xSum / ratio); |
|
for (j = 0; j < yCompLen; ++j) { |
|
ySum[j] /= ratio; |
|
} |
|
yRes.push(ySum); |
|
//reset sum accumulators |
|
xSum = 0; |
|
for (j = 0 , ySum = []; j < yCompLen; ++j) { |
|
ySum[j] = 0; |
|
} |
|
} |
|
} |
|
return { |
|
x: xRes, |
|
y: yRes |
|
}; |
|
}, |
|
// @private Get chart and data boundaries |
|
getBounds: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
data = store.data.items, |
|
i, l, record, |
|
areas = [].concat(me.yField), |
|
areasLen = areas.length, |
|
xValues = [], |
|
yValues = [], |
|
infinity = Infinity, |
|
minX = infinity, |
|
minY = infinity, |
|
maxX = -infinity, |
|
maxY = -infinity, |
|
math = Math, |
|
mmin = math.min, |
|
mmax = math.max, |
|
boundAxis = me.getAxesForXAndYFields(), |
|
boundXAxis = boundAxis.xAxis, |
|
boundYAxis = boundAxis.yAxis, |
|
ends, allowDate, tmp, bbox, xScale, yScale, xValue, yValue, areaIndex, acumY, ln, sumValues, clipBox, areaElem, axis, out; |
|
me.setBBox(); |
|
bbox = me.bbox; |
|
if (axis = chart.axes.get(boundXAxis)) { |
|
if (axis.type === 'Time') { |
|
allowDate = true; |
|
} |
|
ends = axis.applyData(); |
|
minX = ends.from; |
|
maxX = ends.to; |
|
} |
|
if (axis = chart.axes.get(boundYAxis)) { |
|
ends = axis.applyData(); |
|
minY = ends.from; |
|
maxY = ends.to; |
|
} |
|
// If a field was specified without a corresponding axis, create one to get bounds |
|
if (me.xField && !Ext.isNumber(minX)) { |
|
axis = me.getMinMaxXValues(); |
|
allowDate = true; |
|
minX = axis[0]; |
|
maxX = axis[1]; |
|
} |
|
if (me.yField && !Ext.isNumber(minY)) { |
|
axis = me.getMinMaxYValues(); |
|
minY = axis[0]; |
|
maxY = axis[1]; |
|
} |
|
if (!Ext.isNumber(minY)) { |
|
minY = 0; |
|
} |
|
if (!Ext.isNumber(maxY)) { |
|
maxY = 0; |
|
} |
|
l = data.length; |
|
if (l > 0 && allowDate) { |
|
tmp = data[0].get(me.xField); |
|
if (typeof tmp != 'number') { |
|
tmp = +tmp; |
|
if (isNaN(tmp)) { |
|
allowDate = false; |
|
} |
|
} |
|
} |
|
for (i = 0; i < l; i++) { |
|
record = data[i]; |
|
xValue = record.get(me.xField); |
|
yValue = []; |
|
if (typeof xValue != 'number') { |
|
if (allowDate) { |
|
xValue = +xValue; |
|
} else { |
|
xValue = i; |
|
} |
|
} |
|
xValues.push(xValue); |
|
acumY = 0; |
|
for (areaIndex = 0; areaIndex < areasLen; areaIndex++) { |
|
// Excluded series |
|
if (me.__excludes[areaIndex]) { |
|
|
|
continue; |
|
} |
|
areaElem = record.get(areas[areaIndex]); |
|
if (typeof areaElem == 'number') { |
|
yValue.push(areaElem); |
|
} |
|
} |
|
yValues.push(yValue); |
|
} |
|
xScale = bbox.width / ((maxX - minX) || 1); |
|
yScale = bbox.height / ((maxY - minY) || 1); |
|
ln = xValues.length; |
|
if ((ln > bbox.width) && me.areas) { |
|
sumValues = me.shrink(xValues, yValues, bbox.width); |
|
xValues = sumValues.x; |
|
yValues = sumValues.y; |
|
} |
|
return { |
|
bbox: bbox, |
|
minX: minX, |
|
minY: minY, |
|
xValues: xValues, |
|
yValues: yValues, |
|
xScale: xScale, |
|
yScale: yScale, |
|
areasLen: areasLen |
|
}; |
|
}, |
|
// @private Build an array of paths for the chart |
|
getPaths: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
first = true, |
|
bounds = me.getBounds(), |
|
bbox = bounds.bbox, |
|
items = me.items = [], |
|
componentPaths = [], |
|
componentPath, |
|
count = 0, |
|
paths = [], |
|
reverse = me.reverse, |
|
i, ln, x, y, xValue, yValue, acumY, areaIndex, prevAreaIndex, areaElem, path, startX, idx; |
|
ln = bounds.xValues.length; |
|
// Start the path |
|
for (i = 0; i < ln; i++) { |
|
xValue = bounds.xValues[i]; |
|
idx = reverse ? ln - i - 1 : i; |
|
yValue = bounds.yValues[idx]; |
|
x = bbox.x + (xValue - bounds.minX) * bounds.xScale; |
|
if (startX === undefined) { |
|
startX = x; |
|
} |
|
acumY = 0; |
|
count = 0; |
|
for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) { |
|
// Excluded series |
|
if (me.__excludes[areaIndex]) { |
|
|
|
continue; |
|
} |
|
if (!componentPaths[areaIndex]) { |
|
componentPaths[areaIndex] = []; |
|
} |
|
areaElem = yValue[count]; |
|
acumY += areaElem; |
|
y = bbox.y + bbox.height - (acumY - bounds.minY) * bounds.yScale; |
|
if (!paths[areaIndex]) { |
|
paths[areaIndex] = [ |
|
'M', |
|
x, |
|
y |
|
]; |
|
componentPaths[areaIndex].push([ |
|
'L', |
|
x, |
|
y |
|
]); |
|
} else { |
|
paths[areaIndex].push('L', x, y); |
|
componentPaths[areaIndex].push([ |
|
'L', |
|
x, |
|
y |
|
]); |
|
} |
|
if (!items[areaIndex]) { |
|
items[areaIndex] = { |
|
pointsUp: [], |
|
pointsDown: [], |
|
series: me |
|
}; |
|
} |
|
items[areaIndex].pointsUp.push([ |
|
x, |
|
y |
|
]); |
|
count++; |
|
} |
|
} |
|
// Close the paths |
|
for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) { |
|
// Excluded series |
|
if (me.__excludes[areaIndex]) { |
|
|
|
continue; |
|
} |
|
path = paths[areaIndex]; |
|
// Close bottom path to the axis |
|
if (areaIndex == 0 || first) { |
|
first = false; |
|
path.push('L', x, bbox.y + bbox.height, 'L', startX, bbox.y + bbox.height, 'Z'); |
|
} else // Close other paths to the one before them |
|
{ |
|
componentPath = componentPaths[prevAreaIndex]; |
|
componentPath.reverse(); |
|
path.push('L', x, componentPath[0][2]); |
|
for (i = 0; i < ln; i++) { |
|
path.push(componentPath[i][0], componentPath[i][1], componentPath[i][2]); |
|
items[areaIndex].pointsDown[ln - i - 1] = [ |
|
componentPath[i][1], |
|
componentPath[i][2] |
|
]; |
|
} |
|
path.push('L', startX, path[2], 'Z'); |
|
} |
|
prevAreaIndex = areaIndex; |
|
} |
|
return { |
|
paths: paths, |
|
areasLen: bounds.areasLen |
|
}; |
|
}, |
|
/** |
|
* Draws the series for the current chart. |
|
*/ |
|
drawSeries: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
surface = chart.surface, |
|
animate = chart.animate, |
|
group = me.group, |
|
endLineStyle = Ext.apply(me.seriesStyle, me.style), |
|
colorArrayStyle = me.colorArrayStyle, |
|
colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, |
|
themeIndex = me.themeIdx, |
|
areaIndex, areaElem, paths, path, rendererAttributes, idx; |
|
me.unHighlightItem(); |
|
me.cleanHighlights(); |
|
if (!store || !store.getCount() || me.seriesIsHidden) { |
|
me.hide(); |
|
me.items = []; |
|
return; |
|
} |
|
paths = me.getPaths(); |
|
if (!me.areas) { |
|
me.areas = []; |
|
} |
|
for (areaIndex = 0; areaIndex < paths.areasLen; areaIndex++) { |
|
// Excluded series |
|
if (me.__excludes[areaIndex]) { |
|
|
|
continue; |
|
} |
|
idx = themeIndex + areaIndex; |
|
if (!me.areas[areaIndex]) { |
|
me.items[areaIndex].sprite = me.areas[areaIndex] = surface.add(Ext.apply({}, { |
|
type: 'path', |
|
group: group, |
|
// 'clip-rect': me.clipBox, |
|
path: paths.paths[areaIndex], |
|
stroke: endLineStyle.stroke || colorArrayStyle[idx % colorArrayLength], |
|
fill: colorArrayStyle[idx % colorArrayLength] |
|
}, endLineStyle || {})); |
|
} |
|
areaElem = me.areas[areaIndex]; |
|
path = paths.paths[areaIndex]; |
|
if (animate) { |
|
//Add renderer to line. There is not a unique record associated with this. |
|
rendererAttributes = me.renderer(areaElem, false, { |
|
path: path, |
|
// 'clip-rect': me.clipBox, |
|
fill: colorArrayStyle[areaIndex % colorArrayLength], |
|
stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength] |
|
}, areaIndex, store); |
|
//fill should not be used here but when drawing the special fill path object |
|
me.animation = me.onAnimate(areaElem, { |
|
to: rendererAttributes |
|
}); |
|
} else { |
|
rendererAttributes = me.renderer(areaElem, false, { |
|
path: path, |
|
// 'clip-rect': me.clipBox, |
|
hidden: false, |
|
fill: colorArrayStyle[idx % colorArrayLength], |
|
stroke: endLineStyle.stroke || colorArrayStyle[idx % colorArrayLength] |
|
}, areaIndex, store); |
|
me.areas[areaIndex].setAttributes(rendererAttributes, true); |
|
} |
|
} |
|
me.renderLabels(); |
|
me.renderCallouts(); |
|
}, |
|
// @private |
|
onAnimate: function(sprite, attr) { |
|
sprite.show(); |
|
return this.callParent(arguments); |
|
}, |
|
// @private |
|
onCreateLabel: function(storeItem, item, i, display) { |
|
// TODO: Implement labels for Area charts. |
|
// The code in onCreateLabel() and onPlaceLabel() was originally copied |
|
// from another Series but it cannot work because item.point[] doesn't |
|
// exist in Area charts. Instead, the getPaths() methods above prepares |
|
// item.pointsUp[] and item.pointsDown[] which don't have the same structure. |
|
// In other series, there are as many 'items' as there are data points along the |
|
// x-axis. In this series, there are as many 'items' as there are series |
|
// (usually a much smaller number) and each pointsUp[] or pointsDown[] array |
|
// contains as many values as there are data points along the x-axis; |
|
return null; |
|
var me = this, |
|
group = me.labelsGroup, |
|
config = me.label, |
|
bbox = me.bbox, |
|
endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle || {}); |
|
return me.chart.surface.add(Ext.apply({ |
|
'type': 'text', |
|
'text-anchor': 'middle', |
|
'group': group, |
|
'x': Number(item.point[0]), |
|
'y': bbox.y + bbox.height / 2 |
|
}, endLabelStyle || {})); |
|
}, |
|
// @private |
|
onPlaceLabel: function(label, storeItem, item, i, display, animate, index) { |
|
var me = this, |
|
chart = me.chart, |
|
resizing = chart.resizing, |
|
config = me.label, |
|
format = config.renderer, |
|
field = config.field, |
|
bbox = me.bbox, |
|
x = Number(item.point[i][0]), |
|
y = Number(item.point[i][1]), |
|
labelBox, width, height; |
|
label.setAttributes({ |
|
text: format(storeItem.get(field[index]), label, storeItem, item, i, display, animate, index), |
|
hidden: true |
|
}, true); |
|
labelBox = label.getBBox(); |
|
width = labelBox.width / 2; |
|
height = labelBox.height / 2; |
|
//correct label position to fit into the box |
|
if (x < bbox.x + width) { |
|
x = bbox.x + width; |
|
} else if (x + width > bbox.x + bbox.width) { |
|
x = bbox.x + bbox.width - width; |
|
} |
|
y = y - height; |
|
if (y < bbox.y + height) { |
|
y += 2 * height; |
|
} else if (y + height > bbox.y + bbox.height) { |
|
y -= 2 * height; |
|
} |
|
if (me.chart.animate && !me.chart.resizing) { |
|
label.show(true); |
|
me.onAnimate(label, { |
|
to: { |
|
x: x, |
|
y: y |
|
} |
|
}); |
|
} else { |
|
label.setAttributes({ |
|
x: x, |
|
y: y |
|
}, true); |
|
if (resizing && me.animation) { |
|
me.animation.on('afteranimate', function() { |
|
label.show(true); |
|
}); |
|
} else { |
|
label.show(true); |
|
} |
|
} |
|
}, |
|
// @private |
|
onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) { |
|
var me = this, |
|
chart = me.chart, |
|
surface = chart.surface, |
|
resizing = chart.resizing, |
|
config = me.callouts, |
|
items = me.items, |
|
prev = (i == 0) ? false : items[i - 1].point, |
|
next = (i == items.length - 1) ? false : items[i + 1].point, |
|
cur = item.point, |
|
dir, norm, normal, a, aprev, anext, |
|
bbox = (callout && callout.label ? callout.label.getBBox() : { |
|
width: 0, |
|
height: 0 |
|
}), |
|
offsetFromViz = 30, |
|
offsetToSide = 10, |
|
offsetBox = 3, |
|
boxx, boxy, boxw, boxh, p, |
|
clipRect = me.clipRect, |
|
x, y; |
|
if (!bbox.width || !bbox.height) { |
|
return; |
|
} |
|
//get the right two points |
|
if (!prev) { |
|
prev = cur; |
|
} |
|
if (!next) { |
|
next = cur; |
|
} |
|
a = (next[1] - prev[1]) / (next[0] - prev[0]); |
|
aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]); |
|
anext = (next[1] - cur[1]) / (next[0] - cur[0]); |
|
norm = Math.sqrt(1 + a * a); |
|
dir = [ |
|
1 / norm, |
|
a / norm |
|
]; |
|
normal = [ |
|
-dir[1], |
|
dir[0] |
|
]; |
|
//keep the label always on the outer part of the "elbow" |
|
if (aprev > 0 && anext < 0 && normal[1] < 0 || aprev < 0 && anext > 0 && normal[1] > 0) { |
|
normal[0] *= -1; |
|
normal[1] *= -1; |
|
} else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0 || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) { |
|
normal[0] *= -1; |
|
normal[1] *= -1; |
|
} |
|
//position |
|
x = cur[0] + normal[0] * offsetFromViz; |
|
y = cur[1] + normal[1] * offsetFromViz; |
|
//box position and dimensions |
|
boxx = x + (normal[0] > 0 ? 0 : -(bbox.width + 2 * offsetBox)); |
|
boxy = y - bbox.height / 2 - offsetBox; |
|
boxw = bbox.width + 2 * offsetBox; |
|
boxh = bbox.height + 2 * offsetBox; |
|
//now check if we're out of bounds and invert the normal vector correspondingly |
|
//this may add new overlaps between labels (but labels won't be out of bounds). |
|
if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) { |
|
normal[0] *= -1; |
|
} |
|
if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) { |
|
normal[1] *= -1; |
|
} |
|
//update positions |
|
x = cur[0] + normal[0] * offsetFromViz; |
|
y = cur[1] + normal[1] * offsetFromViz; |
|
//update box position and dimensions |
|
boxx = x + (normal[0] > 0 ? 0 : -(bbox.width + 2 * offsetBox)); |
|
boxy = y - bbox.height / 2 - offsetBox; |
|
boxw = bbox.width + 2 * offsetBox; |
|
boxh = bbox.height + 2 * offsetBox; |
|
//set the line from the middle of the pie to the box. |
|
callout.lines.setAttributes({ |
|
path: [ |
|
"M", |
|
cur[0], |
|
cur[1], |
|
"L", |
|
x, |
|
y, |
|
"Z" |
|
] |
|
}, true); |
|
//set box position |
|
callout.box.setAttributes({ |
|
x: boxx, |
|
y: boxy, |
|
width: boxw, |
|
height: boxh |
|
}, true); |
|
//set text position |
|
callout.label.setAttributes({ |
|
x: x + (normal[0] > 0 ? offsetBox : -(bbox.width + offsetBox)), |
|
y: y |
|
}, true); |
|
for (p in callout) { |
|
callout[p].show(true); |
|
} |
|
}, |
|
isItemInPoint: function(x, y, item, i) { |
|
var me = this, |
|
pointsUp = item.pointsUp, |
|
pointsDown = item.pointsDown, |
|
abs = Math.abs, |
|
distChanged = false, |
|
last = false, |
|
reverse = me.reverse, |
|
dist = Infinity, |
|
p, pln, point; |
|
for (p = 0 , pln = pointsUp.length; p < pln; p++) { |
|
point = [ |
|
pointsUp[p][0], |
|
pointsUp[p][1] |
|
]; |
|
distChanged = false; |
|
last = p == pln - 1; |
|
if (dist > abs(x - point[0])) { |
|
dist = abs(x - point[0]); |
|
distChanged = true; |
|
if (last) { |
|
++p; |
|
} |
|
} |
|
if (!distChanged || (distChanged && last)) { |
|
point = pointsUp[p - 1]; |
|
if (y >= point[1] && (!pointsDown.length || y <= (pointsDown[p - 1][1]))) { |
|
idx = reverse ? pln - p : p - 1; |
|
item.storeIndex = idx; |
|
item.storeField = me.yField[i]; |
|
item.storeItem = me.chart.getChartStore().getAt(idx); |
|
item._points = pointsDown.length ? [ |
|
point, |
|
pointsDown[p - 1] |
|
] : [ |
|
point |
|
]; |
|
return true; |
|
} else { |
|
break; |
|
} |
|
} |
|
} |
|
return false; |
|
}, |
|
/** |
|
* Highlight this entire series. |
|
* @param {Object} item Info about the item; same format as returned by #getItemForPoint. |
|
*/ |
|
highlightSeries: function() { |
|
var area, to, fillColor; |
|
if (this._index !== undefined) { |
|
area = this.areas[this._index]; |
|
if (area.__highlightAnim) { |
|
area.__highlightAnim.paused = true; |
|
} |
|
area.__highlighted = true; |
|
area.__prevOpacity = area.__prevOpacity || area.attr.opacity || 1; |
|
area.__prevFill = area.__prevFill || area.attr.fill; |
|
area.__prevLineWidth = area.__prevLineWidth || area.attr.lineWidth; |
|
fillColor = Ext.draw.Color.fromString(area.__prevFill); |
|
to = { |
|
lineWidth: (area.__prevLineWidth || 0) + 2 |
|
}; |
|
if (fillColor) { |
|
to.fill = fillColor.getLighter(0.2).toString(); |
|
} else { |
|
to.opacity = Math.max(area.__prevOpacity - 0.3, 0); |
|
} |
|
if (this.chart.animate) { |
|
area.__highlightAnim = new Ext.fx.Anim(Ext.apply({ |
|
target: area, |
|
to: to |
|
}, this.chart.animate)); |
|
} else { |
|
area.setAttributes(to, true); |
|
} |
|
} |
|
}, |
|
/** |
|
* UnHighlight this entire series. |
|
* @param {Object} item Info about the item; same format as returned by #getItemForPoint. |
|
*/ |
|
unHighlightSeries: function() { |
|
var area; |
|
if (this._index !== undefined) { |
|
area = this.areas[this._index]; |
|
if (area.__highlightAnim) { |
|
area.__highlightAnim.paused = true; |
|
} |
|
if (area.__highlighted) { |
|
area.__highlighted = false; |
|
area.__highlightAnim = new Ext.fx.Anim({ |
|
target: area, |
|
to: { |
|
fill: area.__prevFill, |
|
opacity: area.__prevOpacity, |
|
lineWidth: area.__prevLineWidth |
|
} |
|
}); |
|
} |
|
} |
|
}, |
|
/** |
|
* Highlight the specified item. If no item is provided the whole series will be highlighted. |
|
* @param item {Object} Info about the item; same format as returned by #getItemForPoint |
|
*/ |
|
highlightItem: function(item) { |
|
var me = this, |
|
points, path; |
|
if (!item) { |
|
this.highlightSeries(); |
|
return; |
|
} |
|
points = item._points; |
|
if (points.length === 2) { |
|
path = [ |
|
'M', |
|
points[0][0], |
|
points[0][1], |
|
'L', |
|
points[1][0], |
|
points[1][1] |
|
]; |
|
} else { |
|
path = [ |
|
'M', |
|
points[0][0], |
|
points[0][1], |
|
'L', |
|
points[0][0], |
|
me.bbox.y + me.bbox.height |
|
]; |
|
} |
|
me.highlightSprite.setAttributes({ |
|
path: path, |
|
hidden: false |
|
}, true); |
|
}, |
|
/** |
|
* Un-highlights the specified item. If no item is provided it will un-highlight the entire series. |
|
* @param {Object} item Info about the item; same format as returned by #getItemForPoint |
|
*/ |
|
unHighlightItem: function(item) { |
|
if (!item) { |
|
this.unHighlightSeries(); |
|
} |
|
if (this.highlightSprite) { |
|
this.highlightSprite.hide(true); |
|
} |
|
}, |
|
// @private |
|
hideAll: function(index) { |
|
var me = this; |
|
index = (isNaN(me._index) ? index : me._index) || 0; |
|
me.__excludes[index] = true; |
|
me.areas[index].hide(true); |
|
me.redraw(); |
|
}, |
|
// @private |
|
showAll: function(index) { |
|
var me = this; |
|
index = (isNaN(me._index) ? index : me._index) || 0; |
|
me.__excludes[index] = false; |
|
me.areas[index].show(true); |
|
me.redraw(); |
|
}, |
|
redraw: function() { |
|
//store previous configuration for the legend |
|
//and set it to false so we don't |
|
//re-build label elements if not necessary. |
|
var me = this, |
|
prevLegendConfig; |
|
prevLegendConfig = me.chart.legend.rebuild; |
|
me.chart.legend.rebuild = false; |
|
me.chart.redraw(); |
|
me.chart.legend.rebuild = prevLegendConfig; |
|
}, |
|
hide: function() { |
|
if (this.areas) { |
|
var me = this, |
|
areas = me.areas, |
|
i, j, l, ln, shadows; |
|
if (areas && areas.length) { |
|
for (i = 0 , ln = areas.length; i < ln; ++i) { |
|
if (areas[i]) { |
|
areas[i].hide(true); |
|
} |
|
} |
|
me.hideLabels(); |
|
} |
|
} |
|
}, |
|
/** |
|
* Returns the color of the series (to be displayed as color for the series legend item). |
|
* @param {Object} item Info about the item; same format as returned by #getItemForPoint |
|
*/ |
|
getLegendColor: function(index) { |
|
var me = this; |
|
index += me.themeIdx; |
|
return me.colorArrayStyle[index % me.colorArrayStyle.length]; |
|
} |
|
}); |
|
|
|
/** |
|
* Creates a Bar Chart. A Bar Chart is a useful visualization technique to display quantitative information for |
|
* different categories that can show some progression (or regression) in the dataset. As with all other series, the Bar |
|
* Series must be appended in the *series* Chart array configuration. See the Chart documentation for more information. |
|
* A typical configuration object for the bar series could be: |
|
* |
|
* @example |
|
* var store = Ext.create('Ext.data.JsonStore', { |
|
* fields: ['name', 'data'], |
|
* data: [ |
|
* { 'name': 'metric one', 'data':10 }, |
|
* { 'name': 'metric two', 'data': 7 }, |
|
* { 'name': 'metric three', 'data': 5 }, |
|
* { 'name': 'metric four', 'data': 2 }, |
|
* { 'name': 'metric five', 'data':27 } |
|
* ] |
|
* }); |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* width: 500, |
|
* height: 300, |
|
* animate: true, |
|
* store: store, |
|
* axes: [{ |
|
* type: 'Numeric', |
|
* position: 'bottom', |
|
* fields: ['data'], |
|
* label: { |
|
* renderer: Ext.util.Format.numberRenderer('0,0') |
|
* }, |
|
* title: 'Sample Values', |
|
* grid: true, |
|
* minimum: 0 |
|
* }, { |
|
* type: 'Category', |
|
* position: 'left', |
|
* fields: ['name'], |
|
* title: 'Sample Metrics' |
|
* }], |
|
* series: [{ |
|
* type: 'bar', |
|
* axis: 'bottom', |
|
* highlight: true, |
|
* tips: { |
|
* trackMouse: true, |
|
* width: 140, |
|
* height: 28, |
|
* renderer: function(storeItem, item) { |
|
* this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data') + ' views'); |
|
* } |
|
* }, |
|
* label: { |
|
* display: 'insideEnd', |
|
* field: 'data', |
|
* renderer: Ext.util.Format.numberRenderer('0'), |
|
* orientation: 'horizontal', |
|
* color: '#333', |
|
* 'text-anchor': 'middle' |
|
* }, |
|
* xField: 'name', |
|
* yField: 'data' |
|
* }] |
|
* }); |
|
* |
|
* In this configuration we set `bar` as the series type, bind the values of the bar to the bottom axis and set the |
|
* xField or category field to the `name` parameter of the store. We also set `highlight` to true which enables smooth |
|
* animations when bars are hovered. We also set some configuration for the bar labels to be displayed inside the bar, |
|
* to display the information found in the `data1` property of each element store, to render a formated text with the |
|
* `Ext.util.Format` we pass in, to have an `horizontal` orientation (as opposed to a vertical one) and we also set |
|
* other styles like `color`, `text-anchor`, etc. |
|
*/ |
|
Ext.define('Ext.chart.series.Bar', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.chart.series.Cartesian', |
|
alternateClassName: [ |
|
'Ext.chart.BarSeries', |
|
'Ext.chart.BarChart', |
|
'Ext.chart.StackedBarChart' |
|
], |
|
requires: [ |
|
'Ext.chart.axis.Axis', |
|
'Ext.fx.Anim' |
|
], |
|
/* End Definitions */ |
|
type: 'bar', |
|
alias: 'series.bar', |
|
/** |
|
* @cfg {Boolean} column Whether to set the visualization as column chart or horizontal bar chart. |
|
*/ |
|
column: false, |
|
/** |
|
* @cfg style Style properties that will override the theming series styles. |
|
*/ |
|
style: {}, |
|
/** |
|
* @cfg {Number} gutter The gutter space between single bars, as a percentage of the bar width |
|
*/ |
|
gutter: 38.2, |
|
/** |
|
* @cfg {Number} groupGutter The gutter space between groups of bars, as a percentage of the bar width |
|
*/ |
|
groupGutter: 38.2, |
|
/** |
|
* @cfg {Number/Object} xPadding Padding between the left/right axes and the bars. |
|
* The possible values are a number (the number of pixels for both left and right padding) |
|
* or an object with `{ left, right }` properties. |
|
*/ |
|
xPadding: 0, |
|
/** |
|
* @cfg {Number/Object} yPadding Padding between the top/bottom axes and the bars. |
|
* The possible values are a number (the number of pixels for both top and bottom padding) |
|
* or an object with `{ top, bottom }` properties. |
|
*/ |
|
yPadding: 10, |
|
/** |
|
* @cfg {Boolean} stacked |
|
* If set to `true` then bars for multiple `yField` values will be rendered stacked on top of one another. |
|
* Otherwise, they will be rendered side-by-side. Defaults to `false`. |
|
*/ |
|
defaultRotate: { |
|
x: 0, |
|
y: 0, |
|
degrees: 0 |
|
}, |
|
constructor: function(config) { |
|
this.callParent(arguments); |
|
var me = this, |
|
surface = me.chart.surface, |
|
shadow = me.chart.shadow, |
|
i, l; |
|
config.highlightCfg = Ext.Object.merge({ |
|
lineWidth: 3, |
|
stroke: '#55c', |
|
opacity: 0.8, |
|
color: '#f00' |
|
}, config.highlightCfg); |
|
Ext.apply(me, config, { |
|
shadowAttributes: [ |
|
{ |
|
"stroke-width": 6, |
|
"stroke-opacity": 0.05, |
|
stroke: 'rgb(200, 200, 200)', |
|
translate: { |
|
x: 1.2, |
|
y: 1.2 |
|
} |
|
}, |
|
{ |
|
"stroke-width": 4, |
|
"stroke-opacity": 0.1, |
|
stroke: 'rgb(150, 150, 150)', |
|
translate: { |
|
x: 0.9, |
|
y: 0.9 |
|
} |
|
}, |
|
{ |
|
"stroke-width": 2, |
|
"stroke-opacity": 0.15, |
|
stroke: 'rgb(100, 100, 100)', |
|
translate: { |
|
x: 0.6, |
|
y: 0.6 |
|
} |
|
} |
|
] |
|
}); |
|
me.group = surface.getGroup(me.seriesId + '-bars'); |
|
if (shadow) { |
|
for (i = 0 , l = me.shadowAttributes.length; i < l; i++) { |
|
me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i)); |
|
} |
|
} |
|
}, |
|
// @private returns the padding. |
|
getPadding: function() { |
|
var me = this, |
|
xPadding = me.xPadding, |
|
yPadding = me.yPadding, |
|
padding = {}; |
|
if (Ext.isNumber(xPadding)) { |
|
padding.left = xPadding; |
|
padding.right = xPadding; |
|
} else if (Ext.isObject(xPadding)) { |
|
padding.left = xPadding.left; |
|
padding.right = xPadding.right; |
|
} else { |
|
padding.left = 0; |
|
padding.right = 0; |
|
} |
|
padding.width = padding.left + padding.right; |
|
if (Ext.isNumber(yPadding)) { |
|
padding.bottom = yPadding; |
|
padding.top = yPadding; |
|
} else if (Ext.isObject(yPadding)) { |
|
padding.bottom = yPadding.bottom; |
|
padding.top = yPadding.top; |
|
} else { |
|
padding.bottom = 0; |
|
padding.top = 0; |
|
} |
|
padding.height = padding.bottom + padding.top; |
|
return padding; |
|
}, |
|
// @private returns the bar girth. |
|
getBarGirth: function() { |
|
var me = this, |
|
store = me.chart.getChartStore(), |
|
column = me.column, |
|
ln = store.getCount(), |
|
gutter = me.gutter / 100, |
|
padding, property; |
|
property = (column ? 'width' : 'height'); |
|
if (me.style && me.style[property]) { |
|
me.configuredColumnGirth = true; |
|
return +me.style[property]; |
|
} |
|
padding = me.getPadding(); |
|
return (me.chart.chartBBox[property] - padding[property]) / (ln * (gutter + 1) - gutter); |
|
}, |
|
// @private returns the gutters. |
|
getGutters: function() { |
|
var me = this, |
|
column = me.column, |
|
padding = me.getPadding(), |
|
halfBarGirth = me.getBarGirth() / 2, |
|
lowerGutter = Math.ceil((column ? padding.left : padding.bottom) + halfBarGirth), |
|
upperGutter = Math.ceil((column ? padding.right : padding.top) + halfBarGirth); |
|
return { |
|
lower: lowerGutter, |
|
upper: upperGutter, |
|
verticalAxis: !column |
|
}; |
|
}, |
|
// @private Get chart and data boundaries |
|
getBounds: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
data = store.data.items, |
|
i, ln, record, |
|
bars = [].concat(me.yField), |
|
barsLoc = [], |
|
barsLen = bars.length, |
|
groupBarsLen = barsLen, |
|
groupGutter = me.groupGutter / 100, |
|
column = me.column, |
|
padding = me.getPadding(), |
|
stacked = me.stacked, |
|
barWidth = me.getBarGirth(), |
|
barWidthProperty = column ? 'width' : 'height', |
|
math = Math, |
|
mmin = math.min, |
|
mmax = math.max, |
|
mabs = math.abs, |
|
boundAxes = me.getAxesForXAndYFields(), |
|
boundYAxis = boundAxes.yAxis, |
|
minX, maxX, colsScale, colsZero, gutters, ends, shrunkBarWidth, groupBarWidth, bbox, minY, maxY, axis, out, scale, zero, total, rec, j, plus, minus, inflections, tick, loc; |
|
me.setBBox(true); |
|
bbox = me.bbox; |
|
//Skip excluded series |
|
if (me.__excludes) { |
|
for (j = 0 , total = me.__excludes.length; j < total; j++) { |
|
if (me.__excludes[j]) { |
|
groupBarsLen--; |
|
} |
|
} |
|
} |
|
axis = chart.axes.get(boundYAxis); |
|
if (axis) { |
|
ends = axis.applyData(); |
|
minY = ends.from; |
|
maxY = ends.to; |
|
} |
|
if (me.yField && !Ext.isNumber(minY)) { |
|
out = me.getMinMaxYValues(); |
|
minY = out[0]; |
|
maxY = out[1]; |
|
} |
|
if (!Ext.isNumber(minY)) { |
|
minY = 0; |
|
} |
|
if (!Ext.isNumber(maxY)) { |
|
maxY = 0; |
|
} |
|
scale = (column ? bbox.height - padding.height : bbox.width - padding.width) / (maxY - minY); |
|
shrunkBarWidth = barWidth; |
|
groupBarWidth = (barWidth / ((stacked ? 1 : groupBarsLen) * (groupGutter + 1) - groupGutter)); |
|
if (barWidthProperty in me.style) { |
|
groupBarWidth = mmin(groupBarWidth, me.style[barWidthProperty]); |
|
shrunkBarWidth = groupBarWidth * ((stacked ? 1 : groupBarsLen) * (groupGutter + 1) - groupGutter); |
|
} |
|
zero = (column) ? bbox.y + bbox.height - padding.bottom : bbox.x + padding.left; |
|
if (stacked) { |
|
total = [ |
|
[], |
|
[] |
|
]; |
|
for (i = 0 , ln = data.length; i < ln; i++) { |
|
record = data[i]; |
|
total[0][i] = total[0][i] || 0; |
|
total[1][i] = total[1][i] || 0; |
|
for (j = 0; j < barsLen; j++) { |
|
if (me.__excludes && me.__excludes[j]) { |
|
|
|
continue; |
|
} |
|
rec = record.get(bars[j]); |
|
total[+(rec > 0)][i] += mabs(rec); |
|
} |
|
} |
|
total[+(maxY > 0)].push(mabs(maxY)); |
|
total[+(minY > 0)].push(mabs(minY)); |
|
minus = mmax.apply(math, total[0]); |
|
plus = mmax.apply(math, total[1]); |
|
scale = (column ? bbox.height - padding.height : bbox.width - padding.width) / (plus + minus); |
|
zero = zero + minus * scale * (column ? -1 : 1); |
|
} else if (minY / maxY < 0) { |
|
zero = zero - minY * scale * (column ? -1 : 1); |
|
} |
|
// If the columns are bound to the x-axis, calculate their positions |
|
if (me.boundColumn) { |
|
axis = chart.axes.get(boundAxes.xAxis); |
|
if (axis) { |
|
ends = axis.applyData(); |
|
minX = ends.from; |
|
maxX = ends.to; |
|
} |
|
if (me.xField && !Ext.isNumber(minX)) { |
|
out = me.getMinMaxYValues(); |
|
minX = out[0]; |
|
maxX = out[1]; |
|
} |
|
if (!Ext.isNumber(minX)) { |
|
minX = 0; |
|
} |
|
if (!Ext.isNumber(maxX)) { |
|
maxX = 0; |
|
} |
|
gutters = me.getGutters(); |
|
colsScale = (bbox.width - (gutters.lower + gutters.upper)) / ((maxX - minX) || 1); |
|
colsZero = bbox.x + gutters.lower; |
|
barsLoc = []; |
|
for (i = 0 , ln = data.length; i < ln; i++) { |
|
record = data[i]; |
|
rec = record.get(me.xField); |
|
barsLoc[i] = colsZero + (rec - minX) * colsScale - (groupBarWidth / 2); |
|
} |
|
} |
|
// Or, if column width is configured, place columns over inflections |
|
else if (me.configuredColumnGirth) { |
|
axis = chart.axes.get(boundAxes.xAxis); |
|
if (axis) { |
|
inflections = axis.inflections; |
|
if (axis.isCategoryAxis || inflections.length >= data.length) { |
|
barsLoc = []; |
|
for (i = 0 , ln = data.length; i < ln; i++) { |
|
tick = inflections[i]; |
|
loc = column ? tick[0] : tick[1]; |
|
barsLoc[i] = loc - (shrunkBarWidth / 2); |
|
} |
|
} |
|
} |
|
} |
|
return { |
|
bars: bars, |
|
barsLoc: barsLoc, |
|
bbox: bbox, |
|
shrunkBarWidth: shrunkBarWidth, |
|
barsLen: barsLen, |
|
groupBarsLen: groupBarsLen, |
|
barWidth: barWidth, |
|
groupBarWidth: groupBarWidth, |
|
scale: scale, |
|
zero: zero, |
|
padding: padding, |
|
signed: minY / maxY < 0, |
|
minY: minY, |
|
maxY: maxY |
|
}; |
|
}, |
|
// @private Build an array of paths for the chart |
|
getPaths: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
data = store.data.items, |
|
i, total, record, |
|
bounds = me.bounds = me.getBounds(), |
|
items = me.items = [], |
|
yFields = Ext.isArray(me.yField) ? me.yField : [ |
|
me.yField |
|
], |
|
gutter = me.gutter / 100, |
|
groupGutter = me.groupGutter / 100, |
|
animate = chart.animate, |
|
column = me.column, |
|
group = me.group, |
|
enableShadows = chart.shadow, |
|
shadowGroups = me.shadowGroups, |
|
shadowAttributes = me.shadowAttributes, |
|
shadowGroupsLn = shadowGroups.length, |
|
bbox = bounds.bbox, |
|
barWidth = bounds.barWidth, |
|
shrunkBarWidth = bounds.shrunkBarWidth, |
|
padding = me.getPadding(), |
|
stacked = me.stacked, |
|
barsLen = bounds.barsLen, |
|
colors = me.colorArrayStyle, |
|
colorLength = colors && colors.length || 0, |
|
themeIndex = me.themeIdx, |
|
reverse = me.reverse, |
|
math = Math, |
|
mmax = math.max, |
|
mmin = math.min, |
|
mabs = math.abs, |
|
j, yValue, height, totalDim, totalNegDim, bottom, top, hasShadow, barAttr, attrs, counter, totalPositiveValues, totalNegativeValues, shadowIndex, shadow, sprite, offset, floorY, idx, itemIdx, xPos, yPos, width, usedWidth, barIdx; |
|
for (i = 0 , total = data.length; i < total; i++) { |
|
record = data[i]; |
|
bottom = bounds.zero; |
|
top = bounds.zero; |
|
totalDim = 0; |
|
totalNegDim = 0; |
|
totalPositiveValues = totalNegativeValues = 0; |
|
hasShadow = false; |
|
usedWidth = 0; |
|
for (j = 0 , counter = 0; j < barsLen; j++) { |
|
// Excluded series |
|
if (me.__excludes && me.__excludes[j]) { |
|
|
|
continue; |
|
} |
|
yValue = record.get(bounds.bars[j]); |
|
if (yValue >= 0) { |
|
totalPositiveValues += yValue; |
|
} else { |
|
totalNegativeValues += yValue; |
|
} |
|
height = Math.round((yValue - mmax(bounds.minY, 0)) * bounds.scale); |
|
idx = themeIndex + (barsLen > 1 ? j : 0); |
|
barAttr = { |
|
fill: colors[idx % colorLength] |
|
}; |
|
if (column) { |
|
idx = reverse ? (total - i - 1) : i; |
|
barIdx = reverse ? (barsLen - counter - 1) : counter; |
|
if (me.boundColumn) { |
|
xPos = bounds.barsLoc[idx]; |
|
} else if (me.configuredColumnGirth && bounds.barsLoc.length) { |
|
xPos = bounds.barsLoc[idx] + barIdx * bounds.groupBarWidth * (1 + groupGutter) * !stacked; |
|
} else { |
|
xPos = bbox.x + padding.left + (barWidth - shrunkBarWidth) * 0.5 + idx * barWidth * (1 + gutter) + barIdx * bounds.groupBarWidth * (1 + groupGutter) * !stacked; |
|
} |
|
Ext.apply(barAttr, { |
|
height: height, |
|
width: mmax(bounds.groupBarWidth, 0), |
|
x: xPos, |
|
y: bottom - height |
|
}); |
|
} else { |
|
// draw in reverse order |
|
offset = (total - 1) - i; |
|
width = height + (bottom == bounds.zero); |
|
xPos = bottom + (bottom != bounds.zero); |
|
if (reverse) { |
|
// Subtract 1 for the first item |
|
xPos = bounds.zero + bbox.width - width - (usedWidth === 0 ? 1 : 0); |
|
if (stacked) { |
|
xPos -= usedWidth; |
|
usedWidth += width; |
|
} |
|
} |
|
if (me.configuredColumnGirth && bounds.barsLoc.length) { |
|
yPos = bounds.barsLoc[i] + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked; |
|
} else { |
|
yPos = bbox.y + padding.top + (barWidth - shrunkBarWidth) * 0.5 + offset * barWidth * (1 + gutter) + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked + 1; |
|
} |
|
Ext.apply(barAttr, { |
|
height: mmax(bounds.groupBarWidth, 0), |
|
width: width, |
|
x: xPos, |
|
y: yPos |
|
}); |
|
} |
|
if (height < 0) { |
|
if (column) { |
|
barAttr.y = top; |
|
barAttr.height = mabs(height); |
|
} else { |
|
barAttr.x = top + height; |
|
barAttr.width = mabs(height); |
|
} |
|
} |
|
if (stacked) { |
|
if (height < 0) { |
|
top += height * (column ? -1 : 1); |
|
} else { |
|
bottom += height * (column ? -1 : 1); |
|
} |
|
totalDim += mabs(height); |
|
if (height < 0) { |
|
totalNegDim += mabs(height); |
|
} |
|
} |
|
barAttr.x = Math.floor(barAttr.x) + 1; |
|
floorY = Math.floor(barAttr.y); |
|
if (Ext.isIE8 && barAttr.y > floorY) { |
|
floorY--; |
|
} |
|
barAttr.y = floorY; |
|
barAttr.width = Math.floor(barAttr.width); |
|
barAttr.height = Math.floor(barAttr.height); |
|
items.push({ |
|
series: me, |
|
yField: yFields[j], |
|
storeItem: record, |
|
value: [ |
|
record.get(me.xField), |
|
yValue |
|
], |
|
attr: barAttr, |
|
point: column ? [ |
|
barAttr.x + barAttr.width / 2, |
|
yValue >= 0 ? barAttr.y : barAttr.y + barAttr.height |
|
] : [ |
|
yValue >= 0 ? barAttr.x + barAttr.width : barAttr.x, |
|
barAttr.y + barAttr.height / 2 |
|
] |
|
}); |
|
// When resizing, reset before animating |
|
if (animate && chart.resizing) { |
|
attrs = column ? { |
|
x: barAttr.x, |
|
y: bounds.zero, |
|
width: barAttr.width, |
|
height: 0 |
|
} : { |
|
x: bounds.zero, |
|
y: barAttr.y, |
|
width: 0, |
|
height: barAttr.height |
|
}; |
|
if (enableShadows && (stacked && !hasShadow || !stacked)) { |
|
hasShadow = true; |
|
//update shadows |
|
for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) { |
|
shadow = shadowGroups[shadowIndex].getAt(stacked ? i : (i * barsLen + j)); |
|
if (shadow) { |
|
shadow.setAttributes(attrs, true); |
|
} |
|
} |
|
} |
|
//update sprite position and width/height |
|
sprite = group.getAt(i * barsLen + j); |
|
if (sprite) { |
|
sprite.setAttributes(attrs, true); |
|
} |
|
} |
|
counter++; |
|
} |
|
if (stacked && items.length) { |
|
items[i * counter].totalDim = totalDim; |
|
items[i * counter].totalNegDim = totalNegDim; |
|
items[i * counter].totalPositiveValues = totalPositiveValues; |
|
items[i * counter].totalNegativeValues = totalNegativeValues; |
|
} |
|
} |
|
if (stacked && counter == 0) { |
|
// Remove ghost shadow ref: EXTJSIV-5982 |
|
for (i = 0 , total = data.length; i < total; i++) { |
|
for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) { |
|
shadow = shadowGroups[shadowIndex].getAt(i); |
|
if (shadow) { |
|
shadow.hide(true); |
|
} |
|
} |
|
} |
|
} |
|
}, |
|
// @private render/setAttributes on the shadows |
|
renderShadows: function(i, barAttr, baseAttrs, bounds) { |
|
var me = this, |
|
chart = me.chart, |
|
surface = chart.surface, |
|
animate = chart.animate, |
|
stacked = me.stacked, |
|
shadowGroups = me.shadowGroups, |
|
shadowAttributes = me.shadowAttributes, |
|
shadowGroupsLn = shadowGroups.length, |
|
store = chart.getChartStore(), |
|
column = me.column, |
|
items = me.items, |
|
shadows = [], |
|
reverse = me.reverse, |
|
zero = bounds.zero, |
|
shadowIndex, shadowBarAttr, shadow, totalDim, totalNegDim, j, rendererAttributes; |
|
if ((stacked && (i % bounds.groupBarsLen === 0)) || !stacked) { |
|
j = i / bounds.groupBarsLen; |
|
//create shadows |
|
for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) { |
|
shadowBarAttr = Ext.apply({}, shadowAttributes[shadowIndex]); |
|
shadow = shadowGroups[shadowIndex].getAt(stacked ? j : i); |
|
Ext.copyTo(shadowBarAttr, barAttr, 'x,y,width,height'); |
|
if (!shadow) { |
|
shadow = surface.add(Ext.apply({ |
|
type: 'rect', |
|
isShadow: true, |
|
group: shadowGroups[shadowIndex] |
|
}, Ext.apply({}, baseAttrs, shadowBarAttr))); |
|
} |
|
if (stacked) { |
|
totalDim = items[i].totalDim; |
|
totalNegDim = items[i].totalNegDim; |
|
if (column) { |
|
shadowBarAttr.y = zero + totalNegDim - totalDim - 1; |
|
shadowBarAttr.height = totalDim; |
|
} else { |
|
if (reverse) { |
|
shadowBarAttr.x = zero + bounds.bbox.width - totalDim; |
|
} else { |
|
shadowBarAttr.x = zero - totalNegDim; |
|
} |
|
shadowBarAttr.width = totalDim; |
|
} |
|
} |
|
rendererAttributes = me.renderer(shadow, store.getAt(j), shadowBarAttr, i, store); |
|
rendererAttributes.hidden = !!barAttr.hidden; |
|
if (animate) { |
|
me.onAnimate(shadow, { |
|
zero: bounds.zero + (reverse ? bounds.bbox.width : 0), |
|
to: rendererAttributes |
|
}); |
|
} else { |
|
shadow.setAttributes(rendererAttributes, true); |
|
} |
|
shadows.push(shadow); |
|
} |
|
} |
|
return shadows; |
|
}, |
|
/** |
|
* Draws the series for the current chart. |
|
*/ |
|
drawSeries: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
surface = chart.surface, |
|
animate = chart.animate, |
|
stacked = me.stacked, |
|
column = me.column, |
|
chartAxes = chart.axes, |
|
boundAxes = me.getAxesForXAndYFields(), |
|
boundXAxis = boundAxes.xAxis, |
|
boundYAxis = boundAxes.yAxis, |
|
enableShadows = chart.shadow, |
|
shadowGroups = me.shadowGroups, |
|
shadowGroupsLn = shadowGroups.length, |
|
group = me.group, |
|
seriesStyle = me.seriesStyle, |
|
items, ln, i, j, baseAttrs, sprite, rendererAttributes, shadowIndex, shadowGroup, bounds, endSeriesStyle, barAttr, attrs, anim; |
|
if (!store || !store.getCount() || me.seriesIsHidden) { |
|
me.hide(); |
|
me.items = []; |
|
return; |
|
} |
|
//fill colors are taken from the colors array. |
|
endSeriesStyle = Ext.apply({}, this.style, seriesStyle); |
|
delete endSeriesStyle.fill; |
|
delete endSeriesStyle.x; |
|
delete endSeriesStyle.y; |
|
delete endSeriesStyle.width; |
|
delete endSeriesStyle.height; |
|
me.unHighlightItem(); |
|
me.cleanHighlights(); |
|
me.boundColumn = (boundXAxis && Ext.Array.contains(me.axis, boundXAxis) && chartAxes.get(boundXAxis) && chartAxes.get(boundXAxis).isNumericAxis); |
|
me.getPaths(); |
|
bounds = me.bounds; |
|
items = me.items; |
|
baseAttrs = column ? { |
|
y: bounds.zero, |
|
height: 0 |
|
} : { |
|
x: bounds.zero, |
|
width: 0 |
|
}; |
|
ln = items.length; |
|
// Create new or reuse sprites and animate/display |
|
for (i = 0; i < ln; i++) { |
|
sprite = group.getAt(i); |
|
barAttr = items[i].attr; |
|
if (enableShadows) { |
|
items[i].shadows = me.renderShadows(i, barAttr, baseAttrs, bounds); |
|
} |
|
// Create a new sprite if needed (no height) |
|
if (!sprite) { |
|
attrs = Ext.apply({}, baseAttrs, barAttr); |
|
attrs = Ext.apply(attrs, endSeriesStyle || {}); |
|
sprite = surface.add(Ext.apply({}, { |
|
type: 'rect', |
|
group: group |
|
}, attrs)); |
|
} |
|
if (animate) { |
|
rendererAttributes = me.renderer(sprite, store.getAt(i), barAttr, i, store); |
|
sprite._to = rendererAttributes; |
|
anim = me.onAnimate(sprite, { |
|
zero: bounds.zero + (me.reverse ? bounds.bbox.width : 0), |
|
to: Ext.apply(rendererAttributes, endSeriesStyle) |
|
}); |
|
if (enableShadows && stacked && (i % bounds.barsLen === 0)) { |
|
j = i / bounds.barsLen; |
|
for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) { |
|
anim.on('afteranimate', function() { |
|
this.show(true); |
|
}, shadowGroups[shadowIndex].getAt(j)); |
|
} |
|
} |
|
} else { |
|
rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply(barAttr, { |
|
hidden: false |
|
}), i, store); |
|
sprite.setAttributes(Ext.apply(rendererAttributes, endSeriesStyle), true); |
|
} |
|
items[i].sprite = sprite; |
|
} |
|
// Hide unused sprites |
|
ln = group.getCount(); |
|
for (j = i; j < ln; j++) { |
|
group.getAt(j).hide(true); |
|
} |
|
if (me.stacked) { |
|
// If stacked, we have only store.getCount() shadows. |
|
i = store.getCount(); |
|
} |
|
// Hide unused shadows |
|
if (enableShadows) { |
|
for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) { |
|
shadowGroup = shadowGroups[shadowIndex]; |
|
ln = shadowGroup.getCount(); |
|
for (j = i; j < ln; j++) { |
|
shadowGroup.getAt(j).hide(true); |
|
} |
|
} |
|
} |
|
me.renderLabels(); |
|
}, |
|
// @private called when a label is to be created. |
|
onCreateLabel: function(storeItem, item, i, display) { |
|
var me = this, |
|
surface = me.chart.surface, |
|
group = me.labelsGroup, |
|
config = me.label, |
|
endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle || {}), |
|
sprite; |
|
return surface.add(Ext.apply({ |
|
type: 'text', |
|
group: group |
|
}, endLabelStyle || {})); |
|
}, |
|
// @private called when a label is to be positioned. |
|
onPlaceLabel: function(label, storeItem, item, i, display, animate, index) { |
|
// Determine the label's final position. Starts with the configured preferred value but |
|
// may get flipped from inside to outside or vice-versa depending on space. |
|
var me = this, |
|
opt = me.bounds, |
|
groupBarWidth = opt.groupBarWidth, |
|
column = me.column, |
|
chart = me.chart, |
|
chartBBox = chart.chartBBox, |
|
resizing = chart.resizing, |
|
xValue = item.value[0], |
|
yValue = item.value[1], |
|
attr = item.attr, |
|
config = me.label, |
|
stacked = me.stacked, |
|
stackedDisplay = config.stackedDisplay, |
|
rotate = (config.orientation == 'vertical'), |
|
field = [].concat(config.field), |
|
format = config.renderer, |
|
text, size, width, height, |
|
zero = opt.zero, |
|
insideStart = 'insideStart', |
|
insideEnd = 'insideEnd', |
|
outside = 'outside', |
|
over = 'over', |
|
under = 'under', |
|
labelMarginX = 4, |
|
// leave space around the labels (important when saving chart as image) |
|
labelMarginY = 2, |
|
signed = opt.signed, |
|
reverse = me.reverse, |
|
x, y, finalAttr; |
|
if (display == insideStart || display == insideEnd || display == outside) { |
|
if (stacked && (display == outside)) { |
|
// It doesn't make sense to use 'outside' on a stacked chart |
|
// unless we only want to display the 'stackedDisplay' labels. |
|
label.hide(true); |
|
return; |
|
} |
|
label.setAttributes({ |
|
// Reset the style in case the label is being reused (for instance, if a series is excluded) |
|
// and do it before calling the renderer function. |
|
style: undefined |
|
}); |
|
text = (Ext.isNumber(index) ? format(storeItem.get(field[index]), label, storeItem, item, i, display, animate, index) : ''); |
|
label.setAttributes({ |
|
// Set the text onto the label. |
|
text: text |
|
}); |
|
size = me.getLabelSize(text, label.attr.style); |
|
width = size.width; |
|
height = size.height; |
|
if (column) { |
|
//----------------------------------------- |
|
// Position the label within a column chart |
|
// If there is no label to display, or if the corresponding box in a stacked column |
|
// isn't tall enough to display the label, then leave. |
|
if (!width || !height || (stacked && (attr.height < height))) { |
|
label.hide(true); |
|
return; |
|
} |
|
// Align horizontally the label in the middle of the column |
|
x = attr.x + (rotate ? groupBarWidth / 2 : (groupBarWidth - width) / 2); |
|
// If the label is to be displayed outside, make sure there is room for it, otherwise display it inside. |
|
if (display == outside) { |
|
var free = (yValue >= 0 ? (attr.y - chartBBox.y) : (chartBBox.y + chartBBox.height - attr.y - attr.height)); |
|
if (free < height + labelMarginY) { |
|
display = insideEnd; |
|
} |
|
} |
|
// If the label is to be displayed inside a non-stacked chart, make sure it is |
|
// not taller than the box, otherwise move it outside. |
|
if (!stacked && (display != outside)) { |
|
if (height + labelMarginY > attr.height) { |
|
display = outside; |
|
} |
|
} |
|
// Place the label vertically depending on its config and on whether the value |
|
// it represents is positive (above the X-axis) or negative (below the X-axis) |
|
if (!y) { |
|
y = attr.y; |
|
if (yValue >= 0) { |
|
switch (display) { |
|
case insideStart: |
|
y += attr.height + (rotate ? -labelMarginY : -height / 2); |
|
break; |
|
case insideEnd: |
|
y += (rotate ? height + labelMarginX : height / 2); |
|
break; |
|
case outside: |
|
y += (rotate ? -labelMarginY : -height / 2); |
|
break; |
|
} |
|
} else { |
|
switch (display) { |
|
case insideStart: |
|
y += (rotate ? height + labelMarginY : height / 2); |
|
break; |
|
case insideEnd: |
|
y += (rotate ? attr.height - labelMarginY : attr.height - height / 2); |
|
break; |
|
case outside: |
|
y += (rotate ? attr.height + height + labelMarginY : attr.height + height / 2); |
|
break; |
|
} |
|
} |
|
} |
|
} else { |
|
//----------------------------------------- |
|
// Position the label within a bar chart |
|
// If there is no label to display, or if the corresponding box has no width, then leave. |
|
if (!width || !height || (stacked && !attr.width)) { |
|
label.hide(true); |
|
return; |
|
} |
|
// Align vertically the label in the middle of the bar |
|
y = attr.y + (rotate ? (groupBarWidth + height) / 2 : groupBarWidth / 2); |
|
// If the label is to be displayed outside, make sure there is room for it otherwise display it inside. |
|
if (display == outside) { |
|
var free = (yValue >= 0 ? (chartBBox.x + chartBBox.width - attr.x - attr.width) : (attr.x - chartBBox.x)); |
|
if (free < width + labelMarginX) { |
|
display = insideEnd; |
|
} |
|
} |
|
// If the label is to be displayed inside (and it is not rotated yet), make sure it is |
|
// not wider than the box it represents otherwise (for a stacked chart) rotate it vertically |
|
// and center it, or (for a non-stacked chart) move it outside. |
|
if ((display != outside) && !rotate) { |
|
// Add a slight fudge factor here to make sure we're not flush against the edge |
|
if (width + labelMarginX * 2 >= attr.width) { |
|
if (stacked) { |
|
if (height > attr.width) { |
|
label.hide(true); |
|
return; |
|
} |
|
// Even rotated, there isn't enough room. |
|
x = attr.x + attr.width / 2; |
|
rotate = true; |
|
} else { |
|
display = outside; |
|
} |
|
} |
|
} |
|
// Place the label horizontally depending on its config and on whether the value |
|
// it represents is positive (above the X-axis) or negative (below the X-axis) |
|
if (!x) { |
|
x = attr.x; |
|
if (yValue >= 0) { |
|
switch (display) { |
|
case insideStart: |
|
if (reverse) { |
|
x += attr.width + (rotate ? -width / 2 : -width - labelMarginX); |
|
} else { |
|
x += (rotate ? width / 2 : labelMarginX); |
|
}; |
|
break; |
|
case insideEnd: |
|
if (reverse) { |
|
x -= rotate ? -width / 2 : -width - labelMarginX; |
|
} else { |
|
x += attr.width + (rotate ? -width / 2 : -width - labelMarginX); |
|
}; |
|
break; |
|
case outside: |
|
if (reverse) { |
|
x -= width + (rotate ? width / 2 : labelMarginX); |
|
} else { |
|
x += attr.width + (rotate ? width / 2 : labelMarginX); |
|
}; |
|
break; |
|
} |
|
} else { |
|
switch (display) { |
|
case insideStart: |
|
if (reverse) { |
|
x -= rotate ? -width / 2 : -width - labelMarginX; |
|
} else { |
|
x += attr.width + (rotate ? -width / 2 : -width - labelMarginX); |
|
}; |
|
break; |
|
case insideEnd: |
|
if (reverse) { |
|
x += attr.width + (rotate ? -width / 2 : -width - labelMarginX); |
|
} else { |
|
x += (rotate ? width / 2 : labelMarginX); |
|
}; |
|
break; |
|
case outside: |
|
if (reverse) { |
|
x -= width + (rotate ? width / 2 : labelMarginX); |
|
} else { |
|
x += (rotate ? -width / 2 : -width - labelMarginX); |
|
}; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
} else if (display == over || display == under) { |
|
if (stacked && stackedDisplay) { |
|
//----------------------------------------- |
|
// Position the label on top or at the bottom of a stacked bar/column |
|
text = label.attr.text; |
|
label.setAttributes({ |
|
// The text is already set onto the label: we just need to set the style |
|
// (but don't overwrite any custom style that might have been set by an app override). |
|
style: Ext.applyIf((label.attr && label.attr.style) || {}, { |
|
'font-weight': 'bold', |
|
'font-size': '14px' |
|
}) |
|
}); |
|
size = me.getLabelSize(text, label.attr.style); |
|
width = size.width; |
|
height = size.height; |
|
switch (display) { |
|
case over: |
|
if (column) { |
|
x = attr.x + (rotate ? groupBarWidth / 2 : (groupBarWidth - width) / 2); |
|
y = zero - (item.totalDim - item.totalNegDim) - height / 2 - labelMarginY; |
|
} else { |
|
x = zero + (item.totalDim - item.totalNegDim) + labelMarginX; |
|
y = attr.y + (rotate ? (groupBarWidth + height) / 2 : groupBarWidth / 2); |
|
}; |
|
break; |
|
case under: |
|
if (column) { |
|
x = attr.x + (rotate ? groupBarWidth / 2 : (groupBarWidth - width) / 2); |
|
y = zero + item.totalNegDim + height / 2; |
|
} else { |
|
x = zero - item.totalNegDim - width - labelMarginX; |
|
y = attr.y + (rotate ? (groupBarWidth + height) / 2 : groupBarWidth / 2); |
|
}; |
|
break; |
|
} |
|
} |
|
} |
|
if (x == undefined || y == undefined) { |
|
// bad configuration: x/y are not set |
|
label.hide(true); |
|
return; |
|
} |
|
label.isOutside = (display == outside); |
|
label.setAttributes({ |
|
text: text |
|
}); |
|
//set position |
|
finalAttr = { |
|
x: x, |
|
y: y |
|
}; |
|
// Rotate if we need to, but if not clear any previous rotation because we |
|
// are reusing the label |
|
finalAttr.rotate = rotate ? { |
|
x: x, |
|
y: y, |
|
degrees: 270 |
|
} : me.defaultRotate; |
|
//check for resizing |
|
if (animate && resizing) { |
|
if (column) { |
|
x = attr.x + attr.width / 2; |
|
y = zero; |
|
} else { |
|
x = zero; |
|
y = attr.y + attr.height / 2; |
|
} |
|
label.setAttributes({ |
|
x: x, |
|
y: y |
|
}, true); |
|
if (rotate) { |
|
label.setAttributes({ |
|
rotate: { |
|
x: x, |
|
y: y, |
|
degrees: 270 |
|
} |
|
}, true); |
|
} |
|
} |
|
//handle animation |
|
if (animate) { |
|
me.onAnimate(label, { |
|
zero: item.point[0], |
|
to: finalAttr |
|
}); |
|
} else { |
|
label.setAttributes(Ext.apply(finalAttr, { |
|
hidden: false |
|
}), true); |
|
} |
|
}, |
|
/* @private |
|
* Gets the dimensions of a given bar label. Uses a single hidden sprite to avoid |
|
* changing visible sprites. |
|
* @param value |
|
*/ |
|
getLabelSize: function(value, labelStyle) { |
|
var tester = this.testerLabel, |
|
config = this.label, |
|
endLabelStyle = Ext.apply({}, config, labelStyle, this.seriesLabelStyle || {}), |
|
rotated = config.orientation === 'vertical', |
|
bbox, w, h, undef; |
|
if (!tester) { |
|
tester = this.testerLabel = this.chart.surface.add(Ext.apply({ |
|
type: 'text', |
|
opacity: 0 |
|
}, endLabelStyle)); |
|
} |
|
tester.setAttributes({ |
|
style: labelStyle, |
|
text: value |
|
}, true); |
|
// Flip the width/height if rotated, as getBBox returns the pre-rotated dimensions |
|
bbox = tester.getBBox(); |
|
w = bbox.width; |
|
h = bbox.height; |
|
return { |
|
width: rotated ? h : w, |
|
height: rotated ? w : h |
|
}; |
|
}, |
|
// @private used to animate label, markers and other sprites. |
|
onAnimate: function(sprite, attr) { |
|
var me = this, |
|
to = attr.to, |
|
stacked = me.stacked, |
|
reverse = me.reverse, |
|
width = 0, |
|
isText, bbox, x, from; |
|
sprite.show(); |
|
if (!me.column) { |
|
if (reverse) { |
|
bbox = sprite.getBBox(); |
|
isText = sprite.type == 'text'; |
|
// If we're highlighting, we don't want to reset the position |
|
// so just grab the current position |
|
if (!me.inHighlight) { |
|
if (!stacked) { |
|
if (isText) { |
|
// Fudge factor, if the text has yet to be positioned it will be < 5 |
|
x = bbox.x >= 5 ? x : attr.zero; |
|
} else { |
|
if (bbox.width) { |
|
width = bbox.width; |
|
} |
|
x = bbox.width ? bbox.x : to.x + to.width; |
|
} |
|
} else { |
|
x = attr.zero; |
|
} |
|
} |
|
attr.from = { |
|
x: x, |
|
width: width |
|
}; |
|
} |
|
if (stacked) { |
|
from = attr.from; |
|
if (!from) { |
|
from = attr.from = {}; |
|
} |
|
from.y = to.y; |
|
if (!reverse) { |
|
from.x = attr.zero; |
|
if (sprite.isShadow) { |
|
from.width = 0; |
|
} |
|
} |
|
} |
|
} |
|
return this.callParent(arguments); |
|
}, |
|
isItemInPoint: function(x, y, item) { |
|
var bbox = item.sprite.getBBox(); |
|
return bbox.x <= x && bbox.y <= y && (bbox.x + bbox.width) >= x && (bbox.y + bbox.height) >= y; |
|
}, |
|
// @private hide all markers |
|
hideAll: function(index) { |
|
var axes = this.chart.axes, |
|
axesItems = axes.items, |
|
ln = axesItems.length, |
|
i = 0; |
|
index = (isNaN(this._index) ? index : this._index) || 0; |
|
if (!this.__excludes) { |
|
this.__excludes = []; |
|
} |
|
this.__excludes[index] = true; |
|
this.drawSeries(); |
|
for (i; i < ln; i++) { |
|
axesItems[i].drawAxis(); |
|
} |
|
}, |
|
// @private show all markers |
|
showAll: function(index) { |
|
var axes = this.chart.axes, |
|
axesItems = axes.items, |
|
ln = axesItems.length, |
|
i = 0; |
|
index = (isNaN(this._index) ? index : this._index) || 0; |
|
if (!this.__excludes) { |
|
this.__excludes = []; |
|
} |
|
this.__excludes[index] = false; |
|
this.drawSeries(); |
|
for (i; i < ln; i++) { |
|
axesItems[i].drawAxis(); |
|
} |
|
}, |
|
/** |
|
* Returns a string with the color to be used for the series legend item. |
|
* @param index |
|
*/ |
|
getLegendColor: function(index) { |
|
var me = this, |
|
colors = me.colorArrayStyle, |
|
colorLength = colors && colors.length; |
|
if (me.style && me.style.fill) { |
|
return me.style.fill; |
|
} else { |
|
return (colors ? colors[(me.themeIdx + index) % colorLength] : '#000'); |
|
} |
|
}, |
|
highlightItem: function(item) { |
|
this.callParent(arguments); |
|
this.inHighlight = true; |
|
this.renderLabels(); |
|
delete this.inHighlight; |
|
}, |
|
unHighlightItem: function() { |
|
this.callParent(arguments); |
|
this.inHighlight = true; |
|
this.renderLabels(); |
|
delete this.inHighlight; |
|
}, |
|
cleanHighlights: function() { |
|
this.callParent(arguments); |
|
this.inHighlight = true; |
|
this.renderLabels(); |
|
delete this.inHighlight; |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.series.Column |
|
* |
|
* Creates a Column Chart. Much of the methods are inherited from Bar. A Column Chart is a useful |
|
* visualization technique to display quantitative information for different categories that can |
|
* show some progression (or regression) in the data set. As with all other series, the Column Series |
|
* must be appended in the *series* Chart array configuration. See the Chart documentation for more |
|
* information. A typical configuration object for the column series could be: |
|
* |
|
* @example |
|
* var store = Ext.create('Ext.data.JsonStore', { |
|
* fields: ['name', 'data'], |
|
* data: [ |
|
* { 'name': 'metric one', 'data':10 }, |
|
* { 'name': 'metric two', 'data': 7 }, |
|
* { 'name': 'metric three', 'data': 5 }, |
|
* { 'name': 'metric four', 'data': 2 }, |
|
* { 'name': 'metric five', 'data':27 } |
|
* ] |
|
* }); |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* width: 500, |
|
* height: 300, |
|
* animate: true, |
|
* store: store, |
|
* axes: [ |
|
* { |
|
* type: 'Numeric', |
|
* position: 'left', |
|
* fields: ['data'], |
|
* label: { |
|
* renderer: Ext.util.Format.numberRenderer('0,0') |
|
* }, |
|
* title: 'Sample Values', |
|
* grid: true, |
|
* minimum: 0 |
|
* }, |
|
* { |
|
* type: 'Category', |
|
* position: 'bottom', |
|
* fields: ['name'], |
|
* title: 'Sample Metrics' |
|
* } |
|
* ], |
|
* series: [ |
|
* { |
|
* type: 'column', |
|
* axis: 'left', |
|
* highlight: true, |
|
* tips: { |
|
* trackMouse: true, |
|
* width: 140, |
|
* height: 28, |
|
* renderer: function(storeItem, item) { |
|
* this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data') + ' $'); |
|
* } |
|
* }, |
|
* label: { |
|
* display: 'insideEnd', |
|
* 'text-anchor': 'middle', |
|
* field: 'data', |
|
* renderer: Ext.util.Format.numberRenderer('0'), |
|
* orientation: 'vertical', |
|
* color: '#333' |
|
* }, |
|
* xField: 'name', |
|
* yField: 'data' |
|
* } |
|
* ] |
|
* }); |
|
* |
|
* In this configuration we set `column` as the series type, bind the values of the bars to the bottom axis, |
|
* set `highlight` to true so that bars are smoothly highlighted when hovered and bind the `xField` or category |
|
* field to the data store `name` property and the `yField` as the data1 property of a store element. |
|
*/ |
|
Ext.define('Ext.chart.series.Column', { |
|
/* Begin Definitions */ |
|
alternateClassName: [ |
|
'Ext.chart.ColumnSeries', |
|
'Ext.chart.ColumnChart', |
|
'Ext.chart.StackedColumnChart' |
|
], |
|
extend: 'Ext.chart.series.Bar', |
|
/* End Definitions */ |
|
type: 'column', |
|
alias: 'series.column', |
|
column: true, |
|
// private: true if the columns are bound to a numerical x-axis; otherwise they are evenly distributed along the axis |
|
boundColumn: false, |
|
/** |
|
* @cfg {String} axis |
|
* The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'. |
|
* You must explicitly set this value to bind the values of the column series to the ones in the axis, otherwise a |
|
* relative scale will be used. |
|
*/ |
|
/** |
|
* @cfg {Number/Object} xPadding Padding between the left/right axes and the bars. |
|
* The possible values are a number (the number of pixels for both left and right padding) |
|
* or an object with `{ left, right }` properties. |
|
*/ |
|
xPadding: 10, |
|
/** |
|
* @cfg {Number/Object} yPadding Padding between the top/bottom axes and the bars. |
|
* The possible values are a number (the number of pixels for both top and bottom padding) |
|
* or an object with `{ top, bottom }` properties. |
|
*/ |
|
yPadding: 0 |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.series.Gauge |
|
* |
|
* Creates a Gauge Chart. Gauge Charts are used to show progress in a certain variable. There are two ways of using the Gauge chart. |
|
* One is setting a store element into the Gauge and selecting the field to be used from that store. Another one is instantiating the |
|
* visualization and using the `setValue` method to adjust the value you want. |
|
* |
|
* An example of Gauge visualization: |
|
* |
|
* @example |
|
* var store = Ext.create('Ext.data.JsonStore', { |
|
* fields: ['value'], |
|
* data: [ |
|
* { 'value':80 } |
|
* ] |
|
* }); |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* store: store, |
|
* width: 400, |
|
* height: 250, |
|
* animate: true, |
|
* insetPadding: 30, |
|
* axes: [{ |
|
* type: 'gauge', |
|
* position: 'gauge', |
|
* minimum: 0, |
|
* maximum: 100, |
|
* steps: 10, |
|
* margin: 10 |
|
* }], |
|
* series: [{ |
|
* type: 'gauge', |
|
* field: 'value', |
|
* donut: 30, |
|
* colorSet: ['#F49D10', '#ddd'] |
|
* }] |
|
* }); |
|
* |
|
* Ext.widget("button", { |
|
* renderTo: Ext.getBody(), |
|
* text: "Refresh", |
|
* handler: function() { |
|
* store.getAt(0).set('value', Math.round(Math.random()*100)); |
|
* } |
|
* }); |
|
* |
|
* In this example we create a special Gauge axis to be used with the gauge visualization (describing half-circle markers), and also we're |
|
* setting a maximum, minimum and steps configuration options into the axis. The Gauge series configuration contains the store field to be bound to |
|
* the visual display and the color set to be used with the visualization. |
|
*/ |
|
Ext.define('Ext.chart.series.Gauge', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.chart.series.Series', |
|
/* End Definitions */ |
|
type: "gauge", |
|
alias: 'series.gauge', |
|
rad: Math.PI / 180, |
|
/** |
|
* @cfg {Array} colorSet An array to override the theme's colors array to show the sections in the Gauge in specified colors. |
|
* The order of this array will be used in the order of sections so the first value will always be used for the first section. |
|
*/ |
|
/** |
|
* @cfg {Number} highlightDuration |
|
* The duration for the pie slice highlight effect. |
|
*/ |
|
highlightDuration: 150, |
|
/** |
|
* @cfg {String} angleField (required) |
|
* The store record field name to be used for the pie angles. |
|
* The values bound to this field name must be positive real numbers. |
|
*/ |
|
angleField: false, |
|
/** |
|
* @cfg {Boolean} needle |
|
* Use the Gauge Series as an area series or add a needle to it. Default's false. |
|
*/ |
|
needle: false, |
|
/** |
|
* @cfg {Boolean/Number} donut |
|
* Use the entire disk or just a fraction of it for the gauge. Default's false. |
|
*/ |
|
donut: false, |
|
/** |
|
* @cfg {Boolean} showInLegend |
|
* Whether to add the pie chart elements as legend items. Default's false. |
|
*/ |
|
showInLegend: false, |
|
/** |
|
* @cfg {Object} style |
|
* An object containing styles for overriding series styles from Theming. |
|
*/ |
|
style: {}, |
|
constructor: function(config) { |
|
this.callParent(arguments); |
|
var me = this, |
|
chart = me.chart, |
|
surface = chart.surface, |
|
store = chart.store, |
|
shadow = chart.shadow, |
|
i, l, cfg; |
|
Ext.apply(me, config, { |
|
shadowAttributes: [ |
|
{ |
|
"stroke-width": 6, |
|
"stroke-opacity": 1, |
|
stroke: 'rgb(200, 200, 200)', |
|
translate: { |
|
x: 1.2, |
|
y: 2 |
|
} |
|
}, |
|
{ |
|
"stroke-width": 4, |
|
"stroke-opacity": 1, |
|
stroke: 'rgb(150, 150, 150)', |
|
translate: { |
|
x: 0.9, |
|
y: 1.5 |
|
} |
|
}, |
|
{ |
|
"stroke-width": 2, |
|
"stroke-opacity": 1, |
|
stroke: 'rgb(100, 100, 100)', |
|
translate: { |
|
x: 0.6, |
|
y: 1 |
|
} |
|
} |
|
] |
|
}); |
|
me.group = surface.getGroup(me.seriesId); |
|
if (shadow) { |
|
for (i = 0 , l = me.shadowAttributes.length; i < l; i++) { |
|
me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i)); |
|
} |
|
} |
|
surface.customAttributes.segment = function(opt) { |
|
var series = opt.series || me; |
|
delete opt.series; |
|
return me.getSegment.call(series, opt); |
|
}; |
|
}, |
|
// @private updates some onbefore render parameters. |
|
initialize: function() { |
|
var me = this, |
|
store = me.chart.getChartStore(), |
|
data = store.data.items, |
|
label = me.label, |
|
ln = data.length; |
|
me.yField = []; |
|
if (label && label.field && ln > 0) { |
|
me.yField.push(data[0].get(label.field)); |
|
} |
|
}, |
|
// @private returns an object with properties for a Slice |
|
getSegment: function(opt) { |
|
var me = this, |
|
rad = me.rad, |
|
cos = Math.cos, |
|
sin = Math.sin, |
|
abs = Math.abs, |
|
x = me.centerX, |
|
y = me.centerY, |
|
x1 = 0, |
|
x2 = 0, |
|
x3 = 0, |
|
x4 = 0, |
|
y1 = 0, |
|
y2 = 0, |
|
y3 = 0, |
|
y4 = 0, |
|
delta = 0.01, |
|
r = opt.endRho - opt.startRho, |
|
startAngle = opt.startAngle, |
|
endAngle = opt.endAngle, |
|
midAngle = (startAngle + endAngle) / 2 * rad, |
|
margin = opt.margin || 0, |
|
flag = abs(endAngle - startAngle) > 180, |
|
a1 = Math.min(startAngle, endAngle) * rad, |
|
a2 = Math.max(startAngle, endAngle) * rad, |
|
singleSlice = false; |
|
x += margin * cos(midAngle); |
|
y += margin * sin(midAngle); |
|
x1 = x + opt.startRho * cos(a1); |
|
y1 = y + opt.startRho * sin(a1); |
|
x2 = x + opt.endRho * cos(a1); |
|
y2 = y + opt.endRho * sin(a1); |
|
x3 = x + opt.startRho * cos(a2); |
|
y3 = y + opt.startRho * sin(a2); |
|
x4 = x + opt.endRho * cos(a2); |
|
y4 = y + opt.endRho * sin(a2); |
|
if (abs(x1 - x3) <= delta && abs(y1 - y3) <= delta) { |
|
singleSlice = true; |
|
} |
|
//Solves mysterious clipping bug with IE |
|
if (singleSlice) { |
|
return { |
|
path: [ |
|
[ |
|
"M", |
|
x1, |
|
y1 |
|
], |
|
[ |
|
"L", |
|
x2, |
|
y2 |
|
], |
|
[ |
|
"A", |
|
opt.endRho, |
|
opt.endRho, |
|
0, |
|
+flag, |
|
1, |
|
x4, |
|
y4 |
|
], |
|
[ |
|
"Z" |
|
] |
|
] |
|
}; |
|
} else { |
|
return { |
|
path: [ |
|
[ |
|
"M", |
|
x1, |
|
y1 |
|
], |
|
[ |
|
"L", |
|
x2, |
|
y2 |
|
], |
|
[ |
|
"A", |
|
opt.endRho, |
|
opt.endRho, |
|
0, |
|
+flag, |
|
1, |
|
x4, |
|
y4 |
|
], |
|
[ |
|
"L", |
|
x3, |
|
y3 |
|
], |
|
[ |
|
"A", |
|
opt.startRho, |
|
opt.startRho, |
|
0, |
|
+flag, |
|
0, |
|
x1, |
|
y1 |
|
], |
|
[ |
|
"Z" |
|
] |
|
] |
|
}; |
|
} |
|
}, |
|
// @private utility function to calculate the middle point of a pie slice. |
|
calcMiddle: function(item) { |
|
var me = this, |
|
rad = me.rad, |
|
slice = item.slice, |
|
x = me.centerX, |
|
y = me.centerY, |
|
startAngle = slice.startAngle, |
|
endAngle = slice.endAngle, |
|
radius = Math.max(('rho' in slice) ? slice.rho : me.radius, me.label.minMargin), |
|
donut = +me.donut, |
|
a1 = Math.min(startAngle, endAngle) * rad, |
|
a2 = Math.max(startAngle, endAngle) * rad, |
|
midAngle = -(a1 + (a2 - a1) / 2), |
|
xm = x + (item.endRho + item.startRho) / 2 * Math.cos(midAngle), |
|
ym = y - (item.endRho + item.startRho) / 2 * Math.sin(midAngle); |
|
item.middle = { |
|
x: xm, |
|
y: ym |
|
}; |
|
}, |
|
/** |
|
* Draws the series for the current chart. |
|
*/ |
|
drawSeries: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
group = me.group, |
|
animate = me.chart.animate, |
|
axis = me.chart.axes.get(0), |
|
minimum = axis && axis.minimum || me.minimum || 0, |
|
maximum = axis && axis.maximum || me.maximum || 0, |
|
field = me.angleField || me.field || me.xField, |
|
surface = chart.surface, |
|
chartBBox = chart.chartBBox, |
|
rad = me.rad, |
|
donut = +me.donut, |
|
values = {}, |
|
items = [], |
|
seriesStyle = me.seriesStyle, |
|
seriesLabelStyle = me.seriesLabelStyle, |
|
colorArrayStyle = me.colorArrayStyle, |
|
colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, |
|
cos = Math.cos, |
|
sin = Math.sin, |
|
defaultStart = -180, |
|
reverse = me.reverse, |
|
rendererAttributes, centerX, centerY, slice, slices, sprite, value, item, ln, record, i, j, startAngle, endAngle, middleAngle, sliceLength, path, p, spriteOptions, bbox, splitAngle, sliceA, sliceB; |
|
Ext.apply(seriesStyle, me.style || {}); |
|
me.setBBox(); |
|
bbox = me.bbox; |
|
//override theme colors |
|
if (me.colorSet) { |
|
colorArrayStyle = me.colorSet; |
|
colorArrayLength = colorArrayStyle.length; |
|
} |
|
//if not store or store is empty then there's nothing to draw |
|
if (!store || !store.getCount() || me.seriesIsHidden) { |
|
me.hide(); |
|
me.items = []; |
|
return; |
|
} |
|
centerX = me.centerX = chartBBox.x + (chartBBox.width / 2); |
|
centerY = me.centerY = chartBBox.y + chartBBox.height; |
|
me.radius = Math.min(centerX - chartBBox.x, centerY - chartBBox.y); |
|
me.slices = slices = []; |
|
me.items = items = []; |
|
if (!me.value) { |
|
record = store.getAt(0); |
|
me.value = record.get(field); |
|
} |
|
value = reverse ? maximum - me.value : me.value; |
|
if (me.needle) { |
|
sliceA = { |
|
series: me, |
|
value: value, |
|
startAngle: defaultStart, |
|
endAngle: 0, |
|
rho: me.radius |
|
}; |
|
splitAngle = defaultStart * (1 - (value - minimum) / (maximum - minimum)); |
|
slices.push(sliceA); |
|
} else { |
|
splitAngle = defaultStart * (1 - (value - minimum) / (maximum - minimum)); |
|
sliceA = { |
|
series: me, |
|
value: value, |
|
startAngle: defaultStart, |
|
endAngle: splitAngle, |
|
rho: me.radius |
|
}; |
|
sliceB = { |
|
series: me, |
|
value: maximum - value, |
|
startAngle: splitAngle, |
|
endAngle: 0, |
|
rho: me.radius |
|
}; |
|
if (reverse) { |
|
slices.push(sliceB, sliceA); |
|
} else { |
|
slices.push(sliceA, sliceB); |
|
} |
|
} |
|
//do pie slices after. |
|
for (i = 0 , ln = slices.length; i < ln; i++) { |
|
slice = slices[i]; |
|
sprite = group.getAt(i); |
|
//set pie slice properties |
|
rendererAttributes = Ext.apply({ |
|
segment: { |
|
// This is to pass the series scope to the custom attribute |
|
// processing closure created in the constructor |
|
series: me, |
|
startAngle: slice.startAngle, |
|
endAngle: slice.endAngle, |
|
margin: 0, |
|
rho: slice.rho, |
|
startRho: slice.rho * +donut / 100, |
|
endRho: slice.rho |
|
} |
|
}, Ext.apply(seriesStyle, colorArrayStyle && { |
|
fill: colorArrayStyle[i % colorArrayLength] |
|
} || {})); |
|
item = Ext.apply({}, rendererAttributes.segment, { |
|
slice: slice, |
|
series: me, |
|
storeItem: record, |
|
index: i |
|
}); |
|
items[i] = item; |
|
// Create a new sprite if needed (no height) |
|
if (!sprite) { |
|
spriteOptions = Ext.apply({ |
|
type: "path", |
|
group: group |
|
}, Ext.apply(seriesStyle, colorArrayStyle && { |
|
fill: colorArrayStyle[i % colorArrayLength] |
|
} || {})); |
|
sprite = surface.add(Ext.apply(spriteOptions, rendererAttributes)); |
|
} |
|
slice.sprite = slice.sprite || []; |
|
item.sprite = sprite; |
|
slice.sprite.push(sprite); |
|
if (animate) { |
|
rendererAttributes = me.renderer(sprite, record, rendererAttributes, i, store); |
|
sprite._to = rendererAttributes; |
|
me.onAnimate(sprite, { |
|
to: rendererAttributes |
|
}); |
|
} else { |
|
rendererAttributes = me.renderer(sprite, record, Ext.apply(rendererAttributes, { |
|
hidden: false |
|
}), i, store); |
|
sprite.setAttributes(rendererAttributes, true); |
|
} |
|
} |
|
if (me.needle) { |
|
splitAngle = splitAngle * Math.PI / 180; |
|
if (!me.needleSprite) { |
|
me.needleSprite = me.chart.surface.add({ |
|
type: 'path', |
|
path: [ |
|
'M', |
|
centerX + (me.radius * +donut / 100) * cos(splitAngle), |
|
centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)), |
|
'L', |
|
centerX + me.radius * cos(splitAngle), |
|
centerY + -Math.abs(me.radius * sin(splitAngle)) |
|
], |
|
'stroke-width': 4, |
|
'stroke': '#222' |
|
}); |
|
} else { |
|
if (animate) { |
|
me.onAnimate(me.needleSprite, { |
|
to: { |
|
path: [ |
|
'M', |
|
centerX + (me.radius * +donut / 100) * cos(splitAngle), |
|
centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)), |
|
'L', |
|
centerX + me.radius * cos(splitAngle), |
|
centerY + -Math.abs(me.radius * sin(splitAngle)) |
|
] |
|
} |
|
}); |
|
} else { |
|
me.needleSprite.setAttributes({ |
|
type: 'path', |
|
path: [ |
|
'M', |
|
centerX + (me.radius * +donut / 100) * cos(splitAngle), |
|
centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)), |
|
'L', |
|
centerX + me.radius * cos(splitAngle), |
|
centerY + -Math.abs(me.radius * sin(splitAngle)) |
|
] |
|
}); |
|
} |
|
} |
|
me.needleSprite.setAttributes({ |
|
hidden: false |
|
}, true); |
|
} |
|
delete me.value; |
|
}, |
|
/** |
|
* Sets the Gauge chart to the current specified value. |
|
*/ |
|
setValue: function(value) { |
|
this.value = value; |
|
this.drawSeries(); |
|
}, |
|
// @private callback for when creating a label sprite. |
|
onCreateLabel: function(storeItem, item, i, display) {}, |
|
// @private callback for when placing a label sprite. |
|
onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {}, |
|
// @private callback for when placing a callout. |
|
onPlaceCallout: function() {}, |
|
// @private handles sprite animation for the series. |
|
onAnimate: function(sprite, attr) { |
|
sprite.show(); |
|
return this.callParent(arguments); |
|
}, |
|
isItemInPoint: function(x, y, item, i) { |
|
var me = this, |
|
cx = me.centerX, |
|
cy = me.centerY, |
|
abs = Math.abs, |
|
dx = abs(x - cx), |
|
dy = abs(y - cy), |
|
startAngle = item.startAngle, |
|
endAngle = item.endAngle, |
|
rho = Math.sqrt(dx * dx + dy * dy), |
|
angle = Math.atan2(y - cy, x - cx) / me.rad; |
|
//Only trigger events for the filled portion of the Gauge. |
|
return (i === 0) && (angle >= startAngle && angle < endAngle && rho >= item.startRho && rho <= item.endRho); |
|
}, |
|
/** |
|
* Returns the color of the series (to be displayed as color for the series legend item). |
|
* @param item {Object} Info about the item; same format as returned by #getItemForPoint |
|
*/ |
|
getLegendColor: function(index) { |
|
var colors = this.colorSet || this.colorArrayStyle; |
|
return colors[index % colors.length]; |
|
} |
|
}); |
|
|
|
Ext.define('Ext.rtl.chart.series.Gauge', { |
|
override: 'Ext.chart.series.Gauge', |
|
initialize: function() { |
|
var me = this; |
|
me.callParent(arguments); |
|
if (me.chart.getInherited().rtl) { |
|
me.reverse = true; |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.series.Line |
|
* @extends Ext.chart.series.Cartesian |
|
* |
|
* Creates a Line Chart. A Line Chart is a useful visualization technique to display quantitative information for different |
|
* categories or other real values (as opposed to the bar chart), that can show some progression (or regression) in the dataset. |
|
* As with all other series, the Line Series must be appended in the *series* Chart array configuration. See the Chart |
|
* documentation for more information. A typical configuration object for the line series could be: |
|
* |
|
* @example |
|
* var store = Ext.create('Ext.data.JsonStore', { |
|
* 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': 4, 'data2': 4, 'data3': 36, 'data4': 13, 'data5': 33 } |
|
* ] |
|
* }); |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* width: 500, |
|
* height: 300, |
|
* animate: true, |
|
* store: store, |
|
* axes: [ |
|
* { |
|
* type: 'Numeric', |
|
* position: 'left', |
|
* fields: ['data1', 'data2'], |
|
* label: { |
|
* renderer: Ext.util.Format.numberRenderer('0,0') |
|
* }, |
|
* title: 'Sample Values', |
|
* grid: true, |
|
* minimum: 0 |
|
* }, |
|
* { |
|
* type: 'Category', |
|
* position: 'bottom', |
|
* fields: ['name'], |
|
* title: 'Sample Metrics' |
|
* } |
|
* ], |
|
* series: [ |
|
* { |
|
* type: 'line', |
|
* highlight: { |
|
* size: 7, |
|
* radius: 7 |
|
* }, |
|
* axis: 'left', |
|
* xField: 'name', |
|
* yField: 'data1', |
|
* markerConfig: { |
|
* type: 'cross', |
|
* size: 4, |
|
* radius: 4, |
|
* 'stroke-width': 0 |
|
* } |
|
* }, |
|
* { |
|
* type: 'line', |
|
* highlight: { |
|
* size: 7, |
|
* radius: 7 |
|
* }, |
|
* axis: 'left', |
|
* fill: true, |
|
* xField: 'name', |
|
* yField: 'data2', |
|
* markerConfig: { |
|
* type: 'circle', |
|
* size: 4, |
|
* radius: 4, |
|
* 'stroke-width': 0 |
|
* } |
|
* } |
|
* ] |
|
* }); |
|
* |
|
* In this configuration we're adding two series (or lines), one bound to the `data1` |
|
* property of the store and the other to `data3`. The type for both configurations is |
|
* `line`. The `xField` for both series is the same, the name propert of the store. |
|
* Both line series share the same axis, the left axis. You can set particular marker |
|
* configuration by adding properties onto the markerConfig object. Both series have |
|
* an object as highlight so that markers animate smoothly to the properties in highlight |
|
* when hovered. The second series has `fill=true` which means that the line will also |
|
* have an area below it of the same color. |
|
* |
|
* In some uses, a line will not be continuous and may have gaps. In order to accomplish this, |
|
* the data must return `false` and the series will not be continues for this data point. |
|
* |
|
* @example |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* height: 300, |
|
* width: 500, |
|
* axes: [{ |
|
* position: 'bottom', |
|
* title: 'X', |
|
* fields: ['x'], |
|
* type: 'Numeric' |
|
* }, { |
|
* position: 'left', |
|
* title: 'Y', |
|
* fields: ['y'], |
|
* type: 'Numeric' |
|
* }], |
|
* series: [{ |
|
* xField: 'x', |
|
* yField: 'y', |
|
* type: 'line' |
|
* }], |
|
* store: { |
|
* fields: [ |
|
* 'x', 'y' |
|
* ], |
|
* data: [ |
|
* { x: 0, y: 0 }, |
|
* { x: 25, y: 25 }, |
|
* { x: 50, y: false }, |
|
* { x: 75, y: 75 }, |
|
* { x: 100, y: 100 } |
|
* ] |
|
* } |
|
* }); |
|
* |
|
* The third data point has a `y` value of `false` which will make the line not be drawn for this |
|
* data point causing the line to be split into two different lines. |
|
* |
|
* **Note:** In the series definition remember to explicitly set the axis to bind the |
|
* values of the line series to. This can be done by using the `axis` configuration property. |
|
*/ |
|
Ext.define('Ext.chart.series.Line', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.chart.series.Cartesian', |
|
alternateClassName: [ |
|
'Ext.chart.LineSeries', |
|
'Ext.chart.LineChart' |
|
], |
|
requires: [ |
|
'Ext.chart.axis.Axis', |
|
'Ext.chart.Shape', |
|
'Ext.draw.Draw', |
|
'Ext.fx.Anim' |
|
], |
|
/* End Definitions */ |
|
type: 'line', |
|
alias: 'series.line', |
|
/** |
|
* @cfg {Number} selectionTolerance |
|
* The offset distance from the cursor position to the line series to trigger events (then used for highlighting series, etc). |
|
*/ |
|
selectionTolerance: 20, |
|
/** |
|
* @cfg {Boolean} showMarkers |
|
* Whether markers should be displayed at the data points along the line. If true, |
|
* then the {@link #markerConfig} config item will determine the markers' styling. |
|
*/ |
|
showMarkers: true, |
|
/** |
|
* @cfg {Object} markerConfig |
|
* The display style for the markers. Only used if {@link #showMarkers} is true. |
|
* The markerConfig is a configuration object containing the same set of properties defined in |
|
* the Sprite class. For example, if we were to set red circles as markers to the line series we could |
|
* pass the object: |
|
* |
|
<pre><code> |
|
markerConfig: { |
|
type: 'circle', |
|
radius: 4, |
|
'fill': '#f00' |
|
} |
|
</code></pre> |
|
|
|
*/ |
|
markerConfig: {}, |
|
/** |
|
* @cfg {Object} style |
|
* An object containing style properties for the visualization lines and fill. |
|
* These styles will override the theme styles. The following are valid style properties: |
|
* |
|
* - `stroke` - an rgb or hex color string for the background color of the line |
|
* - `stroke-width` - the width of the stroke (integer) |
|
* - `fill` - the background fill color string (hex or rgb), only works if {@link #fill} is `true` |
|
* - `opacity` - the opacity of the line and the fill color (decimal) |
|
* |
|
* Example usage: |
|
* |
|
* style: { |
|
* stroke: '#00ff00', |
|
* 'stroke-width': 10, |
|
* fill: '#80A080', |
|
* opacity: 0.2 |
|
* } |
|
*/ |
|
style: {}, |
|
/** |
|
* @cfg {Boolean/Number} smooth |
|
* If set to `true` or a non-zero number, the line will be smoothed/rounded around its points; otherwise |
|
* straight line segments will be drawn. |
|
* |
|
* A numeric value is interpreted as a divisor of the horizontal distance between consecutive points in |
|
* the line; larger numbers result in sharper curves while smaller numbers result in smoother curves. |
|
* |
|
* If set to `true` then a default numeric value of 3 will be used. Defaults to `false`. |
|
*/ |
|
smooth: false, |
|
/** |
|
* @private Default numeric smoothing value to be used when {@link #smooth} = true. |
|
*/ |
|
defaultSmoothness: 3, |
|
/** |
|
* @cfg {Boolean} fill |
|
* If true, the area below the line will be filled in using the {@link #style eefill} and |
|
* {@link #style opacity} config properties. Defaults to false. |
|
*/ |
|
fill: false, |
|
constructor: function(config) { |
|
this.callParent(arguments); |
|
var me = this, |
|
surface = me.chart.surface, |
|
shadow = me.chart.shadow, |
|
i, l; |
|
config.highlightCfg = Ext.Object.merge({ |
|
'stroke-width': 3 |
|
}, config.highlightCfg); |
|
Ext.apply(me, config, { |
|
shadowAttributes: [ |
|
{ |
|
"stroke-width": 6, |
|
"stroke-opacity": 0.05, |
|
stroke: 'rgb(0, 0, 0)', |
|
translate: { |
|
x: 1, |
|
y: 1 |
|
} |
|
}, |
|
{ |
|
"stroke-width": 4, |
|
"stroke-opacity": 0.1, |
|
stroke: 'rgb(0, 0, 0)', |
|
translate: { |
|
x: 1, |
|
y: 1 |
|
} |
|
}, |
|
{ |
|
"stroke-width": 2, |
|
"stroke-opacity": 0.15, |
|
stroke: 'rgb(0, 0, 0)', |
|
translate: { |
|
x: 1, |
|
y: 1 |
|
} |
|
} |
|
] |
|
}); |
|
me.group = surface.getGroup(me.seriesId); |
|
if (me.showMarkers) { |
|
me.markerGroup = surface.getGroup(me.seriesId + '-markers'); |
|
} |
|
if (shadow) { |
|
for (i = 0 , l = me.shadowAttributes.length; i < l; i++) { |
|
me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i)); |
|
} |
|
} |
|
}, |
|
// @private makes an average of points when there are more data points than pixels to be rendered. |
|
shrink: function(xValues, yValues, size) { |
|
// Start at the 2nd point... |
|
var len = xValues.length, |
|
ratio = Math.floor(len / size), |
|
i = 1, |
|
xSum = 0, |
|
ySum = 0, |
|
xRes = [ |
|
+xValues[0] |
|
], |
|
yRes = [ |
|
+yValues[0] |
|
]; |
|
for (; i < len; ++i) { |
|
xSum += +xValues[i] || 0; |
|
ySum += +yValues[i] || 0; |
|
if (i % ratio == 0) { |
|
xRes.push(xSum / ratio); |
|
yRes.push(ySum / ratio); |
|
xSum = 0; |
|
ySum = 0; |
|
} |
|
} |
|
return { |
|
x: xRes, |
|
y: yRes |
|
}; |
|
}, |
|
/** |
|
* Draws the series for the current chart. |
|
*/ |
|
drawSeries: function() { |
|
var me = this, |
|
chart = me.chart, |
|
chartAxes = chart.axes, |
|
store = chart.getChartStore(), |
|
data = store.data.items, |
|
record, |
|
storeCount = store.getCount(), |
|
surface = me.chart.surface, |
|
bbox = {}, |
|
group = me.group, |
|
showMarkers = me.showMarkers, |
|
markerGroup = me.markerGroup, |
|
enableShadows = chart.shadow, |
|
shadowGroups = me.shadowGroups, |
|
shadowAttributes = me.shadowAttributes, |
|
smooth = me.smooth, |
|
lnsh = shadowGroups.length, |
|
dummyPath = [ |
|
"M" |
|
], |
|
path = [ |
|
"M" |
|
], |
|
renderPath = [ |
|
"M" |
|
], |
|
smoothPath = [ |
|
"M" |
|
], |
|
markerIndex = chart.markerIndex, |
|
axes = [].concat(me.axis), |
|
shadowBarAttr, |
|
xValues = [], |
|
yValues = [], |
|
onbreak = false, |
|
reverse = me.reverse, |
|
storeIndices = [], |
|
markerStyle = Ext.apply({}, me.markerStyle), |
|
seriesStyle = me.seriesStyle, |
|
colorArrayStyle = me.colorArrayStyle, |
|
colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, |
|
isNumber = Ext.isNumber, |
|
seriesIdx = me.seriesIdx, |
|
boundAxes = me.getAxesForXAndYFields(), |
|
boundXAxis = boundAxes.xAxis, |
|
boundYAxis = boundAxes.yAxis, |
|
xAxis = chartAxes && chartAxes.get(boundXAxis), |
|
yAxis = chartAxes && chartAxes.get(boundYAxis), |
|
xAxisType = boundXAxis ? xAxis && xAxis.type : '', |
|
yAxisType = boundYAxis ? yAxis && yAxis.type : '', |
|
shadows, shadow, shindex, fromPath, fill, fillPath, rendererAttributes, x, y, prevX, prevY, firstX, firstY, markerCount, i, j, ln, axis, ends, marker, markerAux, item, xValue, yValue, coords, xScale, yScale, minX, maxX, minY, maxY, line, animation, endMarkerStyle, endLineStyle, type, count, opacity, lineOpacity, fillOpacity, fillDefaultValue; |
|
if (me.fireEvent('beforedraw', me) === false) { |
|
return; |
|
} |
|
//if store is empty or the series is excluded in the legend then there's nothing to draw. |
|
if (!storeCount || me.seriesIsHidden) { |
|
me.hide(); |
|
me.items = []; |
|
if (me.line) { |
|
me.line.hide(true); |
|
if (me.line.shadows) { |
|
shadows = me.line.shadows; |
|
for (j = 0 , lnsh = shadows.length; j < lnsh; j++) { |
|
shadow = shadows[j]; |
|
shadow.hide(true); |
|
} |
|
} |
|
if (me.fillPath) { |
|
me.fillPath.hide(true); |
|
} |
|
} |
|
me.line = null; |
|
me.fillPath = null; |
|
return; |
|
} |
|
//prepare style objects for line and markers |
|
endMarkerStyle = Ext.apply(markerStyle || {}, me.markerConfig, { |
|
fill: me.seriesStyle.fill || colorArrayStyle[me.themeIdx % colorArrayStyle.length] |
|
}); |
|
type = endMarkerStyle.type; |
|
delete endMarkerStyle.type; |
|
endLineStyle = seriesStyle; |
|
//if no stroke with is specified force it to 0.5 because this is |
|
//about making *lines* |
|
if (!endLineStyle['stroke-width']) { |
|
endLineStyle['stroke-width'] = 0.5; |
|
} |
|
//set opacity values |
|
opacity = 'opacity' in endLineStyle ? endLineStyle.opacity : 1; |
|
fillDefaultValue = 'opacity' in endLineStyle ? endLineStyle.opacity : 0.3; |
|
lineOpacity = 'lineOpacity' in endLineStyle ? endLineStyle.lineOpacity : opacity; |
|
fillOpacity = 'fillOpacity' in endLineStyle ? endLineStyle.fillOpacity : fillDefaultValue; |
|
//If we're using a time axis and we need to translate the points, |
|
//then reuse the first markers as the last markers. |
|
if (markerIndex && markerGroup && markerGroup.getCount()) { |
|
for (i = 0; i < markerIndex; i++) { |
|
marker = markerGroup.getAt(i); |
|
markerGroup.remove(marker); |
|
markerGroup.add(marker); |
|
markerAux = markerGroup.getAt(markerGroup.getCount() - 2); |
|
marker.setAttributes({ |
|
x: 0, |
|
y: 0, |
|
translate: { |
|
x: markerAux.attr.translation.x, |
|
y: markerAux.attr.translation.y |
|
} |
|
}, true); |
|
} |
|
} |
|
me.unHighlightItem(); |
|
me.cleanHighlights(); |
|
me.setBBox(); |
|
bbox = me.bbox; |
|
me.clipRect = [ |
|
bbox.x, |
|
bbox.y, |
|
bbox.width, |
|
bbox.height |
|
]; |
|
if (xAxis) { |
|
ends = xAxis.applyData(); |
|
minX = ends.from; |
|
maxX = ends.to; |
|
} |
|
if (yAxis) { |
|
ends = yAxis.applyData(); |
|
minY = ends.from; |
|
maxY = ends.to; |
|
} |
|
// If a field was specified without a corresponding axis, create one to get bounds |
|
if (me.xField && !Ext.isNumber(minX)) { |
|
axis = me.getMinMaxXValues(); |
|
minX = axis[0]; |
|
maxX = axis[1]; |
|
} |
|
if (me.yField && !Ext.isNumber(minY)) { |
|
axis = me.getMinMaxYValues(); |
|
minY = axis[0]; |
|
maxY = axis[1]; |
|
} |
|
if (isNaN(minX)) { |
|
minX = 0; |
|
xScale = bbox.width / ((storeCount - 1) || 1); |
|
} else { |
|
xScale = bbox.width / ((maxX - minX) || (storeCount - 1) || 1); |
|
} |
|
if (isNaN(minY)) { |
|
minY = 0; |
|
yScale = bbox.height / ((storeCount - 1) || 1); |
|
} else { |
|
yScale = bbox.height / ((maxY - minY) || (storeCount - 1) || 1); |
|
} |
|
// Extract all x and y values from the store |
|
for (i = 0 , ln = data.length; i < ln; i++) { |
|
record = data[i]; |
|
xValue = record.get(me.xField); |
|
if (xAxisType === 'Time' && typeof xValue === "string") { |
|
xValue = Date.parse(xValue); |
|
} |
|
// Ensure a value |
|
if (typeof xValue === 'string' || typeof xValue === 'object' && !Ext.isDate(xValue) || //set as uniform distribution if the axis is a category axis. |
|
xAxisType === 'Category') { |
|
xValue = i; |
|
} |
|
// Filter out values that don't fit within the pan/zoom buffer area |
|
yValue = record.get(me.yField); |
|
if (yAxisType === 'Time' && typeof yValue === "string") { |
|
yValue = Date.parse(yValue); |
|
} |
|
//skip undefined values |
|
if (typeof yValue === 'undefined' || (typeof yValue === 'string' && !yValue)) { |
|
if (Ext.isDefined(Ext.global.console)) { |
|
Ext.global.console.warn("[Ext.chart.series.Line] Skipping a store element with an undefined value at ", record, xValue, yValue); |
|
} |
|
|
|
continue; |
|
} |
|
// Ensure a value |
|
if (typeof yValue === 'string' || typeof yValue === 'object' && !Ext.isDate(yValue) || //set as uniform distribution if the axis is a category axis. |
|
yAxisType === 'Category') { |
|
yValue = i; |
|
} |
|
storeIndices.push(i); |
|
xValues.push(xValue); |
|
yValues.push(yValue); |
|
} |
|
ln = xValues.length; |
|
if (ln > bbox.width) { |
|
coords = me.shrink(xValues, yValues, bbox.width); |
|
xValues = coords.x; |
|
yValues = coords.y; |
|
} |
|
me.items = []; |
|
count = 0; |
|
ln = xValues.length; |
|
for (i = 0; i < ln; i++) { |
|
xValue = xValues[i]; |
|
yValue = yValues[i]; |
|
if (yValue === false) { |
|
if (path.length == 1) { |
|
path = []; |
|
} |
|
onbreak = true; |
|
me.items.push(false); |
|
|
|
continue; |
|
} else { |
|
if (reverse) { |
|
x = bbox.x + bbox.width - ((xValue - minX) * xScale); |
|
} else { |
|
x = (bbox.x + (xValue - minX) * xScale); |
|
} |
|
x = Ext.Number.toFixed(x, 2); |
|
y = Ext.Number.toFixed((bbox.y + bbox.height) - (yValue - minY) * yScale, 2); |
|
if (onbreak) { |
|
onbreak = false; |
|
path.push('M'); |
|
} |
|
path = path.concat([ |
|
x, |
|
y |
|
]); |
|
} |
|
if ((typeof firstY == 'undefined') && (typeof y != 'undefined')) { |
|
firstY = y; |
|
firstX = x; |
|
} |
|
// If this is the first line, create a dummypath to animate in from. |
|
if (!me.line || chart.resizing) { |
|
dummyPath = dummyPath.concat([ |
|
x, |
|
bbox.y + bbox.height / 2 |
|
]); |
|
} |
|
// When resizing, reset before animating |
|
if (chart.animate && chart.resizing && me.line) { |
|
me.line.setAttributes({ |
|
path: dummyPath, |
|
opacity: lineOpacity |
|
}, true); |
|
if (me.fillPath) { |
|
me.fillPath.setAttributes({ |
|
path: dummyPath, |
|
opacity: fillOpacity |
|
}, true); |
|
} |
|
if (me.line.shadows) { |
|
shadows = me.line.shadows; |
|
for (j = 0 , lnsh = shadows.length; j < lnsh; j++) { |
|
shadow = shadows[j]; |
|
shadow.setAttributes({ |
|
path: dummyPath |
|
}, true); |
|
} |
|
} |
|
} |
|
if (showMarkers) { |
|
marker = markerGroup.getAt(count++); |
|
if (!marker) { |
|
marker = Ext.chart.Shape[type](surface, Ext.apply({ |
|
group: [ |
|
group, |
|
markerGroup |
|
], |
|
x: 0, |
|
y: 0, |
|
translate: { |
|
x: +(prevX || x), |
|
y: prevY || (bbox.y + bbox.height / 2) |
|
}, |
|
value: '"' + xValue + ', ' + yValue + '"', |
|
zIndex: 4000 |
|
}, endMarkerStyle)); |
|
marker._to = { |
|
translate: { |
|
x: +x, |
|
y: +y |
|
} |
|
}; |
|
} else { |
|
marker.setAttributes({ |
|
value: '"' + xValue + ', ' + yValue + '"', |
|
x: 0, |
|
y: 0, |
|
hidden: false |
|
}, true); |
|
marker._to = { |
|
translate: { |
|
x: +x, |
|
y: +y |
|
} |
|
}; |
|
} |
|
} |
|
me.items.push({ |
|
series: me, |
|
value: [ |
|
xValue, |
|
yValue |
|
], |
|
point: [ |
|
x, |
|
y |
|
], |
|
sprite: marker, |
|
storeItem: store.getAt(storeIndices[i]) |
|
}); |
|
prevX = x; |
|
prevY = y; |
|
} |
|
if (path.length <= 1) { |
|
//nothing to be rendered |
|
return; |
|
} |
|
if (me.smooth) { |
|
smoothPath = Ext.draw.Draw.smooth(path, isNumber(smooth) ? smooth : me.defaultSmoothness); |
|
} |
|
renderPath = smooth ? smoothPath : path; |
|
//Correct path if we're animating timeAxis intervals |
|
if (chart.markerIndex && me.previousPath) { |
|
fromPath = me.previousPath; |
|
if (!smooth) { |
|
Ext.Array.erase(fromPath, 1, 2); |
|
} |
|
} else { |
|
fromPath = path; |
|
} |
|
// Only create a line if one doesn't exist. |
|
if (!me.line) { |
|
me.line = surface.add(Ext.apply({ |
|
type: 'path', |
|
group: group, |
|
path: dummyPath, |
|
stroke: endLineStyle.stroke || endLineStyle.fill |
|
}, endLineStyle || {})); |
|
//set configuration opacity |
|
me.line.setAttributes({ |
|
opacity: lineOpacity |
|
}, true); |
|
if (enableShadows) { |
|
me.line.setAttributes(Ext.apply({}, me.shadowOptions), true); |
|
} |
|
//unset fill here (there's always a default fill withing the themes). |
|
me.line.setAttributes({ |
|
fill: 'none', |
|
zIndex: 3000 |
|
}); |
|
if (!endLineStyle.stroke && colorArrayLength) { |
|
me.line.setAttributes({ |
|
stroke: colorArrayStyle[me.themeIdx % colorArrayLength] |
|
}, true); |
|
} |
|
if (enableShadows) { |
|
//create shadows |
|
shadows = me.line.shadows = []; |
|
for (shindex = 0; shindex < lnsh; shindex++) { |
|
shadowBarAttr = shadowAttributes[shindex]; |
|
shadowBarAttr = Ext.apply({}, shadowBarAttr, { |
|
path: dummyPath |
|
}); |
|
shadow = surface.add(Ext.apply({}, { |
|
type: 'path', |
|
group: shadowGroups[shindex] |
|
}, shadowBarAttr)); |
|
shadows.push(shadow); |
|
} |
|
} |
|
} |
|
if (me.fill) { |
|
fillPath = renderPath.concat([ |
|
[ |
|
"L", |
|
x, |
|
bbox.y + bbox.height |
|
], |
|
[ |
|
"L", |
|
firstX, |
|
bbox.y + bbox.height |
|
], |
|
[ |
|
"L", |
|
firstX, |
|
firstY |
|
] |
|
]); |
|
if (!me.fillPath) { |
|
me.fillPath = surface.add({ |
|
group: group, |
|
type: 'path', |
|
fill: endLineStyle.fill || colorArrayStyle[me.themeIdx % colorArrayLength], |
|
path: dummyPath |
|
}); |
|
} |
|
} |
|
markerCount = showMarkers && markerGroup.getCount(); |
|
if (chart.animate) { |
|
fill = me.fill; |
|
line = me.line; |
|
//Add renderer to line. There is not unique record associated with this. |
|
rendererAttributes = me.renderer(line, false, { |
|
path: renderPath |
|
}, i, store); |
|
Ext.apply(rendererAttributes, endLineStyle || {}, { |
|
stroke: endLineStyle.stroke || endLineStyle.fill |
|
}); |
|
//fill should not be used here but when drawing the special fill path object |
|
delete rendererAttributes.fill; |
|
line.show(true); |
|
if (chart.markerIndex && me.previousPath) { |
|
me.animation = animation = me.onAnimate(line, { |
|
to: rendererAttributes, |
|
from: { |
|
path: fromPath |
|
} |
|
}); |
|
} else { |
|
me.animation = animation = me.onAnimate(line, { |
|
to: rendererAttributes |
|
}); |
|
} |
|
//animate shadows |
|
if (enableShadows) { |
|
shadows = line.shadows; |
|
for (j = 0; j < lnsh; j++) { |
|
shadows[j].show(true); |
|
if (chart.markerIndex && me.previousPath) { |
|
me.onAnimate(shadows[j], { |
|
to: { |
|
path: renderPath |
|
}, |
|
from: { |
|
path: fromPath |
|
} |
|
}); |
|
} else { |
|
me.onAnimate(shadows[j], { |
|
to: { |
|
path: renderPath |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
//animate fill path |
|
if (fill) { |
|
me.fillPath.show(true); |
|
me.onAnimate(me.fillPath, { |
|
to: Ext.apply({}, { |
|
path: fillPath, |
|
fill: endLineStyle.fill || colorArrayStyle[me.themeIdx % colorArrayLength], |
|
'stroke-width': 0, |
|
opacity: fillOpacity |
|
}, endLineStyle || {}) |
|
}); |
|
} |
|
//animate markers |
|
if (showMarkers) { |
|
count = 0; |
|
for (i = 0; i < ln; i++) { |
|
if (me.items[i]) { |
|
item = markerGroup.getAt(count++); |
|
if (item) { |
|
rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store); |
|
me.onAnimate(item, { |
|
to: Ext.applyIf(rendererAttributes, endMarkerStyle || {}) |
|
}); |
|
item.show(true); |
|
} |
|
} |
|
} |
|
for (; count < markerCount; count++) { |
|
item = markerGroup.getAt(count); |
|
item.hide(true); |
|
} |
|
} |
|
} else // for(i = 0; i < (chart.markerIndex || 0)-1; i++) { |
|
// item = markerGroup.getAt(i); |
|
// item.hide(true); |
|
// } |
|
{ |
|
rendererAttributes = me.renderer(me.line, false, { |
|
path: renderPath, |
|
hidden: false |
|
}, i, store); |
|
Ext.apply(rendererAttributes, endLineStyle || {}, { |
|
stroke: endLineStyle.stroke || endLineStyle.fill |
|
}); |
|
//fill should not be used here but when drawing the special fill path object |
|
delete rendererAttributes.fill; |
|
me.line.setAttributes(rendererAttributes, true); |
|
me.line.setAttributes({ |
|
opacity: lineOpacity |
|
}, true); |
|
//set path for shadows |
|
if (enableShadows) { |
|
shadows = me.line.shadows; |
|
for (j = 0; j < lnsh; j++) { |
|
shadows[j].setAttributes({ |
|
path: renderPath, |
|
hidden: false |
|
}, true); |
|
} |
|
} |
|
if (me.fill) { |
|
me.fillPath.setAttributes({ |
|
path: fillPath, |
|
hidden: false, |
|
opacity: fillOpacity |
|
}, true); |
|
} |
|
if (showMarkers) { |
|
count = 0; |
|
for (i = 0; i < ln; i++) { |
|
if (me.items[i]) { |
|
item = markerGroup.getAt(count++); |
|
if (item) { |
|
rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store); |
|
item.setAttributes(Ext.apply(endMarkerStyle || {}, rendererAttributes || {}), true); |
|
if (!item.attr.hidden) { |
|
item.show(true); |
|
} |
|
} |
|
} |
|
} |
|
for (; count < markerCount; count++) { |
|
item = markerGroup.getAt(count); |
|
item.hide(true); |
|
} |
|
} |
|
} |
|
if (chart.markerIndex) { |
|
if (me.smooth) { |
|
Ext.Array.erase(path, 1, 2); |
|
} else { |
|
Ext.Array.splice(path, 1, 0, path[1], path[2]); |
|
} |
|
me.previousPath = path; |
|
} |
|
me.renderLabels(); |
|
me.renderCallouts(); |
|
me.fireEvent('draw', me); |
|
}, |
|
// @private called when a label is to be created. |
|
onCreateLabel: function(storeItem, item, i, display) { |
|
var me = this, |
|
group = me.labelsGroup, |
|
config = me.label, |
|
bbox = me.bbox, |
|
endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle || {}); |
|
return me.chart.surface.add(Ext.apply({ |
|
'type': 'text', |
|
'text-anchor': 'middle', |
|
'group': group, |
|
'x': Number(item.point[0]), |
|
'y': bbox.y + bbox.height / 2 |
|
}, endLabelStyle || {})); |
|
}, |
|
// @private called when a label is to be positioned. |
|
onPlaceLabel: function(label, storeItem, item, i, display, animate, index) { |
|
var me = this, |
|
chart = me.chart, |
|
resizing = chart.resizing, |
|
config = me.label, |
|
format = config.renderer, |
|
field = config.field, |
|
bbox = me.bbox, |
|
x = Number(item.point[0]), |
|
y = Number(item.point[1]), |
|
radius = item.sprite.attr.radius, |
|
labelBox, markerBox, width, height, xOffset, yOffset; |
|
label.setAttributes({ |
|
text: format(storeItem.get(field), label, storeItem, item, i, display, animate, index), |
|
hidden: true |
|
}, true); |
|
//TODO(nicolas): find out why width/height values in circle bounding boxes are undefined. |
|
markerBox = item.sprite.getBBox(); |
|
markerBox.width = markerBox.width || (radius * 2); |
|
markerBox.height = markerBox.height || (radius * 2); |
|
labelBox = label.getBBox(); |
|
width = labelBox.width / 2; |
|
height = labelBox.height / 2; |
|
if (display == 'rotate') { |
|
//correct label position to fit into the box |
|
xOffset = markerBox.width / 2 + width + height / 2; |
|
if (x + xOffset + width > bbox.x + bbox.width) { |
|
x -= xOffset; |
|
} else { |
|
x += xOffset; |
|
} |
|
label.setAttributes({ |
|
'rotation': { |
|
x: x, |
|
y: y, |
|
degrees: -45 |
|
} |
|
}, true); |
|
} else if (display == 'under' || display == 'over') { |
|
label.setAttributes({ |
|
'rotation': { |
|
degrees: 0 |
|
} |
|
}, true); |
|
//correct label position to fit into the box |
|
if (x < bbox.x + width) { |
|
x = bbox.x + width; |
|
} else if (x + width > bbox.x + bbox.width) { |
|
x = bbox.x + bbox.width - width; |
|
} |
|
yOffset = markerBox.height / 2 + height; |
|
y = y + (display == 'over' ? -yOffset : yOffset); |
|
if (y < bbox.y + height) { |
|
y += 2 * yOffset; |
|
} else if (y + height > bbox.y + bbox.height) { |
|
y -= 2 * yOffset; |
|
} |
|
} |
|
if (me.chart.animate && !me.chart.resizing) { |
|
label.show(true); |
|
me.onAnimate(label, { |
|
to: { |
|
x: x, |
|
y: y |
|
} |
|
}); |
|
} else { |
|
label.setAttributes({ |
|
x: x, |
|
y: y |
|
}, true); |
|
if (resizing && chart.animate) { |
|
me.on({ |
|
single: true, |
|
afterrender: function() { |
|
label.show(true); |
|
} |
|
}); |
|
} else { |
|
label.show(true); |
|
} |
|
} |
|
}, |
|
// @private Overriding highlights.js highlightItem method. |
|
highlightItem: function() { |
|
var me = this, |
|
line = me.line; |
|
me.callParent(arguments); |
|
if (line && !me.highlighted) { |
|
if (!('__strokeWidth' in line)) { |
|
line.__strokeWidth = parseFloat(line.attr['stroke-width']) || 0; |
|
} |
|
if (line.__anim) { |
|
line.__anim.paused = true; |
|
} |
|
line.__anim = new Ext.fx.Anim({ |
|
target: line, |
|
to: { |
|
'stroke-width': line.__strokeWidth + 3 |
|
} |
|
}); |
|
me.highlighted = true; |
|
} |
|
}, |
|
// @private Overriding highlights.js unHighlightItem method. |
|
unHighlightItem: function() { |
|
var me = this, |
|
line = me.line, |
|
width; |
|
me.callParent(arguments); |
|
if (line && me.highlighted) { |
|
width = line.__strokeWidth || parseFloat(line.attr['stroke-width']) || 0; |
|
line.__anim = new Ext.fx.Anim({ |
|
target: line, |
|
to: { |
|
'stroke-width': width |
|
} |
|
}); |
|
me.highlighted = false; |
|
} |
|
}, |
|
// @private called when a callout needs to be placed. |
|
onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) { |
|
if (!display) { |
|
return; |
|
} |
|
var me = this, |
|
chart = me.chart, |
|
surface = chart.surface, |
|
resizing = chart.resizing, |
|
config = me.callouts, |
|
items = me.items, |
|
prev = i == 0 ? false : items[i - 1].point, |
|
next = (i == items.length - 1) ? false : items[i + 1].point, |
|
cur = [ |
|
+item.point[0], |
|
+item.point[1] |
|
], |
|
dir, norm, normal, a, aprev, anext, |
|
offsetFromViz = config.offsetFromViz || 30, |
|
offsetToSide = config.offsetToSide || 10, |
|
offsetBox = config.offsetBox || 3, |
|
boxx, boxy, boxw, boxh, p, |
|
clipRect = me.clipRect, |
|
bbox = { |
|
width: config.styles.width || 10, |
|
height: config.styles.height || 10 |
|
}, |
|
x, y; |
|
//get the right two points |
|
if (!prev) { |
|
prev = cur; |
|
} |
|
if (!next) { |
|
next = cur; |
|
} |
|
a = (next[1] - prev[1]) / (next[0] - prev[0]); |
|
aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]); |
|
anext = (next[1] - cur[1]) / (next[0] - cur[0]); |
|
norm = Math.sqrt(1 + a * a); |
|
dir = [ |
|
1 / norm, |
|
a / norm |
|
]; |
|
normal = [ |
|
-dir[1], |
|
dir[0] |
|
]; |
|
//keep the label always on the outer part of the "elbow" |
|
if (aprev > 0 && anext < 0 && normal[1] < 0 || aprev < 0 && anext > 0 && normal[1] > 0) { |
|
normal[0] *= -1; |
|
normal[1] *= -1; |
|
} else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0 || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) { |
|
normal[0] *= -1; |
|
normal[1] *= -1; |
|
} |
|
//position |
|
x = cur[0] + normal[0] * offsetFromViz; |
|
y = cur[1] + normal[1] * offsetFromViz; |
|
//box position and dimensions |
|
boxx = x + (normal[0] > 0 ? 0 : -(bbox.width + 2 * offsetBox)); |
|
boxy = y - bbox.height / 2 - offsetBox; |
|
boxw = bbox.width + 2 * offsetBox; |
|
boxh = bbox.height + 2 * offsetBox; |
|
//now check if we're out of bounds and invert the normal vector correspondingly |
|
//this may add new overlaps between labels (but labels won't be out of bounds). |
|
if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) { |
|
normal[0] *= -1; |
|
} |
|
if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) { |
|
normal[1] *= -1; |
|
} |
|
//update positions |
|
x = cur[0] + normal[0] * offsetFromViz; |
|
y = cur[1] + normal[1] * offsetFromViz; |
|
//update box position and dimensions |
|
boxx = x + (normal[0] > 0 ? 0 : -(bbox.width + 2 * offsetBox)); |
|
boxy = y - bbox.height / 2 - offsetBox; |
|
boxw = bbox.width + 2 * offsetBox; |
|
boxh = bbox.height + 2 * offsetBox; |
|
if (chart.animate) { |
|
//set the line from the middle of the pie to the box. |
|
me.onAnimate(callout.lines, { |
|
to: { |
|
path: [ |
|
"M", |
|
cur[0], |
|
cur[1], |
|
"L", |
|
x, |
|
y, |
|
"Z" |
|
] |
|
} |
|
}); |
|
//set component position |
|
if (callout.panel) { |
|
callout.panel.setPosition(boxx, boxy, true); |
|
} |
|
} else { |
|
//set the line from the middle of the pie to the box. |
|
callout.lines.setAttributes({ |
|
path: [ |
|
"M", |
|
cur[0], |
|
cur[1], |
|
"L", |
|
x, |
|
y, |
|
"Z" |
|
] |
|
}, true); |
|
//set component position |
|
if (callout.panel) { |
|
callout.panel.setPosition(boxx, boxy); |
|
} |
|
} |
|
for (p in callout) { |
|
callout[p].show(true); |
|
} |
|
}, |
|
isItemInPoint: function(x, y, item, i) { |
|
var me = this, |
|
items = me.items, |
|
ln = items.length, |
|
tolerance = me.selectionTolerance, |
|
prevItem, nextItem, prevPoint, nextPoint, x1, x2, y1, y2, dist1, dist2, dist, |
|
sqrt = Math.sqrt; |
|
nextItem = items[i]; |
|
prevItem = i && items[i - 1]; |
|
if (i >= ln) { |
|
prevItem = items[ln - 1]; |
|
} |
|
prevPoint = prevItem && prevItem.point; |
|
nextPoint = nextItem && nextItem.point; |
|
x1 = prevItem ? prevPoint[0] : nextPoint[0] - tolerance; |
|
y1 = prevItem ? prevPoint[1] : nextPoint[1]; |
|
x2 = nextItem ? nextPoint[0] : prevPoint[0] + tolerance; |
|
y2 = nextItem ? nextPoint[1] : prevPoint[1]; |
|
dist1 = sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1)); |
|
dist2 = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2)); |
|
dist = Math.min(dist1, dist2); |
|
if (dist <= tolerance) { |
|
return dist == dist1 ? prevItem : nextItem; |
|
} |
|
return false; |
|
}, |
|
// @private toggle visibility of all series elements (markers, sprites). |
|
toggleAll: function(show) { |
|
var me = this, |
|
i, ln, shadow, shadows; |
|
if (!show) { |
|
Ext.chart.series.Cartesian.prototype.hideAll.call(me); |
|
} else { |
|
Ext.chart.series.Cartesian.prototype.showAll.call(me); |
|
} |
|
if (me.line) { |
|
me.line.setAttributes({ |
|
hidden: !show |
|
}, true); |
|
//hide shadows too |
|
if (me.line.shadows) { |
|
for (i = 0 , shadows = me.line.shadows , ln = shadows.length; i < ln; i++) { |
|
shadow = shadows[i]; |
|
shadow.setAttributes({ |
|
hidden: !show |
|
}, true); |
|
} |
|
} |
|
} |
|
if (me.fillPath) { |
|
me.fillPath.setAttributes({ |
|
hidden: !show |
|
}, true); |
|
} |
|
}, |
|
// @private hide all series elements (markers, sprites). |
|
hideAll: function() { |
|
this.toggleAll(false); |
|
}, |
|
// @private hide all series elements (markers, sprites). |
|
showAll: function() { |
|
this.toggleAll(true); |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.series.Pie |
|
* |
|
* Creates a Pie Chart. A Pie Chart is a useful visualization technique to display quantitative information for different |
|
* categories that also have a meaning as a whole. |
|
* As with all other series, the Pie Series must be appended in the *series* Chart array configuration. See the Chart |
|
* documentation for more information. A typical configuration object for the pie series could be: |
|
* |
|
* @example |
|
* var store = Ext.create('Ext.data.JsonStore', { |
|
* fields: ['name', 'data'], |
|
* data: [ |
|
* { 'name': 'metric one', 'data': 10 }, |
|
* { 'name': 'metric two', 'data': 7 }, |
|
* { 'name': 'metric three', 'data': 5 }, |
|
* { 'name': 'metric four', 'data': 2 }, |
|
* { 'name': 'metric five', 'data': 27 } |
|
* ] |
|
* }); |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* width: 500, |
|
* height: 350, |
|
* animate: true, |
|
* store: store, |
|
* theme: 'Base:gradients', |
|
* series: [{ |
|
* type: 'pie', |
|
* angleField: 'data', |
|
* showInLegend: true, |
|
* tips: { |
|
* trackMouse: true, |
|
* width: 140, |
|
* height: 28, |
|
* renderer: function(storeItem, item) { |
|
* // calculate and display percentage on hover |
|
* var total = 0; |
|
* store.each(function(rec) { |
|
* total += rec.get('data'); |
|
* }); |
|
* this.setTitle(storeItem.get('name') + ': ' + Math.round(storeItem.get('data') / total * 100) + '%'); |
|
* } |
|
* }, |
|
* highlight: { |
|
* segment: { |
|
* margin: 20 |
|
* } |
|
* }, |
|
* label: { |
|
* field: 'name', |
|
* display: 'rotate', |
|
* contrast: true, |
|
* font: '18px Arial', |
|
* hideLessThan: 18 |
|
* } |
|
* }] |
|
* }); |
|
* |
|
* In this configuration we set `pie` as the type for the series, set an object with specific style properties for highlighting options |
|
* (triggered when hovering elements). We also set true to `showInLegend` so all the pie slices can be represented by a legend item. |
|
* |
|
* We set `data` as the value of the field to determine the angle span for each pie slice. We also set a label configuration object |
|
* where we set the field name of the store field to be renderer as text for the label. The labels will also be displayed rotated. |
|
* |
|
* We set `contrast` to `true` to flip the color of the label if it is to similar to the background color. Use `hideLessThan` to hide |
|
* labels for Pie slices with segment length less than value in pixels. Finally, we set the font family and size through the |
|
* `font` parameter. |
|
* |
|
*/ |
|
Ext.define('Ext.chart.series.Pie', { |
|
/* Begin Definitions */ |
|
alternateClassName: [ |
|
'Ext.chart.PieSeries', |
|
'Ext.chart.PieChart' |
|
], |
|
extend: 'Ext.chart.series.Series', |
|
/* End Definitions */ |
|
type: "pie", |
|
alias: 'series.pie', |
|
accuracy: 100000, |
|
rad: Math.PI * 2 / 100000, |
|
/** |
|
* @cfg {Number} highlightDuration |
|
* The duration for the pie slice highlight effect. |
|
*/ |
|
highlightDuration: 150, |
|
/** |
|
* @cfg {String} angleField (required) |
|
* The store record field name to be used for the pie angles. |
|
* The values bound to this field name must be positive real numbers. |
|
*/ |
|
angleField: false, |
|
/** |
|
* @cfg {String} field |
|
* Alias for {@link #angleField}. |
|
*/ |
|
/** |
|
* @cfg {String} xField |
|
* Alias for {@link #angleField}. |
|
*/ |
|
/** |
|
* @cfg {String} lengthField |
|
* The store record field name to be used for the pie slice lengths. |
|
* The values bound to this field name must be positive real numbers. |
|
*/ |
|
lengthField: false, |
|
/** |
|
* @cfg {Boolean/Number} donut |
|
* Whether to set the pie chart as donut chart. |
|
* Default's false. Can be set to a particular percentage to set the radius |
|
* of the donut chart. |
|
*/ |
|
donut: false, |
|
/** |
|
* @cfg {Boolean} showInLegend |
|
* Whether to add the pie chart elements as legend items. Default's false. |
|
*/ |
|
showInLegend: false, |
|
/** |
|
* @cfg {Array} colorSet |
|
* An array of color values which will be used, in order, as the pie slice fill colors. |
|
*/ |
|
/** |
|
* @cfg {Object} style |
|
* An object containing styles for overriding series styles from Theming. |
|
*/ |
|
style: {}, |
|
/** |
|
* @cfg {Boolean} clockwise |
|
* Whether the pie slices are displayed clockwise. Default's false. |
|
*/ |
|
clockwise: false, |
|
/** |
|
* @cfg {Number|Object} rotation |
|
* If a Number, the angle in radians of the first pie slice. |
|
* 0 means 3 o'clock. (-Math.PI / 2) means noon. (+Math.PI / 2) means 6 o'clock. |
|
* undefined means centered around 3 o'clock. Default's undefined. |
|
* If an Object, the angle can be specified in degrees or radians as in |
|
* `rotation: {degrees: -90}` or `rotation: {radians: -Math.PI / 2}`. |
|
*/ |
|
rotation: undefined, |
|
constructor: function(config) { |
|
this.callParent(arguments); |
|
var me = this, |
|
chart = me.chart, |
|
surface = chart.surface, |
|
store = chart.store, |
|
shadow = chart.shadow, |
|
highlight = config.highlight, |
|
i, l, cfg; |
|
if (highlight) { |
|
config.highlightCfg = Ext.merge({ |
|
segment: { |
|
margin: 20 |
|
} |
|
}, highlight, config.highlightCfg); |
|
} |
|
Ext.apply(me, config, { |
|
shadowAttributes: [ |
|
{ |
|
"stroke-width": 6, |
|
"stroke-opacity": 1, |
|
stroke: 'rgb(200, 200, 200)', |
|
translate: { |
|
x: 1.2, |
|
y: 2 |
|
} |
|
}, |
|
{ |
|
"stroke-width": 4, |
|
"stroke-opacity": 1, |
|
stroke: 'rgb(150, 150, 150)', |
|
translate: { |
|
x: 0.9, |
|
y: 1.5 |
|
} |
|
}, |
|
{ |
|
"stroke-width": 2, |
|
"stroke-opacity": 1, |
|
stroke: 'rgb(100, 100, 100)', |
|
translate: { |
|
x: 0.6, |
|
y: 1 |
|
} |
|
} |
|
] |
|
}); |
|
me.group = surface.getGroup(me.seriesId); |
|
if (shadow) { |
|
for (i = 0 , l = me.shadowAttributes.length; i < l; i++) { |
|
me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i)); |
|
} |
|
} |
|
surface.customAttributes.segment = function(opt) { |
|
//Browsers will complain if we create a path |
|
//element that has no path commands. So ensure a dummy |
|
//path command for an empty path. |
|
var ans = me.getSegment(opt); |
|
if (!ans.path || ans.path.length === 0) { |
|
ans.path = [ |
|
'M', |
|
0, |
|
0 |
|
]; |
|
} |
|
return ans; |
|
}; |
|
me.__excludes = me.__excludes || []; |
|
}, |
|
onRedraw: function() { |
|
this.initialize(); |
|
}, |
|
// @private updates some onbefore render parameters. |
|
initialize: function() { |
|
var me = this, |
|
store = me.chart.getChartStore(), |
|
data = store.data.items, |
|
i, ln, rec; |
|
me.callParent(); |
|
//Add yFields to be used in Legend.js |
|
me.yField = []; |
|
if (me.label.field) { |
|
for (i = 0 , ln = data.length; i < ln; i++) { |
|
rec = data[i]; |
|
me.yField.push(rec.get(me.label.field)); |
|
} |
|
} |
|
}, |
|
// @private returns an object with properties for a PieSlice. |
|
getSegment: function(opt) { |
|
var me = this, |
|
rad = me.rad, |
|
cos = Math.cos, |
|
sin = Math.sin, |
|
x = me.centerX, |
|
y = me.centerY, |
|
x1 = 0, |
|
x2 = 0, |
|
x3 = 0, |
|
x4 = 0, |
|
y1 = 0, |
|
y2 = 0, |
|
y3 = 0, |
|
y4 = 0, |
|
x5 = 0, |
|
y5 = 0, |
|
x6 = 0, |
|
y6 = 0, |
|
delta = 0.01, |
|
startAngle = opt.startAngle, |
|
endAngle = opt.endAngle, |
|
midAngle = (startAngle + endAngle) / 2 * rad, |
|
margin = opt.margin || 0, |
|
a1 = Math.min(startAngle, endAngle) * rad, |
|
a2 = Math.max(startAngle, endAngle) * rad, |
|
c1 = cos(a1), |
|
s1 = sin(a1), |
|
c2 = cos(a2), |
|
s2 = sin(a2), |
|
cm = cos(midAngle), |
|
sm = sin(midAngle), |
|
flag = 0, |
|
hsqr2 = 0.7071067811865476; |
|
// sqrt(0.5) |
|
if (a2 - a1 < delta) { |
|
return { |
|
path: "" |
|
}; |
|
} |
|
if (margin !== 0) { |
|
x += margin * cm; |
|
y += margin * sm; |
|
} |
|
x2 = x + opt.endRho * c1; |
|
y2 = y + opt.endRho * s1; |
|
x4 = x + opt.endRho * c2; |
|
y4 = y + opt.endRho * s2; |
|
x6 = x + opt.endRho * cm; |
|
y6 = y + opt.endRho * sm; |
|
if (opt.startRho !== 0) { |
|
x1 = x + opt.startRho * c1; |
|
y1 = y + opt.startRho * s1; |
|
x3 = x + opt.startRho * c2; |
|
y3 = y + opt.startRho * s2; |
|
x5 = x + opt.startRho * cm; |
|
y5 = y + opt.startRho * sm; |
|
return { |
|
path: [ |
|
[ |
|
"M", |
|
x2, |
|
y2 |
|
], |
|
[ |
|
"A", |
|
opt.endRho, |
|
opt.endRho, |
|
0, |
|
0, |
|
1, |
|
x6, |
|
y6 |
|
], |
|
[ |
|
"L", |
|
x6, |
|
y6 |
|
], |
|
[ |
|
"A", |
|
opt.endRho, |
|
opt.endRho, |
|
0, |
|
flag, |
|
1, |
|
x4, |
|
y4 |
|
], |
|
[ |
|
"L", |
|
x4, |
|
y4 |
|
], |
|
[ |
|
"L", |
|
x3, |
|
y3 |
|
], |
|
[ |
|
"A", |
|
opt.startRho, |
|
opt.startRho, |
|
0, |
|
flag, |
|
0, |
|
x5, |
|
y5 |
|
], |
|
[ |
|
"L", |
|
x5, |
|
y5 |
|
], |
|
[ |
|
"A", |
|
opt.startRho, |
|
opt.startRho, |
|
0, |
|
0, |
|
0, |
|
x1, |
|
y1 |
|
], |
|
[ |
|
"L", |
|
x1, |
|
y1 |
|
], |
|
[ |
|
"Z" |
|
] |
|
] |
|
}; |
|
} else { |
|
return { |
|
path: [ |
|
[ |
|
"M", |
|
x, |
|
y |
|
], |
|
[ |
|
"L", |
|
x2, |
|
y2 |
|
], |
|
[ |
|
"A", |
|
opt.endRho, |
|
opt.endRho, |
|
0, |
|
0, |
|
1, |
|
x6, |
|
y6 |
|
], |
|
[ |
|
"L", |
|
x6, |
|
y6 |
|
], |
|
[ |
|
"A", |
|
opt.endRho, |
|
opt.endRho, |
|
0, |
|
flag, |
|
1, |
|
x4, |
|
y4 |
|
], |
|
[ |
|
"L", |
|
x4, |
|
y4 |
|
], |
|
[ |
|
"L", |
|
x, |
|
y |
|
], |
|
[ |
|
"Z" |
|
] |
|
] |
|
}; |
|
} |
|
}, |
|
// @private utility function to calculate the middle point of a pie slice. |
|
calcMiddle: function(item) { |
|
var me = this, |
|
rad = me.rad, |
|
slice = item.slice, |
|
x = me.centerX, |
|
y = me.centerY, |
|
startAngle = slice.startAngle, |
|
endAngle = slice.endAngle, |
|
donut = +me.donut, |
|
midAngle = -(startAngle + endAngle) * rad / 2, |
|
r = (item.endRho + item.startRho) / 2, |
|
xm = x + r * Math.cos(midAngle), |
|
ym = y - r * Math.sin(midAngle); |
|
item.middle = { |
|
x: xm, |
|
y: ym |
|
}; |
|
}, |
|
/** |
|
* Draws the series for the current chart. |
|
*/ |
|
drawSeries: function() { |
|
var me = this, |
|
store = me.chart.getChartStore(), |
|
data = store.data.items, |
|
record, |
|
group = me.group, |
|
animate = me.chart.animate, |
|
field = me.angleField || me.field || me.xField, |
|
lenField = [].concat(me.lengthField), |
|
totalLenField = 0, |
|
chart = me.chart, |
|
surface = chart.surface, |
|
chartBBox = chart.chartBBox, |
|
enableShadows = chart.shadow, |
|
shadowGroups = me.shadowGroups, |
|
shadowAttributes = me.shadowAttributes, |
|
lnsh = shadowGroups.length, |
|
layers = lenField.length, |
|
rhoAcum = 0, |
|
donut = +me.donut, |
|
layerTotals = [], |
|
items = [], |
|
totalField = 0, |
|
maxLenField = 0, |
|
angle = 0, |
|
rotation = me.rotation, |
|
seriesStyle = me.seriesStyle, |
|
colorArrayStyle = me.colorArrayStyle, |
|
colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, |
|
rendererAttributes, shadowAttr, shadows, shadow, shindex, centerX, centerY, deltaRho, |
|
first = 0, |
|
slice, slices, sprite, value, item, lenValue, ln, i, j, endAngle, path, p, spriteOptions, bbox; |
|
Ext.apply(seriesStyle, me.style || {}); |
|
me.setBBox(); |
|
bbox = me.bbox; |
|
//override theme colors |
|
if (me.colorSet) { |
|
colorArrayStyle = me.colorSet; |
|
colorArrayLength = colorArrayStyle.length; |
|
} |
|
//if not store or store is empty then there's nothing to draw |
|
if (!store || !store.getCount() || me.seriesIsHidden) { |
|
me.hide(); |
|
me.items = []; |
|
return; |
|
} |
|
me.unHighlightItem(); |
|
me.cleanHighlights(); |
|
centerX = me.centerX = chartBBox.x + (chartBBox.width / 2); |
|
centerY = me.centerY = chartBBox.y + (chartBBox.height / 2); |
|
me.radius = Math.min(centerX - chartBBox.x, centerY - chartBBox.y); |
|
me.slices = slices = []; |
|
me.items = items = []; |
|
for (i = 0 , ln = data.length; i < ln; i++) { |
|
record = data[i]; |
|
if (this.__excludes && this.__excludes[i]) { |
|
//hidden series |
|
|
|
continue; |
|
} |
|
totalField += +record.get(field); |
|
if (lenField[0]) { |
|
for (j = 0 , totalLenField = 0; j < layers; j++) { |
|
totalLenField += +record.get(lenField[j]); |
|
} |
|
layerTotals[i] = totalLenField; |
|
maxLenField = Math.max(maxLenField, totalLenField); |
|
} |
|
} |
|
totalField = totalField || 1; |
|
for (i = 0 , ln = data.length; i < ln; i++) { |
|
record = data[i]; |
|
if (this.__excludes && this.__excludes[i]) { |
|
value = 0; |
|
} else { |
|
value = record.get(field); |
|
if (first === 0) { |
|
first = 1; |
|
} |
|
} |
|
// First slice |
|
if (first == 1) { |
|
first = 2; |
|
if (Ext.isEmpty(rotation)) { |
|
me.firstAngle = angle = (me.clockwise ? -1 : 1) * (me.accuracy * value / totalField / 2); |
|
} else { |
|
if (!Ext.isEmpty(rotation.degrees)) { |
|
rotation = Ext.draw.Draw.rad(rotation.degrees); |
|
} else if (!Ext.isEmpty(rotation.radians)) { |
|
rotation = rotation.radians; |
|
} |
|
me.firstAngle = angle = me.accuracy * rotation / (2 * Math.PI); |
|
} |
|
for (j = 0; j < i; j++) { |
|
slices[j].startAngle = slices[j].endAngle = me.firstAngle; |
|
} |
|
} |
|
endAngle = angle + (me.clockwise ? 1 : -1) * (me.accuracy * value / totalField); |
|
slice = { |
|
series: me, |
|
value: value, |
|
startAngle: (me.clockwise ? endAngle : angle), |
|
endAngle: (me.clockwise ? angle : endAngle), |
|
storeItem: record |
|
}; |
|
if (lenField[0] && !(this.__excludes && this.__excludes[i])) { |
|
lenValue = +layerTotals[i]; |
|
//removing the floor will break Opera 11.6* |
|
slice.rho = Math.floor(me.radius / maxLenField * lenValue); |
|
} else { |
|
slice.rho = me.radius; |
|
} |
|
slices[i] = slice; |
|
// Do not remove this closure for the sake of https://sencha.jira.com/browse/EXTJSIV-5836 |
|
(function() { |
|
angle = endAngle; |
|
})(); |
|
} |
|
//do all shadows first. |
|
if (enableShadows) { |
|
for (i = 0 , ln = slices.length; i < ln; i++) { |
|
slice = slices[i]; |
|
slice.shadowAttrs = []; |
|
record = store.getAt(i); |
|
for (j = 0 , rhoAcum = 0 , shadows = []; j < layers; j++) { |
|
sprite = group.getAt(i * layers + j); |
|
if (lenField[j] && !(this.__excludes && this.__excludes[i])) { |
|
deltaRho = record.get(lenField[j]) / layerTotals[i] * slice.rho; |
|
} else { |
|
deltaRho = slice.rho; |
|
} |
|
//set pie slice properties |
|
rendererAttributes = { |
|
segment: { |
|
startAngle: slice.startAngle, |
|
endAngle: slice.endAngle, |
|
margin: 0, |
|
rho: slice.rho, |
|
startRho: rhoAcum + (deltaRho * donut / 100), |
|
endRho: rhoAcum + deltaRho |
|
}, |
|
hidden: !slice.value && (slice.startAngle % me.accuracy) == (slice.endAngle % me.accuracy) |
|
}; |
|
//create shadows |
|
for (shindex = 0 , shadows = []; shindex < lnsh; shindex++) { |
|
shadowAttr = shadowAttributes[shindex]; |
|
shadow = shadowGroups[shindex].getAt(i); |
|
if (!shadow) { |
|
shadow = chart.surface.add(Ext.apply({}, { |
|
type: 'path', |
|
group: shadowGroups[shindex], |
|
strokeLinejoin: "round" |
|
}, rendererAttributes, shadowAttr)); |
|
} |
|
shadowAttr = me.renderer(shadow, record, Ext.apply({}, rendererAttributes, shadowAttr), i, store); |
|
if (animate) { |
|
me.onAnimate(shadow, { |
|
to: shadowAttr |
|
}); |
|
} else { |
|
shadow.setAttributes(shadowAttr, true); |
|
} |
|
shadows.push(shadow); |
|
} |
|
slice.shadowAttrs[j] = shadows; |
|
} |
|
} |
|
} |
|
//do pie slices after. |
|
for (i = 0 , ln = slices.length; i < ln; i++) { |
|
slice = slices[i]; |
|
record = store.getAt(i); |
|
for (j = 0 , rhoAcum = 0; j < layers; j++) { |
|
sprite = group.getAt(i * layers + j); |
|
if (lenField[j] && !(this.__excludes && this.__excludes[i])) { |
|
deltaRho = record.get(lenField[j]) / layerTotals[i] * slice.rho; |
|
} else { |
|
deltaRho = slice.rho; |
|
} |
|
//set pie slice properties |
|
rendererAttributes = Ext.apply({ |
|
segment: { |
|
startAngle: slice.startAngle, |
|
endAngle: slice.endAngle, |
|
margin: 0, |
|
rho: slice.rho, |
|
startRho: rhoAcum + (deltaRho * donut / 100), |
|
endRho: rhoAcum + deltaRho |
|
}, |
|
hidden: (!slice.value && (slice.startAngle % me.accuracy) == (slice.endAngle % me.accuracy)) |
|
}, Ext.apply(seriesStyle, colorArrayStyle && { |
|
fill: colorArrayStyle[(layers > 1 ? j : i) % colorArrayLength] |
|
} || {})); |
|
item = Ext.apply({}, rendererAttributes.segment, { |
|
slice: slice, |
|
series: me, |
|
storeItem: slice.storeItem, |
|
index: i |
|
}); |
|
me.calcMiddle(item); |
|
if (enableShadows) { |
|
item.shadows = slice.shadowAttrs[j]; |
|
} |
|
items[i] = item; |
|
// Create a new sprite if needed (no height) |
|
if (!sprite) { |
|
spriteOptions = Ext.apply({ |
|
type: "path", |
|
group: group, |
|
middle: item.middle |
|
}, Ext.apply(seriesStyle, colorArrayStyle && { |
|
fill: colorArrayStyle[(layers > 1 ? j : i) % colorArrayLength] |
|
} || {})); |
|
sprite = surface.add(Ext.apply(spriteOptions, rendererAttributes)); |
|
} |
|
slice.sprite = slice.sprite || []; |
|
item.sprite = sprite; |
|
slice.sprite.push(sprite); |
|
slice.point = [ |
|
item.middle.x, |
|
item.middle.y |
|
]; |
|
if (animate) { |
|
rendererAttributes = me.renderer(sprite, record, rendererAttributes, i, store); |
|
sprite._to = rendererAttributes; |
|
sprite._animating = true; |
|
me.onAnimate(sprite, { |
|
to: rendererAttributes, |
|
listeners: { |
|
afteranimate: { |
|
fn: function() { |
|
this._animating = false; |
|
}, |
|
scope: sprite |
|
} |
|
} |
|
}); |
|
} else { |
|
rendererAttributes = me.renderer(sprite, record, Ext.apply(rendererAttributes, { |
|
hidden: false |
|
}), i, store); |
|
sprite.setAttributes(rendererAttributes, true); |
|
} |
|
rhoAcum += deltaRho; |
|
} |
|
} |
|
// Hide unused bars |
|
ln = group.getCount(); |
|
for (i = 0; i < ln; i++) { |
|
if (!slices[(i / layers) >> 0] && group.getAt(i)) { |
|
group.getAt(i).hide(true); |
|
} |
|
} |
|
if (enableShadows) { |
|
lnsh = shadowGroups.length; |
|
for (shindex = 0; shindex < ln; shindex++) { |
|
if (!slices[(shindex / layers) >> 0]) { |
|
for (j = 0; j < lnsh; j++) { |
|
if (shadowGroups[j].getAt(shindex)) { |
|
shadowGroups[j].getAt(shindex).hide(true); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
me.renderLabels(); |
|
me.renderCallouts(); |
|
}, |
|
setSpriteAttributes: function(sprite, attrs, animate) { |
|
var me = this; |
|
if (animate) { |
|
sprite.stopAnimation(); |
|
sprite.animate({ |
|
to: attrs, |
|
duration: me.highlightDuration |
|
}); |
|
} else { |
|
sprite.setAttributes(attrs, true); |
|
} |
|
}, |
|
createLabelLine: function(i, hidden) { |
|
var me = this, |
|
calloutLine = me.label.calloutLine, |
|
line = me.chart.surface.add({ |
|
type: 'path', |
|
stroke: (i === undefined ? '#555' : ((calloutLine && calloutLine.color) || me.getLegendColor(i))), |
|
lineWidth: (calloutLine && calloutLine.width) || 2, |
|
path: 'M0,0Z', |
|
hidden: hidden |
|
}); |
|
return line; |
|
}, |
|
drawLabelLine: function(label, from, to, animate) { |
|
var me = this, |
|
line = label.lineSprite, |
|
path = 'M' + from.x + ' ' + from.y + 'L' + to.x + ' ' + to.y + 'Z'; |
|
me.setSpriteAttributes(line, { |
|
'path': path |
|
}, animate); |
|
}, |
|
// @private callback for when creating a label sprite. |
|
onCreateLabel: function(storeItem, item, i, display) { |
|
var me = this, |
|
group = me.labelsGroup, |
|
config = me.label, |
|
centerX = me.centerX, |
|
centerY = me.centerY, |
|
middle = item.middle, |
|
endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config || {}); |
|
return me.chart.surface.add(Ext.apply({ |
|
'type': 'text', |
|
'text-anchor': 'middle', |
|
'group': group, |
|
'x': middle.x, |
|
'y': middle.y |
|
}, endLabelStyle)); |
|
}, |
|
// @private callback for when placing a label sprite. |
|
onPlaceLabel: function(label, storeItem, item, i, display, animate, index) { |
|
var me = this, |
|
rad = me.rad, |
|
chart = me.chart, |
|
resizing = chart.resizing, |
|
config = me.label, |
|
format = config.renderer, |
|
field = config.field, |
|
centerX = me.centerX, |
|
centerY = me.centerY, |
|
startAngle = item.startAngle, |
|
endAngle = item.endAngle, |
|
middle = item.middle, |
|
opt = { |
|
x: middle.x, |
|
y: middle.y |
|
}, |
|
x = middle.x - centerX, |
|
y = middle.y - centerY, |
|
from = {}, |
|
rho = 1, |
|
theta = Math.atan2(y, x || 1), |
|
dg = Ext.draw.Draw.degrees(theta), |
|
prevDg, labelBox, width, height, |
|
isOutside = (display === 'outside'), |
|
calloutLine = label.attr.calloutLine, |
|
lineWidth = (calloutLine && calloutLine.width) || 2, |
|
labelPadding = (label.attr.padding || 20) + (isOutside ? lineWidth / 2 + 4 : 0), |
|
labelPaddingX = 0, |
|
labelPaddingY = 0, |
|
a1, a2, seg; |
|
opt.hidden = false; |
|
if (this.__excludes && this.__excludes[i]) { |
|
opt.hidden = true; |
|
} |
|
if (config.hideLessThan) { |
|
a1 = Math.min(startAngle, endAngle) * rad; |
|
a2 = Math.max(startAngle, endAngle) * rad; |
|
seg = (a2 - a1) * item.rho; |
|
if (seg < config.hideLessThan) { |
|
opt.hidden = label.showOnHighlight = true; |
|
} |
|
} |
|
label.setAttributes({ |
|
opacity: (opt.hidden ? 0 : 1), |
|
text: format(storeItem.get(field), label, storeItem, item, i, display, animate, index) |
|
}, true); |
|
if (label.lineSprite) { |
|
var attrs = { |
|
opacity: (opt.hidden ? 0 : 1) |
|
}; |
|
if (opt.hidden) { |
|
attrs.translate = { |
|
x: 0, |
|
y: 0 |
|
}; |
|
} |
|
me.setSpriteAttributes(label.lineSprite, attrs, false); |
|
} |
|
switch (display) { |
|
case 'outside': |
|
label.isOutside = true; |
|
// calculate the distance to the pie's edge |
|
rho = item.endRho; |
|
// calculate the padding around the label |
|
labelPaddingX = (Math.abs(dg) <= 90 ? labelPadding : -labelPadding); |
|
labelPaddingY = (dg >= 0 ? labelPadding : -labelPadding); |
|
// add the distance from the label's center to its edge, plus padding |
|
label.setAttributes({ |
|
rotation: { |
|
degrees: 0 |
|
} |
|
}, true); |
|
labelBox = label.getBBox(); |
|
width = labelBox.width / 2 * Math.cos(theta); |
|
height = labelBox.height / 2 * Math.sin(theta); |
|
width += labelPaddingX; |
|
height += labelPaddingY; |
|
rho += Math.sqrt(width * width + height * height); |
|
//update positions |
|
opt.x = rho * Math.cos(theta) + centerX; |
|
opt.y = rho * Math.sin(theta) + centerY; |
|
break; |
|
case 'rotate': |
|
dg = Ext.draw.Draw.normalizeDegrees(dg); |
|
dg = (dg > 90 && dg < 270) ? dg + 180 : dg; |
|
prevDg = label.attr.rotation.degrees; |
|
if (prevDg != null && Math.abs(prevDg - dg) > 180 * 0.5) { |
|
if (dg > prevDg) { |
|
dg -= 360; |
|
} else { |
|
dg += 360; |
|
} |
|
dg = dg % 360; |
|
} else { |
|
dg = Ext.draw.Draw.normalizeDegrees(dg); |
|
}; |
|
//update rotation angle |
|
opt.rotate = { |
|
degrees: dg, |
|
x: opt.x, |
|
y: opt.y |
|
}; |
|
break; |
|
default: |
|
break; |
|
} |
|
//ensure the object has zero translation |
|
opt.translate = { |
|
x: 0, |
|
y: 0 |
|
}; |
|
if (animate && !resizing && (display != 'rotate' || prevDg != null)) { |
|
me.onAnimate(label, { |
|
to: opt |
|
}); |
|
} else { |
|
label.setAttributes(opt, true); |
|
} |
|
label._from = from; |
|
// draw a line if the label is outside |
|
if (label.isOutside && calloutLine) { |
|
var line = label.lineSprite, |
|
animateLine = animate, |
|
fromPoint = { |
|
// edge of the pie |
|
x: (item.endRho - lineWidth / 2) * Math.cos(theta) + centerX, |
|
y: (item.endRho - lineWidth / 2) * Math.sin(theta) + centerY |
|
}, |
|
labelCenter = { |
|
// center of the label box |
|
x: opt.x, |
|
y: opt.y |
|
}, |
|
toPoint = {}; |
|
function sign(x) { |
|
return x ? x < 0 ? -1 : 1 : 0; |
|
} |
|
if (calloutLine && calloutLine.length) { |
|
toPoint = { |
|
x: (item.endRho + calloutLine.length) * Math.cos(theta) + centerX, |
|
y: (item.endRho + calloutLine.length) * Math.sin(theta) + centerY |
|
}; |
|
} else { |
|
// Calculate the line length |
|
// |
|
// Our theta, from the rightmost point, runs: |
|
// 0 to -PI counter-clockwise on the upper half of the pie, |
|
// 0 to PI clockwise on the lower half of the pie. |
|
// By normalizing it, it runs 0 to 2*PI counter-clockwise. |
|
var normalTheta = Ext.draw.Draw.normalizeRadians(-theta), |
|
cos = Math.cos(normalTheta), |
|
sin = Math.sin(normalTheta), |
|
labelWidth = (labelBox.width + lineWidth + 4) / 2, |
|
labelHeight = (labelBox.height + lineWidth + 4) / 2; |
|
if (Math.abs(cos) * labelHeight > Math.abs(sin) * labelWidth) { |
|
// the line connects to the right or left sides of the label |
|
toPoint.x = labelCenter.x - labelWidth * sign(cos); |
|
toPoint.y = labelCenter.y + labelWidth * sin / cos * sign(cos); |
|
} else { |
|
// the line connects to the top or bottom sides of the label |
|
toPoint.x = labelCenter.x - labelHeight * cos / sin * sign(sin); |
|
toPoint.y = labelCenter.y + labelHeight * sign(sin); |
|
} |
|
} |
|
if (!line) { |
|
line = label.lineSprite = me.createLabelLine(i, opt.hidden); |
|
animateLine = false; |
|
} |
|
me.drawLabelLine(label, fromPoint, toPoint, animateLine); |
|
} else { |
|
delete label.lineSprite; |
|
} |
|
}, |
|
// @private callback for when placing a callout sprite. |
|
onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) { |
|
var me = this, |
|
chart = me.chart, |
|
centerX = me.centerX, |
|
centerY = me.centerY, |
|
middle = item.middle, |
|
opt = { |
|
x: middle.x, |
|
y: middle.y |
|
}, |
|
x = middle.x - centerX, |
|
y = middle.y - centerY, |
|
rho = 1, |
|
rhoCenter, |
|
theta = Math.atan2(y, x || 1), |
|
bbox = (callout && callout.label ? callout.label.getBBox() : { |
|
width: 0, |
|
height: 0 |
|
}), |
|
offsetFromViz = 20, |
|
offsetToSide = 10, |
|
offsetBox = 10, |
|
p; |
|
if (!bbox.width || !bbox.height) { |
|
return; |
|
} |
|
//should be able to config this. |
|
rho = item.endRho + offsetFromViz; |
|
rhoCenter = (item.endRho + item.startRho) / 2 + (item.endRho - item.startRho) / 3; |
|
//update positions |
|
opt.x = rho * Math.cos(theta) + centerX; |
|
opt.y = rho * Math.sin(theta) + centerY; |
|
x = rhoCenter * Math.cos(theta); |
|
y = rhoCenter * Math.sin(theta); |
|
if (chart.animate) { |
|
//set the line from the middle of the pie to the box. |
|
me.onAnimate(callout.lines, { |
|
to: { |
|
path: [ |
|
"M", |
|
x + centerX, |
|
y + centerY, |
|
"L", |
|
opt.x, |
|
opt.y, |
|
"Z", |
|
"M", |
|
opt.x, |
|
opt.y, |
|
"l", |
|
x > 0 ? offsetToSide : -offsetToSide, |
|
0, |
|
"z" |
|
] |
|
} |
|
}); |
|
//set box position |
|
me.onAnimate(callout.box, { |
|
to: { |
|
x: opt.x + (x > 0 ? offsetToSide : -(offsetToSide + bbox.width + 2 * offsetBox)), |
|
y: opt.y + (y > 0 ? (-bbox.height - offsetBox / 2) : (-bbox.height - offsetBox / 2)), |
|
width: bbox.width + 2 * offsetBox, |
|
height: bbox.height + 2 * offsetBox |
|
} |
|
}); |
|
//set text position |
|
me.onAnimate(callout.label, { |
|
to: { |
|
x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)), |
|
y: opt.y + (y > 0 ? -bbox.height / 4 : -bbox.height / 4) |
|
} |
|
}); |
|
} else { |
|
//set the line from the middle of the pie to the box. |
|
callout.lines.setAttributes({ |
|
path: [ |
|
"M", |
|
x + centerX, |
|
y + centerY, |
|
"L", |
|
opt.x, |
|
opt.y, |
|
"Z", |
|
"M", |
|
opt.x, |
|
opt.y, |
|
"l", |
|
x > 0 ? offsetToSide : -offsetToSide, |
|
0, |
|
"z" |
|
] |
|
}, true); |
|
//set box position |
|
callout.box.setAttributes({ |
|
x: opt.x + (x > 0 ? offsetToSide : -(offsetToSide + bbox.width + 2 * offsetBox)), |
|
y: opt.y + (y > 0 ? (-bbox.height - offsetBox / 2) : (-bbox.height - offsetBox / 2)), |
|
width: bbox.width + 2 * offsetBox, |
|
height: bbox.height + 2 * offsetBox |
|
}, true); |
|
//set text position |
|
callout.label.setAttributes({ |
|
x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)), |
|
y: opt.y + (y > 0 ? -bbox.height / 4 : -bbox.height / 4) |
|
}, true); |
|
} |
|
for (p in callout) { |
|
callout[p].show(true); |
|
} |
|
}, |
|
// @private handles sprite animation for the series. |
|
onAnimate: function(sprite, attr) { |
|
sprite.show(); |
|
return this.callParent(arguments); |
|
}, |
|
isItemInPoint: function(x, y, item, i) { |
|
var me = this, |
|
cx = me.centerX, |
|
cy = me.centerY, |
|
abs = Math.abs, |
|
dx = abs(x - cx), |
|
dy = abs(y - cy), |
|
startAngle = item.startAngle, |
|
endAngle = item.endAngle, |
|
rho = Math.sqrt(dx * dx + dy * dy), |
|
angle = Math.atan2(y - cy, x - cx) / me.rad; |
|
// normalize to the same range of angles created by drawSeries |
|
if (me.clockwise) { |
|
if (angle < me.firstAngle) { |
|
angle += me.accuracy; |
|
} |
|
} else { |
|
if (angle > me.firstAngle) { |
|
angle -= me.accuracy; |
|
} |
|
} |
|
return (angle <= startAngle && angle > endAngle && rho >= item.startRho && rho <= item.endRho); |
|
}, |
|
// @private hides all elements in the series. |
|
hideAll: function(index) { |
|
var i, l, shadow, shadows, sh, lsh, sprite; |
|
index = (isNaN(this._index) ? index : this._index) || 0; |
|
this.__excludes = this.__excludes || []; |
|
this.__excludes[index] = true; |
|
sprite = this.slices[index].sprite; |
|
for (sh = 0 , lsh = sprite.length; sh < lsh; sh++) { |
|
sprite[sh].setAttributes({ |
|
hidden: true |
|
}, true); |
|
var line = sprite[sh].lineSprite; |
|
if (line) { |
|
line.setAttributes({ |
|
hidden: true |
|
}, true); |
|
} |
|
} |
|
if (this.slices[index].shadowAttrs) { |
|
for (i = 0 , shadows = this.slices[index].shadowAttrs , l = shadows.length; i < l; i++) { |
|
shadow = shadows[i]; |
|
for (sh = 0 , lsh = shadow.length; sh < lsh; sh++) { |
|
shadow[sh].setAttributes({ |
|
hidden: true |
|
}, true); |
|
} |
|
} |
|
} |
|
this.drawSeries(); |
|
}, |
|
// @private shows all elements in the series. |
|
showAll: function(index) { |
|
index = (isNaN(this._index) ? index : this._index) || 0; |
|
this.__excludes[index] = false; |
|
this.drawSeries(); |
|
}, |
|
/** |
|
* Highlight the specified item. If no item is provided the whole series will be highlighted. |
|
* @param item {Object} Info about the item; same format as returned by #getItemForPoint |
|
*/ |
|
highlightItem: function(item) { |
|
var me = this, |
|
rad = me.rad, |
|
highlightSegment, animate, attrs, i, shadows, shadow, ln, to, itemHighlightSegment, prop, group, display, label, middle, r, x, y, line; |
|
item = item || this.items[this._index]; |
|
//TODO(nico): sometimes in IE itemmouseover is triggered |
|
//twice without triggering itemmouseout in between. This |
|
//fixes the highlighting bug. Eventually, events should be |
|
//changed to trigger one itemmouseout between two itemmouseovers. |
|
this.unHighlightItem(); |
|
if (!item || me.animating || (item.sprite && item.sprite._animating)) { |
|
return; |
|
} |
|
me.callParent([ |
|
item |
|
]); |
|
if (!me.highlight) { |
|
return; |
|
} |
|
if ('segment' in me.highlightCfg) { |
|
highlightSegment = me.highlightCfg.segment; |
|
animate = me.chart.animate; |
|
//animate labels |
|
if (me.labelsGroup) { |
|
group = me.labelsGroup; |
|
display = me.label.display; |
|
label = group.getAt(item.index); |
|
middle = (item.startAngle + item.endAngle) / 2 * rad; |
|
r = highlightSegment.margin || 0; |
|
x = r * Math.cos(middle); |
|
y = r * Math.sin(middle); |
|
//TODO(nico): rounding to 1e-10 |
|
//gives the right translation. Translation |
|
//was buggy for very small numbers. In this |
|
//case we're not looking to translate to very small |
|
//numbers but not to translate at all. |
|
if (Math.abs(x) < 1.0E-10) { |
|
x = 0; |
|
} |
|
if (Math.abs(y) < 1.0E-10) { |
|
y = 0; |
|
} |
|
attrs = { |
|
translate: { |
|
x: x, |
|
y: y |
|
} |
|
}; |
|
if (label.showOnHighlight) { |
|
attrs.opacity = 1; |
|
attrs.hidden = false; |
|
} |
|
me.setSpriteAttributes(label, attrs, animate); |
|
line = label.lineSprite; |
|
if (line) { |
|
me.setSpriteAttributes(line, attrs, animate); |
|
} |
|
} |
|
//animate shadows |
|
if (me.chart.shadow && item.shadows) { |
|
i = 0; |
|
shadows = item.shadows; |
|
ln = shadows.length; |
|
for (; i < ln; i++) { |
|
shadow = shadows[i]; |
|
to = {}; |
|
itemHighlightSegment = item.sprite._from.segment; |
|
for (prop in itemHighlightSegment) { |
|
if (!(prop in highlightSegment)) { |
|
to[prop] = itemHighlightSegment[prop]; |
|
} |
|
} |
|
attrs = { |
|
segment: Ext.applyIf(to, me.highlightCfg.segment) |
|
}; |
|
me.setSpriteAttributes(shadow, attrs, animate); |
|
} |
|
} |
|
} |
|
}, |
|
/** |
|
* Un-highlights the specified item. If no item is provided it will un-highlight the entire series. |
|
* @param item {Object} Info about the item; same format as returned by #getItemForPoint |
|
*/ |
|
unHighlightItem: function() { |
|
var me = this, |
|
items, animate, shadowsEnabled, group, len, i, j, display, shadowLen, p, to, ihs, hs, sprite, shadows, shadow, item, label, attrs; |
|
if (!me.highlight) { |
|
return; |
|
} |
|
if (('segment' in me.highlightCfg) && me.items) { |
|
items = me.items; |
|
animate = me.chart.animate; |
|
shadowsEnabled = !!me.chart.shadow; |
|
group = me.labelsGroup; |
|
len = items.length; |
|
i = 0; |
|
j = 0; |
|
display = me.label.display; |
|
for (; i < len; i++) { |
|
item = items[i]; |
|
if (!item) { |
|
|
|
continue; |
|
} |
|
sprite = item.sprite; |
|
if (sprite && sprite._highlighted) { |
|
//animate labels |
|
if (group) { |
|
label = group.getAt(item.index); |
|
attrs = Ext.apply({ |
|
translate: { |
|
x: 0, |
|
y: 0 |
|
} |
|
}, display == 'rotate' ? { |
|
rotate: { |
|
x: label.attr.x, |
|
y: label.attr.y, |
|
degrees: label.attr.rotation.degrees |
|
} |
|
} : {}); |
|
if (label.showOnHighlight) { |
|
attrs.opacity = 0; |
|
attrs.hidden = true; |
|
} |
|
me.setSpriteAttributes(label, attrs, animate); |
|
var line = label.lineSprite; |
|
if (line) { |
|
me.setSpriteAttributes(line, attrs, animate); |
|
} |
|
} |
|
if (shadowsEnabled) { |
|
shadows = item.shadows; |
|
shadowLen = shadows.length; |
|
for (; j < shadowLen; j++) { |
|
to = {}; |
|
ihs = item.sprite._to.segment; |
|
hs = item.sprite._from.segment; |
|
Ext.apply(to, hs); |
|
for (p in ihs) { |
|
if (!(p in hs)) { |
|
to[p] = ihs[p]; |
|
} |
|
} |
|
shadow = shadows[j]; |
|
me.setSpriteAttributes(shadow, { |
|
segment: to |
|
}, animate); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
me.callParent(arguments); |
|
}, |
|
/** |
|
* Returns the color of the series (to be displayed as color for the series legend item). |
|
* @param item {Object} Info about the item; same format as returned by #getItemForPoint |
|
*/ |
|
getLegendColor: function(index) { |
|
var me = this; |
|
return (me.colorSet && me.colorSet[index % me.colorSet.length]) || me.colorArrayStyle[index % me.colorArrayStyle.length]; |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.series.Radar |
|
* |
|
* Creates a Radar Chart. A Radar Chart is a useful visualization technique for comparing different quantitative values for |
|
* a constrained number of categories. |
|
* |
|
* As with all other series, the Radar series must be appended in the *series* Chart array configuration. See the Chart |
|
* documentation for more information. A typical configuration object for the radar series could be: |
|
* |
|
* @example |
|
* var store = Ext.create('Ext.data.JsonStore', { |
|
* fields: ['name', 'data1', 'data2', 'data3'], |
|
* data: [ |
|
* { 'name': 'metric one', 'data1': 14, 'data2': 12, 'data3': 13 }, |
|
* { 'name': 'metric two', 'data1': 16, 'data2': 8, 'data3': 3 }, |
|
* { 'name': 'metric three', 'data1': 14, 'data2': 2, 'data3': 7 }, |
|
* { 'name': 'metric four', 'data1': 6, 'data2': 14, 'data3': 23 }, |
|
* { 'name': 'metric five', 'data1': 36, 'data2': 38, 'data3': 33 } |
|
* ] |
|
* }); |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* width: 500, |
|
* height: 300, |
|
* animate: true, |
|
* theme:'Category2', |
|
* store: store, |
|
* axes: [{ |
|
* type: 'Radial', |
|
* position: 'radial', |
|
* label: { |
|
* display: true |
|
* } |
|
* }], |
|
* series: [{ |
|
* type: 'radar', |
|
* xField: 'name', |
|
* yField: 'data1', |
|
* showInLegend: true, |
|
* showMarkers: true, |
|
* markerConfig: { |
|
* radius: 5, |
|
* size: 5 |
|
* }, |
|
* style: { |
|
* 'stroke-width': 2, |
|
* fill: 'none' |
|
* } |
|
* },{ |
|
* type: 'radar', |
|
* xField: 'name', |
|
* yField: 'data2', |
|
* showMarkers: true, |
|
* showInLegend: true, |
|
* markerConfig: { |
|
* radius: 5, |
|
* size: 5 |
|
* }, |
|
* style: { |
|
* 'stroke-width': 2, |
|
* fill: 'none' |
|
* } |
|
* },{ |
|
* type: 'radar', |
|
* xField: 'name', |
|
* yField: 'data3', |
|
* showMarkers: true, |
|
* showInLegend: true, |
|
* markerConfig: { |
|
* radius: 5, |
|
* size: 5 |
|
* }, |
|
* style: { |
|
* 'stroke-width': 2, |
|
* fill: 'none' |
|
* } |
|
* }] |
|
* }); |
|
* |
|
* In this configuration we add three series to the chart. Each of these series is bound to the same |
|
* categories field, `name` but bound to different properties for each category, `data1`, `data2` and |
|
* `data3` respectively. All series display markers by having `showMarkers` enabled. The configuration |
|
* for the markers of each series can be set by adding properties onto the markerConfig object. |
|
* Finally we override some theme styling properties by adding properties to the `style` object. |
|
*/ |
|
Ext.define('Ext.chart.series.Radar', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.chart.series.Series', |
|
requires: [ |
|
'Ext.chart.Shape', |
|
'Ext.fx.Anim' |
|
], |
|
/* End Definitions */ |
|
type: "radar", |
|
alias: 'series.radar', |
|
rad: Math.PI / 180, |
|
showInLegend: false, |
|
/** |
|
* @cfg {Object} style |
|
* An object containing styles for overriding series styles from Theming. |
|
*/ |
|
style: {}, |
|
/** |
|
* @cfg {String} xField |
|
* The name of the data Model field corresponding to the x-axis (angle) value. |
|
*/ |
|
/** |
|
* @cfg {String} yField |
|
* The name of the data Model field corresponding to the y-axis (radius) value. |
|
*/ |
|
/** |
|
* @cfg {Boolean} showMarkers |
|
* Whether markers should be displayed at the data points of the series. If true, |
|
* then the {@link #markerConfig} config item will determine the markers' styling. |
|
*/ |
|
/** |
|
* @cfg {Object} markerConfig |
|
* The display style for the markers. Only used if {@link #showMarkers} is true. |
|
* The markerConfig is a configuration object containing the same set of properties defined in |
|
* the Sprite class. For example, if we were to set red circles as markers to the series we could |
|
* pass the object: |
|
* |
|
* @example |
|
* markerConfig: { |
|
* type: 'circle', |
|
* radius: 4, |
|
* 'fill': '#f00' |
|
* } |
|
*/ |
|
constructor: function(config) { |
|
this.callParent(arguments); |
|
var me = this, |
|
surface = me.chart.surface; |
|
me.group = surface.getGroup(me.seriesId); |
|
if (me.showMarkers) { |
|
me.markerGroup = surface.getGroup(me.seriesId + '-markers'); |
|
} |
|
}, |
|
/** |
|
* Draws the series for the current chart. |
|
*/ |
|
drawSeries: function() { |
|
var me = this, |
|
store = me.chart.getChartStore(), |
|
data = store.data.items, |
|
d, record, |
|
group = me.group, |
|
chart = me.chart, |
|
seriesItems = chart.series.items, |
|
s, sLen, series, |
|
field = me.field || me.yField, |
|
surface = chart.surface, |
|
chartBBox = chart.chartBBox, |
|
colorArrayStyle = me.colorArrayStyle, |
|
centerX, centerY, items, radius, |
|
maxValue = 0, |
|
minValue = 0, |
|
fields = [], |
|
max = Math.max, |
|
cos = Math.cos, |
|
sin = Math.sin, |
|
pi2 = Math.PI * 2, |
|
l = store.getCount(), |
|
startPath, path, x, y, rho, i, nfields, |
|
seriesStyle = me.seriesStyle, |
|
axis = chart.axes && chart.axes.get(0), |
|
aggregate = !(axis && axis.maximum); |
|
me.setBBox(); |
|
maxValue = aggregate ? 0 : (axis.maximum || 0); |
|
minValue = axis.minimum || 0; |
|
Ext.apply(seriesStyle, me.style || {}); |
|
//if the store is empty then there's nothing to draw |
|
if (!store || !store.getCount() || me.seriesIsHidden) { |
|
me.hide(); |
|
me.items = []; |
|
if (me.radar) { |
|
me.radar.hide(true); |
|
} |
|
me.radar = null; |
|
return; |
|
} |
|
if (!seriesStyle['stroke']) { |
|
seriesStyle['stroke'] = colorArrayStyle[me.themeIdx % colorArrayStyle.length]; |
|
} |
|
me.unHighlightItem(); |
|
me.cleanHighlights(); |
|
centerX = me.centerX = chartBBox.x + (chartBBox.width / 2); |
|
centerY = me.centerY = chartBBox.y + (chartBBox.height / 2); |
|
me.radius = radius = Math.min(chartBBox.width, chartBBox.height) / 2; |
|
me.items = items = []; |
|
if (aggregate) { |
|
//get all renderer fields |
|
for (s = 0 , sLen = seriesItems.length; s < sLen; s++) { |
|
series = seriesItems[s]; |
|
fields.push(series.yField); |
|
} |
|
//get maxValue to interpolate |
|
for (d = 0; d < l; d++) { |
|
record = data[d]; |
|
for (i = 0 , nfields = fields.length; i < nfields; i++) { |
|
maxValue = max(+record.get(fields[i]), maxValue); |
|
} |
|
} |
|
} |
|
//ensure non-zero value. |
|
maxValue = maxValue || 1; |
|
if (minValue >= maxValue) { |
|
minValue = maxValue - 1; |
|
} |
|
//create path and items |
|
startPath = []; |
|
path = []; |
|
for (i = 0; i < l; i++) { |
|
record = data[i]; |
|
rho = radius * (record.get(field) - minValue) / (maxValue - minValue); |
|
if (rho < 0) { |
|
rho = 0; |
|
} |
|
x = rho * cos(i / l * pi2); |
|
y = rho * sin(i / l * pi2); |
|
if (i == 0) { |
|
path.push('M', x + centerX, y + centerY); |
|
startPath.push('M', 0.01 * x + centerX, 0.01 * y + centerY); |
|
} else { |
|
path.push('L', x + centerX, y + centerY); |
|
startPath.push('L', 0.01 * x + centerX, 0.01 * y + centerY); |
|
} |
|
items.push({ |
|
sprite: false, |
|
//TODO(nico): add markers |
|
point: [ |
|
centerX + x, |
|
centerY + y |
|
], |
|
storeItem: record, |
|
series: me |
|
}); |
|
} |
|
path.push('Z'); |
|
//create path sprite |
|
if (!me.radar) { |
|
me.radar = surface.add(Ext.apply({ |
|
type: 'path', |
|
group: group, |
|
path: startPath |
|
}, seriesStyle || {})); |
|
} |
|
//reset on resizing |
|
if (chart.resizing) { |
|
me.radar.setAttributes({ |
|
path: startPath |
|
}, true); |
|
} |
|
//render/animate |
|
if (chart.animate) { |
|
me.onAnimate(me.radar, { |
|
to: Ext.apply({ |
|
path: path |
|
}, seriesStyle || {}) |
|
}); |
|
} else { |
|
me.radar.setAttributes(Ext.apply({ |
|
path: path |
|
}, seriesStyle || {}), true); |
|
} |
|
//render markers, labels and callouts |
|
if (me.showMarkers) { |
|
me.drawMarkers(); |
|
} |
|
me.renderLabels(); |
|
me.renderCallouts(); |
|
}, |
|
// @private draws the markers for the lines (if any). |
|
drawMarkers: function() { |
|
var me = this, |
|
chart = me.chart, |
|
surface = chart.surface, |
|
store = chart.getChartStore(), |
|
markerStyle = Ext.apply({}, me.markerStyle || {}), |
|
endMarkerStyle = Ext.apply(markerStyle, me.markerConfig, { |
|
fill: me.colorArrayStyle[me.themeIdx % me.colorArrayStyle.length] |
|
}), |
|
items = me.items, |
|
type = endMarkerStyle.type, |
|
markerGroup = me.markerGroup, |
|
centerX = me.centerX, |
|
centerY = me.centerY, |
|
item, i, l, marker, rendererAttributes; |
|
delete endMarkerStyle.type; |
|
for (i = 0 , l = items.length; i < l; i++) { |
|
item = items[i]; |
|
marker = markerGroup.getAt(i); |
|
if (!marker) { |
|
marker = Ext.chart.Shape[type](surface, Ext.apply({ |
|
group: markerGroup, |
|
x: 0, |
|
y: 0, |
|
translate: { |
|
x: centerX, |
|
y: centerY |
|
} |
|
}, endMarkerStyle)); |
|
} else { |
|
marker.show(); |
|
} |
|
item.sprite = marker; |
|
if (chart.resizing) { |
|
marker.setAttributes({ |
|
x: 0, |
|
y: 0, |
|
translate: { |
|
x: centerX, |
|
y: centerY |
|
} |
|
}, true); |
|
} |
|
marker._to = { |
|
translate: { |
|
x: item.point[0], |
|
y: item.point[1] |
|
} |
|
}; |
|
//render/animate |
|
rendererAttributes = me.renderer(marker, store.getAt(i), marker._to, i, store); |
|
rendererAttributes = Ext.applyIf(rendererAttributes || {}, endMarkerStyle || {}); |
|
if (chart.animate) { |
|
me.onAnimate(marker, { |
|
to: rendererAttributes |
|
}); |
|
} else { |
|
marker.setAttributes(rendererAttributes, true); |
|
} |
|
} |
|
}, |
|
isItemInPoint: function(x, y, item) { |
|
var point, |
|
tolerance = 10, |
|
abs = Math.abs; |
|
point = item.point; |
|
return (abs(point[0] - x) <= tolerance && abs(point[1] - y) <= tolerance); |
|
}, |
|
// @private callback for when creating a label sprite. |
|
onCreateLabel: function(storeItem, item, i, display) { |
|
var me = this, |
|
group = me.labelsGroup, |
|
config = me.label, |
|
centerX = me.centerX, |
|
centerY = me.centerY, |
|
endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle || {}); |
|
return me.chart.surface.add(Ext.apply({ |
|
'type': 'text', |
|
'text-anchor': 'middle', |
|
'group': group, |
|
'x': centerX, |
|
'y': centerY |
|
}, endLabelStyle || {})); |
|
}, |
|
// @private callback for when placing a label sprite. |
|
onPlaceLabel: function(label, storeItem, item, i, display, animate, index) { |
|
var me = this, |
|
chart = me.chart, |
|
resizing = chart.resizing, |
|
config = me.label, |
|
format = config.renderer, |
|
field = config.field, |
|
centerX = me.centerX, |
|
centerY = me.centerY, |
|
opt = { |
|
x: Number(item.point[0]), |
|
y: Number(item.point[1]) |
|
}, |
|
x = opt.x - centerX, |
|
y = opt.y - centerY, |
|
theta = Math.atan2(y, x || 1), |
|
deg = theta * 180 / Math.PI, |
|
labelBox, direction; |
|
function fixAngle(a) { |
|
if (a < 0) { |
|
a += 360; |
|
} |
|
return a % 360; |
|
} |
|
label.setAttributes({ |
|
text: format(storeItem.get(field), label, storeItem, item, i, display, animate, index), |
|
hidden: true |
|
}, true); |
|
// Move the label by half its height or width depending on |
|
// the angle so the label doesn't overlap the graph. |
|
labelBox = label.getBBox(); |
|
deg = fixAngle(deg); |
|
if ((deg > 45 && deg < 135) || (deg > 225 && deg < 315)) { |
|
direction = (deg > 45 && deg < 135 ? 1 : -1); |
|
opt.y += direction * labelBox.height / 2; |
|
} else { |
|
direction = (deg >= 135 && deg <= 225 ? -1 : 1); |
|
opt.x += direction * labelBox.width / 2; |
|
} |
|
if (resizing) { |
|
label.setAttributes({ |
|
x: centerX, |
|
y: centerY |
|
}, true); |
|
} |
|
if (animate) { |
|
label.show(true); |
|
me.onAnimate(label, { |
|
to: opt |
|
}); |
|
} else { |
|
label.setAttributes(opt, true); |
|
label.show(true); |
|
} |
|
}, |
|
// @private for toggling (show/hide) series. |
|
toggleAll: function(show) { |
|
var me = this, |
|
i, ln, shadow, shadows; |
|
if (!show) { |
|
Ext.chart.series.Radar.superclass.hideAll.call(me); |
|
} else { |
|
Ext.chart.series.Radar.superclass.showAll.call(me); |
|
} |
|
if (me.radar) { |
|
me.radar.setAttributes({ |
|
hidden: !show |
|
}, true); |
|
//hide shadows too |
|
if (me.radar.shadows) { |
|
for (i = 0 , shadows = me.radar.shadows , ln = shadows.length; i < ln; i++) { |
|
shadow = shadows[i]; |
|
shadow.setAttributes({ |
|
hidden: !show |
|
}, true); |
|
} |
|
} |
|
} |
|
}, |
|
// @private hide all elements in the series. |
|
hideAll: function() { |
|
this.toggleAll(false); |
|
this.hideMarkers(0); |
|
}, |
|
// @private show all elements in the series. |
|
showAll: function() { |
|
this.toggleAll(true); |
|
}, |
|
// @private hide all markers that belong to `markerGroup` |
|
hideMarkers: function(index) { |
|
var me = this, |
|
count = me.markerGroup && me.markerGroup.getCount() || 0, |
|
i = index || 0; |
|
for (; i < count; i++) { |
|
me.markerGroup.getAt(i).hide(true); |
|
} |
|
}, |
|
// @private return the radial axis as yAxis (there is no xAxis). |
|
// Required by the base class 'Ext.chart.axis.Axis'. |
|
getAxesForXAndYFields: function() { |
|
var me = this, |
|
chart = me.chart, |
|
axes = chart.axes, |
|
axis = [].concat(axes && axes.get(0)); |
|
return { |
|
yAxis: axis |
|
}; |
|
} |
|
}); |
|
|
|
/** |
|
* @class Ext.chart.series.Scatter |
|
* @extends Ext.chart.series.Cartesian |
|
* |
|
* Creates a Scatter Chart. The scatter plot is useful when trying to display more than two variables in the same visualization. |
|
* These variables can be mapped into x, y coordinates and also to an element's radius/size, color, etc. |
|
* As with all other series, the Scatter Series must be appended in the *series* Chart array configuration. See the Chart |
|
* documentation for more information on creating charts. A typical configuration object for the scatter could be: |
|
* |
|
* @example |
|
* var store = Ext.create('Ext.data.JsonStore', { |
|
* 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 } |
|
* ] |
|
* }); |
|
* |
|
* Ext.create('Ext.chart.Chart', { |
|
* renderTo: Ext.getBody(), |
|
* width: 500, |
|
* height: 300, |
|
* animate: true, |
|
* theme:'Category2', |
|
* store: store, |
|
* axes: [{ |
|
* type: 'Numeric', |
|
* position: 'left', |
|
* fields: ['data2', 'data3'], |
|
* title: 'Sample Values', |
|
* grid: true, |
|
* minimum: 0 |
|
* }, { |
|
* type: 'Category', |
|
* position: 'bottom', |
|
* fields: ['name'], |
|
* title: 'Sample Metrics' |
|
* }], |
|
* series: [{ |
|
* type: 'scatter', |
|
* markerConfig: { |
|
* radius: 5, |
|
* size: 5 |
|
* }, |
|
* axis: 'left', |
|
* xField: 'name', |
|
* yField: 'data2' |
|
* }, { |
|
* type: 'scatter', |
|
* markerConfig: { |
|
* radius: 5, |
|
* size: 5 |
|
* }, |
|
* axis: 'left', |
|
* xField: 'name', |
|
* yField: 'data3' |
|
* }] |
|
* }); |
|
* |
|
* In this configuration we add three different categories of scatter series. Each of them is bound to a different field of the same data store, |
|
* `data1`, `data2` and `data3` respectively. All x-fields for the series must be the same field, in this case `name`. |
|
* Each scatter series has a different styling configuration for markers, specified by the `markerConfig` object. Finally we set the left axis as |
|
* axis to show the current values of the elements. |
|
*/ |
|
Ext.define('Ext.chart.series.Scatter', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.chart.series.Cartesian', |
|
requires: [ |
|
'Ext.chart.axis.Axis', |
|
'Ext.chart.Shape', |
|
'Ext.fx.Anim' |
|
], |
|
/* End Definitions */ |
|
type: 'scatter', |
|
alias: 'series.scatter', |
|
/** |
|
* @cfg {Object} markerConfig |
|
* The display style for the scatter series markers. |
|
*/ |
|
/** |
|
* @cfg {Object} style |
|
* Append styling properties to this object for it to override theme properties. |
|
*/ |
|
constructor: function(config) { |
|
this.callParent(arguments); |
|
var me = this, |
|
shadow = me.chart.shadow, |
|
surface = me.chart.surface, |
|
i, l; |
|
Ext.apply(me, config, { |
|
style: {}, |
|
markerConfig: {}, |
|
shadowAttributes: [ |
|
{ |
|
"stroke-width": 6, |
|
"stroke-opacity": 0.05, |
|
stroke: 'rgb(0, 0, 0)' |
|
}, |
|
{ |
|
"stroke-width": 4, |
|
"stroke-opacity": 0.1, |
|
stroke: 'rgb(0, 0, 0)' |
|
}, |
|
{ |
|
"stroke-width": 2, |
|
"stroke-opacity": 0.15, |
|
stroke: 'rgb(0, 0, 0)' |
|
} |
|
] |
|
}); |
|
me.group = surface.getGroup(me.seriesId); |
|
if (shadow) { |
|
for (i = 0 , l = me.shadowAttributes.length; i < l; i++) { |
|
me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i)); |
|
} |
|
} |
|
}, |
|
// @private Get chart and data boundaries |
|
getBounds: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
chartAxes = chart.axes, |
|
boundAxes = me.getAxesForXAndYFields(), |
|
boundXAxis = boundAxes.xAxis, |
|
boundYAxis = boundAxes.yAxis, |
|
bbox, xScale, yScale, ln, minX, minY, maxX, maxY, i, axis, ends; |
|
me.setBBox(); |
|
bbox = me.bbox; |
|
if (axis = chartAxes.get(boundXAxis)) { |
|
ends = axis.applyData(); |
|
minX = ends.from; |
|
maxX = ends.to; |
|
} |
|
if (axis = chartAxes.get(boundYAxis)) { |
|
ends = axis.applyData(); |
|
minY = ends.from; |
|
maxY = ends.to; |
|
} |
|
// If a field was specified without a corresponding axis, create one to get bounds |
|
if (me.xField && !Ext.isNumber(minX)) { |
|
axis = me.getMinMaxXValues(); |
|
minX = axis[0]; |
|
maxX = axis[1]; |
|
} |
|
if (me.yField && !Ext.isNumber(minY)) { |
|
axis = me.getMinMaxYValues(); |
|
minY = axis[0]; |
|
maxY = axis[1]; |
|
} |
|
if (isNaN(minX)) { |
|
minX = 0; |
|
maxX = store.getCount() - 1; |
|
xScale = bbox.width / (store.getCount() - 1); |
|
} else { |
|
xScale = bbox.width / (maxX - minX); |
|
} |
|
if (isNaN(minY)) { |
|
minY = 0; |
|
maxY = store.getCount() - 1; |
|
yScale = bbox.height / (store.getCount() - 1); |
|
} else { |
|
yScale = bbox.height / (maxY - minY); |
|
} |
|
return { |
|
bbox: bbox, |
|
minX: minX, |
|
minY: minY, |
|
xScale: xScale, |
|
yScale: yScale |
|
}; |
|
}, |
|
// @private Build an array of paths for the chart |
|
getPaths: function() { |
|
var me = this, |
|
chart = me.chart, |
|
enableShadows = chart.shadow, |
|
store = chart.getChartStore(), |
|
data = store.data.items, |
|
i, ln, record, |
|
group = me.group, |
|
bounds = me.bounds = me.getBounds(), |
|
bbox = me.bbox, |
|
xScale = bounds.xScale, |
|
yScale = bounds.yScale, |
|
minX = bounds.minX, |
|
minY = bounds.minY, |
|
boxX = bbox.x, |
|
boxY = bbox.y, |
|
boxHeight = bbox.height, |
|
items = me.items = [], |
|
attrs = [], |
|
reverse = me.reverse, |
|
x, y, xValue, yValue, sprite; |
|
for (i = 0 , ln = data.length; i < ln; i++) { |
|
record = data[i]; |
|
xValue = record.get(me.xField); |
|
yValue = record.get(me.yField); |
|
//skip undefined or null values |
|
if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue) || xValue == null || yValue == null) { |
|
if (Ext.isDefined(Ext.global.console)) { |
|
Ext.global.console.warn("[Ext.chart.series.Scatter] Skipping a store element with a value which is either undefined or null at ", record, xValue, yValue); |
|
} |
|
|
|
continue; |
|
} |
|
// Ensure a value |
|
if (typeof xValue == 'string' || typeof xValue == 'object' && !Ext.isDate(xValue)) { |
|
xValue = i; |
|
} |
|
if (typeof yValue == 'string' || typeof yValue == 'object' && !Ext.isDate(yValue)) { |
|
yValue = i; |
|
} |
|
if (reverse) { |
|
x = boxX + bbox.width - ((xValue - minX) * xScale); |
|
} else { |
|
x = boxX + (xValue - minX) * xScale; |
|
} |
|
y = boxY + boxHeight - (yValue - minY) * yScale; |
|
attrs.push({ |
|
x: x, |
|
y: y |
|
}); |
|
me.items.push({ |
|
series: me, |
|
value: [ |
|
xValue, |
|
yValue |
|
], |
|
point: [ |
|
x, |
|
y |
|
], |
|
storeItem: record |
|
}); |
|
// When resizing, reset before animating |
|
if (chart.animate && chart.resizing) { |
|
sprite = group.getAt(i); |
|
if (sprite) { |
|
me.resetPoint(sprite); |
|
if (enableShadows) { |
|
me.resetShadow(sprite); |
|
} |
|
} |
|
} |
|
} |
|
return attrs; |
|
}, |
|
// @private translate point to the center |
|
resetPoint: function(sprite) { |
|
var bbox = this.bbox; |
|
sprite.setAttributes({ |
|
translate: { |
|
x: (bbox.x + bbox.width) / 2, |
|
y: (bbox.y + bbox.height) / 2 |
|
} |
|
}, true); |
|
}, |
|
// @private translate shadows of a sprite to the center |
|
resetShadow: function(sprite) { |
|
var me = this, |
|
shadows = sprite.shadows, |
|
shadowAttributes = me.shadowAttributes, |
|
ln = me.shadowGroups.length, |
|
bbox = me.bbox, |
|
i, attr; |
|
for (i = 0; i < ln; i++) { |
|
attr = Ext.apply({}, shadowAttributes[i]); |
|
// TODO: fix this with setAttributes |
|
if (attr.translate) { |
|
attr.translate.x += (bbox.x + bbox.width) / 2; |
|
attr.translate.y += (bbox.y + bbox.height) / 2; |
|
} else { |
|
attr.translate = { |
|
x: (bbox.x + bbox.width) / 2, |
|
y: (bbox.y + bbox.height) / 2 |
|
}; |
|
} |
|
shadows[i].setAttributes(attr, true); |
|
} |
|
}, |
|
// @private create a new point |
|
createPoint: function(attr, type) { |
|
var me = this, |
|
chart = me.chart, |
|
group = me.group, |
|
bbox = me.bbox; |
|
return Ext.chart.Shape[type](chart.surface, Ext.apply({}, { |
|
x: 0, |
|
y: 0, |
|
group: group, |
|
translate: { |
|
x: (bbox.x + bbox.width) / 2, |
|
y: (bbox.y + bbox.height) / 2 |
|
} |
|
}, attr)); |
|
}, |
|
// @private create a new set of shadows for a sprite |
|
createShadow: function(sprite, endMarkerStyle, type) { |
|
var me = this, |
|
chart = me.chart, |
|
shadowGroups = me.shadowGroups, |
|
shadowAttributes = me.shadowAttributes, |
|
lnsh = shadowGroups.length, |
|
bbox = me.bbox, |
|
i, shadow, shadows, attr; |
|
sprite.shadows = shadows = []; |
|
for (i = 0; i < lnsh; i++) { |
|
attr = Ext.apply({}, shadowAttributes[i]); |
|
if (attr.translate) { |
|
attr.translate.x += (bbox.x + bbox.width) / 2; |
|
attr.translate.y += (bbox.y + bbox.height) / 2; |
|
} else { |
|
Ext.apply(attr, { |
|
translate: { |
|
x: (bbox.x + bbox.width) / 2, |
|
y: (bbox.y + bbox.height) / 2 |
|
} |
|
}); |
|
} |
|
Ext.apply(attr, endMarkerStyle); |
|
shadow = Ext.chart.Shape[type](chart.surface, Ext.apply({}, { |
|
x: 0, |
|
y: 0, |
|
group: shadowGroups[i] |
|
}, attr)); |
|
shadows.push(shadow); |
|
} |
|
}, |
|
/** |
|
* Draws the series for the current chart. |
|
*/ |
|
drawSeries: function() { |
|
var me = this, |
|
chart = me.chart, |
|
store = chart.getChartStore(), |
|
group = me.group, |
|
enableShadows = chart.shadow, |
|
shadowGroups = me.shadowGroups, |
|
shadowAttributes = me.shadowAttributes, |
|
lnsh = shadowGroups.length, |
|
sprite, attrs, attr, ln, i, endMarkerStyle, shindex, type, shadows, rendererAttributes, shadowAttribute; |
|
if (!store || !store.getCount() || me.seriesIsHidden) { |
|
me.hide(); |
|
me.items = []; |
|
return; |
|
} |
|
endMarkerStyle = Ext.apply({}, me.markerStyle, me.markerConfig); |
|
type = endMarkerStyle.type || 'circle'; |
|
delete endMarkerStyle.type; |
|
//if the store is empty then there's nothing to be rendered |
|
if (!store || !store.getCount()) { |
|
me.hide(); |
|
me.items = []; |
|
return; |
|
} |
|
me.unHighlightItem(); |
|
me.cleanHighlights(); |
|
attrs = me.getPaths(); |
|
ln = attrs.length; |
|
for (i = 0; i < ln; i++) { |
|
attr = attrs[i]; |
|
sprite = group.getAt(i); |
|
Ext.apply(attr, endMarkerStyle); |
|
// Create a new sprite if needed (no height) |
|
if (!sprite) { |
|
sprite = me.createPoint(attr, type); |
|
if (enableShadows) { |
|
me.createShadow(sprite, endMarkerStyle, type); |
|
} |
|
} |
|
shadows = sprite.shadows; |
|
if (chart.animate) { |
|
rendererAttributes = me.renderer(sprite, store.getAt(i), { |
|
translate: attr |
|
}, i, store); |
|
sprite._to = rendererAttributes; |
|
me.onAnimate(sprite, { |
|
to: rendererAttributes |
|
}); |
|
//animate shadows |
|
for (shindex = 0; shindex < lnsh; shindex++) { |
|
shadowAttribute = Ext.apply({}, shadowAttributes[shindex]); |
|
rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, { |
|
hidden: false, |
|
translate: { |
|
x: attr.x + (shadowAttribute.translate ? shadowAttribute.translate.x : 0), |
|
y: attr.y + (shadowAttribute.translate ? shadowAttribute.translate.y : 0) |
|
} |
|
}, shadowAttribute), i, store); |
|
me.onAnimate(shadows[shindex], { |
|
to: rendererAttributes |
|
}); |
|
} |
|
} else { |
|
rendererAttributes = me.renderer(sprite, store.getAt(i), { |
|
translate: attr |
|
}, i, store); |
|
sprite._to = rendererAttributes; |
|
sprite.setAttributes(rendererAttributes, true); |
|
//animate shadows |
|
for (shindex = 0; shindex < lnsh; shindex++) { |
|
shadowAttribute = Ext.apply({}, shadowAttributes[shindex]); |
|
rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, { |
|
hidden: false, |
|
translate: { |
|
x: attr.x + (shadowAttribute.translate ? shadowAttribute.translate.x : 0), |
|
y: attr.y + (shadowAttribute.translate ? shadowAttribute.translate.y : 0) |
|
} |
|
}, shadowAttribute), i, store); |
|
shadows[shindex].setAttributes(rendererAttributes, true); |
|
} |
|
} |
|
me.items[i].sprite = sprite; |
|
} |
|
// Hide unused sprites |
|
ln = group.getCount(); |
|
for (i = attrs.length; i < ln; i++) { |
|
sprite = group.getAt(i); |
|
sprite.hide(true); |
|
// Shadow sprites have to be hidden separately |
|
shadows = sprite.shadows; |
|
if (shadows) { |
|
for (shindex = 0; shindex < lnsh; shindex++) { |
|
shadows[shindex].hide(true); |
|
} |
|
} |
|
} |
|
me.renderLabels(); |
|
me.renderCallouts(); |
|
}, |
|
// @private callback for when creating a label sprite. |
|
onCreateLabel: function(storeItem, item, i, display) { |
|
var me = this, |
|
group = me.labelsGroup, |
|
config = me.label, |
|
endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle), |
|
bbox = me.bbox; |
|
return me.chart.surface.add(Ext.apply({ |
|
type: 'text', |
|
'text-anchor': 'middle', |
|
group: group, |
|
x: Number(item.point[0]), |
|
y: bbox.y + bbox.height / 2 |
|
}, endLabelStyle)); |
|
}, |
|
// @private callback for when placing a label sprite. |
|
onPlaceLabel: function(label, storeItem, item, i, display, animate, index) { |
|
var me = this, |
|
chart = me.chart, |
|
resizing = chart.resizing, |
|
config = me.label, |
|
format = config.renderer, |
|
field = config.field, |
|
bbox = me.bbox, |
|
x = Number(item.point[0]), |
|
y = Number(item.point[1]), |
|
radius = item.sprite.attr.radius, |
|
labelBox, markerBox, width, height, xOffset, yOffset, anim; |
|
label.setAttributes({ |
|
text: format(storeItem.get(field), label, storeItem, item, i, display, animate, index), |
|
hidden: true |
|
}, true); |
|
//TODO(nicolas): find out why width/height values in circle bounding boxes are undefined. |
|
markerBox = item.sprite.getBBox(); |
|
markerBox.width = markerBox.width || (radius * 2); |
|
markerBox.height = markerBox.height || (radius * 2); |
|
labelBox = label.getBBox(); |
|
width = labelBox.width / 2; |
|
height = labelBox.height / 2; |
|
if (display == 'rotate') { |
|
//correct label position to fit into the box |
|
xOffset = markerBox.width / 2 + width + height / 2; |
|
if (x + xOffset + width > bbox.x + bbox.width) { |
|
x -= xOffset; |
|
} else { |
|
x += xOffset; |
|
} |
|
label.setAttributes({ |
|
'rotation': { |
|
x: x, |
|
y: y, |
|
degrees: -45 |
|
} |
|
}, true); |
|
} else if (display == 'under' || display == 'over') { |
|
label.setAttributes({ |
|
'rotation': { |
|
degrees: 0 |
|
} |
|
}, true); |
|
//correct label position to fit into the box |
|
if (x < bbox.x + width) { |
|
x = bbox.x + width; |
|
} else if (x + width > bbox.x + bbox.width) { |
|
x = bbox.x + bbox.width - width; |
|
} |
|
yOffset = markerBox.height / 2 + height; |
|
y = y + (display == 'over' ? -yOffset : yOffset); |
|
if (y < bbox.y + height) { |
|
y += 2 * yOffset; |
|
} else if (y + height > bbox.y + bbox.height) { |
|
y -= 2 * yOffset; |
|
} |
|
} |
|
if (!chart.animate) { |
|
label.setAttributes({ |
|
x: x, |
|
y: y |
|
}, true); |
|
label.show(true); |
|
} else { |
|
if (resizing) { |
|
anim = item.sprite.getActiveAnimation(); |
|
if (anim) { |
|
anim.on('afteranimate', function() { |
|
label.setAttributes({ |
|
x: x, |
|
y: y |
|
}, true); |
|
label.show(true); |
|
}); |
|
} else { |
|
label.show(true); |
|
} |
|
} else { |
|
me.onAnimate(label, { |
|
to: { |
|
x: x, |
|
y: y |
|
} |
|
}); |
|
} |
|
} |
|
}, |
|
// @private callback for when placing a callout sprite. |
|
onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) { |
|
var me = this, |
|
chart = me.chart, |
|
surface = chart.surface, |
|
resizing = chart.resizing, |
|
config = me.callouts, |
|
items = me.items, |
|
cur = item.point, |
|
normal, |
|
bbox = callout.label.getBBox(), |
|
offsetFromViz = 30, |
|
offsetToSide = 10, |
|
offsetBox = 3, |
|
boxx, boxy, boxw, boxh, p, |
|
clipRect = me.bbox, |
|
x, y; |
|
//position |
|
normal = [ |
|
Math.cos(Math.PI / 4), |
|
-Math.sin(Math.PI / 4) |
|
]; |
|
x = cur[0] + normal[0] * offsetFromViz; |
|
y = cur[1] + normal[1] * offsetFromViz; |
|
//box position and dimensions |
|
boxx = x + (normal[0] > 0 ? 0 : -(bbox.width + 2 * offsetBox)); |
|
boxy = y - bbox.height / 2 - offsetBox; |
|
boxw = bbox.width + 2 * offsetBox; |
|
boxh = bbox.height + 2 * offsetBox; |
|
//now check if we're out of bounds and invert the normal vector correspondingly |
|
//this may add new overlaps between labels (but labels won't be out of bounds). |
|
if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) { |
|
normal[0] *= -1; |
|
} |
|
if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) { |
|
normal[1] *= -1; |
|
} |
|
//update positions |
|
x = cur[0] + normal[0] * offsetFromViz; |
|
y = cur[1] + normal[1] * offsetFromViz; |
|
//update box position and dimensions |
|
boxx = x + (normal[0] > 0 ? 0 : -(bbox.width + 2 * offsetBox)); |
|
boxy = y - bbox.height / 2 - offsetBox; |
|
boxw = bbox.width + 2 * offsetBox; |
|
boxh = bbox.height + 2 * offsetBox; |
|
if (chart.animate) { |
|
//set the line from the middle of the pie to the box. |
|
me.onAnimate(callout.lines, { |
|
to: { |
|
path: [ |
|
"M", |
|
cur[0], |
|
cur[1], |
|
"L", |
|
x, |
|
y, |
|
"Z" |
|
] |
|
} |
|
}, true); |
|
//set box position |
|
me.onAnimate(callout.box, { |
|
to: { |
|
x: boxx, |
|
y: boxy, |
|
width: boxw, |
|
height: boxh |
|
} |
|
}, true); |
|
//set text position |
|
me.onAnimate(callout.label, { |
|
to: { |
|
x: x + (normal[0] > 0 ? offsetBox : -(bbox.width + offsetBox)), |
|
y: y |
|
} |
|
}, true); |
|
} else { |
|
//set the line from the middle of the pie to the box. |
|
callout.lines.setAttributes({ |
|
path: [ |
|
"M", |
|
cur[0], |
|
cur[1], |
|
"L", |
|
x, |
|
y, |
|
"Z" |
|
] |
|
}, true); |
|
//set box position |
|
callout.box.setAttributes({ |
|
x: boxx, |
|
y: boxy, |
|
width: boxw, |
|
height: boxh |
|
}, true); |
|
//set text position |
|
callout.label.setAttributes({ |
|
x: x + (normal[0] > 0 ? offsetBox : -(bbox.width + offsetBox)), |
|
y: y |
|
}, true); |
|
} |
|
for (p in callout) { |
|
callout[p].show(true); |
|
} |
|
}, |
|
// @private handles sprite animation for the series. |
|
onAnimate: function(sprite, attr) { |
|
sprite.show(); |
|
return this.callParent(arguments); |
|
}, |
|
isItemInPoint: function(x, y, item) { |
|
var point, |
|
tolerance = 10, |
|
abs = Math.abs; |
|
function dist(point) { |
|
var dx = abs(point[0] - x), |
|
dy = abs(point[1] - y); |
|
return Math.sqrt(dx * dx + dy * dy); |
|
} |
|
point = item.point; |
|
return (point[0] - tolerance <= x && point[0] + tolerance >= x && point[1] - tolerance <= y && point[1] + tolerance >= y); |
|
} |
|
}); |
|
|
|
/** |
|
* @private |
|
*/ |
|
Ext.define('Ext.draw.Matrix', { |
|
/* Begin Definitions */ |
|
requires: [ |
|
'Ext.draw.Draw' |
|
], |
|
/* End Definitions */ |
|
constructor: function(a, b, c, d, e, f) { |
|
if (a != null) { |
|
this.matrix = [ |
|
[ |
|
a, |
|
c, |
|
e |
|
], |
|
[ |
|
b, |
|
d, |
|
f |
|
], |
|
[ |
|
0, |
|
0, |
|
1 |
|
] |
|
]; |
|
} else { |
|
this.matrix = [ |
|
[ |
|
1, |
|
0, |
|
0 |
|
], |
|
[ |
|
0, |
|
1, |
|
0 |
|
], |
|
[ |
|
0, |
|
0, |
|
1 |
|
] |
|
]; |
|
} |
|
}, |
|
add: function(a, b, c, d, e, f) { |
|
var me = this, |
|
out = [ |
|
[], |
|
[], |
|
[] |
|
], |
|
matrix = [ |
|
[ |
|
a, |
|
c, |
|
e |
|
], |
|
[ |
|
b, |
|
d, |
|
f |
|
], |
|
[ |
|
0, |
|
0, |
|
1 |
|
] |
|
], |
|
x, y, z, res; |
|
for (x = 0; x < 3; x++) { |
|
for (y = 0; y < 3; y++) { |
|
res = 0; |
|
for (z = 0; z < 3; z++) { |
|
res += me.matrix[x][z] * matrix[z][y]; |
|
} |
|
out[x][y] = res; |
|
} |
|
} |
|
me.matrix = out; |
|
}, |
|
prepend: function(a, b, c, d, e, f) { |
|
var me = this, |
|
out = [ |
|
[], |
|
[], |
|
[] |
|
], |
|
matrix = [ |
|
[ |
|
a, |
|
c, |
|
e |
|
], |
|
[ |
|
b, |
|
d, |
|
f |
|
], |
|
[ |
|
0, |
|
0, |
|
1 |
|
] |
|
], |
|
x, y, z, res; |
|
for (x = 0; x < 3; x++) { |
|
for (y = 0; y < 3; y++) { |
|
res = 0; |
|
for (z = 0; z < 3; z++) { |
|
res += matrix[x][z] * me.matrix[z][y]; |
|
} |
|
out[x][y] = res; |
|
} |
|
} |
|
me.matrix = out; |
|
}, |
|
invert: function() { |
|
var matrix = this.matrix, |
|
a = matrix[0][0], |
|
b = matrix[1][0], |
|
c = matrix[0][1], |
|
d = matrix[1][1], |
|
e = matrix[0][2], |
|
f = matrix[1][2], |
|
x = a * d - b * c; |
|
return new Ext.draw.Matrix(d / x, -b / x, -c / x, a / x, (c * f - d * e) / x, (b * e - a * f) / x); |
|
}, |
|
clone: function() { |
|
var matrix = this.matrix, |
|
a = matrix[0][0], |
|
b = matrix[1][0], |
|
c = matrix[0][1], |
|
d = matrix[1][1], |
|
e = matrix[0][2], |
|
f = matrix[1][2]; |
|
return new Ext.draw.Matrix(a, b, c, d, e, f); |
|
}, |
|
translate: function(x, y) { |
|
this.prepend(1, 0, 0, 1, x, y); |
|
}, |
|
scale: function(x, y, cx, cy) { |
|
var me = this; |
|
if (y == null) { |
|
y = x; |
|
} |
|
me.add(x, 0, 0, y, cx * (1 - x), cy * (1 - y)); |
|
}, |
|
rotate: function(a, x, y) { |
|
a = Ext.draw.Draw.rad(a); |
|
var me = this, |
|
cos = +Math.cos(a).toFixed(9), |
|
sin = +Math.sin(a).toFixed(9); |
|
me.add(cos, sin, -sin, cos, x - cos * x + sin * y, -(sin * x) + y - cos * y); |
|
}, |
|
x: function(x, y) { |
|
var matrix = this.matrix; |
|
return x * matrix[0][0] + y * matrix[0][1] + matrix[0][2]; |
|
}, |
|
y: function(x, y) { |
|
var matrix = this.matrix; |
|
return x * matrix[1][0] + y * matrix[1][1] + matrix[1][2]; |
|
}, |
|
get: function(i, j) { |
|
return +this.matrix[i][j].toFixed(4); |
|
}, |
|
toString: function() { |
|
var me = this; |
|
return [ |
|
me.get(0, 0), |
|
me.get(0, 1), |
|
me.get(1, 0), |
|
me.get(1, 1), |
|
0, |
|
0 |
|
].join(); |
|
}, |
|
toSvg: function() { |
|
var me = this; |
|
return "matrix(" + [ |
|
me.get(0, 0), |
|
me.get(1, 0), |
|
me.get(0, 1), |
|
me.get(1, 1), |
|
me.get(0, 2), |
|
me.get(1, 2) |
|
].join() + ")"; |
|
}, |
|
toFilter: function(dx, dy) { |
|
var me = this; |
|
dx = dx || 0; |
|
dy = dy || 0; |
|
return "progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', filterType='bilinear', M11=" + me.get(0, 0) + ", M12=" + me.get(0, 1) + ", M21=" + me.get(1, 0) + ", M22=" + me.get(1, 1) + ", Dx=" + (me.get(0, 2) + dx) + ", Dy=" + (me.get(1, 2) + dy) + ")"; |
|
}, |
|
offset: function() { |
|
var matrix = this.matrix; |
|
return [ |
|
(matrix[0][2] || 0).toFixed(4), |
|
(matrix[1][2] || 0).toFixed(4) |
|
]; |
|
}, |
|
// Split matrix into Translate, Scale, Shear, and Rotate. |
|
split: function() { |
|
function norm(a) { |
|
return a[0] * a[0] + a[1] * a[1]; |
|
} |
|
function normalize(a) { |
|
var mag = Math.sqrt(norm(a)); |
|
a[0] /= mag; |
|
a[1] /= mag; |
|
} |
|
var matrix = this.matrix, |
|
out = { |
|
translateX: matrix[0][2], |
|
translateY: matrix[1][2] |
|
}, |
|
row; |
|
// scale and shear |
|
row = [ |
|
[ |
|
matrix[0][0], |
|
matrix[0][1] |
|
], |
|
[ |
|
matrix[1][1], |
|
matrix[1][1] |
|
] |
|
]; |
|
out.scaleX = Math.sqrt(norm(row[0])); |
|
normalize(row[0]); |
|
out.shear = row[0][0] * row[1][0] + row[0][1] * row[1][1]; |
|
row[1] = [ |
|
row[1][0] - row[0][0] * out.shear, |
|
row[1][1] - row[0][1] * out.shear |
|
]; |
|
out.scaleY = Math.sqrt(norm(row[1])); |
|
normalize(row[1]); |
|
out.shear /= out.scaleY; |
|
// rotation |
|
out.rotate = Math.asin(-row[0][1]); |
|
out.isSimple = !+out.shear.toFixed(9) && (out.scaleX.toFixed(9) == out.scaleY.toFixed(9) || !out.rotate); |
|
return out; |
|
} |
|
}); |
|
|
|
/** |
|
* DD implementation for Panels. |
|
* @private |
|
*/ |
|
Ext.define('Ext.draw.SpriteDD', { |
|
extend: 'Ext.dd.DragSource', |
|
constructor: function(sprite, cfg) { |
|
var me = this, |
|
el = sprite.el; |
|
me.sprite = sprite; |
|
me.el = el; |
|
me.dragData = { |
|
el: el, |
|
sprite: sprite |
|
}; |
|
me.callParent([ |
|
el, |
|
cfg |
|
]); |
|
me.sprite.setStyle('cursor', 'move'); |
|
}, |
|
showFrame: Ext.emptyFn, |
|
createFrame: Ext.emptyFn, |
|
getDragEl: function(e) { |
|
return this.el; |
|
}, |
|
getRegion: function() { |
|
var me = this, |
|
el = me.el, |
|
pos, x1, x2, y1, y2, t, r, b, l, bbox, sprite; |
|
sprite = me.sprite; |
|
bbox = sprite.getBBox(); |
|
try { |
|
pos = Ext.Element.getXY(el); |
|
} catch (e) {} |
|
if (!pos) { |
|
return null; |
|
} |
|
x1 = pos[0]; |
|
x2 = x1 + bbox.width; |
|
y1 = pos[1]; |
|
y2 = y1 + bbox.height; |
|
return new Ext.util.Region(y1, x2, y2, x1); |
|
}, |
|
/* |
|
TODO(nico): Cumulative translations in VML are handled |
|
differently than in SVG. While in SVG we specify the translation |
|
relative to the original x, y position attributes, in VML the translation |
|
is a delta between the last position of the object (modified by the last |
|
translation) and the new one. |
|
|
|
In VML the translation alters the position |
|
of the object, we should change that or alter the SVG impl. |
|
*/ |
|
startDrag: function(x, y) { |
|
var me = this, |
|
attr = me.sprite.attr; |
|
me.prev = me.sprite.surface.transformToViewBox(x, y); |
|
}, |
|
onDrag: function(e) { |
|
var xy = e.getXY(), |
|
me = this, |
|
sprite = me.sprite, |
|
attr = sprite.attr, |
|
dx, dy; |
|
xy = me.sprite.surface.transformToViewBox(xy[0], xy[1]); |
|
dx = xy[0] - me.prev[0]; |
|
dy = xy[1] - me.prev[1]; |
|
sprite.setAttributes({ |
|
translate: { |
|
x: attr.translation.x + dx, |
|
y: attr.translation.y + dy |
|
} |
|
}, true); |
|
me.prev = xy; |
|
}, |
|
setDragElPos: function() { |
|
// Disable automatic DOM move in DD that spoils layout of VML engine. |
|
return false; |
|
} |
|
}); |
|
|
|
/** |
|
* A Sprite is an object rendered in a Drawing surface. |
|
* |
|
* ## Types |
|
* |
|
* The following sprite types are supported: |
|
* |
|
* ### Rect |
|
* |
|
* Rectangle requires `width` and `height` attributes: |
|
* |
|
* @example |
|
* Ext.create('Ext.draw.Component', { |
|
* renderTo: Ext.getBody(), |
|
* width: 200, |
|
* height: 200, |
|
* items: [{ |
|
* type: 'rect', |
|
* width: 100, |
|
* height: 50, |
|
* radius: 10, |
|
* fill: 'green', |
|
* opacity: 0.5, |
|
* stroke: 'red', |
|
* 'stroke-width': 2 |
|
* }] |
|
* }); |
|
* |
|
* ### Circle |
|
* |
|
* Circle requires `x`, `y` and `radius` attributes: |
|
* |
|
* @example |
|
* Ext.create('Ext.draw.Component', { |
|
* renderTo: Ext.getBody(), |
|
* width: 200, |
|
* height: 200, |
|
* items: [{ |
|
* type: 'circle', |
|
* radius: 90, |
|
* x: 100, |
|
* y: 100, |
|
* fill: 'blue' |
|
* }] |
|
* }); |
|
* |
|
* ### Ellipse |
|
* |
|
* Ellipse requires `x`, `y`, `radiusX` and `radiusY` attributes: |
|
* |
|
* @example |
|
* Ext.create('Ext.draw.Component', { |
|
* renderTo: Ext.getBody(), |
|
* width: 200, |
|
* height: 200, |
|
* items: [{ |
|
* type: "ellipse", |
|
* radiusX: 100, |
|
* radiusY: 50, |
|
* x: 100, |
|
* y: 100, |
|
* fill: 'red' |
|
* }] |
|
* }); |
|
* |
|
* ### Path |
|
* |
|
* Path requires the `path` attribute: |
|
* |
|
* @example |
|
* Ext.create('Ext.draw.Component', { |
|
* renderTo: Ext.getBody(), |
|
* width: 200, |
|
* height: 200, |
|
* items: [{ |
|
* type: "path", |
|
* path: "M-66.6 26C-66.6 26 -75 22 -78.2 18.4C-81.4 14.8 -80.948 19.966 " + |
|
* "-85.8 19.6C-91.647 19.159 -90.6 3.2 -90.6 3.2L-94.6 10.8C-94.6 " + |
|
* "10.8 -95.8 25.2 -87.8 22.8C-83.893 21.628 -82.6 23.2 -84.2 " + |
|
* "24C-85.8 24.8 -78.6 25.2 -81.4 26.8C-84.2 28.4 -69.8 23.2 -72.2 " + |
|
* "33.6L-66.6 26z", |
|
* fill: "purple" |
|
* }] |
|
* }); |
|
* |
|
* ### Text |
|
* |
|
* Text requires the `text` attribute: |
|
* |
|
* @example |
|
* Ext.create('Ext.draw.Component', { |
|
* renderTo: Ext.getBody(), |
|
* width: 200, |
|
* height: 200, |
|
* items: [{ |
|
* type: "text", |
|
* text: "Hello, Sprite!", |
|
* fill: "green", |
|
* font: "18px monospace" |
|
* }] |
|
* }); |
|
* |
|
* ### Image |
|
* |
|
* Image requires `width`, `height` and `src` attributes: |
|
* |
|
* @example |
|
* Ext.create('Ext.draw.Component', { |
|
* renderTo: Ext.getBody(), |
|
* width: 200, |
|
* height: 200, |
|
* items: [{ |
|
* type: "image", |
|
* src: "http://www.sencha.com/img/apple-touch-icon.png", |
|
* width: 200, |
|
* height: 200 |
|
* }] |
|
* }); |
|
* |
|
* ## Creating and adding a Sprite to a Surface |
|
* |
|
* See {@link Ext.draw.Surface} documentation. |
|
* |
|
* ## Transforming sprites |
|
* |
|
* See {@link #setAttributes} method documentation for examples on how to translate, scale and rotate the sprites. |
|
* |
|
*/ |
|
Ext.define('Ext.draw.Sprite', { |
|
/* Begin Definitions */ |
|
mixins: { |
|
observable: 'Ext.util.Observable', |
|
animate: 'Ext.util.Animate' |
|
}, |
|
requires: [ |
|
'Ext.draw.SpriteDD' |
|
], |
|
/* End Definitions */ |
|
/** |
|
* @cfg {String} type The type of the sprite. |
|
* Possible options are 'circle', 'ellipse', 'path', 'rect', 'text', 'image'. |
|
* |
|
* See {@link Ext.draw.Sprite} class documentation for examples of all types. |
|
*/ |
|
/** |
|
* @cfg {Number} width The width of the rect or image sprite. |
|
*/ |
|
/** |
|
* @cfg {Number} height The height of the rect or image sprite. |
|
*/ |
|
/** |
|
* @cfg {Number} radius The radius of the circle sprite. Or in case of rect sprite, the border radius. |
|
*/ |
|
/** |
|
* @cfg {Number} radiusX The radius of the ellipse sprite along x-axis. |
|
*/ |
|
/** |
|
* @cfg {Number} radiusY The radius of the ellipse sprite along y-axis. |
|
*/ |
|
/** |
|
* @cfg {Number} x Sprite position along the x-axis. |
|
*/ |
|
/** |
|
* @cfg {Number} y Sprite position along the y-axis. |
|
*/ |
|
/** |
|
* @cfg {String} path The path of the path sprite written in SVG-like path syntax. |
|
*/ |
|
/** |
|
* @cfg {Number} opacity The opacity of the sprite. A number between 0 and 1. |
|
*/ |
|
/** |
|
* @cfg {String} fill The fill color. |
|
*/ |
|
/** |
|
* @cfg {String} stroke The stroke color. |
|
*/ |
|
/** |
|
* @cfg {Number} stroke-width The width of the stroke. |
|
* |
|
* Note that this attribute needs to be quoted when used. Like so: |
|
* |
|
* "stroke-width": 12, |
|
*/ |
|
/** |
|
* @cfg {String} font Used with text type sprites. The full font description. |
|
* Uses the same syntax as the CSS font parameter |
|
*/ |
|
/** |
|
* @cfg {String} text The actual text to render in text sprites. |
|
*/ |
|
/** |
|
* @cfg {String} src Path to the image to show in image sprites. |
|
*/ |
|
/** |
|
* @cfg {String/String[]} group The group that this sprite belongs to, or an array of groups. |
|
* Only relevant when added to a {@link Ext.draw.Surface Surface}. |
|
*/ |
|
/** |
|
* @cfg {Boolean} draggable True to make the sprite draggable. |
|
*/ |
|
dirty: false, |
|
dirtyHidden: false, |
|
dirtyTransform: false, |
|
dirtyPath: true, |
|
dirtyFont: true, |
|
zIndexDirty: true, |
|
/** |
|
* @property {Boolean} isSprite |
|
* `true` in this class to identify an object as an instantiated Sprite, or subclass thereof. |
|
*/ |
|
isSprite: true, |
|
zIndex: 0, |
|
fontProperties: [ |
|
'font', |
|
'font-size', |
|
'font-weight', |
|
'font-style', |
|
'font-family', |
|
'text-anchor', |
|
'text' |
|
], |
|
pathProperties: [ |
|
'x', |
|
'y', |
|
'd', |
|
'path', |
|
'height', |
|
'width', |
|
'radius', |
|
'r', |
|
'rx', |
|
'ry', |
|
'cx', |
|
'cy' |
|
], |
|
/** |
|
* @event |
|
* Fires before the sprite is destroyed. Return false from an event handler to stop the destroy. |
|
* @param {Ext.draw.Sprite} this |
|
*/ |
|
/** |
|
* @event |
|
* Fires after the sprite is destroyed. |
|
* @param {Ext.draw.Sprite} this |
|
*/ |
|
/** |
|
* @event |
|
* Fires after the sprite markup is rendered. |
|
* @param {Ext.draw.Sprite} this |
|
*/ |
|
/** |
|
* @event |
|
* @inheritdoc Ext.dom.Element#mousedown |
|
*/ |
|
/** |
|
* @event |
|
* @inheritdoc Ext.dom.Element#mouseup |
|
*/ |
|
/** |
|
* @event |
|
* @inheritdoc Ext.dom.Element#mouseover |
|
*/ |
|
/** |
|
* @event |
|
* @inheritdoc Ext.dom.Element#mouseout |
|
*/ |
|
/** |
|
* @event |
|
* @inheritdoc Ext.dom.Element#mousemove |
|
*/ |
|
/** |
|
* @event |
|
* @inheritdoc Ext.dom.Element#click |
|
*/ |
|
constructor: function(config) { |
|
var me = this; |
|
config = Ext.merge({}, config || {}); |
|
me.id = Ext.id(null, 'ext-sprite-'); |
|
me.transformations = []; |
|
Ext.copyTo(this, config, 'surface,group,type,draggable'); |
|
//attribute bucket |
|
me.bbox = {}; |
|
me.attr = { |
|
zIndex: 0, |
|
translation: { |
|
x: null, |
|
y: null |
|
}, |
|
rotation: { |
|
degrees: null, |
|
x: null, |
|
y: null |
|
}, |
|
scaling: { |
|
x: null, |
|
y: null, |
|
cx: null, |
|
cy: null |
|
} |
|
}; |
|
//delete not bucket attributes |
|
delete config.surface; |
|
delete config.group; |
|
delete config.type; |
|
delete config.draggable; |
|
me.setAttributes(config); |
|
me.mixins.observable.constructor.apply(this, arguments); |
|
}, |
|
/** |
|
* @property {Ext.dd.DragSource} dd |
|
* If this Sprite is configured {@link #draggable}, this property will contain |
|
* an instance of {@link Ext.dd.DragSource} which handles dragging the Sprite. |
|
* |
|
* The developer must provide implementations of the abstract methods of {@link Ext.dd.DragSource} |
|
* in order to supply behaviour for each stage of the drag/drop process. See {@link #draggable}. |
|
*/ |
|
initDraggable: function() { |
|
var me = this; |
|
//create element if it doesn't exist. |
|
if (!me.el) { |
|
me.surface.createSpriteElement(me); |
|
} |
|
me.dd = new Ext.draw.SpriteDD(me, Ext.isBoolean(me.draggable) ? null : me.draggable); |
|
me.on('beforedestroy', me.dd.destroy, me.dd); |
|
}, |
|
/** |
|
* Change the attributes of the sprite. |
|
* |
|
* ## Translation |
|
* |
|
* For translate, the configuration object contains x and y attributes that indicate where to |
|
* translate the object. For example: |
|
* |
|
* sprite.setAttributes({ |
|
* translate: { |
|
* x: 10, |
|
* y: 10 |
|
* } |
|
* }, true); |
|
* |
|
* |
|
* ## Rotation |
|
* |
|
* For rotation, the configuration object contains x and y attributes for the center of the rotation (which are optional), |
|
* and a `degrees` attribute that specifies the rotation in degrees. For example: |
|
* |
|
* sprite.setAttributes({ |
|
* rotate: { |
|
* degrees: 90 |
|
* } |
|
* }, true); |
|
* |
|
* That example will create a 90 degrees rotation using the centroid of the Sprite as center of rotation, whereas: |
|
* |
|
* sprite.setAttributes({ |
|
* rotate: { |
|
* x: 0, |
|
* y: 0, |
|
* degrees: 90 |
|
* } |
|
* }, true); |
|
* |
|
* will create a rotation around the `(0, 0)` axis. |
|
* |
|
* |
|
* ## Scaling |
|
* |
|
* For scaling, the configuration object contains x and y attributes for the x-axis and y-axis scaling. For example: |
|
* |
|
* sprite.setAttributes({ |
|
* scale: { |
|
* x: 10, |
|
* y: 3 |
|
* } |
|
* }, true); |
|
* |
|
* You can also specify the center of scaling by adding `cx` and `cy` as properties: |
|
* |
|
* sprite.setAttributes({ |
|
* scale: { |
|
* cx: 0, |
|
* cy: 0, |
|
* x: 10, |
|
* y: 3 |
|
* } |
|
* }, true); |
|
* |
|
* That last example will scale a sprite taking as centers of scaling the `(0, 0)` coordinate. |
|
* |
|
* @param {Object} attrs attributes to be changed on the sprite. |
|
* @param {Boolean} redraw Flag to immediately draw the change. |
|
* @return {Ext.draw.Sprite} this |
|
*/ |
|
setAttributes: function(attrs, redraw) { |
|
var me = this, |
|
fontProps = me.fontProperties, |
|
fontPropsLength = fontProps.length, |
|
pathProps = me.pathProperties, |
|
pathPropsLength = pathProps.length, |
|
hasSurface = !!me.surface, |
|
custom = hasSurface && me.surface.customAttributes || {}, |
|
spriteAttrs = me.attr, |
|
dirtyBBox = false, |
|
attr, i, newTranslation, translation, newRotate, rotation, newScaling, scaling; |
|
attrs = Ext.apply({}, attrs); |
|
for (attr in custom) { |
|
if (attrs.hasOwnProperty(attr) && typeof custom[attr] == "function") { |
|
Ext.apply(attrs, custom[attr].apply(me, [].concat(attrs[attr]))); |
|
} |
|
} |
|
// Flag a change in hidden |
|
if (!!attrs.hidden !== !!spriteAttrs.hidden) { |
|
me.dirtyHidden = true; |
|
} |
|
// Flag path change |
|
for (i = 0; i < pathPropsLength; i++) { |
|
attr = pathProps[i]; |
|
if (attr in attrs && attrs[attr] !== spriteAttrs[attr]) { |
|
me.dirtyPath = true; |
|
dirtyBBox = true; |
|
break; |
|
} |
|
} |
|
// Flag zIndex change |
|
if ('zIndex' in attrs) { |
|
me.zIndexDirty = true; |
|
} |
|
// Flag font/text change |
|
if ('text' in attrs) { |
|
me.dirtyFont = true; |
|
dirtyBBox = true; |
|
attrs.text = me.transformText(attrs.text); |
|
} |
|
for (i = 0; i < fontPropsLength; i++) { |
|
attr = fontProps[i]; |
|
if (attr in attrs && attrs[attr] !== spriteAttrs[attr]) { |
|
me.dirtyFont = true; |
|
dirtyBBox = true; |
|
break; |
|
} |
|
} |
|
newTranslation = attrs.translation || attrs.translate; |
|
delete attrs.translate; |
|
delete attrs.translation; |
|
translation = spriteAttrs.translation; |
|
if (newTranslation) { |
|
if (('x' in newTranslation && newTranslation.x !== translation.x) || ('y' in newTranslation && newTranslation.y !== translation.y)) { |
|
me.dirtyTransform = true; |
|
translation.x = newTranslation.x; |
|
translation.y = newTranslation.y; |
|
} |
|
} |
|
newRotate = attrs.rotation || attrs.rotate; |
|
rotation = spriteAttrs.rotation; |
|
delete attrs.rotate; |
|
delete attrs.rotation; |
|
if (newRotate) { |
|
if (('x' in newRotate && newRotate.x !== rotation.x) || ('y' in newRotate && newRotate.y !== rotation.y) || ('degrees' in newRotate && newRotate.degrees !== rotation.degrees)) { |
|
me.dirtyTransform = true; |
|
rotation.x = newRotate.x; |
|
rotation.y = newRotate.y; |
|
rotation.degrees = newRotate.degrees; |
|
} |
|
} |
|
newScaling = attrs.scaling || attrs.scale; |
|
scaling = spriteAttrs.scaling; |
|
delete attrs.scale; |
|
delete attrs.scaling; |
|
if (newScaling) { |
|
if (('x' in newScaling && newScaling.x !== scaling.x) || ('y' in newScaling && newScaling.y !== scaling.y) || ('cx' in newScaling && newScaling.cx !== scaling.cx) || ('cy' in newScaling && newScaling.cy !== scaling.cy)) { |
|
me.dirtyTransform = true; |
|
scaling.x = newScaling.x; |
|
scaling.y = newScaling.y; |
|
scaling.cx = newScaling.cx; |
|
scaling.cy = newScaling.cy; |
|
} |
|
} |
|
// If the bbox is changed, then the bbox based transforms should be invalidated. |
|
if (!me.dirtyTransform && dirtyBBox) { |
|
if (spriteAttrs.scaling.x === null || spriteAttrs.scaling.y === null || spriteAttrs.rotation.y === null || spriteAttrs.rotation.y === null) { |
|
me.dirtyTransform = true; |
|
} |
|
} |
|
Ext.apply(spriteAttrs, attrs); |
|
me.dirty = true; |
|
if (redraw === true && hasSurface) { |
|
me.redraw(); |
|
} |
|
return this; |
|
}, |
|
transformText: Ext.identityFn, |
|
/** |
|
* Retrieves the bounding box of the sprite. |
|
* This will be returned as an object with x, y, width, and height properties. |
|
* @return {Object} bbox |
|
*/ |
|
getBBox: function() { |
|
return this.surface.getBBox(this); |
|
}, |
|
/** |
|
* Set the text of a Text Sprite. |
|
* @param {String} text The text to display. |
|
* @return {Ext.draw.Sprite} this |
|
*/ |
|
setText: function(text) { |
|
this.attr.text = text; |
|
this.surface.applyAttrs(this); |
|
return this; |
|
}, |
|
/** |
|
* Hides the sprite. |
|
* @param {Boolean} redraw Flag to immediately draw the change. |
|
* @return {Ext.draw.Sprite} this |
|
*/ |
|
hide: function(redraw) { |
|
this.setAttributes({ |
|
hidden: true |
|
}, redraw); |
|
return this; |
|
}, |
|
/** |
|
* Shows the sprite. |
|
* @param {Boolean} redraw Flag to immediately draw the change. |
|
* @return {Ext.draw.Sprite} this |
|
*/ |
|
show: function(redraw) { |
|
this.setAttributes({ |
|
hidden: false |
|
}, redraw); |
|
return this; |
|
}, |
|
/** |
|
* Removes the sprite. |
|
* @return {Boolean} True if sprite was successfully removed. |
|
* False when there was no surface to remove it from. |
|
*/ |
|
remove: function() { |
|
if (this.surface) { |
|
this.surface.remove(this); |
|
return true; |
|
} |
|
return false; |
|
}, |
|
onRemove: function() { |
|
this.surface.onRemove(this); |
|
}, |
|
/** |
|
* Removes the sprite and clears all listeners. |
|
*/ |
|
destroy: function() { |
|
var me = this; |
|
if (me.fireEvent('beforedestroy', me) !== false) { |
|
me.remove(); |
|
me.surface.onDestroy(me); |
|
me.clearListeners(); |
|
me.fireEvent('destroy'); |
|
} |
|
}, |
|
/** |
|
* Redraws the sprite. |
|
* @return {Ext.draw.Sprite} this |
|
*/ |
|
redraw: function() { |
|
var me = this, |
|
changed = !me.el || me.dirty, |
|
surface = me.surface, |
|
owner; |
|
surface.renderItem(me); |
|
// This would be better handled higher up in the hierarchy, but |
|
// we'll check these properties here for performance reasons |
|
// to prevent extraneous function calls |
|
if (changed) { |
|
owner = surface.owner; |
|
if (!me.isBackground && owner && (owner.viewBox || owner.autoSize)) { |
|
owner.configureSurfaceSize(); |
|
} |
|
} |
|
return this; |
|
}, |
|
/** |
|
* Wrapper for setting style properties, also takes single object parameter of multiple styles. |
|
* @param {String/Object} property The style property to be set, or an object of multiple styles. |
|
* @param {String} value (optional) The value to apply to the given property, or null if an object was passed. |
|
* @return {Ext.draw.Sprite} this |
|
*/ |
|
setStyle: function() { |
|
this.el.setStyle.apply(this.el, arguments); |
|
return this; |
|
}, |
|
/** |
|
* Adds one or more CSS classes to the element. Duplicate classes are automatically filtered out. Note this method |
|
* is severly limited in VML. |
|
* @param {String/String[]} className The CSS class to add, or an array of classes |
|
* @return {Ext.draw.Sprite} this |
|
*/ |
|
addCls: function(obj) { |
|
this.surface.addCls(this, obj); |
|
return this; |
|
}, |
|
/** |
|
* Removes one or more CSS classes from the element. |
|
* @param {String/String[]} className The CSS class to remove, or an array of classes. Note this method |
|
* is severly limited in VML. |
|
* @return {Ext.draw.Sprite} this |
|
*/ |
|
removeCls: function(obj) { |
|
this.surface.removeCls(this, obj); |
|
return this; |
|
} |
|
}); |
|
|
|
Ext.define('Ext.rtl.draw.Sprite', { |
|
override: 'Ext.draw.Sprite', |
|
/* |
|
* --------Using RTL text in charts-------- |
|
* |
|
* For RTL charts, the direction of the underlying SVG/VML elements is left |
|
* as LTR. This is to normalize cross browser differences that occur, especially since |
|
* getting the directions to work is mostly as simple as just flipping the order of |
|
* things. As such, by default the text will display in an LTR fashion as well. |
|
* |
|
* One of the possible solutions is to go through and add direction: rtl; on all |
|
* of the text elements, however there are 2 problems with this: |
|
* 1) It doesn't work at all with VML. |
|
* 2) With SVG, the text displays differently between FF & Chrome, and also |
|
* it affects the positioning of the text elements as well. |
|
* |
|
* The option we've gone for is to include the right left mark (below) and |
|
* prepend it to any text. It's the easiest solution and should cover enough |
|
* cases to be handled in the charting package. The RLM tells the browser to |
|
* interpret character groups in a RTL fashion. Text with RTL characters will |
|
* display correctly whether in RTL or LTR mode, the RLM affects how other characters |
|
* are displayed around it. |
|
* |
|
* Let's take the string (you'll need to paste these in browsers, somewhat of |
|
* a pain to get them to show up correctly in the LTR editor): |
|
* "10 \u05E9\u05DC\u05DD" |
|
* |
|
* The above is how it will be displayed without the RLM. |
|
* With the RLM, it will be display as: |
|
* |
|
* "\u200F10 \u05E9\u05DC\u05DD" |
|
* |
|
* As you can see, the ordering of the Hebrew characters do |
|
* not change, however the surrounding characters move around |
|
* relative to what's already there. |
|
*/ |
|
// This character is the right to left mark |
|
// http://en.wikipedia.org/wiki/Right-to-left_mark |
|
// It is used to group characters in an RTL manner |
|
RLM: '\u200f', |
|
// A simple regex to match most strong RTL characters. Indicates that |
|
// the string contains RTL characters |
|
rtlRe: /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/, |
|
transformText: function(text) { |
|
var me = this, |
|
surface = me.surface; |
|
if (text && surface && surface.isRtl && !Ext.isNumber(text) && me.rtlRe.test(text)) { |
|
// IE9m will display a strange visual artefact when showing |
|
// text with the RLM and there are no RTL characters in the string. |
|
// IE6 & 7 will still show the artefact, it seems to be unavoidable. |
|
return me.RLM + text; |
|
} |
|
return me.callParent(arguments); |
|
} |
|
}); |
|
|
|
/** |
|
* This class encapsulates a drawn text item as rendered by the Ext.draw package within a Component which can be |
|
* then used anywhere in an ExtJS application just like any other Component. |
|
* |
|
* ## Example usage |
|
* |
|
* @example |
|
* Ext.create('Ext.panel.Panel', { |
|
* title: 'Panel with VerticalTextItem', |
|
* width: 300, |
|
* height: 200, |
|
* lbar: { |
|
* layout: { |
|
* align: 'center' |
|
* }, |
|
* items: [{ |
|
* xtype: 'text', |
|
* text: 'Sample VerticalTextItem', |
|
* degrees: 90 |
|
* }] |
|
* }, |
|
* renderTo: Ext.getBody() |
|
* }); |
|
* |
|
* @constructor |
|
* Creates a new Text Component |
|
* @param {Object} text A config object containing a `text` property, a `degrees` property, |
|
* and, optionally, a `styleSelector` property which specifies a selector which provides CSS rules to |
|
* give font family, size and color to the drawn text. |
|
*/ |
|
Ext.define('Ext.draw.Text', { |
|
extend: 'Ext.draw.Component', |
|
uses: [ |
|
'Ext.util.CSS' |
|
], |
|
alias: 'widget.text', |
|
/** |
|
* @cfg {String} text |
|
* The text to display (html tags are <b>not</b> accepted) |
|
*/ |
|
text: '', |
|
/** |
|
* @cfg {String} styleSelector |
|
* A CSS selector string which matches a style rule in the document stylesheet from which |
|
* the text's font properties are read. |
|
* |
|
* **Drawn** text is not styled by CSS, but by properties set during its construction, so these styles |
|
* must be programatically read from a stylesheet rule found via a selector at construction time. |
|
*/ |
|
/** |
|
* @cfg {Number} degrees |
|
* The angle by which to initially rotate the text clockwise. Defaults to zero. |
|
*/ |
|
focusable: false, |
|
viewBox: false, |
|
autoSize: true, |
|
baseCls: Ext.baseCSSPrefix + 'surface ' + Ext.baseCSSPrefix + 'draw-text', |
|
initComponent: function() { |
|
var me = this; |
|
me.textConfig = Ext.apply({ |
|
type: 'text', |
|
text: me.text, |
|
rotate: { |
|
degrees: me.degrees || 0 |
|
} |
|
}, me.textStyle); |
|
Ext.apply(me.textConfig, me.getStyles(me.styleSelectors || me.styleSelector)); |
|
// Surface is created from the *initialConfig*, not the current object state, |
|
// So the generated items must go into the initialConfig |
|
me.initialConfig.items = [ |
|
me.textConfig |
|
]; |
|
me.callParent(arguments); |
|
}, |
|
/** |
|
* @private |
|
* Accumulates a style object based upon the styles specified in document stylesheets |
|
* by an array of CSS selectors |
|
*/ |
|
getStyles: function(selectors) { |
|
selectors = Ext.Array.from(selectors); |
|
var i = 0, |
|
len = selectors.length, |
|
rule, style, prop, |
|
result = {}; |
|
for (; i < len; i++) { |
|
// Get the style rule which exactly matches the selector. |
|
rule = Ext.util.CSS.getRule(selectors[i]); |
|
if (rule) { |
|
style = rule.style; |
|
if (style) { |
|
Ext.apply(result, { |
|
'font-family': style.fontFamily, |
|
'font-weight': style.fontWeight, |
|
'line-height': style.lineHeight, |
|
'font-size': style.fontSize, |
|
fill: style.color |
|
}); |
|
} |
|
} |
|
} |
|
return result; |
|
}, |
|
/** |
|
* Sets the clockwise rotation angle relative to the horizontal axis. |
|
* @param {Number} degrees The clockwise angle (in degrees) from the horizontal axis |
|
* by which the text should be rotated. |
|
*/ |
|
setAngle: function(degrees) { |
|
var me = this, |
|
surface, sprite; |
|
if (me.rendered) { |
|
surface = me.surface; |
|
sprite = surface.items.items[0]; |
|
me.degrees = degrees; |
|
sprite.setAttributes({ |
|
rotate: { |
|
degrees: degrees |
|
} |
|
}, true); |
|
if (me.autoSize || me.viewBox) { |
|
me.updateLayout(); |
|
} |
|
} else { |
|
me.degrees = degrees; |
|
} |
|
}, |
|
/** |
|
* Updates this item's text. |
|
* @param {String} t The text to display (html **not** accepted). |
|
*/ |
|
setText: function(text) { |
|
var me = this, |
|
surface, sprite; |
|
if (me.rendered) { |
|
surface = me.surface; |
|
sprite = surface.items.items[0]; |
|
me.text = text || ''; |
|
surface.remove(sprite); |
|
me.textConfig.type = 'text'; |
|
me.textConfig.text = me.text; |
|
sprite = surface.add(me.textConfig); |
|
sprite.setAttributes({ |
|
rotate: { |
|
degrees: me.degrees |
|
} |
|
}, true); |
|
if (me.autoSize || me.viewBox) { |
|
me.updateLayout(); |
|
} |
|
} else { |
|
me.on({ |
|
render: function() { |
|
me.setText(text); |
|
}, |
|
single: true |
|
}); |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* Exports a {@link Ext.draw.Surface Surface} to an image. To do this, |
|
* the svg string must be sent to a remote server and processed. |
|
* |
|
* # Sending the data |
|
* |
|
* A post request is made to the URL. The following fields are sent: |
|
* |
|
* + width: The width of the image |
|
* + height: The height of the image |
|
* + type: The image type to save as, see {@link #supportedTypes} |
|
* + svg: The svg string for the surface |
|
* |
|
* # The response |
|
* |
|
* It is expected that the user will be prompted with an image download. |
|
* As such, the following options should be set on the server: |
|
* |
|
* + Content-Disposition: 'attachment, filename="chart.png"' |
|
* + Content-Type: 'image/png' |
|
* |
|
* **Important**: By default, chart data is sent to a server operated |
|
* by Sencha to do data processing. You may change this default by |
|
* setting the {@link #defaultUrl} of this class. |
|
* In addition, please note that this service only creates PNG images. |
|
*/ |
|
Ext.define('Ext.draw.engine.ImageExporter', { |
|
singleton: true, |
|
/** |
|
* @property {String} [defaultUrl="http://svg.sencha.io"] |
|
* The default URL to submit the form request. |
|
*/ |
|
defaultUrl: 'http://svg.sencha.io', |
|
/** |
|
* @property {Array} [supportedTypes=["image/png", "image/jpeg"]] |
|
* A list of export types supported by the server |
|
*/ |
|
supportedTypes: [ |
|
'image/png', |
|
'image/jpeg' |
|
], |
|
/** |
|
* @property {String} [widthParam="width"] |
|
* The name of the width parameter to be sent to the server. |
|
* The Sencha IO server expects it to be the default value. |
|
*/ |
|
widthParam: 'width', |
|
/** |
|
* @property {String} [heightParam="height"] |
|
* The name of the height parameter to be sent to the server. |
|
* The Sencha IO server expects it to be the default value. |
|
*/ |
|
heightParam: 'height', |
|
/** |
|
* @property {String} [typeParam="type"] |
|
* The name of the type parameter to be sent to the server. |
|
* The Sencha IO server expects it to be the default value. |
|
*/ |
|
typeParam: 'type', |
|
/** |
|
* @property {String} [svgParam="svg"] |
|
* The name of the svg parameter to be sent to the server. |
|
* The Sencha IO server expects it to be the default value. |
|
*/ |
|
svgParam: 'svg', |
|
formCls: Ext.baseCSSPrefix + 'hide-display', |
|
/** |
|
* Exports the surface to an image |
|
* @param {Ext.draw.Surface} surface The surface to export |
|
* @param {Object} [config] The following config options are supported: |
|
* |
|
* @param {Number} config.width A width to send to the server for |
|
* configuring the image width. |
|
* |
|
* @param {Number} config.height A height to send to the server for |
|
* configuring the image height. |
|
* |
|
* @param {String} config.url The url to post the data to. Defaults to |
|
* the {@link #defaultUrl} configuration on the class. |
|
* |
|
* @param {String} config.type The type of image to export. See the |
|
* {@link #supportedTypes} |
|
* |
|
* @param {String} config.widthParam The name of the width parameter to send |
|
* to the server. Defaults to {@link #widthParam} |
|
* |
|
* @param {String} config.heightParam The name of the height parameter to send |
|
* to the server. Defaults to {@link #heightParam} |
|
* |
|
* @param {String} config.typeParam The name of the type parameter to send |
|
* to the server. Defaults to {@link #typeParam} |
|
* |
|
* @param {String} config.svgParam The name of the svg parameter to send |
|
* to the server. Defaults to {@link #svgParam} |
|
* |
|
* @return {Boolean} True if the surface was successfully sent to the server. |
|
*/ |
|
generate: function(surface, config) { |
|
config = config || {}; |
|
var me = this, |
|
type = config.type, |
|
form; |
|
if (Ext.Array.indexOf(me.supportedTypes, type) === -1) { |
|
return false; |
|
} |
|
form = Ext.getBody().createChild({ |
|
tag: 'form', |
|
method: 'POST', |
|
action: config.url || me.defaultUrl, |
|
cls: me.formCls, |
|
children: [ |
|
{ |
|
tag: 'input', |
|
type: 'hidden', |
|
name: config.widthParam || me.widthParam, |
|
value: config.width || surface.width |
|
}, |
|
{ |
|
tag: 'input', |
|
type: 'hidden', |
|
name: config.heightParam || me.heightParam, |
|
value: config.height || surface.height |
|
}, |
|
{ |
|
tag: 'input', |
|
type: 'hidden', |
|
name: config.typeParam || me.typeParam, |
|
value: type |
|
}, |
|
{ |
|
tag: 'input', |
|
type: 'hidden', |
|
name: config.svgParam || me.svgParam |
|
} |
|
] |
|
}); |
|
// Assign the data on the value so it doesn't get messed up in the html insertion |
|
form.last(null, true).value = Ext.draw.engine.SvgExporter.generate(surface); |
|
form.dom.submit(); |
|
form.remove(); |
|
return true; |
|
} |
|
}); |
|
|
|
/** |
|
* Provides specific methods to draw with SVG. |
|
*/ |
|
Ext.define('Ext.draw.engine.Svg', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.draw.Surface', |
|
requires: [ |
|
'Ext.draw.Draw', |
|
'Ext.draw.Sprite', |
|
'Ext.draw.Matrix', |
|
'Ext.Element' |
|
], |
|
/* End Definitions */ |
|
engine: 'Svg', |
|
trimRe: /^\s+|\s+$/g, |
|
spacesRe: /\s+/, |
|
xlink: "http:/" + "/www.w3.org/1999/xlink", |
|
translateAttrs: { |
|
radius: "r", |
|
radiusX: "rx", |
|
radiusY: "ry", |
|
path: "d", |
|
lineWidth: "stroke-width", |
|
fillOpacity: "fill-opacity", |
|
strokeOpacity: "stroke-opacity", |
|
strokeLinejoin: "stroke-linejoin" |
|
}, |
|
parsers: {}, |
|
fontRe: /^font-?/, |
|
minDefaults: { |
|
circle: { |
|
cx: 0, |
|
cy: 0, |
|
r: 0, |
|
fill: "none", |
|
stroke: null, |
|
"stroke-width": null, |
|
opacity: null, |
|
"fill-opacity": null, |
|
"stroke-opacity": null |
|
}, |
|
ellipse: { |
|
cx: 0, |
|
cy: 0, |
|
rx: 0, |
|
ry: 0, |
|
fill: "none", |
|
stroke: null, |
|
"stroke-width": null, |
|
opacity: null, |
|
"fill-opacity": null, |
|
"stroke-opacity": null |
|
}, |
|
rect: { |
|
x: 0, |
|
y: 0, |
|
width: 0, |
|
height: 0, |
|
rx: 0, |
|
ry: 0, |
|
fill: "none", |
|
stroke: null, |
|
"stroke-width": null, |
|
opacity: null, |
|
"fill-opacity": null, |
|
"stroke-opacity": null |
|
}, |
|
text: { |
|
x: 0, |
|
y: 0, |
|
"text-anchor": "start", |
|
"font-family": null, |
|
"font-size": null, |
|
"font-weight": null, |
|
"font-style": null, |
|
fill: "#000", |
|
stroke: null, |
|
"stroke-width": null, |
|
opacity: null, |
|
"fill-opacity": null, |
|
"stroke-opacity": null |
|
}, |
|
path: { |
|
d: "M0,0", |
|
fill: "none", |
|
stroke: null, |
|
"stroke-width": null, |
|
opacity: null, |
|
"fill-opacity": null, |
|
"stroke-opacity": null |
|
}, |
|
image: { |
|
x: 0, |
|
y: 0, |
|
width: 0, |
|
height: 0, |
|
preserveAspectRatio: "none", |
|
opacity: null |
|
} |
|
}, |
|
createSvgElement: function(type, attrs) { |
|
var el = this.domRef.createElementNS("http:/" + "/www.w3.org/2000/svg", type), |
|
key; |
|
if (attrs) { |
|
for (key in attrs) { |
|
el.setAttribute(key, String(attrs[key])); |
|
} |
|
} |
|
return el; |
|
}, |
|
createSpriteElement: function(sprite) { |
|
// Create svg element and append to the DOM. |
|
var el = this.createSvgElement(sprite.type); |
|
el.id = sprite.id; |
|
if (el.style) { |
|
el.style.webkitTapHighlightColor = "rgba(0,0,0,0)"; |
|
} |
|
sprite.el = Ext.get(el); |
|
this.applyZIndex(sprite); |
|
//performs the insertion |
|
sprite.matrix = new Ext.draw.Matrix(); |
|
sprite.bbox = { |
|
plain: 0, |
|
transform: 0 |
|
}; |
|
this.applyAttrs(sprite); |
|
this.applyTransformations(sprite); |
|
sprite.fireEvent("render", sprite); |
|
return el; |
|
}, |
|
getBBoxText: function(sprite) { |
|
var bbox = {}, |
|
bb, height, width, i, ln, el; |
|
if (sprite && sprite.el) { |
|
el = sprite.el.dom; |
|
try { |
|
bbox = el.getBBox(); |
|
return bbox; |
|
} catch (e) {} |
|
// Firefox 3.0.x plays badly here |
|
bbox = { |
|
x: bbox.x, |
|
y: Infinity, |
|
width: 0, |
|
height: 0 |
|
}; |
|
ln = el.getNumberOfChars(); |
|
for (i = 0; i < ln; i++) { |
|
bb = el.getExtentOfChar(i); |
|
bbox.y = Math.min(bb.y, bbox.y); |
|
height = bb.y + bb.height - bbox.y; |
|
bbox.height = Math.max(bbox.height, height); |
|
width = bb.x + bb.width - bbox.x; |
|
bbox.width = Math.max(bbox.width, width); |
|
} |
|
return bbox; |
|
} |
|
}, |
|
hide: function() { |
|
Ext.get(this.el).hide(); |
|
}, |
|
show: function() { |
|
Ext.get(this.el).show(); |
|
}, |
|
hidePrim: function(sprite) { |
|
this.addCls(sprite, Ext.baseCSSPrefix + 'hide-visibility'); |
|
}, |
|
showPrim: function(sprite) { |
|
this.removeCls(sprite, Ext.baseCSSPrefix + 'hide-visibility'); |
|
}, |
|
getDefs: function() { |
|
return this._defs || (this._defs = this.createSvgElement("defs")); |
|
}, |
|
transform: function(sprite, matrixOnly) { |
|
var me = this, |
|
matrix = new Ext.draw.Matrix(), |
|
transforms = sprite.transformations, |
|
transformsLength = transforms.length, |
|
i = 0, |
|
transform, type; |
|
for (; i < transformsLength; i++) { |
|
transform = transforms[i]; |
|
type = transform.type; |
|
if (type == "translate") { |
|
matrix.translate(transform.x, transform.y); |
|
} else if (type == "rotate") { |
|
matrix.rotate(transform.degrees, transform.x, transform.y); |
|
} else if (type == "scale") { |
|
matrix.scale(transform.x, transform.y, transform.centerX, transform.centerY); |
|
} |
|
} |
|
sprite.matrix = matrix; |
|
if (!matrixOnly) { |
|
sprite.el.set({ |
|
transform: matrix.toSvg() |
|
}); |
|
} |
|
}, |
|
setSize: function(width, height) { |
|
var me = this, |
|
el = me.el; |
|
width = +width || me.width; |
|
height = +height || me.height; |
|
me.width = width; |
|
me.height = height; |
|
el.setSize(width, height); |
|
el.set({ |
|
width: width, |
|
height: height |
|
}); |
|
me.callParent([ |
|
width, |
|
height |
|
]); |
|
}, |
|
/** |
|
* Get the region for the surface's canvas area |
|
* @return {Ext.util.Region} |
|
*/ |
|
getRegion: function() { |
|
// Mozilla requires using the background rect because the svg element returns an |
|
// incorrect region. Webkit gives no region for the rect and must use the svg element. |
|
var svgXY = this.el.getXY(), |
|
rectXY = this.bgRect.getXY(), |
|
max = Math.max, |
|
x = max(svgXY[0], rectXY[0]), |
|
y = max(svgXY[1], rectXY[1]); |
|
return { |
|
left: x, |
|
top: y, |
|
right: x + this.width, |
|
bottom: y + this.height |
|
}; |
|
}, |
|
onRemove: function(sprite) { |
|
if (sprite.el) { |
|
sprite.el.destroy(); |
|
delete sprite.el; |
|
} |
|
this.callParent(arguments); |
|
}, |
|
setViewBox: function(x, y, width, height) { |
|
if (isFinite(x) && isFinite(y) && isFinite(width) && isFinite(height)) { |
|
this.callParent(arguments); |
|
this.el.dom.setAttribute("viewBox", [ |
|
x, |
|
y, |
|
width, |
|
height |
|
].join(" ")); |
|
} |
|
}, |
|
render: function(container) { |
|
var me = this, |
|
cfg, el, defs, bgRect, webkitRect; |
|
if (!me.el) { |
|
cfg = { |
|
xmlns: "http:/" + "/www.w3.org/2000/svg", |
|
version: 1.1, |
|
width: me.width || 0, |
|
height: me.height || 0 |
|
}; |
|
if (me.forceLtr) { |
|
cfg.direction = 'ltr'; |
|
} |
|
el = me.createSvgElement('svg', cfg); |
|
defs = me.getDefs(); |
|
// Create a rect that is always the same size as the svg root; this serves 2 purposes: |
|
// (1) It allows mouse events to be fired over empty areas in Webkit, and (2) we can |
|
// use it rather than the svg element for retrieving the correct client rect of the |
|
// surface in Mozilla (see https://bugzilla.mozilla.org/show_bug.cgi?id=530985) |
|
bgRect = me.createSvgElement("rect", { |
|
width: "100%", |
|
height: "100%", |
|
fill: "#000", |
|
stroke: "none", |
|
opacity: 0 |
|
}); |
|
if (Ext.isSafari3) { |
|
// Rect that we will show/hide to fix old WebKit bug with rendering issues. |
|
webkitRect = me.createSvgElement("rect", { |
|
x: -10, |
|
y: -10, |
|
width: "110%", |
|
height: "110%", |
|
fill: "none", |
|
stroke: "#000" |
|
}); |
|
} |
|
el.appendChild(defs); |
|
if (Ext.isSafari3) { |
|
el.appendChild(webkitRect); |
|
} |
|
el.appendChild(bgRect); |
|
container.appendChild(el); |
|
me.el = Ext.get(el); |
|
me.bgRect = Ext.get(bgRect); |
|
if (Ext.isSafari3) { |
|
me.webkitRect = Ext.get(webkitRect); |
|
me.webkitRect.hide(); |
|
} |
|
me.el.on({ |
|
scope: me, |
|
mouseup: me.onMouseUp, |
|
mousedown: me.onMouseDown, |
|
mouseover: me.onMouseOver, |
|
mouseout: me.onMouseOut, |
|
mousemove: me.onMouseMove, |
|
mouseenter: me.onMouseEnter, |
|
mouseleave: me.onMouseLeave, |
|
click: me.onClick, |
|
dblclick: me.onDblClick |
|
}); |
|
} |
|
me.renderAll(); |
|
}, |
|
// private |
|
onMouseEnter: function(e) { |
|
if (this.el.parent().getRegion().contains(e.getPoint())) { |
|
this.fireEvent('mouseenter', e); |
|
} |
|
}, |
|
// private |
|
onMouseLeave: function(e) { |
|
if (!this.el.parent().getRegion().contains(e.getPoint())) { |
|
this.fireEvent('mouseleave', e); |
|
} |
|
}, |
|
// @private - Normalize a delegated single event from the main container to each sprite and sprite group |
|
processEvent: function(name, e) { |
|
var target = e.getTarget(), |
|
surface = this.surface, |
|
sprite; |
|
this.fireEvent(name, e); |
|
// We wrap text types in a tspan, sprite is the parent. |
|
if (target.nodeName == "tspan" && target.parentNode) { |
|
target = target.parentNode; |
|
} |
|
sprite = this.items.get(target.id); |
|
if (sprite) { |
|
sprite.fireEvent(name, sprite, e); |
|
} |
|
}, |
|
/* @private - Wrap SVG text inside a tspan to allow for line wrapping. In addition this normallizes |
|
* the baseline for text the vertical middle of the text to be the same as VML. |
|
*/ |
|
tuneText: function(sprite, attrs) { |
|
var el = sprite.el.dom, |
|
tspans = [], |
|
height, tspan, text, i, ln, texts, factor, x; |
|
if (attrs.hasOwnProperty("text")) { |
|
//only create new tspans for text lines if the text has been |
|
//updated or if it's the first time we're setting the text |
|
//into the sprite. |
|
//get the actual rendered text. |
|
text = sprite.tspans && Ext.Array.map(sprite.tspans, function(t) { |
|
return t.textContent; |
|
}).join(''); |
|
if (!sprite.tspans || attrs.text != text) { |
|
tspans = this.setText(sprite, attrs.text); |
|
sprite.tspans = tspans; |
|
} else //for all other cases reuse the tspans previously created. |
|
{ |
|
tspans = sprite.tspans || []; |
|
} |
|
} |
|
// Normalize baseline via a DY shift of first tspan. Shift other rows by height * line height (1.2) |
|
if (tspans.length) { |
|
height = this.getBBoxText(sprite).height; |
|
x = sprite.el.dom.getAttribute("x"); |
|
for (i = 0 , ln = tspans.length; i < ln; i++) { |
|
// The text baseline for FireFox 3.0 and 3.5 is different than other SVG implementations |
|
// so we are going to normalize that here |
|
factor = (Ext.isFF3_0 || Ext.isFF3_5) ? 2 : 4; |
|
tspans[i].setAttribute("x", x); |
|
tspans[i].setAttribute("dy", i ? height * 1.2 : height / factor); |
|
} |
|
sprite.dirty = true; |
|
} |
|
}, |
|
setText: function(sprite, textString) { |
|
var me = this, |
|
el = sprite.el.dom, |
|
tspans = [], |
|
height, tspan, text, i, ln, texts; |
|
while (el.firstChild) { |
|
el.removeChild(el.firstChild); |
|
} |
|
// Wrap each row into tspan to emulate rows |
|
texts = String(textString).split("\n"); |
|
for (i = 0 , ln = texts.length; i < ln; i++) { |
|
text = texts[i]; |
|
if (text) { |
|
tspan = me.createSvgElement("tspan"); |
|
tspan.appendChild(document.createTextNode(Ext.htmlDecode(text))); |
|
el.appendChild(tspan); |
|
tspans[i] = tspan; |
|
} |
|
} |
|
return tspans; |
|
}, |
|
renderAll: function() { |
|
this.items.each(this.renderItem, this); |
|
}, |
|
renderItem: function(sprite) { |
|
if (!this.el) { |
|
return; |
|
} |
|
if (!sprite.el) { |
|
this.createSpriteElement(sprite); |
|
} |
|
if (sprite.zIndexDirty) { |
|
this.applyZIndex(sprite); |
|
} |
|
if (sprite.dirty) { |
|
this.applyAttrs(sprite); |
|
if (sprite.dirtyTransform) { |
|
this.applyTransformations(sprite); |
|
} |
|
} |
|
}, |
|
redraw: function(sprite) { |
|
sprite.dirty = sprite.zIndexDirty = true; |
|
this.renderItem(sprite); |
|
}, |
|
applyAttrs: function(sprite) { |
|
var me = this, |
|
el = sprite.el, |
|
group = sprite.group, |
|
sattr = sprite.attr, |
|
parsers = me.parsers, |
|
//Safari does not handle linear gradients correctly in quirksmode |
|
//ref: https://bugs.webkit.org/show_bug.cgi?id=41952 |
|
//ref: EXTJSIV-1472 |
|
gradientsMap = me.gradientsMap || {}, |
|
safariFix = Ext.isSafari && !Ext.isStrict, |
|
fontRe = me.fontRe, |
|
groups, i, ln, attrs, font, key, style, name, rect, fontProps, val; |
|
if (group) { |
|
groups = [].concat(group); |
|
ln = groups.length; |
|
for (i = 0; i < ln; i++) { |
|
group = groups[i]; |
|
me.getGroup(group).add(sprite); |
|
} |
|
delete sprite.group; |
|
} |
|
attrs = me.scrubAttrs(sprite) || {}; |
|
// if (sprite.dirtyPath) { |
|
sprite.bbox.plain = 0; |
|
sprite.bbox.transform = 0; |
|
if (sprite.type == "circle" || sprite.type == "ellipse") { |
|
attrs.cx = attrs.cx || attrs.x; |
|
attrs.cy = attrs.cy || attrs.y; |
|
} else if (sprite.type == "rect") { |
|
attrs.rx = attrs.ry = attrs.r; |
|
} else if (sprite.type == "path" && attrs.d) { |
|
attrs.d = Ext.draw.Draw.pathToString(Ext.draw.Draw.pathToAbsolute(attrs.d)); |
|
} |
|
sprite.dirtyPath = false; |
|
// } |
|
// else { |
|
// delete attrs.d; |
|
// } |
|
if (attrs['clip-rect']) { |
|
me.setClip(sprite, attrs); |
|
delete attrs['clip-rect']; |
|
} |
|
if (sprite.type == 'text' && attrs.font && sprite.dirtyFont) { |
|
el.set({ |
|
style: "font: " + attrs.font |
|
}); |
|
} |
|
if (sprite.type == "image") { |
|
el.dom.setAttributeNS(me.xlink, "href", attrs.src); |
|
} |
|
Ext.applyIf(attrs, me.minDefaults[sprite.type]); |
|
if (sprite.dirtyHidden) { |
|
(sattr.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite); |
|
sprite.dirtyHidden = false; |
|
} |
|
for (key in attrs) { |
|
val = attrs[key]; |
|
if (attrs.hasOwnProperty(key) && val != null) { |
|
//Safari does not handle linear gradients correctly in quirksmode |
|
//ref: https://bugs.webkit.org/show_bug.cgi?id=41952 |
|
//ref: EXTJSIV-1472 |
|
//if we're Safari in QuirksMode and we're applying some color attribute and the value of that |
|
//attribute is a reference to a gradient then assign a plain color to that value instead of the gradient. |
|
if (safariFix && ('color|stroke|fill'.indexOf(key) > -1) && (val in gradientsMap)) { |
|
val = gradientsMap[val]; |
|
} |
|
//hidden is not a proper SVG attribute. |
|
if (key == 'hidden' && sprite.type == 'text') { |
|
|
|
continue; |
|
} |
|
if (key in parsers) { |
|
el.dom.setAttribute(key, parsers[key](val, sprite, me)); |
|
} else { |
|
el.dom.setAttribute(key, val); |
|
if (fontRe.test(key)) { |
|
fontProps = fontProps || {}; |
|
fontProps[key] = val; |
|
el.setStyle(key, val); |
|
} |
|
} |
|
} |
|
} |
|
if (sprite.type == 'text') { |
|
me.tuneText(sprite, attrs); |
|
} |
|
sprite.dirtyFont = false; |
|
//set styles |
|
style = sattr.style; |
|
if (style) { |
|
el.setStyle(style); |
|
} |
|
sprite.dirty = false; |
|
if (Ext.isSafari3) { |
|
// Refreshing the view to fix bug EXTJSIV-1: rendering issue in old Safari 3 |
|
me.webkitRect.show(); |
|
Ext.defer(function() { |
|
me.webkitRect.hide(); |
|
}, 1); |
|
} |
|
}, |
|
setClip: function(sprite, params) { |
|
var me = this, |
|
rect = params["clip-rect"], |
|
clipEl, clipPath; |
|
if (rect) { |
|
if (sprite.clip) { |
|
sprite.clip.parentNode.parentNode.removeChild(sprite.clip.parentNode); |
|
} |
|
clipEl = me.createSvgElement('clipPath'); |
|
clipPath = me.createSvgElement('rect'); |
|
clipEl.id = Ext.id(null, 'ext-clip-'); |
|
clipPath.setAttribute("x", rect.x); |
|
clipPath.setAttribute("y", rect.y); |
|
clipPath.setAttribute("width", rect.width); |
|
clipPath.setAttribute("height", rect.height); |
|
clipEl.appendChild(clipPath); |
|
me.getDefs().appendChild(clipEl); |
|
sprite.el.dom.setAttribute("clip-path", "url(#" + clipEl.id + ")"); |
|
sprite.clip = clipPath; |
|
} |
|
}, |
|
// if (!attrs[key]) { |
|
// var clip = Ext.getDoc().dom.getElementById(sprite.el.getAttribute("clip-path").replace(/(^url\(#|\)$)/g, "")); |
|
// clip && clip.parentNode.removeChild(clip); |
|
// sprite.el.setAttribute("clip-path", ""); |
|
// delete attrss.clip; |
|
// } |
|
/** |
|
* Insert or move a given sprite's element to the correct place in the DOM list for its zIndex |
|
* @param {Ext.draw.Sprite} sprite |
|
*/ |
|
applyZIndex: function(sprite) { |
|
var me = this, |
|
items = me.items, |
|
idx = items.indexOf(sprite), |
|
el = sprite.el, |
|
prevEl; |
|
if (me.el.dom.childNodes[idx + 2] !== el.dom) { |
|
//shift by 2 to account for defs and bg rect |
|
if (idx > 0) { |
|
// Find the first previous sprite which has its DOM element created already |
|
do { |
|
prevEl = items.getAt(--idx).el; |
|
} while (!prevEl && idx > 0); |
|
} |
|
el.insertAfter(prevEl || me.bgRect); |
|
} |
|
sprite.zIndexDirty = false; |
|
}, |
|
createItem: function(config) { |
|
var sprite = new Ext.draw.Sprite(config); |
|
sprite.surface = this; |
|
return sprite; |
|
}, |
|
addGradient: function(gradient) { |
|
gradient = Ext.draw.Draw.parseGradient(gradient); |
|
var me = this, |
|
ln = gradient.stops.length, |
|
vector = gradient.vector, |
|
//Safari does not handle linear gradients correctly in quirksmode |
|
//ref: https://bugs.webkit.org/show_bug.cgi?id=41952 |
|
//ref: EXTJSIV-1472 |
|
usePlain = Ext.isSafari && !Ext.isStrict, |
|
gradientEl, stop, stopEl, i, gradientsMap; |
|
gradientsMap = me.gradientsMap || {}; |
|
if (!usePlain) { |
|
if (gradient.type == "linear") { |
|
gradientEl = me.createSvgElement("linearGradient"); |
|
gradientEl.setAttribute("x1", vector[0]); |
|
gradientEl.setAttribute("y1", vector[1]); |
|
gradientEl.setAttribute("x2", vector[2]); |
|
gradientEl.setAttribute("y2", vector[3]); |
|
} else { |
|
gradientEl = me.createSvgElement("radialGradient"); |
|
gradientEl.setAttribute("cx", gradient.centerX); |
|
gradientEl.setAttribute("cy", gradient.centerY); |
|
gradientEl.setAttribute("r", gradient.radius); |
|
if (Ext.isNumber(gradient.focalX) && Ext.isNumber(gradient.focalY)) { |
|
gradientEl.setAttribute("fx", gradient.focalX); |
|
gradientEl.setAttribute("fy", gradient.focalY); |
|
} |
|
} |
|
gradientEl.id = gradient.id; |
|
me.getDefs().appendChild(gradientEl); |
|
for (i = 0; i < ln; i++) { |
|
stop = gradient.stops[i]; |
|
stopEl = me.createSvgElement("stop"); |
|
stopEl.setAttribute("offset", stop.offset + "%"); |
|
stopEl.setAttribute("stop-color", stop.color); |
|
stopEl.setAttribute("stop-opacity", stop.opacity); |
|
gradientEl.appendChild(stopEl); |
|
} |
|
} else { |
|
gradientsMap['url(#' + gradient.id + ')'] = gradient.stops[0].color; |
|
} |
|
me.gradientsMap = gradientsMap; |
|
}, |
|
/** |
|
* Checks if the specified CSS class exists on this element's DOM node. |
|
* @param {Ext.draw.Sprite} sprite The sprite to look into. |
|
* @param {String} className The CSS class to check for |
|
* @return {Boolean} True if the class exists, else false |
|
*/ |
|
hasCls: function(sprite, className) { |
|
return className && (' ' + (sprite.el.dom.getAttribute('class') || '') + ' ').indexOf(' ' + className + ' ') != -1; |
|
}, |
|
addCls: function(sprite, className) { |
|
var el = sprite.el, |
|
i, len, v, |
|
cls = [], |
|
curCls = el.getAttribute('class') || ''; |
|
// Separate case is for speed |
|
if (!Ext.isArray(className)) { |
|
if (typeof className == 'string' && !this.hasCls(sprite, className)) { |
|
el.set({ |
|
'class': curCls + ' ' + className |
|
}); |
|
} |
|
} else { |
|
for (i = 0 , len = className.length; i < len; i++) { |
|
v = className[i]; |
|
if (typeof v == 'string' && (' ' + curCls + ' ').indexOf(' ' + v + ' ') == -1) { |
|
cls.push(v); |
|
} |
|
} |
|
if (cls.length) { |
|
el.set({ |
|
'class': ' ' + cls.join(' ') |
|
}); |
|
} |
|
} |
|
}, |
|
removeCls: function(sprite, className) { |
|
var me = this, |
|
el = sprite.el, |
|
curCls = el.getAttribute('class') || '', |
|
i, idx, len, cls, elClasses; |
|
if (!Ext.isArray(className)) { |
|
className = [ |
|
className |
|
]; |
|
} |
|
if (curCls) { |
|
elClasses = curCls.replace(me.trimRe, ' ').split(me.spacesRe); |
|
for (i = 0 , len = className.length; i < len; i++) { |
|
cls = className[i]; |
|
if (typeof cls == 'string') { |
|
cls = cls.replace(me.trimRe, ''); |
|
idx = Ext.Array.indexOf(elClasses, cls); |
|
if (idx != -1) { |
|
Ext.Array.erase(elClasses, idx, 1); |
|
} |
|
} |
|
} |
|
el.set({ |
|
'class': elClasses.join(' ') |
|
}); |
|
} |
|
}, |
|
destroy: function() { |
|
var me = this; |
|
me.callParent(); |
|
if (me.el) { |
|
me.el.destroy(); |
|
} |
|
if (me._defs) { |
|
Ext.get(me._defs).destroy(); |
|
} |
|
if (me.bgRect) { |
|
Ext.get(me.bgRect).destroy(); |
|
} |
|
if (me.webkitRect) { |
|
Ext.get(me.webkitRect).destroy(); |
|
} |
|
delete me.el; |
|
} |
|
}); |
|
|
|
/** |
|
* A utility class for exporting a {@link Ext.draw.Surface Surface} to a string |
|
* that may be saved or used for processing on the server. |
|
* |
|
* @singleton |
|
*/ |
|
Ext.define('Ext.draw.engine.SvgExporter', function() { |
|
var commaRe = /,/g, |
|
fontRegex = /(-?\d*\.?\d*){1}(em|ex|px|in|cm|mm|pt|pc|%)\s('*.*'*)/, |
|
rgbColorRe = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/g, |
|
rgbaColorRe = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,([\d\.]+)\)/g, |
|
surface, len, width, height, |
|
init = function(s) { |
|
surface = s; |
|
len = surface.length; |
|
width = surface.width; |
|
height = surface.height; |
|
}, |
|
spriteProcessor = { |
|
path: function(sprite) { |
|
var attr = sprite.attr, |
|
path = attr.path, |
|
pathString = '', |
|
props, p, pLen; |
|
if (Ext.isArray(path[0])) { |
|
pLen = path.length; |
|
for (p = 0; p < pLen; p++) { |
|
pathString += path[p].join(' '); |
|
} |
|
} else if (Ext.isArray(path)) { |
|
pathString = path.join(' '); |
|
} else { |
|
pathString = path.replace(commaRe, ' '); |
|
} |
|
props = toPropertyString({ |
|
d: pathString, |
|
fill: attr.fill || 'none', |
|
stroke: attr.stroke, |
|
'fill-opacity': attr.opacity, |
|
'stroke-width': attr['stroke-width'], |
|
'stroke-opacity': attr['stroke-opacity'], |
|
"z-index": attr.zIndex, |
|
transform: sprite.matrix.toSvg() |
|
}); |
|
return '<path ' + props + '/>'; |
|
}, |
|
text: function(sprite) { |
|
// TODO |
|
// implement multi line support (@see Svg.js tuneText) |
|
var attr = sprite.attr, |
|
match = fontRegex.exec(attr.font), |
|
size = (match && match[1]) || "12", |
|
// default font family is Arial |
|
family = (match && match[3]) || 'Arial', |
|
text = attr.text, |
|
factor = (Ext.isFF3_0 || Ext.isFF3_5) ? 2 : 4, |
|
tspanString = '', |
|
props; |
|
sprite.getBBox(); |
|
tspanString += '<tspan x="' + (attr.x || '') + '" dy="'; |
|
tspanString += (size / factor) + '">'; |
|
tspanString += Ext.htmlEncode(text) + '</tspan>'; |
|
props = toPropertyString({ |
|
x: attr.x, |
|
y: attr.y, |
|
'font-size': size, |
|
'font-family': family, |
|
'font-weight': attr['font-weight'], |
|
'text-anchor': attr['text-anchor'], |
|
// if no fill property is set it will be black |
|
fill: attr.fill || '#000', |
|
'fill-opacity': attr.opacity, |
|
transform: sprite.matrix.toSvg() |
|
}); |
|
return '<text ' + props + '>' + tspanString + '</text>'; |
|
}, |
|
rect: function(sprite) { |
|
var attr = sprite.attr, |
|
props = toPropertyString({ |
|
x: attr.x, |
|
y: attr.y, |
|
rx: attr.rx, |
|
ry: attr.ry, |
|
width: attr.width, |
|
height: attr.height, |
|
fill: attr.fill || 'none', |
|
'fill-opacity': attr.opacity, |
|
stroke: attr.stroke, |
|
'stroke-opacity': attr['stroke-opacity'], |
|
'stroke-width': attr['stroke-width'], |
|
transform: sprite.matrix && sprite.matrix.toSvg() |
|
}); |
|
return '<rect ' + props + '/>'; |
|
}, |
|
circle: function(sprite) { |
|
var attr = sprite.attr, |
|
props = toPropertyString({ |
|
cx: attr.x, |
|
cy: attr.y, |
|
r: attr.radius, |
|
fill: attr.translation.fill || attr.fill || 'none', |
|
'fill-opacity': attr.opacity, |
|
stroke: attr.stroke, |
|
'stroke-opacity': attr['stroke-opacity'], |
|
'stroke-width': attr['stroke-width'], |
|
transform: sprite.matrix.toSvg() |
|
}); |
|
return '<circle ' + props + ' />'; |
|
}, |
|
image: function(sprite) { |
|
var attr = sprite.attr, |
|
props = toPropertyString({ |
|
x: attr.x - (attr.width / 2 >> 0), |
|
y: attr.y - (attr.height / 2 >> 0), |
|
width: attr.width, |
|
height: attr.height, |
|
'xlink:href': attr.src, |
|
transform: sprite.matrix.toSvg() |
|
}); |
|
return '<image ' + props + ' />'; |
|
} |
|
}, |
|
svgHeader = function() { |
|
var svg = '<?xml version="1.0" standalone="yes"?>'; |
|
svg += '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'; |
|
return svg; |
|
}, |
|
svgContent = function() { |
|
var svg = '<svg width="' + width + 'px" height="' + height + 'px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">', |
|
defs = '', |
|
item, itemsLen, items, gradient, getSvgString, colorstops, stop, coll, keys, colls, k, kLen, key, collI, i, j, stopsLen, sortedItems, za, zb; |
|
items = surface.items.items; |
|
itemsLen = items.length; |
|
getSvgString = function(node) { |
|
var childs = node.childNodes, |
|
childLength = childs.length, |
|
i = 0, |
|
attrLength, j, |
|
svgString = '', |
|
child, attr, tagName, attrItem; |
|
for (; i < childLength; i++) { |
|
child = childs[i]; |
|
attr = child.attributes; |
|
tagName = child.tagName; |
|
svgString += '<' + tagName; |
|
for (j = 0 , attrLength = attr.length; j < attrLength; j++) { |
|
attrItem = attr.item(j); |
|
svgString += ' ' + attrItem.name + '="' + attrItem.value + '"'; |
|
} |
|
svgString += '>'; |
|
if (child.childNodes.length > 0) { |
|
svgString += getSvgString(child); |
|
} |
|
svgString += '</' + tagName + '>'; |
|
} |
|
return svgString; |
|
}; |
|
if (surface.getDefs) { |
|
defs = getSvgString(surface.getDefs()); |
|
} else { |
|
// IE |
|
coll = surface.gradientsColl; |
|
if (coll) { |
|
keys = coll.keys; |
|
colls = coll.items; |
|
k = 0; |
|
kLen = keys.length; |
|
} |
|
for (; k < kLen; k++) { |
|
key = keys[k]; |
|
collI = colls[k]; |
|
gradient = surface.gradientsColl.getByKey(key); |
|
defs += '<linearGradient id="' + key + '" x1="0" y1="0" x2="1" y2="1">'; |
|
var color = gradient.colors.replace(rgbColorRe, 'rgb($1|$2|$3)'); |
|
color = color.replace(rgbaColorRe, 'rgba($1|$2|$3|$4)'); |
|
colorstops = color.split(','); |
|
for (i = 0 , stopsLen = colorstops.length; i < stopsLen; i++) { |
|
stop = colorstops[i].split(' '); |
|
color = Ext.draw.Color.fromString(stop[1].replace(/\|/g, ',')); |
|
defs += '<stop offset="' + stop[0] + '" stop-color="' + color.toString() + '" stop-opacity="1"></stop>'; |
|
} |
|
defs += '</linearGradient>'; |
|
} |
|
} |
|
svg += '<defs>' + defs + '</defs>'; |
|
// thats the background rectangle |
|
svg += spriteProcessor.rect({ |
|
attr: { |
|
width: '100%', |
|
height: '100%', |
|
fill: '#fff', |
|
stroke: 'none', |
|
opacity: '0' |
|
} |
|
}); |
|
// Sort the items (stable sort guaranteed) |
|
sortedItems = new Array(itemsLen); |
|
for (i = 0; i < itemsLen; i++) { |
|
sortedItems[i] = i; |
|
} |
|
sortedItems.sort(function(a, b) { |
|
za = items[a].attr.zIndex || 0; |
|
zb = items[b].attr.zIndex || 0; |
|
if (za == zb) { |
|
return a - b; |
|
} |
|
return za - zb; |
|
}); |
|
for (i = 0; i < itemsLen; i++) { |
|
item = items[sortedItems[i]]; |
|
if (!item.attr.hidden) { |
|
svg += spriteProcessor[item.type](item); |
|
} |
|
} |
|
svg += '</svg>'; |
|
return svg; |
|
}, |
|
toPropertyString = function(obj) { |
|
var propString = '', |
|
key; |
|
for (key in obj) { |
|
if (obj.hasOwnProperty(key) && obj[key] != null) { |
|
propString += key + '="' + obj[key] + '" '; |
|
} |
|
} |
|
return propString; |
|
}; |
|
return { |
|
singleton: true, |
|
/** |
|
* Exports the passed surface to a SVG string representation |
|
* @param {Ext.draw.Surface} surface The surface to export |
|
* @param {Object} [config] Any configuration for the export. Currently this is |
|
* unused but may provide more options in the future |
|
* @return {String} The SVG as a string |
|
*/ |
|
generate: function(surface, config) { |
|
config = config || {}; |
|
init(surface); |
|
return svgHeader() + svgContent(); |
|
} |
|
}; |
|
}); |
|
|
|
/** |
|
* Provides specific methods to draw with VML. |
|
*/ |
|
Ext.define('Ext.draw.engine.Vml', { |
|
/* Begin Definitions */ |
|
extend: 'Ext.draw.Surface', |
|
requires: [ |
|
'Ext.draw.Draw', |
|
'Ext.draw.Color', |
|
'Ext.draw.Sprite', |
|
'Ext.draw.Matrix', |
|
'Ext.Element' |
|
], |
|
/* End Definitions */ |
|
engine: 'Vml', |
|
map: { |
|
M: "m", |
|
L: "l", |
|
C: "c", |
|
Z: "x", |
|
m: "t", |
|
l: "r", |
|
c: "v", |
|
z: "x" |
|
}, |
|
bitesRe: /([clmz]),?([^clmz]*)/gi, |
|
valRe: /-?[^,\s\-]+/g, |
|
fillUrlRe: /^url\(\s*['"]?([^\)]+?)['"]?\s*\)$/i, |
|
pathlike: /^(path|rect)$/, |
|
NonVmlPathRe: /[ahqstv]/ig, |
|
// Non-VML Pathing ops |
|
partialPathRe: /[clmz]/g, |
|
fontFamilyRe: /^['"]+|['"]+$/g, |
|
baseVmlCls: Ext.baseCSSPrefix + 'vml-base', |
|
vmlGroupCls: Ext.baseCSSPrefix + 'vml-group', |
|
spriteCls: Ext.baseCSSPrefix + 'vml-sprite', |
|
measureSpanCls: Ext.baseCSSPrefix + 'vml-measure-span', |
|
zoom: 21600, |
|
coordsize: 1000, |
|
coordorigin: '0 0', |
|
zIndexShift: 0, |
|
// VML uses CSS z-index and therefore doesn't need sprites to be kept in zIndex order |
|
orderSpritesByZIndex: false, |
|
// @private |
|
// Convert an SVG standard path into a VML path |
|
path2vml: function(path) { |
|
var me = this, |
|
nonVML = me.NonVmlPathRe, |
|
map = me.map, |
|
val = me.valRe, |
|
zoom = me.zoom, |
|
bites = me.bitesRe, |
|
command = Ext.Function.bind(Ext.draw.Draw.pathToAbsolute, Ext.draw.Draw), |
|
res, pa, p, r, i, ii, j, jj; |
|
if (String(path).match(nonVML)) { |
|
command = Ext.Function.bind(Ext.draw.Draw.path2curve, Ext.draw.Draw); |
|
} else if (!String(path).match(me.partialPathRe)) { |
|
res = String(path).replace(bites, function(all, command, args) { |
|
var vals = [], |
|
isMove = command.toLowerCase() == "m", |
|
res = map[command]; |
|
args.replace(val, function(value) { |
|
if (isMove && vals.length === 2) { |
|
res += vals + map[command == "m" ? "l" : "L"]; |
|
vals = []; |
|
} |
|
vals.push(Math.round(value * zoom)); |
|
}); |
|
return res + vals; |
|
}); |
|
return res; |
|
} |
|
pa = command(path); |
|
res = []; |
|
for (i = 0 , ii = pa.length; i < ii; i++) { |
|
p = pa[i]; |
|
r = pa[i][0].toLowerCase(); |
|
if (r == "z") { |
|
r = "x"; |
|
} |
|
for (j = 1 , jj = p.length; j < jj; j++) { |
|
r += Math.round(p[j] * me.zoom) + (j != jj - 1 ? "," : ""); |
|
} |
|
res.push(r); |
|
} |
|
return res.join(" "); |
|
}, |
|
// @private - set of attributes which need to be translated from the sprite API to the native browser API |
|
translateAttrs: { |
|
radius: "r", |
|
radiusX: "rx", |
|
radiusY: "ry", |
|
lineWidth: "stroke-width", |
|
fillOpacity: "fill-opacity", |
|
strokeOpacity: "stroke-opacity", |
|
strokeLinejoin: "stroke-linejoin" |
|
}, |
|
// @private - Minimun set of defaults for different types of sprites. |
|
minDefaults: { |
|
circle: { |
|
fill: "none", |
|
stroke: null, |
|
"stroke-width": null, |
|
opacity: null, |
|
"fill-opacity": null, |
|
"stroke-opacity": null |
|
}, |
|
ellipse: { |
|
cx: 0, |
|
cy: 0, |
|
rx: 0, |
|
ry: 0, |
|
fill: "none", |
|
stroke: null, |
|
"stroke-width": null, |
|
opacity: null, |
|
"fill-opacity": null, |
|
"stroke-opacity": null |
|
}, |
|
rect: { |
|
x: 0, |
|
y: 0, |
|
width: 0, |
|
height: 0, |
|
rx: 0, |
|
ry: 0, |
|
fill: "none", |
|
stroke: null, |
|
"stroke-width": null, |
|
opacity: null, |
|
"fill-opacity": null, |
|
"stroke-opacity": null |
|
}, |
|
text: { |
|
x: 0, |
|
y: 0, |
|
"text-anchor": "start", |
|
font: '10px "Arial"', |
|
fill: "#000", |
|
stroke: null, |
|
"stroke-width": null, |
|
opacity: null, |
|
"fill-opacity": null, |
|
"stroke-opacity": null |
|
}, |
|
path: { |
|
d: "M0,0", |
|
fill: "none", |
|
stroke: null, |
|
"stroke-width": null, |
|
opacity: null, |
|
"fill-opacity": null, |
|
"stroke-opacity": null |
|
}, |
|
image: { |
|
x: 0, |
|
y: 0, |
|
width: 0, |
|
height: 0, |
|
preserveAspectRatio: "none", |
|
opacity: null |
|
} |
|
}, |
|
// private |
|
onMouseEnter: function(e) { |
|
this.fireEvent("mouseenter", e); |
|
}, |
|
// private |
|
onMouseLeave: function(e) { |
|
this.fireEvent("mouseleave", e); |
|
}, |
|
// @private - Normalize a delegated single event from the main container to each sprite and sprite group |
|
processEvent: function(name, e) { |
|
var target = e.getTarget(), |
|
sprite; |
|
this.fireEvent(name, e); |
|
sprite = this.items.get(target.id); |
|
if (sprite) { |
|
sprite.fireEvent(name, sprite, e); |
|
} |
|
}, |
|
// Create the VML element/elements and append them to the DOM |
|
createSpriteElement: function(sprite) { |
|
var me = this, |
|
attr = sprite.attr, |
|
type = sprite.type, |
|
zoom = me.zoom, |
|
vml = sprite.vml || (sprite.vml = {}), |
|
el = (type === 'image') ? me.createNode('image') : me.createNode('shape'), |
|
path, skew, textPath; |
|
el.coordsize = zoom + ' ' + zoom; |
|
el.coordorigin = attr.coordorigin || "0 0"; |
|
Ext.get(el).addCls(me.spriteCls); |
|
if (type == "text") { |
|
vml.path = path = me.createNode("path"); |
|
path.textpathok = true; |
|
vml.textpath = textPath = me.createNode("textpath"); |
|
textPath.on = true; |
|
el.appendChild(textPath); |
|
el.appendChild(path); |
|
} |
|
el.id = sprite.id; |
|
sprite.el = Ext.get(el); |
|
sprite.el.setStyle('zIndex', -me.zIndexShift); |
|
me.el.appendChild(el); |
|
if (type !== 'image') { |
|
skew = me.createNode("skew"); |
|
skew.on = true; |
|
el.appendChild(skew); |
|
sprite.skew = skew; |
|
} |
|
sprite.matrix = new Ext.draw.Matrix(); |
|
sprite.bbox = { |
|
plain: null, |
|
transform: null |
|
}; |
|
this.applyAttrs(sprite); |
|
this.applyTransformations(sprite); |
|
sprite.fireEvent("render", sprite); |
|
return sprite.el; |
|
}, |
|
getBBoxText: function(sprite) { |
|
var vml = sprite.vml; |
|
return { |
|
x: vml.X + (vml.bbx || 0) - vml.W / 2, |
|
y: vml.Y - vml.H / 2, |
|
width: vml.W, |
|
height: vml.H |
|
}; |
|
}, |
|
applyAttrs: function(sprite) { |
|
var me = this, |
|
group = sprite.group, |
|
spriteAttr = sprite.attr, |
|
el = sprite.el, |
|
dom = el.dom, |
|
style, groups, i, ln, scrubbedAttrs, cx, cy, rx, ry; |
|
if (group) { |
|
groups = [].concat(group); |
|
ln = groups.length; |
|
for (i = 0; i < ln; i++) { |
|
group = groups[i]; |
|
me.getGroup(group).add(sprite); |
|
} |
|
delete sprite.group; |
|
} |
|
scrubbedAttrs = me.scrubAttrs(sprite) || {}; |
|
if (sprite.zIndexDirty) { |
|
me.setZIndex(sprite); |
|
} |
|
// Apply minimum default attributes |
|
Ext.applyIf(scrubbedAttrs, me.minDefaults[sprite.type]); |
|
if (sprite.type == 'image') { |
|
Ext.apply(sprite.attr, { |
|
x: scrubbedAttrs.x, |
|
y: scrubbedAttrs.y, |
|
width: scrubbedAttrs.width, |
|
height: scrubbedAttrs.height |
|
}); |
|
el.setStyle({ |
|
width: scrubbedAttrs.width + 'px', |
|
height: scrubbedAttrs.height + 'px' |
|
}); |
|
dom.src = scrubbedAttrs.src; |
|
} |
|
if (dom.href) { |
|
dom.href = scrubbedAttrs.href; |
|
} |
|
if (dom.title) { |
|
dom.title = scrubbedAttrs.title; |
|
} |
|
if (dom.target) { |
|
dom.target = scrubbedAttrs.target; |
|
} |
|
if (dom.cursor) { |
|
dom.cursor = scrubbedAttrs.cursor; |
|
} |
|
// Change visibility |
|
if (sprite.dirtyHidden) { |
|
(scrubbedAttrs.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite); |
|
sprite.dirtyHidden = false; |
|
} |
|
// Update path |
|
if (sprite.dirtyPath) { |
|
if (sprite.type == "circle" || sprite.type == "ellipse") { |
|
cx = scrubbedAttrs.x; |
|
cy = scrubbedAttrs.y; |
|
rx = scrubbedAttrs.rx || scrubbedAttrs.r || 0; |
|
ry = scrubbedAttrs.ry || scrubbedAttrs.r || 0; |
|
dom.path = Ext.String.format("ar{0},{1},{2},{3},{4},{1},{4},{1}", Math.round((cx - rx) * me.zoom), Math.round((cy - ry) * me.zoom), Math.round((cx + rx) * me.zoom), Math.round((cy + ry) * me.zoom), Math.round(cx * me.zoom)); |
|
sprite.dirtyPath = false; |
|
} else { |
|
sprite.attr.path = scrubbedAttrs.path = me.setPaths(sprite, scrubbedAttrs) || scrubbedAttrs.path; |
|
dom.path = me.path2vml(scrubbedAttrs.path); |
|
sprite.dirtyPath = false; |
|
} |
|
} |
|
// Apply clipping |
|
if ("clip-rect" in scrubbedAttrs) { |
|
me.setClip(sprite, scrubbedAttrs); |
|
} |
|
// Handle text (special handling required) |
|
if (sprite.type == "text") { |
|
me.setTextAttributes(sprite, scrubbedAttrs); |
|
} |
|
// Handle fill and opacity |
|
if (scrubbedAttrs.opacity || scrubbedAttrs['stroke-opacity'] || scrubbedAttrs.fill) { |
|
me.setFill(sprite, scrubbedAttrs); |
|
} |
|
// Handle stroke (all fills require a stroke element) |
|
if (scrubbedAttrs.stroke || scrubbedAttrs['stroke-opacity'] || scrubbedAttrs.fill) { |
|
me.setStroke(sprite, scrubbedAttrs); |
|
} |
|
//set styles |
|
style = spriteAttr.style; |
|
if (style) { |
|
el.setStyle(style); |
|
} |
|
sprite.dirty = false; |
|
}, |
|
setZIndex: function(sprite) { |
|
var me = this, |
|
zIndex = sprite.attr.zIndex, |
|
shift = me.zIndexShift, |
|
items, iLen, item, i; |
|
if (zIndex < shift) { |
|
// This means bad thing happened. |
|
// The algorithm below will guarantee O(n) time. |
|
items = me.items.items; |
|
iLen = items.length; |
|
for (i = 0; i < iLen; i++) { |
|
if ((zIndex = items[i].attr.zIndex) && zIndex < shift) { |
|
// zIndex is no longer useful this case |
|
shift = zIndex; |
|
} |
|
} |
|
me.zIndexShift = shift; |
|
for (i = 0; i < iLen; i++) { |
|
item = items[i]; |
|
if (item.el) { |
|
item.el.setStyle('zIndex', item.attr.zIndex - shift); |
|
} |
|
item.zIndexDirty = false; |
|
} |
|
} else if (sprite.el) { |
|
sprite.el.setStyle('zIndex', zIndex - shift); |
|
sprite.zIndexDirty = false; |
|
} |
|
}, |
|
// Normalize all virtualized types into paths. |
|
setPaths: function(sprite, params) { |
|
var spriteAttr = sprite.attr; |
|
// Clear bbox cache |
|
sprite.bbox.plain = null; |
|
sprite.bbox.transform = null; |
|
if (sprite.type == 'circle') { |
|
spriteAttr.rx = spriteAttr.ry = params.r; |
|
return Ext.draw.Draw.ellipsePath(sprite); |
|
} else if (sprite.type == 'ellipse') { |
|
spriteAttr.rx = params.rx; |
|
spriteAttr.ry = params.ry; |
|
return Ext.draw.Draw.ellipsePath(sprite); |
|
} else if (sprite.type == 'rect') { |
|
spriteAttr.rx = spriteAttr.ry = params.r; |
|
return Ext.draw.Draw.rectPath(sprite); |
|
} else if (sprite.type == 'path' && spriteAttr.path) { |
|
return Ext.draw.Draw.pathToAbsolute(spriteAttr.path); |
|
} |
|
return false; |
|
}, |
|
setFill: function(sprite, params) { |
|
var me = this, |
|
el = sprite.el.dom, |
|
fillEl = el.fill, |
|
newfill = false, |
|
gradient, fillUrl, rotation, angle; |
|
if (!fillEl) { |
|
// NOT an expando (but it sure looks like one)... |
|
fillEl = el.fill = me.createNode("fill"); |
|
newfill = true; |
|
} |
|
if (Ext.isArray(params.fill)) { |
|
params.fill = params.fill[0]; |
|
} |
|
if (params.fill == "none") { |
|
fillEl.on = false; |
|
} else { |
|
if (typeof params.opacity == "number") { |
|
fillEl.opacity = params.opacity; |
|
} |
|
if (typeof params["fill-opacity"] == "number") { |
|
fillEl.opacity = params["fill-opacity"]; |
|
} |
|
fillEl.on = true; |
|
if (typeof params.fill == "string") { |
|
fillUrl = params.fill.match(me.fillUrlRe); |
|
if (fillUrl) { |
|
fillUrl = fillUrl[1]; |
|
// If the URL matches one of the registered gradients, render that gradient |
|
if (fillUrl.charAt(0) == "#") { |
|
gradient = me.gradientsColl.getByKey(fillUrl.substring(1)); |
|
} |
|
if (gradient) { |
|
// VML angle is offset and inverted from standard, and must be adjusted to match rotation transform |
|
rotation = params.rotation; |
|
angle = -(gradient.angle + 270 + (rotation ? rotation.degrees : 0)) % 360; |
|
// IE will flip the angle at 0 degrees... |
|
if (angle === 0) { |
|
angle = 180; |
|
} |
|
fillEl.angle = angle; |
|
fillEl.type = "gradient"; |
|
fillEl.method = "sigma"; |
|
if (fillEl.colors) { |
|
fillEl.colors.value = gradient.colors; |
|
} else { |
|
fillEl.colors = gradient.colors; |
|
} |
|
} else // Otherwise treat it as an image |
|
{ |
|
fillEl.src = fillUrl; |
|
fillEl.type = "tile"; |
|
} |
|
} else { |
|
fillEl.color = Ext.draw.Color.toHex(params.fill); |
|
fillEl.src = ""; |
|
fillEl.type = "solid"; |
|
} |
|
} |
|
} |
|
if (newfill) { |
|
el.appendChild(fillEl); |
|
} |
|
}, |
|
setStroke: function(sprite, params) { |
|
var me = this, |
|
el = sprite.el.dom, |
|
strokeEl = sprite.strokeEl, |
|
newStroke = false, |
|
width, opacity; |
|
if (!strokeEl) { |
|
strokeEl = sprite.strokeEl = me.createNode("stroke"); |
|
newStroke = true; |
|
} |
|
if (Ext.isArray(params.stroke)) { |
|
params.stroke = params.stroke[0]; |
|
} |
|
if (!params.stroke || params.stroke == "none" || params.stroke == 0 || params["stroke-width"] == 0) { |
|
strokeEl.on = false; |
|
} else { |
|
strokeEl.on = true; |
|
if (params.stroke && !params.stroke.match(me.fillUrlRe)) { |
|
// VML does NOT support a gradient stroke :( |
|
strokeEl.color = Ext.draw.Color.toHex(params.stroke); |
|
} |
|
strokeEl.dashstyle = params["stroke-dasharray"] ? "dash" : "solid"; |
|
strokeEl.joinstyle = params["stroke-linejoin"]; |
|
strokeEl.endcap = params["stroke-linecap"] || "round"; |
|
strokeEl.miterlimit = params["stroke-miterlimit"] || 8; |
|
width = parseFloat(params["stroke-width"] || 1) * 0.75; |
|
opacity = params["stroke-opacity"] || 1; |
|
// VML Does not support stroke widths under 1, so we're going to fiddle with stroke-opacity instead. |
|
if (Ext.isNumber(width) && width < 1) { |
|
strokeEl.weight = 1; |
|
strokeEl.opacity = opacity * width; |
|
} else { |
|
strokeEl.weight = width; |
|
strokeEl.opacity = opacity; |
|
} |
|
} |
|
if (newStroke) { |
|
el.appendChild(strokeEl); |
|
} |
|
}, |
|
setClip: function(sprite, params) { |
|
var me = this, |
|
clipEl = sprite.clipEl, |
|
rect = String(params["clip-rect"]).split(me.separatorRe); |
|
if (!clipEl) { |
|
clipEl = sprite.clipEl = me.el.insertFirst(Ext.getDoc().dom.createElement("div")); |
|
clipEl.addCls(Ext.baseCSSPrefix + 'vml-sprite'); |
|
} |
|
if (rect.length == 4) { |
|
rect[2] = +rect[2] + (+rect[0]); |
|
rect[3] = +rect[3] + (+rect[1]); |
|
clipEl.setStyle("clip", Ext.String.format("rect({1}px {2}px {3}px {0}px)", rect[0], rect[1], rect[2], rect[3])); |
|
clipEl.setSize(me.el.width, me.el.height); |
|
} else { |
|
clipEl.setStyle("clip", ""); |
|
} |
|
}, |
|
setTextAttributes: function(sprite, params) { |
|
var me = this, |
|
vml = sprite.vml, |
|
textStyle = vml.textpath.style, |
|
spanCacheStyle = me.span.style, |
|
zoom = me.zoom, |
|
fontObj = { |
|
fontSize: "font-size", |
|
fontWeight: "font-weight", |
|
fontStyle: "font-style" |
|
}, |
|
fontProp, paramProp; |
|
if (sprite.dirtyFont) { |
|
if (params.font) { |
|
textStyle.font = spanCacheStyle.font = params.font; |
|
} |
|
if (params["font-family"]) { |
|
textStyle.fontFamily = '"' + params["font-family"].split(",")[0].replace(me.fontFamilyRe, "") + '"'; |
|
spanCacheStyle.fontFamily = params["font-family"]; |
|
} |
|
for (fontProp in fontObj) { |
|
paramProp = params[fontObj[fontProp]]; |
|
if (paramProp) { |
|
textStyle[fontProp] = spanCacheStyle[fontProp] = paramProp; |
|
} |
|
} |
|
me.setText(sprite, params.text); |
|
if (vml.textpath.string) { |
|
me.span.innerHTML = String(vml.textpath.string).replace(/</g, "<").replace(/&/g, "&").replace(/\n/g, "<br/>"); |
|
} |
|
vml.W = me.span.offsetWidth; |
|
vml.H = me.span.offsetHeight + 2; |
|
// TODO handle baseline differences and offset in VML Textpath |
|
// text-anchor emulation |
|
if (params["text-anchor"] == "middle") { |
|
textStyle["v-text-align"] = "center"; |
|
} else if (params["text-anchor"] == "end") { |
|
textStyle["v-text-align"] = "right"; |
|
vml.bbx = -Math.round(vml.W / 2); |
|
} else { |
|
textStyle["v-text-align"] = "left"; |
|
vml.bbx = Math.round(vml.W / 2); |
|
} |
|
} |
|
vml.X = params.x; |
|
vml.Y = params.y; |
|
vml.path.v = Ext.String.format("m{0},{1}l{2},{1}", Math.round(vml.X * zoom), Math.round(vml.Y * zoom), Math.round(vml.X * zoom) + 1); |
|
// Clear bbox cache |
|
sprite.bbox.plain = null; |
|
sprite.bbox.transform = null; |
|
sprite.dirtyFont = false; |
|
}, |
|
setText: function(sprite, text) { |
|
sprite.vml.textpath.string = Ext.htmlDecode(text); |
|
}, |
|
hide: function() { |
|
this.el.hide(); |
|
}, |
|
show: function() { |
|
this.el.show(); |
|
}, |
|
hidePrim: function(sprite) { |
|
sprite.el.addCls(Ext.baseCSSPrefix + 'hide-visibility'); |
|
}, |
|
showPrim: function(sprite) { |
|
sprite.el.removeCls(Ext.baseCSSPrefix + 'hide-visibility'); |
|
}, |
|
setSize: function(width, height) { |
|
var me = this; |
|
width = width || me.width; |
|
height = height || me.height; |
|
me.width = width; |
|
me.height = height; |
|
if (me.el) { |
|
// Size outer div |
|
if (width != undefined) { |
|
me.el.setWidth(width); |
|
} |
|
if (height != undefined) { |
|
me.el.setHeight(height); |
|
} |
|
} |
|
me.callParent(arguments); |
|
}, |
|
/** |
|
* @private Using the current viewBox property and the surface's width and height, calculate the |
|
* appropriate viewBoxShift that will be applied as a persistent transform to all sprites. |
|
*/ |
|
applyViewBox: function() { |
|
var me = this, |
|
viewBox = me.viewBox, |
|
width = me.width, |
|
height = me.height, |
|
items, iLen, i; |
|
me.callParent(); |
|
if (viewBox && (width || height)) { |
|
items = me.items.items; |
|
iLen = items.length; |
|
for (i = 0; i < iLen; i++) { |
|
me.applyTransformations(items[i]); |
|
} |
|
} |
|
}, |
|
onAdd: function(item) { |
|
this.callParent(arguments); |
|
if (this.el) { |
|
this.renderItem(item); |
|
} |
|
}, |
|
onRemove: function(sprite) { |
|
if (sprite.el) { |
|
sprite.el.destroy(); |
|
delete sprite.el; |
|
} |
|
this.callParent(arguments); |
|
}, |
|
render: function(container) { |
|
var me = this, |
|
doc = Ext.getDoc().dom, |
|
el; |
|
// VML Node factory method (createNode) |
|
if (!me.createNode) { |
|
try { |
|
if (!doc.namespaces.rvml) { |
|
doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml"); |
|
} |
|
me.createNode = function(tagName) { |
|
return doc.createElement("<rvml:" + tagName + ' class="rvml">'); |
|
}; |
|
} catch (e) { |
|
me.createNode = function(tagName) { |
|
return doc.createElement("<" + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">'); |
|
}; |
|
} |
|
} |
|
if (!me.el) { |
|
el = doc.createElement("div"); |
|
me.el = Ext.get(el); |
|
me.el.addCls(me.baseVmlCls); |
|
// Measuring span (offscrren) |
|
me.span = doc.createElement("span"); |
|
Ext.get(me.span).addCls(me.measureSpanCls); |
|
el.appendChild(me.span); |
|
me.el.setSize(me.width || 0, me.height || 0); |
|
container.appendChild(el); |
|
me.el.on({ |
|
scope: me, |
|
mouseup: me.onMouseUp, |
|
mousedown: me.onMouseDown, |
|
mouseover: me.onMouseOver, |
|
mouseout: me.onMouseOut, |
|
mousemove: me.onMouseMove, |
|
mouseenter: me.onMouseEnter, |
|
mouseleave: me.onMouseLeave, |
|
click: me.onClick, |
|
dblclick: me.onDblClick |
|
}); |
|
} |
|
me.renderAll(); |
|
}, |
|
renderAll: function() { |
|
this.items.each(this.renderItem, this); |
|
}, |
|
redraw: function(sprite) { |
|
sprite.dirty = true; |
|
this.renderItem(sprite); |
|
}, |
|
renderItem: function(sprite) { |
|
// Does the surface element exist? |
|
if (!this.el) { |
|
return; |
|
} |
|
// Create sprite element if necessary |
|
if (!sprite.el) { |
|
this.createSpriteElement(sprite); |
|
} |
|
if (sprite.dirty) { |
|
this.applyAttrs(sprite); |
|
if (sprite.dirtyTransform) { |
|
this.applyTransformations(sprite); |
|
} |
|
} |
|
}, |
|
rotationCompensation: function(deg, dx, dy) { |
|
var matrix = new Ext.draw.Matrix(); |
|
matrix.rotate(-deg, 0.5, 0.5); |
|
return { |
|
x: matrix.x(dx, dy), |
|
y: matrix.y(dx, dy) |
|
}; |
|
}, |
|
transform: function(sprite, matrixOnly) { |
|
var me = this, |
|
bbox = me.getBBox(sprite, true), |
|
matrix = new Ext.draw.Matrix(), |
|
transforms = sprite.transformations, |
|
transformsLength = transforms.length, |
|
i = 0, |
|
deltaDegrees = 0, |
|
deltaScaleX = 1, |
|
deltaScaleY = 1, |
|
el = sprite.el, |
|
dom = el.dom, |
|
domStyle = dom.style, |
|
skew = sprite.skew, |
|
shift = me.viewBoxShift, |
|
transform, type, offset; |
|
for (; i < transformsLength; i++) { |
|
transform = transforms[i]; |
|
type = transform.type; |
|
if (type == "translate") { |
|
matrix.translate(transform.x, transform.y); |
|
} else if (type == "rotate") { |
|
matrix.rotate(transform.degrees, transform.x, transform.y); |
|
deltaDegrees += transform.degrees; |
|
} else if (type == "scale") { |
|
matrix.scale(transform.x, transform.y, transform.centerX, transform.centerY); |
|
deltaScaleX *= transform.x; |
|
deltaScaleY *= transform.y; |
|
} |
|
} |
|
sprite.matrix = matrix.clone(); |
|
if (matrixOnly) { |
|
return; |
|
} |
|
if (shift) { |
|
matrix.prepend(shift.scale, 0, 0, shift.scale, shift.dx * shift.scale, shift.dy * shift.scale); |
|
} |
|
// Hide element while we transform |
|
if (sprite.type != "image" && skew) { |
|
skew.origin = "0,0"; |
|
// matrix transform via VML skew |
|
skew.matrix = matrix.toString(); |
|
// skew.offset = '32767,1' OK |
|
// skew.offset = '32768,1' Crash |
|
// M$, R U kidding?? |
|
offset = matrix.offset(); |
|
if (offset[0] > 32767) { |
|
offset[0] = 32767; |
|
} else if (offset[0] < -32768) { |
|
offset[0] = -32768; |
|
} |
|
if (offset[1] > 32767) { |
|
offset[1] = 32767; |
|
} else if (offset[1] < -32768) { |
|
offset[1] = -32768; |
|
} |
|
skew.offset = offset; |
|
} else { |
|
domStyle.filter = matrix.toFilter(); |
|
domStyle.left = Math.min(matrix.x(bbox.x, bbox.y), matrix.x(bbox.x + bbox.width, bbox.y), matrix.x(bbox.x, bbox.y + bbox.height), matrix.x(bbox.x + bbox.width, bbox.y + bbox.height)) + 'px'; |
|
domStyle.top = Math.min(matrix.y(bbox.x, bbox.y), matrix.y(bbox.x + bbox.width, bbox.y), matrix.y(bbox.x, bbox.y + bbox.height), matrix.y(bbox.x + bbox.width, bbox.y + bbox.height)) + 'px'; |
|
} |
|
}, |
|
createItem: function(config) { |
|
return Ext.create('Ext.draw.Sprite', config); |
|
}, |
|
getRegion: function() { |
|
return this.el.getRegion(); |
|
}, |
|
addCls: function(sprite, className) { |
|
if (sprite && sprite.el) { |
|
sprite.el.addCls(className); |
|
} |
|
}, |
|
removeCls: function(sprite, className) { |
|
if (sprite && sprite.el) { |
|
sprite.el.removeCls(className); |
|
} |
|
}, |
|
/** |
|
* Adds a definition to this Surface for a linear gradient. We convert the gradient definition |
|
* to its corresponding VML attributes and store it for later use by individual sprites. |
|
* @param {Object} gradient |
|
*/ |
|
addGradient: function(gradient) { |
|
var gradients = this.gradientsColl || (this.gradientsColl = Ext.create('Ext.util.MixedCollection')), |
|
colors = [], |
|
stops = Ext.create('Ext.util.MixedCollection'), |
|
keys, items, iLen, key, item, i; |
|
// Build colors string |
|
stops.addAll(gradient.stops); |
|
stops.sortByKey("ASC", function(a, b) { |
|
a = parseInt(a, 10); |
|
b = parseInt(b, 10); |
|
return a > b ? 1 : (a < b ? -1 : 0); |
|
}); |
|
keys = stops.keys; |
|
items = stops.items; |
|
iLen = keys.length; |
|
for (i = 0; i < iLen; i++) { |
|
key = keys[i]; |
|
item = items[i]; |
|
colors.push(key + '% ' + item.color); |
|
} |
|
gradients.add(gradient.id, { |
|
colors: colors.join(","), |
|
angle: gradient.angle |
|
}); |
|
}, |
|
destroy: function() { |
|
var me = this; |
|
me.callParent(arguments); |
|
if (me.el) { |
|
me.el.destroy(); |
|
} |
|
delete me.el; |
|
} |
|
}); |
|
|
|
|