New attempt using Rickshaw.

This commit is contained in:
Antoine Nguyen
2014-01-31 14:30:12 +01:00
parent ff80b3740d
commit 102ab17eb5
19 changed files with 388 additions and 15450 deletions
@@ -15,8 +15,8 @@ def menu(target, user):
@events.observe("GetGraphSets")
def get_default_graph_sets():
from modoboa.extensions.stats.graph_templates import MailTraffic
def get_default_graphic_sets():
from modoboa.extensions.stats.graphics import MailTraffic
gset = MailTraffic()
return {gset.html_id: gset}
@@ -1,79 +0,0 @@
# coding: utf-8
from django.utils.translation import ugettext_lazy
class Graph(object):
width = 540
height = 120
cf = 'AVERAGE'
def __init__(self, **ds):
self.vars = {}
for dsname, dsdef in ds.iteritems():
self.vars[dsname] = dsdef
@property
def display_name(self):
return self.__class__.__name__.lower()
class AvgTraffic(Graph):
title = ugettext_lazy('Average normal traffic')
vertlabel = ugettext_lazy('msgs/min')
def __init__(self):
super(AvgTraffic, self).__init__(
sent={'type': 'AREA', 'color': '#00FF00',
"legend": ugettext_lazy("sent messages")},
recv={"type": "AREA", "color": "#0000FF",
"legend": ugettext_lazy("received messages")}
)
class AvgBadTraffic(Graph):
title = ugettext_lazy('Average bad traffic')
vertlabel = ugettext_lazy('msgs/min')
def __init__(self):
super(AvgBadTraffic, self).__init__(
bounced={'type': 'AREA', 'color': '#FFFF00',
"legend": ugettext_lazy("bounced messages")},
reject={"type": "AREA", "color": "#FF0000",
"legend": ugettext_lazy("rejected messages")}
)
class AvgTrafficSize(Graph):
title = ugettext_lazy('Average normal traffic size')
vertlabel = ugettext_lazy('bytes/min')
def __init__(self):
super(AvgTrafficSize, self).__init__(
size_recv={'type': 'AREA', 'color': '#FF9900',
"legend": ugettext_lazy("received size")},
size_sent={"type": "AREA", "color": "#339999",
"legend": ugettext_lazy("sent size")}
)
class GraphSet(object):
title = None
graphs = []
@property
def html_id(self):
return self.__class__.__name__.lower()
def get_graphs(self):
if not hasattr(self, '__graph_instances'):
self.__graph_instances = [gc() for gc in self.graphs]
return self.__graph_instances
def get_graph_names(self):
return [g.display_name for g in self.get_graphs()]
class MailTraffic(GraphSet):
title = ugettext_lazy('Mail traffic')
graphs = [AvgTraffic, AvgBadTraffic, AvgTrafficSize]
-129
View File
@@ -1,129 +0,0 @@
# coding: utf-8
import sys
import os
import time
from lxml import etree
from lxml import objectify
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import parameters
from modoboa.lib.sysutils import exec_cmd
periods = [{"name": "day", "label": ugettext_lazy("Day")},
{"name": "week", "label": ugettext_lazy("Week")},
{"name": "month", "label": ugettext_lazy("Month")},
{"name": "year", "label": ugettext_lazy("Year")}]
def str2Time(y, M, d, h="00", m="00", s="00"):
"""Date conversion
Returns a date and a time in seconds from the epoch.
:return: an integer
"""
try:
if not M.isdigit():
local = time.strptime("%s %s %s %s:%s:%s" % (y, M, d, h, m, s),
"%Y %b %d %H:%M:%S")
else:
local = time.strptime("%s %s %s %s:%s:%s" % (y, M, d, h, m, s),
"%Y %m %d %H:%M:%S")
except ValueError:
print >> sys.stderr, "Error: failed to convert date and time"
return 0
return int(time.mktime(local))
class Grapher(object):
def __init__(self):
self.rrd_rootdir = parameters.get_admin("RRD_ROOTDIR")
self.img_rootdir = parameters.get_admin("IMG_ROOTDIR")
def process(self, target, suffix, start, end, graph_tpl):
import rrdtool
rrdfile = "%s/%s.rrd" % (self.rrd_rootdir, target)
if not os.path.exists(rrdfile):
return
ext = "png"
path = "%s/%s_%s_%s_%s.%s" % (self.img_rootdir, graph_tpl.display_name,
target, graph_tpl.cf, suffix, ext)
start = str(start)
end = str(end)
defs = []
lines = []
for v, d in graph_tpl.vars.iteritems():
defs += [str('DEF:%s=%s:%s:%s' % (v, rrdfile, v, graph_tpl.cf)),
str('CDEF:%spm=%s,60,*' % (v, v)),
str('VDEF:%s_total=%s,TOTAL' % (v, v))]
type = d["type"] if "type" in d else "LINE"
lines += [str("%s:%spm%s:%s:STACK" % (type, v, d["color"],
(d["legend"].encode("utf8")).ljust(20)))]
lines.append('GPRINT:%s_total:%s%%7.0lf%%s\\l' % (v, _("Total").encode('utf-8')))
first_ds = graph_tpl.vars.keys()[0]
defs.append(str('VDEF:%s_first=%s,FIRST' % (first_ds, first_ds)))
defs.append(str('VDEF:%s_last=%s,LAST' % (first_ds, first_ds)))
defs.append('GPRINT:%s_first:%s %%c:strftime' % (first_ds, "From"))
defs.append('GPRINT:%s_last:%s %%c\\c:strftime' % (first_ds, "to"))
defs.append('COMMENT:\\s')
params = defs + lines
rrdtool.graph(
str(path),
"--imgformat", "PNG",
"--width", str(graph_tpl.width),
"--height", str(graph_tpl.height),
"--start", start,
"--end", end,
"--vertical-label", graph_tpl.vertlabel.encode("utf-8"),
"--lower-limit", "0",
"--slope-mode",
"--units-exponent", "0",
"--title", str("%s: %s" % (target, graph_tpl.title.encode("utf-8"))),
*params
)
if not os.path.exists(path):
print "[graph] Impossible to create %s graph" % path
def export(self, target, suffix, start, end, graph_tpl):
rrdfile = "%s/%s.rrd" % (self.rrd_rootdir, target)
if not os.path.exists(rrdfile):
print 'pouet'
return []
start = str(start)
end = str(end)
defs = []
result = []
for v, d in graph_tpl.vars.iteritems():
result += [{'key': d['legend'].encode('utf-8'), 'values': [], 'color': d['color']}]
defs += [str('DEF:%s=%s:%s:%s' % (v, rrdfile, v, graph_tpl.cf)),
str('CDEF:%spm=%s,60,*' % (v, v)),
str('XPORT:%spm:"%s"' % (v, d['legend'].encode('utf-8')))]
cmd = "rrdtool xport --start %s --end %s " % (start, end)
cmd += " ".join(defs)
code, output = exec_cmd(cmd)
tree = etree.fromstring(output)
start = int(tree.xpath('/xport/meta/start')[0].text)
end = int(tree.xpath('/xport/meta/end')[0].text)
step = int(tree.xpath('/xport/meta/step')[0].text)
for pos, r in enumerate(tree.xpath('/xport/data/row')):
ts = int(r.find('t').text) - start
for vindex, value in enumerate(r.findall('v')):
if value.text == 'NaN':
result[vindex]['values'].append({'x': pos, 'y': 0})
else:
result[vindex]['values'].append(
{'x': pos, 'y': float(value.text)}
)
return result
def make_defaults(self, target, graph_tpl):
end = "%d" % int(time.mktime(time.localtime()))
self.process(target, "day", "-1day", end, graph_tpl)
self.process(target, "week", "-1week", end, graph_tpl)
self.process(target, "month", "-1month", end, graph_tpl)
self.process(target, "year", "-1year", end, graph_tpl)
+152
View File
@@ -0,0 +1,152 @@
import os
import inspect
from lxml import etree
from django.utils.translation import ugettext_lazy
from modoboa.lib import parameters
from modoboa.lib.sysutils import exec_cmd
class Curve(object):
"""Graphic curve.
Simple way to represent a graphic curve.
"""
def __init__(self, dsname, color, legend, cfunc="AVERAGE"):
"""Constructor.
"""
self.dsname = dsname
self.color = color
self.legend = legend.encode("utf-8")
self.cfunc = cfunc
def to_rrd_command_args(self, rrdfile):
"""Convert this curve to the approriate RRDtool command.
:param str rrdfile: RRD file name
:return: a list
"""
rrdfile = os.path.join(
parameters.get_admin("RRD_ROOTDIR"), "%s.rrd" % rrdfile
)
return [
'DEF:%s=%s:%s:%s' %
(self.dsname, rrdfile, self.dsname, self.cfunc),
'CDEF:%(ds)spm=%(ds)s,60,*' % {"ds": self.dsname},
'XPORT:%spm:"%s"' % (self.dsname, self.legend)
]
class Graphic(object):
"""Graphic.
"""
def __init__(self):
"""Constructor.
"""
self._curves = []
try:
order = getattr(self, "order")
except AttributeError:
for member in inspect.getmembers(self):
if isinstance(member[1], Curve):
self._curves += [member[1]]
else:
for name in order:
try:
curve = getattr(self, name)
except AttributeError:
continue
if not isinstance(curve, Curve):
continue
self._curves += [curve]
@property
def display_name(self):
return self.__class__.__name__.lower()
def export(self, rrdfile, start, end):
"""
"""
result = []
cmdargs = []
for curve in self._curves:
result += [{
"name": curve.legend, "color": curve.color, "data": []
}]
cmdargs += curve.to_rrd_command_args(rrdfile)
cmd = "rrdtool xport --start %s --end %s " % (str(start), str(end))
cmd += " ".join(cmdargs)
code, output = exec_cmd(cmd)
if code:
return []
tree = etree.fromstring(output)
for row in tree.xpath('/xport/data/row'):
timestamp = int(row.find('t').text)
for vindex, value in enumerate(row.findall('v')):
if value.text == 'NaN':
result[vindex]['data'].append({'x': timestamp, 'y': 0})
else:
result[vindex]['data'].append(
{'x': timestamp, 'y': float(value.text)}
)
return result
class AverageTraffic(Graphic):
"""Average traffic.
"""
title = ugettext_lazy('Average traffic')
vertlabel = ugettext_lazy('msgs/min')
# Curve definitions
sent = Curve("sent", "lawngreen", ugettext_lazy("sent messages"))
recv = Curve("recv", "steelblue", ugettext_lazy("received messages"))
bounced = Curve("bounced", "yellow", ugettext_lazy("bounced messages"))
reject = Curve("reject", "tomato", ugettext_lazy("rejected messages"))
order = ['bounced', 'recv', 'sent', 'reject']
class AverageTrafficSize(Graphic):
"""Average traffic size.
"""
title = ugettext_lazy('Average normal traffic size')
vertlabel = ugettext_lazy('bytes/min')
# Curve definitions
size_recv = Curve("size_recv", "orange", ugettext_lazy("received size"))
size_sent = Curve(
"size_sent", "mediumturquoise", ugettext_lazy("sent size")
)
class GraphicSet(object):
title = None
_graphics = []
def __init__(self):
self.__ginstances = []
@property
def html_id(self):
return self.__class__.__name__.lower()
@property
def graphics(self):
if not self.__ginstances:
self.__ginstances = [graphic() for graphic in self._graphics]
return self.__ginstances
def get_graphic_names(self):
return [graphic.display_name for graphic in self._graphics]
def export(self, rrdfile, start, end):
return dict(
(graph.display_name, graph.export(rrdfile, start, end))
for graph in self.graphics
)
class MailTraffic(GraphicSet):
title = ugettext_lazy('Mail traffic')
_graphics = [AverageTraffic, AverageTrafficSize]
+21
View File
@@ -0,0 +1,21 @@
# coding: utf-8
import sys
import time
def date_to_timestamp(timetuple):
"""Date conversion.
Returns a date and a time in seconds from the epoch.
:param list timetuple: list containing date
:return: an integer
"""
date = " ".join(timetuple)
fmt = "%Y %m %d %H:%M:%S" if timetuple[1].isdigit() else "%Y %b %d %H:%M:%S"
try:
local = time.strptime(date, fmt)
except ValueError:
print >> sys.stderr, "Error: failed to convert date and time"
return 0
return int(time.mktime(local))
@@ -28,8 +28,8 @@ from optparse import make_option
from modoboa.lib import parameters
from modoboa.extensions.admin.models import Domain
from modoboa.extensions.stats import Stats
from modoboa.extensions.stats.grapher import str2Time, Grapher
from modoboa.extensions.stats.graph_templates import MailTraffic
from modoboa.extensions.stats.lib import date_to_timestamp
rrdstep = 60
xpoints = 540
@@ -123,7 +123,7 @@ class LogParser(object):
ye = match.group("year")
except IndexError:
ye = self.year(mo)
self.cur_t = str2Time(ye, mo, da, ho, mi, se)
self.cur_t = date_to_timestamp([ye, mo, da, ho, mi, se])
self.cur_t = self.cur_t - self.cur_t % rrdstep
self._prev_mi = mi
self._prev_ho = ho
@@ -329,15 +329,11 @@ class LogParser(object):
for line in self.f.readlines():
self._parse_line(line)
G = Grapher()
for dom, data in self.data.iteritems():
self._dprint("[rrd] dealing with domain %s" % dom)
for t in sorted(data.keys()):
self.update_rrd(dom, t)
for graph_tpl in MailTraffic().get_graphs():
G.make_defaults(dom, graph_tpl)
class Command(BaseCommand):
help = 'Log file parser'
@@ -1,769 +0,0 @@
/********************
* HTML CSS
*/
.chartWrap {
margin: 0;
padding: 0;
overflow: hidden;
}
/********************
Box shadow and border radius styling
*/
.nvtooltip.with-3d-shadow, .with-3d-shadow .nvtooltip {
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
box-shadow: 0 5px 10px rgba(0,0,0,.2);
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
}
/********************
* TOOLTIP CSS
*/
.nvtooltip {
position: absolute;
background-color: rgba(255,255,255,1.0);
padding: 1px;
border: 1px solid rgba(0,0,0,.2);
z-index: 10000;
font-family: Arial;
font-size: 13px;
text-align: left;
pointer-events: none;
white-space: nowrap;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/*Give tooltips that old fade in transition by
putting a "with-transitions" class on the container div.
*/
.nvtooltip.with-transitions, .with-transitions .nvtooltip {
transition: opacity 250ms linear;
-moz-transition: opacity 250ms linear;
-webkit-transition: opacity 250ms linear;
transition-delay: 250ms;
-moz-transition-delay: 250ms;
-webkit-transition-delay: 250ms;
}
.nvtooltip.x-nvtooltip,
.nvtooltip.y-nvtooltip {
padding: 8px;
}
.nvtooltip h3 {
margin: 0;
padding: 4px 14px;
line-height: 18px;
font-weight: normal;
background-color: rgba(247,247,247,0.75);
text-align: center;
border-bottom: 1px solid #ebebeb;
-webkit-border-radius: 5px 5px 0 0;
-moz-border-radius: 5px 5px 0 0;
border-radius: 5px 5px 0 0;
}
.nvtooltip p {
margin: 0;
padding: 5px 14px;
text-align: center;
}
.nvtooltip span {
display: inline-block;
margin: 2px 0;
}
.nvtooltip table {
margin: 6px;
border-spacing:0;
}
.nvtooltip table td {
padding: 2px 9px 2px 0;
vertical-align: middle;
}
.nvtooltip table td.key {
font-weight:normal;
}
.nvtooltip table td.value {
text-align: right;
font-weight: bold;
}
.nvtooltip table tr.highlight td {
padding: 1px 9px 1px 0;
border-bottom-style: solid;
border-bottom-width: 1px;
border-top-style: solid;
border-top-width: 1px;
}
.nvtooltip table td.legend-color-guide div {
width: 8px;
height: 8px;
vertical-align: middle;
}
.nvtooltip .footer {
padding: 3px;
text-align: center;
}
.nvtooltip-pending-removal {
position: absolute;
pointer-events: none;
}
/********************
* SVG CSS
*/
svg {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Trying to get SVG to act like a greedy block in all browsers */
display: block;
width:100%;
height:100%;
}
svg text {
font: normal 12px Arial;
}
svg .title {
font: bold 14px Arial;
}
.nvd3 .nv-background {
fill: white;
fill-opacity: 0;
/*
pointer-events: none;
*/
}
.nvd3.nv-noData {
font-size: 18px;
font-weight: bold;
}
/**********
* Brush
*/
.nv-brush .extent {
fill-opacity: .125;
shape-rendering: crispEdges;
}
/**********
* Legend
*/
.nvd3 .nv-legend .nv-series {
cursor: pointer;
}
.nvd3 .nv-legend .disabled circle {
fill-opacity: 0;
}
/**********
* Axes
*/
.nvd3 .nv-axis {
pointer-events:none;
}
.nvd3 .nv-axis path {
fill: none;
stroke: #000;
stroke-opacity: .75;
shape-rendering: crispEdges;
}
.nvd3 .nv-axis path.domain {
stroke-opacity: .75;
}
.nvd3 .nv-axis.nv-x path.domain {
stroke-opacity: 0;
}
.nvd3 .nv-axis line {
fill: none;
stroke: #e5e5e5;
shape-rendering: crispEdges;
}
.nvd3 .nv-axis .zero line,
/*this selector may not be necessary*/ .nvd3 .nv-axis line.zero {
stroke-opacity: .75;
}
.nvd3 .nv-axis .nv-axisMaxMin text {
font-weight: bold;
}
.nvd3 .x .nv-axis .nv-axisMaxMin text,
.nvd3 .x2 .nv-axis .nv-axisMaxMin text,
.nvd3 .x3 .nv-axis .nv-axisMaxMin text {
text-anchor: middle
}
/**********
* Brush
*/
.nv-brush .resize path {
fill: #eee;
stroke: #666;
}
/**********
* Bars
*/
.nvd3 .nv-bars .negative rect {
zfill: brown;
}
.nvd3 .nv-bars rect {
zfill: steelblue;
fill-opacity: .75;
transition: fill-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear;
}
.nvd3 .nv-bars rect.hover {
fill-opacity: 1;
}
.nvd3 .nv-bars .hover rect {
fill: lightblue;
}
.nvd3 .nv-bars text {
fill: rgba(0,0,0,0);
}
.nvd3 .nv-bars .hover text {
fill: rgba(0,0,0,1);
}
/**********
* Bars
*/
.nvd3 .nv-multibar .nv-groups rect,
.nvd3 .nv-multibarHorizontal .nv-groups rect,
.nvd3 .nv-discretebar .nv-groups rect {
stroke-opacity: 0;
transition: fill-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear;
}
.nvd3 .nv-multibar .nv-groups rect:hover,
.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,
.nvd3 .nv-discretebar .nv-groups rect:hover {
fill-opacity: 1;
}
.nvd3 .nv-discretebar .nv-groups text,
.nvd3 .nv-multibarHorizontal .nv-groups text {
font-weight: bold;
fill: rgba(0,0,0,1);
stroke: rgba(0,0,0,0);
}
/***********
* Pie Chart
*/
.nvd3.nv-pie path {
stroke-opacity: 0;
transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-pie .nv-slice text {
stroke: #000;
stroke-width: 0;
}
.nvd3.nv-pie path {
stroke: #fff;
stroke-width: 1px;
stroke-opacity: 1;
}
.nvd3.nv-pie .hover path {
fill-opacity: .7;
}
.nvd3.nv-pie .nv-label {
pointer-events: none;
}
.nvd3.nv-pie .nv-label rect {
fill-opacity: 0;
stroke-opacity: 0;
}
/**********
* Lines
*/
.nvd3 .nv-groups path.nv-line {
fill: none;
stroke-width: 1.5px;
/*
stroke-linecap: round;
shape-rendering: geometricPrecision;
transition: stroke-width 250ms linear;
-moz-transition: stroke-width 250ms linear;
-webkit-transition: stroke-width 250ms linear;
transition-delay: 250ms
-moz-transition-delay: 250ms;
-webkit-transition-delay: 250ms;
*/
}
.nvd3 .nv-groups path.nv-line.nv-thin-line {
stroke-width: 1px;
}
.nvd3 .nv-groups path.nv-area {
stroke: none;
/*
stroke-linecap: round;
shape-rendering: geometricPrecision;
stroke-width: 2.5px;
transition: stroke-width 250ms linear;
-moz-transition: stroke-width 250ms linear;
-webkit-transition: stroke-width 250ms linear;
transition-delay: 250ms
-moz-transition-delay: 250ms;
-webkit-transition-delay: 250ms;
*/
}
.nvd3 .nv-line.hover path {
stroke-width: 6px;
}
/*
.nvd3.scatter .groups .point {
fill-opacity: 0.1;
stroke-opacity: 0.1;
}
*/
.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point {
fill-opacity: 0;
stroke-opacity: 0;
}
.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point {
fill-opacity: .5 !important;
stroke-opacity: .5 !important;
}
.with-transitions .nvd3 .nv-groups .nv-point {
transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
-moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-scatter .nv-groups .nv-point.hover,
.nvd3 .nv-groups .nv-point.hover {
stroke-width: 7px;
fill-opacity: .95 !important;
stroke-opacity: .95 !important;
}
.nvd3 .nv-point-paths path {
stroke: #aaa;
stroke-opacity: 0;
fill: #eee;
fill-opacity: 0;
}
.nvd3 .nv-indexLine {
cursor: ew-resize;
}
/**********
* Distribution
*/
.nvd3 .nv-distribution {
pointer-events: none;
}
/**********
* Scatter
*/
/* **Attempting to remove this for useVoronoi(false), need to see if it's required anywhere
.nvd3 .nv-groups .nv-point {
pointer-events: none;
}
*/
.nvd3 .nv-groups .nv-point.hover {
stroke-width: 20px;
stroke-opacity: .5;
}
.nvd3 .nv-scatter .nv-point.hover {
fill-opacity: 1;
}
/*
.nv-group.hover .nv-point {
fill-opacity: 1;
}
*/
/**********
* Stacked Area
*/
.nvd3.nv-stackedarea path.nv-area {
fill-opacity: .7;
/*
stroke-opacity: .65;
fill-opacity: 1;
*/
stroke-opacity: 0;
transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
/*
transition-delay: 500ms;
-moz-transition-delay: 500ms;
-webkit-transition-delay: 500ms;
*/
}
.nvd3.nv-stackedarea path.nv-area.hover {
fill-opacity: .9;
/*
stroke-opacity: .85;
*/
}
/*
.d3stackedarea .groups path {
stroke-opacity: 0;
}
*/
.nvd3.nv-stackedarea .nv-groups .nv-point {
stroke-opacity: 0;
fill-opacity: 0;
}
/*
.nvd3.nv-stackedarea .nv-groups .nv-point.hover {
stroke-width: 20px;
stroke-opacity: .75;
fill-opacity: 1;
}*/
/**********
* Line Plus Bar
*/
.nvd3.nv-linePlusBar .nv-bar rect {
fill-opacity: .75;
}
.nvd3.nv-linePlusBar .nv-bar rect:hover {
fill-opacity: 1;
}
/**********
* Bullet
*/
.nvd3.nv-bullet { font: 10px sans-serif; }
.nvd3.nv-bullet .nv-measure { fill-opacity: .8; }
.nvd3.nv-bullet .nv-measure:hover { fill-opacity: 1; }
.nvd3.nv-bullet .nv-marker { stroke: #000; stroke-width: 2px; }
.nvd3.nv-bullet .nv-markerTriangle { stroke: #000; fill: #fff; stroke-width: 1.5px; }
.nvd3.nv-bullet .nv-tick line { stroke: #666; stroke-width: .5px; }
.nvd3.nv-bullet .nv-range.nv-s0 { fill: #eee; }
.nvd3.nv-bullet .nv-range.nv-s1 { fill: #ddd; }
.nvd3.nv-bullet .nv-range.nv-s2 { fill: #ccc; }
.nvd3.nv-bullet .nv-title { font-size: 14px; font-weight: bold; }
.nvd3.nv-bullet .nv-subtitle { fill: #999; }
.nvd3.nv-bullet .nv-range {
fill: #bababa;
fill-opacity: .4;
}
.nvd3.nv-bullet .nv-range:hover {
fill-opacity: .7;
}
/**********
* Sparkline
*/
.nvd3.nv-sparkline path {
fill: none;
}
.nvd3.nv-sparklineplus g.nv-hoverValue {
pointer-events: none;
}
.nvd3.nv-sparklineplus .nv-hoverValue line {
stroke: #333;
stroke-width: 1.5px;
}
.nvd3.nv-sparklineplus,
.nvd3.nv-sparklineplus g {
pointer-events: all;
}
.nvd3 .nv-hoverArea {
fill-opacity: 0;
stroke-opacity: 0;
}
.nvd3.nv-sparklineplus .nv-xValue,
.nvd3.nv-sparklineplus .nv-yValue {
/*
stroke: #666;
*/
stroke-width: 0;
font-size: .9em;
font-weight: normal;
}
.nvd3.nv-sparklineplus .nv-yValue {
stroke: #f66;
}
.nvd3.nv-sparklineplus .nv-maxValue {
stroke: #2ca02c;
fill: #2ca02c;
}
.nvd3.nv-sparklineplus .nv-minValue {
stroke: #d62728;
fill: #d62728;
}
.nvd3.nv-sparklineplus .nv-currentValue {
/*
stroke: #444;
fill: #000;
*/
font-weight: bold;
font-size: 1.1em;
}
/**********
* historical stock
*/
.nvd3.nv-ohlcBar .nv-ticks .nv-tick {
stroke-width: 2px;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover {
stroke-width: 4px;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive {
stroke: #2ca02c;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative {
stroke: #d62728;
}
.nvd3.nv-historicalStockChart .nv-axis .nv-axislabel {
font-weight: bold;
}
.nvd3.nv-historicalStockChart .nv-dragTarget {
fill-opacity: 0;
stroke: none;
cursor: move;
}
.nvd3 .nv-brush .extent {
/*
cursor: ew-resize !important;
*/
fill-opacity: 0 !important;
}
.nvd3 .nv-brushBackground rect {
stroke: #000;
stroke-width: .4;
fill: #fff;
fill-opacity: .7;
}
/**********
* Indented Tree
*/
/**
* TODO: the following 3 selectors are based on classes used in the example. I should either make them standard and leave them here, or move to a CSS file not included in the library
*/
.nvd3.nv-indentedtree .name {
margin-left: 5px;
}
.nvd3.nv-indentedtree .clickable {
color: #08C;
cursor: pointer;
}
.nvd3.nv-indentedtree span.clickable:hover {
color: #005580;
text-decoration: underline;
}
.nvd3.nv-indentedtree .nv-childrenCount {
display: inline-block;
margin-left: 5px;
}
.nvd3.nv-indentedtree .nv-treeicon {
cursor: pointer;
/*
cursor: n-resize;
*/
}
.nvd3.nv-indentedtree .nv-treeicon.nv-folded {
cursor: pointer;
/*
cursor: s-resize;
*/
}
/**********
* Parallel Coordinates
*/
.nvd3 .background path {
fill: none;
stroke: #ccc;
stroke-opacity: .4;
shape-rendering: crispEdges;
}
.nvd3 .foreground path {
fill: none;
stroke: steelblue;
stroke-opacity: .7;
}
.nvd3 .brush .extent {
fill-opacity: .3;
stroke: #fff;
shape-rendering: crispEdges;
}
.nvd3 .axis line, .axis path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.nvd3 .axis text {
text-shadow: 0 1px 0 #fff;
}
/****
Interactive Layer
*/
.nvd3 .nv-interactiveGuideLine {
pointer-events:none;
}
.nvd3 line.nv-guideline {
stroke: #ccc;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +1,68 @@
svg {
height: 400px;
}
/*svg {
width: 100%;
}*/
.graph_container {
position: relative;
margin-bottom: 10px;
padding-right: 40px;
}
.legend {
position: absolute;
right: 10px;
top: 0px;
background-color: transparent;
color: #404040;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
/*.area {
fill-opacity: .85;
clip-path: url(#clip);
}
.brush .extent {
stroke: #fff;
fill-opacity: .125;
shape-rendering: crispEdges;
}
.legend rect {
fill: white;
stroke: black;
opacity: 0.8;
}
.d3-tip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
}
.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;
}
.d3-tip.n:after {
margin: -1px 0 0 0;
top: 100%;
left: 0;
}*/
@@ -0,0 +1,99 @@
var Graphic = function(data, options) {
this.initialize(data, options);
};
Graphic.prototype = {
constructor: Graphic,
defaults: {
},
initialize: function(data, options) {
this.options = $.extend({}, this.defaults, options);
this.graphics = {};
this.setup(data);
$(window).resize($.proxy(this.resize_graphics, this));
},
setup: function(data) {
$.each(data.graphs, $.proxy(function(id, graph) {
var chart = new Rickshaw.Graph({
element: document.getElementById(id),
renderer: 'area',
stroke: true,
series: graph,
height: 350
});
var preview = new Rickshaw.Graph.RangeSlider.Preview({
graph: chart,
element: document.getElementById(id + '_preview')
});
var xAxis = new Rickshaw.Graph.Axis.Time({
graph: chart,
ticksTreatment: 'glow'
});
var yAxis = new Rickshaw.Graph.Axis.Y({
graph: chart
});
var previewXAxis = new Rickshaw.Graph.Axis.Time({
graph: preview.previews[0],
/*timeFixture: new Rickshaw.Fixtures.Time.Local(),*/
ticksTreatment: 'glow'
});
var hoverDetail = new Rickshaw.Graph.HoverDetail( {
graph: chart,
xFormatter: function(x) {
return new Date(x * 1000).toString();
}
});
var legend = new Rickshaw.Graph.Legend({
graph: chart,
element: document.getElementById(id + '_legend')
});
var shelving = new Rickshaw.Graph.Behavior.Series.Toggle({
graph: chart,
legend: legend
});
chart.render();
xAxis.render();
yAxis.render();
previewXAxis.render();
this.graphics[id] = {
chart: chart, preview: preview
};
}, this));
},
resize_graphics: function(e) {
$.each(this.graphics, function(id, graphic) {
var w = $('#' + id).parent().width();
$.each(graphic, function(id, object) {
object.configure({width: w});
object.render();
});
});
},
update: function(data) {
$.each(data.graphs, $.proxy(function(id, graph) {
var $parent = $('#' + id).parent();
$('#' + id).remove();
$('#' + id + '_legend').remove();
$('#' + id + '_preview').remove();
$parent.append($("<div />", {id: id}));
$parent.append($("<div />", {id: id + '_preview'}));
$parent.append($("<div />", {id: id + '_legend', 'class': 'legend'}));
}, this));
this.setup(data);
}
};
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@ Stats.prototype = {
constructor: Stats,
defaults: {
deflocation: "d3graphs/",
deflocation: "graphs/",
language: "en"
},
@@ -80,26 +80,12 @@ Stats.prototype = {
},
graphs_cb: function(data) {
/*if (data.content) {
$(".tab-pane.active").html(data.content);
}*/
var $container = $("#graphs_mailtraffic");
$.each(data.graphs, function(id, data) {
var chart = nv.models.stackedAreaChart();
//var $svg = $("<svg />", {id: id});
//$container.append($svg);
chart.xAxis.axisLabel('Time (s)')
.tickFormat(d3.format(',r'));
chart.yAxis.axisLabel('Mail')
.tickFormat(d3.format('.02f'));
d3.select('#' + id + ' svg')
.datum(data)
.transition().duration(500)
.call(chart);
//nv.utils.windowResize(chart.update);
});
//this.graphicset = new GraphicSet("#gset", data);
if (this.graphic === undefined) {
this.graphic = new Graphic(data, {});
} else {
this.graphic.update(data);
}
},
search_domain: function(value) {
@@ -1,6 +0,0 @@
{% load i18n %}
{% for name in graphs %}
<p><img src="{{ MEDIA_URL }}stats/{{ name }}_{{ domain }}_AVERAGE_{% ifequal period 'custom' %}{{ period_name }}{% else %}{{ period }}{% endifequal %}.png"
alt="{% trans 'No statistics available' %}" /></p>
<div id="chart_{{ name }}"><svg></svg></div>
{% endfor %}
@@ -1,13 +1,17 @@
{% extends "twocols.html" %}{% load i18n %}{% load url from future %}
{% block pagetitle %}{% trans "Graphical statistics" %}{% endblock %}
{% block html_head %}
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}stats/css/nv.d3.css" />
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}stats/css/rickshaw.css" />
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}stats/css/stats.css" />
<script type="text/javascript" src="{{ STATIC_URL }}js/history.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/autocompleter.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}stats/js/stats.js"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script type="text/javascript" src="{{ STATIC_URL }}stats/js/nv.d3.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}stats/js/rickshaw.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}stats/js/graphic.js"></script>
<!--<script type="text/javascript" src="{{ STATIC_URL }}stats/js/d3.legend.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}stats/js/graphicset.js"></script>-->
<script type="text/javascript" src="{{ STATIC_URL }}stats/js/stats.js"></script>
<script type="text/javascript">
function get_domains_list(container) {
$.getJSON("{% url 'modoboa.extensions.admin.views.domain.domains_list' %}", function(data) {
@@ -73,9 +77,13 @@ $(document).ready(function() {
</ul>{% endfor %}
<div class="tab-content">{% for id, gset in graph_sets.items %}
<div class="tab-pane {% if forloop.first %}active{% endif %}" id="graphs_{{ gset.html_id }}">
<div id="avgtraffic"><svg /></div>
<div id="avgtrafficsize"><svg /></div>
<div id="badtraffic"><svg /></div>
<div id="gset"></div>
{% for graph in gset.graphics %}<h6>{{ graph.title }}</h6>
<div class="graph_container">
<div id="{{ graph.display_name }}"></div>
<div id="{{ graph.display_name }}_preview"></div>
<div id="{{ graph.display_name }}_legend" class="legend"></div>
</div>{% endfor %}
</div>
</div>{% endfor %}
</div>
-1
View File
@@ -5,5 +5,4 @@ urlpatterns = patterns(
'modoboa.extensions.stats.views',
url(r'^$', 'index', name='fullindex'),
url(r'^graphs/$', "graphs"),
url(r'^d3graphs/$', "export"),
)
+18 -73
View File
@@ -1,5 +1,6 @@
# coding: utf-8
import re
import time
from django.shortcuts import render
from django.utils.translation import ugettext as _
from django.contrib.auth.decorators import (
@@ -8,12 +9,12 @@ from django.contrib.auth.decorators import (
from modoboa.lib import events
from modoboa.lib.exceptions import BadRequest, PermDeniedException, NotFound
from modoboa.lib.webutils import (
_render_to_string, render_to_json_response
render_to_json_response
)
from modoboa.extensions.admin.models import (
Domain
)
from modoboa.extensions.stats.grapher import periods, str2Time, Grapher
from modoboa.extensions.stats.lib import date_to_timestamp
@login_required
@@ -22,16 +23,18 @@ def index(request):
"""
FIXME: how to select a default graph set ?
"""
deflocation = "d3graphs/?gset=mailtraffic"
deflocation = "graphs/?gset=mailtraffic"
if not request.user.is_superuser:
if not Domain.objects.get_for_admin(request.user).count():
raise NotFound(_("No statistics available"))
period = request.GET.get("period", "day")
graph_sets = events.raiseDictEvent('GetGraphSets')
periods = [{"name": "day", "label": _("Day")},
{"name": "week", "label": _("Week")},
{"name": "month", "label": _("Month")},
{"name": "year", "label": _("Year")}]
return render(request, 'stats/index.html', {
"periods": periods,
"period": period,
"selection": "stats",
"deflocation": deflocation,
"graph_sets": graph_sets
@@ -41,63 +44,6 @@ def index(request):
@login_required
@user_passes_test(lambda u: u.group != "SimpleUsers")
def graphs(request):
gset = request.GET.get("gset", None)
gsets = events.raiseDictEvent("GetGraphSets")
if not gset in gsets:
raise NotFound(_("Unknown graphic set"))
searchq = request.GET.get("searchquery", None)
period = request.GET.get("period", "day")
tplvars = dict(graphs=[], period=period)
if searchq in [None, "global"]:
if not request.user.is_superuser:
if not Domain.objects.get_for_admin(request.user).count():
return render_to_json_response({})
tplvars.update(
domain=Domain.objects.get_for_admin(request.user)[0].name
)
else:
tplvars.update(domain="global")
else:
domain = Domain.objects.filter(name__contains=searchq)
if domain.count() != 1:
return render_to_json_response({})
if not request.user.can_access(domain[0]):
raise PermDeniedException
tplvars.update(domain=domain[0].name)
if period == "custom":
if not "start" in request.GET or not "end" in request.GET:
raise BadRequest(_("Bad custom period"))
start = request.GET["start"]
end = request.GET["end"]
G = Grapher()
expr = re.compile(r'[:\- ]')
period_name = "%s_%s" % (expr.sub('', start), expr.sub('', end))
for tpl in gsets[gset].get_graphs():
tplvars['graphs'].append(tpl.display_name)
G.process(
tplvars["domain"], period_name, str2Time(*expr.split(start)),
str2Time(*expr.split(end)), tpl
)
G.export(
tplvars["domain"], period_name, str2Time(*expr.split(start)),
str2Time(*expr.split(end)), tpl
)
tplvars["period_name"] = period_name
tplvars["start"] = start
tplvars["end"] = end
else:
tplvars['graphs'] = gsets[gset].get_graph_names()
return render_to_json_response({
'content': _render_to_string(request, "stats/graphs.html", tplvars)
})
@login_required
@user_passes_test(lambda u: u.group != "SimpleUsers")
def export(request):
gset = request.GET.get("gset", None)
gsets = events.raiseDictEvent("GetGraphSets")
if not gset in gsets:
@@ -126,19 +72,18 @@ def export(request):
raise BadRequest(_("Bad custom period"))
start = request.GET["start"]
end = request.GET["end"]
G = Grapher()
expr = re.compile(r'[:\- ]')
period_name = "%s_%s" % (expr.sub('', start), expr.sub('', end))
for tpl in gsets[gset].get_graphs():
tplvars['graphs'][tpl.display_name] = G.export(
tplvars["domain"], period_name, str2Time(*expr.split(start)),
str2Time(*expr.split(end)), tpl
)
tplvars["period_name"] = period_name
tplvars["start"] = start
tplvars["end"] = end
start = date_to_timestamp(expr.split(start))
end = date_to_timestamp(expr.split(end))
else:
tplvars['graphs'] = gsets[gset].get_graph_names()
end = int(time.mktime(time.localtime()))
start = "-1%s" % period
period_name = period
tplvars['graphs'] = gsets[gset].export(tplvars["domain"], start, end)
tplvars["period_name"] = period_name
tplvars["start"] = start
tplvars["end"] = end
return render_to_json_response(tplvars)