summaryrefslogtreecommitdiff
path: root/addons/web_graph/static/src
diff options
context:
space:
mode:
authorAntony Lesuisse <al@openerp.com>2012-06-08 18:59:32 +0200
committerAntony Lesuisse <al@openerp.com>2012-06-08 18:59:32 +0200
commite9b238bfa747f096fe740ed238f504ff65a37557 (patch)
treebb96931b4ff15c26c239273e92dccc57dcb65326 /addons/web_graph/static/src
parentf4018bb36ba26ac5590a4ef548a015b23b7f9172 (diff)
parent84806f8929b437a8a58d9c0c8d9be1a39d5f3669 (diff)
[MERGE] trunk
bzr revid: al@openerp.com-20120608165932-qp494qy31xa6oi6t
Diffstat (limited to 'addons/web_graph/static/src')
-rw-r--r--addons/web_graph/static/src/css/flotr2.css69
-rw-r--r--addons/web_graph/static/src/css/graph.css44
-rw-r--r--addons/web_graph/static/src/js/graph.js625
-rw-r--r--addons/web_graph/static/src/xml/web_graph.xml58
4 files changed, 411 insertions, 385 deletions
diff --git a/addons/web_graph/static/src/css/flotr2.css b/addons/web_graph/static/src/css/flotr2.css
new file mode 100644
index 00000000000..fae7b033811
--- /dev/null
+++ b/addons/web_graph/static/src/css/flotr2.css
@@ -0,0 +1,69 @@
+/* Flotr Styles */
+
+.flotr-datagrid-container {
+ border: 1px solid #999;
+ border-bottom: none;
+ background: #fff;
+}
+.flotr-datagrid {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+.flotr-datagrid td, .flotr-datagrid th {
+ border: 1px solid #ccc;
+ padding: 1px 3px;
+ min-width: 2em;
+}
+.flotr-datagrid tr:hover, .flotr-datagrid col.hover {
+ background: #f3f3f3;
+}
+.flotr-datagrid tr:hover th, .flotr-datagrid th.hover {
+ background: #999;
+ color: #fff;
+}
+.flotr-datagrid th {
+ text-align: left;
+ background: #e3e3e3;
+ border: 2px outset #fff;
+}
+.flotr-datagrid-toolbar {
+ padding: 1px;
+ border-bottom: 1px solid #ccc;
+ background: #f9f9f9;
+}
+.flotr-datagrid td:hover {
+ background: #ccc;
+}
+.flotr-datagrid .first-row th {
+ text-align: center;
+}
+.flotr-canvas {
+ margin-bottom: -3px;
+ padding-bottom: 1px;
+}
+.flotr-tabs-group {
+ border-top: 1px solid #999;
+}
+.flotr-tab {
+ border: 1px solid #666;
+ border-top: none;
+ margin: 0 3px;
+ padding: 1px 4px;
+ cursor: pointer;
+ -moz-border-radius: 0 0 4px 4px;
+ -webkit-border-bottom-left-radius: 4px;
+ -webkit-border-bottom-right-radius: 4px;
+ border-radius: 0 0 4px 4px;
+ opacity: 0.5;
+ -moz-opacity: 0.5;
+}
+.flotr-tab.selected {
+ background: #ddd;
+ opacity: 1;
+ -moz-opacity: 1;
+}
+.flotr-tab:hover {
+ background: #ccc;
+}
+
+
diff --git a/addons/web_graph/static/src/css/graph.css b/addons/web_graph/static/src/css/graph.css
new file mode 100644
index 00000000000..7e01346cc3a
--- /dev/null
+++ b/addons/web_graph/static/src/css/graph.css
@@ -0,0 +1,44 @@
+.dropdown-menu-icon {
+ z-index: 5000;
+ position: absolute;
+ right: 0px;
+}
+
+.editor-render {
+ position: relative;
+}
+#editor-render-body {
+ position: relative;
+ width: 650px;
+ height: 350px;
+}
+.graph-dropdown {
+ padding: 5px;
+ width: 200px;
+ border: 1px #333 solid;
+ display: none;
+ position: absolute;
+ right: 0px;
+ background: #fafaf5;
+ z-index: 100;
+}
+.open .graph-dropdown {
+ display: block;
+}
+.graph-menu {
+ padding: 3px;
+}
+.graph-menu .active {
+ background: #aaaaff;
+}
+li {
+ padding: 2px;
+ margin-left: 10px;
+}
+a .graph-menu-options {
+ color: white;
+}
+div .graph-menu-options {
+ float: right;
+ top: 0px;
+}
diff --git a/addons/web_graph/static/src/js/graph.js b/addons/web_graph/static/src/js/graph.js
index 7016b9d8d85..52162285f27 100644
--- a/addons/web_graph/static/src/js/graph.js
+++ b/addons/web_graph/static/src/js/graph.js
@@ -3,19 +3,22 @@
*---------------------------------------------------------*/
openerp.web_graph = function (instance) {
-var COLOR_PALETTE = [
- '#cc99ff', '#ccccff', '#48D1CC', '#CFD784', '#8B7B8B', '#75507b',
- '#b0008c', '#ff0000', '#ff8e00', '#9000ff', '#0078ff', '#00ff00',
- '#e6ff00', '#ffff00', '#905000', '#9b0000', '#840067', '#9abe00',
- '#ffc900', '#510090', '#0000c9', '#009b00', '#75507b', '#3465a4',
- '#73d216', '#c17d11', '#edd400', '#fcaf3e', '#ef2929', '#ff00c9',
- '#ad7fa8', '#729fcf', '#8ae234', '#e9b96e', '#fce94f', '#f57900',
- '#cc0000', '#d400a8'];
-
-var QWeb = instance.web.qweb,
- _lt = instance.web._lt;
+
+var _lt = instance.web._lt;
+
+// removed ``undefined`` values
+var filter_values = function (o) {
+ var out = {};
+ for (var k in o) {
+ if (!o.hasOwnProperty(k) || o[k] === undefined) { continue; }
+ out[k] = o[k];
+ }
+ return out;
+};
+
instance.web.views.add('graph', 'instance.web_graph.GraphView');
instance.web_graph.GraphView = instance.web.View.extend({
+ template: "GraphView",
display_name: _lt('Graph'),
view_type: "graph",
@@ -25,412 +28,270 @@ instance.web_graph.GraphView = instance.web.View.extend({
this.dataset = dataset;
this.view_id = view_id;
- this.first_field = null;
- this.abscissa = null;
- this.ordinate = null;
- this.columns = [];
- this.group_field = null;
- this.is_loaded = $.Deferred();
+ this.mode = "bar"; // line, bar, area, pie, radar
+ this.orientation = false; // true: horizontal, false: vertical
+ this.stacked = true;
- this.renderer = null;
+ this.spreadsheet = false; // Display data grid, allows copy to CSV
+ this.forcehtml = false;
+ this.legend = "top"; // top, inside, no
+
+ this.domain = [];
+ this.context = {};
+ this.group_by = [];
+
+ this.graph = null;
},
destroy: function () {
- if (this.renderer) {
- clearTimeout(this.renderer);
+ if (this.graph) {
+ this.graph.destroy();
}
this._super();
},
+
on_loaded: function(fields_view_get) {
+ // TODO: move to load_view and document
var self = this;
- self.fields_view = fields_view_get;
- return this.dataset.call_and_eval('fields_get', [false, {}], null, 1).pipe(function(fields_result) {
- self.fields = fields_result;
- return self.on_loaded_2();
+ this.fields_view = fields_view_get;
+
+ this.mode = this.fields_view.arch.attrs.type || 'bar';
+ this.orientation = this.fields_view.arch.attrs.orientation == 'horizontal';
+
+ var width = this.$element.parent().width();
+ this.$element.css("width", width);
+ this.container = this.$element.find("#editor-render-body").css({
+ width: width,
+ height: Math.min(500, width * 0.8)
+ })[0];
+
+ var graph_render = this.proxy('graph_render');
+ this.$element.on('click', '.oe_graph_options a', function (evt) {
+ var $el = $(evt.target);
+
+ self.graph_render({data: filter_values({
+ mode: $el.data('mode'),
+ legend: $el.data('legend'),
+ orientation: $el.data('orientation'),
+ stacked: $el.data('stacked')
+ })});
});
- },
- /**
- * Returns all object fields involved in the graph view
- */
- list_fields: function () {
- var fs = [this.abscissa];
- fs.push.apply(fs, _(this.columns).pluck('name'));
- if (this.group_field) {
- fs.push(this.group_field);
- }
- return fs;
- },
- on_loaded_2: function() {
- this.chart = this.fields_view.arch.attrs.type || 'pie';
- this.orientation = this.fields_view.arch.attrs.orientation || 'vertical';
-
- _.each(this.fields_view.arch.children, function (field) {
- var attrs = field.attrs;
- if (attrs.group) {
- this.group_field = attrs.name;
- } else if(!this.abscissa) {
- this.first_field = this.abscissa = attrs.name;
- } else {
- this.columns.push({
- name: attrs.name,
- operator: attrs.operator || '+'
- });
+
+ this.$element.find("#graph_show_data").click(function () {
+ self.spreadsheet = ! self.spreadsheet;
+ self.graph_render();
+ });
+ this.$element.find("#graph_switch").click(function () {
+ if (self.mode != 'radar') {
+ self.orientation = ! self.orientation;
}
- }, this);
- this.ordinate = this.columns[0].name;
- this.is_loaded.resolve();
- return $.when();
- },
- schedule_chart: function(results) {
- var self = this;
- this.$element.html(QWeb.render("GraphView", {
- "fields_view": this.fields_view,
- "chart": this.chart,
- 'element_id': this.getParent().element_id
- }));
-
- var fields = _(this.columns).pluck('name').concat([this.abscissa]);
- if (this.group_field) { fields.push(this.group_field); }
- // transform search result into usable records (convert from OpenERP
- // value shapes to usable atomic types
- var records = _(results).map(function (result) {
- var point = {};
- _(result).each(function (value, field) {
- if (!_(fields).contains(field)) { return; }
- if (value === false) { point[field] = false; return; }
- switch (self.fields[field].type) {
- case 'selection':
- point[field] = _(self.fields[field].selection).detect(function (choice) {
- return choice[0] === value;
- })[1];
- break;
- case 'many2one':
- point[field] = value[1];
- break;
- case 'integer': case 'float': case 'char':
- case 'date': case 'datetime':
- point[field] = value;
- break;
- default:
- throw new Error(
- "Unknown field type " + self.fields[field].type
- + "for field " + field + " (" + value + ")");
- }
- });
- return point;
+ self.graph_render();
});
- // aggregate data, because dhtmlx is crap. Aggregate on abscissa field,
- // leave split on group field => max m*n records where m is the # of
- // values for the abscissa and n is the # of values for the group field
- var graph_data = [];
- _(records).each(function (record) {
- var abscissa = record[self.abscissa],
- group = record[self.group_field];
- var r = _(graph_data).detect(function (potential) {
- return potential[self.abscissa] === abscissa
- && (!self.group_field
- || potential[self.group_field] === group);
- });
- var datapoint = r || {};
-
- datapoint[self.abscissa] = abscissa;
- if (self.group_field) { datapoint[self.group_field] = group; }
- _(self.columns).each(function (column) {
- var val = record[column.name],
- aggregate = datapoint[column.name];
- switch(column.operator) {
- case '+':
- datapoint[column.name] = (aggregate || 0) + val;
- return;
- case '*':
- datapoint[column.name] = (aggregate || 1) * val;
- return;
- case 'min':
- datapoint[column.name] = (aggregate || Infinity) > val
- ? val
- : aggregate;
- return;
- case 'max':
- datapoint[column.name] = (aggregate || -Infinity) < val
- ? val
- : aggregate;
- }
- });
- if (!r) { graph_data.push(datapoint); }
- });
- graph_data = _(graph_data).sortBy(function (point) {
- return point[self.abscissa] + '[[--]]' + point[self.group_field];
+ this.$element.find("#graph_download").click(function () {
+ if (self.legend == "top") { self.legend = "inside"; }
+ self.forcehtml = true;
+
+ self.graph_get_data().then(function (result) {
+ self.graph_render_all(result).download.saveImage('png');
+ }).always(function () {
+ self.forcehtml = false;
+ });
});
- if (_.include(['bar','line','area'],this.chart)) {
- return this.schedule_bar_line_area(graph_data);
- } else if (this.chart == "pie") {
- return this.schedule_pie(graph_data);
- }
+ return this._super();
},
- schedule_bar_line_area: function(results) {
- var self = this;
- var group_list,
- view_chart = (self.chart == 'line')?'line':(self.chart == 'area')?'area':'';
- if (!this.group_field || !results.length) {
- if (self.chart == 'bar'){
- view_chart = (this.orientation === 'horizontal') ? 'barH' : 'bar';
- }
- group_list = _(this.columns).map(function (column, index) {
- return {
- group: column.name,
- text: self.fields[column.name].string,
- color: COLOR_PALETTE[index % (COLOR_PALETTE.length)]
- }
- });
- } else {
- // dhtmlx handles clustered bar charts (> 1 column per abscissa
- // value) and stacked bar charts (basically the same but with the
- // columns on top of one another instead of side by side), but it
- // does not handle clustered stacked bar charts
- if (self.chart == 'bar' && (this.columns.length > 1)) {
- this.$element.text(
- 'OpenERP Web does not support combining grouping and '
- + 'multiple columns in graph at this time.');
- throw new Error(
- 'dhtmlx can not handle columns counts of that magnitude');
- }
- // transform series for clustered charts into series for stacked
- // charts
- if (self.chart == 'bar'){
- view_chart = (this.orientation === 'horizontal')
- ? 'stackedBarH' : 'stackedBar';
- }
- group_list = _(results).chain()
- .pluck(this.group_field)
- .uniq()
- .map(function (value, index) {
- var groupval = '';
- if(value) {
- groupval = value.toLowerCase().replace(/[\s\/]+/g,'_');
- }
- return {
- group: _.str.sprintf('%s_%s', self.ordinate, groupval),
- text: value,
- color: COLOR_PALETTE[index % COLOR_PALETTE.length]
- };
- }).value();
-
- results = _(results).chain()
- .groupBy(function (record) { return record[self.abscissa]; })
- .map(function (records) {
- var r = {};
- // second argument is coerced to a str, no good for boolean
- r[self.abscissa] = records[0][self.abscissa];
- _(records).each(function (record) {
- var value = record[self.group_field];
- if(value) {
- value = value.toLowerCase().replace(/[\s\/]+/g,'_');
- }
- var key = _.str.sprintf('%s_%s', self.ordinate, value);
- r[key] = record[self.ordinate];
- });
- return r;
- })
- .value();
+
+ get_format: function (options) {
+ options = options || {};
+ var legend = {
+ show: this.legend != 'no',
+ };
+
+ switch (this.legend) {
+ case 'top':
+ legend.noColumns = 4;
+ legend.container = this.$element.find("div.graph_header_legend")[0];
+ break;
+ case 'inside':
+ legend.position = 'nw';
+ legend.backgroundColor = '#D2E8FF';
+ break;
}
- var abscissa_description = {
- title: "<b>" + this.fields[this.abscissa].string + "</b>",
- template: function (obj) {
- return obj[self.abscissa] || 'Undefined';
+
+ return _.extend({
+ legend: legend,
+ mouse: {
+ track: true,
+ relative: true
+ },
+ spreadsheet : {
+ show: this.spreadsheet,
+ initialTab: "data"
+ },
+ HtmlText : (options.xaxis && options.xaxis.labelsAngle) ? false : !this.forcehtml,
+ }, options);
+ },
+
+ make_graph: function (mode, container, data) {
+ if (mode === 'area') { mode = 'line'; }
+ return Flotr.draw(
+ container, data.data,
+ this.get_format(this['options_' + mode](data)));
+ },
+
+ options_bar: function (data) {
+ var min = _(data.data).chain()
+ .map(function (record) {
+ return _.min(record.data, function (item) {
+ return item[1];
+ })[1];
+ }).min().value();
+ return {
+ bars : {
+ show : true,
+ stacked : this.stacked,
+ horizontal : this.orientation,
+ barWidth : 0.7,
+ lineWidth : 1
+ },
+ grid : {
+ verticalLines : this.orientation,
+ horizontalLines : !this.orientation,
+ outline : "sw",
+ },
+ yaxis : {
+ ticks: this.orientation ? data.ticks : false,
+ min: !this.orientation ? (min < 0 ? min : 0) : null
+ },
+ xaxis : {
+ labelsAngle: 45,
+ ticks: this.orientation ? false : data.ticks,
+ min: this.orientation ? (min < 0 ? min : 0) : null
}
};
- var ordinate_description = {
- lines: true,
- title: "<b>" + this.fields[this.ordinate].string + "</b>"
+ },
+
+ options_pie: function (data) {
+ return {
+ pie : {
+ show: true
+ },
+ grid : {
+ verticalLines : false,
+ horizontalLines : false,
+ outline : "",
+ },
+ xaxis : {showLabels: false},
+ yaxis : {showLabels: false},
};
+ },
- var x_axis, y_axis;
- if (self.chart == 'bar' && self.orientation == 'horizontal') {
- x_axis = ordinate_description;
- y_axis = abscissa_description;
- } else {
- x_axis = abscissa_description;
- y_axis = ordinate_description;
- }
- var renderer = function () {
- if (self.$element.is(':hidden')) {
- self.renderer = setTimeout(renderer, 100);
- return;
- }
- self.renderer = null;
- var charts = new dhtmlXChart({
- view: view_chart,
- container: self.getParent().element_id+"-"+self.chart+"chart",
- value:"#"+group_list[0].group+"#",
- gradient: (self.chart == "bar") ? "3d" : "light",
- alpha: (self.chart == "area") ? 0.6 : 1,
- border: false,
- width: 1024,
- tooltip:{
- template: _.str.sprintf("#%s#, %s=#%s#",
- self.abscissa, group_list[0].text, group_list[0].group)
- },
- radius: 0,
- color: (self.chart != "line") ? group_list[0].color : "",
- item: (self.chart == "line") ? {
- borderColor: group_list[0].color,
- color: "#000000"
- } : "",
- line: (self.chart == "line") ? {
- color: group_list[0].color,
- width: 3
- } : "",
- origin:0,
- xAxis: x_axis,
- yAxis: y_axis,
- padding: {
- left: 75
- },
- legend: {
- values: group_list,
- align:"left",
- valign:"top",
- layout: "x",
- marker: {
- type:"round",
- width:12
- }
- }
- });
- self.$element.find("#"+self.getParent().element_id+"-"+self.chart+"chart").width(
- self.$element.find("#"+self.getParent().element_id+"-"+self.chart+"chart").width()+120);
-
- for (var m = 1; m<group_list.length;m++){
- var column = group_list[m];
- if (column.group === self.group_field) { continue; }
- charts.addSeries({
- value: "#"+column.group+"#",
- tooltip:{
- template: _.str.sprintf("#%s#, %s=#%s#",
- self.abscissa, column.text, column.group)
- },
- color: (self.chart != "line") ? column.color : "",
- item: (self.chart == "line") ? {
- borderColor: column.color,
- color: "#000000"
- } : "",
- line: (self.chart == "line") ? {
- color: column.color,
- width: 3
- } : ""
- });
- }
- charts.parse(results, "json");
- self.$element.find("#"+self.getParent().element_id+"-"+self.chart+"chart").height(
- self.$element.find("#"+self.getParent().element_id+"-"+self.chart+"chart").height()+50);
- charts.attachEvent("onItemClick", function(id) {
- self.open_list_view(charts.get(id));
- });
+ options_radar: function (data) {
+ return {
+ radar : {
+ show : true,
+ stacked : this.stacked
+ },
+ grid : {
+ circular : true,
+ minorHorizontalLines : true
+ },
+ xaxis : {
+ ticks: data.ticks
+ },
};
- if (this.renderer) {
- clearTimeout(this.renderer);
- }
- this.renderer = setTimeout(renderer, 0);
},
- schedule_pie: function(result) {
- var self = this;
- var renderer = function () {
- if (self.$element.is(':hidden')) {
- self.renderer = setTimeout(renderer, 100);
- return;
+
+ options_line: function (data) {
+ return {
+ lines : {
+ show : true,
+ stacked : this.stacked
+ },
+ grid : {
+ verticalLines : this.orientation,
+ horizontalLines : !this.orientation,
+ outline : "sw",
+ },
+ yaxis : {
+ ticks: this.orientation ? data.ticks : false
+ },
+ xaxis : {
+ labelsAngle: 45,
+ ticks: this.orientation ? false : data.ticks
}
- self.renderer = null;
- var chart = new dhtmlXChart({
- view:"pie3D",
- container:self.getParent().element_id+"-piechart",
- value:"#"+self.ordinate+"#",
- pieInnerText:function(obj) {
- var sum = chart.sum("#"+self.ordinate+"#");
- var val = obj[self.ordinate] / sum * 100 ;
- return val.toFixed(1) + "%";
- },
- tooltip:{
- template:"#"+self.abscissa+"#"+"="+"#"+self.ordinate+"#"
- },
- gradient:"3d",
- height: 20,
- radius: 200,
- legend: {
- width: 300,
- align:"left",
- valign:"top",
- layout: "x",
- marker:{
- type:"round",
- width:12
- },
- template:function(obj){
- return obj[self.abscissa] || 'Undefined';
- }
- }
- });
- chart.parse(result,"json");
- chart.attachEvent("onItemClick", function(id) {
- self.open_list_view(chart.get(id));
- });
};
- if (this.renderer) {
- clearTimeout(this.renderer);
- }
- this.renderer = setTimeout(renderer, 0);
},
- open_list_view : function (id){
- var self = this;
- // unconditionally nuke tooltips before switching view
- $(".dhx_tooltip").remove('div');
- id = id[this.abscissa];
- if(this.fields[this.abscissa].type == "selection"){
- id = _.detect(this.fields[this.abscissa].selection,function(select_value){
- return _.include(select_value, id);
- });
+
+ graph_get_data: function () {
+ return this.rpc('/web_graph/graph/data_get', {
+ model: this.dataset.model,
+ domain: this.domain,
+ context: this.context,
+ group_by: this.group_by,
+ view_id: this.view_id,
+ mode: this.mode,
+ orientation: this.orientation,
+ stacked: this.stacked
+ });
+ },
+
+ // Render the graph and update menu styles
+ graph_render: function (options) {
+ options = options || {};
+ _.extend(this, options.data);
+
+ return this.graph_get_data()
+ .then(this.proxy('graph_render_all'));
+ },
+
+ graph_render_all: function (data) {
+ var i;
+ if (this.mode=='area') {
+ for (i=0; i<data.data.length; i++) {
+ data.data[i].lines = {fill: true}
+ }
}
- if (typeof id == 'object'){
- id = id[0];
+ if (this.graph) {
+ this.graph.destroy();
}
- var views;
- if (this.getParent().action) {
- views = this.getParent().action.views;
- if (!_(views).detect(function (view) {
- return view[1] === 'list' })) {
- views = [[false, 'list']].concat(views);
- }
- } else {
- views = _(["list", "form", "graph"]).map(function(mode) {
- return [false, mode];
- });
+ // Render the graph
+ this.$element.find(".graph_header_legend").children().remove();
+ this.graph = this.make_graph(this.mode, this.container, data);
+
+ // Update styles of menus
+
+ this.$element.find("a").removeClass("active");
+
+ var $active = this.$element.find('a[data-mode=' + this.mode + ']');
+ if ($active.length > 1) {
+ $active = $active.filter('[data-stacked=' + this.stacked + ']');
}
- this.do_action({
- res_model : this.dataset.model,
- domain: [[this.abscissa, '=', id], ['id','in',this.dataset.ids]],
- views: views,
- type: "ir.actions.act_window",
- flags: {default_view: 'list'}
- });
+ $active = $active.add(
+ this.$element.find('a:not([data-mode])[data-legend=' + this.legend + ']'));
+
+ $active.addClass('active');
+
+ if (this.spreadsheet) {
+ this.$element.find("#graph_show_data").addClass("active");
+ }
+ return this.graph;
},
+ // render the graph using the domain, context and group_by
+ // calls the 'graph_data_get' python controller to process all data
+ // TODO: check is group_by should better be in the context
do_search: function(domain, context, group_by) {
- var self = this;
- return $.when(this.is_loaded).pipe(function() {
- // TODO: handle non-empty group_by with read_group?
- if (!_(group_by).isEmpty()) {
- self.abscissa = group_by[0];
- } else {
- self.abscissa = self.first_field;
- }
- return self.dataset.read_slice(self.list_fields()).then($.proxy(self, 'schedule_chart'));
- });
+ this.domain = domain;
+ this.context = context;
+ this.group_by = group_by;
+
+ this.graph_render();
},
do_show: function() {
this.do_push_state({});
return this._super();
- }
+ },
});
};
-// vim:et fdc=0 fdl=0:
diff --git a/addons/web_graph/static/src/xml/web_graph.xml b/addons/web_graph/static/src/xml/web_graph.xml
index 90349623946..5cdd0d46ee1 100644
--- a/addons/web_graph/static/src/xml/web_graph.xml
+++ b/addons/web_graph/static/src/xml/web_graph.xml
@@ -1,4 +1,56 @@
<template>
- <div t-name="GraphView" t-att-id="element_id+'-'+chart+'chart'"
- style="height:300px;position:relative;"/>
-</template> \ No newline at end of file
+ <div t-name="GraphView" id="element-chart" class="editor-render" style="position:relative; width: 300px;">
+ <div class="graph_header_legend">
+ </div>
+ <a href="#" class="oe_i dropdown-menu-icon" data-toggle="dropdown" title="Graph Options">B</a>
+ <div class="graph-dropdown">
+ <div class="menu-section">
+ Graph Mode:
+ </div>
+ <ul class="graph-menu oe_graph_options">
+ <li><a href="#" data-mode="pie">Pie</a></li>
+ <li>
+ <a href="#" data-mode="bar" data-legend="top"
+ data-stacked="true">Bars</a>
+ <div class="oe_i graph-menu-options">
+ <a href="#" data-mode="bar" data-legend="top"
+ data-stacked="true" title="Stacked Bars">w</a>
+ <a href="#" data-mode="bar" data-legend="top"
+ data-stacked="false" title="Multiple Bars">O</a>
+ </div>
+ </li>
+ <li><a href="#" data-mode="line">Lines</a></li>
+ <li><a href="#" data-mode="area" data-legend="top"
+ data-stacked="true">Areas</a>
+ <div class="oe_i graph-menu-options">
+ <a href="#" data-mode="area" data-legend="top"
+ data-stacked="true" title="Stacked Areas">w</a>
+ <a href="#" data-mode="area" data-legend="top"
+ data-stacked="false" title="Multiple Areas">O</a>
+ </div>
+ </li>
+ <li><a href="#" data-mode="radar" data-legend="inside"
+ data-orientation="0">Radar</a></li>
+ </ul>
+ <div class="menu-section">
+ Legend:
+ </div>
+ <ul class="graph-menu oe_graph_options">
+ <li><a href="#" data-legend="no">Hide</a></li>
+ <li><a href="#" data-legend="inside">Inside</a></li>
+ <li><a href="#" data-legend="top">Top</a></li>
+ </ul>
+ <div class="menu-section">
+ Actions:
+ </div>
+ <ul class="graph-menu">
+ <li><a href="#" id="graph_switch">Switch Axis</a></li>
+ <li><a href="#" id="graph_show_data">Show Data</a></li>
+ <li><a href="#" id="graph_download">Download as PNG</a></li>
+ </ul>
+ </div>
+ <div id="editor-render-body">
+ </div>
+ </div>
+
+</template>