Skip to content

Commit

Permalink
Merge pull request #343 from martinRenou/text-html
Browse files Browse the repository at this point in the history
Implement image/png repr
  • Loading branch information
SylvainCorlay authored Sep 21, 2021
2 parents 54b2a22 + bbfe34d commit 4faf35f
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 67 deletions.
43 changes: 3 additions & 40 deletions examples/ipympl.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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. "
]
},
{
Expand All @@ -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",
Expand Down Expand Up @@ -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": {},
Expand Down Expand Up @@ -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"
},
Expand All @@ -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": {
Expand Down
141 changes: 117 additions & 24 deletions ipympl/backend_nbagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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 = "<img src='data:image/png;base64,{0}' width={1}/>"
data = data.format(b64encode(buf.getvalue()).decode('utf-8'), width)
data = "<img src='data:image/png;base64,{0}'/>"
data = data.format(b64encode(buf.getvalue()).decode('utf-8'))
display(HTML(data))

@default('toolitems')
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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)
3 changes: 0 additions & 3 deletions js/src/mpl_widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down

0 comments on commit 4faf35f

Please sign in to comment.