diff --git a/examples/ipympl.ipynb b/examples/ipympl.ipynb
index 9d80dcc8..35e8c84b 100644
--- a/examples/ipympl.ipynb
+++ b/examples/ipympl.ipynb
@@ -201,7 +201,7 @@
"source": [
"# Interactions with other widgets and layouting\n",
"\n",
- "When you want to embed the figure into a layout of other widgets you should call `plt.ioff()` before creating the figure otherwise code inside of `plt.figure()` will display the canvas automatically and outside of your layout. "
+ "When you want to embed the figure into a layout of other widgets you should call `plt.ioff()` before creating the figure otherwise `plt.figure()` will trigger a display of the canvas automatically and outside of your layout. "
]
},
{
@@ -225,7 +225,6 @@
"# this is default but if this notebook is executed out of order it may have been turned off\n",
"plt.ion()\n",
"\n",
- "\n",
"fig = plt.figure()\n",
"ax = fig.gca()\n",
"ax.imshow(Z)\n",
@@ -268,35 +267,6 @@
")"
]
},
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Fixing the double display with `ipywidgets.Output`\n",
- "\n",
- "Using `plt.ioff` use matplotlib to avoid the double display of the plot. You can also use `ipywidgets.Output` to capture the plot display to prevent this"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "out = widgets.Output()\n",
- "with out:\n",
- " fig = plt.figure()\n",
- "\n",
- "ax = fig.gca()\n",
- "ax.imshow(Z)\n",
- "\n",
- "widgets.AppLayout(\n",
- " center=out,\n",
- " footer=widgets.Button(icon='check'),\n",
- " pane_heights=[0, 6, 1]\n",
- ")"
- ]
- },
{
"cell_type": "markdown",
"metadata": {},
@@ -446,18 +416,11 @@
"display(widgets.VBox([slider, fig.canvas]))\n",
"display(out)"
]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
}
],
"metadata": {
"kernelspec": {
- "display_name": "Python 3",
+ "display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
@@ -471,7 +434,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.7.8"
+ "version": "3.9.7"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
diff --git a/ipympl/backend_nbagg.py b/ipympl/backend_nbagg.py
index a9fa0777..1c955cb2 100644
--- a/ipympl/backend_nbagg.py
+++ b/ipympl/backend_nbagg.py
@@ -5,16 +5,17 @@
import io
from IPython.display import display, HTML
+from IPython import get_ipython
+from IPython import version_info as ipython_version_info
from ipywidgets import DOMWidget, widget_serialization
from traitlets import (
- Unicode, Bool, CInt, Float, List, Instance, CaselessStrEnum, Enum,
+ Unicode, Bool, CInt, List, Instance, CaselessStrEnum, Enum,
default
)
import matplotlib
-from matplotlib import rcParams
-from matplotlib import is_interactive
+from matplotlib import rcParams, is_interactive
from matplotlib.backends.backend_webagg_core import (FigureManagerWebAgg,
FigureCanvasWebAggCore,
NavigationToolbar2WebAgg,
@@ -40,7 +41,6 @@ def connection_info():
use.
"""
- from matplotlib._pylab_helpers import Gcf
result = []
for manager in Gcf.get_all_fig_managers():
fig = manager.canvas.figure
@@ -83,16 +83,8 @@ def __init__(self, canvas, *args, **kwargs):
def export(self):
buf = io.BytesIO()
self.canvas.figure.savefig(buf, format='png', dpi='figure')
- # Figure width in pixels
- pwidth = (self.canvas.figure.get_figwidth() *
- self.canvas.figure.get_dpi())
- # Scale size to match widget on HiDPI monitors.
- if hasattr(self.canvas, 'device_pixel_ratio'): # Matplotlib 3.5+
- width = pwidth / self.canvas.device_pixel_ratio
- else:
- width = pwidth / self.canvas._dpi_ratio
- data = "
"
- data = data.format(b64encode(buf.getvalue()).decode('utf-8'), width)
+ data = "
"
+ data = data.format(b64encode(buf.getvalue()).decode('utf-8'))
display(HTML(data))
@default('toolitems')
@@ -160,7 +152,9 @@ class Canvas(DOMWidget, FigureCanvasWebAggCore):
_png_is_old = Bool()
_force_full = Bool()
_current_image_mode = Unicode()
- _dpi_ratio = Float(1.0)
+
+ # Static as it should be the same for all canvases
+ current_dpi_ratio = 1.0
def __init__(self, figure, *args, **kwargs):
DOMWidget.__init__(self, *args, **kwargs)
@@ -172,9 +166,15 @@ def _handle_message(self, object, content, buffers):
# Every content has a "type".
if content['type'] == 'closing':
self._closed = True
+
elif content['type'] == 'initialized':
_, _, w, h = self.figure.bbox.bounds
self.manager.resize(w, h)
+
+ elif content['type'] == 'set_dpi_ratio':
+ Canvas.current_dpi_ratio = content['dpi_ratio']
+ self.manager.handle_json(content)
+
else:
self.manager.handle_json(content)
@@ -208,6 +208,41 @@ def send_binary(self, data):
def new_timer(self, *args, **kwargs):
return TimerTornado(*args, **kwargs)
+ def _repr_mimebundle_(self, **kwargs):
+ # now happens before the actual display call.
+ if hasattr(self, '_handle_displayed'):
+ self._handle_displayed(**kwargs)
+ plaintext = repr(self)
+ if len(plaintext) > 110:
+ plaintext = plaintext[:110] + '…'
+
+ buf = io.BytesIO()
+ self.figure.savefig(buf, format='png', dpi='figure')
+ data_url = b64encode(buf.getvalue()).decode('utf-8')
+
+ data = {
+ 'text/plain': plaintext,
+ 'image/png': data_url,
+ 'application/vnd.jupyter.widget-view+json': {
+ 'version_major': 2,
+ 'version_minor': 0,
+ 'model_id': self._model_id
+ }
+ }
+
+ return data
+
+ def _ipython_display_(self, **kwargs):
+ """Called when `IPython.display.display` is called on a widget.
+ Note: if we are in IPython 6.1 or later, we return NotImplemented so
+ that _repr_mimebundle_ is used directly.
+ """
+ if ipython_version_info >= (6, 1):
+ raise NotImplementedError
+
+ data = self._repr_mimebundle_(**kwargs)
+ display(data, raw=True)
+
if matplotlib.__version__ < '3.4':
# backport the Python side changes to match the js changes
def _handle_key(self, event):
@@ -294,14 +329,18 @@ class _Backend_ipympl(_Backend):
FigureCanvas = Canvas
FigureManager = FigureManager
+ _to_show = []
+ _draw_called = False
+
@staticmethod
def new_figure_manager_given_figure(num, figure):
canvas = Canvas(figure)
if 'nbagg.transparent' in rcParams and rcParams['nbagg.transparent']:
figure.patch.set_alpha(0)
manager = FigureManager(canvas, num)
+
if is_interactive():
- manager.show()
+ _Backend_ipympl._to_show.append(figure)
figure.canvas.draw_idle()
def destroy(event):
@@ -312,17 +351,17 @@ def destroy(event):
return manager
@staticmethod
- def show(block=None):
- # TODO: something to do when keyword block==False ?
+ def show(close=None, block=None):
+ # # TODO: something to do when keyword block==False ?
+ interactive = is_interactive()
- managers = Gcf.get_all_fig_managers()
- if not managers:
+ manager = Gcf.get_active()
+ if manager is None:
return
- interactive = is_interactive()
-
- for manager in managers:
- manager.show()
+ try:
+ display(manager.canvas)
+ # metadata=_fetch_figure_metadata(manager.canvas.figure)
# plt.figure adds an event which makes the figure in focus the
# active one. Disable this behaviour, as it results in
@@ -333,3 +372,57 @@ def show(block=None):
if not interactive:
Gcf.figs.pop(manager.num, None)
+ finally:
+ if manager.canvas.figure in _Backend_ipympl._to_show:
+ _Backend_ipympl._to_show.remove(manager.canvas.figure)
+
+ @staticmethod
+ def draw_if_interactive():
+ # If matplotlib was manually set to non-interactive mode, this function
+ # should be a no-op (otherwise we'll generate duplicate plots, since a
+ # user who set ioff() manually expects to make separate draw/show
+ # calls).
+ if not is_interactive():
+ return
+
+ manager = Gcf.get_active()
+ if manager is None:
+ return
+ fig = manager.canvas.figure
+
+ # ensure current figure will be drawn, and each subsequent call
+ # of draw_if_interactive() moves the active figure to ensure it is
+ # drawn last
+ try:
+ _Backend_ipympl._to_show.remove(fig)
+ except ValueError:
+ # ensure it only appears in the draw list once
+ pass
+ # Queue up the figure for drawing in next show() call
+ _Backend_ipympl._to_show.append(fig)
+ _Backend_ipympl._draw_called = True
+
+
+def flush_figures():
+ if rcParams['backend'] == 'module://ipympl.backend_nbagg':
+ if not _Backend_ipympl._draw_called:
+ return
+
+ try:
+ # exclude any figures that were closed:
+ active = set([
+ fm.canvas.figure for fm in Gcf.get_all_fig_managers()
+ ])
+
+ for fig in [
+ fig for fig in _Backend_ipympl._to_show if fig in active]:
+ # display(fig.canvas, metadata=_fetch_figure_metadata(fig))
+ display(fig.canvas)
+ finally:
+ # clear flags for next round
+ _Backend_ipympl._to_show = []
+ _Backend_ipympl._draw_called = False
+
+
+ip = get_ipython()
+ip.events.register('post_execute', flush_figures)
diff --git a/js/src/mpl_widget.js b/js/src/mpl_widget.js
index 4e1acca7..d58f5427 100644
--- a/js/src/mpl_widget.js
+++ b/js/src/mpl_widget.js
@@ -185,9 +185,6 @@ export class MPLCanvasModel extends widgets.DOMWidgetModel {
this.image.src = image_url;
- // Tell Jupyter that the notebook contents must change.
- this.send_message('ack');
-
this.waiting = false;
}