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