f9c95a3c1b6da01a71606426816e3c81e6a13b68
[odoo/odoo.git] / addons / web_graph / static / src / js / graph.js
1 /*---------------------------------------------------------
2  * OpenERP web_graph
3  *---------------------------------------------------------*/
4
5 openerp.web_graph = function (openerp) {
6 var COLOR_PALETTE = [
7     '#cc99ff', '#ccccff', '#48D1CC', '#CFD784', '#8B7B8B', '#75507b',
8     '#b0008c', '#ff0000', '#ff8e00', '#9000ff', '#0078ff', '#00ff00',
9     '#e6ff00', '#ffff00', '#905000', '#9b0000', '#840067', '#9abe00',
10     '#ffc900', '#510090', '#0000c9', '#009b00', '#75507b', '#3465a4',
11     '#73d216', '#c17d11', '#edd400', '#fcaf3e', '#ef2929', '#ff00c9',
12     '#ad7fa8', '#729fcf', '#8ae234', '#e9b96e', '#fce94f', '#f57900',
13     '#cc0000', '#d400a8'];
14
15 var QWeb = openerp.web.qweb,
16      _lt = openerp.web._lt;
17 openerp.web.views.add('graph', 'openerp.web_graph.GraphView');
18 openerp.web_graph.GraphView = openerp.web.View.extend({
19     display_name: _lt('Graph'),
20
21     init: function(parent, dataset, view_id, options) {
22         this._super(parent);
23         this.set_default_options(options);
24         this.dataset = dataset;
25         this.view_id = view_id;
26
27         this.first_field = null;
28         this.abscissa = null;
29         this.ordinate = null;
30         this.columns = [];
31         this.group_field = null;
32         this.is_loaded = $.Deferred();
33
34         this.renderer = null;
35     },
36     stop: function () {
37         if (this.renderer) {
38             clearTimeout(this.renderer);
39         }
40         this._super();
41     },
42     start: function() {
43         var self = this;
44         this._super();
45         var loaded;
46         if (this.embedded_view) {
47             loaded = $.when([self.embedded_view]);
48         } else {
49             loaded = this.rpc('/web/view/load', {
50                     model: this.dataset.model,
51                     view_id: this.view_id,
52                     view_type: 'graph'
53             });
54         }
55         return $.when(
56             this.dataset.call_and_eval('fields_get', [false, {}], null, 1),
57             loaded)
58             .then(function (fields_result, view_result) {
59                 self.fields = fields_result[0];
60                 self.fields_view = view_result[0];
61                 self.on_loaded(self.fields_view);
62             });
63     },
64     /**
65      * Returns all object fields involved in the graph view
66      */
67     list_fields: function () {
68         var fs = [this.abscissa];
69         fs.push.apply(fs, _(this.columns).pluck('name'));
70         if (this.group_field) {
71             fs.push(this.group_field);
72         }
73         return _.uniq(fs);
74     },
75     on_loaded: function() {
76         this.chart = this.fields_view.arch.attrs.type || 'pie';
77         this.orientation = this.fields_view.arch.attrs.orientation || 'vertical';
78
79         _.each(this.fields_view.arch.children, function (field) {
80             var attrs = field.attrs;
81             if (attrs.group) {
82                 this.group_field = attrs.name;
83             } else if(!this.abscissa) {
84                 this.first_field = this.abscissa = attrs.name;
85             } else {
86                 this.columns.push({
87                     name: attrs.name,
88                     operator: attrs.operator || '+'
89                 });
90             }
91         }, this);
92         this.ordinate = this.columns[0].name;
93         this.is_loaded.resolve();
94     },
95     schedule_chart: function(results) {
96         var self = this;
97         this.$element.html(QWeb.render("GraphView", {
98             "fields_view": this.fields_view,
99             "chart": this.chart,
100             'element_id': this.widget_parent.element_id
101         }));
102
103         var fields = _(this.columns).pluck('name').concat([this.abscissa]);
104         if (this.group_field) { fields.push(this.group_field); }
105         // transform search result into usable records (convert from OpenERP
106         // value shapes to usable atomic types
107         if(self.fields[self.abscissa].type == 'selection'){
108            results = self.sortSelection(results);
109         }
110         var records = _(results).map(function (result) {
111             var point = {};
112             _(result).each(function (value, field) {
113                 if (!_(fields).contains(field)) { return; }
114                 if (value === false) { point[field] = false; return; }
115                 switch (self.fields[field].type) {
116                 case 'selection':
117                     point[field] = _(self.fields[field].selection).detect(function (choice) {
118                         return choice[0] === value;
119                     })[1];
120                     break;
121                 case 'many2one':
122                     point[field] = value[1];
123                     break;
124                 case 'integer': case 'float': case 'char':
125                 case 'date': case 'datetime':
126                     point[field] = value;
127                     break;
128                 default:
129                     throw new Error(
130                         "Unknown field type " + self.fields[field].type
131                         + "for field " + field + " (" + value + ")");
132                 }
133             });
134             return point;
135         });
136         // aggregate data, because dhtmlx is crap. Aggregate on abscissa field,
137         // leave split on group field => max m*n records where m is the # of
138         // values for the abscissa and n is the # of values for the group field
139         var graph_data = [];
140         _(records).each(function (record) {
141             var abscissa = record[self.abscissa],
142                 group = record[self.group_field];
143             var r = _(graph_data).detect(function (potential) {
144                 return potential[self.abscissa] === abscissa
145                         && (!self.group_field
146                             || potential[self.group_field] === group);
147             });
148             var datapoint = r || {};
149
150             datapoint[self.abscissa] = abscissa;
151             if (self.group_field) { datapoint[self.group_field] = group; }
152             _(self.columns).each(function (column) {
153                 var val = record[column.name],
154                     aggregate = datapoint[column.name];
155                 switch(column.operator) {
156                 case '+':
157                     datapoint[column.name] = (aggregate || 0) + val;
158                     return;
159                 case '*':
160                     datapoint[column.name] = (aggregate || 1) * val;
161                     return;
162                 case 'min':
163                     datapoint[column.name] = (aggregate || Infinity) > val
164                                            ? val
165                                            : aggregate;
166                     return;
167                 case 'max':
168                     datapoint[column.name] = (aggregate || -Infinity) < val
169                                            ? val
170                                            : aggregate;
171                 }
172             });
173
174             if (!r) { graph_data.push(datapoint); }
175         });
176         if(self.fields[self.abscissa].type != 'selection'){
177            graph_data = _(graph_data).sortBy(function (point) {
178                return point[self.abscissa] + '[[--]]' + point[self.group_field];
179            });
180         }
181         if (_.include(['bar','line','area'],this.chart)) {
182             return this.schedule_bar_line_area(graph_data);
183         } else if (this.chart == "pie") {
184             return this.schedule_pie(graph_data);
185         }
186     },
187     schedule_bar_line_area: function(results) {
188         var self = this;
189         var group_list,
190         view_chart = (self.chart == 'line')?'line':(self.chart == 'area')?'area':'';
191         if (!this.group_field || !results.length) {
192             if (self.chart == 'bar'){
193                 view_chart = (this.orientation === 'horizontal') ? 'barH' : 'bar';
194             }
195             group_list = _(this.columns).map(function (column, index) {
196                 return {
197                     group: column.name,
198                     text: self.fields[column.name].string,
199                     color: COLOR_PALETTE[index % (COLOR_PALETTE.length)]
200                 }
201             });
202         } else {
203             // dhtmlx handles clustered bar charts (> 1 column per abscissa
204             // value) and stacked bar charts (basically the same but with the
205             // columns on top of one another instead of side by side), but it
206             // does not handle clustered stacked bar charts
207             if (self.chart == 'bar' && (this.columns.length > 1)) {
208                 this.$element.text(
209                     'OpenERP Web does not support combining grouping and '
210                   + 'multiple columns in graph at this time.');
211                 throw new Error(
212                     'dhtmlx can not handle columns counts of that magnitude');
213             }
214             // transform series for clustered charts into series for stacked
215             // charts
216             if (self.chart == 'bar'){
217                 view_chart = (this.orientation === 'horizontal')
218                         ? 'stackedBarH' : 'stackedBar';
219             }
220             group_list = _(results).chain()
221                     .pluck(this.group_field)
222                     .uniq()
223                     .map(function (value, index) {
224                         var groupval = '';
225                         if(value) {
226                             groupval = value.toLowerCase().replace(/[\s\/]+/g,'_');
227                         }
228                         return {
229                             group: _.str.sprintf('%s_%s', self.ordinate, groupval),
230                             text: value,
231                             color: COLOR_PALETTE[index % COLOR_PALETTE.length]
232                         };
233                     }).value();
234
235             results = _(results).chain()
236                 .groupBy(function (record) { return record[self.abscissa]; })
237                 .map(function (records) {
238                     var r = {};
239                     // second argument is coerced to a str, no good for boolean
240                     r[self.abscissa] = records[0][self.abscissa];
241                     _(records).each(function (record) {
242                         var value = record[self.group_field];
243                         if(value) {
244                             value = value.toLowerCase().replace(/[\s\/]+/g,'_');
245                         }
246                         var key = _.str.sprintf('%s_%s', self.ordinate, value);
247                         r[key] = record[self.ordinate];
248                     });
249                     return r;
250                 })
251                 .value();
252         }
253         var abscissa_description = {
254             title: "<b>" + this.fields[this.abscissa].string + "</b>",
255             template: function (obj) {
256                 return obj[self.abscissa] || 'Undefined';
257             }
258         };
259         var ordinate_description = {
260             lines: true,
261             title: "<b>" + this.fields[this.ordinate].string + "</b>"
262         };
263
264         var x_axis, y_axis;
265         if (self.chart == 'bar' && self.orientation == 'horizontal') {
266             x_axis = ordinate_description;
267             y_axis = abscissa_description;
268         } else {
269             x_axis = abscissa_description;
270             y_axis = ordinate_description;
271         }
272         var renderer = function () {
273             if (self.$element.is(':hidden')) {
274                 self.renderer = setTimeout(renderer, 100);
275                 return;
276             }
277             self.renderer = null;
278             var charts = new dhtmlXChart({
279                 view: view_chart,
280                 container: self.widget_parent.element_id+"-"+self.chart+"chart",
281                 value:"#"+group_list[0].group+"#",
282                 gradient: (self.chart == "bar") ? "3d" : "light",
283                 alpha: (self.chart == "area") ? 0.6 : 1,
284                 border: false,
285                 width: 1024,
286                 tooltip:{
287                     template: _.str.sprintf("#%s#, %s=#%s#",
288                         self.abscissa, group_list[0].text, group_list[0].group)
289                 },
290                 radius: 0,
291                 color: (self.chart != "line") ? group_list[0].color : "",
292                 item: (self.chart == "line") ? {
293                             borderColor: group_list[0].color,
294                             color: "#000000"
295                         } : "",
296                 line: (self.chart == "line") ? {
297                             color: group_list[0].color,
298                             width: 3
299                         } : "",
300                 origin:0,
301                 xAxis: x_axis,
302                 yAxis: y_axis,
303                 padding: {
304                     left: 75
305                 },
306                 legend: {
307                     values: group_list,
308                     align:"left",
309                     valign:"top",
310                     layout: "x",
311                     marker: {
312                         type:"round",
313                         width:12
314                     }
315                 }
316             });
317             self.$element.find("#"+self.widget_parent.element_id+"-"+self.chart+"chart").width(
318                 self.$element.find("#"+self.widget_parent.element_id+"-"+self.chart+"chart").width()+120);
319
320             for (var m = 1; m<group_list.length;m++){
321                 var column = group_list[m];
322                 if (column.group === self.group_field) { continue; }
323                 charts.addSeries({
324                     value: "#"+column.group+"#",
325                     tooltip:{
326                         template: _.str.sprintf("#%s#, %s=#%s#",
327                             self.abscissa, column.text, column.group)
328                     },
329                     color: (self.chart != "line") ? column.color : "",
330                     item: (self.chart == "line") ? {
331                             borderColor: column.color,
332                             color: "#000000"
333                         } : "",
334                     line: (self.chart == "line") ? {
335                             color: column.color,
336                             width: 3
337                         } : ""
338                 });
339             }
340             charts.parse(results, "json");
341             self.$element.find("#"+self.widget_parent.element_id+"-"+self.chart+"chart").height(
342                 self.$element.find("#"+self.widget_parent.element_id+"-"+self.chart+"chart").height()+50);
343             charts.attachEvent("onItemClick", function(id) {
344                 self.open_list_view(charts.get(id));
345             });
346         };
347         if (this.renderer) {
348             clearTimeout(this.renderer);
349         }
350         this.renderer = setTimeout(renderer, 0);
351     },
352     schedule_pie: function(result) {
353         var self = this;
354         var renderer = function () {
355             if (self.$element.is(':hidden')) {
356                 self.renderer = setTimeout(renderer, 100);
357                 return;
358             }
359             self.renderer = null;
360             var chart =  new dhtmlXChart({
361                 view:"pie3D",
362                 container:self.widget_parent.element_id+"-piechart",
363                 value:"#"+self.ordinate+"#",
364                 pieInnerText:function(obj) {
365                     var sum = chart.sum("#"+self.ordinate+"#");
366                     var val = obj[self.ordinate] / sum * 100 ;
367                     return val.toFixed(1) + "%";
368                 },
369                 tooltip:{
370                     template:"#"+self.abscissa+"#"+"="+"#"+self.ordinate+"#"
371                 },
372                 gradient:"3d",
373                 height: 20,
374                 radius: 200,
375                 legend: {
376                     width: 300,
377                     align:"left",
378                     valign:"top",
379                     layout: "x",
380                     marker:{
381                         type:"round",
382                         width:12
383                     },
384                     template:function(obj){
385                         return obj[self.abscissa] || 'Undefined';
386                     }
387                 }
388             });
389             chart.parse(result,"json");
390             chart.attachEvent("onItemClick", function(id) {
391                 self.open_list_view(chart.get(id));
392             });
393         };
394         if (this.renderer) {
395             clearTimeout(this.renderer);
396         }
397         this.renderer = setTimeout(renderer, 0);
398     },
399     sortSelection: function(results){
400        var self = this;
401        var grouped_data = _(results).groupBy(function(result){  
402            return result[self.abscissa];
403        });
404        var options = _(self.fields[self.abscissa].selection).map(function(option){ return option[0]; })
405        var sorted_data = _(options).chain().map(function(option){ 
406             return grouped_data[option];
407        }).filter(function(data){ return data != undefined; }).value();
408        if(sorted_data.length){
409            sorted_data =  _.reduceRight(sorted_data, function(a, b){ return b.concat(a);})
410        }
411        return sorted_data;
412     },
413     open_list_view : function (id){
414         var self = this;
415         // unconditionally nuke tooltips before switching view
416         $(".dhx_tooltip").remove('div');
417         id = id[this.abscissa];
418         if(this.fields[this.abscissa].type == "selection"){
419             id = _.detect(this.fields[this.abscissa].selection,function(select_value){
420                 return _.include(select_value, id);
421             });
422         }
423         if (typeof id == 'object'){
424             id = id[0];
425         }
426
427         var views;
428         if (this.widget_parent.action) {
429             views = this.widget_parent.action.views;
430             if (!_(views).detect(function (view) {
431                     return view[1] === 'list' })) {
432                 views = [[false, 'list']].concat(views);
433             }
434         } else {
435             views = _(["list", "form", "graph"]).map(function(mode) {
436                 return [false, mode];
437             });
438         }
439         this.do_action({
440             res_model : this.dataset.model,
441             domain: [[this.abscissa, '=', id], ['id','in',this.dataset.ids]],
442             views: views,
443             type: "ir.actions.act_window",
444             flags: {default_view: 'list'}
445         });
446     },
447
448     do_search: function(domain, context, group_by) {
449         var self = this;
450         return $.when(this.is_loaded).pipe(function() {
451             // TODO: handle non-empty group_by with read_group?
452             if (!_(group_by).isEmpty()) {
453                 self.abscissa = group_by[0];
454             } else {
455                 self.abscissa = self.first_field;
456             }
457             return self.dataset.read_slice(self.list_fields()).then($.proxy(self, 'schedule_chart'));
458         });
459     },
460
461     do_show: function() {
462         this.do_push_state({});
463         return this._super();
464     }
465 });
466 };
467 // vim:et fdc=0 fdl=0: