mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-19 05:58:34 +02:00
New attempt using Rickshaw.
This commit is contained in:
@@ -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]
|
||||
@@ -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)
|
||||
@@ -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]
|
||||
@@ -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>
|
||||
|
||||
@@ -5,5 +5,4 @@ urlpatterns = patterns(
|
||||
'modoboa.extensions.stats.views',
|
||||
url(r'^$', 'index', name='fullindex'),
|
||||
url(r'^graphs/$', "graphs"),
|
||||
url(r'^d3graphs/$', "export"),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user