Add chart view

This commit is contained in:
Nicolas Évrard
2015-05-29 18:44:24 +02:00
parent 9fc17c9fec
commit 8a46006ebf
3 changed files with 329 additions and 1 deletions
+4 -1
View File
@@ -21,7 +21,10 @@
"eonasdan-bootstrap-datetimepicker": "~4.7.14",
"typeahead.js": "~0.10.5",
"typeahead.js-bootstrap3.less": "~0",
"gettext.js": "~0.5.0"
"gettext.js": "~0.5.0",
"d3": "~3.5.0",
"c3": "~0.4.10"
"typeahead.js-bootstrap3.less": "~0"
},
"devDependencies": {
"qunit": "~1.18.0"
+3
View File
@@ -12,9 +12,12 @@ this repository contains the full copyright notices and license terms. -->
<script type="text/javascript" src="bower_components/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js"></script>
<script type="text/javascript" src="bower_components/typeahead.js/dist/typeahead.bundle.js"></script>
<script type="text/javascript" src="bower_components/gettext.js/dist/gettext.js"></script>
<script type="text/javascript" src="bower_components/d3/d3.min.js"></script>
<script type="text/javascript" src="bower_components/c3/c3.js"></script>
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" href="bower_components/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css">
<link rel="stylesheet" href="bower_components/typeahead.js-bootstrap3.less/typeahead.css"/>
<link rel="stylesheet" href="bower_components/c3/c3.min.css"/>
<link rel="stylesheet" href="offcanvas.css">
<script type="text/javascript" src="dist/sao.min.js"></script>
<link rel="stylesheet" type="text/css" href="dist/sao.min.css"
+322
View File
@@ -51,6 +51,8 @@
return new Sao.View.Tree(screen, xml, children_field);
case 'form':
return new Sao.View.Form(screen, xml);
case 'graph':
return new Sao.View.Graph(screen, xml);
}
};
@@ -3906,4 +3908,324 @@
}
});
Sao.View.Graph = Sao.class_(Sao.View, {
init: function(screen, xml) {
Sao.View.Graph._super.init.call(this, screen, xml);
this.view_type = 'graph';
this.el = jQuery('<div/>', {
'class': 'graph'
});
this.widgets = {};
this.widget = this.parse(xml.children()[0]);
this.widgets.root = this.widget;
this.el.append(this.widget.el);
},
parse: function(node) {
var field, xfield = null, yfields = [], yattrs;
var child, node_child;
var i, len, j, c_len;
var get_attributes = function(node) {
var attributes = {}, attribute;
for (var i = 0, len = node.attributes.length; i < len; i++) {
attribute = node.attributes[i];
attributes[attribute.name] = attribute.value;
}
return attributes;
};
for (i=0, len=node.children.length; i < len; i++) {
child = node.children[i];
switch (child.tagName) {
case 'x':
for (j=0, c_len=child.children.length; j < c_len; j++) {
xfield = get_attributes(child.children[j]);
field = this.screen.model.fields[xfield.name];
if (!(xfield.string || '')) {
xfield.string = field.description.string;
}
}
break;
case 'y':
for (j=0, c_len=child.children.length; j < c_len; j++) {
yattrs = get_attributes(child.children[j]);
if (!(yattrs.string || '') &&
(yattrs.name != '#')) {
field = this.screen.model.fields[yattrs.name];
yattrs.string = field.description.string;
}
yfields.push(yattrs);
}
break;
}
}
var Widget;
switch (this.attributes.type) {
case 'hbar':
Widget = Sao.View.Graph.HorizontalBar;
break;
case 'line':
Widget = Sao.View.Graph.Line;
break;
case 'pie':
Widget = Sao.View.Graph.Pie;
break;
default:
Widget = Sao.View.Graph.VerticalBar;
}
return new Widget(this, xfield, yfields);
},
display: function() {
return this.widget.display(this.screen.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/>');
this.el.uniqueId();
},
update_data: function(group) {
var data = {};
var record, yfield, key;
var i, len, j, y_len;
this.ids = {};
data.columns = [['labels']];
var key2columns = {};
var fields2load = [this.xfield.name];
for (i = 0, len = this.yfields.length; i < len; i++) {
yfield = this.yfields[i];
data.columns.push([yfield.string]);
key2columns[yfield.key || yfield.name] = i + 1;
fields2load.push(yfield.name);
}
var prms = [];
var set_data = function(index) {
return function () {
record = group[index];
var x = record.field_get(this.xfield.name);
data.columns[0][index + 1] = x;
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 (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)) {
column[index + 1] = 0;
continue;
}
}
if (yfield.name == '#') {
column[index + 1] = 1;
} else {
var value = record.field_get(yfield.name);
if (value && value.isTimeDelta) {
value = value.asSeconds();
}
column[index + 1] = value || 0;
}
}
}.bind(this);
}.bind(this);
var load_field = function(record) {
return function(fname) {
prms.push(record.load(fname));
};
};
var r_prms = [];
for (i = 0, len = group.length; i < len; i++) {
record = group[i];
fields2load.forEach(load_field(group[i]));
for (j = 0, y_len = data.columns.length; j < y_len; j++) {
data.columns[j].push(undefined);
}
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) {
// c3 do not use the moment instance but its date repr when calling
// onclick
var id_x = (key.isDate || key.isDateTime) ? key._d : key;
if (!(id_x in this.ids)) {
this.ids[id_x] = [];
}
this.ids[id_x].push(id);
},
display: function(group) {
var update_prm = this.update_data(group);
update_prm.done(function(data) {
c3.generate(this._c3_config(data));
}.bind(this));
return update_prm;
},
_c3_config: function(data) {
var c3_config = {};
c3_config.bindto = '#' + this.el.attr('id');
c3_config.data = data;
c3_config.data.type = this._chart_type;
c3_config.data.x = 'labels';
c3_config.data.onclick = this.action.bind(this);
var i, len;
var found, labels;
for (i = 0, len = data.columns.length; i < len; i++) {
labels = data.columns[i];
if (labels[0] == 'labels') {
found = true;
break;
}
}
if (found && (labels.length > 1) &&
(labels[1] && (labels[1].isDateTime || labels[1].isDate)))
{
var format_func, date_format, time_format;
date_format = this.view.screen.context.date_format || '%x';
time_format = '%X';
if (labels[1].isDateTime) {
format_func = function(dt) {
return Sao.common.format_datetime(date_format,
time_format, moment(dt));
};
} else {
format_func = function(dt) {
return Sao.common.format_date(date_format, moment(dt));
};
}
c3_config.axis = {
x: {
type: 'timeseries',
tick: {
format: format_func,
}
}
};
}
return c3_config;
},
action: function(data, element) {
var ids = this.ids[this._action_key(data)];
var ctx = jQuery.extend({}, this.view.screen.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) {
return data.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',
_c3_config: function(data) {
var config = Sao.View.Graph.HorizontalBar._super._c3_config
.call(this, data);
config.axis.rotated = true;
}
});
Sao.View.Graph.Line = Sao.class_(Sao.View.Graph.Chart, {
_chart_type: 'line'
});
Sao.View.Graph.Pie = Sao.class_(Sao.View.Graph.Chart, {
_chart_type: 'pie',
_c3_config: function(data) {
var config = Sao.View.Graph.Pie._super._c3_config.call(this, data);
var pie_columns = [];
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, date_format, datetime_format;
if ((labels.length > 0) &&
(labels[0].isDateTime || labels[0].isDate)) {
date_format = this.view.screen.context.date_format || '%x';
datetime_format = date_format + ' %X';
if (labels[1].isDateTime) {
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([label, values[i]]);
}
config.data.columns = pie_columns;
return config;
},
_add_id: function(key, id) {
var id_x = key;
if (key.isDateTime || key.isDate) {
var date_format = this.view.screen.context.date_format || '%x';
var datetime_format = date_format + ' %X';
if (key.isDateTime) {
id_x = Sao.common.format_datetime(datetime_format, key);
} else {
id_x = Sao.common.format_date(date_format, key);
}
}
if (!(id_x in this.ids)) {
this.ids[id_x] = [];
}
this.ids[id_x].push(id);
},
_action_key: function(data) {
return data.id;
}
});
}());