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