918a698e7b7c2103447c2bed5ce14ec5068d9e62
[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 options = {
182                         svg: this.$('.graph_main_content svg')[0],
183                         mode: this.mode,
184                         pivot: this.pivot_table,
185                         width: this.$el.width(),
186                         height: Math.min(Math.max(document.documentElement.clientHeight - 116 - 60, 250), Math.round(0.8*this.$el.width())),
187                         measure_label: this.measure_label()
188                     };
189                     openerp.web_graph.draw_chart(options);
190                 }
191             }
192         }
193     },
194
195 /******************************************************************************
196  * Event handling methods...
197  ******************************************************************************/
198     handle_header_event: function (options) {
199         var pivot = this.pivot_table,
200             id = options.id,
201             header = pivot.get_header(id),
202             dim = header.root.groupby.length;
203
204         if (header.is_expanded) {
205             pivot.fold(header);
206             this.register_groupby();
207         } else {
208             if (header.path.length < header.root.groupby.length) {
209                 var field = header.root.groupby[header.path.length];
210                 pivot.expand(id, field).then(this.proxy('register_groupby'));
211             } else {
212                 this.display_dropdown({id:header.id,
213                                        target: $(options.event.target),
214                                        x: options.event.pageX,
215                                        y: options.event.pageY});
216             }
217         }
218     },
219
220     mode_selection: function (event) {
221         event.preventDefault();
222         var mode = event.target.attributes['data-mode'].nodeValue;
223         this.mode = mode;
224         this.display_data();
225     },
226
227     register_groupby: function() {
228         var self = this,
229             query = this.search_view.query;
230         this.groupby_mode = 'manual';
231
232         var rows = _.map(this.pivot_table.rows.groupby, function (group) {
233             return make_facet('GroupBy', group);
234         });
235         var cols = _.map(this.pivot_table.cols.groupby, function (group) {
236             return make_facet('ColGroupBy', group);
237         });
238
239         query.reset(rows.concat(cols));
240
241         function make_facet (category, fields) {
242             var values,
243                 icon,
244                 backbone_field,
245                 cat_name;
246             if (!(fields instanceof Array)) { fields = [fields]; }
247             if (category === 'GroupBy') {
248                 cat_name = 'group_by';
249                 icon = 'w';
250                 backbone_field = self.search_view._s_groupby;
251             } else {
252                 cat_name = 'col_group_by';
253                 icon = 'f';
254                 backbone_field = self.search_field;
255             }
256             values =  _.map(fields, function (field) {
257                 var context = {};
258                 context[cat_name] = field;
259                 return {label: self.fields[field].string, value: {attrs:{domain: [], context: context}}};
260             });
261             return {category:category, values: values, icon:icon, field: backbone_field};
262         }
263     },
264
265     measure_selection: function (event) {
266         event.preventDefault();
267         var measure = event.target.attributes['data-choice'].nodeValue;
268         this.pivot_table.set_measure((measure === '__count') ? null : measure);
269         this.display_data();
270     },
271
272     expand_selection: function (event) {
273         event.preventDefault();
274         switch (event.target.attributes['data-choice'].nodeValue) {
275             case 'fold_columns':
276                 this.pivot_table.fold_cols();
277                 this.register_groupby();
278                 break;
279             case 'fold_rows':
280                 this.pivot_table.fold_rows();
281                 this.register_groupby();
282                 break;
283             case 'fold_all':
284                 this.pivot_table.fold_cols();
285                 this.pivot_table.fold_rows();
286                 this.register_groupby();
287                 break;
288             case 'expand_all':
289                 this.pivot_table.invalidate_data();
290                 this.display_data();
291                 break;
292         }
293     },
294
295     option_selection: function (event) {
296         event.preventDefault();
297         switch (event.target.attributes['data-choice'].nodeValue) {
298             case 'swap_axis':
299                 this.pivot_table.swap_axis();
300                 this.register_groupby();
301                 break;
302             case 'update_values':
303                 this.pivot_table.stale_data = true;
304                 this.display_data();
305                 break;
306             case 'export_data':
307                 // Export code...  To do...
308                 break;
309         }
310     },
311
312
313     cell_click_callback: function (event) {
314         event.preventDefault();
315         event.stopPropagation();
316         var id = event.target.attributes['data-id'].nodeValue;
317         this.handle_header_event({id:id, event:event});
318     },
319
320     field_selection: function (event) {
321         var self = this,
322             id = event.target.attributes['data-id'].nodeValue,
323             field_id = event.target.attributes['data-field-id'].nodeValue;
324         event.preventDefault();
325         this.pivot_table.expand(id, field_id).then(function () {
326             self.register_groupby();
327         });
328     },
329
330     display_dropdown: function (options) {
331         var self = this,
332             pivot = this.pivot_table,
333             dropdown_options = {
334                 header_id: options.id,
335                 fields: _.map(self.important_fields, function (field) {
336                     return {id: field, value: self.fields[field].string};
337             })};
338         this.dropdown = $(QWeb.render('field_selection', dropdown_options));
339         options.target.after(this.dropdown);
340         this.dropdown.css({position:'absolute',
341                            left:options.x,
342                            top:options.y});
343         this.$('.field-selection').next('.dropdown-menu').toggle();
344     },
345
346 /******************************************************************************
347  * Drawing pivot table methods...
348  ******************************************************************************/
349     draw_table: function () {
350         this.pivot_table.rows.main.title = 'Total';
351         this.pivot_table.cols.main.title = this.measure_label();
352         this.draw_top_headers();
353         _.each(this.pivot_table.rows.headers, this.proxy('draw_row'));
354     },
355
356     measure_label: function () {
357         var pivot = this.pivot_table;
358         return (pivot.measure) ? this.fields[pivot.measure].string : 'Quantity';
359     },
360
361     make_border_cell: function (colspan, rowspan) {
362         return $('<td></td>').addClass('graph_border')
363                              .attr('colspan', colspan)
364                              .attr('rowspan', rowspan);
365     },
366
367     make_header_title: function (header) {
368         return $('<span> </span>')
369             .addClass('web_graph_click')
370             .attr('href', '#')
371             .addClass((header.is_expanded) ? 'icon-minus-sign' : 'icon-plus-sign')
372             .append((header.title !== undefined) ? header.title : 'Undefined');
373     },
374
375     draw_top_headers: function () {
376         var self = this,
377             pivot = this.pivot_table,
378             height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})),
379             header_cells = [[this.make_border_cell(1, height)]];
380
381         function set_dim (cols) {
382             _.each(cols.children, set_dim);
383             if (cols.children.length === 0) {
384                 cols.height = height - cols.path.length + 1;
385                 cols.width = 1;
386             } else {
387                 cols.height = 1;
388                 cols.width = _.reduce(cols.children, function (sum,c) { return sum + c.width;}, 0);
389             }
390         }
391
392         function make_col_header (col) {
393             var cell = self.make_border_cell(col.width, col.height);
394             return cell.append(self.make_header_title(col).attr('data-id', col.id));
395         }
396
397         function make_cells (queue, level) {
398             var col = queue[0];
399             queue = _.rest(queue).concat(col.children);
400             if (col.path.length == level) {
401                 _.last(header_cells).push(make_col_header(col));
402             } else {
403                 level +=1;
404                 header_cells.push([make_col_header(col)]);
405             }
406             if (queue.length !== 0) {
407                 make_cells(queue, level);
408             }
409         }
410
411         set_dim(pivot.cols.main);  // add width and height info to columns headers
412         if (pivot.cols.main.children.length === 0) {
413             make_cells(pivot.cols.headers, 0);
414         } else {
415             make_cells(pivot.cols.main.children, 1);
416             header_cells[0].push(self.make_border_cell(1, height).append('Total').css('font-weight', 'bold'));
417         }
418
419         _.each(header_cells, function (cells) {
420             self.table.append($('<tr></tr>').append(cells));
421         });
422     },
423
424     get_measure_type: function () {
425         var measure = this.pivot_table.measure;
426         return (measure) ? this.fields[measure].type : 'integer';
427     },
428
429     draw_row: function (row) {
430         var self = this,
431             pivot = this.pivot_table,
432             measure_type = this.get_measure_type(),
433             html_row = $('<tr></tr>'),
434             row_header = this.make_border_cell(1,1)
435                 .append(this.make_header_title(row).attr('data-id', row.id))
436                 .addClass('graph_border');
437
438         for (var i in _.range(row.path.length)) {
439             row_header.prepend($('<span/>', {class:'web_graph_indent'}));
440         }
441
442         html_row.append(row_header);
443
444         _.each(pivot.cols.headers, function (col) {
445             if (col.children.length === 0) {
446                 var value = pivot.get_value(row.id, col.id),
447                     cell = make_cell(value, col);
448                 html_row.append(cell);
449             }
450         });
451
452         if (pivot.cols.main.children.length > 0) {
453             var cell = make_cell(pivot.get_total(row), pivot.cols.main)
454                             .css('font-weight', 'bold');
455             html_row.append(cell);
456         }
457
458         this.table.append(html_row);
459
460         function make_cell (value, col) {
461             var color,
462                 total,
463                 cell = $('<td></td>');
464             if ((self.mode === 'pivot') && (row.is_expanded) && (row.path.length <=2)) {
465                 color = row.path.length * 5 + 240;
466                 cell.css('background-color', $.Color(color, color, color));
467             }
468             if (value === undefined) {
469                 return cell;
470             }
471             cell.append(instance.web.format_value(value, {type: measure_type}));
472             if (self.mode === 'heatmap') {
473                 total = pivot.get_total();
474                 color = Math.floor(50 + 205*(total - value)/total);
475                 cell.css('background-color', $.Color(255, color, color));
476             }
477             if (self.mode === 'row_heatmap') {
478                 total = pivot.get_total(row);
479                 color = Math.floor(50 + 205*(total - value)/total);
480                 cell.css('background-color', $.Color(255, color, color));
481             }
482             if (self.mode === 'col_heatmap') {
483                 total = pivot.get_total(col);
484                 color = Math.floor(50 + 205*(total - value)/total);
485                 cell.css('background-color', $.Color(255, color, color));
486             }
487             return cell;
488         }
489     },
490 });
491
492 };