8cf0ffd487746c78587151868266695a837d4bd6
[odoo/odoo.git] / addons / web_graph / static / src / js / graph.js
1 /*---------------------------------------------------------
2  * OpenERP web_graph
3  *---------------------------------------------------------*/
4
5 /* jshint undef: false  */
6
7 openerp.web_graph = function (instance) {
8 'use strict';
9
10 var _lt = instance.web._lt;
11 var _t = instance.web._t;
12 var QWeb = instance.web.qweb;
13
14 instance.web.views.add('graph', 'instance.web_graph.GraphView');
15
16  /**
17   * GraphView 
18   */
19 instance.web_graph.GraphView = instance.web.View.extend({
20     template: 'GraphView',
21     display_name: _lt('Graph'),
22     view_type: 'graph',
23
24     events: {
25         'click .graph_mode_selection li' : 'mode_selection',
26         'click .graph_measure_selection li' : 'measure_selection',
27         'click .graph_expand_selection li' : 'expand_selection',
28         'click .graph_options_selection li' : 'option_selection',
29         'click .web_graph_click' : 'cell_click_callback',
30         'click a.field-selection' : 'field_selection',
31     },
32
33     init: function(parent, dataset, view_id, options) {
34         this._super(parent);
35         this.model = new instance.web.Model(dataset.model, {group_by_no_leaf: true});
36         this.dataset = dataset;
37         this.pivot_table = new openerp.web_graph.PivotTable(this.model, dataset.domain);
38         this.set_default_options(options);
39         this.dropdown = null;
40         this.mode = 'pivot'; // pivot, bar_chart, line_chart, pie_chart, heatmap, row_heatmap, col_heatmap
41         this.measure_list = [];
42         this.important_fields = [];
43         this.search_view = parent.searchview;
44         this.groupby_mode = 'default';  // 'default' or 'manual'
45         this.default_row_groupby = [];
46         this.search_field = {
47             get_context: function () { },
48             get_domain: function () { },
49             get_groupby: function () { },
50         };
51     },
52
53     start: function () {
54         this.table = $('<table></table>');
55         this.$('.graph_main_content').append(this.table);
56         instance.web.bus.on('click', this, function (ev) {
57             if (this.dropdown) {
58                 this.dropdown.remove();
59                 this.dropdown = null;
60             }
61         });
62         return this.load_view();
63     },
64
65     view_loading: function (fields_view_get) {
66         var self = this,
67             measure = null;
68
69         if (fields_view_get.arch.attrs.type === 'bar') {
70             this.mode = 'bar_chart';
71         }
72
73         // get the default groupbys and measure defined in the field view
74         _.each(fields_view_get.arch.children, function (field) {
75             if ('name' in field.attrs) {
76                 if ('operator' in field.attrs) {
77                     self.measure_list.push(field.attrs.name);
78                 } else {
79                     self.default_row_groupby.push(field.attrs.name);
80                 }
81             }
82         });
83         if (this.measure_list.length > 0) {
84             measure = this.measure_list[0];
85             this.pivot_table.set_measure(measure);
86         }
87
88         // get the most important fields (of the model) by looking at the
89         // groupby filters defined in the search view
90         var options = {model:this.model, view_type: 'search'},
91             deferred1 = instance.web.fields_view_get(options).then(function (search_view) {
92                 var groups = _.select(search_view.arch.children, function (c) {
93                     return (c.tag == 'group') && (c.attrs.string != 'Display');
94                 });
95                 _.each(groups, function(g) {
96                     _.each(g.children, function (g) {
97                         if (g.attrs.context) {
98                             var field_id = py.eval(g.attrs.context).group_by;
99                             self.important_fields.push(field_id);
100                         }
101                     });
102                 });
103             });
104
105         // get the fields descriptions from the model
106         var deferred2 = this.model.call('fields_get', []).then(function (fs) {
107             self.fields = fs;
108             var measure_selection = self.$('.graph_measure_selection');
109             _.each(self.measure_list, function (measure) {
110                 var choice = $('<a></a>').attr('data-choice', measure)
111                                          .attr('href', '#')
112                                          .append(self.fields[measure].string);
113                 measure_selection.append($('<li></li>').append(choice));
114
115             });
116         });
117
118         return $.when(deferred1, deferred2);
119     },
120
121     do_search: function (domain, context, group_by) {
122         var self = this,
123             col_groupby = get_col_groupby('ColGroupBy');
124
125         if (group_by.length || col_groupby.length) {
126             this.groupby_mode = 'manual';
127         }
128
129         this.pivot_table.set_domain(domain);
130         if (this.groupby_mode === 'manual') {
131             this.pivot_table.set_row_groupby(group_by);
132             this.pivot_table.set_col_groupby(col_groupby);
133         } else {
134             this.pivot_table.set_row_groupby(_.toArray(this.default_row_groupby));
135             this.pivot_table.set_col_groupby([]);
136         }
137         this.display_data();
138
139         function get_col_groupby() {
140             var groupby = [],
141                 search = _.find(self.search_view.query.models, function (model) {
142                 return model.attributes.category == 'ColGroupBy';
143             });
144             if (search) {
145                 groupby = _.map(search.values.models, function (val) {
146                     return val.attributes.value.attrs.context.col_group_by;
147                 });
148             }
149             return groupby;
150         }
151     },
152
153     do_show: function () {
154         this.do_push_state({});
155         return this._super();
156     },
157
158     display_data: function () {
159         var pivot = this.pivot_table;
160         if (pivot.stale_data) {
161             pivot.update_data().done(this.proxy('display_data'));
162         } else {
163             this.$('.graph_main_content svg').remove();
164             this.table.empty();
165
166             if (pivot.no_data) {
167                 var msg = 'No data available. Try to remove any filter or add some data.';
168                 this.table.append($('<tr><td>' + msg + '</td></tr>'));
169             } else {
170                 var table_modes = ['pivot', 'heatmap', 'row_heatmap', 'col_heatmap'];
171                 if (_.contains(table_modes, this.mode)) {
172                     this.draw_table();
173                 } else {
174                     this.$('.graph_main_content').append($('<div><svg></svg></div>'));
175                     var svg = this.$('.graph_main_content svg')[0];
176                     openerp.web_graph.draw_chart(this.mode, this.pivot_table, svg);
177                 }
178             }
179         }
180     },
181
182
183 /******************************************************************************
184  * Event handling methods...
185  ******************************************************************************/
186     handle_header_event: function (options) {
187         var pivot = this.pivot_table,
188             id = options.id,
189             header = pivot.get_header(id),
190             dim = header.root.groupby.length;
191
192         if (header.is_expanded) {
193             pivot.fold(header);
194             this.register_groupby();
195         } else {
196             if (header.path.length < header.root.groupby.length) {
197                 var field = header.root.groupby[header.path.length];
198                 pivot.expand(id, field).then(this.proxy('register_groupby'));
199             } else {
200                 this.display_dropdown({id:header.id,
201                                        target: $(options.event.target),
202                                        x: options.event.pageX,
203                                        y: options.event.pageY});
204             }
205         }
206     },
207
208     mode_selection: function (event) {
209         event.preventDefault();
210         var mode = event.target.attributes['data-mode'].nodeValue;
211         this.mode = mode;
212         this.display_data();
213     },
214
215     register_groupby: function() {
216         var self = this,
217             query = this.search_view.query;
218         this.groupby_mode = 'manual';
219
220         var rows = _.map(this.pivot_table.rows.groupby, function (group) {
221             return make_facet('GroupBy', group);
222         });
223         var cols = _.map(this.pivot_table.cols.groupby, function (group) {
224             return make_facet('ColGroupBy', group);
225         });
226
227         query.reset(rows.concat(cols));
228
229         function make_facet (category, fields) {
230             var values,
231                 icon,
232                 backbone_field,
233                 cat_name;
234             if (!(fields instanceof Array)) { fields = [fields]; }
235             if (category === 'GroupBy') {
236                 cat_name = 'group_by';
237                 icon = 'w';
238                 backbone_field = self.search_view._s_groupby;
239             } else {
240                 cat_name = 'col_group_by';
241                 icon = 'f';
242                 backbone_field = self.search_field;
243             }
244             values =  _.map(fields, function (field) {
245                 var context = {};
246                 context[cat_name] = field;
247                 return {label: self.fields[field].string, value: {attrs:{domain: [], context: context}}};
248             });
249             return {category:category, values: values, icon:icon, field: backbone_field};
250         }
251     },
252
253     measure_selection: function (event) {
254         event.preventDefault();
255         var measure = event.target.attributes['data-choice'].nodeValue;
256         this.pivot_table.set_measure((measure === '__count') ? null : measure);
257         this.display_data();
258     },
259
260     expand_selection: function (event) {
261         event.preventDefault();
262         switch (event.target.attributes['data-choice'].nodeValue) {
263             case 'fold_columns':
264                 this.pivot_table.fold_cols();
265                 this.register_groupby();
266                 break;
267             case 'fold_rows':
268                 this.pivot_table.fold_rows();
269                 this.register_groupby();
270                 break;
271             case 'fold_all':
272                 this.pivot_table.fold_cols();
273                 this.pivot_table.fold_rows();
274                 this.register_groupby();
275                 break;
276             case 'expand_all':
277                 this.pivot_table.invalidate_data();
278                 this.display_data();
279                 break;
280         }
281     },
282
283     option_selection: function (event) {
284         event.preventDefault();
285         switch (event.target.attributes['data-choice'].nodeValue) {
286             case 'swap_axis':
287                 this.pivot_table.swap_axis();
288                 this.register_groupby();
289                 break;
290             case 'update_values':
291                 this.pivot_table.stale_data = true;
292                 this.display_data();
293                 break;
294             case 'export_data':
295                 // Export code...  To do...
296                 break;
297         }
298     },
299
300
301     cell_click_callback: function (event) {
302         event.preventDefault();
303         event.stopPropagation();
304         var id = event.target.attributes['data-id'].nodeValue;
305         this.handle_header_event({id:id, event:event});
306     },
307
308     field_selection: function (event) {
309         var self = this,
310             id = event.target.attributes['data-id'].nodeValue,
311             field_id = event.target.attributes['data-field-id'].nodeValue;
312         event.preventDefault();
313         this.pivot_table.expand(id, field_id).then(function () {
314             self.register_groupby();
315         });
316     },
317
318     display_dropdown: function (options) {
319         var self = this,
320             pivot = this.pivot_table,
321             already_grouped = pivot.rows.groupby.concat(pivot.cols.groupby),
322             possible_groups = _.difference(self.important_fields, already_grouped),
323             dropdown_options = {
324                 header_id: options.id,
325                 fields: _.map(possible_groups, function (field) {
326                     return {id: field, value: self.fields[field].string};
327             })};
328
329         this.dropdown = $(QWeb.render('field_selection', dropdown_options));
330         options.target.after(this.dropdown);
331         this.dropdown.css({position:'absolute',
332                            left:options.x,
333                            top:options.y});
334         this.$('.field-selection').next('.dropdown-menu').toggle();
335     },
336
337 /******************************************************************************
338  * Drawing pivot table methods...
339  ******************************************************************************/
340     draw_table: function () {
341         this.pivot_table.rows.main.title = 'Total';
342         this.pivot_table.cols.main.title = this.measure_label();
343         this.draw_top_headers();
344         _.each(this.pivot_table.rows.headers, this.proxy('draw_row'));
345     },
346
347     measure_label: function () {
348         var pivot = this.pivot_table;
349         return (pivot.measure) ? this.fields[pivot.measure].string : 'Quantity';
350     },
351
352     make_border_cell: function (colspan, rowspan) {
353         return $('<td></td>').addClass('graph_border')
354                              .attr('colspan', colspan)
355                              .attr('rowspan', rowspan);
356     },
357
358     make_header_title: function (header) {
359         return $('<span> </span>')
360             .addClass('web_graph_click')
361             .attr('href', '#')
362             .addClass((header.is_expanded) ? 'icon-minus-sign' : 'icon-plus-sign')
363             .append((header.title !== undefined) ? header.title : 'Undefined');
364     },
365
366     draw_top_headers: function () {
367         var self = this,
368             pivot = this.pivot_table,
369             height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})),
370             header_cells = [[this.make_border_cell(1, height)]];
371
372         function set_dim (cols) {
373             _.each(cols.children, set_dim);
374             if (cols.children.length === 0) {
375                 cols.height = height - cols.path.length + 1;
376                 cols.width = 1;
377             } else {
378                 cols.height = 1;
379                 cols.width = _.reduce(cols.children, function (sum,c) { return sum + c.width;}, 0);
380             }
381         }
382
383         function make_col_header (col) {
384             var cell = self.make_border_cell(col.width, col.height);
385             return cell.append(self.make_header_title(col).attr('data-id', col.id));
386         }
387
388         function make_cells (queue, level) {
389             var col = queue[0];
390             queue = _.rest(queue).concat(col.children);
391             if (col.path.length == level) {
392                 _.last(header_cells).push(make_col_header(col));
393             } else {
394                 level +=1;
395                 header_cells.push([make_col_header(col)]);
396             }
397             if (queue.length !== 0) {
398                 make_cells(queue, level);
399             }
400         }
401
402         set_dim(pivot.cols.main);  // add width and height info to columns headers
403         if (pivot.cols.main.children.length === 0) {
404             make_cells(pivot.cols.headers, 0);
405         } else {
406             make_cells(pivot.cols.main.children, 1);
407             header_cells[0].push(self.make_border_cell(1, height).append('Total').css('font-weight', 'bold'));
408         }
409
410         _.each(header_cells, function (cells) {
411             self.table.append($('<tr></tr>').append(cells));
412         });
413     },
414
415     draw_row: function (row) {
416         var self = this,
417             pivot = this.pivot_table,
418             html_row = $('<tr></tr>'),
419             row_header = this.make_border_cell(1,1)
420                 .append(this.make_header_title(row).attr('data-id', row.id))
421                 .addClass('graph_border');
422
423         for (var i in _.range(row.path.length)) {
424             row_header.prepend($('<span/>', {class:'web_graph_indent'}));
425         }
426
427         html_row.append(row_header);
428
429         _.each(pivot.cols.headers, function (col) {
430             if (col.children.length === 0) {
431                 var value = pivot.get_value(row.id, col.id),
432                     cell = make_cell(value, col);
433                 html_row.append(cell);
434             }
435         });
436
437         if (pivot.cols.main.children.length > 0) {
438             var cell = make_cell(pivot.get_total(row), pivot.cols.main)
439                             .css('font-weight', 'bold');
440             html_row.append(cell);
441         }
442
443         this.table.append(html_row);
444
445         function make_cell (value, col) {
446             var color,
447                 total,
448                 cell = $('<td></td>');
449             if ((self.mode === 'pivot') && (row.is_expanded) && (row.path.length <=2)) {
450                 color = row.path.length * 5 + 240;
451                 cell.css('background-color', $.Color(color, color, color));
452             }
453             if (value === undefined) {
454                 return cell;
455             }
456             cell.append(value);
457             if (self.mode === 'heatmap') {
458                 total = pivot.get_total();
459                 color = Math.floor(50 + 205*(total - value)/total);
460                 cell.css('background-color', $.Color(255, color, color));
461             }
462             if (self.mode === 'row_heatmap') {
463                 total = pivot.get_total(row);
464                 color = Math.floor(50 + 205*(total - value)/total);
465                 cell.css('background-color', $.Color(255, color, color));
466             }
467             if (self.mode === 'col_heatmap') {
468                 total = pivot.get_total(col);
469                 color = Math.floor(50 + 205*(total - value)/total);
470                 cell.css('background-color', $.Color(255, color, color));
471             }
472             return cell;
473         }
474     },
475 });
476
477 };