Skip to content

Commit 4faf35f

Browse files
Merge pull request #343 from martinRenou/text-html
Implement image/png repr
2 parents 54b2a22 + bbfe34d commit 4faf35f

File tree

3 files changed

+120
-67
lines changed

3 files changed

+120
-67
lines changed

examples/ipympl.ipynb

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@
201201
"source": [
202202
"# Interactions with other widgets and layouting\n",
203203
"\n",
204-
"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. "
204+
"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. "
205205
]
206206
},
207207
{
@@ -225,7 +225,6 @@
225225
"# this is default but if this notebook is executed out of order it may have been turned off\n",
226226
"plt.ion()\n",
227227
"\n",
228-
"\n",
229228
"fig = plt.figure()\n",
230229
"ax = fig.gca()\n",
231230
"ax.imshow(Z)\n",
@@ -268,35 +267,6 @@
268267
")"
269268
]
270269
},
271-
{
272-
"cell_type": "markdown",
273-
"metadata": {},
274-
"source": [
275-
"### Fixing the double display with `ipywidgets.Output`\n",
276-
"\n",
277-
"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"
278-
]
279-
},
280-
{
281-
"cell_type": "code",
282-
"execution_count": null,
283-
"metadata": {},
284-
"outputs": [],
285-
"source": [
286-
"out = widgets.Output()\n",
287-
"with out:\n",
288-
" fig = plt.figure()\n",
289-
"\n",
290-
"ax = fig.gca()\n",
291-
"ax.imshow(Z)\n",
292-
"\n",
293-
"widgets.AppLayout(\n",
294-
" center=out,\n",
295-
" footer=widgets.Button(icon='check'),\n",
296-
" pane_heights=[0, 6, 1]\n",
297-
")"
298-
]
299-
},
300270
{
301271
"cell_type": "markdown",
302272
"metadata": {},
@@ -446,18 +416,11 @@
446416
"display(widgets.VBox([slider, fig.canvas]))\n",
447417
"display(out)"
448418
]
449-
},
450-
{
451-
"cell_type": "code",
452-
"execution_count": null,
453-
"metadata": {},
454-
"outputs": [],
455-
"source": []
456419
}
457420
],
458421
"metadata": {
459422
"kernelspec": {
460-
"display_name": "Python 3",
423+
"display_name": "Python 3 (ipykernel)",
461424
"language": "python",
462425
"name": "python3"
463426
},
@@ -471,7 +434,7 @@
471434
"name": "python",
472435
"nbconvert_exporter": "python",
473436
"pygments_lexer": "ipython3",
474-
"version": "3.7.8"
437+
"version": "3.9.7"
475438
},
476439
"widgets": {
477440
"application/vnd.jupyter.widget-state+json": {

ipympl/backend_nbagg.py

Lines changed: 117 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@
55
import io
66

77
from IPython.display import display, HTML
8+
from IPython import get_ipython
9+
from IPython import version_info as ipython_version_info
810

911
from ipywidgets import DOMWidget, widget_serialization
1012
from traitlets import (
11-
Unicode, Bool, CInt, Float, List, Instance, CaselessStrEnum, Enum,
13+
Unicode, Bool, CInt, List, Instance, CaselessStrEnum, Enum,
1214
default
1315
)
1416

1517
import matplotlib
16-
from matplotlib import rcParams
17-
from matplotlib import is_interactive
18+
from matplotlib import rcParams, is_interactive
1819
from matplotlib.backends.backend_webagg_core import (FigureManagerWebAgg,
1920
FigureCanvasWebAggCore,
2021
NavigationToolbar2WebAgg,
@@ -40,7 +41,6 @@ def connection_info():
4041
use.
4142
4243
"""
43-
from matplotlib._pylab_helpers import Gcf
4444
result = []
4545
for manager in Gcf.get_all_fig_managers():
4646
fig = manager.canvas.figure
@@ -83,16 +83,8 @@ def __init__(self, canvas, *args, **kwargs):
8383
def export(self):
8484
buf = io.BytesIO()
8585
self.canvas.figure.savefig(buf, format='png', dpi='figure')
86-
# Figure width in pixels
87-
pwidth = (self.canvas.figure.get_figwidth() *
88-
self.canvas.figure.get_dpi())
89-
# Scale size to match widget on HiDPI monitors.
90-
if hasattr(self.canvas, 'device_pixel_ratio'): # Matplotlib 3.5+
91-
width = pwidth / self.canvas.device_pixel_ratio
92-
else:
93-
width = pwidth / self.canvas._dpi_ratio
94-
data = "<img src='data:image/png;base64,{0}' width={1}/>"
95-
data = data.format(b64encode(buf.getvalue()).decode('utf-8'), width)
86+
data = "<img src='data:image/png;base64,{0}'/>"
87+
data = data.format(b64encode(buf.getvalue()).decode('utf-8'))
9688
display(HTML(data))
9789

9890
@default('toolitems')
@@ -160,7 +152,9 @@ class Canvas(DOMWidget, FigureCanvasWebAggCore):
160152
_png_is_old = Bool()
161153
_force_full = Bool()
162154
_current_image_mode = Unicode()
163-
_dpi_ratio = Float(1.0)
155+
156+
# Static as it should be the same for all canvases
157+
current_dpi_ratio = 1.0
164158

165159
def __init__(self, figure, *args, **kwargs):
166160
DOMWidget.__init__(self, *args, **kwargs)
@@ -172,9 +166,15 @@ def _handle_message(self, object, content, buffers):
172166
# Every content has a "type".
173167
if content['type'] == 'closing':
174168
self._closed = True
169+
175170
elif content['type'] == 'initialized':
176171
_, _, w, h = self.figure.bbox.bounds
177172
self.manager.resize(w, h)
173+
174+
elif content['type'] == 'set_dpi_ratio':
175+
Canvas.current_dpi_ratio = content['dpi_ratio']
176+
self.manager.handle_json(content)
177+
178178
else:
179179
self.manager.handle_json(content)
180180

@@ -208,6 +208,41 @@ def send_binary(self, data):
208208
def new_timer(self, *args, **kwargs):
209209
return TimerTornado(*args, **kwargs)
210210

211+
def _repr_mimebundle_(self, **kwargs):
212+
# now happens before the actual display call.
213+
if hasattr(self, '_handle_displayed'):
214+
self._handle_displayed(**kwargs)
215+
plaintext = repr(self)
216+
if len(plaintext) > 110:
217+
plaintext = plaintext[:110] + '…'
218+
219+
buf = io.BytesIO()
220+
self.figure.savefig(buf, format='png', dpi='figure')
221+
data_url = b64encode(buf.getvalue()).decode('utf-8')
222+
223+
data = {
224+
'text/plain': plaintext,
225+
'image/png': data_url,
226+
'application/vnd.jupyter.widget-view+json': {
227+
'version_major': 2,
228+
'version_minor': 0,
229+
'model_id': self._model_id
230+
}
231+
}
232+
233+
return data
234+
235+
def _ipython_display_(self, **kwargs):
236+
"""Called when `IPython.display.display` is called on a widget.
237+
Note: if we are in IPython 6.1 or later, we return NotImplemented so
238+
that _repr_mimebundle_ is used directly.
239+
"""
240+
if ipython_version_info >= (6, 1):
241+
raise NotImplementedError
242+
243+
data = self._repr_mimebundle_(**kwargs)
244+
display(data, raw=True)
245+
211246
if matplotlib.__version__ < '3.4':
212247
# backport the Python side changes to match the js changes
213248
def _handle_key(self, event):
@@ -294,14 +329,18 @@ class _Backend_ipympl(_Backend):
294329
FigureCanvas = Canvas
295330
FigureManager = FigureManager
296331

332+
_to_show = []
333+
_draw_called = False
334+
297335
@staticmethod
298336
def new_figure_manager_given_figure(num, figure):
299337
canvas = Canvas(figure)
300338
if 'nbagg.transparent' in rcParams and rcParams['nbagg.transparent']:
301339
figure.patch.set_alpha(0)
302340
manager = FigureManager(canvas, num)
341+
303342
if is_interactive():
304-
manager.show()
343+
_Backend_ipympl._to_show.append(figure)
305344
figure.canvas.draw_idle()
306345

307346
def destroy(event):
@@ -312,17 +351,17 @@ def destroy(event):
312351
return manager
313352

314353
@staticmethod
315-
def show(block=None):
316-
# TODO: something to do when keyword block==False ?
354+
def show(close=None, block=None):
355+
# # TODO: something to do when keyword block==False ?
356+
interactive = is_interactive()
317357

318-
managers = Gcf.get_all_fig_managers()
319-
if not managers:
358+
manager = Gcf.get_active()
359+
if manager is None:
320360
return
321361

322-
interactive = is_interactive()
323-
324-
for manager in managers:
325-
manager.show()
362+
try:
363+
display(manager.canvas)
364+
# metadata=_fetch_figure_metadata(manager.canvas.figure)
326365

327366
# plt.figure adds an event which makes the figure in focus the
328367
# active one. Disable this behaviour, as it results in
@@ -333,3 +372,57 @@ def show(block=None):
333372

334373
if not interactive:
335374
Gcf.figs.pop(manager.num, None)
375+
finally:
376+
if manager.canvas.figure in _Backend_ipympl._to_show:
377+
_Backend_ipympl._to_show.remove(manager.canvas.figure)
378+
379+
@staticmethod
380+
def draw_if_interactive():
381+
# If matplotlib was manually set to non-interactive mode, this function
382+
# should be a no-op (otherwise we'll generate duplicate plots, since a
383+
# user who set ioff() manually expects to make separate draw/show
384+
# calls).
385+
if not is_interactive():
386+
return
387+
388+
manager = Gcf.get_active()
389+
if manager is None:
390+
return
391+
fig = manager.canvas.figure
392+
393+
# ensure current figure will be drawn, and each subsequent call
394+
# of draw_if_interactive() moves the active figure to ensure it is
395+
# drawn last
396+
try:
397+
_Backend_ipympl._to_show.remove(fig)
398+
except ValueError:
399+
# ensure it only appears in the draw list once
400+
pass
401+
# Queue up the figure for drawing in next show() call
402+
_Backend_ipympl._to_show.append(fig)
403+
_Backend_ipympl._draw_called = True
404+
405+
406+
def flush_figures():
407+
if rcParams['backend'] == 'module://ipympl.backend_nbagg':
408+
if not _Backend_ipympl._draw_called:
409+
return
410+
411+
try:
412+
# exclude any figures that were closed:
413+
active = set([
414+
fm.canvas.figure for fm in Gcf.get_all_fig_managers()
415+
])
416+
417+
for fig in [
418+
fig for fig in _Backend_ipympl._to_show if fig in active]:
419+
# display(fig.canvas, metadata=_fetch_figure_metadata(fig))
420+
display(fig.canvas)
421+
finally:
422+
# clear flags for next round
423+
_Backend_ipympl._to_show = []
424+
_Backend_ipympl._draw_called = False
425+
426+
427+
ip = get_ipython()
428+
ip.events.register('post_execute', flush_figures)

js/src/mpl_widget.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,6 @@ export class MPLCanvasModel extends widgets.DOMWidgetModel {
185185

186186
this.image.src = image_url;
187187

188-
// Tell Jupyter that the notebook contents must change.
189-
this.send_message('ack');
190-
191188
this.waiting = false;
192189
}
193190

0 commit comments

Comments
 (0)