From ca27aca7c9150e9333ac2aea8f81c8001548af54 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 5 Feb 2014 14:10:43 +0100 Subject: [PATCH] Personal replacement. --- modoboa/extensions/stats/graphics.py | 2 +- .../stats/static/stats/css/stats.css | 23 +- .../stats/static/stats/js/d3.legend.js | 63 +++++ .../stats/static/stats/js/graphicset.js | 235 ++++++++++++++++++ .../extensions/stats/static/stats/js/stats.js | 7 +- .../stats/templates/stats/index.html | 17 +- 6 files changed, 327 insertions(+), 20 deletions(-) create mode 100644 modoboa/extensions/stats/static/stats/js/d3.legend.js create mode 100644 modoboa/extensions/stats/static/stats/js/graphicset.js 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 + "

" ) + .style("display", "block"); + }); + + var legend = focus.append("g") + .attr("class", "legend") + .attr("transform", "translate(50,30)") + .style("font-size", "12px") + .call(d3.legend); + + curve2.append("path") + .attr("class", "area2") + .attr("d", function(d) { return area2(d.data); }) + .attr("stroke", function(d) { return d.color; }) + .attr("stroke-width", "2px") + .attr("stroke-opacity", 0.8) + .style("fill", function(d) { return d.color; }); + + context.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + this.options.preview_height + ")") + .call(xAxis2); + + context.append("g") + .attr("class", "x brush") + .call(brush) + .selectAll("rect") + .attr("y", -6) + .attr("height", this.options.preview_height + this.options.brush_height); + + function brushed() { + x.domain(brush.empty() ? x2.domain() : brush.extent()); + focus.selectAll(".area") + .attr("d", function(d) { return area(d.data); }); + focus.select(".x.axis").call(xAxis); + } + } +}; + +/* + * + */ +var GraphicSet = function(selector, data, options) { + this.initialize(selector, data, options); +}; + +GraphicSet.prototype = { + constructor: GraphicSet, + + initialize: function(selector, data, options) { + var graph = new Graphic(selector, data.graphs.averagetraffic, options); + } + +}; diff --git a/modoboa/extensions/stats/static/stats/js/stats.js b/modoboa/extensions/stats/static/stats/js/stats.js index a6d73c711..f85614325 100644 --- a/modoboa/extensions/stats/static/stats/js/stats.js +++ b/modoboa/extensions/stats/static/stats/js/stats.js @@ -80,12 +80,13 @@ Stats.prototype = { }, graphs_cb: function(data) { - //this.graphicset = new GraphicSet("#gset", data); - if (this.graphic === undefined) { + this.graphicset = new GraphicSet("#gset", data); + + /*if (this.graphic === undefined) { this.graphic = new Graphic(data, {}); } else { this.graphic.update(data); - } + }*/ }, search_domain: function(value) { diff --git a/modoboa/extensions/stats/templates/stats/index.html b/modoboa/extensions/stats/templates/stats/index.html index 7fc0ced86..ca047ab47 100644 --- a/modoboa/extensions/stats/templates/stats/index.html +++ b/modoboa/extensions/stats/templates/stats/index.html @@ -1,16 +1,17 @@ {% extends "twocols.html" %}{% load i18n %}{% load url from future %} {% block pagetitle %}{% trans "Graphical statistics" %}{% endblock %} {% block html_head %} - + - - - + + + - + +