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