diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/web/views/plots.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/views/plots.py Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,228 @@
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see .
+"""basic plot views"""
+
+__docformat__ = "restructuredtext en"
+from cubicweb import _
+
+from six import add_metaclass
+from six.moves import range
+
+from logilab.common.date import datetime2ticks
+from logilab.common.deprecation import class_deprecated
+from logilab.common.registry import objectify_predicate
+from logilab.mtconverter import xml_escape
+
+from cubicweb.utils import UStringIO, json_dumps
+from cubicweb.predicates import multi_columns_rset
+from cubicweb.web.views import baseviews
+
+@objectify_predicate
+def all_columns_are_numbers(cls, req, rset=None, *args, **kwargs):
+ """accept result set with at least one line and two columns of result
+ all columns after second must be of numerical types"""
+ for etype in rset.description[0]:
+ if etype not in ('Int', 'BigInt', 'Float'):
+ return 0
+ return 1
+
+@objectify_predicate
+def second_column_is_number(cls, req, rset=None, *args, **kwargs):
+ etype = rset.description[0][1]
+ if etype not in ('Int', 'BigInt', 'Float'):
+ return 0
+ return 1
+
+@objectify_predicate
+def columns_are_date_then_numbers(cls, req, rset=None, *args, **kwargs):
+ etypes = rset.description[0]
+ if etypes[0] not in ('Date', 'Datetime', 'TZDatetime'):
+ return 0
+ for etype in etypes[1:]:
+ if etype not in ('Int', 'BigInt', 'Float'):
+ return 0
+ return 1
+
+
+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)
+
+class PlotWidget(object):
+ # XXX refactor with cubicweb.web.views.htmlwidgets.HtmlWidget
+ def _initialize_stream(self, w=None):
+ if w:
+ self.w = w
+ else:
+ self._stream = UStringIO()
+ self.w = self._stream.write
+
+ def render(self, *args, **kwargs):
+ w = kwargs.pop('w', None)
+ self._initialize_stream(w)
+ self._render(*args, **kwargs)
+ if w is None:
+ return self._stream.getvalue()
+
+ def _render(self, *args, **kwargs):
+ raise NotImplementedError
+
+
+@add_metaclass(class_deprecated)
+class FlotPlotWidget(PlotWidget):
+ """PlotRenderer widget using Flot"""
+ __deprecation_warning__ = '[3.14] cubicweb.web.views.plots module is deprecated, use the jqplot cube instead'
+ onload = u"""
+var fig = jQuery('#%(figid)s');
+if (fig.attr('cubicweb:type') != 'prepared-plot') {
+ %(plotdefs)s
+ jQuery.plot(jQuery('#%(figid)s'), [%(plotdata)s],
+ {points: {show: true},
+ lines: {show: true},
+ grid: {hoverable: true},
+ /*yaxis : {tickFormatter : suffixFormatter},*/
+ xaxis: {mode: %(mode)s}});
+ jQuery('#%(figid)s').data({mode: %(mode)s, dateformat: %(dateformat)s});
+ jQuery('#%(figid)s').bind('plothover', onPlotHover);
+ fig.attr('cubicweb:type','prepared-plot');
+}
+"""
+
+ def __init__(self, labels, plots, timemode=False):
+ self.labels = labels
+ self.plots = plots # list of list of couples
+ self.timemode = timemode
+
+ def dump_plot(self, plot):
+ if self.timemode:
+ plot = [(datetime2ticks(x), y) for x, y in plot]
+ return json_dumps(plot)
+
+ def _render(self, req, width=500, height=400):
+ if req.ie_browser():
+ req.add_js('excanvas.js')
+ req.add_js(('jquery.flot.js', 'cubicweb.flot.js'))
+ figid = u'figure%s' % next(req.varmaker)
+ plotdefs = []
+ plotdata = []
+ self.w(u'
' %
+ (figid, width, height))
+ for idx, (label, plot) in enumerate(zip(self.labels, self.plots)):
+ plotid = '%s_%s' % (figid, idx)
+ plotdefs.append('var %s = %s;' % (plotid, self.dump_plot(plot)))
+ # XXX ugly but required in order to not crash my demo
+ plotdata.append("{label: '%s', data: %s}" % (label.replace(u'&', u''), plotid))
+ fmt = req.property_value('ui.date-format') # XXX datetime-format
+ # XXX TODO make plot options customizable
+ req.html_headers.add_onload(self.onload %
+ {'plotdefs': '\n'.join(plotdefs),
+ 'figid': figid,
+ 'plotdata': ','.join(plotdata),
+ 'mode': self.timemode and "'time'" or 'null',
+ 'dateformat': '"%s"' % fmt})
+
+
+@add_metaclass(class_deprecated)
+class PlotView(baseviews.AnyRsetView):
+ __deprecation_warning__ = '[3.14] cubicweb.web.views.plots module is deprecated, use the jqplot cube instead'
+ __regid__ = 'plot'
+ title = _('generic plot')
+ __select__ = multi_columns_rset() & all_columns_are_numbers()
+ timemode = False
+ paginable = False
+
+ def call(self, width=500, height=400):
+ # prepare data
+ rqlst = self.cw_rset.syntax_tree()
+ # XXX try to make it work with unions
+ varnames = [var.name for var in rqlst.children[0].get_selected_variables()][1:]
+ abscissa = [row[0] for row in self.cw_rset]
+ plots = []
+ nbcols = len(self.cw_rset.rows[0])
+ for col in range(1, nbcols):
+ data = [row[col] for row in self.cw_rset]
+ plots.append(filterout_nulls(abscissa, data))
+ plotwidget = FlotPlotWidget(varnames, plots, timemode=self.timemode)
+ plotwidget.render(self._cw, width, height, w=self.w)
+
+
+class TimeSeriePlotView(PlotView):
+ __select__ = multi_columns_rset() & columns_are_date_then_numbers()
+ timemode = True
+
+
+try:
+ from GChartWrapper import Pie, Pie3D
+except ImportError:
+ pass
+else:
+
+ class PieChartWidget(PlotWidget):
+ def __init__(self, labels, values, pieclass=Pie, title=None):
+ self.labels = labels
+ self.values = values
+ self.pieclass = pieclass
+ self.title = title
+
+ def _render(self, width=None, height=None):
+ piechart = self.pieclass(self.values)
+ piechart.label(*self.labels)
+ if width is not None:
+ height = height or width
+ piechart.size(width, height)
+ if self.title:
+ piechart.title(self.title)
+ self.w(u'' % xml_escape(piechart.url))
+
+ class PieChartView(baseviews.AnyRsetView):
+ __regid__ = 'piechart'
+ pieclass = Pie
+ paginable = False
+
+ __select__ = multi_columns_rset() & second_column_is_number()
+
+ def _guess_vid(self, row):
+ etype = self.cw_rset.description[row][0]
+ if self._cw.vreg.schema.eschema(etype).final:
+ return 'final'
+ return 'textincontext'
+
+ def call(self, title=None, width=None, height=None):
+ labels = []
+ values = []
+ for rowidx, (_, value) in enumerate(self.cw_rset):
+ if value is not None:
+ vid = self._guess_vid(rowidx)
+ label = '%s: %s' % (self._cw.view(vid, self.cw_rset, row=rowidx, col=0),
+ value)
+ labels.append(label.encode(self._cw.encoding))
+ values.append(value)
+ pie = PieChartWidget(labels, values, pieclass=self.pieclass,
+ title=title)
+ if width is not None:
+ height = height or width
+ pie.render(width, height, w=self.w)
+
+
+ class PieChart3DView(PieChartView):
+ __regid__ = 'piechart3D'
+ pieclass = Pie3D