web/views/plots.py
author Adrien Di Mascio <Adrien.DiMascio@logilab.fr>
Wed, 20 May 2009 22:28:45 +0200
changeset 1886 f0e28ddba7c5
parent 1133 8a409ea0c9ec
child 1888 f36d43f00f32
permissions -rw-r--r--
[views] add pie chart views with google chart / GChartWrapper

"""basic plot views

:organization: Logilab
:copyright: 2007-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"

import os

from logilab.common import flatten
from logilab.mtconverter import html_escape

from cubicweb.vregistry import objectify_selector
from cubicweb.web.views import baseviews

@objectify_selector
def plot_selector(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
    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()

        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

            if height is None:
                if 'height' in self.req.form:
                    height = int(self.req.form['height'])
                else:
                    height = 400
            dpi = 100.

            # 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

            # save plot
            filename = self.build_figname()
            fig.savefig(filename, dpi=100)
            img = open(filename, 'rb')
            self.w(img.read())
            img.close()
            os.remove(filename)

        def build_figname(self):
            self.__class__._plot_count += 1
            return '/tmp/burndown_chart_%s_%d.png' % (self.config.appid,
                                                      self.__class__._plot_count)

try:
    from GChartWrapper import Pie, Pie3D
except ImportError:
    pass
else:
    class PieChartView(baseviews.AnyRsetView):
        id = 'piechart'
        pieclass = Pie
        __select__ = piechart_selector()

        def call(self, title=None, width=None, height=None):
            piechart = self.pieclass([(row[1] or 0) for row in self.rset])
            labels = ['%s: %s' % (row[0].encode(self.req.encoding), row[1])
                      for row in self.rset]
            piechart.label(*labels)
            if width is not None:
                height = height or width
                piechart.size(width, height)
            if title:
                piechart.title(title)
            self.w(u'<img src="%s" />' % html_escape(piechart.url))


    class PieChart3DView(PieChartView):
        id = 'piechart3D'
        pieclass = Pie3D