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