1 /*---------------------------------------------------------
3 *---------------------------------------------------------*/
5 openerp.web_graph = function (openerp) {
7 '#cc99ff', '#ccccff', '#48D1CC', '#CFD784', '#8B7B8B', '#75507b',
8 '#b0008c', '#ff0000', '#ff8e00', '#9000ff', '#0078ff', '#00ff00',
9 '#e6ff00', '#ffff00', '#905000', '#9b0000', '#840067', '#9abe00',
10 '#ffc900', '#510090', '#0000c9', '#009b00', '#75507b', '#3465a4',
11 '#73d216', '#c17d11', '#edd400', '#fcaf3e', '#ef2929', '#ff00c9',
12 '#ad7fa8', '#729fcf', '#8ae234', '#e9b96e', '#fce94f', '#f57900',
13 '#cc0000', '#d400a8'];
15 var QWeb = openerp.web.qweb,
16 _lt = openerp.web._lt;
17 openerp.web.views.add('graph', 'openerp.web_graph.GraphView');
18 openerp.web_graph.GraphView = openerp.web.View.extend({
19 display_name: _lt('Graph'),
21 init: function(parent, dataset, view_id, options) {
23 this.set_default_options(options);
24 this.dataset = dataset;
25 this.view_id = view_id;
27 this.first_field = null;
31 this.group_field = null;
32 this.is_loaded = $.Deferred();
38 clearTimeout(this.renderer);
46 if (this.embedded_view) {
47 loaded = $.when([self.embedded_view]);
49 loaded = this.rpc('/web/view/load', {
50 model: this.dataset.model,
51 view_id: this.view_id,
56 this.dataset.call_and_eval('fields_get', [false, {}], null, 1),
58 .then(function (fields_result, view_result) {
59 self.fields = fields_result[0];
60 self.fields_view = view_result[0];
61 self.on_loaded(self.fields_view);
65 * Returns all object fields involved in the graph view
67 list_fields: function () {
68 var fs = [this.abscissa];
69 fs.push.apply(fs, _(this.columns).pluck('name'));
70 if (this.group_field) {
71 fs.push(this.group_field);
75 on_loaded: function() {
76 this.chart = this.fields_view.arch.attrs.type || 'pie';
77 this.orientation = this.fields_view.arch.attrs.orientation || 'vertical';
79 _.each(this.fields_view.arch.children, function (field) {
80 var attrs = field.attrs;
82 this.group_field = attrs.name;
83 } else if(!this.abscissa) {
84 this.first_field = this.abscissa = attrs.name;
88 operator: attrs.operator || '+'
92 this.ordinate = this.columns[0].name;
93 this.is_loaded.resolve();
95 schedule_chart: function(results) {
97 this.$element.html(QWeb.render("GraphView", {
98 "fields_view": this.fields_view,
100 'element_id': this.widget_parent.element_id
103 var fields = _(this.columns).pluck('name').concat([this.abscissa]);
104 if (this.group_field) { fields.push(this.group_field); }
105 // transform search result into usable records (convert from OpenERP
106 // value shapes to usable atomic types
107 if(self.fields[self.abscissa].type == 'selection'){
108 results = self.sortSelection(results);
110 var records = _(results).map(function (result) {
112 _(result).each(function (value, field) {
113 if (!_(fields).contains(field)) { return; }
114 if (value === false) { point[field] = false; return; }
115 switch (self.fields[field].type) {
117 point[field] = _(self.fields[field].selection).detect(function (choice) {
118 return choice[0] === value;
122 point[field] = value[1];
124 case 'integer': case 'float': case 'char':
125 case 'date': case 'datetime':
126 point[field] = value;
130 "Unknown field type " + self.fields[field].type
131 + "for field " + field + " (" + value + ")");
136 // aggregate data, because dhtmlx is crap. Aggregate on abscissa field,
137 // leave split on group field => max m*n records where m is the # of
138 // values for the abscissa and n is the # of values for the group field
140 _(records).each(function (record) {
141 var abscissa = record[self.abscissa],
142 group = record[self.group_field];
143 var r = _(graph_data).detect(function (potential) {
144 return potential[self.abscissa] === abscissa
145 && (!self.group_field
146 || potential[self.group_field] === group);
148 var datapoint = r || {};
150 datapoint[self.abscissa] = abscissa;
151 if (self.group_field) { datapoint[self.group_field] = group; }
152 _(self.columns).each(function (column) {
153 var val = record[column.name],
154 aggregate = datapoint[column.name];
155 switch(column.operator) {
157 datapoint[column.name] = (aggregate || 0) + val;
160 datapoint[column.name] = (aggregate || 1) * val;
163 datapoint[column.name] = (aggregate || Infinity) > val
168 datapoint[column.name] = (aggregate || -Infinity) < val
174 if (!r) { graph_data.push(datapoint); }
176 if(self.fields[self.abscissa].type != 'selection'){
177 graph_data = _(graph_data).sortBy(function (point) {
178 return point[self.abscissa] + '[[--]]' + point[self.group_field];
181 if (_.include(['bar','line','area'],this.chart)) {
182 return this.schedule_bar_line_area(graph_data);
183 } else if (this.chart == "pie") {
184 return this.schedule_pie(graph_data);
187 schedule_bar_line_area: function(results) {
190 view_chart = (self.chart == 'line')?'line':(self.chart == 'area')?'area':'';
191 if (!this.group_field || !results.length) {
192 if (self.chart == 'bar'){
193 view_chart = (this.orientation === 'horizontal') ? 'barH' : 'bar';
195 group_list = _(this.columns).map(function (column, index) {
198 text: self.fields[column.name].string,
199 color: COLOR_PALETTE[index % (COLOR_PALETTE.length)]
203 // dhtmlx handles clustered bar charts (> 1 column per abscissa
204 // value) and stacked bar charts (basically the same but with the
205 // columns on top of one another instead of side by side), but it
206 // does not handle clustered stacked bar charts
207 if (self.chart == 'bar' && (this.columns.length > 1)) {
209 'OpenERP Web does not support combining grouping and '
210 + 'multiple columns in graph at this time.');
212 'dhtmlx can not handle columns counts of that magnitude');
214 // transform series for clustered charts into series for stacked
216 if (self.chart == 'bar'){
217 view_chart = (this.orientation === 'horizontal')
218 ? 'stackedBarH' : 'stackedBar';
220 group_list = _(results).chain()
221 .pluck(this.group_field)
223 .map(function (value, index) {
226 groupval = value.toLowerCase().replace(/[\s\/]+/g,'_');
229 group: _.str.sprintf('%s_%s', self.ordinate, groupval),
231 color: COLOR_PALETTE[index % COLOR_PALETTE.length]
235 results = _(results).chain()
236 .groupBy(function (record) { return record[self.abscissa]; })
237 .map(function (records) {
239 // second argument is coerced to a str, no good for boolean
240 r[self.abscissa] = records[0][self.abscissa];
241 _(records).each(function (record) {
242 var value = record[self.group_field];
244 value = value.toLowerCase().replace(/[\s\/]+/g,'_');
246 var key = _.str.sprintf('%s_%s', self.ordinate, value);
247 r[key] = record[self.ordinate];
253 var abscissa_description = {
254 title: "<b>" + this.fields[this.abscissa].string + "</b>",
255 template: function (obj) {
256 return obj[self.abscissa] || 'Undefined';
259 var ordinate_description = {
261 title: "<b>" + this.fields[this.ordinate].string + "</b>"
265 if (self.chart == 'bar' && self.orientation == 'horizontal') {
266 x_axis = ordinate_description;
267 y_axis = abscissa_description;
269 x_axis = abscissa_description;
270 y_axis = ordinate_description;
272 var renderer = function () {
273 if (self.$element.is(':hidden')) {
274 self.renderer = setTimeout(renderer, 100);
277 self.renderer = null;
278 var charts = new dhtmlXChart({
280 container: self.widget_parent.element_id+"-"+self.chart+"chart",
281 value:"#"+group_list[0].group+"#",
282 gradient: (self.chart == "bar") ? "3d" : "light",
283 alpha: (self.chart == "area") ? 0.6 : 1,
287 template: _.str.sprintf("#%s#, %s=#%s#",
288 self.abscissa, group_list[0].text, group_list[0].group)
291 color: (self.chart != "line") ? group_list[0].color : "",
292 item: (self.chart == "line") ? {
293 borderColor: group_list[0].color,
296 line: (self.chart == "line") ? {
297 color: group_list[0].color,
317 self.$element.find("#"+self.widget_parent.element_id+"-"+self.chart+"chart").width(
318 self.$element.find("#"+self.widget_parent.element_id+"-"+self.chart+"chart").width()+120);
320 for (var m = 1; m<group_list.length;m++){
321 var column = group_list[m];
322 if (column.group === self.group_field) { continue; }
324 value: "#"+column.group+"#",
326 template: _.str.sprintf("#%s#, %s=#%s#",
327 self.abscissa, column.text, column.group)
329 color: (self.chart != "line") ? column.color : "",
330 item: (self.chart == "line") ? {
331 borderColor: column.color,
334 line: (self.chart == "line") ? {
340 charts.parse(results, "json");
341 self.$element.find("#"+self.widget_parent.element_id+"-"+self.chart+"chart").height(
342 self.$element.find("#"+self.widget_parent.element_id+"-"+self.chart+"chart").height()+50);
343 charts.attachEvent("onItemClick", function(id) {
344 self.open_list_view(charts.get(id));
348 clearTimeout(this.renderer);
350 this.renderer = setTimeout(renderer, 0);
352 schedule_pie: function(result) {
354 var renderer = function () {
355 if (self.$element.is(':hidden')) {
356 self.renderer = setTimeout(renderer, 100);
359 self.renderer = null;
360 var chart = new dhtmlXChart({
362 container:self.widget_parent.element_id+"-piechart",
363 value:"#"+self.ordinate+"#",
364 pieInnerText:function(obj) {
365 var sum = chart.sum("#"+self.ordinate+"#");
366 var val = obj[self.ordinate] / sum * 100 ;
367 return val.toFixed(1) + "%";
370 template:"#"+self.abscissa+"#"+"="+"#"+self.ordinate+"#"
384 template:function(obj){
385 return obj[self.abscissa] || 'Undefined';
389 chart.parse(result,"json");
390 chart.attachEvent("onItemClick", function(id) {
391 self.open_list_view(chart.get(id));
395 clearTimeout(this.renderer);
397 this.renderer = setTimeout(renderer, 0);
399 sortSelection: function(results){
401 var grouped_data = _(results).groupBy(function(result){
402 return result[self.abscissa];
404 var options = _(self.fields[self.abscissa].selection).map(function(option){ return option[0]; })
405 var sorted_data = _(options).chain().map(function(option){
406 return grouped_data[option];
407 }).filter(function(data){ return data != undefined; }).value();
408 if(sorted_data.length){
409 sorted_data = _.reduceRight(sorted_data, function(a, b){ return b.concat(a);})
413 open_list_view : function (id){
415 // unconditionally nuke tooltips before switching view
416 $(".dhx_tooltip").remove('div');
417 id = id[this.abscissa];
418 if(this.fields[this.abscissa].type == "selection"){
419 id = _.detect(this.fields[this.abscissa].selection,function(select_value){
420 return _.include(select_value, id);
423 if (typeof id == 'object'){
428 if (this.widget_parent.action) {
429 views = this.widget_parent.action.views;
430 if (!_(views).detect(function (view) {
431 return view[1] === 'list' })) {
432 views = [[false, 'list']].concat(views);
435 views = _(["list", "form", "graph"]).map(function(mode) {
436 return [false, mode];
440 res_model : this.dataset.model,
441 domain: [[this.abscissa, '=', id], ['id','in',this.dataset.ids]],
443 type: "ir.actions.act_window",
444 flags: {default_view: 'list'}
448 do_search: function(domain, context, group_by) {
450 return $.when(this.is_loaded).pipe(function() {
451 // TODO: handle non-empty group_by with read_group?
452 if (!_(group_by).isEmpty()) {
453 self.abscissa = group_by[0];
455 self.abscissa = self.first_field;
457 return self.dataset.read_slice(self.list_fields()).then($.proxy(self, 'schedule_chart'));
461 do_show: function() {
462 this.do_push_state({});
463 return this._super();
467 // vim:et fdc=0 fdl=0: