7d73f0df69588ff0a70b7daed7a702712e5b5b1b
[odoo/odoo.git] / addons / web_graph / static / src / js / graph.js
1 /*---------------------------------------------------------
2  * OpenERP web_graph
3  *---------------------------------------------------------*/
4
5 /* jshint undef: false  */
6
7
8 openerp.web_graph = function (instance) {
9 'use strict';
10
11 var _lt = instance.web._lt;
12 var _t = instance.web._t;
13 var QWeb = instance.web.qweb;
14
15 instance.web.views.add('graph', 'instance.web_graph.GraphView');
16
17  /**
18   * GraphView view.  It mostly contains a widget (PivotTable), some data, and 
19   * calls to charts function.
20   */
21 instance.web_graph.GraphView = instance.web.View.extend({
22     template: 'GraphView',
23     display_name: _lt('Graph'),
24     view_type: 'graph',
25     mode: 'pivot',   // pivot, bar_chart, line_chart or pie_chart
26     pivot_table: null,
27
28     events: {
29         'click .graph_mode_selection li' : function (event) {
30             event.preventDefault();
31             this.mode = event.target.attributes['data-mode'].nodeValue;
32             this.display_data();
33         },
34     },
35
36     view_loading: function (fields_view_get) {
37         var self = this;
38         var model = new instance.web.Model(fields_view_get.model, {group_by_no_leaf: true});
39         var domain = [];
40         var col_groupby = [];
41         var row_groupby = [];
42         var measure = null;
43         var fields;
44         var important_fields = [];
45
46         // get the default groupbys and measure defined in the field view
47         _.each(fields_view_get.arch.children, function (field) {
48             if ('name' in field.attrs) {
49                 if ('operator' in field.attrs) {
50                     measure = field.attrs.name;
51                 } else {
52                     row_groupby.push(field.attrs.name);
53                 }
54             }
55         });
56
57         // get the most important fields (of the model) by looking at the
58         // groupby filters defined in the search view
59         var load_view = instance.web.fields_view_get({
60             model: model,
61             view_type: 'search',
62         });
63
64         var important_fields_def = $.when(load_view).then(function (search_view) {
65             var groups = _.select(search_view.arch.children, function (c) {
66                 return (c.tag == 'group') && (c.attrs.string != 'Display');
67             });
68             _.each(groups, function(g) {
69                 _.each(g.children, function (g) {
70                     if (g.attrs.context) {
71                         var field_id = py.eval(g.attrs.context).group_by;
72                         important_fields.push(field_id);
73                     }
74                 });
75             });
76         });
77
78         // get the fields descriptions from the model
79         var field_descr_def = model.call('fields_get', [])
80             .then(function (fs) { fields = fs; });
81
82         return $.when(important_fields_def, field_descr_def)
83             .then(function () {
84                 self.data = {
85                     model: model,
86                     domain: domain,
87                     fields: fields,
88                     important_fields: important_fields,
89                     measure: measure,
90                     measure_label: fields[measure].string,
91                     col_groupby: [],
92                     row_groupby: row_groupby,
93                     groups: [],
94                     total: null,
95                 };
96             });
97     },
98
99     display_data : function () {
100         var content = this.$el.filter('.graph_main_content');
101         content.find('svg').remove();
102         var self = this;
103         if (this.mode === 'pivot') {
104             this.pivot_table.show();
105         } else {
106             this.pivot_table.hide();
107             content.append('<svg></svg>');
108             var view_fields = this.data.row_groupby.concat(this.data.measure, this.data.col_groupby);
109             query_groups(this.data.model, view_fields, this.data.domain, this.data.row_groupby).then(function (groups) {
110                 Charts[self.mode](groups, self.data.measure, self.data.measure_label);
111             });
112
113         }
114     },
115
116     do_search: function (domain, context, group_by) {
117         this.data.domain = new instance.web.CompoundDomain(domain);
118
119         if (this.pivot_table) {
120             this.pivot_table.draw(true);
121         } else {
122             this.pivot_table = new PivotTable(this.data);
123             this.pivot_table.appendTo('.graph_main_content');
124         }
125         this.display_data();
126     },
127
128     do_show: function () {
129         this.do_push_state({});
130         return this._super();
131     },
132
133 });
134
135
136  /**
137   * PivotTable widget.  It displays the data in tabular data and allows the
138   * user to drill down and up in the table
139   */
140 var PivotTable = instance.web.Widget.extend({
141     template: 'pivot_table',
142     data: null,
143     headers: [],
144     rows: [],
145     cols: [],
146     id_seed : 0,
147
148     events: {
149         'click .web_graph_click' : function (event) {
150             event.preventDefault();
151
152             if (event.target.attributes['data-row-id'] !== undefined) {
153                 this.handle_row_event(event);
154             }
155             if (event.target.attributes['data-col-id'] !== undefined) {
156                 this.handle_col_event(event);
157             }
158         },
159
160         'click a.field-selection' : function (event) {
161             var id,
162                 field_id = event.target.attributes['data-field-id'].nodeValue;
163             event.preventDefault();
164             this.dropdown.remove();
165             if (event.target.attributes['data-row-id'] !== undefined) {
166                 id = event.target.attributes['data-row-id'].nodeValue;
167                 this.expand_row(id, field_id);
168             } 
169             if (event.target.attributes['data-col-id'] !== undefined) {
170                 id = event.target.attributes['data-col-id'].nodeValue;
171                 this.expand_col(id, field_id);
172             } 
173         },
174     },
175
176     handle_row_event: function (event) {
177         var row_id = event.target.attributes['data-row-id'].nodeValue,
178             row = this.get_row(row_id);
179
180         if (row.expanded) {
181             this.fold_row(row_id);
182         } else {
183             if (row.path.length < this.data.row_groupby.length) {
184                 var field_to_expand = this.data.row_groupby[row.path.length];
185                 this.expand_row(row_id, field_to_expand);
186             } else {
187                 this.display_dropdown({row_id:row_id, 
188                                        target: $(event.target), 
189                                        x: event.pageX, 
190                                        y: event.pageY});
191             }
192         }
193     },
194
195     handle_col_event: function (event) {
196         var col_id = event.target.attributes['data-col-id'].nodeValue,
197             col = this.get_col(col_id);
198
199         if (col.expanded) {
200             this.fold_col(col_id);
201         } else {
202             if (col.path.length < this.data.col_groupby.length) {
203                 var field_to_expand = this.data.col_groupby[col.path.length];
204                 this.expand_col(col_id, field_to_expand);
205             } else {
206                 this.display_dropdown({col_id: col_id, 
207                                        target: $(event.target), 
208                                        x: event.pageX, 
209                                        y: event.pageY});
210             }
211         }
212     },
213
214     init: function (data) {
215         this.data = data;
216     },
217
218     start: function () {
219         this.draw(true);
220     },
221
222     draw: function (load_data) {
223         var self = this;
224
225         if (load_data) {
226             var view_fields = this.data.row_groupby.concat(this.data.measure, this.data.col_groupby);
227             query_groups_data(this.data.model, view_fields, this.data.domain, this.data.col_groupby, this.data.row_groupby[0])
228                 .then(function (groups) {
229                     self.data.groups = groups;
230                     return self.get_groups([]);
231                 }).then(function (total) {
232                     total[0].path = [];
233                     self.data.total = [total];
234                     self.build_table();
235                     self.draw(false);
236                 });
237         } else {
238             this.$el.empty();
239
240             this.draw_top_headers();
241
242             _.each(this.rows, function (row) {
243                 self.$el.append(row.html);
244             });
245         }
246     },
247
248     show: function () {
249         this.$el.css('display', 'block');
250     },
251
252     hide: function () {
253         this.$el.css('display', 'none');
254     },
255
256     display_dropdown: function (options) {
257         var self = this,
258             already_grouped = self.data.row_groupby.concat(self.data.col_groupby),
259             possible_groups = _.difference(self.data.important_fields, already_grouped),
260             dropdown_options = {
261                 fields: _.map(possible_groups, function (field) {
262                     return {id: field, value: self.get_descr(field)};
263             })};
264         if (options.row_id) {
265             dropdown_options.row_id= options.row_id;
266         } else {
267             dropdown_options.col_id = options.col_id;
268         }
269
270         this.dropdown = $(QWeb.render('field_selection', dropdown_options));
271         options.target.after(this.dropdown);
272         this.dropdown.css({position:'absolute',
273                            left:options.x,
274                            top:options.y});
275         $('.field-selection').next('.dropdown-menu').toggle();
276     },
277
278     build_table: function () {
279         var self = this;
280         this.rows = [];
281
282
283         var col_id = this.generate_id();
284
285         this.cols= [{
286             id: col_id,
287             path: [],
288             value: this.data.measure_label,
289             expanded: false,
290             parent: null,
291             children: [],
292             cells: [],    // a cell is {td:<jquery td>, row_id:<some id>}
293             domain: this.data.domain,
294         }];
295
296         self.make_top_headers();
297
298         var main_row = this.make_row(this.data.total[0]);
299
300         _.each(this.data.groups, function (group) {
301             self.make_row(group, main_row.id);
302         });
303     },
304
305     get_descr: function (field_id) {
306         return this.data.fields[field_id].string;
307     },
308
309     get_groups: function (groupby) {
310         var view_fields = this.data.row_groupby.concat(this.data.measure, this.data.col_groupby);
311         return query_groups(this.data.model, view_fields, this.data.domain, groupby);
312     },
313
314     make_top_headers : function () {
315         var self = this,
316             header;
317
318         function partition (columns) {
319             return _.reduce(columns, function (partial, col) {
320                 if (partial.length === 0) return [[col]];
321                 if (col.path.length > _.first(_.last(partial)).path.length) {
322                     _.last(partial).push(col);
323                 } else {
324                     partial.push([col]);
325                 }
326                 return partial;
327             }, []);
328         }
329
330         function side_by_side(blocks) {
331             var result = _.zip.apply(_,blocks);
332             result = _.map(result, function(line) {return _.compact(_.flatten(line))});
333             return result;
334         }
335
336         function make_header_cell(col, span) {
337             var options = {
338                 is_border:true,
339                 foldable:true,
340                 row_span: (span === undefined) ? 1 : span,
341                 col_id: col.id,
342             };
343             var result = self.make_cell(col.value, options);
344             if (col.expanded) {
345                 result.find('.icon-plus-sign')
346                     .removeClass('icon-plus-sign')
347                     .addClass('icon-minus-sign');
348             }
349             return result;
350         }
351
352         function calculate_width(cols) {
353             if (cols.length === 1) {
354                 return 1;
355             }
356             var p = partition(_.rest(cols));
357             return _.reduce(p, function(x, y){ return x + calculate_width(y); }, 0);
358         }
359
360         function make_header_cells(cols, height) {
361             var p = partition(cols);
362             if ((p.length === 1) && (p[0].length === 1)) {
363                 var result = [[make_header_cell(cols[0], height)]];
364                 return result;
365             }
366             if ((p.length === 1) && (p[0].length > 1)) {
367                 var cell = make_header_cell(p[0][0]);
368                 cell.attr('colspan', calculate_width(cols));
369                 return [[cell]].concat(make_header_cells(_.rest(cols), height - 1));
370             }
371             if (p.length > 1) {
372                 return side_by_side(_.map(p, function (group) {
373                     return make_header_cells(group, height);
374                 }));
375             }
376         }
377
378         if (this.cols.length === 1) {
379             header = $('<tr></tr>');
380             header.append(this.make_cell('', {is_border:true}));
381             header.append(this.make_cell(this.cols[0].value, 
382                 {is_border:true, foldable:true, col_id:this.cols[0].id}));
383             header.addClass('graph_top');
384             this.headers = [header];
385         } else {
386             var height = _.max(_.map(self.cols, function(g) {return g.path.length;}));
387             var header_rows = make_header_cells(_.rest(this.cols), height);
388
389             header_rows[0].splice(0,0,self.make_cell('', {is_border:true, }).attr('rowspan', height))
390             self.headers = [];
391             _.each(header_rows, function (cells) {
392                 header = $('<tr></tr>');
393                 header.append(cells);
394                 header.addClass('graph_top');
395                 self.headers.push(header);
396             });
397         }
398     },
399
400     draw_top_headers: function () {
401         var self = this;
402         $("tr.graph_top").remove();
403         _.each(this.headers.reverse(), function (header) {
404             self.$el.prepend(header);
405         });
406
407     },
408
409     make_row: function (groups, parent_id) {
410         var self = this,
411             path,
412             value,
413             expanded,
414             domain,
415             parent,
416             has_parent = (parent_id !== undefined),
417             row_id = this.generate_id();
418
419         if (has_parent) {
420             parent = this.get_row(parent_id);
421             path = parent.path.concat(groups[0].attributes.value[1]);
422             value = groups[0].attributes.value[1];
423             expanded = false;
424             parent.children.push(row_id);
425             domain = groups[0].model._domain;
426         } else {
427             parent = null;
428             path = [];
429             value = 'Total';
430             expanded = true;
431             domain = this.data.domain;
432         }
433
434         var jquery_row = $('<tr></tr>');
435
436         var header = self.make_cell(value, {is_border:true, indent: path.length, foldable:true, row_id: row_id});
437         jquery_row.append(header);
438
439         var cell;
440
441         _.each(this.cols, function (col) {
442             var element = _.find(groups, function (group) {
443                 return _.isEqual(_.rest(group.path), col.path);
444             });
445             if (element === undefined) {
446                 cell = self.make_cell('');
447             } else {
448                 cell = self.make_cell(element.attributes.aggregates[self.data.measure]);                
449             }
450             if (col.expanded) {
451                 cell.css('display', 'none');
452             }
453             col.cells.push({td:cell, row_id:row_id});
454             jquery_row.append(cell);
455         });
456
457         if (!has_parent) {
458             header.find('.icon-plus-sign')
459                 .removeClass('icon-plus-sign')
460                 .addClass('icon-minus-sign');            
461         }
462
463         var row = {
464             id: row_id,
465             path: path,
466             value: value,
467             expanded: expanded,
468             parent: parent_id,
469             children: [],
470             html: jquery_row,
471             domain: domain,
472         };
473         this.rows.push(row);  // to do, insert it properly, after all childs of parent
474         return row;
475     },
476
477     generate_id: function () {
478         this.id_seed += 1;
479         return this.id_seed - 1;
480     },
481
482     get_row: function (id) {
483         return _.find(this.rows, function(row) {
484             return (row.id == id);
485         });
486     },
487
488     get_col: function (id) {
489         return _.find(this.cols, function(col) {
490             return (col.id == id);
491         });
492     },
493
494     make_cell: function (content, options) {
495         options = _.extend({is_border: false, indent:0, foldable:false}, options);
496         content = (content !== undefined) ? content : 'Undefined';
497
498         var cell = $('<td></td>');
499         if (options.is_border) cell.addClass('graph_border');
500         if (options.row_span) cell.attr('rowspan', options.row_span);
501         if (options.col_span) cell.attr('rowspan', options.col_span);
502         _.each(_.range(options.indent), function () {
503             cell.prepend($('<span/>', {class:'web_graph_indent'}));
504         });
505
506         if (options.foldable) {
507             var attrs = {class:'icon-plus-sign web_graph_click', href:'#'};
508             if (options.row_id !== undefined) attrs['data-row-id'] = options.row_id;
509             if (options.col_id !== undefined) attrs['data-col-id'] = options.col_id;
510             var plus = $('<span/>', attrs);
511             plus.append(' ');
512             plus.append(content);
513             cell.append(plus);
514         } else {
515             cell.append(content);
516         }
517         return cell;
518     },
519
520     expand_row: function (row_id, field_id) {
521         var self = this;
522         var row = this.get_row(row_id);
523
524         if (row.path.length == this.data.row_groupby.length) {
525             this.data.row_groupby.push(field_id);
526         }
527         row.expanded = true;
528         row.html.find('.icon-plus-sign')
529             .removeClass('icon-plus-sign')
530             .addClass('icon-minus-sign');
531
532         var visible_fields = this.data.row_groupby.concat(this.data.col_groupby, this.data.measure);
533         query_groups_data(this.data.model, visible_fields, row.domain, this.data.col_groupby, field_id)
534             .then(function (groups) {
535                 _.each(groups.reverse(), function (group) {
536                     var new_row = self.make_row(group, row_id);
537                     row.html.after(new_row.html);
538                 });
539         });
540
541     },
542
543     expand_col: function (col_id, field_id) {
544         var self = this;
545         var col = this.get_col(col_id);
546
547         if (col.path.length == this.data.col_groupby.length) {
548             this.data.col_groupby.push(field_id);
549         }
550         col.expanded = true;
551
552         var visible_fields = this.data.row_groupby.concat(this.data.col_groupby, this.data.measure);
553         query_groups_data(this.data.model, visible_fields, col.domain, this.data.row_groupby, field_id)
554             .then(function (groups) {
555                 _.each(groups, function (group) {
556                     var new_col = {
557                         id: self.generate_id(),
558                         path: col.path.concat(group[0].attributes.value[1]),
559                         value: group[0].attributes.value[1],
560                         expanded: false,
561                         parent: col_id,
562                         children: [],
563                         cells: [],    // a cell is {td:<jquery td>, row_id:<some id>}
564                         domain: group[0].model._domain,
565                     };
566                     col.children.push(new_col.id);
567                     insertAfter(self.cols, col, new_col)
568                     _.each(col.cells, function (cell) {
569                         var col_path = self.get_row(cell.row_id).path;
570
571                         var datapt = _.find(group, function (g) {
572                             return _.isEqual(g.path.slice(1), col_path);
573                         });
574
575                         var value;
576                         if (datapt === undefined) {
577                             value = '';
578                         } else {
579                             value = datapt.attributes.aggregates[self.data.measure];
580                         }
581                         var new_cell = {
582                             row_id: cell.row_id,
583                             td: self.make_cell(value)
584                         };
585                         new_col.cells.push(new_cell);
586                         cell.td.after(new_cell.td);
587                         cell.td.css('display','none');
588                     });
589
590                 });
591             self.make_top_headers();
592             self.draw_top_headers();
593         });
594     },
595
596     fold_row: function (row_id) {
597         var self = this;
598         var row = this.get_row(row_id);
599
600         _.each(row.children, function (child_row) {
601             self.remove_row(child_row);
602         });
603         row.children = [];
604
605         row.expanded = false;
606         row.html.find('.icon-minus-sign')
607             .removeClass('icon-minus-sign')
608             .addClass('icon-plus-sign');
609
610         var fold_levels = _.map(self.rows, function(g) {return g.path.length;});
611         var new_groupby_length = _.max(fold_levels); 
612
613         this.data.row_groupby.splice(new_groupby_length);
614     },
615
616     remove_row: function (row_id) {
617         var self = this;
618         var row = this.get_row(row_id);
619
620         _.each(row.children, function (child_row) {
621             self.remove_row(child_row);
622         });
623
624         row.html.remove();
625         removeFromArray(this.rows, row);
626
627         _.each(this.cols, function (col) {
628             col.cells = _.filter(col.cells, function (cell) {
629                 return cell.row_id !== row_id;
630             });
631         });
632     },
633
634     fold_col: function (col_id) {
635         var self = this;
636         var col = this.get_col(col_id);
637
638         _.each(col.children, function (child_col) {
639             self.remove_col(child_col);
640         });
641         col.children = [];
642
643         _.each(col.cells, function (cell) {
644             cell.td.css('display','table-cell');
645         });
646         col.expanded = false;
647         // row.html.find('.icon-minus-sign')
648         //     .removeClass('icon-minus-sign')
649         //     .addClass('icon-plus-sign');
650
651         var fold_levels = _.map(self.cols, function(g) {return g.path.length;});
652         var new_groupby_length = _.max(fold_levels); 
653
654         this.data.col_groupby.splice(new_groupby_length);
655         this.make_top_headers();
656         this.draw_top_headers();
657
658             
659     },
660     
661     remove_col: function (col_id) {
662         var self = this;
663         var col = this.get_col(col_id);
664
665         _.each(col.children, function (child_col) {
666             self.remove_col(child_col);
667         });
668
669         _.each(col.cells, function (cell) {
670             cell.td.remove();
671         });
672         // row.html.remove();
673         removeFromArray(this.cols, col);
674
675         // _.each(this.cols, function (col) {
676         //     col.cells = _.filter(col.cells, function (cell) {
677         //         return cell.row_id !== row_id;
678         //     });
679         // });
680     },
681
682
683 });
684
685 };