Skip to content

Commit

Permalink
Add support for interactive mode on Matplotlib (#1469)
Browse files Browse the repository at this point in the history
* Add support for interactive mode on Matplotlib

* Minor fixes

* Add ipympl

* Fix for pn.ipywidget
  • Loading branch information
philippjfr authored Jul 8, 2020
1 parent f3f6fe0 commit 49c9ecf
Show file tree
Hide file tree
Showing 12 changed files with 106 additions and 24 deletions.
19 changes: 18 additions & 1 deletion examples/reference/panes/Matplotlib.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"#### Parameters:\n",
"\n",
"* **``dpi``** (int, default=144): The dots per inch of the exported png\n",
"* **``interactive``** (boolean, default=False): Whether to use the interactive ipympl backend\n",
"* **``tight``** (bool, default=False): Automatically adjust the figure size to fit the subplots and other artist elements.\n",
"* **``object``** (matplotlib.Figure): The Matplotlib Figure object to display\n",
"\n",
Expand Down Expand Up @@ -111,7 +112,23 @@
"ax.set_zlabel('Z')\n",
"ax.set_zlim(-100, 100)\n",
"\n",
"mpl_pane.object= fig"
"mpl_pane.object = fig"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If you have installed `ipympl` you will also be able to use the `interactive` backend:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.pane.Matplotlib(fig, interactive=True, dpi=72)"
]
},
{
Expand Down
2 changes: 1 addition & 1 deletion panel/io/notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ def ipywidget(obj, **kwargs):
from jupyter_bokeh import BokehModel
from ..pane import panel
model = panel(obj, **kwargs).get_root()
widget = BokehModel(model)
widget = BokehModel(model, combine_events=True)
if hasattr(widget, '_view_count'):
widget._view_count = 0
def view_count_changed(change, current=[model]):
Expand Down
2 changes: 1 addition & 1 deletion panel/pane/ace.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,5 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
self._models[root.ref['id']] = (model, parent)
return model

def _update(self, model):
def _update(self, ref=None, model=None):
model.code = self.object if self.object else ''
4 changes: 2 additions & 2 deletions panel/pane/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def _init_properties(self):
def _update_object(self, ref, doc, root, parent, comm):
old_model = self._models[ref][0]
if self._updates:
self._update(old_model)
self._update(ref, old_model)
else:
new_model = self._get_model(doc, root, parent, comm)
try:
Expand Down Expand Up @@ -196,7 +196,7 @@ def _update_pane(self, *events):
else:
cb()

def _update(self, model):
def _update(self, ref=None, model=None):
"""
If _updates=True this method is used to update an existing
Bokeh model instead of replacing the model entirely. The
Expand Down
2 changes: 1 addition & 1 deletion panel/pane/deckgl.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
self._models[root.ref["id"]] = (model, parent)
return model

def _update(self, model):
def _update(self, ref=None, model=None):
data, properties = self._get_properties(layout=False)
self._update_sources(data, model.data_sources)
properties['data'] = data
Expand Down
21 changes: 12 additions & 9 deletions panel/pane/ipywidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,28 @@ class IPyWidget(PaneBase):
def applies(cls, obj):
return (hasattr(obj, 'traits') and hasattr(obj, 'get_manager_state') and hasattr(obj, 'comm'))

def _get_model(self, doc, root=None, parent=None, comm=None):
if root is None:
return self._get_root(doc, comm)

def _get_ipywidget(self, obj, doc, root, comm, **kwargs):
if isinstance(comm, JupyterComm) and not config.embed:
IPyWidget = _BkIPyWidget
else:
import ipykernel
from ipywidgets_bokeh.widget import IPyWidget
from ipywidgets_bokeh.kernel import BokehKernel
from ipywidgets_bokeh.widget import IPyWidget
if not isinstance(ipykernel.kernelbase.Kernel._instance, BokehKernel):
from ..io.ipywidget import PanelKernel
kernel = PanelKernel(key=root.ref['id'].encode('utf-8'), document=doc)
for w in self.object.widgets.values():
kernel = PanelKernel(document=doc, key=str(id(doc)).encode('utf-8'))
for w in obj.widgets.values():
w.comm.kernel = kernel
w.comm.open()

props = self._process_param_change(self._init_properties())
model = IPyWidget(widget=self.object, **props)
model = IPyWidget(widget=obj, **kwargs)
return model

def _get_model(self, doc, root=None, parent=None, comm=None):
if root is None:
return self.get_root(doc, comm)
kwargs = self._process_param_change(self._init_properties())
model = self._get_ipywidget(self.object, doc, root, comm, **kwargs)
self._models[root.ref['id']] = (model, parent)
return model

Expand Down
2 changes: 1 addition & 1 deletion panel/pane/markup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
self._models[root.ref['id']] = (model, parent)
return model

def _update(self, model):
def _update(self, ref=None, model=None):
model.update(**self._get_properties())


Expand Down
63 changes: 62 additions & 1 deletion panel/pane/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ..io.notebook import push
from ..viewable import Layoutable
from .base import PaneBase
from .ipywidget import IPyWidget
from .markup import HTML
from .image import PNG

Expand Down Expand Up @@ -113,7 +114,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
return model


class Matplotlib(PNG):
class Matplotlib(PNG, IPyWidget):
"""
A Matplotlib pane renders a matplotlib figure to png and wraps the
base64 encoded data in a bokeh Div model. The size of the image in
Expand All @@ -125,6 +126,9 @@ class Matplotlib(PNG):
dpi = param.Integer(default=144, bounds=(1, None), doc="""
Scales the dpi of the matplotlib figure.""")

interactive = param.Boolean(default=False, constant=True, doc="""
""")

tight = param.Boolean(default=False, doc="""
Automatically adjust the figure size to fit the
subplots and other artist elements.""")
Expand All @@ -142,6 +146,63 @@ def applies(cls, obj):
'cannot be rendered.')
return is_fig

