[MERGE] forward port of branch saas-3 up to c89d1a0
[odoo/odoo.git] / addons / web_graph / static / src / js / graph_widget.js
index 350cd3f..a08ee52 100644 (file)
@@ -25,6 +25,7 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
         this.bar_ui = options.bar_ui || 'group';
         this.graph_view = options.graph_view || null;
         this.pivot_options = options;
+        this.title = options.title || 'Data';
     },
 
     start: function() {
@@ -42,12 +43,21 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
             this.$('.graph_main_content').addClass('graph_pivot_mode');
         }
 
-        return this.model.call('fields_get', {
-                context: self.graph_view.dataset.context
-            }).then(function (f) {
+        // get search view
+        var parent = this.getParent();
+        while (!(parent instanceof openerp.web.ViewManager)) {
+            parent = parent.getParent();
+        }
+        this.search_view = parent.searchview;
+
+        openerp.session.rpc('/web_graph/check_xlwt').then(function (result) {
+            self.$('.graph_options_selection label').last().toggle(result);
+        });
+
+        return this.model.call('fields_get', []).then(function (f) {
             self.fields = f;
             self.fields.__count = {field:'__count', type: 'integer', string:_t('Count')};
-            self.important_fields = self.get_search_fields();
+            self.groupby_fields = self.get_groupby_fields();
             self.measure_list = self.get_measures();
             self.add_measures_to_options();
             self.pivot_options.row_groupby = self.create_field_values(self.pivot_options.row_groupby || []);
@@ -60,8 +70,9 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
                     self.graph_view.register_groupby(self.pivot.rows.groupby, self.pivot.cols.groupby);
                 }
             });
-            openerp.web.bus.on('click', self, function () {
+            openerp.web.bus.on('click', self, function (event) {
                 if (self.dropdown) {
+                    self.$row_clicked = $(event.target).closest('tr');
                     self.dropdown.remove();
                     self.dropdown = null;
                 }
@@ -70,19 +81,31 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
         });
     },
 
+    get_groupby_fields: function () {
+        var search_fields = this.get_search_fields(),
+            search_field_names = _.pluck(search_fields, 'field'),
+            other_fields = [],
+            groupable_types = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime'];
+
+        _.each(this.fields, function (val, key) {
+            if (!_.contains(search_field_names, key) && 
+                _.contains(groupable_types, val.type) && 
+                val.store === true) {
+                other_fields.push({
+                    field: key,
+                    string: val.string,
+                });
+            }
+        });
+        return search_fields.concat(other_fields);
+    },
+
     // this method gets the fields that appear in the search view, under the 
     // 'Groupby' heading
     get_search_fields: function () {
-        var self = this,
-            parent = this.getParent();
-
-        while (!(parent instanceof openerp.web.ViewManager)) {
-            parent = parent.getParent();
-        }
-
-        var search_view = parent.searchview;
+        var self = this;
 
-        var groupbygroups = _(search_view.inputs).select(function (g) {
+        var groupbygroups = _(this.search_view.drawer.inputs).select(function (g) {
             return g instanceof openerp.web.search.GroupbyGroup;
         });
 
@@ -90,8 +113,8 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
             groupbys = _.flatten(_.map(filters, function (filter) {
                 var groupby = py.eval(filter.attrs.context).group_by;
                 if (!(groupby instanceof Array)) { groupby = [groupby]; }
-                return _.map(groupby, function(g) { 
-                    return {field: g, filter: filter}; 
+                return _.map(groupby, function(g) {
+                    return {field: g, filter: filter};
                 });
             }));
 
@@ -110,7 +133,9 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
     // Extracts the integer/float fields which are not 'id'
     get_measures: function() {
         return _.compact(_.map(this.fields, function (f, id) {
-            if (((f.type === 'integer') || (f.type === 'float')) && (id !== 'id')) {
+            if (((f.type === 'integer') || (f.type === 'float')) && 
+                (id !== 'id') &&
+                (f.store !== false)) {
                 return {field:id, type: f.type, string: f.string};
             }
         }));
@@ -128,41 +153,49 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
     // ----------------------------------------------------------------------
     // Configuration methods
     // ----------------------------------------------------------------------
-    set: function (domain, row_groupby, col_groupby) {
+    set: function (domain, row_groupby, col_groupby, measures_groupby) {
         if (!this.pivot) {
             this.pivot_options.domain = domain;
             this.pivot_options.row_groupby = row_groupby;
             this.pivot_options.col_groupby = col_groupby;
+            this.pivot_options.measures_groupby = measures_groupby;
             return;
         }
         var row_gbs = this.create_field_values(row_groupby),
             col_gbs = this.create_field_values(col_groupby),
+            measures_gbs = this.create_field_values(measures_groupby),
             dom_changed = !_.isEqual(this.pivot.domain, domain),
             row_gb_changed = !_.isEqual(row_gbs, this.pivot.rows.groupby),
             col_gb_changed = !_.isEqual(col_gbs, this.pivot.cols.groupby),
+            measures_gb_changed = !_.isEqual(measures_gbs, this.pivot.measures),
             row_reduced = is_strict_beginning_of(row_gbs, this.pivot.rows.groupby),
-            col_reduced = is_strict_beginning_of(col_gbs, this.pivot.cols.groupby);
+            col_reduced = is_strict_beginning_of(col_gbs, this.pivot.cols.groupby),
+            measures_reduced = is_strict_beginning_of(measures_gbs, this.pivot.measures);
 
-        if (!dom_changed && row_reduced && !col_gb_changed) {
+        if (!dom_changed && row_reduced && !col_gb_changed && !measures_gb_changed) {
             this.pivot.fold_with_depth(this.pivot.rows, row_gbs.length);
             this.display_data();
             return;
         }
-        if (!dom_changed && col_reduced && !row_gb_changed) {
+        if (!dom_changed && col_reduced && !row_gb_changed && !measures_gb_changed) {
             this.pivot.fold_with_depth(this.pivot.cols, col_gbs.length);
             this.display_data();
             return;
         }
 
-        if (!dom_changed && col_reduced && row_reduced) {
+        if (!dom_changed && col_reduced && row_reduced && !measures_gb_changed) {
             this.pivot.fold_with_depth(this.pivot.rows, row_gbs.length);
             this.pivot.fold_with_depth(this.pivot.cols, col_gbs.length);
             this.display_data();
             return;
-        } 
+        }
+
+        if (dom_changed || row_gb_changed || col_gb_changed || measures_gb_changed) {
+            this.pivot.set(domain, row_gbs, col_gbs, measures_gbs).then(this.proxy('display_data'));
+        }
 
-        if (dom_changed || row_gb_changed || col_gb_changed) {
-            this.pivot.set(domain, row_gbs, col_gbs).then(this.proxy('display_data'));
+        if (measures_gb_changed) {
+            this.put_measure_checkmarks();
         }
     },
 
@@ -190,7 +223,7 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
 
     create_field_value: function (f) {
         var field = (_.contains(f, ':')) ? f.split(':')[0] : f,
-            groupby_field = _.findWhere(this.important_fields, {field:field}),
+            groupby_field = _.findWhere(this.groupby_fields, {field:field}),
             string = groupby_field ? groupby_field.string : this.fields[field].string,
             result =  {field: f, string: string, type: this.fields[field].type };
 
@@ -210,6 +243,10 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
         return _.pluck(this.pivot.cols.groupby, 'field');
     },
 
+    get_current_measures: function () {
+        return _.pluck(this.pivot.measures, 'field');
+    },
+
     // ----------------------------------------------------------------------
     // UI code
     // ----------------------------------------------------------------------
@@ -257,24 +294,18 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
     option_selection: function (event) {
         event.preventDefault();
         switch (event.currentTarget.getAttribute('data-choice')) {
-            case 'bar_grouped':
-                this.bar_ui = 'group';
-                if (this.mode === 'bar') {
-                    this.display_data();
-                }
-                break;
-            case 'bar_stacked':
-                this.bar_ui = 'stack';
-                if (this.mode === 'bar') {
-                    this.display_data();
-                }
-                break;
             case 'swap_axis':
                 this.swap_axis();
                 break;
+            case 'expand_all':
+                this.pivot.expand_all().then(this.proxy('display_data'));
+                break;
             case 'update_values':
                 this.pivot.update_data().then(this.proxy('display_data'));
                 break;
+            case 'export_data':
+                this.export_xls();
+                break;
         }
     },
 
@@ -297,18 +328,23 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
             self = this;
 
         if (header.expanded) {
-            this.fold(header);
+            if (header.root === this.pivot.rows) {
+                this.fold_row(header, event);
+            } else {
+                this.fold_col(header);
+            }
             return;
-        } 
+        }
         if (header.path.length < header.root.groupby.length) {
+            this.$row_clicked = $(event.target).closest('tr');
             this.expand(id);
             return;
-        } 
-        if (!this.important_fields.length) {
+        }
+        if (!this.groupby_fields.length) {
             return;
         }
 
-        var fields = _.map(this.important_fields, function (field) {
+        var fields = _.map(this.groupby_fields, function (field) {
                 return {id: field.field, value: field.string, type:self.fields[field.field.split(':')[0]].type};
         });
         if (this.dropdown) {
@@ -316,12 +352,11 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
         }
         this.dropdown = $(QWeb.render('field_selection', {fields:fields, header_id:id}));
         $(event.target).after(this.dropdown);
-        this.dropdown.css({position:'absolute',
-                           left:event.pageX,
-                           top:event.pageY});
-        this.$('.field-selection').next('.dropdown-menu').toggle();
-        
-        
+        this.dropdown.css({
+            position:'absolute',
+            left:event.originalEvent.layerX,
+        });
+        this.$('.field-selection').next('.dropdown-menu').first().toggle();        
     },
 
     field_selection: function (event) {
@@ -348,17 +383,47 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
         groupby = groupby || header.root.groupby[header.path.length];
 
         this.pivot.expand(header_id, groupby).then(function () {
+            if (header.root === self.pivot.rows) {
+                // expanding rows can be done by only inserting in the dom
+                // console.log(event.target);
+                var rows = self.build_rows(header.children);
+                var doc_fragment = $(document.createDocumentFragment());
+                rows.map(function (row) {
+                    doc_fragment.append(self.draw_row(row, 0));
+                });
+                self.$row_clicked.after(doc_fragment);
+            } else {
+                // expanding cols will redraw the full table
+                self.display_data();                
+            }
             if (update_groupby && self.graph_view) {
                 self.graph_view.register_groupby(self.pivot.rows.groupby, self.pivot.cols.groupby);
             }
-            self.display_data();
         });
 
     },
 
-    fold: function (header) {
-        var update_groupby = this.pivot.fold(header);
+    fold_row: function (header, event) {
+        var rows_before = this.pivot.rows.headers.length,
+            update_groupby = this.pivot.fold(header),
+            rows_after = this.pivot.rows.headers.length,
+            rows_removed = rows_before - rows_after;
 
+        if (rows_after === 1) {
+            // probably faster to redraw the unique row instead of removing everything
+            this.display_data();
+        } else {
+            var $row = $(event.target).parent().parent();
+            $row.nextAll().slice(0,rows_removed).remove();
+        }
+        if (update_groupby && this.graph_view) {
+            this.graph_view.register_groupby(this.pivot.rows.groupby, this.pivot.cols.groupby);
+        }
+    },
+
+    fold_col: function (header) {
+        var update_groupby = this.pivot.fold(header);
+        
         this.display_data();
         if (update_groupby && this.graph_view) {
             this.graph_view.register_groupby(this.pivot.rows.groupby, this.pivot.cols.groupby);
@@ -372,13 +437,141 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
     },
 
     // ----------------------------------------------------------------------
+    // Convert Pivot data structure into table structure :
+    //      compute rows, cols, colors, cell width, cell height, ...
+    // ----------------------------------------------------------------------
+    build_table: function(raw) {
+        return {
+            headers: this.build_headers(),
+            measure_row: this.build_measure_row(),
+            rows: this.build_rows(this.pivot.rows.headers,raw),
+            nbr_measures: this.pivot.measures.length,
+            title: this.title,
+        };
+    },
+
+    build_headers: function () {
+        var pivot = this.pivot,
+            nbr_measures = pivot.measures.length,
+            height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})) + 1,
+            rows = [];
+
+        _.each(pivot.cols.headers, function (col) {
+            var cell_width = nbr_measures * (col.expanded ? pivot.get_ancestor_leaves(col).length : 1),
+                cell_height = col.expanded ? 1 : height - col.path.length,
+                cell = {width: cell_width, height: cell_height, title: col.title, id: col.id, expanded: col.expanded};
+            if (rows[col.path.length]) {
+                rows[col.path.length].push(cell);
+            } else {
+                rows[col.path.length] = [cell];
+            }
+        });
+
+        if (pivot.get_cols_leaves().length > 1) {
+            rows[0].push({width: nbr_measures, height: height, title: ' ', id: pivot.main_col().id });
+        }
+        if (pivot.cols.headers.length === 1) {
+            rows = [[{width: nbr_measures, height: 1, title: _t('Total'), id: pivot.main_col().id, expanded: false}]];
+        }
+        return rows;
+    },
+
+    build_measure_row: function () {
+        var nbr_leaves = this.pivot.get_cols_leaves().length,
+            nbr_cols = nbr_leaves + ((nbr_leaves > 1) ? 1 : 0),
+            result = [],
+            add_total = this.pivot.get_cols_leaves().length > 1,
+            i, m;
+        for (i = 0; i < nbr_cols; i++) {
+            for (m = 0; m < this.pivot.measures.length; m++) {
+                result.push({
+                    text:this.pivot.measures[m].string,
+                    is_bold: add_total && (i === nbr_cols - 1)
+                });
+            }
+        }
+        return result;
+    },
+
+    make_cell: function (row, col, value, index, raw) {
+        var formatted_value = raw && !_.isUndefined(value) ? value : openerp.web.format_value(value, {type:this.pivot.measures[index].type}),
+            cell = {value:formatted_value};
+
+        if (this.heatmap_mode === 'none') { return cell; }
+        var total = (this.heatmap_mode === 'both') ? this.pivot.get_total()[index]
+                  : (this.heatmap_mode === 'row')  ? this.pivot.get_total(row)[index]
+                  : this.pivot.get_total(col)[index];
+        var color = Math.floor(90 + 165*(total - Math.abs(value))/total);
+        if (color < 255) {
+            cell.color = color;
+        }
+        return cell;
+    },
+
+    build_rows: function (headers, raw) {
+        var self = this,
+            pivot = this.pivot,
+            m, i, j, k, cell, row;
+
+        var rows = [];
+        var cells, pivot_cells, values;
+
+        var nbr_of_rows = headers.length;
+        var col_headers = pivot.get_cols_leaves();
+
+        for (i = 0; i < nbr_of_rows; i++) {
+            row = headers[i];
+            cells = [];
+            pivot_cells = [];
+            for (j = 0; j < pivot.cells.length; j++) {
+                if (pivot.cells[j].x == row.id || pivot.cells[j].y == row.id) {
+                    pivot_cells.push(pivot.cells[j]);
+                }              
+            }
+
+            for (j = 0; j < col_headers.length; j++) {
+                values = undefined;
+                for (k = 0; k < pivot_cells.length; k++) {
+                    if (pivot_cells[k].x == col_headers[j].id || pivot_cells[k].y == col_headers[j].id) {
+                        values = pivot_cells[k].values;
+                        break;
+                    }               
+                }
+                if (!values) { values = new Array(pivot.measures.length);}
+                for (m = 0; m < pivot.measures.length; m++) {
+                    cells.push(self.make_cell(row,col_headers[j],values[m], m, raw));
+                }
+            }
+            if (col_headers.length > 1) {
+                var totals = pivot.get_total(row);
+                for (m = 0; m < pivot.measures.length; m++) {
+                    cell = self.make_cell(row, pivot.cols.headers[0], totals[m], m, raw);
+                    cell.is_bold = 'true';
+                    cells.push(cell);
+                }
+            }
+            rows.push({
+                id: row.id,
+                indent: row.path.length,
+                title: row.title,
+                expanded: row.expanded,
+                cells: cells,
+            });
+        }
+
+        return rows;
+    },
+
+    // ----------------------------------------------------------------------
     // Main display method
     // ----------------------------------------------------------------------
     display_data: function () {
+        var scroll = $(window).scrollTop();
         this.$('.graph_main_content svg').remove();
         this.$('.graph_main_content div').remove();
         this.table.empty();
         this.table.toggleClass('heatmap', this.heatmap_mode !== 'none');
+        this.$('.graph_options_selection label').last().toggleClass('disabled', this.pivot.no_data);
         this.width = this.$el.width();
         this.height = Math.min(Math.max(document.documentElement.clientHeight - 116 - 60, 250), Math.round(0.8*this.$el.width()));
 
@@ -388,6 +581,7 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
         } else {
             if (this.mode === 'pivot') {
                 this.draw_table();
+                $(window).scrollTop(scroll);
             } else {
                 this.$('.graph_main_content').append($('<div><svg>'));
                 this.svg = this.$('.graph_main_content svg')[0];
@@ -400,158 +594,101 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
     // Drawing the table
     // ----------------------------------------------------------------------
     draw_table: function () {
-        this.draw_top_headers();
-        _.each(this.pivot.rows.headers, this.proxy('draw_row'));
-    },
-
-    make_border_cell: function (colspan, rowspan, headercell) {
-        var tag = (headercell) ? $('<th>') : $('<td>');
-        return tag.addClass('graph_border')
-                             .attr('colspan', colspan || 1)
-                             .attr('rowspan', rowspan || 1);
-    },
-
-    make_header_title: function (header) {
-        return $('<span> ')
-            .addClass('web_graph_click')
-            .attr('href', '#')
-            .addClass((header.expanded) ? 'fa fa-minus-square' : 'fa fa-plus-square')
-            .text(' ' + (header.title || 'Undefined'));
-    },
-
-    draw_top_headers: function () {
-        var self = this,
-            thead = $('<thead>'),
-            pivot = this.pivot,
-            height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})),
-            header_cells = [[this.make_border_cell(1, height, true)]];
-
-        function set_dim (cols) {
-            _.each(cols.children, set_dim);
-            if (cols.children.length === 0) {
-                cols.height = height - cols.path.length + 1;
-                cols.width = 1;
-            } else {
-                cols.height = 1;
-                cols.width = _.reduce(cols.children, function (sum,c) { return sum + c.width;}, 0);
-            }
-        }
-
-        function make_col_header (col) {
-            var cell = self.make_border_cell(col.width*pivot.measures.length, col.height, true);
-            return cell.append(self.make_header_title(col).attr('data-id', col.id));
-        }
-
-        function make_cells (queue, level) {
-            var col = queue[0];
-            queue = _.rest(queue).concat(col.children);
-            if (col.path.length == level) {
-                _.last(header_cells).push(make_col_header(col));
-            } else {
-                level +=1;
-                header_cells.push([make_col_header(col)]);
+        var custom_gbs = this.graph_view.get_custom_filter_groupbys(),
+            frozen_rows = custom_gbs.groupby.length,
+            frozen_cols = custom_gbs.col_groupby.length;
+
+        var table = this.build_table();
+        var doc_fragment = $(document.createDocumentFragment());
+        this.draw_headers(table.headers, doc_fragment, frozen_cols);
+        this.draw_measure_row(table.measure_row, doc_fragment);
+        this.draw_rows(table.rows, doc_fragment, frozen_rows);
+        this.table.append(doc_fragment);
+    },
+
+    make_header_cell: function (header, frozen) {
+        var cell = (_.has(header, 'cells') ? $('<td>') : $('<th>'))
+                        .addClass('graph_border')
+                        .attr('rowspan', header.height)
+                        .attr('colspan', header.width);
+        var $content = $('<span>').attr('href','#')
+                                 .text(' ' + (header.title || _t('Undefined')))
+                                 .css('margin-left', header.indent*30 + 'px')
+                                 .attr('data-id', header.id);
+        if (_.has(header, 'expanded')) {
+            if (('indent' in header) && header.indent >= frozen) {
+                $content.addClass(header.expanded ? 'fa fa-minus-square' : 'fa fa-plus-square');
+                $content.addClass('web_graph_click');
             }
-            if (queue.length !== 0) {
-                make_cells(queue, level);
+            if (!('indent' in header) && header.lvl >= frozen) {
+                $content.addClass(header.expanded ? 'fa fa-minus-square' : 'fa fa-plus-square');
+                $content.addClass('web_graph_click');
             }
-        }
-
-        set_dim(pivot.main_col());  // add width and height info to columns headers
-        if (pivot.main_col().children.length === 0) {
-            make_cells(pivot.cols.headers, 0);
         } else {
-            make_cells(pivot.main_col().children, 1);
-            if (pivot.get_cols_leaves().length > 1) {
-                header_cells[0].push(self.make_border_cell(pivot.measures.length, height, true).text(_t('Total')).css('font-weight', 'bold'));
-            }
+            $content.css('font-weight', 'bold');
         }
-
-        _.each(header_cells, function (cells) {
-            thead.append($('<tr>').append(cells));
-        });
-        
-        if (pivot.measures.length >= 2) {
-            thead.append(self.make_measure_row());
-        }
-
-        self.table.append(thead);
+        return cell.append($content);
     },
 
-    make_measure_cells: function () {
-        return _.map(this.pivot.measures, function (measure) {
-            return $('<th>').addClass('measure_row').text(measure.string);
-        });
-    },
-
-    make_measure_row: function() {
-        var self = this,
-            cols = this.pivot.cols.headers,
-            measure_row = $('<tr>');
+    draw_headers: function (headers, doc_fragment, frozen_cols) {
+        var make_cell = this.make_header_cell,
+            $empty_cell = $('<th>').attr('rowspan', headers.length),
+            $thead = $('<thead>');
 
-        measure_row.append($('<th>'));
-
-        _.each(cols, function (col) {
-            if (!col.children.length) {
-                measure_row.append(self.make_measure_cells());
-            }
+        _.each(headers, function (row, lvl) {
+            var $row = $('<tr>');
+            _.each(row, function (header) {
+                header.lvl = lvl;
+                $row.append(make_cell(header, frozen_cols));
+            });
+            $thead.append($row);
         });
-
-        if (this.pivot.get_cols_leaves().length > 1) {
-            measure_row.append(self.make_measure_cells());
-        }
-        return measure_row;
-    },
-
-    draw_row: function (row) {
-        var self = this,
-            pivot = this.pivot,
-            measure_types = _.pluck(this.pivot.measures, 'type'),
-            html_row = $('<tr>'),
-            row_header = this.make_border_cell(1,1)
-                .append(this.make_header_title(row).attr('data-id', row.id))
-                .addClass('graph_border');
-
-        for (var i = 0; i < row.path.length; i++) {
-            row_header.prepend($('<span>', {class:'web_graph_indent'}));
-        }
-
-        html_row.append(row_header);
-
-        _.each(pivot.cols.headers, function (col) {
-            if (!col.children.length) {
-                var values = pivot.get_values(row.id, col.id);
-                for (var i = 0; i < values.length; i++) {
-                    html_row.append(make_cell(values[i], measure_types[i], i, col));
-                }
-            }
+        $thead.children(':first').prepend($empty_cell);
+        doc_fragment.append($thead);
+        this.$thead = $thead;
+    },
+    
+    draw_measure_row: function (measure_row) {
+        var $row = $('<tr>').append('<th>');
+        _.each(measure_row, function (cell) {
+            var $cell = $('<th>').addClass('measure_row').text(cell.text);
+            if (cell.is_bold) {$cell.css('font-weight', 'bold');}
+            $row.append($cell);
         });
-
-        if (pivot.get_cols_leaves().length > 1) {
-            var total_vals = pivot.get_total(row);
-            for (var j = 0; j < total_vals.length; j++) {
-                var cell = make_cell(total_vals[j], measure_types[j], j, pivot.cols[0]).css('font-weight', 'bold');
-                html_row.append(cell);
+        this.$thead.append($row);
+    },
+    
+    draw_row: function (row, frozen_rows) {
+        var $row = $('<tr>')
+            .attr('data-indent', row.indent)
+            .append(this.make_header_cell(row, frozen_rows));
+        
+        var cells_length = row.cells.length;
+        var cells_list = [];
+        var cell, hcell;
+
+        for (var j = 0; j < cells_length; j++) {
+            cell = row.cells[j];
+            hcell = '<td';
+            if (cell.is_bold || cell.color) {
+                hcell += ' style="';
+                if (cell.is_bold) hcell += 'font-weight: bold;';
+                if (cell.color) hcell += 'background-color:' + $.Color(255, cell.color, cell.color) + ';';
+                hcell += '"';
             }
+            hcell += '>' + cell.value + '</td>';
+            cells_list[j] = hcell;
         }
+        return $row.append(cells_list.join(''));
+    },
 
-        this.table.append(html_row);
+    draw_rows: function (rows, doc_fragment, frozen_rows) {
+        var rows_length = rows.length,
+            $tbody = $('<tbody>');
 
-        function make_cell (value, measure_type, index, col) {
-            var cell = $('<td>');
-            if (value === undefined) {
-                return cell;
-            }
-            cell.text(openerp.web.format_value(value, {type: measure_type}));
-            var total = (self.heatmap_mode === 'both') ? pivot.get_total()[index]
-                  : (self.heatmap_mode === 'row')  ? pivot.get_total(row)[index]
-                  : (self.heatmap_mode === 'col')  ? pivot.get_total(col)[index]
-                  : undefined;
-
-            if (self.heatmap_mode !== 'none') {
-                var color = Math.floor(90 + 165*(total - Math.abs(value))/total);
-                cell.css('background-color', $.Color(255, color, color));
-            }
-            return cell;
+        doc_fragment.append($tbody);
+        for (var i = 0; i < rows_length; i++) {
+            $tbody.append(this.draw_row(rows[i], frozen_rows));
         }
     },
 
@@ -569,7 +706,7 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
         if ((dim_x === 0) && (dim_y === 0)) {
             data = [{key: _t('Total'), values:[{
                 x: _t('Total'),
-                y: this.pivot.get_total(),
+                y: this.pivot.get_total()[0],
             }]}];
         // Only column groupbys 
         } else if ((dim_x === 0) && (dim_y >= 1)){
@@ -582,7 +719,7 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
         // Just 1 row groupby 
         } else if ((dim_x === 1) && (dim_y === 0))  {
             data = _.map(this.pivot.main_row().children, function (pt) {
-                var value = self.pivot.get_total(pt),
+                var value = self.pivot.get_total(pt)[0],
                     title = (pt.title !== undefined) ? pt.title : _t('Undefined');
                 return {x: title, y: value};
             });
@@ -619,8 +756,6 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
 
         nv.addGraph(function () {
           var chart = nv.models.multiBarChart()
-                .width(self.width)
-                .height(self.height)
                 .reduceXTicks(false)
                 .stacked(self.bar_ui === 'stack')
                 .showControls(show_controls);
@@ -648,9 +783,12 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
             dim_x = this.pivot.rows.groupby.length,
             dim_y = this.pivot.cols.groupby.length;
 
+        var rows = this.pivot.get_rows_with_depth(dim_x),
+            labels = _.pluck(rows, 'title');
+
         var data = _.map(this.pivot.get_cols_leaves(), function (col) {
-            var values = _.map(self.pivot.get_rows_with_depth(dim_x), function (row) {
-                return {x: row.title, y: self.pivot.get_values(row.id,col.id)[0] || 0};
+            var values = _.map(rows, function (row, index) {
+                return {x: index, y: self.pivot.get_values(row.id,col.id)[0] || 0};
             });
             var title = _.map(col.path, function (p) {
                 return p || _t('Undefined');
@@ -663,10 +801,9 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
 
         nv.addGraph(function () {
             var chart = nv.models.lineChart()
-                .x(function (d,u) { return u; })
-                .width(self.width)
-                .height(self.height)
-                .margin({top: 30, right: 20, bottom: 20, left: 60});
+                .x(function (d,u) { return u; });
+
+            chart.xAxis.tickFormat(function (d,u) {return labels[d];});
 
             d3.select(self.svg)
                 .attr('width', self.width)
@@ -688,14 +825,14 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
             if (dim_x === 0) {
                 title = self.measure_label;
             }
-            return {x: title, y: self.pivot.get_total(row)};
+            return {x: title, y: self.pivot.get_total(row)[0]};
         });
 
         nv.addGraph(function () {
             var chart = nv.models.pieChart()
-                .color(d3.scale.category10().range())
                 .width(self.width)
-                .height(self.height);
+                .height(self.height)
+                .color(d3.scale.category10().range());
 
             d3.select(self.svg)
                 .datum(data)
@@ -709,6 +846,20 @@ openerp.web_graph.Graph = openerp.web.Widget.extend({
         });
     },
 
+    // ----------------------------------------------------------------------
+    // Controller stuff...
+    // ----------------------------------------------------------------------
+    export_xls: function() {
+        var c = openerp.webclient.crashmanager;
+        openerp.web.blockUI();
+        this.session.get_file({
+            url: '/web_graph/export_xls',
+            data: {data: JSON.stringify(this.build_table(true))},
+            complete: openerp.web.unblockUI,
+            error: c.rpc_error.bind(c)
+        });
+    },
+
 });
 
 // Utility function: returns true if the beginning of array2 is array1 and