3c0fe1a8be56dc0c9c651b2d71ffb594f869e19c
[odoo/odoo.git] / addons / web_graph / static / src / js / graph.js
1 /*---------------------------------------------------------
2  * OpenERP web_graph
3  *---------------------------------------------------------*/
4
5 /* jshint undef: false  */
6
7 openerp.web_graph = function (instance) {
8 'use strict';
9
10 var _lt = instance.web._lt;
11 var _t = instance.web._t;
12 var QWeb = instance.web.qweb;
13
14 instance.web.views.add('graph', 'instance.web_graph.GraphView');
15
16 instance.web_graph.GraphView = instance.web.View.extend({
17     display_name: _lt('Graph'),
18     view_type: 'graph',
19
20     init: function(parent, dataset, view_id, options) {
21         this._super(parent);
22         this.dataset = dataset;
23         this.model = new instance.web.Model(dataset.model, {group_by_no_leaf: true});
24         this.search_view = parent.searchview;
25         this.groupby_mode = 'default';  // 'default' or 'manual'
26         this.default_row_groupby = [];
27         this.default_col_groupby = [];
28         this.search_field = {
29             get_context: this.proxy('get_context'),
30             get_domain: function () {},
31             get_groupby: function () { },
32         };
33     },
34
35     get_context: function (facet) {
36         var col_group_by = _.map(facet.values.models, function (model) {
37             return model.attributes.value.attrs.context.col_group_by;
38         });
39         return {col_group_by : col_group_by};
40     },
41
42     start: function () {
43         var options = {enabled:false};
44         this.graph_widget = new openerp.web_graph.Graph(this, this.model, options);
45         this.graph_widget.appendTo(this.$el);
46         this.graph_widget.pivot.on('groupby_changed', this, this.proxy('register_groupby'));
47         return this.load_view();
48     },
49
50     view_loading: function (fields_view_get) {
51         var self = this,
52             measure = null;
53
54         if (fields_view_get.arch.attrs.type === 'bar') {
55             this.mode = 'bar_chart';
56         }
57
58         _.each(fields_view_get.arch.children, function (field) {
59             if ('name' in field.attrs) {
60                 if ('operator' in field.attrs) {
61                     measure = field.attrs.name;
62                 } else {
63                     if (measure) {
64                         self.default_col_groupby.push(field.attrs.name);
65                     } else {
66                         self.default_row_groupby.push(field.attrs.name);
67                     }
68                 }
69             }
70         });
71         this.graph_widget.pivot.config({measure:measure, update:false});
72     },
73
74     do_search: function (domain, context, group_by) {
75         var self = this,
76             col_groupby = context.col_group_by || [],
77             options = {domain:domain};
78
79         if (group_by.length || col_groupby.length) {
80             this.groupby_mode = 'manual';
81         }
82         if (!this.graph_widget.enabled) { 
83             options.update = false;
84             options.silent = true;
85         }
86
87         if (this.groupby_mode === 'manual') {
88             options.row_groupby = group_by;
89             options.col_groupby = col_groupby;
90         } else {
91             options.row_groupby = _.toArray(this.default_row_groupby);
92             options.col_groupby = _.toArray(this.default_col_groupby);
93         }
94         this.graph_widget.pivot.config(options);
95
96         if (!this.graph_widget.enabled) {
97             this.graph_widget.activate_display();
98         }
99     },
100
101     do_show: function () {
102         this.do_push_state({});
103         return this._super();
104     },
105
106     register_groupby: function() {
107         var self = this,
108             query = this.search_view.query;
109         this.groupby_mode = 'manual';
110
111         var rows = _.map(this.graph_widget.pivot.rows.groupby, function (group) {
112             return make_facet('GroupBy', group);
113         });
114         var cols = _.map(this.graph_widget.pivot.cols.groupby, function (group) {
115             return make_facet('ColGroupBy', group);
116         });
117
118         query.reset(rows.concat(cols));
119
120         function make_facet (category, fields) {
121             var values,
122                 icon,
123                 backbone_field,
124                 cat_name;
125             if (!(fields instanceof Array)) { fields = [fields]; }
126             if (category === 'GroupBy') {
127                 cat_name = 'group_by';
128                 icon = 'w';
129                 backbone_field = self.search_view._s_groupby;
130             } else {
131                 cat_name = 'col_group_by';
132                 icon = 'f';
133                 backbone_field = self.search_field;
134             }
135             values =  _.map(fields, function (field) {
136                 var context = {};
137                 context[cat_name] = field;
138                 return {label: self.graph_widget.fields[field].string, value: {attrs:{domain: [], context: context}}};
139             });
140             return {category:category, values: values, icon:icon, field: backbone_field};
141         }
142     },
143 });
144
145 instance.web_graph.Graph = instance.web.Widget.extend({
146     template: "GraphWidget",
147
148     events: {
149         'click .graph_mode_selection li' : 'mode_selection',
150         'click .graph_measure_selection li' : 'measure_selection',
151         'click .graph_options_selection li' : 'option_selection',
152         'click .web_graph_click' : 'header_cell_clicked',
153         'click a.field-selection' : 'field_selection',
154     },
155
156     init: function(parent, model, options) {
157         this._super(parent);
158         this.model = model;
159         this.important_fields = [];
160         this.measure_list = [];
161         this.fields = [];
162         this.pivot = new openerp.web_graph.PivotTable(model, []);
163         this.mode = 'pivot';
164         if (_.has(options, 'mode')) { this.mode = mode; }
165         this.enabled = true;
166         if (_.has(options, 'enabled')) { this.enabled = options.enabled; }
167         this.visible_ui = true;
168         this.config(options || {});
169     },
170
171     // hide ui/show, stacked/grouped
172     config: function (options) {
173         if (_.has(options, 'visible_ui')) {
174             this.visible_ui = options.visible_ui;
175         }
176         this.pivot.config(options);
177     },
178
179     start: function() {
180         var self = this;
181         this.table = $('<table></table>');
182         this.$('.graph_main_content').append(this.table);
183         // get the most important fields (of the model) by looking at the
184         // groupby filters defined in the search view
185         var options = {model:this.model, view_type: 'search'},
186             deferred1 = instance.web.fields_view_get(options).then(function (search_view) {
187                 var groups = _.select(search_view.arch.children, function (c) {
188                     return (c.tag == 'group') && (c.attrs.string != 'Display');
189                 });
190                 _.each(groups, function(g) {
191                     _.each(g.children, function (g) {
192                         if (g.attrs.context) {
193                             var field_id = py.eval(g.attrs.context).group_by;
194                             self.important_fields.push(field_id);
195                         }
196                     });
197                 });
198             });
199
200         // get the fields descriptions and measure list from the model
201         var deferred2 = this.model.call('fields_get', []).then(function (fs) {
202             self.fields = fs;
203             var temp = _.map(fs, function (field, name) {
204                 return {name:name, type: field.type};
205             });
206             temp = _.filter(temp, function (field) {
207                 return (((field.type === 'integer') || (field.type === 'float')) && (field.name !== 'id'));
208             });
209             self.measure_list = _.map(temp, function (field) {
210                 return field.name;
211             });
212
213             var measure_selection = self.$('.graph_measure_selection');
214             _.each(self.measure_list, function (measure) {
215                 var choice = $('<a></a>').attr('data-choice', measure)
216                                          .attr('href', '#')
217                                          .append(self.fields[measure].string);
218                 measure_selection.append($('<li></li>').append(choice));
219
220             });
221         });
222
223         return $.when(deferred1, deferred2).then(function () {
224             if (this.enabled) {
225                 this.activate_display();
226             }
227         });
228     },
229
230     activate_display: function () {
231         this.pivot.on('redraw_required', this, this.proxy('display_data'));
232         this.pivot.update_data();
233         this.enabled = true;
234         instance.web.bus.on('click', this, function (ev) {
235             if (this.dropdown) {
236                 this.dropdown.remove();
237                 this.dropdown = null;
238             }
239         });
240     },
241
242     display_data: function () {
243         var pivot = this.pivot;
244         this.$('.graph_main_content svg').remove();
245         this.table.empty();
246
247         if (this.visible_ui) {
248             this.$('.graph_header').css('display', 'block');
249         } else {
250             this.$('.graph_header').css('display', 'none');
251         }
252         if (pivot.no_data) {
253             var msg = 'No data available. Try to remove any filter or add some data.';
254             this.table.append($('<tr><td>' + msg + '</td></tr>'));
255         } else {
256             var table_modes = ['pivot', 'heatmap', 'row_heatmap', 'col_heatmap'];
257             if (_.contains(table_modes, this.mode)) {
258                 this.draw_table();
259             } else {
260                 this.$('.graph_main_content').append($('<div><svg></svg></div>'));
261                 this.svg = this.$('.graph_main_content svg')[0];
262                 this.width = this.$el.width();
263                 this.height = Math.min(Math.max(document.documentElement.clientHeight - 116 - 60, 250), Math.round(0.8*this.$el.width()));
264                 // var options = {
265                 //     svg: 
266                 //     mode: this.mode,
267                 //     width: this.$el.width(),
268                 //     height: Math.min(Math.max(document.documentElement.clientHeight - 116 - 60, 250), Math.round(0.8*this.$el.width())),
269                 //     measure_label: this.measure_label()
270                 // };
271                 this[this.mode]();
272                 // openerp.web_graph.draw_chart(options);
273             }
274         }
275     },
276
277     mode_selection: function (event) {
278         event.preventDefault();
279         var mode = event.target.attributes['data-mode'].nodeValue;
280         this.mode = mode;
281         this.display_data();
282     },
283
284     measure_selection: function (event) {
285         event.preventDefault();
286         var measure = event.target.attributes['data-choice'].nodeValue;
287         var actual_measure = (measure === '__count') ? null : measure;
288         this.pivot.config({measure:actual_measure});
289     },
290
291     option_selection: function (event) {
292         event.preventDefault();
293         switch (event.target.attributes['data-choice'].nodeValue) {
294             case 'swap_axis':
295                 this.pivot.swap_axis();
296                 break;
297             case 'expand_all':
298                 this.pivot.rows.headers = null;
299                 this.pivot.cols.headers = null;
300                 this.pivot.update_data();
301                 break;
302             case 'update_values':
303                 this.pivot.update_data();
304                 break;
305             case 'export_data':
306                 // Export code...  To do...
307                 break;
308         }
309     },
310
311     header_cell_clicked: function (event) {
312         event.preventDefault();
313         event.stopPropagation();
314         var id = event.target.attributes['data-id'].nodeValue,
315             header = this.pivot.get_header(id),
316             self = this,
317             dim = header.root.groupby.length;
318
319         if (header.is_expanded) {
320             this.pivot.fold(header);
321         } else {
322             if (header.path.length < header.root.groupby.length) {
323                 var field = header.root.groupby[header.path.length];
324                 this.pivot.expand(id, field);
325             } else {
326                 var fields = _.map(this.important_fields, function (field) {
327                         return {id: field, value: self.fields[field].string};
328                 });
329                 this.dropdown = $(QWeb.render('field_selection', {fields:fields, header_id:id}));
330                 $(event.target).after(this.dropdown);
331                 this.dropdown.css({position:'absolute',
332                                    left:event.pageX,
333                                    top:event.pageY});
334                 this.$('.field-selection').next('.dropdown-menu').toggle();
335             }
336         }
337     },
338
339     field_selection: function (event) {
340         var self = this,
341             id = event.target.attributes['data-id'].nodeValue,
342             field_id = event.target.attributes['data-field-id'].nodeValue;
343         event.preventDefault();
344         this.pivot.expand(id, field_id);
345     },
346
347 /******************************************************************************
348  * Drawing pivot table methods...
349  ******************************************************************************/
350     draw_table: function () {
351         this.pivot.rows.main.title = 'Total';
352         this.pivot.cols.main.title = this.measure_label();
353         this.draw_top_headers();
354         _.each(this.pivot.rows.headers, this.proxy('draw_row'));
355     },
356
357     measure_label: function () {
358         return (this.pivot.measure) ? this.fields[this.pivot.measure].string : 'Quantity';
359     },
360
361     make_border_cell: function (colspan, rowspan) {
362         return $('<td></td>').addClass('graph_border')
363                              .attr('colspan', colspan)
364                              .attr('rowspan', rowspan);
365     },
366
367     make_header_title: function (header) {
368         return $('<span> </span>')
369             .addClass('web_graph_click')
370             .attr('href', '#')
371             .addClass((header.is_expanded) ? 'fa fa-minus-square' : 'fa fa-plus-square')
372             .append((header.title !== undefined) ? header.title : 'Undefined');
373     },
374
375     draw_top_headers: function () {
376         var self = this,
377             pivot = this.pivot,
378             height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})),
379             header_cells = [[this.make_border_cell(1, height)]];
380
381         function set_dim (cols) {
382             _.each(cols.children, set_dim);
383             if (cols.children.length === 0) {
384                 cols.height = height - cols.path.length + 1;
385                 cols.width = 1;
386             } else {
387                 cols.height = 1;
388                 cols.width = _.reduce(cols.children, function (sum,c) { return sum + c.width;}, 0);
389             }
390         }
391
392         function make_col_header (col) {
393             var cell = self.make_border_cell(col.width, col.height);
394             return cell.append(self.make_header_title(col).attr('data-id', col.id));
395         }
396
397         function make_cells (queue, level) {
398             var col = queue[0];
399             queue = _.rest(queue).concat(col.children);
400             if (col.path.length == level) {
401                 _.last(header_cells).push(make_col_header(col));
402             } else {
403                 level +=1;
404                 header_cells.push([make_col_header(col)]);
405             }
406             if (queue.length !== 0) {
407                 make_cells(queue, level);
408             }
409         }
410
411         set_dim(pivot.cols.main);  // add width and height info to columns headers
412         if (pivot.cols.main.children.length === 0) {
413             make_cells(pivot.cols.headers, 0);
414         } else {
415             make_cells(pivot.cols.main.children, 1);
416             header_cells[0].push(self.make_border_cell(1, height).append('Total').css('font-weight', 'bold'));
417         }
418
419         _.each(header_cells, function (cells) {
420             self.table.append($('<tr></tr>').append(cells));
421         });
422     },
423
424     get_measure_type: function () {
425         var measure = this.pivot.measure;
426         return (measure) ? this.fields[measure].type : 'integer';
427     },
428
429     draw_row: function (row) {
430         var self = this,
431             pivot = this.pivot,
432             measure_type = this.get_measure_type(),
433             html_row = $('<tr></tr>'),
434             row_header = this.make_border_cell(1,1)
435                 .append(this.make_header_title(row).attr('data-id', row.id))
436                 .addClass('graph_border');
437
438         for (var i in _.range(row.path.length)) {
439             row_header.prepend($('<span/>', {class:'web_graph_indent'}));
440         }
441
442         html_row.append(row_header);
443
444         _.each(pivot.cols.headers, function (col) {
445             if (col.children.length === 0) {
446                 var value = pivot.get_value(row.id, col.id),
447                     cell = make_cell(value, col);
448                 html_row.append(cell);
449             }
450         });
451
452         if (pivot.cols.main.children.length > 0) {
453             var cell = make_cell(pivot.get_total(row), pivot.cols.main)
454                             .css('font-weight', 'bold');
455             html_row.append(cell);
456         }
457
458         this.table.append(html_row);
459
460         function make_cell (value, col) {
461             var color,
462                 total,
463                 cell = $('<td></td>');
464             if ((self.mode === 'pivot') && (row.is_expanded) && (row.path.length <=2)) {
465                 color = row.path.length * 5 + 240;
466                 cell.css('background-color', $.Color(color, color, color));
467             }
468             if (value === undefined) {
469                 return cell;
470             }
471             cell.append(instance.web.format_value(value, {type: measure_type}));
472             if (self.mode === 'heatmap') {
473                 total = pivot.get_total();
474                 color = Math.floor(50 + 205*(total - value)/total);
475                 cell.css('background-color', $.Color(255, color, color));
476             }
477             if (self.mode === 'row_heatmap') {
478                 total = pivot.get_total(row);
479                 color = Math.floor(50 + 205*(total - value)/total);
480                 cell.css('background-color', $.Color(255, color, color));
481             }
482             if (self.mode === 'col_heatmap') {
483                 total = pivot.get_total(col);
484                 color = Math.floor(50 + 205*(total - value)/total);
485                 cell.css('background-color', $.Color(255, color, color));
486             }
487             return cell;
488         }
489     },
490
491 /******************************************************************************
492  * Drawing charts methods...
493  ******************************************************************************/
494     bar_chart: function () {
495         var self = this,
496             dim_x = this.pivot.rows.groupby.length,
497             dim_y = this.pivot.cols.groupby.length,
498             data = [];
499
500         // No groupby **************************************************************
501         if ((dim_x === 0) && (dim_y === 0)) {
502             data = [{key: 'Total', values:[{
503                 title: 'Total',
504                 value: this.pivot.get_value(this.pivot.rows.main.id, this.pivot.cols.main.id),
505             }]}];
506             nv.addGraph(function () {
507               var chart = nv.models.discreteBarChart()
508                     .x(function(d) { return d.title;})
509                     .y(function(d) { return d.value;})
510                     .tooltips(false)
511                     .showValues(true)
512                     .width(self.width)
513                     .height(self.height)
514                     .staggerLabels(true);
515
516                 d3.select(self.svg)
517                     .datum(data)
518                     .attr('width', self.width)
519                     .attr('height', self.height)
520                     .call(chart);
521
522                 nv.utils.windowResize(chart.update);
523                 return chart;
524             });
525         // Only column groupbys ****************************************************
526         } else if ((dim_x === 0) && (dim_y >= 1)){
527             data =  _.map(this.pivot.get_columns_depth(1), function (header) {
528                 return {
529                     key: header.title,
530                     values: [{x:header.root.main.title, y: self.pivot.get_total(header)}]
531                 };
532             });
533             nv.addGraph(function() {
534                 var chart = nv.models.multiBarChart()
535                         .stacked(true)
536                         .tooltips(false)
537                         .width(self.width)
538                         .height(self.height)
539                         .showControls(false);
540
541                 d3.select(self.svg)
542                     .datum(data)
543                     .attr('width', self.width)
544                     .attr('height', self.height)
545                     .transition()
546                     .duration(500)
547                     .call(chart);
548
549                 nv.utils.windowResize(chart.update);
550
551                 return chart;
552             });
553         // Just 1 row groupby ******************************************************
554         } else if ((dim_x === 1) && (dim_y === 0))  {
555             data = _.map(this.pivot.rows.main.children, function (pt) {
556                 var value = self.pivot.get_value(pt.id, self.pivot.cols.main.id),
557                     title = (pt.title !== undefined) ? pt.title : 'Undefined';
558                 return {title: title, value: value};
559             });
560             data = [{key: this.measure_label(), values:data}];
561             nv.addGraph(function () {
562               var chart = nv.models.discreteBarChart()
563                     .x(function(d) { return d.title;})
564                     .y(function(d) { return d.value;})
565                     .tooltips(false)
566                     .showValues(true)
567                     .width(self.width)
568                     .height(self.height)
569                     .staggerLabels(true);
570
571                 d3.select(self.svg)
572                     .datum(data)
573                     .attr('width', self.width)
574                     .attr('height', self.height)
575                     .call(chart);
576
577                 nv.utils.windowResize(chart.update);
578                 return chart;
579             });
580         // 1 row groupby and some col groupbys**************************************
581         } else if ((dim_x === 1) && (dim_y >= 1))  {
582             data = _.map(this.pivot.get_columns_depth(1), function (colhdr) {
583                 var values = _.map(self.pivot.get_rows_depth(1), function (header) {
584                     return {
585                         x: header.title || 'Undefined',
586                         y: self.pivot.get_value(header.id, colhdr.id, 0)
587                     };
588                 });
589                 return {key: colhdr.title || 'Undefined', values: values};
590             });
591
592             nv.addGraph(function () {
593               var chart = nv.models.multiBarChart()
594                     .stacked(true)
595                     .staggerLabels(true)
596                     .width(self.width)
597                     .height(self.height)
598                     .tooltips(false);
599
600                 d3.select(self.svg)
601                     .datum(data)
602                     .attr('width', self.width)
603                     .attr('height', self.height)
604                     .call(chart);
605
606                 nv.utils.windowResize(chart.update);
607                 return chart;
608             });
609         // At least two row groupby*************************************************
610         } else {
611             var keys = _.uniq(_.map(this.pivot.get_rows_depth(2), function (hdr) {
612                 return hdr.title || 'Undefined';
613             }));
614             data = _.map(keys, function (key) {
615                 var values = _.map(self.pivot.get_rows_depth(1), function (hdr) {
616                     var subhdr = _.find(hdr.children, function (child) {
617                         return ((child.title === key) || ((child.title === undefined) && (key === 'Undefined')));
618                     });
619                     return {
620                         x: hdr.title || 'Undefined',
621                         y: (subhdr) ? self.pivot.get_total(subhdr) : 0
622                     };
623                 });
624                 return {key:key, values: values};
625             });
626
627             nv.addGraph(function () {
628               var chart = nv.models.multiBarChart()
629                     .stacked(true)
630                     .staggerLabels(true)
631                     .width(self.width)
632                     .height(self.height)
633                     .tooltips(false);
634
635                 d3.select(self.svg)
636                     .datum(data)
637                     .attr('width', self.width)
638                     .attr('height', self.height)
639                     .call(chart);
640
641                 nv.utils.windowResize(chart.update);
642                 return chart;
643             });
644         }
645     },
646
647     line_chart: function () {
648         var self = this,
649             dim_x = this.pivot.rows.groupby.length,
650             dim_y = this.pivot.cols.groupby.length;
651
652         var data = _.map(this.pivot.get_cols_leaves(), function (col) {
653             var values = _.map(self.pivot.get_rows_depth(dim_x), function (row) {
654                 return {x: row.title, y: self.pivot.get_value(row.id,col.id, 0)};
655             });
656             var title = _.map(col.path, function (p) {
657                 return p || 'Undefined';
658             }).join('/');
659             if (dim_y === 0) {
660                 title = self.measure_label();
661             }
662             return {values: values, key: title};
663         });
664
665         nv.addGraph(function () {
666             var chart = nv.models.lineChart()
667                 .x(function (d,u) { return u; })
668                 .width(self.width)
669                 .height(self.height)
670                 .margin({top: 30, right: 20, bottom: 20, left: 60});
671
672             d3.select(self.svg)
673                 .attr('width', self.width)
674                 .attr('height', self.height)
675                 .datum(data)
676                 .call(chart);
677
678             return chart;
679           });
680     },
681
682     pie_chart: function () {
683         var self = this,
684             dim_x = this.pivot.rows.groupby.length,
685             dim_y = this.pivot.cols.groupby.length;
686
687         var data = _.map(this.pivot.get_rows_leaves(), function (row) {
688             var title = _.map(row.path, function (p) {
689                 return p || 'Undefined';
690             }).join('/');
691             if (dim_x === 0) {
692                 title = self.measure_label;
693             }
694             return {x: title, y: self.pivot.get_total(row)};
695         });
696
697         nv.addGraph(function () {
698             var chart = nv.models.pieChart()
699                 .color(d3.scale.category10().range())
700                 .width(self.width)
701                 .height(self.height);
702
703             d3.select(self.svg)
704                 .datum(data)
705                 .transition().duration(1200)
706                 .attr('width', self.width)
707                 .attr('height', self.height)
708                 .call(chart);
709
710             nv.utils.windowResize(chart.update);
711             return chart;
712         });
713     },
714
715 });
716
717 };
718
719
720
721
722
723
724
725