1
|
|
"""
|
2
|
|
Pane class which render plots from different libraries
|
3
|
|
"""
|
4
|
6
|
from __future__ import absolute_import, division, unicode_literals
|
5
|
|
|
6
|
6
|
import sys
|
7
|
|
|
8
|
6
|
from io import BytesIO
|
9
|
|
|
10
|
6
|
from contextlib import contextmanager
|
11
|
|
|
12
|
6
|
import param
|
13
|
|
|
14
|
6
|
from bokeh.models import CustomJS, LayoutDOM, Model, Spacer as BkSpacer
|
15
|
|
|
16
|
6
|
from ..io import remove_root
|
17
|
6
|
from ..io.notebook import push
|
18
|
6
|
from ..viewable import Layoutable
|
19
|
6
|
from .base import PaneBase
|
20
|
6
|
from .ipywidget import IPyWidget
|
21
|
6
|
from .markup import HTML
|
22
|
6
|
from .image import PNG
|
23
|
|
|
24
|
|
|
25
|
6
|
@contextmanager
|
26
|
1
|
def _wrap_callback(cb, wrapped, doc, comm, callbacks):
|
27
|
|
"""
|
28
|
|
Wraps a bokeh callback ensuring that any events triggered by it
|
29
|
|
appropriately dispatch events in the notebook. Also temporarily
|
30
|
|
replaces the wrapped callback with the real one while the callback
|
31
|
|
is exectuted to ensure the callback can be removed as usual.
|
32
|
|
"""
|
33
|
0
|
hold = doc._hold
|
34
|
0
|
doc.hold('combine')
|
35
|
0
|
if wrapped in callbacks:
|
36
|
0
|
index = callbacks.index(wrapped)
|
37
|
0
|
callbacks[index] = cb
|
38
|
0
|
yield
|
39
|
0
|
if cb in callbacks:
|
40
|
0
|
index = callbacks.index(cb)
|
41
|
0
|
callbacks[index] = wrapped
|
42
|
0
|
push(doc, comm)
|
43
|
0
|
doc.hold(hold)
|
44
|
|
|
45
|
|
|
46
|
6
|
class Bokeh(PaneBase):
|
47
|
|
"""
|
48
|
|
Bokeh panes allow including any Bokeh model in a panel.
|
49
|
|
"""
|
50
|
|
|
51
|
6
|
priority = 0.8
|
52
|
|
|
53
|
6
|
@classmethod
|
54
|
1
|
def applies(cls, obj):
|
55
|
6
|
return isinstance(obj, LayoutDOM)
|
56
|
|
|
57
|
6
|
@classmethod
|
58
|
1
|
def _property_callback_wrapper(cls, cb, doc, comm, callbacks):
|
59
|
0
|
def wrapped_callback(attr, old, new):
|
60
|
0
|
with _wrap_callback(cb, wrapped_callback, doc, comm, callbacks):
|
61
|
0
|
cb(attr, old, new)
|
62
|
0
|
return wrapped_callback
|
63
|
|
|
64
|
6
|
@classmethod
|
65
|
1
|
def _event_callback_wrapper(cls, cb, doc, comm, callbacks):
|
66
|
0
|
def wrapped_callback(event):
|
67
|
0
|
with _wrap_callback(cb, wrapped_callback, doc, comm, callbacks):
|
68
|
0
|
cb(event)
|
69
|
0
|
return wrapped_callback
|
70
|
|
|
71
|
6
|
@classmethod
|
72
|
1
|
def _wrap_bokeh_callbacks(cls, root, bokeh_model, doc, comm):
|
73
|
6
|
for model in bokeh_model.select({'type': Model}):
|
74
|
6
|
for key, cbs in model._callbacks.items():
|
75
|
0
|
callbacks = model._callbacks[key]
|
76
|
0
|
callbacks[:] = [
|
77
|
|
cls._property_callback_wrapper(cb, doc, comm, callbacks)
|
78
|
|
for cb in cbs
|
79
|
|
]
|
80
|
6
|
for key, cbs in model._event_callbacks.items():
|
81
|
0
|
callbacks = model._event_callbacks[key]
|
82
|
0
|
callbacks[:] = [
|
83
|
|
cls._event_callback_wrapper(cb, doc, comm, callbacks)
|
84
|
|
for cb in cbs
|
85
|
|
]
|
86
|
|
|
87
|
6
|
def _get_model(self, doc, root=None, parent=None, comm=None):
|
88
|
6
|
if root is None:
|
89
|
0
|
return self._get_root(doc, comm)
|
90
|
|
|
91
|
6
|
if self.object is None:
|
92
|
0
|
model = BkSpacer()
|
93
|
|
else:
|
94
|
6
|
model = self.object
|
95
|
|
|
96
|
6
|
properties = {}
|
97
|
6
|
for p, value in self.param.get_param_values():
|
98
|
6
|
if (p not in Layoutable.param or p == 'name' or
|
99
|
|
value is self.param[p].default):
|
100
|
6
|
continue
|
101
|
6
|
properties[p] = value
|
102
|
6
|
model.update(**properties)
|
103
|
6
|
if comm:
|
104
|
6
|
self._wrap_bokeh_callbacks(root, model, doc, comm)
|
105
|
|
|
106
|
6
|
ref = root.ref['id']
|
107
|
6
|
for js in model.select({'type': CustomJS}):
|
108
|
0
|
js.code = js.code.replace(model.ref['id'], ref)
|
109
|
|
|
110
|
6
|
if model._document and doc is not model._document:
|
111
|
0
|
remove_root(model, doc)
|
112
|
|
|
113
|
6
|
self._models[ref] = (model, parent)
|
114
|
6
|
return model
|
115
|
|
|
116
|
|
|
117
|
6
|
class Matplotlib(PNG, IPyWidget):
|
118
|
|
"""
|
119
|
|
A Matplotlib pane renders a matplotlib figure to png and wraps the
|
120
|
|
base64 encoded data in a bokeh Div model. The size of the image in
|
121
|
|
pixels is determined by scaling the size of the figure in inches
|
122
|
|
by a dpi of 72, increasing the dpi therefore controls the
|
123
|
|
resolution of the image not the displayed size.
|
124
|
|
"""
|
125
|
|
|
126
|
6
|
dpi = param.Integer(default=144, bounds=(1, None), doc="""
|
127
|
|
Scales the dpi of the matplotlib figure.""")
|
128
|
|
|
129
|
6
|
interactive = param.Boolean(default=False, constant=True, doc="""
|
130
|
|
""")
|
131
|
|
|
132
|
6
|
tight = param.Boolean(default=False, doc="""
|
133
|
|
Automatically adjust the figure size to fit the
|
134
|
|
subplots and other artist elements.""")
|
135
|
|
|
136
|
6
|
_rename = {'object': 'text', 'interactive': None, 'dpi': None, 'tight': None}
|
137
|
|
|
138
|
6
|
_rerender_params = PNG._rerender_params + ['object', 'dpi', 'tight']
|
139
|
|
|
140
|
6
|
@classmethod
|
141
|
1
|
def applies(cls, obj):
|
142
|
6
|
if 'matplotlib' not in sys.modules:
|
143
|
0
|
return False
|
144
|
6
|
from matplotlib.figure import Figure
|
145
|
6
|
is_fig = isinstance(obj, Figure)
|
146
|
6
|
if is_fig and obj.canvas is None:
|
147
|
0
|
raise ValueError('Matplotlib figure has no canvas and '
|
148
|
|
'cannot be rendered.')
|
149
|
6
|
return is_fig
|
150
|
|
|
151
|
6
|
def __init__(self, object=None, **params):
|
152
|
6
|
super(Matplotlib, self).__init__(object, **params)
|
153
|
6
|
self._managers = {}
|
154
|
|
|
155
|
6
|
def _get_widget(self, fig):
|
156
|
0
|
import matplotlib
|
157
|
0
|
old_backend = getattr(matplotlib.backends, 'backend', 'agg')
|
158
|
|
|
159
|
0
|
from ipympl.backend_nbagg import FigureManager, Canvas, is_interactive
|
160
|
0
|
from matplotlib._pylab_helpers import Gcf
|
161
|
|
|
162
|
0
|
matplotlib.use(old_backend)
|
163
|
|
|
164
|
0
|
def closer(event):
|
165
|
0
|
Gcf.destroy(0)
|
166
|
|
|
167
|
0
|
canvas = Canvas(fig)
|
168
|
0
|
fig.patch.set_alpha(0)
|
169
|
0
|
manager = FigureManager(canvas, 0)
|
170
|
|
|
171
|
0
|
if is_interactive():
|
172
|
0
|
fig.canvas.draw_idle()
|
173
|
|
|
174
|
0
|
canvas.mpl_connect('close_event', closer)
|
175
|
0
|
return manager
|
176
|
|
|
177
|
6
|
def _get_model(self, doc, root=None, parent=None, comm=None):
|
178
|
6
|
if not self.interactive:
|
179
|
6
|
return PNG._get_model(self, doc, root, parent, comm)
|
180
|
0
|
self.object.set_dpi(self.dpi)
|
181
|
0
|
manager = self._get_widget(self.object)
|
182
|
0
|
props = self._process_param_change(self._init_properties())
|
183
|
0
|
kwargs = {k: v for k, v in props.items()
|
184
|
|
if k not in self._rerender_params+['interactive']}
|
185
|
0
|
model = self._get_ipywidget(manager.canvas, doc, root, comm,
|
186
|
|
**kwargs)
|
187
|
0
|
if root is None:
|
188
|
0
|
root = model
|
189
|
0
|
self._models[root.ref['id']] = (model, parent)
|
190
|
0
|
self._managers[root.ref['id']] = manager
|
191
|
0
|
return model
|
192
|
|
|
193
|
6
|
def _update(self, ref=None, model=None):
|
194
|
6
|
if not self.interactive:
|
195
|
6
|
model.update(**self._get_properties())
|
196
|
6
|
return
|
197
|
0
|
manager = self._managers[ref]
|
198
|
0
|
if self.object is not manager.canvas.figure:
|
199
|
0
|
self.object.set_dpi(self.dpi)
|
200
|
0
|
self.object.patch.set_alpha(0)
|
201
|
0
|
manager.canvas.figure = self.object
|
202
|
0
|
self.object.set_canvas(manager.canvas)
|
203
|
0
|
event = {'width': manager.canvas._width,
|
204
|
|
'height': manager.canvas._height}
|
205
|
0
|
manager.canvas.handle_resize(event)
|
206
|
0
|
manager.canvas.draw_idle()
|
207
|
|
|
208
|
6
|
def _imgshape(self, data):
|
209
|
|
"""Calculate and return image width,height"""
|
210
|
6
|
w, h = self.object.get_size_inches()
|
211
|
6
|
return int(w*72), int(h*72)
|
212
|
|
|
213
|
6
|
def _img(self):
|
214
|
6
|
self.object.set_dpi(self.dpi)
|
215
|
6
|
b = BytesIO()
|
216
|
|
|
217
|
6
|
if self.tight:
|
218
|
0
|
bbox_inches = 'tight'
|
219
|
|
else:
|
220
|
6
|
bbox_inches = None
|
221
|
|
|
222
|
6
|
self.object.canvas.print_figure(b, bbox_inches=bbox_inches)
|
223
|
6
|
return b.getvalue()
|
224
|
|
|
225
|
|
|
226
|
6
|
class RGGPlot(PNG):
|
227
|
|
"""
|
228
|
|
An RGGPlot pane renders an r2py-based ggplot2 figure to png
|
229
|
|
and wraps the base64-encoded data in a bokeh Div model.
|
230
|
|
"""
|
231
|
|
|
232
|
6
|
height = param.Integer(default=400)
|
233
|
|
|
234
|
6
|
width = param.Integer(default=400)
|
235
|
|
|
236
|
6
|
dpi = param.Integer(default=144, bounds=(1, None))
|
237
|
|
|
238
|
6
|
_rerender_params = PNG._rerender_params + ['object', 'dpi', 'width', 'height']
|
239
|
|
|
240
|
6
|
@classmethod
|
241
|
1
|
def applies(cls, obj):
|
242
|
6
|
return type(obj).__name__ == 'GGPlot' and hasattr(obj, 'r_repr')
|
243
|
|
|
244
|
6
|
def _img(self):
|
245
|
0
|
from rpy2.robjects.lib import grdevices
|
246
|
0
|
from rpy2 import robjects
|
247
|
0
|
with grdevices.render_to_bytesio(grdevices.png,
|
248
|
|
type="cairo-png", width=self.width, height=self.height,
|
249
|
|
res=self.dpi, antialias="subpixel") as b:
|
250
|
0
|
robjects.r("print")(self.object)
|
251
|
0
|
return b.getvalue()
|
252
|
|
|
253
|
|
|
254
|
6
|
class YT(HTML):
|
255
|
|
"""
|
256
|
|
YT panes wrap plottable objects from the YT library.
|
257
|
|
By default, the height and width are calculated by summing all
|
258
|
|
contained plots, but can optionally be specified explicitly to
|
259
|
|
provide additional space.
|
260
|
|
"""
|
261
|
|
|
262
|
6
|
priority = 0.5
|
263
|
|
|
264
|
6
|
@classmethod
|
265
|
1
|
def applies(cls, obj):
|
266
|
6
|
return (getattr(obj, '__module__', '').startswith('yt.') and
|
267
|
|
hasattr(obj, "plots") and
|
268
|
|
hasattr(obj, "_repr_html_"))
|
269
|
|
|
270
|
6
|
def _get_properties(self):
|
271
|
6
|
p = super(YT, self)._get_properties()
|
272
|
6
|
if self.object is None:
|
273
|
6
|
return p
|
274
|
|
|
275
|
0
|
width = height = 0
|
276
|
0
|
if self.width is None or self.height is None:
|
277
|
0
|
for k,v in self.object.plots.items():
|
278
|
0
|
if hasattr(v, "_repr_png_"):
|
279
|
0
|
img = v._repr_png_()
|
280
|
0
|
w,h = PNG._imgshape(img)
|
281
|
0
|
height += h
|
282
|
0
|
width = max(w, width)
|
283
|
|
|
284
|
0
|
if self.width is None: p["width"] = width
|
285
|
0
|
if self.height is None: p["height"] = height
|
286
|
|
|
287
|
0
|
return p
|
288
|
|
|
289
|
|
|
290
|
6
|
class Folium(HTML):
|
291
|
|
"""
|
292
|
|
The Folium pane wraps Folium map components.
|
293
|
|
"""
|
294
|
|
|
295
|
6
|
sizing_mode = param.ObjectSelector(default='stretch_width', objects=[
|
296
|
|
'fixed', 'stretch_width', 'stretch_height', 'stretch_both',
|
297
|
|
'scale_width', 'scale_height', 'scale_both', None])
|
298
|
|
|
299
|
6
|
priority = 0.6
|
300
|
|
|
301
|
6
|
@classmethod
|
302
|
1
|
def applies(cls, obj):
|
303
|
6
|
return (getattr(obj, '__module__', '').startswith('folium.') and
|
304
|
|
hasattr(obj, "_repr_html_"))
|