diff --git a/modoboa/extensions/stats/static/stats/css/stats.css b/modoboa/extensions/stats/static/stats/css/stats.css index 4b242b68b..b0565e636 100644 --- a/modoboa/extensions/stats/static/stats/css/stats.css +++ b/modoboa/extensions/stats/static/stats/css/stats.css @@ -16,6 +16,13 @@ color: #404040; }*/ +#tooltip { + position: absolute; + z-index: 20; + top: 60px; + display: none; +} + .grid .tick { stroke: lightgrey; opacity: 0.7; @@ -24,6 +31,13 @@ stroke-width: 0; } +.line_over { + stroke: gray; + stroke-width: 1.5px; + stroke-dasharray: 5, 5; + display: none; +} + .axis path, .axis line { fill: none; @@ -42,34 +56,23 @@ } .legend rect { - fill: white; + fill: none; stroke: black; - opacity: 0.8; + opacity: 0; } -.d3-tip { - line-height: 1; - font-weight: bold; - padding: 12px; - background: rgba(0, 0, 0, 0.8); +.tip { + font-size: 0.9em; + padding: 5px; + background: rgba(0, 0, 0, 0.6); color: #fff; - border-radius: 2px; + border-radius: 5px; } -.d3-tip:after { - box-sizing: border-box; - display: inline; - font-size: 10px; - width: 100%; - line-height: 1; - color: rgba(0, 0, 0, 0.8); - content: "\25BC"; - position: absolute; - text-align: center; +.tip p { + margin: 0; } -.d3-tip.n:after { - margin: -1px 0 0 0; - top: 100%; - left: 0; +.tracker { + display: none; } \ 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 index 7fb5b8a3e..52b267a6c 100644 --- a/modoboa/extensions/stats/static/stats/js/d3.legend.js +++ b/modoboa/extensions/stats/static/stats/js/d3.legend.js @@ -42,13 +42,20 @@ .call(function(d) { d.exit().remove(); }) .attr("cy", function(d, i) { return i - 0.25 + "em"; }) .attr("cx", 0) - .attr("r", "0.4em") + .attr("r", "4px") + .style("stroke", function(d) { return d.value.color; }) + .style("stroke-width", "1px") + .style("stroke-opacity", 0.8) .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"; + var curve = d3.selectAll("[data-legend='" + d.key + "']"); + var state = (curve.style("display") == "none") + ? "block" : "none"; curve.style("display", state); + d3.select(this).style( + "fill", (state == "none") ? "none" : d.value.color + ); }); // Reposition and resize the box diff --git a/modoboa/extensions/stats/static/stats/js/graphicset.js b/modoboa/extensions/stats/static/stats/js/graphicset.js index 75ad41601..cdfcb8e0a 100644 --- a/modoboa/extensions/stats/static/stats/js/graphicset.js +++ b/modoboa/extensions/stats/static/stats/js/graphicset.js @@ -61,11 +61,13 @@ Graphic.prototype = { 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 bisectDate = d3.bisector(function(d) { return d.x * 1000; }).left; - var brush = d3.svg.brush() .x(x2) .on("brush", brushed); + + var points = []; x.domain(d3.extent(this.data[0].data.map(function(d) { return d.x * 1000; }))); y.domain([ @@ -91,6 +93,9 @@ Graphic.prototype = { var focus = svg.append("g") .attr("class", "focus") + .attr("width", width - 2 * this.options.margin.left) + .attr("height", this.options.height) + .attr("pointer-events", "all") .attr("transform", "translate(" + this.options.margin.left + ",0)");// + this.options.margin.top * 2 + ")"); @@ -111,14 +116,41 @@ Graphic.prototype = { .enter().append("g") .attr("class", "curve"); - curve.append("path") + var line = focus.append("line") + .attr("class", "line_over") + .attr("x1", 0) + .attr("y1", 0) + .attr("x2", 0) + .attr("y2", 300); + + var rect = focus.append("rect") + .attr("fill", "none") + .attr("width", width - 2 * this.options.margin.left) + .attr("height", this.options.height); + + var tooltip = d3.select(".chart") + .append("div") + .attr("id", "tooltip") + .attr("class", "remove") + .attr("class", "tip"); + + var path = curve.append("path") .attr("class", "area") - .attr("d", function(d) { + .attr("d", function(d, i) { + var path = this; + + points[path] = {}; + d.data.forEach(function(d) { + points[path][d.x] = d.y; + }); + focus.append("circle") + .attr("class", "tracker tracker-" + i) + .attr("r", 4); 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-width", "2px") .attr("stroke-opacity", 0.8) .style("fill", function(d) { return d.color; }); @@ -157,35 +189,67 @@ Graphic.prototype = { .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") + var data = this.data; + rect + .on("mouseover", function(d) { + line.style("display", "block"); + tooltip.style("display", "block"); + data.forEach(function(d, cid) { + var display = d3.selectAll("[data-legend='" + d.name + "']") + .style("display"); + if (display == "none") { + return; + } + focus.select(".tracker-" + cid).style("display", "block"); + }); + }) .on("mousemove", function(d) { - var mousex = d3.mouse(this)[0]; + var mousex = d3.mouse(this)[0], + mousey = d3.mouse(this)[1]; var date = x.invert(mousex); + var element = document.getElementById("tooltip"); + var text = ""; - for (var k = 0; k < d.data.length; k++) { - datearray[k] = x.invert(d.data[k].x).value; + line.attr("x1", mousex) + .attr("x2", mousex); + + data.forEach(function(d, cid) { + var display = d3.selectAll("[data-legend='" + d.name + "']") + .style("display"); + if (display == "none") { + return; + } + var i = bisectDate(d.data, date.getTime(), 1), + d0 = d.data[i - 1], + d1 = d.data[i], + point = date.getTime() - (d0.x * 1000) > (d1.x * 1000) - date.getTime() ? d1 : d0; + text += "
" + d.name + ": " + point.y.toFixed(3) + "
"; + + var tracker_x = x(point.x * 1000), + tracker_y = y(point.y); + + focus.selectAll(".tracker-" + cid) + .style("fill", d.color) + .attr("cx", tracker_x) + .attr("cy", tracker_y); + }); + if (mousex + element.clientWidth >= width - 2 * 40) { + tooltip + .style("left", (mousex - (element.clientWidth - 30)) + "px"); + } else { + tooltip.style("left", (mousex + 50) + "px"); } - var mousedate = datearray.indexOf(date.value); - var pro = d.data[mousedate].y; - - tooltip.html( "" + date + "
" + pro + "
" + date + "
" + text); + }) + .on("mouseout", function(d) { + tooltip.style("display", "none"); + line.style("display", "none"); + focus.selectAll(".tracker").style("display", "none"); }); var legend = focus.append("g") .attr("class", "legend") - .attr("transform", "translate(50,30)") + .attr("transform", "translate(50,10)") .style("font-size", "12px") .call(d3.legend); @@ -195,6 +259,7 @@ Graphic.prototype = { .attr("stroke", function(d) { return d.color; }) .attr("stroke-width", "2px") .attr("stroke-opacity", 0.8) + .attr("data-legend", function(d) { return d.name; }) .style("fill", function(d) { return d.color; }); context.append("g") diff --git a/modoboa/extensions/stats/templates/stats/index.html b/modoboa/extensions/stats/templates/stats/index.html index ca047ab47..a8c9a0197 100644 --- a/modoboa/extensions/stats/templates/stats/index.html +++ b/modoboa/extensions/stats/templates/stats/index.html @@ -5,11 +5,10 @@ - - + + - diff --git a/modoboa/static/js/d3.v3.js b/modoboa/static/js/d3.v3.js new file mode 100644 index 000000000..77615c0cf --- /dev/null +++ b/modoboa/static/js/d3.v3.js @@ -0,0 +1,5 @@ +d3=function(){function n(n){return null!=n&&!isNaN(n)}function t(n){return n.length}function e(n){for(var t=1;n*t%1;)t*=10;return t}function r(n,t){try{for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}catch(r){n.prototype=t}}function u(){}function i(){}function o(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function a(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.substring(1);for(var e=0,r=Do.length;r>e;++e){var u=Do[e]+t;if(u in n)return u}}function c(){}function l(){}function s(n){function t(){for(var t,r=e,u=-1,i=r.length;++ue;e++)for(var u,i=n[e],o=0,a=i.length;a>o;o++)(u=i[o])&&t(u,o,e);return n}function T(n){return Lo(n,Io),n}function q(n){var t,e;return function(r,u,i){var o,a=n[i].update,c=a.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(o=a[t])&&++t=o.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,i={},o=[],a=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(mo.map,e,0),0)},i.key=function(n){return o.push(n),i},i.sortKeys=function(n){return a[o.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},mo.set=function(n){var t=new i;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},r(i,{has:function(n){return zo+n in this},add:function(n){return this[zo+n]=!0,n},remove:function(n){return n=zo+n,n in this&&delete this[n]},values:function(){var n=[];return this.forEach(function(t){n.push(t)}),n},forEach:function(n){for(var t in this)t.charCodeAt(0)===Co&&n.call(this,t.substring(1))}}),mo.behavior={},mo.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r=0&&(r=n.substring(e+1),n=n.substring(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},mo.event=null,mo.requote=function(n){return n.replace(jo,"\\$&")};var jo=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,Lo={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},Ho=function(n,t){return t.querySelector(n)},Fo=function(n,t){return t.querySelectorAll(n)},Po=bo[a(bo,"matchesSelector")],Oo=function(n,t){return Po.call(n,t)};"function"==typeof Sizzle&&(Ho=function(n,t){return Sizzle(n,t)[0]||null},Fo=function(n,t){return Sizzle.uniqueSort(Sizzle(n,t))},Oo=Sizzle.matchesSelector),mo.selection=function(){return Uo};var Ro=mo.selection.prototype=[];Ro.select=function(n){var t,e,r,u,i=[];n=d(n);for(var o=-1,a=this.length;++o=0&&(e=n.substring(0,t),n=n.substring(t+1)),Yo.hasOwnProperty(e)?{space:Yo[e],local:n}:n}},Ro.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=mo.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(m(t,n[t]));return this}return this.each(m(n,t))},Ro.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=n.trim().split(/^|\s+/g)).length,u=-1;if(t=e.classList){for(;++u =0?n.substring(0,t):n,r=t>=0?n.substring(t+1):"in";return e=bc.get(e)||xc,r=_c.get(r)||dt,Ar(r(e.apply(null,Array.prototype.slice.call(arguments,1))))},mo.interpolateHcl=Rr,mo.interpolateHsl=Yr,mo.interpolateLab=Ir,mo.interpolateRound=Ur,mo.transform=function(n){var t=xo.createElementNS(mo.ns.prefix.svg,"g");return(mo.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new Zr(e?e.matrix:wc)})(n)},Zr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var wc={a:1,b:0,c:0,d:1,e:0,f:0};mo.interpolateTransform=Br,mo.layout={},mo.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++e