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