[IMP] web: comparisons in graph view

A previous commit [333ac27] has introduced a new widget TimeRangeMenu
that allows to create two sets of data to compare relatively
to a given date or datetime field.

This commit allows the graph view to render appropriately two
sets of data as would be expected in the different modes.

Task Id: 1835644
This commit is contained in:
Adrien Dieudonne
2018-08-14 12:37:36 +02:00
committed by Mathieu Duckerts-Antoine
parent d4461cd434
commit 89931d1749
5 changed files with 147 additions and 68 deletions
@@ -62,6 +62,8 @@ return AbstractModel.extend({
this.fields = params.fields;
this.modelName = params.modelName;
this.chart = {
compare: params.compare,
comparisonTimeRange: params.comparisonTimeRange,
data: [],
groupedBy: params.groupedBy.length ? params.groupedBy : groupBys,
// this parameter is not used anywhere for now.
@@ -72,6 +74,7 @@ return AbstractModel.extend({
intervalMapping: params.intervalMapping,
measure: params.context.graph_measure || params.measure,
mode: params.context.graph_mode || params.mode,
timeRange: params.timeRange,
domain: params.domain,
context: params.context,
};
@@ -146,16 +149,30 @@ return AbstractModel.extend({
}
var context = _.extend({fill_temporal: true}, this.chart.context);
return this._rpc({
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,
method: 'read_group',
context: context,
domain: this.chart.domain,
domain: this.chart.domain.concat(this.chart.comparisonTimeRange),
fields: fields,
groupBy: groupedBy,
lazy: false,
})
.then(this._processData.bind(this));
}).then(this._processData.bind(this, 'comparisonData')));
}
return $.when.apply($, defs);
},
/**
* Since read_group is insane and returns its result on different keys
@@ -166,14 +183,15 @@ return AbstractModel.extend({
* 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
*/
_processData: function (raw_data) {
_processData: function (dataKey, raw_data) {
var self = this;
var is_count = this.chart.measure === '__count__';
var data_pt, labels;
this.chart.data = [];
this.chart[dataKey] = [];
for (var i = 0; i < raw_data.length; i++) {
data_pt = raw_data[i];
labels = _.map(this.chart.groupedBy, function (field) {
@@ -191,7 +209,7 @@ return AbstractModel.extend({
// value for that field.
value = 1;
}
this.chart.data.push({
this.chart[dataKey].push({
count: count,
value: value,
labels: labels,
@@ -23,7 +23,7 @@ var CHART_TYPES = ['pie', 'bar', 'line'];
var MAX_LEGEND_LENGTH = 25 * (Math.max(1, config.device.size_class));
return AbstractRenderer.extend({
className: "o_graph_svg_container",
className: "o_graph_container",
/**
* @override
* @param {Widget} parent
@@ -33,7 +33,8 @@ return AbstractRenderer.extend({
*/
init: function (parent, state, params) {
this._super.apply(this, arguments);
this.stacked = params.stacked;
this.isComparison = !!state.comparisonData;
this.stacked = this.isComparison ? false : params.stacked;
},
/**
* @override
@@ -117,13 +118,13 @@ return AbstractRenderer.extend({
_renderBarChart: function () {
// prepare data for bar chart
var self = this;
var data, values;
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);
// zero groupbys
if (this.state.groupedBy.length === 0) {
data = [{
values: [{
@@ -131,20 +132,29 @@ return AbstractRenderer.extend({
y: this.state.data[0].value}],
key: measure
}];
}
// one groupby
if (this.state.groupedBy.length === 1) {
values = this.state.data.map(function (datapt) {
return {x: datapt.labels, y: datapt.value};
});
data = [
{
values: values,
key: measure,
} else if (this.state.groupedBy.length === 1) {
values = this.state.data.map(function (datapt, index) {
if (self.state.comparisonData) {
return {x: index, y: datapt.value};
} else {
return {x: datapt.labels, y: datapt.value};
}
];
}
if (this.state.groupedBy.length > 1) {
});
data.push({
values: values,
key: measure,
});
if (this.state.comparisonData) {
values = this.state.comparisonData.map(function (datapt, index) {
return {x: index, y: datapt.value};
});
data.push({
values: values,
key: measure + ' (compare)',
color: '#ff7f0e',
});
}
} else if (this.state.groupedBy.length > 1) {
var xlabels = [],
series = [],
label, serie, value;
@@ -174,14 +184,15 @@ return AbstractRenderer.extend({
data.push(current_serie);
}
}
// For Bar chart View, we keep only groups where count > 0
data[0].values = _.filter(data[0].values, function (elem, index) {
return self.state.data[index].count > 0;
});
// SVG
var svg = d3.select(this.$el[0]).append('svg');
var $svgContainer = $('<div/>', {class: 'o_graph_svg_container'});
this.$el.append($svgContainer);
var svg = d3.select($svgContainer[0]).append('svg');
svg.datum(data);
svg.transition().duration(0);
@@ -216,7 +227,7 @@ return AbstractRenderer.extend({
*
* @returns {nvd3 chart}
*/
_renderPieChart: function () {
_renderPieChart: function (stateData) {
var self = this;
var data = [];
var all_negative = true;
@@ -224,9 +235,9 @@ return AbstractRenderer.extend({
var all_zero = true;
// undefined label value becomes a string 'Undefined' translated
this.state.data.forEach(self._sanitizeLabel);
stateData.forEach(self._sanitizeLabel);
this.state.data.forEach(function (datapt) {
stateData.forEach(function (datapt) {
all_negative = all_negative && (datapt.value < 0);
some_negative = some_negative || (datapt.value < 0);
all_zero = all_zero && (datapt.value === 0);
@@ -248,17 +259,19 @@ return AbstractRenderer.extend({
return;
}
if (this.state.groupedBy.length) {
data = this.state.data.map(function (datapt) {
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 self.state.data[index].count > 0;
return stateData[index].count > 0;
});
var svg = d3.select(this.$el[0]).append('svg');
var $svgContainer = $('<div/>', {class: 'o_graph_svg_container'});
this.$el.append($svgContainer);
var svg = d3.select($svgContainer[0]).append('svg');
svg.datum(data);
svg.transition().duration(100);
@@ -294,34 +307,67 @@ return AbstractRenderer.extend({
// undefined label value becomes a string 'Undefined' translated
this.state.data.forEach(self._sanitizeLabel);
if (graphData.length < 2) {
this.$el.append(qweb.render('GraphView.error', {
title: _t("Not enough data points"),
description: _t("You need at least two data points to display a line chart.")
}));
return;
}
var data = [];
var tickValues;
var tickFormat;
var measure = this.state.fields[this.state.measure].string;
var values;
if (this.state.groupedBy.length === 1) {
var values = graphData.map(function (datapt, index) {
values = graphData.map(function (datapt, index) {
return {x: index, y: datapt.value};
});
data = [
{
data.push({
area: true,
values: values,
key: measure,
area: true,
});
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: measure + ' (compare)',
color: '#ff7f0e',
});
if (this.state.comparisonData.length > graphData.length) {
tickValues = this.state.comparisonData.map(function (d, i) {
return i;
});
}
];
tickValues = graphData.map(function (d, i) { return i;});
tickFormat = function (d) {return self.state.data[d].labels;};
}
if (this.state.groupedBy.length > 1) {
data = [];
}
if (!tickValues) {
tickValues = graphData.map(function (d, i) {
return i;
});
}
var ticksLabels = [];
for (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]);
}
}
}
tickFormat = function (d) {
return ticksLabels[d];
};
} else if (this.state.groupedBy.length > 1) {
var data_dict = {};
var tick = 0;
var tickLabels = [];
@@ -349,8 +395,9 @@ return AbstractRenderer.extend({
}
tickFormat = function (d) {return tickLabels[d];};
}
var svg = d3.select(this.$el[0]).append('svg');
var $svgContainer = $('<div/>', {class: 'o_graph_svg_container'});
this.$el.append($svgContainer);
var svg = d3.select($svgContainer[0]).append('svg');
svg.datum(data);
svg.transition().duration(0);
@@ -382,12 +429,23 @@ return AbstractRenderer.extend({
* @private
*/
_renderGraph: function () {
var self = this;
this.$el.empty();
var chart = this['_render' + _.str.capitalize(this.state.mode) + 'Chart']();
if (chart && chart.tooltip.chartContainer) {
this.to_remove = chart.update;
nv.utils.onWindowResize(chart.update);
chart.tooltip.chartContainer(this.el);
var chartResize = function (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 chart1 = this['_render' + _.str.capitalize(this.state.mode) + 'Chart'](this.state.data);
chartResize(chart1);
if (this.state.mode === 'pie' && this.isComparison) {
var chart2 = this['_render' + _.str.capitalize(this.state.mode) + 'Chart'](this.state.comparisonData);
chartResize(chart2);
chart1.update();
}
},
/**
@@ -36,6 +36,7 @@ var GraphView = AbstractView.extend({
Renderer: GraphRenderer,
},
viewType: 'graph',
enableTimeRangeMenu: 'true',
/**
* @override
*/
@@ -86,6 +87,7 @@ var GraphView = AbstractView.extend({
this.loadParams.groupBys = groupBys || [];
this.loadParams.intervalMapping = intervalMapping;
this.loadParams.fields = this.fields;
this.loadParams.comparisonDomain = params.comparisonDomain;
},
});
+8 -4
View File
@@ -1,10 +1,14 @@
.o_graph {
.o_graph_svg_container {
direction: ltr#{"/*rtl:ignore*/"};
.o_graph_container {
height: 100%;
overflow: auto;
svg {
background-color: $o-view-background-color;
.o_graph_svg_container {
direction: ltr#{"/*rtl:ignore*/"};
height: 100%;
overflow: auto;
svg {
background-color: $o-view-background-color;
}
}
}
}
+4 -7
View File
@@ -132,11 +132,9 @@ QUnit.module('Views', {
});
QUnit.test('displaying line chart with only 1 data point', function (assert) {
assert.expect(2);
// this test makes sure the line chart does not crash when only one data
// point is displayed. This was the case since a line cannot be drawn with
// only one point of reference.
assert.expect(1);
// this test makes sure the line chart does not crash when only one data
// point is displayed.
var done = assert.async();
this.data.foo.records = this.data.foo.records.slice(0,1);
var graph = createView({
@@ -148,8 +146,7 @@ QUnit.module('Views', {
'</graph>',
});
return concurrency.delay(0).then(function () {
assert.ok(!graph.$('svg').length, "should not have a svg");
assert.ok(graph.$('.o_view_nocontent').length, "should have an error message");
assert.ok(graph.$('svg').length, "should have a svg");
graph.destroy();
done();
});