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 6d50dd8c269..fa9a7180e06 100644 --- a/addons/web/static/src/js/views/graph/graph_model.js +++ b/addons/web/static/src/js/views/graph/graph_model.js @@ -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, 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 d58bf2fcdf6..9f6fe65cd5a 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -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 = $('
', {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 = $('', {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 = $('', {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(); } }, /** diff --git a/addons/web/static/src/js/views/graph/graph_view.js b/addons/web/static/src/js/views/graph/graph_view.js index 07c486cf5b7..23ab4cecd01 100644 --- a/addons/web/static/src/js/views/graph/graph_view.js +++ b/addons/web/static/src/js/views/graph/graph_view.js @@ -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; }, }); diff --git a/addons/web/static/src/scss/graph_view.scss b/addons/web/static/src/scss/graph_view.scss index 900299949e9..7c4f33ca9cf 100644 --- a/addons/web/static/src/scss/graph_view.scss +++ b/addons/web/static/src/scss/graph_view.scss @@ -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; + } } } } diff --git a/addons/web/static/tests/views/graph_tests.js b/addons/web/static/tests/views/graph_tests.js index 3dbb298e23c..eea3729c3f6 100644 --- a/addons/web/static/tests/views/graph_tests.js +++ b/addons/web/static/tests/views/graph_tests.js @@ -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', { '', }); 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(); });