summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/graph/graph_renderer.js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js/views/graph/graph_renderer.js')
-rw-r--r--addons/web/static/src/js/views/graph/graph_renderer.js459
1 files changed, 242 insertions, 217 deletions
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;
- });
- },
});
});