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