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 two widgets (PivotTable and ChartView)
21 instance.web_graph.GraphView = instance.web.View.extend({
22 template: 'GraphView',
23 display_name: _lt('Graph'),
25 mode: 'pivot', // pivot => display pivot table, chart => display chart
28 'click .graph_mode_selection li' : function (event) {
29 event.preventDefault();
30 var view_mode = event.target.attributes['data-mode'].nodeValue;
31 if (view_mode === 'data') {
35 this.chart_view.set_mode(view_mode);
39 'click .graph_clear_groups' : function (event) {
40 this.pivot_table.clear_groups();
44 view_loading: function (fields_view_get) {
46 var model = new instance.web.Model(fields_view_get.model, {group_by_no_leaf: true});
49 options.col_groupby = [];
51 // get the default groupbys and measure defined in the field view
52 options.measure = null;
53 options.row_groupby = [];
54 _.each(fields_view_get.arch.children, function (field) {
55 if ('name' in field.attrs) {
56 if ('operator' in field.attrs) {
57 options.measure = field.attrs.name;
59 options.row_groupby.push(field.attrs.name);
64 // get the most important fields (of the model) by looking at the
65 // groupby filters defined in the search view
66 options.important_fields = [];
67 var load_view = instance.web.fields_view_get({
72 var important_fields_def = $.when(load_view).then(function (search_view) {
73 var groups = _.select(search_view.arch.children, function (c) {
74 return (c.tag == 'group') && (c.attrs.string != 'Display');
77 _.each(groups, function(g) {
78 _.each(g.children, function (g) {
79 if (g.attrs.context) {
80 var field_id = py.eval(g.attrs.context).group_by;
81 options.important_fields.push(field_id);
87 // get the fields descriptions from the model
88 var field_descr_def = model.call('fields_get', [])
89 .then(function (fields) { options.fields = fields; });
92 return $.when(important_fields_def, field_descr_def)
94 self.pivot_table = new PivotTable(model, options);
95 self.chart_view = new ChartView(model, options);
98 return self.pivot_table.appendTo('.graph_main_content');
101 return self.chart_view.appendTo('.graph_main_content');
105 display_data : function () {
106 if (this.mode === 'pivot') {
107 this.chart_view.hide();
108 this.pivot_table.show();
110 this.pivot_table.hide();
111 this.chart_view.show();
115 do_search: function (domain, context, group_by) {
116 this.domain = new instance.web.CompoundDomain(domain);
117 this.pivot_table.set_domain(domain);
118 this.chart_view.set_domain(domain);
122 do_show: function () {
123 this.do_push_state({});
124 return this._super();
130 * BasicDataView widget. Basic widget to manage show/hide functionality
131 * and to initialize some attributes. It is inherited by PivotTable
132 * and ChartView widget.
134 var BasicDataView = instance.web.Widget.extend({
139 // model: model to display
140 // fields: dictionary returned by field_get on model (desc of model)
141 // domain: constraints on model records
142 // row_groupby: groubys on rows (so, row headers in the pivot table)
143 // col_groupby: idem, but on col
144 // measure: quantity to display. either a field from the model, or
145 // null, in which case we use the "count" measure
146 init: function (model, options) {
147 console.log("initializing", model, options);
150 this.fields = options.fields;
151 this.domain = options.domain;
153 row: options.row_groupby,
154 col: options.col_groupby,
157 this.measure = options.measure;
158 this.measure_label = options.measure ? options.fields[options.measure].string : 'Quantity';
160 this.need_redraw = true;
161 this.important_fields = options.important_fields;
164 get_descr: function (field_id) {
165 return this.fields[field_id].string;
168 set_domain: function (domain) {
169 this.domain = domain;
170 this.need_redraw = true;
173 set_row_groupby: function (row_groupby) {
174 this.groupby.row = row_groupby;
175 this.need_redraw = true;
178 set_col_groupby: function (col_groupby) {
179 this.groupby.col = col_groupby;
180 this.need_redraw = true;
183 set_measure: function (measure) {
184 this.measure = measure;
185 this.need_redraw = true;
189 if (this.need_redraw) {
191 this.need_redraw = false;
193 this.$el.css('display', 'block');
197 this.$el.css('display', 'none');
203 get_data: function (groupby) {
204 var view_fields = this.groupby.row.concat(this.measure, this.groupby.col);
205 return query_groups(this.model, view_fields, this.domain, groupby);
211 * PivotTable widget. It displays the data in tabular data and allows the
212 * user to drill down and up in the table
214 var PivotTable = BasicDataView.extend({
215 template: 'pivot_table',
221 'click .graph_border > a' : function (event) {
223 event.preventDefault();
224 var row_id = event.target.attributes['data-row-id'].nodeValue;
226 var row = this.get_row(row_id);
228 this.fold_row(row_id);
230 if (row.path.length < this.groupby.row.length) {
231 var field_to_expand = this.groupby.row[row.path.length];
232 this.expand_row(row_id, field_to_expand);
234 var already_grouped = self.groupby.row.concat(self.groupby.col);
235 var possible_groups = _.difference(self.important_fields, already_grouped);
236 var dropdown_options = {
237 fields: _.map(possible_groups, function (field) {
238 return {id: field, value: self.get_descr(field)};
242 this.dropdown = $(QWeb.render('field_selection', dropdown_options));
243 $(event.target).after(this.dropdown);
244 $('.field-selection').next('.dropdown-menu').toggle();
250 'click a.field-selection' : function (event) {
251 event.preventDefault();
252 this.dropdown.remove();
253 var row_id = event.target.attributes['data-row-id'].nodeValue;
254 var field_id = event.target.attributes['data-field-id'].nodeValue;
255 this.expand_row(row_id, field_id);
259 clear_groups: function () {
260 this.groupby.row = [];
261 this.groupby.col = [];
267 init: function (model, options) {
268 this._super(model, options);
271 generate_id: function () {
272 this.current_row_id += 1;
273 return this.current_row_id - 1;
276 get_row: function (id) {
277 return _.find(this.rows, function(row) {
278 return (row.id == id);
282 make_cell: function (content, options) {
284 if (options && options.is_border) {
285 attrs.push('class="graph_border"');
289 if (options && options.indent) {
290 _.each(_.range(options.indent), function () {
291 attrs.push('<span class="web_graph_indent"></span>');
294 if (options && options.foldable) {
295 attrs.push('<a data-row-id="'+ options.row_id + '" href="#" class="icon-plus-sign"> </a>');
297 return attrs.join(' ') + content + '</td>';
300 make_row: function (data, parent_id) {
301 var has_parent = (parent_id !== undefined);
302 var parent = has_parent ? this.get_row(parent_id) : null;
305 path = parent.path.concat(data.attributes.grouped_on);
306 } else if (data.attributes.grouped_on !== undefined) {
307 path = [data.attributes.grouped_on];
312 var indent_level = has_parent ? parent.path.length : 0;
313 var value = (this.groupby.row.length > 0) ? data.attributes.value[1] : 'Total';
316 var jquery_row = $('<tr></tr>');
317 var row_id = this.generate_id();
319 var header = $(this.make_cell(value, {is_border:true, indent: indent_level, foldable:true, row_id: row_id}));
320 jquery_row.html(header);
321 jquery_row.append(this.make_cell(data.attributes.aggregates[this.measure]));
331 domain: data.model._domain,
333 // rows.splice(index of parent if any,0,row);
334 this.rows.push(row); // to do, insert it properly
336 if (this.groupby.row.length === 0) {
337 row.remove_when_expanded = true;
338 row.domain = this.domain;
341 parent.children.push(row.id);
346 expand_row: function (row_id, field_id) {
348 var row = this.get_row(row_id);
350 if (row.path.length == this.groupby.row.length) {
351 this.groupby.row.push(field_id);
354 var visible_fields = this.groupby.row.concat(this.groupby.col, this.measure);
356 if (row.remove_when_expanded) {
360 row.html_tr.find('.icon-plus-sign')
361 .removeClass('icon-plus-sign')
362 .addClass('icon-minus-sign');
365 query_groups(this.model, visible_fields, row.domain, [field_id])
366 .then(function (data) {
367 _.each(data.reverse(), function (datapt) {
369 if (row.remove_when_expanded) {
370 new_row = self.make_row(datapt);
371 self.$('tr.graph_table_header').after(new_row.html_tr);
373 new_row = self.make_row(datapt, row_id);
374 row.html_tr.after(new_row.html_tr);
377 if (row.remove_when_expanded) {
378 row.html_tr.remove();
384 fold_row: function (row_id) {
386 var row = this.get_row(row_id);
388 _.each(row.children, function (child_row) {
389 self.remove_row(child_row);
393 row.expanded = false;
394 row.html_tr.find('.icon-minus-sign')
395 .removeClass('icon-minus-sign')
396 .addClass('icon-plus-sign');
398 var fold_levels = _.map(self.rows, function(g) {return g.path.length;});
399 var new_groupby_length = _.reduce(fold_levels, function (x, y) {
400 return Math.max(x,y);
403 this.groupby.row.splice(new_groupby_length);
406 remove_row: function (row_id) {
408 var row = this.get_row(row_id);
410 _.each(row.children, function (child_row) {
411 self.remove_row(child_row);
414 row.html_tr.remove();
415 removeFromArray(this.rows, row);
419 this.get_data(this.groupby.row)
420 .then(this.proxy('build_table'))
421 .done(this.proxy('_draw'));
424 build_table: function (data) {
430 value: this.measure_label,
436 header: $(this.make_cell(this.measure_label, {is_border:true})),
439 _.each(data, function (datapt) {
440 self.make_row(datapt);
450 if (this.groupby.row.length > 0) {
451 header = '<tr><td class="graph_border">' +
452 this.fields[this.groupby.row[0]].string +
453 '</td><td class="graph_border">' +
457 header = '<tr class="graph_table_header"><td class="graph_border">' +
458 '</td><td class="graph_border">' +
462 this.$el.append(header);
464 _.each(this.rows, function (row) {
465 self.$el.append(row.html_tr);
472 * ChartView widget. It displays the data in chart form, using the nvd3
473 * library. Various modes include bar charts, pie charts or line charts.
475 var ChartView = BasicDataView.extend({
476 template: 'chart_view',
478 set_mode: function (mode) {
479 this.render = this['render_' + mode];
480 this.need_redraw = true;
486 this.$el.append('<svg></svg>');
487 this.get_data(this.groupby.row).done(function (data) {
492 format_data: function (datapt) {
493 var val = datapt.attributes;
495 x: datapt.attributes.value[1],
496 y: this.measure ? val.aggregates[this.measure] : val.length,
500 render_bar_chart: function (data) {
501 var formatted_data = [{
503 values: _.map(data, this.proxy('format_data')),
506 nv.addGraph(function () {
507 var chart = nv.models.discreteBarChart()
514 d3.select('.graph_chart svg')
515 .datum(formatted_data)
520 nv.utils.windowResize(chart.update);
525 render_line_chart: function (data) {
526 var formatted_data = [{
527 key: this.measure_label,
528 values: _.map(data, this.proxy('format_data'))
531 nv.addGraph(function () {
532 var chart = nv.models.lineChart()
533 .x(function (d,u) { return u; })
536 .margin({top: 30, right: 20, bottom: 20, left: 60});
538 d3.select('.graph_chart svg')
541 .datum(formatted_data)
548 render_pie_chart: function (data) {
549 var formatted_data = _.map(data, this.proxy('format_data'));
551 nv.addGraph(function () {
552 var chart = nv.models.pieChart()
553 .color(d3.scale.category10().range())
557 d3.select('.graph_chart svg')
558 .datum(formatted_data)
559 .transition().duration(1200)
564 nv.utils.windowResize(chart.update);
572 function removeFromArray(array, element) {
573 var index = array.indexOf(element);
575 array.splice(index, 1);
580 * Query the server and return a deferred which will return the data
581 * with all the groupbys applied (this is done for now, but the goal
582 * is to modify read_group in order to allow eager and lazy groupbys
584 function query_groups (model, fields, domain, groupbys) {
585 return model.query(fields)
588 .then(function (results) {
589 var non_empty_results = _.filter(results, function (group) {
590 return group.attributes.length > 0;
592 if (groupbys.length <= 1) {
593 return non_empty_results;
595 var get_subgroups = $.when.apply(null, _.map(non_empty_results, function (result) {
596 var new_domain = result.model._domain;
597 var new_groupings = groupbys.slice(1);
598 return query_groups(model, fields,new_domain, new_groupings).then(function (subgroups) {
599 result.subgroups_data = subgroups;
602 return get_subgroups.then(function () {
603 return non_empty_results;