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