Currently working for bar charts
[odoo/odoo.git] / addons / web_graph / static / src / js / graph.js
1 /*---------------------------------------------------------
2  * OpenERP web_graph
3  *---------------------------------------------------------*/
4
5 openerp.web_graph = function (instance) {
6
7 var _lt = instance.web._lt;
8
9 // removed ``undefined`` values
10 var filter_values = function (o) {
11     var out = {};
12     for (var k in o) {
13         if (!o.hasOwnProperty(k) || o[k] === undefined) { continue; }
14         out[k] = o[k];
15     }
16     return out;
17 };
18
19 instance.web.views.add('graph', 'instance.web_graph.GraphView');
20 instance.web_graph.GraphView = instance.web.View.extend({
21     template: "GraphView",
22     display_name: _lt('Graph'),
23     view_type: "graph",
24
25     init: function(parent, dataset, view_id, options) {
26         var self = this;
27         this._super(parent);
28         this.set_default_options(options);
29         this.dataset = dataset;
30         this.view_id = view_id;
31
32         this.mode = "bar";          // line, bar, area, pie, radar
33         this.orientation = false;    // true: horizontal, false: vertical
34         this.stacked = true;
35
36         this.spreadsheet = false;   // Display data grid, allows copy to CSV
37         this.forcehtml = false;
38         this.legend = "top";        // top, inside, no
39
40         this.domain = [];
41         this.context = {};
42         this.group_by = [];
43
44         this.graph = null;
45         this.on('view_loaded', self, self.load_graph);
46     },
47     destroy: function () {
48         if (this.graph) {
49             this.graph.destroy();
50         }
51         this._super();
52     },
53
54     load_graph: function(fields_view_get) {
55         // TODO: move  to load_view and document
56         var self = this;
57         this.fields_view = fields_view_get;
58         this.$el.addClass(this.fields_view.arch.attrs['class']);
59
60         this.mode = this.fields_view.arch.attrs.type || 'bar';
61         this.orientation = this.fields_view.arch.attrs.orientation == 'horizontal';
62
63         var width = this.$el.parent().width();
64         this.$el.css("width", width);
65         this.container = this.$el.find("#editor-render-body").css({
66             width: width,
67             height: Math.min(500, width * 0.8)
68         })[0];
69
70         var graph_render = this.proxy('graph_render');
71         this.$el.on('click', '.oe_graph_options a', function (evt) {
72             var $el = $(evt.target);
73
74             self.graph_render({data: filter_values({
75                 mode: $el.data('mode'),
76                 legend: $el.data('legend'),
77                 orientation: $el.data('orientation'),
78                 stacked: $el.data('stacked')
79             })});
80         });
81
82         this.$el.find("#graph_show_data").click(function () {
83             self.spreadsheet = ! self.spreadsheet;
84             self.graph_render();
85         });
86         this.$el.find("#graph_switch").click(function () {
87             if (self.mode != 'radar') {
88                 self.orientation = ! self.orientation;
89             }
90             self.graph_render();
91         });
92
93         this.$el.find("#graph_download").click(function () {
94             if (self.legend == "top") { self.legend = "inside"; }
95             self.forcehtml = true;
96
97             self.graph_get_data().then(function (result) {
98                 self.graph_render_all(result).download.saveImage('png');
99             }).always(function () {
100                 self.forcehtml = false;
101             });
102         });
103         this.trigger('graph_view_loaded', fields_view_get);
104     },
105
106     get_format: function (options) {
107         options = options || {};
108         var legend = {
109             show: this.legend != 'no',
110         };
111
112         switch (this.legend) {
113         case 'top':
114             legend.noColumns = 4;
115             legend.container = this.$el.find("div.graph_header_legend")[0];
116             break;
117         case 'inside':
118             legend.position = 'nw';
119             legend.backgroundColor = '#D2E8FF';
120             break;
121         }
122
123         return _.extend({
124             legend: legend,
125             mouse: {
126                 track: true,
127                 relative: true
128             },
129             spreadsheet : {
130                 show: this.spreadsheet,
131                 initialTab: "data"
132             },
133             HtmlText : (options.xaxis && options.xaxis.labelsAngle) ? false : !this.forcehtml,
134         }, options);
135     },
136
137     make_graph: function (mode, container, data) {
138         if (mode === 'area') { mode = 'line'; }
139         return Flotr.draw(
140             container, data.data,
141             this.get_format(this['options_' + mode](data)));
142     },
143
144     options_bar: function (data) {
145         var min = _(data.data).chain()
146             .map(function (record) {
147                 if (record.data.length > 0){
148                         return _.min(record.data, function (item) {
149                             return item[1];
150                         })[1];
151                 }
152             }).min().value();
153         return {
154             bars : {
155                 show : true,
156                 stacked : this.stacked,
157                 horizontal : this.orientation,
158                 barWidth : 0.7,
159                 lineWidth : 1
160             },
161             grid : {
162                 verticalLines : this.orientation,
163                 horizontalLines : !this.orientation,
164                 outline : "sw",
165             },
166             yaxis : {
167                 ticks: this.orientation ? data.ticks : false,
168                 min: !this.orientation ? (min < 0 ? min : 0) : null
169             },
170             xaxis : {
171                 labelsAngle: 45,
172                 ticks: this.orientation ? false : data.ticks,
173                 min: this.orientation ? (min < 0 ? min : 0) : null
174             }
175         };
176     },
177
178     options_pie: function (data) {
179         return {
180             pie : {
181                 show: true
182             },
183             grid : {
184                 verticalLines : false,
185                 horizontalLines : false,
186                 outline : "",
187             },
188             xaxis :  {showLabels: false},
189             yaxis :  {showLabels: false},
190         };
191     },
192
193     options_radar: function (data) {
194         return {
195             radar : {
196                 show : true,
197                 stacked : this.stacked
198             },
199             grid : {
200                 circular : true,
201                 minorHorizontalLines : true
202             },
203             xaxis : {
204                 ticks: data.ticks
205             },
206         };
207     },
208
209     options_line: function (data) {
210         return {
211             lines : {
212                 show : true,
213                 stacked : this.stacked
214             },
215             grid : {
216                 verticalLines : this.orientation,
217                 horizontalLines : !this.orientation,
218                 outline : "sw",
219             },
220             yaxis : {
221                 ticks: this.orientation ? data.ticks : false
222             },
223             xaxis : {
224                 labelsAngle: 45,
225                 ticks: this.orientation ? false : data.ticks
226             }
227         };
228     },
229
230     graph_get_data: function () {
231         var model = this.dataset.model,
232             domain = new instance.web.CompoundDomain(this.domain || []),
233             context = new instance.web.CompoundContext(this.context || {}),
234             group_by = this.group_by || [],
235             view_id = this.view_id  || false,
236             mode = this.mode || 'bar',
237             orientation = this.orientation || false,
238             stacked = this.stacked || false;
239
240         var obj = new instance.web.Model(model);
241         var view_get;
242         var fields;
243         var result = [];
244         var ticks = {};
245
246         return obj.call("fields_view_get", [view_id, 'graph']).pipe(function(tmp) {
247             view_get = tmp;
248             fields = view_get['fields'];
249             var toload = _.select(group_by, function(x) { return fields[x] === undefined });
250             if (toload.length >= 1)
251                 return obj.call("fields_get", [toload, context]);
252             else
253                 return $.when([]);
254         }).pipe(function (fields_to_add) {
255             _.extend(fields, fields_to_add);
256
257             var tree = $($.parseXML(view_get['arch']));
258             
259             var pos = 0;
260             var xaxis = group_by || [];
261             var yaxis = [];
262             tree.find("field").each(function() {
263                 var field = $(this);
264                 if (! field.attr("name"))
265                     return;
266                 if ((group_by.length == 0) && ((! pos) || field.attr('group'))) {
267                     xaxis.push(field.attr('name'));
268                 }
269                 if (pos && ! field.attr('group')) {
270                     yaxis.push(field.attr('name'));
271                 }
272                 pos += 1;
273             });
274
275             if (xaxis.length === 0)
276                 throw new Error("No field for the X axis!");
277             if (yaxis.length === 0)
278                 throw new Error("No field for the Y axis!");
279
280             // Convert a field's data into a displayable string
281
282             function _convert_key(field, data) {
283                 if (fields[field]['type'] === 'many2one')
284                     data = data && data[0];
285                 return data;
286             }
287
288             function _convert(field, data, tick) {
289                 tick = tick === undefined ? true : false;
290                 if (fields[field]['type'] === 'many2one') {
291                     data = data && data[1];
292                 } else if ((fields[field]['type'] === 'selection') && (fields[field]['selection'] instanceof Array)) {
293                     var d = {};
294                     _.each(fields[field]['selection'], function(el) {
295                         d[el[0]] = el[1];
296                     });
297                     data = d[data];
298                 }
299                 if (tick) {
300                     if (ticks[data] === undefined)
301                         ticks[data] = _.size(ticks);
302                     return ticks[data];
303                 }
304                 return data || 0;
305             }
306
307             function _orientation(x, y) {
308                 if (! orientation)
309                     return [x, y]
310                 return [y, x]
311             }
312
313             if (mode === "pie") {
314                 return obj.call("read_group", [domain, yaxis.concat([xaxis[0]]), [xaxis[0]]], {context: context}).pipe(function(res) {
315                     _.each(res, function(record) {
316                         result.push({
317                             'data': [[_convert(xaxis[0], record[xaxis[0]]), record[yaxis[0]]]],
318                             'label': _convert(xaxis[0], record[xaxis[0]], false)
319                         });
320                     });
321                 });
322             } else if ((! stacked) || (xaxis.length < 2)) {
323                 var defs = [];
324                 _.each(xaxis, function(x) {
325                     defs.push(obj.call("read_group", [domain, yaxis.concat([x]), [x]], {context: context}).pipe(function(res) {
326                         return [x, res];
327                     }));
328                 });
329                 return $.when.apply($, defs).pipe(function() {
330                     _.each(_.toArray(arguments), function(res) {
331                         var x = res[0];
332                         res = res[1];
333                         result.push({
334                             'data': _.map(res, function(record) {
335                                 return _orientation(_convert(x, record[x]), record[yaxis[0]] || 0);
336                             }),
337                             'label': fields[x]['string']
338                         });
339                     });
340                 });
341             } else {
342                 xaxis.reverse();
343                 return obj.call("read_group", [domain, yaxis.concat(xaxis.slice(0, 1)), xaxis.slice(0, 1)], {context: context}).pipe(function(axis) {
344                     var defs = [];
345                     _.each(axis, function(x) {
346                         var key = x[xaxis[0]]
347                         defs.push(obj.call("read_group", [domain+[(xaxis[0],'=',_convert_key(xaxis[0], key))], yaxis.concat(xaxis.slice(1, 2)), xaxis.slice(1, 2)],
348                                 {context: context}).pipe(function(res) {
349                             return [x, key, res];
350                         }));
351                     });
352                     return $.when.apply($, defs).pipe(function(res) {
353                         var x = res[0];
354                         var key = res[1];
355                         res = res[2];
356                         result.push({
357                             'data': _.map(res, function(record) {
358                                 return _orientation(_convert(xaxis[1], record[xaxis[1]]), record[yaxis[0]] || 0);
359                             }),
360                             'label': _convert(xaxis[0], key, false)
361                         })
362                     });
363                 });
364             }
365         }).pipe(function() {
366             var res = {
367                 'data': result,
368                 'ticks': _.map(ticks, function(el, key) { return [el, key] })
369             };
370             return res;
371         });
372     },
373
374     // Render the graph and update menu styles
375     graph_render: function (options) {
376         options = options || {};
377         _.extend(this, options.data);
378
379         return this.graph_get_data()
380             .then(this.proxy('graph_render_all'));
381     },
382
383     graph_render_all: function (data) {
384         var i;
385         if (this.mode=='area') {
386             for (i=0; i<data.data.length; i++) {
387                 data.data[i].lines = {fill: true}
388             }
389         }
390         if (this.graph) {
391             this.graph.destroy();
392         }
393
394         // Render the graph
395         this.$el.find(".graph_header_legend").children().remove();
396         this.graph = this.make_graph(this.mode, this.container, data);
397
398         // Update styles of menus
399
400         this.$el.find("a").removeClass("active");
401
402         var $active = this.$el.find('a[data-mode=' + this.mode + ']');
403         if ($active.length > 1) {
404             $active = $active.filter('[data-stacked=' + this.stacked + ']');
405         }
406         $active = $active.add(
407             this.$el.find('a:not([data-mode])[data-legend=' + this.legend + ']'));
408
409         $active.addClass('active');
410
411         if (this.spreadsheet) {
412             this.$el.find("#graph_show_data").addClass("active");
413         }
414         return this.graph;
415     },
416
417     // render the graph using the domain, context and group_by
418     // calls the 'graph_data_get' python controller to process all data
419     // TODO: check is group_by should better be in the context
420     do_search: function(domain, context, group_by) {
421         this.domain = domain;
422         this.context = context;
423         this.group_by = group_by;
424
425         this.graph_render();
426     },
427
428     do_show: function() {
429         this.do_push_state({});
430         return this._super();
431     },
432 });
433 };