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) {
21 this._super(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();
122 this.ViewManager.on('switch_mode', this, function () {this.graph_widget.pivot.update_data(); });
126 do_show: function () {
127 this.do_push_state({});
128 return this._super();
131 register_groupby: function() {
133 query = this.search_view.query;
135 this.groupby_mode = 'manual';
136 if (_.isEqual(this.search_view_groupby, this.graph_widget.pivot.rows.groupby) ||
137 (!_.has(this.search_view, '_s_groupby'))) {
140 var rows = _.map(this.graph_widget.pivot.rows.groupby, function (group) {
141 return make_facet('GroupBy', group);
143 var cols = _.map(this.graph_widget.pivot.cols.groupby, function (group) {
144 return make_facet('ColGroupBy', group);
147 query.reset(rows.concat(cols));
149 function make_facet (category, fields) {
154 if (!(fields instanceof Array)) { fields = [fields]; }
155 if (category === 'GroupBy') {
156 cat_name = 'group_by';
158 backbone_field = self.search_view._s_groupby;
160 cat_name = 'col_group_by';
162 backbone_field = self.search_field;
164 values = _.map(fields, function (field) {
166 context[cat_name] = field;
167 return {label: self.graph_widget.fields[field].string, value: {attrs:{domain: [], context: context}}};
169 return {category:category, values: values, icon:icon, field: backbone_field};
174 instance.web_graph.Graph = instance.web.Widget.extend({
175 template: 'GraphWidget',
178 'click .graph_mode_selection li' : 'mode_selection',
179 'click .graph_measure_selection li' : 'measure_selection',
180 'click .graph_options_selection li' : 'option_selection',
181 'click .web_graph_click' : 'header_cell_clicked',
182 'click a.field-selection' : 'field_selection',
185 init: function(parent, model, options) {
188 this.important_fields = [];
189 this.measure_list = [];
191 this.pivot = new openerp.web_graph.PivotTable(model, []);
193 if (_.has(options, 'mode')) { this.mode = mode; }
195 if (_.has(options, 'enabled')) { this.enabled = options.enabled; }
196 this.visible_ui = true;
197 this.bar_ui = 'group'; // group or stack
198 this.config(options || {});
201 // hide ui/show, stacked/grouped
202 config: function (options) {
203 if (_.has(options, 'visible_ui')) {
204 this.visible_ui = options.visible_ui;
206 if (_.has(options, 'bar_ui')) {
207 this.bar_ui = options.bar_ui;
209 this.pivot.config(options);
214 this.table = $('<table></table>');
215 this.$('.graph_main_content').append(this.table);
216 // get the most important fields (of the model) by looking at the
217 // groupby filters defined in the search view
218 var options = {model:this.model, view_type: 'search'},
219 deferred1 = instance.web.fields_view_get(options).then(function (search_view) {
220 var groups = _.select(search_view.arch.children, function (c) {
221 return (c.tag == 'group') && (c.attrs.string != 'Display');
223 _.each(groups, function(g) {
224 _.each(g.children, function (g) {
225 if (g.attrs.context) {
226 var field_id = py.eval(g.attrs.context).group_by;
227 self.important_fields.push(field_id);
233 // get the fields descriptions and measure list from the model
234 var deferred2 = this.model.call('fields_get', []).then(function (fs) {
236 var temp = _.map(fs, function (field, name) {
237 return {name:name, type: field.type};
239 temp = _.filter(temp, function (field) {
240 return (((field.type === 'integer') || (field.type === 'float')) && (field.name !== 'id'));
242 self.measure_list = _.map(temp, function (field) {
246 var measure_selection = self.$('.graph_measure_selection');
247 _.each(self.measure_list, function (measure) {
248 var choice = $('<a></a>').attr('data-choice', measure)
250 .append(self.fields[measure].string);
251 measure_selection.append($('<li></li>').append(choice));
255 return $.when(deferred1, deferred2).then(function () {
257 this.activate_display();
262 activate_display: function () {
263 this.pivot.on('redraw_required', this, this.proxy('display_data'));
264 this.pivot.update_data();
266 instance.web.bus.on('click', this, function () {
268 this.dropdown.remove();
269 this.dropdown = null;
274 display_data: function () {
275 this.$('.graph_main_content svg').remove();
276 this.$('.graph_main_content div').remove();
279 if (this.visible_ui) {
280 this.$('.graph_header').css('display', 'block');
282 this.$('.graph_header').css('display', 'none');
284 if (this.pivot.no_data) {
285 this.$('.graph_main_content').append($(QWeb.render('graph_no_data')));
287 var table_modes = ['pivot', 'heatmap', 'row_heatmap', 'col_heatmap'];
288 if (_.contains(table_modes, this.mode)) {
291 this.$('.graph_main_content').append($('<div><svg></svg></div>'));
292 this.svg = this.$('.graph_main_content svg')[0];
293 this.width = this.$el.width();
294 this.height = Math.min(Math.max(document.documentElement.clientHeight - 116 - 60, 250), Math.round(0.8*this.$el.width()));
300 mode_selection: function (event) {
301 event.preventDefault();
302 var mode = event.target.attributes['data-mode'].nodeValue;
307 measure_selection: function (event) {
308 event.preventDefault();
309 var measure = event.target.attributes['data-choice'].nodeValue;
310 var actual_measure = (measure === '__count') ? null : measure;
311 this.pivot.config({measure:actual_measure});
314 option_selection: function (event) {
315 event.preventDefault();
316 switch (event.target.attributes['data-choice'].nodeValue) {
318 this.pivot.swap_axis();
321 this.pivot.rows.headers = null;
322 this.pivot.cols.headers = null;
323 this.pivot.update_data();
325 case 'update_values':
326 this.pivot.update_data();
329 // Export code... To do...
334 header_cell_clicked: function (event) {
335 event.preventDefault();
336 event.stopPropagation();
337 var id = event.target.attributes['data-id'].nodeValue,
338 header = this.pivot.get_header(id),
341 if (header.is_expanded) {
342 this.pivot.fold(header);
344 if (header.path.length < header.root.groupby.length) {
345 var field = header.root.groupby[header.path.length];
346 this.pivot.expand(id, field);
348 if (!this.important_fields.length) {
351 var fields = _.map(this.important_fields, function (field) {
352 return {id: field, value: self.fields[field].string};
354 this.dropdown = $(QWeb.render('field_selection', {fields:fields, header_id:id}));
355 $(event.target).after(this.dropdown);
356 this.dropdown.css({position:'absolute',
359 this.$('.field-selection').next('.dropdown-menu').toggle();
364 field_selection: function (event) {
365 var id = event.target.attributes['data-id'].nodeValue,
366 field_id = event.target.attributes['data-field-id'].nodeValue;
367 event.preventDefault();
368 this.pivot.expand(id, field_id);
371 /******************************************************************************
372 * Drawing pivot table methods...
373 ******************************************************************************/
374 draw_table: function () {
375 this.pivot.rows.main.title = 'Total';
376 this.pivot.cols.main.title = this.measure_label();
377 this.draw_top_headers();
378 _.each(this.pivot.rows.headers, this.proxy('draw_row'));
381 measure_label: function () {
382 return (this.pivot.measure) ? this.fields[this.pivot.measure].string : 'Quantity';
385 make_border_cell: function (colspan, rowspan) {
386 return $('<td></td>').addClass('graph_border')
387 .attr('colspan', colspan)
388 .attr('rowspan', rowspan);
391 make_header_title: function (header) {
392 return $('<span> </span>')
393 .addClass('web_graph_click')
395 .addClass((header.is_expanded) ? 'fa fa-minus-square' : 'fa fa-plus-square')
396 .append((header.title !== undefined) ? header.title : 'Undefined');
399 draw_top_headers: function () {
402 height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})),
403 header_cells = [[this.make_border_cell(1, height)]];
405 function set_dim (cols) {
406 _.each(cols.children, set_dim);
407 if (cols.children.length === 0) {
408 cols.height = height - cols.path.length + 1;
412 cols.width = _.reduce(cols.children, function (sum,c) { return sum + c.width;}, 0);
416 function make_col_header (col) {
417 var cell = self.make_border_cell(col.width, col.height);
418 return cell.append(self.make_header_title(col).attr('data-id', col.id));
421 function make_cells (queue, level) {
423 queue = _.rest(queue).concat(col.children);
424 if (col.path.length == level) {
425 _.last(header_cells).push(make_col_header(col));
428 header_cells.push([make_col_header(col)]);
430 if (queue.length !== 0) {
431 make_cells(queue, level);
435 set_dim(pivot.cols.main); // add width and height info to columns headers
436 if (pivot.cols.main.children.length === 0) {
437 make_cells(pivot.cols.headers, 0);
439 make_cells(pivot.cols.main.children, 1);
440 header_cells[0].push(self.make_border_cell(1, height).append('Total').css('font-weight', 'bold'));
443 _.each(header_cells, function (cells) {
444 self.table.append($('<tr></tr>').append(cells));
448 get_measure_type: function () {
449 var measure = this.pivot.measure;
450 return (measure) ? this.fields[measure].type : 'integer';
453 draw_row: function (row) {
456 measure_type = this.get_measure_type(),
457 html_row = $('<tr></tr>'),
458 row_header = this.make_border_cell(1,1)
459 .append(this.make_header_title(row).attr('data-id', row.id))
460 .addClass('graph_border');
462 for (var i in _.range(row.path.length)) {
463 row_header.prepend($('<span/>', {class:'web_graph_indent'}));
466 html_row.append(row_header);
468 _.each(pivot.cols.headers, function (col) {
469 if (col.children.length === 0) {
470 var value = pivot.get_value(row.id, col.id),
471 cell = make_cell(value, col);
472 html_row.append(cell);
476 if (pivot.cols.main.children.length > 0) {
477 var cell = make_cell(pivot.get_total(row), pivot.cols.main)
478 .css('font-weight', 'bold');
479 html_row.append(cell);
482 this.table.append(html_row);
484 function make_cell (value, col) {
487 cell = $('<td></td>');
488 if ((self.mode === 'pivot') && (row.is_expanded) && (row.path.length <=2)) {
489 color = row.path.length * 5 + 240;
490 cell.css('background-color', $.Color(color, color, color));
492 if (value === undefined) {
495 cell.append(instance.web.format_value(value, {type: measure_type}));
496 if (self.mode === 'heatmap') {
497 total = pivot.get_total();
498 color = Math.floor(50 + 205*(total - value)/total);
499 cell.css('background-color', $.Color(255, color, color));
501 if (self.mode === 'row_heatmap') {
502 total = pivot.get_total(row);
503 color = Math.floor(50 + 205*(total - value)/total);
504 cell.css('background-color', $.Color(255, color, color));
506 if (self.mode === 'col_heatmap') {
507 total = pivot.get_total(col);
508 color = Math.floor(50 + 205*(total - value)/total);
509 cell.css('background-color', $.Color(255, color, color));
515 /******************************************************************************
516 * Drawing charts methods...
517 ******************************************************************************/
518 bar_chart: function () {
520 dim_x = this.pivot.rows.groupby.length,
521 dim_y = this.pivot.cols.groupby.length,
524 // No groupby **************************************************************
525 if ((dim_x === 0) && (dim_y === 0)) {
526 data = [{key: 'Total', values:[{
528 y: this.pivot.get_value(this.pivot.rows.main.id, this.pivot.cols.main.id),
530 // Only column groupbys ****************************************************
531 } else if ((dim_x === 0) && (dim_y >= 1)){
532 data = _.map(this.pivot.get_columns_depth(1), function (header) {
535 values: [{x:header.root.main.title, y: self.pivot.get_total(header)}]
538 // Just 1 row groupby ******************************************************
539 } else if ((dim_x === 1) && (dim_y === 0)) {
540 data = _.map(this.pivot.rows.main.children, function (pt) {
541 var value = self.pivot.get_value(pt.id, self.pivot.cols.main.id),
542 title = (pt.title !== undefined) ? pt.title : 'Undefined';
543 return {x: title, y: value};
545 data = [{key: this.measure_label(), values:data}];
546 // 1 row groupby and some col groupbys**************************************
547 } else if ((dim_x === 1) && (dim_y >= 1)) {
548 data = _.map(this.pivot.get_columns_depth(1), function (colhdr) {
549 var values = _.map(self.pivot.get_rows_depth(1), function (header) {
551 x: header.title || 'Undefined',
552 y: self.pivot.get_value(header.id, colhdr.id, 0)
555 return {key: colhdr.title || 'Undefined', values: values};
557 // At least two row groupby*************************************************
559 var keys = _.uniq(_.map(this.pivot.get_rows_depth(2), function (hdr) {
560 return hdr.title || 'Undefined';
562 data = _.map(keys, function (key) {
563 var values = _.map(self.pivot.get_rows_depth(1), function (hdr) {
564 var subhdr = _.find(hdr.children, function (child) {
565 return ((child.title === key) || ((child.title === undefined) && (key === 'Undefined')));
568 x: hdr.title || 'Undefined',
569 y: (subhdr) ? self.pivot.get_total(subhdr) : 0
572 return {key:key, values: values};
576 nv.addGraph(function () {
577 var chart = nv.models.multiBarChart()
580 .stacked(self.bar_ui === 'stack')
581 .staggerLabels(true);
583 if (dim_x === 1 && dim_y === 0) { chart.showControls(false); }
587 .attr('width', self.width)
588 .attr('height', self.height)
591 nv.utils.windowResize(chart.update);
597 line_chart: function () {
599 dim_x = this.pivot.rows.groupby.length,
600 dim_y = this.pivot.cols.groupby.length;
602 var data = _.map(this.pivot.get_cols_leaves(), function (col) {
603 var values = _.map(self.pivot.get_rows_depth(dim_x), function (row) {
604 return {x: row.title, y: self.pivot.get_value(row.id,col.id, 0)};
606 var title = _.map(col.path, function (p) {
607 return p || 'Undefined';
610 title = self.measure_label();
612 return {values: values, key: title};
615 nv.addGraph(function () {
616 var chart = nv.models.lineChart()
617 .x(function (d,u) { return u; })
620 .margin({top: 30, right: 20, bottom: 20, left: 60});
623 .attr('width', self.width)
624 .attr('height', self.height)
632 pie_chart: function () {
634 dim_x = this.pivot.rows.groupby.length;
636 var data = _.map(this.pivot.get_rows_leaves(), function (row) {
637 var title = _.map(row.path, function (p) {
638 return p || 'Undefined';
641 title = self.measure_label;
643 return {x: title, y: self.pivot.get_total(row)};
646 nv.addGraph(function () {
647 var chart = nv.models.pieChart()
648 .color(d3.scale.category10().range())
650 .height(self.height);
654 .transition().duration(1200)
655 .attr('width', self.width)
656 .attr('height', self.height)
659 nv.utils.windowResize(chart.update);