1 /*---------------------------------------------------------
3 *---------------------------------------------------------*/
5 /* jshint undef: false */
7 openerp.web_graph = function (instance) {
10 var _lt = instance.web._lt;
11 var _t = instance.web._t;
12 var QWeb = instance.web.qweb;
14 instance.web.views.add('graph', 'instance.web_graph.GraphView');
16 instance.web_graph.GraphView = instance.web.View.extend({
17 display_name: _lt('Graph'),
20 init: function(parent, dataset, view_id, options) {
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.search_view_groupby = [];
26 this.groupby_mode = 'default'; // 'default' or 'manual'
27 this.default_row_groupby = [];
28 this.default_col_groupby = [];
30 get_context: this.proxy('get_context'),
31 get_domain: function () {},
32 get_groupby: function () { },
36 get_context: function (facet) {
37 var col_group_by = _.map(facet.values.models, function (model) {
38 return model.attributes.value.attrs.context.col_group_by;
40 return {col_group_by : col_group_by};
44 var options = {enabled:false};
45 this.graph_widget = new openerp.web_graph.Graph(this, this.model, options);
46 this.graph_widget.appendTo(this.$el);
47 this.graph_widget.pivot.on('groupby_changed', this, this.proxy('register_groupby'));
48 return this.load_view();
51 view_loading: function (fields_view_get) {
53 arch = fields_view_get.arch,
57 if (arch.attrs.type === 'bar' || !_.has(arch.attrs, 'type')) {
58 this.graph_widget.mode = 'bar_chart';
60 if (arch.attrs.stacked === 'True') {
64 _.each(arch.children, function (field) {
65 if (_.has(field.attrs, 'type')) {
66 switch (field.attrs.type) {
68 self.default_row_groupby.push(field.attrs.name);
71 self.default_col_groupby.push(field.attrs.name);
74 measure = field.attrs.name;
77 } else { // old style, kept for backward compatibility
78 if ('operator' in field.attrs) {
79 measure = (measure) ? measure : field.attrs.name;
81 self.default_row_groupby.push(field.attrs.name);
85 this.graph_widget.config({
88 bar_ui: (stacked) ? 'stack' : 'group'
92 do_search: function (domain, context, group_by) {
93 var col_groupby = context.col_group_by || [],
94 options = {domain:domain};
96 this.search_view_groupby = group_by;
98 if (group_by.length && this.groupby_mode !== 'manual') {
99 if (_.isEqual(col_groupby, [])) {
100 col_groupby = this.default_col_groupby;
103 if (group_by.length || col_groupby.length) {
104 this.groupby_mode = 'manual';
106 if (!this.graph_widget.enabled) {
107 options.update = false;
108 options.silent = true;
111 if (this.groupby_mode === 'manual') {
112 options.row_groupby = group_by;
113 options.col_groupby = col_groupby;
115 options.row_groupby = _.toArray(this.default_row_groupby);
116 options.col_groupby = _.toArray(this.default_col_groupby);
118 this.graph_widget.pivot.config(options);
120 if (!this.graph_widget.enabled) {
121 this.graph_widget.activate_display();
125 do_show: function () {
126 this.do_push_state({});
127 return this._super();
130 register_groupby: function() {
132 query = this.search_view.query;
134 this.groupby_mode = 'manual';
135 if (_.isEqual(this.search_view_groupby, this.graph_widget.pivot.rows.groupby) ||
136 (!_.has(this.search_view, '_s_groupby'))) {
139 var rows = _.map(this.graph_widget.pivot.rows.groupby, function (group) {
140 return make_facet('GroupBy', group);
142 var cols = _.map(this.graph_widget.pivot.cols.groupby, function (group) {
143 return make_facet('ColGroupBy', group);
146 query.reset(rows.concat(cols));
148 function make_facet (category, fields) {
153 if (!(fields instanceof Array)) { fields = [fields]; }
154 if (category === 'GroupBy') {
155 cat_name = 'group_by';
157 backbone_field = self.search_view._s_groupby;
159 cat_name = 'col_group_by';
161 backbone_field = self.search_field;
163 values = _.map(fields, function (field) {
165 context[cat_name] = field;
166 return {label: self.graph_widget.fields[field].string, value: {attrs:{domain: [], context: context}}};
168 return {category:category, values: values, icon:icon, field: backbone_field};
173 instance.web_graph.Graph = instance.web.Widget.extend({
174 template: 'GraphWidget',
177 'click .graph_mode_selection li' : 'mode_selection',
178 'click .graph_measure_selection li' : 'measure_selection',
179 'click .graph_options_selection li' : 'option_selection',
180 'click .web_graph_click' : 'header_cell_clicked',
181 'click a.field-selection' : 'field_selection',
184 init: function(parent, model, options) {
187 this.important_fields = [];
188 this.measure_list = [];
190 this.pivot = new openerp.web_graph.PivotTable(model, []);
192 if (_.has(options, 'mode')) { this.mode = mode; }
194 if (_.has(options, 'enabled')) { this.enabled = options.enabled; }
195 this.visible_ui = true;
196 this.bar_ui = 'group'; // group or stack
197 this.config(options || {});
200 // hide ui/show, stacked/grouped
201 config: function (options) {
202 if (_.has(options, 'visible_ui')) {
203 this.visible_ui = options.visible_ui;
205 if (_.has(options, 'bar_ui')) {
206 this.bar_ui = options.bar_ui;
208 this.pivot.config(options);
213 this.table = $('<table></table>');
214 this.$('.graph_main_content').append(this.table);
215 // get the most important fields (of the model) by looking at the
216 // groupby filters defined in the search view
217 var options = {model:this.model, view_type: 'search'},
218 deferred1 = instance.web.fields_view_get(options).then(function (search_view) {
219 var groups = _.select(search_view.arch.children, function (c) {
220 return (c.tag == 'group') && (c.attrs.string != 'Display');
222 _.each(groups, function(g) {
223 _.each(g.children, function (g) {
224 if (g.attrs.context) {
225 var field_id = py.eval(g.attrs.context).group_by;
226 self.important_fields.push(field_id);
232 // get the fields descriptions and measure list from the model
233 var deferred2 = this.model.call('fields_get', []).then(function (fs) {
235 var temp = _.map(fs, function (field, name) {
236 return {name:name, type: field.type};
238 temp = _.filter(temp, function (field) {
239 return (((field.type === 'integer') || (field.type === 'float')) && (field.name !== 'id'));
241 self.measure_list = _.map(temp, function (field) {
245 var measure_selection = self.$('.graph_measure_selection');
246 _.each(self.measure_list, function (measure) {
247 var choice = $('<a></a>').attr('data-choice', measure)
249 .append(self.fields[measure].string);
250 measure_selection.append($('<li></li>').append(choice));
254 return $.when(deferred1, deferred2).then(function () {
256 this.activate_display();
261 activate_display: function () {
262 this.pivot.on('redraw_required', this, this.proxy('display_data'));
263 this.pivot.update_data();
265 instance.web.bus.on('click', this, function () {
267 this.dropdown.remove();
268 this.dropdown = null;
273 display_data: function () {
274 this.$('.graph_main_content svg').remove();
275 this.$('.graph_main_content div').remove();
278 if (this.visible_ui) {
279 this.$('.graph_header').css('display', 'block');
281 this.$('.graph_header').css('display', 'none');
283 if (this.pivot.no_data) {
284 this.$('.graph_main_content').append($(QWeb.render('graph_no_data')));
285 // var msg = 'No data available. Try to remove any filter or add some data.';
286 // this.table.append($('<tr><td>' + msg + '</td></tr>'));
288 var table_modes = ['pivot', 'heatmap', 'row_heatmap', 'col_heatmap'];
289 if (_.contains(table_modes, this.mode)) {
292 this.$('.graph_main_content').append($('<div><svg></svg></div>'));
293 this.svg = this.$('.graph_main_content svg')[0];
294 this.width = this.$el.width();
295 this.height = Math.min(Math.max(document.documentElement.clientHeight - 116 - 60, 250), Math.round(0.8*this.$el.width()));
301 mode_selection: function (event) {
302 event.preventDefault();
303 var mode = event.target.attributes['data-mode'].nodeValue;
308 measure_selection: function (event) {
309 event.preventDefault();
310 var measure = event.target.attributes['data-choice'].nodeValue;
311 var actual_measure = (measure === '__count') ? null : measure;
312 this.pivot.config({measure:actual_measure});
315 option_selection: function (event) {
316 event.preventDefault();
317 switch (event.target.attributes['data-choice'].nodeValue) {
319 this.pivot.swap_axis();
322 this.pivot.rows.headers = null;
323 this.pivot.cols.headers = null;
324 this.pivot.update_data();
326 case 'update_values':
327 this.pivot.update_data();
330 // Export code... To do...
335 header_cell_clicked: function (event) {
336 event.preventDefault();
337 event.stopPropagation();
338 var id = event.target.attributes['data-id'].nodeValue,
339 header = this.pivot.get_header(id),
342 if (header.is_expanded) {
343 this.pivot.fold(header);
345 if (header.path.length < header.root.groupby.length) {
346 var field = header.root.groupby[header.path.length];
347 this.pivot.expand(id, field);
349 if (!this.important_fields.length) {
352 var fields = _.map(this.important_fields, function (field) {
353 return {id: field, value: self.fields[field].string};
355 this.dropdown = $(QWeb.render('field_selection', {fields:fields, header_id:id}));
356 $(event.target).after(this.dropdown);
357 this.dropdown.css({position:'absolute',
360 this.$('.field-selection').next('.dropdown-menu').toggle();
365 field_selection: function (event) {
366 var id = event.target.attributes['data-id'].nodeValue,
367 field_id = event.target.attributes['data-field-id'].nodeValue;
368 event.preventDefault();
369 this.pivot.expand(id, field_id);
372 /******************************************************************************
373 * Drawing pivot table methods...
374 ******************************************************************************/
375 draw_table: function () {
376 this.pivot.rows.main.title = 'Total';
377 this.pivot.cols.main.title = this.measure_label();
378 this.draw_top_headers();
379 _.each(this.pivot.rows.headers, this.proxy('draw_row'));
382 measure_label: function () {
383 return (this.pivot.measure) ? this.fields[this.pivot.measure].string : 'Quantity';
386 make_border_cell: function (colspan, rowspan) {
387 return $('<td></td>').addClass('graph_border')
388 .attr('colspan', colspan)
389 .attr('rowspan', rowspan);
392 make_header_title: function (header) {
393 return $('<span> </span>')
394 .addClass('web_graph_click')
396 .addClass((header.is_expanded) ? 'fa fa-minus-square' : 'fa fa-plus-square')
397 .append((header.title !== undefined) ? header.title : 'Undefined');
400 draw_top_headers: function () {
403 height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})),
404 header_cells = [[this.make_border_cell(1, height)]];
406 function set_dim (cols) {
407 _.each(cols.children, set_dim);
408 if (cols.children.length === 0) {
409 cols.height = height - cols.path.length + 1;
413 cols.width = _.reduce(cols.children, function (sum,c) { return sum + c.width;}, 0);
417 function make_col_header (col) {
418 var cell = self.make_border_cell(col.width, col.height);
419 return cell.append(self.make_header_title(col).attr('data-id', col.id));
422 function make_cells (queue, level) {
424 queue = _.rest(queue).concat(col.children);
425 if (col.path.length == level) {
426 _.last(header_cells).push(make_col_header(col));
429 header_cells.push([make_col_header(col)]);
431 if (queue.length !== 0) {
432 make_cells(queue, level);
436 set_dim(pivot.cols.main); // add width and height info to columns headers
437 if (pivot.cols.main.children.length === 0) {
438 make_cells(pivot.cols.headers, 0);
440 make_cells(pivot.cols.main.children, 1);
441 header_cells[0].push(self.make_border_cell(1, height).append('Total').css('font-weight', 'bold'));
444 _.each(header_cells, function (cells) {
445 self.table.append($('<tr></tr>').append(cells));
449 get_measure_type: function () {
450 var measure = this.pivot.measure;
451 return (measure) ? this.fields[measure].type : 'integer';
454 draw_row: function (row) {
457 measure_type = this.get_measure_type(),
458 html_row = $('<tr></tr>'),
459 row_header = this.make_border_cell(1,1)
460 .append(this.make_header_title(row).attr('data-id', row.id))
461 .addClass('graph_border');
463 for (var i in _.range(row.path.length)) {
464 row_header.prepend($('<span/>', {class:'web_graph_indent'}));
467 html_row.append(row_header);
469 _.each(pivot.cols.headers, function (col) {
470 if (col.children.length === 0) {
471 var value = pivot.get_value(row.id, col.id),
472 cell = make_cell(value, col);
473 html_row.append(cell);
477 if (pivot.cols.main.children.length > 0) {
478 var cell = make_cell(pivot.get_total(row), pivot.cols.main)
479 .css('font-weight', 'bold');
480 html_row.append(cell);
483 this.table.append(html_row);
485 function make_cell (value, col) {
488 cell = $('<td></td>');
489 if ((self.mode === 'pivot') && (row.is_expanded) && (row.path.length <=2)) {
490 color = row.path.length * 5 + 240;
491 cell.css('background-color', $.Color(color, color, color));
493 if (value === undefined) {
496 cell.append(instance.web.format_value(value, {type: measure_type}));
497 if (self.mode === 'heatmap') {
498 total = pivot.get_total();
499 color = Math.floor(50 + 205*(total - value)/total);
500 cell.css('background-color', $.Color(255, color, color));
502 if (self.mode === 'row_heatmap') {
503 total = pivot.get_total(row);
504 color = Math.floor(50 + 205*(total - value)/total);
505 cell.css('background-color', $.Color(255, color, color));
507 if (self.mode === 'col_heatmap') {
508 total = pivot.get_total(col);
509 color = Math.floor(50 + 205*(total - value)/total);
510 cell.css('background-color', $.Color(255, color, color));
516 /******************************************************************************
517 * Drawing charts methods...
518 ******************************************************************************/
519 bar_chart: function () {
521 dim_x = this.pivot.rows.groupby.length,
522 dim_y = this.pivot.cols.groupby.length,
525 // No groupby **************************************************************
526 if ((dim_x === 0) && (dim_y === 0)) {
527 data = [{key: 'Total', values:[{
529 y: this.pivot.get_value(this.pivot.rows.main.id, this.pivot.cols.main.id),
531 // Only column groupbys ****************************************************
532 } else if ((dim_x === 0) && (dim_y >= 1)){
533 data = _.map(this.pivot.get_columns_depth(1), function (header) {
536 values: [{x:header.root.main.title, y: self.pivot.get_total(header)}]
539 // Just 1 row groupby ******************************************************
540 } else if ((dim_x === 1) && (dim_y === 0)) {
541 data = _.map(this.pivot.rows.main.children, function (pt) {
542 var value = self.pivot.get_value(pt.id, self.pivot.cols.main.id),
543 title = (pt.title !== undefined) ? pt.title : 'Undefined';
544 return {x: title, y: value};
546 data = [{key: this.measure_label(), values:data}];
547 // 1 row groupby and some col groupbys**************************************
548 } else if ((dim_x === 1) && (dim_y >= 1)) {
549 data = _.map(this.pivot.get_columns_depth(1), function (colhdr) {
550 var values = _.map(self.pivot.get_rows_depth(1), function (header) {
552 x: header.title || 'Undefined',
553 y: self.pivot.get_value(header.id, colhdr.id, 0)
556 return {key: colhdr.title || 'Undefined', values: values};
558 // At least two row groupby*************************************************
560 var keys = _.uniq(_.map(this.pivot.get_rows_depth(2), function (hdr) {
561 return hdr.title || 'Undefined';
563 data = _.map(keys, function (key) {
564 var values = _.map(self.pivot.get_rows_depth(1), function (hdr) {
565 var subhdr = _.find(hdr.children, function (child) {
566 return ((child.title === key) || ((child.title === undefined) && (key === 'Undefined')));
569 x: hdr.title || 'Undefined',
570 y: (subhdr) ? self.pivot.get_total(subhdr) : 0
573 return {key:key, values: values};
577 nv.addGraph(function () {
578 var chart = nv.models.multiBarChart()
581 .stacked(self.bar_ui === 'stack')
582 .staggerLabels(true);
584 if (dim_x === 1 && dim_y === 0) { chart.showControls(false); }
588 .attr('width', self.width)
589 .attr('height', self.height)
592 nv.utils.windowResize(chart.update);
598 line_chart: function () {
600 dim_x = this.pivot.rows.groupby.length,
601 dim_y = this.pivot.cols.groupby.length;
603 var data = _.map(this.pivot.get_cols_leaves(), function (col) {
604 var values = _.map(self.pivot.get_rows_depth(dim_x), function (row) {
605 return {x: row.title, y: self.pivot.get_value(row.id,col.id, 0)};
607 var title = _.map(col.path, function (p) {
608 return p || 'Undefined';
611 title = self.measure_label();
613 return {values: values, key: title};
616 nv.addGraph(function () {
617 var chart = nv.models.lineChart()
618 .x(function (d,u) { return u; })
621 .margin({top: 30, right: 20, bottom: 20, left: 60});
624 .attr('width', self.width)
625 .attr('height', self.height)
633 pie_chart: function () {
635 dim_x = this.pivot.rows.groupby.length;
637 var data = _.map(this.pivot.get_rows_leaves(), function (row) {
638 var title = _.map(row.path, function (p) {
639 return p || 'Undefined';
642 title = self.measure_label;
644 return {x: title, y: self.pivot.get_total(row)};
647 nv.addGraph(function () {
648 var chart = nv.models.pieChart()
649 .color(d3.scale.category10().range())
651 .height(self.height);
655 .transition().duration(1200)
656 .attr('width', self.width)
657 .attr('height', self.height)
660 nv.utils.windowResize(chart.update);