|
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 |