Closes the Panel. By default, this method, removes it from the DOM, {@link Ext.Component#destroy destroy}s
- * the Panel object and all its descendant Components. The {@link #beforeclose beforeclose}
- * event is fired before the close happens and will cancel the close action if it returns false.
If this Panel is configured {@link #draggable}, this property will contain
- * an instance of {@link Ext.dd.DragSource} which handles dragging the Panel.
- * 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}.
- * @type Ext.dd.DragSource.
- * @property dd
- */
- this.dd = Ext.create('Ext.panel.DD', this, Ext.isBoolean(this.draggable) ? null : this.draggable);
+ initComponent : function(){
+ var me = this;
+
+ me.target = me.target || Ext.getDoc();
+ me.targets = me.targets || {};
+ me.callParent();
},
- // private - helper function for ghost
- ghostTools : function() {
- var tools = [],
- origTools = this.initialConfig.tools;
+ /**
+ * Configures a new quick tip instance and assigns it to a target element. The following config values are
+ * supported (for example usage, see the {@link Ext.tip.QuickTipManager} class header):
+ *
+ *
+ * This does several things. First it creates a global variable called 'MyApp' - all of your Application's classes (such
+ * as its Models, Views and Controllers) will reside under this single namespace, which drastically lowers the chances
+ * of colliding global variables.
+ *
+ * When the page is ready and all of your JavaScript has loaded, your Application's {@link #launch} function is called,
+ * at which time you can run the code that starts your app. Usually this consists of creating a Viewport, as we do in
+ * the example above.
+ *
+ * # Telling Application about the rest of the app
+ *
+ * Because an Ext.app.Application represents an entire app, we should tell it about the other parts of the app - namely
+ * the Models, Views and Controllers that are bundled with the application. Let's say we have a blog management app; we
+ * might have Models and Controllers for Posts and Comments, and Views for listing, adding and editing Posts and Comments.
+ * Here's how we'd tell our Application about all these things:
+ *
+ * Ext.application({
+ * name: 'Blog',
+ * models: ['Post', 'Comment'],
+ * controllers: ['Posts', 'Comments'],
+ *
+ * launch: function() {
+ * ...
+ * }
+ * });
+ *
+ * Note that we didn't actually list the Views directly in the Application itself. This is because Views are managed by
+ * Controllers, so it makes sense to keep those dependencies there. The Application will load each of the specified
+ * Controllers using the pathing conventions laid out in the [application architecture guide][mvc] -
+ * in this case expecting the controllers to reside in `app/controller/Posts.js` and
+ * `app/controller/Comments.js`. In turn, each Controller simply needs to list the Views it uses and they will be
+ * automatically loaded. Here's how our Posts controller like be defined:
+ *
+ * Ext.define('MyApp.controller.Posts', {
+ * extend: 'Ext.app.Controller',
+ * views: ['posts.List', 'posts.Edit'],
+ *
+ * //the rest of the Controller here
+ * });
+ *
+ * Because we told our Application about our Models and Controllers, and our Controllers about their Views, Ext JS will
+ * automatically load all of our app files for us. This means we don't have to manually add script tags into our html
+ * files whenever we add a new class, but more importantly it enables us to create a minimized build of our entire
+ * application using the Ext JS 4 SDK Tools.
+ *
+ * For more information about writing Ext JS 4 applications, please see the
+ * [application architecture guide][mvc].
+ *
+ * [mvc]: #!/guide/application_architecture
+ *
+ * @docauthor Ed Spencer
*/
-Ext.define('Ext.tip.ToolTip', {
- extend: 'Ext.tip.Tip',
- alias: 'widget.tooltip',
- alternateClassName: 'Ext.ToolTip',
- /**
- * When a ToolTip is configured with the {@link #delegate}
- * option to cause selected child elements of the {@link #target}
- * Element to each trigger a seperate show event, this property is set to
- * the DOM element which triggered the show.
- * @type DOMElement
- * @property triggerElement
- */
- /**
- * @cfg {Mixed} target The target HTMLElement, Ext.core.Element or id to monitor
- * for mouseover events to trigger showing this ToolTip.
- */
- /**
- * @cfg {Boolean} autoHide True to automatically hide the tooltip after the
- * mouse exits the target element or after the {@link #dismissDelay}
- * has expired if set (defaults to true). If {@link #closable} = true
- * a close tool button will be rendered into the tooltip header.
- */
- /**
- * @cfg {Number} showDelay Delay in milliseconds before the tooltip displays
- * after the mouse enters the target element (defaults to 500)
- */
- showDelay: 500,
- /**
- * @cfg {Number} hideDelay Delay in milliseconds after the mouse exits the
- * target element but before the tooltip actually hides (defaults to 200).
- * Set to 0 for the tooltip to hide immediately.
- */
- hideDelay: 200,
+Ext.define('Ext.app.Application', {
+ extend: 'Ext.app.Controller',
+
+ requires: [
+ 'Ext.ModelManager',
+ 'Ext.data.Model',
+ 'Ext.data.StoreManager',
+ 'Ext.tip.QuickTipManager',
+ 'Ext.ComponentManager',
+ 'Ext.app.EventBus'
+ ],
+
/**
- * @cfg {Number} dismissDelay Delay in milliseconds before the tooltip
- * automatically hides (defaults to 5000). To disable automatic hiding, set
- * dismissDelay = 0.
+ * @cfg {String} name The name of your application. This will also be the namespace for your views, controllers
+ * models and stores. Don't use spaces or special characters in the name.
*/
- dismissDelay: 5000,
+
/**
- * @cfg {Array} mouseOffset An XY offset from the mouse position where the
- * tooltip should be shown (defaults to [15,18]).
+ * @cfg {Object} scope The scope to execute the {@link #launch} function in. Defaults to the Application
+ * instance.
*/
+ scope: undefined,
+
/**
- * @cfg {Boolean} trackMouse True to have the tooltip follow the mouse as it
- * moves over the target element (defaults to false).
+ * @cfg {Boolean} enableQuickTips True to automatically set up Ext.tip.QuickTip support.
*/
- trackMouse: false,
+ enableQuickTips: true,
+
/**
- * @cfg {String} anchor If specified, indicates that the tip should be anchored to a
- * particular side of the target element or mouse pointer ("top", "right", "bottom",
- * or "left"), with an arrow pointing back at the target or mouse pointer. If
- * {@link #constrainPosition} is enabled, this will be used as a preferred value
- * only and may be flipped as needed.
+ * @cfg {String} defaultUrl When the app is first loaded, this url will be redirected to.
*/
+
/**
- * @cfg {Boolean} anchorToTarget True to anchor the tooltip to the target
- * element, false to anchor it relative to the mouse coordinates (defaults
- * to true). When anchorToTarget
is true, use
- * {@link #defaultAlign}
to control tooltip alignment to the
- * target element. When anchorToTarget
is false, use
- * {@link #anchorPosition}
instead to control alignment.
+ * @cfg {String} appFolder The path to the directory which contains all application's classes.
+ * This path will be registered via {@link Ext.Loader#setPath} for the namespace specified in the {@link #name name} config.
*/
- anchorToTarget: true,
+ appFolder: 'app',
+
/**
- * @cfg {Number} anchorOffset A numeric pixel value used to offset the
- * default position of the anchor arrow (defaults to 0). When the anchor
- * position is on the top or bottom of the tooltip, anchorOffset
- * will be used as a horizontal offset. Likewise, when the anchor position
- * is on the left or right side, anchorOffset
will be used as
- * a vertical offset.
+ * @cfg {Boolean} autoCreateViewport True to automatically load and instantiate AppName.view.Viewport
+ * before firing the launch function.
*/
- anchorOffset: 0,
+ autoCreateViewport: false,
+
/**
- * @cfg {String} delegate Optional. A {@link Ext.DomQuery DomQuery}
- * selector which allows selection of individual elements within the
- * {@link #target}
element to trigger showing and hiding the
- * ToolTip as the mouse moves within the target.
- * When specified, the child element of the target which caused a show
- * event is placed into the {@link #triggerElement}
property
- * before the ToolTip is shown.
- * This may be useful when a Component has regular, repeating elements
- * in it, each of which need a ToolTip which contains information specific
- * to that element. For example:
-var myGrid = Ext.create('Ext.grid.GridPanel', gridConfig);
-myGrid.on('render', function(grid) {
- var view = grid.getView(); // Capture the grid's view.
- grid.tip = Ext.create('Ext.tip.ToolTip', {
- target: view.el, // The overall target element.
- delegate: view.itemSelector, // Each grid row causes its own seperate show and hide.
- trackMouse: true, // Moving within the row should not hide the tip.
- renderTo: Ext.getBody(), // Render immediately so that tip.body can be referenced prior to the first show.
- listeners: { // Change content dynamically depending on which element triggered the show.
- beforeshow: function(tip) {
- tip.update('Over Record ID ' + view.getRecord(tip.triggerElement).id);
- }
- }
- });
-});
- *
+ * Creates new Application.
+ * @param {Object} [config] Config object.
*/
+ constructor: function(config) {
+ config = config || {};
+ Ext.apply(this, config);
- // private
- targetCounter: 0,
- quickShowInterval: 250,
-
- // private
- initComponent: function() {
- var me = this;
- me.callParent(arguments);
- me.lastActive = new Date();
- me.setTarget(me.target);
- me.origAnchor = me.anchor;
- },
+ var requires = config.requires || [];
- // private
- onRender: function(ct, position) {
- var me = this;
- me.callParent(arguments);
- me.anchorCls = Ext.baseCSSPrefix + 'tip-anchor-' + me.getAnchorPosition();
- me.anchorEl = me.el.createChild({
- cls: Ext.baseCSSPrefix + 'tip-anchor ' + me.anchorCls
- });
- },
+ Ext.Loader.setPath(this.name, this.appFolder);
- // private
- afterRender: function() {
- var me = this,
- zIndex;
+ if (this.paths) {
+ Ext.Object.each(this.paths, function(key, value) {
+ Ext.Loader.setPath(key, value);
+ });
+ }
- me.callParent(arguments);
- zIndex = parseInt(me.el.getZIndex(), 10) || 0;
- me.anchorEl.setStyle('z-index', zIndex + 1).setVisibilityMode(Ext.core.Element.DISPLAY);
- },
+ this.callParent(arguments);
- /**
- * Binds this ToolTip to the specified element. The tooltip will be displayed when the mouse moves over the element.
- * @param {Mixed} t The Element, HtmlElement, or ID of an element to bind to
- */
- setTarget: function(target) {
- var me = this,
- t = Ext.get(target),
- tg;
+ this.eventbus = Ext.create('Ext.app.EventBus');
- if (me.target) {
- tg = Ext.get(me.target);
- me.mun(tg, 'mouseover', me.onTargetOver, me);
- me.mun(tg, 'mouseout', me.onTargetOut, me);
- me.mun(tg, 'mousemove', me.onMouseMove, me);
- }
-
- me.target = t;
- if (t) {
-
- me.mon(t, {
- // TODO - investigate why IE6/7 seem to fire recursive resize in e.getXY
- // breaking QuickTip#onTargetOver (EXTJSIV-1608)
- freezeEvent: true,
+ var controllers = Ext.Array.from(this.controllers),
+ ln = controllers && controllers.length,
+ i, controller;
- mouseover: me.onTargetOver,
- mouseout: me.onTargetOut,
- mousemove: me.onMouseMove,
- scope: me
- });
- }
- if (me.anchor) {
- me.anchorTarget = me.target;
- }
- },
+ this.controllers = Ext.create('Ext.util.MixedCollection');
- // private
- onMouseMove: function(e) {
- var me = this,
- t = me.delegate ? e.getTarget(me.delegate) : me.triggerElement = true,
- xy;
- if (t) {
- me.targetXY = e.getXY();
- if (t === me.triggerElement) {
- if (!me.hidden && me.trackMouse) {
- xy = me.getTargetXY();
- if (me.constrainPosition) {
- xy = me.el.adjustForConstraints(xy, me.el.dom.parentNode);
- }
- me.setPagePosition(xy);
- }
- } else {
- me.hide();
- me.lastActive = new Date(0);
- me.onTargetOver(e);
- }
- } else if ((!me.closable && me.isVisible()) && me.autoHide !== false) {
- me.hide();
+ if (this.autoCreateViewport) {
+ requires.push(this.getModuleClassName('Viewport', 'view'));
}
- },
- // private
- getTargetXY: function() {
- var me = this,
- mouseOffset;
- if (me.delegate) {
- me.anchorTarget = me.triggerElement;
+ for (i = 0; i < ln; i++) {
+ requires.push(this.getModuleClassName(controllers[i], 'controller'));
}
- if (me.anchor) {
- me.targetCounter++;
- var offsets = me.getOffsets(),
- xy = (me.anchorToTarget && !me.trackMouse) ? me.el.getAlignToXY(me.anchorTarget, me.getAnchorAlign()) : me.targetXY,
- dw = Ext.core.Element.getViewWidth() - 5,
- dh = Ext.core.Element.getViewHeight() - 5,
- de = document.documentElement,
- bd = document.body,
- scrollX = (de.scrollLeft || bd.scrollLeft || 0) + 5,
- scrollY = (de.scrollTop || bd.scrollTop || 0) + 5,
- axy = [xy[0] + offsets[0], xy[1] + offsets[1]],
- sz = me.getSize(),
- constrainPosition = me.constrainPosition;
- me.anchorEl.removeCls(me.anchorCls);
+ Ext.require(requires);
- if (me.targetCounter < 2 && constrainPosition) {
- if (axy[0] < scrollX) {
- if (me.anchorToTarget) {
- me.defaultAlign = 'l-r';
- if (me.mouseOffset) {
- me.mouseOffset[0] *= -1;
- }
- }
- me.anchor = 'left';
- return me.getTargetXY();
- }
- if (axy[0] + sz.width > dw) {
- if (me.anchorToTarget) {
- me.defaultAlign = 'r-l';
- if (me.mouseOffset) {
- me.mouseOffset[0] *= -1;
- }
- }
- me.anchor = 'right';
- return me.getTargetXY();
- }
- if (axy[1] < scrollY) {
- if (me.anchorToTarget) {
- me.defaultAlign = 't-b';
- if (me.mouseOffset) {
- me.mouseOffset[1] *= -1;
- }
- }
- me.anchor = 'top';
- return me.getTargetXY();
- }
- if (axy[1] + sz.height > dh) {
- if (me.anchorToTarget) {
- me.defaultAlign = 'b-t';
- if (me.mouseOffset) {
- me.mouseOffset[1] *= -1;
- }
- }
- me.anchor = 'bottom';
- return me.getTargetXY();
- }
+ Ext.onReady(function() {
+ for (i = 0; i < ln; i++) {
+ controller = this.getController(controllers[i]);
+ controller.init(this);
}
- me.anchorCls = Ext.baseCSSPrefix + 'tip-anchor-' + me.getAnchorPosition();
- me.anchorEl.addCls(me.anchorCls);
- me.targetCounter = 0;
- return axy;
- } else {
- mouseOffset = me.getMouseOffset();
- return (me.targetXY) ? [me.targetXY[0] + mouseOffset[0], me.targetXY[1] + mouseOffset[1]] : mouseOffset;
- }
+ this.onBeforeLaunch.call(this);
+ }, this);
},
- getMouseOffset: function() {
- var me = this,
- offset = me.anchor ? [0, 0] : [15, 18];
- if (me.mouseOffset) {
- offset[0] += me.mouseOffset[0];
- offset[1] += me.mouseOffset[1];
- }
- return offset;
+ control: function(selectors, listeners, controller) {
+ this.eventbus.control(selectors, listeners, controller);
},
- // private
- getAnchorPosition: function() {
- var me = this,
- m;
- if (me.anchor) {
- me.tipAnchor = me.anchor.charAt(0);
- } else {
- m = me.defaultAlign.match(/^([a-z]+)-([a-z]+)(\?)?$/);
- //
- if (!m) {
- Ext.Error.raise('The AnchorTip.defaultAlign value "' + me.defaultAlign + '" is invalid.');
- }
- //
- me.tipAnchor = m[1].charAt(0);
- }
+ /**
+ * Called automatically when the page has completely loaded. This is an empty function that should be
+ * overridden by each application that needs to take action on page load
+ * @property launch
+ * @type Function
+ * @param {String} profile The detected {@link #profiles application profile}
+ * @return {Boolean} By default, the Application will dispatch to the configured startup controller and
+ * action immediately after running the launch function. Return false to prevent this behavior.
+ */
+ launch: Ext.emptyFn,
- switch (me.tipAnchor) {
- case 't':
- return 'top';
- case 'b':
- return 'bottom';
- case 'r':
- return 'right';
+ /**
+ * @private
+ */
+ onBeforeLaunch: function() {
+ if (this.enableQuickTips) {
+ Ext.tip.QuickTipManager.init();
}
- return 'left';
- },
- // private
- getAnchorAlign: function() {
- switch (this.anchor) {
- case 'top':
- return 'tl-bl';
- case 'left':
- return 'tl-tr';
- case 'right':
- return 'tr-tl';
- default:
- return 'bl-tl';
+ if (this.autoCreateViewport) {
+ this.getView('Viewport').create();
}
- },
- // private
- getOffsets: function() {
- var me = this,
- mouseOffset,
- offsets,
- ap = me.getAnchorPosition().charAt(0);
- if (me.anchorToTarget && !me.trackMouse) {
- switch (ap) {
- case 't':
- offsets = [0, 9];
- break;
- case 'b':
- offsets = [0, -13];
- break;
- case 'r':
- offsets = [ - 13, 0];
- break;
- default:
- offsets = [9, 0];
- break;
- }
- } else {
- switch (ap) {
- case 't':
- offsets = [ - 15 - me.anchorOffset, 30];
- break;
- case 'b':
- offsets = [ - 19 - me.anchorOffset, -13 - me.el.dom.offsetHeight];
- break;
- case 'r':
- offsets = [ - 15 - me.el.dom.offsetWidth, -13 - me.anchorOffset];
- break;
- default:
- offsets = [25, -13 - me.anchorOffset];
- break;
- }
- }
- mouseOffset = me.getMouseOffset();
- offsets[0] += mouseOffset[0];
- offsets[1] += mouseOffset[1];
+ this.launch.call(this.scope || this);
+ this.launched = true;
+ this.fireEvent('launch', this);
- return offsets;
+ this.controllers.each(function(controller) {
+ controller.onLaunch(this);
+ }, this);
},
- // private
- onTargetOver: function(e) {
- var me = this,
- t;
+ getModuleClassName: function(name, type) {
+ var namespace = Ext.Loader.getPrefix(name);
- if (me.disabled || e.within(me.target.dom, true)) {
- return;
- }
- t = e.getTarget(me.delegate);
- if (t) {
- me.triggerElement = t;
- me.clearTimer('hide');
- me.targetXY = e.getXY();
- me.delayShow();
+ if (namespace.length > 0 && namespace !== name) {
+ return name;
}
- },
- // private
- delayShow: function() {
- var me = this;
- if (me.hidden && !me.showTimer) {
- if (Ext.Date.getElapsed(me.lastActive) < me.quickShowInterval) {
- me.show();
- } else {
- me.showTimer = Ext.defer(me.show, me.showDelay, me);
- }
- }
- else if (!me.hidden && me.autoHide !== false) {
- me.show();
- }
+ return this.name + '.' + type + '.' + name;
},
- // private
- onTargetOut: function(e) {
- var me = this;
- if (me.disabled || e.within(me.target.dom, true)) {
- return;
- }
- me.clearTimer('show');
- if (me.autoHide !== false) {
- me.delayHide();
+ getController: function(name) {
+ var controller = this.controllers.get(name);
+
+ if (!controller) {
+ controller = Ext.create(this.getModuleClassName(name, 'controller'), {
+ application: this,
+ id: name
+ });
+
+ this.controllers.add(controller);
}
+
+ return controller;
},
- // private
- delayHide: function() {
- var me = this;
- if (!me.hidden && !me.hideTimer) {
- me.hideTimer = Ext.defer(me.hide, me.hideDelay, me);
+ getStore: function(name) {
+ var store = Ext.StoreManager.get(name);
+
+ if (!store) {
+ store = Ext.create(this.getModuleClassName(name, 'store'), {
+ storeId: name
+ });
}
+
+ return store;
},
- /**
- * Hides this tooltip if visible.
- */
- hide: function() {
- var me = this;
- me.clearTimer('dismiss');
- me.lastActive = new Date();
- if (me.anchorEl) {
- me.anchorEl.hide();
- }
- me.callParent(arguments);
- delete me.triggerElement;
+ getModel: function(model) {
+ model = this.getModuleClassName(model, 'model');
+
+ return Ext.ModelManager.getModel(model);
},
- /**
- * Shows this tooltip at the current event target XY position.
- */
- show: function() {
- var me = this;
+ getView: function(view) {
+ view = this.getModuleClassName(view, 'view');
- // Show this Component first, so that sizing can be calculated
- // pre-show it off screen so that the el will have dimensions
- this.callParent();
- if (this.hidden === false) {
- me.setPagePosition(-10000, -10000);
+ return Ext.ClassManager.get(view);
+ }
+});
- if (me.anchor) {
- me.anchor = me.origAnchor;
- }
- me.showAt(me.getTargetXY());
+/**
+ * @class Ext.chart.Callout
+ * A mixin providing callout functionality for Ext.chart.series.Series.
+ */
+Ext.define('Ext.chart.Callout', {
- if (me.anchor) {
- me.syncAnchor();
- me.anchorEl.show();
- } else {
- me.anchorEl.hide();
+ /* 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.store,
+ len = store.getCount(),
+ ratio = items.length / len,
+ previouslyPlacedCallouts = [],
+ i,
+ count,
+ j,
+ p;
+
+ for (i = 0, count = 0; i < len; i++) {
+ for (j = 0; j < ratio; j++) {
+ var item = items[count],
+ label = group[count],
+ storeItem = store.getAt(i),
+ display;
+
+ display = 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);
+ }
+ }
+ }
+ }
+ config.renderer(label, storeItem);
+ me.onPlaceCallout(label, storeItem, item, i, display, animate,
+ j, count, previouslyPlacedCallouts);
+ previouslyPlacedCallouts.push(label);
+ count++;
}
}
+ this.hideCallouts(count);
},
- // inherit docs
- showAt: function(xy) {
- var me = this;
- me.lastActive = new Date();
- me.clearTimers();
+ onCreateCallout: function(storeItem, item, i, display) {
+ var me = this,
+ group = me.calloutsGroup,
+ config = me.callouts,
+ styles = config.styles,
+ width = styles.width,
+ height = styles.height,
+ chart = me.chart,
+ surface = chart.surface,
+ calloutObj = {
+ //label: false,
+ //box: false,
+ lines: false
+ };
- // Only call if this is hidden. May have been called from show above.
- if (!me.isVisible()) {
- this.callParent(arguments);
- }
+ calloutObj.lines = surface.add(Ext.apply({},
+ {
+ type: 'path',
+ path: 'M0,0',
+ stroke: me.getLegendColor() || '#555'
+ },
+ styles));
- // Show may have been vetoed.
- if (me.isVisible()) {
- me.setPagePosition(xy[0], xy[1]);
- if (me.constrainPosition || me.constrain) {
- me.doConstrain();
- }
- me.toFront(true);
+ if (config.items) {
+ calloutObj.panel = Ext.create('widget.panel', {
+ style: "position: absolute;",
+ width: width,
+ height: height,
+ items: config.items,
+ renderTo: chart.el
+ });
}
- if (me.dismissDelay && me.autoHide !== false) {
- me.dismissTimer = Ext.defer(me.hide, me.dismissDelay, me);
- }
- if (me.anchor) {
- me.syncAnchor();
- if (!me.anchorEl.isVisible()) {
- me.anchorEl.show();
- }
- } else {
- me.anchorEl.hide();
- }
+ return calloutObj;
},
- // private
- syncAnchor: function() {
- var me = this,
- anchorPos,
- targetPos,
- offset;
- switch (me.tipAnchor.charAt(0)) {
- case 't':
- anchorPos = 'b';
- targetPos = 'tl';
- offset = [20 + me.anchorOffset, 1];
- break;
- case 'r':
- anchorPos = 'l';
- targetPos = 'tr';
- offset = [ - 1, 12 + me.anchorOffset];
- break;
- case 'b':
- anchorPos = 't';
- targetPos = 'bl';
- offset = [20 + me.anchorOffset, -1];
- break;
- default:
- anchorPos = 'r';
- targetPos = 'tl';
- offset = [1, 12 + me.anchorOffset];
- break;
+ 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);
+ }
+ }
}
- me.anchorEl.alignTo(me.el, anchorPos + '-' + targetPos, offset);
- },
+ }
+});
- // private
- setPagePosition: function(x, y) {
- var me = this;
- me.callParent(arguments);
- if (me.anchor) {
- me.syncAnchor();
- }
- },
+/**
+ * @class Ext.draw.CompositeSprite
+ * @extends Ext.util.MixedCollection
+ *
+ * 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', {
- // private
- clearTimer: function(name) {
- name = name + 'Timer';
- clearTimeout(this[name]);
- delete this[name];
- },
+ /* Begin Definitions */
- // private
- clearTimers: function() {
- var me = this;
- me.clearTimer('show');
- me.clearTimer('dismiss');
- me.clearTimer('hide');
+ extend: 'Ext.util.MixedCollection',
+ mixins: {
+ animate: 'Ext.util.Animate'
},
- // private
- onShow: function() {
+ /* End Definitions */
+ isCompositeSprite: true,
+ constructor: function(config) {
var me = this;
- me.callParent();
- me.mon(Ext.getDoc(), 'mousedown', me.onDocMouseDown, me);
- },
+
+ config = config || {};
+ Ext.apply(me, config);
- // private
- onHide: function() {
- var me = this;
+ me.addEvents(
+ 'mousedown',
+ 'mouseup',
+ 'mouseover',
+ 'mouseout',
+ 'click'
+ );
+ me.id = Ext.id(null, 'ext-sprite-group-');
me.callParent();
- me.mun(Ext.getDoc(), 'mousedown', me.onDocMouseDown, me);
},
- // private
- onDocMouseDown: function(e) {
- var me = this;
- if (me.autoHide !== true && !me.closable && !e.within(me.el.dom)) {
- me.disable();
- Ext.defer(me.doEnable, 100, me);
- }
+ // @private
+ onClick: function(e) {
+ this.fireEvent('click', e);
},
- // private
- doEnable: function() {
- if (!this.isDestroyed) {
- this.enable();
- }
+ // @private
+ onMouseUp: function(e) {
+ this.fireEvent('mouseup', e);
},
- // private
- onDisable: function() {
- this.callParent();
- this.clearTimers();
- this.hide();
+ // @private
+ onMouseDown: function(e) {
+ this.fireEvent('mousedown', e);
},
- beforeDestroy: function() {
- var me = this;
- me.clearTimers();
- Ext.destroy(me.anchorEl);
- delete me.anchorEl;
- delete me.target;
- delete me.anchorTarget;
- delete me.triggerElement;
- me.callParent();
+ // @private
+ onMouseOver: function(e) {
+ this.fireEvent('mouseover', e);
},
- // private
- onDestroy: function() {
- Ext.getDoc().un('mousedown', this.onDocMouseDown, this);
- this.callParent();
- }
-});
+ // @private
+ onMouseOut: function(e) {
+ this.fireEvent('mouseout', e);
+ },
-/**
- * @class Ext.tip.QuickTip
- * @extends Ext.tip.ToolTip
- * A specialized tooltip class for tooltips that can be specified in markup and automatically managed by the global
- * {@link Ext.tip.QuickTipManager} instance. See the QuickTipManager class header for additional usage details and examples.
- * @constructor
- * Create a new Tip
- * @param {Object} config The configuration options
- * @xtype quicktip
- */
-Ext.define('Ext.tip.QuickTip', {
- extend: 'Ext.tip.ToolTip',
- alternateClassName: 'Ext.QuickTip',
- /**
- * @cfg {Mixed} target The target HTMLElement, Ext.core.Element or id to associate with this Quicktip (defaults to the document).
- */
- /**
- * @cfg {Boolean} interceptTitles True to automatically use the element's DOM title value if available (defaults to false).
- */
- interceptTitles : false,
+ 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
+ });
+ },
- // Force creation of header Component
- title: ' ',
+ // Inherit docs from MixedCollection
+ add: function(key, o) {
+ var result = this.callParent(arguments);
+ this.attachEvents(result);
+ return result;
+ },
- // private
- tagConfig : {
- namespace : "data-",
- attribute : "qtip",
- width : "qwidth",
- target : "target",
- title : "qtitle",
- hide : "hide",
- cls : "qclass",
- align : "qalign",
- anchor : "anchor"
+ insert: function(index, key, o) {
+ return this.callParent(arguments);
},
- // private
- initComponent : function(){
+ // Inherit docs from MixedCollection
+ remove: function(o) {
var me = this;
- me.target = me.target || Ext.getDoc();
- me.targets = me.targets || {};
- me.callParent();
+ o.un({
+ scope: me,
+ mousedown: me.onMouseDown,
+ mouseup: me.onMouseUp,
+ mouseover: me.onMouseOver,
+ mouseout: me.onMouseOut,
+ click: me.onClick
+ });
+ return me.callParent(arguments);
},
-
+
/**
- * Configures a new quick tip instance and assigns it to a target element. The following config values are
- * supported (for example usage, see the {@link Ext.tip.QuickTipManager} class header):
- *
- * - autoHide
- * - cls
- * - dismissDelay (overrides the singleton value)
- * - target (required)
- * - text (required)
- * - title
- * - width
- * @param {Object} config The config object
+ * Returns the group bounding box.
+ * Behaves like {@link Ext.draw.Sprite#getBBox} method.
+ * @return {Object} an object with x, y, width, and height properties.
*/
- register : function(config){
- var configs = Ext.isArray(config) ? config : arguments,
- i = 0,
- len = configs.length,
- target, j, targetLen;
-
+ 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++) {
- config = configs[i];
- target = config.target;
- if (target) {
- if (Ext.isArray(target)) {
- for (j = 0, targetLen = target.length; j < targetLen; j++) {
- this.targets[Ext.id(target[j])] = config;
- }
- } else{
- this.targets[Ext.id(target)] = config;
- }
+ sprite = items[i];
+ if (sprite.el) {
+ 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
+ };
},
/**
- * Removes this quick tip from its element and destroys it.
- * @param {String/HTMLElement/Element} el The element from which the quick tip is to be removed.
+ * 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 immediatly draw the change.
+ * @return {Ext.draw.CompositeSprite} this
*/
- unregister : function(el){
- delete this.targets[Ext.id(el)];
+ 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 a visible tip or cancels an impending show for a particular element.
- * @param {String/HTMLElement/Element} el The element that is the target of the tip.
+ * Hides all sprites. If the first parameter of the method is true
+ * then a redraw will be forced for each sprite.
+ * @param {Boolean} redraw Flag to immediatly draw the change.
+ * @return {Ext.draw.CompositeSprite} this
*/
- cancelShow: function(el){
- var me = this,
- activeTarget = me.activeTarget;
+ hide: function(redraw) {
+ var i = 0,
+ items = this.items,
+ len = this.length;
- el = Ext.get(el).dom;
- if (me.isVisible()) {
- if (activeTarget && activeTarget.el == el) {
- me.hide();
- }
- } else if (activeTarget && activeTarget.el == el) {
- me.clearTimer('show');
+ for (; i < len; i++) {
+ items[i].hide(redraw);
}
+ return this;
},
-
- getTipCfg: function(e) {
- var t = e.getTarget(),
- ttp,
- cfg;
-
- if(this.interceptTitles && t.title && Ext.isString(t.title)){
- ttp = t.title;
- t.qtip = ttp;
- t.removeAttribute("title");
- e.preventDefault();
- }
- else {
- cfg = this.tagConfig;
- t = e.getTarget('[' + cfg.namespace + cfg.attribute + ']');
- if (t) {
- ttp = t.getAttribute(cfg.namespace + cfg.attribute);
- }
+
+ /**
+ * Shows all sprites. If the first parameter of the method is true
+ * then a redraw will be forced for each sprite.
+ * @param {Boolean} redraw Flag to immediatly 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 ttp;
+ return this;
},
- // private
- onTargetOver : function(e){
+ redraw: function() {
var me = this,
- target = e.getTarget(),
- elTarget,
- cfg,
- ns,
- ttp,
- autoHide;
-
- if (me.disabled) {
- return;
- }
-
- // TODO - this causes "e" to be recycled in IE6/7 (EXTJSIV-1608) so ToolTip#setTarget
- // was changed to include freezeEvent. The issue seems to be a nested 'resize' event
- // that smashed Ext.EventObject.
- me.targetXY = e.getXY();
-
- if(!target || target.nodeType !== 1 || target == document || target == document.body){
- return;
- }
-
- if (me.activeTarget && ((target == me.activeTarget.el) || Ext.fly(me.activeTarget.el).contains(target))) {
- me.clearTimer('hide');
- me.show();
- return;
- }
+ i = 0,
+ items = me.items,
+ surface = me.getSurface(),
+ len = me.length;
- if (target) {
- Ext.Object.each(me.targets, function(key, value) {
- var targetEl = Ext.fly(value.target);
- if (targetEl && (targetEl.dom === target || targetEl.contains(target))) {
- elTarget = targetEl.dom;
- return false;
- }
- });
- if (elTarget) {
- me.activeTarget = me.targets[elTarget.id];
- me.activeTarget.el = target;
- me.anchor = me.activeTarget.anchor;
- if (me.anchor) {
- me.anchorTarget = target;
- }
- me.delayShow();
- return;
+ if (surface) {
+ for (; i < len; i++) {
+ surface.renderItem(items[i]);
}
}
+ return me;
+ },
- elTarget = Ext.get(target);
- cfg = me.tagConfig;
- ns = cfg.namespace;
- ttp = me.getTipCfg(e);
-
- if (ttp) {
- autoHide = elTarget.getAttribute(ns + cfg.hide);
-
- me.activeTarget = {
- el: target,
- text: ttp,
- width: +elTarget.getAttribute(ns + cfg.width) || null,
- autoHide: autoHide != "user" && autoHide !== 'false',
- title: elTarget.getAttribute(ns + cfg.title),
- cls: elTarget.getAttribute(ns + cfg.cls),
- align: elTarget.getAttribute(ns + cfg.align)
-
- };
- me.anchor = elTarget.getAttribute(ns + cfg.anchor);
- if (me.anchor) {
- me.anchorTarget = target;
+ 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);
}
- me.delayShow();
}
},
- // private
- onTargetOut : function(e){
- var me = this;
+ addCls: function(obj) {
+ var i = 0,
+ items = this.items,
+ surface = this.getSurface(),
+ len = this.length;
- // If moving within the current target, and it does not have a new tip, ignore the mouseout
- if (me.activeTarget && e.within(me.activeTarget.el) && !me.getTipCfg(e)) {
- return;
- }
-
- me.clearTimer('show');
- if (me.autoHide !== false) {
- me.delayHide();
+ if (surface) {
+ for (; i < len; i++) {
+ surface.addCls(items[i], obj);
+ }
}
},
- // inherit docs
- showAt : function(xy){
- var me = this,
- target = me.activeTarget;
+ removeCls: function(obj) {
+ var i = 0,
+ items = this.items,
+ surface = this.getSurface(),
+ len = this.length;
- if (target) {
- if (!me.rendered) {
- me.render(Ext.getBody());
- me.activeTarget = target;
- }
- if (target.title) {
- me.setTitle(target.title || '');
- me.header.show();
- } else {
- me.header.hide();
- }
- me.body.update(target.text);
- me.autoHide = target.autoHide;
- me.dismissDelay = target.dismissDelay || me.dismissDelay;
- if (me.lastCls) {
- me.el.removeCls(me.lastCls);
- delete me.lastCls;
- }
- if (target.cls) {
- me.el.addCls(target.cls);
- me.lastCls = target.cls;
+ if (surface) {
+ for (; i < len; i++) {
+ surface.removeCls(items[i], obj);
}
-
- me.setWidth(target.width);
+ }
+ },
+
+ /**
+ * 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 the SpriteGroup
+ */
+ destroy: function(){
+ var me = this,
+ surface = me.getSurface(),
+ item;
- if (me.anchor) {
- me.constrainPosition = false;
- } else if (target.align) { // TODO: this doesn't seem to work consistently
- xy = me.el.getAlignToXY(target.el, target.align);
- me.constrainPosition = false;
- }else{
- me.constrainPosition = true;
+ if (surface) {
+ while (me.getCount() > 0) {
+ item = me.first();
+ me.remove(item);
+ surface.remove(item);
}
}
- me.callParent([xy]);
- },
-
- // inherit docs
- hide: function(){
- delete this.activeTarget;
- this.callParent();
+ me.clearListeners();
}
});
/**
- * @class Ext.tip.QuickTipManager
- * Provides attractive and customizable tooltips for any element. The QuickTips
- * singleton is used to configure and manage tooltips globally for multiple elements
- * in a generic manner. To create individual tooltips with maximum customizability,
- * you should consider either {@link Ext.tip.Tip} or {@link Ext.tip.ToolTip}.
- * Quicktips can be configured via tag attributes directly in markup, or by
- * registering quick tips programmatically via the {@link #register} method.
- * The singleton's instance of {@link Ext.tip.QuickTip} is available via
- * {@link #getQuickTip}, and supports all the methods, and all the all the
- * configuration properties of Ext.tip.QuickTip. These settings will apply to all
- * tooltips shown by the singleton.
- * Below is the summary of the configuration properties which can be used.
- * For detailed descriptions see the config options for the {@link Ext.tip.QuickTip QuickTip} class
- * QuickTips singleton configs (all are optional)
- * - dismissDelay
- * - hideDelay
- * - maxWidth
- * - minWidth
- * - showDelay
- * - trackMouse
- * Target element configs (optional unless otherwise noted)
- * - autoHide
- * - cls
- * - dismissDelay (overrides singleton value)
- * - target (required)
- * - text (required)
- * - title
- * - width
- * Here is an example showing how some of these config options could be used:
- *
- * {@img Ext.tip.QuickTipManager/Ext.tip.QuickTipManager.png Ext.tip.QuickTipManager component}
- *
- * ## Code
- * // Init the singleton. Any tag-based quick tips will start working.
- * Ext.tip.QuickTipManager.init();
- *
- * // Apply a set of config properties to the singleton
- * Ext.apply(Ext.tip.QuickTipManager.getQuickTip(), {
- * maxWidth: 200,
- * minWidth: 100,
- * showDelay: 50 // Show 50ms after entering target
- * });
- *
- * // Create a small panel to add a quick tip to
- * Ext.create('Ext.container.Container', {
- * id: 'quickTipContainer',
- * width: 200,
- * height: 150,
- * style: {
- * backgroundColor:'#000000'
- * },
- * renderTo: Ext.getBody()
- * });
+ * @class Ext.layout.component.Auto
+ * @extends Ext.layout.component.Component
+ * @private
*
- *
- * // Manually register a quick tip for a specific element
- * Ext.tip.QuickTipManager.register({
- * target: 'quickTipContainer',
- * title: 'My Tooltip',
- * text: 'This tooltip was added in code',
- * width: 100,
- * dismissDelay: 10000 // Hide after 10 seconds hover
- * });
-
- * To register a quick tip in markup, you simply add one or more of the valid QuickTip attributes prefixed with
- * the ext: namespace. The HTML element itself is automatically set as the quick tip target. Here is the summary
- * of supported attributes (optional unless otherwise noted):
- * - hide: Specifying "user" is equivalent to setting autoHide = false. Any other value will be the
- * same as autoHide = true.
- * - qclass: A CSS class to be applied to the quick tip (equivalent to the 'cls' target element config).
- * - qtip (required): The quick tip text (equivalent to the 'text' target element config).
- * - qtitle: The quick tip title (equivalent to the 'title' target element config).
- * - qwidth: The quick tip width (equivalent to the 'width' target element config).
- * Here is an example of configuring an HTML element to display a tooltip from markup:
- *
-// Add a quick tip to an HTML button
-<input type="button" value="OK" ext:qtitle="OK Button" ext:qwidth="100"
- data-qtip="This is a quick tip from markup!"></input>
-
- * @singleton
+ * The AutoLayout is the default layout manager delegated by {@link Ext.Component} to
+ * render any child Elements when no {@link Ext.container.Container#layout layout} is configured.
*/
-Ext.define('Ext.tip.QuickTipManager', function() {
- var tip,
- disabled = false;
- return {
- requires: ['Ext.tip.QuickTip'],
- singleton: true,
- alternateClassName: 'Ext.QuickTips',
- /**
- * Initialize the global QuickTips instance and prepare any quick tips.
- * @param {Boolean} autoRender True to render the QuickTips container immediately to preload images. (Defaults to true)
- */
- init : function(autoRender){
- if (!tip) {
- if (!Ext.isReady) {
- Ext.onReady(function(){
- Ext.tip.QuickTipManager.init(autoRender);
+Ext.define('Ext.layout.component.Auto', {
+
+ /* Begin Definitions */
+
+ alias: 'layout.autocomponent',
+
+ extend: 'Ext.layout.component.Component',
+
+ /* End Definitions */
+
+ type: 'autocomponent',
+
+ onLayout : function(width, height) {
+ this.setTargetSize(width, height);
+ }
+});
+/**
+ * @class Ext.chart.theme.Theme
+ *
+ * Provides chart theming.
+ *
+ * Used as mixins by Ext.chart.Chart.
+ */
+Ext.define('Ext.chart.theme.Theme', {
+
+ /* 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;
}
- tip = Ext.create('Ext.tip.QuickTip', {
- disabled: disabled,
- renderTo: autoRender !== false ? document.body : undefined
- });
}
+ //
+ Ext.Error.raise('No theme found named "' + theme + '"');
+ //
+ }
+ }
+},
+// This callback is executed right after when the class is created. This scope refers to the newly created class itself
+function() {
+ /* Theme constructor: takes either a complex object with styles like:
+
+ {
+ axis: {
+ fill: '#000',
+ 'stroke-width': 1
},
-
- /**
- * Destroy the QuickTips instance.
- */
- destroy: function() {
- if (tip) {
- var undef;
- tip.destroy();
- tip = undef;
- }
+ axisLabelTop: {
+ fill: '#000',
+ font: '11px Arial'
},
-
- // Protected method called by the dd classes
- ddDisable : function(){
- // don't disable it if we don't need to
- if(tip && !disabled){
- tip.disable();
- }
+ axisLabelLeft: {
+ fill: '#000',
+ font: '11px Arial'
},
-
- // Protected method called by the dd classes
- ddEnable : function(){
- // only enable it if it hasn't been disabled
- if(tip && !disabled){
- tip.enable();
- }
+ axisLabelRight: {
+ fill: '#000',
+ font: '11px Arial'
},
-
- /**
- * Enable quick tips globally.
- */
- enable : function(){
- if(tip){
- tip.enable();
- }
- disabled = false;
+ axisLabelBottom: {
+ fill: '#000',
+ font: '11px Arial'
},
-
- /**
- * Disable quick tips globally.
- */
- disable : function(){
- if(tip){
- tip.disable();
- }
- disabled = true;
+ axisTitleTop: {
+ fill: '#000',
+ font: '11px Arial'
},
-
- /**
- * Returns true if quick tips are enabled, else false.
- * @return {Boolean}
- */
- isEnabled : function(){
- return tip !== undefined && !tip.disabled;
+ axisTitleLeft: {
+ fill: '#000',
+ font: '11px Arial'
},
-
- /**
- * Gets the single {@link Ext.tip.QuickTip QuickTip} instance used to show tips from all registered elements.
- * @return {Ext.tip.QuickTip}
- */
- getQuickTip : function(){
- return tip;
+ axisTitleRight: {
+ fill: '#000',
+ font: '11px Arial'
},
-
- /**
- * Configures a new quick tip instance and assigns it to a target element. See
- * {@link Ext.tip.QuickTip#register} for details.
- * @param {Object} config The config object
- */
- register : function(){
- tip.register.apply(tip, arguments);
+ axisTitleBottom: {
+ fill: '#000',
+ font: '11px Arial'
},
-
- /**
- * Removes any registered quick tip from the target element and destroys it.
- * @param {String/HTMLElement/Element} el The element from which the quick tip is to be removed.
- */
- unregister : function(){
- tip.unregister.apply(tip, arguments);
+ 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'
+ }
+ */
- /**
- * Alias of {@link #register}.
- * @param {Object} config The config object
- */
- tips : function(){
- tip.register.apply(tip, arguments);
+(function() {
+ Ext.chart.theme = function(config, base) {
+ config = config || {};
+ var i = 0, 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);
+ 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);
};
-}());
-/**
- * @class Ext.app.Application
- * @constructor
- *
- * Represents an Ext JS 4 application, which is typically a single page app using a {@link Ext.container.Viewport Viewport}.
- * A typical Ext.app.Application might look like this:
- *
- * Ext.application({
- name: 'MyApp',
- launch: function() {
- Ext.create('Ext.container.Viewport', {
- items: {
- html: 'My App'
- }
- });
- }
- });
- *
- * This does several things. First it creates a global variable called 'MyApp' - all of your Application's classes (such
- * as its Models, Views and Controllers) will reside under this single namespace, which drastically lowers the chances
- * of colliding global variables.
- *
- * When the page is ready and all of your JavaScript has loaded, your Application's {@link #launch} function is called,
- * at which time you can run the code that starts your app. Usually this consists of creating a Viewport, as we do in
- * the example above.
- *
- * Telling Application about the rest of the app
- *
- * Because an Ext.app.Application represents an entire app, we should tell it about the other parts of the app - namely
- * the Models, Views and Controllers that are bundled with the application. Let's say we have a blog management app; we
- * might have Models and Controllers for Posts and Comments, and Views for listing, adding and editing Posts and Comments.
- * Here's how we'd tell our Application about all these things:
- *
- * Ext.application({
- name: 'Blog',
- models: ['Post', 'Comment'],
- controllers: ['Posts', 'Comments'],
+})();
+});
- launch: function() {
- ...
- }
- });
- *
- * Note that we didn't actually list the Views directly in the Application itself. This is because Views are managed by
- * Controllers, so it makes sense to keep those dependencies there. The Application will load each of the specified
- * Controllers using the pathing conventions laid out in the application
- * architecture guide - in this case expecting the controllers to reside in app/controller/Posts.js and
- * app/controller/Comments.js. In turn, each Controller simply needs to list the Views it uses and they will be
- * automatically loaded. Here's how our Posts controller like be defined:
- *
- * Ext.define('MyApp.controller.Posts', {
- extend: 'Ext.app.Controller',
- views: ['posts.List', 'posts.Edit'],
+/**
+ * @class Ext.chart.Mask
+ *
+ * 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();
+ }
+ }
+ },
- //the rest of the Controller here
- });
- *
- * Because we told our Application about our Models and Controllers, and our Controllers about their Views, Ext JS will
- * automatically load all of our app files for us. This means we don't have to manually add script tags into our html
- * files whenever we add a new class, but more importantly it enables us to create a minimized build of our entire
- * application using the Ext JS 4 SDK Tools.
- *
- * For more information about writing Ext JS 4 applications, please see the
- * application architecture guide.
+ * 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`.
*
- * @markdown
- * @docauthor Ed Spencer
*/
-Ext.define('Ext.app.Application', {
- extend: 'Ext.app.Controller',
-
- requires: [
- 'Ext.ModelManager',
- 'Ext.data.Model',
- 'Ext.data.StoreManager',
- 'Ext.tip.QuickTipManager',
- 'Ext.ComponentManager',
- 'Ext.app.EventBus'
- ],
-
- /**
- * @cfg {Object} name The name of your application. This will also be the namespace for your views, controllers
- * models and stores. Don't use spaces or special characters in the name.
- */
-
- /**
- * @cfg {Object} scope The scope to execute the {@link #launch} function in. Defaults to the Application
- * instance.
- */
- scope: undefined,
-
- /**
- * @cfg {Boolean} enableQuickTips True to automatically set up Ext.tip.QuickTip support (defaults to true)
- */
- enableQuickTips: true,
-
- /**
- * @cfg {String} defaultUrl When the app is first loaded, this url will be redirected to. Defaults to undefined
- */
-
- /**
- * @cfg {String} appFolder The path to the directory which contains all application's classes.
- * This path will be registered via {@link Ext.Loader#setPath} for the namespace specified in the {@link #name name} config.
- * Defaults to 'app'
- */
- appFolder: 'app',
-
+Ext.define('Ext.chart.Mask', {
+ require: ['Ext.chart.MaskLayer'],
/**
- * @cfg {Boolean} autoCreateViewport Automatically loads and instantiates AppName.view.Viewport before firing the launch function.
+ * Creates new Mask.
+ * @param {Object} config (optional) Config object.
*/
- autoCreateViewport: true,
-
constructor: function(config) {
- config = config || {};
- Ext.apply(this, config);
-
- var requires = config.requires || [];
-
- Ext.Loader.setPath(this.name, this.appFolder);
-
- if (this.paths) {
- Ext.Object.each(this.paths, function(key, value) {
- Ext.Loader.setPath(key, value);
- });
- }
-
- this.callParent(arguments);
-
- this.eventbus = Ext.create('Ext.app.EventBus');
-
- var controllers = this.controllers,
- ln = controllers.length,
- i, controller;
+ var me = this;
- this.controllers = Ext.create('Ext.util.MixedCollection');
+ me.addEvents('select');
- if (this.autoCreateViewport) {
- requires.push(this.getModuleClassName('Viewport', 'view'));
+ if (config) {
+ Ext.apply(me, config);
}
-
- for (i = 0; i < ln; i++) {
- requires.push(this.getModuleClassName(controllers[i], 'controller'));
+ if (me.mask) {
+ me.on('afterrender', function() {
+ //create a mask layer component
+ var comp = Ext.create('Ext.chart.MaskLayer', {
+ renderTo: me.el
+ });
+ comp.el.on({
+ 'mousemove': function(e) {
+ me.onMouseMove(e);
+ },
+ 'mouseup': function(e) {
+ me.resized(e);
+ }
+ });
+ //create a resize handler for the component
+ var resizeHandler = Ext.create('Ext.resizer.Resizer', {
+ el: comp.el,
+ handles: 'all',
+ pinned: true
+ });
+ resizeHandler.on({
+ 'resize': function(e) {
+ me.resized(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.7,
+ hidden: true,
+ stroke: '#444'
+ });
+ }, me, { single: true });
}
-
- Ext.require(requires);
-
- Ext.onReady(function() {
- for (i = 0; i < ln; i++) {
- controller = this.getController(controllers[i]);
- controller.init(this);
- }
-
- this.onBeforeLaunch.call(this);
- }, this);
},
-
- control: function(selectors, listeners, controller) {
- this.eventbus.control(selectors, listeners, controller);
+
+ resized: function(e) {
+ var me = this,
+ bbox = me.bbox || me.chartBBox,
+ x = bbox.x,
+ y = bbox.y,
+ width = bbox.width,
+ height = bbox.height,
+ box = me.mask.getBox(true),
+ max = Math.max,
+ min = Math.min,
+ staticX = box.x - x,
+ staticY = box.y - y;
+
+ staticX = max(staticX, x);
+ staticY = max(staticY, y);
+ staticX = min(staticX, width);
+ staticY = min(staticY, height);
+ box.x = staticX;
+ box.y = staticY;
+ me.fireEvent('select', me, box);
},
- /**
- * Called automatically when the page has completely loaded. This is an empty function that should be
- * overridden by each application that needs to take action on page load
- * @property launch
- * @type Function
- * @param {String} profile The detected {@link #profiles application profile}
- * @return {Boolean} By default, the Application will dispatch to the configured startup controller and
- * action immediately after running the launch function. Return false to prevent this behavior.
- */
- launch: Ext.emptyFn,
-
- /**
- * @private
- */
- onBeforeLaunch: function() {
- if (this.enableQuickTips) {
- Ext.tip.QuickTipManager.init();
- }
-
- if (this.autoCreateViewport) {
- this.getView('Viewport').create();
+ onMouseUp: function(e) {
+ var me = this,
+ bbox = me.bbox || me.chartBBox,
+ sel = me.maskSelection;
+ me.maskMouseDown = false;
+ me.mouseDown = false;
+ if (me.mouseMoved) {
+ me.onMouseMove(e);
+ me.mouseMoved = false;
+ me.fireEvent('select', me, {
+ x: sel.x - bbox.x,
+ y: sel.y - bbox.y,
+ width: sel.width,
+ height: sel.height
+ });
}
-
- this.launch.call(this.scope || this);
- this.launched = true;
- this.fireEvent('launch', this);
-
- this.controllers.each(function(controller) {
- controller.onLaunch(this);
- }, this);
},
- getModuleClassName: function(name, type) {
- var namespace = Ext.Loader.getPrefix(name);
-
- if (namespace.length > 0 && namespace !== name) {
- return name;
- }
-
- return this.name + '.' + type + '.' + name;
+ onMouseDown: function(e) {
+ var me = this;
+ me.mouseDown = true;
+ me.mouseMoved = false;
+ me.maskMouseDown = {
+ x: e.getPageX() - me.el.getX(),
+ y: e.getPageY() - me.el.getY()
+ };
},
- getController: function(name) {
- var controller = this.controllers.get(name);
-
- if (!controller) {
- controller = Ext.create(this.getModuleClassName(name, 'controller'), {
- application: this,
- id: name
- });
-
- this.controllers.add(controller);
+ onMouseMove: 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),
+ posX = e.getPageX(),
+ posY = e.getPageY(),
+ staticX = posX - me.el.getX(),
+ staticY = posY - me.el.getY(),
+ maskMouseDown = me.maskMouseDown,
+ path;
+
+ me.mouseMoved = me.mouseDown;
+ staticX = max(staticX, x);
+ staticY = max(staticY, y);
+ staticX = min(staticX, width);
+ staticY = min(staticY, height);
+ if (maskMouseDown && me.mouseDown) {
+ if (mask == 'horizontal') {
+ staticY = y;
+ maskMouseDown.y = height;
+ posY = me.el.getY() + bbox.height + me.insetPadding;
+ }
+ 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,
+ y: height > 0 ? staticY : staticY + height,
+ width: abs(width),
+ height: abs(height)
+ };
+ me.mask.updateBox(me.maskSelection);
+ me.mask.show();
+ me.maskSprite.setAttributes({
+ hidden: true
+ }, true);
}
-
- return controller;
- },
-
- getStore: function(name) {
- var store = Ext.StoreManager.get(name);
-
- if (!store) {
- store = Ext.create(this.getModuleClassName(name, 'store'), {
- storeId: name
- });
+ 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,
+ fill: me.maskMouseDown ? me.maskSprite.stroke : false,
+ 'stroke-width': mask === true ? 1 : 3,
+ hidden: false
+ }, true);
}
-
- return store;
},
- getModel: function(model) {
- model = this.getModuleClassName(model, 'model');
+ 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', {
- return Ext.ModelManager.getModel(model);
+ constructor: function() {
+ this.originalStore = this.store;
},
- getView: function(view) {
- view = this.getModuleClassName(view, 'view');
+ /**
+ * 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();
+ * }
+ * }
+ * }
+ * }
+ */
+ setZoom: function(zoomConfig) {
+ var me = this,
+ axes = me.axes,
+ bbox = me.chartBBox,
+ xScale = 1 / bbox.width,
+ yScale = 1 / bbox.height,
+ zoomer = {
+ x : zoomConfig.x * xScale,
+ y : zoomConfig.y * yScale,
+ width : zoomConfig.width * xScale,
+ height : zoomConfig.height * yScale
+ };
+ axes.each(function(axis) {
+ var ends = axis.calcEnds();
+ if (axis.position == 'bottom' || axis.position == 'top') {
+ var from = (ends.to - ends.from) * zoomer.x + ends.from,
+ to = (ends.to - ends.from) * zoomer.width + from;
+ axis.minimum = from;
+ axis.maximum = to;
+ } else {
+ var to = (ends.to - ends.from) * (1 - zoomer.y) + ends.from,
+ from = to - (ends.to - ends.from) * zoomer.height;
+ axis.minimum = from;
+ axis.maximum = to;
+ }
+ });
+ me.redraw(false);
+ },
- return Ext.ClassManager.get(view);
+ /**
+ * 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() {
+ this.store = this.substore = this.originalStore;
+ this.redraw(true);
}
+
});
/**
- * @class Ext.chart.Callout
+ * @class Ext.chart.Shape
* @ignore
*/
-Ext.define('Ext.chart.Callout', {
+Ext.define('Ext.chart.Shape', {
/* Begin Definitions */
+ singleton: true,
+
/* 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 = [];
- }
+ circle: function (surface, opts) {
+ return surface.add(Ext.apply({
+ type: 'circle',
+ x: opts.x,
+ y: opts.y,
+ stroke: null,
+ radius: opts.radius
+ }, opts));
},
-
- 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.store,
- len = store.getCount(),
- ratio = items.length / len,
- previouslyPlacedCallouts = [],
- i,
- count,
- j,
- p;
-
- for (i = 0, count = 0; i < len; i++) {
- for (j = 0; j < ratio; j++) {
- var item = items[count],
- label = group[count],
- storeItem = store.getAt(i),
- display;
-
- display = 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);
- }
- }
- }
- }
- config.renderer(label, storeItem);
- me.onPlaceCallout(label, storeItem, item, i, display, animate,
- j, count, previouslyPlacedCallouts);
- previouslyPlacedCallouts.push(label);
- count++;
- }
- }
- this.hideCallouts(count);
+ 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));
},
-
- onCreateCallout: function(storeItem, item, i, display) {
- var me = this,
- group = me.calloutsGroup,
- config = me.callouts,
- styles = config.styles,
- width = styles.width,
- height = styles.height,
- chart = me.chart,
- surface = chart.surface,
- calloutObj = {
- //label: false,
- //box: false,
- lines: false
- };
-
- calloutObj.lines = surface.add(Ext.apply({},
- {
+ 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: 'M0,0',
- stroke: me.getLegendColor() || '#555'
- },
- styles));
-
- if (config.items) {
- calloutObj.panel = Ext.create('widget.panel', {
- style: "position: absolute;",
- width: width,
- height: height,
- items: config.items,
- renderTo: chart.el
- });
- }
-
- return calloutObj;
+ 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));
},
-
- 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);
- }
+ 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.draw.CompositeSprite
- * @extends Ext.util.MixedCollection
+ * A Surface is an interface to render methods inside a draw {@link Ext.draw.Component}.
+ * A Surface contains methods to render sprites, get bounding boxes of sprites, add
+ * sprites to the canvas, initialize other graphic components, etc. One of the most used
+ * methods for this class is the `add` method, to add Sprites to the surface.
*
- * 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.
+ * Most of the Surface methods are abstract and they have a concrete implementation
+ * in VML or SVG engines.
*
- * CompositeSprite extends {@link Ext.util.MixedCollection} so you can use the same methods
- * in `MixedCollection` to iterate through sprites, add and remove elements, etc.
+ * A Surface instance can be accessed as a property of a draw component. For example:
*
- * In order to create a CompositeSprite, one has to provide a handle to the surface where it is
- * rendered:
+ * drawComponent.surface.add({
+ * type: 'circle',
+ * fill: '#ffc',
+ * radius: 100,
+ * x: 100,
+ * y: 100
+ * });
*
- * var group = Ext.create('Ext.draw.CompositeSprite', {
- * surface: drawComponent.surface
+ * The configuration object passed in the `add` method is the same as described in the {@link Ext.draw.Sprite}
+ * class documentation.
+ *
+ * # Listeners
+ *
+ * You can also add event listeners to the surface using the `Observable` listener syntax. Supported events are:
+ *
+ * - mousedown
+ * - mouseup
+ * - mouseover
+ * - mouseout
+ * - mousemove
+ * - mouseenter
+ * - mouseleave
+ * - click
+ *
+ * For example:
+ *
+ * drawComponent.surface.on({
+ * 'mousemove': function() {
+ * console.log('moving the mouse over the surface');
+ * }
+ * });
+ *
+ * # 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
+ * }
+ * }
* });
- *
- * 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', {
+Ext.define('Ext.draw.Surface', {
/* Begin Definitions */
- extend: 'Ext.util.MixedCollection',
mixins: {
- animate: 'Ext.util.Animate'
+ observable: 'Ext.util.Observable'
+ },
+
+ requires: ['Ext.draw.CompositeSprite'],
+ uses: ['Ext.draw.engine.Svg', 'Ext.draw.engine.Vml'],
+
+ separatorRe: /[, ]+/,
+
+ 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 || ['Svg', 'Vml'];
+
+ var i = 0,
+ len = enginePriority.length,
+ surfaceClass;
+
+ for (; i < len; i++) {
+ if (Ext.supports[enginePriority[i]]) {
+ return Ext.create('Ext.draw.engine.' + enginePriority[i], config);
+ }
+ }
+ return false;
+ }
},
/* End Definitions */
- isCompositeSprite: true,
+
+ // @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: "#000",
+ "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,
+
+ /**
+ * @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,
+
+
+ /**
+ * 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.addEvents(
'mousedown',
'mouseup',
'mouseover',
'mouseout',
+ 'mousemove',
+ 'mouseenter',
+ 'mouseleave',
'click'
);
- me.id = Ext.id(null, 'ext-sprite-group-');
- me.callParent();
- },
- // @private
- onClick: function(e) {
- this.fireEvent('click', e);
- },
+ me.mixins.observable.constructor.call(me);
- // @private
- onMouseUp: function(e) {
- this.fireEvent('mouseup', e);
+ me.getId();
+ me.initGradients();
+ me.initItems();
+ if (me.renderTo) {
+ me.render(me.renderTo);
+ delete me.renderTo;
+ }
+ me.initBackground(config.background);
},
- // @private
- onMouseDown: function(e) {
- this.fireEvent('mousedown', e);
- },
+ // @private called to initialize components in the surface
+ // this is dependent on the underlying implementation.
+ initSurface: Ext.emptyFn,
- // @private
- onMouseOver: function(e) {
- this.fireEvent('mouseover', e);
- },
+ // @private called to setup the surface to render an item
+ //this is dependent on the underlying implementation.
+ renderItem: Ext.emptyFn,
// @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
- });
- },
-
- /** Add a Sprite to the Group */
- add: function(key, o) {
- var result = this.callParent(arguments);
- this.attachEvents(result);
- return result;
- },
-
- insert: function(index, key, o) {
- return this.callParent(arguments);
- },
+ renderItems: Ext.emptyFn,
- /** Remove a Sprite from the Group */
- 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
- });
- me.callParent(arguments);
- },
-
- /**
- * Returns the group bounding box.
- * Behaves like {@link Ext.draw.Sprite} getBBox method.
- */
- 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) {
- 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);
- }
+ // @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();
}
-
- 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.
+ * 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
*/
- setAttributes: function(attrs, redraw) {
- var i = 0,
- items = this.items,
- len = this.length;
-
- for (; i < len; i++) {
- items[i].setAttributes(attrs, redraw);
- }
- return this;
- },
+ addCls: Ext.emptyFn,
/**
- * Hides all sprites. If the first parameter of the method is true
- * then a redraw will be forced for each sprite.
+ * 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
*/
- hide: function(attrs) {
- var i = 0,
- items = this.items,
- len = this.length;
-
- for (; i < len; i++) {
- items[i].hide();
- }
- return this;
- },
+ removeCls: Ext.emptyFn,
/**
- * Shows all sprites. If the first parameter of the method is true
- * then a redraw will be forced for each sprite.
+ * 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
*/
- show: function(attrs) {
- var i = 0,
- items = this.items,
- len = this.length;
-
- for (; i < len; i++) {
- items[i].show();
- }
- return this;
- },
+ setStyle: Ext.emptyFn,
- 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]);
- }
+ // @private
+ initGradients: function() {
+ var gradients = this.gradients;
+ if (gradients) {
+ Ext.each(gradients, this.addGradient, this);
}
- return me;
},
- 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);
- }
+ // @private
+ initItems: function() {
+ var items = this.items;
+ this.items = Ext.create('Ext.draw.CompositeSprite');
+ this.groups = Ext.create('Ext.draw.CompositeSprite');
+ if (items) {
+ this.add(items);
}
},
- 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);
+ // @private
+ initBackground: function(config) {
+ var me = this,
+ width = me.width,
+ height = me.height,
+ gradientId, gradient, backgroundSprite;
+ if (config) {
+ if (config.gradient) {
+ gradient = config.gradient;
+ gradientId = gradient.id;
+ me.addGradient(gradient);
+ me.background = me.add({
+ type: 'rect',
+ x: 0,
+ y: 0,
+ width: width,
+ height: height,
+ fill: 'url(#' + gradientId + ')'
+ });
+ } else if (config.fill) {
+ me.background = me.add({
+ type: 'rect',
+ x: 0,
+ y: 0,
+ width: width,
+ height: height,
+ fill: config.fill
+ });
+ } else if (config.image) {
+ me.background = me.add({
+ type: 'image',
+ x: 0,
+ y: 0,
+ width: width,
+ height: height,
+ src: config.image
+ });
}
}
},
- 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
+ * 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.
*/
- getSurface: function(){
- var first = this.first();
- if (first) {
- return first.surface;
+ setSize: function(w, h) {
+ if (this.background) {
+ this.background.setAttributes({
+ width: w,
+ height: h,
+ hidden: false
+ }, true);
}
- return null;
+ this.applyViewBox();
},
-
- /**
- * Destroys the SpriteGroup
- */
- destroy: function(){
- var me = this,
- surface = me.getSurface(),
- item;
-
- if (surface) {
- while (me.getCount() > 0) {
- item = me.first();
- me.remove(item);
- surface.remove(item);
+
+ // @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];
}
}
- me.clearListeners();
- }
-});
-
-/**
- * @class Ext.layout.component.Draw
- * @extends Ext.layout.component.Component
- * @private
- *
- */
+ return attrs;
+ },
-Ext.define('Ext.layout.component.Draw', {
+ // @private
+ onClick: function(e) {
+ this.processEvent('click', e);
+ },
- /* Begin Definitions */
+ // @private
+ onMouseUp: function(e) {
+ this.processEvent('mouseup', e);
+ },
- alias: 'layout.draw',
+ // @private
+ onMouseDown: function(e) {
+ this.processEvent('mousedown', e);
+ },
- extend: 'Ext.layout.component.Auto',
+ // @private
+ onMouseOver: function(e) {
+ this.processEvent('mouseover', e);
+ },
- /* End Definitions */
+ // @private
+ onMouseOut: function(e) {
+ this.processEvent('mouseout', e);
+ },
- type: 'draw',
+ // @private
+ onMouseMove: function(e) {
+ this.fireEvent('mousemove', e);
+ },
- onLayout : function(width, height) {
- this.owner.surface.setSize(width, height);
- this.callParent(arguments);
- }
-});
-/**
- * @class Ext.chart.theme.Theme
- * @ignore
- */
-Ext.define('Ext.chart.theme.Theme', {
+ // @private
+ onMouseEnter: Ext.emptyFn,
- /* Begin Definitions */
+ // @private
+ onMouseLeave: Ext.emptyFn,
- requires: ['Ext.draw.Color'],
+ /**
+ * 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'
+ * }
+ * }
+ * });
+ *
+ * @method
+ */
+ addGradient: Ext.emptyFn,
- /* End Definitions */
+ /**
+ * 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
+ * });
+ *
+ */
+ add: function() {
+ var args = Array.prototype.slice.call(arguments),
+ sprite,
+ index;
- 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 + '"');
- //
- }
- }
-},
-// This callback is executed right after when the class is created. This scope refers to the newly created class itself
-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'
- }
- */
+ var hasMultipleArgs = args.length > 1;
+ if (hasMultipleArgs || Ext.isArray(args[0])) {
+ var items = hasMultipleArgs ? args : args[0],
+ results = [],
+ i, ln, item;
-(function() {
- Ext.chart.theme = function(config, base) {
- config = config || {};
- var i = 0, 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);
+ for (i = 0, ln = items.length; i < ln; i++) {
+ item = items[i];
+ item = this.add(item);
+ results.push(item);
}
- 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];
- }
- }
+ return results;
}
- 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);
+ 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;
}
- 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);
- gradients.push({
- id: key,
- angle: 45,
- stops: {
- 0: {
- color: midColor.toString()
- },
- 100: {
- color: color.toString()
- }
- }
- });
- colors[i] = 'url(#' + key + ')';
+ else {
+ break;
}
}
- base.gradients = gradients;
- base.colors = colors;
+ // Step forward to the end of a sequence of the same or lower z-index
+ while (idx < len && sprites[idx].attr.zIndex <= zIndex) {
+ idx++;
+ }
}
- /*
- 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);
- };
-})();
-});
-
-/**
- * @class Ext.chart.Mask
- *
- * 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`.
- *
- * @constructor
- */
-Ext.define('Ext.chart.Mask', {
- constructor: function(config) {
- var me = this;
-
- me.addEvents('select');
- if (config) {
- Ext.apply(me, config);
- }
- if (me.mask) {
- me.on('afterrender', function() {
- //create a mask layer component
- var comp = Ext.create('Ext.chart.MaskLayer', {
- renderTo: me.el
- });
- comp.el.on({
- 'mousemove': function(e) {
- me.onMouseMove(e);
- },
- 'mouseup': function(e) {
- me.resized(e);
- }
- });
- //create a resize handler for the component
- var resizeHandler = Ext.create('Ext.resizer.Resizer', {
- el: comp.el,
- handles: 'all',
- pinned: true
- });
- resizeHandler.on({
- 'resize': function(e) {
- me.resized(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.7,
- hidden: true,
- stroke: '#444'
- });
- }, me, { single: true });
- }
- },
-
- resized: function(e) {
- var me = this,
- bbox = me.bbox || me.chartBBox,
- x = bbox.x,
- y = bbox.y,
- width = bbox.width,
- height = bbox.height,
- box = me.mask.getBox(true),
- max = Math.max,
- min = Math.min,
- staticX = box.x - x,
- staticY = box.y - y;
-
- staticX = max(staticX, x);
- staticY = max(staticY, y);
- staticX = min(staticX, width);
- staticY = min(staticY, height);
- box.x = staticX;
- box.y = staticY;
- me.fireEvent('select', me, box);
+ me.items.insert(idx, sprite);
+ return idx;
},
- onMouseUp: function(e) {
- var me = this,
- bbox = me.bbox || me.chartBBox,
- sel = me.maskSelection;
- me.maskMouseDown = false;
- me.mouseDown = false;
- if (me.mouseMoved) {
- me.onMouseMove(e);
- me.mouseMoved = false;
- me.fireEvent('select', me, {
- x: sel.x - bbox.x,
- y: sel.y - bbox.y,
- width: sel.width,
- height: sel.height
+ 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
+ * @return {Number} the sprite's new index in the list
+ */
+ remove: function(sprite, destroySprite) {
+ if (sprite) {
+ this.items.remove(sprite);
+ this.groups.each(function(item) {
+ item.remove(sprite);
});
+ sprite.onRemove();
+ if (destroySprite === true) {
+ sprite.destroy();
+ }
}
},
- onMouseDown: function(e) {
- var me = this;
- me.mouseDown = true;
- me.mouseMoved = false;
- me.maskMouseDown = {
- x: e.getPageX() - me.el.getX(),
- y: e.getPageY() - me.el.getY()
- };
+ /**
+ * 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.
+ * @return {Number} The sprite's new index in the list.
+ */
+ removeAll: function(destroySprites) {
+ var items = this.items.items,
+ ln = items.length,
+ i;
+ for (i = ln - 1; i > -1; i--) {
+ this.remove(items[i], destroySprites);
+ }
},
- onMouseMove: function(e) {
+ 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,
- 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),
- posX = e.getPageX(),
- posY = e.getPageY(),
- staticX = posX - me.el.getX(),
- staticY = posY - me.el.getY(),
- maskMouseDown = me.maskMouseDown,
- path;
-
- me.mouseMoved = me.mouseDown;
- staticX = max(staticX, x);
- staticY = max(staticY, y);
- staticX = min(staticX, width);
- staticY = min(staticY, height);
- if (maskMouseDown && me.mouseDown) {
- if (mask == 'horizontal') {
- staticY = y;
- maskMouseDown.y = height;
- posY = me.el.getY() + bbox.height + me.insetPadding;
+ viewBox = me.viewBox,
+ width = me.width,
+ height = me.height,
+ 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;
+
+ if (viewBoxWidth * relativeHeight < width) {
+ viewBoxX -= (width - viewBoxWidth * relativeHeight) / 2 / relativeHeight;
}
- else if (mask == 'vertical') {
- staticX = x;
- maskMouseDown.x = width;
+ if (viewBoxHeight * relativeWidth < height) {
+ viewBoxY -= (height - viewBoxHeight * relativeWidth) / 2 / relativeWidth;
}
- 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,
- y: height > 0 ? staticY : staticY + height,
- width: abs(width),
- height: abs(height)
+
+ size = 1 / Math.min(viewBoxWidth, relativeHeight);
+
+ me.viewBoxShift = {
+ dx: -viewBoxX,
+ dy: -viewBoxY,
+ scale: size
};
- me.mask.updateBox({
- x: posX - abs(width),
- y: posY - abs(height),
- width: abs(width),
- height: abs(height)
- });
- 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,
- fill: me.maskMouseDown ? me.maskSprite.stroke : false,
- 'stroke-width': mask === true ? 1 : 3,
- hidden: false
- }, true);
+ },
+
+ 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];
}
},
- 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.
- *
- * @ignore
- */
-Ext.define('Ext.chart.Navigation', {
+ // @private
+ applyTransformations: function(sprite) {
+ sprite.bbox.transform = 0;
+ this.transform(sprite);
- constructor: function() {
- this.originalStore = this.store;
- },
-
- //filters the store to the specified interval(s)
- setZoom: function(zoomConfig) {
var me = this,
- store = me.substore || me.store,
- bbox = me.chartBBox,
- len = store.getCount(),
- from = (zoomConfig.x / bbox.width * len) >> 0,
- to = Math.ceil(((zoomConfig.x + zoomConfig.width) / bbox.width * len)),
- recFieldsLen, recFields = [], curField, json = [], obj;
-
- store.each(function(rec, i) {
- if (i < from || i > to) {
- return;
- }
- obj = {};
- //get all record field names in a simple array
- if (!recFields.length) {
- rec.fields.each(function(f) {
- recFields.push(f.name);
- });
- recFieldsLen = recFields.length;
- }
- //append record values to an aggregation record
- for (i = 0; i < recFieldsLen; i++) {
- curField = recFields[i];
- obj[curField] = rec.get(curField);
- }
- json.push(obj);
+ dirty = false,
+ attr = sprite.attr;
+
+ if (attr.translation.x != null || attr.translation.y != null) {
+ me.translate(sprite);
+ dirty = true;
+ }
+ if (attr.scaling.x != null || attr.scaling.y != null) {
+ me.scale(sprite);
+ dirty = true;
+ }
+ if (attr.rotation.degrees != null) {
+ me.rotate(sprite);
+ dirty = true;
+ }
+ if (dirty) {
+ sprite.bbox.transform = 0;
+ this.transform(sprite);
+ 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);
+ 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
});
- me.store = me.substore = Ext.create('Ext.data.JsonStore', {
- fields: recFields,
- data: json
+ },
+
+ // @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
});
- me.redraw(true);
},
- restoreZoom: function() {
- this.store = this.substore = this.originalStore;
- this.redraw(true);
- }
-
-});
-/**
- * @class Ext.chart.Shape
- * @ignore
- */
-Ext.define('Ext.chart.Shape', {
+ // @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;
- /* Begin Definitions */
+ if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) {
+ bbox = this.getBBox(sprite);
+ 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
+ });
+ },
- singleton: true,
+ // @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"]];
+ },
- /* End Definitions */
+ // @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"]];
+ },
- circle: function (surface, opts) {
- return surface.add(Ext.apply({
- type: 'circle',
- x: opts.x,
- y: opts.y,
- stroke: null,
- radius: opts.radius
- }, opts));
+ // @private
+ getPathpath: function (el) {
+ return el.attr.path;
},
- 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));
+
+ // @private
+ getPathcircle: function (el) {
+ var a = el.attr;
+ return this.ellipsePath(a.x, a.y, a.radius, a.radius);
},
- 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));
+
+ // @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);
},
- 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));
+
+ // @private
+ getPathrect: function (el) {
+ var a = el.attr;
+ return this.rectPath(a.x, a.y, a.width, a.height, a.r);
},
- 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));
+
+ // @private
+ getPathimage: function (el) {
+ var a = el.attr;
+ return this.rectPath(a.x || 0, a.y || 0, a.width, a.height);
},
- 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));
+
+ // @private
+ getPathtext: function (el) {
+ var bbox = this.getBBoxText(el);
+ return this.rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
},
- 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));
+
+ createGroup: function(id) {
+ var group = this.groups.get(id);
+ if (!group) {
+ group = Ext.create('Ext.draw.CompositeSprite', {
+ surface: this
+ });
+ group.id = id || Ext.id(null, 'ext-surface-group-');
+ this.groups.add(group);
+ }
+ return group;
},
- 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));
+
+ /**
+ * 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) {
+ if (typeof id == "string") {
+ var group = this.groups.get(id);
+ if (!group) {
+ group = this.createGroup(id);
+ }
+ } else {
+ group = id;
+ }
+ return group;
},
- 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
+
+ // @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;
}
- });
- 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'
- });
+ }
+ 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() {
+ delete this.domRef;
+ this.removeAll();
}
});
/**
- * @class Ext.draw.Surface
- * @extends Object
+ * @class Ext.layout.component.Draw
+ * @extends Ext.layout.component.Component
+ * @private
*
- * A Surface is an interface to render methods inside a draw {@link Ext.draw.Component}.
- * A Surface contains methods to render sprites, get bounding boxes of sprites, add
- * sprites to the canvas, initialize other graphic components, etc. One of the most used
- * methods for this class is the `add` method, to add Sprites to the surface.
+ */
+
+Ext.define('Ext.layout.component.Draw', {
+
+ /* Begin Definitions */
+
+ alias: 'layout.draw',
+
+ extend: 'Ext.layout.component.Auto',
+
+ /* End Definitions */
+
+ type: 'draw',
+
+ onLayout : function(width, height) {
+ this.owner.surface.setSize(width, height);
+ this.callParent(arguments);
+ }
+});
+/**
+ * @class Ext.draw.Component
+ * @extends Ext.Component
+ *
+ * The Draw Component is a surface in which sprites can be rendered. The Draw Component
+ * manages and holds a `Surface` instance: an interface that has
+ * an SVG or VML implementation depending on the browser capabilities and where
+ * 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();
*
- * Most of the Surface methods are abstract and they have a concrete implementation
- * in VML or SVG engines.
+ * In this case we created a draw component and added a sprite to it.
+ * The *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.
*
- * A Surface instance can be accessed as a property of a draw component. For example:
+ * You can also add sprites by using the surface's add method:
*
* drawComponent.surface.add({
* type: 'circle',
- * fill: '#ffc',
+ * fill: '#79BB3F',
* 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.
- *
- * ### Listeners
- *
- * You can also add event listeners to the surface using the `Observable` listener syntax. Supported events are:
- *
- * - mousedown
- * - mouseup
- * - mouseover
- * - mouseout
- * - mousemove
- * - mouseenter
- * - mouseleave
- * - click
- *
- * For example:
- *
- drawComponent.surface.on({
- 'mousemove': function() {
- console.log('moving the mouse over the surface');
- }
- });
+ * For more information on Sprites, the core elements added to a draw component's surface,
+ * refer to the Ext.draw.Sprite documentation.
*/
-Ext.define('Ext.draw.Surface', {
+Ext.define('Ext.draw.Component', {
/* Begin Definitions */
- mixins: {
- observable: 'Ext.util.Observable'
- },
+ alias: 'widget.draw',
- requires: ['Ext.draw.CompositeSprite'],
- uses: ['Ext.draw.engine.Svg', 'Ext.draw.engine.Vml'],
+ extend: 'Ext.Component',
- separatorRe: /[, ]+/,
+ requires: [
+ 'Ext.draw.Surface',
+ 'Ext.layout.component.Draw'
+ ],
- statics: {
- /**
- * Create and return a new concrete Surface instance appropriate for the current environment.
- * @param {Object} config Initial configuration for the Surface instance
- * @param {Array} enginePriority Optional order of implementations to use; the first one that is
- * available in the current environment will be used. Defaults to
- * ['Svg', 'Vml']
.
- */
- create: function(config, enginePriority) {
- enginePriority = enginePriority || ['Svg', 'Vml'];
+ /* End Definitions */
- var i = 0,
- len = enginePriority.length,
- surfaceClass;
+ /**
+ * @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'],
- for (; i < len; i++) {
- if (Ext.supports[enginePriority[i]]) {
- return Ext.create('Ext.draw.engine.' + enginePriority[i], config);
+ 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 yor items. Defaults to true.
+ */
+ viewBox: true,
+
+ /**
+ * @cfg {Boolean} autoSize
+ * Turn on autoSize support which will set the bounding div's size to the natural size of the contents. Defaults to false.
+ */
+ 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);
+ */
+ initComponent: function() {
+ this.callParent(arguments);
+
+ this.addEvents(
+ 'mousedown',
+ 'mouseup',
+ 'mousemove',
+ 'mouseenter',
+ 'mouseleave',
+ 'click'
+ );
+ },
+
+ /**
+ * @private
+ *
+ * Create the Surface on initial render
+ */
+ onRender: function() {
+ var me = this,
+ viewBox = me.viewBox,
+ autoSize = me.autoSize,
+ bbox, items, width, height, x, y;
+ me.callParent(arguments);
+
+ if (me.createSurface() !== false) {
+ items = me.surface.items;
+
+ if (viewBox || autoSize) {
+ bbox = items.getBBox();
+ width = bbox.width;
+ height = bbox.height;
+ x = bbox.x;
+ y = bbox.y;
+ if (me.viewBox) {
+ me.surface.setViewBox(x, y, width, height);
+ }
+ else {
+ // AutoSized
+ me.autoSizeSurface();
}
}
- return false;
}
},
- /* End Definitions */
+ //@private
+ autoSizeSurface: function() {
+ var me = this,
+ items = me.surface.items,
+ bbox = items.getBBox(),
+ width = bbox.width,
+ height = bbox.height;
+ items.setAttributes({
+ translate: {
+ x: -bbox.x,
+ //Opera has a slight offset in the y axis.
+ y: -bbox.y + (+Ext.isOpera)
+ }
+ }, true);
+ if (me.rendered) {
+ me.setSize(width, height);
+ me.surface.setSize(width, height);
+ }
+ else {
+ me.surface.setSize(width, height);
+ }
+ me.el.setSize(width, height);
+ },
- // @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: "#000",
- "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
+ /**
+ * 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);
+ */
+ createSurface: function() {
+ var surface = Ext.draw.Surface.create(Ext.apply({}, {
+ width: this.width,
+ height: this.height,
+ renderTo: this.el
+ }, this.initialConfig));
+ if (!surface) {
+ // In case we cannot create a surface, return false so we can stop
+ return false;
+ }
+ this.surface = surface;
+
+
+ function refire(eventName) {
+ return function(e) {
+ this.fireEvent(eventName, e);
+ };
+ }
+
+ surface.on({
+ scope: this,
+ mouseup: refire('mouseup'),
+ mousedown: refire('mousedown'),
+ mousemove: refire('mousemove'),
+ mouseenter: refire('mouseenter'),
+ mouseleave: refire('mouseleave'),
+ click: refire('click')
+ });
},
- /**
- * @cfg {Number} height
- * The height of this component in pixels (defaults to auto).
- * Note to express this dimension as a percentage or offset see {@link Ext.Component#anchor}.
- */
- /**
- * @cfg {Number} width
- * The width of this component in pixels (defaults to auto).
- * Note to express this dimension as a percentage or offset see {@link Ext.Component#anchor}.
- */
- container: undefined,
- height: 352,
- width: 512,
+
+ /**
+ * @private
+ *
+ * Clean up the Surface instance on component destruction
+ */
+ onDestroy: function() {
+ var surface = this.surface;
+ if (surface) {
+ surface.destroy();
+ }
+ this.callParent(arguments);
+ }
+
+});
+
+/**
+ * @class Ext.chart.LegendItem
+ * @extends Ext.draw.CompositeSprite
+ * 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 */
+
+ // Position of the item, relative to the upper-left corner of the legend box
x: 0,
y: 0,
+ zIndex: 500,
constructor: function(config) {
- var me = this;
- config = config || {};
- Ext.apply(me, config);
+ this.callParent(arguments);
+ this.createLegend(config);
+ },
- me.domRef = Ext.getDoc().dom;
+ /**
+ * Creates all the individual sprites for this legend item
+ */
+ createLegend: function(config) {
+ var me = this,
+ index = config.yFieldIndex,
+ series = me.series,
+ seriesType = series.type,
+ idx = me.yFieldIndex,
+ legend = me.legend,
+ surface = me.surface,
+ refX = legend.x + me.x,
+ refY = legend.y + me.y,
+ bbox, z = me.zIndex,
+ markerConfig, label, mask,
+ radius, toggle = false,
+ seriesStyle = Ext.apply(series.seriesStyle, series.style);
+
+ function getSeriesProp(name) {
+ var val = series[name];
+ return (Ext.isArray(val) ? val[idx] : val);
+ }
- me.customAttributes = {};
+ label = me.add('label', surface.add({
+ type: 'text',
+ x: 20,
+ y: 0,
+ zIndex: z || 0,
+ font: legend.labelFont,
+ text: getSeriesProp('title') || getSeriesProp('yField')
+ }));
- me.addEvents(
- 'mousedown',
- 'mouseup',
- 'mouseover',
- 'mouseout',
- 'mousemove',
- 'mouseenter',
- 'mouseleave',
- 'click'
- );
+ // Line series - display as short line with optional marker in the middle
+ if (seriesType === 'line' || seriesType === 'scatter') {
+ if(seriesType === 'line') {
+ me.add('line', surface.add({
+ type: 'path',
+ path: 'M0.5,0.5L16.5,0.5',
+ zIndex: z,
+ "stroke-width": series.lineWidth,
+ "stroke-linejoin": "round",
+ "stroke-dasharray": series.dash,
+ stroke: seriesStyle.stroke || '#000',
+ style: {
+ cursor: 'pointer'
+ }
+ }));
+ }
+ if (series.showMarkers || seriesType === 'scatter') {
+ markerConfig = Ext.apply(series.markerStyle, series.markerConfig || {});
+ me.add('marker', Ext.chart.Shape[markerConfig.type](surface, {
+ fill: markerConfig.fill,
+ x: 8.5,
+ y: 0.5,
+ zIndex: z,
+ radius: markerConfig.radius || markerConfig.size,
+ style: {
+ cursor: 'pointer'
+ }
+ }));
+ }
+ }
+ // All other series types - display as filled box
+ else {
+ me.add('box', surface.add({
+ type: 'rect',
+ zIndex: z,
+ x: 0,
+ y: 0,
+ width: 12,
+ height: 12,
+ fill: series.getLegendColor(index),
+ style: {
+ cursor: 'pointer'
+ }
+ }));
+ }
+
+ me.setAttributes({
+ hidden: false
+ }, true);
+
+ bbox = me.getBBox();
+
+ mask = me.add('mask', surface.add({
+ type: 'rect',
+ x: bbox.x,
+ y: bbox.y,
+ width: bbox.width || 20,
+ height: bbox.height || 20,
+ zIndex: (z || 0) + 1000,
+ fill: '#f00',
+ opacity: 0,
+ style: {
+ 'cursor': 'pointer'
+ }
+ }));
+
+ //add toggle listener
+ me.on('mouseover', function() {
+ label.setStyle({
+ 'font-weight': 'bold'
+ });
+ mask.setStyle({
+ 'cursor': 'pointer'
+ });
+ series._index = index;
+ series.highlightItem();
+ }, me);
+
+ me.on('mouseout', function() {
+ label.setStyle({
+ 'font-weight': 'normal'
+ });
+ series._index = index;
+ series.unHighlightItem();
+ }, me);
+
+ if (!series.visibleInLegend(index)) {
+ toggle = true;
+ label.setAttributes({
+ opacity: 0.5
+ }, true);
+ }
+
+ me.on('mousedown', function() {
+ if (!toggle) {
+ series.hideAll();
+ label.setAttributes({
+ opacity: 0.5
+ }, true);
+ } else {
+ series.showAll();
+ label.setAttributes({
+ opacity: 1
+ }, true);
+ }
+ toggle = !toggle;
+ }, me);
+ me.updatePosition({x:0, y:0}); //Relative to 0,0 at first so that the bbox is calculated correctly
+ },
+
+ /**
+ * 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,
+ i = 0,
+ item;
+ if (!relativeTo) {
+ relativeTo = me.legend;
+ }
+ for (; i < ln; i++) {
+ item = items[i];
+ switch (item.type) {
+ case 'text':
+ item.setAttributes({
+ x: 20 + relativeTo.x + me.x,
+ y: relativeTo.y + me.y
+ }, true);
+ break;
+ case 'rect':
+ item.setAttributes({
+ translate: {
+ x: relativeTo.x + me.x,
+ y: relativeTo.y + me.y - 6
+ }
+ }, true);
+ break;
+ default:
+ item.setAttributes({
+ translate: {
+ x: relativeTo.x + me.x,
+ y: relativeTo.y + me.y
+ }
+ }, 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 `showInSeries` 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',
+ * grid: true,
+ * 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,
- me.mixins.observable.constructor.call(me);
+ /**
+ * @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',
- me.getId();
- me.initGradients();
- me.initItems();
- if (me.renderTo) {
- me.render(me.renderTo);
- delete me.renderTo;
- }
- me.initBackground(config.background);
- },
+ /**
+ * @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,
- // @private called to initialize components in the surface
- // this is dependent on the underlying implementation.
- initSurface: Ext.emptyFn,
+ /**
+ * @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,
- // @private called to setup the surface to render an item
- //this is dependent on the underlying implementation.
- renderItem: Ext.emptyFn,
+ /**
+ * @cfg {String} labelFont
+ * Font to be used for the legend labels, eg '12px Helvetica'
+ */
+ labelFont: '12px Helvetica, sans-serif',
- // @private
- renderItems: Ext.emptyFn,
+ /**
+ * @cfg {String} boxStroke
+ * Style of the stroke for the legend box
+ */
+ boxStroke: '#000',
- // @private
- setViewBox: Ext.emptyFn,
+ /**
+ * @cfg {String} boxStrokeWidth
+ * Width of the stroke for the legend box
+ */
+ boxStrokeWidth: 1,
/**
- * 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/Array} className The CSS class to add, or an array of classes
+ * @cfg {String} boxFill
+ * Fill style for the legend box
*/
- addCls: Ext.emptyFn,
+ boxFill: '#FFF',
/**
- * 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/Array} className The CSS class to remove, or an array of classes
+ * @cfg {Number} itemSpacing
+ * Amount of space between legend items
*/
- removeCls: Ext.emptyFn,
+ itemSpacing: 10,
/**
- * 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.
+ * @cfg {Number} padding
+ * Amount of padding between the legend box's border and its items
*/
- setStyle: Ext.emptyFn,
+ padding: 5,
// @private
- initGradients: function() {
- var gradients = this.gradients;
- if (gradients) {
- Ext.each(gradients, this.addGradient, this);
- }
- },
-
+ width: 0,
// @private
- initItems: function() {
- var items = this.items;
- this.items = Ext.create('Ext.draw.CompositeSprite');
- this.groups = Ext.create('Ext.draw.CompositeSprite');
- if (items) {
- this.add(items);
+ 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
- initBackground: function(config) {
- var gradientId,
- gradient,
- backgroundSprite,
- width = this.width,
- height = this.height;
- if (config) {
- if (config.gradient) {
- gradient = config.gradient;
- gradientId = gradient.id;
- this.addGradient(gradient);
- this.background = this.add({
- type: 'rect',
- x: 0,
- y: 0,
- width: width,
- height: height,
- fill: 'url(#' + gradientId + ')'
- });
- } else if (config.fill) {
- this.background = this.add({
- type: 'rect',
- x: 0,
- y: 0,
- width: width,
- height: height,
- fill: config.fill
- });
- } else if (config.image) {
- this.background = this.add({
- type: 'image',
- x: 0,
- y: 0,
- width: width,
- height: height,
- src: config.image
+
+ /**
+ * @private Create all the sprites for the legend
+ */
+ create: function() {
+ var me = this;
+ me.createBox();
+ me.createItems();
+ if (!me.created && me.isDisplayed()) {
+ me.created = true;
+
+ // Listen for changes to series titles to trigger regeneration of the legend
+ me.chart.series.each(function(series) {
+ series.on('titlechange', function() {
+ me.create();
+ me.updatePosition();
});
- }
+ });
}
},
-
+
/**
- * 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.
+ * @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.
*/
- setSize: function(w, h) {
- if (this.background) {
- this.background.setAttributes({
- width: w,
- height: h,
- hidden: false
- }, true);
- }
+ isDisplayed: function() {
+ return this.visible && this.chart.series.findIndex('showInLegend', true) !== -1;
},
- // @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;
+ /**
+ * @private Create the series markers and labels
+ */
+ createItems: function() {
+ var me = this,
+ chart = me.chart,
+ surface = chart.surface,
+ items = me.items,
+ padding = me.padding,
+ itemSpacing = me.itemSpacing,
+ spacingOffset = 2,
+ maxWidth = 0,
+ maxHeight = 0,
+ totalWidth = 0,
+ totalHeight = 0,
+ vertical = me.isVertical,
+ math = Math,
+ mfloor = math.floor,
+ mmax = math.max,
+ index = 0,
+ i = 0,
+ len = items ? items.length : 0,
+ x, y, spacing, item, bbox, height, width;
+
+ //remove all legend items
+ if (len) {
+ for (; i < len; i++) {
+ items[i].destroy();
}
- else if (this.availableAttrs.hasOwnProperty(i) && !exclude[i]) {
- // Passtrhough attr
- attrs[i] = sattr[i];
+ }
+ //empty array
+ items.length = [];
+ // Create all the item labels, collecting their dimensions and positioning each one
+ // properly in relation to the previous item
+ chart.series.each(function(series, i) {
+ if (series.showInLegend) {
+ Ext.each([].concat(series.yField), function(field, j) {
+ item = Ext.create('Ext.chart.LegendItem', {
+ legend: this,
+ series: series,
+ surface: chart.surface,
+ yFieldIndex: j
+ });
+ 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;
+
+ if (i + j === 0) {
+ spacing = vertical ? padding + height / 2 : padding;
+ }
+ else {
+ spacing = itemSpacing / (vertical ? 2 : 1);
+ }
+ // Set the item's position relative to the legend box
+ item.x = mfloor(vertical ? padding : totalWidth + spacing);
+ item.y = mfloor(vertical ? totalHeight + spacing : padding + height / 2);
+
+ // Collect cumulative dimensions
+ totalWidth += width + spacing;
+ totalHeight += height + spacing;
+ maxWidth = mmax(maxWidth, width);
+ maxHeight = mmax(maxHeight, height);
+
+ items.push(item);
+ }, this);
}
+ }, me);
+
+ // Store the collected dimensions for later
+ me.width = mfloor((vertical ? maxWidth : totalWidth) + padding * 2);
+ if (vertical && items.length === 1) {
+ spacingOffset = 1;
}
- return attrs;
+ me.height = mfloor((vertical ? totalHeight - spacingOffset * spacing : maxHeight) + (padding * 2));
+ me.itemHeight = maxHeight;
},
- // @private
- onClick: function(e) {
- this.processEvent('click', e);
+ /**
+ * @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,
+ height: me.height
+ };
},
- // @private
- onMouseUp: function(e) {
- this.processEvent('mouseup', e);
- },
+ /**
+ * @private Create the box around the legend items
+ */
+ createBox: function() {
+ var me = this,
+ box;
- // @private
- onMouseDown: function(e) {
- this.processEvent('mousedown', e);
- },
+ if (me.boxSprite) {
+ me.boxSprite.destroy();
+ }
+
+ box = me.boxSprite = me.chart.surface.add(Ext.apply({
+ type: 'rect',
+ stroke: me.boxStroke,
+ "stroke-width": me.boxStrokeWidth,
+ fill: me.boxFill,
+ zIndex: me.boxZIndex
+ }, me.getBBox()));
- // @private
- onMouseOver: function(e) {
- this.processEvent('mouseover', e);
+ box.redraw();
},
- // @private
- onMouseOut: function(e) {
- this.processEvent('mouseout', e);
- },
+ /**
+ * @private Update the position of all the legend's sprites to match its current x/y values
+ */
+ updatePosition: function() {
+ var me = this,
+ x, y,
+ legendWidth = me.width,
+ legendHeight = me.height,
+ padding = me.padding,
+ 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;
- // @private
- onMouseMove: function(e) {
- this.fireEvent('mousemove', e);
+ if (me.isDisplayed()) {
+ // 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;
+ }
+ me.x = x;
+ me.y = y;
+
+ // Update the position of each item
+ Ext.each(me.items, function(item) {
+ item.updatePosition();
+ });
+ // Update the position of the outer box
+ me.boxSprite.setAttributes(me.getBBox(), true);
+ }
+ }
+});
+
+/**
+ * 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 [Simple Chart Example](doc-resources/Ext.chart.Chart/examples/simple_chart/index.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 [Drawing and Charting Guide](#/guide/drawing_and_charting).
+ *
+ */
+Ext.define('Ext.chart.Chart', {
+
+ /* Begin Definitions */
+
+ alias: 'widget.chart',
+
+ extend: 'Ext.draw.Component',
+
+ mixins: {
+ themeManager: 'Ext.chart.theme.Theme',
+ mask: 'Ext.chart.Mask',
+ navigation: 'Ext.chart.Navigation'
},
- // @private
- onMouseEnter: Ext.emptyFn,
+ requires: [
+ 'Ext.util.MixedCollection',
+ 'Ext.data.StoreManager',
+ 'Ext.chart.Legend',
+ 'Ext.util.DelayedTask'
+ ],
+
+ /* End Definitions */
// @private
- onMouseLeave: Ext.emptyFn,
+ viewBox: false,
/**
- * Add 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'
- }
- }
- });
+ * @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'.
*/
- addGradient: Ext.emptyFn,
/**
- * Add 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
- * });
- *
- */
- add: function() {
- var args = Array.prototype.slice.call(arguments),
- sprite,
- index;
-
- var hasMultipleArgs = args.length > 1;
- if (hasMultipleArgs || Ext.isArray(args[0])) {
- var items = hasMultipleArgs ? args : args[0],
- results = [],
- i, ln, item;
-
- for (i = 0, ln = items.length; i < ln; i++) {
- item = items[i];
- item = this.add(item);
- results.push(item);
- }
+ * @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,
- return results;
- }
- sprite = this.prepareItems(args[0], true)[0];
- this.normalizeSpriteCollection(sprite);
- this.onAdd(sprite);
- return sprite;
- },
+ /**
+ * @cfg {Boolean/Object} legend
+ * True for the default legend display or a legend config object. Defaults to false.
+ */
+ legend: false,
/**
- * @private
- * Insert or move a given sprite into the correct position in the items
- * MixedCollection, according to its zIndex. Will be inserted at the end of
- * an existing series of sprites with the same or lower zIndex. If the sprite
- * is already positioned within an appropriate zIndex group, it will not be moved.
- * This ordering can be used by subclasses to assist in rendering 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
+ * @cfg {Number} insetPadding
+ * The amount of inset padding in pixels for the chart. Defaults to 10.
*/
- normalizeSpriteCollection: function(sprite) {
- var items = this.items,
- zIndex = sprite.attr.zIndex,
- idx = items.indexOf(sprite);
-
- if (idx < 0 || (idx > 0 && items.getAt(idx - 1).attr.zIndex > zIndex) ||
- (idx < items.length - 1 && items.getAt(idx + 1).attr.zIndex < zIndex)) {
- items.removeAt(idx);
- idx = items.findIndexBy(function(otherSprite) {
- return otherSprite.attr.zIndex > zIndex;
- });
- if (idx < 0) {
- idx = items.length;
- }
- items.insert(idx, sprite);
- }
- return idx;
- },
+ insetPadding: 10,
- 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();
- }
- },
+ /**
+ * @cfg {String[]} enginePriority
+ * Defines the priority order for which Surface implementation to use. The first one supported by the current
+ * environment will be used. Defaults to `['Svg', 'Vml']`.
+ */
+ enginePriority: ['Svg', 'Vml'],
/**
- * Remove a given sprite from the surface, optionally destroying the sprite in the process.
- * You can also call the sprite own `remove` method.
+ * @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
*
- * For example:
+ * background: {
+ * //color string
+ * fill: '#ccc'
+ * }
*
- * drawComponent.surface.remove(sprite);
- * //or...
- * sprite.remove();
- *
- * @param {Ext.draw.Sprite} sprite
- * @param {Boolean} destroySprite
- * @return {Number} the sprite's new index in the list
+ * 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'
+ * }
+ * }
+ * }
+ * }
*/
- remove: function(sprite, destroySprite) {
- if (sprite) {
- this.items.remove(sprite);
- this.groups.each(function(item) {
- item.remove(sprite);
- });
- sprite.onRemove();
- if (destroySprite === true) {
- sprite.destroy();
- }
- }
- },
+ background: false,
/**
- * Remove all sprites from the surface, optionally destroying the sprites in the process.
+ * @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:
*
- * drawComponent.surface.removeAll();
- *
- * @param {Boolean} destroySprites Whether to destroy all sprites when removing them.
- * @return {Number} The sprite's new index in the list.
+ * 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);
*/
- 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,
+ /**
+ * @cfg {Ext.data.Store} store
+ * The store that supplies data to this chart.
+ */
- // @private
- applyTransformations: function(sprite) {
- sprite.bbox.transform = 0;
- this.transform(sprite);
+ /**
+ * @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'
+ * }]
+ */
+ constructor: function(config) {
var me = this,
- dirty = false,
- attr = sprite.attr;
-
- if (attr.translation.x != null || attr.translation.y != null) {
- me.translate(sprite);
- dirty = true;
- }
- if (attr.scaling.x != null || attr.scaling.y != null) {
- me.scale(sprite);
- dirty = true;
+ defaultAnim;
+
+ config = Ext.apply({}, config);
+ me.initTheme(config.theme || me.theme);
+ if (me.gradients) {
+ Ext.apply(config, { gradients: me.gradients });
}
- if (attr.rotation.degrees != null) {
- me.rotate(sprite);
- dirty = true;
+ if (me.background) {
+ Ext.apply(config, { background: me.background });
}
- if (dirty) {
- sprite.bbox.transform = 0;
- this.transform(sprite);
- sprite.transformations = [];
+ 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.mask.constructor.call(me, config);
+ me.mixins.navigation.constructor.call(me, config);
+ me.callParent([config]);
},
-
- // @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);
- 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
- });
+
+ getChartStore: function(){
+ return this.substore || this.store;
},
- // @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
+ initComponent: function() {
+ var me = this,
+ axes,
+ series;
+ me.callParent();
+ me.addEvents(
+ 'itemmousedown',
+ 'itemmouseup',
+ 'itemmouseover',
+ 'itemmouseout',
+ 'itemclick',
+ 'itemdoubleclick',
+ 'itemdragstart',
+ 'itemdrag',
+ 'itemdragend',
+ /**
+ * @event beforerefresh
+ * Fires before a refresh to the chart data is called. If the beforerefresh handler returns false the
+ * {@link #refresh} action will be cancelled.
+ * @param {Ext.chart.Chart} this
+ */
+ 'beforerefresh',
+ /**
+ * @event refresh
+ * Fires after the chart data has been refreshed.
+ * @param {Ext.chart.Chart} this
+ */
+ 'refresh'
+ );
+ Ext.applyIf(me, {
+ zoom: {
+ width: 1,
+ height: 1,
+ x: 0,
+ y: 0
+ }
});
- },
-
- // @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);
- centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX;
- centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY;
+ me.maxGutter = [0, 0];
+ me.store = Ext.data.StoreManager.lookup(me.store);
+ axes = me.axes;
+ me.axes = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.position; });
+ if (axes) {
+ me.axes.addAll(axes);
}
- 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"]];
+ series = me.series;
+ me.series = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.seriesId || (a.seriesId = Ext.id(null, 'ext-chart-series-')); });
+ if (series) {
+ me.series.addAll(series);
}
- 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;
+ if (me.legend !== false) {
+ me.legend = Ext.create('Ext.chart.Legend', Ext.applyIf({chart:me}, me.legend));
}
- 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.radiusY);
- },
-
- // @private
- getPathrect: function (el) {
- var a = el.attr;
- return this.rectPath(a.x, a.y, a.width, a.height, a.r);
- },
-
- // @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);
+ me.on({
+ mousemove: me.onMouseMove,
+ mouseleave: me.onMouseLeave,
+ mousedown: me.onMouseDown,
+ mouseup: me.onMouseUp,
+ scope: me
+ });
},
- createGroup: function(id) {
- var group = this.groups.get(id);
- if (!group) {
- group = Ext.create('Ext.draw.CompositeSprite', {
- surface: this
- });
- group.id = id || Ext.id(null, 'ext-surface-group-');
- this.groups.add(group);
+ // @private overrides the component method to set the correct dimensions to the chart.
+ afterComponentLayout: function(width, height) {
+ var me = this;
+ if (Ext.isNumber(width) && Ext.isNumber(height)) {
+ me.curWidth = width;
+ me.curHeight = height;
+ me.redraw(true);
}
- return group;
+ this.callParent(arguments);
},
/**
- * 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}.
+ * 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.
*/
- getGroup: function(id) {
- if (typeof id == "string") {
- var group = this.groups.get(id);
- if (!group) {
- group = this.createGroup(id);
- }
- } else {
- group = id;
- }
- return group;
- },
+ redraw: function(resize) {
+ var me = this,
+ chartBBox = me.chartBBox = {
+ x: 0,
+ y: 0,
+ height: me.curHeight,
+ width: me.curWidth
+ },
+ legend = me.legend;
+ me.surface.setSize(chartBBox.width, chartBBox.height);
+ // Instantiate Series and Axes
+ me.series.each(me.initializeSeries, me);
+ me.axes.each(me.initializeAxis, me);
+ //process all views (aggregated data etc) on stores
+ //before rendering.
+ me.axes.each(function(axis) {
+ axis.processView();
+ });
+ me.axes.each(function(axis) {
+ axis.drawAxis(true);
+ });
- // @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;
- }
+ // Create legend if not already created
+ if (legend !== false) {
+ legend.create();
}
- 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.
- */
- 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() {
- delete this.domRef;
- this.removeAll();
- }
-});
-/**
- * @class Ext.draw.Component
- * @extends Ext.Component
- *
- * The Draw Component is a surface in which sprites can be rendered. The Draw Component
- * manages and holds a `Surface` instance: an interface that has
- * an SVG or VML implementation depending on the browser capabilities and where
- * Sprites can be appended.
- * {@img Ext.draw.Component/Ext.draw.Component.png Ext.draw.Component component}
- * One way to create a draw component is:
- *
- * 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 sprite to it.
- * The *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
- * });
- *
- * For more information on Sprites, the core elements added to a draw component's surface,
- * refer to the Ext.draw.Sprite documentation.
- */
-Ext.define('Ext.draw.Component', {
+ // Place axes properly, including influence from each other
+ me.alignAxes();
- /* Begin Definitions */
+ // Reposition legend based on new axis alignment
+ if (me.legend !== false) {
+ legend.updatePosition();
+ }
- alias: 'widget.draw',
+ // Find the max gutter
+ me.getMaxGutter();
- extend: 'Ext.Component',
+ // Draw axes and series
+ me.resizing = !!resize;
- requires: [
- 'Ext.draw.Surface',
- 'Ext.layout.component.Draw'
- ],
+ me.axes.each(me.drawAxis, me);
+ me.series.each(me.drawCharts, me);
+ me.resizing = false;
+ },
- /* End Definitions */
+ // @private set the store after rendering the chart.
+ afterRender: function() {
+ var ref,
+ me = this;
+ this.callParent();
- /**
- * @cfg {Array} 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'],
+ if (me.categoryNames) {
+ me.setCategoryNames(me.categoryNames);
+ }
- baseCls: Ext.baseCSSPrefix + 'surface',
+ if (me.tipRenderer) {
+ ref = me.getFunctionRef(me.tipRenderer);
+ me.setTipRenderer(ref.fn, ref.scope);
+ }
+ me.bindStore(me.store, true);
+ me.refresh();
+ },
- componentLayout: 'draw',
+ // @private get x and y position of the mouse cursor.
+ getEventXY: function(e) {
+ var me = this,
+ box = this.surface.getRegion(),
+ pageXY = e.getXY(),
+ x = pageXY[0] - box.left,
+ y = pageXY[1] - box.top;
+ return [x, y];
+ },
- /**
- * @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 yor items. Defaults to true.
- */
- viewBox: true,
+ // @private wrap the mouse down position to delegate the event to the series.
+ onClick: function(e) {
+ var me = this,
+ position = me.getEventXY(e),
+ item;
- /**
- * @cfg {Boolean} autoSize
- * Turn on autoSize support which will set the bounding div's size to the natural size of the contents. Defaults to false.
- */
- autoSize: false,
-
- /**
- * @cfg {Array} 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
- *
- *
-
- For example:
-
-
- gradients: [{
- id: 'gradientId',
- angle: 45,
- stops: {
- 0: {
- color: '#555'
- },
- 100: {
- color: '#ddd'
+ // Ask each series if it has an item corresponding to (not necessarily exactly
+ // on top of) the current mouse coords. Fire itemclick event.
+ me.series.each(function(series) {
+ 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('itemclick', item);
+ }
}
}
- }, {
- id: 'gradientId2',
- angle: 0,
- stops: {
- 0: {
- color: '#590'
- },
- 20: {
- color: '#599'
- },
- 100: {
- color: '#ddd'
+ }, me);
+ },
+
+ // @private wrap the mouse down position to delegate the event to the series.
+ onMouseDown: function(e) {
+ var me = this,
+ position = me.getEventXY(e),
+ item;
+
+ if (me.mask) {
+ 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 mousedown event.
+ me.series.each(function(series) {
+ 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);
+ }
}
}
- }]
-
-
- Then the sprites can use `gradientId` and `gradientId2` by setting the fill attributes to those ids, for example:
-
-
- sprite.setAttributes({
- fill: 'url(#gradientId)'
- }, true);
-
-
- */
+ }, me);
+ },
- initComponent: function() {
- this.callParent(arguments);
+ // @private wrap the mouse up event to delegate it to the series.
+ onMouseUp: function(e) {
+ var me = this,
+ position = me.getEventXY(e),
+ item;
- this.addEvents(
- 'mousedown',
- 'mouseup',
- 'mousemove',
- 'mouseenter',
- 'mouseleave',
- 'click'
- );
+ if (me.mask) {
+ 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 mousedown event.
+ me.series.each(function(series) {
+ 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);
+ }
+ }
+ }
+ }, me);
},
- /**
- * @private
- *
- * Create the Surface on initial render
- */
- onRender: function() {
+ // @private wrap the mouse move event so it can be delegated to the series.
+ onMouseMove: function(e) {
var me = this,
- viewBox = me.viewBox,
- autoSize = me.autoSize,
- bbox, items, width, height, x, y;
- me.callParent(arguments);
+ position = me.getEventXY(e),
+ item, last, storeItem, storeField;
- me.createSurface();
+ if (me.mask) {
+ 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.
+ me.series.each(function(series) {
+ 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;
- items = me.surface.items;
- if (viewBox || autoSize) {
- bbox = items.getBBox();
- width = bbox.width;
- height = bbox.height;
- x = bbox.x;
- y = bbox.y;
- if (me.viewBox) {
- me.surface.setViewBox(x, y, width, height);
- }
- else {
- // AutoSized
- me.autoSizeSurface();
+ 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;
+ }
}
- }
+ }, me);
},
- //@private
- autoSizeSurface: function() {
- var me = this,
- items = me.surface.items,
- bbox = items.getBBox(),
- width = bbox.width,
- height = bbox.height;
- items.setAttributes({
- translate: {
- x: -bbox.x,
- //Opera has a slight offset in the y axis.
- y: -bbox.y + (+Ext.isOpera)
- }
- }, true);
- if (me.rendered) {
- me.setSize(width, height);
- }
- else {
- me.surface.setSize(width, height);
+ // @private handle mouse leave event.
+ onMouseLeave: function(e) {
+ var me = this;
+ if (me.mask) {
+ me.mixins.mask.onMouseLeave.call(me, e);
}
- me.el.setSize(width, height);
+ me.series.each(function(series) {
+ delete series._lastItemForPoint;
+ });
},
- /**
- * 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);
-
- */
- createSurface: function() {
- var surface = Ext.draw.Surface.create(Ext.apply({}, {
- width: this.width,
- height: this.height,
- renderTo: this.el
- }, this.initialConfig));
- this.surface = surface;
-
- function refire(eventName) {
- return function(e) {
- this.fireEvent(eventName, e);
- };
+ // @private buffered refresh for when we update the store
+ delayRefresh: function() {
+ var me = this;
+ if (!me.refreshTask) {
+ me.refreshTask = Ext.create('Ext.util.DelayedTask', me.refresh, me);
}
-
- surface.on({
- scope: this,
- mouseup: refire('mouseup'),
- mousedown: refire('mousedown'),
- mousemove: refire('mousemove'),
- mouseenter: refire('mouseenter'),
- mouseleave: refire('mouseleave'),
- click: refire('click')
- });
+ me.refreshTask.delay(me.refreshBuffer);
},
+ // @private
+ refresh: function() {
+ var me = this;
+ if (me.rendered && me.curWidth !== undefined && me.curHeight !== undefined) {
+ if (me.fireEvent('beforerefresh', me) !== false) {
+ me.redraw();
+ me.fireEvent('refresh', me);
+ }
+ }
+ },
/**
- * @private
- *
- * Clean up the Surface instance on component destruction
+ * Changes the data store bound to this chart and refreshes it.
+ * @param {Ext.data.Store} store The store to bind to this chart
*/
- onDestroy: function() {
- var surface = this.surface;
- if (surface) {
- surface.destroy();
+ bindStore: function(store, initial) {
+ var me = this;
+ if (!initial && me.store) {
+ if (store !== me.store && me.store.autoDestroy) {
+ me.store.destroyStore();
+ }
+ else {
+ me.store.un('datachanged', me.refresh, me);
+ me.store.un('add', me.delayRefresh, me);
+ me.store.un('remove', me.delayRefresh, me);
+ me.store.un('update', me.delayRefresh, me);
+ me.store.un('clear', me.refresh, me);
+ }
}
- this.callParent(arguments);
- }
-
-});
-
-/**
- * @class Ext.chart.LegendItem
- * @extends Ext.draw.CompositeSprite
- * A single item of a legend (marker plus label)
- * @constructor
- */
-Ext.define('Ext.chart.LegendItem', {
-
- /* Begin Definitions */
-
- extend: 'Ext.draw.CompositeSprite',
-
- requires: ['Ext.chart.Shape'],
-
- /* End Definitions */
-
- // Position of the item, relative to the upper-left corner of the legend box
- x: 0,
- y: 0,
- zIndex: 500,
+ if (store) {
+ store = Ext.data.StoreManager.lookup(store);
+ store.on({
+ scope: me,
+ datachanged: me.refresh,
+ add: me.delayRefresh,
+ remove: me.delayRefresh,
+ update: me.delayRefresh,
+ clear: me.refresh
+ });
+ }
+ me.store = store;
+ if (store && !initial) {
+ me.refresh();
+ }
+ },
- constructor: function(config) {
- this.callParent(arguments);
- this.createLegend(config);
+ // @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,
+ 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);
+ }
+ 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 = me.axes.replace(Ext.createByAlias('axis.' + axis.type.toLowerCase(), config));
+ }
+ else {
+ Ext.apply(axis, config);
+ }
},
+
/**
- * Creates all the individual sprites for this legend item
+ * @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.
*/
- createLegend: function(config) {
+ alignAxes: function() {
var me = this,
- index = config.yFieldIndex,
- series = me.series,
- seriesType = series.type,
- idx = me.yFieldIndex,
+ axes = me.axes,
legend = me.legend,
- surface = me.surface,
- refX = legend.x + me.x,
- refY = legend.y + me.y,
- bbox, z = me.zIndex,
- markerConfig, label, mask,
- radius, toggle = false,
- seriesStyle = Ext.apply(series.seriesStyle, series.style);
+ edges = ['top', 'right', 'bottom', 'left'],
+ chartBBox,
+ insetPadding = me.insetPadding,
+ insets = {
+ top: insetPadding,
+ right: insetPadding,
+ bottom: insetPadding,
+ left: insetPadding
+ };
- function getSeriesProp(name) {
- var val = series[name];
- return (Ext.isArray(val) ? val[idx] : val);
+ function getAxis(edge) {
+ var i = axes.findIndex('position', edge);
+ return (i < 0) ? null : axes.getAt(i);
}
-
- label = me.add('label', surface.add({
- type: 'text',
- x: 20,
- y: 0,
- zIndex: z || 0,
- font: legend.labelFont,
- text: getSeriesProp('title') || getSeriesProp('yField')
- }));
- // Line series - display as short line with optional marker in the middle
- if (seriesType === 'line' || seriesType === 'scatter') {
- if(seriesType === 'line') {
- me.add('line', surface.add({
- type: 'path',
- path: 'M0.5,0.5L16.5,0.5',
- zIndex: z,
- "stroke-width": series.lineWidth,
- "stroke-linejoin": "round",
- "stroke-dasharray": series.dash,
- stroke: seriesStyle.stroke || '#000',
- style: {
- cursor: 'pointer'
- }
- }));
- }
- if (series.showMarkers || seriesType === 'scatter') {
- markerConfig = Ext.apply(series.markerStyle, series.markerConfig || {});
- me.add('marker', Ext.chart.Shape[markerConfig.type](surface, {
- fill: markerConfig.fill,
- x: 8.5,
- y: 0.5,
- zIndex: z,
- radius: markerConfig.radius || markerConfig.size,
- style: {
- cursor: 'pointer'
- }
- }));
- }
- }
- // All other series types - display as filled box
- else {
- me.add('box', surface.add({
- type: 'rect',
- zIndex: z,
- x: 0,
- y: 0,
- width: 12,
- height: 12,
- fill: series.getLegendColor(index),
- style: {
- cursor: 'pointer'
+ // Find the space needed by axes and legend as a positive inset from each edge
+ Ext.each(edges, function(edge) {
+ var isVertical = (edge === 'left' || edge === 'right'),
+ axis = getAxis(edge),
+ bbox;
+
+ // 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) + insets[edge];
}
- }));
- }
-
- me.setAttributes({
- hidden: false
- }, true);
-
- bbox = me.getBBox();
-
- mask = me.add('mask', surface.add({
- type: 'rect',
- x: bbox.x,
- y: bbox.y,
- width: bbox.width || 20,
- height: bbox.height || 20,
- zIndex: (z || 0) + 1000,
- fill: '#f00',
- opacity: 0,
- style: {
- 'cursor': 'pointer'
}
- }));
- //add toggle listener
- me.on('mouseover', function() {
- label.setStyle({
- 'font-weight': 'bold'
- });
- mask.setStyle({
- 'cursor': 'pointer'
- });
- series._index = index;
- series.highlightItem();
- }, me);
+ // 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);
+ }
+ });
+ // 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;
- me.on('mouseout', function() {
- label.setStyle({
- 'font-weight': 'normal'
- });
- series._index = index;
- series.unHighlightItem();
- }, me);
-
- if (!series.visibleInLegend(index)) {
- toggle = true;
- label.setAttributes({
- opacity: 0.5
- }, true);
- }
+ // Go back through each axis and set its length and position based on the
+ // corresponding edge of the chartBBox
+ axes.each(function(axis) {
+ var pos = axis.position,
+ isVertical = (pos === 'left' || pos === 'right');
- me.on('mousedown', function() {
- if (!toggle) {
- series.hideAll();
- label.setAttributes({
- opacity: 0.5
- }, true);
+ 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) {
+ var me = this,
+ themeAttrs = me.themeAttrs,
+ seriesObj, markerObj, seriesThemes, st,
+ markerThemes, colorArrayStyle = [],
+ i = 0, l,
+ 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[idx % seriesThemes.length]);
+ config.seriesLabelStyle = Ext.apply({}, themeAttrs.seriesLabel);
+ config.markerStyle = Ext.apply(markerObj, markerThemes[idx % markerThemes.length]);
+ if (themeAttrs.colors) {
+ config.colorArrayStyle = themeAttrs.colors;
} else {
- series.showAll();
- label.setAttributes({
- opacity: 1
- }, true);
+ 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;
+ }
}
- toggle = !toggle;
- }, me);
- me.updatePosition({x:0, y:0}); //Relative to 0,0 at first so that the bbox is calculated correctly
+ config.seriesIdx = idx;
+ }
+ if (series instanceof Ext.chart.series.Series) {
+ Ext.apply(series, config);
+ } else {
+ Ext.applyIf(config, series);
+ series = me.series.replace(Ext.createByAlias('series.' + series.type.toLowerCase(), config));
+ }
+ if (series.initialize) {
+ series.initialize();
+ }
},
- /**
- * 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) {
+ // @private
+ getMaxGutter: function() {
var me = this,
- items = me.items,
- ln = items.length,
- i = 0,
- item;
- if (!relativeTo) {
- relativeTo = me.legend;
- }
- for (; i < ln; i++) {
- item = items[i];
- switch (item.type) {
- case 'text':
- item.setAttributes({
- x: 20 + relativeTo.x + me.x,
- y: relativeTo.y + me.y
- }, true);
- break;
- case 'rect':
- item.setAttributes({
- translate: {
- x: relativeTo.x + me.x,
- y: relativeTo.y + me.y - 6
- }
- }, true);
- break;
- default:
- item.setAttributes({
- translate: {
- x: relativeTo.x + me.x,
- y: relativeTo.y + me.y
- }
- }, true);
- }
+ maxGutter = [0, 0];
+ me.series.each(function(s) {
+ var gutter = s.getGutters && s.getGutters() || [0, 0];
+ maxGutter[0] = Math.max(maxGutter[0], gutter[0]);
+ maxGutter[1] = Math.max(maxGutter[1], gutter[1]);
+ });
+ me.maxGutter = maxGutter;
+ },
+
+ // @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');
}
+ },
+
+ // @private remove gently.
+ destroy: function() {
+ Ext.destroy(this.surface);
+ this.bindStore(null);
+ this.callParent(arguments);
}
});
+
/**
- * @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 `showInSeries` 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'
- * },
- *
- * Full 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',
- grid: true,
- 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
- }
- }]
- });
-
- *
- * @constructor
+ * @class Ext.chart.Highlight
+ * A mixin providing highlight functionality for Ext.chart.series.Series.
*/
-Ext.define('Ext.chart.Legend', {
+Ext.define('Ext.chart.Highlight', {
/* Begin Definitions */
- requires: ['Ext.chart.LegendItem'],
+ requires: ['Ext.fx.Anim'],
/* End Definitions */
/**
- * @cfg {Boolean} visible
- * Whether or not the legend should be displayed.
- */
- visible: 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} 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 {String} 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
+ * Highlight the given series item.
+ * @param {Boolean/Object} Default's false. Can also be an object width style properties (i.e fill, stroke, radius)
+ * or just use default styles per series by setting highlight = true.
*/
- padding: 5,
-
- // @private
- width: 0,
- // @private
- height: 0,
+ highlight: false,
- /**
- * @cfg {Number} boxZIndex
- * Sets the z-index for the legend. Defaults to 100.
- */
- boxZIndex: 100,
+ highlightCfg : null,
constructor: function(config) {
- var me = this;
- if (config) {
- Ext.apply(me, config);
+ if (config.highlight) {
+ if (config.highlight !== true) { //is an object
+ this.highlightCfg = Ext.apply({}, config.highlight);
+ }
+ else {
+ this.highlightCfg = {
+ fill: '#fdd',
+ radius: 20,
+ lineWidth: 5,
+ stroke: '#f55'
+ };
+ }
}
- 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
+ * Highlight the given series item.
+ * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
*/
- create: function() {
- var me = this;
- me.createItems();
- if (!me.created && me.isDisplayed()) {
- me.createBox();
- me.created = true;
+ highlightItem: function(item) {
+ if (!item) {
+ return;
+ }
+
+ var me = this,
+ sprite = item.sprite,
+ opts = me.highlightCfg,
+ surface = me.chart.surface,
+ animate = me.chart.animate,
+ p, from, to, pi;
- // Listen for changes to series titles to trigger regeneration of the legend
- me.chart.series.each(function(series) {
- series.on('titlechange', function() {
- me.create();
- me.updatePosition();
- });
+ 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 = {};
+ 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 = Ext.create('Ext.fx.Anim', {
+ target: sprite,
+ from: sprite._from,
+ to: sprite._to,
+ duration: 150
});
+ } else {
+ sprite.setAttributes(sprite._to, true);
}
},
/**
- * @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.
+ * Un-highlight any existing highlights
*/
- isDisplayed: function() {
- return this.visible && this.chart.series.findIndex('showInLegend', true) !== -1;
- },
+ unHighlightItem: function() {
+ if (!this.highlight || !this.items) {
+ return;
+ }
- /**
- * @private Create the series markers and labels
- */
- createItems: function() {
var me = this,
- chart = me.chart,
- surface = chart.surface,
items = me.items,
- padding = me.padding,
- itemSpacing = me.itemSpacing,
- spacingOffset = 2,
- maxWidth = 0,
- maxHeight = 0,
- totalWidth = 0,
- totalHeight = 0,
- vertical = me.isVertical,
- math = Math,
- mfloor = math.floor,
- mmax = math.max,
- index = 0,
- i = 0,
- len = items ? items.length : 0,
- x, y, spacing, item, bbox, height, width;
+ len = items.length,
+ opts = me.highlightCfg,
+ animate = me.chart.animate,
+ i = 0,
+ obj, p, sprite;
- //remove all legend items
- if (len) {
- for (; i < len; i++) {
- items[i].destroy();
+ for (; i < len; i++) {
+ if (!items[i]) {
+ continue;
}
- }
- //empty array
- items.length = [];
- // Create all the item labels, collecting their dimensions and positioning each one
- // properly in relation to the previous item
- chart.series.each(function(series, i) {
- if (series.showInLegend) {
- Ext.each([].concat(series.yField), function(field, j) {
- item = Ext.create('Ext.chart.LegendItem', {
- legend: this,
- series: series,
- surface: chart.surface,
- yFieldIndex: j
- });
- 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;
-
- if (i + j === 0) {
- spacing = vertical ? padding + height / 2 : padding;
+ 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(obj[p], sprite._defaults[p]);
}
else {
- spacing = itemSpacing / (vertical ? 2 : 1);
+ obj[p] = sprite._defaults[p];
}
- // Set the item's position relative to the legend box
- item.x = mfloor(vertical ? padding : totalWidth + spacing);
- item.y = mfloor(vertical ? totalHeight + spacing : padding + height / 2);
-
- // Collect cumulative dimensions
- totalWidth += width + spacing;
- totalHeight += height + spacing;
- maxWidth = mmax(maxWidth, width);
- maxHeight = mmax(maxHeight, height);
-
- items.push(item);
- }, this);
+ }
+ if (animate) {
+ //sprite._to = obj;
+ sprite._endStyle = obj;
+ sprite._anim = Ext.create('Ext.fx.Anim', {
+ target: sprite,
+ to: obj,
+ duration: 150
+ });
+ }
+ else {
+ sprite.setAttributes(obj, true);
+ }
+ delete sprite._highlighted;
+ //delete sprite._defaults;
}
- }, me);
-
- // Store the collected dimensions for later
- me.width = mfloor((vertical ? maxWidth : totalWidth) + padding * 2);
- if (vertical && items.length === 1) {
- spacingOffset = 1;
}
- me.height = mfloor((vertical ? totalHeight - spacingOffset * spacing : maxHeight) + (padding * 2));
- me.itemHeight = maxHeight;
},
- /**
- * @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,
- height: me.height
- };
- },
-
- /**
- * @private Create the box around the legend items
- */
- createBox: function() {
- var me = this,
- box = me.boxSprite = me.chart.surface.add(Ext.apply({
- type: 'rect',
- stroke: me.boxStroke,
- "stroke-width": me.boxStrokeWidth,
- fill: me.boxFill,
- zIndex: me.boxZIndex
- }, me.getBBox()));
- box.redraw();
- },
+ cleanHighlights: function() {
+ if (!this.highlight) {
+ return;
+ }
- /**
- * @private Update the position of all the legend's sprites to match its current x/y values
- */
- updatePosition: function() {
- var me = this,
- x, y,
- legendWidth = me.width,
- legendHeight = me.height,
- padding = me.padding,
- 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;
-
- if (me.isDisplayed()) {
- // 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;
+ 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;
}
- me.x = x;
- me.y = y;
-
- // Update the position of each item
- Ext.each(me.items, function(item) {
- item.updatePosition();
- });
- // Update the position of the outer box
- me.boxSprite.setAttributes(me.getBBox(), true);
}
}
});
/**
- * @class Ext.chart.Chart
- * @extends Ext.draw.Component
- *
- * The Ext.chart package provides the capability to visualize data.
- * Each chart binds directly to an Ext.data.Store enabling automatic updates of the chart.
- * A chart configuration object has some overall styling options as well as an array of axes
- * and series. A chart instance example could look like:
- *
-
- Ext.create('Ext.chart.Chart', {
- renderTo: Ext.getBody(),
- width: 800,
- height: 600,
- animate: true,
- store: store1,
- shadow: true,
- theme: 'Category1',
- legend: {
- position: 'right'
- },
- axes: [ ...some axes options... ],
- series: [ ...some series options... ]
- });
-
+ * @class Ext.chart.Label
+ *
+ * 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 methods implemented by the Series are:
*
- * In this example we set the `width` and `height` of the chart, we decide whether our series are
- * animated or not and we select a store to be bound to the chart. We also turn on shadows for all series,
- * select a color theme `Category1` for coloring the series, set the legend to the right part of the chart and
- * then tell the chart to render itself in the body element of the document. For more information about the axes and
- * series configurations please check the documentation of each series (Line, Bar, Pie, etc).
+ * - **`onCreateLabel(storeItem, item, i, display)`** Called each time a new label is created.
+ * The arguments of the method are:
+ * - *`storeItem`* The element of the store that is related to the label sprite.
+ * - *`item`* The item related to the label 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).
+ * - *`i`* The index of the element created (i.e the first created label, second created label, etc)
+ * - *`display`* The display type. May be false if the label is hidden
*
- * @xtype chart
+ * - **`onPlaceLabel(label, storeItem, item, i, display, animate)`** Called for updating the position of the label.
+ * The arguments of the method are:
+ * - *`label`* The sprite label.
+ * - *`storeItem`* The element of the store that is related to the label sprite
+ * - *`item`* The item related to the label 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).
+ * - *`i`* The index of the element to be updated (i.e. whether it is the first, second, third from the labelGroup)
+ * - *`display`* The display type. May be false if the label is hidden.
+ * - *`animate`* A boolean value to set or unset animations for the labels.
*/
-
-Ext.define('Ext.chart.Chart', {
+Ext.define('Ext.chart.Label', {
/* Begin Definitions */
- alias: 'widget.chart',
-
- extend: 'Ext.draw.Component',
-
- mixins: {
- themeManager: 'Ext.chart.theme.Theme',
- mask: 'Ext.chart.Mask',
- navigation: 'Ext.chart.Navigation'
- },
-
- requires: [
- 'Ext.util.MixedCollection',
- 'Ext.data.StoreManager',
- 'Ext.chart.Legend',
- 'Ext.util.DelayedTask'
- ],
+ requires: ['Ext.draw.Color'],
/* End Definitions */
- // @private
- viewBox: false,
-
- /**
- * @cfg {String} theme (optional) 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 (optional) 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 (optional) true for the default legend display or a legend config object. Defaults to false.
+ * @cfg {Object} label
+ * Object with the following properties:
+ *
+ * - **display** : String
+ *
+ * Specifies the presence and position of labels for each pie slice. Either "rotate", "middle", "insideStart",
+ * "insideEnd", "outside", "over", "under", or "none" to prevent label rendering.
+ * Default value: 'none'.
+ *
+ * - **color** : String
+ *
+ * The color of the label text.
+ * Default value: '#000' (black).
+ *
+ * - **contrast** : Boolean
+ *
+ * True to render the label in contrasting color with the backround.
+ * Default value: false.
+ *
+ * - **field** : String
+ *
+ * The name of the field to be displayed in the label.
+ * Default value: 'name'.
+ *
+ * - **minMargin** : Number
+ *
+ * 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.
+ *
+ * - **font** : String
+ *
+ * The font used for the labels.
+ * Default value: "11px Helvetica, sans-serif".
+ *
+ * - **orientation** : String
+ *
+ * Either "horizontal" or "vertical".
+ * Dafault value: "horizontal".
+ *
+ * - **renderer** : Function
+ *
+ * Optional function for formatting the label into a displayable value.
+ * Default value: function(v) { return v; }
*/
- legend: false,
- /**
- * @cfg {integer} insetPadding (optional) Set the amount of inset padding in pixels for the chart. Defaults to 10.
- */
- insetPadding: 10,
+ //@private a regex to parse url type colors.
+ colorStringRe: /url\s*\(\s*#([^\/)]+)\s*\)/,
- /**
- * @cfg {Array} 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'],
+ //@private the mixin constructor. Used internally by Series.
+ constructor: function(config) {
+ var me = this;
+ me.label = Ext.applyIf(me.label || {},
+ {
+ display: "none",
+ color: "#000",
+ field: "name",
+ minMargin: 50,
+ font: "11px Helvetica, sans-serif",
+ orientation: "horizontal",
+ renderer: function(v) {
+ return v;
+ }
+ });
- /**
- * @cfg {Object|Boolean} background (optional) Set 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'
+ if (me.label.display !== 'none') {
+ me.labelsGroup = me.chart.surface.getGroup(me.seriesId + '-labels');
}
-
+ },
- You can specify an image by using:
+ //@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,
+ color = config.color,
+ field = [].concat(config.field),
+ group = me.labelsGroup,
+ groupLength = (group || 0) && group.length,
+ store = me.chart.store,
+ 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;
-
- background: {
- image: 'http://path.to.image/'
+ if (display == 'none') {
+ 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] && ratio > 1) {
+ if(field[j]){
+ hides.push(groupIndex);
+ }
+ index++;
- Also you can specify a gradient by using the gradient object syntax:
+ }
-
- background: {
- gradient: {
- id: 'gradientId',
- angle: 45,
- stops: {
- 0: {
- color: '#555'
+ if (!item && label) {
+ label.hide(true);
+ groupIndex++;
}
- 100: {
- color: '#ddd'
+
+ if (item && field[j]) {
+ if (!label) {
+ label = me.onCreateLabel(storeItem, item, i, display, j, index);
+ }
+ me.onPlaceLabel(label, storeItem, item, i, display, animate, j, index);
+ groupIndex++;
+
+ //set contrast
+ if (config.contrast && item.sprite) {
+ sprite = item.sprite;
+ //set the color string to the color to be set.
+ if (sprite._endStyle) {
+ colorString = sprite._endStyle.fill;
+ }
+ else if (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.color || label.attr.fill).getHSL();
+ labelColor[2] = spriteBrightness > 0.5 ? 0.2 : 0.8;
+ label.setAttributes({
+ fill: String(Color.fromHSL.apply({}, labelColor))
+ }, true);
+ }
+
}
+ count++;
+ index++;
}
}
}
-
- */
- background: false,
+ me.hideLabels(hides);
+ },
+ hideLabels: function(hides){
+ var labelsGroup = this.labelsGroup,
+ hlen = hides.length;
+ while(hlen--)
+ labelsGroup.getAt(hides[hlen]).hide(true);
+ }
+});
+Ext.define('Ext.chart.MaskLayer', {
+ extend: 'Ext.Component',
+
+ constructor: function(config) {
+ config = Ext.apply(config || {}, {
+ style: 'position:absolute;background-color:#888;cursor:move;opacity:0.6;border:1px solid #222;'
+ });
+ this.callParent([config]);
+ },
+
+ initComponent: function() {
+ var me = this;
+ me.callParent(arguments);
+ me.addEvents(
+ 'mousedown',
+ 'mouseup',
+ 'mousemove',
+ 'mouseenter',
+ 'mouseleave'
+ );
+ },
- /**
- * @cfg {Array} 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
- *
- *
+ 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();
+ }
+ };
+ }
+});
+/**
+ * @class Ext.chart.TipSurface
+ * @ignore
+ */
+Ext.define('Ext.chart.TipSurface', {
+
+ /* Begin Definitions */
- For example:
+ extend: 'Ext.draw.Component',
-
- gradients: [{
- id: 'gradientId',
- angle: 45,
- stops: {
- 0: {
- color: '#555'
- },
- 100: {
- color: '#ddd'
- }
- }
- }, {
- id: 'gradientId2',
- angle: 0,
- stops: {
- 0: {
- color: '#590'
- },
- 20: {
- color: '#599'
+ /* 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
},
- 100: {
- color: '#ddd'
- }
+ true);
}
- }]
-
+ }
+ }
+});
- Then the sprites can use `gradientId` and `gradientId2` by setting the fill attributes to those ids, for example:
+/**
+ * @class Ext.chart.Tip
+ * Provides tips for Ext.chart.series.Series.
+ */
+Ext.define('Ext.chart.Tip', {
-
- sprite.setAttributes({
- fill: 'url(#gradientId)'
- }, true);
-
+ /* Begin Definitions */
- */
+ requires: ['Ext.tip.ToolTip', 'Ext.chart.TipSurface'],
+ /* End Definitions */
constructor: function(config) {
var me = this,
- defaultAnim;
- 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;
+ surface,
+ sprites,
+ tipSurface;
+ if (config.tips) {
+ me.tipTimeout = null;
+ me.tipConfig = Ext.apply({}, config.tips, {
+ renderer: Ext.emptyFn,
+ constrainPosition: false
+ });
+ me.tooltip = Ext.create('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 = Ext.create('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;
}
}
- me.mixins.mask.constructor.call(me, config);
- me.mixins.navigation.constructor.call(me, config);
- me.callParent([config]);
},
- initComponent: function() {
- var me = this,
- axes,
- series;
- me.callParent();
- me.addEvents(
- 'itemmousedown',
- 'itemmouseup',
- 'itemmouseover',
- 'itemmouseout',
- 'itemclick',
- 'itemdoubleclick',
- 'itemdragstart',
- 'itemdrag',
- 'itemdragend',
- /**
- * @event beforerefresh
- * Fires before a refresh to the chart data is called. If the beforerefresh handler returns
- * false the {@link #refresh} action will be cancelled.
- * @param {Chart} this
- */
- 'beforerefresh',
- /**
- * @event refresh
- * Fires after the chart data has been refreshed.
- * @param {Chart} this
- */
- 'refresh'
- );
- Ext.applyIf(me, {
- zoom: {
- width: 1,
- height: 1,
- x: 0,
- y: 0
- }
- });
- me.maxGutter = [0, 0];
- me.store = Ext.data.StoreManager.lookup(me.store);
- axes = me.axes;
- me.axes = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.position; });
- if (axes) {
- me.axes.addAll(axes);
+ showTip: function(item) {
+ var me = this;
+ if (!me.tooltip) {
+ return;
}
- series = me.series;
- me.series = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.seriesId || (a.seriesId = Ext.id(null, 'ext-chart-series-')); });
- if (series) {
- me.series.addAll(series);
+ clearTimeout(me.tipTimeout);
+ var tooltip = me.tooltip,
+ spriteTip = me.spriteTip,
+ tipConfig = me.tipConfig,
+ trackMouse = tooltip.trackMouse,
+ sprite, surface, surfaceExt, pos, x, y;
+ 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 (me.legend !== false) {
- me.legend = Ext.create('Ext.chart.Legend', Ext.applyIf({chart:me}, me.legend));
+ if (spriteTip) {
+ tipConfig.renderer.call(tooltip, item.storeItem, item, spriteTip.surface);
+ } else {
+ tipConfig.renderer.call(tooltip, item.storeItem, item);
}
-
- me.on({
- mousemove: me.onMouseMove,
- mouseleave: me.onMouseLeave,
- mousedown: me.onMouseDown,
- mouseup: me.onMouseUp,
- scope: me
- });
+ tooltip.show();
+ tooltip.trackMouse = trackMouse;
},
- // @private overrides the component method to set the correct dimensions to the chart.
- afterComponentLayout: function(width, height) {
- var me = this;
- if (Ext.isNumber(width) && Ext.isNumber(height)) {
- me.curWidth = width;
- me.curHeight = height;
- me.redraw(true);
+ hideTip: function(item) {
+ var tooltip = this.tooltip;
+ if (!tooltip) {
+ return;
}
- this.callParent(arguments);
- },
+ clearTimeout(this.tipTimeout);
+ this.tipTimeout = setTimeout(function() {
+ tooltip.hide();
+ }, 0);
+ }
+});
+/**
+ * @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 */
/**
- * Redraw the chart. If animations are set this will animate the chart too.
- * @cfg {boolean} resize Optional flag which changes the default origin points of the chart for animations.
+ * Creates new Axis.
+ * @param {Object} config (optional) Config options.
*/
- redraw: function(resize) {
+ constructor: function(config) {
+ config = config || {};
+
var me = this,
- chartBBox = me.chartBBox = {
- x: 0,
- y: 0,
- height: me.curHeight,
- width: me.curWidth
- },
- legend = me.legend;
- me.surface.setSize(chartBBox.width, chartBBox.height);
- // Instantiate Series and Axes
- me.series.each(me.initializeSeries, me);
- me.axes.each(me.initializeAxis, me);
- //process all views (aggregated data etc) on stores
- //before rendering.
- me.axes.each(function(axis) {
- axis.processView();
- });
- me.axes.each(function(axis) {
- axis.drawAxis(true);
- });
+ pos = config.position || 'left';
- // Create legend if not already created
- if (legend !== false) {
- legend.create();
- }
+ 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 = [].concat(me.fields);
+ this.callParent();
+ me.labels = [];
+ me.getId();
+ me.labelGroup = me.chart.surface.getGroup(me.axisId + "-labels");
+ },
- // Place axes properly, including influence from each other
- me.alignAxes();
+ alignment: null,
+ grid: false,
+ steps: 10,
+ x: 0,
+ y: 0,
+ minValue: 0,
+ maxValue: 0,
- // Reposition legend based on new axis alignment
- if (me.legend !== false) {
- legend.updatePosition();
- }
+ getId: function() {
+ return this.axisId || (this.axisId = Ext.id(null, 'ext-axis-'));
+ },
- // Find the max gutter
- me.getMaxGutter();
+ /*
+ 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,
- // Draw axes and series
- me.resizing = !!resize;
+ drawAxis: Ext.emptyFn,
+ addDisplayAndLabels: Ext.emptyFn
+});
+
+/**
+ * @class Ext.chart.axis.Axis
+ * @extends Ext.chart.axis.Abstract
+ *
+ * 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',
+ * grid: true,
+ * position: 'left',
+ * 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',
+ * grid: true,
+ * 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.
+ */
+
+ /**
+ * @cfg {Number} minorTickSteps
+ * The number of small ticks between two major ticks. Default is zero.
+ */
- me.axes.each(me.drawAxis, me);
- me.series.each(me.drawCharts, me);
- me.resizing = false;
- },
+ /**
+ * @cfg {String} title
+ * The title for the Axis
+ */
- // @private set the store after rendering the chart.
- afterRender: function() {
- var ref,
- me = this;
- this.callParent();
+ //@private force min/max values from store
+ forceMinMax: false,
- if (me.categoryNames) {
- me.setCategoryNames(me.categoryNames);
- }
+ /**
+ * @cfg {Number} dashSize
+ * The size of the dash marker. Default's 3.
+ */
+ dashSize: 3,
- if (me.tipRenderer) {
- ref = me.getFunctionRef(me.tipRenderer);
- me.setTipRenderer(ref.fn, ref.scope);
- }
- me.bindStore(me.store, true);
- me.refresh();
- },
+ /**
+ * @cfg {String} position
+ * Where to set the axis. Available options are `left`, `bottom`, `right`, `top`. Default's `bottom`.
+ */
+ position: 'bottom',
- // @private get x and y position of the mouse cursor.
- getEventXY: function(e) {
- var me = this,
- box = this.surface.getRegion(),
- pageXY = e.getXY(),
- x = pageXY[0] - box.left,
- y = pageXY[1] - box.top;
- return [x, y];
- },
+ // @private
+ skipFirst: false,
- // @private wrap the mouse down position to delegate the event to the series.
- onClick: function(e) {
- var me = this,
- position = me.getEventXY(e),
- item;
+ /**
+ * @cfg {Number} length
+ * Offset axis position. Default's 0.
+ */
+ length: 0,
- // Ask each series if it has an item corresponding to (not necessarily exactly
- // on top of) the current mouse coords. Fire itemclick event.
- me.series.each(function(series) {
- 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('itemclick', item);
- }
- }
- }
- }, me);
- },
+ /**
+ * @cfg {Number} width
+ * Offset axis width. Default's 0.
+ */
+ width: 0,
- // @private wrap the mouse down position to delegate the event to the series.
- onMouseDown: function(e) {
- var me = this,
- position = me.getEventXY(e),
- item;
+ majorTickSteps: false,
- if (me.mask) {
- 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 mousedown event.
- me.series.each(function(series) {
- 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);
- }
- }
- }
- }, me);
- },
+ // @private
+ applyData: Ext.emptyFn,
- // @private wrap the mouse up event to delegate it to the series.
- onMouseUp: function(e) {
+ getRange: function () {
var me = this,
- position = me.getEventXY(e),
- item;
+ store = me.chart.getChartStore(),
+ fields = me.fields,
+ ln = fields.length,
+ math = Math,
+ mmax = math.max,
+ mmin = math.min,
+ aggregate = false,
+ min = isNaN(me.minimum) ? Infinity : me.minimum,
+ max = isNaN(me.maximum) ? -Infinity : me.maximum,
+ total = 0, i, l, value, values, rec,
+ excludes = [],
+ series = me.chart.series.items;
- if (me.mask) {
- me.mixins.mask.onMouseUp.call(me, e);
+ //if one series is stacked I have to aggregate the values
+ //for the scale.
+ // TODO(zhangbei): the code below does not support series that stack on 1 side but non-stacked axis
+ // listed in axis config. For example, a Area series whose axis : ['left', 'bottom'].
+ // Assuming only stack on y-axis.
+ // CHANGED BY Nicolas: I removed the check `me.position == 'left'` and `me.position == 'right'` since
+ // it was constraining the minmax calculation to y-axis stacked
+ // visualizations.
+ for (i = 0, l = series.length; !aggregate && i < l; i++) {
+ aggregate = aggregate || series[i].stacked;
+ excludes = series[i].__excludes || excludes;
}
- // Ask each series if it has an item corresponding to (not necessarily exactly
- // on top of) the current mouse coords. Fire mousedown event.
- me.series.each(function(series) {
- 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);
+ store.each(function(record) {
+ if (aggregate) {
+ if (!isFinite(min)) {
+ min = 0;
+ }
+ for (values = [0, 0], i = 0; i < ln; i++) {
+ if (excludes[i]) {
+ continue;
}
+ rec = record.get(fields[i]);
+ values[+(rec > 0)] += math.abs(rec);
}
+ max = mmax(max, -values[0], +values[1]);
+ min = mmin(min, -values[0], +values[1]);
}
- }, me);
- },
-
- // @private wrap the mouse move event so it can be delegated to the series.
- onMouseMove: function(e) {
- var me = this,
- position = me.getEventXY(e),
- item, last, storeItem, storeField;
-
- if (me.mask) {
- 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.
- me.series.each(function(series) {
- 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 {
+ for (i = 0; i < ln; i++) {
+ if (excludes[i]) {
+ continue;
}
- }
- } else {
- last = series._lastItemForPoint;
- if (last) {
- series.fireEvent('itemmouseout', last);
- delete series._lastItemForPoint;
- delete series._lastStoreField;
- delete series._lastStoreItem;
+ value = record.get(fields[i]);
+ max = mmax(max, +value);
+ min = mmin(min, +value);
}
}
- }, me);
- },
-
- // @private handle mouse leave event.
- onMouseLeave: function(e) {
- var me = this;
- if (me.mask) {
- me.mixins.mask.onMouseLeave.call(me, e);
- }
- me.series.each(function(series) {
- delete series._lastItemForPoint;
});
- },
-
- // @private buffered refresh for when we update the store
- delayRefresh: function() {
- var me = this;
- if (!me.refreshTask) {
- me.refreshTask = Ext.create('Ext.util.DelayedTask', me.refresh, me);
+ if (!isFinite(max)) {
+ max = me.prevMax || 0;
+ }
+ if (!isFinite(min)) {
+ min = me.prevMin || 0;
+ }
+ //normalize min max for snapEnds.
+ if (min != max && (max != Math.floor(max))) {
+ max = Math.floor(max) + 1;
}
- me.refreshTask.delay(me.refreshBuffer);
- },
- // @private
- refresh: function() {
- var me = this;
- if (me.rendered && me.curWidth != undefined && me.curHeight != undefined) {
- if (me.fireEvent('beforerefresh', me) !== false) {
- me.redraw();
- me.fireEvent('refresh', me);
- }
+ if (!isNaN(me.minimum)) {
+ min = me.minimum;
+ }
+
+ if (!isNaN(me.maximum)) {
+ max = me.maximum;
}
+
+ return {min: min, max: max};
},
- /**
- * Changes the data store bound to this chart and refreshes it.
- * @param {Store} store The store to bind to this chart
- */
- bindStore: function(store, initial) {
- var me = this;
- if (!initial && me.store) {
- if (store !== me.store && me.store.autoDestroy) {
- me.store.destroy();
+ // @private creates a structure with start, end and step points.
+ calcEnds: function() {
+ var me = this,
+ fields = me.fields,
+ range = me.getRange(),
+ min = range.min,
+ max = range.max,
+ outfrom, outto, out;
+
+ out = Ext.draw.Draw.snapEnds(min, max, me.majorTickSteps !== false ? (me.majorTickSteps +1) : me.steps);
+ outfrom = out.from;
+ outto = out.to;
+ if (me.forceMinMax) {
+ if (!isNaN(max)) {
+ out.to = max;
}
- else {
- me.store.un('datachanged', me.refresh, me);
- me.store.un('add', me.delayRefresh, me);
- me.store.un('remove', me.delayRefresh, me);
- me.store.un('update', me.delayRefresh, me);
- me.store.un('clear', me.refresh, me);
+ if (!isNaN(min)) {
+ out.from = min;
}
}
- if (store) {
- store = Ext.data.StoreManager.lookup(store);
- store.on({
- scope: me,
- datachanged: me.refresh,
- add: me.delayRefresh,
- remove: me.delayRefresh,
- update: me.delayRefresh,
- clear: me.refresh
- });
+ if (!isNaN(me.maximum)) {
+ //TODO(nico) users are responsible for their own minimum/maximum values set.
+ //Clipping should be added to remove lines in the chart which are below the axis.
+ out.to = me.maximum;
}
- me.store = store;
- if (store && !initial) {
- me.refresh();
+ if (!isNaN(me.minimum)) {
+ //TODO(nico) users are responsible for their own minimum/maximum values set.
+ //Clipping should be added to remove lines in the chart which are below the axis.
+ out.from = me.minimum;
}
- },
- // @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,
- 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);
- }
- 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 = me.axes.replace(Ext.createByAlias('axis.' + axis.type.toLowerCase(), config));
+ //Adjust after adjusting minimum and maximum
+ out.step = (out.to - out.from) / (outto - outfrom) * out.step;
+
+ if (me.adjustMaximumByMajorUnit) {
+ out.to += out.step;
}
- else {
- Ext.apply(axis, config);
+ if (me.adjustMinimumByMajorUnit) {
+ out.from -= out.step;
}
+ me.prevMin = min == max? 0 : min;
+ me.prevMax = max;
+ return out;
},
-
/**
- * @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.
+ * Renders the axis into the screen and updates its position.
*/
- alignAxes: function() {
+ drawAxis: function (init) {
var me = this,
- axes = me.axes,
- legend = me.legend,
- edges = ['top', 'right', 'bottom', 'left'],
- chartBBox,
- insetPadding = me.insetPadding,
- insets = {
- top: insetPadding,
- right: insetPadding,
- bottom: insetPadding,
- left: insetPadding
- };
+ i, j,
+ x = me.x,
+ y = me.y,
+ gutterX = me.chart.maxGutter[0],
+ gutterY = me.chart.maxGutter[1],
+ dashSize = me.dashSize,
+ subDashesX = me.minorTickSteps || 0,
+ subDashesY = me.minorTickSteps || 0,
+ length = me.length,
+ position = me.position,
+ inflections = [],
+ calcLabels = false,
+ stepCalcs = me.applyData(),
+ step = stepCalcs.step,
+ steps = stepCalcs.steps,
+ from = stepCalcs.from,
+ to = stepCalcs.to,
+ trueLength,
+ currentX,
+ currentY,
+ path,
+ prev,
+ dashesX,
+ dashesY,
+ delta;
- function getAxis(edge) {
- var i = axes.findIndex('position', edge);
- return (i < 0) ? null : axes.getAt(i);
+ //If no steps are specified
+ //then don't draw the axis. This generally happens
+ //when an empty store.
+ if (me.hidden || isNaN(step) || (from == to)) {
+ return;
}
- // Find the space needed by axes and legend as a positive inset from each edge
- Ext.each(edges, function(edge) {
- var isVertical = (edge === 'left' || edge === 'right'),
- axis = getAxis(edge),
- bbox;
+ me.from = stepCalcs.from;
+ me.to = stepCalcs.to;
+ if (position == 'left' || position == 'right') {
+ currentX = Math.floor(x) + 0.5;
+ path = ["M", currentX, y, "l", 0, -length];
+ trueLength = length - (gutterY * 2);
+ }
+ else {
+ currentY = Math.floor(y) + 0.5;
+ path = ["M", x, currentY, "l", length, 0];
+ trueLength = length - (gutterX * 2);
+ }
- // 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) + insets[edge];
+ delta = trueLength / (steps || 1);
+ dashesX = Math.max(subDashesX +1, 0);
+ dashesY = Math.max(subDashesY +1, 0);
+ if (me.type == 'Numeric' || me.type == 'Time') {
+ calcLabels = true;
+ me.labels = [stepCalcs.from];
+ }
+ if (position == 'right' || position == 'left') {
+ currentY = y - gutterY;
+ currentX = x - ((position == 'left') * dashSize * 2);
+ while (currentY >= y - gutterY - trueLength) {
+ path.push("M", currentX, Math.floor(currentY) + 0.5, "l", dashSize * 2 + 1, 0);
+ if (currentY != y - gutterY) {
+ for (i = 1; i < dashesY; i++) {
+ path.push("M", currentX + dashSize, Math.floor(currentY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0);
+ }
+ }
+ inflections.push([ Math.floor(x), Math.floor(currentY) ]);
+ currentY -= delta;
+ if (calcLabels) {
+ me.labels.push(me.labels[me.labels.length -1] + step);
+ }
+ if (delta === 0) {
+ break;
}
}
-
- // 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);
+ if (Math.round(currentY + delta - (y - gutterY - trueLength))) {
+ path.push("M", currentX, Math.floor(y - length + gutterY) + 0.5, "l", dashSize * 2 + 1, 0);
+ for (i = 1; i < dashesY; i++) {
+ path.push("M", currentX + dashSize, Math.floor(y - length + gutterY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0);
+ }
+ inflections.push([ Math.floor(x), Math.floor(currentY) ]);
+ if (calcLabels) {
+ me.labels.push(me.labels[me.labels.length -1] + step);
+ }
}
- });
- // 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
- axes.each(function(axis) {
- var 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);
- });
+ } else {
+ currentX = x + gutterX;
+ currentY = y - ((position == 'top') * dashSize * 2);
+ while (currentX <= x + gutterX + trueLength) {
+ path.push("M", Math.floor(currentX) + 0.5, currentY, "l", 0, dashSize * 2 + 1);
+ if (currentX != x + gutterX) {
+ for (i = 1; i < dashesX; i++) {
+ path.push("M", Math.floor(currentX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1);
+ }
+ }
+ inflections.push([ Math.floor(currentX), Math.floor(y) ]);
+ currentX += delta;
+ if (calcLabels) {
+ me.labels.push(me.labels[me.labels.length -1] + step);
+ }
+ if (delta === 0) {
+ break;
+ }
+ }
+ if (Math.round(currentX - delta - (x + gutterX + trueLength))) {
+ path.push("M", Math.floor(x + length - gutterX) + 0.5, currentY, "l", 0, dashSize * 2 + 1);
+ for (i = 1; i < dashesX; i++) {
+ path.push("M", Math.floor(x + length - gutterX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1);
+ }
+ inflections.push([ Math.floor(currentX), Math.floor(y) ]);
+ if (calcLabels) {
+ me.labels.push(me.labels[me.labels.length -1] + step);
+ }
+ }
+ }
+ 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();
},
- // @private initialize the series.
- initializeSeries: function(series, idx) {
+ /**
+ * Renders an horizontal and/or vertical grid into the Surface.
+ */
+ drawGrid: function() {
var me = this,
- themeAttrs = me.themeAttrs,
- seriesObj, markerObj, seriesThemes, st,
- markerThemes, colorArrayStyle = [],
- i = 0, l,
- 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[idx % seriesThemes.length]);
- config.seriesLabelStyle = Ext.apply({}, themeAttrs.seriesLabel);
- config.markerStyle = Ext.apply(markerObj, markerThemes[idx % markerThemes.length]);
- if (themeAttrs.colors) {
- config.colorArrayStyle = themeAttrs.colors;
+ 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,
+ gutter = me.chart.maxGutter,
+ width = me.width - 2,
+ vert = false,
+ point, prevPoint,
+ i = 1,
+ path = [], styles, lineWidth, dlineWidth,
+ oddPath = [], evenPath = [];
+
+ if ((gutter[1] !== 0 && (position == 'left' || position == 'right')) ||
+ (gutter[0] !== 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 {
- colorArrayStyle = [];
- for (l = seriesThemes.length; i < l; i++) {
- st = seriesThemes[i];
- if (st.fill || st.stroke) {
- colorArrayStyle.push(st.fill || st.stroke);
- }
+ if (position == 'left') {
+ path = path.concat(["M", point[0] + 0.5, point[1] + 0.5, "l", width, 0]);
}
- if (colorArrayStyle.length) {
- config.colorArrayStyle = colorArrayStyle;
+ 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]);
}
}
- config.seriesIdx = idx;
}
- if (series instanceof Ext.chart.series.Series) {
- Ext.apply(series, config);
- } else {
- Ext.applyIf(config, series);
- series = me.series.replace(Ext.createByAlias('series.' + series.type.toLowerCase(), config));
+ 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);
+ }
}
- if (series.initialize) {
- series.initialize();
+ 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
- getMaxGutter: function() {
+ //@private
+ getOrCreateLabel: function(i, text) {
var me = this,
- maxGutter = [0, 0];
- me.series.each(function(s) {
- var gutter = s.getGutters && s.getGutters() || [0, 0];
- maxGutter[0] = Math.max(maxGutter[0], gutter[0]);
- maxGutter[1] = Math.max(maxGutter[1], gutter[1]);
- });
- me.maxGutter = maxGutter;
- },
-
- // @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');
+ 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;
},
- // @private remove gently.
- destroy: function() {
- this.surface.destroy();
- this.bindStore(null);
- this.callParent(arguments);
- }
-});
+ 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);
-/**
- * @class Ext.chart.Highlight
- * @ignore
- */
-Ext.define('Ext.chart.Highlight', {
+ p2[0] = matrix.x.apply(matrix, p2p);
+ p2[1] = matrix.y.apply(matrix, p2p);
- /* Begin Definitions */
+ p3[0] = matrix.x.apply(matrix, p3p);
+ p3[1] = matrix.y.apply(matrix, p3p);
- requires: ['Ext.fx.Anim'],
+ p4[0] = matrix.x.apply(matrix, p4p);
+ p4[1] = matrix.y.apply(matrix, p4p);
+ return [p1, p2, p3, p4];
+ },
- /* End Definitions */
+ intersect: function(l1, l2) {
+ var r1 = this.rect2pointArray(l1),
+ r2 = this.rect2pointArray(l2);
+ return !!Ext.draw.Draw.intersect(r1, r2).length;
+ },
- /**
- * Highlight the given series item.
- * @param {Boolean|Object} Default's false. Can also be an object width style properties (i.e fill, stroke, radius)
- * or just use default styles per series by setting highlight = true.
- */
- highlight: false,
+ drawHorizontalLabels: function() {
+ var me = this,
+ labelConf = me.label,
+ floor = Math.floor,
+ max = Math.max,
+ axes = me.chart.axes,
+ position = me.position,
+ inflections = me.inflections,
+ ln = inflections.length,
+ labels = me.labels,
+ labelGroup = me.labelGroup,
+ maxHeight = 0,
+ ratio,
+ gutterY = me.chart.maxGutter[1],
+ ubbox, bbox, point, prevX, prevLabel,
+ projectedWidth = 0,
+ textLabel, attr, textRight, text,
+ label, last, x, y, i, firstLabel;
- highlightCfg : null,
+ 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)));
- constructor: function(config) {
- if (config.highlight) {
- if (config.highlight !== true) { //is an object
- this.highlightCfg = Ext.apply({}, config.highlight);
+ for (i = 0; i < ln; i++) {
+ point = inflections[i];
+ text = me.label.renderer(labels[i]);
+ 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 (me.chart.maxGutter[0] == 0) {
+ if (i == 0 && axes.findIndex('position', 'left') == -1) {
+ x = point[0];
+ }
+ else if (i == last && axes.findIndex('position', 'right') == -1) {
+ x = point[0] - bbox.width;
+ }
+ }
+ if (position == 'top') {
+ y = point[1] - (me.dashSize * 2) - me.label.padding - (bbox.height / 2);
}
else {
- this.highlightCfg = {
- fill: '#fdd',
- radius: 20,
- lineWidth: 5,
- stroke: '#f55'
- };
+ 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))) {
+ textLabel.hide(true);
+ continue;
}
+
+ prevLabel = textLabel;
}
+
+ return maxHeight;
},
- /**
- * Highlight the given series item.
- * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
- */
- highlightItem: function(item) {
- if (!item) {
- return;
- }
-
+ drawVerticalLabels: function() {
var me = this,
- sprite = item.sprite,
- opts = me.highlightCfg,
- surface = me.chart.surface,
- animate = me.chart.animate,
- p,
- from,
- to,
- pi;
+ inflections = me.inflections,
+ position = me.position,
+ ln = inflections.length,
+ labels = me.labels,
+ maxWidth = 0,
+ max = Math.max,
+ floor = Math.floor,
+ ceil = Math.ceil,
+ axes = me.chart.axes,
+ gutterY = me.chart.maxGutter[1],
+ ubbox, bbox, point, prevLabel,
+ projectedWidth = 0,
+ textLabel, attr, textRight, text,
+ label, last, x, y, i;
- 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._defaults || {},
- sprite.attr);
- from = {};
- to = {};
- for (p in opts) {
- if (! (p in sprite._defaults)) {
- sprite._defaults[p] = surface.availableAttrs[p];
+ last = ln;
+ for (i = 0; i < last; 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 (gutterY < bbox.height / 2) {
+ if (i == last - 1 && axes.findIndex('position', 'top') == -1) {
+ y = me.y - me.length + ceil(bbox.height / 2);
}
- 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];
- }
- }
+ else if (i == 0 && axes.findIndex('position', 'bottom') == -1) {
+ y = me.y - floor(bbox.height / 2);
}
}
- sprite._from = from;
- sprite._to = to;
- }
- if (animate) {
- sprite._anim = Ext.create('Ext.fx.Anim', {
- target: sprite,
- from: sprite._from,
- to: sprite._to,
- duration: 150
- });
- } else {
- sprite.setAttributes(sprite._to, true);
+ 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)) {
+ textLabel.hide(true);
+ continue;
+ }
+ prevLabel = textLabel;
}
+
+ return maxWidth;
},
/**
- * Un-highlight any existing highlights
+ * Renders the labels in the axes.
*/
- unHighlightItem: function() {
- if (!this.highlight || !this.items) {
- return;
+ 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();
}
- var me = this,
- items = me.items,
- len = items.length,
- opts = me.highlightCfg,
- animate = me.chart.animate,
- i = 0,
- obj,
- p,
- sprite;
+ // Hide unused bars
+ ln = labelGroup.getCount();
+ i = inflections.length;
+ for (; i < ln; i++) {
+ labelGroup.getAt(i).hide(true);
+ }
- 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(obj[p], sprite._defaults[p]);
- }
- else {
- obj[p] = sprite._defaults[p];
- }
- }
- if (animate) {
- sprite._anim = Ext.create('Ext.fx.Anim', {
- target: sprite,
- to: obj,
- duration: 150
- });
- }
- else {
- sprite.setAttributes(obj, 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);
+ }
+ },
+
+ // @private creates the elipsis for the text.
+ elipsis: function(sprite, text, desiredWidth, minWidth, center) {
+ var bbox,
+ x;
+
+ if (desiredWidth < minWidth) {
+ sprite.hide(true);
+ return false;
+ }
+ while (text.length > 4) {
+ text = text.substr(0, text.length - 4) + "...";
+ sprite.setAttributes({
+ text: text
+ }, true);
+ bbox = sprite.getBBox();
+ if (bbox.width < desiredWidth) {
+ if (typeof center == 'number') {
+ sprite.setAttributes({
+ x: Math.floor(center - (bbox.width / 2))
+ }, true);
}
- delete sprite._highlighted;
- //delete sprite._defaults;
+ break;
}
}
+ return true;
+ },
+
+ /**
+ * Updates the {@link #title} of this axis.
+ * @param {String} title
+ */
+ setTitle: function(title) {
+ this.title = title;
+ this.drawLabel();
},
- cleanHighlights: function() {
- if (!this.highlight) {
- return;
+ // @private draws the title for the axis.
+ drawTitle: function(maxWidth, maxHeight) {
+ var me = this,
+ position = me.position,
+ 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;
- var group = this.group,
- markerGroup = this.markerGroup,
- i = 0,
- l;
- for (l = group.getCount(); i < l; i++) {
- delete group.getAt(i)._defaults;
+ if (rotate) {
+ 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;
}
- if (markerGroup) {
- for (l = markerGroup.getCount(); i < l; i++) {
- delete markerGroup.getAt(i)._defaults;
+ else {
+ 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);
}
});
+
/**
- * @class Ext.chart.Label
+ * @class Ext.chart.axis.Category
+ * @extends Ext.chart.axis.Axis
*
- * Labels is a mixin whose methods are appended onto the Series class. Labels is an interface with methods implemented
- * in each of the Series (Pie, Bar, etc) for label creation and label placement.
+ * 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.
*
- * The methods implemented by the Series are:
- *
- * - **`onCreateLabel(storeItem, item, i, display)`** Called each time a new label is created.
- * The arguments of the method are:
- * - *`storeItem`* The element of the store that is related to the label sprite.
- * - *`item`* The item related to the label 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).
- * - *`i`* The index of the element created (i.e the first created label, second created label, etc)
- * - *`display`* The display type. May be false if the label is hidden
+ * As with other axis you can set the position of the axis and its title. For example:
*
- * - **`onPlaceLabel(label, storeItem, item, i, display, animate)`** Called for updating the position of the label.
- * The arguments of the method are:
- * - *`label`* The sprite label.
- * - *`storeItem`* The element of the store that is related to the label sprite
- * - *`item`* The item related to the label 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).
- * - *`i`* The index of the element to be updated (i.e. whether it is the first, second, third from the labelGroup)
- * - *`display`* The display type. May be false if the label is hidden.
- * - *`animate`* A boolean value to set or unset animations for the labels.
+ * @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',
+ * grid: true,
+ * 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.Label', {
+Ext.define('Ext.chart.axis.Category', {
/* Begin Definitions */
- requires: ['Ext.draw.Color'],
-
+ extend: 'Ext.chart.axis.Axis',
+
+ alternateClassName: 'Ext.chart.CategoryAxis',
+
+ alias: 'axis.category',
+
/* End Definitions */
/**
- * @cfg {String} display
- * Specifies the presence and position of labels for each pie slice. Either "rotate", "middle", "insideStart",
- * "insideEnd", "outside", "over", "under", or "none" to prevent label rendering.
- * Default value: 'none'.
+ * A list of category names to display along this axis.
+ * @property {String} categoryNames
*/
+ categoryNames: null,
/**
- * @cfg {String} color
- * The color of the label text.
- * Default value: '#000' (black).
+ * Indicates whether or not to calculate the number of categories (ticks and
+ * labels) when there is not enough room to display all labels on the axis.
+ * If set to true, the axis will determine the number of categories to plot.
+ * If not, all categories will be plotted.
+ *
+ * @property calculateCategoryCount
+ * @type Boolean
*/
+ calculateCategoryCount: false,
+
+ // @private creates an array of labels to be used when rendering.
+ setLabels: function() {
+ var store = this.chart.store,
+ fields = this.fields,
+ ln = fields.length,
+ i;
+
+ this.labels = [];
+ store.each(function(record) {
+ for (i = 0; i < ln; i++) {
+ this.labels.push(record.get(fields[i]));
+ }
+ }, this);
+ },
+
+ // @private calculates labels positions and marker positions for rendering.
+ applyData: function() {
+ this.callParent();
+ this.setLabels();
+ var count = this.chart.store.getCount();
+ return {
+ from: 0,
+ to: count,
+ power: 1,
+ step: 1,
+ steps: count - 1
+ };
+ }
+});
+
+/**
+ * @class Ext.chart.axis.Gauge
+ * @extends Ext.chart.axis.Abstract
+ *
+ * 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 {String} field
- * The name of the field to be displayed in the label.
- * Default value: 'name'.
+ * @cfg {Number} minimum (required)
+ * The minimum value of the interval to be displayed in the axis.
*/
/**
- * @cfg {Number} 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} maximum (required)
+ * The maximum value of the interval to be displayed in the axis.
*/
/**
- * @cfg {String} font
- * The font used for the labels.
- * Defautl value: "11px Helvetica, sans-serif".
+ * @cfg {Number} steps (required)
+ * The number of steps and tick marks to add to the interval.
*/
/**
- * @cfg {String} orientation
- * Either "horizontal" or "vertical".
- * Dafault value: "horizontal".
+ * @cfg {Number} [margin=10]
+ * The offset positioning of the tick marks and labels in pixels.
*/
/**
- * @cfg {Function} renderer
- * Optional function for formatting the label into a displayable value.
- * Default value: function(v) { return v; }
- * @param v
+ * @cfg {String} title
+ * The title for the Axis.
*/
- //@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",
- color: "#000",
- field: "name",
- minMargin: 50,
- font: "11px Helvetica, sans-serif",
- orientation: "horizontal",
- renderer: function(v) {
- return v;
- }
- });
+ position: 'gauge',
- if (me.label.display !== 'none') {
- me.labelsGroup = me.chart.surface.getGroup(me.seriesId + '-labels');
- }
- },
+ alias: 'axis.gauge',
- //@private a method to render all labels in the labelGroup
- renderLabels: function() {
- var me = this,
- chart = me.chart,
- gradients = chart.gradients,
- gradient,
- items = me.items,
- animate = chart.animate,
- config = me.label,
- display = config.display,
- color = config.color,
- field = [].concat(config.field),
- group = me.labelsGroup,
- store = me.chart.store,
- len = store.getCount(),
- ratio = items.length / len,
- i, count, j,
- k, gradientsCount = (gradients || 0) && gradients.length,
- colorStopTotal, colorStopIndex, colorStop,
- item, label, storeItem,
- sprite, spriteColor, spriteBrightness, labelColor,
- Color = Ext.draw.Color,
- colorString;
+ 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 (display == 'none') {
+ if (this.sprites && !chart.resizing) {
+ this.drawLabel();
return;
}
- for (i = 0, count = 0; i < len; i++) {
- for (j = 0; j < ratio; j++) {
- item = items[count];
- label = group.getAt(count);
- storeItem = store.getAt(i);
-
- if (!item && label) {
- label.hide(true);
+ 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);
}
-
- if (item && field[j]) {
- if (!label) {
- label = me.onCreateLabel(storeItem, item, i, display, j, count);
- }
- me.onPlaceLabel(label, storeItem, item, i, display, animate, j, count);
-
- //set contrast
- if (config.contrast && item.sprite) {
- sprite = item.sprite;
- colorString = sprite._to && sprite._to.fill || 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;
- }
- labelColor = Color.fromString(label.attr.color || label.attr.fill).getHSL();
-
- labelColor[2] = spriteBrightness > 0.5? 0.2 : 0.8;
- label.setAttributes({
- fill: String(Color.fromHSL.apply({}, labelColor))
- }, true);
- }
+ } 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);
}
- count++;
}
}
- me.hideLabels(count);
- },
-
- //@private a method to hide labels.
- hideLabels: function(index) {
- var labelsGroup = this.labelsGroup, len;
- if (labelsGroup) {
- len = labelsGroup.getCount();
- while (len-->index) {
- labelsGroup.getAt(len).hide(true);
- }
- }
- }
-});
-Ext.define('Ext.chart.MaskLayer', {
- extend: 'Ext.Component',
-
- constructor: function(config) {
- config = Ext.apply(config || {}, {
- style: 'position:absolute;background-color:#888;cursor:move;opacity:0.6;border:1px solid #222;'
- });
- this.callParent([config]);
- },
-
- initComponent: function() {
- var me = this;
- me.callParent(arguments);
- me.addEvents(
- 'mousedown',
- 'mouseup',
- 'mousemove',
- 'mouseenter',
- 'mouseleave'
- );
- },
-
- 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();
- }
- };
- }
-});
-/**
- * @class Ext.chart.TipSurface
- * @ignore
- */
-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;
+ this.sprites = sprites;
+ this.drawLabel();
+ if (this.title) {
+ this.drawTitle();
}
},
- onRender: function() {
+ drawTitle: 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
- * @ignore
- */
-Ext.define('Ext.chart.Tip', {
-
- /* Begin Definitions */
-
- requires: ['Ext.tip.ToolTip', 'Ext.chart.TipSurface'],
-
- /* End Definitions */
+ chart = me.chart,
+ surface = chart.surface,
+ bbox = chart.chartBBox,
+ labelSprite = me.titleSprite,
+ labelBBox;
- 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: false
+ if (!labelSprite) {
+ me.titleSprite = labelSprite = surface.add({
+ type: 'text',
+ zIndex: 2
});
- me.tooltip = Ext.create('Ext.tip.ToolTip', me.tipConfig);
- Ext.getBody().on('mousemove', me.tooltip.onMouseMove, me.tooltip);
- if (me.tipConfig.surface) {
- //initialize a surface
- surface = me.tipConfig.surface;
- sprites = surface.sprites;
- tipSurface = Ext.create('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;
- }
}
+ 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);
},
- showTip: function(item) {
- var me = this;
- if (!me.tooltip) {
- return;
- }
- clearTimeout(me.tipTimeout);
- var tooltip = me.tooltip,
- spriteTip = me.spriteTip,
- tipConfig = me.tipConfig,
- trackMouse = tooltip.trackMouse,
- sprite, surface, surfaceExt, pos, x, y;
- 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];
+ /**
+ * Updates the {@link #title} of this axis.
+ * @param {String} title
+ */
+ setTitle: function(title) {
+ this.title = title;
+ this.drawTitle();
+ },
+
+ drawLabel: function() {
+ 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 + 2 * margin,
+ round = Math.round,
+ labelArray = [], label,
+ maxValue = this.maximum || 0,
+ steps = this.steps, i = 0,
+ adjY,
+ pi = Math.PI,
+ cos = Math.cos,
+ sin = Math.sin,
+ labelConf = this.label,
+ renderer = labelConf.renderer || function(v) { return v; };
+
+ 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;
+ label = surface.add({
+ type: 'text',
+ text: renderer(round(i / steps * maxValue)),
+ 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);
}
}
- if (spriteTip) {
- tipConfig.renderer.call(tooltip, item.storeItem, item, spriteTip.surface);
- } else {
- tipConfig.renderer.call(tooltip, item.storeItem, item);
- }
- tooltip.show();
- tooltip.trackMouse = trackMouse;
- },
-
- hideTip: function(item) {
- var tooltip = this.tooltip;
- if (!tooltip) {
- return;
+ 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;
+ labelArray[i].setAttributes({
+ text: renderer(round(i / steps * maxValue)),
+ x: centerX + rho * cos(i / steps * pi - pi),
+ y: centerY + rho * sin(i / steps * pi - pi) - adjY
+ }, true);
+ }
}
- clearTimeout(this.tipTimeout);
- this.tipTimeout = setTimeout(function() {
- tooltip.hide();
- }, 0);
+ this.labelArray = labelArray;
}
});
/**
- * @class Ext.chart.axis.Abstract
- * @ignore
+ * @class Ext.chart.axis.Numeric
+ * @extends Ext.chart.axis.Axis
+ *
+ * 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',
+ * grid: true,
+ * 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.Abstract', {
+Ext.define('Ext.chart.axis.Numeric', {
/* Begin Definitions */
- requires: ['Ext.chart.Chart'],
+ extend: 'Ext.chart.axis.Axis',
+
+ alternateClassName: 'Ext.chart.NumericAxis',
/* End Definitions */
- constructor: function(config) {
- config = config || {};
+ type: 'numeric',
+
+ alias: 'axis.numeric',
+ constructor: function(config) {
var me = this,
- pos = config.position || 'left';
+ hasLabel = !!(config.label && config.label.renderer),
+ label;
- 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 = [].concat(me.fields);
- this.callParent();
- me.labels = [];
- me.getId();
- me.labelGroup = me.chart.surface.getGroup(me.axisId + "-labels");
+ me.callParent([config]);
+ label = me.label;
+ if (me.roundToDecimal === false) {
+ return;
+ }
+ if (!hasLabel) {
+ label.renderer = function(v) {
+ return me.roundToDecimal(v, me.decimals);
+ };
+ }
},
- 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-'));
+ roundToDecimal: function(v, dec) {
+ var val = Math.pow(10, dec || 0);
+ return Math.floor(v * val) / val;
},
- /*
- 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.chart.axis.Axis
- * @extends Ext.chart.axis.Abstract
- *
- * 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',
- grid: true,
- position: 'left',
- 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 */
+ /**
+ * The minimum value drawn by the axis. If not set explicitly, the axis
+ * minimum will be calculated automatically.
+ *
+ * @property {Number} minimum
+ */
+ minimum: NaN,
/**
- * @cfg {Number} majorTickSteps
- * If `minimum` and `maximum` are specified it forces the number of major ticks to the specified value.
+ * The maximum value drawn by the axis. If not set explicitly, the axis
+ * maximum will be calculated automatically.
+ *
+ * @property {Number} maximum
*/
+ maximum: NaN,
/**
- * @cfg {Number} minorTickSteps
- * The number of small ticks between two major ticks. Default is zero.
+ * The number of decimals to round the value to.
+ *
+ * @property {Number} decimals
*/
+ decimals: 2,
/**
- * @cfg {Number} dashSize
- * The size of the dash marker. Default's 3.
+ * The scaling algorithm to use on this axis. May be "linear" or
+ * "logarithmic". Currently only linear scale is implemented.
+ *
+ * @property {String} scale
+ * @private
*/
- dashSize: 3,
-
+ scale: "linear",
+
/**
- * @cfg {String} position
- * Where to set the axis. Available options are `left`, `bottom`, `right`, `top`. Default's `bottom`.
+ * Indicates the position of the axis relative to the chart
+ *
+ * @property {String} position
*/
- position: 'bottom',
-
- // @private
- skipFirst: false,
-
+ position: 'left',
+
/**
- * @cfg {Number} length
- * Offset axis position. Default's 0.
+ * Indicates whether to extend maximum beyond data's maximum to the nearest
+ * majorUnit.
+ *
+ * @property {Boolean} adjustMaximumByMajorUnit
*/
- length: 0,
-
+ adjustMaximumByMajorUnit: false,
+
/**
- * @cfg {Number} width
- * Offset axis width. Default's 0.
+ * Indicates whether to extend the minimum beyond data's minimum to the
+ * nearest majorUnit.
+ *
+ * @property {Boolean} adjustMinimumByMajorUnit
*/
- width: 0,
-
- majorTickSteps: false,
+ adjustMinimumByMajorUnit: false,
- // @private
- applyData: Ext.emptyFn,
+ // @private apply data.
+ applyData: function() {
+ this.callParent();
+ return this.calcEnds();
+ }
+});
- // @private creates a structure with start, end and step points.
- calcEnds: function() {
- var me = this,
- math = Math,
- mmax = math.max,
- mmin = math.min,
- store = me.chart.substore || me.chart.store,
- series = me.chart.series.items,
- fields = me.fields,
- ln = fields.length,
- min = isNaN(me.minimum) ? Infinity : me.minimum,
- max = isNaN(me.maximum) ? -Infinity : me.maximum,
- prevMin = me.prevMin,
- prevMax = me.prevMax,
- aggregate = false,
- total = 0,
- excludes = [],
- outfrom, outto,
- i, l, values, rec, out;
+/**
+ * @class Ext.chart.axis.Radial
+ * @extends Ext.chart.axis.Abstract
+ * @ignore
+ */
+Ext.define('Ext.chart.axis.Radial', {
- //if one series is stacked I have to aggregate the values
- //for the scale.
- for (i = 0, l = series.length; !aggregate && i < l; i++) {
- aggregate = aggregate || series[i].stacked;
- excludes = series[i].__excludes || excludes;
- }
- store.each(function(record) {
- if (aggregate) {
- if (!isFinite(min)) {
- min = 0;
- }
- for (values = [0, 0], i = 0; i < ln; i++) {
- if (excludes[i]) {
- continue;
- }
- rec = record.get(fields[i]);
- values[+(rec > 0)] += math.abs(rec);
- }
- max = mmax(max, -values[0], values[1]);
- min = mmin(min, -values[0], values[1]);
- }
- else {
- for (i = 0; i < ln; i++) {
- if (excludes[i]) {
- continue;
- }
- value = record.get(fields[i]);
- max = mmax(max, value);
- min = mmin(min, value);
- }
- }
- });
- if (!isFinite(max)) {
- max = me.prevMax || 0;
- }
- if (!isFinite(min)) {
- min = me.prevMin || 0;
- }
- //normalize min max for snapEnds.
- if (min != max && (max != (max >> 0))) {
- max = (max >> 0) + 1;
- }
- out = Ext.draw.Draw.snapEnds(min, max, me.majorTickSteps !== false ? (me.majorTickSteps +1) : me.steps);
- outfrom = out.from;
- outto = out.to;
- if (!isNaN(me.maximum)) {
- //TODO(nico) users are responsible for their own minimum/maximum values set.
- //Clipping should be added to remove lines in the chart which are below the axis.
- out.to = me.maximum;
- }
- if (!isNaN(me.minimum)) {
- //TODO(nico) users are responsible for their own minimum/maximum values set.
- //Clipping should be added to remove lines in the chart which are below the axis.
- out.from = me.minimum;
- }
-
- //Adjust after adjusting minimum and maximum
- out.step = (out.to - out.from) / (outto - outfrom) * out.step;
-
- if (me.adjustMaximumByMajorUnit) {
- out.to += out.step;
- }
- if (me.adjustMinimumByMajorUnit) {
- out.from -= out.step;
- }
- me.prevMin = min == max? 0 : min;
- me.prevMax = max;
- return out;
- },
+ /* Begin Definitions */
- /**
- * Renders the axis into the screen and updates it's position.
- */
- drawAxis: function (init) {
- var me = this,
- i, j,
- x = me.x,
- y = me.y,
- gutterX = me.chart.maxGutter[0],
- gutterY = me.chart.maxGutter[1],
- dashSize = me.dashSize,
- subDashesX = me.minorTickSteps || 0,
- subDashesY = me.minorTickSteps || 0,
- length = me.length,
- position = me.position,
- inflections = [],
- calcLabels = false,
- stepCalcs = me.applyData(),
- step = stepCalcs.step,
- steps = stepCalcs.steps,
- from = stepCalcs.from,
- to = stepCalcs.to,
- trueLength,
- currentX,
- currentY,
- path,
- prev,
- dashesX,
- dashesY,
- delta;
-
- //If no steps are specified
- //then don't draw the axis. This generally happens
- //when an empty store.
- if (me.hidden || isNaN(step) || (from == to)) {
+ extend: 'Ext.chart.axis.Abstract',
+
+ /* End Definitions */
+
+ position: 'radial',
+
+ alias: 'axis.radial',
+
+ drawAxis: function(init) {
+ var chart = this.chart,
+ surface = chart.surface,
+ bbox = chart.chartBBox,
+ store = chart.store,
+ 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;
}
- me.from = stepCalcs.from;
- me.to = stepCalcs.to;
- if (position == 'left' || position == 'right') {
- currentX = Math.floor(x) + 0.5;
- path = ["M", currentX, y, "l", 0, -length];
- trueLength = length - (gutterY * 2);
- }
- else {
- currentY = Math.floor(y) + 0.5;
- path = ["M", x, currentY, "l", length, 0];
- trueLength = length - (gutterX * 2);
- }
-
- delta = trueLength / (steps || 1);
- dashesX = Math.max(subDashesX +1, 0);
- dashesY = Math.max(subDashesY +1, 0);
- if (me.type == 'Numeric') {
- calcLabels = true;
- me.labels = [stepCalcs.from];
- }
- if (position == 'right' || position == 'left') {
- currentY = y - gutterY;
- currentX = x - ((position == 'left') * dashSize * 2);
- while (currentY >= y - gutterY - trueLength) {
- path.push("M", currentX, Math.floor(currentY) + 0.5, "l", dashSize * 2 + 1, 0);
- if (currentY != y - gutterY) {
- for (i = 1; i < dashesY; i++) {
- path.push("M", currentX + dashSize, Math.floor(currentY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0);
- }
- }
- inflections.push([ Math.floor(x), Math.floor(currentY) ]);
- currentY -= delta;
- if (calcLabels) {
- me.labels.push(me.labels[me.labels.length -1] + step);
- }
- if (delta === 0) {
- break;
- }
- }
- if (Math.round(currentY + delta - (y - gutterY - trueLength))) {
- path.push("M", currentX, Math.floor(y - length + gutterY) + 0.5, "l", dashSize * 2 + 1, 0);
- for (i = 1; i < dashesY; i++) {
- path.push("M", currentX + dashSize, Math.floor(y - length + gutterY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0);
- }
- inflections.push([ Math.floor(x), Math.floor(currentY) ]);
- if (calcLabels) {
- me.labels.push(me.labels[me.labels.length -1] + step);
- }
+ 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
+ store.each(function(rec, 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 {
- currentX = x + gutterX;
- currentY = y - ((position == 'top') * dashSize * 2);
- while (currentX <= x + gutterX + trueLength) {
- path.push("M", Math.floor(currentX) + 0.5, currentY, "l", 0, dashSize * 2 + 1);
- if (currentX != x + gutterX) {
- for (i = 1; i < dashesX; i++) {
- path.push("M", Math.floor(currentX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1);
- }
- }
- inflections.push([ Math.floor(currentX), Math.floor(y) ]);
- currentX += delta;
- if (calcLabels) {
- me.labels.push(me.labels[me.labels.length -1] + step);
- }
- if (delta === 0) {
- break;
- }
- }
- if (Math.round(currentX - delta - (x + gutterX + trueLength))) {
- path.push("M", Math.floor(x + length - gutterX) + 0.5, currentY, "l", 0, dashSize * 2 + 1);
- for (i = 1; i < dashesX; i++) {
- path.push("M", Math.floor(x + length - gutterX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1);
- }
- inflections.push([ Math.floor(currentX), Math.floor(y) ]);
- if (calcLabels) {
- me.labels.push(me.labels[me.labels.length -1] + step);
- }
+ 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
+ store.each(function(rec, 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);
+ });
}
- 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();
+ this.sprites = sprites;
+
+ this.drawLabel();
+ },
+
+ drawLabel: function() {
+ var chart = this.chart,
+ surface = chart.surface,
+ bbox = chart.chartBBox,
+ store = chart.store,
+ 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,
+ 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;
}
- 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,
- gutter = me.chart.maxGutter,
- width = me.width - 2,
- vert = false,
- point, prevPoint,
- i = 1,
- path = [], styles, lineWidth, dlineWidth,
- oddPath = [], evenPath = [];
+ //get all rendered fields
+ chart.series.each(function(series) {
+ fields.push(series.yField);
+ xField = series.xField;
+ });
- if ((gutter[1] !== 0 && (position == 'left' || position == 'right')) ||
- (gutter[0] !== 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]);
+ //get maxValue to interpolate
+ store.each(function(record, i) {
+ if (aggregate) {
+ for (i = 0, nfields = fields.length; i < nfields; i++) {
+ maxValue = max(+record.get(fields[i]), maxValue);
}
}
- }
- if (odd || even) {
- if (oddPath.length) {
- if (!me.gridOdd && oddPath.length) {
- me.gridOdd = surface.add({
- type: 'path',
- path: oddPath
+ categories.push(record.get(xField));
+ });
+ 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);
}
- 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
+ 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')
});
- }
- me.gridEven.setAttributes(Ext.apply({
- path: evenPath,
- hidden: false
- }, even || {}), true);
+ label.setAttributes({
+ hidden: false
+ }, true);
+ labelArray.push(label);
+ }
}
}
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'
- });
+ labelArray = this.labelArray;
+ if (display != 'categories') {
+ //draw values
+ for (i = 0; i < steps; i++) {
+ labelArray[i].setAttributes({
+ text: round((i + 1) / steps * maxValue),
+ x: centerX,
+ y: centerY - rho * (i + 1) / steps,
+ 'text-anchor': 'middle',
+ 'stroke-width': 0.1,
+ stroke: '#333'
+ }, true);
}
- 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();
+ 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);
+ }
+ }
}
}
- 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;
- },
+ this.labelArray = labelArray;
+ }
+});
+/**
+ * @author Ed Spencer
+ *
+ * AbstractStore is a superclass of {@link Ext.data.Store} and {@link Ext.data.TreeStore}. It's never used directly,
+ * but offers a set of methods used by both of those subclasses.
+ *
+ * We've left it here in the docs for reference purposes, but unless you need to make a whole new type of Store, what
+ * you're probably looking for is {@link Ext.data.Store}. If you're still interested, here's a brief description of what
+ * AbstractStore is and is not.
+ *
+ * AbstractStore provides the basic configuration for anything that can be considered a Store. It expects to be
+ * given a {@link Ext.data.Model Model} that represents the type of data in the Store. It also expects to be given a
+ * {@link Ext.data.proxy.Proxy Proxy} that handles the loading of data into the Store.
+ *
+ * AbstractStore provides a few helpful methods such as {@link #load} and {@link #sync}, which load and save data
+ * respectively, passing the requests through the configured {@link #proxy}. Both built-in Store subclasses add extra
+ * behavior to each of these functions. Note also that each AbstractStore subclass has its own way of storing data -
+ * in {@link Ext.data.Store} the data is saved as a flat {@link Ext.util.MixedCollection MixedCollection}, whereas in
+ * {@link Ext.data.TreeStore TreeStore} we use a {@link Ext.data.Tree} to maintain the data's hierarchy.
+ *
+ * The store provides filtering and sorting support. This sorting/filtering can happen on the client side
+ * or can be completed on the server. This is controlled by the {@link Ext.data.Store#remoteSort remoteSort} and
+ * {@link Ext.data.Store#remoteFilter remoteFilter} config options. For more information see the {@link #sort} and
+ * {@link Ext.data.Store#filter filter} methods.
+ */
+Ext.define('Ext.data.AbstractStore', {
+ requires: ['Ext.util.MixedCollection', 'Ext.data.Operation', 'Ext.util.Filter'],
- 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];
+ mixins: {
+ observable: 'Ext.util.Observable',
+ sortable: 'Ext.util.Sortable'
},
- intersect: function(l1, l2) {
- var r1 = this.rect2pointArray(l1),
- r2 = this.rect2pointArray(l2);
- return !!Ext.draw.Draw.intersect(r1, r2).length;
+ statics: {
+ create: function(store){
+ if (!store.isStore) {
+ if (!store.type) {
+ store.type = 'store';
+ }
+ store = Ext.createByAlias('store.' + store.type, store);
+ }
+ return store;
+ }
},
- drawHorizontalLabels: function() {
- var me = this,
- labelConf = me.label,
- floor = Math.floor,
- max = Math.max,
- axes = me.chart.axes,
- position = me.position,
- inflections = me.inflections,
- ln = inflections.length,
- labels = me.labels,
- labelGroup = me.labelGroup,
- maxHeight = 0,
- ratio,
- gutterY = me.chart.maxGutter[1],
- ubbox, bbox, point, prevX, prevLabel,
- projectedWidth = 0,
- textLabel, attr, textRight, text,
- label, last, x, y, i, firstLabel;
+ remoteSort : false,
+ remoteFilter: false,
- 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.abs(Math.sin(labelConf.rotate && (labelConf.rotate.degrees * Math.PI / 180) || 0)) >> 0;
+ /**
+ * @cfg {String/Ext.data.proxy.Proxy/Object} proxy
+ * The Proxy to use for this Store. This can be either a string, a config object or a Proxy instance -
+ * see {@link #setProxy} for details.
+ */
+
+ /**
+ * @cfg {Boolean/Object} autoLoad
+ * If data is not specified, and if autoLoad is true or an Object, this store's load method is automatically called
+ * after creation. If the value of autoLoad is an Object, this Object will be passed to the store's load method.
+ * Defaults to false.
+ */
+ autoLoad: false,
+
+ /**
+ * @cfg {Boolean} autoSync
+ * True to automatically sync the Store with its Proxy after every edit to one of its Records. Defaults to false.
+ */
+ autoSync: false,
+
+ /**
+ * @property {String} batchUpdateMode
+ * Sets the updating behavior based on batch synchronization. 'operation' (the default) will update the Store's
+ * internal representation of the data after each operation of the batch has completed, 'complete' will wait until
+ * the entire batch has been completed before updating the Store's data. 'complete' is a good choice for local
+ * storage proxies, 'operation' is better for remote proxies, where there is a comparatively high latency.
+ */
+ batchUpdateMode: 'operation',
+
+ /**
+ * @property {Boolean} filterOnLoad
+ * If true, any filters attached to this Store will be run after loading data, before the datachanged event is fired.
+ * Defaults to true, ignored if {@link Ext.data.Store#remoteFilter remoteFilter} is true
+ */
+ filterOnLoad: true,
+
+ /**
+ * @property {Boolean} sortOnLoad
+ * If true, any sorters attached to this Store will be run after loading data, before the datachanged event is fired.
+ * Defaults to true, igored if {@link Ext.data.Store#remoteSort remoteSort} is true
+ */
+ sortOnLoad: true,
+
+ /**
+ * @property {Boolean} implicitModel
+ * True if a model was created implicitly for this Store. This happens if a fields array is passed to the Store's
+ * constructor instead of a model constructor or name.
+ * @private
+ */
+ implicitModel: false,
+
+ /**
+ * @property {String} defaultProxyType
+ * The string type of the Proxy to create if none is specified. This defaults to creating a
+ * {@link Ext.data.proxy.Memory memory proxy}.
+ */
+ defaultProxyType: 'memory',
+
+ /**
+ * @property {Boolean} isDestroyed
+ * True if the Store has already been destroyed. If this is true, the reference to Store should be deleted
+ * as it will not function correctly any more.
+ */
+ isDestroyed: false,
+
+ isStore: true,
+
+ /**
+ * @cfg {String} storeId
+ * Unique identifier for this store. If present, this Store will be registered with the {@link Ext.data.StoreManager},
+ * making it easy to reuse elsewhere. Defaults to undefined.
+ */
+
+ /**
+ * @cfg {Object[]} fields
+ * This may be used in place of specifying a {@link #model} configuration. The fields should be a
+ * set of {@link Ext.data.Field} configuration objects. The store will automatically create a {@link Ext.data.Model}
+ * with these fields. In general this configuration option should be avoided, it exists for the purposes of
+ * backwards compatibility. For anything more complicated, such as specifying a particular id property or
+ * assocations, a {@link Ext.data.Model} should be defined and specified for the {@link #model}
+ * config.
+ */
+
+ /**
+ * @cfg {String} model
+ * Name of the {@link Ext.data.Model Model} associated with this store.
+ * The string is used as an argument for {@link Ext.ModelManager#getModel}.
+ */
+
+ sortRoot: 'data',
+
+ //documented above
+ constructor: function(config) {
+ var me = this,
+ filters;
- for (i = 0; i < ln; i++) {
- point = inflections[i];
- text = me.label.renderer(labels[i]);
- 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 (me.chart.maxGutter[0] == 0) {
- if (i == 0 && axes.findIndex('position', 'left') == -1) {
- x = point[0];
- }
- else if (i == last && axes.findIndex('position', 'right') == -1) {
- x = point[0] - bbox.width;
- }
- }
- 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);
- }
+ me.addEvents(
+ /**
+ * @event add
+ * Fired when a Model instance has been added to this Store
+ * @param {Ext.data.Store} store The store
+ * @param {Ext.data.Model[]} records The Model instances that were added
+ * @param {Number} index The index at which the instances were inserted
+ */
+ 'add',
+
+ /**
+ * @event remove
+ * Fired when a Model instance has been removed from this Store
+ * @param {Ext.data.Store} store The Store object
+ * @param {Ext.data.Model} record The record that was removed
+ * @param {Number} index The index of the record that was removed
+ */
+ 'remove',
- textLabel.setAttributes({
- hidden: false,
- x: x,
- y: y
- }, true);
+ /**
+ * @event update
+ * Fires when a Model instance has been updated
+ * @param {Ext.data.Store} this
+ * @param {Ext.data.Model} record The Model instance that was updated
+ * @param {String} operation The update operation being performed. Value may be one of:
+ *
+ * Ext.data.Model.EDIT
+ * Ext.data.Model.REJECT
+ * Ext.data.Model.COMMIT
+ */
+ 'update',
- // Skip label if there isn't available minimum space
- if (i != 0 && (me.intersect(textLabel, prevLabel)
- || me.intersect(textLabel, firstLabel))) {
- textLabel.hide(true);
- continue;
- }
+ /**
+ * @event datachanged
+ * Fires whenever the records in the Store have changed in some way - this could include adding or removing
+ * records, or updating the data in existing records
+ * @param {Ext.data.Store} this The data store
+ */
+ 'datachanged',
+
+ /**
+ * @event beforeload
+ * Fires before a request is made for a new data object. If the beforeload handler returns false the load
+ * action will be canceled.
+ * @param {Ext.data.Store} store This Store
+ * @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to
+ * load the Store
+ */
+ 'beforeload',
+
+ /**
+ * @event load
+ * Fires whenever the store reads data from a remote data source.
+ * @param {Ext.data.Store} this
+ * @param {Ext.data.Model[]} records An array of records
+ * @param {Boolean} successful True if the operation was successful.
+ */
+ 'load',
- prevLabel = textLabel;
+ /**
+ * @event write
+ * Fires whenever a successful write has been made via the configured {@link #proxy Proxy}
+ * @param {Ext.data.Store} store This Store
+ * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object that was used in
+ * the write
+ */
+ 'write',
+
+ /**
+ * @event beforesync
+ * Fired before a call to {@link #sync} is executed. Return false from any listener to cancel the synv
+ * @param {Object} options Hash of all records to be synchronized, broken down into create, update and destroy
+ */
+ 'beforesync',
+ /**
+ * @event clear
+ * Fired after the {@link #removeAll} method is called.
+ * @param {Ext.data.Store} this
+ */
+ 'clear'
+ );
+
+ Ext.apply(me, config);
+ // don't use *config* anymore from here on... use *me* instead...
+
+ /**
+ * Temporary cache in which removed model instances are kept until successfully synchronised with a Proxy,
+ * at which point this is cleared.
+ * @private
+ * @property {Ext.data.Model[]} removed
+ */
+ me.removed = [];
+
+ me.mixins.observable.constructor.apply(me, arguments);
+ me.model = Ext.ModelManager.getModel(me.model);
+
+ /**
+ * @property {Object} modelDefaults
+ * @private
+ * A set of default values to be applied to every model instance added via {@link #insert} or created via {@link #create}.
+ * This is used internally by associations to set foreign keys and other fields. See the Association classes source code
+ * for examples. This should not need to be used by application developers.
+ */
+ Ext.applyIf(me, {
+ modelDefaults: {}
+ });
+
+ //Supports the 3.x style of simply passing an array of fields to the store, implicitly creating a model
+ if (!me.model && me.fields) {
+ me.model = Ext.define('Ext.data.Store.ImplicitModel-' + (me.storeId || Ext.id()), {
+ extend: 'Ext.data.Model',
+ fields: me.fields,
+ proxy: me.proxy || me.defaultProxyType
+ });
+
+ delete me.fields;
+
+ me.implicitModel = true;
+ }
+
+ //
+ if (!me.model) {
+ if (Ext.isDefined(Ext.global.console)) {
+ Ext.global.console.warn('Store defined with no model. You may have mistyped the model name.');
+ }
+ }
+ //
+
+ //ensures that the Proxy is instantiated correctly
+ me.setProxy(me.proxy || me.model.getProxy());
+
+ if (me.id && !me.storeId) {
+ me.storeId = me.id;
+ delete me.id;
}
- return maxHeight;
+ if (me.storeId) {
+ Ext.data.StoreManager.register(me);
+ }
+
+ me.mixins.sortable.initSortable.call(me);
+
+ /**
+ * @property {Ext.util.MixedCollection} filters
+ * The collection of {@link Ext.util.Filter Filters} currently applied to this Store
+ */
+ filters = me.decodeFilters(me.filters);
+ me.filters = Ext.create('Ext.util.MixedCollection');
+ me.filters.addAll(filters);
},
-
- drawVerticalLabels: function() {
- var me = this,
- inflections = me.inflections,
- position = me.position,
- ln = inflections.length,
- labels = me.labels,
- maxWidth = 0,
- max = Math.max,
- floor = Math.floor,
- ceil = Math.ceil,
- axes = me.chart.axes,
- gutterY = me.chart.maxGutter[1],
- ubbox, bbox, point, prevLabel,
- projectedWidth = 0,
- textLabel, attr, textRight, text,
- label, last, x, y, i;
- last = ln;
- for (i = 0; i < last; 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 (gutterY < bbox.height / 2) {
- if (i == last - 1 && axes.findIndex('position', 'top') == -1) {
- y = me.y - me.length + ceil(bbox.height / 2);
- }
- else if (i == 0 && axes.findIndex('position', 'bottom') == -1) {
- y = me.y - 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)) {
- textLabel.hide(true);
- continue;
+ /**
+ * Sets the Store's Proxy by string, config object or Proxy instance
+ * @param {String/Object/Ext.data.proxy.Proxy} proxy The new Proxy, which can be either a type string, a configuration object
+ * or an Ext.data.proxy.Proxy instance
+ * @return {Ext.data.proxy.Proxy} The attached Proxy object
+ */
+ setProxy: function(proxy) {
+ var me = this;
+
+ if (proxy instanceof Ext.data.proxy.Proxy) {
+ proxy.setModel(me.model);
+ } else {
+ if (Ext.isString(proxy)) {
+ proxy = {
+ type: proxy
+ };
}
- prevLabel = textLabel;
+ Ext.applyIf(proxy, {
+ model: me.model
+ });
+
+ proxy = Ext.createByAlias('proxy.' + proxy.type, proxy);
}
- return maxWidth;
+ me.proxy = proxy;
+
+ return me.proxy;
},
/**
- * Renders the labels in the axes.
+ * Returns the proxy currently attached to this proxy instance
+ * @return {Ext.data.proxy.Proxy} The Proxy instance
*/
- drawLabel: function() {
+ getProxy: function() {
+ return this.proxy;
+ },
+
+ //saves any phantom records
+ create: function(data, options) {
var me = this,
- position = me.position,
- labelGroup = me.labelGroup,
- inflections = me.inflections,
- maxWidth = 0,
- maxHeight = 0,
- ln, i;
+ instance = Ext.ModelManager.create(Ext.applyIf(data, me.modelDefaults), me.model.modelName),
+ operation;
+
+ options = options || {};
- if (position == 'left' || position == 'right') {
- maxWidth = me.drawVerticalLabels();
- } else {
- maxHeight = me.drawHorizontalLabels();
- }
+ Ext.applyIf(options, {
+ action : 'create',
+ records: [instance]
+ });
- // Hide unused bars
- ln = labelGroup.getCount();
- i = inflections.length;
- for (; i < ln; i++) {
- labelGroup.getAt(i).hide(true);
- }
+ operation = Ext.create('Ext.data.Operation', options);
- 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);
- }
+ me.proxy.create(operation, me.onProxyWrite, me);
+
+ return instance;
},
- // @private creates the elipsis for the text.
- elipsis: function(sprite, text, desiredWidth, minWidth, center) {
- var bbox,
- x;
+ read: function() {
+ return this.load.apply(this, arguments);
+ },
- if (desiredWidth < minWidth) {
- sprite.hide(true);
- return false;
- }
- while (text.length > 4) {
- text = text.substr(0, text.length - 4) + "...";
- sprite.setAttributes({
- text: text
- }, true);
- bbox = sprite.getBBox();
- if (bbox.width < desiredWidth) {
- if (typeof center == 'number') {
- sprite.setAttributes({
- x: Math.floor(center - (bbox.width / 2))
- }, true);
- }
- break;
- }
- }
- return true;
+ onProxyRead: Ext.emptyFn,
+
+ update: function(options) {
+ var me = this,
+ operation;
+ options = options || {};
+
+ Ext.applyIf(options, {
+ action : 'update',
+ records: me.getUpdatedRecords()
+ });
+
+ operation = Ext.create('Ext.data.Operation', options);
+
+ return me.proxy.update(operation, me.onProxyWrite, me);
},
/**
- * Updates the {@link #title} of this axis.
- * @param {String} title
+ * @private
+ * Callback for any write Operation over the Proxy. Updates the Store's MixedCollection to reflect
+ * the updates provided by the Proxy
*/
- setTitle: function(title) {
- this.title = title;
- this.drawLabel();
- },
-
- // @private draws the title for the axis.
- drawTitle: function(maxWidth, maxHeight) {
+ onProxyWrite: function(operation) {
var me = this,
- position = me.position,
- surface = me.chart.surface,
- displaySprite = me.displaySprite,
- title = me.title,
- rotate = (position == 'left' || position == 'right'),
- x = me.x,
- y = me.y,
- base, bbox, pad;
+ success = operation.wasSuccessful(),
+ records = operation.getRecords();
- 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);
+ switch (operation.action) {
+ case 'create':
+ me.onCreateRecords(records, operation, success);
+ break;
+ case 'update':
+ me.onUpdateRecords(records, operation, success);
+ break;
+ case 'destroy':
+ me.onDestroyRecords(records, operation, success);
+ break;
}
- bbox = displaySprite.getBBox();
- pad = me.dashSize + me.label.padding;
- if (rotate) {
- 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 {
- 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;
+ if (success) {
+ me.fireEvent('write', me, operation);
+ me.fireEvent('datachanged', me);
}
- displaySprite.setAttributes({
- translate: {
- x: x,
- y: y
- }
- }, true);
- }
-});
-/**
- * @class Ext.chart.axis.Category
- * @extends Ext.chart.axis.Axis
- *
- * 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:
- * {@img Ext.chart.axis.Category/Ext.chart.axis.Category.png Ext.chart.axis.Category chart axis}
-
- 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',
- grid: true,
- 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', {
+ //this is a callback that would have been passed to the 'create', 'update' or 'destroy' function and is optional
+ Ext.callback(operation.callback, operation.scope || me, [records, operation, success]);
+ },
- /* Begin Definitions */
- extend: 'Ext.chart.axis.Axis',
+ //tells the attached proxy to destroy the given records
+ destroy: function(options) {
+ var me = this,
+ operation;
+
+ options = options || {};
- alternateClassName: 'Ext.chart.CategoryAxis',
+ Ext.applyIf(options, {
+ action : 'destroy',
+ records: me.getRemovedRecords()
+ });
- alias: 'axis.category',
+ operation = Ext.create('Ext.data.Operation', options);
- /* End Definitions */
+ return me.proxy.destroy(operation, me.onProxyWrite, me);
+ },
/**
- * A list of category names to display along this axis.
- *
- * @property categoryNames
- * @type Array
+ * @private
+ * Attached as the 'operationcomplete' event listener to a proxy's Batch object. By default just calls through
+ * to onProxyWrite.
*/
- categoryNames: null,
+ onBatchOperationComplete: function(batch, operation) {
+ return this.onProxyWrite(operation);
+ },
/**
- * Indicates whether or not to calculate the number of categories (ticks and
- * labels) when there is not enough room to display all labels on the axis.
- * If set to true, the axis will determine the number of categories to plot.
- * If not, all categories will be plotted.
- *
- * @property calculateCategoryCount
- * @type Boolean
+ * @private
+ * Attached as the 'complete' event listener to a proxy's Batch object. Iterates over the batch operations
+ * and updates the Store's internal data MixedCollection.
*/
- calculateCategoryCount: false,
-
- // @private creates an array of labels to be used when rendering.
- setLabels: function() {
- var store = this.chart.store,
- fields = this.fields,
- ln = fields.length,
+ onBatchComplete: function(batch, operation) {
+ var me = this,
+ operations = batch.operations,
+ length = operations.length,
i;
- this.labels = [];
- store.each(function(record) {
- for (i = 0; i < ln; i++) {
- this.labels.push(record.get(fields[i]));
- }
- }, this);
- },
+ me.suspendEvents();
- // @private calculates labels positions and marker positions for rendering.
- applyData: function() {
- this.callParent();
- this.setLabels();
- var count = this.chart.store.getCount();
- return {
- from: 0,
- to: count,
- power: 1,
- step: 1,
- steps: count - 1
- };
- }
-});
+ for (i = 0; i < length; i++) {
+ me.onProxyWrite(operations[i]);
+ }
-/**
- * @class Ext.chart.axis.Gauge
- * @extends Ext.chart.axis.Abstract
- *
- * 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', {
+ me.resumeEvents();
- /* Begin Definitions */
+ me.fireEvent('datachanged', me);
+ },
- extend: 'Ext.chart.axis.Abstract',
+ onBatchException: function(batch, operation) {
+ // //decide what to do... could continue with the next operation
+ // batch.start();
+ //
+ // //or retry the last operation
+ // batch.retry();
+ },
- /* End Definitions */
-
/**
- * @cfg {Number} minimum (required) the minimum value of the interval to be displayed in the axis.
+ * @private
+ * Filter function for new records.
*/
+ filterNew: function(item) {
+ // only want phantom records that are valid
+ return item.phantom === true && item.isValid();
+ },
/**
- * @cfg {Number} maximum (required) the maximum value of the interval to be displayed in the axis.
+ * Returns all Model instances that are either currently a phantom (e.g. have no id), or have an ID but have not
+ * yet been saved on this Store (this happens when adding a non-phantom record from another Store into this one)
+ * @return {Ext.data.Model[]} The Model instances
*/
+ getNewRecords: function() {
+ return [];
+ },
/**
- * @cfg {Number} steps (required) the number of steps and tick marks to add to the interval.
+ * Returns all Model instances that have been updated in the Store but not yet synchronized with the Proxy
+ * @return {Ext.data.Model[]} The updated Model instances
*/
+ getUpdatedRecords: function() {
+ return [];
+ },
/**
- * @cfg {Number} margin (optional) the offset positioning of the tick marks and labels in pixels. Default's 10.
+ * @private
+ * Filter function for updated records.
*/
-
- 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({
- type: 'text',
- zIndex: 2
- });
- }
- 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);
+ filterUpdated: function(item) {
+ // only want dirty records, not phantoms that are valid
+ return item.dirty === true && item.phantom !== true && item.isValid();
},
/**
- * Updates the {@link #title} of this axis.
- * @param {String} title
+ * Returns any records that have been removed from the store but not yet destroyed on the proxy.
+ * @return {Ext.data.Model[]} The removed Model instances
*/
- setTitle: function(title) {
- this.title = title;
- this.drawTitle();
+ getRemovedRecords: function() {
+ return this.removed;
},
- drawLabel: function() {
- 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 + 2 * margin,
- round = Math.round,
- labelArray = [], label,
- maxValue = this.maximum || 0,
- steps = this.steps, i = 0,
- adjY,
- pi = Math.PI,
- cos = Math.cos,
- sin = Math.sin,
- labelConf = this.label,
- renderer = labelConf.renderer || function(v) { return v; };
+ filter: function(filters, value) {
- 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;
- label = surface.add({
- type: 'text',
- text: renderer(round(i / steps * maxValue)),
- 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;
- labelArray[i].setAttributes({
- text: renderer(round(i / steps * maxValue)),
- x: centerX + rho * cos(i / steps * pi - pi),
- y: centerY + rho * sin(i / steps * pi - pi) - adjY
- }, true);
+ },
+
+ /**
+ * @private
+ * Normalizes an array of filter objects, ensuring that they are all Ext.util.Filter instances
+ * @param {Object[]} filters The filters array
+ * @return {Ext.util.Filter[]} Array of Ext.util.Filter objects
+ */
+ decodeFilters: function(filters) {
+ if (!Ext.isArray(filters)) {
+ if (filters === undefined) {
+ filters = [];
+ } else {
+ filters = [filters];
}
}
- this.labelArray = labelArray;
- }
-});
-/**
- * @class Ext.chart.axis.Numeric
- * @extends Ext.chart.axis.Axis
- *
- * 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.
- * {@img Ext.chart.axis.Numeric/Ext.chart.axis.Numeric.png Ext.chart.axis.Numeric chart axis}
- * For 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',
- grid: true,
- position: 'left',
- fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
- title: 'Sample Values',
- grid: {
- odd: {
- opacity: 1,
- fill: '#ddd',
- stroke: '#bbb',
- 'stroke-width': 1
+ var length = filters.length,
+ Filter = Ext.util.Filter,
+ config, i;
+
+ for (i = 0; i < length; i++) {
+ config = filters[i];
+
+ if (!(config instanceof Filter)) {
+ Ext.apply(config, {
+ root: 'data'
+ });
+
+ //support for 3.x style filters where a function can be defined as 'fn'
+ if (config.fn) {
+ config.filterFn = config.fn;
}
- },
- minimum: 0,
- adjustMinimumByMajorUnit: 0
- }, {
- type: 'Category',
- position: 'bottom',
- fields: ['name'],
- title: 'Sample Metrics',
- grid: true,
- label: {
- rotate: {
- degrees: 315
+
+ //support a function to be passed as a filter definition
+ if (typeof config == 'function') {
+ config = {
+ filterFn: config
+ };
}
+
+ filters[i] = new Filter(config);
}
- }],
- 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.
- *
- *
- * @constructor
- */
-Ext.define('Ext.chart.axis.Numeric', {
+ return filters;
+ },
- /* Begin Definitions */
+ clearFilter: function(supressEvent) {
- extend: 'Ext.chart.axis.Axis',
+ },
- alternateClassName: 'Ext.chart.NumericAxis',
+ isFiltered: function() {
- /* End Definitions */
+ },
- type: 'numeric',
+ filterBy: function(fn, scope) {
- alias: 'axis.numeric',
+ },
+
+ /**
+ * Synchronizes the Store with its Proxy. This asks the Proxy to batch together any new, updated
+ * and deleted records in the store, updating the Store's internal representation of the records
+ * as each operation completes.
+ */
+ sync: function() {
+ var me = this,
+ options = {},
+ toCreate = me.getNewRecords(),
+ toUpdate = me.getUpdatedRecords(),
+ toDestroy = me.getRemovedRecords(),
+ needsSync = false;
- constructor: function(config) {
- var me = this, label, f;
- me.callParent([config]);
- label = me.label;
- if (me.roundToDecimal === false) {
- return;
+ if (toCreate.length > 0) {
+ options.create = toCreate;
+ needsSync = true;
}
- if (label.renderer) {
- f = label.renderer;
- label.renderer = function(v) {
- return me.roundToDecimal( f(v), me.decimals );
+
+ if (toUpdate.length > 0) {
+ options.update = toUpdate;
+ needsSync = true;
+ }
+
+ if (toDestroy.length > 0) {
+ options.destroy = toDestroy;
+ needsSync = true;
+ }
+
+ if (needsSync && me.fireEvent('beforesync', options) !== false) {
+ me.proxy.batch(options, me.getBatchListeners());
+ }
+ },
+
+
+ /**
+ * @private
+ * Returns an object which is passed in as the listeners argument to proxy.batch inside this.sync.
+ * This is broken out into a separate function to allow for customisation of the listeners
+ * @return {Object} The listeners object
+ */
+ getBatchListeners: function() {
+ var me = this,
+ listeners = {
+ scope: me,
+ exception: me.onBatchException
};
+
+ if (me.batchUpdateMode == 'operation') {
+ listeners.operationcomplete = me.onBatchOperationComplete;
} else {
- label.renderer = function(v) {
- return me.roundToDecimal(v, me.decimals);
- };
+ listeners.complete = me.onBatchComplete;
}
+
+ return listeners;
},
-
- roundToDecimal: function(v, dec) {
- var val = Math.pow(10, dec || 0);
- return ((v * val) >> 0) / val;
+
+ //deprecated, will be removed in 5.0
+ save: function() {
+ return this.sync.apply(this, arguments);
},
-
- /**
- * The minimum value drawn by the axis. If not set explicitly, the axis
- * minimum will be calculated automatically.
- *
- * @property minimum
- * @type Number
- */
- minimum: NaN,
/**
- * The maximum value drawn by the axis. If not set explicitly, the axis
- * maximum will be calculated automatically.
- *
- * @property maximum
- * @type Number
+ * Loads the Store using its configured {@link #proxy}.
+ * @param {Object} options (optional) config object. This is passed into the {@link Ext.data.Operation Operation}
+ * object that is created and then sent to the proxy's {@link Ext.data.proxy.Proxy#read} function
*/
- maximum: NaN,
+ load: function(options) {
+ var me = this,
+ operation;
+
+ options = options || {};
+
+ Ext.applyIf(options, {
+ action : 'read',
+ filters: me.filters.items,
+ sorters: me.getSorters()
+ });
+
+ operation = Ext.create('Ext.data.Operation', options);
+
+ if (me.fireEvent('beforeload', me, operation) !== false) {
+ me.loading = true;
+ me.proxy.read(operation, me.onProxyLoad, me);
+ }
+
+ return me;
+ },
/**
- * The number of decimals to round the value to.
- * Default's 2.
- *
- * @property decimals
- * @type Number
+ * @private
+ * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
+ * @param {Ext.data.Model} record The model instance that was edited
*/
- decimals: 2,
+ afterEdit : function(record) {
+ var me = this;
+
+ if (me.autoSync) {
+ me.sync();
+ }
+
+ me.fireEvent('update', me, record, Ext.data.Model.EDIT);
+ },
/**
- * The scaling algorithm to use on this axis. May be "linear" or
- * "logarithmic".
- *
- * @property scale
- * @type String
+ * @private
+ * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to..
+ * @param {Ext.data.Model} record The model instance that was edited
*/
- scale: "linear",
+ afterReject : function(record) {
+ this.fireEvent('update', this, record, Ext.data.Model.REJECT);
+ },
/**
- * Indicates the position of the axis relative to the chart
- *
- * @property position
- * @type String
+ * @private
+ * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
+ * @param {Ext.data.Model} record The model instance that was edited
*/
- position: 'left',
+ afterCommit : function(record) {
+ this.fireEvent('update', this, record, Ext.data.Model.COMMIT);
+ },
+
+ clearData: Ext.emptyFn,
+
+ destroyStore: function() {
+ var me = this;
+
+ if (!me.isDestroyed) {
+ if (me.storeId) {
+ Ext.data.StoreManager.unregister(me);
+ }
+ me.clearData();
+ me.data = null;
+ me.tree = null;
+ // Ext.destroy(this.proxy);
+ me.reader = me.writer = null;
+ me.clearListeners();
+ me.isDestroyed = true;
+
+ if (me.implicitModel) {
+ Ext.destroy(me.model);
+ }
+ }
+ },
+
+ doSort: function(sorterFn) {
+ var me = this;
+ if (me.remoteSort) {
+ //the load function will pick up the new sorters and request the sorted data from the proxy
+ me.load();
+ } else {
+ me.data.sortBy(sorterFn);
+ me.fireEvent('datachanged', me);
+ }
+ },
+
+ getCount: Ext.emptyFn,
+ getById: Ext.emptyFn,
+
/**
- * Indicates whether to extend maximum beyond data's maximum to the nearest
- * majorUnit.
- *
- * @property adjustMaximumByMajorUnit
- * @type Boolean
+ * Removes all records from the store. This method does a "fast remove",
+ * individual remove events are not called. The {@link #clear} event is
+ * fired upon completion.
+ * @method
*/
- adjustMaximumByMajorUnit: false,
+ removeAll: Ext.emptyFn,
+ // individual substores should implement a "fast" remove
+ // and fire a clear event afterwards
/**
- * Indicates whether to extend the minimum beyond data's minimum to the
- * nearest majorUnit.
- *
- * @property adjustMinimumByMajorUnit
- * @type Boolean
+ * Returns true if the Store is currently performing a load operation
+ * @return {Boolean} True if the Store is currently loading
*/
- adjustMinimumByMajorUnit: false,
-
- // @private apply data.
- applyData: function() {
- this.callParent();
- return this.calcEnds();
- }
+ isLoading: function() {
+ return !!this.loading;
+ }
});
/**
- * @class Ext.chart.axis.Radial
- * @extends Ext.chart.axis.Abstract
- * @ignore
+ * @class Ext.util.Grouper
+ * @extends Ext.util.Sorter
+
+Represents a single grouper that can be applied to a Store. The grouper works
+in the same fashion as the {@link Ext.util.Sorter}.
+
+ * @markdown
*/
-Ext.define('Ext.chart.axis.Radial', {
+
+Ext.define('Ext.util.Grouper', {
/* Begin Definitions */
- extend: 'Ext.chart.axis.Abstract',
+ extend: 'Ext.util.Sorter',
/* End Definitions */
- position: 'radial',
-
- alias: 'axis.radial',
-
- drawAxis: function(init) {
- var chart = this.chart,
- surface = chart.surface,
- bbox = chart.chartBBox,
- store = chart.store,
- 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;
+ /**
+ * Returns the value for grouping to be used.
+ * @param {Ext.data.Model} instance The Model instance
+ * @return {String} The group string for this model
+ */
+ getGroupString: function(instance) {
+ return instance.get(this.property);
+ }
+});
+/**
+ * @author Ed Spencer
+ * @class Ext.data.Store
+ * @extends Ext.data.AbstractStore
+ *
+ * The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects. Stores load
+ * data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions for {@link #sort sorting},
+ * {@link #filter filtering} and querying the {@link Ext.data.Model model} instances contained within it.
+ *
+ * Creating a Store is easy - we just tell it the Model and the Proxy to use to load and save its data:
+ *
+
+// Set up a {@link Ext.data.Model model} to use in our Store
+Ext.define('User', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {name: 'firstName', type: 'string'},
+ {name: 'lastName', type: 'string'},
+ {name: 'age', type: 'int'},
+ {name: 'eyeColor', type: 'string'}
+ ]
+});
- if (this.sprites && !chart.resizing) {
- this.drawLabel();
- return;
+var myStore = Ext.create('Ext.data.Store', {
+ model: 'User',
+ proxy: {
+ type: 'ajax',
+ url : '/users.json',
+ reader: {
+ type: 'json',
+ root: 'users'
}
+ },
+ autoLoad: true
+});
+
- 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
- store.each(function(rec, 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
- store.each(function(rec, 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);
- });
+ * In the example above we configured an AJAX proxy to load data from the url '/users.json'. We told our Proxy
+ * to use a {@link Ext.data.reader.Json JsonReader} to parse the response from the server into Model object -
+ * {@link Ext.data.reader.Json see the docs on JsonReader} for details.
+ *
+ * Inline data
+ *
+ * Stores can also load data inline. Internally, Store converts each of the objects we pass in as {@link #data}
+ * into Model instances:
+ *
+
+Ext.create('Ext.data.Store', {
+ model: 'User',
+ data : [
+ {firstName: 'Ed', lastName: 'Spencer'},
+ {firstName: 'Tommy', lastName: 'Maintz'},
+ {firstName: 'Aaron', lastName: 'Conran'},
+ {firstName: 'Jamie', lastName: 'Avins'}
+ ]
+});
+
+ *
+ * Loading inline data using the method above is great if the data is in the correct format already (e.g. it doesn't need
+ * to be processed by a {@link Ext.data.reader.Reader reader}). If your inline data requires processing to decode the data structure,
+ * use a {@link Ext.data.proxy.Memory MemoryProxy} instead (see the {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).
+ *
+ * Additional data can also be loaded locally using {@link #add}.
+ *
+ * Loading Nested Data
+ *
+ * Applications often need to load sets of associated data - for example a CRM system might load a User and her Orders.
+ * Instead of issuing an AJAX request for the User and a series of additional AJAX requests for each Order, we can load a nested dataset
+ * and allow the Reader to automatically populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader} intro
+ * docs for a full explanation:
+ *
+
+var store = Ext.create('Ext.data.Store', {
+ autoLoad: true,
+ model: "User",
+ proxy: {
+ type: 'ajax',
+ url : 'users.json',
+ reader: {
+ type: 'json',
+ root: 'users'
+ }
+ }
+});
+
+ *
+ * Which would consume a response like this:
+ *
+
+{
+ "users": [
+ {
+ "id": 1,
+ "name": "Ed",
+ "orders": [
+ {
+ "id": 10,
+ "total": 10.76,
+ "status": "invoiced"
+ },
+ {
+ "id": 11,
+ "total": 13.45,
+ "status": "shipped"
+ }
+ ]
+ }
+ ]
+}
+
+ *
+ * See the {@link Ext.data.reader.Reader} intro docs for a full explanation.
+ *
+ * Filtering and Sorting
+ *
+ * Stores can be sorted and filtered - in both cases either remotely or locally. The {@link #sorters} and {@link #filters} are
+ * held inside {@link Ext.util.MixedCollection MixedCollection} instances to make them easy to manage. Usually it is sufficient to
+ * either just specify sorters and filters in the Store configuration or call {@link #sort} or {@link #filter}:
+ *
+
+var store = Ext.create('Ext.data.Store', {
+ model: 'User',
+ sorters: [
+ {
+ property : 'age',
+ direction: 'DESC'
+ },
+ {
+ property : 'firstName',
+ direction: 'ASC'
}
- this.sprites = sprites;
+ ],
- this.drawLabel();
- },
+ filters: [
+ {
+ property: 'firstName',
+ value : /Ed/
+ }
+ ]
+});
+
+ *
+ * The new Store will keep the configured sorters and filters in the MixedCollection instances mentioned above. By default, sorting
+ * and filtering are both performed locally by the Store - see {@link #remoteSort} and {@link #remoteFilter} to allow the server to
+ * perform these operations instead.
+ *
+ * Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter} adds another filter to the Store
+ * and automatically filters the dataset (calling {@link #filter} with no arguments simply re-applies all existing filters). Note that by
+ * default {@link #sortOnFilter} is set to true, which means that your sorters are automatically reapplied if using local sorting.
+ *
+
+store.filter('eyeColor', 'Brown');
+
+ *
+ * Change the sorting at any time by calling {@link #sort}:
+ *
+
+store.sort('height', 'ASC');
+
+ *
+ * Note that all existing sorters will be removed in favor of the new sorter data (if {@link #sort} is called with no arguments,
+ * the existing sorters are just reapplied instead of being removed). To keep existing sorters and add new ones, just add them
+ * to the MixedCollection:
+ *
+
+store.sorters.add(new Ext.util.Sorter({
+ property : 'shoeSize',
+ direction: 'ASC'
+}));
- drawLabel: function() {
- var chart = this.chart,
- surface = chart.surface,
- bbox = chart.chartBBox,
- store = chart.store,
- 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,
- 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;
+store.sort();
+
+ *
+ * Registering with StoreManager
+ *
+ * Any Store that is instantiated with a {@link #storeId} will automatically be registed with the {@link Ext.data.StoreManager StoreManager}.
+ * This makes it easy to reuse the same store in multiple views:
+ *
+
+//this store can be used several times
+Ext.create('Ext.data.Store', {
+ model: 'User',
+ storeId: 'usersStore'
+});
- if (!draw) {
- return;
- }
+new Ext.List({
+ store: 'usersStore',
- //get all rendered fields
- chart.series.each(function(series) {
- fields.push(series.yField);
- xField = series.xField;
- });
-
- //get maxValue to interpolate
- store.each(function(record, i) {
- if (aggregate) {
- for (i = 0, nfields = fields.length; i < nfields; i++) {
- maxValue = max(+record.get(fields[i]), maxValue);
- }
- }
- categories.push(record.get(xField));
- });
- 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),
- 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;
- }
+ //other config goes here
});
-/**
- * @author Ed Spencer
- * @class Ext.data.AbstractStore
+
+new Ext.view.View({
+ store: 'usersStore',
+
+ //other config goes here
+});
+
+ *
+ * Further Reading
+ *
+ * Stores are backed up by an ecosystem of classes that enables their operation. To gain a full understanding of these
+ * pieces and how they fit together, see:
+ *
+ *
+ * - {@link Ext.data.proxy.Proxy Proxy} - overview of what Proxies are and how they are used
+ * - {@link Ext.data.Model Model} - the core class in the data package
+ * - {@link Ext.data.reader.Reader Reader} - used by any subclass of {@link Ext.data.proxy.Server ServerProxy} to read a response
+ *
*
- * AbstractStore is a superclass of {@link Ext.data.Store} and {@link Ext.data.TreeStore}. It's never used directly,
- * but offers a set of methods used by both of those subclasses.
- *
- * We've left it here in the docs for reference purposes, but unless you need to make a whole new type of Store, what
- * you're probably looking for is {@link Ext.data.Store}. If you're still interested, here's a brief description of what
- * AbstractStore is and is not.
- *
- * AbstractStore provides the basic configuration for anything that can be considered a Store. It expects to be
- * given a {@link Ext.data.Model Model} that represents the type of data in the Store. It also expects to be given a
- * {@link Ext.data.proxy.Proxy Proxy} that handles the loading of data into the Store.
- *
- * AbstractStore provides a few helpful methods such as {@link #load} and {@link #sync}, which load and save data
- * respectively, passing the requests through the configured {@link #proxy}. Both built-in Store subclasses add extra
- * behavior to each of these functions. Note also that each AbstractStore subclass has its own way of storing data -
- * in {@link Ext.data.Store} the data is saved as a flat {@link Ext.util.MixedCollection MixedCollection}, whereas in
- * {@link Ext.data.TreeStore TreeStore} we use a {@link Ext.data.Tree} to maintain the data's hierarchy.
- *
- * TODO: Update these docs to explain about the sortable and filterable mixins.
- * Finally, AbstractStore provides an API for sorting and filtering data via its {@link #sorters} and {@link #filters}
- * {@link Ext.util.MixedCollection MixedCollections}. Although this functionality is provided by AbstractStore, there's a
- * good description of how to use it in the introduction of {@link Ext.data.Store}.
- *
*/
-Ext.define('Ext.data.AbstractStore', {
- requires: ['Ext.util.MixedCollection', 'Ext.data.Operation', 'Ext.util.Filter'],
-
- mixins: {
- observable: 'Ext.util.Observable',
- sortable: 'Ext.util.Sortable'
- },
-
- statics: {
- create: function(store){
- if (!store.isStore) {
- if (!store.type) {
- store.type = 'store';
- }
- store = Ext.createByAlias('store.' + store.type, store);
- }
- return store;
- }
- },
-
- remoteSort : false,
+Ext.define('Ext.data.Store', {
+ extend: 'Ext.data.AbstractStore',
+
+ alias: 'store.store',
+
+ requires: ['Ext.data.StoreManager', 'Ext.ModelManager', 'Ext.data.Model', 'Ext.util.Grouper'],
+ uses: ['Ext.data.proxy.Memory'],
+
+ /**
+ * @cfg {Boolean} remoteSort
+ * True to defer any sorting operation to the server. If false, sorting is done locally on the client. Defaults to false.
+ */
+ remoteSort: false,
+
+ /**
+ * @cfg {Boolean} remoteFilter
+ * True to defer any filtering operation to the server. If false, filtering is done locally on the client. Defaults to false.
+ */
remoteFilter: false,
+ /**
+ * @cfg {Boolean} remoteGroup
+ * True if the grouping should apply on the server side, false if it is local only. If the
+ * grouping is local, it can be applied immediately to the data. If it is remote, then it will simply act as a
+ * helper, automatically sending the grouping information to the server.
+ */
+ remoteGroup : false,
+
/**
* @cfg {String/Ext.data.proxy.Proxy/Object} proxy The Proxy to use for this Store. This can be either a string, a config
* object or a Proxy instance - see {@link #setProxy} for details.
*/
/**
- * @cfg {Boolean/Object} autoLoad If data is not specified, and if autoLoad is true or an Object, this store's load method
- * is automatically called after creation. If the value of autoLoad is an Object, this Object will be passed to the store's
- * load method. Defaults to false.
+ * @cfg {Object[]/Ext.data.Model[]} data Optional array of Model instances or data objects to load locally. See "Inline data" above for details.
*/
- autoLoad: false,
/**
- * @cfg {Boolean} autoSync True to automatically sync the Store with its Proxy after every edit to one of its Records.
- * Defaults to false.
+ * @property {String} groupField
+ * The field by which to group data in the store. Internally, grouping is very similar to sorting - the
+ * groupField and {@link #groupDir} are injected as the first sorter (see {@link #sort}). Stores support a single
+ * level of grouping, and groups can be fetched via the {@link #getGroups} method.
*/
- autoSync: false,
+ groupField: undefined,
/**
- * Sets the updating behavior based on batch synchronization. 'operation' (the default) will update the Store's
- * internal representation of the data after each operation of the batch has completed, 'complete' will wait until
- * the entire batch has been completed before updating the Store's data. 'complete' is a good choice for local
- * storage proxies, 'operation' is better for remote proxies, where there is a comparatively high latency.
- * @property batchUpdateMode
+ * The direction in which sorting should be applied when grouping. Defaults to "ASC" - the other supported value is "DESC"
+ * @property groupDir
* @type String
*/
- batchUpdateMode: 'operation',
+ groupDir: "ASC",
/**
- * If true, any filters attached to this Store will be run after loading data, before the datachanged event is fired.
- * Defaults to true, ignored if {@link #remoteFilter} is true
- * @property filterOnLoad
- * @type Boolean
+ * @cfg {Number} pageSize
+ * The number of records considered to form a 'page'. This is used to power the built-in
+ * paging using the nextPage and previousPage functions. Defaults to 25.
*/
- filterOnLoad: true,
+ pageSize: 25,
/**
- * If true, any sorters attached to this Store will be run after loading data, before the datachanged event is fired.
- * Defaults to true, igored if {@link #remoteSort} is true
- * @property sortOnLoad
- * @type Boolean
+ * The page that the Store has most recently loaded (see {@link #loadPage})
+ * @property currentPage
+ * @type Number
*/
- sortOnLoad: true,
+ currentPage: 1,
/**
- * True if a model was created implicitly for this Store. This happens if a fields array is passed to the Store's constructor
- * instead of a model constructor or name.
- * @property implicitModel
- * @type Boolean
- * @private
+ * @cfg {Boolean} clearOnPageLoad True to empty the store when loading another page via {@link #loadPage},
+ * {@link #nextPage} or {@link #previousPage}. Setting to false keeps existing records, allowing
+ * large data sets to be loaded one page at a time but rendered all together.
*/
- implicitModel: false,
+ clearOnPageLoad: true,
/**
- * The string type of the Proxy to create if none is specified. This defaults to creating a {@link Ext.data.proxy.Memory memory proxy}.
- * @property defaultProxyType
- * @type String
+ * @property {Boolean} loading
+ * True if the Store is currently loading via its Proxy
+ * @private
*/
- defaultProxyType: 'memory',
+ loading: false,
/**
- * True if the Store has already been destroyed via {@link #destroyStore}. If this is true, the reference to Store should be deleted
- * as it will not function correctly any more.
- * @property isDestroyed
- * @type Boolean
+ * @cfg {Boolean} sortOnFilter For local filtering only, causes {@link #sort} to be called whenever {@link #filter} is called,
+ * causing the sorters to be reapplied after filtering. Defaults to true
*/
- isDestroyed: false,
-
- isStore: true,
+ sortOnFilter: true,
/**
- * @cfg {String} storeId Optional unique identifier for this store. If present, this Store will be registered with
- * the {@link Ext.data.StoreManager}, making it easy to reuse elsewhere. Defaults to undefined.
+ * @cfg {Boolean} buffered
+ * Allow the store to buffer and pre-fetch pages of records. This is to be used in conjunction with a view will
+ * tell the store to pre-fetch records ahead of a time.
*/
-
+ buffered: false,
+
/**
- * @cfg {Array} fields
- * This may be used in place of specifying a {@link #model} configuration. The fields should be a
- * set of {@link Ext.data.Field} configuration objects. The store will automatically create a {@link Ext.data.Model}
- * with these fields. In general this configuration option should be avoided, it exists for the purposes of
- * backwards compatibility. For anything more complicated, such as specifying a particular id property or
- * assocations, a {@link Ext.data.Model} should be defined and specified for the {@link #model} config.
+ * @cfg {Number} purgePageCount
+ * The number of pages to keep in the cache before purging additional records. A value of 0 indicates to never purge the prefetched data.
+ * This option is only relevant when the {@link #buffered} option is set to true.
*/
+ purgePageCount: 5,
- sortRoot: 'data',
-
- //documented above
+ isStore: true,
+
+ onClassExtended: function(cls, data) {
+ var model = data.model;
+
+ if (typeof model == 'string') {
+ var onBeforeClassCreated = data.onBeforeClassCreated;
+
+ data.onBeforeClassCreated = function(cls, data) {
+ var me = this;
+
+ Ext.require(model, function() {
+ onBeforeClassCreated.call(me, cls, data);
+ });
+ };
+ }
+ },
+
+ /**
+ * Creates the store.
+ * @param {Object} config (optional) Config object
+ */
constructor: function(config) {
- var me = this;
-
- me.addEvents(
- /**
- * @event add
- * Fired when a Model instance has been added to this Store
- * @param {Ext.data.Store} store The store
- * @param {Array} records The Model instances that were added
- * @param {Number} index The index at which the instances were inserted
- */
- 'add',
+ // Clone the config so we don't modify the original config object
+ config = Ext.Object.merge({}, config);
- /**
- * @event remove
- * Fired when a Model instance has been removed from this Store
- * @param {Ext.data.Store} store The Store object
- * @param {Ext.data.Model} record The record that was removed
- * @param {Number} index The index of the record that was removed
- */
- 'remove',
-
- /**
- * @event update
- * Fires when a Record has been updated
- * @param {Store} this
- * @param {Ext.data.Model} record The Model instance that was updated
- * @param {String} operation The update operation being performed. Value may be one of:
- *
- Ext.data.Model.EDIT
- Ext.data.Model.REJECT
- Ext.data.Model.COMMIT
- *
- */
- 'update',
+ var me = this,
+ groupers = config.groupers || me.groupers,
+ groupField = config.groupField || me.groupField,
+ proxy,
+ data;
- /**
- * @event datachanged
- * Fires whenever the records in the Store have changed in some way - this could include adding or removing records,
- * or updating the data in existing records
- * @param {Ext.data.Store} this The data store
- */
- 'datachanged',
+ if (config.buffered || me.buffered) {
+ me.prefetchData = Ext.create('Ext.util.MixedCollection', false, function(record) {
+ return record.index;
+ });
+ me.pendingRequests = [];
+ me.pagesRequested = [];
- /**
- * @event beforeload
- * Event description
- * @param {Ext.data.Store} store This Store
- * @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to load the Store
- */
- 'beforeload',
+ me.sortOnLoad = false;
+ me.filterOnLoad = false;
+ }
+ me.addEvents(
/**
- * @event load
- * Fires whenever the store reads data from a remote data source.
+ * @event beforeprefetch
+ * Fires before a prefetch occurs. Return false to cancel.
* @param {Ext.data.Store} this
- * @param {Array} records An array of records
- * @param {Boolean} successful True if the operation was successful.
+ * @param {Ext.data.Operation} operation The associated operation
*/
- 'load',
-
+ 'beforeprefetch',
/**
- * @event beforesync
- * Called before a call to {@link #sync} is executed. Return false from any listener to cancel the synv
- * @param {Object} options Hash of all records to be synchronized, broken down into create, update and destroy
+ * @event groupchange
+ * Fired whenever the grouping in the grid changes
+ * @param {Ext.data.Store} store The store
+ * @param {Ext.util.Grouper[]} groupers The array of grouper objects
*/
- 'beforesync',
+ 'groupchange',
/**
- * @event clear
- * Fired after the {@link #removeAll} method is called.
+ * @event load
+ * Fires whenever records have been prefetched
* @param {Ext.data.Store} this
+ * @param {Ext.util.Grouper[]} records An array of records
+ * @param {Boolean} successful True if the operation was successful.
+ * @param {Ext.data.Operation} operation The associated operation
*/
- 'clear'
+ 'prefetch'
);
-
- Ext.apply(me, config);
-
- /**
- * Temporary cache in which removed model instances are kept until successfully synchronised with a Proxy,
- * at which point this is cleared.
- * @private
- * @property removed
- * @type Array
- */
- me.removed = [];
-
- me.mixins.observable.constructor.apply(me, arguments);
- me.model = Ext.ModelManager.getModel(config.model || me.model);
-
- /**
- * @property modelDefaults
- * @type Object
- * @private
- * A set of default values to be applied to every model instance added via {@link #insert} or created via {@link #create}.
- * This is used internally by associations to set foreign keys and other fields. See the Association classes source code
- * for examples. This should not need to be used by application developers.
- */
- Ext.applyIf(me, {
- modelDefaults: {}
- });
-
- //Supports the 3.x style of simply passing an array of fields to the store, implicitly creating a model
- if (!me.model && me.fields) {
- me.model = Ext.define('Ext.data.Store.ImplicitModel-' + (me.storeId || Ext.id()), {
- extend: 'Ext.data.Model',
- fields: me.fields,
- proxy: me.proxy || me.defaultProxyType
- });
-
- delete me.fields;
-
- me.implicitModel = true;
- }
-
- //ensures that the Proxy is instantiated correctly
- me.setProxy(config.proxy || me.proxy || me.model.getProxy());
-
- if (me.id && !me.storeId) {
- me.storeId = me.id;
- delete me.id;
- }
+ data = config.data || me.data;
- if (me.storeId) {
- Ext.data.StoreManager.register(me);
- }
-
- me.mixins.sortable.initSortable.call(me);
-
/**
- * The collection of {@link Ext.util.Filter Filters} currently applied to this Store
- * @property filters
- * @type Ext.util.MixedCollection
- */
- me.filters = Ext.create('Ext.util.MixedCollection');
- me.filters.addAll(me.decodeFilters(config.filters));
- },
-
- /**
- * Sets the Store's Proxy by string, config object or Proxy instance
- * @param {String|Object|Ext.data.proxy.Proxy} proxy The new Proxy, which can be either a type string, a configuration object
- * or an Ext.data.proxy.Proxy instance
- * @return {Ext.data.proxy.Proxy} The attached Proxy object
- */
- setProxy: function(proxy) {
- var me = this;
-
- if (proxy instanceof Ext.data.proxy.Proxy) {
- proxy.setModel(me.model);
- } else {
- if (Ext.isString(proxy)) {
- proxy = {
- type: proxy
- };
- }
- Ext.applyIf(proxy, {
- model: me.model
- });
-
- proxy = Ext.createByAlias('proxy.' + proxy.type, proxy);
- }
-
- me.proxy = proxy;
-
- return me.proxy;
- },
-
- /**
- * Returns the proxy currently attached to this proxy instance
- * @return {Ext.data.proxy.Proxy} The Proxy instance
- */
- getProxy: function() {
- return this.proxy;
- },
-
- //saves any phantom records
- create: function(data, options) {
- var me = this,
- instance = Ext.ModelManager.create(Ext.applyIf(data, me.modelDefaults), me.model.modelName),
- operation;
-
- options = options || {};
-
- Ext.applyIf(options, {
- action : 'create',
- records: [instance]
+ * The MixedCollection that holds this store's local cache of records
+ * @property data
+ * @type Ext.util.MixedCollection
+ */
+ me.data = Ext.create('Ext.util.MixedCollection', false, function(record) {
+ return record.internalId;
});
- operation = Ext.create('Ext.data.Operation', options);
+ if (data) {
+ me.inlineData = data;
+ delete config.data;
+ }
- me.proxy.create(operation, me.onProxyWrite, me);
-
- return instance;
- },
+ if (!groupers && groupField) {
+ groupers = [{
+ property : groupField,
+ direction: config.groupDir || me.groupDir
+ }];
+ }
+ delete config.groupers;
- read: function() {
- return this.load.apply(this, arguments);
- },
+ /**
+ * The collection of {@link Ext.util.Grouper Groupers} currently applied to this Store
+ * @property groupers
+ * @type Ext.util.MixedCollection
+ */
+ me.groupers = Ext.create('Ext.util.MixedCollection');
+ me.groupers.addAll(me.decodeGroupers(groupers));
- onProxyRead: Ext.emptyFn,
+ this.callParent([config]);
+ // don't use *config* anymore from here on... use *me* instead...
- update: function(options) {
- var me = this,
- operation;
- options = options || {};
+ if (me.groupers.items.length) {
+ me.sort(me.groupers.items, 'prepend', false);
+ }
- Ext.applyIf(options, {
- action : 'update',
- records: me.getUpdatedRecords()
- });
+ proxy = me.proxy;
+ data = me.inlineData;
- operation = Ext.create('Ext.data.Operation', options);
+ if (data) {
+ if (proxy instanceof Ext.data.proxy.Memory) {
+ proxy.data = data;
+ me.read();
+ } else {
+ me.add.apply(me, data);
+ }
- return me.proxy.update(operation, me.onProxyWrite, me);
+ me.sort();
+ delete me.inlineData;
+ } else if (me.autoLoad) {
+ Ext.defer(me.load, 10, me, [typeof me.autoLoad === 'object' ? me.autoLoad: undefined]);
+ // Remove the defer call, we may need reinstate this at some point, but currently it's not obvious why it's here.
+ // this.load(typeof this.autoLoad == 'object' ? this.autoLoad : undefined);
+ }
+ },
+
+ onBeforeSort: function() {
+ var groupers = this.groupers;
+ if (groupers.getCount() > 0) {
+ this.sort(groupers.items, 'prepend', false);
+ }
},
/**
* @private
- * Callback for any write Operation over the Proxy. Updates the Store's MixedCollection to reflect
- * the updates provided by the Proxy
+ * Normalizes an array of grouper objects, ensuring that they are all Ext.util.Grouper instances
+ * @param {Object[]} groupers The groupers array
+ * @return {Ext.util.Grouper[]} Array of Ext.util.Grouper objects
*/
- onProxyWrite: function(operation) {
- var me = this,
- success = operation.wasSuccessful(),
- records = operation.getRecords();
-
- switch (operation.action) {
- case 'create':
- me.onCreateRecords(records, operation, success);
- break;
- case 'update':
- me.onUpdateRecords(records, operation, success);
- break;
- case 'destroy':
- me.onDestroyRecords(records, operation, success);
- break;
+ decodeGroupers: function(groupers) {
+ if (!Ext.isArray(groupers)) {
+ if (groupers === undefined) {
+ groupers = [];
+ } else {
+ groupers = [groupers];
+ }
}
- if (success) {
- me.fireEvent('write', me, operation);
- me.fireEvent('datachanged', me);
- }
- //this is a callback that would have been passed to the 'create', 'update' or 'destroy' function and is optional
- Ext.callback(operation.callback, operation.scope || me, [records, operation, success]);
- },
+ var length = groupers.length,
+ Grouper = Ext.util.Grouper,
+ config, i;
+ for (i = 0; i < length; i++) {
+ config = groupers[i];
- //tells the attached proxy to destroy the given records
- destroy: function(options) {
- var me = this,
- operation;
-
- options = options || {};
+ if (!(config instanceof Grouper)) {
+ if (Ext.isString(config)) {
+ config = {
+ property: config
+ };
+ }
- Ext.applyIf(options, {
- action : 'destroy',
- records: me.getRemovedRecords()
- });
+ Ext.applyIf(config, {
+ root : 'data',
+ direction: "ASC"
+ });
- operation = Ext.create('Ext.data.Operation', options);
+ //support for 3.x style sorters where a function can be defined as 'fn'
+ if (config.fn) {
+ config.sorterFn = config.fn;
+ }
- return me.proxy.destroy(operation, me.onProxyWrite, me);
- },
+ //support a function to be passed as a sorter definition
+ if (typeof config == 'function') {
+ config = {
+ sorterFn: config
+ };
+ }
- /**
- * @private
- * Attached as the 'operationcomplete' event listener to a proxy's Batch object. By default just calls through
- * to onProxyWrite.
- */
- onBatchOperationComplete: function(batch, operation) {
- return this.onProxyWrite(operation);
+ groupers[i] = new Grouper(config);
+ }
+ }
+
+ return groupers;
},
/**
- * @private
- * Attached as the 'complete' event listener to a proxy's Batch object. Iterates over the batch operations
- * and updates the Store's internal data MixedCollection.
+ * Group data in the store
+ * @param {String/Object[]} groupers Either a string name of one of the fields in this Store's configured {@link Ext.data.Model Model},
+ * or an Array of grouper configurations.
+ * @param {String} direction The overall direction to group the data by. Defaults to "ASC".
*/
- onBatchComplete: function(batch, operation) {
+ group: function(groupers, direction) {
var me = this,
- operations = batch.operations,
- length = operations.length,
- i;
+ hasNew = false,
+ grouper,
+ newGroupers;
- me.suspendEvents();
+ if (Ext.isArray(groupers)) {
+ newGroupers = groupers;
+ } else if (Ext.isObject(groupers)) {
+ newGroupers = [groupers];
+ } else if (Ext.isString(groupers)) {
+ grouper = me.groupers.get(groupers);
- for (i = 0; i < length; i++) {
- me.onProxyWrite(operations[i]);
+ if (!grouper) {
+ grouper = {
+ property : groupers,
+ direction: direction
+ };
+ newGroupers = [grouper];
+ } else if (direction === undefined) {
+ grouper.toggle();
+ } else {
+ grouper.setDirection(direction);
+ }
}
- me.resumeEvents();
-
- me.fireEvent('datachanged', me);
- },
+ if (newGroupers && newGroupers.length) {
+ hasNew = true;
+ newGroupers = me.decodeGroupers(newGroupers);
+ me.groupers.clear();
+ me.groupers.addAll(newGroupers);
+ }
- onBatchException: function(batch, operation) {
- // //decide what to do... could continue with the next operation
- // batch.start();
- //
- // //or retry the last operation
- // batch.retry();
+ if (me.remoteGroup) {
+ me.load({
+ scope: me,
+ callback: me.fireGroupChange
+ });
+ } else {
+ // need to explicitly force a sort if we have groupers
+ me.sort(null, null, null, hasNew);
+ me.fireGroupChange();
+ }
},
/**
- * @private
- * Filter function for new records.
+ * Clear any groupers in the store
*/
- filterNew: function(item) {
- // only want phantom records that are valid
- return item.phantom === true && item.isValid();
+ clearGrouping: function(){
+ var me = this;
+ // Clear any groupers we pushed on to the sorters
+ me.groupers.each(function(grouper){
+ me.sorters.remove(grouper);
+ });
+ me.groupers.clear();
+ if (me.remoteGroup) {
+ me.load({
+ scope: me,
+ callback: me.fireGroupChange
+ });
+ } else {
+ me.sort();
+ me.fireEvent('groupchange', me, me.groupers);
+ }
},
/**
- * Returns all Model instances that are either currently a phantom (e.g. have no id), or have an ID but have not
- * yet been saved on this Store (this happens when adding a non-phantom record from another Store into this one)
- * @return {Array} The Model instances
+ * Checks if the store is currently grouped
+ * @return {Boolean} True if the store is grouped.
*/
- getNewRecords: function() {
- return [];
+ isGrouped: function() {
+ return this.groupers.getCount() > 0;
},
/**
- * Returns all Model instances that have been updated in the Store but not yet synchronized with the Proxy
- * @return {Array} The updated Model instances
+ * Fires the groupchange event. Abstracted out so we can use it
+ * as a callback
+ * @private
*/
- getUpdatedRecords: function() {
- return [];
+ fireGroupChange: function(){
+ this.fireEvent('groupchange', this, this.groupers);
},
/**
- * @private
- * Filter function for updated records.
- */
- filterUpdated: function(item) {
- // only want dirty records, not phantoms that are valid
- return item.dirty === true && item.phantom !== true && item.isValid();
- },
+ * Returns an array containing the result of applying grouping to the records in this store. See {@link #groupField},
+ * {@link #groupDir} and {@link #getGroupString}. Example for a store containing records with a color field:
+
+var myStore = Ext.create('Ext.data.Store', {
+ groupField: 'color',
+ groupDir : 'DESC'
+});
- //returns any records that have been removed from the store but not yet destroyed on the proxy
- getRemovedRecords: function() {
- return this.removed;
+myStore.getGroups(); //returns:
+[
+ {
+ name: 'yellow',
+ children: [
+ //all records where the color field is 'yellow'
+ ]
},
+ {
+ name: 'red',
+ children: [
+ //all records where the color field is 'red'
+ ]
+ }
+]
+
+ * @param {String} groupName (Optional) Pass in an optional groupName argument to access a specific group as defined by {@link #getGroupString}
+ * @return {Object/Object[]} The grouped data
+ */
+ getGroups: function(requestGroupString) {
+ var records = this.data.items,
+ length = records.length,
+ groups = [],
+ pointers = {},
+ record,
+ groupStr,
+ group,
+ i;
- filter: function(filters, value) {
+ for (i = 0; i < length; i++) {
+ record = records[i];
+ groupStr = this.getGroupString(record);
+ group = pointers[groupStr];
+
+ if (group === undefined) {
+ group = {
+ name: groupStr,
+ children: []
+ };
+
+ groups.push(group);
+ pointers[groupStr] = group;
+ }
+
+ group.children.push(record);
+ }
+ return requestGroupString ? pointers[requestGroupString] : groups;
},
/**
* @private
- * Normalizes an array of filter objects, ensuring that they are all Ext.util.Filter instances
- * @param {Array} filters The filters array
- * @return {Array} Array of Ext.util.Filter objects
+ * For a given set of records and a Grouper, returns an array of arrays - each of which is the set of records
+ * matching a certain group.
*/
- decodeFilters: function(filters) {
- if (!Ext.isArray(filters)) {
- if (filters === undefined) {
- filters = [];
- } else {
- filters = [filters];
- }
- }
-
- var length = filters.length,
- Filter = Ext.util.Filter,
- config, i;
+ getGroupsForGrouper: function(records, grouper) {
+ var length = records.length,
+ groups = [],
+ oldValue,
+ newValue,
+ record,
+ group,
+ i;
for (i = 0; i < length; i++) {
- config = filters[i];
-
- if (!(config instanceof Filter)) {
- Ext.apply(config, {
- root: 'data'
- });
+ record = records[i];
+ newValue = grouper.getGroupString(record);
- //support for 3.x style filters where a function can be defined as 'fn'
- if (config.fn) {
- config.filterFn = config.fn;
- }
+ if (newValue !== oldValue) {
+ group = {
+ name: newValue,
+ grouper: grouper,
+ records: []
+ };
+ groups.push(group);
+ }
- //support a function to be passed as a filter definition
- if (typeof config == 'function') {
- config = {
- filterFn: config
- };
- }
+ group.records.push(record);
- filters[i] = new Filter(config);
- }
+ oldValue = newValue;
}
- return filters;
+ return groups;
},
- clearFilter: function(supressEvent) {
+ /**
+ * @private
+ * This is used recursively to gather the records into the configured Groupers. The data MUST have been sorted for
+ * this to work properly (see {@link #getGroupData} and {@link #getGroupsForGrouper}) Most of the work is done by
+ * {@link #getGroupsForGrouper} - this function largely just handles the recursion.
+ * @param {Ext.data.Model[]} records The set or subset of records to group
+ * @param {Number} grouperIndex The grouper index to retrieve
+ * @return {Object[]} The grouped records
+ */
+ getGroupsForGrouperIndex: function(records, grouperIndex) {
+ var me = this,
+ groupers = me.groupers,
+ grouper = groupers.getAt(grouperIndex),
+ groups = me.getGroupsForGrouper(records, grouper),
+ length = groups.length,
+ i;
- },
+ if (grouperIndex + 1 < groupers.length) {
+ for (i = 0; i < length; i++) {
+ groups[i].children = me.getGroupsForGrouperIndex(groups[i].records, grouperIndex + 1);
+ }
+ }
- isFiltered: function() {
+ for (i = 0; i < length; i++) {
+ groups[i].depth = grouperIndex;
+ }
+ return groups;
},
- filterBy: function(fn, scope) {
+ /**
+ * @private
+ * Returns records grouped by the configured {@link #groupers grouper} configuration. Sample return value (in
+ * this case grouping by genre and then author in a fictional books dataset):
+
+[
+ {
+ name: 'Fantasy',
+ depth: 0,
+ records: [
+ //book1, book2, book3, book4
+ ],
+ children: [
+ {
+ name: 'Rowling',
+ depth: 1,
+ records: [
+ //book1, book2
+ ]
+ },
+ {
+ name: 'Tolkein',
+ depth: 1,
+ records: [
+ //book3, book4
+ ]
+ }
+ ]
+ }
+]
+
+ * @param {Boolean} sort True to call {@link #sort} before finding groups. Sorting is required to make grouping
+ * function correctly so this should only be set to false if the Store is known to already be sorted correctly
+ * (defaults to true)
+ * @return {Object[]} The group data
+ */
+ getGroupData: function(sort) {
+ var me = this;
+ if (sort !== false) {
+ me.sort();
+ }
+ return me.getGroupsForGrouperIndex(me.data.items, 0);
},
-
+
/**
- * Synchronizes the Store with its Proxy. This asks the Proxy to batch together any new, updated
- * and deleted records in the store, updating the Store's internal representation of the records
- * as each operation completes.
+ * Returns the string to group on for a given model instance. The default implementation of this method returns
+ * the model's {@link #groupField}, but this can be overridden to group by an arbitrary string. For example, to
+ * group by the first letter of a model's 'name' field, use the following code:
+
+Ext.create('Ext.data.Store', {
+ groupDir: 'ASC',
+ getGroupString: function(instance) {
+ return instance.get('name')[0];
+ }
+});
+
+ * @param {Ext.data.Model} instance The model instance
+ * @return {String} The string to compare when forming groups
*/
- sync: function() {
- var me = this,
- options = {},
- toCreate = me.getNewRecords(),
- toUpdate = me.getUpdatedRecords(),
- toDestroy = me.getRemovedRecords(),
- needsSync = false;
+ getGroupString: function(instance) {
+ var group = this.groupers.first();
+ if (group) {
+ return instance.get(group.property);
+ }
+ return '';
+ },
+ /**
+ * Inserts Model instances into the Store at the given index and fires the {@link #add} event.
+ * See also {@link #add}
.
+ * @param {Number} index The start index at which to insert the passed Records.
+ * @param {Ext.data.Model[]} records An Array of Ext.data.Model objects to add to the cache.
+ */
+ insert: function(index, records) {
+ var me = this,
+ sync = false,
+ i,
+ record,
+ len;
- if (toCreate.length > 0) {
- options.create = toCreate;
- needsSync = true;
+ records = [].concat(records);
+ for (i = 0, len = records.length; i < len; i++) {
+ record = me.createModel(records[i]);
+ record.set(me.modelDefaults);
+ // reassign the model in the array in case it wasn't created yet
+ records[i] = record;
+
+ me.data.insert(index + i, record);
+ record.join(me);
+
+ sync = sync || record.phantom === true;
}
- if (toUpdate.length > 0) {
- options.update = toUpdate;
- needsSync = true;
+ if (me.snapshot) {
+ me.snapshot.addAll(records);
}
- if (toDestroy.length > 0) {
- options.destroy = toDestroy;
- needsSync = true;
+ me.fireEvent('add', me, records, index);
+ me.fireEvent('datachanged', me);
+ if (me.autoSync && sync) {
+ me.sync();
}
+ },
- if (needsSync && me.fireEvent('beforesync', options) !== false) {
- me.proxy.batch(options, me.getBatchListeners());
+ /**
+ * Adds Model instance to the Store. This method accepts either:
+ *
+ * - An array of Model instances or Model configuration objects.
+ * - Any number of Model instance or Model configuration object arguments.
+ *
+ * The new Model instances will be added at the end of the existing collection.
+ *
+ * Sample usage:
+ *
+ * myStore.add({some: 'data'}, {some: 'other data'});
+ *
+ * @param {Ext.data.Model[]/Ext.data.Model...} model An array of Model instances
+ * or Model configuration objects, or variable number of Model instance or config arguments.
+ * @return {Ext.data.Model[]} The model instances that were added
+ */
+ add: function(records) {
+ //accept both a single-argument array of records, or any number of record arguments
+ if (!Ext.isArray(records)) {
+ records = Array.prototype.slice.apply(arguments);
+ }
+
+ var me = this,
+ i = 0,
+ length = records.length,
+ record;
+
+ for (; i < length; i++) {
+ record = me.createModel(records[i]);
+ // reassign the model in the array in case it wasn't created yet
+ records[i] = record;
}
- },
+ me.insert(me.data.length, records);
+
+ return records;
+ },
/**
+ * Converts a literal to a model, if it's not a model already
* @private
- * Returns an object which is passed in as the listeners argument to proxy.batch inside this.sync.
- * This is broken out into a separate function to allow for customisation of the listeners
- * @return {Object} The listeners object
+ * @param record {Ext.data.Model/Object} The record to create
+ * @return {Ext.data.Model}
*/
- getBatchListeners: function() {
- var me = this,
- listeners = {
- scope: me,
- exception: me.onBatchException
- };
-
- if (me.batchUpdateMode == 'operation') {
- listeners.operationcomplete = me.onBatchOperationComplete;
- } else {
- listeners.complete = me.onBatchComplete;
+ createModel: function(record) {
+ if (!record.isModel) {
+ record = Ext.ModelManager.create(record, this.model);
}
- return listeners;
+ return record;
},
- //deprecated, will be removed in 5.0
- save: function() {
- return this.sync.apply(this, arguments);
+ /**
+ * Calls the specified function for each of the {@link Ext.data.Model Records} in the cache.
+ * @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed as the first parameter.
+ * Returning false aborts and exits the iteration.
+ * @param {Object} scope (optional) The scope (this
reference) in which the function is executed.
+ * Defaults to the current {@link Ext.data.Model Record} in the iteration.
+ */
+ each: function(fn, scope) {
+ this.data.each(fn, scope);
},
/**
- * Loads the Store using its configured {@link #proxy}.
- * @param {Object} options Optional config object. This is passed into the {@link Ext.data.Operation Operation}
- * object that is created and then sent to the proxy's {@link Ext.data.proxy.Proxy#read} function
+ * Removes the given record from the Store, firing the 'remove' event for each instance that is removed, plus a single
+ * 'datachanged' event after removal.
+ * @param {Ext.data.Model/Ext.data.Model[]} records The Ext.data.Model instance or array of instances to remove
*/
- load: function(options) {
+ remove: function(records, /* private */ isMove) {
+ if (!Ext.isArray(records)) {
+ records = [records];
+ }
+
+ /*
+ * Pass the isMove parameter if we know we're going to be re-inserting this record
+ */
+ isMove = isMove === true;
var me = this,
- operation;
+ sync = false,
+ i = 0,
+ length = records.length,
+ isPhantom,
+ index,
+ record;
- options = options || {};
+ for (; i < length; i++) {
+ record = records[i];
+ index = me.data.indexOf(record);
- Ext.applyIf(options, {
- action : 'read',
- filters: me.filters.items,
- sorters: me.getSorters()
- });
-
- operation = Ext.create('Ext.data.Operation', options);
+ if (me.snapshot) {
+ me.snapshot.remove(record);
+ }
- if (me.fireEvent('beforeload', me, operation) !== false) {
- me.loading = true;
- me.proxy.read(operation, me.onProxyLoad, me);
+ if (index > -1) {
+ isPhantom = record.phantom === true;
+ if (!isMove && !isPhantom) {
+ // don't push phantom records onto removed
+ me.removed.push(record);
+ }
+
+ record.unjoin(me);
+ me.data.remove(record);
+ sync = sync || !isPhantom;
+
+ me.fireEvent('remove', me, record, index);
+ }
}
-
- return me;
- },
- /**
- * @private
- * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
- * @param {Ext.data.Model} record The model instance that was edited
- */
- afterEdit : function(record) {
- var me = this;
-
- if (me.autoSync) {
+ me.fireEvent('datachanged', me);
+ if (!isMove && me.autoSync && sync) {
me.sync();
}
-
- me.fireEvent('update', me, record, Ext.data.Model.EDIT);
},
/**
- * @private
- * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to..
- * @param {Ext.data.Model} record The model instance that was edited
+ * Removes the model instance at the given index
+ * @param {Number} index The record index
*/
- afterReject : function(record) {
- this.fireEvent('update', this, record, Ext.data.Model.REJECT);
+ removeAt: function(index) {
+ var record = this.getAt(index);
+
+ if (record) {
+ this.remove(record);
+ }
},
/**
- * @private
- * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
- * @param {Ext.data.Model} record The model instance that was edited
+ * Loads data into the Store via the configured {@link #proxy}. This uses the Proxy to make an
+ * asynchronous call to whatever storage backend the Proxy uses, automatically adding the retrieved
+ * instances into the Store and calling an optional callback if required. Example usage:
+ *
+
+store.load({
+ scope : this,
+ callback: function(records, operation, success) {
+ //the {@link Ext.data.Operation operation} object contains all of the details of the load operation
+ console.log(records);
+ }
+});
+
+ *
+ * If the callback scope does not need to be set, a function can simply be passed:
+ *
+
+store.load(function(records, operation, success) {
+ console.log('loaded records');
+});
+
+ *
+ * @param {Object/Function} options (Optional) config object, passed into the Ext.data.Operation object before loading.
*/
- afterCommit : function(record) {
- this.fireEvent('update', this, record, Ext.data.Model.COMMIT);
- },
-
- clearData: Ext.emptyFn,
-
- destroyStore: function() {
+ load: function(options) {
var me = this;
-
- if (!me.isDestroyed) {
- if (me.storeId) {
- Ext.data.StoreManager.unregister(me);
- }
- me.clearData();
- me.data = null;
- me.tree = null;
- // Ext.destroy(this.proxy);
- me.reader = me.writer = null;
- me.clearListeners();
- me.isDestroyed = true;
- if (me.implicitModel) {
- Ext.destroy(me.model);
- }
- }
- },
-
- doSort: function(sorterFn) {
- var me = this;
- if (me.remoteSort) {
- //the load function will pick up the new sorters and request the sorted data from the proxy
- me.load();
- } else {
- me.data.sortBy(sorterFn);
- me.fireEvent('datachanged', me);
+ options = options || {};
+
+ if (Ext.isFunction(options)) {
+ options = {
+ callback: options
+ };
}
- },
- getCount: Ext.emptyFn,
+ Ext.applyIf(options, {
+ groupers: me.groupers.items,
+ page: me.currentPage,
+ start: (me.currentPage - 1) * me.pageSize,
+ limit: me.pageSize,
+ addRecords: false
+ });
- getById: Ext.emptyFn,
-
- /**
- * Removes all records from the store. This method does a "fast remove",
- * individual remove events are not called. The {@link #clear} event is
- * fired upon completion.
- */
- removeAll: Ext.emptyFn,
- // individual substores should implement a "fast" remove
- // and fire a clear event afterwards
+ return me.callParent([options]);
+ },
/**
- * Returns true if the Store is currently performing a load operation
- * @return {Boolean} True if the Store is currently loading
+ * @private
+ * Called internally when a Proxy has completed a load request
*/
- isLoading: function() {
- return this.loading;
- }
-});
+ onProxyLoad: function(operation) {
+ var me = this,
+ resultSet = operation.getResultSet(),
+ records = operation.getRecords(),
+ successful = operation.wasSuccessful();
-/**
- * @class Ext.util.Grouper
- * @extends Ext.util.Sorter
- */
-
-Ext.define('Ext.util.Grouper', {
+ if (resultSet) {
+ me.totalCount = resultSet.total;
+ }
- /* Begin Definitions */
+ if (successful) {
+ me.loadRecords(records, operation);
+ }
- extend: 'Ext.util.Sorter',
+ me.loading = false;
+ me.fireEvent('load', me, records, successful);
- /* End Definitions */
+ //TODO: deprecate this event, it should always have been 'load' instead. 'load' is now documented, 'read' is not.
+ //People are definitely using this so can't deprecate safely until 2.x
+ me.fireEvent('read', me, records, operation.wasSuccessful());
+
+ //this is a callback that would have been passed to the 'read' function and is optional
+ Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
+ },
/**
- * Function description
- * @param {Ext.data.Model} instance The Model instance
- * @return {String} The group string for this model
+ * Create any new records when a write is returned from the server.
+ * @private
+ * @param {Ext.data.Model[]} records The array of new records
+ * @param {Ext.data.Operation} operation The operation that just completed
+ * @param {Boolean} success True if the operation was successful
*/
- getGroupString: function(instance) {
- return instance.get(this.property);
- }
-});
-/**
- * @author Ed Spencer
- * @class Ext.data.Store
- * @extends Ext.data.AbstractStore
- *
- * The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects. Stores load
- * data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions for {@link #sort sorting},
- * {@link #filter filtering} and querying the {@link Ext.data.Model model} instances contained within it.
- *
- * Creating a Store is easy - we just tell it the Model and the Proxy to use to load and save its data:
- *
-
-// Set up a {@link Ext.data.Model model} to use in our Store
-Ext.define('User', {
- extend: 'Ext.data.Model',
- fields: [
- {name: 'firstName', type: 'string'},
- {name: 'lastName', type: 'string'},
- {name: 'age', type: 'int'},
- {name: 'eyeColor', type: 'string'}
- ]
-});
+ onCreateRecords: function(records, operation, success) {
+ if (success) {
+ var i = 0,
+ data = this.data,
+ snapshot = this.snapshot,
+ length = records.length,
+ originalRecords = operation.records,
+ record,
+ original,
+ index;
-var myStore = new Ext.data.Store({
- model: 'User',
- proxy: {
- type: 'ajax',
- url : '/users.json',
- reader: {
- type: 'json',
- root: 'users'
+ /*
+ * Loop over each record returned from the server. Assume they are
+ * returned in order of how they were sent. If we find a matching
+ * record, replace it with the newly created one.
+ */
+ for (; i < length; ++i) {
+ record = records[i];
+ original = originalRecords[i];
+ if (original) {
+ index = data.indexOf(original);
+ if (index > -1) {
+ data.removeAt(index);
+ data.insert(index, record);
+ }
+ if (snapshot) {
+ index = snapshot.indexOf(original);
+ if (index > -1) {
+ snapshot.removeAt(index);
+ snapshot.insert(index, record);
+ }
+ }
+ record.phantom = false;
+ record.join(this);
+ }
+ }
}
},
- autoLoad: true
-});
-
- * In the example above we configured an AJAX proxy to load data from the url '/users.json'. We told our Proxy
- * to use a {@link Ext.data.reader.Json JsonReader} to parse the response from the server into Model object -
- * {@link Ext.data.reader.Json see the docs on JsonReader} for details.
- *
- * Inline data
- *
- * Stores can also load data inline. Internally, Store converts each of the objects we pass in as {@link #data}
- * into Model instances:
- *
-
-new Ext.data.Store({
- model: 'User',
- data : [
- {firstName: 'Ed', lastName: 'Spencer'},
- {firstName: 'Tommy', lastName: 'Maintz'},
- {firstName: 'Aaron', lastName: 'Conran'},
- {firstName: 'Jamie', lastName: 'Avins'}
- ]
-});
-
- *
- * Loading inline data using the method above is great if the data is in the correct format already (e.g. it doesn't need
- * to be processed by a {@link Ext.data.reader.Reader reader}). If your inline data requires processing to decode the data structure,
- * use a {@link Ext.data.proxy.Memory MemoryProxy} instead (see the {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).
- *
- * Additional data can also be loaded locally using {@link #add}.
- *
- * Loading Nested Data
- *
- * Applications often need to load sets of associated data - for example a CRM system might load a User and her Orders.
- * Instead of issuing an AJAX request for the User and a series of additional AJAX requests for each Order, we can load a nested dataset
- * and allow the Reader to automatically populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader} intro
- * docs for a full explanation:
- *
-
-var store = new Ext.data.Store({
- autoLoad: true,
- model: "User",
- proxy: {
- type: 'ajax',
- url : 'users.json',
- reader: {
- type: 'json',
- root: 'users'
- }
- }
-});
-
- *
- * Which would consume a response like this:
- *
-
-{
- "users": [
- {
- "id": 1,
- "name": "Ed",
- "orders": [
- {
- "id": 10,
- "total": 10.76,
- "status": "invoiced"
- },
- {
- "id": 11,
- "total": 13.45,
- "status": "shipped"
+ /**
+ * Update any records when a write is returned from the server.
+ * @private
+ * @param {Ext.data.Model[]} records The array of updated records
+ * @param {Ext.data.Operation} operation The operation that just completed
+ * @param {Boolean} success True if the operation was successful
+ */
+ onUpdateRecords: function(records, operation, success){
+ if (success) {
+ var i = 0,
+ length = records.length,
+ data = this.data,
+ snapshot = this.snapshot,
+ record;
+
+ for (; i < length; ++i) {
+ record = records[i];
+ data.replace(record);
+ if (snapshot) {
+ snapshot.replace(record);
}
- ]
- }
- ]
-}
-
- *
- * See the {@link Ext.data.reader.Reader} intro docs for a full explanation.
- *
- * Filtering and Sorting
- *
- * Stores can be sorted and filtered - in both cases either remotely or locally. The {@link #sorters} and {@link #filters} are
- * held inside {@link Ext.util.MixedCollection MixedCollection} instances to make them easy to manage. Usually it is sufficient to
- * either just specify sorters and filters in the Store configuration or call {@link #sort} or {@link #filter}:
- *
-
-var store = new Ext.data.Store({
- model: 'User',
- sorters: [
- {
- property : 'age',
- direction: 'DESC'
- },
- {
- property : 'firstName',
- direction: 'ASC'
+ record.join(this);
+ }
}
- ],
+ },
- filters: [
- {
- property: 'firstName',
- value : /Ed/
+ /**
+ * Remove any records when a write is returned from the server.
+ * @private
+ * @param {Ext.data.Model[]} records The array of removed records
+ * @param {Ext.data.Operation} operation The operation that just completed
+ * @param {Boolean} success True if the operation was successful
+ */
+ onDestroyRecords: function(records, operation, success){
+ if (success) {
+ var me = this,
+ i = 0,
+ length = records.length,
+ data = me.data,
+ snapshot = me.snapshot,
+ record;
+
+ for (; i < length; ++i) {
+ record = records[i];
+ record.unjoin(me);
+ data.remove(record);
+ if (snapshot) {
+ snapshot.remove(record);
+ }
+ }
+ me.removed = [];
}
- ]
-});
-
- *
- * The new Store will keep the configured sorters and filters in the MixedCollection instances mentioned above. By default, sorting
- * and filtering are both performed locally by the Store - see {@link #remoteSort} and {@link #remoteFilter} to allow the server to
- * perform these operations instead.
- *
- * Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter} adds another filter to the Store
- * and automatically filters the dataset (calling {@link #filter} with no arguments simply re-applies all existing filters). Note that by
- * default {@link #sortOnFilter} is set to true, which means that your sorters are automatically reapplied if using local sorting.
- *
-
-store.filter('eyeColor', 'Brown');
-
- *
- * Change the sorting at any time by calling {@link #sort}:
- *
-
-store.sort('height', 'ASC');
-
- *
- * Note that all existing sorters will be removed in favor of the new sorter data (if {@link #sort} is called with no arguments,
- * the existing sorters are just reapplied instead of being removed). To keep existing sorters and add new ones, just add them
- * to the MixedCollection:
- *
-
-store.sorters.add(new Ext.util.Sorter({
- property : 'shoeSize',
- direction: 'ASC'
-}));
+ },
-store.sort();
-
- *
- * Registering with StoreManager
- *
- * Any Store that is instantiated with a {@link #storeId} will automatically be registed with the {@link Ext.data.StoreManager StoreManager}.
- * This makes it easy to reuse the same store in multiple views:
- *
-
-//this store can be used several times
-new Ext.data.Store({
- model: 'User',
- storeId: 'usersStore'
-});
+ //inherit docs
+ getNewRecords: function() {
+ return this.data.filterBy(this.filterNew).items;
+ },
-new Ext.List({
- store: 'usersStore',
+ //inherit docs
+ getUpdatedRecords: function() {
+ return this.data.filterBy(this.filterUpdated).items;
+ },
- //other config goes here
-});
+ /**
+ * Filters the loaded set of records by a given set of filters.
+ *
+ * Filtering by single field:
+ *
+ * store.filter("email", /\.com$/);
+ *
+ * Using multiple filters:
+ *
+ * store.filter([
+ * {property: "email", value: /\.com$/},
+ * {filterFn: function(item) { return item.get("age") > 10; }}
+ * ]);
+ *
+ * Using Ext.util.Filter instances instead of config objects
+ * (note that we need to specify the {@link Ext.util.Filter#root root} config option in this case):
+ *
+ * store.filter([
+ * Ext.create('Ext.util.Filter', {property: "email", value: /\.com$/, root: 'data'}),
+ * Ext.create('Ext.util.Filter', {filterFn: function(item) { return item.get("age") > 10; }, root: 'data'})
+ * ]);
+ *
+ * @param {Object[]/Ext.util.Filter[]/String} filters The set of filters to apply to the data. These are stored internally on the store,
+ * but the filtering itself is done on the Store's {@link Ext.util.MixedCollection MixedCollection}. See
+ * MixedCollection's {@link Ext.util.MixedCollection#filter filter} method for filter syntax. Alternatively,
+ * pass in a property string
+ * @param {String} value (optional) value to filter by (only if using a property string as the first argument)
+ */
+ filter: function(filters, value) {
+ if (Ext.isString(filters)) {
+ filters = {
+ property: filters,
+ value: value
+ };
+ }
-new Ext.view.View({
- store: 'usersStore',
+ var me = this,
+ decoded = me.decodeFilters(filters),
+ i = 0,
+ doLocalSort = me.sortOnFilter && !me.remoteSort,
+ length = decoded.length;
- //other config goes here
-});
-
- *
- * Further Reading
- *
- * Stores are backed up by an ecosystem of classes that enables their operation. To gain a full understanding of these
- * pieces and how they fit together, see:
- *
- *
- * - {@link Ext.data.proxy.Proxy Proxy} - overview of what Proxies are and how they are used
- * - {@link Ext.data.Model Model} - the core class in the data package
- * - {@link Ext.data.reader.Reader Reader} - used by any subclass of {@link Ext.data.proxy.Server ServerProxy} to read a response
- *
- *
- * @constructor
- * @param {Object} config Optional config object
- */
-Ext.define('Ext.data.Store', {
- extend: 'Ext.data.AbstractStore',
+ for (; i < length; i++) {
+ me.filters.replace(decoded[i]);
+ }
- alias: 'store.store',
+ if (me.remoteFilter) {
+ //the load function will pick up the new filters and request the filtered data from the proxy
+ me.load();
+ } else {
+ /**
+ * A pristine (unfiltered) collection of the records in this store. This is used to reinstate
+ * records when a filter is removed or changed
+ * @property snapshot
+ * @type Ext.util.MixedCollection
+ */
+ if (me.filters.getCount()) {
+ me.snapshot = me.snapshot || me.data.clone();
+ me.data = me.data.filter(me.filters.items);
- requires: ['Ext.ModelManager', 'Ext.data.Model', 'Ext.util.Grouper'],
- uses: ['Ext.data.proxy.Memory'],
+ if (doLocalSort) {
+ me.sort();
+ }
+ // fire datachanged event if it hasn't already been fired by doSort
+ if (!doLocalSort || me.sorters.length < 1) {
+ me.fireEvent('datachanged', me);
+ }
+ }
+ }
+ },
/**
- * @cfg {Boolean} remoteSort
- * True to defer any sorting operation to the server. If false, sorting is done locally on the client. Defaults to false.
+ * Revert to a view of the Record cache with no filtering applied.
+ * @param {Boolean} suppressEvent If true the filter is cleared silently without firing the
+ * {@link #datachanged} event.
*/
- remoteSort: false,
+ clearFilter: function(suppressEvent) {
+ var me = this;
- /**
- * @cfg {Boolean} remoteFilter
- * True to defer any filtering operation to the server. If false, filtering is done locally on the client. Defaults to false.
- */
- remoteFilter: false,
-
- /**
- * @cfg {Boolean} remoteGroup
- * True if the grouping should apply on the server side, false if it is local only (defaults to false). If the
- * grouping is local, it can be applied immediately to the data. If it is remote, then it will simply act as a
- * helper, automatically sending the grouping information to the server.
- */
- remoteGroup : false,
+ me.filters.clear();
- /**
- * @cfg {String/Ext.data.proxy.Proxy/Object} proxy The Proxy to use for this Store. This can be either a string, a config
- * object or a Proxy instance - see {@link #setProxy} for details.
- */
+ if (me.remoteFilter) {
+ me.load();
+ } else if (me.isFiltered()) {
+ me.data = me.snapshot.clone();
+ delete me.snapshot;
- /**
- * @cfg {Array} data Optional array of Model instances or data objects to load locally. See "Inline data" above for details.
- */
+ if (suppressEvent !== true) {
+ me.fireEvent('datachanged', me);
+ }
+ }
+ },
/**
- * @cfg {String} model The {@link Ext.data.Model} associated with this store
+ * Returns true if this store is currently filtered
+ * @return {Boolean}
*/
+ isFiltered: function() {
+ var snapshot = this.snapshot;
+ return !! snapshot && snapshot !== this.data;
+ },
/**
- * The (optional) field by which to group data in the store. Internally, grouping is very similar to sorting - the
- * groupField and {@link #groupDir} are injected as the first sorter (see {@link #sort}). Stores support a single
- * level of grouping, and groups can be fetched via the {@link #getGroups} method.
- * @property groupField
- * @type String
+ * Filter by a function. The specified function will be called for each
+ * Record in this Store. If the function returns true the Record is included,
+ * otherwise it is filtered out.
+ * @param {Function} fn The function to be called. It will be passed the following parameters:
+ * @param {Object} scope (optional) The scope (this
reference) in which the function is executed. Defaults to this Store.
*/
- groupField: undefined,
+ filterBy: function(fn, scope) {
+ var me = this;
- /**
- * The direction in which sorting should be applied when grouping. Defaults to "ASC" - the other supported value is "DESC"
- * @property groupDir
- * @type String
- */
- groupDir: "ASC",
+ me.snapshot = me.snapshot || me.data.clone();
+ me.data = me.queryBy(fn, scope || me);
+ me.fireEvent('datachanged', me);
+ },
/**
- * The number of records considered to form a 'page'. This is used to power the built-in
- * paging using the nextPage and previousPage functions. Defaults to 25.
- * @property pageSize
- * @type Number
- */
- pageSize: 25,
+ * Query the cached records in this Store using a filtering function. The specified function
+ * will be called with each record in this Store. If the function returns true the record is
+ * included in the results.
+ * @param {Function} fn The function to be called. It will be passed the following parameters:
+ * @param {Object} scope (optional) The scope (this
reference) in which the function is executed. Defaults to this Store.
+ * @return {Ext.util.MixedCollection} Returns an Ext.util.MixedCollection of the matched records
+ **/
+ queryBy: function(fn, scope) {
+ var me = this,
+ data = me.snapshot || me.data;
+ return data.filterBy(fn, scope || me);
+ },
/**
- * The page that the Store has most recently loaded (see {@link #loadPage})
- * @property currentPage
- * @type Number
+ * Loads an array of data straight into the Store.
+ *
+ * Using this method is great if the data is in the correct format already (e.g. it doesn't need to be
+ * processed by a reader). If your data requires processing to decode the data structure, use a
+ * {@link Ext.data.proxy.Memory MemoryProxy} instead.
+ *
+ * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances will be cast
+ * into model instances.
+ * @param {Boolean} [append=false] True to add the records to the existing records in the store, false
+ * to remove the old ones first.
*/
- currentPage: 1,
+ loadData: function(data, append) {
+ var model = this.model,
+ length = data.length,
+ newData = [],
+ i,
+ record;
- /**
- * @cfg {Boolean} clearOnPageLoad True to empty the store when loading another page via {@link #loadPage},
- * {@link #nextPage} or {@link #previousPage} (defaults to true). Setting to false keeps existing records, allowing
- * large data sets to be loaded one page at a time but rendered all together.
- */
- clearOnPageLoad: true,
+ //make sure each data element is an Ext.data.Model instance
+ for (i = 0; i < length; i++) {
+ record = data[i];
+
+ if (!(record instanceof Ext.data.Model)) {
+ record = Ext.ModelManager.create(record, model);
+ }
+ newData.push(record);
+ }
+
+ this.loadRecords(newData, {addRecords: append});
+ },
- /**
- * True if the Store is currently loading via its Proxy
- * @property loading
- * @type Boolean
- * @private
- */
- loading: false,
/**
- * @cfg {Boolean} sortOnFilter For local filtering only, causes {@link #sort} to be called whenever {@link #filter} is called,
- * causing the sorters to be reapplied after filtering. Defaults to true
- */
- sortOnFilter: true,
-
- /**
- * @cfg {Boolean} buffered
- * Allow the store to buffer and pre-fetch pages of records. This is to be used in conjunction with a view will
- * tell the store to pre-fetch records ahead of a time.
- */
- buffered: false,
-
+ * Loads data via the bound Proxy's reader
+ *
+ * Use this method if you are attempting to load data and want to utilize the configured data reader.
+ *
+ * @param {Object[]} data The full JSON object you'd like to load into the Data store.
+ * @param {Boolean} [append=false] True to add the records to the existing records in the store, false
+ * to remove the old ones first.
+ */
+ loadRawData : function(data, append) {
+ var me = this,
+ result = me.proxy.reader.read(data),
+ records = result.records;
+
+ if (result.success) {
+ me.loadRecords(records, { addRecords: append });
+ me.fireEvent('load', me, records, true);
+ }
+ },
+
+
/**
- * @cfg {Number} purgePageCount
- * The number of pages to keep in the cache before purging additional records. A value of 0 indicates to never purge the prefetched data.
- * This option is only relevant when the {@link #buffered} option is set to true.
+ * Loads an array of {@link Ext.data.Model model} instances into the store, fires the datachanged event. This should only usually
+ * be called internally when loading from the {@link Ext.data.proxy.Proxy Proxy}, when adding records manually use {@link #add} instead
+ * @param {Ext.data.Model[]} records The array of records to load
+ * @param {Object} options {addRecords: true} to add these records to the existing records, false to remove the Store's existing records first
*/
- purgePageCount: 5,
+ loadRecords: function(records, options) {
+ var me = this,
+ i = 0,
+ length = records.length;
- isStore: true,
+ options = options || {};
- //documented above
- constructor: function(config) {
- config = config || {};
- var me = this,
- groupers = config.groupers,
- proxy,
- data;
-
- if (config.buffered || me.buffered) {
- me.prefetchData = Ext.create('Ext.util.MixedCollection', false, function(record) {
- return record.index;
- });
- me.pendingRequests = [];
- me.pagesRequested = [];
-
- me.sortOnLoad = false;
- me.filterOnLoad = false;
+ if (!options.addRecords) {
+ delete me.snapshot;
+ me.clearData();
}
-
- me.addEvents(
- /**
- * @event beforeprefetch
- * Fires before a prefetch occurs. Return false to cancel.
- * @param {Ext.data.store} this
- * @param {Ext.data.Operation} operation The associated operation
- */
- 'beforeprefetch',
- /**
- * @event groupchange
- * Fired whenever the grouping in the grid changes
- * @param {Ext.data.Store} store The store
- * @param {Array} groupers The array of grouper objects
- */
- 'groupchange',
- /**
- * @event load
- * Fires whenever records have been prefetched
- * @param {Ext.data.store} this
- * @param {Array} records An array of records
- * @param {Boolean} successful True if the operation was successful.
- * @param {Ext.data.Operation} operation The associated operation
- */
- 'prefetch'
- );
- data = config.data || me.data;
- /**
- * The MixedCollection that holds this store's local cache of records
- * @property data
- * @type Ext.util.MixedCollection
- */
- me.data = Ext.create('Ext.util.MixedCollection', false, function(record) {
- return record.internalId;
- });
+ me.data.addAll(records);
- if (data) {
- me.inlineData = data;
- delete config.data;
- }
-
- if (!groupers && config.groupField) {
- groupers = [{
- property : config.groupField,
- direction: config.groupDir
- }];
- }
- delete config.groupers;
-
- /**
- * The collection of {@link Ext.util.Grouper Groupers} currently applied to this Store
- * @property groupers
- * @type Ext.util.MixedCollection
- */
- me.groupers = Ext.create('Ext.util.MixedCollection');
- me.groupers.addAll(me.decodeGroupers(groupers));
+ //FIXME: this is not a good solution. Ed Spencer is totally responsible for this and should be forced to fix it immediately.
+ for (; i < length; i++) {
+ if (options.start !== undefined) {
+ records[i].index = options.start + i;
- this.callParent([config]);
-
- if (me.groupers.items.length) {
- me.sort(me.groupers.items, 'prepend', false);
+ }
+ records[i].join(me);
}
- proxy = me.proxy;
- data = me.inlineData;
+ /*
+ * this rather inelegant suspension and resumption of events is required because both the filter and sort functions
+ * fire an additional datachanged event, which is not wanted. Ideally we would do this a different way. The first
+ * datachanged event is fired by the call to this.add, above.
+ */
+ me.suspendEvents();
- if (data) {
- if (proxy instanceof Ext.data.proxy.Memory) {
- proxy.data = data;
- me.read();
- } else {
- me.add.apply(me, data);
- }
+ if (me.filterOnLoad && !me.remoteFilter) {
+ me.filter();
+ }
+ if (me.sortOnLoad && !me.remoteSort) {
me.sort();
- delete me.inlineData;
- } else if (me.autoLoad) {
- Ext.defer(me.load, 10, me, [typeof me.autoLoad === 'object' ? me.autoLoad: undefined]);
- // Remove the defer call, we may need reinstate this at some point, but currently it's not obvious why it's here.
- // this.load(typeof this.autoLoad == 'object' ? this.autoLoad : undefined);
}
+
+ me.resumeEvents();
+ me.fireEvent('datachanged', me, records);
},
-
- onBeforeSort: function() {
- this.sort(this.groupers.items, 'prepend', false);
- },
-
+
+ // PAGING METHODS
/**
- * @private
- * Normalizes an array of grouper objects, ensuring that they are all Ext.util.Grouper instances
- * @param {Array} groupers The groupers array
- * @return {Array} Array of Ext.util.Grouper objects
+ * Loads a given 'page' of data by setting the start and limit values appropriately. Internally this just causes a normal
+ * load operation, passing in calculated 'start' and 'limit' params
+ * @param {Number} page The number of the page to load
+ * @param {Object} options See options for {@link #load}
*/
- decodeGroupers: function(groupers) {
- if (!Ext.isArray(groupers)) {
- if (groupers === undefined) {
- groupers = [];
- } else {
- groupers = [groupers];
- }
- }
-
- var length = groupers.length,
- Grouper = Ext.util.Grouper,
- config, i;
+ loadPage: function(page, options) {
+ var me = this;
+ options = Ext.apply({}, options);
- for (i = 0; i < length; i++) {
- config = groupers[i];
+ me.currentPage = page;
- if (!(config instanceof Grouper)) {
- if (Ext.isString(config)) {
- config = {
- property: config
- };
- }
-
- Ext.applyIf(config, {
- root : 'data',
- direction: "ASC"
- });
+ me.read(Ext.applyIf(options, {
+ page: page,
+ start: (page - 1) * me.pageSize,
+ limit: me.pageSize,
+ addRecords: !me.clearOnPageLoad
+ }));
+ },
- //support for 3.x style sorters where a function can be defined as 'fn'
- if (config.fn) {
- config.sorterFn = config.fn;
- }
+ /**
+ * Loads the next 'page' in the current data set
+ * @param {Object} options See options for {@link #load}
+ */
+ nextPage: function(options) {
+ this.loadPage(this.currentPage + 1, options);
+ },
- //support a function to be passed as a sorter definition
- if (typeof config == 'function') {
- config = {
- sorterFn: config
- };
- }
+ /**
+ * Loads the previous 'page' in the current data set
+ * @param {Object} options See options for {@link #load}
+ */
+ previousPage: function(options) {
+ this.loadPage(this.currentPage - 1, options);
+ },
- groupers[i] = new Grouper(config);
- }
- }
+ // private
+ clearData: function() {
+ var me = this;
+ me.data.each(function(record) {
+ record.unjoin(me);
+ });
- return groupers;
+ me.data.clear();
},
-
+
+ // Buffering
/**
- * Group data in the store
- * @param {String|Array} groupers Either a string name of one of the fields in this Store's configured {@link Ext.data.Model Model},
- * or an Array of grouper configurations.
- * @param {String} direction The overall direction to group the data by. Defaults to "ASC".
+ * Prefetches data into the store using its configured {@link #proxy}.
+ * @param {Object} options (Optional) config object, passed into the Ext.data.Operation object before loading.
+ * See {@link #load}
*/
- group: function(groupers, direction) {
+ prefetch: function(options) {
var me = this,
- grouper,
- newGroupers;
-
- if (Ext.isArray(groupers)) {
- newGroupers = groupers;
- } else if (Ext.isObject(groupers)) {
- newGroupers = [groupers];
- } else if (Ext.isString(groupers)) {
- grouper = me.groupers.get(groupers);
+ operation,
+ requestId = me.getRequestId();
- if (!grouper) {
- grouper = {
- property : groupers,
- direction: direction
- };
- newGroupers = [grouper];
- } else if (direction === undefined) {
- grouper.toggle();
- } else {
- grouper.setDirection(direction);
- }
- }
-
- if (newGroupers && newGroupers.length) {
- newGroupers = me.decodeGroupers(newGroupers);
- me.groupers.clear();
- me.groupers.addAll(newGroupers);
- }
-
- if (me.remoteGroup) {
- me.load({
- scope: me,
- callback: me.fireGroupChange
- });
- } else {
- me.sort();
- me.fireEvent('groupchange', me, me.groupers);
- }
- },
-
- /**
- * Clear any groupers in the store
- */
- clearGrouping: function(){
- var me = this;
- // Clear any groupers we pushed on to the sorters
- me.groupers.each(function(grouper){
- me.sorters.remove(grouper);
+ options = options || {};
+
+ Ext.applyIf(options, {
+ action : 'read',
+ filters: me.filters.items,
+ sorters: me.sorters.items,
+ requestId: requestId
});
- me.groupers.clear();
- if (me.remoteGroup) {
- me.load({
- scope: me,
- callback: me.fireGroupChange
- });
- } else {
- me.sort();
- me.fireEvent('groupchange', me, me.groupers);
+ me.pendingRequests.push(requestId);
+
+ operation = Ext.create('Ext.data.Operation', options);
+
+ // HACK to implement loadMask support.
+ //if (operation.blocking) {
+ // me.fireEvent('beforeload', me, operation);
+ //}
+ if (me.fireEvent('beforeprefetch', me, operation) !== false) {
+ me.loading = true;
+ me.proxy.read(operation, me.onProxyPrefetch, me);
}
+
+ return me;
},
-
+
/**
- * Checks if the store is currently grouped
- * @return {Boolean} True if the store is grouped.
+ * Prefetches a page of data.
+ * @param {Number} page The page to prefetch
+ * @param {Object} options (Optional) config object, passed into the Ext.data.Operation object before loading.
+ * See {@link #load}
*/
- isGrouped: function() {
- return this.groupers.getCount() > 0;
+ prefetchPage: function(page, options) {
+ var me = this,
+ pageSize = me.pageSize,
+ start = (page - 1) * me.pageSize,
+ end = start + pageSize;
+
+ // Currently not requesting this page and range isn't already satisified
+ if (Ext.Array.indexOf(me.pagesRequested, page) === -1 && !me.rangeSatisfied(start, end)) {
+ options = options || {};
+ me.pagesRequested.push(page);
+ Ext.applyIf(options, {
+ page : page,
+ start: start,
+ limit: pageSize,
+ callback: me.onWaitForGuarantee,
+ scope: me
+ });
+
+ me.prefetch(options);
+ }
+
},
-
+
/**
- * Fires the groupchange event. Abstracted out so we can use it
- * as a callback
+ * Returns a unique requestId to track requests.
* @private
*/
- fireGroupChange: function(){
- this.fireEvent('groupchange', this, this.groupers);
+ getRequestId: function() {
+ this.requestSeed = this.requestSeed || 1;
+ return this.requestSeed++;
},
/**
- * Returns an object containing the result of applying grouping to the records in this store. See {@link #groupField},
- * {@link #groupDir} and {@link #getGroupString}. Example for a store containing records with a color field:
-
-var myStore = new Ext.data.Store({
- groupField: 'color',
- groupDir : 'DESC'
-});
-
-myStore.getGroups(); //returns:
-[
- {
- name: 'yellow',
- children: [
- //all records where the color field is 'yellow'
- ]
- },
- {
- name: 'red',
- children: [
- //all records where the color field is 'red'
- ]
- }
-]
-
- * @param {String} groupName (Optional) Pass in an optional groupName argument to access a specific group as defined by {@link #getGroupString}
- * @return {Array} The grouped data
+ * Called after the configured proxy completes a prefetch operation.
+ * @private
+ * @param {Ext.data.Operation} operation The operation that completed
*/
- getGroups: function(requestGroupString) {
- var records = this.data.items,
- length = records.length,
- groups = [],
- pointers = {},
- record,
- groupStr,
- group,
- i;
+ onProxyPrefetch: function(operation) {
+ var me = this,
+ resultSet = operation.getResultSet(),
+ records = operation.getRecords(),
- for (i = 0; i < length; i++) {
- record = records[i];
- groupStr = this.getGroupString(record);
- group = pointers[groupStr];
+ successful = operation.wasSuccessful();
- if (group === undefined) {
- group = {
- name: groupStr,
- children: []
- };
+ if (resultSet) {
+ me.totalCount = resultSet.total;
+ me.fireEvent('totalcountchange', me.totalCount);
+ }
- groups.push(group);
- pointers[groupStr] = group;
- }
+ if (successful) {
+ me.cacheRecords(records, operation);
+ }
+ Ext.Array.remove(me.pendingRequests, operation.requestId);
+ if (operation.page) {
+ Ext.Array.remove(me.pagesRequested, operation.page);
+ }
- group.children.push(record);
+ me.loading = false;
+ me.fireEvent('prefetch', me, records, successful, operation);
+
+ // HACK to support loadMask
+ if (operation.blocking) {
+ me.fireEvent('load', me, records, successful);
}
- return requestGroupString ? pointers[requestGroupString] : groups;
+ //this is a callback that would have been passed to the 'read' function and is optional
+ Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
},
/**
+ * Caches the records in the prefetch and stripes them with their server-side
+ * index.
* @private
- * For a given set of records and a Grouper, returns an array of arrays - each of which is the set of records
- * matching a certain group.
+ * @param {Ext.data.Model[]} records The records to cache
+ * @param {Ext.data.Operation} The associated operation
*/
- getGroupsForGrouper: function(records, grouper) {
- var length = records.length,
- groups = [],
- oldValue,
- newValue,
- record,
- group,
- i;
-
- for (i = 0; i < length; i++) {
- record = records[i];
- newValue = grouper.getGroupString(record);
+ cacheRecords: function(records, operation) {
+ var me = this,
+ i = 0,
+ length = records.length,
+ start = operation ? operation.start : 0;
- if (newValue !== oldValue) {
- group = {
- name: newValue,
- grouper: grouper,
- records: []
- };
- groups.push(group);
- }
+ if (!Ext.isDefined(me.totalCount)) {
+ me.totalCount = records.length;
+ me.fireEvent('totalcountchange', me.totalCount);
+ }
- group.records.push(record);
+ for (; i < length; i++) {
+ // this is the true index, not the viewIndex
+ records[i].index = start + i;
+ }
- oldValue = newValue;
+ me.prefetchData.addAll(records);
+ if (me.purgePageCount) {
+ me.purgeRecords();
}
- return groups;
},
+
/**
- * @private
- * This is used recursively to gather the records into the configured Groupers. The data MUST have been sorted for
- * this to work properly (see {@link #getGroupData} and {@link #getGroupsForGrouper}) Most of the work is done by
- * {@link #getGroupsForGrouper} - this function largely just handles the recursion.
- * @param {Array} records The set or subset of records to group
- * @param {Number} grouperIndex The grouper index to retrieve
- * @return {Array} The grouped records
+ * Purge the least recently used records in the prefetch if the purgeCount
+ * has been exceeded.
*/
- getGroupsForGrouperIndex: function(records, grouperIndex) {
+ purgeRecords: function() {
var me = this,
- groupers = me.groupers,
- grouper = groupers.getAt(grouperIndex),
- groups = me.getGroupsForGrouper(records, grouper),
- length = groups.length,
- i;
+ prefetchCount = me.prefetchData.getCount(),
+ purgeCount = me.purgePageCount * me.pageSize,
+ numRecordsToPurge = prefetchCount - purgeCount - 1,
+ i = 0;
- if (grouperIndex + 1 < groupers.length) {
- for (i = 0; i < length; i++) {
- groups[i].children = me.getGroupsForGrouperIndex(groups[i].records, grouperIndex + 1);
- }
+ for (; i <= numRecordsToPurge; i++) {
+ me.prefetchData.removeAt(0);
}
+ },
- for (i = 0; i < length; i++) {
- groups[i].depth = grouperIndex;
+ /**
+ * Determines if the range has already been satisfied in the prefetchData.
+ * @private
+ * @param {Number} start The start index
+ * @param {Number} end The end index in the range
+ */
+ rangeSatisfied: function(start, end) {
+ var me = this,
+ i = start,
+ satisfied = true;
+
+ for (; i < end; i++) {
+ if (!me.prefetchData.getByKey(i)) {
+ satisfied = false;
+ //
+ if (end - i > me.pageSize) {
+ Ext.Error.raise("A single page prefetch could never satisfy this request.");
+ }
+ //
+ break;
+ }
}
+ return satisfied;
+ },
- return groups;
+ /**
+ * Determines the page from a record index
+ * @param {Number} index The record index
+ * @return {Number} The page the record belongs to
+ */
+ getPageFromRecordIndex: function(index) {
+ return Math.floor(index / this.pageSize) + 1;
},
/**
+ * Handles a guaranteed range being loaded
* @private
- * Returns records grouped by the configured {@link #groupers grouper} configuration. Sample return value (in
- * this case grouping by genre and then author in a fictional books dataset):
-
-[
- {
- name: 'Fantasy',
- depth: 0,
- records: [
- //book1, book2, book3, book4
- ],
- children: [
- {
- name: 'Rowling',
- depth: 1,
- records: [
- //book1, book2
- ]
- },
- {
- name: 'Tolkein',
- depth: 1,
- records: [
- //book3, book4
- ]
- }
- ]
- }
-]
-
- * @param {Boolean} sort True to call {@link #sort} before finding groups. Sorting is required to make grouping
- * function correctly so this should only be set to false if the Store is known to already be sorted correctly
- * (defaults to true)
- * @return {Array} The group data
*/
- getGroupData: function(sort) {
- var me = this;
- if (sort !== false) {
- me.sort();
+ onGuaranteedRange: function() {
+ var me = this,
+ totalCount = me.getTotalCount(),
+ start = me.requestStart,
+ end = ((totalCount - 1) < me.requestEnd) ? totalCount - 1 : me.requestEnd,
+ range = [],
+ record,
+ i = start;
+
+ end = Math.max(0, end);
+
+ //
+ if (start > end) {
+ Ext.log({
+ level: 'warn',
+ msg: 'Start (' + start + ') was greater than end (' + end +
+ ') for the range of records requested (' + me.requestStart + '-' +
+ me.requestEnd + ')' + (this.storeId ? ' from store "' + this.storeId + '"' : '')
+ });
}
+ //
- return me.getGroupsForGrouperIndex(me.data.items, 0);
+ if (start !== me.guaranteedStart && end !== me.guaranteedEnd) {
+ me.guaranteedStart = start;
+ me.guaranteedEnd = end;
+
+ for (; i <= end; i++) {
+ record = me.prefetchData.getByKey(i);
+ //
+// if (!record) {
+// Ext.log('Record with key "' + i + '" was not found and store said it was guaranteed');
+// }
+ //
+ if (record) {
+ range.push(record);
+ }
+ }
+ me.fireEvent('guaranteedrange', range, start, end);
+ if (me.cb) {
+ me.cb.call(me.scope || me, range);
+ }
+ }
+
+ me.unmask();
+ },
+
+ // hack to support loadmask
+ mask: function() {
+ this.masked = true;
+ this.fireEvent('beforeload');
+ },
+
+ // hack to support loadmask
+ unmask: function() {
+ if (this.masked) {
+ this.fireEvent('load');
+ }
},
/**
- * Returns the string to group on for a given model instance. The default implementation of this method returns
- * the model's {@link #groupField}, but this can be overridden to group by an arbitrary string. For example, to
- * group by the first letter of a model's 'name' field, use the following code:
-
-new Ext.data.Store({
- groupDir: 'ASC',
- getGroupString: function(instance) {
- return instance.get('name')[0];
- }
-});
-
- * @param {Ext.data.Model} instance The model instance
- * @return {String} The string to compare when forming groups
+ * Returns the number of pending requests out.
*/
- getGroupString: function(instance) {
- var group = this.groupers.first();
- if (group) {
- return instance.get(group.property);
+ hasPendingRequests: function() {
+ return this.pendingRequests.length;
+ },
+
+
+ // wait until all requests finish, until guaranteeing the range.
+ onWaitForGuarantee: function() {
+ if (!this.hasPendingRequests()) {
+ this.onGuaranteedRange();
}
- return '';
},
+
/**
- * Inserts Model instances into the Store at the given index and fires the {@link #add} event.
- * See also {@link #add}
.
- * @param {Number} index The start index at which to insert the passed Records.
- * @param {Ext.data.Model[]} records An Array of Ext.data.Model objects to add to the cache.
+ * Guarantee a specific range, this will load the store with a range (that
+ * must be the pageSize or smaller) and take care of any loading that may
+ * be necessary.
*/
- insert: function(index, records) {
+ guaranteeRange: function(start, end, cb, scope) {
+ //
+ if (start && end) {
+ if (end - start > this.pageSize) {
+ Ext.Error.raise({
+ start: start,
+ end: end,
+ pageSize: this.pageSize,
+ msg: "Requested a bigger range than the specified pageSize"
+ });
+ }
+ }
+ //
+
+ end = (end > this.totalCount) ? this.totalCount - 1 : end;
+
var me = this,
- sync = false,
- i,
- record,
- len;
+ i = start,
+ prefetchData = me.prefetchData,
+ range = [],
+ startLoaded = !!prefetchData.getByKey(start),
+ endLoaded = !!prefetchData.getByKey(end),
+ startPage = me.getPageFromRecordIndex(start),
+ endPage = me.getPageFromRecordIndex(end);
- records = [].concat(records);
- for (i = 0, len = records.length; i < len; i++) {
- record = me.createModel(records[i]);
- record.set(me.modelDefaults);
- // reassign the model in the array in case it wasn't created yet
- records[i] = record;
-
- me.data.insert(index + i, record);
- record.join(me);
+ me.cb = cb;
+ me.scope = scope;
- sync = sync || record.phantom === true;
+ me.requestStart = start;
+ me.requestEnd = end;
+ // neither beginning or end are loaded
+ if (!startLoaded || !endLoaded) {
+ // same page, lets load it
+ if (startPage === endPage) {
+ me.mask();
+ me.prefetchPage(startPage, {
+ //blocking: true,
+ callback: me.onWaitForGuarantee,
+ scope: me
+ });
+ // need to load two pages
+ } else {
+ me.mask();
+ me.prefetchPage(startPage, {
+ //blocking: true,
+ callback: me.onWaitForGuarantee,
+ scope: me
+ });
+ me.prefetchPage(endPage, {
+ //blocking: true,
+ callback: me.onWaitForGuarantee,
+ scope: me
+ });
+ }
+ // Request was already satisfied via the prefetch
+ } else {
+ me.onGuaranteedRange();
}
+ },
- if (me.snapshot) {
- me.snapshot.addAll(records);
+ // because prefetchData is stored by index
+ // this invalidates all of the prefetchedData
+ sort: function() {
+ var me = this,
+ prefetchData = me.prefetchData,
+ sorters,
+ start,
+ end,
+ range;
+
+ if (me.buffered) {
+ if (me.remoteSort) {
+ prefetchData.clear();
+ me.callParent(arguments);
+ } else {
+ sorters = me.getSorters();
+ start = me.guaranteedStart;
+ end = me.guaranteedEnd;
+
+ if (sorters.length) {
+ prefetchData.sort(sorters);
+ range = prefetchData.getRange();
+ prefetchData.clear();
+ me.cacheRecords(range);
+ delete me.guaranteedStart;
+ delete me.guaranteedEnd;
+ me.guaranteeRange(start, end);
+ }
+ me.callParent(arguments);
+ }
+ } else {
+ me.callParent(arguments);
}
+ },
- me.fireEvent('add', me, records, index);
- me.fireEvent('datachanged', me);
- if (me.autoSync && sync) {
- me.sync();
+ // overriden to provide striping of the indexes as sorting occurs.
+ // this cannot be done inside of sort because datachanged has already
+ // fired and will trigger a repaint of the bound view.
+ doSort: function(sorterFn) {
+ var me = this;
+ if (me.remoteSort) {
+ //the load function will pick up the new sorters and request the sorted data from the proxy
+ me.load();
+ } else {
+ me.data.sortBy(sorterFn);
+ if (!me.buffered) {
+ var range = me.getRange(),
+ ln = range.length,
+ i = 0;
+ for (; i < ln; i++) {
+ range[i].index = i;
+ }
+ }
+ me.fireEvent('datachanged', me);
}
},
/**
- * Adds Model instances to the Store by instantiating them based on a JavaScript object. When adding already-
- * instantiated Models, use {@link #insert} instead. The instances will be added at the end of the existing collection.
- * This method accepts either a single argument array of Model instances or any number of model instance arguments.
- * Sample usage:
- *
-
-myStore.add({some: 'data'}, {some: 'other data'});
-
- *
- * @param {Object} data The data for each model
- * @return {Array} The array of newly created model instances
+ * Finds the index of the first matching Record in this store by a specific field value.
+ * @param {String} fieldName The name of the Record field to test.
+ * @param {String/RegExp} value Either a string that the field value
+ * should begin with, or a RegExp to test against the field.
+ * @param {Number} startIndex (optional) The index to start searching at
+ * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning
+ * @param {Boolean} caseSensitive (optional) True for case sensitive comparison
+ * @param {Boolean} exactMatch (optional) True to force exact match (^ and $ characters added to the regex). Defaults to false.
+ * @return {Number} The matched index or -1
*/
- add: function(records) {
- //accept both a single-argument array of records, or any number of record arguments
- if (!Ext.isArray(records)) {
- records = Array.prototype.slice.apply(arguments);
+ find: function(property, value, start, anyMatch, caseSensitive, exactMatch) {
+ var fn = this.createFilterFn(property, value, anyMatch, caseSensitive, exactMatch);
+ return fn ? this.data.findIndexBy(fn, null, start) : -1;
+ },
+
+ /**
+ * Finds the first matching Record in this store by a specific field value.
+ * @param {String} fieldName The name of the Record field to test.
+ * @param {String/RegExp} value Either a string that the field value
+ * should begin with, or a RegExp to test against the field.
+ * @param {Number} startIndex (optional) The index to start searching at
+ * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning
+ * @param {Boolean} caseSensitive (optional) True for case sensitive comparison
+ * @param {Boolean} exactMatch (optional) True to force exact match (^ and $ characters added to the regex). Defaults to false.
+ * @return {Ext.data.Model} The matched record or null
+ */
+ findRecord: function() {
+ var me = this,
+ index = me.find.apply(me, arguments);
+ return index !== -1 ? me.getAt(index) : null;
+ },
+
+ /**
+ * @private
+ * Returns a filter function used to test a the given property's value. Defers most of the work to
+ * Ext.util.MixedCollection's createValueMatcher function
+ * @param {String} property The property to create the filter function for
+ * @param {String/RegExp} value The string/regex to compare the property value to
+ * @param {Boolean} [anyMatch=false] True if we don't care if the filter value is not the full value.
+ * @param {Boolean} [caseSensitive=false] True to create a case-sensitive regex.
+ * @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters added to the regex).
+ * Ignored if anyMatch is true.
+ */
+ createFilterFn: function(property, value, anyMatch, caseSensitive, exactMatch) {
+ if (Ext.isEmpty(value)) {
+ return false;
}
+ value = this.data.createValueMatcher(value, anyMatch, caseSensitive, exactMatch);
+ return function(r) {
+ return value.test(r.data[property]);
+ };
+ },
+
+ /**
+ * Finds the index of the first matching Record in this store by a specific field value.
+ * @param {String} fieldName The name of the Record field to test.
+ * @param {Object} value The value to match the field against.
+ * @param {Number} startIndex (optional) The index to start searching at
+ * @return {Number} The matched index or -1
+ */
+ findExact: function(property, value, start) {
+ return this.data.findIndexBy(function(rec) {
+ return rec.get(property) == value;
+ },
+ this, start);
+ },
+
+ /**
+ * Find the index of the first matching Record in this Store by a function.
+ * If the function returns true it is considered a match.
+ * @param {Function} fn The function to be called. It will be passed the following parameters:
+ * @param {Object} scope (optional) The scope (this
reference) in which the function is executed. Defaults to this Store.
+ * @param {Number} startIndex (optional) The index to start searching at
+ * @return {Number} The matched index or -1
+ */
+ findBy: function(fn, scope, start) {
+ return this.data.findIndexBy(fn, scope, start);
+ },
+ /**
+ * Collects unique values for a particular dataIndex from this store.
+ * @param {String} dataIndex The property to collect
+ * @param {Boolean} allowNull (optional) Pass true to allow null, undefined or empty string values
+ * @param {Boolean} bypassFilter (optional) Pass true to collect from all records, even ones which are filtered
+ * @return {Object[]} An array of the unique values
+ **/
+ collect: function(dataIndex, allowNull, bypassFilter) {
var me = this,
- i = 0,
- length = records.length,
- record;
-
- for (; i < length; i++) {
- record = me.createModel(records[i]);
- // reassign the model in the array in case it wasn't created yet
- records[i] = record;
- }
-
- me.insert(me.data.length, records);
+ data = (bypassFilter === true && me.snapshot) ? me.snapshot: me.data;
- return records;
+ return data.collect(dataIndex, 'data', allowNull);
},
/**
- * Converts a literal to a model, if it's not a model already
- * @private
- * @param record {Ext.data.Model/Object} The record to create
- * @return {Ext.data.Model}
+ * Gets the number of cached records.
+ * If using paging, this may not be the total size of the dataset. If the data object
+ * used by the Reader contains the dataset size, then the {@link #getTotalCount} function returns
+ * the dataset size. Note: see the Important note in {@link #load}.
+ * @return {Number} The number of Records in the Store's cache.
*/
- createModel: function(record) {
- if (!record.isModel) {
- record = Ext.ModelManager.create(record, this.model);
- }
-
- return record;
+ getCount: function() {
+ return this.data.length || 0;
},
/**
- * Calls the specified function for each of the {@link Ext.data.Model Records} in the cache.
- * @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed as the first parameter.
- * Returning false aborts and exits the iteration.
- * @param {Object} scope (optional) The scope (this
reference) in which the function is executed.
- * Defaults to the current {@link Ext.data.Model Record} in the iteration.
+ * Returns the total number of {@link Ext.data.Model Model} instances that the {@link Ext.data.proxy.Proxy Proxy}
+ * indicates exist. This will usually differ from {@link #getCount} when using paging - getCount returns the
+ * number of records loaded into the Store at the moment, getTotalCount returns the number of records that
+ * could be loaded into the Store if the Store contained all data
+ * @return {Number} The total number of Model instances available via the Proxy
*/
- each: function(fn, scope) {
- this.data.each(fn, scope);
+ getTotalCount: function() {
+ return this.totalCount;
},
/**
- * Removes the given record from the Store, firing the 'remove' event for each instance that is removed, plus a single
- * 'datachanged' event after removal.
- * @param {Ext.data.Model/Array} records The Ext.data.Model instance or array of instances to remove
+ * Get the Record at the specified index.
+ * @param {Number} index The index of the Record to find.
+ * @return {Ext.data.Model} The Record at the passed index. Returns undefined if not found.
*/
- remove: function(records, /* private */ isMove) {
- if (!Ext.isArray(records)) {
- records = [records];
- }
-
- /*
- * Pass the isMove parameter if we know we're going to be re-inserting this record
- */
- isMove = isMove === true;
- var me = this,
- sync = false,
- i = 0,
- length = records.length,
- isPhantom,
- index,
- record;
-
- for (; i < length; i++) {
- record = records[i];
- index = me.data.indexOf(record);
-
- if (me.snapshot) {
- me.snapshot.remove(record);
- }
-
- if (index > -1) {
- isPhantom = record.phantom === true;
- if (!isMove && !isPhantom) {
- // don't push phantom records onto removed
- me.removed.push(record);
- }
-
- record.unjoin(me);
- me.data.remove(record);
- sync = sync || !isPhantom;
-
- me.fireEvent('remove', me, record, index);
- }
- }
-
- me.fireEvent('datachanged', me);
- if (!isMove && me.autoSync && sync) {
- me.sync();
- }
+ getAt: function(index) {
+ return this.data.getAt(index);
},
/**
- * Removes the model instance at the given index
- * @param {Number} index The record index
+ * Returns a range of Records between specified indices.
+ * @param {Number} [startIndex=0] The starting index
+ * @param {Number} [endIndex] The ending index. Defaults to the last Record in the Store.
+ * @return {Ext.data.Model[]} An array of Records
*/
- removeAt: function(index) {
- var record = this.getAt(index);
+ getRange: function(start, end) {
+ return this.data.getRange(start, end);
+ },
- if (record) {
- this.remove(record);
- }
+ /**
+ * Get the Record with the specified id.
+ * @param {String} id The id of the Record to find.
+ * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.
+ */
+ getById: function(id) {
+ return (this.snapshot || this.data).findBy(function(record) {
+ return record.getId() === id;
+ });
},
/**
- * Loads data into the Store via the configured {@link #proxy}. This uses the Proxy to make an
- * asynchronous call to whatever storage backend the Proxy uses, automatically adding the retrieved
- * instances into the Store and calling an optional callback if required. Example usage:
- *
-
-store.load({
- scope : this,
- callback: function(records, operation, success) {
- //the {@link Ext.data.Operation operation} object contains all of the details of the load operation
- console.log(records);
- }
-});
-
- *
- * If the callback scope does not need to be set, a function can simply be passed:
- *
-
-store.load(function(records, operation, success) {
- console.log('loaded records');
-});
-
- *
- * @param {Object/Function} options Optional config object, passed into the Ext.data.Operation object before loading.
+ * Get the index within the cache of the passed Record.
+ * @param {Ext.data.Model} record The Ext.data.Model object to find.
+ * @return {Number} The index of the passed Record. Returns -1 if not found.
*/
- load: function(options) {
- var me = this;
-
- options = options || {};
+ indexOf: function(record) {
+ return this.data.indexOf(record);
+ },
- if (Ext.isFunction(options)) {
- options = {
- callback: options
- };
- }
- Ext.applyIf(options, {
- groupers: me.groupers.items,
- page: me.currentPage,
- start: (me.currentPage - 1) * me.pageSize,
- limit: me.pageSize,
- addRecords: false
- });
+ /**
+ * Get the index within the entire dataset. From 0 to the totalCount.
+ * @param {Ext.data.Model} record The Ext.data.Model object to find.
+ * @return {Number} The index of the passed Record. Returns -1 if not found.
+ */
+ indexOfTotal: function(record) {
+ var index = record.index;
+ if (index || index === 0) {
+ return index;
+ }
+ return this.indexOf(record);
+ },
- return me.callParent([options]);
+ /**
+ * Get the index within the cache of the Record with the passed id.
+ * @param {String} id The id of the Record to find.
+ * @return {Number} The index of the Record. Returns -1 if not found.
+ */
+ indexOfId: function(id) {
+ return this.indexOf(this.getById(id));
},
/**
- * @private
- * Called internally when a Proxy has completed a load request
+ * Remove all items from the store.
+ * @param {Boolean} silent Prevent the `clear` event from being fired.
*/
- onProxyLoad: function(operation) {
- var me = this,
- resultSet = operation.getResultSet(),
- records = operation.getRecords(),
- successful = operation.wasSuccessful();
+ removeAll: function(silent) {
+ var me = this;
- if (resultSet) {
- me.totalCount = resultSet.total;
+ me.clearData();
+ if (me.snapshot) {
+ me.snapshot.clear();
}
-
- if (successful) {
- me.loadRecords(records, operation);
+ if (silent !== true) {
+ me.fireEvent('clear', me);
}
+ },
- me.loading = false;
- me.fireEvent('load', me, records, successful);
-
- //TODO: deprecate this event, it should always have been 'load' instead. 'load' is now documented, 'read' is not.
- //People are definitely using this so can't deprecate safely until 2.x
- me.fireEvent('read', me, records, operation.wasSuccessful());
+ /*
+ * Aggregation methods
+ */
- //this is a callback that would have been passed to the 'read' function and is optional
- Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
- },
-
/**
- * Create any new records when a write is returned from the server.
- * @private
- * @param {Array} records The array of new records
- * @param {Ext.data.Operation} operation The operation that just completed
- * @param {Boolean} success True if the operation was successful
+ * Convenience function for getting the first model instance in the store
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the first record being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Ext.data.Model/undefined} The first model instance in the store, or undefined
*/
- onCreateRecords: function(records, operation, success) {
- if (success) {
- var i = 0,
- data = this.data,
- snapshot = this.snapshot,
- length = records.length,
- originalRecords = operation.records,
- record,
- original,
- index;
+ first: function(grouped) {
+ var me = this;
- /**
- * Loop over each record returned from the server. Assume they are
- * returned in order of how they were sent. If we find a matching
- * record, replace it with the newly created one.
- */
- for (; i < length; ++i) {
- record = records[i];
- original = originalRecords[i];
- if (original) {
- index = data.indexOf(original);
- if (index > -1) {
- data.removeAt(index);
- data.insert(index, record);
- }
- if (snapshot) {
- index = snapshot.indexOf(original);
- if (index > -1) {
- snapshot.removeAt(index);
- snapshot.insert(index, record);
- }
- }
- record.phantom = false;
- record.join(this);
- }
- }
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(function(records) {
+ return records.length ? records[0] : undefined;
+ }, me, true);
+ } else {
+ return me.data.first();
}
},
/**
- * Update any records when a write is returned from the server.
- * @private
- * @param {Array} records The array of updated records
- * @param {Ext.data.Operation} operation The operation that just completed
- * @param {Boolean} success True if the operation was successful
+ * Convenience function for getting the last model instance in the store
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the last record being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Ext.data.Model/undefined} The last model instance in the store, or undefined
*/
- onUpdateRecords: function(records, operation, success){
- if (success) {
- var i = 0,
- length = records.length,
- data = this.data,
- snapshot = this.snapshot,
- record;
+ last: function(grouped) {
+ var me = this;
- for (; i < length; ++i) {
- record = records[i];
- data.replace(record);
- if (snapshot) {
- snapshot.replace(record);
- }
- record.join(this);
- }
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(function(records) {
+ var len = records.length;
+ return len ? records[len - 1] : undefined;
+ }, me, true);
+ } else {
+ return me.data.last();
}
},
/**
- * Remove any records when a write is returned from the server.
- * @private
- * @param {Array} records The array of removed records
- * @param {Ext.data.Operation} operation The operation that just completed
- * @param {Boolean} success True if the operation was successful
+ * Sums the value of property for each {@link Ext.data.Model record} between start
+ * and end and returns the result.
+ * @param {String} field A field in each record
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the sum for that group being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Number} The sum
*/
- onDestroyRecords: function(records, operation, success){
- if (success) {
- var me = this,
- i = 0,
- length = records.length,
- data = me.data,
- snapshot = me.snapshot,
- record;
+ sum: function(field, grouped) {
+ var me = this;
- for (; i < length; ++i) {
- record = records[i];
- record.unjoin(me);
- data.remove(record);
- if (snapshot) {
- snapshot.remove(record);
- }
- }
- me.removed = [];
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(me.getSum, me, true, [field]);
+ } else {
+ return me.getSum(me.data.items, field);
}
},
- //inherit docs
- getNewRecords: function() {
- return this.data.filterBy(this.filterNew).items;
- },
+ // @private, see sum
+ getSum: function(records, field) {
+ var total = 0,
+ i = 0,
+ len = records.length;
- //inherit docs
- getUpdatedRecords: function() {
- return this.data.filterBy(this.filterUpdated).items;
+ for (; i < len; ++i) {
+ total += records[i].get(field);
+ }
+
+ return total;
},
/**
- * Filters the loaded set of records by a given set of filters.
- * @param {Mixed} filters The set of filters to apply to the data. These are stored internally on the store,
- * but the filtering itself is done on the Store's {@link Ext.util.MixedCollection MixedCollection}. See
- * MixedCollection's {@link Ext.util.MixedCollection#filter filter} method for filter syntax. Alternatively,
- * pass in a property string
- * @param {String} value Optional value to filter by (only if using a property string as the first argument)
+ * Gets the count of items in the store.
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the count for each group being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Number} the count
*/
- filter: function(filters, value) {
- if (Ext.isString(filters)) {
- filters = {
- property: filters,
- value: value
- };
- }
-
- var me = this,
- decoded = me.decodeFilters(filters),
- i = 0,
- doLocalSort = me.sortOnFilter && !me.remoteSort,
- length = decoded.length;
-
- for (; i < length; i++) {
- me.filters.replace(decoded[i]);
- }
+ count: function(grouped) {
+ var me = this;
- if (me.remoteFilter) {
- //the load function will pick up the new filters and request the filtered data from the proxy
- me.load();
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(function(records) {
+ return records.length;
+ }, me, true);
} else {
- /**
- * A pristine (unfiltered) collection of the records in this store. This is used to reinstate
- * records when a filter is removed or changed
- * @property snapshot
- * @type Ext.util.MixedCollection
- */
- if (me.filters.getCount()) {
- me.snapshot = me.snapshot || me.data.clone();
- me.data = me.data.filter(me.filters.items);
-
- if (doLocalSort) {
- me.sort();
- }
- // fire datachanged event if it hasn't already been fired by doSort
- if (!doLocalSort || me.sorters.length < 1) {
- me.fireEvent('datachanged', me);
- }
- }
+ return me.getCount();
}
},
/**
- * Revert to a view of the Record cache with no filtering applied.
- * @param {Boolean} suppressEvent If true the filter is cleared silently without firing the
- * {@link #datachanged} event.
+ * Gets the minimum value in the store.
+ * @param {String} field The field in each record
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the minimum in the group being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Object} The minimum value, if no items exist, undefined.
*/
- clearFilter: function(suppressEvent) {
+ min: function(field, grouped) {
var me = this;
- me.filters.clear();
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(me.getMin, me, true, [field]);
+ } else {
+ return me.getMin(me.data.items, field);
+ }
+ },
- if (me.remoteFilter) {
- me.load();
- } else if (me.isFiltered()) {
- me.data = me.snapshot.clone();
- delete me.snapshot;
+ // @private, see min
+ getMin: function(records, field){
+ var i = 1,
+ len = records.length,
+ value, min;
- if (suppressEvent !== true) {
- me.fireEvent('datachanged', me);
- }
+ if (len > 0) {
+ min = records[0].get(field);
}
- },
- /**
- * Returns true if this store is currently filtered
- * @return {Boolean}
- */
- isFiltered: function() {
- var snapshot = this.snapshot;
- return !! snapshot && snapshot !== this.data;
+ for (; i < len; ++i) {
+ value = records[i].get(field);
+ if (value < min) {
+ min = value;
+ }
+ }
+ return min;
},
/**
- * Filter by a function. The specified function will be called for each
- * Record in this Store. If the function returns true the Record is included,
- * otherwise it is filtered out.
- * @param {Function} fn The function to be called. It will be passed the following parameters:
- * @param {Object} scope (optional) The scope (this
reference) in which the function is executed. Defaults to this Store.
+ * Gets the maximum value in the store.
+ * @param {String} field The field in each record
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the maximum in the group being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Object} The maximum value, if no items exist, undefined.
*/
- filterBy: function(fn, scope) {
+ max: function(field, grouped) {
var me = this;
- me.snapshot = me.snapshot || me.data.clone();
- me.data = me.queryBy(fn, scope || me);
- me.fireEvent('datachanged', me);
- },
-
- /**
- * Query the cached records in this Store using a filtering function. The specified function
- * will be called with each record in this Store. If the function returns true the record is
- * included in the results.
- * @param {Function} fn The function to be called. It will be passed the following parameters:
- * @param {Object} scope (optional) The scope (this
reference) in which the function is executed. Defaults to this Store.
- * @return {MixedCollection} Returns an Ext.util.MixedCollection of the matched records
- **/
- queryBy: function(fn, scope) {
- var me = this,
- data = me.snapshot || me.data;
- return data.filterBy(fn, scope || me);
- },
-
- /**
- * Loads an array of data straight into the Store
- * @param {Array} data Array of data to load. Any non-model instances will be cast into model instances first
- * @param {Boolean} append True to add the records to the existing records in the store, false to remove the old ones first
- */
- loadData: function(data, append) {
- var model = this.model,
- length = data.length,
- i,
- record;
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(me.getMax, me, true, [field]);
+ } else {
+ return me.getMax(me.data.items, field);
+ }
+ },
- //make sure each data element is an Ext.data.Model instance
- for (i = 0; i < length; i++) {
- record = data[i];
+ // @private, see max
+ getMax: function(records, field) {
+ var i = 1,
+ len = records.length,
+ value,
+ max;
- if (! (record instanceof Ext.data.Model)) {
- data[i] = Ext.ModelManager.create(record, model);
- }
+ if (len > 0) {
+ max = records[0].get(field);
}
- this.loadRecords(data, {addRecords: append});
+ for (; i < len; ++i) {
+ value = records[i].get(field);
+ if (value > max) {
+ max = value;
+ }
+ }
+ return max;
},
/**
- * Loads an array of {@Ext.data.Model model} instances into the store, fires the datachanged event. This should only usually
- * be called internally when loading from the {@link Ext.data.proxy.Proxy Proxy}, when adding records manually use {@link #add} instead
- * @param {Array} records The array of records to load
- * @param {Object} options {addRecords: true} to add these records to the existing records, false to remove the Store's existing records first
+ * Gets the average value in the store.
+ * @param {String} field The field in each record
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the group average being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Object} The average value, if no items exist, 0.
*/
- loadRecords: function(records, options) {
- var me = this,
- i = 0,
- length = records.length;
-
- options = options || {};
-
-
- if (!options.addRecords) {
- delete me.snapshot;
- me.data.clear();
+ average: function(field, grouped) {
+ var me = this;
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(me.getAverage, me, true, [field]);
+ } else {
+ return me.getAverage(me.data.items, field);
}
+ },
- me.data.addAll(records);
-
- //FIXME: this is not a good solution. Ed Spencer is totally responsible for this and should be forced to fix it immediately.
- for (; i < length; i++) {
- if (options.start !== undefined) {
- records[i].index = options.start + i;
+ // @private, see average
+ getAverage: function(records, field) {
+ var i = 0,
+ len = records.length,
+ sum = 0;
+ if (records.length > 0) {
+ for (; i < len; ++i) {
+ sum += records[i].get(field);
}
- records[i].join(me);
+ return sum / len;
}
+ return 0;
+ },
- /*
- * this rather inelegant suspension and resumption of events is required because both the filter and sort functions
- * fire an additional datachanged event, which is not wanted. Ideally we would do this a different way. The first
- * datachanged event is fired by the call to this.add, above.
- */
- me.suspendEvents();
+ /**
+ * Runs the aggregate function for all the records in the store.
+ * @param {Function} fn The function to execute. The function is called with a single parameter,
+ * an array of records for that group.
+ * @param {Object} scope (optional) The scope to execute the function in. Defaults to the store.
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the group average being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @param {Array} args (optional) Any arguments to append to the function call
+ * @return {Object} An object literal with the group names and their appropriate values.
+ */
+ aggregate: function(fn, scope, grouped, args) {
+ args = args || [];
+ if (grouped && this.isGrouped()) {
+ var groups = this.getGroups(),
+ i = 0,
+ len = groups.length,
+ out = {},
+ group;
- if (me.filterOnLoad && !me.remoteFilter) {
- me.filter();
+ for (; i < len; ++i) {
+ group = groups[i];
+ out[group.name] = fn.apply(scope || this, [group.children].concat(args));
+ }
+ return out;
+ } else {
+ return fn.apply(scope || this, [this.data.items].concat(args));
}
+ }
+}, function() {
+ // A dummy empty store with a fieldless Model defined in it.
+ // Just for binding to Views which are instantiated with no Store defined.
+ // They will be able to run and render fine, and be bound to a generated Store later.
+ Ext.regStore('ext-empty-store', {fields: [], proxy: 'proxy'});
+});
- if (me.sortOnLoad && !me.remoteSort) {
- me.sort();
- }
+/**
+ * @author Ed Spencer
+ * @class Ext.data.JsonStore
+ * @extends Ext.data.Store
+ * @ignore
+ *
+ * Small helper class to make creating {@link Ext.data.Store}s from JSON data easier.
+ * A JsonStore will be automatically configured with a {@link Ext.data.reader.Json}.
+ *
+ * A store configuration would be something like:
+ *
+
+var store = new Ext.data.JsonStore({
+ // store configs
+ autoDestroy: true,
+ storeId: 'myStore',
- me.resumeEvents();
- me.fireEvent('datachanged', me, records);
+ proxy: {
+ type: 'ajax',
+ url: 'get-images.php',
+ reader: {
+ type: 'json',
+ root: 'images',
+ idProperty: 'name'
+ }
},
- // PAGING METHODS
+ //alternatively, a {@link Ext.data.Model} name can be given (see {@link Ext.data.Store} for an example)
+ fields: ['name', 'url', {name:'size', type: 'float'}, {name:'lastmod', type:'date'}]
+});
+
+ *
+ * This store is configured to consume a returned object of the form:
+{
+ images: [
+ {name: 'Image one', url:'/GetImage.php?id=1', size:46.5, lastmod: new Date(2007, 10, 29)},
+ {name: 'Image Two', url:'/GetImage.php?id=2', size:43.2, lastmod: new Date(2007, 10, 30)}
+ ]
+}
+
+ *
+ * An object literal of this form could also be used as the {@link #data} config option.
+ *
+ * @xtype jsonstore
+ */
+Ext.define('Ext.data.JsonStore', {
+ extend: 'Ext.data.Store',
+ alias: 'store.json',
+
/**
- * Loads a given 'page' of data by setting the start and limit values appropriately. Internally this just causes a normal
- * load operation, passing in calculated 'start' and 'limit' params
- * @param {Number} page The number of the page to load
+ * @cfg {Ext.data.DataReader} reader @hide
*/
- loadPage: function(page) {
- var me = this;
-
- me.currentPage = page;
+ constructor: function(config) {
+ config = config || {};
- me.read({
- page: page,
- start: (page - 1) * me.pageSize,
- limit: me.pageSize,
- addRecords: !me.clearOnPageLoad
+ Ext.applyIf(config, {
+ proxy: {
+ type : 'ajax',
+ reader: 'json',
+ writer: 'json'
+ }
});
- },
- /**
- * Loads the next 'page' in the current data set
- */
- nextPage: function() {
- this.loadPage(this.currentPage + 1);
- },
+ this.callParent([config]);
+ }
+});
- /**
- * Loads the previous 'page' in the current data set
- */
- previousPage: function() {
- this.loadPage(this.currentPage - 1);
- },
+/**
+ * @class Ext.chart.axis.Time
+ * @extends Ext.chart.axis.Numeric
+ *
+ * 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', {
- // private
- clearData: function() {
- this.data.each(function(record) {
- record.unjoin();
- });
+ /* Begin Definitions */
- this.data.clear();
- },
-
- // Buffering
- /**
- * Prefetches data the Store using its configured {@link #proxy}.
- * @param {Object} options Optional config object, passed into the Ext.data.Operation object before loading.
- * See {@link #load}
- */
- prefetch: function(options) {
- var me = this,
- operation,
- requestId = me.getRequestId();
+ extend: 'Ext.chart.axis.Numeric',
- options = options || {};
+ alternateClassName: 'Ext.chart.TimeAxis',
- Ext.applyIf(options, {
- action : 'read',
- filters: me.filters.items,
- sorters: me.sorters.items,
- requestId: requestId
- });
- me.pendingRequests.push(requestId);
+ alias: 'axis.time',
- operation = Ext.create('Ext.data.Operation', options);
+ requires: ['Ext.data.Store', 'Ext.data.JsonStore'],
- // HACK to implement loadMask support.
- //if (operation.blocking) {
- // me.fireEvent('beforeload', me, operation);
- //}
- if (me.fireEvent('beforeprefetch', me, operation) !== false) {
- me.loading = true;
- me.proxy.read(operation, me.onProxyPrefetch, me);
- }
-
- return me;
- },
-
- /**
- * Prefetches a page of data.
- * @param {Number} page The page to prefetch
- * @param {Object} options Optional config object, passed into the Ext.data.Operation object before loading.
- * See {@link #load}
- * @param
- */
- prefetchPage: function(page, options) {
- var me = this,
- pageSize = me.pageSize,
- start = (page - 1) * me.pageSize,
- end = start + pageSize;
-
- // Currently not requesting this page and range isn't already satisified
- if (Ext.Array.indexOf(me.pagesRequested, page) === -1 && !me.rangeSatisfied(start, end)) {
- options = options || {};
- me.pagesRequested.push(page);
- Ext.applyIf(options, {
- page : page,
- start: start,
- limit: pageSize,
- callback: me.onWaitForGuarantee,
- scope: me
- });
-
- me.prefetch(options);
- }
-
- },
-
- /**
- * Returns a unique requestId to track requests.
- * @private
- */
- getRequestId: function() {
- this.requestSeed = this.requestSeed || 1;
- return this.requestSeed++;
- },
-
- /**
- * Handles a success pre-fetch
- * @private
- * @param {Ext.data.Operation} operation The operation that completed
- */
- onProxyPrefetch: function(operation) {
- var me = this,
- resultSet = operation.getResultSet(),
- records = operation.getRecords(),
-
- successful = operation.wasSuccessful();
-
- if (resultSet) {
- me.totalCount = resultSet.total;
- me.fireEvent('totalcountchange', me.totalCount);
- }
-
- if (successful) {
- me.cacheRecords(records, operation);
- }
- Ext.Array.remove(me.pendingRequests, operation.requestId);
- if (operation.page) {
- Ext.Array.remove(me.pagesRequested, operation.page);
- }
-
- me.loading = false;
- me.fireEvent('prefetch', me, records, successful, operation);
-
- // HACK to support loadMask
- if (operation.blocking) {
- me.fireEvent('load', me, records, successful);
- }
+ /* End Definitions */
- //this is a callback that would have been passed to the 'read' function and is optional
- Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
- },
-
- /**
- * Caches the records in the prefetch and stripes them with their server-side
- * index.
- * @private
- * @param {Array} records The records to cache
- * @param {Ext.data.Operation} The associated operation
- */
- cacheRecords: function(records, operation) {
- var me = this,
- i = 0,
- length = records.length,
- start = operation ? operation.start : 0;
-
- if (!Ext.isDefined(me.totalCount)) {
- me.totalCount = records.length;
- me.fireEvent('totalcountchange', me.totalCount);
- }
-
- for (; i < length; i++) {
- // this is the true index, not the viewIndex
- records[i].index = start + i;
- }
-
- me.prefetchData.addAll(records);
- if (me.purgePageCount) {
- me.purgeRecords();
- }
-
- },
-
-
/**
- * Purge the least recently used records in the prefetch if the purgeCount
- * has been exceeded.
+ * @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}
*/
- purgeRecords: function() {
- var me = this,
- prefetchCount = me.prefetchData.getCount(),
- purgeCount = me.purgePageCount * me.pageSize,
- numRecordsToPurge = prefetchCount - purgeCount - 1,
- i = 0;
+ dateFormat: false,
- for (; i <= numRecordsToPurge; i++) {
- me.prefetchData.removeAt(0);
- }
- },
-
/**
- * Determines if the range has already been satisfied in the prefetchData.
- * @private
- * @param {Number} start The start index
- * @param {Number} end The end index in the range
+ * @cfg {Date} fromDate The starting date for the time axis.
*/
- rangeSatisfied: function(start, end) {
- var me = this,
- i = start,
- satisfied = true;
+ fromDate: false,
- for (; i < end; i++) {
- if (!me.prefetchData.getByKey(i)) {
- satisfied = false;
- //
- if (end - i > me.pageSize) {
- Ext.Error.raise("A single page prefetch could never satisfy this request.");
- }
- //
- break;
- }
- }
- return satisfied;
- },
-
- /**
- * Determines the page from a record index
- * @param {Number} index The record index
- * @return {Number} The page the record belongs to
- */
- getPageFromRecordIndex: function(index) {
- return Math.floor(index / this.pageSize) + 1;
- },
-
/**
- * Handles a guaranteed range being loaded
- * @private
+ * @cfg {Date} toDate The ending date for the time axis.
*/
- onGuaranteedRange: function() {
- var me = this,
- totalCount = me.getTotalCount(),
- start = me.requestStart,
- end = ((totalCount - 1) < me.requestEnd) ? totalCount - 1 : me.requestEnd,
- range = [],
- record,
- i = start;
-
- //
- if (start > end) {
- Ext.Error.raise("Start (" + start + ") was greater than end (" + end + ")");
- }
- //
-
- if (start !== me.guaranteedStart && end !== me.guaranteedEnd) {
- me.guaranteedStart = start;
- me.guaranteedEnd = end;
-
- for (; i <= end; i++) {
- record = me.prefetchData.getByKey(i);
- //
- if (!record) {
- Ext.Error.raise("Record was not found and store said it was guaranteed");
- }
- //
- range.push(record);
- }
- me.fireEvent('guaranteedrange', range, start, end);
- if (me.cb) {
- me.cb.call(me.scope || me, range);
- }
- }
-
- me.unmask();
- },
-
- // hack to support loadmask
- mask: function() {
- this.masked = true;
- this.fireEvent('beforeload');
- },
-
- // hack to support loadmask
- unmask: function() {
- if (this.masked) {
- this.fireEvent('load');
- }
- },
-
+ toDate: false,
+
/**
- * Returns the number of pending requests out.
+ * @cfg {Array/Boolean} step
+ * An array with two components: The first is the unit of the step (day, month, year, etc).
+ * The second one is the number of units for the step (1, 2, etc.).
+ * Defaults to `[Ext.Date.DAY, 1]`.
*/
- hasPendingRequests: function() {
- return this.pendingRequests.length;
- },
-
-
- // wait until all requests finish, until guaranteeing the range.
- onWaitForGuarantee: function() {
- if (!this.hasPendingRequests()) {
- this.onGuaranteedRange();
- }
- },
+ step: [Ext.Date.DAY, 1],
- /**
- * Guarantee a specific range, this will load the store with a range (that
- * must be the pageSize or smaller) and take care of any loading that may
- * be necessary.
- */
- guaranteeRange: function(start, end, cb, scope) {
- //
- if (start && end) {
- if (end - start > this.pageSize) {
- Ext.Error.raise({
- start: start,
- end: end,
- pageSize: this.pageSize,
- msg: "Requested a bigger range than the specified pageSize"
- });
- }
- }
- //
-
- end = (end > this.totalCount) ? this.totalCount - 1 : end;
-
- var me = this,
- i = start,
- prefetchData = me.prefetchData,
- range = [],
- startLoaded = !!prefetchData.getByKey(start),
- endLoaded = !!prefetchData.getByKey(end),
- startPage = me.getPageFromRecordIndex(start),
- endPage = me.getPageFromRecordIndex(end);
-
- me.cb = cb;
- me.scope = scope;
-
- me.requestStart = start;
- me.requestEnd = end;
- // neither beginning or end are loaded
- if (!startLoaded || !endLoaded) {
- // same page, lets load it
- if (startPage === endPage) {
- me.mask();
- me.prefetchPage(startPage, {
- //blocking: true,
- callback: me.onWaitForGuarantee,
- scope: me
- });
- // need to load two pages
- } else {
- me.mask();
- me.prefetchPage(startPage, {
- //blocking: true,
- callback: me.onWaitForGuarantee,
- scope: me
- });
- me.prefetchPage(endPage, {
- //blocking: true,
- callback: me.onWaitForGuarantee,
- scope: me
- });
+ /**
+ * @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,
+
+ // Avoid roundtoDecimal call in Numeric Axis's constructor
+ roundToDecimal: 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);
+ };
}
- // Request was already satisfied via the prefetch
- } else {
- me.onGuaranteedRange();
}
},
-
- // because prefetchData is stored by index
- // this invalidates all of the prefetchedData
- sort: function() {
+
+ doConstrain: function () {
var me = this,
- prefetchData = me.prefetchData,
- sorters,
- start,
- end,
- range;
-
- if (me.buffered) {
- if (me.remoteSort) {
- prefetchData.clear();
- me.callParent(arguments);
- } else {
- sorters = me.getSorters();
- start = me.guaranteedStart;
- end = me.guaranteedEnd;
- range;
-
- if (sorters.length) {
- prefetchData.sort(sorters);
- range = prefetchData.getRange();
- prefetchData.clear();
- me.cacheRecords(range);
- delete me.guaranteedStart;
- delete me.guaranteedEnd;
- me.guaranteeRange(start, end);
+ store = me.chart.store,
+ data = [],
+ series = me.chart.series.items,
+ math = Math,
+ mmax = math.max,
+ mmin = math.min,
+ fields = me.fields,
+ ln = fields.length,
+ range = me.getRange(),
+ min = range.min, max = range.max, i, l, excludes = [],
+ value, values, rec, data = [];
+ for (i = 0, l = series.length; i < l; i++) {
+ excludes[i] = series[i].__excludes;
+ }
+ store.each(function(record) {
+ for (i = 0; i < ln; i++) {
+ if (excludes[i]) {
+ continue;
}
- me.callParent(arguments);
+ value = record.get(fields[i]);
+ if (+value < +min) return;
+ if (+value > +max) return;
}
- } else {
- me.callParent(arguments);
- }
+ data.push(record);
+ })
+ me.chart.substore = Ext.create('Ext.data.JsonStore', { model: store.model, data: data });
},
- // overriden to provide striping of the indexes as sorting occurs.
- // this cannot be done inside of sort because datachanged has already
- // fired and will trigger a repaint of the bound view.
- doSort: function(sorterFn) {
+ // Before rendering, set current default step count to be number of records.
+ processView: function () {
var me = this;
- if (me.remoteSort) {
- //the load function will pick up the new sorters and request the sorted data from the proxy
- me.load();
- } else {
- me.data.sortBy(sorterFn);
- if (!me.buffered) {
- var range = me.getRange(),
- ln = range.length,
- i = 0;
- for (; i < ln; i++) {
- range[i].index = i;
- }
+ 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;
}
- me.fireEvent('datachanged', me);
+ if (me.maximum) {
+ range.to = me.maximum;
+ }
+ range.step = (range.to - range.from) / range.steps;
+ 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:
+ *
+ * - `itemmouseup` When the user interacts with a marker.
+ * - `itemmousedown` When the user interacts with a marker.
+ * - `itemmousemove` When the user iteracts 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 */
+
/**
- * Finds the index of the first matching Record in this store by a specific field value.
- * @param {String} fieldName The name of the Record field to test.
- * @param {String/RegExp} value Either a string that the field value
- * should begin with, or a RegExp to test against the field.
- * @param {Number} startIndex (optional) The index to start searching at
- * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning
- * @param {Boolean} caseSensitive (optional) True for case sensitive comparison
- * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false.
- * @return {Number} The matched index or -1
+ * @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.
*/
- find: function(property, value, start, anyMatch, caseSensitive, exactMatch) {
- var fn = this.createFilterFn(property, value, anyMatch, caseSensitive, exactMatch);
- return fn ? this.data.findIndexBy(fn, null, start) : -1;
- },
/**
- * Finds the first matching Record in this store by a specific field value.
- * @param {String} fieldName The name of the Record field to test.
- * @param {String/RegExp} value Either a string that the field value
- * should begin with, or a RegExp to test against the field.
- * @param {Number} startIndex (optional) The index to start searching at
- * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning
- * @param {Boolean} caseSensitive (optional) True for case sensitive comparison
- * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false.
- * @return {Ext.data.Model} The matched record or null
+ * @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,
+ * width: 140,
+ * height: 28,
+ * renderer: function(storeItem, item) {
+ * this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' views');
+ * }
+ * },
*/
- findRecord: function() {
- var me = this,
- index = me.find.apply(me, arguments);
- return index !== -1 ? me.getAt(index) : null;
- },
/**
- * @private
- * Returns a filter function used to test a the given property's value. Defers most of the work to
- * Ext.util.MixedCollection's createValueMatcher function
- * @param {String} property The property to create the filter function for
- * @param {String/RegExp} value The string/regex to compare the property value to
- * @param {Boolean} anyMatch True if we don't care if the filter value is not the full value (defaults to false)
- * @param {Boolean} caseSensitive True to create a case-sensitive regex (defaults to false)
- * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false.
- * Ignored if anyMatch is true.
+ * @cfg {String} type
+ * The type of series. Set in subclasses.
*/
- createFilterFn: function(property, value, anyMatch, caseSensitive, exactMatch) {
- if (Ext.isEmpty(value)) {
- return false;
- }
- value = this.data.createValueMatcher(value, anyMatch, caseSensitive, exactMatch);
- return function(r) {
- return value.test(r.data[property]);
- };
- },
+ type: null,
/**
- * Finds the index of the first matching Record in this store by a specific field value.
- * @param {String} fieldName The name of the Record field to test.
- * @param {Mixed} value The value to match the field against.
- * @param {Number} startIndex (optional) The index to start searching at
- * @return {Number} The matched index or -1
+ * @cfg {String} title
+ * The human-readable name of the series.
*/
- findExact: function(property, value, start) {
- return this.data.findIndexBy(function(rec) {
- return rec.get(property) === value;
- },
- this, start);
- },
+ title: null,
/**
- * Find the index of the first matching Record in this Store by a function.
- * If the function returns true it is considered a match.
- * @param {Function} fn The function to be called. It will be passed the following parameters:
- * @param {Object} scope (optional) The scope (this
reference) in which the function is executed. Defaults to this Store.
- * @param {Number} startIndex (optional) The index to start searching at
- * @return {Number} The matched index or -1
+ * @cfg {Boolean} showInLegend
+ * Whether to show this series in the legend.
*/
- findBy: function(fn, scope, start) {
- return this.data.findIndexBy(fn, scope, start);
- },
+ showInLegend: true,
/**
- * Collects unique values for a particular dataIndex from this store.
- * @param {String} dataIndex The property to collect
- * @param {Boolean} allowNull (optional) Pass true to allow null, undefined or empty string values
- * @param {Boolean} bypassFilter (optional) Pass true to collect from all records, even ones which are filtered
- * @return {Array} An array of the unique values
- **/
- collect: function(dataIndex, allowNull, bypassFilter) {
- var me = this,
- data = (bypassFilter === true && me.snapshot) ? me.snapshot: me.data;
-
- return data.collect(dataIndex, 'data', allowNull);
+ * @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.
+ */
+ renderer: function(sprite, record, attributes, index, store) {
+ return attributes;
},
/**
- * Gets the number of cached records.
- * If using paging, this may not be the total size of the dataset. If the data object
- * used by the Reader contains the dataset size, then the {@link #getTotalCount} function returns
- * the dataset size. Note: see the Important note in {@link #load}.
- * @return {Number} The number of Records in the Store's cache.
+ * @cfg {Array} shadowAttributes
+ * An array with shadow attributes
*/
- getCount: function() {
- return this.data.length || 0;
- },
+ shadowAttributes: null,
+
+ //@private triggerdrawlistener flag
+ triggerAfterDraw: false,
/**
- * Returns the total number of {@link Ext.data.Model Model} instances that the {@link Ext.data.proxy.Proxy Proxy}
- * indicates exist. This will usually differ from {@link #getCount} when using paging - getCount returns the
- * number of records loaded into the Store at the moment, getTotalCount returns the number of records that
- * could be loaded into the Store if the Store contained all data
- * @return {Number} The total number of Model instances available via the Proxy
+ * @cfg {Object} listeners
+ * An (optional) object with event callbacks. All event callbacks get the target *item* as first parameter. The callback functions are:
+ *
+ * - itemmouseover
+ * - itemmouseout
+ * - itemmousedown
+ * - itemmouseup
*/
- getTotalCount: function() {
- return this.totalCount;
- },
+ 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.addEvents({
+ scope: me,
+ itemmouseover: true,
+ itemmouseout: true,
+ itemmousedown: true,
+ itemmouseup: true,
+ mouseleave: true,
+ afterdraw: true,
+
+ /**
+ * @event titlechange
+ * Fires when the series title is changed via {@link #setTitle}.
+ * @param {String} title The new title value
+ * @param {Number} index The index in the collection of titles
+ */
+ titlechange: true
+ });
+
+ me.mixins.observable.constructor.call(me, config);
+
+ me.on({
+ scope: me,
+ itemmouseover: me.onItemMouseOver,
+ itemmouseout: me.onItemMouseOut,
+ mouseleave: me.onMouseLeave
+ });
+ },
+
/**
- * Get the Record at the specified index.
- * @param {Number} index The index of the Record to find.
- * @return {Ext.data.Model} The Record at the passed index. Returns undefined if not found.
+ * 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.
*/
- getAt: function(index) {
- return this.data.getAt(index);
+ eachRecord: function(fn, scope) {
+ var chart = this.chart;
+ (chart.substore || chart.store).each(fn, scope);
},
/**
- * Returns a range of Records between specified indices.
- * @param {Number} startIndex (optional) The starting index (defaults to 0)
- * @param {Number} endIndex (optional) The ending index (defaults to the last Record in the Store)
- * @return {Ext.data.Model[]} An array of Records
+ * 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.
*/
- getRange: function(start, end) {
- return this.data.getRange(start, end);
+ getRecordCount: function() {
+ var chart = this.chart,
+ store = chart.substore || chart.store;
+ return store ? store.getCount() : 0;
},
/**
- * Get the Record with the specified id.
- * @param {String} id The id of the Record to find.
- * @return {Ext.data.Model} The Record with the passed id. Returns undefined if not found.
+ * Determines whether the series item at the given index has been excluded, i.e. toggled off in the legend.
+ * @param index
*/
- getById: function(id) {
- return (this.snapshot || this.data).findBy(function(record) {
- return record.getId() === id;
- });
+ isExcluded: function(index) {
+ var excludes = this.__excludes;
+ return !!(excludes && excludes[index]);
},
- /**
- * Get the index within the cache of the passed Record.
- * @param {Ext.data.Model} record The Ext.data.Model object to find.
- * @return {Number} The index of the passed Record. Returns -1 if not found.
- */
- indexOf: function(record) {
- return this.data.indexOf(record);
+ // @private set the bbox and clipBox for the series
+ setBBox: function(noGutter) {
+ var me = this,
+ chart = me.chart,
+ chartBBox = chart.chartBBox,
+ gutterX = noGutter ? 0 : chart.maxGutter[0],
+ gutterY = noGutter ? 0 : chart.maxGutter[1],
+ clipBox, bbox;
+
+ clipBox = {
+ x: chartBBox.x,
+ y: chartBBox.y,
+ width: chartBBox.width,
+ height: chartBBox.height
+ };
+ me.clipBox = clipBox;
+
+ bbox = {
+ x: (clipBox.x + gutterX) - (chart.zoom.x * chart.zoom.width),
+ y: (clipBox.y + gutterY) - (chart.zoom.y * chart.zoom.height),
+ width: (clipBox.width - (gutterX * 2)) * chart.zoom.width,
+ height: (clipBox.height - (gutterY * 2)) * chart.zoom.height
+ };
+ me.bbox = bbox;
+ },
+
+ // @private set the animation for the sprite
+ onAnimate: function(sprite, attr) {
+ var me = this;
+ sprite.stopAnimation();
+ if (me.triggerAfterDraw) {
+ return sprite.animate(Ext.applyIf(attr, me.chart.animate));
+ } else {
+ me.triggerAfterDraw = true;
+ return sprite.animate(Ext.apply(Ext.applyIf(attr, me.chart.animate), {
+ listeners: {
+ 'afteranimate': function() {
+ me.triggerAfterDraw = false;
+ me.fireEvent('afterrender');
+ }
+ }
+ }));
+ }
},
+ // @private return the gutter.
+ getGutters: function() {
+ return [0, 0];
+ },
- /**
- * Get the index within the entire dataset. From 0 to the totalCount.
- * @param {Ext.data.Model} record The Ext.data.Model object to find.
- * @return {Number} The index of the passed Record. Returns -1 if not found.
- */
- indexOfTotal: function(record) {
- return record.index || this.indexOf(record);
+ // @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();
+ }
},
/**
- * Get the index within the cache of the Record with the passed id.
- * @param {String} id The id of the Record to find.
- * @return {Number} The index of the Record. Returns -1 if not found.
+ * 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.
*/
- indexOfId: function(id) {
- return this.data.indexOfKey(id);
+ 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;
+ },
+
/**
- * Remove all items from the store.
- * @param {Boolean} silent Prevent the `clear` event from being fired.
+ * Hides all the elements in the series.
*/
- removeAll: function(silent) {
- var me = this;
+ hideAll: function() {
+ var me = this,
+ items = me.items,
+ item, len, i, j, l, sprite, shadows;
- me.clearData();
- if (me.snapshot) {
- me.snapshot.clear();
- }
- if (silent !== true) {
- me.fireEvent('clear', me);
+ 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);
+ }
+ }
}
},
- /*
- * Aggregation methods
+ /**
+ * 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;
+ },
/**
- * Convenience function for getting the first model instance in the store
- * @param {Boolean} grouped (Optional) True to perform the operation for each group
- * in the store. The value returned will be an object literal with the key being the group
- * name and the first record being the value. The grouped parameter is only honored if
- * the store has a groupField.
- * @return {Ext.data.Model/undefined} The first model instance in the store, or undefined
+ * Returns a string with the color to be used for the series legend item.
*/
- first: function(grouped) {
- var me = this;
-
- if (grouped && me.isGrouped()) {
- return me.aggregate(function(records) {
- return records.length ? records[0] : undefined;
- }, me, true);
- } else {
- return me.data.first();
+ 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;
+ }
+ return stroke;
}
+ return '#000';
},
/**
- * Convenience function for getting the last model instance in the store
- * @param {Boolean} grouped (Optional) True to perform the operation for each group
- * in the store. The value returned will be an object literal with the key being the group
- * name and the last record being the value. The grouped parameter is only honored if
- * the store has a groupField.
- * @return {Ext.data.Model/undefined} The last model instance in the store, or undefined
+ * Checks whether the data field should be visible in the legend
+ * @private
+ * @param {Number} index The index of the current item
*/
- last: function(grouped) {
- var me = this;
-
- if (grouped && me.isGrouped()) {
- return me.aggregate(function(records) {
- var len = records.length;
- return len ? records[len - 1] : undefined;
- }, me, true);
- } else {
- return me.data.last();
+ visibleInLegend: function(index){
+ var excludes = this.__excludes;
+ if (excludes) {
+ return !excludes[index];
}
+ return !this.seriesIsHidden;
},
/**
- * Sums the value of property for each {@link Ext.data.Model record} between start
- * and end and returns the result.
- * @param {String} field A field in each record
- * @param {Boolean} grouped (Optional) True to perform the operation for each group
- * in the store. The value returned will be an object literal with the key being the group
- * name and the sum for that group being the value. The grouped parameter is only honored if
- * the store has a groupField.
- * @return {Number} The sum
+ * Changes the value of the {@link #title} for the series.
+ * Arguments can take two forms:
+ *
+ * - A single String value: this will be used as the new single title for the series (applies
+ * to series with only one yField)
+ * - A numeric index and a String value: this will set the title for a single indexed yField.
+ *
+ * @param {Number} index
+ * @param {String} title
*/
- sum: function(field, grouped) {
- var me = this;
+ setTitle: function(index, title) {
+ var me = this,
+ oldTitle = me.title;
- if (grouped && me.isGrouped()) {
- return me.aggregate(me.getSum, me, true, [field]);
+ if (Ext.isString(index)) {
+ title = index;
+ index = 0;
+ }
+
+ if (Ext.isArray(oldTitle)) {
+ oldTitle[index] = title;
} else {
- return me.getSum(me.data.items, field);
+ me.title = title;
}
- },
- // @private, see sum
- getSum: function(records, field) {
- var total = 0,
- i = 0,
- len = records.length;
+ me.fireEvent('titlechange', title, index);
+ }
+});
- for (; i < len; ++i) {
- total += records[i].get(field);
- }
+/**
+ * @class Ext.chart.series.Cartesian
+ * @extends Ext.chart.series.Series
+ *
+ * Common base class for series implementations which plot values using x/y coordinates.
+ */
+Ext.define('Ext.chart.series.Cartesian', {
- return total;
- },
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.series.Series',
+
+ alternateClassName: ['Ext.chart.CartesianSeries', 'Ext.chart.CartesianChart'],
+
+ /* End Definitions */
/**
- * Gets the count of items in the store.
- * @param {Boolean} grouped (Optional) True to perform the operation for each group
- * in the store. The value returned will be an object literal with the key being the group
- * name and the count for each group being the value. The grouped parameter is only honored if
- * the store has a groupField.
- * @return {Number} the count
+ * The field used to access the x axis value from the items from the data
+ * source.
+ *
+ * @cfg xField
+ * @type String
*/
- count: function(grouped) {
- var me = this;
+ xField: null,
- if (grouped && me.isGrouped()) {
- return me.aggregate(function(records) {
- return records.length;
- }, me, true);
- } else {
- return me.getCount();
- }
- },
+ /**
+ * The field used to access the y-axis value from the items from the data
+ * source.
+ *
+ * @cfg yField
+ * @type String
+ */
+ yField: null,
/**
- * Gets the minimum value in the store.
- * @param {String} field The field in each record
- * @param {Boolean} grouped (Optional) True to perform the operation for each group
- * in the store. The value returned will be an object literal with the key being the group
- * name and the minimum in the group being the value. The grouped parameter is only honored if
- * the store has a groupField.
- * @return {Mixed/undefined} The minimum value, if no items exist, undefined.
+ * @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 line series to the ones in the axis, otherwise a
+ * relative scale will be used.
*/
- min: function(field, grouped) {
- var me = this;
+ axis: 'left',
- if (grouped && me.isGrouped()) {
- return me.aggregate(me.getMin, me, true, [field]);
- } else {
- return me.getMin(me.data.items, field);
- }
- },
+ getLegendLabels: function() {
+ var me = this,
+ labels = [],
+ combinations = me.combinations;
- // @private, see min
- getMin: function(records, field){
- var i = 1,
- len = records.length,
- value, min;
+ Ext.each([].concat(me.yField), function(yField, i) {
+ var title = me.title;
+ // Use the 'title' config if present, otherwise use the raw yField name
+ labels.push((Ext.isArray(title) ? title[i] : title) || yField);
+ });
- if (len > 0) {
- min = records[0].get(field);
+ // Handle yFields combined via legend drag-drop
+ if (combinations) {
+ Ext.each(combinations, function(combo) {
+ var label0 = labels[combo[0]],
+ label1 = labels[combo[1]];
+ labels[combo[1]] = label0 + ' & ' + label1;
+ labels.splice(combo[0], 1);
+ });
}
- for (; i < len; ++i) {
- value = records[i].get(field);
- if (value < min) {
- min = value;
- }
- }
- return min;
+ return labels;
},
/**
- * Gets the maximum value in the store.
- * @param {String} field The field in each record
- * @param {Boolean} grouped (Optional) True to perform the operation for each group
- * in the store. The value returned will be an object literal with the key being the group
- * name and the maximum in the group being the value. The grouped parameter is only honored if
- * the store has a groupField.
- * @return {Mixed/undefined} The maximum value, if no items exist, undefined.
+ * @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
*/
- max: function(field, grouped) {
- var me = this;
+ eachYValue: function(record, fn, scope) {
+ Ext.each(this.getYValueAccessors(), function(accessor, i) {
+ fn.call(scope, accessor(record), i);
+ });
+ },
- if (grouped && me.isGrouped()) {
- return me.aggregate(me.getMax, me, true, [field]);
- } else {
- return me.getMax(me.data.items, field);
- }
+ /**
+ * @protected Returns the number of yField values, taking into account fields combined
+ * via legend drag-drop.
+ * @return {Number}
+ */
+ getYValueCount: function() {
+ return this.getYValueAccessors().length;
},
- // @private, see max
- getMax: function(records, field) {
- var i = 1,
- len = records.length,
- value,
- max;
+ combine: function(index1, index2) {
+ var me = this,
+ accessors = me.getYValueAccessors(),
+ accessor1 = accessors[index1],
+ accessor2 = accessors[index2];
- if (len > 0) {
- max = records[0].get(field);
- }
+ // Combine the yValue accessors for the two indexes into a single accessor that returns their sum
+ accessors[index2] = function(record) {
+ return accessor1(record) + accessor2(record);
+ };
+ accessors.splice(index1, 1);
- for (; i < len; ++i) {
- value = records[i].get(field);
- if (value > max) {
- max = value;
- }
- }
- return max;
+ me.callParent([index1, index2]);
+ },
+
+ clearCombinations: function() {
+ // Clear combined accessors, they'll get regenerated on next call to getYValueAccessors
+ delete this.yValueAccessors;
+ this.callParent();
},
/**
- * Gets the average value in the store.
- * @param {String} field The field in each record
- * @param {Boolean} grouped (Optional) True to perform the operation for each group
- * in the store. The value returned will be an object literal with the key being the group
- * name and the group average being the value. The grouped parameter is only honored if
- * the store has a groupField.
- * @return {Mixed/undefined} The average value, if no items exist, 0.
+ * @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
*/
- average: function(field, grouped) {
- var me = this;
- if (grouped && me.isGrouped()) {
- return me.aggregate(me.getAverage, me, true, [field]);
- } else {
- return me.getAverage(me.data.items, field);
+ getYValueAccessors: function() {
+ var me = this,
+ accessors = me.yValueAccessors;
+ if (!accessors) {
+ accessors = me.yValueAccessors = [];
+ Ext.each([].concat(me.yField), function(yField) {
+ accessors.push(function(record) {
+ return record.get(yField);
+ });
+ });
}
+ return accessors;
},
- // @private, see average
- getAverage: function(records, field) {
- var i = 0,
- len = records.length,
- sum = 0;
+ /**
+ * Calculate the min and max values for this series's xField.
+ * @return {Array} [min, max]
+ */
+ getMinMaxXValues: function() {
+ var me = this,
+ min, max,
+ xField = me.xField;
- if (records.length > 0) {
- for (; i < len; ++i) {
- sum += records[i].get(field);
- }
- return sum / len;
+ if (me.getRecordCount() > 0) {
+ min = Infinity;
+ max = -min;
+ me.eachRecord(function(record) {
+ var xValue = record.get(xField);
+ if (xValue > max) {
+ max = xValue;
+ }
+ if (xValue < min) {
+ min = xValue;
+ }
+ });
+ } else {
+ min = max = 0;
}
- return 0;
+ return [min, max];
},
/**
- * Runs the aggregate function for all the records in the store.
- * @param {Function} fn The function to execute. The function is called with a single parameter,
- * an array of records for that group.
- * @param {Object} scope (optional) The scope to execute the function in. Defaults to the store.
- * @param {Boolean} grouped (Optional) True to perform the operation for each group
- * in the store. The value returned will be an object literal with the key being the group
- * name and the group average being the value. The grouped parameter is only honored if
- * the store has a groupField.
- * @param {Array} args (optional) Any arguments to append to the function call
- * @return {Object} An object literal with the group names and their appropriate values.
+ * Calculate the min and max values for this series's yField(s). Takes into account yField
+ * combinations, exclusions, and stacking.
+ * @return {Array} [min, max]
*/
- aggregate: function(fn, scope, grouped, args) {
- args = args || [];
- if (grouped && this.isGrouped()) {
- var groups = this.getGroups(),
- i = 0,
- len = groups.length,
- out = {},
- group;
+ getMinMaxYValues: function() {
+ var me = this,
+ stacked = me.stacked,
+ min, max,
+ positiveTotal, negativeTotal;
- for (; i < len; ++i) {
- group = groups[i];
- out[group.name] = fn.apply(scope || this, [group.children].concat(args));
+ function eachYValueStacked(yValue, i) {
+ if (!me.isExcluded(i)) {
+ if (yValue < 0) {
+ negativeTotal += yValue;
+ } else {
+ positiveTotal += yValue;
+ }
}
- return out;
- } else {
- return fn.apply(scope || this, [this.data.items].concat(args));
}
- }
-});
-/**
- * @author Ed Spencer
- * @class Ext.data.JsonStore
- * @extends Ext.data.Store
- * @ignore
- *
- * Small helper class to make creating {@link Ext.data.Store}s from JSON data easier.
- * A JsonStore will be automatically configured with a {@link Ext.data.reader.Json}.
- *
- * A store configuration would be something like:
- *
-
-var store = new Ext.data.JsonStore({
- // store configs
- autoDestroy: true,
- storeId: 'myStore'
+ function eachYValue(yValue, i) {
+ if (!me.isExcluded(i)) {
+ if (yValue > max) {
+ max = yValue;
+ }
+ if (yValue < min) {
+ min = yValue;
+ }
+ }
+ }
- proxy: {
- type: 'ajax',
- url: 'get-images.php',
- reader: {
- type: 'json',
- root: 'images',
- idProperty: 'name'
+ if (me.getRecordCount() > 0) {
+ min = Infinity;
+ max = -min;
+ me.eachRecord(function(record) {
+ 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);
+ }
+ });
+ } else {
+ min = max = 0;
}
+ return [min, max];
},
- //alternatively, a {@link Ext.data.Model} name can be given (see {@link Ext.data.Store} for an example)
- fields: ['name', 'url', {name:'size', type: 'float'}, {name:'lastmod', type:'date'}]
-});
-
- *
- * This store is configured to consume a returned object of the form:
-{
- images: [
- {name: 'Image one', url:'/GetImage.php?id=1', size:46.5, lastmod: new Date(2007, 10, 29)},
- {name: 'Image Two', url:'/GetImage.php?id=2', size:43.2, lastmod: new Date(2007, 10, 30)}
- ]
-}
-
- *
- * An object literal of this form could also be used as the {@link #data} config option.
- *
- * @constructor
- * @param {Object} config
- * @xtype jsonstore
- */
-Ext.define('Ext.data.JsonStore', {
- extend: 'Ext.data.Store',
- alias: 'store.json',
+ getAxesForXAndYFields: function() {
+ var me = this,
+ axes = me.chart.axes,
+ axis = [].concat(me.axis),
+ xAxis, yAxis;
- /**
- * @cfg {Ext.data.DataReader} reader @hide
- */
- constructor: function(config) {
- config = config || {};
+ if (Ext.Array.indexOf(axis, 'top') > -1) {
+ xAxis = 'top';
+ } else if (Ext.Array.indexOf(axis, 'bottom') > -1) {
+ xAxis = 'bottom';
+ } else {
+ if (axes.get('top')) {
+ xAxis = 'top';
+ } else if (axes.get('bottom')) {
+ xAxis = 'bottom';
+ }
+ }
- Ext.applyIf(config, {
- proxy: {
- type : 'ajax',
- reader: 'json',
- writer: 'json'
+ if (Ext.Array.indexOf(axis, 'left') > -1) {
+ yAxis = 'left';
+ } else if (Ext.Array.indexOf(axis, 'right') > -1) {
+ yAxis = 'right';
+ } else {
+ if (axes.get('left')) {
+ yAxis = 'left';
+ } else if (axes.get('right')) {
+ yAxis = 'right';
}
- });
+ }
- this.callParent([config]);
+ return {
+ xAxis: xAxis,
+ yAxis: yAxis
+ };
}
+
+
});
/**
- * @class Ext.chart.axis.Time
- * @extends Ext.chart.axis.Axis
+ * @class Ext.chart.series.Area
+ * @extends Ext.chart.series.Cartesian
*
- * 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.
+ * 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:
*
- * 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 }
+ * ]
+ * });
*
-
- axes: [{
- type: 'Time',
- position: 'bottom',
- fields: 'date',
- title: 'Day',
- dateFormat: 'M d',
- groupBy: 'year,month,day',
- aggregateOp: 'sum',
-
- constrain: true,
- fromDate: new Date('1/1/11'),
- toDate: new Date('1/7/11')
- }]
-
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 500,
+ * height: 300,
+ * store: store,
+ * axes: [
+ * {
+ * type: 'Numeric',
+ * grid: true,
+ * 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'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 betwee 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.
- *
- * @constructor
+ * 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.
+ *
+ * @xtype area
*/
-Ext.define('Ext.chart.axis.Time', {
+Ext.define('Ext.chart.series.Area', {
/* Begin Definitions */
- extend: 'Ext.chart.axis.Category',
-
- alternateClassName: 'Ext.chart.TimeAxis',
+ extend: 'Ext.chart.series.Cartesian',
- alias: 'axis.time',
+ alias: 'series.area',
- requires: ['Ext.data.Store', 'Ext.data.JsonStore'],
+ requires: ['Ext.chart.axis.Axis', 'Ext.draw.Color', 'Ext.fx.Anim'],
/* End Definitions */
- /**
- * The minimum value drawn by the axis. If not set explicitly, the axis
- * minimum will be calculated automatically.
- * @property calculateByLabelSize
- * @type Boolean
- */
- calculateByLabelSize: true,
-
- /**
- * Indicates the format the date will be rendered on.
- * For example: 'M d' will render the dates as 'Jan 30', etc.
- *
- * @property dateFormat
- * @type {String|Boolean}
- */
- dateFormat: false,
-
- /**
- * Indicates the time unit to use for each step. Can be 'day', 'month', 'year' or a comma-separated combination of all of them.
- * Default's 'year,month,day'.
- *
- * @property timeUnit
- * @type {String}
- */
- groupBy: 'year,month,day',
-
- /**
- * Aggregation operation when grouping. Possible options are 'sum', 'avg', 'max', 'min'. Default's 'sum'.
- *
- * @property aggregateOp
- * @type {String}
- */
- aggregateOp: 'sum',
-
- /**
- * The starting date for the time axis.
- * @property fromDate
- * @type Date
- */
- fromDate: false,
-
- /**
- * The ending date for the time axis.
- * @property toDate
- * @type Date
- */
- toDate: false,
-
- /**
- * An array with two components: The first is the unit of the step (day, month, year, etc). The second one is the number of units for the step (1, 2, etc.).
- * Default's [Ext.Date.DAY, 1].
- *
- * @property step
- * @type Array
- */
- step: [Ext.Date.DAY, 1],
-
+ type: 'area',
+
+ // @private Area charts are alyways stacked
+ stacked: true,
+
/**
- * 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.
- * Default's [Ext.Date.DAY, 1].
- *
- * @property constrain
- * @type Boolean
+ * @cfg {Object} style
+ * Append styling properties to this object for it to override theme properties.
*/
- constrain: false,
-
- // @private a wrapper for date methods.
- dateMethods: {
- 'year': function(date) {
- return date.getFullYear();
- },
- 'month': function(date) {
- return date.getMonth() + 1;
- },
- 'day': function(date) {
- return date.getDate();
- },
- 'hour': function(date) {
- return date.getHours();
- },
- 'minute': function(date) {
- return date.getMinutes();
- },
- 'second': function(date) {
- return date.getSeconds();
- },
- 'millisecond': function(date) {
- return date.getMilliseconds();
- }
- },
-
- // @private holds aggregate functions.
- aggregateFn: (function() {
- var etype = (function() {
- var rgxp = /^\[object\s(.*)\]$/,
- toString = Object.prototype.toString;
- return function(e) {
- return toString.call(e).match(rgxp)[1];
- };
- })();
- return {
- 'sum': function(list) {
- var i = 0, l = list.length, acum = 0;
- if (!list.length || etype(list[0]) != 'Number') {
- return list[0];
- }
- for (; i < l; i++) {
- acum += list[i];
- }
- return acum;
- },
- 'max': function(list) {
- if (!list.length || etype(list[0]) != 'Number') {
- return list[0];
- }
- return Math.max.apply(Math, list);
- },
- 'min': function(list) {
- if (!list.length || etype(list[0]) != 'Number') {
- return list[0];
- }
- return Math.min.apply(Math, list);
- },
- 'avg': function(list) {
- var i = 0, l = list.length, acum = 0;
- if (!list.length || etype(list[0]) != 'Number') {
- return list[0];
- }
- for (; i < l; i++) {
- acum += list[i];
- }
- return acum / l;
- }
- };
- })(),
-
- // @private normalized the store to fill date gaps in the time interval.
- constrainDates: function() {
- var fromDate = Ext.Date.clone(this.fromDate),
- toDate = Ext.Date.clone(this.toDate),
- step = this.step,
- field = this.fields,
- store = this.chart.store,
- record, recObj, fieldNames = [],
- newStore = Ext.create('Ext.data.Store', {
- model: store.model
- });
-
- var getRecordByDate = (function() {
- var index = 0, l = store.getCount();
- return function(date) {
- var rec, recDate;
- for (; index < l; index++) {
- rec = store.getAt(index);
- recDate = rec.get(field);
- if (+recDate > +date) {
- return false;
- } else if (+recDate == +date) {
- return rec;
- }
- }
- return false;
- };
- })();
-
- if (!this.constrain) {
- this.chart.filteredStore = this.chart.store;
- return;
- }
+ style: {},
- while(+fromDate <= +toDate) {
- record = getRecordByDate(fromDate);
- recObj = {};
- if (record) {
- newStore.add(record.data);
- } else {
- newStore.model.prototype.fields.each(function(f) {
- recObj[f.name] = false;
- });
- recObj.date = fromDate;
- newStore.add(recObj);
+ constructor: function(config) {
+ this.callParent(arguments);
+ var me = this,
+ surface = me.chart.surface,
+ i, l;
+ Ext.apply(me, config, {
+ __excludes: [],
+ highlightCfg: {
+ lineWidth: 3,
+ stroke: '#55c',
+ opacity: 0.8,
+ color: '#f00'
}
- fromDate = Ext.Date.add(fromDate, step[0], step[1]);
+ });
+ if (me.highlight) {
+ me.highlightSprite = surface.add({
+ type: 'path',
+ path: ['M', 0, 0],
+ zIndex: 1000,
+ opacity: 0.3,
+ lineWidth: 5,
+ hidden: true,
+ stroke: '#444'
+ });
}
-
- this.chart.filteredStore = newStore;
+ me.group = surface.getGroup(me.seriesId);
},
-
- // @private aggregates values if multiple store elements belong to the same time step.
- aggregate: function() {
- var aggStore = {},
- aggKeys = [], key, value,
- op = this.aggregateOp,
- field = this.fields, i,
- fields = this.groupBy.split(','),
- curField,
- recFields = [],
- recFieldsLen = 0,
- obj,
- dates = [],
- json = [],
- l = fields.length,
- dateMethods = this.dateMethods,
- aggregateFn = this.aggregateFn,
- store = this.chart.filteredStore || this.chart.store;
-
- store.each(function(rec) {
- //get all record field names in a simple array
- if (!recFields.length) {
- rec.fields.each(function(f) {
- recFields.push(f.name);
- });
- recFieldsLen = recFields.length;
- }
- //get record date value
- value = rec.get(field);
- //generate key for grouping records
- for (i = 0; i < l; i++) {
- if (i == 0) {
- key = String(dateMethods[fields[i]](value));
- } else {
- key += '||' + dateMethods[fields[i]](value);
- }
- }
- //get aggregation record from hash
- if (key in aggStore) {
- obj = aggStore[key];
- } else {
- obj = aggStore[key] = {};
- aggKeys.push(key);
- dates.push(value);
+
+ // @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];
}
- //append record values to an aggregation record
- for (i = 0; i < recFieldsLen; i++) {
- curField = recFields[i];
- if (!obj[curField]) {
- obj[curField] = [];
+ if (i % ratio == 0) {
+ //push averages
+ xRes.push(xSum/ratio);
+ for (j = 0; j < yCompLen; ++j) {
+ ySum[j] /= ratio;
}
- if (rec.get(curField) !== undefined) {
- obj[curField].push(rec.get(curField));
+ yRes.push(ySum);
+ //reset sum accumulators
+ xSum = 0;
+ for (j = 0, ySum = []; j < yCompLen; ++j) {
+ ySum[j] = 0;
}
}
- });
- //perform aggregation operations on fields
- for (key in aggStore) {
- obj = aggStore[key];
- for (i = 0; i < recFieldsLen; i++) {
- curField = recFields[i];
- obj[curField] = aggregateFn[op](obj[curField]);
- }
- json.push(obj);
- }
- this.chart.substore = Ext.create('Ext.data.JsonStore', {
- fields: recFields,
- data: json
- });
-
- this.dates = dates;
+ }
+ return {
+ x: xRes,
+ y: yRes
+ };
},
-
- // @private creates a label array to be used as the axis labels.
- setLabels: function() {
- var store = this.chart.substore,
- fields = this.fields,
- format = this.dateFormat,
- labels, i, dates = this.dates,
- formatFn = Ext.Date.format;
- this.labels = labels = [];
- store.each(function(record, i) {
- if (!format) {
- labels.push(record.get(fields));
- } else {
- labels.push(formatFn(dates[i], format));
+
+ // @private Get chart and data boundaries
+ getBounds: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ 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,
+ bbox, xScale, yScale, xValue, yValue, areaIndex, acumY, ln, sumValues, clipBox, areaElem;
+
+ me.setBBox();
+ bbox = me.bbox;
+
+ // Run through the axis
+ if (me.axis) {
+ axis = chart.axes.get(me.axis);
+ if (axis) {
+ out = axis.calcEnds();
+ minY = out.from || axis.prevMin;
+ maxY = mmax(out.to || axis.prevMax, 0);
}
- }, this);
- },
+ }
- processView: function() {
- //TODO(nico): fix this eventually...
- if (this.constrain) {
- this.constrainDates();
- this.aggregate();
- this.chart.substore = this.chart.filteredStore;
- } else {
- this.aggregate();
- }
- },
+ if (me.yField && !Ext.isNumber(minY)) {
+ axis = Ext.create('Ext.chart.axis.Axis', {
+ chart: chart,
+ fields: [].concat(me.yField)
+ });
+ out = axis.calcEnds();
+ minY = out.from || axis.prevMin;
+ maxY = mmax(out.to || axis.prevMax, 0);
+ }
- // @private modifies the store and creates the labels for the axes.
- applyData: function() {
- this.setLabels();
- var count = this.chart.substore.getCount();
- return {
- from: 0,
- to: count,
- steps: count - 1,
- step: 1
- };
- }
- });
+ if (!Ext.isNumber(minY)) {
+ minY = 0;
+ }
+ if (!Ext.isNumber(maxY)) {
+ maxY = 0;
+ }
+ store.each(function(record, i) {
+ xValue = record.get(me.xField);
+ yValue = [];
+ if (typeof xValue != 'number') {
+ xValue = i;
+ }
+ xValues.push(xValue);
+ acumY = 0;
+ for (areaIndex = 0; areaIndex < areasLen; areaIndex++) {
+ areaElem = record.get(areas[areaIndex]);
+ if (typeof areaElem == 'number') {
+ minY = mmin(minY, areaElem);
+ yValue.push(areaElem);
+ acumY += areaElem;
+ }
+ }
+ minX = mmin(minX, xValue);
+ maxX = mmax(maxX, xValue);
+ maxY = mmax(maxY, acumY);
+ yValues.push(yValue);
+ }, me);
-/**
- * @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:
- *
- * - `itemmouseup` When the user interacts with a marker.
- * - `itemmousedown` When the user interacts with a marker.
- * - `itemmousemove` When the user iteracts 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', {
+ xScale = bbox.width / ((maxX - minX) || 1);
+ yScale = bbox.height / ((maxY - minY) || 1);
- /* Begin Definitions */
+ ln = xValues.length;
+ if ((ln > bbox.width) && me.areas) {
+ sumValues = me.shrink(xValues, yValues, bbox.width);
+ xValues = sumValues.x;
+ yValues = sumValues.y;
+ }
- mixins: {
- observable: 'Ext.util.Observable',
- labels: 'Ext.chart.Label',
- highlights: 'Ext.chart.Highlight',
- tips: 'Ext.chart.Tip',
- callouts: 'Ext.chart.Callout'
+ return {
+ bbox: bbox,
+ minX: minX,
+ minY: minY,
+ xValues: xValues,
+ yValues: yValues,
+ xScale: xScale,
+ yScale: yScale,
+ areasLen: areasLen
+ };
},
- /* 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.
- */
+ // @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,
+ paths = [],
+ i, ln, x, y, xValue, yValue, acumY, areaIndex, prevAreaIndex, areaElem, path;
- /**
- * @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,
- * width: 140,
- * height: 28,
- * renderer: function(storeItem, item) {
- * this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' views');
- * }
- * },
- */
+ ln = bounds.xValues.length;
+ // Start the path
+ for (i = 0; i < ln; i++) {
+ xValue = bounds.xValues[i];
+ yValue = bounds.yValues[i];
+ x = bbox.x + (xValue - bounds.minX) * bounds.xScale;
+ acumY = 0;
+ for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
+ // Excluded series
+ if (me.__excludes[areaIndex]) {
+ continue;
+ }
+ if (!componentPaths[areaIndex]) {
+ componentPaths[areaIndex] = [];
+ }
+ areaElem = yValue[areaIndex];
+ 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]);
+ }
+ }
- /**
- * @cfg {String} type
- * The type of series. Set in subclasses.
- */
- type: null,
+ // 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', bbox.x, bbox.y + bbox.height,
+ 'Z');
+ }
+ // Close other paths to the one before them
+ else {
+ 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', bbox.x, path[2], 'Z');
+ }
+ prevAreaIndex = areaIndex;
+ }
+ return {
+ paths: paths,
+ areasLen: bounds.areasLen
+ };
+ },
/**
- * @cfg {String} title
- * The human-readable name of the series.
+ * Draws the series for the current chart.
*/
- title: null,
+ 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,
+ areaIndex, areaElem, paths, path, rendererAttributes;
- /**
- * @cfg {Boolean} showInLegend
- * Whether to show this series in the legend.
- */
- showInLegend: true,
+ me.unHighlightItem();
+ me.cleanHighlights();
- /**
- * @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.
- */
- renderer: function(sprite, record, attributes, index, store) {
- return attributes;
- },
+ if (!store || !store.getCount()) {
+ return;
+ }
- /**
- * @cfg {Array} shadowAttributes
- * An array with shadow attributes
- */
- shadowAttributes: null,
-
- //@private triggerdrawlistener flag
- triggerAfterDraw: false,
+ paths = me.getPaths();
- /**
- * @cfg {Object} listeners
- * An (optional) object with event callbacks. All event callbacks get the target *item* as first parameter. The callback functions are:
- *
- *
- * - itemmouseover
- * - itemmouseout
- * - itemmousedown
- * - itemmouseup
- *
- */
-
- constructor: function(config) {
- var me = this;
- if (config) {
- Ext.apply(me, config);
+ if (!me.areas) {
+ me.areas = [];
}
-
- 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.addEvents({
- scope: me,
- itemmouseover: true,
- itemmouseout: true,
- itemmousedown: true,
- itemmouseup: true,
- mouseleave: true,
- afterdraw: true,
+ for (areaIndex = 0; areaIndex < paths.areasLen; areaIndex++) {
+ // Excluded series
+ if (me.__excludes[areaIndex]) {
+ continue;
+ }
+ 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[areaIndex % colorArrayLength],
+ fill: colorArrayStyle[areaIndex % 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[areaIndex % colorArrayLength],
+ stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength]
+ }, areaIndex, store);
+ me.areas[areaIndex].setAttributes(rendererAttributes, true);
+ }
+ }
+ me.renderLabels();
+ me.renderCallouts();
+ },
- /**
- * @event titlechange
- * Fires when the series title is changed via {@link #setTitle}.
- * @param {String} title The new title value
- * @param {Number} index The index in the collection of titles
- */
- titlechange: true
- });
+ // @private
+ onAnimate: function(sprite, attr) {
+ sprite.show();
+ return this.callParent(arguments);
+ },
- me.mixins.observable.constructor.call(me, config);
+ // @private
+ onCreateLabel: function(storeItem, item, i, display) {
+ var me = this,
+ group = me.labelsGroup,
+ config = me.label,
+ bbox = me.bbox,
+ endLabelStyle = Ext.apply(config, me.seriesLabelStyle);
- me.on({
- scope: me,
- itemmouseover: me.onItemMouseOver,
- itemmouseout: me.onItemMouseOut,
- mouseleave: me.onMouseLeave
- });
+ return me.chart.surface.add(Ext.apply({
+ 'type': 'text',
+ 'text-anchor': 'middle',
+ 'group': group,
+ 'x': item.point[0],
+ 'y': bbox.y + bbox.height / 2
+ }, endLabelStyle || {}));
},
- // @private set the bbox and clipBox for the series
- setBBox: function(noGutter) {
+ // @private
+ onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
var me = this,
chart = me.chart,
- chartBBox = chart.chartBBox,
- gutterX = noGutter ? 0 : chart.maxGutter[0],
- gutterY = noGutter ? 0 : chart.maxGutter[1],
- clipBox, bbox;
+ resizing = chart.resizing,
+ config = me.label,
+ format = config.renderer,
+ field = config.field,
+ bbox = me.bbox,
+ x = item.point[0],
+ y = item.point[1],
+ bb, width, height;
- clipBox = {
- x: chartBBox.x,
- y: chartBBox.y,
- width: chartBBox.width,
- height: chartBBox.height
- };
- me.clipBox = clipBox;
+ label.setAttributes({
+ text: format(storeItem.get(field[index])),
+ hidden: true
+ }, true);
- bbox = {
- x: (clipBox.x + gutterX) - (chart.zoom.x * chart.zoom.width),
- y: (clipBox.y + gutterY) - (chart.zoom.y * chart.zoom.height),
- width: (clipBox.width - (gutterX * 2)) * chart.zoom.width,
- height: (clipBox.height - (gutterY * 2)) * chart.zoom.height
- };
- me.bbox = bbox;
- },
+ bb = label.getBBox();
+ width = bb.width / 2;
+ height = bb.height / 2;
- // @private set the animation for the sprite
- onAnimate: function(sprite, attr) {
- var me = this;
- sprite.stopAnimation();
- if (me.triggerAfterDraw) {
- return sprite.animate(Ext.applyIf(attr, me.chart.animate));
+ x = x - width < bbox.x? bbox.x + width : x;
+ x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
+ y = y - height < bbox.y? bbox.y + height : y;
+ y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
+
+ if (me.chart.animate && !me.chart.resizing) {
+ label.show(true);
+ me.onAnimate(label, {
+ to: {
+ x: x,
+ y: y
+ }
+ });
} else {
- me.triggerAfterDraw = true;
- return sprite.animate(Ext.apply(Ext.applyIf(attr, me.chart.animate), {
- listeners: {
- 'afteranimate': function() {
- me.triggerAfterDraw = false;
- me.fireEvent('afterrender');
- }
- }
- }));
+ label.setAttributes({
+ x: x,
+ y: y
+ }, true);
+ if (resizing) {
+ me.animation.on('afteranimate', function() {
+ label.show(true);
+ });
+ } else {
+ label.show(true);
+ }
}
},
-
- // @private return the gutter.
- getGutters: function() {
- return [0, 0];
- },
- // @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
+ 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.label.getBBox(),
+ offsetFromViz = 30,
+ offsetToSide = 10,
+ offsetBox = 3,
+ boxx, boxy, boxw, boxh,
+ p, clipRect = me.clipRect,
+ 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;
}
- },
- // @private wrapper for the itemmouseout event.
- onItemMouseOut: function(item) {
- var me = this;
- if (item.series === me) {
- me.unHighlightItem();
- if (me.tooltip) {
- me.hideTip(item);
- }
+ //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;
}
- },
- // @private wrapper for the mouseleave event.
- onMouseLeave: function() {
- var me = this;
- me.unHighlightItem();
- if (me.tooltip) {
- me.hideTip();
+ //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);
}
},
- /**
- * 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 at least the following:
- *
- * - {Ext.chart.series.Series} series - the Series object to which the item belongs
- * - {Object} value - the value(s) of the item's data point
- * - {Array} 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.
- * - {Ext.draw.Sprite} 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;
- }
+ isItemInPoint: function(x, y, item, i) {
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];
+ pointsUp = item.pointsUp,
+ pointsDown = item.pointsDown,
+ abs = Math.abs,
+ dist = Infinity, p, pln, point;
+
+ for (p = 0, pln = pointsUp.length; p < pln; p++) {
+ point = [pointsUp[p][0], pointsUp[p][1]];
+ if (dist > abs(x - point[0])) {
+ dist = abs(x - point[0]);
+ } else {
+ point = pointsUp[p -1];
+ if (y >= point[1] && (!pointsDown.length || y <= (pointsDown[p -1][1]))) {
+ item.storeIndex = p -1;
+ item.storeField = me.yField[i];
+ item.storeItem = me.chart.store.getAt(p -1);
+ item._points = pointsDown.length? [point, pointsDown[p -1]] : [point];
+ return true;
+ } else {
+ break;
+ }
}
}
-
- return null;
- },
-
- isItemInPoint: function(x, y, item, i) {
return false;
},
/**
- * Hides all the elements in the series.
+ * Highlight this entire series.
+ * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
*/
- hideAll: function() {
- var me = this,
- items = me.items,
- item, len, i, sprite;
-
- 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);
+ 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 = Ext.create('Ext.fx.Anim', Ext.apply({
+ target: area,
+ to: to
+ }, this.chart.animate));
+ }
+ else {
+ area.setAttributes(to, 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;
- },
-
- /**
- * Returns a string with the color to be used for the series legend item.
+ * UnHighlight this entire series.
+ * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
*/
- 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;
+ 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 = Ext.create('Ext.fx.Anim', {
+ target: area,
+ to: {
+ fill: area.__prevFill,
+ opacity: area.__prevOpacity,
+ lineWidth: area.__prevLineWidth
+ }
+ });
}
- return stroke;
}
- return '#000';
},
-
+
/**
- * Checks whether the data field should be visible in the legend
- * @private
- * @param {Number} index The index of the current item
+ * 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
*/
- visibleInLegend: function(index){
- var excludes = this.__excludes;
- if (excludes) {
- return !excludes[index];
+ highlightItem: function(item) {
+ var me = this,
+ points, path;
+ if (!item) {
+ this.highlightSeries();
+ return;
}
- return !this.seriesIsHidden;
+ points = item._points;
+ path = points.length == 2? ['M', points[0][0], points[0][1], 'L', points[1][0], points[1][1]]
+ : ['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);
},
/**
- * Changes the value of the {@link #title} for the series.
- * Arguments can take two forms:
- *
- * - A single String value: this will be used as the new single title for the series (applies
- * to series with only one yField)
- * - A numeric index and a String value: this will set the title for a single indexed yField.
- *
- * @param {Number} index
- * @param {String} title
+ * 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
*/
- setTitle: function(index, title) {
- var me = this,
- oldTitle = me.title;
-
- if (Ext.isString(index)) {
- title = index;
- index = 0;
+ unHighlightItem: function(item) {
+ if (!item) {
+ this.unHighlightSeries();
}
- if (Ext.isArray(oldTitle)) {
- oldTitle[index] = title;
- } else {
- me.title = title;
+ if (this.highlightSprite) {
+ this.highlightSprite.hide(true);
}
+ },
- me.fireEvent('titlechange', title, index);
- }
-});
-
-/**
- * @class Ext.chart.series.Cartesian
- * @extends Ext.chart.series.Series
- *
- * Common base class for series implementations which plot values using x/y coordinates.
- *
- * @constructor
- */
-Ext.define('Ext.chart.series.Cartesian', {
-
- /* Begin Definitions */
-
- extend: 'Ext.chart.series.Series',
-
- alternateClassName: ['Ext.chart.CartesianSeries', 'Ext.chart.CartesianChart'],
-
- /* End Definitions */
-
- /**
- * The field used to access the x axis value from the items from the data
- * source.
- *
- * @cfg xField
- * @type String
- */
- xField: null,
+ // @private
+ hideAll: function() {
+ if (!isNaN(this._index)) {
+ this.__excludes[this._index] = true;
+ this.areas[this._index].hide(true);
+ this.drawSeries();
+ }
+ },
- /**
- * The field used to access the y-axis value from the items from the data
- * source.
- *
- * @cfg yField
- * @type String
- */
- yField: null,
+ // @private
+ showAll: function() {
+ if (!isNaN(this._index)) {
+ this.__excludes[this._index] = false;
+ this.areas[this._index].show(true);
+ this.drawSeries();
+ }
+ },
/**
- * Indicates which axis the series will bind to
- *
- * @property axis
- * @type String
+ * 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
*/
- axis: 'left'
+ getLegendColor: function(index) {
+ var me = this;
+ return me.colorArrayStyle[index % me.colorArrayStyle.length];
+ }
});
-
/**
* @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:
-
-{@img Ext.chart.series.Area/Ext.chart.series.Area.png Ext.chart.series.Area chart series}
-
- 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',
- grid: true,
- 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.
-
-
+ *
+ * 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',
+ * grid: true,
+ * 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.
+ *
* @xtype area
- *
*/
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'],
@@ -45731,7 +49439,7 @@ Ext.define('Ext.chart.series.Area', {
stacked: true,
/**
- * @cfg {Object} style
+ * @cfg {Object} style
* Append styling properties to this object for it to override theme properties.
*/
style: {},
@@ -45807,7 +49515,7 @@ Ext.define('Ext.chart.series.Area', {
getBounds: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
areas = [].concat(me.yField),
areasLen = areas.length,
xValues = [],
@@ -45874,8 +49582,8 @@ Ext.define('Ext.chart.series.Area', {
yValues.push(yValue);
}, me);
- xScale = bbox.width / (maxX - minX);
- yScale = bbox.height / (maxY - minY);
+ xScale = bbox.width / ((maxX - minX) || 1);
+ yScale = bbox.height / ((maxY - minY) || 1);
ln = xValues.length;
if ((ln > bbox.width) && me.areas) {
@@ -45900,7 +49608,7 @@ Ext.define('Ext.chart.series.Area', {
getPaths: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
first = true,
bounds = me.getBounds(),
bbox = bounds.bbox,
@@ -45945,7 +49653,7 @@ Ext.define('Ext.chart.series.Area', {
items[areaIndex].pointsUp.push([x, y]);
}
}
-
+
// Close the paths
for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
// Excluded series
@@ -45987,7 +49695,7 @@ Ext.define('Ext.chart.series.Area', {
drawSeries: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
surface = chart.surface,
animate = chart.animate,
group = me.group,
@@ -46002,7 +49710,7 @@ Ext.define('Ext.chart.series.Area', {
if (!store || !store.getCount()) {
return;
}
-
+
paths = me.getPaths();
if (!me.areas) {
@@ -46028,7 +49736,7 @@ Ext.define('Ext.chart.series.Area', {
path = paths.paths[areaIndex];
if (animate) {
//Add renderer to line. There is not a unique record associated with this.
- rendererAttributes = me.renderer(areaElem, false, {
+ rendererAttributes = me.renderer(areaElem, false, {
path: path,
// 'clip-rect': me.clipBox,
fill: colorArrayStyle[areaIndex % colorArrayLength],
@@ -46039,7 +49747,7 @@ Ext.define('Ext.chart.series.Area', {
to: rendererAttributes
});
} else {
- rendererAttributes = me.renderer(areaElem, false, {
+ rendererAttributes = me.renderer(areaElem, false, {
path: path,
// 'clip-rect': me.clipBox,
hidden: false,
@@ -46088,16 +49796,16 @@ Ext.define('Ext.chart.series.Area', {
x = item.point[0],
y = item.point[1],
bb, width, height;
-
+
label.setAttributes({
text: format(storeItem.get(field[index])),
hidden: true
}, true);
-
+
bb = label.getBBox();
width = bb.width / 2;
height = bb.height / 2;
-
+
x = x - width < bbox.x? bbox.x + width : x;
x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
y = y - height < bbox.y? bbox.y + height : y;
@@ -46156,11 +49864,11 @@ Ext.define('Ext.chart.series.Area', {
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;
@@ -46173,13 +49881,13 @@ Ext.define('Ext.chart.series.Area', {
//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])) {
@@ -46192,13 +49900,13 @@ Ext.define('Ext.chart.series.Area', {
//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"]
@@ -46219,14 +49927,14 @@ Ext.define('Ext.chart.series.Area', {
callout[p].show(true);
}
},
-
+
isItemInPoint: function(x, y, item, i) {
var me = this,
pointsUp = item.pointsUp,
pointsDown = item.pointsDown,
abs = Math.abs,
dist = Infinity, p, pln, point;
-
+
for (p = 0, pln = pointsUp.length; p < pln; p++) {
point = [pointsUp[p][0], pointsUp[p][1]];
if (dist > abs(x - point[0])) {
@@ -46377,19 +50085,18 @@ Ext.define('Ext.chart.series.Area', {
* 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:
*
- * {@img Ext.chart.series.Bar/Ext.chart.series.Bar.png Ext.chart.series.Bar chart series}
- *
+ * @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}
+ * { '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,
@@ -46463,12 +50170,12 @@ Ext.define('Ext.chart.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
*/
@@ -46502,7 +50209,7 @@ Ext.define('Ext.chart.series.Bar', {
opacity: 0.8,
color: '#f00'
},
-
+
shadowAttributes: [{
"stroke-width": 6,
"stroke-opacity": 0.05,
@@ -46540,11 +50247,11 @@ Ext.define('Ext.chart.series.Bar', {
// @private sets the bar girth.
getBarGirth: function() {
var me = this,
- store = me.chart.store,
+ store = me.chart.getChartStore(),
column = me.column,
ln = store.getCount(),
gutter = me.gutter / 100;
-
+
return (me.chart.chartBBox[column ? 'width' : 'height'] - me[column ? 'xPadding' : 'yPadding'] * 2) / (ln * (gutter + 1) - gutter);
},
@@ -46560,7 +50267,7 @@ Ext.define('Ext.chart.series.Bar', {
getBounds: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
bars = [].concat(me.yField),
barsLen = bars.length,
groupBarsLen = barsLen,
@@ -46592,8 +50299,8 @@ Ext.define('Ext.chart.series.Bar', {
axis = chart.axes.get(me.axis);
if (axis) {
out = axis.calcEnds();
- minY = out.from || axis.prevMin;
- maxY = mmax(out.to || axis.prevMax, 0);
+ minY = out.from;
+ maxY = out.to;
}
}
@@ -46603,8 +50310,8 @@ Ext.define('Ext.chart.series.Bar', {
fields: [].concat(me.yField)
});
out = axis.calcEnds();
- minY = out.from || axis.prevMin;
- maxY = mmax(out.to || axis.prevMax, 0);
+ minY = out.from;
+ maxY = out.to;
}
if (!Ext.isNumber(minY)) {
@@ -46661,7 +50368,7 @@ Ext.define('Ext.chart.series.Bar', {
getPaths: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
bounds = me.bounds = me.getBounds(),
items = me.items = [],
gutter = me.gutter / 100,
@@ -46692,14 +50399,14 @@ Ext.define('Ext.chart.series.Bar', {
top = bounds.zero;
totalDim = 0;
totalNegDim = 0;
- hasShadow = false;
+ hasShadow = false;
for (j = 0, counter = 0; j < barsLen; j++) {
// Excluded series
if (me.__excludes && me.__excludes[j]) {
continue;
}
yValue = record.get(bounds.bars[j]);
- height = Math.round((yValue - ((bounds.minY < 0) ? 0 : bounds.minY)) * bounds.scale);
+ height = Math.round((yValue - mmax(bounds.minY, 0)) * bounds.scale);
barAttr = {
fill: colors[(barsLen > 1 ? j : 0) % colorLength]
};
@@ -46805,7 +50512,7 @@ Ext.define('Ext.chart.series.Bar', {
shadowGroups = me.shadowGroups,
shadowAttributes = me.shadowAttributes,
shadowGroupsLn = shadowGroups.length,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
column = me.column,
items = me.items,
shadows = [],
@@ -46863,7 +50570,7 @@ Ext.define('Ext.chart.series.Bar', {
drawSeries: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
surface = chart.surface,
animate = chart.animate,
stacked = me.stacked,
@@ -46875,11 +50582,11 @@ Ext.define('Ext.chart.series.Bar', {
seriesStyle = me.seriesStyle,
items, ln, i, j, baseAttrs, sprite, rendererAttributes, shadowIndex, shadowGroup,
bounds, endSeriesStyle, barAttr, attrs, anim;
-
+
if (!store || !store.getCount()) {
return;
}
-
+
//fill colors are taken from the colors array.
delete seriesStyle.fill;
endSeriesStyle = Ext.apply(seriesStyle, this.style);
@@ -46953,7 +50660,7 @@ Ext.define('Ext.chart.series.Bar', {
}
me.renderLabels();
},
-
+
// @private handled when creating a label.
onCreateLabel: function(storeItem, item, i, display) {
var me = this,
@@ -46967,9 +50674,9 @@ Ext.define('Ext.chart.series.Bar', {
group: group
}, endLabelStyle || {}));
},
-
+
// @private callback used when placing a label.
- onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
+ onPlaceLabel: function(label, storeItem, item, i, display, animate, j, 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,
@@ -47003,6 +50710,7 @@ Ext.define('Ext.chart.series.Bar', {
text: text
});
+ label.isOutside = false;
if (column) {
if (display == outside) {
if (height + offsetY + attr.height > (yValue >= 0 ? zero - chartBBox.y : chartBBox.y + chartBBox.height - zero)) {
@@ -47011,6 +50719,7 @@ Ext.define('Ext.chart.series.Bar', {
} else {
if (height + offsetY > attr.height) {
display = outside;
+ label.isOutside = true;
}
}
x = attr.x + groupBarWidth / 2;
@@ -47028,6 +50737,7 @@ Ext.define('Ext.chart.series.Bar', {
else {
if (width + offsetX > attr.width) {
display = outside;
+ label.isOutside = true;
}
}
x = display == insideStart ?
@@ -47120,14 +50830,14 @@ Ext.define('Ext.chart.series.Bar', {
sprite.show();
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() {
var axes = this.chart.axes;
@@ -47157,109 +50867,115 @@ Ext.define('Ext.chart.series.Bar', {
});
}
},
-
+
/**
* Returns a string with the color to be used for the series legend item.
* @param index
*/
getLegendColor: function(index) {
- var me = this;
- return me.colorArrayStyle[index % me.colorArrayStyle.length];
+ var me = this,
+ colorLength = me.colorArrayStyle.length;
+
+ if (me.style && me.style.fill) {
+ return me.style.fill;
+ } else {
+ return me.colorArrayStyle[index % colorLength];
+ }
+ },
+
+ highlightItem: function(item) {
+ this.callParent(arguments);
+ this.renderLabels();
+ },
+
+ unHighlightItem: function() {
+ this.callParent(arguments);
+ this.renderLabels();
+ },
+
+ cleanHighlights: function() {
+ this.callParent(arguments);
+ this.renderLabels();
}
});
/**
* @class Ext.chart.series.Column
* @extends Ext.chart.series.Bar
- *
-
- 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:
-
-{@img Ext.chart.series.Column/Ext.chart.series.Column.png Ext.chart.series.Column chart series
-
- 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,
- axes: [{
- type: 'Numeric',
- position: 'bottom',
- fields: ['data1'],
- label: {
- renderer: Ext.util.Format.numberRenderer('0,0')
- },
- title: 'Sample Values',
- grid: true,
- minimum: 0
- }, {
- type: 'Category',
- position: 'left',
- fields: ['name'],
- title: 'Sample Metrics'
- }],
- axes: [{
- type: 'Numeric',
- position: 'left',
- fields: ['data1'],
- 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('data1') + ' $');
- }
- },
- label: {
- display: 'insideEnd',
- 'text-anchor': 'middle',
- field: 'data1',
- renderer: Ext.util.Format.numberRenderer('0'),
- orientation: 'vertical',
- color: '#333'
- },
- xField: 'name',
- yField: 'data1'
- }]
- });
-
-
-
- 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.
-
+ *
+ * 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', '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,
+ * axes: [
+ * {
+ * type: 'Numeric',
+ * position: 'left',
+ * fields: ['data1'],
+ * 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('data1') + ' $');
+ * }
+ * },
+ * label: {
+ * display: 'insideEnd',
+ * 'text-anchor': 'middle',
+ * field: 'data1',
+ * renderer: Ext.util.Format.numberRenderer('0'),
+ * orientation: 'vertical',
+ * color: '#333'
+ * },
+ * xField: 'name',
+ * yField: 'data1'
+ * }
+ * ]
+ * });
+ *
+ * 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 */
@@ -47292,7 +51008,7 @@ Ext.define('Ext.chart.series.Column', {
* @extends Ext.chart.series.Series
*
* 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 instanciating the
+ * 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.
*
* A chart/series configuration for the Gauge visualization could look like this:
@@ -47342,10 +51058,9 @@ Ext.define('Ext.chart.series.Gauge', {
highlightDuration: 150,
/**
- * @cfg {String} angleField
+ * @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.
- * This parameter is required.
*/
angleField: false,
@@ -47356,7 +51071,7 @@ Ext.define('Ext.chart.series.Gauge', {
needle: false,
/**
- * @cfg {Boolean|Number} donut
+ * @cfg {Boolean/Number} donut
* Use the entire disk or just a fraction of it for the gauge. Default's false.
*/
donut: false,
@@ -47423,7 +51138,7 @@ Ext.define('Ext.chart.series.Gauge', {
//@private updates some onbefore render parameters.
initialize: function() {
var me = this,
- store = me.chart.substore || me.chart.store;
+ store = me.chart.getChartStore();
//Add yFields to be used in Legend.js
me.yField = [];
if (me.label.field) {
@@ -47524,7 +51239,7 @@ Ext.define('Ext.chart.series.Gauge', {
drawSeries: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
group = me.group,
animate = me.chart.animate,
axis = me.chart.axes.get(0),
@@ -47745,91 +51460,98 @@ Ext.define('Ext.chart.series.Gauge', {
/**
* @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:
-
-{@img Ext.chart.series.Line/Ext.chart.series.Line.png Ext.chart.series.Line chart series}
-
- 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,
- axes: [{
- type: 'Numeric',
- position: 'bottom',
- fields: ['data1'],
- 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: 'line',
- highlight: {
- size: 7,
- radius: 7
- },
- axis: 'left',
- xField: 'name',
- yField: 'data1',
- markerCfg: {
- type: 'cross',
- size: 4,
- radius: 4,
- 'stroke-width': 0
- }
- }, {
- type: 'line',
- highlight: {
- size: 7,
- radius: 7
- },
- axis: 'left',
- fill: true,
- xField: 'name',
- yField: 'data3',
- markerCfg: {
- 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.
-
+ *
+ * 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.
+ *
+ * **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 */
@@ -47843,15 +51565,22 @@ Ext.define('Ext.chart.series.Line', {
/* End Definitions */
type: 'line',
-
+
alias: 'series.line',
+ /**
+ * @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 line series to the ones in the axis, otherwise a
+ * relative scale will be used.
+ */
+
/**
* @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,
@@ -47873,28 +51602,52 @@ Ext.define('Ext.chart.series.Line', {
'fill': '#f00'
}
-
+
*/
markerConfig: {},
/**
* @cfg {Object} style
- * An object containing styles for the visualization lines. These styles will override the theme styles.
- * Some options contained within the style object will are described next.
+ * 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} smooth
- * If true, the line will be smoothed/rounded around its points, otherwise straight line
- * segments will be drawn. Defaults to false.
+ * @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.
+ * 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,
@@ -47939,12 +51692,12 @@ Ext.define('Ext.chart.series.Line', {
me.markerGroup = surface.getGroup(me.seriesId + '-markers');
}
if (shadow) {
- for (i = 0, l = this.shadowAttributes.length; i < l; i++) {
+ 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...
@@ -47955,7 +51708,7 @@ Ext.define('Ext.chart.series.Line', {
ySum = 0,
xRes = [xValues[0]],
yRes = [yValues[0]];
-
+
for (; i < len; ++i) {
xSum += xValues[i] || 0;
ySum += yValues[i] || 0;
@@ -47978,48 +51731,68 @@ Ext.define('Ext.chart.series.Line', {
drawSeries: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
- surface = chart.surface,
- chartBBox = chart.chartBBox,
+ chartAxes = chart.axes,
+ store = chart.getChartStore(),
+ storeCount = store.getCount(),
+ surface = me.chart.surface,
bbox = {},
group = me.group,
- gutterX = chart.maxGutter[0],
- gutterY = chart.maxGutter[1],
showMarkers = me.showMarkers,
markerGroup = me.markerGroup,
enableShadows = chart.shadow,
shadowGroups = me.shadowGroups,
- shadowAttributes = this.shadowAttributes,
+ shadowAttributes = me.shadowAttributes,
+ smooth = me.smooth,
lnsh = shadowGroups.length,
dummyPath = ["M"],
path = ["M"],
+ renderPath = ["M"],
+ smoothPath = ["M"],
markerIndex = chart.markerIndex,
axes = [].concat(me.axis),
- shadowGroup,
shadowBarAttr,
xValues = [],
+ xValueMap = {},
yValues = [],
+ yValueMap = {},
onbreak = false,
+ storeIndices = [],
markerStyle = me.markerStyle,
- seriesStyle = me.seriesStyle,
- seriesLabelStyle = me.seriesLabelStyle,
+ seriesStyle = me.style,
colorArrayStyle = me.colorArrayStyle,
colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
- seriesIdx = me.seriesIdx, shadows, shadow, shindex, fromPath, fill, fillPath, rendererAttributes,
- x, y, prevX, prevY, firstY, markerCount, i, j, ln, axis, ends, marker, markerAux, item, xValue,
+ isNumber = Ext.isNumber,
+ seriesIdx = me.seriesIdx,
+ boundAxes = me.getAxesForXAndYFields(),
+ boundXAxis = boundAxes.xAxis,
+ boundYAxis = boundAxes.yAxis,
+ 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, props, firstMarker;
-
- //if store is empty then there's nothing to draw.
- if (!store || !store.getCount()) {
+ endLineStyle, type, count, items;
+
+ 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) {
+ items = this.items;
+ if (items) {
+ for (i = 0, ln = items.length; i < ln; ++i) {
+ if (items[i].sprite) {
+ items[i].sprite.hide(true);
+ }
+ }
+ }
+ return;
+ }
+
//prepare style objects for line and markers
- endMarkerStyle = Ext.apply(markerStyle, me.markerConfig);
+ endMarkerStyle = Ext.apply(markerStyle || {}, me.markerConfig);
type = endMarkerStyle.type;
delete endMarkerStyle.type;
- endLineStyle = Ext.apply(seriesStyle, me.style);
+ endLineStyle = seriesStyle;
//if no stroke with is specified force it to 0.5 because this is
//about making *lines*
if (!endLineStyle['stroke-width']) {
@@ -48043,17 +51816,15 @@ Ext.define('Ext.chart.series.Line', {
}, true);
}
}
-
+
me.unHighlightItem();
me.cleanHighlights();
me.setBBox();
bbox = me.bbox;
-
me.clipRect = [bbox.x, bbox.y, bbox.width, bbox.height];
-
- for (i = 0, ln = axes.length; i < ln; i++) {
- axis = chart.axes.get(axes[i]);
+ for (i = 0, ln = axes.length; i < ln; i++) {
+ axis = chartAxes.get(axes[i]);
if (axis) {
ends = axis.calcEnds();
if (axis.position == 'top' || axis.position == 'bottom') {
@@ -48069,8 +51840,9 @@ Ext.define('Ext.chart.series.Line', {
// If a field was specified without a corresponding axis, create one to get bounds
//only do this for the axis where real values are bound (that's why we check for
//me.axis)
- if (me.xField && !Ext.isNumber(minX)
- && (me.axis == 'bottom' || me.axis == 'top')) {
+ if (me.xField && !isNumber(minX) &&
+ (boundXAxis == 'bottom' || boundXAxis == 'top') &&
+ !chartAxes.get(boundXAxis)) {
axis = Ext.create('Ext.chart.axis.Axis', {
chart: chart,
fields: [].concat(me.xField)
@@ -48078,8 +51850,9 @@ Ext.define('Ext.chart.series.Line', {
minX = axis.from;
maxX = axis.to;
}
- if (me.yField && !Ext.isNumber(minY)
- && (me.axis == 'right' || me.axis == 'left')) {
+ if (me.yField && !isNumber(minY) &&
+ (boundYAxis == 'right' || boundYAxis == 'left') &&
+ !chartAxes.get(boundYAxis)) {
axis = Ext.create('Ext.chart.axis.Axis', {
chart: chart,
fields: [].concat(me.yField)
@@ -48087,25 +51860,38 @@ Ext.define('Ext.chart.series.Line', {
minY = axis.from;
maxY = axis.to;
}
-
if (isNaN(minX)) {
minX = 0;
- xScale = bbox.width / (store.getCount() - 1);
+ xScale = bbox.width / ((storeCount - 1) || 1);
}
else {
- xScale = bbox.width / (maxX - minX);
+ xScale = bbox.width / ((maxX - minX) || (storeCount -1) || 1);
}
if (isNaN(minY)) {
minY = 0;
- yScale = bbox.height / (store.getCount() - 1);
- }
+ yScale = bbox.height / ((storeCount - 1) || 1);
+ }
else {
- yScale = bbox.height / (maxY - minY);
+ yScale = bbox.height / ((maxY - minY) || (storeCount - 1) || 1);
}
- store.each(function(record, i) {
+ // Extract all x and y values from the store
+ me.eachRecord(function(record, i) {
xValue = record.get(me.xField);
+
+ // Ensure a value
+ if (typeof xValue == 'string' || typeof xValue == 'object' && !Ext.isDate(xValue)
+ //set as uniform distribution if the axis is a category axis.
+ || boundXAxis && chartAxes.get(boundXAxis) && chartAxes.get(boundXAxis).type == 'Category') {
+ if (xValue in xValueMap) {
+ xValue = xValueMap[xValue];
+ } else {
+ xValue = xValueMap[xValue] = i;
+ }
+ }
+
+ // Filter out values that don't fit within the pan/zoom buffer area
yValue = record.get(me.yField);
//skip undefined values
if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) {
@@ -48117,19 +51903,15 @@ Ext.define('Ext.chart.series.Line', {
return;
}
// Ensure a value
- if (typeof xValue == 'string' || typeof xValue == 'object'
- //set as uniform distribution if the axis is a category axis.
- || (me.axis != 'top' && me.axis != 'bottom')) {
- xValue = i;
- }
- if (typeof yValue == 'string' || typeof yValue == 'object'
+ if (typeof yValue == 'string' || typeof yValue == 'object' && !Ext.isDate(yValue)
//set as uniform distribution if the axis is a category axis.
- || (me.axis != 'left' && me.axis != 'right')) {
+ || boundYAxis && chartAxes.get(boundYAxis) && chartAxes.get(boundYAxis).type == 'Category') {
yValue = i;
}
+ storeIndices.push(i);
xValues.push(xValue);
yValues.push(yValue);
- }, me);
+ });
ln = xValues.length;
if (ln > bbox.width) {
@@ -48140,6 +51922,7 @@ Ext.define('Ext.chart.series.Line', {
me.items = [];
+ count = 0;
ln = xValues.length;
for (i = 0; i < ln; i++) {
xValue = xValues[i];
@@ -48157,11 +51940,12 @@ Ext.define('Ext.chart.series.Line', {
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) {
@@ -48190,21 +51974,22 @@ Ext.define('Ext.chart.series.Line', {
}
}
if (showMarkers) {
- marker = markerGroup.getAt(i);
+ 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,
+ x: +(prevX || x),
y: prevY || (bbox.y + bbox.height / 2)
},
- value: '"' + xValue + ', ' + yValue + '"'
+ value: '"' + xValue + ', ' + yValue + '"',
+ zIndex: 4000
}, endMarkerStyle));
marker._to = {
translate: {
- x: x,
- y: y
+ x: +x,
+ y: +y
}
};
} else {
@@ -48215,7 +52000,8 @@ Ext.define('Ext.chart.series.Line', {
}, true);
marker._to = {
translate: {
- x: x, y: y
+ x: +x,
+ y: +y
}
};
}
@@ -48225,25 +52011,29 @@ Ext.define('Ext.chart.series.Line', {
value: [xValue, yValue],
point: [x, y],
sprite: marker,
- storeItem: store.getAt(i)
+ storeItem: store.getAt(storeIndices[i])
});
prevX = x;
prevY = y;
}
-
+
if (path.length <= 1) {
//nothing to be rendered
- return;
+ return;
}
-
+
if (me.smooth) {
- path = Ext.draw.Draw.smooth(path, 6);
+ 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;
- fromPath.splice(1, 2);
+ if (!smooth) {
+ Ext.Array.erase(fromPath, 1, 2);
+ }
} else {
fromPath = path;
}
@@ -48256,9 +52046,15 @@ Ext.define('Ext.chart.series.Line', {
path: dummyPath,
stroke: endLineStyle.stroke || endLineStyle.fill
}, endLineStyle || {}));
+
+ 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'
+ fill: 'none',
+ zIndex: 3000
});
if (!endLineStyle.stroke && colorArrayLength) {
me.line.setAttributes({
@@ -48267,11 +52063,11 @@ Ext.define('Ext.chart.series.Line', {
}
if (enableShadows) {
//create shadows
- shadows = me.line.shadows = [];
+ shadows = me.line.shadows = [];
for (shindex = 0; shindex < lnsh; shindex++) {
shadowBarAttr = shadowAttributes[shindex];
shadowBarAttr = Ext.apply({}, shadowBarAttr, { path: dummyPath });
- shadow = chart.surface.add(Ext.apply({}, {
+ shadow = surface.add(Ext.apply({}, {
type: 'path',
group: shadowGroups[shindex]
}, shadowBarAttr));
@@ -48280,17 +52076,17 @@ Ext.define('Ext.chart.series.Line', {
}
}
if (me.fill) {
- fillPath = path.concat([
+ fillPath = renderPath.concat([
["L", x, bbox.y + bbox.height],
- ["L", bbox.x, bbox.y + bbox.height],
- ["L", bbox.x, firstY]
+ ["L", firstX, bbox.y + bbox.height],
+ ["L", firstX, firstY]
]);
if (!me.fillPath) {
me.fillPath = surface.add({
group: group,
type: 'path',
opacity: endLineStyle.opacity || 0.3,
- fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill,
+ fill: endLineStyle.fill || colorArrayStyle[seriesIdx % colorArrayLength],
path: dummyPath
});
}
@@ -48300,12 +52096,13 @@ Ext.define('Ext.chart.series.Line', {
fill = me.fill;
line = me.line;
//Add renderer to line. There is not unique record associated with this.
- rendererAttributes = me.renderer(line, false, { path: path }, i, store);
+ 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,
@@ -48322,46 +52119,47 @@ Ext.define('Ext.chart.series.Line', {
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: path },
+ to: { path: renderPath },
from: { path: fromPath }
});
} else {
me.onAnimate(shadows[j], {
- to: { path: path }
+ to: { path: renderPath }
});
}
}
}
//animate fill path
if (fill) {
+ me.fillPath.show(true);
me.onAnimate(me.fillPath, {
to: Ext.apply({}, {
path: fillPath,
- fill: colorArrayStyle[seriesIdx % colorArrayLength] || endLineStyle.fill
+ fill: endLineStyle.fill || colorArrayStyle[seriesIdx % colorArrayLength],
+ 'stroke-width': 0
}, endLineStyle || {})
});
}
//animate markers
if (showMarkers) {
+ count = 0;
for(i = 0; i < ln; i++) {
- item = markerGroup.getAt(i);
- if (item) {
- if (me.items[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.apply(rendererAttributes, endMarkerStyle || {})
});
- } else {
- item.setAttributes(Ext.apply({
- hidden: true
- }, item._to), true);
+ item.show(true);
}
}
}
- for(; i < markerCount; i++) {
- item = markerGroup.getAt(i);
+ for(; count < markerCount; count++) {
+ item = markerGroup.getAt(count);
item.hide(true);
}
// for(i = 0; i < (chart.markerIndex || 0)-1; i++) {
@@ -48370,7 +52168,7 @@ Ext.define('Ext.chart.series.Line', {
// }
}
} else {
- rendererAttributes = me.renderer(me.line, false, { path: path, hidden: false }, i, store);
+ rendererAttributes = me.renderer(me.line, false, { path: renderPath, hidden: false }, i, store);
Ext.apply(rendererAttributes, endLineStyle || {}, {
stroke: endLineStyle.stroke || endLineStyle.fill
});
@@ -48382,42 +52180,50 @@ Ext.define('Ext.chart.series.Line', {
shadows = me.line.shadows;
for(j = 0; j < lnsh; j++) {
shadows[j].setAttributes({
- path: path
+ path: renderPath,
+ hidden: false
}, true);
}
}
if (me.fill) {
me.fillPath.setAttributes({
- path: fillPath
+ path: fillPath,
+ hidden: false
}, true);
}
if (showMarkers) {
+ count = 0;
for(i = 0; i < ln; i++) {
- item = markerGroup.getAt(i);
- if (item) {
- if (me.items[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);
- } else {
- item.hide(true);
+ item.show(true);
}
}
}
- for(; i < markerCount; i++) {
- item = markerGroup.getAt(i);
+ for(; count < markerCount; count++) {
+ item = markerGroup.getAt(count);
item.hide(true);
}
}
}
if (chart.markerIndex) {
- path.splice(1, 0, path[1], path[2]);
+ 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,
@@ -48434,7 +52240,7 @@ Ext.define('Ext.chart.series.Line', {
'y': bbox.y + bbox.height / 2
}, endLabelStyle || {}));
},
-
+
// @private called when a label is to be created.
onPlaceLabel: function(label, storeItem, item, i, display, animate) {
var me = this,
@@ -48448,12 +52254,12 @@ Ext.define('Ext.chart.series.Line', {
y = item.point[1],
radius = item.sprite.attr.radius,
bb, width, height;
-
+
label.setAttributes({
text: format(storeItem.get(field)),
hidden: true
}, true);
-
+
if (display == 'rotate') {
label.setAttributes({
'text-anchor': 'start',
@@ -48470,7 +52276,7 @@ Ext.define('Ext.chart.series.Line', {
x = x < bbox.x? bbox.x : x;
x = (x + width > bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x;
y = (y - height < bbox.y)? bbox.y + height : y;
-
+
} else if (display == 'under' || display == 'over') {
//TODO(nicolas): find out why width/height values in circle bounding boxes are undefined.
bb = item.sprite.getBBox();
@@ -48486,7 +52292,7 @@ Ext.define('Ext.chart.series.Line', {
y = y - height < bbox.y? bbox.y + height : y;
y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
}
-
+
if (me.chart.animate && !me.chart.resizing) {
label.show(true);
me.onAnimate(label, {
@@ -48500,7 +52306,7 @@ Ext.define('Ext.chart.series.Line', {
x: x,
y: y
}, true);
- if (resizing) {
+ if (resizing && me.animation) {
me.animation.on('afteranimate', function() {
label.show(true);
});
@@ -48514,20 +52320,20 @@ Ext.define('Ext.chart.series.Line', {
highlightItem: function() {
var me = this;
me.callParent(arguments);
- if (this.line && !this.highlighted) {
- if (!('__strokeWidth' in this.line)) {
- this.line.__strokeWidth = this.line.attr['stroke-width'] || 0;
+ if (me.line && !me.highlighted) {
+ if (!('__strokeWidth' in me.line)) {
+ me.line.__strokeWidth = me.line.attr['stroke-width'] || 0;
}
- if (this.line.__anim) {
- this.line.__anim.paused = true;
+ if (me.line.__anim) {
+ me.line.__anim.paused = true;
}
- this.line.__anim = Ext.create('Ext.fx.Anim', {
- target: this.line,
+ me.line.__anim = Ext.create('Ext.fx.Anim', {
+ target: me.line,
to: {
- 'stroke-width': this.line.__strokeWidth + 3
+ 'stroke-width': me.line.__strokeWidth + 3
}
});
- this.highlighted = true;
+ me.highlighted = true;
}
},
@@ -48535,14 +52341,14 @@ Ext.define('Ext.chart.series.Line', {
unHighlightItem: function() {
var me = this;
me.callParent(arguments);
- if (this.line && this.highlighted) {
- this.line.__anim = Ext.create('Ext.fx.Anim', {
- target: this.line,
+ if (me.line && me.highlighted) {
+ me.line.__anim = Ext.create('Ext.fx.Anim', {
+ target: me.line,
to: {
- 'stroke-width': this.line.__strokeWidth
+ 'stroke-width': me.line.__strokeWidth
}
});
- this.highlighted = false;
+ me.highlighted = false;
}
},
@@ -48551,7 +52357,7 @@ Ext.define('Ext.chart.series.Line', {
if (!display) {
return;
}
-
+
var me = this,
chart = me.chart,
surface = chart.surface,
@@ -48583,11 +52389,11 @@ Ext.define('Ext.chart.series.Line', {
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) {
@@ -48607,7 +52413,7 @@ Ext.define('Ext.chart.series.Line', {
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])) {
@@ -48620,13 +52426,13 @@ Ext.define('Ext.chart.series.Line', {
//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, {
@@ -48653,7 +52459,7 @@ Ext.define('Ext.chart.series.Line', {
callout[p].show(true);
}
},
-
+
isItemInPoint: function(x, y, item, i) {
var me = this,
items = me.items,
@@ -48672,10 +52478,10 @@ Ext.define('Ext.chart.series.Line', {
yIntersect,
dist1, dist2, dist, midx, midy,
sqrt = Math.sqrt, abs = Math.abs;
-
+
nextItem = items[i];
prevItem = i && items[i - 1];
-
+
if (i >= ln) {
prevItem = items[ln - 1];
}
@@ -48688,22 +52494,22 @@ Ext.define('Ext.chart.series.Line', {
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.Line.superclass.hideAll.call(me);
+ Ext.chart.series.Cartesian.prototype.hideAll.call(me);
}
else {
- Ext.chart.series.Line.superclass.showAll.call(me);
+ Ext.chart.series.Cartesian.prototype.showAll.call(me);
}
if (me.line) {
me.line.setAttributes({
@@ -48725,88 +52531,87 @@ Ext.define('Ext.chart.series.Line', {
}, 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
* @extends Ext.chart.series.Series
- *
- * Creates a Pie Chart. A Pie Chart is a useful visualization technique to display quantitative information for different
+ *
+ * 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
+ * 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:
- *
-{@img Ext.chart.series.Pie/Ext.chart.series.Pie.png Ext.chart.series.Pie chart series}
-
- 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,
- theme: 'Base:gradients',
- series: [{
- type: 'pie',
- field: 'data1',
- 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('data1');
- });
- this.setTitle(storeItem.get('name') + ': ' + Math.round(storeItem.get('data1') / total * 100) + '%');
- }
- },
- highlight: {
- segment: {
- margin: 20
- }
- },
- label: {
- field: 'name',
- display: 'rotate',
- contrast: true,
- font: '18px Arial'
- }
- }]
- });
-
-
- *
- * 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 `data1` 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. Finally, we set the font family
- * and size through the `font` parameter.
- *
- * @xtype pie
*
+ * @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: 350,
+ * animate: true,
+ * store: store,
+ * theme: 'Base:gradients',
+ * series: [{
+ * type: 'pie',
+ * field: 'data1',
+ * 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('data1');
+ * });
+ * this.setTitle(storeItem.get('name') + ': ' + Math.round(storeItem.get('data1') / total * 100) + '%');
+ * }
+ * },
+ * highlight: {
+ * segment: {
+ * margin: 20
+ * }
+ * },
+ * label: {
+ * field: 'name',
+ * display: 'rotate',
+ * contrast: true,
+ * font: '18px Arial'
+ * }
+ * }]
+ * });
+ *
+ * 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 `data1` 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. Finally, we set the font family
+ * and size through the `font` parameter.
+ *
+ * @xtype pie
*/
Ext.define('Ext.chart.series.Pie', {
@@ -48819,7 +52624,7 @@ Ext.define('Ext.chart.series.Pie', {
/* End Definitions */
type: "pie",
-
+
alias: 'series.pie',
rad: Math.PI / 180,
@@ -48831,10 +52636,9 @@ Ext.define('Ext.chart.series.Pie', {
highlightDuration: 150,
/**
- * @cfg {String} angleField
+ * @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.
- * This parameter is required.
*/
angleField: false,
@@ -48842,12 +52646,11 @@ Ext.define('Ext.chart.series.Pie', {
* @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.
- * This parameter is optional.
*/
lengthField: false,
/**
- * @cfg {Boolean|Number} donut
+ * @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.
@@ -48864,13 +52667,13 @@ Ext.define('Ext.chart.series.Pie', {
* @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: {},
-
+
constructor: function(config) {
this.callParent(arguments);
var me = this,
@@ -48885,7 +52688,7 @@ Ext.define('Ext.chart.series.Pie', {
}
}
});
- Ext.apply(me, config, {
+ Ext.apply(me, config, {
shadowAttributes: [{
"stroke-width": 6,
"stroke-opacity": 1,
@@ -48923,12 +52726,13 @@ Ext.define('Ext.chart.series.Pie', {
surface.customAttributes.segment = function(opt) {
return me.getSegment(opt);
};
+ me.__excludes = me.__excludes || [];
},
-
+
//@private updates some onbefore render parameters.
initialize: function() {
var me = this,
- store = me.chart.substore || me.chart.store;
+ store = me.chart.getChartStore();
//Add yFields to be used in Legend.js
me.yField = [];
if (me.label.field) {
@@ -48944,58 +52748,81 @@ Ext.define('Ext.chart.series.Pie', {
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,
+ x5 = 0, y5 = 0, x6 = 0, y6 = 0,
delta = 1e-2,
- 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;
+ 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)
- x += margin * cos(midAngle);
- y += margin * sin(midAngle);
-
- x1 = x + opt.startRho * cos(a1);
- y1 = y + opt.startRho * sin(a1);
+ if (a2 - a1 < delta) {
+ return {path: ""};
+ }
- x2 = x + opt.endRho * cos(a1);
- y2 = y + opt.endRho * sin(a1);
+ if (margin !== 0) {
+ x += margin * cm;
+ y += margin * sm;
+ }
- x3 = x + opt.startRho * cos(a2);
- y3 = y + opt.startRho * sin(a2);
+ x2 = x + opt.endRho * c1;
+ y2 = y + opt.endRho * s1;
- x4 = x + opt.endRho * cos(a2);
- y4 = y + opt.endRho * sin(a2);
+ x4 = x + opt.endRho * c2;
+ y4 = y + opt.endRho * s2;
- if (abs(x1 - x3) <= delta && abs(y1 - y3) <= delta) {
- singleSlice = true;
+ if (Math.abs(x2 - x4) + Math.abs(y2 - y4) < delta) {
+ cm = hsqr2;
+ sm = -hsqr2;
+ flag = 1;
}
- //Solves mysterious clipping bug with IE
- if (singleSlice) {
+
+ x6 = x + opt.endRho * cm;
+ y6 = y + opt.endRho * sm;
+
+ // TODO(bei): It seems that the canvas engine cannot render half circle command correctly on IE.
+ // Better fix the VML engine for half circles.
+
+ 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", x1, y1],
- ["L", x2, y2],
- ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
- ["Z"]]
+ ["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", 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"]]
+ ["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"]
+ ]
};
}
},
@@ -49010,11 +52837,10 @@ Ext.define('Ext.chart.series.Pie', {
startAngle = slice.startAngle,
endAngle = slice.endAngle,
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);
+ 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,
@@ -49027,7 +52853,7 @@ Ext.define('Ext.chart.series.Pie', {
*/
drawSeries: function() {
var me = this,
- store = me.chart.substore || me.chart.store,
+ store = me.chart.getChartStore(),
group = me.group,
animate = me.chart.animate,
field = me.angleField || me.field || me.xField,
@@ -49061,6 +52887,7 @@ Ext.define('Ext.chart.series.Pie', {
colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
gutterX = chart.maxGutter[0],
gutterY = chart.maxGutter[1],
+ abs = Math.abs,
rendererAttributes,
shadowGroup,
shadowAttr,
@@ -49088,7 +52915,7 @@ Ext.define('Ext.chart.series.Pie', {
path,
p,
spriteOptions, bbox;
-
+
Ext.apply(seriesStyle, me.style || {});
me.setBBox();
@@ -49099,12 +52926,12 @@ Ext.define('Ext.chart.series.Pie', {
colorArrayStyle = me.colorSet;
colorArrayLength = colorArrayStyle.length;
}
-
+
//if not store or store is empty then there's nothing to draw
if (!store || !store.getCount()) {
return;
}
-
+
me.unHighlightItem();
me.cleanHighlights();
@@ -49129,25 +52956,26 @@ Ext.define('Ext.chart.series.Pie', {
}
}, this);
+ totalField = totalField || 1;
store.each(function(record, i) {
if (this.__excludes && this.__excludes[i]) {
- //hidden series
- return;
- }
- value = record.get(field);
- middleAngle = angle - 360 * value / totalField / 2;
- // TODO - Put up an empty circle
- if (isNaN(middleAngle)) {
- middleAngle = 360;
- value = 1;
- totalField = 1;
+ value = 0;
+ } else {
+ value = record.get(field);
+ if (first == 0) {
+ first = 1;
+ }
}
+
// First slice
- if (!i || first == 0) {
- angle = 360 - middleAngle;
- me.firstAngle = angle;
- middleAngle = angle - 360 * value / totalField / 2;
+ if (first == 1) {
+ first = 2;
+ me.firstAngle = angle = 360 * value / totalField / 2;
+ for (j = 0; j < i; j++) {
+ slices[j].startAngle = slices[j].endAngle = me.firstAngle;
+ }
}
+
endAngle = angle - 360 * value / totalField;
slice = {
series: me,
@@ -49163,20 +52991,11 @@ Ext.define('Ext.chart.series.Pie', {
slice.rho = me.radius;
}
slices[i] = slice;
- if((slice.startAngle % 360) == (slice.endAngle % 360)) {
- slice.startAngle -= 0.0001;
- }
angle = endAngle;
- first++;
}, me);
-
//do all shadows first.
if (enableShadows) {
for (i = 0, ln = slices.length; i < ln; i++) {
- if (this.__excludes && this.__excludes[i]) {
- //hidden series
- continue;
- }
slice = slices[i];
slice.shadowAttrs = [];
for (j = 0, rhoAcum = 0, shadows = []; j < layers; j++) {
@@ -49191,32 +53010,28 @@ Ext.define('Ext.chart.series.Pie', {
rho: slice.rho,
startRho: rhoAcum + (deltaRho * donut / 100),
endRho: rhoAcum + deltaRho
- }
+ },
+ hidden: !slice.value && (slice.startAngle % 360) == (slice.endAngle % 360)
};
//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({},
- {
+ shadow = chart.surface.add(Ext.apply({}, {
type: 'path',
group: shadowGroups[shindex],
strokeLinejoin: "round"
- },
- rendererAttributes, shadowAttr));
+ }, rendererAttributes, shadowAttr));
}
if (animate) {
- rendererAttributes = me.renderer(shadow, store.getAt(i), Ext.apply({},
- rendererAttributes, shadowAttr), i, store);
+ shadowAttr = me.renderer(shadow, store.getAt(i), Ext.apply({}, rendererAttributes, shadowAttr), i, store);
me.onAnimate(shadow, {
- to: rendererAttributes
+ to: shadowAttr
});
} else {
- rendererAttributes = me.renderer(shadow, store.getAt(i), Ext.apply(shadowAttr, {
- hidden: false
- }), i, store);
- shadow.setAttributes(rendererAttributes, true);
+ shadowAttr = me.renderer(shadow, store.getAt(i), shadowAttr, i, store);
+ shadow.setAttributes(shadowAttr, true);
}
shadows.push(shadow);
}
@@ -49226,10 +53041,6 @@ Ext.define('Ext.chart.series.Pie', {
}
//do pie slices after.
for (i = 0, ln = slices.length; i < ln; i++) {
- if (this.__excludes && this.__excludes[i]) {
- //hidden series
- continue;
- }
slice = slices[i];
for (j = 0, rhoAcum = 0; j < layers; j++) {
sprite = group.getAt(i * layers + j);
@@ -49243,7 +53054,8 @@ Ext.define('Ext.chart.series.Pie', {
rho: slice.rho,
startRho: rhoAcum + (deltaRho * donut / 100),
endRho: rhoAcum + deltaRho
- }
+ },
+ hidden: (!slice.value && (slice.startAngle % 360) == (slice.endAngle % 360))
}, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {}));
item = Ext.apply({},
rendererAttributes.segment, {
@@ -49294,7 +53106,7 @@ Ext.define('Ext.chart.series.Pie', {
rhoAcum += deltaRho;
}
}
-
+
// Hide unused bars
ln = group.getCount();
for (i = 0; i < ln; i++) {
@@ -49327,7 +53139,7 @@ Ext.define('Ext.chart.series.Pie', {
centerY = me.centerY,
middle = item.middle,
endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config || {});
-
+
return me.chart.surface.add(Ext.apply({
'type': 'text',
'text-anchor': 'middle',
@@ -49359,9 +53171,13 @@ Ext.define('Ext.chart.series.Pie', {
theta = Math.atan2(y, x || 1),
dg = theta * 180 / Math.PI,
prevDg;
-
+ if (this.__excludes && this.__excludes[i]) {
+ opt.hidden = true;
+ }
function fixAngle(a) {
- if (a < 0) a += 360;
+ if (a < 0) {
+ a += 360;
+ }
return a % 360;
}
@@ -49405,7 +53221,7 @@ Ext.define('Ext.chart.series.Pie', {
}
//ensure the object has zero translation
opt.translate = {
- x: 0, y: 0
+ x: 0, y: 0
};
if (animate && !resizing && (display != 'rotate' || prevDg != null)) {
me.onAnimate(label, {
@@ -49516,8 +53332,8 @@ Ext.define('Ext.chart.series.Pie', {
startAngle = item.startAngle,
endAngle = item.endAngle,
rho = Math.sqrt(dx * dx + dy * dy),
- angle = Math.atan2(y - cy, x - cx) / me.rad + 360;
-
+ angle = Math.atan2(y - cy, x - cx) / me.rad;
+
// normalize to the same range of angles created by drawSeries
if (angle > me.firstAngle) {
angle -= 360;
@@ -49525,7 +53341,7 @@ Ext.define('Ext.chart.series.Pie', {
return (angle <= startAngle && angle > endAngle
&& rho >= item.startRho && rho <= item.endRho);
},
-
+
// @private hides all elements in the series.
hideAll: function() {
var i, l, shadow, shadows, sh, lsh, sprite;
@@ -49551,7 +53367,7 @@ Ext.define('Ext.chart.series.Pie', {
this.drawSeries();
}
},
-
+
// @private shows all elements in the series.
showAll: function() {
if (!isNaN(this._index)) {
@@ -49568,13 +53384,13 @@ Ext.define('Ext.chart.series.Pie', {
var me = this,
rad = me.rad;
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 || item.sprite && item.sprite._animating) {
return;
}
@@ -49607,7 +53423,7 @@ Ext.define('Ext.chart.series.Pie', {
if (Math.abs(y) < 1e-10) {
y = 0;
}
-
+
if (animate) {
label.stopAnimation();
label.animate({
@@ -49662,7 +53478,7 @@ Ext.define('Ext.chart.series.Pie', {
},
/**
- * un-highlights the specified item. If no item is provided it will un-highlight the entire series.
+ * 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() {
@@ -49749,14 +53565,14 @@ Ext.define('Ext.chart.series.Pie', {
}
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.colorArrayStyle[index % me.colorArrayStyle.length];
+ return (me.colorSet && me.colorSet[index % me.colorSet.length]) || me.colorArrayStyle[index % me.colorArrayStyle.length];
}
});
@@ -49764,91 +53580,91 @@ Ext.define('Ext.chart.series.Pie', {
/**
* @class Ext.chart.series.Radar
* @extends Ext.chart.series.Series
- *
- * Creates a Radar Chart. A Radar Chart is a useful visualization technique for comparing different quantitative values for
+ *
+ * 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
+ *
+ * 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:
- *
- {@img Ext.chart.series.Radar/Ext.chart.series.Radar.png Ext.chart.series.Radar chart series}
-
- 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: 'Radial',
- position: 'radial',
- label: {
- display: true
- }
- }],
- series: [{
- type: 'radar',
- xField: 'name',
- yField: 'data3',
- 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: 'data5',
- 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.
- *
+ *
+ * @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: 'Radial',
+ * position: 'radial',
+ * label: {
+ * display: true
+ * }
+ * }],
+ * series: [{
+ * type: 'radar',
+ * xField: 'name',
+ * yField: 'data3',
+ * 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: 'data5',
+ * 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.
+ *
* @xtype radar
- *
*/
Ext.define('Ext.chart.series.Radar', {
@@ -49863,7 +53679,7 @@ Ext.define('Ext.chart.series.Radar', {
type: "radar",
alias: 'series.radar',
-
+
rad: Math.PI / 180,
showInLegend: false,
@@ -49873,7 +53689,7 @@ Ext.define('Ext.chart.series.Radar', {
* An object containing styles for overriding series styles from Theming.
*/
style: {},
-
+
constructor: function(config) {
this.callParent(arguments);
var me = this,
@@ -49889,7 +53705,7 @@ Ext.define('Ext.chart.series.Radar', {
*/
drawSeries: function() {
var me = this,
- store = me.chart.substore || me.chart.store,
+ store = me.chart.getChartStore(),
group = me.group,
sprite,
chart = me.chart,
@@ -49915,18 +53731,18 @@ Ext.define('Ext.chart.series.Radar', {
first = chart.resizing || !me.radar,
axis = chart.axes && chart.axes.get(0),
aggregate = !(axis && axis.maximum);
-
+
me.setBBox();
maxValue = aggregate? 0 : (axis.maximum || 0);
-
+
Ext.apply(seriesStyle, me.style || {});
-
+
//if the store is empty then there's nothing to draw
if (!store || !store.getCount()) {
return;
}
-
+
me.unHighlightItem();
me.cleanHighlights();
@@ -50002,7 +53818,7 @@ Ext.define('Ext.chart.series.Radar', {
me.renderLabels();
me.renderCallouts();
},
-
+
// @private draws the markers for the lines (if any).
drawMarkers: function() {
var me = this,
@@ -50010,15 +53826,15 @@ Ext.define('Ext.chart.series.Radar', {
surface = chart.surface,
markerStyle = Ext.apply({}, me.markerStyle || {}),
endMarkerStyle = Ext.apply(markerStyle, me.markerConfig),
- items = me.items,
+ items = me.items,
type = endMarkerStyle.type,
markerGroup = me.markerGroup,
centerX = me.centerX,
centerY = me.centerY,
item, i, l, marker;
-
+
delete endMarkerStyle.type;
-
+
for (i = 0, l = items.length; i < l; i++) {
item = items[i];
marker = markerGroup.getAt(i);
@@ -50063,7 +53879,7 @@ Ext.define('Ext.chart.series.Radar', {
}
}
},
-
+
isItemInPoint: function(x, y, item) {
var point,
tolerance = 10,
@@ -50082,7 +53898,7 @@ Ext.define('Ext.chart.series.Radar', {
centerY = me.centerY,
point = item.point,
endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config);
-
+
return me.chart.surface.add(Ext.apply({
'type': 'text',
'text-anchor': 'middle',
@@ -50114,14 +53930,14 @@ Ext.define('Ext.chart.series.Radar', {
hidden: true
},
true);
-
+
if (resizing) {
label.setAttributes({
x: centerX,
y: centerY
}, true);
}
-
+
if (animate) {
label.show(true);
me.onAnimate(label, {
@@ -50133,7 +53949,7 @@ Ext.define('Ext.chart.series.Radar', {
}
},
- // @private for toggling (show/hide) series.
+ // @private for toggling (show/hide) series.
toggleAll: function(show) {
var me = this,
i, ln, shadow, shadows;
@@ -50158,18 +53974,18 @@ Ext.define('Ext.chart.series.Radar', {
}
}
},
-
+
// @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,
@@ -50185,75 +54001,71 @@ Ext.define('Ext.chart.series.Radar', {
/**
* @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.
+ *
+ * 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
+ * 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:
*
-{@img Ext.chart.series.Scatter/Ext.chart.series.Scatter.png Ext.chart.series.Scatter chart series}
-
- 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: 'bottom',
- fields: ['data1', 'data2', 'data3'],
- title: 'Sample Values',
- grid: true,
- minimum: 0
- }, {
- type: 'Category',
- position: 'left',
- 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
+ * @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.
- *
+ *
* @xtype scatter
- *
*/
Ext.define('Ext.chart.series.Scatter', {
@@ -50272,11 +54084,18 @@ Ext.define('Ext.chart.series.Scatter', {
* @cfg {Object} markerConfig
* The display style for the scatter series markers.
*/
-
+
/**
- * @cfg {Object} style
+ * @cfg {Object} style
* Append styling properties to this object for it to override theme properties.
*/
+
+ /**
+ * @cfg {String/Array} 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. If multiple axes are being used, they should both be specified in in the configuration.
+ */
constructor: function(config) {
this.callParent(arguments);
@@ -50312,14 +54131,14 @@ Ext.define('Ext.chart.series.Scatter', {
getBounds: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
axes = [].concat(me.axis),
bbox, xScale, yScale, ln, minX, minY, maxX, maxY, i, axis, ends;
me.setBBox();
bbox = me.bbox;
- for (i = 0, ln = axes.length; i < ln; i++) {
+ for (i = 0, ln = axes.length; i < ln; i++) {
axis = chart.axes.get(axes[i]);
if (axis) {
ends = axis.calcEnds();
@@ -50364,7 +54183,7 @@ Ext.define('Ext.chart.series.Scatter', {
minY = 0;
maxY = store.getCount() - 1;
yScale = bbox.height / (store.getCount() - 1);
- }
+ }
else {
yScale = bbox.height / (maxY - minY);
}
@@ -50383,7 +54202,7 @@ Ext.define('Ext.chart.series.Scatter', {
var me = this,
chart = me.chart,
enableShadows = chart.shadow,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
group = me.group,
bounds = me.bounds = me.getBounds(),
bbox = me.bbox,
@@ -50411,10 +54230,10 @@ Ext.define('Ext.chart.series.Scatter', {
return;
}
// Ensure a value
- if (typeof xValue == 'string' || typeof xValue == 'object') {
+ if (typeof xValue == 'string' || typeof xValue == 'object' && !Ext.isDate(xValue)) {
xValue = i;
}
- if (typeof yValue == 'string' || typeof yValue == 'object') {
+ if (typeof yValue == 'string' || typeof yValue == 'object' && !Ext.isDate(yValue)) {
yValue = i;
}
x = boxX + (xValue - minX) * xScale;
@@ -50540,7 +54359,7 @@ Ext.define('Ext.chart.series.Scatter', {
drawSeries: function() {
var me = this,
chart = me.chart,
- store = chart.substore || chart.store,
+ store = chart.getChartStore(),
group = me.group,
enableShadows = chart.shadow,
shadowGroups = me.shadowGroups,
@@ -50587,23 +54406,28 @@ Ext.define('Ext.chart.series.Scatter', {
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), Ext.apply({ translate: attr }, { hidden: false }), i, store);
+ rendererAttributes = me.renderer(sprite, store.getAt(i), { translate: attr }, i, store);
+ sprite._to = rendererAttributes;
sprite.setAttributes(rendererAttributes, true);
- //update shadows
+ //animate shadows
for (shindex = 0; shindex < lnsh; shindex++) {
- shadowAttribute = shadowAttributes[shindex];
- rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({
- x: attr.x,
- y: attr.y
+ 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);
}
@@ -50619,7 +54443,7 @@ Ext.define('Ext.chart.series.Scatter', {
me.renderLabels();
me.renderCallouts();
},
-
+
// @private callback for when creating a label sprite.
onCreateLabel: function(storeItem, item, i, display) {
var me = this,
@@ -50627,7 +54451,7 @@ Ext.define('Ext.chart.series.Scatter', {
config = me.label,
endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle),
bbox = me.bbox;
-
+
return me.chart.surface.add(Ext.apply({
type: 'text',
group: group,
@@ -50635,7 +54459,7 @@ Ext.define('Ext.chart.series.Scatter', {
y: bbox.y + bbox.height / 2
}, endLabelStyle));
},
-
+
// @private callback for when placing a label sprite.
onPlaceLabel: function(label, storeItem, item, i, display, animate) {
var me = this,
@@ -50649,12 +54473,12 @@ Ext.define('Ext.chart.series.Scatter', {
y = item.point[1],
radius = item.sprite.attr.radius,
bb, width, height, anim;
-
+
label.setAttributes({
text: format(storeItem.get(field)),
hidden: true
}, true);
-
+
if (display == 'rotate') {
label.setAttributes({
'text-anchor': 'start',
@@ -50671,7 +54495,7 @@ Ext.define('Ext.chart.series.Scatter', {
x = x < bbox.x? bbox.x : x;
x = (x + width > bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x;
y = (y - height < bbox.y)? bbox.y + height : y;
-
+
} else if (display == 'under' || display == 'over') {
//TODO(nicolas): find out why width/height values in circle bounding boxes are undefined.
bb = item.sprite.getBBox();
@@ -50705,7 +54529,7 @@ Ext.define('Ext.chart.series.Scatter', {
y: y
}, true);
label.show(true);
- });
+ });
}
else {
label.show(true);
@@ -50721,8 +54545,8 @@ Ext.define('Ext.chart.series.Scatter', {
}
}
},
-
- // @private callback for when placing a callout sprite.
+
+ // @private callback for when placing a callout sprite.
onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) {
var me = this,
chart = me.chart,
@@ -50739,18 +54563,18 @@ Ext.define('Ext.chart.series.Scatter', {
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])) {
@@ -50759,17 +54583,17 @@ Ext.define('Ext.chart.series.Scatter', {
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, {
@@ -50997,58 +54821,46 @@ Ext.define('Ext.chart.theme.Base', {
/**
* @author Ed Spencer
- * @class Ext.data.ArrayStore
- * @extends Ext.data.Store
- * @ignore
*
- * Small helper class to make creating {@link Ext.data.Store}s from Array data easier.
- * An ArrayStore will be automatically configured with a {@link Ext.data.reader.Array}.
+ * Small helper class to make creating {@link Ext.data.Store}s from Array data easier. An ArrayStore will be
+ * automatically configured with a {@link Ext.data.reader.Array}.
*
- * A store configuration would be something like:
-
-var store = new Ext.data.ArrayStore({
- // store configs
- autoDestroy: true,
- storeId: 'myStore',
- // reader configs
- idIndex: 0,
- fields: [
- 'company',
- {name: 'price', type: 'float'},
- {name: 'change', type: 'float'},
- {name: 'pctChange', type: 'float'},
- {name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
- ]
-});
-
- * This store is configured to consume a returned object of the form:
-
-var myData = [
- ['3m Co',71.72,0.02,0.03,'9/1 12:00am'],
- ['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am'],
- ['Boeing Co.',75.43,0.53,0.71,'9/1 12:00am'],
- ['Hewlett-Packard Co.',36.53,-0.03,-0.08,'9/1 12:00am'],
- ['Wal-Mart Stores, Inc.',45.45,0.73,1.63,'9/1 12:00am']
-];
-
-*
- * An object literal of this form could also be used as the {@link #data} config option.
+ * A store configuration would be something like:
*
- * *Note: Although not listed here, this class accepts all of the configuration options of
- * {@link Ext.data.reader.Array ArrayReader}.
+ * var store = Ext.create('Ext.data.ArrayStore', {
+ * // store configs
+ * autoDestroy: true,
+ * storeId: 'myStore',
+ * // reader configs
+ * idIndex: 0,
+ * fields: [
+ * 'company',
+ * {name: 'price', type: 'float'},
+ * {name: 'change', type: 'float'},
+ * {name: 'pctChange', type: 'float'},
+ * {name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
+ * ]
+ * });
*
- * @constructor
- * @param {Object} config
- * @xtype arraystore
+ * This store is configured to consume a returned object of the form:
+ *
+ * var myData = [
+ * ['3m Co',71.72,0.02,0.03,'9/1 12:00am'],
+ * ['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am'],
+ * ['Boeing Co.',75.43,0.53,0.71,'9/1 12:00am'],
+ * ['Hewlett-Packard Co.',36.53,-0.03,-0.08,'9/1 12:00am'],
+ * ['Wal-Mart Stores, Inc.',45.45,0.73,1.63,'9/1 12:00am']
+ * ];
+ *
+ * An object literal of this form could also be used as the {@link #data} config option.
+ *
+ * **Note:** This class accepts all of the configuration options of {@link Ext.data.reader.Array ArrayReader}.
*/
Ext.define('Ext.data.ArrayStore', {
extend: 'Ext.data.Store',
alias: 'store.array',
uses: ['Ext.data.reader.Array'],
- /**
- * @cfg {Ext.data.DataReader} reader @hide
- */
constructor: function(config) {
config = config || {};
@@ -51086,73 +54898,68 @@ Ext.define('Ext.data.ArrayStore', {
/**
* @author Ed Spencer
* @class Ext.data.Batch
- *
+ *
* Provides a mechanism to run one or more {@link Ext.data.Operation operations} in a given order. Fires the 'operationcomplete' event
* after the completion of each Operation, and the 'complete' event when all Operations have been successfully executed. Fires an 'exception'
* event if any of the Operations encounter an exception.
- *
+ *
* Usually these are only used internally by {@link Ext.data.proxy.Proxy} classes
- *
- * @constructor
- * @param {Object} config Optional config object
+ *
*/
Ext.define('Ext.data.Batch', {
mixins: {
observable: 'Ext.util.Observable'
},
-
+
/**
- * True to immediately start processing the batch as soon as it is constructed (defaults to false)
- * @property autoStart
- * @type Boolean
+ * @property {Boolean} autoStart
+ * True to immediately start processing the batch as soon as it is constructed.
*/
autoStart: false,
-
+
/**
+ * @property {Number} current
* The index of the current operation being executed
- * @property current
- * @type Number
*/
current: -1,
-
+
/**
+ * @property {Number} total
* The total number of operations in this batch. Read only
- * @property total
- * @type Number
*/
total: 0,
-
+
/**
+ * @property {Boolean} isRunning
* True if the batch is currently running
- * @property isRunning
- * @type Boolean
*/
isRunning: false,
-
+
/**
+ * @property {Boolean} isComplete
* True if this batch has been executed completely
- * @property isComplete
- * @type Boolean
*/
isComplete: false,
-
+
/**
+ * @property {Boolean} hasException
* True if this batch has encountered an exception. This is cleared at the start of each operation
- * @property hasException
- * @type Boolean
*/
hasException: false,
-
+
/**
- * True to automatically pause the execution of the batch if any operation encounters an exception (defaults to true)
- * @property pauseOnException
- * @type Boolean
+ * @property {Boolean} pauseOnException
+ * True to automatically pause the execution of the batch if any operation encounters an exception
*/
pauseOnException: true,
-
- constructor: function(config) {
+
+ /**
+ * Creates new Batch object.
+ * @param {Object} [config] Config object
+ */
+ constructor: function(config) {
var me = this;
-
+
me.addEvents(
/**
* @event complete
@@ -51161,7 +54968,7 @@ Ext.define('Ext.data.Batch', {
* @param {Object} operation The last operation that was executed
*/
'complete',
-
+
/**
* @event exception
* Fired when a operation encountered an exception
@@ -51169,7 +54976,7 @@ Ext.define('Ext.data.Batch', {
* @param {Object} operation The operation that encountered the exception
*/
'exception',
-
+
/**
* @event operationcomplete
* Fired when each operation of the batch completes
@@ -51178,29 +54985,28 @@ Ext.define('Ext.data.Batch', {
*/
'operationcomplete'
);
-
+
me.mixins.observable.constructor.call(me, config);
-
+
/**
* Ordered array of operations that will be executed by this batch
- * @property operations
- * @type Array
+ * @property {Ext.data.Operation[]} operations
*/
me.operations = [];
},
-
+
/**
* Adds a new operation to this batch
* @param {Object} operation The {@link Ext.data.Operation Operation} object
*/
add: function(operation) {
this.total++;
-
+
operation.setBatch(this);
-
+
this.operations.push(operation);
},
-
+
/**
* Kicks off the execution of the batch, continuing from the next operation if the previous
* operation encountered an exception, or if execution was paused
@@ -51208,10 +55014,10 @@ Ext.define('Ext.data.Batch', {
start: function() {
this.hasException = false;
this.isRunning = true;
-
+
this.runNextOperation();
},
-
+
/**
* @private
* Runs the next operation, relative to this.current.
@@ -51219,14 +55025,14 @@ Ext.define('Ext.data.Batch', {
runNextOperation: function() {
this.runOperation(this.current + 1);
},
-
+
/**
* Pauses execution of the batch, but does not cancel the current operation
*/
pause: function() {
this.isRunning = false;
},
-
+
/**
* Executes a operation by its numeric index
* @param {Number} index The operation index to run
@@ -51236,17 +55042,17 @@ Ext.define('Ext.data.Batch', {
operations = me.operations,
operation = operations[index],
onProxyReturn;
-
+
if (operation === undefined) {
me.isRunning = false;
me.isComplete = true;
me.fireEvent('complete', me, operations[operations.length - 1]);
} else {
me.current = index;
-
+
onProxyReturn = function(operation) {
var hasException = operation.hasException();
-
+
if (hasException) {
me.hasException = true;
me.fireEvent('exception', me, operation);
@@ -51261,9 +55067,9 @@ Ext.define('Ext.data.Batch', {
me.runNextOperation();
}
};
-
+
operation.setStarted();
-
+
me.proxy[operation.action](operation, onProxyReturn, me);
}
}
@@ -51273,117 +55079,110 @@ Ext.define('Ext.data.Batch', {
* @class Ext.data.BelongsToAssociation
* @extends Ext.data.Association
*
- * Represents a many to one association with another model. The owner model is expected to have
- * a foreign key which references the primary key of the associated model:
+ * Represents a many to one association with another model. The owner model is expected to have
+ * a foreign key which references the primary key of the associated model:
*
-
-Ext.define('Category', {
- extend: 'Ext.data.Model',
- fields: [
- {name: 'id', type: 'int'},
- {name: 'name', type: 'string'}
- ]
-});
-
-Ext.define('Product', {
- extend: 'Ext.data.Model',
- fields: [
- {name: 'id', type: 'int'},
- {name: 'category_id', type: 'int'},
- {name: 'name', type: 'string'}
- ],
- // we can use the belongsTo shortcut on the model to create a belongsTo association
- belongsTo: {type: 'belongsTo', model: 'Category'}
-});
-
- * In the example above we have created models for Products and Categories, and linked them together
+ * Ext.define('Category', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * { name: 'id', type: 'int' },
+ * { name: 'name', type: 'string' }
+ * ]
+ * });
+ *
+ * Ext.define('Product', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * { name: 'id', type: 'int' },
+ * { name: 'category_id', type: 'int' },
+ * { name: 'name', type: 'string' }
+ * ],
+ * // we can use the belongsTo shortcut on the model to create a belongsTo association
+ * associations: [
+ * { type: 'belongsTo', model: 'Category' }
+ * ]
+ * });
+ *
+ * In the example above we have created models for Products and Categories, and linked them together
* by saying that each Product belongs to a Category. This automatically links each Product to a Category
- * based on the Product's category_id, and provides new functions on the Product model:
+ * based on the Product's category_id, and provides new functions on the Product model:
*
- * Generated getter function
+ * ## Generated getter function
*
- * The first function that is added to the owner model is a getter function:
+ * The first function that is added to the owner model is a getter function:
*
-
-var product = new Product({
- id: 100,
- category_id: 20,
- name: 'Sneakers'
-});
-
-product.getCategory(function(category, operation) {
- //do something with the category object
- alert(category.get('id')); //alerts 20
-}, this);
-
-*
- * The getCategory function was created on the Product model when we defined the association. This uses the
+ * var product = new Product({
+ * id: 100,
+ * category_id: 20,
+ * name: 'Sneakers'
+ * });
+ *
+ * product.getCategory(function(category, operation) {
+ * // do something with the category object
+ * alert(category.get('id')); // alerts 20
+ * }, this);
+ *
+ * The getCategory function was created on the Product model when we defined the association. This uses the
* Category's configured {@link Ext.data.proxy.Proxy proxy} to load the Category asynchronously, calling the provided
- * callback when it has loaded.
+ * callback when it has loaded.
*
- * The new getCategory function will also accept an object containing success, failure and callback properties
+ * The new getCategory function will also accept an object containing success, failure and callback properties
* - callback will always be called, success will only be called if the associated model was loaded successfully
- * and failure will only be called if the associatied model could not be loaded:
+ * and failure will only be called if the associatied model could not be loaded:
*
-
-product.getCategory({
- callback: function(category, operation) {}, //a function that will always be called
- success : function(category, operation) {}, //a function that will only be called if the load succeeded
- failure : function(category, operation) {}, //a function that will only be called if the load did not succeed
- scope : this //optionally pass in a scope object to execute the callbacks in
-});
-
+ * product.getCategory({
+ * callback: function(category, operation) {}, // a function that will always be called
+ * success : function(category, operation) {}, // a function that will only be called if the load succeeded
+ * failure : function(category, operation) {}, // a function that will only be called if the load did not succeed
+ * scope : this // optionally pass in a scope object to execute the callbacks in
+ * });
*
- * In each case above the callbacks are called with two arguments - the associated model instance and the
+ * In each case above the callbacks are called with two arguments - the associated model instance and the
* {@link Ext.data.Operation operation} object that was executed to load that instance. The Operation object is
- * useful when the instance could not be loaded.
+ * useful when the instance could not be loaded.
*
- * Generated setter function
+ * ## Generated setter function
*
- * The second generated function sets the associated model instance - if only a single argument is passed to
- * the setter then the following two calls are identical:
+ * The second generated function sets the associated model instance - if only a single argument is passed to
+ * the setter then the following two calls are identical:
*
-
-//this call
-product.setCategory(10);
-
-//is equivalent to this call:
-product.set('category_id', 10);
-
- * If we pass in a second argument, the model will be automatically saved and the second argument passed to
- * the owner model's {@link Ext.data.Model#save save} method:
-
-product.setCategory(10, function(product, operation) {
- //the product has been saved
- alert(product.get('category_id')); //now alerts 10
-});
-
-//alternative syntax:
-product.setCategory(10, {
- callback: function(product, operation), //a function that will always be called
- success : function(product, operation), //a function that will only be called if the load succeeded
- failure : function(product, operation), //a function that will only be called if the load did not succeed
- scope : this //optionally pass in a scope object to execute the callbacks in
-})
-
-*
- * Customisation
+ * // this call...
+ * product.setCategory(10);
*
- * Associations reflect on the models they are linking to automatically set up properties such as the
- * {@link #primaryKey} and {@link #foreignKey}. These can alternatively be specified:
+ * // is equivalent to this call:
+ * product.set('category_id', 10);
*
-
-Ext.define('Product', {
- fields: [...],
-
- associations: [
- {type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id'}
- ]
-});
-
+ * If we pass in a second argument, the model will be automatically saved and the second argument passed to
+ * the owner model's {@link Ext.data.Model#save save} method:
+ *
+ * product.setCategory(10, function(product, operation) {
+ * // the product has been saved
+ * alert(product.get('category_id')); //now alerts 10
+ * });
+ *
+ * //alternative syntax:
+ * product.setCategory(10, {
+ * callback: function(product, operation), // a function that will always be called
+ * success : function(product, operation), // a function that will only be called if the load succeeded
+ * failure : function(product, operation), // a function that will only be called if the load did not succeed
+ * scope : this //optionally pass in a scope object to execute the callbacks in
+ * })
+ *
+ * ## Customisation
+ *
+ * Associations reflect on the models they are linking to automatically set up properties such as the
+ * {@link #primaryKey} and {@link #foreignKey}. These can alternatively be specified:
+ *
+ * Ext.define('Product', {
+ * fields: [...],
*
- * Here we replaced the default primary key (defaults to 'id') and foreign key (calculated as 'category_id')
- * with our own settings. Usually this will not be needed.
+ * associations: [
+ * { type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id' }
+ * ]
+ * });
+ *
+ * Here we replaced the default primary key (defaults to 'id') and foreign key (calculated as 'category_id')
+ * with our own settings. Usually this will not be needed.
*/
Ext.define('Ext.data.BelongsToAssociation', {
extend: 'Ext.data.Association',
@@ -51394,26 +55193,25 @@ Ext.define('Ext.data.BelongsToAssociation', {
* @cfg {String} foreignKey The name of the foreign key on the owner model that links it to the associated
* model. Defaults to the lowercased name of the associated model plus "_id", e.g. an association with a
* model called Product would set up a product_id foreign key.
- *
-Ext.define('Order', {
- extend: 'Ext.data.Model',
- fields: ['id', 'date'],
- hasMany: 'Product'
-});
-
-Ext.define('Product', {
- extend: 'Ext.data.Model',
- fields: ['id', 'name', 'order_id'], // refers to the id of the order that this product belongs to
- belongsTo: 'Group'
-});
-var product = new Product({
- id: 1,
- name: 'Product 1',
- order_id: 22
-}, 1);
-product.getOrder(); // Will make a call to the server asking for order_id 22
-
- *
+ *
+ * Ext.define('Order', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'date'],
+ * hasMany: 'Product'
+ * });
+ *
+ * Ext.define('Product', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name', 'order_id'], // refers to the id of the order that this product belongs to
+ * belongsTo: 'Group'
+ * });
+ * var product = new Product({
+ * id: 1,
+ * name: 'Product 1',
+ * order_id: 22
+ * }, 1);
+ * product.getOrder(); // Will make a call to the server asking for order_id 22
+ *
*/
/**
@@ -51425,18 +55223,16 @@ product.getOrder(); // Will make a call to the server asking for order_id 22
* @cfg {String} setterName The name of the setter function that will be added to the local model's prototype.
* Defaults to 'set' + the name of the foreign model, e.g. setCategory
*/
-
+
/**
* @cfg {String} type The type configuration can be used when creating associations using a configuration object.
* Use 'belongsTo' to create a HasManyAssocation
- *
-associations: [{
- type: 'belongsTo',
- model: 'User'
-}]
- *
+ *
+ * associations: [{
+ * type: 'belongsTo',
+ * model: 'User'
+ * }]
*/
-
constructor: function(config) {
this.callParent(arguments);
@@ -51505,38 +55301,37 @@ associations: [{
return function(options, scope) {
options = options || {};
- var foreignKeyId = this.get(foreignKey),
- instance, callbackFn;
+ var model = this,
+ foreignKeyId = model.get(foreignKey),
+ instance,
+ args;
- if (this[instanceName] === undefined) {
+ if (model[instanceName] === undefined) {
instance = Ext.ModelManager.create({}, associatedName);
instance.set(primaryKey, foreignKeyId);
if (typeof options == 'function') {
options = {
callback: options,
- scope: scope || this
+ scope: scope || model
};
}
associatedModel.load(foreignKeyId, options);
+ model[instanceName] = associatedModel;
+ return associatedModel;
} else {
- instance = this[instanceName];
+ instance = model[instanceName];
+ args = [instance];
+ scope = scope || model;
//TODO: We're duplicating the callback invokation code that the instance.load() call above
//makes here - ought to be able to normalize this - perhaps by caching at the Model.load layer
//instead of the association layer.
- if (typeof options == 'function') {
- options.call(scope || this, instance);
- }
-
- if (options.success) {
- options.success.call(scope || this, instance);
- }
-
- if (options.callback) {
- options.callback.call(scope || this, instance);
- }
+ Ext.callback(options, scope, args);
+ Ext.callback(options.success, scope, args);
+ Ext.callback(options.failure, scope, args);
+ Ext.callback(options.callback, scope, args);
return instance;
}
@@ -51571,73 +55366,62 @@ Ext.define('Ext.data.BufferStore', {
}
});
/**
- * @class Ext.direct.Manager
- * Overview
+ * Ext.Direct aims to streamline communication between the client and server by providing a single interface that
+ * reduces the amount of common code typically required to validate data and handle returned data packets (reading data,
+ * error conditions, etc).
*
- * Ext.Direct aims to streamline communication between the client and server
- * by providing a single interface that reduces the amount of common code
- * typically required to validate data and handle returned data packets
- * (reading data, error conditions, etc).
+ * The Ext.direct namespace includes several classes for a closer integration with the server-side. The Ext.data
+ * namespace also includes classes for working with Ext.data.Stores which are backed by data from an Ext.Direct method.
*
- * The Ext.direct namespace includes several classes for a closer integration
- * with the server-side. The Ext.data namespace also includes classes for working
- * with Ext.data.Stores which are backed by data from an Ext.Direct method.
+ * # Specification
*
- * Specification
+ * For additional information consult the [Ext.Direct Specification][1].
*
- * For additional information consult the
- * Ext.Direct Specification.
+ * # Providers
*
- * Providers
+ * Ext.Direct uses a provider architecture, where one or more providers are used to transport data to and from the
+ * server. There are several providers that exist in the core at the moment:
*
- * Ext.Direct uses a provider architecture, where one or more providers are
- * used to transport data to and from the server. There are several providers
- * that exist in the core at the moment:
+ * - {@link Ext.direct.JsonProvider JsonProvider} for simple JSON operations
+ * - {@link Ext.direct.PollingProvider PollingProvider} for repeated requests
+ * - {@link Ext.direct.RemotingProvider RemotingProvider} exposes server side on the client.
*
- * - {@link Ext.direct.JsonProvider JsonProvider} for simple JSON operations
- * - {@link Ext.direct.PollingProvider PollingProvider} for repeated requests
- * - {@link Ext.direct.RemotingProvider RemotingProvider} exposes server side
- * on the client.
- *
+ * A provider does not need to be invoked directly, providers are added via {@link Ext.direct.Manager}.{@link #addProvider}.
*
- * A provider does not need to be invoked directly, providers are added via
- * {@link Ext.direct.Manager}.{@link Ext.direct.Manager#add add}.
+ * # Router
*
- * Router
+ * Ext.Direct utilizes a "router" on the server to direct requests from the client to the appropriate server-side
+ * method. Because the Ext.Direct API is completely platform-agnostic, you could completely swap out a Java based server
+ * solution and replace it with one that uses C# without changing the client side JavaScript at all.
*
- * Ext.Direct utilizes a "router" on the server to direct requests from the client
- * to the appropriate server-side method. Because the Ext.Direct API is completely
- * platform-agnostic, you could completely swap out a Java based server solution
- * and replace it with one that uses C# without changing the client side JavaScript
- * at all.
+ * # Server side events
*
- * Server side events
+ * Custom events from the server may be handled by the client by adding listeners, for example:
+ *
+ * {"type":"event","name":"message","data":"Successfully polled at: 11:19:30 am"}
+ *
+ * // add a handler for a 'message' event sent by the server
+ * Ext.direct.Manager.on('message', function(e){
+ * out.append(String.format('{0}
', e.data));
+ * out.el.scrollTo('t', 100000, true);
+ * });
+ *
+ * [1]: http://sencha.com/products/extjs/extdirect
*
- * Custom events from the server may be handled by the client by adding
- * listeners, for example:
- *
-{"type":"event","name":"message","data":"Successfully polled at: 11:19:30 am"}
-
-// add a handler for a 'message' event sent by the server
-Ext.direct.Manager.on('message', function(e){
- out.append(String.format('<p><i>{0}</i></p>', e.data));
- out.el.scrollTo('t', 100000, true);
-});
- *
* @singleton
+ * @alternateClassName Ext.Direct
*/
-
Ext.define('Ext.direct.Manager', {
-
+
/* Begin Definitions */
singleton: true,
-
+
mixins: {
observable: 'Ext.util.Observable'
},
-
+
requires: ['Ext.util.MixedCollection'],
-
+
statics: {
exceptions: {
TRANSPORT: 'xhr',
@@ -51646,74 +55430,75 @@ Ext.define('Ext.direct.Manager', {
SERVER: 'exception'
}
},
-
+
/* End Definitions */
-
+
constructor: function(){
var me = this;
-
+
me.addEvents(
/**
* @event event
* Fires after an event.
- * @param {event} e The Ext.direct.Event type that occurred.
+ * @param {Ext.direct.Event} e The Ext.direct.Event type that occurred.
* @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
*/
'event',
/**
* @event exception
* Fires after an event exception.
- * @param {event} e The Ext.direct.Event type that occurred.
+ * @param {Ext.direct.Event} e The event type that occurred.
*/
'exception'
);
me.transactions = Ext.create('Ext.util.MixedCollection');
me.providers = Ext.create('Ext.util.MixedCollection');
-
+
me.mixins.observable.constructor.call(me);
},
-
- /**
- * Adds an Ext.Direct Provider and creates the proxy or stub methods to execute server-side methods.
- * If the provider is not already connected, it will auto-connect.
- *
-var pollProv = new Ext.direct.PollingProvider({
- url: 'php/poll2.php'
-});
-Ext.direct.Manager.addProvider({
- "type":"remoting", // create a {@link Ext.direct.RemotingProvider}
- "url":"php\/router.php", // url to connect to the Ext.Direct server-side router.
- "actions":{ // each property within the actions object represents a Class
- "TestAction":[ // array of methods within each server side Class
- {
- "name":"doEcho", // name of method
- "len":1
- },{
- "name":"multiply",
- "len":1
- },{
- "name":"doForm",
- "formHandler":true, // handle form on server with Ext.Direct.Transaction
- "len":1
- }]
- },
- "namespace":"myApplication",// namespace to create the Remoting Provider in
-},{
- type: 'polling', // create a {@link Ext.direct.PollingProvider}
- url: 'php/poll.php'
-}, pollProv); // reference to previously created instance
- *
- * @param {Object/Array} provider Accepts either an Array of Provider descriptions (an instance
- * or config object for a Provider) or any number of Provider descriptions as arguments. Each
- * Provider description instructs Ext.Direct how to create client-side stub methods.
+ /**
+ * Adds an Ext.Direct Provider and creates the proxy or stub methods to execute server-side methods. If the provider
+ * is not already connected, it will auto-connect.
+ *
+ * var pollProv = new Ext.direct.PollingProvider({
+ * url: 'php/poll2.php'
+ * });
+ *
+ * Ext.direct.Manager.addProvider({
+ * "type":"remoting", // create a {@link Ext.direct.RemotingProvider}
+ * "url":"php\/router.php", // url to connect to the Ext.Direct server-side router.
+ * "actions":{ // each property within the actions object represents a Class
+ * "TestAction":[ // array of methods within each server side Class
+ * {
+ * "name":"doEcho", // name of method
+ * "len":1
+ * },{
+ * "name":"multiply",
+ * "len":1
+ * },{
+ * "name":"doForm",
+ * "formHandler":true, // handle form on server with Ext.Direct.Transaction
+ * "len":1
+ * }]
+ * },
+ * "namespace":"myApplication",// namespace to create the Remoting Provider in
+ * },{
+ * type: 'polling', // create a {@link Ext.direct.PollingProvider}
+ * url: 'php/poll.php'
+ * }, pollProv); // reference to previously created instance
+ *
+ * @param {Ext.direct.Provider/Object...} provider
+ * Accepts any number of Provider descriptions (an instance or config object for
+ * a Provider). Each Provider description instructs Ext.Directhow to create
+ * client-side stub methods.
*/
addProvider : function(provider){
var me = this,
args = arguments,
i = 0,
len;
-
+
if (args.length > 1) {
for (len = args.length; i < len; ++i) {
me.addProvider(args[i]);
@@ -51735,17 +55520,16 @@ Ext.direct.Manager.addProvider({
return provider;
},
-
+
/**
- * Retrieve a {@link Ext.direct.Provider provider} by the
- * {@link Ext.direct.Provider#id id} specified when the provider is
- * {@link #addProvider added}.
- * @param {String/Ext.data.Provider} id The id of the provider, or the provider instance.
+ * Retrieves a {@link Ext.direct.Provider provider} by the **{@link Ext.direct.Provider#id id}** specified when the
+ * provider is {@link #addProvider added}.
+ * @param {String/Ext.direct.Provider} id The id of the provider, or the provider instance.
*/
getProvider : function(id){
return id.isProvider ? id : this.providers.get(id);
},
-
+
/**
* Removes the provider.
* @param {String/Ext.direct.Provider} provider The provider instance or the id of the provider.
@@ -51753,9 +55537,10 @@ Ext.direct.Manager.addProvider({
*/
removeProvider : function(provider){
var me = this,
- providers = me.providers,
- provider = provider.isProvider ? provider : providers.get(provider);
-
+ providers = me.providers;
+
+ provider = provider.isProvider ? provider : providers.get(provider);
+
if (provider) {
provider.un('data', me.onProviderData, me);
providers.remove(provider);
@@ -51763,9 +55548,9 @@ Ext.direct.Manager.addProvider({
}
return null;
},
-
+
/**
- * Add a transaction to the manager.
+ * Adds a transaction to the manager.
* @private
* @param {Ext.direct.Transaction} transaction The transaction to add
* @return {Ext.direct.Transaction} transaction
@@ -51776,7 +55561,7 @@ Ext.direct.Manager.addProvider({
},
/**
- * Remove a transaction from the manager.
+ * Removes a transaction from the manager.
* @private
* @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction to remove
* @return {Ext.direct.Transaction} transaction
@@ -51796,12 +55581,12 @@ Ext.direct.Manager.addProvider({
getTransaction: function(transaction){
return transaction.isTransaction ? transaction : this.transactions.get(transaction);
},
-
+
onProviderData : function(provider, event){
var me = this,
i = 0,
len;
-
+
if (Ext.isArray(event)) {
for (len = event.length; i < len; ++i) {
me.onProviderData(provider, event[i]);
@@ -51810,7 +55595,7 @@ Ext.direct.Manager.addProvider({
}
if (event.name && event.name != 'event' && event.name != 'exception') {
me.fireEvent(event.name, event);
- } else if (event.type == 'exception') {
+ } else if (event.status === false) {
me.fireEvent('exception', event);
}
me.fireEvent('event', event, provider);
@@ -51821,27 +55606,26 @@ Ext.direct.Manager.addProvider({
});
/**
- * @class Ext.data.proxy.Direct
- * @extends Ext.data.proxy.Server
- *
- * This class is used to send requests to the server using {@link Ext.direct}. When a request is made,
- * the transport mechanism is handed off to the appropriate {@link Ext.direct.RemotingProvider Provider}
- * to complete the call.
- *
- * ## Specifying the function
+ * This class is used to send requests to the server using {@link Ext.direct.Manager Ext.Direct}. When a
+ * request is made, the transport mechanism is handed off to the appropriate
+ * {@link Ext.direct.RemotingProvider Provider} to complete the call.
+ *
+ * # Specifying the function
+ *
* This proxy expects a Direct remoting method to be passed in order to be able to complete requests.
* This can be done by specifying the {@link #directFn} configuration. This will use the same direct
* method for all requests. Alternatively, you can provide an {@link #api} configuration. This
* allows you to specify a different remoting method for each CRUD action.
- *
- * ## Paramaters
+ *
+ * # Parameters
+ *
* This proxy provides options to help configure which parameters will be sent to the server.
* By specifying the {@link #paramsAsHash} option, it will send an object literal containing each
* of the passed parameters. The {@link #paramOrder} option can be used to specify the order in which
* the remoting method parameters are passed.
- *
- * ## Example Usage
- *
+ *
+ * # Example Usage
+ *
* Ext.define('User', {
* extend: 'Ext.data.Model',
* fields: ['firstName', 'lastName'],
@@ -51855,34 +55639,34 @@ Ext.direct.Manager.addProvider({
*/
Ext.define('Ext.data.proxy.Direct', {
/* Begin Definitions */
-
+
extend: 'Ext.data.proxy.Server',
alternateClassName: 'Ext.data.DirectProxy',
-
+
alias: 'proxy.direct',
-
+
requires: ['Ext.direct.Manager'],
-
+
/* End Definitions */
-
- /**
- * @cfg {Array/String} paramOrder Defaults to undefined. A list of params to be executed
- * server side. Specify the params in the order in which they must be executed on the server-side
- * as either (1) an Array of String values, or (2) a String of params delimited by either whitespace,
- * comma, or pipe. For example,
- * any of the following would be acceptable:
-paramOrder: ['param1','param2','param3']
-paramOrder: 'param1 param2 param3'
-paramOrder: 'param1,param2,param3'
-paramOrder: 'param1|param2|param'
-
+
+ /**
+ * @cfg {String/String[]} paramOrder
+ * Defaults to undefined. A list of params to be executed server side. Specify the params in the order in
+ * which they must be executed on the server-side as either (1) an Array of String values, or (2) a String
+ * of params delimited by either whitespace, comma, or pipe. For example, any of the following would be
+ * acceptable:
+ *
+ * paramOrder: ['param1','param2','param3']
+ * paramOrder: 'param1 param2 param3'
+ * paramOrder: 'param1,param2,param3'
+ * paramOrder: 'param1|param2|param'
*/
paramOrder: undefined,
/**
* @cfg {Boolean} paramsAsHash
- * Send parameters as a collection of named arguments (defaults to true). Providing a
- * {@link #paramOrder} nullifies this configuration.
+ * Send parameters as a collection of named arguments.
+ * Providing a {@link #paramOrder} nullifies this configuration.
*/
paramsAsHash: true,
@@ -51892,30 +55676,32 @@ paramOrder: 'param1|param2|param'
* for Store's which will not implement a full CRUD api.
*/
directFn : undefined,
-
+
/**
- * @cfg {Object} api The same as {@link Ext.data.proxy.Server#api}, however instead of providing urls, you should provide a direct
+ * @cfg {Object} api
+ * The same as {@link Ext.data.proxy.Server#api}, however instead of providing urls, you should provide a direct
* function call.
*/
-
+
/**
- * @cfg {Object} extraParams Extra parameters that will be included on every read request. Individual requests with params
+ * @cfg {Object} extraParams
+ * Extra parameters that will be included on every read request. Individual requests with params
* of the same name will override these params when they are in conflict.
*/
-
+
// private
paramOrderRe: /[\s,|]/,
-
+
constructor: function(config){
var me = this;
-
+
Ext.apply(me, config);
if (Ext.isString(me.paramOrder)) {
me.paramOrder = me.paramOrder.split(me.paramOrderRe);
}
me.callParent(arguments);
},
-
+
doRequest: function(operation, callback, scope) {
var me = this,
writer = me.getWriter(),
@@ -51927,21 +55713,21 @@ paramOrder: 'param1|param2|param'
method,
i = 0,
len;
-
+
//
if (!fn) {
Ext.Error.raise('No direct function specified for this proxy');
}
//
-
+
if (operation.allowWrite()) {
request = writer.write(request);
}
-
+
if (operation.action == 'read') {
// We need to pass params
method = fn.directCfg.method;
-
+
if (method.ordered) {
if (method.len > 0) {
if (paramOrder) {
@@ -51958,7 +55744,7 @@ paramOrder: 'param1|param2|param'
} else {
args.push(request.jsonData);
}
-
+
Ext.apply(request, {
args: args,
directFn: fn
@@ -51966,7 +55752,7 @@ paramOrder: 'param1|param2|param'
args.push(me.createRequestCallback(request, operation, callback, scope), me);
fn.apply(window, args);
},
-
+
/*
* Inherit docs. We don't apply any encoding here because
* all of the direct requests go out as jsonData
@@ -51974,25 +55760,25 @@ paramOrder: 'param1|param2|param'
applyEncoding: function(value){
return value;
},
-
+
createRequestCallback: function(request, operation, callback, scope){
var me = this;
-
+
return function(data, event){
me.processResponse(event.status, operation, request, event, callback, scope);
};
},
-
+
// inherit docs
extractResponseData: function(response){
return Ext.isDefined(response.result) ? response.result : response.data;
},
-
+
// inherit docs
setException: function(operation, response) {
operation.setException(response.message);
},
-
+
// inherit docs
buildUrl: function(){
return '';
@@ -52000,39 +55786,28 @@ paramOrder: 'param1|param2|param'
});
/**
- * @class Ext.data.DirectStore
- * @extends Ext.data.Store
- * Small helper class to create an {@link Ext.data.Store} configured with an
- * {@link Ext.data.proxy.Direct} and {@link Ext.data.reader.Json} to make interacting
- * with an {@link Ext.Direct} Server-side {@link Ext.direct.Provider Provider} easier.
- * To create a different proxy/reader combination create a basic {@link Ext.data.Store}
- * configured as needed.
+ * Small helper class to create an {@link Ext.data.Store} configured with an {@link Ext.data.proxy.Direct}
+ * and {@link Ext.data.reader.Json} to make interacting with an {@link Ext.direct.Manager} server-side
+ * {@link Ext.direct.Provider Provider} easier. To create a different proxy/reader combination create a basic
+ * {@link Ext.data.Store} configured as needed.
*
- * *Note: Although they are not listed, this class inherits all of the config options of:
- *
- * - {@link Ext.data.Store Store}
- *
+ * **Note:** Although they are not listed, this class inherits all of the config options of:
*
- *
- * - {@link Ext.data.reader.Json JsonReader}
- *
- * - {@link Ext.data.reader.Json#root root}
- * - {@link Ext.data.reader.Json#idProperty idProperty}
- * - {@link Ext.data.reader.Json#totalProperty totalProperty}
- *
+ * - **{@link Ext.data.Store Store}**
*
- * - {@link Ext.data.proxy.Direct DirectProxy}
- *
- * - {@link Ext.data.proxy.Direct#directFn directFn}
- * - {@link Ext.data.proxy.Direct#paramOrder paramOrder}
- * - {@link Ext.data.proxy.Direct#paramsAsHash paramsAsHash}
- *
- *
+ * - **{@link Ext.data.reader.Json JsonReader}**
+ *
+ * - **{@link Ext.data.reader.Json#root root}**
+ * - **{@link Ext.data.reader.Json#idProperty idProperty}**
+ * - **{@link Ext.data.reader.Json#totalProperty totalProperty}**
+ *
+ * - **{@link Ext.data.proxy.Direct DirectProxy}**
+ *
+ * - **{@link Ext.data.proxy.Direct#directFn directFn}**
+ * - **{@link Ext.data.proxy.Direct#paramOrder paramOrder}**
+ * - **{@link Ext.data.proxy.Direct#paramsAsHash paramsAsHash}**
*
- * @constructor
- * @param {Object} config
*/
-
Ext.define('Ext.data.DirectStore', {
/* Begin Definitions */
@@ -52043,8 +55818,8 @@ Ext.define('Ext.data.DirectStore', {
requires: ['Ext.data.proxy.Direct'],
/* End Definitions */
-
- constructor : function(config){
+
+ constructor : function(config){
config = Ext.apply({}, config);
if (!config.proxy) {
var proxy = {
@@ -52062,51 +55837,40 @@ Ext.define('Ext.data.DirectStore', {
});
/**
- * @class Ext.util.Inflector
- * @extends Object
- * General purpose inflector class that {@link #pluralize pluralizes}, {@link #singularize singularizes} and
- * {@link #ordinalize ordinalizes} words. Sample usage:
- *
-
-//turning singular words into plurals
-Ext.util.Inflector.pluralize('word'); //'words'
-Ext.util.Inflector.pluralize('person'); //'people'
-Ext.util.Inflector.pluralize('sheep'); //'sheep'
-
-//turning plurals into singulars
-Ext.util.Inflector.singularize('words'); //'word'
-Ext.util.Inflector.singularize('people'); //'person'
-Ext.util.Inflector.singularize('sheep'); //'sheep'
-
-//ordinalizing numbers
-Ext.util.Inflector.ordinalize(11); //"11th"
-Ext.util.Inflector.ordinalize(21); //"21th"
-Ext.util.Inflector.ordinalize(1043); //"1043rd"
-
- *
- * Customization
- *
- * The Inflector comes with a default set of US English pluralization rules. These can be augmented with additional
+ * General purpose inflector class that {@link #pluralize pluralizes}, {@link #singularize singularizes} and
+ * {@link #ordinalize ordinalizes} words. Sample usage:
+ *
+ * //turning singular words into plurals
+ * Ext.util.Inflector.pluralize('word'); //'words'
+ * Ext.util.Inflector.pluralize('person'); //'people'
+ * Ext.util.Inflector.pluralize('sheep'); //'sheep'
+ *
+ * //turning plurals into singulars
+ * Ext.util.Inflector.singularize('words'); //'word'
+ * Ext.util.Inflector.singularize('people'); //'person'
+ * Ext.util.Inflector.singularize('sheep'); //'sheep'
+ *
+ * //ordinalizing numbers
+ * Ext.util.Inflector.ordinalize(11); //"11th"
+ * Ext.util.Inflector.ordinalize(21); //"21th"
+ * Ext.util.Inflector.ordinalize(1043); //"1043rd"
+ *
+ * # Customization
+ *
+ * The Inflector comes with a default set of US English pluralization rules. These can be augmented with additional
* rules if the default rules do not meet your application's requirements, or swapped out entirely for other languages.
- * Here is how we might add a rule that pluralizes "ox" to "oxen":
- *
-
-Ext.util.Inflector.plural(/^(ox)$/i, "$1en");
-
- *
- * Each rule consists of two items - a regular expression that matches one or more rules, and a replacement string.
- * In this case, the regular expression will only match the string "ox", and will replace that match with "oxen".
- * Here's how we could add the inverse rule:
- *
-
-Ext.util.Inflector.singular(/^(ox)en$/i, "$1");
-
- *
- * Note that the ox/oxen rules are present by default.
- *
- * @singleton
+ * Here is how we might add a rule that pluralizes "ox" to "oxen":
+ *
+ * Ext.util.Inflector.plural(/^(ox)$/i, "$1en");
+ *
+ * Each rule consists of two items - a regular expression that matches one or more rules, and a replacement string. In
+ * this case, the regular expression will only match the string "ox", and will replace that match with "oxen". Here's
+ * how we could add the inverse rule:
+ *
+ * Ext.util.Inflector.singular(/^(ox)en$/i, "$1");
+ *
+ * Note that the ox/oxen rules are present by default.
*/
-
Ext.define('Ext.util.Inflector', {
/* Begin Definitions */
@@ -52120,8 +55884,7 @@ Ext.define('Ext.util.Inflector', {
* The registered plural tuples. Each item in the array should contain two items - the first must be a regular
* expression that matchers the singular form of a word, the second must be a String that replaces the matched
* part of the regular expression. This is managed by the {@link #plural} method.
- * @property plurals
- * @type Array
+ * @property {Array} plurals
*/
plurals: [
[(/(quiz)$/i), "$1zes" ],
@@ -52145,14 +55908,13 @@ Ext.define('Ext.util.Inflector', {
[(/s$/i), "s" ],
[(/$/), "s" ]
],
-
+
/**
* @private
- * The set of registered singular matchers. Each item in the array should contain two items - the first must be a
- * regular expression that matches the plural form of a word, the second must be a String that replaces the
+ * The set of registered singular matchers. Each item in the array should contain two items - the first must be a
+ * regular expression that matches the plural form of a word, the second must be a String that replaces the
* matched part of the regular expression. This is managed by the {@link #singular} method.
- * @property singulars
- * @type Array
+ * @property {Array} singulars
*/
singulars: [
[(/(quiz)zes$/i), "$1" ],
@@ -52181,12 +55943,11 @@ Ext.define('Ext.util.Inflector', {
[(/people$/i), "person" ],
[(/s$/i), "" ]
],
-
+
/**
* @private
* The registered uncountable words
- * @property uncountable
- * @type Array
+ * @property {String[]} uncountable
*/
uncountable: [
"sheep",
@@ -52203,7 +55964,7 @@ Ext.define('Ext.util.Inflector', {
"deer",
"means"
],
-
+
/**
* Adds a new singularization rule to the Inflector. See the intro docs for more information
* @param {RegExp} matcher The matcher regex
@@ -52212,7 +55973,7 @@ Ext.define('Ext.util.Inflector', {
singular: function(matcher, replacer) {
this.singulars.unshift([matcher, replacer]);
},
-
+
/**
* Adds a new pluralization rule to the Inflector. See the intro docs for more information
* @param {RegExp} matcher The matcher regex
@@ -52221,21 +55982,21 @@ Ext.define('Ext.util.Inflector', {
plural: function(matcher, replacer) {
this.plurals.unshift([matcher, replacer]);
},
-
+
/**
* Removes all registered singularization rules
*/
clearSingulars: function() {
this.singulars = [];
},
-
+
/**
* Removes all registered pluralization rules
*/
clearPlurals: function() {
this.plurals = [];
},
-
+
/**
* Returns true if the given word is transnumeral (the word is its own singular and plural form - e.g. sheep, fish)
* @param {String} word The word to test
@@ -52258,19 +56019,19 @@ Ext.define('Ext.util.Inflector', {
var plurals = this.plurals,
length = plurals.length,
tuple, regex, i;
-
+
for (i = 0; i < length; i++) {
tuple = plurals[i];
regex = tuple[0];
-
+
if (regex == word || (regex.test && regex.test(word))) {
return word.replace(regex, tuple[1]);
}
}
-
+
return word;
},
-
+
/**
* Returns the singularized form of a word (e.g. Ext.util.Inflector.singularize('words') returns 'word')
* @param {String} word The word to singularize
@@ -52284,21 +56045,21 @@ Ext.define('Ext.util.Inflector', {
var singulars = this.singulars,
length = singulars.length,
tuple, regex, i;
-
+
for (i = 0; i < length; i++) {
tuple = singulars[i];
regex = tuple[0];
-
+
if (regex == word || (regex.test && regex.test(word))) {
return word.replace(regex, tuple[1]);
}
}
-
+
return word;
},
-
+
/**
- * Returns the correct {@link Ext.data.Model Model} name for a given string. Mostly used internally by the data
+ * Returns the correct {@link Ext.data.Model Model} name for a given string. Mostly used internally by the data
* package
* @param {String} word The word to classify
* @return {String} The classified version of the word
@@ -52306,9 +56067,9 @@ Ext.define('Ext.util.Inflector', {
classify: function(word) {
return Ext.String.capitalize(this.singularize(word));
},
-
+
/**
- * Ordinalizes a given number by adding a prefix such as 'st', 'nd', 'rd' or 'th' based on the last digit of the
+ * Ordinalizes a given number by adding a prefix such as 'st', 'nd', 'rd' or 'th' based on the last digit of the
* number. 21 -> 21st, 22 -> 22nd, 23 -> 23rd, 24 -> 24th etc
* @param {Number} number The number to ordinalize
* @return {String} The ordinalized number
@@ -52317,7 +56078,7 @@ Ext.define('Ext.util.Inflector', {
var parsed = parseInt(number, 10),
mod10 = parsed % 10,
mod100 = parsed % 100;
-
+
//11 through 13 are a special case
if (11 <= mod100 && mod100 <= 13) {
return number + "th";
@@ -52366,7 +56127,7 @@ Ext.define('Ext.util.Inflector', {
vita: 'vitae'
},
singular;
-
+
for (singular in irregulars) {
this.plural(singular, irregulars[singular]);
this.singular(irregulars[singular], singular);
@@ -52409,7 +56170,7 @@ Ext.define('User', {
*
//first, we load up a User with id of 1
-var user = Ext.ModelManager.create({id: 1, name: 'Ed'}, 'User');
+var user = Ext.create('User', {id: 1, name: 'Ed'});
//the user.products function was created automatically by the association and returns a {@link Ext.data.Store Store}
//the created store is automatically scoped to the set of Products for the User with id of 1
@@ -52465,7 +56226,7 @@ var store = new Search({query: 'Sencha Touch'}).tweets();
* equivalent to this:
*
-var store = new Ext.data.Store({
+var store = Ext.create('Ext.data.Store', {
model: 'Tweet',
filters: [
{
@@ -52661,22 +56422,21 @@ associations: [{
* @class Ext.data.JsonP
* @singleton
* This class is used to create JSONP requests. JSONP is a mechanism that allows for making
- * requests for data cross domain. More information is available here:
- * http://en.wikipedia.org/wiki/JSONP
+ * requests for data cross domain. More information is available here.
*/
Ext.define('Ext.data.JsonP', {
-
+
/* Begin Definitions */
-
+
singleton: true,
-
+
statics: {
requestCount: 0,
requests: {}
},
-
+
/* End Definitions */
-
+
/**
* @property timeout
* @type Number
@@ -52684,21 +56444,21 @@ Ext.define('Ext.data.JsonP', {
* failure callback will be fired. The timeout is in ms. Defaults to 30000.
*/
timeout: 30000,
-
+
/**
* @property disableCaching
* @type Boolean
* True to add a unique cache-buster param to requests. Defaults to true.
*/
disableCaching: true,
-
+
/**
- * @property disableCachingParam
+ * @property disableCachingParam
* @type String
* Change the parameter which is sent went disabling caching through a cache buster. Defaults to '_dc'.
*/
disableCachingParam: '_dc',
-
+
/**
* @property callbackKey
* @type String
@@ -52707,7 +56467,7 @@ Ext.define('Ext.data.JsonP', {
* url?callback=Ext.data.JsonP.callback1
*/
callbackKey: 'callback',
-
+
/**
* Makes a JSONP request.
* @param {Object} options An object which may contain the following properties. Note that options will
@@ -52718,11 +56478,16 @@ Ext.define('Ext.data.JsonP', {
* key value pairs that will be sent along with the request.