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.groupby_mode = 'default'; // 'default' or 'manual'
26 this.default_row_groupby = [];
27 this.default_col_groupby = [];
29 get_context: this.proxy('get_context'),
30 get_domain: function () {},
31 get_groupby: function () { },
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;
39 return {col_group_by : col_group_by};
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();
50 view_loading: function (fields_view_get) {
54 if (fields_view_get.arch.attrs.type === 'bar') {
55 this.mode = 'bar_chart';
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;
64 self.default_col_groupby.push(field.attrs.name);
66 self.default_row_groupby.push(field.attrs.name);
71 this.graph_widget.pivot.config({measure:measure, update:false});
74 do_search: function (domain, context, group_by) {
76 col_groupby = context.col_group_by || [],
77 options = {domain:domain};
79 if (group_by.length || col_groupby.length) {
80 this.groupby_mode = 'manual';
82 if (!this.graph_widget.enabled) {
83 options.update = false;
84 options.silent = true;
87 if (this.groupby_mode === 'manual') {
88 options.row_groupby = group_by;
89 options.col_groupby = col_groupby;
91 options.row_groupby = _.toArray(this.default_row_groupby);
92 options.col_groupby = _.toArray(this.default_col_groupby);
94 this.graph_widget.pivot.config(options);
96 if (!this.graph_widget.enabled) {
97 this.graph_widget.activate_display();
101 do_show: function () {
102 this.do_push_state({});
103 return this._super();
106 register_groupby: function() {
108 query = this.search_view.query;
109 this.groupby_mode = 'manual';
111 var rows = _.map(this.graph_widget.pivot.rows.groupby, function (group) {
112 return make_facet('GroupBy', group);
114 var cols = _.map(this.graph_widget.pivot.cols.groupby, function (group) {
115 return make_facet('ColGroupBy', group);
118 query.reset(rows.concat(cols));
120 function make_facet (category, fields) {
125 if (!(fields instanceof Array)) { fields = [fields]; }
126 if (category === 'GroupBy') {
127 cat_name = 'group_by';
129 backbone_field = self.search_view._s_groupby;
131 cat_name = 'col_group_by';
133 backbone_field = self.search_field;
135 values = _.map(fields, function (field) {
137 context[cat_name] = field;
138 return {label: self.graph_widget.fields[field].string, value: {attrs:{domain: [], context: context}}};
140 return {category:category, values: values, icon:icon, field: backbone_field};
145 instance.web_graph.Graph = instance.web.Widget.extend({
146 template: "GraphWidget",
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',
156 init: function(parent, model, options) {
159 this.important_fields = [];
160 this.measure_list = [];
162 this.pivot = new openerp.web_graph.PivotTable(model, []);
164 if (_.has(options, 'mode')) { this.mode = mode; }
166 if (_.has(options, 'enabled')) { this.enabled = options.enabled; }
167 this.visible_ui = true;
168 this.config(options || {});
171 // hide ui/show, stacked/grouped
172 config: function (options) {
173 if (_.has(options, 'visible_ui')) {
174 this.visible_ui = options.visible_ui;
176 this.pivot.config(options);
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');
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);
200 // get the fields descriptions and measure list from the model
201 var deferred2 = this.model.call('fields_get', []).then(function (fs) {
203 var temp = _.map(fs, function (field, name) {
204 return {name:name, type: field.type};
206 temp = _.filter(temp, function (field) {
207 return (((field.type === 'integer') || (field.type === 'float')) && (field.name !== 'id'));
209 self.measure_list = _.map(temp, function (field) {
213 var measure_selection = self.$('.graph_measure_selection');
214 _.each(self.measure_list, function (measure) {
215 var choice = $('<a></a>').attr('data-choice', measure)
217 .append(self.fields[measure].string);
218 measure_selection.append($('<li></li>').append(choice));
223 return $.when(deferred1, deferred2).then(function () {
225 this.activate_display();
230 activate_display: function () {
231 this.pivot.on('redraw_required', this, this.proxy('display_data'));
232 this.pivot.update_data();
234 instance.web.bus.on('click', this, function (ev) {
236 this.dropdown.remove();
237 this.dropdown = null;
242 display_data: function () {
243 var pivot = this.pivot;
244 this.$('.graph_main_content svg').remove();
247 if (this.visible_ui) {
248 this.$('.graph_header').css('display', 'block');
250 this.$('.graph_header').css('display', 'none');
253 var msg = 'No data available. Try to remove any filter or add some data.';
254 this.table.append($('<tr><td>' + msg + '</td></tr>'));
256 var table_modes = ['pivot', 'heatmap', 'row_heatmap', 'col_heatmap'];
257 if (_.contains(table_modes, this.mode)) {
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()));
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()
272 // openerp.web_graph.draw_chart(options);
277 mode_selection: function (event) {
278 event.preventDefault();
279 var mode = event.target.attributes['data-mode'].nodeValue;
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});
291 option_selection: function (event) {
292 event.preventDefault();
293 switch (event.target.attributes['data-choice'].nodeValue) {
295 this.pivot.swap_axis();
298 this.pivot.rows.headers = null;
299 this.pivot.cols.headers = null;
300 this.pivot.update_data();
302 case 'update_values':
303 this.pivot.update_data();
306 // Export code... To do...
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),
317 dim = header.root.groupby.length;
319 if (header.is_expanded) {
320 this.pivot.fold(header);
322 if (header.path.length < header.root.groupby.length) {
323 var field = header.root.groupby[header.path.length];
324 this.pivot.expand(id, field);
326 var fields = _.map(this.important_fields, function (field) {
327 return {id: field, value: self.fields[field].string};
329 this.dropdown = $(QWeb.render('field_selection', {fields:fields, header_id:id}));
330 $(event.target).after(this.dropdown);
331 this.dropdown.css({position:'absolute',
334 this.$('.field-selection').next('.dropdown-menu').toggle();
339 field_selection: function (event) {
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);
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'));
357 measure_label: function () {
358 return (this.pivot.measure) ? this.fields[this.pivot.measure].string : 'Quantity';
361 make_border_cell: function (colspan, rowspan) {
362 return $('<td></td>').addClass('graph_border')
363 .attr('colspan', colspan)
364 .attr('rowspan', rowspan);
367 make_header_title: function (header) {
368 return $('<span> </span>')
369 .addClass('web_graph_click')
371 .addClass((header.is_expanded) ? 'fa fa-minus-square' : 'fa fa-plus-square')
372 .append((header.title !== undefined) ? header.title : 'Undefined');
375 draw_top_headers: function () {
378 height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})),
379 header_cells = [[this.make_border_cell(1, height)]];
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;
388 cols.width = _.reduce(cols.children, function (sum,c) { return sum + c.width;}, 0);
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));
397 function make_cells (queue, level) {
399 queue = _.rest(queue).concat(col.children);
400 if (col.path.length == level) {
401 _.last(header_cells).push(make_col_header(col));
404 header_cells.push([make_col_header(col)]);
406 if (queue.length !== 0) {
407 make_cells(queue, level);
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);
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'));
419 _.each(header_cells, function (cells) {
420 self.table.append($('<tr></tr>').append(cells));
424 get_measure_type: function () {
425 var measure = this.pivot.measure;
426 return (measure) ? this.fields[measure].type : 'integer';
429 draw_row: function (row) {
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');
438 for (var i in _.range(row.path.length)) {
439 row_header.prepend($('<span/>', {class:'web_graph_indent'}));
442 html_row.append(row_header);
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);
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);
458 this.table.append(html_row);
460 function make_cell (value, col) {
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));
468 if (value === undefined) {
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));
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));
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));
491 /******************************************************************************
492 * Drawing charts methods...
493 ******************************************************************************/
494 bar_chart: function () {
496 dim_x = this.pivot.rows.groupby.length,
497 dim_y = this.pivot.cols.groupby.length,
500 // No groupby **************************************************************
501 if ((dim_x === 0) && (dim_y === 0)) {
502 data = [{key: 'Total', values:[{
504 value: this.pivot.get_value(this.pivot.rows.main.id, this.pivot.cols.main.id),
506 nv.addGraph(function () {
507 var chart = nv.models.discreteBarChart()
508 .x(function(d) { return d.title;})
509 .y(function(d) { return d.value;})
514 .staggerLabels(true);
518 .attr('width', self.width)
519 .attr('height', self.height)
522 nv.utils.windowResize(chart.update);
525 // Only column groupbys ****************************************************
526 } else if ((dim_x === 0) && (dim_y >= 1)){
527 data = _.map(this.pivot.get_columns_depth(1), function (header) {
530 values: [{x:header.root.main.title, y: self.pivot.get_total(header)}]
533 nv.addGraph(function() {
534 var chart = nv.models.multiBarChart()
539 .showControls(false);
543 .attr('width', self.width)
544 .attr('height', self.height)
549 nv.utils.windowResize(chart.update);
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};
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;})
569 .staggerLabels(true);
573 .attr('width', self.width)
574 .attr('height', self.height)
577 nv.utils.windowResize(chart.update);
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) {
585 x: header.title || 'Undefined',
586 y: self.pivot.get_value(header.id, colhdr.id, 0)
589 return {key: colhdr.title || 'Undefined', values: values};
592 nv.addGraph(function () {
593 var chart = nv.models.multiBarChart()
602 .attr('width', self.width)
603 .attr('height', self.height)
606 nv.utils.windowResize(chart.update);
609 // At least two row groupby*************************************************
611 var keys = _.uniq(_.map(this.pivot.get_rows_depth(2), function (hdr) {
612 return hdr.title || 'Undefined';
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')));
620 x: hdr.title || 'Undefined',
621 y: (subhdr) ? self.pivot.get_total(subhdr) : 0
624 return {key:key, values: values};
627 nv.addGraph(function () {
628 var chart = nv.models.multiBarChart()
637 .attr('width', self.width)
638 .attr('height', self.height)
641 nv.utils.windowResize(chart.update);
647 line_chart: function () {
649 dim_x = this.pivot.rows.groupby.length,
650 dim_y = this.pivot.cols.groupby.length;
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)};
656 var title = _.map(col.path, function (p) {
657 return p || 'Undefined';
660 title = self.measure_label();
662 return {values: values, key: title};
665 nv.addGraph(function () {
666 var chart = nv.models.lineChart()
667 .x(function (d,u) { return u; })
670 .margin({top: 30, right: 20, bottom: 20, left: 60});
673 .attr('width', self.width)
674 .attr('height', self.height)
682 pie_chart: function () {
684 dim_x = this.pivot.rows.groupby.length,
685 dim_y = this.pivot.cols.groupby.length;
687 var data = _.map(this.pivot.get_rows_leaves(), function (row) {
688 var title = _.map(row.path, function (p) {
689 return p || 'Undefined';
692 title = self.measure_label;
694 return {x: title, y: self.pivot.get_total(row)};
697 nv.addGraph(function () {
698 var chart = nv.models.pieChart()
699 .color(d3.scale.category10().range())
701 .height(self.height);
705 .transition().duration(1200)
706 .attr('width', self.width)
707 .attr('height', self.height)
710 nv.utils.windowResize(chart.update);