diff --git a/web_graph_export_png/README.rst b/web_graph_export_png/README.rst new file mode 100644 index 00000000..bbf62667 --- /dev/null +++ b/web_graph_export_png/README.rst @@ -0,0 +1,45 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +=================== +Graph Export To PNG +=================== + +This module extends the functionality of web_graph module allow export to png + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed `feedback +`_. + +Credits +======= + +Contributors +------------ + +* Veronika Kotovich +* Florent THOMAS (financial support) + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/web_graph_export_png/__init__.py b/web_graph_export_png/__init__.py new file mode 100644 index 00000000..e095c40c --- /dev/null +++ b/web_graph_export_png/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# © 2016 IT-Projects +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). \ No newline at end of file diff --git a/web_graph_export_png/__openerp__.py b/web_graph_export_png/__openerp__.py new file mode 100644 index 00000000..b36be41c --- /dev/null +++ b/web_graph_export_png/__openerp__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# © 2016 IT-Projects +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + 'name': 'Graph Export to PNG', + 'version': '8.0.1.0.0', + 'category': 'Web', + 'summary': 'Add graph export to png option.', + 'author': "IT-Projects LLC,Odoo Community Association (OCA)", + 'license': 'AGPL-3', + 'website': 'https://twitter.com/vkotovi4', + 'depends': ['web_graph'], + 'qweb': ['static/src/xml/web_graph_export_png.xml'], + 'data': ['view/web_graph_export_png.xml'], + 'installable': True, + 'auto_install': False, +} \ No newline at end of file diff --git a/web_graph_export_png/static/description/icon.png b/web_graph_export_png/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/web_graph_export_png/static/description/icon.png differ diff --git a/web_graph_export_png/static/src/js/web_graph_export_png.js b/web_graph_export_png/static/src/js/web_graph_export_png.js new file mode 100644 index 00000000..c90354ee --- /dev/null +++ b/web_graph_export_png/static/src/js/web_graph_export_png.js @@ -0,0 +1,42 @@ +openerp.web_graph_export_png = function(instance) { + + var _t = instance.web._t; + + instance.web_graph.Graph.include({ + option_selection: function (event) { + this._super(event); + if (event.currentTarget.getAttribute('data-choice') == 'export_png') { + var svg = document.querySelector("svg"); + + if (typeof window.XMLSerializer != "undefined") { + var svgData = (new XMLSerializer()).serializeToString(svg); + } else if (typeof svg.xml != "undefined") { + var svgData = svg.xml; + } + + var canvas = document.createElement("canvas"); + var svgSize = svg.getBoundingClientRect(); + canvas.width = svgSize.width; + canvas.height = svgSize.height; + var ctx = canvas.getContext("2d"); + //set background color + ctx.fillStyle = 'white'; + //draw background / rect on entire canvas + ctx.fillRect(0,0,canvas.width,canvas.height); + + var img = document.createElement("img"); + img.setAttribute("src", "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svgData))) ); + img.onload = function() { + ctx.drawImage(img, 0, 0); + var imgsrc = canvas.toDataURL("image/png", 1.0); + var a = document.createElement("a"); + a.download = "graph"+".png"; + a.href = imgsrc; + document.body.appendChild(a); + a.click(); + a.remove(); + }; + } + }, + }); +} diff --git a/web_graph_export_png/static/src/xml/web_graph_export_png.xml b/web_graph_export_png/static/src/xml/web_graph_export_png.xml new file mode 100644 index 00000000..85b999c8 --- /dev/null +++ b/web_graph_export_png/static/src/xml/web_graph_export_png.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/web_graph_export_png/view/web_graph_export_png.xml b/web_graph_export_png/view/web_graph_export_png.xml new file mode 100644 index 00000000..a01af259 --- /dev/null +++ b/web_graph_export_png/view/web_graph_export_png.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/web_graph_radar/README.rst b/web_graph_radar/README.rst new file mode 100644 index 00000000..80454f89 --- /dev/null +++ b/web_graph_radar/README.rst @@ -0,0 +1,45 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +================= +Graph Radar Chart +================= + +This module extends the functionality of web_graph module to support radar mode + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed `feedback +`_. + +Credits +======= + +Contributors +------------ + +* Veronika Kotovich +* Florent THOMAS (financial support) + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/web_graph_radar/__init__.py b/web_graph_radar/__init__.py new file mode 100644 index 00000000..e095c40c --- /dev/null +++ b/web_graph_radar/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# © 2016 IT-Projects +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). \ No newline at end of file diff --git a/web_graph_radar/__openerp__.py b/web_graph_radar/__openerp__.py new file mode 100644 index 00000000..f1c07967 --- /dev/null +++ b/web_graph_radar/__openerp__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# © 2016 IT-Projects +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + 'name': 'Radar Chart', + 'version': '8.0.1.0.0', + 'category': 'Web', + 'summary': 'Add graph radar view.', + 'author': "IT-Projects LLC,Odoo Community Association (OCA)", + 'license': 'AGPL-3', + 'website': 'https://twitter.com/vkotovi4', + 'depends': ['web_graph'], + 'qweb': ['static/src/xml/web_graph_radar.xml'], + 'data': ['view/web_graph_radar.xml'], + 'installable': True, + 'auto_install': False, +} \ No newline at end of file diff --git a/web_graph_radar/static/description/icon.png b/web_graph_radar/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/web_graph_radar/static/description/icon.png differ diff --git a/web_graph_radar/static/lib/nvd3-radar.js b/web_graph_radar/static/lib/nvd3-radar.js new file mode 100644 index 00000000..4c2c5a53 --- /dev/null +++ b/web_graph_radar/static/lib/nvd3-radar.js @@ -0,0 +1,885 @@ +nv.models.radar = function() { + + //============================================================ + // Public Variables with Default Settings + //------------------------------------------------------------ + + var margin = {top: 0, right: 0, bottom: 0, left: 0} + , width = 500 + , height = 500 + , color = nv.utils.defaultColor() // a function that returns a color + , getValue = function(d) { return d.value } // accessor to get the x value from a data point + , size = 5 + , scales = d3.scale.linear() + , radius + , max = 5 + , startAngle = 0 + , cursor = 0 + , clipEdge = false + ; + + var line = d3.svg.line() + .x(function(d) { return d.x}) + .y(function(d) { return d.y}); + + var scatter = nv.models.scatter() + .size(16) // default size + .sizeDomain([16,256]) + ; + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + + var availableWidth = width - margin.left - margin.right, + availableHeight = height - margin.top - margin.bottom, + container = d3.select(this) + ; + + // max = max || d3.max(data, getValue) > 0 ? d3.max(data, getValue) : 1 + + scales.domain([0, max]).range([0,radius]); + + var current = 0; + if (cursor < 0) { + current = Math.abs(cursor); + } + else if (cursor > 0) { + current = size - cursor; + } + + + //------------------------------------------------------------ + // Setup Scales + //compute proportions + var maxValue = 0; + for(var i=0; i maxValue) { + maxValue = serie[j].value; + } + } + } + var factor = maxValue ? (radius-40)/maxValue/max/2 : 0; + data = data.map(function(serie, i) { + serie.values = serie.values.map(function(value, j) { + value.x = calculateX(value.value*factor, j, size); + value.y = calculateY(value.value*factor, j, size); + value.serie = i; + value.focus = (current == j) ? true : false; + return value; + }); + return serie; + }); + + //------------------------------------------------------------ + + + + //------------------------------------------------------------ + // Setup containers and skeleton of chart + + var wrap = container.selectAll('g.nv-wrap.nv-radar').data([data]); + var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-radar'); + var defsEnter = wrapEnter.append('defs'); + var gEnter = wrapEnter.append('g'); + var g = wrap.select('g') + + gEnter.append('g').attr('class', 'nv-groups'); + gEnter.append('g').attr('class', 'nv-scatterWrap'); + + + // wrap.attr('transform', 'translate(' + radius + ',' + radius + ')'); + + //------------------------------------------------------------ + + // Points + scatter + .xScale(scales) + .yScale(scales) + .zScale(scales) + .color(color) + .useVoronoi(false) + .width(availableWidth) + .height(availableHeight); + + var scatterWrap = wrap.select('.nv-scatterWrap'); + //.datum(data); // Data automatically trickles down from the wrap + + d3.transition(scatterWrap).call(scatter); + + defsEnter.append('clipPath') + .attr('id', 'nv-edge-clip-' + scatter.id()) + .append('rect'); + + wrap.select('#nv-edge-clip-' + scatter.id() + ' rect') + .attr('width', availableWidth) + .attr('height', availableHeight); + + g.attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + scatter.id() + ')' : ''); + scatterWrap + .attr('clip-path', clipEdge ? 'url(#nv-edge-clip-' + scatter.id() + ')' : ''); + + + // Series + var groups = wrap.select('.nv-groups').selectAll('.nv-group').data(function(d) { return d }, function(d) { return d.key }); + groups.enter().append('g') + .style('stroke-opacity', 1e-6) + .style('fill-opacity', 1e-6); + d3.transition(groups.exit()) + .style('stroke-opacity', 1e-6) + .style('fill-opacity', 1e-6) + .remove(); + groups + .attr('class', function(d,i) { return 'nv-group nv-series-' + i }) + .style('fill', function(d,i){ return color(d,i); }) + .style('stroke', function(d,i){ return color(d,i); }); + d3.transition(groups) + .style('stroke-opacity', 1) + .style('fill-opacity', .5); + + var lineRadar = groups.selectAll('path.nv-line').data(function(d) { return [d.values] }); + + lineRadar.enter().append('path') + .attr('class', 'nv-line') + .attr('d', line ); + + + d3.transition(lineRadar.exit()) + .attr('d', line) + .remove(); + + lineRadar + .style('fill', function(d){ return color(d,d[0].serie); }) + .style('stroke', function(d,i,j){ return color(d,d[0].serie); }) + + d3.transition(lineRadar) + .attr('d', line ); + + + + }); + + return chart; + } + + // compute an angle + function angle(i, length) { + return i * (2 * Math.PI / length ) + ((2 * Math.PI) * startAngle / 360) + (cursor*2*Math.PI)/length; + } + + // x-caclulator + // d is the datapoint, i is the index, length is the length of the data + function calculateX(d, i, length) { + var l = scales(d); + return Math.sin(angle(i, length)) * l; + } + + // y-calculator + function calculateY(d, i, length) { + var l = scales(d); + return Math.cos(angle(i, length)) * l; + } + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + chart.dispatch = scatter.dispatch; + chart.scatter = scatter; + + chart.margin = function(_) { + if (!arguments.length) return margin; + margin.top = typeof _.top != 'undefined' ? _.top : margin.top; + margin.right = typeof _.right != 'undefined' ? _.right : margin.right; + margin.bottom = typeof _.bottom != 'undefined' ? _.bottom : margin.bottom; + margin.left = typeof _.left != 'undefined' ? _.left : margin.left; + return chart; + }; + + chart.width = function(_) { + if (!arguments.length) return width; + width = _; + return chart; + }; + + chart.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.size = function(_) { + if (!arguments.length) return size; + size = _; + return chart; + }; + + chart.scales = function(_) { + if (!arguments.length) return scales; + scales = _; + return chart; + }; + + chart.max = function(_) { + if (!arguments.length) return max; + max = _; + return chart; + }; + + chart.radius = function(_) { + if (!arguments.length) return radius; + radius = _; + return chart; + }; + + chart.color = function(_) { + if (!arguments.length) return color; + color = nv.utils.getColor(_); + return chart; + }; + + chart.startAngle = function(_) { + if (!arguments.length) return startAngle; + startAngle = _; + return chart; + }; + + chart.cursor = function(_) { + if (!arguments.length) return cursor; + cursor = _; + return chart; + }; + + //============================================================ + + + return chart; +} + +nv.models.radarChart = function() { + + //============================================================ + // Public Variables with Default Settings + //------------------------------------------------------------ + + var radars = nv.models.radar() + , legend = nv.models.legend(); + + var margin = {top: 0, right: 0, bottom: 0, left: 0} + , color = nv.utils.defaultColor() + , width = null + , height = null + , showLegend = true + , legs = [] + , ticks = 10 //Temp to test radar size issue + , scales = d3.scale.linear() + , edit = false + , radius + , startAngle = 180 + , cursor = 0 + , tooltips = true + , transitionDuration = 250 + , tooltip = function(key, leg, value, e, graph) { + return '

