[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:
@@ -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;
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user