diff options
Diffstat (limited to 'addons/web_graph/static/src/js')
| -rw-r--r-- | addons/web_graph/static/src/js/graph.js | 434 | ||||
| -rw-r--r-- | addons/web_graph/static/src/js/graph_view.js | 222 | ||||
| -rw-r--r-- | addons/web_graph/static/src/js/graph_widget.js | 761 | ||||
| -rw-r--r-- | addons/web_graph/static/src/js/pivot_table.js | 469 |
4 files changed, 1452 insertions, 434 deletions
diff --git a/addons/web_graph/static/src/js/graph.js b/addons/web_graph/static/src/js/graph.js deleted file mode 100644 index a84bb53fc85..00000000000 --- a/addons/web_graph/static/src/js/graph.js +++ /dev/null @@ -1,434 +0,0 @@ -/*--------------------------------------------------------- - * OpenERP web_graph - *---------------------------------------------------------*/ - -openerp.web_graph = function (instance) { - -var _lt = instance.web._lt; -var _t = instance.web._t; - -// removed ``undefined`` values -var filter_values = function (o) { - var out = {}; - for (var k in o) { - if (!o.hasOwnProperty(k) || o[k] === undefined) { continue; } - out[k] = o[k]; - } - return out; -}; - -instance.web.views.add('graph', 'instance.web_graph.GraphView'); -instance.web_graph.GraphView = instance.web.View.extend({ - template: "GraphView", - display_name: _lt('Graph'), - view_type: "graph", - - init: function(parent, dataset, view_id, options) { - var self = this; - this._super(parent); - this.set_default_options(options); - this.dataset = dataset; - this.view_id = view_id; - - this.mode = "bar"; // line, bar, area, pie, radar - this.orientation = false; // true: horizontal, false: vertical - this.stacked = true; - - this.spreadsheet = false; // Display data grid, allows copy to CSV - this.forcehtml = false; - this.legend = "top"; // top, inside, no - - this.domain = []; - this.context = {}; - this.group_by = []; - - this.graph = null; - }, - view_loading: function(r) { - return this.load_graph(r); - }, - destroy: function () { - if (this.graph) { - this.graph.destroy(); - } - this._super(); - }, - - load_graph: function(fields_view_get) { - // TODO: move to load_view and document - var self = this; - this.fields_view = fields_view_get; - this.$el.addClass(this.fields_view.arch.attrs['class']); - - this.mode = this.fields_view.arch.attrs.type || 'bar'; - this.orientation = this.fields_view.arch.attrs.orientation == 'horizontal'; - - var width = this.$el.parent().width(); - this.$el.css("width", width); - this.container = this.$el.find("#editor-render-body").css({ - width: width, - height: Math.min(500, width * 0.8) - })[0]; - - var graph_render = this.proxy('graph_render'); - this.$el.on('click', '.oe_graph_options a', function (evt) { - var $el = $(evt.target); - - self.graph_render({data: filter_values({ - mode: $el.data('mode'), - legend: $el.data('legend'), - orientation: $el.data('orientation'), - stacked: $el.data('stacked') - })}); - }); - - this.$el.find("#graph_show_data").click(function () { - self.spreadsheet = ! self.spreadsheet; - self.graph_render(); - }); - this.$el.find("#graph_switch").click(function () { - if (self.mode != 'radar') { - self.orientation = ! self.orientation; - } - self.graph_render(); - }); - - this.$el.find("#graph_download").click(function () { - if (self.legend == "top") { self.legend = "inside"; } - self.forcehtml = true; - - self.graph_get_data().done(function (result) { - self.graph_render_all(result).download.saveImage('png'); - }).always(function () { - self.forcehtml = false; - }); - }); - this.trigger('graph_view_loaded', fields_view_get); - }, - - get_format: function (options) { - options = options || {}; - var legend = { - show: this.legend != 'no', - }; - - switch (this.legend) { - case 'top': - legend.noColumns = 4; - legend.container = this.$el.find("div.graph_header_legend")[0]; - break; - case 'inside': - legend.position = 'nw'; - legend.backgroundColor = '#D2E8FF'; - break; - } - - return _.extend({ - legend: legend, - mouse: { - track: true, - relative: true - }, - spreadsheet : { - show: this.spreadsheet, - initialTab: "data" - }, - HtmlText : (options.xaxis && options.xaxis.labelsAngle) ? false : !this.forcehtml, - }, options); - }, - - make_graph: function (mode, container, data) { - if (mode === 'area') { mode = 'line'; } - var format = this.get_format(this['options_' + mode](data)); - return Flotr.draw(container, data.data, format); - }, - - options_bar: function (data) { - var min = _(data.data).chain() - .map(function (record) { - if (record.data.length > 0){ - return _.min(record.data, function (item) { - return item[1]; - })[1]; - } - }).min().value(); - return { - bars : { - show : true, - stacked : this.stacked, - horizontal : this.orientation, - barWidth : 0.7, - lineWidth : 1 - }, - grid : { - verticalLines : this.orientation, - horizontalLines : !this.orientation, - outline : "sw", - }, - yaxis : { - ticks: this.orientation ? data.ticks : false, - min: !this.orientation ? (min < 0 ? min : 0) : null - }, - xaxis : { - labelsAngle: 45, - ticks: this.orientation ? false : data.ticks, - min: this.orientation ? (min < 0 ? min : 0) : null - } - }; - }, - - options_pie: function (data) { - return { - pie : { - show: true - }, - grid : { - verticalLines : false, - horizontalLines : false, - outline : "", - }, - xaxis : {showLabels: false}, - yaxis : {showLabels: false}, - }; - }, - - options_radar: function (data) { - return { - radar : { - show : true, - stacked : this.stacked - }, - grid : { - circular : true, - minorHorizontalLines : true - }, - xaxis : { - ticks: data.ticks - }, - }; - }, - - options_line: function (data) { - return { - lines : { - show : true, - }, - points: { - show: true, - }, - grid : { - verticalLines : this.orientation, - horizontalLines : !this.orientation, - outline : "sw", - }, - yaxis : { - ticks: this.orientation ? data.ticks : false - }, - xaxis : { - labelsAngle: 45, - ticks: this.orientation ? false : data.ticks - } - }; - }, - - graph_get_data: function () { - var model = this.dataset.model, - domain = new instance.web.CompoundDomain(this.domain || []), - context = new instance.web.CompoundContext(this.context || {}), - group_by = this.group_by || [], - view_id = this.view_id || false, - mode = this.mode || 'bar', - orientation = this.orientation || false, - stacked = this.stacked || false; - - var obj = new instance.web.Model(model); - var view_get; - var fields; - var result = []; - var ticks = {}; - - return this.alive(obj.call("fields_view_get", [view_id, 'graph', context]).then(function(tmp) { - view_get = tmp; - fields = view_get['fields']; - var toload = _.select(group_by, function(x) { return fields[x] === undefined }); - if (toload.length >= 1) - return obj.call("fields_get", [toload, context]); - else - return $.when([]); - }).then(function (fields_to_add) { - _.extend(fields, fields_to_add); - - var tree = $($.parseXML(view_get['arch'])); - - var pos = 0; - var xaxis = _.clone(group_by || []); - var yaxis = []; - tree.find("field").each(function() { - var field = $(this); - if (! field.attr("name")) - return; - if ((group_by.length == 0) && ((! pos) || instance.web.py_eval(field.attr('group') || "false"))) { - xaxis.push(field.attr('name')); - } - if (pos && ! instance.web.py_eval(field.attr('group') || "false")) { - yaxis.push(field.attr('name')); - } - pos += 1; - }); - - if (xaxis.length === 0) - throw new Error("No field for the X axis!"); - if (yaxis.length === 0) - throw new Error("No field for the Y axis!"); - - // Convert a field's data into a displayable string - - function _convert_key(field, data) { - if (fields[field]['type'] === 'many2one') - data = data && data[0]; - return data; - } - - function _convert(field, data, tick) { - tick = tick === undefined ? true : false; - try { - data = instance.web.format_value(data, fields[field]); - } catch(e) { - data = "" + data; - } - if (tick) { - if (ticks[data] === undefined) - ticks[data] = _.size(ticks); - return ticks[data]; - } - return data || 0; - } - - function _orientation(x, y) { - if (! orientation) - return [x, y] - return [y, x] - } - - if (mode === "pie") { - return obj.call("read_group", [domain, yaxis.concat([xaxis[0]]), [xaxis[0]]], {context: context}).then(function(res) { - _.each(res, function(record) { - result.push({ - 'data': [[_convert(xaxis[0], record[xaxis[0]]), record[yaxis[0]]]], - 'label': _convert(xaxis[0], record[xaxis[0]], false) - }); - }); - }); - } else if ((! stacked) || (xaxis.length < 2)) { - var defs = []; - _.each(xaxis, function(x) { - defs.push(obj.call("read_group", [domain, yaxis.concat([x]), [x]], {context: context}).then(function(res) { - return [x, res]; - })); - }); - return $.when.apply($, defs).then(function() { - _.each(_.toArray(arguments), function(res) { - var x = res[0]; - res = res[1]; - result.push({ - 'data': _.map(res, function(record) { - return _orientation(_convert(x, record[x]), record[yaxis[0]] || 0); - }), - 'label': fields[x]['string'] - }); - }); - }); - } else { - xaxis.reverse(); - return obj.call("read_group", [domain, yaxis.concat(xaxis.slice(0, 1)), xaxis.slice(0, 1)], {context: context}).then(function(axis) { - var defs = []; - _.each(axis, function(x) { - var key = x[xaxis[0]] - defs.push(obj.call("read_group", [x['__domain'], yaxis.concat(xaxis.slice(1, 2)), xaxis.slice(1, 2)], {context: context}).then(function(res) { - return [x, key, res]; - })); - }); - return $.when.apply($, defs).then(function() { - _.each(_.toArray(arguments), function(res) { - var x = res[0]; - var key = res[1]; - res = res[2]; - result.push({ - 'data': _.map(res, function(record) { - return _orientation(_convert(xaxis[1], record[xaxis[1]]), record[yaxis[0]] || 0); - }), - 'label': _convert(xaxis[0], key, false) - }) - }); - }); - }); - } - }).then(function() { - var res = { - 'data': result, - 'ticks': _.map(ticks, function(el, key) { return [el, key || _t("Undefined")] }) - }; - return res; - })); - }, - - // Render the graph and update menu styles - graph_render: function (options) { - options = options || {}; - _.extend(this, options.data); - - return this.graph_get_data() - .done(this.proxy('graph_render_all')); - }, - - graph_render_all: function (data) { - var i; - if (this.mode=='area') { - for (i=0; i<data.data.length; i++) { - data.data[i].lines = {fill: true} - } - } - if (this.graph) { - this.graph.destroy(); - } - - // Render the graph - this.$el.find(".graph_header_legend").children().remove(); - this.graph = this.make_graph(this.mode, this.container, data); - - // Update styles of menus - - this.$el.find("a").removeClass("active"); - - var $active = this.$el.find('a[data-mode=' + this.mode + ']'); - if ($active.length > 1) { - $active = $active.filter('[data-stacked=' + this.stacked + ']'); - } - $active = $active.add( - this.$el.find('a:not([data-mode])[data-legend=' + this.legend + ']')); - - $active.addClass('active'); - - if (this.spreadsheet) { - this.$el.find("#graph_show_data").addClass("active"); - } - return this.graph; - }, - - // render the graph using the domain, context and group_by - // calls the 'graph_data_get' python controller to process all data - // TODO: check is group_by should better be in the context - do_search: function(domain, context, group_by) { - this.domain = domain; - this.context = context; - this.group_by = group_by; - - this.graph_render(); - }, - - do_show: function() { - this.do_push_state({}); - return this._super(); - }, -}); -}; diff --git a/addons/web_graph/static/src/js/graph_view.js b/addons/web_graph/static/src/js/graph_view.js new file mode 100644 index 00000000000..fa39fc581db --- /dev/null +++ b/addons/web_graph/static/src/js/graph_view.js @@ -0,0 +1,222 @@ +/*--------------------------------------------------------- + * OpenERP web_graph + *---------------------------------------------------------*/ + +/* jshint undef: false */ + +openerp.web_graph = function (instance) { +'use strict'; + +var _lt = instance.web._lt; +var _t = instance.web._t; + +instance.web.views.add('graph', 'instance.web_graph.GraphView'); + +instance.web_graph.GraphView = instance.web.View.extend({ + display_name: _lt('Graph'), + view_type: 'graph', + + // ---------------------------------------------------------------------- + // Init stuff + // ---------------------------------------------------------------------- + init: function(parent, dataset, view_id, options) { + var self = this; + this._super(parent, dataset, view_id, options); + this.dataset = dataset; + this.model = new instance.web.Model(dataset.model, {group_by_no_leaf: true}); + this.search_view = parent.searchview; + this.col_search_field = { + get_context: function() { return { col_group_by: self.graph_widget.get_col_groupbys()};}, + get_domain: function () {}, + get_groupby: function () {}, + }; + }, + + view_loading: function (fields_view_get) { + var self = this, + arch = fields_view_get.arch; + + this.widget_config = { + title: arch.attrs.string, + stacked : (arch.attrs.stacked === 'True'), + mode: arch.attrs.type || 'bar', + measures: [], + row_groupby: [], + col_groupby: [], + graph_view: this, + }; + + _.each(arch.children, function (field) { + var field_name = field.attrs.name; + if (_.has(field.attrs, 'interval')) { + field_name = field.attrs.name + ':' + field.attrs.interval; + } + if (_.has(field.attrs, 'type')) { + switch (field.attrs.type) { + case 'row': + self.widget_config.row_groupby.push(field_name); + break; + case 'col': + self.widget_config.col_groupby.push(field_name); + break; + case 'measure': + self.widget_config.measures.push(field_name); + break; + } + } else { // old style, kept for backward compatibility + if ('operator' in field.attrs) { + self.widget_config.measures.push(field_name); + } else { + self.widget_config.row_groupby.push(field_name); + } + } + }); + if (self.widget_config.measures.length === 0) { + self.widget_config.measures.push('__count'); + } + }, + + do_search: function (domain, context, group_by) { + if (this.ignore_do_search) { + this.ignore_do_search = false; + return; + } + var self = this, + groupbys = this.get_groupbys_from_searchview(), + col_group_by = groupbys.col_group_by; + + if (!this.graph_widget) { + if (group_by.length) { + this.widget_config.row_groupby = group_by; + } + if (col_group_by.length) { + this.widget_config.col_groupby = col_group_by; + } + this.graph_widget = new openerp.web_graph.Graph(this, this.model, domain, this.widget_config); + this.graph_widget.appendTo(this.$el); + this.ViewManager.on('switch_mode', this, function (e) { + if (e === 'graph') { + var group_bys = self.get_groupbys_from_searchview(); + this.graph_widget.set(domain, group_bys.group_by, group_bys.col_group_by); + } + }); + return; + } + + this.graph_widget.set(domain, group_by, col_group_by); + }, + + get_groupbys_from_searchview: function () { + var result = { group_by: [], col_group_by: []}, + searchdata = this.search_view.build_search_data(); + + _.each(searchdata.groupbys, function (data) { + data = (_.isString(data)) ? py.eval(data) : data; + result.group_by = result.group_by.concat(data.group_by); + if (data.col_group_by) { + result.col_group_by = result.col_group_by.concat(data.col_group_by); + } + }); + + if (result.col_group_by.length) { + return result; + } + _.each(searchdata.contexts, function (context) { + if (context.col_group_by) { + result.col_group_by = result.col_group_by.concat(context.col_group_by); + } + }); + return result; + }, + + do_show: function () { + this.do_push_state({}); + return this._super(); + }, + + // ---------------------------------------------------------------------- + // Search view integration + // ---------------------------------------------------------------------- + + // add groupby to the search view + register_groupby: function(row_groupby, col_groupby) { + var query = this.search_view.query; + + if (!_.has(this.search_view, '_s_groupby')) { return; } + + if (row_groupby.length && col_groupby.length) { + // when two changes to the search view will be done, the method do_search + // will be called twice, once with the correct groupby and incorrect col_groupby, + // and once with correct informations. This flag is necessary to prevent the + // incorrect informations to propagate and trigger useless queries + this.ignore_do_search = true; + } + + // add row groupbys + var row_facet = this.make_row_groupby_facets(row_groupby), + row_search_facet = query.findWhere({category:'GroupBy'}); + + if (row_search_facet) { + row_search_facet.values.reset(row_facet.values); + } else { + if (row_groupby.length) { + query.add(row_facet); + } + } + // add col groupbys + var col_facet = this.make_col_groupby_facets(col_groupby), + col_search_facet = query.findWhere({category:'ColGroupBy'}); + + if (col_search_facet) { + col_search_facet.values.reset(col_facet.values); + } else { + if (col_groupby.length) { + query.add(col_facet); + } + } + }, + + make_row_groupby_facets: function(groupbys) { + return { + category:'GroupBy', + values: this.make_groupby_values(groupbys, 'group_by'), + icon:'w', + field: this.search_view._s_groupby + }; + }, + + make_col_groupby_facets: function(groupbys) { + return { + category:'ColGroupBy', + values: this.make_groupby_values(groupbys, 'col_group_by'), + icon:'f', + field: this.col_search_field + }; + }, + + make_groupby_values: function (groupbys, category) { + return _.map(groupbys, function (groupby) { + var context = {}; + context[category] = groupby.field; + var value; + if (category === 'group_by' && groupby.type !== 'date' && groupby.type !== 'datetime') { + value = groupby.filter || {attrs: {domain: [], context: context}}; + } else { + value = {attrs: {domain: [], context: context}}; + } + return { + label: groupby.string, + value: value + }; + }); + }, +}); +}; + + + + + + + + diff --git a/addons/web_graph/static/src/js/graph_widget.js b/addons/web_graph/static/src/js/graph_widget.js new file mode 100644 index 00000000000..b4f06aee3f1 --- /dev/null +++ b/addons/web_graph/static/src/js/graph_widget.js @@ -0,0 +1,761 @@ + +/* jshint undef: false */ + +(function () { +'use strict'; +var QWeb = openerp.web.qweb; +var _lt = openerp.web._lt; +var _t = openerp.web._t; + +nv.dev = false; // sets nvd3 library in production mode + +openerp.web_graph.Graph = openerp.web.Widget.extend({ + template: 'GraphWidget', + + // ---------------------------------------------------------------------- + // Init stuff + // ---------------------------------------------------------------------- + init: function(parent, model, domain, options) { + this._super(parent); + this.model = model; + this.domain = domain; + this.mode = options.mode || 'pivot'; // pivot, bar, pie, line + this.heatmap_mode = options.heatmap_mode || 'none'; + this.visible_ui = options.visible_ui || true; + 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() { + var self = this; + this.table = $('<table>'); + this.$('.graph_main_content').append(this.table); + + var indexes = {'pivot': 0, 'bar': 1, 'line': 2, 'chart': 3}; + this.$('.graph_mode_selection label').eq(indexes[this.mode]).addClass('active'); + + if (this.mode !== 'pivot') { + this.$('.graph_heatmap label').addClass('disabled'); + } + + openerp.session.rpc('/web_graph/check_xlwt').then(function (result) { + self.$('.graph_options_selection label').toggle(result); + }); + + return this.model.call('fields_get', []).then(function (f) { + self.fields = f; + self.fields.__count = {field:'__count', type: 'integer', string:_t('Quantity')}; + self.important_fields = self.get_search_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 || []); + self.pivot_options.col_groupby = self.create_field_values(self.pivot_options.col_groupby || []); + self.pivot_options.measures = self.create_field_values(self.pivot_options.measures || [{field:'__count', type: 'integer', string:'Quantity'}]); + self.pivot = new openerp.web_graph.PivotTable(self.model, self.domain, self.fields, self.pivot_options); + self.pivot.update_data().then(function () { + self.display_data(); + if (self.graph_view) { + self.graph_view.register_groupby(self.pivot.rows.groupby, self.pivot.cols.groupby); + } + }); + openerp.web.bus.on('click', self, function () { + if (self.dropdown) { + self.dropdown.remove(); + self.dropdown = null; + } + }); + self.put_measure_checkmarks(); + }); + }, + + // 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 groupbygroups = _(search_view.inputs).select(function (g) { + return g instanceof openerp.web.search.GroupbyGroup; + }); + + var filters = _.flatten(_.pluck(groupbygroups, 'filters'), true), + 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 _.uniq(_.map(groupbys, function (groupby) { + var field = groupby.field, + filter = groupby.filter, + raw_field = field.split(':')[0], + string = (field === raw_field) ? filter.attrs.string : self.fields[raw_field].string; + + filter = (field === raw_field) ? filter : undefined; + + return { field: raw_field, string: string, filter: filter }; + }), false, function (filter) {return filter.field;}); + }, + + // 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')) { + return {field:id, type: f.type, string: f.string}; + } + })); + }, + + add_measures_to_options: function() { + this.$('.graph_measure_selection').append( + _.map(this.measure_list, function (measure) { + return $('<li>').append($('<a>').attr('data-choice', measure.field) + .attr('href', '#') + .text(measure.string)); + })); + }, + + // ---------------------------------------------------------------------- + // Configuration methods + // ---------------------------------------------------------------------- + set: function (domain, row_groupby, col_groupby) { + if (!this.pivot) { + this.pivot_options.domain = domain; + this.pivot_options.row_groupby = row_groupby; + this.pivot_options.col_groupby = col_groupby; + return; + } + var row_gbs = this.create_field_values(row_groupby), + col_gbs = this.create_field_values(col_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), + row_reduced = is_strict_beginning_of(row_gbs, this.pivot.rows.groupby), + col_reduced = is_strict_beginning_of(col_gbs, this.pivot.cols.groupby); + + if (!dom_changed && row_reduced && !col_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) { + this.pivot.fold_with_depth(this.pivot.cols, col_gbs.length); + this.display_data(); + return; + } + + if (!dom_changed && col_reduced && row_reduced) { + 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) { + this.pivot.set(domain, row_gbs, col_gbs).then(this.proxy('display_data')); + } + }, + + set_mode: function (mode) { + this.mode = mode; + + if (mode === 'pivot') { + this.$('.graph_heatmap label').removeClass('disabled'); + this.$('.graph_main_content').css('position', 'inherit') + } else { + this.$('.graph_heatmap label').addClass('disabled'); + this.$('.graph_main_content').css('position', 'relative') + } + this.display_data(); + }, + + set_heatmap_mode: function (mode) { // none, row, col, all + this.heatmap_mode = mode; + if (mode === 'none') { + this.$('.graph_heatmap label').removeClass('disabled'); + this.$('.graph_heatmap label').removeClass('active'); + } + this.display_data(); + }, + + create_field_value: function (f) { + var field = (_.contains(f, ':')) ? f.split(':')[0] : f, + groupby_field = _.findWhere(this.important_fields, {field:field}), + string = groupby_field ? groupby_field.string : this.fields[field].string, + result = {field: f, string: string, type: this.fields[field].type }; + + if (groupby_field) { + result.filter = groupby_field.filter; + } + + return result; + }, + + create_field_values: function (field_ids) { + return _.map(field_ids, this.proxy('create_field_value')); + }, + + + get_col_groupbys: function () { + return _.pluck(this.pivot.cols.groupby, 'field'); + }, + + // ---------------------------------------------------------------------- + // UI code + // ---------------------------------------------------------------------- + events: { + 'click .graph_mode_selection label' : 'mode_selection', + 'click .graph_measure_selection li' : 'measure_selection', + 'click .graph_options_selection label' : 'option_selection', + 'click .graph_heatmap label' : 'heatmap_mode_selection', + 'click .web_graph_click' : 'header_cell_clicked', + 'click a.field-selection' : 'field_selection', + }, + + mode_selection: function (event) { + event.preventDefault(); + var mode = event.currentTarget.getAttribute('data-mode'); + this.set_mode(mode); + }, + + measure_selection: function (event) { + event.preventDefault(); + event.stopPropagation(); + var measure_field = event.target.getAttribute('data-choice'); + var measure = { + field: measure_field, + type: this.fields[measure_field].type, + string: this.fields[measure_field].string + }; + + this.pivot.toggle_measure(measure).then(this.proxy('display_data')); + this.put_measure_checkmarks(); + }, + + put_measure_checkmarks: function () { + var self = this, + measures_li = this.$('.graph_measure_selection a'); + measures_li.removeClass('oe_selected'); + _.each(this.measure_list, function (measure, index) { + if (_.findWhere(self.pivot.measures, measure)) { + measures_li.eq(index).addClass('oe_selected'); + } + }); + + }, + + 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 'update_values': + this.pivot.update_data().then(this.proxy('display_data')); + break; + case 'export_data': + this.export_xls(); + break; + } + }, + + heatmap_mode_selection: function (event) { + event.preventDefault(); + var mode = event.currentTarget.getAttribute('data-mode'); + if (this.heatmap_mode === mode) { + event.stopPropagation(); + this.set_heatmap_mode('none'); + } else { + this.set_heatmap_mode(mode); + } + }, + + header_cell_clicked: function (event) { + event.preventDefault(); + event.stopPropagation(); + var id = event.target.getAttribute('data-id'), + header = this.pivot.get_header(id), + self = this; + + if (header.expanded) { + this.fold(header); + return; + } + if (header.path.length < header.root.groupby.length) { + this.expand(id); + return; + } + if (!this.important_fields.length) { + return; + } + + var fields = _.map(this.important_fields, function (field) { + return {id: field.field, value: field.string, type:self.fields[field.field.split(':')[0]].type}; + }); + if (this.dropdown) { + this.dropdown.remove(); + } + 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(); + + + }, + + field_selection: function (event) { + var id = event.target.getAttribute('data-id'), + field_id = event.target.getAttribute('data-field-id'), + interval, + groupby = this.create_field_value(field_id); + event.preventDefault(); + if (this.fields[field_id].type === 'date' || this.fields[field_id].type === 'datetime') { + interval = event.target.getAttribute('data-interval'); + groupby.field = groupby.field + ':' + interval; + } + this.expand(id, groupby); + }, + + // ---------------------------------------------------------------------- + // Pivot Table integration + // ---------------------------------------------------------------------- + expand: function (header_id, groupby) { + var self = this, + header = this.pivot.get_header(header_id), + update_groupby = !!groupby; + + groupby = groupby || header.root.groupby[header.path.length]; + + this.pivot.expand(header_id, groupby).then(function () { + 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); + + this.display_data(); + if (update_groupby && this.graph_view) { + this.graph_view.register_groupby(this.pivot.rows.groupby, this.pivot.cols.groupby); + } + }, + + swap_axis: function () { + this.pivot.swap_axis(); + this.display_data(); + this.graph_view.register_groupby(this.pivot.rows.groupby, this.pivot.cols.groupby); + }, + + // ---------------------------------------------------------------------- + // Convert Pivot data structure into table structure : + // compute rows, cols, colors, cell width, cell height, ... + // ---------------------------------------------------------------------- + build_table: function() { + return { + headers: this.build_headers(), + measure_row: this.build_measure_row(), + rows: this.build_rows(), + 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;})), + rows = []; + + _.each(pivot.cols.headers, function (col) { + if (col.path.length === 0) { return;} + var cell_width = nbr_measures * (col.expanded ? pivot.get_ancestor_leaves(col).length : 1), + cell_height = col.expanded ? 1 : height - col.path.length + 1, + cell = {width: cell_width, height: cell_height, title: col.title, id: col.id, expanded: col.expanded}; + if (rows[col.path.length - 1]) { + rows[col.path.length - 1].push(cell); + } else { + rows[col.path.length - 1] = [cell]; + } + }); + + if (pivot.get_cols_leaves().length > 1) { + rows[0].push({width: nbr_measures, height: height, title: _t('Total'), 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) { + var formatted_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 () { + var self = this, + pivot = this.pivot, + m, cell; + + return _.map(pivot.rows.headers, function (row) { + var cells = []; + _.each(pivot.get_cols_leaves(), function (col) { + var values = pivot.get_values(row.id,col.id); + for (m = 0; m < pivot.measures.length; m++) { + cells.push(self.make_cell(row,col,values[m], m)); + } + }); + if (pivot.get_cols_leaves().length > 1) { + var totals = pivot.get_total(row); + for (m = 0; m < pivot.measures.length; m++) { + cell = self.make_cell(row, pivot.main_col(), totals[m], m); + cell.is_bold = 'true'; + cells.push(cell); + } + } + return { + id: row.id, + indent: row.path.length, + title: row.title, + expanded: row.expanded, + cells: cells, + }; + }); + }, + + // ---------------------------------------------------------------------- + // Main display method + // ---------------------------------------------------------------------- + display_data: function () { + this.$('.graph_main_content svg').remove(); + this.$('.graph_main_content div').remove(); + this.table.empty(); + this.table.toggleClass('heatmap', this.heatmap_mode !== 'none'); + this.width = this.$el.width(); + this.height = Math.min(Math.max(document.documentElement.clientHeight - 116 - 60, 250), Math.round(0.8*this.$el.width())); + + this.$('.graph_header').toggle(this.visible_ui); + if (this.pivot.no_data) { + this.$('.graph_main_content').append($(QWeb.render('graph_no_data'))); + } else { + if (this.mode === 'pivot') { + this.draw_table(); + } else { + this.$('.graph_main_content').append($('<div><svg>')); + this.svg = this.$('.graph_main_content svg')[0]; + this[this.mode](); + } + } + }, + + // ---------------------------------------------------------------------- + // Drawing the table + // ---------------------------------------------------------------------- + draw_table: function () { + var table = this.build_table(); + this.draw_headers(table.headers); + this.draw_measure_row(table.measure_row); + this.draw_rows(table.rows); + }, + + make_header_cell: function (header) { + var cell = (_.has(header, 'cells') ? $('<td>') : $('<th>')) + .addClass('graph_border') + .attr('rowspan', header.height) + .attr('colspan', header.width); + var content = $('<span>').addClass('web_graph_click') + .attr('href','#') + .text(' ' + (header.title || _t('Undefined'))) + .attr('data-id', header.id); + if (_.has(header, 'expanded')) { + content.addClass(header.expanded ? 'fa fa-minus-square' : 'fa fa-plus-square'); + } else { + content.css('font-weight', 'bold'); + } + if (_.has(header, 'indent')) { + for (var i = 0; i < header.indent; i++) { cell.prepend($('<span>', {class:'web_graph_indent'})); } + } + return cell.append(content); + }, + + draw_headers: function (headers) { + var make_cell = this.make_header_cell, + empty_cell = $('<th>').attr('rowspan', headers.length), + thead = $('<thead>'); + + _.each(headers, function (row) { + var html_row = $('<tr>'); + _.each(row, function (header) { + html_row.append(make_cell(header)); + }); + thead.append(html_row); + }); + thead.children(':first').prepend(empty_cell); + this.table.append(thead); + }, + + draw_measure_row: function (measure_row) { + if (this.pivot.measures.length === 1) { return; } + var html_row = $('<tr>').append('<th>'); + _.each(measure_row, function (cell) { + var measure_cell = $('<th>').addClass('measure_row').text(cell.text); + if (cell.is_bold) {measure_cell.css('font-weight', 'bold');} + html_row.append(measure_cell); + }); + this.$('thead').append(html_row); + }, + + draw_rows: function (rows) { + var table = this.table, + make_cell = this.make_header_cell; + + _.each(rows, function (row) { + var html_row = $('<tr>').append(make_cell(row)); + _.each(row.cells, function (cell) { + var html_cell = $('<td>').text(cell.value); + if (_.has(cell, 'color')) { + html_cell.css('background-color', $.Color(255, cell.color, cell.color)); + } + if (cell.is_bold) { html_cell.css('font-weight', 'bold'); } + html_row.append(html_cell); + }); + table.append(html_row); + }); + }, + + // ---------------------------------------------------------------------- + // Drawing charts code + // ---------------------------------------------------------------------- + bar: function () { + var self = this, + dim_x = this.pivot.rows.groupby.length, + dim_y = this.pivot.cols.groupby.length, + show_controls = (this.width > 400 && this.height > 300 && dim_x + dim_y >=2), + data; + + // No groupby + if ((dim_x === 0) && (dim_y === 0)) { + data = [{key: _t('Total'), values:[{ + x: _t('Total'), + y: this.pivot.get_total(), + }]}]; + // Only column groupbys + } else if ((dim_x === 0) && (dim_y >= 1)){ + data = _.map(this.pivot.get_cols_with_depth(1), function (header) { + return { + key: header.title, + values: [{x:header.title, y: self.pivot.get_total(header)[0]}] + }; + }); + // 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), + title = (pt.title !== undefined) ? pt.title : _t('Undefined'); + return {x: title, y: value}; + }); + data = [{key: self.pivot.measures[0].string, values:data}]; + // 1 row groupby and some col groupbys + } else if ((dim_x === 1) && (dim_y >= 1)) { + data = _.map(this.pivot.get_cols_with_depth(1), function (colhdr) { + var values = _.map(self.pivot.get_rows_with_depth(1), function (header) { + return { + x: header.title || _t('Undefined'), + y: self.pivot.get_values(header.id, colhdr.id)[0] || 0 + }; + }); + return {key: colhdr.title || _t('Undefined'), values: values}; + }); + // At least two row groupby + } else { + var keys = _.uniq(_.map(this.pivot.get_rows_with_depth(2), function (hdr) { + return hdr.title || _t('Undefined'); + })); + data = _.map(keys, function (key) { + var values = _.map(self.pivot.get_rows_with_depth(1), function (hdr) { + var subhdr = _.find(hdr.children, function (child) { + return ((child.title === key) || ((child.title === undefined) && (key === _t('Undefined')))); + }); + return { + x: hdr.title || _t('Undefined'), + y: (subhdr) ? self.pivot.get_total(subhdr)[0] : 0 + }; + }); + return {key:key, values: values}; + }); + } + + nv.addGraph(function () { + var chart = nv.models.multiBarChart() + .width(self.width) + .height(self.height) + .reduceXTicks(false) + .stacked(self.bar_ui === 'stack') + .showControls(show_controls); + + if (self.width / data[0].values.length < 80) { + chart.rotateLabels(-15); + chart.reduceXTicks(true); + chart.margin({bottom:40}); + } + + d3.select(self.svg) + .datum(data) + .attr('width', self.width) + .attr('height', self.height) + .call(chart); + + nv.utils.windowResize(chart.update); + return chart; + }); + + }, + + line: function () { + var self = this, + dim_x = this.pivot.rows.groupby.length, + dim_y = this.pivot.cols.groupby.length; + + 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 title = _.map(col.path, function (p) { + return p || _t('Undefined'); + }).join('/'); + if (dim_y === 0) { + title = self.pivot.measures[0].string; + } + return {values: values, key: title}; + }); + + 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}); + + d3.select(self.svg) + .attr('width', self.width) + .attr('height', self.height) + .datum(data) + .call(chart); + + return chart; + }); + }, + + pie: function () { + var self = this, + dim_x = this.pivot.rows.groupby.length; + var data = _.map(this.pivot.get_rows_leaves(), function (row) { + var title = _.map(row.path, function (p) { + return p || _t('Undefined'); + }).join('/'); + if (dim_x === 0) { + title = self.measure_label; + } + return {x: title, y: self.pivot.get_total(row)}; + }); + + nv.addGraph(function () { + var chart = nv.models.pieChart() + .color(d3.scale.category10().range()) + .width(self.width) + .height(self.height); + + d3.select(self.svg) + .datum(data) + .transition().duration(1200) + .attr('width', self.width) + .attr('height', self.height) + .call(chart); + + nv.utils.windowResize(chart.update); + return chart; + }); + }, + + // ---------------------------------------------------------------------- + // 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())}, + complete: openerp.web.unblockUI, + error: c.rpc_error.bind(c) + }); + }, + +}); + +// Utility function: returns true if the beginning of array2 is array1 and +// if array1 is not array2 +function is_strict_beginning_of (array1, array2) { + if (array1.length >= array2.length) { return false; } + var result = true; + for (var i = 0; i < array1.length; i++) { + if (!_.isEqual(array1[i], array2[i])) { return false;} + } + return result; +} +})(); diff --git a/addons/web_graph/static/src/js/pivot_table.js b/addons/web_graph/static/src/js/pivot_table.js new file mode 100644 index 00000000000..f77d49f9a1a --- /dev/null +++ b/addons/web_graph/static/src/js/pivot_table.js @@ -0,0 +1,469 @@ + +/* jshint undef: false */ + +(function () { +'use strict'; + +var _lt = openerp.web._lt; +var _t = openerp.web._t; + +// PivotTable requires a call to update_data after initialization +openerp.web_graph.PivotTable = openerp.web.Class.extend({ + + init: function (model, domain, fields, options) { + this.cells = []; + this.domain = domain; + this.no_data = true; + this.model = model; + this.fields = fields; + this.fields.__count = {type: 'integer', string:_t('Quantity')}; + this.measures = options.measures || []; + this.rows = { groupby: options.row_groupby, headers: null }; + this.cols = { groupby: options.col_groupby, headers: null }; + }, + + // ---------------------------------------------------------------------- + // Configuration methods + // ---------------------------------------------------------------------- + // this.measures: list of measure [measure], measure = {field: _, string: _, type: _} + // this.rows.groupby, this.cols.groupby : list of groupbys used for describing rows (...), + // a groupby is also {field:_, string:_, type:_} + // If its type is date/datetime, field can have the corresponding interval in its description, + // for example 'create_date:week'. + set_measures: function (measures) { + this.measures = measures; + return this.update_data(); + }, + + toggle_measure: function (measure) { + var current_measure = _.findWhere(this.measures, measure); + if (current_measure) { // remove current_measure + var index = this.measures.indexOf(current_measure); + this.measures = _.without(this.measures, current_measure); + if (this.measures.length === 0) { + this.no_data = true; + } else { + _.each(this.cells, function (cell) { + cell.values.splice(index, 1); + }); + } + return $.Deferred().resolve(); + } else { // add a new measure + this.measures.push(measure); + return this.update_data(); + } + }, + + set: function (domain, row_groupby, col_groupby) { + var row_gb_changed = !_.isEqual(row_groupby, this.rows.groupby), + col_gb_changed = !_.isEqual(col_groupby, this.cols.groupby); + + this.domain = domain; + this.rows.groupby = row_groupby; + this.cols.groupby = col_groupby; + + if (row_gb_changed) { this.rows.headers = null; } + if (col_gb_changed) { this.cols.headers = null; } + + return this.update_data(); + }, + + // ---------------------------------------------------------------------- + // Cells manipulation methods + // ---------------------------------------------------------------------- + // cells are objects {x:_, y:_, values:_} where x < y and values is an array + // of values (one for each measure). The condition x < y might look + // unnecessary, but it makes the rest of the code simpler: headers + // don't krow if they are rows or cols, they just know their id, so + // it is useful that a call get_values(id1, id2) is the same as get_values(id2, id1) + add_cell : function (id1, id2, values) { + this.cells.push({x: Math.min(id1, id2), y: Math.max(id1, id2), values: values}); + }, + + get_values: function (id1, id2, default_values) { + var cell = _.findWhere(this.cells, {x: Math.min(id1, id2), y: Math.max(id1, id2)}); + return (cell !== undefined) ? cell.values : (default_values || new Array(this.measures.length)); + }, + + // ---------------------------------------------------------------------- + // Headers/Rows/Cols manipulation methods + // ---------------------------------------------------------------------- + // this.rows.headers, this.cols.headers = [header] describe the tree structure + // of rows/cols. Headers are objects + // { + // id:_, (unique id obviously) + // path: [...], (array of all parents title, with its own title at the end) + // title:_, (name of the row/col) + // children:[_], (subrows or sub cols of this row/col) + // domain:_, (domain of data in this row/col) + // root:_ (ref to this.rows or this.cols corresponding to the header) + // expanded:_ (boolean, true if it has been expanded) + // } + is_row: function (id) { + return !!_.findWhere(this.rows.headers, {id:id}); + }, + + is_col: function (id) { + return !!_.findWhere(this.cols.headers, {id:id}); + }, + + get_header: function (id) { + return _.findWhere(this.rows.headers.concat(this.cols.headers), {id:id}); + }, + + _get_headers_with_depth: function (headers, depth) { + return _.filter(headers, function (header) { + return header.path.length === depth; + }); + }, + + // return all columns with a path length of 'depth' + get_cols_with_depth: function (depth) { + return this._get_headers_with_depth(this.cols.headers, depth); + }, + + // return all rows with a path length of 'depth' + get_rows_with_depth: function (depth) { + return this._get_headers_with_depth(this.rows.headers, depth); + }, + + get_ancestor_leaves: function (header) { + return _.where(this.get_ancestors_and_self(header), {expanded:false}); + }, + + // return all non expanded rows + get_rows_leaves: function () { + return _.where(this.rows.headers, {expanded:false}); + }, + + // return all non expanded cols + get_cols_leaves: function () { + return _.where(this.cols.headers, {expanded:false}); + }, + + get_ancestors: function (header) { + var self = this; + if (!header.children) return []; + return [].concat.apply([], _.map(header.children, function (c) {return self.get_ancestors_and_self(c); })); + }, + + get_ancestors_and_self: function (header) { + var self = this; + return [].concat.apply([header], _.map(header.children, function (c) { return self.get_ancestors_and_self(c); })); + }, + + get_total: function (header) { + return (header) ? this.get_values(header.id, this.get_other_root(header).headers[0].id) + : this.get_values(this.rows.headers[0].id, this.cols.headers[0].id); + }, + + get_other_root: function (header) { + return (header.root === this.rows) ? this.cols : this.rows; + }, + + main_row: function () { return this.rows.headers[0]; }, + + main_col: function () { return this.cols.headers[0]; }, + + // ---------------------------------------------------------------------- + // Table manipulation methods : fold/expand/swap + // ---------------------------------------------------------------------- + // return true if the folding changed the groupbys, false if not + fold: function (header) { + var ancestors = this.get_ancestors(header), + removed_ids = _.pluck(ancestors, 'id'); + + header.root.headers = _.difference(header.root.headers, ancestors); + header.children = []; + header.expanded = false; + + this.cells = _.reject(this.cells, function (cell) { + return (_.contains(removed_ids, cell.x) || _.contains(removed_ids, cell.y)); + }); + + var new_groupby_length = _.max(_.pluck(_.pluck(header.root.headers, 'path'), 'length')); + if (new_groupby_length < header.root.groupby.length) { + header.root.groupby.splice(new_groupby_length); + return true; + } + return false; + }, + + fold_with_depth: function (root, depth) { + var self = this; + _.each(this._get_headers_with_depth(root.headers, depth), function (header) { + self.fold(header); + }); + }, + + expand_all: function () { + this.rows.headers = null; + this.cols.headers = null; + return this.update_data(); + }, + + expand: function (header_id, groupby) { + var self = this, + header = this.get_header(header_id), + otherRoot = this.get_other_root(header), + fields = otherRoot.groupby.concat(this.measures); + + if (header.path.length === header.root.groupby.length) { + header.root.groupby.push(groupby); + } + groupby = [groupby].concat(otherRoot.groupby); + + return this.get_groups(groupby, fields, header.domain).then(function (groups) { + _.each(groups.reverse(), function (group) { + // make header + var child = self.make_header(group, header); + child.expanded = false; + header.children.splice(0,0, child); + header.root.headers.splice(header.root.headers.indexOf(header) + 1, 0, child); + // make cells + _.each(self.get_ancestors_and_self(group), function (data) { + var values = _.map(self.measures, function (m) { + return data.attributes.aggregates[m.field]; + }); + var other = _.find(otherRoot.headers, function (h) { + if (header.root === self.cols) { + return _.isEqual(data.path.slice(1), h.path); + } else { + return _.isEqual(_.rest(data.path), h.path); + } + }); + if (other) { + self.add_cell(child.id, other.id, values); + } + }); + }); + header.expanded = true; + }); + }, + + make_header: function (group, parent) { + var title = parent ? group.attributes.value : _t('Total'); + return { + id: _.uniqueId(), + path: parent ? parent.path.concat(title) : [], + title: title, + children: [], + domain: parent ? group.model._domain : this.domain, + root: parent ? parent.root : undefined, + }; + }, + + swap_axis: function () { + var temp = this.rows; + this.rows = this.cols; + this.cols = temp; + }, + + // ---------------------------------------------------------------------- + // Data updating methods + // ---------------------------------------------------------------------- + // Load the data from the db, using the method this.load_data + // update_data will try to preserve the expand/not expanded status of each + // column/row. If you want to expand all, then set this.cols.headers/this.rows.headers + // to null before calling update_data. + update_data: function () { + var self = this; + + return this.load_data().then (function (result) { + if (result) { + self.no_data = false; + self[self.cols.headers ? 'update_headers' : 'expand_headers'](self.cols, result.col_headers); + self[self.rows.headers ? 'update_headers' : 'expand_headers'](self.rows, result.row_headers); + } else { + self.no_data = true; + } + }); + }, + + expand_headers: function (root, new_headers) { + root.headers = new_headers; + _.each(root.headers, function (header) { + header.root = root; + header.expanded = (header.children.length > 0); + }); + }, + + update_headers: function (root, new_headers) { + _.each(root.headers, function (header) { + var corresponding_header = _.find(new_headers, function (h) { + return _.isEqual(h.path, header.path); + }); + if (corresponding_header && header.expanded) { + corresponding_header.expanded = true; + _.each(corresponding_header.children, function (c) { + c.expanded = false; + }); + } + if (corresponding_header && (!header.expanded)) { + corresponding_header.expanded = false; + } + }); + var updated_headers = _.filter(new_headers, function (header) { + return (header.expanded !== undefined); + }); + _.each(updated_headers, function (header) { + if (!header.expanded) { + header.children = []; + } + header.root = root; + }); + root.headers = updated_headers; + }, + + // ---------------------------------------------------------------------- + // Data loading methods + // ---------------------------------------------------------------------- + + // To obtain all the values required to draw the full table, we have to do + // at least 2 + min(row.groupby.length, col.groupby.length) + // calls to readgroup. To simplify the code, we will always do + // 2 + row.groupby.length calls. For example, if row.groupby = [r1, r2, r3] + // and col.groupby = [c1, c2], then we will make the call with the following + // groupbys: [r1,r2,r3], [c1,r1,r2,r3], [c1,c2,r1,r2,r3], []. + load_data: function () { + var self = this, + cols = this.cols.groupby, + rows = this.rows.groupby, + visible_fields = rows.concat(cols, self.measures); + + if (this.measures.length === 0) { + return $.Deferred.resolve().promise(); + } + + var groupbys = _.map(_.range(cols.length + 1), function (i) { + return cols.slice(0, i).concat(rows); + }); + groupbys.push([]); + + var get_data_requests = _.map(groupbys, function (groupby) { + return self.get_groups(groupby, visible_fields, self.domain); + }); + + return $.when.apply(null, get_data_requests).then(function () { + var data = Array.prototype.slice.call(arguments), + row_data = data[0], + col_data = (cols.length !== 0) ? data[data.length - 2] : [], + has_data = data[data.length - 1][0]; + + return has_data && self.format_data(col_data, row_data, data); + }); + }, + + get_groups: function (groupbys, fields, domain, path) { + var self = this, + groupby = (groupbys.length) ? groupbys[0] : []; + path = path || []; + + return this._query_db(groupby, fields, domain, path).then(function (groups) { + if (groupbys.length > 1) { + var get_subgroups = $.when.apply(null, _.map(groups, function (group) { + return self.get_groups(_.rest(groupbys), fields, group.model._domain, path.concat(group.attributes.value)).then(function (subgroups) { + group.children = subgroups; + }); + })); + return get_subgroups.then(function () { + return groups; + }); + } else { + return groups; + } + }); + + }, + + _query_db: function (groupby, fields, domain, path) { + var self = this, + field_ids = _.without(_.pluck(fields, 'field'), '__count'), + fields = _.map(field_ids, function(f) { return self.raw_field(f); }); + + return this.model.query(field_ids) + .filter(domain) + .group_by(groupby.field) + .then(function (results) { + var groups = _.filter(results, function (group) { + return group.attributes.length > 0; + }); + return _.map(groups, function (g) { return self.format_group(g, path); }); + }); + }, + + // if field is a fieldname, returns field, if field is field_id:interval, retuns field_id + raw_field: function (field) { + return field.split(':')[0]; + }, + + // add the path to the group and sanitize the value... + format_group: function (group, current_path) { + var attrs = group.attributes, + value = attrs.value, + grouped_on = attrs.grouped_on ? this.raw_field(attrs.grouped_on) : false; + + if (value === false) { + group.attributes.value = _t('Undefined'); + } else if (grouped_on && this.fields[grouped_on].type === 'selection') { + var selection = this.fields[grouped_on].selection, + value_lookup = _.where(selection, {0:value}); + group.attributes.value = value_lookup ? value_lookup[0][1] : _t('Undefined'); + } else if (value instanceof Array) { + group.attributes.value = value[1]; + } + + group.path = (value !== undefined) ? (current_path || []).concat(group.attributes.value) : []; + group.attributes.aggregates.__count = group.attributes.length; + + return group; + }, + + format_data: function (col_data, row_data, cell_data) { + var self = this, + dim_row = this.rows.groupby.length, + dim_col = this.cols.groupby.length, + col_headers = this.get_ancestors_and_self(this.make_headers(col_data, dim_col)), + row_headers = this.get_ancestors_and_self(this.make_headers(row_data, dim_row)); + + this.cells = []; + _.each(cell_data, function (data, index) { + self.make_cells(data, index, [], row_headers, col_headers); + }); // not pretty. make it more functional? + + return {col_headers: col_headers, row_headers: row_headers}; + }, + + make_headers: function (data, depth, parent) { + var self = this, + main = this.make_header(data, parent); + + if (main.path.length < depth) { + main.children = _.map(data.children || data, function (data_pt) { + return self.make_headers (data_pt, depth, main); + }); + } + return main; + }, + + make_cells: function (data, index, current_path, rows, cols) { + var self = this; + _.each(data, function (group) { + var attr = group.attributes, + path = attr.grouped_on ? current_path.concat(attr.value) : current_path, + values = _.map(self.measures, function (measure) { return attr.aggregates[measure.field]; }), + row = _.find(rows, function (header) { return _.isEqual(header.path, path.slice(index)); }), + col = _.find(cols, function (header) { return _.isEqual(header.path, path.slice(0, index)); }); + + self.add_cell(row.id, col.id, values); + if (group.children) { + self.make_cells (group.children, index, path, rows, cols); + } + }); + }, + +}); + +})(); + + |
