[IMP] web: Handle date interval in graph charts

This commit only applies for chart grouped on date or datetime fields.

In that case:
- Line Charts : fill missing groups and doesn't display undefined values.
- Bar Charts : doesn't fill missing groups but display undefined values.
- Pie Charts : doesn't fill missing groups but display undefined values.

This commit is part of task #1835644
This commit is contained in:
Francois Volral
2018-08-10 17:21:17 +02:00
parent 4a40db2097
commit 3d2ca8104e
3 changed files with 102 additions and 21 deletions
@@ -135,6 +135,7 @@ return AbstractModel.extend({
var fields = _.map(groupedBy, function (groupBy) { var fields = _.map(groupedBy, function (groupBy) {
return groupBy.split(':')[0]; return groupBy.split(':')[0];
}); });
if (this.chart.measure !== '__count__') { if (this.chart.measure !== '__count__') {
if (this.fields[this.chart.measure].type === 'many2one') { if (this.fields[this.chart.measure].type === 'many2one') {
fields = fields.concat(this.chart.measure + ":count_distinct"); fields = fields.concat(this.chart.measure + ":count_distinct");
@@ -143,10 +144,12 @@ return AbstractModel.extend({
fields = fields.concat(this.chart.measure); fields = fields.concat(this.chart.measure);
} }
} }
var context = _.extend({fill_temporal: true}, this.chart.context);
return this._rpc({ return this._rpc({
model: this.modelName, model: this.modelName,
method: 'read_group', method: 'read_group',
context: this.chart.context, context: context,
domain: this.chart.domain, domain: this.chart.domain,
fields: fields, fields: fields,
groupBy: groupedBy, groupBy: groupedBy,
@@ -176,7 +179,8 @@ return AbstractModel.extend({
labels = _.map(this.chart.groupedBy, function (field) { labels = _.map(this.chart.groupedBy, function (field) {
return self._sanitizeValue(data_pt[field], field); return self._sanitizeValue(data_pt[field], field);
}); });
var value = is_count ? data_pt.__count || data_pt[this.chart.groupedBy[0]+'_count'] : data_pt[this.chart.measure]; var count = data_pt.__count || data_pt[this.chart.groupedBy[0]+'_count'] || 0;
var value = is_count ? count : data_pt[this.chart.measure];
if (value instanceof Array) { if (value instanceof Array) {
// when a many2one field is used as a measure AND as a grouped // when a many2one field is used as a measure AND as a grouped
// field, bad things happen. The server will only return the // field, bad things happen. The server will only return the
@@ -188,8 +192,9 @@ return AbstractModel.extend({
value = 1; value = 1;
} }
this.chart.data.push({ this.chart.data.push({
count: count,
value: value, value: value,
labels: labels labels: labels,
}); });
} }
}, },
@@ -203,7 +208,9 @@ return AbstractModel.extend({
*/ */
_sanitizeValue: function (value, field) { _sanitizeValue: function (value, field) {
var fieldName = field.split(':')[0]; var fieldName = field.split(':')[0];
if (value === false && this.fields[fieldName].type !== 'boolean') return _t("Undefined"); if (value === false && this.fields[fieldName].type !== 'boolean') {
return undefined;
}
if (value instanceof Array) return value[1]; if (value instanceof Array) return value[1];
if (field && (this.fields[fieldName].type === 'selection')) { if (field && (this.fields[fieldName].type === 'selection')) {
var selected = _.where(this.fields[fieldName].selection, {0: value})[0]; var selected = _.where(this.fields[fieldName].selection, {0: value})[0];
@@ -120,6 +120,9 @@ return AbstractRenderer.extend({
var data, values; var data, values;
var measure = this.state.fields[this.state.measure].string; 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 // zero groupbys
if (this.state.groupedBy.length === 0) { if (this.state.groupedBy.length === 0) {
data = [{ data = [{
@@ -171,6 +174,13 @@ return AbstractRenderer.extend({
data.push(current_serie); 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 svg = d3.select(this.$el[0]).append('svg');
svg.datum(data); svg.datum(data);
@@ -207,11 +217,15 @@ return AbstractRenderer.extend({
* @returns {nvd3 chart} * @returns {nvd3 chart}
*/ */
_renderPieChart: function () { _renderPieChart: function () {
var self = this;
var data = []; var data = [];
var all_negative = true; var all_negative = true;
var some_negative = false; var some_negative = false;
var all_zero = true; var all_zero = true;
// undefined label value becomes a string 'Undefined' translated
this.state.data.forEach(self._sanitizeLabel);
this.state.data.forEach(function (datapt) { this.state.data.forEach(function (datapt) {
all_negative = all_negative && (datapt.value < 0); all_negative = all_negative && (datapt.value < 0);
some_negative = some_negative || (datapt.value < 0); some_negative = some_negative || (datapt.value < 0);
@@ -238,6 +252,12 @@ return AbstractRenderer.extend({
return {x:datapt.labels.join("/"), y: datapt.value}; 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;
});
var svg = d3.select(this.$el[0]).append('svg'); var svg = d3.select(this.$el[0]).append('svg');
svg.datum(data); svg.datum(data);
@@ -264,31 +284,40 @@ return AbstractRenderer.extend({
* @returns {nvd3 chart} * @returns {nvd3 chart}
*/ */
_renderLineChart: function () { _renderLineChart: function () {
if (this.state.data.length < 2) { var self = this;
// Remove Undefined of first GroupBy
var graphData = _.filter(this.state.data, function(elem){
return elem.labels[0] !== undefined;
});
// 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', { this.$el.append(qweb.render('GraphView.error', {
title: _t("Not enough data points"), title: _t("Not enough data points"),
description: "You need at least two data points to display a line chart." description: _t("You need at least two data points to display a line chart.")
})); }));
return; return;
} }
var self = this;
var data = []; var data = [];
var tickValues; var tickValues;
var tickFormat; var tickFormat;
var measure = this.state.fields[this.state.measure].string; var measure = this.state.fields[this.state.measure].string;
if (this.state.groupedBy.length === 1) { if (this.state.groupedBy.length === 1) {
var values = this.state.data.map(function (datapt, index) { var values = graphData.map(function (datapt, index) {
return {x: index, y: datapt.value}; return {x: index, y: datapt.value};
}); });
data = [ data = [
{ {
values: values, values: values,
key: measure, key: measure,
area: true area: true,
} }
]; ];
tickValues = this.state.data.map(function (d, i) { return i;}); tickValues = graphData.map(function (d, i) { return i;});
tickFormat = function (d) {return self.state.data[d].labels;}; tickFormat = function (d) {return self.state.data[d].labels;};
} }
if (this.state.groupedBy.length > 1) { if (this.state.groupedBy.length > 1) {
@@ -300,13 +329,13 @@ return AbstractRenderer.extend({
var identity = function (p) {return p;}; var identity = function (p) {return p;};
tickValues = []; tickValues = [];
for (var i = 0; i < this.state.data.length; i++) { for (var i = 0; i < this.state.data.length; i++) {
if (this.state.data[i].labels[0] !== tickLabel) { if (graphData[i].labels[0] !== tickLabel) {
tickLabel = this.state.data[i].labels[0]; tickLabel = this.state.data[i].labels[0];
tickValues.push(tick); tickValues.push(tick);
tickLabels.push(tickLabel); tickLabels.push(tickLabel);
tick++; tick++;
} }
serie = this.state.data[i].labels[1]; serie = graphData[i].labels[1];
if (!data_dict[serie]) { if (!data_dict[serie]) {
data_dict[serie] = { data_dict[serie] = {
values: [], values: [],
@@ -314,7 +343,7 @@ return AbstractRenderer.extend({
}; };
} }
data_dict[serie].values.push({ data_dict[serie].values.push({
x: tick, y: this.state.data[i].value, x: tick, y: graphData[i].value,
}); });
data = _.map(data_dict, identity); data = _.map(data_dict, identity);
} }
@@ -333,9 +362,7 @@ return AbstractRenderer.extend({
showLegend: _.size(data) <= MAX_LEGEND_LENGTH, showLegend: _.size(data) <= MAX_LEGEND_LENGTH,
showXAxis: true, showXAxis: true,
showYAxis: true, showYAxis: true,
}); });
chart.xAxis.tickValues(tickValues) chart.xAxis.tickValues(tickValues)
.tickFormat(tickFormat); .tickFormat(tickFormat);
chart.yAxis.tickFormat(function (d) { chart.yAxis.tickFormat(function (d) {
@@ -362,6 +389,20 @@ return AbstractRenderer.extend({
chart.tooltip.chartContainer(this.el); chart.tooltip.chartContainer(this.el);
} }
}, },
/**
* 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;
});
},
}); });
}); });
+38 -5
View File
@@ -16,13 +16,14 @@ QUnit.module('Views', {
bar: {string: "bar", type: "boolean"}, bar: {string: "bar", type: "boolean"},
product_id: {string: "Product", type: "many2one", relation: 'product', store: true}, product_id: {string: "Product", type: "many2one", relation: 'product', store: true},
color_id: {string: "Color", type: "many2one", relation: 'color'}, color_id: {string: "Color", type: "many2one", relation: 'color'},
date: {string: "Date", type: 'date'},
}, },
records: [ records: [
{id: 1, foo: 3, bar: true, product_id: 37}, {id: 1, foo: 3, bar: true, product_id: 37, date: "2016-01-01"},
{id: 2, foo: 53, bar: true, product_id: 37, color_id: 7}, {id: 2, foo: 53, bar: true, product_id: 37, color_id: 7, date: "2016-01-03"},
{id: 3, foo: 2, bar: true, product_id: 37}, {id: 3, foo: 2, bar: true, product_id: 37, date: "2016-03-04"},
{id: 4, foo: 24, bar: false, product_id: 37}, {id: 4, foo: 24, bar: false, product_id: 37, date: "2016-03-07"},
{id: 5, foo: 4, bar: false, product_id: 41}, {id: 5, foo: 4, bar: false, product_id: 41, date: "2016-05-01"},
{id: 6, foo: 63, bar: false, product_id: 41}, {id: 6, foo: 63, bar: false, product_id: 41},
{id: 7, foo: 42, bar: false, product_id: 41}, {id: 7, foo: 42, bar: false, product_id: 41},
] ]
@@ -310,6 +311,9 @@ QUnit.module('Views', {
var done = assert.async(); var done = assert.async();
assert.expect(6); assert.expect(6);
var lastOne = _.last(this.data.foo.records);
lastOne.color_id = 14;
var graph = createView({ var graph = createView({
View: GraphView, View: GraphView,
model: "foo", model: "foo",
@@ -350,6 +354,9 @@ QUnit.module('Views', {
var done = assert.async(); var done = assert.async();
assert.expect(2); assert.expect(2);
var lastOne = _.last(this.data.foo.records);
lastOne.color_id = 14;
var graph = createView({ var graph = createView({
View: GraphView, View: GraphView,
model: 'foo', model: 'foo',
@@ -377,6 +384,9 @@ QUnit.module('Views', {
var done = assert.async(); var done = assert.async();
assert.expect(8); assert.expect(8);
var lastOne = _.last(this.data.foo.records);
lastOne.color_id = 14;
var graph = createView({ var graph = createView({
View: GraphView, View: GraphView,
model: "foo", model: "foo",
@@ -646,6 +656,29 @@ QUnit.module('Views', {
graph.destroy(); graph.destroy();
}); });
QUnit.test('Undefined should appear in bar, pie graph but not in line graph', function (assert) {
assert.expect(4);
var graph = createView({
View: GraphView,
model: "foo",
groupBy:['date'],
data: this.data,
arch: '<graph string="Partners" type="line">' +
'<field name="bar"/>' +
'</graph>',
});
assert.strictEqual(graph.$("svg.nvd3-svg:contains('Undefined')").length, 0);
assert.strictEqual(graph.$("svg.nvd3-svg:contains('January')").length, 1);
graph.$buttons.find('.o_graph_button[data-mode=bar]').click();
assert.strictEqual(graph.$("svg.nvd3-svg:contains('Undefined')").length, 1);
assert.strictEqual(graph.$("svg.nvd3-svg:contains('January')").length, 1);
graph.destroy();
});
}); });
}); });