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

Implement image/png repr #343

Merged
merged 12 commits into from
Sep 21, 2021
65 changes: 53 additions & 12 deletions ipympl/backend_nbagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import json
import io

from IPython.display import display, HTML
from IPython.display import update_display, display, HTML
from IPython import version_info as ipython_version_info

from ipywidgets import DOMWidget, widget_serialization
from traitlets import (
Expand Down Expand Up @@ -83,16 +84,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)
Comment on lines -86 to -95
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martinRenou why is it ok to remove this dpi scaling? It seems like we will now have imperfect behavior on hi dpi displays.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might have been a mistake, we should open an issue to track this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do it, actually. If I understand correctly, the PNG doesn't get saved at high res.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: we should not determine the dpi based on the browser because it could differ between clients.
It should be a fixed value.

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 @@ -168,13 +161,20 @@ def __init__(self, figure, *args, **kwargs):

self.on_msg(self._handle_message)

self._rendered = False

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'] == 'ack':
update_display(
self._repr_mimebundle_(),
raw=True, display_id='matplotlib_{0}'.format(self._model_id)
)
else:
self.manager.handle_json(content)

Expand Down Expand Up @@ -208,6 +208,44 @@ 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')

martinRenou marked this conversation as resolved.
Show resolved Hide resolved
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

display(
self._repr_mimebundle_(**kwargs),
raw=True, display_id='matplotlib_{0}'.format(self._model_id)
)

if matplotlib.__version__ < '3.4':
# backport the Python side changes to match the js changes
def _handle_key(self, event):
Expand Down Expand Up @@ -281,7 +319,10 @@ def __init__(self, canvas, num):
def show(self):
if self.canvas._closed:
self.canvas._closed = False
display(self.canvas)
display(
self.canvas,
display_id='matplotlib_{0}'.format(self.canvas._model_id)
)
else:
self.canvas.draw_idle()

Expand Down
8 changes: 7 additions & 1 deletion js/src/mpl_widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export class MPLCanvasModel extends widgets.DOMWidgetModel {
this.ratio = (window.devicePixelRatio || 1) / backingStore;
this._init_image();

this.acknowledged_rendered = false;

this.on('msg:custom', this.on_comm_message.bind(this));
this.on('change:resizable', () => {
this._for_each_view(function (view) {
Expand Down Expand Up @@ -186,7 +188,11 @@ export class MPLCanvasModel extends widgets.DOMWidgetModel {
this.image.src = image_url;

// Tell Jupyter that the notebook contents must change.
this.send_message('ack');
if (!this.acknowledged_rendered) {
this.send_message('ack');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a round-trip to the front-end??

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we were trying to update the mime bundle with an "update display" message, but honnestly I don't remmember.

We should probably try to keep the PR as simple as possible for now...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried implementing it without the round-trip but I wasn't able. I need to look deeper into that.


this.acknowledged_rendered = true;
}

this.waiting = false;
}
Expand Down