[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:
committed by
Mathieu Duckerts-Antoine
parent
d4461cd434
commit
89931d1749
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user