diff --git a/modoboa/extensions/stats/graphics.py b/modoboa/extensions/stats/graphics.py index e77edd6d9..63592bd85 100644 --- a/modoboa/extensions/stats/graphics.py +++ b/modoboa/extensions/stats/graphics.py @@ -104,7 +104,7 @@ class AverageTraffic(Graphic): bounced = Curve("bounced", "yellow", ugettext_lazy("bounced messages")) reject = Curve("reject", "tomato", ugettext_lazy("rejected messages")) - order = ['bounced', 'recv', 'sent', 'reject'] + order = ['reject', 'bounced', 'recv', 'sent'] class AverageTrafficSize(Graphic): diff --git a/modoboa/extensions/stats/static/stats/css/stats.css b/modoboa/extensions/stats/static/stats/css/stats.css index 1ded0c960..4b242b68b 100644 --- a/modoboa/extensions/stats/static/stats/css/stats.css +++ b/modoboa/extensions/stats/static/stats/css/stats.css @@ -2,18 +2,26 @@ width: 100%; }*/ -.graph_container { +.chart { position: relative; - margin-bottom: 10px; - padding-right: 40px; + /*margin-bottom: 10px; + padding-right: 40px;*/ } -.legend { +/*.legend { position: absolute; right: 10px; top: 0px; background-color: transparent; color: #404040; +}*/ + +.grid .tick { + stroke: lightgrey; + opacity: 0.7; +} +.grid path { + stroke-width: 0; } .axis path, @@ -23,9 +31,8 @@ shape-rendering: crispEdges; } -/*.area { - fill-opacity: .85; - clip-path: url(#clip); +.area, .area2 { + fill-opacity: .6; } .brush .extent { @@ -65,4 +72,4 @@ margin: -1px 0 0 0; top: 100%; left: 0; -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/modoboa/extensions/stats/static/stats/js/d3.legend.js b/modoboa/extensions/stats/static/stats/js/d3.legend.js new file mode 100644 index 000000000..7fb5b8a3e --- /dev/null +++ b/modoboa/extensions/stats/static/stats/js/d3.legend.js @@ -0,0 +1,63 @@ +// d3.legend.js +// (C) 2012 ziggy.jonsson.nyc@gmail.com +// MIT licence + +(function() { + d3.legend = function(g, clicked) { + g.each(function() { + var g = d3.select(this), + items = {}, + svg = d3.select(g.property("nearestViewportElement")), + legendPadding = g.attr("data-style-padding") || 5, + lb = g.selectAll(".legend-box").data([true]), + li = g.selectAll(".legend-items").data([true]); + + lb.enter().append("rect").classed("legend-box", true); + li.enter().append("g").classed("legend-items", true); + + svg.selectAll("[data-legend]").each(function() { + var self = d3.select(this); + + items[self.attr("data-legend")] = { + pos: self.attr("data-legend-pos") || this.getBBox().y, + color: self.attr("data-legend-color") != undefined ? self.attr("data-legend-color") : self.style("fill") != 'none' ? self.style("fill") : self.style("stroke") + }; + }); + + items = d3.entries(items).sort(function(a, b) { + return a.value.pos - b.value.pos; + }); + + li.selectAll("text") + .data(items, function(d) { return d.key; }) + .call(function(d) { d.enter().append("text"); }) + .call(function(d) { d.exit().remove(); }) + .attr("y", function(d, i) { return i + "em"; }) + .attr("x", "1em") + .text(function(d) { return d.key; }); + + li.selectAll("circle") + .data(items, function(d) { return d.key; }) + .call(function(d) { d.enter().append("circle"); }) + .call(function(d) { d.exit().remove(); }) + .attr("cy", function(d, i) { return i - 0.25 + "em"; }) + .attr("cx", 0) + .attr("r", "0.4em") + .style("fill", function(d) { return d.value.color; }) + .on("click", function(d) { + var curve = d3.select("[data-legend='" + d.key + "']"); + var state = (curve.style("display") == "none") ? "block" : "none"; + + curve.style("display", state); + }); + + // Reposition and resize the box + var lbbox = li[0][0].getBBox(); + lb.attr("x", (lbbox.x - legendPadding)) + .attr("y", (lbbox.y - legendPadding)) + .attr("height", (lbbox.height + 2 * legendPadding)) + .attr("width", (lbbox.width + 2 * legendPadding)); + }); + return g; + }; +})(); diff --git a/modoboa/extensions/stats/static/stats/js/graphicset.js b/modoboa/extensions/stats/static/stats/js/graphicset.js new file mode 100644 index 000000000..75ad41601 --- /dev/null +++ b/modoboa/extensions/stats/static/stats/js/graphicset.js @@ -0,0 +1,235 @@ +/* + * + */ +var Graphic = function(selector, options) { + this.initialize(selector, options); +}; + +Graphic.prototype = { + constructor: Graphic, + + defaults: { + height: 300, + preview_height: 80, + brush_height: 7, + margin: { + left: 40, + top: 10, + bottom: 20 + } + }, + + initialize: function(selector, data, options) { + this.options = $.extend({}, this.defaults, options); + this.selector = selector; + this.data = data; + $(window).resize($.proxy(this.resize, this)); + this.render(); + }, + + resize: function() { + d3.selectAll("svg").remove(); + this.render(); + }, + + height: function() { + return this.options.height + 2 * this.options.margin.top + + 2 * this.options.margin.bottom + this.options.preview_height + + this.options.brush_height; + }, + + render: function() { + var $element = $(this.selector); + var width = $element.width(); + var container = d3.select(this.selector) + .append("div") + .attr("class", "chart"); + var svg = container + .append("svg") + .attr("width", width) + .attr("height", this.height()); + + svg.append("defs").append("clipPath") + .attr("id", "clip") + .append("rect") + .attr("width", width - 2 * this.options.margin.left) + .attr("height", this.options.height); + + var x = d3.time.scale().range([0, width - 2 * this.options.margin.left]), + x2 = d3.time.scale().range([0, width - 2 * this.options.margin.left]); + var xAxis = d3.svg.axis().scale(x).orient("bottom"), + xAxis2 = d3.svg.axis().scale(x2).orient("bottom"); + var y = d3.scale.linear().range([this.options.height, 0]), + y2 = d3.scale.linear().range([this.options.preview_height, 0]); + + + var brush = d3.svg.brush() + .x(x2) + .on("brush", brushed); + + x.domain(d3.extent(this.data[0].data.map(function(d) { return d.x * 1000; }))); + y.domain([ + d3.min(this.data, function(d) { return d3.min(d.data, function(v) { return v.y; }); }), + d3.max(this.data, function(d) { return d3.max(d.data, function(v) { return v.y; }); }) + ]); + x2.domain(x.domain()); + y2.domain(y.domain()); + + var yAxis = d3.svg.axis().scale(y).orient("left"); + + var area = d3.svg.area() + .interpolate("cardinal") + .x(function(d) { return x(d.x * 1000); }) + .y0(this.options.height) + .y1(function(d) { return y(d.y); }); + + var area2 = d3.svg.area() + .interpolate("cardinal") + .x(function(d) { return x2(d.x * 1000); }) + .y0(this.options.preview_height) + .y1(function(d) { return y2(d.y); }); + + var focus = svg.append("g") + .attr("class", "focus") + .attr("transform", + "translate(" + this.options.margin.left + ",0)");// + this.options.margin.top * 2 + ")"); + + var context = svg.append("g") + .attr("class", "context") + .attr("transform", "translate(" + this.options.margin.left + "," + (this.options.height + this.options.margin.top + this.options.margin.bottom + this.options.brush_height) + ")"); + + var curve2 = context.selectAll(".curve") + .data(this.data) + .enter().append("g") + .attr("class", "curve"); + + var curvegroup = focus.append("g") + .attr('clip-path', 'url(#clip)'); + + var curve = curvegroup.selectAll(".curve") + .data(this.data) + .enter().append("g") + .attr("class", "curve"); + + curve.append("path") + .attr("class", "area") + .attr("d", function(d) { + return area(d.data); + }) + .attr("data-legend", function(d) { return d.name; }) + .attr("stroke", function(d) { return d.color; }) + .attr("stroke-width", "3px") + .attr("stroke-opacity", 0.8) + .style("fill", function(d) { return d.color; }); + + function make_x_axis() { + return d3.svg.axis() + .scale(x) + .orient("bottom") + .ticks(5) + .tickSize(-this.options.height, 0, 0) + .tickFormat(""); + } + + function make_y_axis() { + return d3.svg.axis() + .scale(y) + .orient("left") + .ticks(5) + .tickSize(-(width - 2 * this.options.margin.left), 0, 0) + .tickFormat(""); + } + + focus.append("g") + .attr("class", "grid") + .attr("transform", "translate(0," + (this.options.height + this.options.margin.top) + ")") + .call(make_x_axis.apply(this)); + + focus.append("g") + .attr("class", "grid") + .call(make_y_axis.apply(this)); + + focus.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + this.options.height + ")") + .call(xAxis); + focus.append("g") + .attr("class", "y axis") + .call(yAxis); + + var tooltip = d3.select(".chart") + .append("div") + .attr("class", "remove") + .style("position", "absolute") + .style("z-index", "20") + .style("display", "none") + .style("top", "0px") + .style("right", "10px"); + + var datearray = []; + + focus.selectAll(".area") + .on("mousemove", function(d) { + var mousex = d3.mouse(this)[0]; + var date = x.invert(mousex); + + for (var k = 0; k < d.data.length; k++) { + datearray[k] = x.invert(d.data[k].x).value; + } + var mousedate = datearray.indexOf(date.value); + var pro = d.data[mousedate].y; + + tooltip.html( "
" + date + "
" + pro + "