Billboard must receive the graph element directly because the chart can be rendered while it is still detached from the document. Closes #14970 (grafted from 00123388953eb5d0e7cee0ff3b8d005a826db9e9) --HG-- branch : 8.0
376 lines
14 KiB
JavaScript
376 lines
14 KiB
JavaScript
/* This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
this repository contains the full copyright notices and license terms. */
|
|
(function() {
|
|
'use strict';
|
|
|
|
Sao.View.GraphXMLViewParser = Sao.class_(Sao.View.XMLViewParser, {
|
|
init: function(view, exclude_field, fields) {
|
|
Sao.View.GraphXMLViewParser._super.init.call(
|
|
this, view, exclude_field, fields);
|
|
this._xfield = null;
|
|
this._yfields = [];
|
|
},
|
|
_node_attributes: function(node) {
|
|
var node_attrs = {};
|
|
for (const attribute of node.attributes) {
|
|
node_attrs[attribute.name] = attribute.value;
|
|
}
|
|
if (node_attrs.name) {
|
|
if (!node_attrs.string && (node_attrs.name != '#')) {
|
|
var field = this.field_attrs[node_attrs.name];
|
|
node_attrs.string = field.string;
|
|
}
|
|
}
|
|
return node_attrs;
|
|
},
|
|
_parse_graph: function(node, attributes) {
|
|
for (const child of node.childNodes) {
|
|
this.parse(child);
|
|
}
|
|
var Widget = Sao.View.GraphXMLViewParser.WIDGETS[
|
|
attributes.type || 'vbar'];
|
|
var widget = new Widget(this.view, this._xfield, this._yfields);
|
|
this.view.el.append(widget.el);
|
|
this.view.widgets.root = widget;
|
|
},
|
|
_parse_x: function(node, attributes) {
|
|
for (const child of node.children) {
|
|
this._xfield = this._node_attributes(child);
|
|
}
|
|
},
|
|
_parse_y: function(node, attributes) {
|
|
for (const child of node.children) {
|
|
this._yfields.push(this._node_attributes(child));
|
|
}
|
|
}
|
|
});
|
|
|
|
Sao.View.Graph = Sao.class_(Sao.View, {
|
|
editable: false,
|
|
view_type: 'graph',
|
|
xml_parser: Sao.View.GraphXMLViewParser,
|
|
init: function(view_id, screen, xml, children_field) {
|
|
this.el = jQuery('<div/>', {
|
|
'class': 'graph'
|
|
});
|
|
|
|
Sao.View.Graph._super.init.call(this, view_id, screen, xml);
|
|
},
|
|
display: function() {
|
|
return this.widgets.root.display(this.group);
|
|
}
|
|
});
|
|
|
|
Sao.View.Graph.Chart = Sao.class_(Object, {
|
|
_chart_type: undefined,
|
|
|
|
init: function(view, xfield, yfields) {
|
|
this.view = view;
|
|
this.xfield = xfield;
|
|
this.yfields = yfields;
|
|
this.el = jQuery('<div/>');
|
|
},
|
|
update_data: function(group) {
|
|
var data = {};
|
|
var record, yfield, key;
|
|
var i, len, j, y_len;
|
|
|
|
this.ids = {};
|
|
data.columns = [['labels']];
|
|
data.names = {};
|
|
var key2columns = {};
|
|
var fields2load = [this.xfield.name];
|
|
for (i = 0, len = this.yfields.length; i < len; i++) {
|
|
yfield = this.yfields[i];
|
|
key = yfield.key || yfield.name;
|
|
data.columns.push([key]);
|
|
data.names[key] = yfield.string;
|
|
key2columns[key] = i + 1;
|
|
fields2load.push(yfield.name);
|
|
}
|
|
|
|
var prms = [];
|
|
const set_data = index => {
|
|
return () => {
|
|
record = group[index];
|
|
var x = record.field_get_client(this.xfield.name);
|
|
// billboard does not support moment
|
|
if (x && (x.isDate || x.isDateTime)) {
|
|
x = x.toString();
|
|
}
|
|
var pos = data.columns[0].indexOf(x);
|
|
if (pos < 0) {
|
|
pos = data.columns[0].push(x) - 1;
|
|
}
|
|
this._add_id(x, record.id);
|
|
|
|
var column;
|
|
for (j = 0, y_len = this.yfields.length; j < y_len; j++) {
|
|
yfield = this.yfields[j];
|
|
key = yfield.key || yfield.name;
|
|
column = data.columns[key2columns[key]];
|
|
if (column[pos] === undefined) {
|
|
column[pos] = null;
|
|
}
|
|
if (yfield.domain) {
|
|
var ctx = jQuery.extend({},
|
|
Sao.Session.current_session.context);
|
|
ctx.context = ctx;
|
|
ctx._user = Sao.Session.current_session.user_id;
|
|
for (var field in group.model.fields) {
|
|
ctx[field] = record.field_get(field);
|
|
}
|
|
var decoder = new Sao.PYSON.Decoder(ctx);
|
|
if (!decoder.decode(yfield.domain)) {
|
|
continue;
|
|
}
|
|
}
|
|
if (!column[pos]) {
|
|
column[pos] = 0;
|
|
}
|
|
if (yfield.name == '#') {
|
|
column[pos] += 1;
|
|
} else {
|
|
var value = record.field_get(yfield.name);
|
|
if (value && value.isTimeDelta) {
|
|
value = value.asSeconds();
|
|
}
|
|
column[pos] += value || 0;
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
var r_prms = [];
|
|
for (i = 0, len = group.length; i < len; i++) {
|
|
record = group[i];
|
|
for (const fname of fields2load) {
|
|
prms.push(record.load(fname));
|
|
}
|
|
r_prms.push(
|
|
jQuery.when.apply(jQuery, prms).then(set_data(i)));
|
|
}
|
|
return jQuery.when.apply(jQuery, r_prms).then(function() {
|
|
return data;
|
|
});
|
|
},
|
|
_add_id: function(key, id) {
|
|
if (!(key in this.ids)) {
|
|
this.ids[key] = [];
|
|
}
|
|
this.ids[key].push(id);
|
|
},
|
|
display: function(group) {
|
|
var update_prm = this.update_data(group);
|
|
update_prm.done(data => {
|
|
bb.generate(this._bb_config(data));
|
|
});
|
|
return update_prm;
|
|
},
|
|
_bb_config: function(data) {
|
|
var bb_config = {};
|
|
|
|
bb_config.bindto = this.el.get(0);
|
|
bb_config.data = data;
|
|
bb_config.data.type = this._chart_type;
|
|
bb_config.data.x = 'labels';
|
|
bb_config.data.onclick = this.action.bind(this);
|
|
|
|
var type = this.view.screen.model.fields[this.xfield.name]
|
|
.description.type;
|
|
if ((type == 'date') || (type == 'datetime')) {
|
|
var format_func, date_format, time_format;
|
|
date_format = Sao.common.date_format(
|
|
this.view.screen.context.date_format);
|
|
time_format = '%X';
|
|
if (type == 'datetime') {
|
|
bb_config.data.xFormat = '%Y-%m-%d %H:%M:%S';
|
|
format_func = function(dt) {
|
|
return Sao.common.format_datetime(
|
|
date_format + ' ' + time_format, moment(dt));
|
|
};
|
|
} else {
|
|
bb_config.data.xFormat = '%Y-%m-%d';
|
|
format_func = function(dt) {
|
|
return Sao.common.format_date(date_format, moment(dt));
|
|
};
|
|
}
|
|
bb_config.axis = {
|
|
x: {
|
|
type: 'timeseries',
|
|
tick: {
|
|
format: format_func,
|
|
}
|
|
}
|
|
};
|
|
} else {
|
|
bb_config.axis = {
|
|
x: {
|
|
type: 'category',
|
|
}
|
|
};
|
|
}
|
|
let keys = this._data_keys(data);
|
|
var color = this.view.attributes.color || Sao.config.graph_color;
|
|
var rgb = Sao.common.hex2rgb(
|
|
Sao.common.COLOR_SCHEMES[color] || color);
|
|
var maxcolor = Math.max.apply(null, rgb);
|
|
var colors = Sao.common.generateColorscheme(
|
|
color, keys, maxcolor / (keys.length || 1));
|
|
for (let i = 0; i < this.yfields.length; i++) {
|
|
let yfield = this.yfields[i];
|
|
if (yfield.color) {
|
|
colors[yfield.key || yfield.name] = yfield.color;
|
|
}
|
|
}
|
|
bb_config.data.color = function(color, column) {
|
|
// column is an object when called for legend
|
|
var key = column.id || column;
|
|
return colors[key] || color;
|
|
};
|
|
return bb_config;
|
|
},
|
|
_data_keys: function(data) {
|
|
let keys = [];
|
|
for (let i = 0; i < this.yfields.length; i++) {
|
|
let yfield = this.yfields[i];
|
|
keys.push(yfield.key || yfield.name);
|
|
}
|
|
return keys;
|
|
},
|
|
action: function(data, element) {
|
|
var ids = this.ids[this._action_key(data)];
|
|
var ctx = jQuery.extend({}, this.view.screen.group.local_context);
|
|
delete ctx.active_ids;
|
|
delete ctx.active_id;
|
|
Sao.Action.exec_keyword('graph_open', {
|
|
model: this.view.screen.model_name,
|
|
id: ids[0],
|
|
ids: ids
|
|
}, ctx, false);
|
|
},
|
|
_action_key: function(data) {
|
|
var x = data.x;
|
|
var type = this.view.screen.model.fields[this.xfield.name]
|
|
.description.type;
|
|
if (x && (type == 'datetime')) {
|
|
x = Sao.DateTime(x).toString();
|
|
} else if (x && (type == 'date')) {
|
|
x = Sao.Date(x).toString();
|
|
}
|
|
return x;
|
|
}
|
|
});
|
|
|
|
Sao.View.Graph.VerticalBar = Sao.class_(Sao.View.Graph.Chart, {
|
|
_chart_type: 'bar'
|
|
});
|
|
|
|
Sao.View.Graph.HorizontalBar = Sao.class_(Sao.View.Graph.Chart, {
|
|
_chart_type: 'bar',
|
|
_bb_config: function(data) {
|
|
var config = Sao.View.Graph.HorizontalBar._super._bb_config
|
|
.call(this, data);
|
|
config.axis.rotated = true;
|
|
return config;
|
|
}
|
|
});
|
|
|
|
Sao.View.Graph.Line = Sao.class_(Sao.View.Graph.Chart, {
|
|
_chart_type: 'line',
|
|
_bb_config: function(data) {
|
|
var config = Sao.View.Graph.Line._super._bb_config
|
|
.call(this, data);
|
|
config.line = {
|
|
connectNull: true,
|
|
};
|
|
return config;
|
|
}
|
|
});
|
|
|
|
Sao.View.Graph.Pie = Sao.class_(Sao.View.Graph.Chart, {
|
|
_chart_type: 'pie',
|
|
_bb_config: function(data) {
|
|
var config = Sao.View.Graph.Pie._super._bb_config.call(this, data);
|
|
var pie_columns = [], pie_names = {};
|
|
var i, len;
|
|
var labels, values;
|
|
|
|
for (i = 0, len = data.columns.length; i < len; i++) {
|
|
if (data.columns[i][0] == 'labels') {
|
|
labels = data.columns[i].slice(1);
|
|
} else {
|
|
values = data.columns[i].slice(1);
|
|
}
|
|
}
|
|
|
|
// Pie chart do not support axis definition.
|
|
delete config.axis;
|
|
delete config.data.x;
|
|
var format_func;
|
|
var type = this.view.screen.model.fields[this.xfield.name]
|
|
.description.type;
|
|
if ((type == 'date') || (type == 'datetime')) {
|
|
var date_format = Sao.common.date_format(
|
|
this.view.screen.context.date_format);
|
|
var datetime_format = date_format + ' %X';
|
|
if (type == 'datetime') {
|
|
format_func = function(dt) {
|
|
return Sao.common.format_datetime(datetime_format, dt);
|
|
};
|
|
} else {
|
|
format_func = function(dt) {
|
|
return Sao.common.format_date(date_format, dt);
|
|
};
|
|
}
|
|
}
|
|
var label;
|
|
for (i = 0, len = labels.length; i < len; i++) {
|
|
label = labels[i];
|
|
if (format_func) {
|
|
label = format_func(label);
|
|
}
|
|
pie_columns.push([i, values[i]]);
|
|
pie_names[i] = label;
|
|
}
|
|
|
|
config.data.columns = pie_columns;
|
|
config.data.names = pie_names;
|
|
config.data.order = null;
|
|
return config;
|
|
},
|
|
_data_keys: function(data) {
|
|
let keys = [];
|
|
for (let i = 0; i < data.columns[1].length - 1; i++) {
|
|
keys.push(i);
|
|
};
|
|
return keys;
|
|
},
|
|
_add_id: function(key, id) {
|
|
var type = this.xfield.type;
|
|
if ((type == 'date') || (type == 'datetime')) {
|
|
var date_format = Sao.common.date_format(
|
|
this.view.screen.context.date_format);
|
|
var datetime_format = date_format + ' %X';
|
|
if (type == 'datetime') {
|
|
key = Sao.common.format_datetime(datetime_format, key);
|
|
} else {
|
|
key = Sao.common.format_date(date_format, key);
|
|
}
|
|
}
|
|
Sao.View.Graph.Pie._super._add_id.call(this, key, id);
|
|
},
|
|
_action_key: function(data) {
|
|
// data.name is the label used for the x axis
|
|
return data.name;
|
|
}
|
|
});
|
|
|
|
Sao.View.GraphXMLViewParser.WIDGETS = {
|
|
'hbar': Sao.View.Graph.HorizontalBar,
|
|
'line': Sao.View.Graph.Line,
|
|
'pie': Sao.View.Graph.Pie,
|
|
'vbar': Sao.View.Graph.VerticalBar,
|
|
};
|
|
}());
|