[MERGE] forward port of branch 8.0 up to 591e329
[odoo/odoo.git] / addons / web_graph / static / src / js / graph_widget.js
1
2 /* jshint undef: false  */
3
4 (function () {
5 'use strict';
6 var QWeb = openerp.web.qweb;
7 var _lt = openerp.web._lt;
8 var _t = openerp.web._t;
9
10 nv.dev = false;  // sets nvd3 library in production mode
11
12 openerp.web_graph.Graph = openerp.web.Widget.extend({
13     template: 'GraphWidget',
14
15     // ----------------------------------------------------------------------
16     // Init stuff
17     // ----------------------------------------------------------------------
18     init: function(parent, model,  domain, options) {
19         this._super(parent);
20         this.model = model;
21         this.domain = domain;
22         this.mode = options.mode || 'pivot';  // pivot, bar, pie, line
23         this.visible_ui = options.visible_ui || true;
24         this.bar_ui = options.bar_ui || 'group';
25         this.graph_view = options.graph_view || null;
26         this.pivot_options = options;
27         this.title = options.title || 'Data';
28         this.$buttons = options.$buttons;
29     },
30
31     start: function() {
32         var self = this;
33         this.table = $('<table>');
34         this.$('.graph_main_content').append(this.table);
35
36         this.$buttons.find('.oe-pivot-mode').click(function () {
37             self.set_mode.bind(self)('pivot');
38         });
39         this.$measure_list = this.$buttons.find('.oe-measure-list');
40
41         this.$buttons.find('.oe-bar-mode').click(function () {
42             self.set_mode.bind(self)('bar');
43         });
44         this.$buttons.find('.oe-line-mode').click(function () {
45             self.set_mode.bind(self)('line');
46         });
47         this.$buttons.find('.oe-pie-mode').click(function () {
48             self.set_mode.bind(self)('pie');
49         });
50         this.$buttons.find('.fa-expand').click(this.swap_axis.bind(this));
51         this.$buttons.find('.fa-arrows-alt').click(function () {
52             self.pivot.expand_all().then(self.proxy('display_data'));
53         });
54         this.$buttons.find('.fa-download').click(this.export_xls.bind(this));
55
56         var indexes = {'pivot': 0, 'bar': 1, 'line': 2, 'chart': 3};
57         this.$('.graph_mode_selection label').eq(indexes[this.mode]).addClass('selected');
58
59         if (this.mode !== 'pivot') {
60             this.$('.graph_heatmap label').addClass('disabled');
61             this.$('.graph_main_content').addClass('graph_chart_mode');
62         } else {
63             this.$('.graph_main_content').addClass('graph_pivot_mode');
64         }
65         // get search view
66         var parent = this.getParent();
67         while (!(parent instanceof openerp.web.ViewManager)) {
68             parent = parent.getParent();
69         }
70         this.search_view = parent.searchview;
71
72         openerp.session.rpc('/web_graph/check_xlwt').then(function (result) {
73             self.$('.graph_options_selection label').last().toggle(result);
74         });
75
76         this.$buttons.find('button').tooltip();
77         
78         return this.model.call('fields_get', {
79                     context: this.graph_view.dataset.context
80                 }).then(function (f) {
81             self.fields = f;
82             self.fields.__count = {field:'__count', type: 'integer', string:_t('Count')};
83             self.groupby_fields = self.get_groupby_fields();
84             self.measure_list = self.get_measures();
85             self.add_measures_to_options();
86             self.pivot_options.row_groupby = self.create_field_values(self.pivot_options.row_groupby || []);
87             self.pivot_options.col_groupby = self.create_field_values(self.pivot_options.col_groupby || []);
88             self.pivot_options.measures = self.create_field_values(self.pivot_options.measures || [{field:'__count', type: 'integer', string:'Count'}]);
89             self.pivot = new openerp.web_graph.PivotTable(self.model, self.domain, self.fields, self.pivot_options);
90             self.pivot.update_data().then(function () {
91                 self.display_data();
92                 if (self.graph_view) {
93                     self.graph_view.register_groupby(self.pivot.rows.groupby, self.pivot.cols.groupby);
94                 }
95             });
96             openerp.web.bus.on('click', self, function (event) {
97                 if (self.dropdown) {
98                     self.$row_clicked = $(event.target).closest('tr');
99                     self.dropdown.remove();
100                     self.dropdown = null;
101                 }
102             });
103             self.put_measure_checkmarks();
104         });
105     },
106
107     get_groupby_fields: function () {
108         var search_fields = this.get_search_fields(),
109             search_field_names = _.pluck(search_fields, 'field'),
110             other_fields = [],
111             groupable_types = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime'];
112
113         _.each(this.fields, function (val, key) {
114             if (!_.contains(search_field_names, key) && 
115                 _.contains(groupable_types, val.type) && 
116                 val.store === true) {
117                 other_fields.push({
118                     field: key,
119                     string: val.string,
120                 });
121             }
122         });
123         return search_fields.concat(other_fields);
124     },
125
126     // this method gets the fields that appear in the search view, under the 
127     // 'Groupby' heading
128     get_search_fields: function () {
129         // this method is disabled for now.  This requires extensive changes because the
130         // search view works quite differently:  But the graph view is going to be split 
131         // soon in pivot view and graph view.  The pivot view will then properly handle 
132         // groupbys.  
133         return [];  
134         var self = this;
135
136         var groupbygroups = _(this.search_view.drawer.inputs).select(function (g) {
137             return g instanceof openerp.web.search.GroupbyGroup;
138         });
139
140         var filters = _.flatten(_.pluck(groupbygroups, 'filters'), true),
141             groupbys = _.flatten(_.map(filters, function (filter) {
142                 var groupby = py.eval(filter.attrs.context).group_by;
143                 if (!(groupby instanceof Array)) { groupby = [groupby]; }
144                 return _.map(groupby, function(g) {
145                     return {field: g, filter: filter};
146                 });
147             }));
148
149         return _.uniq(_.map(groupbys, function (groupby) {
150             var field = groupby.field,
151                 filter = groupby.filter,
152                 raw_field = field.split(':')[0],
153                 string = (field === raw_field) ? filter.attrs.string : self.fields[raw_field].string;
154             
155             filter = (field === raw_field) ? filter : undefined;
156
157             return { field: raw_field, string: string, filter: filter };
158         }), false, function (filter) {return filter.field;});
159     },
160
161     // Extracts the integer/float fields which are not 'id'
162     get_measures: function() {
163         return _.compact(_.map(this.fields, function (f, id) {
164             if (((f.type === 'integer') || (f.type === 'float')) && 
165                 (id !== 'id') &&
166                 (f.store !== false)) {
167                 return {field:id, type: f.type, string: f.string};
168             }
169         }));
170     },
171
172     add_measures_to_options: function() {
173         this.$measure_list.append(
174         _.map(this.measure_list, function (measure) {
175             return $('<li>').append($('<a>').attr('data-choice', measure.field)
176                                      .attr('href', '#')
177                                      .text(measure.string));
178         }));
179         this.$measure_list.find('li').click(this.measure_selection.bind(this));
180     },
181
182     // ----------------------------------------------------------------------
183     // Configuration methods
184     // ----------------------------------------------------------------------
185     set: function (domain, row_groupby, col_groupby, measures_groupby) {
186         if (!this.pivot) {
187             this.pivot_options.domain = domain;
188             this.pivot_options.row_groupby = row_groupby;
189             this.pivot_options.col_groupby = col_groupby;
190             this.pivot_options.measures_groupby = measures_groupby;
191             return;
192         }
193         var row_gbs = this.create_field_values(row_groupby),
194             col_gbs = this.create_field_values(col_groupby),
195             measures_gbs = this.create_field_values(measures_groupby),
196             dom_changed = !_.isEqual(this.pivot.domain, domain),
197             row_gb_changed = !_.isEqual(row_gbs, this.pivot.rows.groupby),
198             col_gb_changed = !_.isEqual(col_gbs, this.pivot.cols.groupby),
199             measures_gb_changed = !_.isEqual(measures_gbs, this.pivot.measures),
200             row_reduced = is_strict_beginning_of(row_gbs, this.pivot.rows.groupby),
201             col_reduced = is_strict_beginning_of(col_gbs, this.pivot.cols.groupby),
202             measures_reduced = is_strict_beginning_of(measures_gbs, this.pivot.measures);
203
204         if (!dom_changed && row_reduced && !col_gb_changed && !measures_gb_changed) {
205             this.pivot.fold_with_depth(this.pivot.rows, row_gbs.length);
206             this.display_data();
207             return;
208         }
209         if (!dom_changed && col_reduced && !row_gb_changed && !measures_gb_changed) {
210             this.pivot.fold_with_depth(this.pivot.cols, col_gbs.length);
211             this.display_data();
212             return;
213         }
214
215         if (!dom_changed && col_reduced && row_reduced && !measures_gb_changed) {
216             this.pivot.fold_with_depth(this.pivot.rows, row_gbs.length);
217             this.pivot.fold_with_depth(this.pivot.cols, col_gbs.length);
218             this.display_data();
219             return;
220         }
221
222         if (dom_changed || row_gb_changed || col_gb_changed || measures_gb_changed) {
223             this.pivot.set(domain, row_gbs, col_gbs, measures_gbs).then(this.proxy('display_data'));
224         }
225
226         if (measures_gb_changed) {
227             this.put_measure_checkmarks();
228         }
229     },
230
231     set_mode: function (mode) {
232         this.mode = mode;
233
234         if (mode === 'pivot') {
235             this.$('.graph_heatmap label').removeClass('disabled');
236             this.$('.graph_main_content').removeClass('graph_chart_mode').addClass('graph_pivot_mode');
237         } else {
238             this.$('.graph_heatmap label').addClass('disabled');
239             this.$('.graph_main_content').removeClass('graph_pivot_mode').addClass('graph_chart_mode');
240         }
241         this.display_data();
242     },
243
244     create_field_value: function (f) {
245         var field = (_.contains(f, ':')) ? f.split(':')[0] : f,
246             groupby_field = _.findWhere(this.groupby_fields, {field:field}),
247             string = groupby_field ? groupby_field.string : this.fields[field].string,
248             result =  {field: f, string: string, type: this.fields[field].type };
249
250         if (groupby_field) {
251             result.filter = groupby_field.filter;
252         }
253
254         return result;
255     },
256
257     create_field_values: function (field_ids) {
258         return _.map(field_ids, this.proxy('create_field_value'));
259     },
260
261
262     get_col_groupbys: function () {
263         return _.pluck(this.pivot.cols.groupby, 'field');
264     },
265
266     get_current_measures: function () {
267         return _.pluck(this.pivot.measures, 'field');
268     },
269
270     // ----------------------------------------------------------------------
271     // UI code
272     // ----------------------------------------------------------------------
273     events: {
274         'click .graph_mode_selection label' : 'mode_selection',
275         'click .graph_measure_selection li' : 'measure_selection',
276         'click .graph_options_selection label' : 'option_selection',
277         'click .graph_heatmap label' : 'heatmap_mode_selection',
278         'click .web_graph_click' : 'header_cell_clicked',
279         'click a.field-selection' : 'field_selection',
280     },
281
282     mode_selection: function (event) {
283         event.preventDefault();
284         var mode = event.currentTarget.getAttribute('data-mode');
285         this.set_mode(mode);
286     },
287
288     measure_selection: function (event) {
289         event.preventDefault();
290         event.stopPropagation();
291         var measure_field = event.target.getAttribute('data-choice');
292         var measure = {
293             field: measure_field,
294             type: this.fields[measure_field].type,
295             string: this.fields[measure_field].string
296         };
297
298         this.pivot.toggle_measure(measure).then(this.proxy('display_data'));
299         this.put_measure_checkmarks();
300     },
301
302     put_measure_checkmarks: function () {
303         var self = this,
304             measures_li = this.$measure_list.find('li');
305         measures_li.removeClass('selected');
306         _.each(this.measure_list, function (measure, index) {
307             if (_.findWhere(self.pivot.measures, measure)) {
308                 measures_li.eq(index).addClass('selected');
309             }
310         });
311
312     },
313
314     option_selection: function (event) {
315         event.preventDefault();
316         switch (event.currentTarget.getAttribute('data-choice')) {
317             case 'swap_axis':
318                 this.swap_axis();
319                 break;
320             case 'expand_all':
321                 this.pivot.expand_all().then(this.proxy('display_data'));
322                 break;
323             case 'update_values':
324                 this.pivot.update_data().then(this.proxy('display_data'));
325                 break;
326             case 'export_data':
327                 this.export_xls();
328                 break;
329         }
330     },
331
332     header_cell_clicked: function (event) {
333         event.preventDefault();
334         event.stopPropagation();
335         var id = event.target.getAttribute('data-id'),
336             header = this.pivot.get_header(id),
337             self = this;
338
339         if (header.expanded) {
340             if (header.root === this.pivot.rows) {
341                 this.fold_row(header, event);
342             } else {
343                 this.fold_col(header);
344             }
345             return;
346         }
347         if (header.path.length < header.root.groupby.length) {
348             this.$row_clicked = $(event.target).closest('tr');
349             this.expand(id);
350             return;
351         }
352         if (!this.groupby_fields.length) {
353             return;
354         }
355
356         var fields = _.map(this.groupby_fields, function (field) {
357                 return {id: field.field, value: field.string, type:self.fields[field.field.split(':')[0]].type};
358         });
359         if (this.dropdown) {
360             this.dropdown.remove();
361         }
362         this.dropdown = $(QWeb.render('field_selection', {fields:fields, header_id:id}));
363         $(event.target).after(this.dropdown);
364         this.dropdown.css({
365             position:'absolute',
366             left:event.originalEvent.layerX,
367         });
368         this.$('.field-selection').next('.dropdown-menu').first().toggle();        
369     },
370
371     field_selection: function (event) {
372         var id = event.target.getAttribute('data-id'),
373             field_id = event.target.getAttribute('data-field-id'),
374             interval,
375             groupby = this.create_field_value(field_id);
376         event.preventDefault();
377         if (this.fields[field_id].type === 'date' || this.fields[field_id].type === 'datetime') {
378             interval = event.target.getAttribute('data-interval');
379             groupby.field =  groupby.field + ':' + interval;
380         }
381         this.expand(id, groupby);
382     },
383
384     // ----------------------------------------------------------------------
385     // Pivot Table integration
386     // ----------------------------------------------------------------------
387     expand: function (header_id, groupby) {
388         var self = this,
389             header = this.pivot.get_header(header_id),
390             update_groupby = !!groupby;
391         
392         groupby = groupby || header.root.groupby[header.path.length];
393
394         this.pivot.expand(header_id, groupby).then(function () {
395             if (header.root === self.pivot.rows) {
396                 // expanding rows can be done by only inserting in the dom
397                 // console.log(event.target);
398                 var rows = self.build_rows(header.children);
399                 var doc_fragment = $(document.createDocumentFragment());
400                 rows.map(function (row) {
401                     doc_fragment.append(self.draw_row(row, 0));
402                 });
403                 self.$row_clicked.after(doc_fragment);
404             } else {
405                 // expanding cols will redraw the full table
406                 self.display_data();                
407             }
408             if (update_groupby && self.graph_view) {
409                 self.graph_view.register_groupby(self.pivot.rows.groupby, self.pivot.cols.groupby);
410             }
411         });
412
413     },
414
415     fold_row: function (header, event) {
416         var rows_before = this.pivot.rows.headers.length,
417             update_groupby = this.pivot.fold(header),
418             rows_after = this.pivot.rows.headers.length,
419             rows_removed = rows_before - rows_after;
420
421         if (rows_after === 1) {
422             // probably faster to redraw the unique row instead of removing everything
423             this.display_data();
424         } else {
425             var $row = $(event.target).parent().parent();
426             $row.nextAll().slice(0,rows_removed).remove();
427         }
428         if (update_groupby && this.graph_view) {
429             this.graph_view.register_groupby(this.pivot.rows.groupby, this.pivot.cols.groupby);
430         }
431     },
432
433     fold_col: function (header) {
434         var update_groupby = this.pivot.fold(header);
435         
436         this.display_data();
437         if (update_groupby && this.graph_view) {
438             this.graph_view.register_groupby(this.pivot.rows.groupby, this.pivot.cols.groupby);
439         }
440     },
441
442     swap_axis: function () {
443         this.pivot.swap_axis();
444         this.display_data();
445         this.graph_view.register_groupby(this.pivot.rows.groupby, this.pivot.cols.groupby);
446     },
447
448     // ----------------------------------------------------------------------
449     // Convert Pivot data structure into table structure :
450     //      compute rows, cols, colors, cell width, cell height, ...
451     // ----------------------------------------------------------------------
452     build_table: function(raw) {
453         return {
454             headers: this.build_headers(),
455             measure_row: this.build_measure_row(),
456             rows: this.build_rows(this.pivot.rows.headers,raw),
457             nbr_measures: this.pivot.measures.length,
458             title: this.title,
459         };
460     },
461
462     build_headers: function () {
463         var pivot = this.pivot,
464             nbr_measures = pivot.measures.length,
465             height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})) + 1,
466             rows = [];
467
468         _.each(pivot.cols.headers, function (col) {
469             var cell_width = nbr_measures * (col.expanded ? pivot.get_ancestor_leaves(col).length : 1),
470                 cell_height = col.expanded ? 1 : height - col.path.length,
471                 cell = {width: cell_width, height: cell_height, title: col.title, id: col.id, expanded: col.expanded};
472             if (rows[col.path.length]) {
473                 rows[col.path.length].push(cell);
474             } else {
475                 rows[col.path.length] = [cell];
476             }
477         });
478
479         if (pivot.get_cols_leaves().length > 1) {
480             rows[0].push({width: nbr_measures, height: height, title: ' ', id: pivot.main_col().id });
481         }
482         if (pivot.cols.headers.length === 1) {
483             rows = [[{width: nbr_measures, height: 1, title: _t('Total'), id: pivot.main_col().id, expanded: false}]];
484         }
485         return rows;
486     },
487
488     build_measure_row: function () {
489         var nbr_leaves = this.pivot.get_cols_leaves().length,
490             nbr_cols = nbr_leaves + ((nbr_leaves > 1) ? 1 : 0),
491             result = [],
492             add_total = this.pivot.get_cols_leaves().length > 1,
493             i, m;
494         for (i = 0; i < nbr_cols; i++) {
495             for (m = 0; m < this.pivot.measures.length; m++) {
496                 result.push({
497                     text:this.pivot.measures[m].string,
498                     is_bold: add_total && (i === nbr_cols - 1)
499                 });
500             }
501         }
502         return result;
503     },
504
505     make_cell: function (row, col, value, index, raw) {
506         var formatted_value = raw && !_.isUndefined(value) ? value : openerp.web.format_value(value, {type:this.pivot.measures[index].type}),
507             cell = {value:formatted_value};
508
509         return cell;
510     },
511
512     build_rows: function (headers, raw) {
513         var self = this,
514             pivot = this.pivot,
515             m, i, j, k, cell, row;
516
517         var rows = [];
518         var cells, pivot_cells, values;
519
520         var nbr_of_rows = headers.length;
521         var col_headers = pivot.get_cols_leaves();
522
523         for (i = 0; i < nbr_of_rows; i++) {
524             row = headers[i];
525             cells = [];
526             pivot_cells = [];
527             for (j = 0; j < pivot.cells.length; j++) {
528                 if (pivot.cells[j].x == row.id || pivot.cells[j].y == row.id) {
529                     pivot_cells.push(pivot.cells[j]);
530                 }              
531             }
532
533             for (j = 0; j < col_headers.length; j++) {
534                 values = undefined;
535                 for (k = 0; k < pivot_cells.length; k++) {
536                     if (pivot_cells[k].x == col_headers[j].id || pivot_cells[k].y == col_headers[j].id) {
537                         values = pivot_cells[k].values;
538                         break;
539                     }               
540                 }
541                 if (!values) { values = new Array(pivot.measures.length);}
542                 for (m = 0; m < pivot.measures.length; m++) {
543                     cells.push(self.make_cell(row,col_headers[j],values[m], m, raw));
544                 }
545             }
546             if (col_headers.length > 1) {
547                 var totals = pivot.get_total(row);
548                 for (m = 0; m < pivot.measures.length; m++) {
549                     cell = self.make_cell(row, pivot.cols.headers[0], totals[m], m, raw);
550                     cell.is_bold = 'true';
551                     cells.push(cell);
552                 }
553             }
554             rows.push({
555                 id: row.id,
556                 indent: row.path.length,
557                 title: row.title,
558                 expanded: row.expanded,
559                 cells: cells,
560             });
561         }
562
563         return rows;
564     },
565
566     // ----------------------------------------------------------------------
567     // Main display method
568     // ----------------------------------------------------------------------
569     display_data: function () {
570         var scroll = $(window).scrollTop();
571         this.$('.graph_main_content svg').remove();
572         this.$('.graph_main_content div').remove();
573         this.table.empty();
574         this.$('.graph_options_selection label').last().toggleClass('disabled', this.pivot.no_data);
575         this.width = this.$el.width();
576         this.height = Math.min(Math.max(document.documentElement.clientHeight - 116 - 60, 250), Math.round(0.8*this.$el.width()));
577
578         this.$('.graph_header').toggle(this.visible_ui);
579         if (this.pivot.no_data) {
580             this.$('.graph_main_content').append($(QWeb.render('graph_no_data')));
581         } else {
582             if (this.mode === 'pivot') {
583                 this.draw_table();
584                 $(window).scrollTop(scroll);
585             } else {
586                 this.$('.graph_main_content').append($('<div><svg>'));
587                 this.svg = this.$('.graph_main_content svg')[0];
588                 this[this.mode]();
589             }
590         }
591     },
592
593     // ----------------------------------------------------------------------
594     // Drawing the table
595     // ----------------------------------------------------------------------
596     draw_table: function () {
597         var custom_gbs = this.graph_view.get_custom_filter_groupbys(),
598             frozen_rows = custom_gbs.groupby.length,
599             frozen_cols = custom_gbs.col_groupby.length;
600
601         var table = this.build_table();
602         var doc_fragment = $(document.createDocumentFragment());
603         this.draw_headers(table.headers, doc_fragment, frozen_cols);
604         this.draw_measure_row(table.measure_row, doc_fragment);
605         this.draw_rows(table.rows, doc_fragment, frozen_rows);
606         this.table.append(doc_fragment);
607     },
608
609     make_header_cell: function (header, frozen) {
610         var cell = (_.has(header, 'cells') ? $('<td>') : $('<th>'))
611                         .addClass('graph_border')
612                         .attr('rowspan', header.height)
613                         .attr('colspan', header.width);
614         var $content = $('<span>').attr('href','#')
615                                  .text(' ' + (header.title || _t('Undefined')))
616                                  .css('margin-left', header.indent*30 + 'px')
617                                  .attr('data-id', header.id);
618         if (_.has(header, 'expanded')) {
619             if (('indent' in header) && header.indent >= frozen) {
620                 $content.addClass(header.expanded ? 'fa fa-minus-square' : 'fa fa-plus-square');
621                 $content.addClass('web_graph_click');
622             }
623             if (!('indent' in header) && header.lvl >= frozen) {
624                 $content.addClass(header.expanded ? 'fa fa-minus-square' : 'fa fa-plus-square');
625                 $content.addClass('web_graph_click');
626             }
627         } else {
628             $content.css('font-weight', 'bold');
629         }
630         return cell.append($content);
631     },
632
633     draw_headers: function (headers, doc_fragment, frozen_cols) {
634         var make_cell = this.make_header_cell,
635             $empty_cell = $('<th>').attr('rowspan', headers.length),
636             $thead = $('<thead>');
637
638         _.each(headers, function (row, lvl) {
639             var $row = $('<tr>');
640             _.each(row, function (header) {
641                 header.lvl = lvl;
642                 $row.append(make_cell(header, frozen_cols));
643             });
644             $thead.append($row);
645         });
646         $thead.children(':first').prepend($empty_cell);
647         doc_fragment.append($thead);
648         this.$thead = $thead;
649     },
650     
651     draw_measure_row: function (measure_row) {
652         var $row = $('<tr>').append('<th>');
653         _.each(measure_row, function (cell) {
654             var $cell = $('<th>').addClass('measure_row').text(cell.text);
655             if (cell.is_bold) {$cell.css('font-weight', 'bold');}
656             $row.append($cell);
657         });
658         this.$thead.append($row);
659     },
660     
661     draw_row: function (row, frozen_rows) {
662         var $row = $('<tr>')
663             .attr('data-indent', row.indent)
664             .append(this.make_header_cell(row, frozen_rows));
665         
666         var cells_length = row.cells.length;
667         var cells_list = [];
668         var cell, hcell;
669
670         for (var j = 0; j < cells_length; j++) {
671             cell = row.cells[j];
672             hcell = '<td';
673             if (cell.is_bold || cell.color) {
674                 hcell += ' style="';
675                 if (cell.is_bold) hcell += 'font-weight: bold;';
676                 if (cell.color) hcell += 'background-color:' + $.Color(255, cell.color, cell.color) + ';';
677                 hcell += '"';
678             }
679             hcell += '>' + cell.value + '</td>';
680             cells_list[j] = hcell;
681         }
682         return $row.append(cells_list.join(''));
683     },
684
685     draw_rows: function (rows, doc_fragment, frozen_rows) {
686         var rows_length = rows.length,
687             $tbody = $('<tbody>');
688
689         doc_fragment.append($tbody);
690         for (var i = 0; i < rows_length; i++) {
691             $tbody.append(this.draw_row(rows[i], frozen_rows));
692         }
693     },
694
695     // ----------------------------------------------------------------------
696     // Drawing charts code
697     // ----------------------------------------------------------------------
698     bar: function () {
699         var self = this,
700             dim_x = this.pivot.rows.groupby.length,
701             dim_y = this.pivot.cols.groupby.length,
702             show_controls = (this.width > 400 && this.height > 300 && dim_x + dim_y >=2),
703             data;
704
705         // No groupby 
706         if ((dim_x === 0) && (dim_y === 0)) {
707             data = [{key: _t('Total'), values:[{
708                 x: _t('Total'),
709                 y: this.pivot.get_total()[0],
710             }]}];
711         // Only column groupbys 
712         } else if ((dim_x === 0) && (dim_y >= 1)){
713             data =  _.map(this.pivot.get_cols_with_depth(1), function (header) {
714                 return {
715                     key: header.title,
716                     values: [{x:header.title, y: self.pivot.get_total(header)[0]}]
717                 };
718             });
719         // Just 1 row groupby 
720         } else if ((dim_x === 1) && (dim_y === 0))  {
721             data = _.map(this.pivot.main_row().children, function (pt) {
722                 var value = self.pivot.get_total(pt)[0],
723                     title = (pt.title !== undefined) ? pt.title : _t('Undefined');
724                 return {x: title, y: value};
725             });
726             data = [{key: self.pivot.measures[0].string, values:data}];
727         // 1 row groupby and some col groupbys
728         } else if ((dim_x === 1) && (dim_y >= 1))  {
729             data = _.map(this.pivot.get_cols_with_depth(1), function (colhdr) {
730                 var values = _.map(self.pivot.get_rows_with_depth(1), function (header) {
731                     return {
732                         x: header.title || _t('Undefined'),
733                         y: self.pivot.get_values(header.id, colhdr.id)[0] || 0
734                     };
735                 });
736                 return {key: colhdr.title || _t('Undefined'), values: values};
737             });
738         // At least two row groupby
739         } else {
740             var keys = _.uniq(_.map(this.pivot.get_rows_with_depth(2), function (hdr) {
741                 return hdr.title || _t('Undefined');
742             }));
743             data = _.map(keys, function (key) {
744                 var values = _.map(self.pivot.get_rows_with_depth(1), function (hdr) {
745                     var subhdr = _.find(hdr.children, function (child) {
746                         return ((child.title === key) || ((child.title === undefined) && (key === _t('Undefined'))));
747                     });
748                     return {
749                         x: hdr.title || _t('Undefined'),
750                         y: (subhdr) ? self.pivot.get_total(subhdr)[0] : 0
751                     };
752                 });
753                 return {key:key, values: values};
754             });
755         }
756
757         nv.addGraph(function () {
758           var chart = nv.models.multiBarChart()
759                 .reduceXTicks(false)
760                 .stacked(self.bar_ui === 'stack')
761                 .showControls(show_controls);
762
763             if (self.width / data[0].values.length < 80) {
764                 chart.rotateLabels(-15);
765                 chart.reduceXTicks(true);
766                 chart.margin({bottom:40});
767             }
768
769             d3.select(self.svg)
770                 .datum(data)
771                 .attr('width', self.width)
772                 .attr('height', self.height)
773                 .call(chart);
774
775             nv.utils.windowResize(chart.update);
776             return chart;
777         });
778
779     },
780
781     line: function () {
782         var self = this,
783             dim_x = this.pivot.rows.groupby.length,
784             dim_y = this.pivot.cols.groupby.length;
785
786         var rows = this.pivot.get_rows_with_depth(dim_x),
787             labels = _.pluck(rows, 'title');
788
789         var data = _.map(this.pivot.get_cols_leaves(), function (col) {
790             var values = _.map(rows, function (row, index) {
791                 return {x: index, y: self.pivot.get_values(row.id,col.id)[0] || 0};
792             });
793             var title = _.map(col.path, function (p) {
794                 return p || _t('Undefined');
795             }).join('/');
796             if (dim_y === 0) {
797                 title = self.pivot.measures[0].string;
798             }
799             return {values: values, key: title};
800         });
801
802         nv.addGraph(function () {
803             var chart = nv.models.lineChart()
804                 .x(function (d,u) { return u; });
805
806             chart.xAxis.tickFormat(function (d,u) {return labels[d];});
807
808             d3.select(self.svg)
809                 .attr('width', self.width)
810                 .attr('height', self.height)
811                 .datum(data)
812                 .call(chart);
813
814             return chart;
815           });
816     },
817
818     pie: function () {
819         var self = this,
820             dim_x = this.pivot.rows.groupby.length;
821         var data = _.map(this.pivot.get_rows_leaves(), function (row) {
822             var title = _.map(row.path, function (p) {
823                 return p || _t('Undefined');
824             }).join('/');
825             if (dim_x === 0) {
826                 title = self.measure_label;
827             }
828             return {x: title, y: self.pivot.get_total(row)[0]};
829         });
830
831         nv.addGraph(function () {
832             var chart = nv.models.pieChart()
833                 .width(self.width)
834                 .height(self.height)
835                 .color(d3.scale.category10().range());
836
837             d3.select(self.svg)
838                 .datum(data)
839                 .transition().duration(1200)
840                 .attr('width', self.width)
841                 .attr('height', self.height)
842                 .call(chart);
843
844             nv.utils.windowResize(chart.update);
845             return chart;
846         });
847     },
848
849     // ----------------------------------------------------------------------
850     // Controller stuff...
851     // ----------------------------------------------------------------------
852     export_xls: function() {
853         var c = openerp.webclient.crashmanager;
854         openerp.web.blockUI();
855         this.session.get_file({
856             url: '/web_graph/export_xls',
857             data: {data: JSON.stringify(this.build_table(true))},
858             complete: openerp.web.unblockUI,
859             error: c.rpc_error.bind(c)
860         });
861     },
862
863 });
864
865 // Utility function: returns true if the beginning of array2 is array1 and
866 // if array1 is not array2
867 function is_strict_beginning_of (array1, array2) {
868     if (array1.length >= array2.length) { return false; }
869     var result = true;
870     for (var i = 0; i < array1.length; i++) {
871         if (!_.isEqual(array1[i], array2[i])) { return false;}
872     }
873     return result;
874 }
875 })();