3b53744d9f39117e471f39601d4e89dfd4f696ba
[odoo/odoo.git] / addons / web_graph / static / src / js / graph.js
1 /*---------------------------------------------------------
2  * OpenERP web_graph
3  *---------------------------------------------------------*/
4
5 /* jshint undef: false  */
6
7
8 openerp.web_graph = function (instance) {
9 'use strict';
10
11 var _lt = instance.web._lt;
12 var _t = instance.web._t;
13 var QWeb = instance.web.qweb;
14
15 instance.web.views.add('graph', 'instance.web_graph.GraphView');
16
17  /**
18   * GraphView view.  It mostly contains two widgets (PivotTable and ChartView)
19   * and some data.
20   */
21 instance.web_graph.GraphView = instance.web.View.extend({
22     template: 'GraphView',
23     display_name: _lt('Graph'),
24     view_type: 'graph',
25     mode: 'pivot',   // pivot, bar_chart, line_chart or pie_chart
26
27     events: {
28         'click .graph_mode_selection li' : function (event) {
29             event.preventDefault();
30             this.mode = event.target.attributes['data-mode'].nodeValue;
31             this.display_data();
32         },
33         'click .graph_clear_groups' : function (event) {
34             this.pivot_table.clear_groups();
35         },
36     },
37
38     view_loading: function (fields_view_get) {
39         var self = this;
40         var model = new instance.web.Model(fields_view_get.model, {group_by_no_leaf: true});
41         var options = {};
42         options.domain = [];
43         options.col_groupby = [];
44
45         // get the default groupbys and measure defined in the field view
46         options.measure = null;
47         options.row_groupby = [];
48         _.each(fields_view_get.arch.children, function (field) {
49             if ('name' in field.attrs) {
50                 if ('operator' in field.attrs) {
51                     options.measure = field.attrs.name;
52                 } else {
53                     options.row_groupby.push(field.attrs.name);
54                 }
55             }
56         });
57
58         // get the most important fields (of the model) by looking at the
59         // groupby filters defined in the search view
60         options.important_fields = [];
61         var load_view = instance.web.fields_view_get({
62             model: model,
63             view_type: 'search',
64         });
65
66         var important_fields_def = $.when(load_view).then(function (search_view) {
67             var groups = _.select(search_view.arch.children, function (c) {
68                 return (c.tag == 'group') && (c.attrs.string != 'Display');
69             });
70
71             _.each(groups, function(g) {
72                 _.each(g.children, function (g) {
73                     if (g.attrs.context) {
74                         var field_id = py.eval(g.attrs.context).group_by;
75                         options.important_fields.push(field_id);
76                     }
77                 });
78             });
79         });
80
81         // get the fields descriptions from the model
82         var field_descr_def = model.call('fields_get', [])
83             .then(function (fields) { options.fields = fields; });
84
85
86         return $.when(important_fields_def, field_descr_def)
87             .then(function () {
88
89                 self.data = {
90                     model: model,
91                     domain: options.domain,
92                     fields: options.fields,
93                     important_fields: options.important_fields,
94                     measure: options.measure,
95                     measure_label: options.fields[options.measure].string,
96                     col_groupby: [],
97                     row_groupby: options.row_groupby,
98                     groups: [],
99                     total: null,
100                 };
101                 self.pivot_table = new PivotTable(model, options);
102             })
103             .then(function () {
104                 return self.pivot_table.appendTo('.graph_main_content');
105             });
106
107     },
108
109     display_data : function () {
110         var content = this.$el.filter('.graph_main_content');
111         content.find('svg').remove();
112         var self = this;
113         if (this.mode === 'pivot') {
114             this.pivot_table.show();
115
116         } else {
117             this.pivot_table.hide();
118             content.append('<svg></svg>');
119             var view_fields = this.data.row_groupby.concat(this.data.measure, this.data.col_groupby);
120             query_groups(this.data.model, view_fields, this.data.domain, this.data.row_groupby).then(function (groups) {
121                 Charts[self.mode](groups, self.data.measure, self.data.measure_label);
122             });
123
124         }
125     },
126
127     do_search: function (domain, context, group_by) {
128         this.data.domain = new instance.web.CompoundDomain(domain);
129         this.pivot_table.set_domain(domain);
130         this.display_data();
131     },
132
133     do_show: function () {
134         this.do_push_state({});
135         return this._super();
136     },
137
138 });
139
140
141  /**
142   * PivotTable widget.  It displays the data in tabular data and allows the
143   * user to drill down and up in the table
144   */
145 var PivotTable = instance.web.Widget.extend({
146     template: 'pivot_table',
147     rows: [],
148     cols: [],
149     current_row_id : 0,
150     need_redraw: false,
151
152     // Input parameters: 
153     //      model: model to display
154     //      fields: dictionary returned by field_get on model (desc of model)
155     //      domain: constraints on model records 
156     //      row_groupby: groubys on rows (so, row headers in the pivot table)
157     //      col_groupby: idem, but on col
158     //      measure: quantity to display. either a field from the model, or 
159     //            null, in which case we use the "count" measure
160     init: function (model, options) {
161         var self = this;
162         this.model = model;
163         this.fields = options.fields;
164         this.domain = options.domain;
165         this.groupby = {
166             row: options.row_groupby,
167             col: options.col_groupby,
168         };
169
170         this.measure = options.measure;
171         this.measure_label = options.measure ? options.fields[options.measure].string : 'Quantity';
172         this.data = [];
173         this.need_redraw = true;
174         this.important_fields = options.important_fields;
175     },
176
177     get_descr: function (field_id) {
178         return this.fields[field_id].string;
179     },
180
181     set_domain: function (domain) {
182         this.domain = domain;
183         this.need_redraw = true;
184     },
185
186     set_row_groupby: function (row_groupby) {
187         this.groupby.row = row_groupby;
188         this.need_redraw = true;
189     },
190
191     set_col_groupby: function (col_groupby) {
192         this.groupby.col = col_groupby;
193         this.need_redraw = true;
194     },
195
196     set_measure: function (measure) {
197         this.measure = measure;
198         this.need_redraw = true;
199     },
200
201     show: function () {
202         if (this.need_redraw) {
203             this.draw();
204             this.need_redraw = false;
205         }
206         this.$el.css('display', 'block');
207     },
208
209     hide: function () {
210         this.$el.css('display', 'none');
211     },
212
213
214     get_data: function (groupby) {
215         var view_fields = this.groupby.row.concat(this.measure, this.groupby.col);
216         return query_groups(this.model, view_fields, this.domain, groupby);
217     },
218
219
220     events: {
221         'click .web_graph_click' : function (event) {
222             var self = this;
223             event.preventDefault();
224             var row_id = event.target.attributes['data-row-id'].nodeValue;
225
226             var row = this.get_row(row_id);
227             if (row.expanded) {
228                 this.fold_row(row_id);
229             } else {
230                 if (row.path.length < this.groupby.row.length) {
231                     var field_to_expand = this.groupby.row[row.path.length];
232                     this.expand_row(row_id, field_to_expand);
233                 } else {
234                     var already_grouped = self.groupby.row.concat(self.groupby.col);
235                     var possible_groups = _.difference(self.important_fields, already_grouped);
236                     var dropdown_options = {
237                         fields: _.map(possible_groups, function (field) {
238                             return {id: field, value: self.get_descr(field)};
239                         }),
240                         row_id: row_id,
241                     };
242                     this.dropdown = $(QWeb.render('field_selection', dropdown_options));
243                     $(event.target).after(this.dropdown);
244                     this.dropdown.css({position:"absolute",
245                                        left:event.pageX, 
246                                        top:event.pageY});
247                     $('.field-selection').next('.dropdown-menu').toggle();
248                 }
249             }
250
251     
252         },
253         'click a.field-selection' : function (event) {
254             event.preventDefault();
255             this.dropdown.remove();
256             var row_id = event.target.attributes['data-row-id'].nodeValue;
257             var field_id = event.target.attributes['data-field-id'].nodeValue;
258             this.expand_row(row_id, field_id);
259         },
260     },
261
262     clear_groups: function () {
263         this.groupby.row = [];
264         this.groupby.col = [];
265         this.rows = [];
266         this.cols = [];
267         this.draw();
268     },
269
270     generate_id: function () {
271         this.current_row_id += 1;
272         return this.current_row_id - 1;
273     },
274
275     get_row: function (id) {
276         return _.find(this.rows, function(row) {
277             return (row.id == id);
278         });
279     },
280
281     make_cell: function (content, options) {
282         var attrs = ['<td'];
283         if (options && options.is_border) {
284             attrs.push('class="graph_border"');
285         }
286
287         attrs.push('>');
288         if (options && options.indent) {
289             _.each(_.range(options.indent), function () {
290                 attrs.push('<span class="web_graph_indent"></span>');
291             });
292         }
293         if (options && options.foldable) {
294             attrs.push('<span data-row-id="'+ options.row_id + '" href="#" class="icon-plus-sign web_graph_click">');
295         }
296         if (content) {
297             attrs.push(content);
298         } else {
299             attrs.push('Undefined');
300         }
301         if (options && options.foldable) {
302             attrs.push('</span>');
303         }
304         attrs.push('</td>');
305         return attrs.join(' ');
306     },
307
308     make_row: function (data, parent_id) {
309         var has_parent = (parent_id !== undefined);
310         var parent = has_parent ? this.get_row(parent_id) : null;
311         var path;
312         if (has_parent) {
313             path = parent.path.concat(data.attributes.grouped_on);
314         } else if (data.attributes.grouped_on !== undefined) {
315             path = [data.attributes.grouped_on];
316         } else {
317             path = [];
318         }
319
320         var indent_level = has_parent ? parent.path.length : 0;
321         var value = (this.groupby.row.length > 0) ? data.attributes.value[1] : 'Total';
322
323
324         var jquery_row = $('<tr></tr>');
325         var row_id = this.generate_id();
326
327         var header = $(this.make_cell(value, {is_border:true, indent: indent_level, foldable:true, row_id: row_id}));
328         jquery_row.html(header);
329         jquery_row.append(this.make_cell(data.attributes.aggregates[this.measure]));
330
331         var row = {
332             id: row_id,
333             path: path,
334             value: value,
335             expanded: false,
336             parent: parent_id,
337             children: [],
338             html_tr: jquery_row,
339             domain: data.model._domain,
340         };
341         // rows.splice(index of parent if any,0,row);
342         this.rows.push(row);  // to do, insert it properly
343
344         if (this.groupby.row.length === 0) {
345             row.remove_when_expanded = true;
346             row.domain = this.domain;
347         }
348         if (has_parent) {
349             parent.children.push(row.id);
350         }
351         return row;
352     },
353
354     expand_row: function (row_id, field_id) {
355         var self = this;
356         var row = this.get_row(row_id);
357
358         if (row.path.length == this.groupby.row.length) {
359             this.groupby.row.push(field_id);
360         }
361
362         var visible_fields = this.groupby.row.concat(this.groupby.col, this.measure);
363
364         if (row.remove_when_expanded) {
365             this.rows = [];
366         } else {
367             row.expanded = true;
368             row.html_tr.find('.icon-plus-sign')
369                 .removeClass('icon-plus-sign')
370                 .addClass('icon-minus-sign');
371         }
372
373         query_groups(this.model, visible_fields, row.domain, [field_id])
374             .then(function (data) {
375                 _.each(data.reverse(), function (datapt) {
376                     var new_row;
377                     if (row.remove_when_expanded) {
378                         new_row = self.make_row(datapt);
379                         self.$('tr.graph_table_header').after(new_row.html_tr);
380                     } else {
381                         new_row = self.make_row(datapt, row_id);
382                         row.html_tr.after(new_row.html_tr);
383                     }
384                 });
385                 if (row.remove_when_expanded) {
386                     row.html_tr.remove();
387                 }
388         });
389
390     },
391
392     fold_row: function (row_id) {
393         var self = this;
394         var row = this.get_row(row_id);
395
396         _.each(row.children, function (child_row) {
397             self.remove_row(child_row);
398         });
399         row.children = [];
400
401         row.expanded = false;
402         row.html_tr.find('.icon-minus-sign')
403             .removeClass('icon-minus-sign')
404             .addClass('icon-plus-sign');
405
406         var fold_levels = _.map(self.rows, function(g) {return g.path.length;});
407         var new_groupby_length = _.reduce(fold_levels, function (x, y) {
408             return Math.max(x,y);
409         }, 0);
410
411         this.groupby.row.splice(new_groupby_length);
412     },
413
414     remove_row: function (row_id) {
415         var self = this;
416         var row = this.get_row(row_id);
417
418         _.each(row.children, function (child_row) {
419             self.remove_row(child_row);
420         });
421
422         row.html_tr.remove();
423         removeFromArray(this.rows, row);
424     },
425
426     draw: function () {
427         this.get_data(this.groupby.row)
428             .then(this.proxy('build_table'))
429             .done(this.proxy('_draw'));
430     },
431
432     build_table: function (data) {
433         var self = this;
434         this.rows = [];
435
436         this.cols = [{
437             path: [],
438             value: this.measure_label,
439             expanded: false,
440             parent: null,
441             children: [],
442             html_tds: [],
443             domain: this.domain,
444             header: $(this.make_cell(this.measure_label, {is_border:true})),
445         }];
446
447         _.each(data, function (datapt) {
448             self.make_row(datapt);
449         });
450     },
451
452     _draw: function () {
453
454         this.$el.empty();
455         var self = this;
456         var header;
457
458         if (this.groupby.row.length > 0) {
459             header = '<tr><td class="graph_border">' +
460                     this.fields[this.groupby.row[0]].string +
461                     '</td><td class="graph_border">' +
462                     this.measure_label +
463                     '</td></tr>';
464         } else {
465             header = '<tr class="graph_table_header"><td class="graph_border">' +
466                     '</td><td class="graph_border">' +
467                     this.measure_label +
468                     '</td></tr>';
469         }
470         this.$el.append(header);
471
472         _.each(this.rows, function (row) {
473             self.$el.append(row.html_tr);
474         });
475
476     }
477 });
478
479
480 };
481