' + key + '

' + + '

' + leg + ': ' + value + '

' + } + , dispatch = d3.dispatch('tooltipShow', 'tooltipHide','prevClick','stateChange') + ; + + var line = d3.svg.line() + .x(function(d) { return d.x}) + .y(function(d) { return d.y}); + + //============================================================ + + + //============================================================ + // Private Variables + //------------------------------------------------------------ + + var showTooltip = function(e, offsetElement) { + + // New addition to calculate position if SVG is scaled with viewBox, may move TODO: consider implementing everywhere else + if (offsetElement) { + var svg = d3.select(offsetElement).select('svg'); + var viewBox = svg.attr('viewBox'); + if (viewBox) { + viewBox = viewBox.split(' '); + var ratio = parseInt(svg.style('width')) / viewBox[2]; + e.pos[0] = e.pos[0] * ratio; + e.pos[1] = e.pos[1] * ratio; + } + } + + var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), + top = e.pos[1] + ( offsetElement.offsetTop || 0), + val = e.series.values[e.pointIndex].value, + leg = legs[e.pointIndex].label, + content = tooltip(e.series.key, leg, val, e, chart); + nv.tooltip.show([left, top], content, null, null, offsetElement); + }; + + //============================================================ + + + function chart(selection) { + selection.each(function(data) { + legs=data[0].values;//TODO: Think in a better way to put only the legs of the radar + var container = d3.select(this), + that = this, + size = legs.length, + availableWidth = (width || parseInt(container.style('width')) || 500) - margin.left - margin.right, + availableHeight = (height || parseInt(container.style('height')) || 500) - margin.top - margin.bottom; + + chart.update = function() { container.transition().duration(transitionDuration).call(chart) }; + chart.container = this; + + var current = 0; + if (cursor < 0) { + current = Math.abs(cursor); + } + else if (cursor > 0) { + current = legs.length - cursor; + } + + //------------------------------------------------------------ + // Setup Scales + + // scales = radars.scales(); + radius = (availableWidth-300 >= availableHeight) ? (availableHeight)/2 : (availableWidth-300)/2; + scales.domain([0, ticks]).range([0,radius]); + + //------------------------------------------------------------ + + //------------------------------------------------------------ + // Setup containers and skeleton of chart + + var wrap = container.selectAll('g.nv-wrap.nv-radarChart').data([data]); + var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-radarChart'); + var gEnter = wrapEnter.append('g'); + var g = wrap.select('g'); + + gEnter.append('g').attr('class', 'nv-controlWrap'); + gEnter.append('g').attr('class', 'nv-gridWrap'); + gEnter.append('g').attr('class', 'nv-radarsWrap'); + gEnter.append('g').attr('class', 'nv-legendWrap'); + + var gridWrap = wrap.select('g.nv-gridWrap'); + gridWrap.append("g").attr("class", "grid"); + gridWrap.append("g").attr("class", "axes"); + + wrap.attr('transform', 'translate(' + parseFloat(radius + margin.left) + ',' + parseFloat(radius + margin.top) + ')'); + + //------------------------------------------------------------ + + + //------------------------------------------------------------ + // Legend + + if (showLegend) { + legend.width(30); + + g.select('.nv-legendWrap') + .datum(data) + .call(legend); + + /* + if ( margin.top != legend.height()) { + margin.top = legend.height(); + availableHeight = (height || parseInt(container.style('height')) || 400) + - margin.top - margin.bottom; + } + */ + g.select('.nv-legendWrap') + .attr('transform', 'translate(' + (radius + margin.left + margin.right) + ',' + (-radius) +')'); + } + + //------------------------------------------------------------ + + if (edit) { + startAngle = 135 + //Focus + var currentLeg = legs[current]; + var rgbLeg = hexToRgb("#000000"); + var controlWrap = wrap.select('g.nv-controlWrap'); + + wrap.select('g.control').remove(); + var controlEnter = controlWrap.append("g") + .attr("class", "control"); + + var controlLine = controlEnter.append("svg:line") + .attr('class', 'indicator') + .style("stroke", "#000000") + .style("fill", "none") + .style("opacity", 1) + .style("stroke-width", 1.5) + .attr("x1", Math.sin(angle(current, size)) * scales(scales.domain()[1])) + .attr("y1", Math.cos(angle(current, size)) * scales(scales.domain()[1])) + .attr("x2", Math.sin(angle(current, size)) * scales(scales.domain()[1])) + .attr("y2", Math.cos(angle(current, size)) * scales(scales.domain()[1])); + + var controlDescription = controlEnter.append("svg:foreignObject") + .attr('width',200) + .attr('height',0) + .attr("x", Math.sin(angle(current, size)) * scales(scales.domain()[1]) * 2) + .attr("y", Math.cos(angle(current, size)) * scales(scales.domain()[1])); + + controlDescription.append("xhtml:div") + .attr('class', 'radar-description') + .style("background-color", 'rgba('+rgbLeg.r+','+rgbLeg.g+','+rgbLeg.b+',0.1)') + .style('border-bottom', '1px solid '+"#000000") + .style("padding", "10px") + .style("text-align", "justify") + .text( currentLeg.description ); + + + var controlActionContent = controlEnter.append("svg:foreignObject") + .attr('width',200) + .attr('height',50) + .attr("x", Math.sin(angle(current, size)) * scales(scales.domain()[1]) * 2) + .attr("y", Math.cos(angle(current, size)) * scales(scales.domain()[1]) - 25); + + controlActionContent.append("xhtml:button") + .attr('type','button') + .attr('class','radar-prev btn btn-mini icon-arrow-left') + .text('prev'); + + + var controlSelect = controlActionContent.append("xhtml:select") + .attr('class','radar-select-note'); + + controlSelect.append('xhtml:option') + .attr('value',0) + // .attr('selected', function(d,i){ return (d[0].values[current].value == 0) ? true : false;}) + .text('Note') + controlSelect.append('xhtml:option') + .attr('value',1) + // .attr('selected', function(d,i){ return (d[0].values[current].value == 1) ? true : false;}) + .text('Nul') + controlSelect.append('xhtml:option') + .attr('value',2) + //.attr('selected', function(d,i){ return (d[0].values[current].value == 2) ? true : false;}) + .text('Mauvais') + controlSelect.append('xhtml:option') + .attr('value',3) + // .attr('selected', function(d,i){ return (d[0].values[current].value == 3) ? true : false;}) + .text('Nul') + controlSelect.append('xhtml:option') + .attr('value',4) + // .attr('selected', function(d,i){ return (d[0].values[current].value == 4) ? true : false;}) + .text('Bien') + controlSelect.append('xhtml:option') + .attr('value',5) + // .attr('selected', function(d,i){ return (d[0].values[current].value == 4) ? true : false;}) + .text('Très bien') + + controlActionContent.append("xhtml:button") + .attr('type','button') + .attr('class','radar-next btn btn-mini icon-arrow-right') + .text('next'); + + + var checkOption = function (d) { + if(d[0].values[current].value == this.value){ + return d3.select(this).attr("selected", "selected"); + } + }; + + controlSelect.selectAll("option").each(checkOption); + + // Animation + controlLine.transition().duration(500) + .attr("x1", Math.sin(angle(current, size)) * scales(scales.domain()[1])) + .attr("y1", Math.cos(angle(current, size)) * scales(scales.domain()[1])) + .attr("x2", Math.sin(angle(current, size)) * scales(scales.domain()[1]) * 2 + 200) + .attr("y2", Math.cos(angle(current, size)) * scales(scales.domain()[1])) + .each('end', function(d){ controlDescription.transition().duration(300).attr('height','100%') }); + + // Controls + controlWrap.select('.radar-prev') + .on('click', function(d) { + chart.prev(); + selection.transition().call(chart); + }); + controlWrap.select('.radar-next') + .on('click', function(d) { + chart.next(); + selection.transition().call(chart); + }); + + controlWrap.select('.radar-select-note') + .on('change', function(d) { + d[0].values[current].value = this.value; + chart.next(); + selection.transition().call(chart); + }); + + //change + } else { + cursor = 0; + startAngle = 180; + wrap.select('g.control').remove(); + } + + //------------------------------------------------------------ + // Main Chart Component(s) + + radars + .width(availableWidth) + .height(availableHeight) + .size(legs.length) + .max(ticks) + .startAngle(startAngle) + .cursor(cursor) + // .scales(scales) + .radius(radius) + .color(data.map(function(d,i) { + return d.color || color(d, i); + }).filter(function(d,i) { return !data[i].disabled })) + ; + + + var radarWrap = g.select('.nv-radarsWrap') + .datum(data.filter(function(d) { return !d.disabled })); + + d3.transition(radarWrap).call(radars); + + //------------------------------------------------------------ + + //------------------------------------------------------------ + // Setup Axes + + // the grid data, number of ticks + var gridData = buildAxisGrid(size, ticks); + + // Grid + var grid = wrap.select('.grid').selectAll('.gridlevel').data(gridData); + grid.exit().remove(); + + grid.enter().append("path") + .attr("class", "gridlevel") + .attr("d", line); + + + d3.transition(grid) + .attr('d', line ); + + grid.style("stroke", "#000") + .style("fill", "none") + .style("opacity", 0.3); + + // Axes + var ax = wrap.select("g.axes").selectAll("g.axis").data(legs); + ax.exit().remove(); + + var axEnter = ax.enter().append("g") + .attr("class", "axis"); + + var legText = axEnter.append("svg:text") + .style("text-anchor", function(d, i) { + var x = Math.sin(angle(i, size)) * scales(scales.domain()[1]); + if (Math.abs(x) < 0.1) { + return "middle" + } + if (x > 0) { + return "start" + } + + return "end" + }) + .attr("dy", function(d, i) { + var y = Math.cos(angle(i, size)) * scales(scales.domain()[1]); + + if (Math.abs(y) < 0.1) { + return ".72em" + } + + if (y > 0) { + return "1em" + } + return "-.3em" + }) + .style("fill", function(d){ return d.color; }) + .style("font-size", "9pt") + .style("font-weight",function(d,i){ return (i == current && edit) ? "bold": "normal"; }) + .style("opacity", function(d,i){ return (i == current && edit) ? 1: 0.4; }) + .text(function(d){ return d.label}) + .attr("x", function(d, i) { return Math.sin(angle(i, size)) * scales(scales.domain()[1]);}) + .attr("y", function(d, i) { return Math.cos(angle(i, size)) * scales(scales.domain()[1]);}) + ; + + legText.on('click', function(d,i) { + chart.cursor(legs.length - i); + selection.transition().call(chart); + }); + + d3.transition(ax) + .select("text") + .style("text-anchor", function(d, i) { + var x = Math.sin(angle(i, size)) * scales(scales.domain()[1]); + if (Math.abs(x) < 0.1) { + return "middle" + } + if (x > 0) { + return "start" + } + + return "end" + }) + .attr("dy", function(d, i) { + var y = Math.cos(angle(i, size)) * scales(scales.domain()[1]); + + if (Math.abs(y) < 0.1) { + return ".72em" + } + + if (y > 0) { + return "1em" + } + return "-.3em" + }) + .style("font-weight",function(d,i){ return (i == current && edit) ? "bold": "normal"; }) + .style("opacity", function(d,i){ return (i == current && edit) ? 1: 0.4; }) + .attr("x", function(d, i) { return Math.sin(angle(i, size)) * scales(scales.domain()[1]);}) + .attr("y", function(d, i) { return Math.cos(angle(i, size)) * scales(scales.domain()[1]);}); + + axEnter.append("svg:line") + .style("stroke", function(d){ return d.color; }) + .style("fill", "none") + .style("stroke-width", 2) + .style("opacity", function(d,i){ return (i == current && edit) ? 1: 0.4; }) + .attr("x1", function(d, i) { return Math.sin(angle(i, size)) * scales(scales.domain()[0]);}) + .attr("y1", function(d, i) { return Math.cos(angle(i, size)) * scales(scales.domain()[0]);}) + .attr("x2", function(d, i) { return Math.sin(angle(i, size)) * scales(scales.domain()[1]);}) + .attr("y2", function(d, i) { return Math.cos(angle(i, size)) * scales(scales.domain()[1]);}); + + d3.transition(ax) + .select("line") + .style("opacity", function(d,i){ return (i == current && edit) ? 1: 0.4; }) + .attr("x1", function(d, i) { return Math.sin(angle(i, size)) * scales(scales.domain()[0]);}) + .attr("y1", function(d, i) { return Math.cos(angle(i, size)) * scales(scales.domain()[0]);}) + .attr("x2", function(d, i) { return Math.sin(angle(i, size)) * scales(scales.domain()[1]);}) + .attr("y2", function(d, i) { return Math.cos(angle(i, size)) * scales(scales.domain()[1]);}); + //------------------------------------------------------------ + + //============================================================ + // Event Handling/Dispatching (in chart's scope) + //------------------------------------------------------------ + + radars.dispatch.on('elementClick', function(d,i) { + chart.cursor(legs.length - d.pointIndex); + selection.transition().call(chart); + }); + legend.dispatch.on('stateChange', function(newState) { + state = newState; + dispatch.stateChange(state); + chart.update(); + }); + /*legend.dispatch.on('legendClick', function(d,i) { + if (!d.disabled) return; + data = data.map(function(s) { + s.disabled = true; + return s; + }); + d.disabled = false; + + switch (d.key) { + case 'Grouped': + multibar.stacked(false); + break; + case 'Stacked': + multibar.stacked(true); + break; + } + + state.stacked = multibar.stacked(); + dispatch.stateChange(state); + + chart.update(); + });*/ + + /* legend.dispatch.on('legendClick', function(d,i) { + d.disabled = !d.disabled; + + if (!data.filter(function(d) { return !d.disabled }).length) { + data.map(function(d) { + d.disabled = false; + wrap.selectAll('.nv-series').classed('disabled', false); + + return d; + }); + } + chart.update(); + });*/ + + dispatch.on('tooltipShow', function(e) { + e.pos = [parseFloat(e.pos[0] + availableHeight/2 + margin.left), parseFloat(e.pos[1] + availableHeight/2 + margin.top)]; + if (tooltips) showTooltip(e, that.parentNode); + }); + + //============================================================ + + }); + + return chart; + } + + function hexToRgb(hex,opacity) { + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; + } + + function rgbToHex(r, g, b) { + return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); + } + + // compute an angle + function angle(i, length) { + return i * (2 * Math.PI / length ) + ((2 * Math.PI) * startAngle / 360) + (cursor*2*Math.PI)/length; + } + + // x-caclulator + // d is the datapoint, i is the index, length is the length of the data + function calculateX(d, i, length) { + var l = scales(d); + return Math.sin(angle(i, length)) * l; + } + + // y-calculator + function calculateY(d, i, length) { + var l = scales(d); + return Math.cos(angle(i, length)) * l; + } + + // * build the spider axis * // + // rewrite this to conform to d3 axis style? // + function buildAxisGrid(length, ticks) { + var min = scales.domain()[0]; + var max = scales.domain()[1] > 0 ? scales.domain()[1] : 1; + var increase = max/ticks; + + var gridData = [] + for (var i = 0; i <= ticks; i++ ) { + var val = min + i*increase; + var d = [val]; + var gridPoints = []; + + for (var j = 0; j <= length; j++) { + gridPoints.push({ + x: calculateX(d, j, length), + y: calculateY(d, j, length), + }); + } + + gridData.push(gridPoints) + } + + return gridData; + } + + //============================================================ + // Event Handling/Dispatching (out of chart's scope) + //------------------------------------------------------------ + + radars.dispatch.on('elementMouseover.tooltip', function(e) { + dispatch.tooltipShow(e); + }); + + radars.dispatch.on('elementMouseout.tooltip', function(e) { + dispatch.tooltipHide(e); + }); + + dispatch.on('tooltipHide', function() { + if (tooltips) nv.tooltip.cleanup(); + }); + + //============================================================ + + + //============================================================ + // Expose Public Variables + //------------------------------------------------------------ + + // expose chart's sub-components + chart.dispatch = dispatch; + chart.radars = radars; + + + chart.margin = function(_) { + if (!arguments.length) return margin; + margin.top = typeof _.top != 'undefined' ? _.top : margin.top; + margin.right = typeof _.right != 'undefined' ? _.right : margin.right; + margin.bottom = typeof _.bottom != 'undefined' ? _.bottom : margin.bottom; + margin.left = typeof _.left != 'undefined' ? _.left : margin.left; + return chart; + }; + + chart.width = function(_) { + if (!arguments.length) return width; + width = _; + return chart; + }; + + chart.height = function(_) { + if (!arguments.length) return height; + height = _; + return chart; + }; + + chart.legs = function(_) { + if (!arguments.length) return legs; + legs = _; + return chart; + }; + + chart.showLegend = function(_) { + if (!arguments.length) return showLegend; + showLegend = _; + return chart; + }; + + chart.cursor = function(_) { + if (!arguments.length) return cursor; + cursor = _; + return chart; + }; + + chart.next = function(_) { + cursor = cursor - 1; + if (Math.abs(cursor) > legs.length-1) cursor = 0; + return chart; + }; + + chart.prev = function(_) { + cursor = cursor + 1; + if (cursor > legs.length-1) cursor = 0; + return chart; + }; + + chart.edit = function(_) { + if (!arguments.length) return edit; + edit = _; + return chart; + }; + //============================================================ + + + return chart; +} diff --git a/web_graph_radar/static/src/js/web_graph_radar.js b/web_graph_radar/static/src/js/web_graph_radar.js new file mode 100644 index 00000000..3d461ad0 --- /dev/null +++ b/web_graph_radar/static/src/js/web_graph_radar.js @@ -0,0 +1,82 @@ +openerp.web_graph_radar = function(instance) { + + var _t = instance.web._t; + + instance.web_graph.Graph.include({ + template: 'GraphWidgetRadar', + radar: function() { + var self = this, + dim_x = this.pivot.rows.groupby.length, + dim_y = this.pivot.cols.groupby.length, + data; + + // No groupby + if ((dim_x === 0) && (dim_y === 0)) { + data = [{key: _t('Total'), values:[{ + label: _t('Total'), + value: this.pivot.get_total()[0], + }]}]; + // Only column groupbys + } else if ((dim_x === 0) && (dim_y >= 1)){ + data = _.map(this.pivot.get_cols_with_depth(1), function (header) { + return { + key: header.title, + values: [{label:header.title, value: self.pivot.get_total(header)[0]}] + }; + }); + // Just 1 row groupby + } else if ((dim_x === 1) && (dim_y === 0)) { + data = _.map(self.pivot.measures, function(measure, i) { + var series = _.map(self.pivot.main_row().children, function (pt) { + var value = self.pivot.get_total(pt)[i], + title = (pt.title !== undefined) ? pt.title : _t('Undefined'); + return {label: title, value: value}; + }); + return {key: self.pivot.measures[i].string, values:series}; + }); + // 1 row groupby and some col groupbys + } else if ((dim_x === 1) && (dim_y >= 1)) { + data = _.map(this.pivot.get_cols_with_depth(1), function (colhdr) { + var values = _.map(self.pivot.get_rows_with_depth(1), function (header) { + return { + label: header.title || _t('Undefined'), + value: self.pivot.get_values(header.id, colhdr.id)[0] || 0 + }; + }); + return {key: colhdr.title || _t('Undefined'), values: values}; + }); + // At least two row groupby + } else { + var keys = _.uniq(_.map(this.pivot.get_rows_with_depth(2), function (hdr) { + return hdr.title || _t('Undefined'); + })); + data = _.map(keys, function (key) { + var values = _.map(self.pivot.get_rows_with_depth(1), function (hdr) { + var subhdr = _.find(hdr.children, function (child) { + return ((child.title === key) || ((child.title === undefined) && (key === _t('Undefined')))); + }); + return { + label: hdr.title || _t('Undefined'), + value: (subhdr) ? self.pivot.get_total(subhdr)[0] : 0 + }; + }); + return {key:key, values: values}; + }); + } + nv.addGraph(function () { + var chart = nv.models.radarChart(); + + chart.margin({left:200, top:20, bottom:20}); + + d3.select(self.svg) + .datum(data) + .attr('width', self.width) + .attr('height', self.height) + .call(chart); + + return chart; + }); + + } + }); +} diff --git a/web_graph_radar/static/src/xml/web_graph_radar.xml b/web_graph_radar/static/src/xml/web_graph_radar.xml new file mode 100644 index 00000000..9e5bf4ac --- /dev/null +++ b/web_graph_radar/static/src/xml/web_graph_radar.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/web_graph_radar/view/web_graph_radar.xml b/web_graph_radar/view/web_graph_radar.xml new file mode 100644 index 00000000..50f95d50 --- /dev/null +++ b/web_graph_radar/view/web_graph_radar.xml @@ -0,0 +1,20 @@ + + + + + + + + +