summaryrefslogtreecommitdiff
path: root/addons/web_graph/static/src/js/pivot_table.js
diff options
context:
space:
mode:
authorGery Debongnie <ged@openerp.com>2014-01-14 13:51:42 +0100
committerGery Debongnie <ged@openerp.com>2014-01-14 13:51:42 +0100
commit11de0f36ed923f0222d8e1ecc603b53366eabf75 (patch)
treef1498ef9a4b530a33f538bf05b0cc834dad774f9 /addons/web_graph/static/src/js/pivot_table.js
parentde85a1cefb2d62aae5a4a29295dcf36b6c545ad6 (diff)
[REF] separates graph view and graph widget into two files, and renames pivot into pivot_table.js (addon web_graph)
bzr revid: ged@openerp.com-20140114125142-9qj3mvu0a1fp4sis
Diffstat (limited to 'addons/web_graph/static/src/js/pivot_table.js')
-rw-r--r--addons/web_graph/static/src/js/pivot_table.js539
1 files changed, 539 insertions, 0 deletions
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..9c927150f20
--- /dev/null
+++ b/addons/web_graph/static/src/js/pivot_table.js
@@ -0,0 +1,539 @@
+
+/* jshint undef: false */
+
+(function () {
+'use strict';
+
+// Pivot Table emits the events 'groupby_changed' and 'redraw_required' when necessary.
+// PivotTable is initialized by default 'inactive', and require a call to activate()
+// after init to load initial data and start triggering events.
+openerp.web_graph.PivotTable = openerp.web.Class.extend(openerp.EventDispatcherMixin, {
+
+ init: function (model, domain, fields, options) {
+ openerp.EventDispatcherMixin.init.call(this);
+ this.cells = [];
+ this.domain = domain;
+ this.no_data = true;
+ this.model = model;
+ this.fields = fields;
+ this.fields.__count = {type: 'integer', string:'Quantity'};
+ this.measures = [{field:'__count', type: this.fields.type, string:this.fields.string}];
+ this.active = false;
+ this.rows = { groupby: this.create_field_values(options.row_groupby || []), headers: null };
+ this.cols = { groupby: this.create_field_values(options.col_groupby || []), headers: null };
+ if (options.measures) { this.set_measures(options.measures); }
+ },
+
+ // ----------------------------------------------------------------------
+ // 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:_} but it also has a interval
+ // attribute if its type is date/datetime.
+ // this.active: If PivotTable is active then it triggers events and updates
+ // its values when necessary
+ activate: function() {
+ this.active = true;
+ this.update_data();
+ },
+
+ set_measures: function (measures) {
+ var self = this;
+ if (!_.isEqual(measures, this.measures)) {
+ this.measures = [];
+ _.each(measures, function (m) { self._add_measure(m); });
+ if (this.active) { this.update_data(); }
+ }
+ },
+
+ _add_measure: function (measure) {
+ if (measure.field && measure.string && measure.type) {
+ this.measures.push(measure);
+ } else {
+ this.measures.push({
+ field: measure,
+ string: this.fields[measure].string,
+ type: this.fields[measure].type,
+ });
+ }
+ },
+
+ toggle_measure: function (field_id) {
+ var current_measure = _.findWhere(this.measures, {field:field_id});
+ if (current_measure) {
+ this.measures = _.without(this.measures, current_measure);
+ } else {
+ this.measures.push({
+ field: field_id,
+ type: this.fields[field_id].type,
+ string: this.fields[field_id].string
+ });
+ }
+ if (this.active) { this.update_data(); }
+ },
+
+ set: function (domain, row_groupby, col_groupby) {
+ var row_gbs = this.create_field_values(row_groupby),
+ col_gbs = this.create_field_values(col_groupby),
+ dom_changed = !_.isEqual(this.domain, domain),
+ gb_changed = !(_.isEqual(row_gbs, this.rows.groupby) &&
+ _.isEqual(col_gbs, this.cols.groupby));
+
+ if (dom_changed) {
+ this.domain = domain;
+ }
+ if (gb_changed) {
+ this.cols.groupby = col_gbs;
+ this.rows.groupby = row_gbs;
+ if (this.active) { this.trigger('groupby_changed'); }
+ }
+ if (this.active && (gb_changed || dom_changed)) {
+ this.update_data();
+ }
+ },
+
+ create_field_value: function (f) {
+ if (f.field && f.interval) {
+ return { field:f.field,
+ string: this.fields[f.field].string,
+ type:this.fields[f.field].type,
+ interval: f.interval };
+ }
+ return (f.field && f.string && f.type) ? f : { field: f,
+ string: this.fields[f].string,
+ type: this.fields[f].type };
+ },
+
+ create_field_values: function (field_ids) {
+ var self = this;
+ return _.map(field_ids, function (f) { return self.create_field_value(f); });
+ },
+
+ _add_groupby: function(groupby_list, groupby) {
+ groupby_list.push(this.create_field_value(groupby));
+ },
+
+ // ----------------------------------------------------------------------
+ // 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 = _.find(this.cells, function (c) {
+ return ((c.x == Math.min(id1, id2)) && (c.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 !!_.find(this.rows.headers, function (header) {
+ return header.id === id;
+ });
+ },
+
+ is_col: function (id) {
+ return !!_.find(this.cols.headers, function (header) {
+ return header.id === id;
+ });
+ },
+
+ get_header: function (id) {
+ return _.find(this.rows.headers.concat(this.cols.headers), function (header) {
+ return header.id === id;
+ });
+ },
+
+ // return all columns with a path length of 'depth'
+ get_cols_with_depth: function (depth) {
+ return _.filter(this.cols.headers, function (header) {
+ return header.path.length === depth;
+ });
+ },
+
+ // return all rows with a path length of 'depth'
+ get_rows_with_depth: function (depth) {
+ return _.filter(this.rows.headers, function (header) {
+ return header.path.length === depth;
+ });
+ },
+
+ // return all non expanded rows
+ get_rows_leaves: function () {
+ return _.filter(this.rows.headers, function (header) {
+ return !header.expanded;
+ });
+ },
+
+ // return all non expanded cols
+ get_cols_leaves: function () {
+ return _.filter(this.cols.headers, function (header) {
+ return !header.expanded;
+ });
+ },
+
+ 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) {
+ if (header) {
+ return this.get_values(header.id, this.get_other_root(header).headers[0].id);
+ }
+ return 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
+ // ----------------------------------------------------------------------
+ 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(_.map(header.root.headers, function(g) {return g.path.length;}));
+ if (new_groupby_length < header.root.groupby.length) {
+ header.root.groupby.splice(new_groupby_length);
+ this.trigger('groupby_changed');
+ }
+ this.trigger('redraw_required');
+ },
+
+ 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) {
+ self._add_groupby(header.root.groupby, groupby);
+ this.trigger('groupby_changed');
+ }
+ groupby = [header.root.groupby[header.path.length]].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;
+ self.trigger('redraw_required');
+ });
+ },
+
+ make_header: function (group, parent) {
+ var title = parent ? group.attributes.value : '';
+ 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;
+ this.trigger('groupby_changed');
+ this.trigger('redraw_required');
+ },
+
+ // ----------------------------------------------------------------------
+ // 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;
+ }
+ self.trigger('redraw_required');
+ });
+ },
+
+ 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;
+ },
+
+ expand_all: function () {
+ this.rows.headers = null;
+ this.cols.headers = null;
+ this.update_data();
+ },
+
+ // ----------------------------------------------------------------------
+ // 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);
+
+ var groupbys = _.map(_.range(cols.length + 1), function (i) {
+ return cols.slice(0, i).concat(rows);
+ });
+ groupbys.push([]);
+
+ var def_array = _.map(groupbys, function (groupby) {
+ return self.get_groups(groupby, visible_fields, self.domain);
+ });
+
+ return $.when.apply(null, def_array).then(function () {
+ var data = Array.prototype.slice.call(arguments),
+ row_data = data[0],
+ col_data = (cols.length !== 0) ? data[data.length - 2] : [],
+ total = data[data.length - 1][0];
+
+ return (total === undefined) ? undefined
+ : self.format_data(total, col_data, row_data, data);
+ });
+ },
+
+ get_groups: function (groupbys, fields, domain, path) {
+ path = path || [];
+ var self = this,
+ groupby = (groupbys.length) ? groupbys[0] : [];
+
+ 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'),
+ context = {};
+
+ if (groupby.interval) {
+ context.datetime_format = {};
+ var display_format;
+ switch (groupby.interval) {
+ case 'day':
+ display_format = 'dd MMMM YYYY';
+ break;
+ case 'week':
+ display_format = 'w YYYY';
+ break;
+ case 'month':
+ display_format = 'MMMM YYYY';
+ break;
+ case 'quarter':
+ display_format = 'QQQ YYYY';
+ break;
+ case 'year':
+ display_format = 'YYYY';
+ break;
+ }
+ context.datetime_format[groupby.field] = {
+ interval: groupby.interval,
+ display_format: display_format
+ };
+ }
+ return this.model.query(field_ids)
+ .filter(domain)
+ .context(context)
+ .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); });
+ });
+ },
+
+ // add the path to the group and sanitize the value...
+ format_group: function (group, current_path) {
+ var value = group.attributes.value;
+
+ if (group.attributes.grouped_on && this.fields[group.attributes.grouped_on].type === 'selection') {
+ var selection = this.fields[group.attributes.grouped_on].selection,
+ value_lookup = _.find(selection, function (val) { return val[0] === value; });
+ group.attributes.value = value_lookup ? value_lookup[1] : 'undefined';
+ } else if (value === false) {
+ group.attributes.value = 'undefined';
+ } else if (value instanceof Array) {
+ group.attributes.value = value[1];
+ }
+
+ group.path = value ? (current_path || []).concat(group.attributes.value) : [];
+ group.attributes.aggregates.__count = group.attributes.length;
+
+ return group;
+ },
+
+ format_data: function (total, 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 (measure.field === '__count') ? attr.length : attr.aggregates[measure.field];
+ });
+
+ var row = _.find(rows, function (header) { return _.isEqual(header.path, path.slice(index)); });
+ var 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);
+ }
+ });
+ },
+
+});
+})();
+
+