--- a/web/views/plots.py Thu May 21 00:42:05 2009 +0200
+++ b/web/views/plots.py Thu May 21 00:44:57 2009 +0200
@@ -7,120 +7,124 @@
__docformat__ = "restructuredtext en"
import os
+import time
+
+from simplejson import dumps
from logilab.common import flatten
from logilab.mtconverter import html_escape
+from cubicweb.utils import make_uid
from cubicweb.vregistry import objectify_selector
from cubicweb.web.views import baseviews
@objectify_selector
-def plot_selector(cls, req, rset, *args, **kwargs):
+def at_least_two_columns(cls, req, rset, *args, **kwargs):
+ if not rset:
+ return 0
+ return len(rset.rows[0]) >= 2
+
+@objectify_selector
+def all_columns_are_numbers(cls, req, rset, *args, **kwargs):
"""accept result set with at least one line and two columns of result
all columns after second must be of numerical types"""
- if not rset:
- return 0
- if len(rset.rows[0]) < 2:
- return 0
for etype in rset.description[0]:
if etype not in ('Int', 'Float'):
return 0
return 1
@objectify_selector
-def piechart_selector(cls, req, rset, *args, **kwargs):
- if not rset:
- return 0
- if len(rset.rows[0]) < 2:
- return 0
+def second_column_is_number(cls, req, rset, *args, **kwargs):
etype = rset.description[0][1]
if etype not in ('Int', 'Float'):
return 0
return 1
-try:
- import matplotlib
- import sys
- if 'matplotlib.backends' not in sys.modules:
- matplotlib.use('Agg')
- from pylab import figure
-except ImportError:
- pass
-else:
- class PlotView(baseviews.AnyRsetView):
- id = 'plot'
- title = _('generic plot')
- binary = True
- content_type = 'image/png'
- _plot_count = 0
- __select__ = plot_selector()
+@objectify_selector
+def columns_are_date_then_numbers(cls, req, rset, *args, **kwargs):
+ etypes = rset.description[0]
+ if etypes[0] not in ('Date', 'Datetime'):
+ return 0
+ for etype in etypes[1:]:
+ if etype not in ('Int', 'Float'):
+ return 0
+ return 1
+
- def call(self, width=None, height=None):
- # compute dimensions
- if width is None:
- if 'width' in self.req.form:
- width = int(self.req.form['width'])
- else:
- width = 500
+def filterout_nulls(abscissa, plot):
+ filtered = []
+ for x, y in zip(abscissa, plot):
+ if x is None or y is None:
+ continue
+ filtered.append( (x, y) )
+ return sorted(filtered)
- if height is None:
- if 'height' in self.req.form:
- height = int(self.req.form['height'])
- else:
- height = 400
- dpi = 100.
+class PlotView(baseviews.AnyRsetView):
+ id = 'plot'
+ title = _('generic plot')
+ __select__ = at_least_two_columns() & all_columns_are_numbers()
+ mode = 'null' # null or time, meant for jquery.flot.js
+
+ def _build_abscissa(self):
+ return [row[0] for row in self.rset]
+
+ def _build_data(self, abscissa, plot):
+ return filterout_nulls(abscissa, plot)
- # compute data
- abscisses = [row[0] for row in self.rset]
- courbes = []
- nbcols = len(self.rset.rows[0])
- for col in xrange(1, nbcols):
- courbe = [row[col] for row in self.rset]
- courbes.append(courbe)
- if not courbes:
- raise Exception('no data')
- # plot data
- fig = figure(figsize=(width/dpi, height/dpi), dpi=dpi)
- ax = fig.add_subplot(111)
- colors = 'brgybrgy'
- try:
- float(abscisses[0])
- xlabels = None
- except ValueError:
- xlabels = abscisses
- abscisses = range(len(xlabels))
- for idx, courbe in enumerate(courbes):
- ax.plot(abscisses, courbe, '%sv-' % colors[idx], label=self.rset.description[0][idx+1])
- ax.autoscale_view()
- alldata = flatten(courbes)
- m, M = min(alldata or [0]), max(alldata or [1])
- if m is None: m = 0
- if M is None: M = 0
- margin = float(M-m)/10
- ax.set_ylim(m-margin, M+margin)
- ax.grid(True)
- ax.legend(loc='best')
- if xlabels is not None:
- ax.set_xticks(abscisses)
- ax.set_xticklabels(xlabels)
- try:
- fig.autofmt_xdate()
- except AttributeError:
- # XXX too old version of matplotlib. Ignore safely.
- pass
+ def call(self, width=500, height=400):
+ # XXX add excanvas.js if IE
+ self.req.add_js( ('jquery.flot.js', 'cubicweb.flot.js') )
+ # prepare data
+ abscissa = self._build_abscissa()
+ plots = []
+ nbcols = len(self.rset.rows[0])
+ for col in xrange(1, nbcols):
+ plots.append([row[col] for row in self.rset])
+ # plot data
+ plotuid = 'plot%s' % make_uid('foo')
+ self.w(u'<div id="%s" style="width: %spx; height: %spx;"></div>' %
+ (plotuid, width, height))
+ rqlst = self.rset.syntax_tree()
+ # XXX try to make it work with unions
+ varnames = [var.name for var in rqlst.children[0].get_selected_variables()][1:]
+ plotdefs = []
+ plotdata = []
+ for idx, (varname, plot) in enumerate(zip(varnames, plots)):
+ plotid = '%s_%s' % (plotuid, idx)
+ data = self._build_data(abscissa, plot)
+ plotdefs.append('var %s = %s;' % (plotid, dumps(data)))
+ plotdata.append("{label: '%s', data: %s}" % (varname, plotid))
+ self.req.html_headers.add_onload('''
+%(plotdefs)s
+jQuery.plot(jQuery("#%(plotuid)s"), [%(plotdata)s],
+ {points: {show: true},
+ lines: {show: true},
+ grid: {hoverable: true},
+ xaxis: {mode: %(mode)s}});
+jQuery('#%(plotuid)s').bind('plothover', onPlotHover);
+''' % {'plotdefs': '\n'.join(plotdefs),
+ 'plotuid': plotuid,
+ 'plotdata': ','.join(plotdata),
+ 'mode': self.mode})
- # save plot
- filename = self.build_figname()
- fig.savefig(filename, dpi=100)
- img = open(filename, 'rb')
- self.w(img.read())
- img.close()
- os.remove(filename)
+
+class TimeSeriePlotView(PlotView):
+ id = 'plot'
+ title = _('generic plot')
+ __select__ = at_least_two_columns() & columns_are_date_then_numbers()
+ mode = '"time"'
- def build_figname(self):
- self.__class__._plot_count += 1
- return '/tmp/burndown_chart_%s_%d.png' % (self.config.appid,
- self.__class__._plot_count)
+ def _build_abscissa(self):
+ abscissa = [time.mktime(row[0].timetuple()) * 1000
+ for row in self.rset]
+ return abscissa
+
+ def _build_data(self, abscissa, plot):
+ data = []
+ # XXX find a way to get rid of the 3rd column and find 'mode' in JS
+ for x, y in filterout_nulls(abscissa, plot):
+ data.append( (x, y, x) )
+ return data
try:
from GChartWrapper import Pie, Pie3D
@@ -130,7 +134,7 @@
class PieChartView(baseviews.AnyRsetView):
id = 'piechart'
pieclass = Pie
- __select__ = piechart_selector()
+ __select__ = at_least_two_columns() & second_column_is_number()
def call(self, title=None, width=None, height=None):
piechart = self.pieclass([(row[1] or 0) for row in self.rset])