web/views/plots.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """basic plot views"""
       
    19 
       
    20 __docformat__ = "restructuredtext en"
       
    21 from cubicweb import _
       
    22 
       
    23 from six import add_metaclass
       
    24 from six.moves import range
       
    25 
       
    26 from logilab.common.date import datetime2ticks
       
    27 from logilab.common.deprecation import class_deprecated
       
    28 from logilab.common.registry import objectify_predicate
       
    29 from logilab.mtconverter import xml_escape
       
    30 
       
    31 from cubicweb.utils import UStringIO, json_dumps
       
    32 from cubicweb.predicates import multi_columns_rset
       
    33 from cubicweb.web.views import baseviews
       
    34 
       
    35 @objectify_predicate
       
    36 def all_columns_are_numbers(cls, req, rset=None, *args, **kwargs):
       
    37     """accept result set with at least one line and two columns of result
       
    38     all columns after second must be of numerical types"""
       
    39     for etype in rset.description[0]:
       
    40         if etype not in ('Int', 'BigInt', 'Float'):
       
    41             return 0
       
    42     return 1
       
    43 
       
    44 @objectify_predicate
       
    45 def second_column_is_number(cls, req, rset=None, *args, **kwargs):
       
    46     etype = rset.description[0][1]
       
    47     if etype not  in ('Int', 'BigInt', 'Float'):
       
    48         return 0
       
    49     return 1
       
    50 
       
    51 @objectify_predicate
       
    52 def columns_are_date_then_numbers(cls, req, rset=None, *args, **kwargs):
       
    53     etypes = rset.description[0]
       
    54     if etypes[0] not in ('Date', 'Datetime', 'TZDatetime'):
       
    55         return 0
       
    56     for etype in etypes[1:]:
       
    57         if etype not in ('Int', 'BigInt', 'Float'):
       
    58             return 0
       
    59     return 1
       
    60 
       
    61 
       
    62 def filterout_nulls(abscissa, plot):
       
    63     filtered = []
       
    64     for x, y in zip(abscissa, plot):
       
    65         if x is None or y is None:
       
    66             continue
       
    67         filtered.append( (x, y) )
       
    68     return sorted(filtered)
       
    69 
       
    70 class PlotWidget(object):
       
    71     # XXX refactor with cubicweb.web.views.htmlwidgets.HtmlWidget
       
    72     def _initialize_stream(self, w=None):
       
    73         if w:
       
    74             self.w = w
       
    75         else:
       
    76             self._stream = UStringIO()
       
    77             self.w = self._stream.write
       
    78 
       
    79     def render(self, *args, **kwargs):
       
    80         w = kwargs.pop('w', None)
       
    81         self._initialize_stream(w)
       
    82         self._render(*args, **kwargs)
       
    83         if w is None:
       
    84             return self._stream.getvalue()
       
    85 
       
    86     def _render(self, *args, **kwargs):
       
    87         raise NotImplementedError
       
    88 
       
    89 
       
    90 @add_metaclass(class_deprecated)
       
    91 class FlotPlotWidget(PlotWidget):
       
    92     """PlotRenderer widget using Flot"""
       
    93     __deprecation_warning__ = '[3.14] cubicweb.web.views.plots module is deprecated, use the jqplot cube instead'
       
    94     onload = u"""
       
    95 var fig = jQuery('#%(figid)s');
       
    96 if (fig.attr('cubicweb:type') != 'prepared-plot') {
       
    97     %(plotdefs)s
       
    98     jQuery.plot(jQuery('#%(figid)s'), [%(plotdata)s],
       
    99         {points: {show: true},
       
   100          lines: {show: true},
       
   101          grid: {hoverable: true},
       
   102          /*yaxis : {tickFormatter : suffixFormatter},*/
       
   103          xaxis: {mode: %(mode)s}});
       
   104     jQuery('#%(figid)s').data({mode: %(mode)s, dateformat: %(dateformat)s});
       
   105     jQuery('#%(figid)s').bind('plothover', onPlotHover);
       
   106     fig.attr('cubicweb:type','prepared-plot');
       
   107 }
       
   108 """
       
   109 
       
   110     def __init__(self, labels, plots, timemode=False):
       
   111         self.labels = labels
       
   112         self.plots = plots # list of list of couples
       
   113         self.timemode = timemode
       
   114 
       
   115     def dump_plot(self, plot):
       
   116         if self.timemode:
       
   117             plot = [(datetime2ticks(x), y) for x, y in plot]
       
   118         return json_dumps(plot)
       
   119 
       
   120     def _render(self, req, width=500, height=400):
       
   121         if req.ie_browser():
       
   122             req.add_js('excanvas.js')
       
   123         req.add_js(('jquery.flot.js', 'cubicweb.flot.js'))
       
   124         figid = u'figure%s' % next(req.varmaker)
       
   125         plotdefs = []
       
   126         plotdata = []
       
   127         self.w(u'<div id="%s" style="width: %spx; height: %spx;"></div>' %
       
   128                (figid, width, height))
       
   129         for idx, (label, plot) in enumerate(zip(self.labels, self.plots)):
       
   130             plotid = '%s_%s' % (figid, idx)
       
   131             plotdefs.append('var %s = %s;' % (plotid, self.dump_plot(plot)))
       
   132             # XXX ugly but required in order to not crash my demo
       
   133             plotdata.append("{label: '%s', data: %s}" % (label.replace(u'&', u''), plotid))
       
   134         fmt = req.property_value('ui.date-format') # XXX datetime-format
       
   135         # XXX TODO make plot options customizable
       
   136         req.html_headers.add_onload(self.onload %
       
   137                                     {'plotdefs': '\n'.join(plotdefs),
       
   138                                      'figid': figid,
       
   139                                      'plotdata': ','.join(plotdata),
       
   140                                      'mode': self.timemode and "'time'" or 'null',
       
   141                                      'dateformat': '"%s"' % fmt})
       
   142 
       
   143 
       
   144 @add_metaclass(class_deprecated)
       
   145 class PlotView(baseviews.AnyRsetView):
       
   146     __deprecation_warning__ = '[3.14] cubicweb.web.views.plots module is deprecated, use the jqplot cube instead'
       
   147     __regid__ = 'plot'
       
   148     title = _('generic plot')
       
   149     __select__ = multi_columns_rset() & all_columns_are_numbers()
       
   150     timemode = False
       
   151     paginable = False
       
   152 
       
   153     def call(self, width=500, height=400):
       
   154         # prepare data
       
   155         rqlst = self.cw_rset.syntax_tree()
       
   156         # XXX try to make it work with unions
       
   157         varnames = [var.name for var in rqlst.children[0].get_selected_variables()][1:]
       
   158         abscissa = [row[0] for row in self.cw_rset]
       
   159         plots = []
       
   160         nbcols = len(self.cw_rset.rows[0])
       
   161         for col in range(1, nbcols):
       
   162             data = [row[col] for row in self.cw_rset]
       
   163             plots.append(filterout_nulls(abscissa, data))
       
   164         plotwidget = FlotPlotWidget(varnames, plots, timemode=self.timemode)
       
   165         plotwidget.render(self._cw, width, height, w=self.w)
       
   166 
       
   167 
       
   168 class TimeSeriePlotView(PlotView):
       
   169     __select__ = multi_columns_rset() & columns_are_date_then_numbers()
       
   170     timemode = True
       
   171 
       
   172 
       
   173 try:
       
   174     from GChartWrapper import Pie, Pie3D
       
   175 except ImportError:
       
   176     pass
       
   177 else:
       
   178 
       
   179     class PieChartWidget(PlotWidget):
       
   180         def __init__(self, labels, values, pieclass=Pie, title=None):
       
   181             self.labels = labels
       
   182             self.values = values
       
   183             self.pieclass = pieclass
       
   184             self.title = title
       
   185 
       
   186         def _render(self, width=None, height=None):
       
   187             piechart = self.pieclass(self.values)
       
   188             piechart.label(*self.labels)
       
   189             if width is not None:
       
   190                 height = height or width
       
   191                 piechart.size(width, height)
       
   192             if self.title:
       
   193                 piechart.title(self.title)
       
   194             self.w(u'<img src="%s" />' % xml_escape(piechart.url))
       
   195 
       
   196     class PieChartView(baseviews.AnyRsetView):
       
   197         __regid__ = 'piechart'
       
   198         pieclass = Pie
       
   199         paginable = False
       
   200 
       
   201         __select__ = multi_columns_rset() & second_column_is_number()
       
   202 
       
   203         def _guess_vid(self, row):
       
   204             etype = self.cw_rset.description[row][0]
       
   205             if self._cw.vreg.schema.eschema(etype).final:
       
   206                 return 'final'
       
   207             return 'textincontext'
       
   208 
       
   209         def call(self, title=None, width=None, height=None):
       
   210             labels = []
       
   211             values = []
       
   212             for rowidx, (_, value) in enumerate(self.cw_rset):
       
   213                 if value is not None:
       
   214                     vid = self._guess_vid(rowidx)
       
   215                     label = '%s: %s' % (self._cw.view(vid, self.cw_rset, row=rowidx, col=0),
       
   216                                         value)
       
   217                     labels.append(label.encode(self._cw.encoding))
       
   218                     values.append(value)
       
   219             pie = PieChartWidget(labels, values, pieclass=self.pieclass,
       
   220                                  title=title)
       
   221             if width is not None:
       
   222                 height = height or width
       
   223             pie.render(width, height, w=self.w)
       
   224 
       
   225 
       
   226     class PieChart3DView(PieChartView):
       
   227         __regid__ = 'piechart3D'
       
   228         pieclass = Pie3D