    /**
     * @class Ext.chart.axis.Time
     * @extends Ext.chart.axis.Axis
     *
     * 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',
     *         groupBy: 'year,month,day',
     *         aggregateOp: 'sum',
     *
     *         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.
     *
     * @constructor
     */
    Ext.define('Ext.chart.axis.Time', {

        /* Begin Definitions */

        extend: 'Ext.chart.axis.Category',

        alternateClassName: 'Ext.chart.TimeAxis',

        alias: 'axis.time',

        requires: ['Ext.data.Store', 'Ext.data.JsonStore'],

        /* 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],

        /**
         * 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
         */
        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;
            }

            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);
                }
                fromDate = Ext.Date.add(fromDate, step[0], step[1]);
            }

            this.chart.filteredStore = newStore;
        },

        // @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);
                }
                //append record values to an aggregation record
                for (i = 0; i < recFieldsLen; i++) {
                    curField = recFields[i];
                    if (!obj[curField]) {
                        obj[curField] = [];
                    }
                    if (rec.get(curField) !== undefined) {
                        obj[curField].push(rec.get(curField));
                    }
                }
            });
            //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;
        },

        // @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));
                }
             }, this);
         },

        processView: function() {
             //TODO(nico): fix this eventually...
             if (this.constrain) {
                 this.constrainDates();
                 this.aggregate();
                 this.chart.substore = this.chart.filteredStore;
             } else {
                 this.aggregate();
             }
        },

         // @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
             };
         }
     });