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