def __init__(self, object=None, **params):
super(Matplotlib, self).__init__(object, **params)
self._managers = {}

def _get_widget(self, fig):
import matplotlib
old_backend = getattr(matplotlib.backends, 'backend', 'agg')

from ipympl.backend_nbagg import FigureManager, Canvas, is_interactive
from matplotlib._pylab_helpers import Gcf

matplotlib.use(old_backend)

def closer(event):
Gcf.destroy(0)

canvas = Canvas(fig)
fig.patch.set_alpha(0)
manager = FigureManager(canvas, 0)

if is_interactive():
fig.canvas.draw_idle()

canvas.mpl_connect('close_event', closer)
return manager

def _get_model(self, doc, root=None, parent=None, comm=None):
if not self.interactive:
return PNG._get_model(self, doc, root, parent, comm)
self.object.set_dpi(self.dpi)
manager = self._get_widget(self.object)
props = self._process_param_change(self._init_properties())
kwargs = {k: v for k, v in props.items()
if k not in self._rerender_params+['interactive']}
model = self._get_ipywidget(manager.canvas, doc, root, comm,
**kwargs)
if root is None:
root = model
self._models[root.ref['id']] = (model, parent)
self._managers[root.ref['id']] = manager
return model

def _update(self, ref=None, model=None):
if not self.interactive:
model.update(**self._get_properties())
return
manager = self._managers[ref]
if self.object is not manager.canvas.figure:
self.object.set_dpi(self.dpi)
self.object.patch.set_alpha(0)
manager.canvas.figure = self.object
self.object.set_canvas(manager.canvas)
event = {'width': manager.canvas._width,
'height': manager.canvas._height}
manager.canvas.handle_resize(event)
manager.canvas.draw_idle()

def _imgshape(self, data):
"""Calculate and return image width,height"""
w, h = self.object.get_size_inches()
Expand Down
2 changes: 1 addition & 1 deletion panel/pane/plotly.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
self._models[root.ref['id']] = (model, parent)
return model

def _update(self, model):
def _update(self, ref=None, model=None):
if self.object is None:
model.update(data=[], layout={})
model._render_count += 1
Expand Down
2 changes: 1 addition & 1 deletion panel/pane/vega.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
self._models[root.ref['id']] = (model, parent)
return model

def _update(self, model):
def _update(self, ref=None, model=None):
if self.object is None:
json = None
else:
Expand Down
10 changes: 5 additions & 5 deletions panel/pane/vtk/vtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ def __init__(self, object=None, **params):
super(VTKRenderWindow, self).__init__(object, **params)
if object is not None:
self.color_mappers = self.get_color_mappers()
self._update(model=None)
self._update()

def _get_model(self, doc, root=None, parent=None, comm=None):
VTKSynchronizedPlot = super(VTKRenderWindow, self)._get_model(doc, root=None, parent=None, comm=None)
Expand All @@ -383,7 +383,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
self._models[root.ref['id']] = (model, parent)
return model

def _update(self, model):
def _update(self, ref=None, model=None):
import panel.pane.vtk.synchronizable_serializer as rws
context = rws.SynchronizationContext(id_root=make_globally_unique_id(), debug=self._debug_serializer)
self._scene, self._arrays = self._serialize_ren_win(
Expand Down Expand Up @@ -450,7 +450,7 @@ def _cleanup(self, root):
self._contexts.pop(ref, None)
super(VTKRenderWindowSynchronized, self)._cleanup(root)

def _update(self, model):
def _update(self, ref=None, model=None):
context = self._contexts[model.id]
scene, arrays = self._serialize_ren_win(
self.object,
Expand Down Expand Up @@ -670,7 +670,7 @@ def _process_property_change(self, msg):
msg[k] = int(np.round(v * ori_dim[index] / sub_dim[index]))
return msg

def _update(self, model=None):
def _update(self, ref=None, model=None):
self._volume_data = self._get_volume_data()
if self._volume_data is not None:
self._orginal_dimensions = self._get_object_dimensions()
Expand Down Expand Up @@ -814,7 +814,7 @@ def _get_vtkjs(self):
self._vtkjs = vtkjs
return self._vtkjs

def _update(self, model):
def _update(self, ref=None, model=None):
self._vtkjs = None
vtkjs = self._get_vtkjs()
model.data = base64encode(vtkjs) if vtkjs is not None else vtkjs
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def run(self):
'pytest-cov',
'codecov',
'folium',
'ipympl'
]

extras_require = {
Expand Down

0 comments on commit 49c9ecf

Please sign in to comment.