Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / chart / axis / Time.js
1 /**
2  * @class Ext.chart.axis.Time
3  * @extends Ext.chart.axis.Axis
4  *
5  * A type of axis whose units are measured in time values. Use this axis
6  * for listing dates that you will want to group or dynamically change.
7  * If you just want to display dates as categories then use the
8  * Category class for axis instead.
9  *
10  * For example:
11  *
12  *     axes: [{
13  *         type: 'Time',
14  *         position: 'bottom',
15  *         fields: 'date',
16  *         title: 'Day',
17  *         dateFormat: 'M d',
18  *         groupBy: 'year,month,day',
19  *         aggregateOp: 'sum',
20  *     
21  *         constrain: true,
22  *         fromDate: new Date('1/1/11'),
23  *         toDate: new Date('1/7/11')
24  *     }]
25  *
26  * In this example we're creating a time axis that has as title *Day*.
27  * The field the axis is bound to is `date`.
28  * The date format to use to display the text for the axis labels is `M d`
29  * which is a three letter month abbreviation followed by the day number.
30  * The time axis will show values for dates between `fromDate` and `toDate`.
31  * Since `constrain` is set to true all other values for other dates not between
32  * the fromDate and toDate will not be displayed.
33  * 
34  * @constructor
35  */
36 Ext.define('Ext.chart.axis.Time', {
37
38     /* Begin Definitions */
39
40     extend: 'Ext.chart.axis.Category',
41
42     alternateClassName: 'Ext.chart.TimeAxis',
43
44     alias: 'axis.time',
45
46     requires: ['Ext.data.Store', 'Ext.data.JsonStore'],
47
48     /* End Definitions */
49
50      /**
51       * The minimum value drawn by the axis. If not set explicitly, the axis
52       * minimum will be calculated automatically.
53       * @property calculateByLabelSize
54       * @type Boolean
55       */
56     calculateByLabelSize: true,
57     
58      /**
59      * Indicates the format the date will be rendered on. 
60      * For example: 'M d' will render the dates as 'Jan 30', etc.
61       *
62      * @property dateFormat
63      * @type {String|Boolean}
64       */
65     dateFormat: false,
66     
67      /**
68      * Indicates the time unit to use for each step. Can be 'day', 'month', 'year' or a comma-separated combination of all of them.
69      * Default's 'year,month,day'.
70      *
71      * @property timeUnit
72      * @type {String}
73      */
74     groupBy: 'year,month,day',
75     
76     /**
77      * Aggregation operation when grouping. Possible options are 'sum', 'avg', 'max', 'min'. Default's 'sum'.
78      * 
79      * @property aggregateOp
80      * @type {String}
81       */
82     aggregateOp: 'sum',
83     
84     /**
85      * The starting date for the time axis.
86      * @property fromDate
87      * @type Date
88      */
89     fromDate: false,
90     
91     /**
92      * The ending date for the time axis.
93      * @property toDate
94      * @type Date
95      */
96     toDate: false,
97     
98     /**
99      * 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.).
100      * Default's [Ext.Date.DAY, 1].
101      * 
102      * @property step 
103      * @type Array
104      */
105     step: [Ext.Date.DAY, 1],
106     
107     /**
108      * If true, the values of the chart will be rendered only if they belong between the fromDate and toDate. 
109      * If false, the time axis will adapt to the new values by adding/removing steps.
110      * Default's [Ext.Date.DAY, 1].
111      * 
112      * @property constrain 
113      * @type Boolean
114      */
115     constrain: false,
116     
117     // @private a wrapper for date methods.
118     dateMethods: {
119         'year': function(date) {
120             return date.getFullYear();
121         },
122         'month': function(date) {
123             return date.getMonth() + 1;
124         },
125         'day': function(date) {
126             return date.getDate();
127         },
128         'hour': function(date) {
129             return date.getHours();
130         },
131         'minute': function(date) {
132             return date.getMinutes();
133         },
134         'second': function(date) {
135             return date.getSeconds();
136         },
137         'millisecond': function(date) {
138             return date.getMilliseconds();
139         }
140     },
141     
142     // @private holds aggregate functions.
143     aggregateFn: (function() {
144         var etype = (function() {
145             var rgxp = /^\[object\s(.*)\]$/,
146                 toString = Object.prototype.toString;
147             return function(e) {
148                 return toString.call(e).match(rgxp)[1];
149             };
150         })();
151         return {
152             'sum': function(list) {
153                 var i = 0, l = list.length, acum = 0;
154                 if (!list.length || etype(list[0]) != 'Number') {
155                     return list[0];
156                 }
157                 for (; i < l; i++) {
158                     acum += list[i];
159                 }
160                 return acum;
161             },
162             'max': function(list) {
163                 if (!list.length || etype(list[0]) != 'Number') {
164                     return list[0];
165                 }
166                 return Math.max.apply(Math, list);
167             },
168             'min': function(list) {
169                 if (!list.length || etype(list[0]) != 'Number') {
170                     return list[0];
171                 }
172                 return Math.min.apply(Math, list);
173             },
174             'avg': function(list) {
175                 var i = 0, l = list.length, acum = 0;
176                 if (!list.length || etype(list[0]) != 'Number') {
177                     return list[0];
178                 }
179                 for (; i < l; i++) {
180                     acum += list[i];
181                 }
182                 return acum / l;
183             }
184         };
185     })(),
186     
187     // @private normalized the store to fill date gaps in the time interval.
188     constrainDates: function() {
189         var fromDate = Ext.Date.clone(this.fromDate),
190             toDate = Ext.Date.clone(this.toDate),
191             step = this.step,
192             field = this.fields,
193             store = this.chart.store,
194             record, recObj, fieldNames = [],
195             newStore = Ext.create('Ext.data.Store', {
196                 model: store.model
197             });
198         
199         var getRecordByDate = (function() {
200             var index = 0, l = store.getCount();
201             return function(date) {
202                 var rec, recDate;
203                 for (; index < l; index++) {
204                     rec = store.getAt(index);
205                     recDate = rec.get(field);
206                     if (+recDate > +date) {
207                         return false;
208                     } else if (+recDate == +date) {
209                         return rec;
210                     }
211                 }
212                 return false;
213             };
214         })();
215         
216         if (!this.constrain) {
217             this.chart.filteredStore = this.chart.store;
218             return;
219         }
220
221         while(+fromDate <= +toDate) {
222             record = getRecordByDate(fromDate);
223             recObj = {};
224             if (record) {
225                 newStore.add(record.data);
226             } else {
227                 newStore.model.prototype.fields.each(function(f) {
228                     recObj[f.name] = false;
229                 });
230                 recObj.date = fromDate;
231                 newStore.add(recObj);
232             }
233             fromDate = Ext.Date.add(fromDate, step[0], step[1]);
234         }
235          
236         this.chart.filteredStore = newStore;
237     },
238     
239     // @private aggregates values if multiple store elements belong to the same time step.
240     aggregate: function() {
241         var aggStore = {}, 
242             aggKeys = [], key, value,
243             op = this.aggregateOp,
244             field = this.fields, i,
245             fields = this.groupBy.split(','),
246             curField,
247             recFields = [],
248             recFieldsLen = 0,
249             obj,
250             dates = [],
251             json = [],
252             l = fields.length,
253             dateMethods = this.dateMethods,
254             aggregateFn = this.aggregateFn,
255             store = this.chart.filteredStore || this.chart.store;
256         
257         store.each(function(rec) {
258             //get all record field names in a simple array
259             if (!recFields.length) {
260                 rec.fields.each(function(f) {
261                     recFields.push(f.name);
262                 });
263                 recFieldsLen = recFields.length;
264             }
265             //get record date value
266             value = rec.get(field);
267             //generate key for grouping records
268             for (i = 0; i < l; i++) {
269                 if (i == 0) {
270                     key = String(dateMethods[fields[i]](value));
271                 } else {
272                     key += '||' + dateMethods[fields[i]](value);
273                 }
274             }
275             //get aggregation record from hash
276             if (key in aggStore) {
277                 obj = aggStore[key];
278             } else {
279                 obj = aggStore[key] = {};
280                 aggKeys.push(key);
281                 dates.push(value);
282             }
283             //append record values to an aggregation record
284             for (i = 0; i < recFieldsLen; i++) {
285                 curField = recFields[i];
286                 if (!obj[curField]) {
287                     obj[curField] = [];
288                 }
289                 if (rec.get(curField) !== undefined) {
290                     obj[curField].push(rec.get(curField));
291                 }
292             }
293         });
294         //perform aggregation operations on fields
295         for (key in aggStore) {
296             obj = aggStore[key];
297             for (i = 0; i < recFieldsLen; i++) {
298                 curField = recFields[i];
299                 obj[curField] = aggregateFn[op](obj[curField]);
300             }
301             json.push(obj);
302         }
303         this.chart.substore = Ext.create('Ext.data.JsonStore', {
304             fields: recFields,
305             data: json
306         });
307         
308         this.dates = dates;
309     },
310     
311     // @private creates a label array to be used as the axis labels.
312      setLabels: function() {
313         var store = this.chart.substore,
314             fields = this.fields,
315             format = this.dateFormat,
316             labels, i, dates = this.dates,
317             formatFn = Ext.Date.format;
318         this.labels = labels = [];
319         store.each(function(record, i) {
320             if (!format) {
321                 labels.push(record.get(fields));
322             } else {
323                 labels.push(formatFn(dates[i], format));
324             }
325          }, this);
326      },
327
328     processView: function() {
329          //TODO(nico): fix this eventually...
330          if (this.constrain) {
331              this.constrainDates();
332              this.aggregate();
333              this.chart.substore = this.chart.filteredStore;
334          } else {
335              this.aggregate();
336          }
337     },
338
339      // @private modifies the store and creates the labels for the axes.
340      applyData: function() {
341         this.setLabels();
342         var count = this.chart.substore.getCount();
343          return {
344              from: 0,
345              to: count,
346              steps: count - 1,
347              step: 1
348          };
349      }
350  });
351