diff options
| author | Antony Lesuisse <al@openerp.com> | 2012-06-08 18:59:32 +0200 |
|---|---|---|
| committer | Antony Lesuisse <al@openerp.com> | 2012-06-08 18:59:32 +0200 |
| commit | e9b238bfa747f096fe740ed238f504ff65a37557 (patch) | |
| tree | bb96931b4ff15c26c239273e92dccc57dcb65326 /addons/web_graph/static/src | |
| parent | f4018bb36ba26ac5590a4ef548a015b23b7f9172 (diff) | |
| parent | 84806f8929b437a8a58d9c0c8d9be1a39d5f3669 (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.css | 69 | ||||
| -rw-r--r-- | addons/web_graph/static/src/css/graph.css | 44 | ||||
| -rw-r--r-- | addons/web_graph/static/src/js/graph.js | 625 | ||||
| -rw-r--r-- | addons/web_graph/static/src/xml/web_graph.xml | 58 |
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> |
