[IMP] avoid potential error when specifying duplicated fields in read
[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         var records = _(results).map(function (result) {
108             var point = {};
109             _(result).each(function (value, field) {
110                 if (!_(fields).contains(field)) { return; }
111                 if (value === false) { point[field] = false; return; }
112                 switch (self.fields[field].type) {
113                 case 'selection':
114                     point[field] = _(self.fields[field].selection).detect(function (choice) {
115                         return choice[0] === value;
116                     })[1];
117                     break;
118                 case 'many2one':
119                     point[field] = value[1];
120                     break;
121                 case 'integer': case 'float': case 'char':
122                 case 'date': case 'datetime':
123                     point[field] = value;
124                     break;
125                 default:
126                     throw new Error(
127                         "Unknown field type " + self.fields[field].type
128                         + "for field " + field + " (" + value + ")");
129                 }
130             });
131             return point;
132         });
133         // aggregate data, because dhtmlx is crap. Aggregate on abscissa field,
134         // leave split on group field => max m*n records where m is the # of
135         // values for the abscissa and n is the # of values for the group field
136         var graph_data = [];
137         _(records).each(function (record) {
138             var abscissa = record[self.abscissa],
139                 group = record[self.group_field];
140             var r = _(graph_data).detect(function (potential) {
141                 return potential[self.abscissa] === abscissa
142                         && (!self.group_field
143                             || potential[self.group_field] === group);
144             });
145             var datapoint = r || {};
146
147             datapoint[self.abscissa] = abscissa;
148             if (self.group_field) { datapoint[self.group_field] = group; }
149             _(self.columns).each(function (column) {
150                 var val = record[column.name],
151                     aggregate = datapoint[column.name];
152                 switch(column.operator) {
153                 case '+':
154                     datapoint[column.name] = (aggregate || 0) + val;
155                     return;
156                 case '*':
157                     datapoint[column.name] = (aggregate || 1) * val;
158                     return;
159                 case 'min':
160                     datapoint[column.name] = (aggregate || Infinity) > val
161                                            ? val
162                                            : aggregate;
163                     return;
164                 case 'max':
165                     datapoint[column.name] = (aggregate || -Infinity) < val
166                                            ? val
167                                            : aggregate;
168                 }
169             });
170
171             if (!r) { graph_data.push(datapoint); }
172         });
173         graph_data = _(graph_data).sortBy(function (point) {
174             return point[self.abscissa] + '[[--]]' + point[self.group_field];
175         });
176         if (_.include(['bar','line','area'],this.chart)) {
177             return this.schedule_bar_line_area(graph_data);
178         } else if (this.chart == "pie") {
179             return this.schedule_pie(graph_data);
180         }
181     },
182     schedule_bar_line_area: function(results) {
183         var self = this;
184         var group_list,
185         view_chart = (self.chart == 'line')?'line':(self.chart == 'area')?'area':'';
186         if (!this.group_field || !results.length) {
187             if (self.chart == 'bar'){
188                 view_chart = (this.orientation === 'horizontal') ? 'barH' : 'bar';
189             }
190             group_list = _(this.columns).map(function (column, index) {
191                 return {
192                     group: column.name,
193                     text: self.fields[column.name].string,
194                     color: COLOR_PALETTE[index % (COLOR_PALETTE.length)]
195                 }
196             });
197         } else {
198             // dhtmlx handles clustered bar charts (> 1 column per abscissa
199             // value) and stacked bar charts (basically the same but with the
200             // columns on top of one another instead of side by side), but it
201             // does not handle clustered stacked bar charts
202             if (self.chart == 'bar' && (this.columns.length > 1)) {
203                 this.$element.text(
204                     'OpenERP Web does not support combining grouping and '
205                   + 'multiple columns in graph at this time.');
206                 throw new Error(
207                     'dhtmlx can not handle columns counts of that magnitude');
208             }
209             // transform series for clustered charts into series for stacked
210             // charts
211             if (self.chart == 'bar'){
212                 view_chart = (this.orientation === 'horizontal')
213                         ? 'stackedBarH' : 'stackedBar';
214             }
215             group_list = _(results).chain()
216                     .pluck(this.group_field)
217                     .uniq()
218                     .map(function (value, index) {
219                         var groupval = '';
220                         if(value) {
221                             groupval = value.toLowerCase().replace(/[\s\/]+/g,'_');
222                         }
223                         return {
224                             group: _.str.sprintf('%s_%s', self.ordinate, groupval),
225                             text: value,
226                             color: COLOR_PALETTE[index % COLOR_PALETTE.length]
227                         };
228                     }).value();
229
230             results = _(results).chain()
231                 .groupBy(function (record) { return record[self.abscissa]; })
232                 .map(function (records) {
233                     var r = {};
234                     // second argument is coerced to a str, no good for boolean
235                     r[self.abscissa] = records[0][self.abscissa];
236                     _(records).each(function (record) {
237                         var value = record[self.group_field];
238                         if(value) {
239                             value = value.toLowerCase().replace(/[\s\/]+/g,'_');
240                         }
241                         var key = _.str.sprintf('%s_%s', self.ordinate, value);
242                         r[key] = record[self.ordinate];
243                     });
244                     return r;
245                 })
246                 .value();
247         }
248         var abscissa_description = {
249             title: "<b>" + this.fields[this.abscissa].string + "</b>",
250             template: function (obj) {
251                 return obj[self.abscissa] || 'Undefined';
252             }
253         };
254         var ordinate_description = {
255             lines: true,
256             title: "<b>" + this.fields[this.ordinate].string + "</b>"
257         };
258
259         var x_axis, y_axis;
260         if (self.chart == 'bar' && self.orientation == 'horizontal') {
261             x_axis = ordinate_description;
262             y_axis = abscissa_description;
263         } else {
264             x_axis = abscissa_description;
265             y_axis = ordinate_description;
266         }
267         var renderer = function () {
268             if (self.$element.is(':hidden')) {
269                 self.renderer = setTimeout(renderer, 100);
270                 return;
271             }
272             self.renderer = null;
273             var charts = new dhtmlXChart({
274                 view: view_chart,
275                 container: self.widget_parent.element_id+"-"+self.chart+"chart",
276                 value:"#"+group_list[0].group+"#",
277                 gradient: (self.chart == "bar") ? "3d" : "light",
278                 alpha: (self.chart == "area") ? 0.6 : 1,
279                 border: false,
280                 width: 1024,
281                 tooltip:{
282                     template: _.str.sprintf("#%s#, %s=#%s#",
283                         self.abscissa, group_list[0].text, group_list[0].group)
284                 },
285                 radius: 0,
286                 color: (self.chart != "line") ? group_list[0].color : "",
287                 item: (self.chart == "line") ? {
288                             borderColor: group_list[0].color,
289                             color: "#000000"
290                         } : "",
291                 line: (self.chart == "line") ? {
292                             color: group_list[0].color,
293                             width: 3
294                         } : "",
295                 origin:0,
296                 xAxis: x_axis,
297                 yAxis: y_axis,
298                 padding: {
299                     left: 75
300                 },
301                 legend: {
302                     values: group_list,
303                     align:"left",
304                     valign:"top",
305                     layout: "x",
306                     marker: {
307                         type:"round",
308                         width:12
309                     }
310                 }
311             });
312             self.$element.find("#"+self.widget_parent.element_id+"-"+self.chart+"chart").width(
313                 self.$element.find("#"+self.widget_parent.element_id+"-"+self.chart+"chart").width()+120);
314
315             for (var m = 1; m<group_list.length;m++){
316                 var column = group_list[m];
317                 if (column.group === self.group_field) { continue; }
318                 charts.addSeries({
319                     value: "#"+column.group+"#",
320                     tooltip:{
321                         template: _.str.sprintf("#%s#, %s=#%s#",
322                             self.abscissa, column.text, column.group)
323                     },
324                     color: (self.chart != "line") ? column.color : "",
325                     item: (self.chart == "line") ? {
326                             borderColor: column.color,
327                             color: "#000000"
328                         } : "",
329                     line: (self.chart == "line") ? {
330                             color: column.color,
331                             width: 3
332                         } : ""
333                 });
334             }
335             charts.parse(results, "json");
336             self.$element.find("#"+self.widget_parent.element_id+"-"+self.chart+"chart").height(
337                 self.$element.find("#"+self.widget_parent.element_id+"-"+self.chart+"chart").height()+50);
338             charts.attachEvent("onItemClick", function(id) {
339                 self.open_list_view(charts.get(id));
340             });
341         };
342         if (this.renderer) {
343             clearTimeout(this.renderer);
344         }
345         this.renderer = setTimeout(renderer, 0);
346     },
347     schedule_pie: function(result) {
348         var self = this;
349         var renderer = function () {
350             if (self.$element.is(':hidden')) {
351                 self.renderer = setTimeout(renderer, 100);
352                 return;
353             }
354             self.renderer = null;
355             var chart =  new dhtmlXChart({
356                 view:"pie3D",
357                 container:self.widget_parent.element_id+"-piechart",
358                 value:"#"+self.ordinate+"#",
359                 pieInnerText:function(obj) {
360                     var sum = chart.sum("#"+self.ordinate+"#");
361                     var val = obj[self.ordinate] / sum * 100 ;
362                     return val.toFixed(1) + "%";
363                 },
364                 tooltip:{
365                     template:"#"+self.abscissa+"#"+"="+"#"+self.ordinate+"#"
366                 },
367                 gradient:"3d",
368                 height: 20,
369                 radius: 200,
370                 legend: {
371                     width: 300,
372                     align:"left",
373                     valign:"top",
374                     layout: "x",
375                     marker:{
376                         type:"round",
377                         width:12
378                     },
379                     template:function(obj){
380                         return obj[self.abscissa] || 'Undefined';
381                     }
382                 }
383             });
384             chart.parse(result,"json");
385             chart.attachEvent("onItemClick", function(id) {
386                 self.open_list_view(chart.get(id));
387             });
388         };
389         if (this.renderer) {
390             clearTimeout(this.renderer);
391         }
392         this.renderer = setTimeout(renderer, 0);
393     },
394     open_list_view : function (id){
395         var self = this;
396         // unconditionally nuke tooltips before switching view
397         $(".dhx_tooltip").remove('div');
398         id = id[this.abscissa];
399         if(this.fields[this.abscissa].type == "selection"){
400             id = _.detect(this.fields[this.abscissa].selection,function(select_value){
401                 return _.include(select_value, id);
402             });
403         }
404         if (typeof id == 'object'){
405             id = id[0];
406         }
407
408         var views;
409         if (this.widget_parent.action) {
410             views = this.widget_parent.action.views;
411             if (!_(views).detect(function (view) {
412                     return view[1] === 'list' })) {
413                 views = [[false, 'list']].concat(views);
414             }
415         } else {
416             views = _(["list", "form", "graph"]).map(function(mode) {
417                 return [false, mode];
418             });
419         }
420         this.do_action({
421             res_model : this.dataset.model,
422             domain: [[this.abscissa, '=', id], ['id','in',this.dataset.ids]],
423             views: views,
424             type: "ir.actions.act_window",
425             flags: {default_view: 'list'}
426         });
427     },
428
429     do_search: function(domain, context, group_by) {
430         var self = this;
431         return $.when(this.is_loaded).pipe(function() {
432             // TODO: handle non-empty group_by with read_group?
433             if (!_(group_by).isEmpty()) {
434                 self.abscissa = group_by[0];
435             } else {
436                 self.abscissa = self.first_field;
437             }
438             return self.dataset.read_slice(self.list_fields()).then($.proxy(self, 'schedule_chart'));
439         });
440     },
441
442     do_show: function() {
443         this.do_push_state({});
444         return this._super();
445     }
446 });
447 };
448 // vim:et fdc=0 fdl=0: