summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js')
-rw-r--r--addons/web/static/src/js/core/data_comparison_utils.js111
-rw-r--r--addons/web/static/src/js/views/abstract_view.js1
-rw-r--r--addons/web/static/src/js/views/control_panel/control_panel_model.js1
-rw-r--r--addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js6
-rw-r--r--addons/web/static/src/js/views/graph/graph_controller.js16
-rw-r--r--addons/web/static/src/js/views/graph/graph_model.js172
-rw-r--r--addons/web/static/src/js/views/graph/graph_renderer.js459
-rw-r--r--addons/web/static/src/js/views/graph/graph_view.js6
8 files changed, 438 insertions, 334 deletions
diff --git a/addons/web/static/src/js/core/data_comparison_utils.js b/addons/web/static/src/js/core/data_comparison_utils.js
index 0e62f2d3ffa..60f88e67d8d 100644
--- a/addons/web/static/src/js/core/data_comparison_utils.js
+++ b/addons/web/static/src/js/core/data_comparison_utils.js
@@ -2,7 +2,87 @@ odoo.define('web.dataComparisonUtils', function (require) {
"use strict";
var fieldUtils = require('web.field_utils');
+var Class = require('web.Class');
+var DateClasses = Class.extend({
+ init: function (dateSets, interval) {
+ // dateSets are assumed pairwise disjoint and ordered naturally.
+ // at least one of them is non empty.
+
+ // we complete the first inhabited dateSet. Its elements will be
+ // the default representatives for the classes.
+ this._maximalLength = 1;
+ this._referenceIndex = null;
+ for (var i = 0; i < dateSets.length; i++) {
+ var dateSet = dateSets[i];
+ if (dateSet.length && this._referenceIndex === null) {
+ this._referenceIndex = i;
+ }
+ if (dateSet.length > this._maximalLength) {
+ this._maximalLength = dateSet.length;
+ }
+ }
+ this._referenceSet = this._contructReferenceSet(dateSets[this._referenceIndex], interval);
+ this._dateClasses = this._constructDateClasses(dateSets);
+ },
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ representative: function (date, index) {
+ index = index || this._referenceIndex;
+ return this._dateClass(date)[index];
+ },
+
+ //----------------------------------------------------------------------
+ // Private
+ //----------------------------------------------------------------------
+
+ /**
+ * @param {moment[]} dateSet, ordered dateSet
+ * @param {number} num, indicates number of dates to add
+ * @param {string} interval, determines time interval between two added dates
+ * @returns {function}
+ */
+ _contructReferenceSet: function (dateSet, interval) {
+ var additionalDates = [];
+ var dateSetLength = dateSet.length;
+ var diff = this._maximalLength - dateSetLength;
+ var lastDate = dateSet[dateSetLength - 1];
+ for (var i = 0; i < diff; i++) {
+ var date = moment(lastDate).add(i + 1, interval).format('DD MMM YYYY');
+ additionalDates.push(date);
+ }
+ return dateSet.concat(additionalDates);
+ },
+ _constructDateClasses: function (dateSets) {
+ var dateClasses = [];
+ for (var index = 0; index < this._maximalLength; index++) {
+ var dateClass = [];
+ for (var j = 0; j < dateSets.length; j++) {
+ var dateSet = j=== this._referenceIndex ? this._referenceSet : dateSets[j];
+ if (index < dateSet.length) {
+ dateClass.push(dateSet[index]);
+ } else {
+ dateClass.push(undefined);
+ }
+ }
+ dateClasses.push(dateClass);
+ }
+ return dateClasses;
+ },
+ _dateClass: function (date) {
+ var dateClass;
+ for (var i = 0; i < this._dateClasses.length; i++) {
+ dateClass = this._dateClasses[i];
+ if (_.contains(dateClass, date)) {
+ break;
+ }
+ }
+ return dateClass;
+ },
+});
/**
* @param {Number} value
* @param {Number} comparisonValue
@@ -32,11 +112,10 @@ function computeVariation (value, comparisonValue) {
signClass = ' o_null';
}
return {magnitude: magnitude, signClass: signClass};
- } else {
- return {magnitude: NaN};
- }
+ } else {
+ return {magnitude: NaN};
+ }
}
-
/**
* @param {Object} variation
* @param {Object} field
@@ -44,9 +123,9 @@ function computeVariation (value, comparisonValue) {
* @returns {Object}
*/
function renderVariation (variation, field, options) {
- var $variation;
+ var $variation;
if (!isNaN(variation.magnitude)) {
- $variation = $('<div>', {class: 'o_variation' + variation.signClass}).html(
+ $variation = $('<div>', {class: 'o_variation' + variation.signClass}).html(
fieldUtils.format.percentage(variation.magnitude, field, options
));
} else {
@@ -54,7 +133,6 @@ function renderVariation (variation, field, options) {
}
return $variation;
}
-
/**
* @param {JQuery} $node
* @param {Number} value
@@ -67,18 +145,19 @@ function renderVariation (variation, field, options) {
*/
function renderComparison ($node, value, comparisonValue, variation, formatter, field, options) {
var $variation = renderVariation(variation, field, options);
- $node.append($variation);
- if (!isNaN(variation.magnitude)) {
- $node.append(
- $('<div>', {class: 'o_comparison'})
- .html(formatter(value, field, options) + ' <span>vs</span> ' + formatter(comparisonValue, field, options))
- );
- }
+ $node.append($variation);
+ if (!isNaN(variation.magnitude)) {
+ $node.append(
+ $('<div>', {class: 'o_comparison'})
+ .html(formatter(value, field, options) + ' <span>vs</span> ' + formatter(comparisonValue, field, options))
+ );
+ }
}
return {
- computeVariation: computeVariation,
- renderComparison: renderComparison
+ computeVariation: computeVariation,
+ DateClasses: DateClasses,
+ renderComparison: renderComparison
};
});
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index ba3c198bd21..276b29b71eb 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -330,6 +330,7 @@ var AbstractView = Factory.extend({
var timeRangeDescription = timeRangeMenuData.timeRangeDescription || '';
this.loadParams = _.extend(this.loadParams, {
compare: comparisonTimeRange.length > 0,
+ comparisonField: timeRangeMenuData.comparisonField,
comparisonTimeRange: comparisonTimeRange,
comparisonTimeRangeDescription: comparisonTimeRangeDescription,
context: searchQuery.context,
diff --git a/addons/web/static/src/js/views/control_panel/control_panel_model.js b/addons/web/static/src/js/views/control_panel/control_panel_model.js
index ad0d80de7a3..f2e28858ba3 100644
--- a/addons/web/static/src/js/views/control_panel/control_panel_model.js
+++ b/addons/web/static/src/js/views/control_panel/control_panel_model.js
@@ -893,6 +893,7 @@ var ControlPanelModel = mvc.Model.extend({
}
context = {
timeRangeMenuData: {
+ comparisonField: filter.fieldName,
timeRange: timeRange,
timeRangeDescription: timeRangeDescription,
comparisonTimeRange: comparisonTimeRange,
diff --git a/addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js b/addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js
index 56d43ee6708..dc7a5dc3cc8 100644
--- a/addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js
+++ b/addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js
@@ -42,7 +42,7 @@ var GroupByMenuMixin = {
return groupableFields[fieldName].string;
});
this.groupableFields = groupableFields;
- var groupBys = this._getGroupBys(this.model.get().groupedBy);
+ var groupBys = this._getGroupBys(this.model.get().groupBy);
this.groupByMenu = new GroupByMenu(this, groupBys, this.groupableFields);
this.groupByMenu.insertAfter($node.find('div:first'));
},
@@ -110,7 +110,7 @@ var GroupByMenuMixin = {
_onItemOptionClicked: function (ev) {
var fieldName = ev.data.id;
var optionId = ev.data.optionId;
- var activeGroupBys = this.model.get().groupedBy;
+ var activeGroupBys = this.model.get().groupBy;
var currentOptionId = activeGroupBys.reduce(
function (optionId, groupby) {
if (groupby.split(':')[0] === fieldName){
@@ -140,7 +140,7 @@ var GroupByMenuMixin = {
*/
_onMenuItemClicked: function (ev) {
var fieldName = ev.data.id;
- var activeGroupBys = this.model.get().groupedBy;
+ var activeGroupBys = this.model.get().groupBy;
var groupByFieldNames = _.map(activeGroupBys, function (groupby) {
return groupby.split(':')[0];
});
diff --git a/addons/web/static/src/js/views/graph/graph_controller.js b/addons/web/static/src/js/views/graph/graph_controller.js
index 15f7fbeaa35..a88a94b786f 100644
--- a/addons/web/static/src/js/views/graph/graph_controller.js
+++ b/addons/web/static/src/js/views/graph/graph_controller.js
@@ -71,13 +71,7 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{
context: {
graph_measure: state.measure,
graph_mode: state.mode,
- graph_groupbys: state.groupedBy,
- // this parameter is not used anywher for now
- // the idea would be to seperate intervals from
- // fieldnames in groupbys. This could be done
- // in graph view only or everywhere but this is
- // a big refactoring.
- graph_intervalMapping: state.intervalMapping,
+ graph_groupbys: state.groupBy,
}
};
},
@@ -115,10 +109,10 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{
* override
*
* @private
- * @param {string[]} groupBys
+ * @param {string[]} groupBy
*/
- _setGroupby: function (groupBys) {
- this.update({groupBy: groupBys});
+ _setGroupby: function (groupBy) {
+ this.update({groupBy: groupBy});
},
/**
@@ -127,7 +121,7 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{
*
* private
*
- * @param {string} mode one of 'pie', 'line' or 'bar'
+ * @param {'pie'|'line'|'bar'} mode
*/
_setMode: function (mode) {
this.update({mode: mode});
diff --git a/addons/web/static/src/js/views/graph/graph_model.js b/addons/web/static/src/js/views/graph/graph_model.js
index 91b017524d8..33b8896b29d 100644
--- a/addons/web/static/src/js/views/graph/graph_model.js
+++ b/addons/web/static/src/js/views/graph/graph_model.js
@@ -1,11 +1,13 @@
odoo.define('web.GraphModel', function (require) {
"use strict";
+var core = require('web.core');
+var _t = core._t;
+
/**
* The graph model is responsible for fetching and processing data from the
- * server. It basically just do a read_group and format/normalize data.
+ * server. It basically just do a(some) read_group(s) and format/normalize data.
*/
-
var AbstractModel = require('web.AbstractModel');
return AbstractModel.extend({
@@ -23,18 +25,12 @@ return AbstractModel.extend({
//--------------------------------------------------------------------------
/**
- * We defend against outside modifications by extending the chart data. It
- * may be overkill.
- *
- * @todo Adding the fields parameter looks wrong. If the renderer or the
- * controller need the fields, they should get it via their init method.
+ * override
*
* @returns {Object}
*/
get: function () {
- return _.extend({}, this.chart, {
- fields: this.fields
- });
+ return this.chart;
},
/**
* Initial loading.
@@ -43,13 +39,19 @@ return AbstractModel.extend({
* should be done by the graphView I think.
*
* @param {Object} params
- * @param {string} params.mode one of 'pie', 'bar', 'line
- * @param {string} params.measure a valid field name
- * @param {string[]} params.groupBys a list of valid field names
+ * @param {boolean} params.compare
* @param {Object} params.context
+ * @param {Object} params.fields
+ * @param {string[]} params.comparisonTimeRange
* @param {string[]} params.domain
- * @param {Object} params.intervalMapping object linking fieldNames with intervals.
- * this could be useful to simplify the code. For now this parameter is not used.
+ * @param {string[]} params.groupBy a list of valid field names
+ * @param {string[]} params.timeRange
+ * @param {string} params.comparisonField
+ * @param {string} params.comparisonTimeRangeDescription
+ * @param {string} params.measure a valid field name
+ * @param {'pie'|'bar'|'line'} params.mode
+ * @param {string} params.modelName
+ * @param {string} params.timeRangeDescription
* @returns {Promise} The promise does not return a handle, we don't need
* to keep track of various entities.
*/
@@ -59,25 +61,21 @@ return AbstractModel.extend({
this.fields = params.fields;
this.modelName = params.modelName;
this.chart = {
- compare: params.compare,
+ comparisonField: params.comparisonField,
comparisonTimeRange: params.comparisonTimeRange,
comparisonTimeRangeDescription: params.comparisonTimeRangeDescription,
- data: [],
- groupedBy: params.groupedBy.length ? params.groupedBy : groupBys,
- // this parameter is not used anywhere for now.
- // the idea would be to seperate intervals from
- // fieldnames in groupbys. This could be done
- // in graph view only or everywhere but this is
- // a big refactoring.
- intervalMapping: params.intervalMapping,
+ compare: params.compare,
+ context: params.context,
+ dataPoints: [],
+ domain: params.domain,
+ groupBy: params.groupedBy.length ? params.groupedBy : groupBys,
measure: params.context.graph_measure || params.measure,
mode: params.context.graph_mode || params.mode,
+ origins: [],
timeRange: params.timeRange,
timeRangeDescription: params.timeRangeDescription,
- domain: params.domain,
- context: params.context,
};
- return this._loadGraph();
+ return this._loadGraph(this._getDomains());
},
/**
* Reload data. It is similar to the load function. Note that we ignore the
@@ -88,31 +86,33 @@ return AbstractModel.extend({
*
* @param {any} handle ignored!
* @param {Object} params
+ * @param {Object} [params.context]
* @param {string[]} [params.domain]
* @param {string[]} [params.groupBy]
- * @param {string} [params.mode] one of 'bar', 'pie', 'line'
* @param {string} [params.measure] a valid field name
+ * @param {string} [params.mode] one of 'bar', 'pie', 'line'
* @returns {Promise}
*/
reload: function (handle, params) {
if ('context' in params) {
this.chart.context = params.context;
- this.chart.groupedBy = params.context.graph_groupbys || this.chart.groupedBy;
+ this.chart.groupBy = params.context.graph_groupbys || this.chart.groupBy;
this.chart.measure = params.context.graph_measure || this.chart.measure;
this.chart.mode = params.context.graph_mode || this.chart.mode;
var timeRangeMenuData = params.context.timeRangeMenuData;
if (timeRangeMenuData) {
+ this.chart.comparisonField = timeRangeMenuData.comparisonField || undefined;
this.chart.comparisonTimeRange = timeRangeMenuData.comparisonTimeRange || [];
this.chart.compare = this.chart.comparisonTimeRange.length > 0;
- this.chart.timeRange = timeRangeMenuData.timeRange || [];
this.chart.comparisonTimeRangeDescription = timeRangeMenuData.comparisonTimeRangeDescription;
+ this.chart.timeRange = timeRangeMenuData.timeRange || [];
this.chart.timeRangeDescription = timeRangeMenuData.timeRangeDescription;
} else {
- this.chart = _.omit(this.chart, 'comparisonData');
+ this.chart.comparisonField = undefined;
this.chart.comparisonTimeRange = [];
this.chart.compare = false;
- this.chart.timeRange = [];
this.chart.comparisonTimeRangeDescription = undefined;
+ this.chart.timeRange = [];
this.chart.timeRangeDescription = undefined;
}
}
@@ -120,10 +120,7 @@ return AbstractModel.extend({
this.chart.domain = params.domain;
}
if ('groupBy' in params) {
- this.chart.groupedBy = params.groupBy.length ? params.groupBy : this.initialGroupBys;
- }
- if ('intervalMapping' in params) {
- this.chart.intervalMapping = params.intervalMapping;
+ this.chart.groupBy = params.groupBy.length ? params.groupBy : this.initialGroupBys;
}
if ('measure' in params) {
this.chart.measure = params.measure;
@@ -132,7 +129,7 @@ return AbstractModel.extend({
this.chart.mode = params.mode;
return Promise.resolve();
}
- return this._loadGraph();
+ return this._loadGraph(this._getDomains());
},
//--------------------------------------------------------------------------
@@ -140,16 +137,32 @@ return AbstractModel.extend({
//--------------------------------------------------------------------------
/**
- * Fetch and process graph data. It is basically a read_group with correct
- * fields. We have to do some light processing to separate date groups
- * in the field list, because they can be defined with an aggregation
- * function, such as my_date:week
+ * @returns {Object[]}
+ */
+ _getDomains: function () {
+ var domains = [this.chart.domain.concat(this.chart.timeRange)];
+ this.chart.origins = [this.chart.timeRangeDescription];
+ if (this.chart.compare) {
+ domains.push(this.chart.domain.concat(this.chart.comparisonTimeRange));
+ this.chart.origins.push(this.chart.comparisonTimeRangeDescription);
+ }
+ return domains;
+ },
+
+ /**
+ * Fetch and process graph data. It is basically a(some) read_group(s)
+ * with correct fields for each domain. We have to do some light processing
+ * to separate date groups in the field list, because they can be defined
+ * with an aggregation function, such as my_date:week.
*
+ * @param {Array[]} domains
* @returns {Promise}
*/
- _loadGraph: function () {
- var groupedBy = this.chart.groupedBy;
- var fields = _.map(groupedBy, function (groupBy) {
+ _loadGraph: function (domains) {
+ var self = this;
+ this.chart.dataPoints = [];
+ var groupBy = this.chart.groupBy;
+ var fields = _.map(groupBy, function (groupBy) {
return groupBy.split(':')[0];
});
@@ -164,55 +177,46 @@ return AbstractModel.extend({
var context = _.extend({fill_temporal: true}, this.chart.context);
var defs = [];
- defs.push(this._rpc({
- model: this.modelName,
- method: 'read_group',
- context: context,
- domain: this.chart.domain.concat(this.chart.timeRange),
- fields: fields,
- groupBy: groupedBy,
- lazy: false,
- }).then(this._processData.bind(this, 'data')));
- if (this.chart.compare) {
- defs.push(this._rpc({
- model: this.modelName,
+ domains.forEach(function (domain, originIndex) {
+ defs.push(self._rpc({
+ model: self.modelName,
method: 'read_group',
context: context,
- domain: this.chart.domain.concat(this.chart.comparisonTimeRange),
+ domain: domain,
fields: fields,
- groupBy: groupedBy,
+ groupBy: groupBy,
lazy: false,
- }).then(this._processData.bind(this, 'comparisonData')));
- }
-
+ }).then(self._processData.bind(self, originIndex)));
+ });
return Promise.all(defs);
},
/**
* Since read_group is insane and returns its result on different keys
* depending of some input, we have to normalize the result.
- * The final chart data is added to this.chart object.
+ * Each group comming from the read_group produces a dataPoint
*
* @todo This is not good for race conditions. The processing should get
* the object this.chart in argument, or an array or something. We want to
* avoid writing on a this.chart object modified by a subsequent read_group
*
- * @param {String} dataKey
- * @param {any} raw_data result from the read_group
+ * @param {number} originIndex
+ * @param {any} rawData result from the read_group
*/
- _processData: function (dataKey, raw_data) {
+ _processData: function (originIndex, rawData) {
var self = this;
- var is_count = this.chart.measure === '__count__';
- var data_pt, labels;
+ var isCount = this.chart.measure === '__count__';
+ var labels;
- this.chart[dataKey] = [];
- for (var i = 0; i < raw_data.length; i++) {
- data_pt = raw_data[i];
- labels = _.map(this.chart.groupedBy, function (field) {
- return self._sanitizeValue(data_pt[field], field);
+ function getLabels (dataPt) {
+ return self.chart.groupBy.map(function (field) {
+ return self._sanitizeValue(dataPt[field], field.split(":")[0]);
});
- var count = data_pt.__count || data_pt[this.chart.groupedBy[0]+'_count'] || 0;
- var value = is_count ? count : data_pt[this.chart.measure];
+ }
+ rawData.forEach(function (dataPt){
+ labels = getLabels(dataPt);
+ var count = dataPt.__count || dataPt[self.chart.groupBy[0]+'_count'] || 0;
+ var value = isCount ? count : dataPt[self.chart.measure];
if (value instanceof Array) {
// when a many2one field is used as a measure AND as a grouped
// field, bad things happen. The server will only return the
@@ -223,28 +227,30 @@ return AbstractModel.extend({
// value for that field.
value = 1;
}
- this.chart[dataKey].push({
+ self.chart.dataPoints.push({
count: count,
value: value,
labels: labels,
+ originIndex: originIndex,
});
- }
+ });
},
/**
* Helper function (for _processData), turns various values in a usable
* string form, that we can display in the interface.
*
- * @param {any} value some value received by the read_group rpc
- * @param {string} field the name of the corresponding field
+ * @param {any} value value for the field fieldName received by the read_group rpc
+ * @param {string} fieldName
* @returns {string}
*/
- _sanitizeValue: function (value, field) {
- var fieldName = field.split(':')[0];
+ _sanitizeValue: function (value, fieldName) {
if (value === false && this.fields[fieldName].type !== 'boolean') {
- return undefined;
+ return _t("Undefined");
+ }
+ if (value instanceof Array) {
+ return value[1];
}
- if (value instanceof Array) return value[1];
- if (field && (this.fields[fieldName].type === 'selection')) {
+ if (fieldName && (this.fields[fieldName].type === 'selection')) {
var selected = _.where(this.fields[fieldName].selection, {0: value})[0];
return selected ? selected[1] : value;
}
diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js
index 6311df5d8b2..34d0ed4eb80 100644
--- a/addons/web/static/src/js/views/graph/graph_renderer.js
+++ b/addons/web/static/src/js/views/graph/graph_renderer.js
@@ -13,9 +13,11 @@ var AbstractRenderer = require('web.AbstractRenderer');
var config = require('web.config');
var core = require('web.core');
var field_utils = require('web.field_utils');
+var dataComparisonUtils = require('web.dataComparisonUtils');
var _t = core._t;
var qweb = core.qweb;
+var DateClasses = dataComparisonUtils.DateClasses;
var CHART_TYPES = ['pie', 'bar', 'line'];
@@ -34,10 +36,24 @@ return AbstractRenderer.extend({
*/
init: function (parent, state, params) {
this._super.apply(this, arguments);
- this.isComparison = !!state.comparisonData;
+ this.datasets = [];
this.isEmbedded = params.isEmbedded;
- this.stacked = this.isComparison ? false : params.stacked;
+ this.stacked = this.state.compare ? false : params.stacked;
this.title = params.title || '';
+ this.fields = params.fields || {};
+ this.formatOptions = {
+ // allow to decide if utils.human_number should be used
+ humanReadable: function (value) {
+ return Math.abs(value) >= 1000;
+ },
+ // with the choices below, 1236 is represented by 1.24k
+ minDigits: 1,
+ decimals: 2,
+ // avoid comma separators for thousands in numbers when human_number is used
+ formatterCallback: function (str) {
+ return str;
+ }
+ };
},
/**
* @override
@@ -80,8 +96,7 @@ return AbstractRenderer.extend({
* @param {Object} params
*/
updateState: function (state, params) {
- this.isComparison = !!state.comparisonData;
- this.stacked = this.isComparison ? false : params.stacked;
+ this.stacked = this.state.compare ? false : params.stacked;
return this._super.apply(this, arguments);
},
@@ -90,6 +105,16 @@ return AbstractRenderer.extend({
//--------------------------------------------------------------------------
/**
+ * Used to format correctly the value in tooltips and y axis
+ *
+ * @private
+ * @param {number} value
+ * @returns {strin} The number formatted
+ */
+ _formatValue: function (value) {
+ return field_utils.format.float(value, this.formatOption);
+ },
+ /**
* Render the chart.
*
* Note that This method is synchronous, but the actual rendering is done
@@ -111,7 +136,7 @@ return AbstractRenderer.extend({
title: _t('Invalid mode for chart'),
message: _t('Cannot render chart with mode : ') + this.state.mode
});
- } else if (!this.state.data.length && this.state.mode !== 'pie') {
+ } else if (!this.state.dataPoints.length && this.state.mode !== 'pie') {
this.$el.empty();
this.$el.append(qweb.render('GraphView.error', {
title: _t("No data to display"),
@@ -127,6 +152,126 @@ return AbstractRenderer.extend({
}
return this._super.apply(this, arguments);
},
+
+ _prepareDataSets: function (dataPoints) {
+ var self = this;
+ var measure = this.fields[this.state.measure].string;
+
+ function getGroupLabel(dataPt) {
+ // ([origin] + second to last groupBys) or measure
+ var z = dataPt.labels.slice(1).join("/");
+ if (self.state.compare) {
+ z = self.state.origins[dataPt.originIndex] + (z ? ('/' + z) : '');
+ }
+ z = z || measure;
+ return z;
+ }
+
+ function getXLabel(dataPt) {
+ return dataPt.labels.slice(0, 1).join("/") || _t('Total');
+ }
+
+ var comparisonField = this.state.comparisonField;
+ var groupBys = this.state.groupBy.map(function (gb) {
+ return gb.split(":")[0];
+ });
+
+ // if dateIdentification is true, we will identify dates coming from
+ // different origins if they have same rank in the corresponding date sets
+ // (date sets are assumed pairwise disjoint, naturally ordered and not all empty)
+ var dateIdentification = groupBys.indexOf(comparisonField) === 0;
+
+ var dateSets;
+ var dateClasses;
+ if (dateIdentification) {
+ dateSets = this.state.origins.map(function () {
+ return [];
+ });
+ dataPoints.forEach(function (dataPt) {
+ dateSets[dataPt.originIndex].push(dataPt.labels[0]);
+ });
+ dateSets = dateSets.map(function (dateSet) {
+ return _.uniq(dateSet);
+ });
+ var interval = this.state.groupBy[0].split(":")[1] || 'month';
+ dateClasses = new DateClasses(dateSets, interval);
+ }
+
+ // dataPoints --> points separated into different groups
+ var groups = {};
+ var groupInfo = {};
+ var xRange = [];
+ dataPoints.forEach(function (dataPt) {
+ var groupLabel = getGroupLabel(dataPt);
+ if (!(groupLabel in groups)) {
+ groups[groupLabel] = {};
+ }
+ if (!(groupLabel in groupInfo)) {
+ groupInfo[groupLabel] = {
+ // by construction, all data points in the same group
+ // have the same origin.
+ originIndex: dataPt.originIndex,
+ xLabels: {}
+ };
+ }
+ var xLabel = getXLabel(dataPt);
+ var xValue;
+ if (dateIdentification) {
+ xValue = dateClasses.representative(xLabel);
+ } else {
+ xValue = xLabel;
+ }
+ xRange.push(xValue);
+
+ groups[groupLabel][xValue] = dataPt.value;
+ groupInfo[groupLabel].xLabels[xValue] = xLabel;
+ });
+
+ xRange = _.uniq(xRange);
+
+ // If not dateIdentification, each group is completed to have points over each x in xrange
+ // If dateIdentification, we complete each group to have points over each x corresponding
+ // to a date coming from a datapoint from the same origin.
+ // Each group is transformed into a single datum for nvd3.
+ var datasets = Object.keys(groups).map(function (groupLabel) {
+ var range;
+ var originIndex = groupInfo[groupLabel].originIndex;
+ if (dateIdentification) {
+ range = xRange.slice(0, dateSets[originIndex].length);
+ } else {
+ range = xRange;
+ }
+ var points = range.reduce(
+ function (acc, xValue) {
+ var y = groups[groupLabel][xValue];
+ if (!y) {
+ if (dateIdentification) {
+ groupInfo[groupLabel].xLabels[xValue] = dateClasses.representative(xValue, originIndex);
+ } else {
+ groupInfo[groupLabel].xLabels[xValue] = xValue;
+ }
+ }
+ acc.push({
+ x: xValue,
+ y: y || 0,
+ });
+ return acc;
+ },
+ []
+ );
+ return {
+ key: groupLabel,
+ values: points
+ };
+ });
+
+ return {
+ datasets: datasets,
+ ticksLabels: xRange,
+ groupInfo: groupInfo,
+ };
+ },
+
/**
* Helper function to set up data properly for the multiBarChart model in
* nvd3.
@@ -134,88 +279,32 @@ return AbstractRenderer.extend({
* @returns {nvd3 chart}
*/
_renderBarChart: function () {
- // prepare data for bar chart
var self = this;
- var data = [];
- var values;
- var measure = this.state.fields[this.state.measure].string;
-
- // undefined label value becomes a string 'Undefined' translated
- this.state.data.forEach(self._sanitizeLabel);
-
- if (this.state.groupedBy.length === 0) {
- data = [{
- values: [{
- x: _t('Total'),
- y: this.state.data[0].value}],
- key: this.state.timeRangeDescription ?
- this.state.timeRangeDescription:
- measure,
- }];
- } else if (this.state.groupedBy.length === 1) {
- values = this.state.data.map(function (datapt, index) {
- return {x: datapt.labels, y: datapt.value};
- });
- data.push({
- values: values,
- key: this.state.timeRangeDescription ?
- this.state.timeRangeDescription:
- measure,
- });
- if (this.state.comparisonData) {
- values = this.state.comparisonData.map(function (datapt, index) {
- return {x: datapt.labels, y: datapt.value};
- });
- data.push({
- values: values,
- key: this.state.comparisonTimeRangeDescription ?
- this.state.comparisonTimeRangeDescription:
- measure,
- color: '#ff7f0e',
- });
- }
- } else if (this.state.groupedBy.length > 1) {
- var xlabels = [],
- series = [],
- label, serie, value;
- values = {};
- for (var i = 0; i < this.state.data.length; i++) {
- label = this.state.data[i].labels[0];
- serie = this.state.data[i].labels.slice(1).join("/");
- value = this.state.data[i].value;
- if ((!xlabels.length) || (xlabels[xlabels.length-1] !== label)) {
- xlabels.push(label);
- }
- series.push(serie);
- if (!(serie in values)) {values[serie] = {};}
- values[serie][label] = this.state.data[i].value;
- }
- series = _.uniq(series);
- data = [];
- var current_serie, j;
- for (i = 0; i < series.length; i++) {
- current_serie = {values: [], key: series[i]};
- for (j = 0; j < xlabels.length; j++) {
- current_serie.values.push({
- x: xlabels[j],
- y: values[series[i]][xlabels[j]] || 0,
- });
- }
- data.push(current_serie);
- }
- }
- // For one level Bar chart View, we keep only groups where count > 0
- if (this.state.groupedBy.length <= 1) {
- data[0].values = _.filter(data[0].values, function (elem, index) {
- return self.state.data[index].count > 0;
+ // prepare data for bar chart
+ var dataPoints = this.state.dataPoints.filter(function (dataPt) {
+ return dataPt.count > 0;
+ });
+
+ // put data in a format for nvd3
+ var dataProcessed = this._prepareDataSets(dataPoints);
+ var groupInfo = dataProcessed.groupInfo;
+ this.datasets = dataProcessed.datasets;
+
+ // style data
+ if (this.state.groupBy.length === 1) {
+ this.datasets.forEach(function (group) {
+ if (groupInfo[group.key].originIndex === 1) {
+ group.color = '#ff7f0e';
+ }
});
}
+ // nvd3 specific
var $svgContainer = $('<div/>', {class: 'o_graph_svg_container'});
this.$el.append($svgContainer);
var svg = d3.select($svgContainer[0]).append('svg');
- svg.datum(data);
+ svg.datum(this.datasets);
svg.transition().duration(0);
@@ -228,37 +317,39 @@ return AbstractRenderer.extend({
'grouped': _t('Grouped'),
'stacked': _t('Stacked'),
},
- showLegend: _.size(data) <= MAX_LEGEND_LENGTH,
+ showLegend: _.size(this.datasets) <= MAX_LEGEND_LENGTH,
showXAxis: true,
showYAxis: true,
rightAlignYAxis: false,
stacked: this.stacked,
reduceXTicks: false,
rotateLabels: -20,
- showControls: (this.state.groupedBy.length > 1)
+ showControls: (this.state.groupBy.length > 1)
});
- chart.yAxis.tickFormat(function (d) {
- var measure_field = self.state.fields[self.measure];
- return field_utils.format.float(d, {
- digits: measure_field && measure_field.digits || [69, 2],
+
+ chart.yAxis
+ .tickFormat(function (d) {
+ return self._formatValue(d);
});
- });
+
chart.tooltip.contentGenerator(function (data) {
var lines = data.series.map(function (serie) {
- var label = data.value;
- if (self.state.groupedBy.length > 1 || self.isComparison) {
+ var label = groupInfo[serie.key].xLabels[data.value];
+ if (self.state.groupBy.length > 1 || self.state.compare) {
label = label + "/" + serie.key;
}
return {
color: serie.color,
label: label,
- value: serie.value,
+ value: self._formatValue(serie.value),
};
});
return qweb.render("web.Chart.Tooltip", {
- title: self.state.fields[self.state.measure].string,
- lines: lines,
+ title: self.fields[self.state.measure].string,
+ lines: lines.sort(function (line1, line2) {
+ return line2.value - line1.value;
+ }),
});
});
@@ -274,17 +365,16 @@ return AbstractRenderer.extend({
*.
* @returns {nvd3 chart|undefined}
*/
- _renderPieChart: function (stateData) {
+ _renderPieChart: function (originIndex) {
var self = this;
- var data = [];
var all_negative = true;
var some_negative = false;
var all_zero = true;
- // undefined label value becomes a string 'Undefined' translated
- stateData.forEach(self._sanitizeLabel);
-
- stateData.forEach(function (datapt) {
+ var dataPoints = this.state.dataPoints.filter(function (dataPt) {
+ return dataPt.originIndex === originIndex && dataPt.count > 0;
+ });
+ dataPoints.forEach(function (datapt) {
all_negative = all_negative && (datapt.value < 0);
some_negative = some_negative || (datapt.value < 0);
all_zero = all_zero && (datapt.value === 0);
@@ -298,9 +388,9 @@ return AbstractRenderer.extend({
return;
}
if (all_zero) {
- if (this.isEmbedded || this.isComparison) {
+ if (this.isEmbedded || this.state.compare) {
// add fake data to display an empty pie chart
- data = [{
+ this.datasets = [{
x : "No data" ,
y : 1
}];
@@ -313,22 +403,18 @@ return AbstractRenderer.extend({
return;
}
} else {
- if (this.state.groupedBy.length) {
- data = stateData.map(function (datapt) {
- return {x:datapt.labels.join("/"), y: datapt.value};
- });
- }
-
- // We only keep groups where count > 0
- data = _.filter(data, function (elem, index) {
- return stateData[index].count > 0;
+ this.datasets = dataPoints.map(function (datapt) {
+ return {
+ x: datapt.labels.join("/") || _t('Total'),
+ y: datapt.value,
+ };
});
}
var $svgContainer = $('<div/>', {class: 'o_graph_svg_container'});
this.$el.append($svgContainer);
var svg = d3.select($svgContainer[0]).append('svg');
- svg.datum(data);
+ svg.datum(this.datasets);
svg.transition().duration(100);
@@ -348,7 +434,7 @@ return AbstractRenderer.extend({
var chart = nv.models.pieChart().labelType('percent');
chart.options({
delay: 250,
- showLegend: !all_zero && (legend_right || _.size(data) <= MAX_LEGEND_LENGTH),
+ showLegend: !all_zero && (legend_right || _.size(this.datasets) <= MAX_LEGEND_LENGTH),
legendPosition: legend_right ? 'right' : 'top',
transition: 100,
color: color,
@@ -357,11 +443,11 @@ return AbstractRenderer.extend({
chart.tooltip.contentGenerator(function (data) {
return qweb.render("web.Chart.Tooltip", {
- title: self.state.fields[self.state.measure].string,
+ title: self.fields[self.state.measure].string,
lines : [{
color: data.color,
label: data.data.x,
- value: data.data.y,
+ value: self._formatValue(data.data.y),
}],
});
});
@@ -378,93 +464,44 @@ return AbstractRenderer.extend({
_renderLineChart: function () {
var self = this;
- // Remove Undefined of first GroupBy
- var graphData = _.filter(this.state.data, function (elem) {
- return elem.labels[0] !== undefined && elem.labels[0] !== _t("Undefined");
+ // remove some data points
+ var dataPoints = this.state.dataPoints.filter(function (dataPt) {
+ return dataPt.labels[0] !== _t("Undefined");
});
- // undefined label value becomes a string 'Undefined' translated
- this.state.data.forEach(self._sanitizeLabel);
-
- var data = [];
- var ticksLabels = [];
- var measure = this.state.fields[this.state.measure].string;
- var values;
+ var dataProcessed = this._prepareDataSets(dataPoints);
+ var ticksLabels = dataProcessed.ticksLabels;
+ var groupInfo = dataProcessed.groupInfo;
+ this.datasets = dataProcessed.datasets;
- if (this.state.groupedBy.length === 1) {
- values = graphData.map(function (datapt, index) {
- return {x: index, y: datapt.value};
- });
- data.push({
- area: true,
- values: values,
- key: this.state.timeRangeDescription ?
- this.state.timeRangeDescription:
- measure,
- });
- if (this.state.comparisonData && this.state.comparisonData.length > 0) {
- values = this.state.comparisonData.map(function (datapt, index) {
- return {x: index, y: datapt.value};
- });
- data.push({
- values: values,
- key: this.state.comparisonTimeRangeDescription ?
- this.state.comparisonTimeRangeDescription:
- measure,
- color: '#ff7f0e',
- });
- }
-
- for (var i = 0; i < graphData.length; i++) {
- ticksLabels.push(graphData[i].labels);
- }
- if (this.state.comparisonData && this.state.comparisonData.length > this.state.data.length) {
- var diff = this.state.comparisonData.length - this.state.data.length;
- var length = self.state.data.length;
- var diffTime = 0;
- if (length < self.state.data.length) {
- var date1 = moment(self.state.data[length - 1].labels[0]);
- var date2 = moment(self.state.data[length - 2].labels[0]);
- diffTime = date1 - date2;
- for (i = 0; i < diff; i++) {
- var value = moment(date1).add(diffTime + i * diffTime).format('DD MMM YYYY');
- ticksLabels.push([value]);
- }
- }
- }
- } else if (this.state.groupedBy.length > 1) {
- var data_dict = {};
- var tick = 0;
- var serie, tickLabel;
- var identity = function (p) {return p;};
- for (var i = 0; i < graphData.length; i++) {
- if (graphData[i].labels[0] !== tickLabel) {
- tickLabel = graphData[i].labels[0];
- ticksLabels.push(tickLabel);
- tick++;
+ // style data
+ if (this.state.groupBy.length === 1) {
+ this.datasets.forEach(function (group) {
+ if (groupInfo[group.key].originIndex === 0) {
+ group.area = true;
}
- serie = graphData[i].labels.slice(1).join("/");
- if (!data_dict[serie]) {
- data_dict[serie] = {
- values: [],
- key: serie,
- };
+ if (groupInfo[group.key].originIndex === 1) {
+ group.color = '#ff7f0e';
}
- data_dict[serie].values.push({
- x: tick - 1, y: graphData[i].value,
- });
- data = _.map(data_dict, identity);
- }
+ });
}
+ this.datasets.forEach(function (group) {
+ group.values = group.values.map(function (value) {
+ return {
+ x: ticksLabels.indexOf(value.x),
+ y: value.y
+ };
+ });
+ });
var $svgContainer = $('<div/>', { class: 'o_graph_svg_container'});
// Split the tooltip into columns for large data because some portion goes out off the screen.
- if (data.length >= SPLIT_THRESHOLD) {
+ if (this.datasets.length >= SPLIT_THRESHOLD) {
$svgContainer.addClass('o_tooltip_split_in_columns');
}
this.$el.append($svgContainer);
var svg = d3.select($svgContainer[0]).append('svg');
- svg.datum(data);
+ svg.datum(this.datasets);
svg.transition().duration(0);
@@ -472,9 +509,10 @@ return AbstractRenderer.extend({
chart.options({
margin: {left: 0, bottom: 20, top: 0, right: 0},
useInteractiveGuideline: true,
- showLegend: _.size(data) <= MAX_LEGEND_LENGTH,
+ showLegend: _.size(this.datasets) <= MAX_LEGEND_LENGTH,
showXAxis: true,
showYAxis: true,
+ stacked: true,
});
chart.forceY([0]);
chart.xAxis
@@ -484,35 +522,37 @@ return AbstractRenderer.extend({
chart.yAxis
.showMaxMin(false)
.tickFormat(function (d) {
- return field_utils.format.float(d, {
- digits : self.state.fields[self.state.measure] && self.state.fields[self.state.measure].digits || [69, 2],
- });
+ return self._formatValue(d);
});
chart.yAxis.tickPadding(5);
chart.yAxis.orient("right");
chart.interactiveLayer.tooltip.contentGenerator(function (data) {
- var lines = data.series.map(function (serie) {
- var label = ticksLabels[data.value];
- if (self.state.groupedBy.length > 1 || self.isComparison) {
+ var lines = data.series.filter(function (serie) {
+ return serie.data.x === data.value;
+ }).map(function (serie) {
+ var label = groupInfo[serie.key].xLabels[ticksLabels[data.value]];
+ if (self.state.groupBy.length > 1 || self.state.compare) {
label = label + "/" + serie.key;
}
return {
color: serie.color,
label: label,
- value: serie.value,
+ value: self._formatValue(serie.value),
};
});
return qweb.render("web.Chart.Tooltip", {
- title: self.state.fields[self.state.measure].string,
- lines: lines,
+ title: self.fields[self.state.measure].string,
+ lines: lines.sort(function (line1, line2) {
+ return line2.value - line1.value;
+ }),
});
});
chart(svg);
// Bigger line (stroke-width 1.5 is hardcoded in nv.d3)
- $svgContainer.find('.nvd3 .nv-groups g.nv-group').css('stroke-width', '2px')
+ $svgContainer.find('.nvd3 .nv-groups g.nv-group').css('stroke-width', '2px');
// Delete first and last label because there is no enough space because
// of the tiny margins.
@@ -533,14 +573,14 @@ return AbstractRenderer.extend({
this.$el.empty();
- var chartResize = function (chart){
+ function chartResize (chart){
if (chart && chart.tooltip.chartContainer) {
self.to_remove = chart.update;
nv.utils.onWindowResize(chart.update);
chart.tooltip.chartContainer(self.$('.o_graph_svg_container').last()[0]);
}
}
- var chart = this['_render' + _.str.capitalize(this.state.mode) + 'Chart'](this.state.data);
+ var chart = this['_render' + _.str.capitalize(this.state.mode) + 'Chart'](0);
if (chart) {
chart.dispatch.on('renderEnd', function () {
@@ -548,12 +588,11 @@ return AbstractRenderer.extend({
$('.nv-y .tick > line').attr('x2', function (i, value) {
return Math.abs(value);
});
- })
-
+ });
chartResize(chart);
}
- if (this.state.mode === 'pie' && this.isComparison) {
+ if (this.state.mode === 'pie' && this.state.compare) {
// Render graph title
var chartTitle = this.title + ' (' + this.state.timeRangeDescription + ')';
this.$('.o_graph_svg_container').last().prepend($('<label/>', {
@@ -561,7 +600,7 @@ return AbstractRenderer.extend({
}));
// Instantiate comparison graph
- var comparisonChart = this['_render' + _.str.capitalize(this.state.mode) + 'Chart'](this.state.comparisonData);
+ var comparisonChart = this['_render' + _.str.capitalize(this.state.mode) + 'Chart'](1);
// Render comparison graph title
var comparisonChartTitle = this.title + ' (' + this.state.comparisonTimeRangeDescription + ')';
this.$('.o_graph_svg_container').last().prepend($('<label/>', {
@@ -577,20 +616,6 @@ return AbstractRenderer.extend({
}));
}
},
- /**
- * Helper function, turns label value into a usable string form if it is
- * undefined, that we can display in the interface.
- *
- * @param {Array} datapt an array that contains groupby labels of the graph
- * received by the read_group rpc.
- * @returns {string}
- */
- _sanitizeLabel: function (datapt) {
- datapt.labels = datapt.labels.map(function(label) {
- if (label === undefined) return _t("Undefined");
- return label;
- });
- },
});
});
diff --git a/addons/web/static/src/js/views/graph/graph_view.js b/addons/web/static/src/js/views/graph/graph_view.js
index 4567daf0ead..e731835bc30 100644
--- a/addons/web/static/src/js/views/graph/graph_view.js
+++ b/addons/web/static/src/js/views/graph/graph_view.js
@@ -49,14 +49,12 @@ var GraphView = AbstractView.extend({
var groupBys = [];
var measures = {__count__: {string: _t("Count"), type: "integer"}};
var groupableFields = {};
- var intervalMapping = {};
this.fields.__count__ = {string: _t("Count"), type: "integer"};
this.arch.children.forEach(function (field) {
var fieldName = field.attrs.name;
var interval = field.attrs.interval;
if (interval) {
- intervalMapping[fieldName] = interval;
fieldName = fieldName + ':' + interval;
}
if (field.attrs.type === 'measure') {
@@ -81,13 +79,13 @@ var GraphView = AbstractView.extend({
this.controllerParams.measures = measures;
this.controllerParams.groupableFields = groupableFields;
+ this.rendererParams.fields = this.fields;
this.rendererParams.stacked = this.arch.attrs.stacked !== "False";
this.rendererParams.title = this.arch.attrs.title; // TODO: use attrs.string instead
this.loadParams.mode = this.arch.attrs.type || 'bar';
this.loadParams.measure = measure || '__count__';
- this.loadParams.groupBys = groupBys || [];
- this.loadParams.intervalMapping = intervalMapping;
+ this.loadParams.groupBys = groupBys;
this.loadParams.fields = this.fields;
this.loadParams.comparisonDomain = params.comparisonDomain;
},