Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for interactive mode on Matplotlib #1469

Merged
merged 4 commits into from
Jul 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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