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