1 /*---------------------------------------------------------
3 *---------------------------------------------------------*/
5 /* jshint undef: false */
8 openerp.web_graph = function (instance) {
11 var _lt = instance.web._lt;
12 var _t = instance.web._t;
13 var QWeb = instance.web.qweb;
15 instance.web.views.add('graph', 'instance.web_graph.GraphView');
18 * GraphView view. It mostly contains a widget (PivotTable), some data, and
19 * calls to charts function.
21 instance.web_graph.GraphView = instance.web.View.extend({
22 template: 'GraphView',
23 display_name: _lt('Graph'),
25 mode: 'pivot', // pivot, bar_chart, line_chart or pie_chart
29 'click .graph_mode_selection li' : function (event) {
30 event.preventDefault();
31 this.mode = event.target.attributes['data-mode'].nodeValue;
36 view_loading: function (fields_view_get) {
38 var model = new instance.web.Model(fields_view_get.model, {group_by_no_leaf: true});
44 var important_fields = [];
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;
52 row_groupby.push(field.attrs.name);
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({
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');
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);
78 // get the fields descriptions from the model
79 var field_descr_def = model.call('fields_get', [])
80 .then(function (fs) { fields = fs; });
82 return $.when(important_fields_def, field_descr_def)
88 important_fields: important_fields,
90 measure_label: fields[measure].string,
92 row_groupby: row_groupby,
99 display_data : function () {
100 var content = this.$el.filter('.graph_main_content');
101 content.find('svg').remove();
103 if (this.mode === 'pivot') {
104 this.pivot_table.show();
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);
116 do_search: function (domain, context, group_by) {
117 this.data.domain = new instance.web.CompoundDomain(domain);
119 if (this.pivot_table) {
120 this.pivot_table.draw(true);
122 this.pivot_table = new PivotTable(this.data);
123 this.pivot_table.appendTo('.graph_main_content');
128 do_show: function () {
129 this.do_push_state({});
130 return this._super();
137 * PivotTable widget. It displays the data in tabular data and allows the
138 * user to drill down and up in the table
140 var PivotTable = instance.web.Widget.extend({
141 template: 'pivot_table',
149 'click .web_graph_click' : function (event) {
150 event.preventDefault();
152 if (event.target.attributes['data-row-id'] !== undefined) {
153 this.handle_row_event(event);
155 if (event.target.attributes['data-col-id'] !== undefined) {
156 this.handle_col_event(event);
160 'click a.field-selection' : function (event) {
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);
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);
176 handle_row_event: function (event) {
177 var row_id = event.target.attributes['data-row-id'].nodeValue,
178 row = this.get_row(row_id);
181 this.fold_row(row_id);
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);
187 this.display_dropdown({row_id:row_id,
188 target: $(event.target),
195 handle_col_event: function (event) {
196 var col_id = event.target.attributes['data-col-id'].nodeValue,
197 col = this.get_col(col_id);
200 this.fold_col(col_id);
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);
206 this.display_dropdown({col_id: col_id,
207 target: $(event.target),
214 init: function (data) {
222 draw: function (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) {
233 self.data.total = [total];
240 this.draw_top_headers();
242 _.each(this.rows, function (row) {
243 self.$el.append(row.html);
249 this.$el.css('display', 'block');
253 this.$el.css('display', 'none');
256 display_dropdown: function (options) {
258 already_grouped = self.data.row_groupby.concat(self.data.col_groupby),
259 possible_groups = _.difference(self.data.important_fields, already_grouped),
261 fields: _.map(possible_groups, function (field) {
262 return {id: field, value: self.get_descr(field)};
264 if (options.row_id) {
265 dropdown_options.row_id= options.row_id;
267 dropdown_options.col_id = options.col_id;
270 this.dropdown = $(QWeb.render('field_selection', dropdown_options));
271 options.target.after(this.dropdown);
272 this.dropdown.css({position:'absolute',
275 $('.field-selection').next('.dropdown-menu').toggle();
278 build_table: function () {
283 var col_id = this.generate_id();
288 value: this.data.measure_label,
292 cells: [], // a cell is {td:<jquery td>, row_id:<some id>}
293 domain: this.data.domain,
296 self.make_top_headers();
298 var main_row = this.make_row(this.data.total[0]);
300 _.each(this.data.groups, function (group) {
301 self.make_row(group, main_row.id);
305 get_descr: function (field_id) {
306 return this.data.fields[field_id].string;
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);
314 make_top_headers : function () {
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);
330 function side_by_side(blocks) {
331 var result = _.zip.apply(_,blocks);
332 result = _.map(result, function(line) {return _.compact(_.flatten(line))});
336 function make_header_cell(col, span) {
340 row_span: (span === undefined) ? 1 : span,
343 var result = self.make_cell(col.value, options);
345 result.find('.icon-plus-sign')
346 .removeClass('icon-plus-sign')
347 .addClass('icon-minus-sign');
352 function calculate_width(cols) {
353 if (cols.length === 1) {
356 var p = partition(_.rest(cols));
357 return _.reduce(p, function(x, y){ return x + calculate_width(y); }, 0);
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)]];
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));
372 return side_by_side(_.map(p, function (group) {
373 return make_header_cells(group, height);
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];
386 var height = _.max(_.map(self.cols, function(g) {return g.path.length;}));
387 var header_rows = make_header_cells(_.rest(this.cols), height);
389 header_rows[0].splice(0,0,self.make_cell('', {is_border:true, }).attr('rowspan', height))
391 _.each(header_rows, function (cells) {
392 header = $('<tr></tr>');
393 header.append(cells);
394 header.addClass('graph_top');
395 self.headers.push(header);
400 draw_top_headers: function () {
402 $("tr.graph_top").remove();
403 _.each(this.headers.reverse(), function (header) {
404 self.$el.prepend(header);
409 make_row: function (groups, parent_id) {
416 has_parent = (parent_id !== undefined),
417 row_id = this.generate_id();
420 parent = this.get_row(parent_id);
421 path = parent.path.concat(groups[0].attributes.value[1]);
422 value = groups[0].attributes.value[1];
424 parent.children.push(row_id);
425 domain = groups[0].model._domain;
431 domain = this.data.domain;
434 var jquery_row = $('<tr></tr>');
436 var header = self.make_cell(value, {is_border:true, indent: path.length, foldable:true, row_id: row_id});
437 jquery_row.append(header);
441 _.each(this.cols, function (col) {
442 var element = _.find(groups, function (group) {
443 return _.isEqual(_.rest(group.path), col.path);
445 if (element === undefined) {
446 cell = self.make_cell('');
448 cell = self.make_cell(element.attributes.aggregates[self.data.measure]);
451 cell.css('display', 'none');
453 col.cells.push({td:cell, row_id:row_id});
454 jquery_row.append(cell);
458 header.find('.icon-plus-sign')
459 .removeClass('icon-plus-sign')
460 .addClass('icon-minus-sign');
473 this.rows.push(row); // to do, insert it properly, after all childs of parent
477 generate_id: function () {
479 return this.id_seed - 1;
482 get_row: function (id) {
483 return _.find(this.rows, function(row) {
484 return (row.id == id);
488 get_col: function (id) {
489 return _.find(this.cols, function(col) {
490 return (col.id == id);
494 make_cell: function (content, options) {
495 options = _.extend({is_border: false, indent:0, foldable:false}, options);
496 content = (content !== undefined) ? content : 'Undefined';
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'}));
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);
512 plus.append(content);
515 cell.append(content);
520 expand_row: function (row_id, field_id) {
522 var row = this.get_row(row_id);
524 if (row.path.length == this.data.row_groupby.length) {
525 this.data.row_groupby.push(field_id);
528 row.html.find('.icon-plus-sign')
529 .removeClass('icon-plus-sign')
530 .addClass('icon-minus-sign');
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);
543 expand_col: function (col_id, field_id) {
545 var col = this.get_col(col_id);
547 if (col.path.length == this.data.col_groupby.length) {
548 this.data.col_groupby.push(field_id);
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) {
557 id: self.generate_id(),
558 path: col.path.concat(group[0].attributes.value[1]),
559 value: group[0].attributes.value[1],
563 cells: [], // a cell is {td:<jquery td>, row_id:<some id>}
564 domain: group[0].model._domain,
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;
571 var datapt = _.find(group, function (g) {
572 return _.isEqual(g.path.slice(1), col_path);
576 if (datapt === undefined) {
579 value = datapt.attributes.aggregates[self.data.measure];
583 td: self.make_cell(value)
585 new_col.cells.push(new_cell);
586 cell.td.after(new_cell.td);
587 cell.td.css('display','none');
591 self.make_top_headers();
592 self.draw_top_headers();
596 fold_row: function (row_id) {
598 var row = this.get_row(row_id);
600 _.each(row.children, function (child_row) {
601 self.remove_row(child_row);
605 row.expanded = false;
606 row.html.find('.icon-minus-sign')
607 .removeClass('icon-minus-sign')
608 .addClass('icon-plus-sign');
610 var fold_levels = _.map(self.rows, function(g) {return g.path.length;});
611 var new_groupby_length = _.max(fold_levels);
613 this.data.row_groupby.splice(new_groupby_length);
616 remove_row: function (row_id) {
618 var row = this.get_row(row_id);
620 _.each(row.children, function (child_row) {
621 self.remove_row(child_row);
625 removeFromArray(this.rows, row);
627 _.each(this.cols, function (col) {
628 col.cells = _.filter(col.cells, function (cell) {
629 return cell.row_id !== row_id;
634 fold_col: function (col_id) {
636 var col = this.get_col(col_id);
638 _.each(col.children, function (child_col) {
639 self.remove_col(child_col);
643 _.each(col.cells, function (cell) {
644 cell.td.css('display','table-cell');
646 col.expanded = false;
647 // row.html.find('.icon-minus-sign')
648 // .removeClass('icon-minus-sign')
649 // .addClass('icon-plus-sign');
651 var fold_levels = _.map(self.cols, function(g) {return g.path.length;});
652 var new_groupby_length = _.max(fold_levels);
654 this.data.col_groupby.splice(new_groupby_length);
655 this.make_top_headers();
656 this.draw_top_headers();
661 remove_col: function (col_id) {
663 var col = this.get_col(col_id);
665 _.each(col.children, function (child_col) {
666 self.remove_col(child_col);
669 _.each(col.cells, function (cell) {
672 // row.html.remove();
673 removeFromArray(this.cols, col);
675 // _.each(this.cols, function (col) {
676 // col.cells = _.filter(col.cells, function (cell) {
677 // return cell.row_id !== row_id;