From 3d2ca8104e46721b5db319a2aeb0ee971cb009d7 Mon Sep 17 00:00:00 2001 From: Francois Volral Date: Mon, 9 Jul 2018 14:58:05 +0200 Subject: [PATCH] [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 --- .../static/src/js/views/graph/graph_model.js | 15 +++-- .../src/js/views/graph/graph_renderer.js | 65 +++++++++++++++---- addons/web/static/tests/views/graph_tests.js | 43 ++++++++++-- 3 files changed, 102 insertions(+), 21 deletions(-) diff --git a/addons/web/static/src/js/views/graph/graph_model.js b/addons/web/static/src/js/views/graph/graph_model.js index 3c1d26ea252..6d50dd8c269 100644 --- a/addons/web/static/src/js/views/graph/graph_model.js +++ b/addons/web/static/src/js/views/graph/graph_model.js @@ -135,6 +135,7 @@ return AbstractModel.extend({ var fields = _.map(groupedBy, function (groupBy) { return groupBy.split(':')[0]; }); + if (this.chart.measure !== '__count__') { if (this.fields[this.chart.measure].type === 'many2one') { fields = fields.concat(this.chart.measure + ":count_distinct"); @@ -143,10 +144,12 @@ return AbstractModel.extend({ fields = fields.concat(this.chart.measure); } } + + var context = _.extend({fill_temporal: true}, this.chart.context); return this._rpc({ model: this.modelName, method: 'read_group', - context: this.chart.context, + context: context, domain: this.chart.domain, fields: fields, groupBy: groupedBy, @@ -176,7 +179,8 @@ return AbstractModel.extend({ labels = _.map(this.chart.groupedBy, function (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) { // when a many2one field is used as a measure AND as a grouped // field, bad things happen. The server will only return the @@ -188,8 +192,9 @@ return AbstractModel.extend({ value = 1; } this.chart.data.push({ + count: count, value: value, - labels: labels + labels: labels, }); } }, @@ -203,7 +208,9 @@ return AbstractModel.extend({ */ _sanitizeValue: function (value, field) { 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 (field && (this.fields[fieldName].type === 'selection')) { var selected = _.where(this.fields[fieldName].selection, {0: value})[0]; 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 ce46087e9c5..c1ab6276123 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -120,6 +120,9 @@ return AbstractRenderer.extend({ var data, 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 = [{ @@ -171,6 +174,13 @@ 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'); svg.datum(data); @@ -207,11 +217,15 @@ return AbstractRenderer.extend({ * @returns {nvd3 chart} */ _renderPieChart: function () { + 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 + this.state.data.forEach(self._sanitizeLabel); + this.state.data.forEach(function (datapt) { all_negative = all_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}; }); } + + // 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'); svg.datum(data); @@ -264,31 +284,40 @@ return AbstractRenderer.extend({ * @returns {nvd3 chart} */ _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', { 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; } - var self = this; var data = []; var tickValues; var tickFormat; var measure = this.state.fields[this.state.measure].string; 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}; }); data = [ { values: values, 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;}; } if (this.state.groupedBy.length > 1) { @@ -300,13 +329,13 @@ return AbstractRenderer.extend({ var identity = function (p) {return p;}; tickValues = []; 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]; tickValues.push(tick); tickLabels.push(tickLabel); tick++; } - serie = this.state.data[i].labels[1]; + serie = graphData[i].labels[1]; if (!data_dict[serie]) { data_dict[serie] = { values: [], @@ -314,7 +343,7 @@ return AbstractRenderer.extend({ }; } data_dict[serie].values.push({ - x: tick, y: this.state.data[i].value, + x: tick, y: graphData[i].value, }); data = _.map(data_dict, identity); } @@ -333,9 +362,7 @@ return AbstractRenderer.extend({ showLegend: _.size(data) <= MAX_LEGEND_LENGTH, showXAxis: true, showYAxis: true, - }); - chart.xAxis.tickValues(tickValues) .tickFormat(tickFormat); chart.yAxis.tickFormat(function (d) { @@ -343,7 +370,7 @@ return AbstractRenderer.extend({ digits : self.state.fields[self.state.measure] && self.state.fields[self.state.measure].digits || [69, 2], }); }); - + chart(svg); return chart; }, @@ -362,6 +389,20 @@ return AbstractRenderer.extend({ 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; + }); + }, }); }); diff --git a/addons/web/static/tests/views/graph_tests.js b/addons/web/static/tests/views/graph_tests.js index 1ed9e799a4a..3dbb298e23c 100644 --- a/addons/web/static/tests/views/graph_tests.js +++ b/addons/web/static/tests/views/graph_tests.js @@ -16,13 +16,14 @@ QUnit.module('Views', { bar: {string: "bar", type: "boolean"}, product_id: {string: "Product", type: "many2one", relation: 'product', store: true}, color_id: {string: "Color", type: "many2one", relation: 'color'}, + date: {string: "Date", type: 'date'}, }, records: [ - {id: 1, foo: 3, bar: true, product_id: 37}, - {id: 2, foo: 53, bar: true, product_id: 37, color_id: 7}, - {id: 3, foo: 2, bar: true, product_id: 37}, - {id: 4, foo: 24, bar: false, product_id: 37}, - {id: 5, foo: 4, bar: false, product_id: 41}, + {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, date: "2016-01-03"}, + {id: 3, foo: 2, bar: true, product_id: 37, date: "2016-03-04"}, + {id: 4, foo: 24, bar: false, product_id: 37, date: "2016-03-07"}, + {id: 5, foo: 4, bar: false, product_id: 41, date: "2016-05-01"}, {id: 6, foo: 63, bar: false, product_id: 41}, {id: 7, foo: 42, bar: false, product_id: 41}, ] @@ -309,6 +310,9 @@ QUnit.module('Views', { QUnit.test('correctly uses graph_ keys from the context', function (assert) { var done = assert.async(); assert.expect(6); + + var lastOne = _.last(this.data.foo.records); + lastOne.color_id = 14; var graph = createView({ View: GraphView, @@ -350,6 +354,9 @@ QUnit.module('Views', { var done = assert.async(); assert.expect(2); + var lastOne = _.last(this.data.foo.records); + lastOne.color_id = 14; + var graph = createView({ View: GraphView, model: 'foo', @@ -377,6 +384,9 @@ QUnit.module('Views', { var done = assert.async(); assert.expect(8); + var lastOne = _.last(this.data.foo.records); + lastOne.color_id = 14; + var graph = createView({ View: GraphView, model: "foo", @@ -643,6 +653,29 @@ QUnit.module('Views', { "Bouh should be the first measure"); assert.strictEqual(graph.$buttons.find('.o_graph_measures_list .dropdown-item:last').data('field'), '__count__', "Count should be the last measure"); + + 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: '' + + '' + + '', + }); + + 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(); });