summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/graph/graph_renderer.js
diff options
context:
space:
mode:
authorGéry Debongnie <ged@odoo.com>2017-03-05 10:05:56 +0100
committerAaron Bohy <aab@odoo.com>2017-04-11 19:44:38 +0200
commit905e01921f3c3ef43ace6fc537d1d8c0d280002c (patch)
tree265c43ab21c5401a5d9f0f3d64d86f1ac417451c /addons/web/static/src/js/views/graph/graph_renderer.js
parent54d4c73af4ad05c7640d28c1892247c9538b8017 (diff)
[REF] web, *: redesign all JS views
This commit introduce a full redesign of all JS views. We started basically from scratch. The goal was to unify all the various views under a common framework, to make them testable, to make then usable in different conditions (in studio, or in the frontend), and to make our lives easier. Some important points are: - we introduced new coding guidelines (camelCase, 80 chars width, ...) - we have a brand new testing framework (still QUnit based) - kanban view moved to the web addon - calendar view (formerly web_calender) moved to web as well - the tree view was removed - all new code should be documented We hope that this code is the start of a new era for the Odoo web client, we want to have a high quality codebase, well documented, well tested, well designed. Work done by the framework team: mostly aab, ged, chm, dmo, qsm
Diffstat (limited to 'addons/web/static/src/js/views/graph/graph_renderer.js')
-rw-r--r--addons/web/static/src/js/views/graph/graph_renderer.js334
1 files changed, 334 insertions, 0 deletions
diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js
new file mode 100644
index 00000000000..e5fe00cc473
--- /dev/null
+++ b/addons/web/static/src/js/views/graph/graph_renderer.js
@@ -0,0 +1,334 @@
+odoo.define('web.GraphRenderer', function (require) {
+"use strict";
+
+/**
+ * The graph renderer turns the data from the graph model into a nice looking
+ * svg chart. This code uses the nvd3 library.
+ *
+ * Note that we use a custom build for the nvd3, with only the model we actually
+ * use.
+ */
+
+var AbstractRenderer = require('web.AbstractRenderer');
+var config = require('web.config');
+var core = require('web.core');
+var formats = require('web.formats');
+
+var _t = core._t;
+var qweb = core.qweb;
+
+var CHART_TYPES = ['pie', 'bar', 'line'];
+
+// hide top legend when too many items for device size
+var MAX_LEGEND_LENGTH = 25 * (1 + config.device.size_class);
+
+return AbstractRenderer.extend({
+ className: "o_graph_svg_container",
+ /**
+ * @override
+ * @param {Widget} parent
+ * @param {Object} state
+ * @param {Object} params
+ * @param {boolean} params.stacked
+ */
+ init: function (parent, state, params) {
+ this._super.apply(this, arguments);
+ this.stacked = params.stacked;
+ this.$el.css({minWidth: '100px', minHeight: '100px'});
+ },
+ /**
+ * @override
+ */
+ destroy: function () {
+ nv.utils.offWindowResize(this.to_remove);
+ this._super();
+ },
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Render the chart.
+ *
+ * Note that This method is synchronous, but the actual rendering is done
+ * asynchronously (in a setTimeout). The reason for that is that nvd3/d3
+ * needs to be in the DOM to correctly render itself. So, we trick Odoo by
+ * returning immediately, then wait a tiny interval before actually
+ * displaying the data.
+ *
+ * @returns {Deferred} The _super deferred is actually resolved immediately
+ */
+ _render: function () {
+ if (this.to_remove) {
+ nv.utils.offWindowResize(this.to_remove);
+ }
+ if (!_.contains(CHART_TYPES, this.state.mode)) {
+ this.$el.empty();
+ this.trigger_up('warning', {
+ title: _t('Invalid mode for chart'),
+ message: _t('Cannot render chart with mode : ') + this.state.mode
+ });
+ } else if (!this.state.data.length) {
+ this.$el.empty();
+ this.$el.append(qweb.render('GraphView.error', {
+ title: _t("No data to display"),
+ description: _t("No data available for this chart. " +
+ "Try to add some records, or make sure that " +
+ "there is no active filter in the search bar."),
+ }));
+ } else {
+ var self = this;
+ setTimeout(function () {
+ self.$el.empty();
+ var chart = self['_render' + _.str.capitalize(self.state.mode) + 'Chart']();
+ if (chart) {
+ self.to_remove = chart.update;
+ nv.utils.onWindowResize(chart.update);
+ chart.tooltip.chartContainer(self.el);
+ }
+ }, 0);
+ }
+ return this._super.apply(this, arguments);
+ },
+
+ /**
+ * Helper function to set up data properly for the multiBarChart model in
+ * nvd3.
+ *
+ * @returns {nvd3 chart}
+ */
+ _renderBarChart: function () {
+ // prepare data for bar chart
+ var self = this;
+ var data, values;
+ var measure = this.state.fields[this.state.measure].string;
+
+ // zero groupbys
+ if (this.state.groupedBy.length === 0) {
+ data = [{
+ values: [{
+ x: measure,
+ 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,
+ }
+ ];
+ }
+ if (this.state.groupedBy.length > 1) {
+ var xlabels = [],
+ series = [],
+ label, serie, value;
+ values = {};
+ for (var i = 0; i < this.state.data.length; i++) {
+ label = this.state.data[i].labels[0];
+ serie = this.state.data[i].labels[1];
+ value = this.state.data[i].value;
+ if ((!xlabels.length) || (xlabels[xlabels.length-1] !== label)) {
+ xlabels.push(label);
+ }
+ series.push(this.state.data[i].labels[1]);
+ if (!(serie in values)) {values[serie] = {};}
+ values[serie][label] = this.state.data[i].value;
+ }
+ series = _.uniq(series);
+ data = [];
+ var current_serie, j;
+ for (i = 0; i < series.length; i++) {
+ current_serie = {values: [], key: series[i]};
+ for (j = 0; j < xlabels.length; j++) {
+ current_serie.values.push({
+ x: xlabels[j],
+ y: values[series[i]][xlabels[j]] || 0,
+ });
+ }
+ data.push(current_serie);
+ }
+ }
+ var svg = d3.select(this.$el[0]).append('svg');
+ svg.datum(data);
+
+ svg.transition().duration(0);
+
+ var chart = nv.models.multiBarChart();
+ chart.options({
+ margin: {left: 120, bottom: 60},
+ delay: 250,
+ transition: 10,
+ showLegend: _.size(data) <= MAX_LEGEND_LENGTH,
+ showXAxis: true,
+ showYAxis: true,
+ rightAlignYAxis: false,
+ stacked: this.stacked,
+ reduceXTicks: false,
+ rotateLabels: -20,
+ showControls: (this.state.groupedBy.length > 1)
+ });
+ chart.yAxis.tickFormat(function (d) {
+ var measure_field = self.state.fields[self.measure];
+ return formats.format_value(d, {
+ type: 'float',
+ digits: measure_field && measure_field.digits || [69, 2],
+ });
+ });
+
+ chart(svg);
+ return chart;
+ },
+ /**
+ * Helper function to set up data properly for the pieChart model in
+ * nvd3.
+ *
+ * @returns {nvd3 chart}
+ */
+ _renderPieChart: function () {
+ var data = [];
+ var all_negative = true;
+ var some_negative = false;
+ var all_zero = true;
+
+ this.state.data.forEach(function (datapt) {
+ all_negative = all_negative && (datapt.value < 0);
+ some_negative = some_negative || (datapt.value < 0);
+ all_zero = all_zero && (datapt.value === 0);
+ });
+ if (some_negative && !all_negative) {
+ this.$el.append(qweb.render('GraphView.error', {
+ title: _t("Invalid data"),
+ description: _t("Pie chart cannot mix positive and negative numbers. " +
+ "Try to change your domain to only display positive results"),
+ }));
+ return;
+ }
+ if (all_zero) {
+ this.$el.append(qweb.render('GraphView.error', {
+ title: _t("Invalid data"),
+ description: _t("Pie chart cannot display all zero numbers.. " +
+ "Try to change your domain to display positive results"),
+ }));
+ return;
+ }
+ if (this.state.groupedBy.length) {
+ data = this.state.data.map(function (datapt) {
+ return {x:datapt.labels.join("/"), y: datapt.value};
+ });
+ }
+ var svg = d3.select(this.$el[0]).append('svg');
+ svg.datum(data);
+
+ svg.transition().duration(100);
+
+ var legend_right = config.device.size_class > config.device.SIZES.XS;
+
+ var chart = nv.models.pieChart().labelType('percent');
+ chart.options({
+ delay: 250,
+ showLegend: legend_right || _.size(data) <= MAX_LEGEND_LENGTH,
+ legendPosition: legend_right ? 'right' : 'top',
+ transition: 100,
+ color: d3.scale.category10().range(),
+ });
+
+ chart(svg);
+ return chart;
+ },
+ /**
+ * Helper function to set up data properly for the line model in
+ * nvd3.
+ *
+ * @returns {nvd3 chart}
+ */
+ _renderLineChart: function () {
+ if (this.state.data.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."
+ }));
+ 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) {
+ return {x: index, y: datapt.value};
+ });
+ data = [
+ {
+ values: values,
+ key: measure,
+ }
+ ];
+ tickValues = this.state.data.map(function (d, i) { return i;});
+ tickFormat = function (d) {return self.state.data[d].labels;};
+ }
+ if (this.state.groupedBy.length > 1) {
+ data = [];
+ var data_dict = {};
+ var tick = 0;
+ var tickLabels = [];
+ var serie, tickLabel;
+ 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) {
+ tickLabel = this.state.data[i].labels[0];
+ tickValues.push(tick);
+ tickLabels.push(tickLabel);
+ tick++;
+ }
+ serie = this.state.data[i].labels[1];
+ if (!data_dict[serie]) {
+ data_dict[serie] = {
+ values: [],
+ key: serie,
+ };
+ }
+ data_dict[serie].values.push({
+ x: tick, y: this.state.data[i].value,
+ });
+ data = _.map(data_dict, identity);
+ }
+ tickFormat = function (d) {return tickLabels[d];};
+ }
+
+ var svg = d3.select(this.$el[0]).append('svg');
+ svg.datum(data);
+
+ svg.transition().duration(0);
+
+ var chart = nv.models.lineChart();
+ chart.options({
+ margin: {left: 120, bottom: 60},
+ useInteractiveGuideline: true,
+ showLegend: _.size(data) <= MAX_LEGEND_LENGTH,
+ showXAxis: true,
+ showYAxis: true,
+ });
+ chart.xAxis.tickValues(tickValues)
+ .tickFormat(tickFormat);
+ chart.yAxis.tickFormat(function (d) {
+ return formats.format_value(d, {
+ type : 'float',
+ digits : self.state.fields[self.state.measure] && self.state.fields[self.state.measure].digits || [69, 2],
+ });
+ });
+
+ chart(svg);
+ return chart;
+ },
+});
+
+});