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