Skip to content

Commit 9c100c9

Browse files
committed
Merge pull request #5754 from blink1073/ipython-widget
ENH: IPython Widget based nbagg
2 parents 6bed1be + 1ec611a commit 9c100c9

File tree

11 files changed

+370
-367
lines changed

11 files changed

+370
-367
lines changed

lib/matplotlib/__init__.py

+11
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,17 @@ def tk_window_focus():
14541454
except (KeyError, ValueError):
14551455
pass
14561456

1457+
1458+
# Jupyter extension paths
1459+
def _jupyter_nbextension_paths():
1460+
return [{
1461+
'section': 'notebook',
1462+
'src': 'backends/web_backend/js',
1463+
'dest': 'matplotlib',
1464+
'require': 'matplotlib/extension'
1465+
}]
1466+
1467+
14571468
default_test_modules = [
14581469
'matplotlib.tests.test_agg',
14591470
'matplotlib.tests.test_animation',

lib/matplotlib/backend_bases.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -2161,7 +2161,8 @@ def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None,
21612161
origfacecolor = self.figure.get_facecolor()
21622162
origedgecolor = self.figure.get_edgecolor()
21632163

2164-
self.figure.dpi = dpi
2164+
if dpi != 'figure':
2165+
self.figure.dpi = dpi
21652166
self.figure.set_facecolor(facecolor)
21662167
self.figure.set_edgecolor(edgecolor)
21672168

lib/matplotlib/backends/backend_nbagg.py

+139-148
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@
33
# lib/matplotlib/backends/web_backend/nbagg_uat.ipynb to help verify
44
# that changes made maintain expected behaviour.
55

6-
import datetime
76
from base64 import b64encode
87
import json
98
import io
9+
from tempfile import mkdtemp
10+
import shutil
1011
import os
1112
from matplotlib.externals import six
1213
from uuid import uuid4 as uuid
1314

14-
import tornado.ioloop
15-
16-
from IPython.display import display, Javascript, HTML
15+
from IPython.display import display, HTML
16+
from IPython import version_info
1717
try:
1818
# Jupyter/IPython 4.x or later
19-
from ipykernel.comm import Comm
19+
from ipywidgets import DOMWidget
20+
from traitlets import Unicode, Bool, Float, List, Any
21+
from notebook.nbextensions import install_nbextension, check_nbextension
2022
except ImportError:
2123
# Jupyter/IPython 3.x or earlier
22-
from IPython.kernel.comm import Comm
24+
from IPython.html.widgets import DOMWidget
25+
from IPython.utils.traitlets import Unicode, Bool, Float, List, Any
26+
from IPython.html.nbextensions import install_nbextension
2327

2428
from matplotlib import rcParams
2529
from matplotlib.figure import Figure
@@ -33,6 +37,7 @@
3337

3438

3539
class Show(ShowBase):
40+
3641
def __call__(self, block=None):
3742
from matplotlib._pylab_helpers import Gcf
3843

@@ -98,6 +103,7 @@ def connection_info():
98103
'zoom_to_rect': 'fa fa-square-o icon-check-empty',
99104
'move': 'fa fa-arrows icon-move',
100105
'download': 'fa fa-floppy-o icon-save',
106+
'export': 'fa fa-file-picture-o icon-picture',
101107
None: None
102108
}
103109

@@ -109,84 +115,74 @@ class NavigationIPy(NavigationToolbar2WebAgg):
109115
_FONT_AWESOME_CLASSES[image_file], name_of_method)
110116
for text, tooltip_text, image_file, name_of_method
111117
in (NavigationToolbar2.toolitems +
112-
(('Download', 'Download plot', 'download', 'download'),))
118+
(('Download', 'Download plot', 'download', 'download'),
119+
('Export', 'Export plot', 'export', 'export')))
113120
if image_file in _FONT_AWESOME_CLASSES]
114121

122+
def export(self):
123+
buf = io.BytesIO()
124+
self.canvas.figure.savefig(buf, format='png', dpi='figure')
125+
data = "<img src='data:image/png;base64,{0}'/>"
126+
data = data.format(b64encode(buf.getvalue()).decode('utf-8'))
127+
display(HTML(data))
128+
129+
130+
class FigureCanvasNbAgg(DOMWidget, FigureCanvasWebAggCore):
131+
_view_module = Unicode("matplotlib", sync=True)
132+
_view_name = Unicode('MPLCanvasView', sync=True)
133+
_toolbar_items = List(sync=True)
134+
_closed = Bool(True)
135+
_id = Unicode('', sync=True)
136+
137+
# Must declare the superclass private members.
138+
_png_is_old = Bool()
139+
_force_full = Bool()
140+
_current_image_mode = Unicode()
141+
_dpi_ratio = Float(1.0)
142+
_is_idle_drawing = Bool()
143+
_is_saving = Bool()
144+
_button = Any()
145+
_key = Any()
146+
_lastx = Any()
147+
_lasty = Any()
148+
_is_idle_drawing = Bool()
149+
150+
def __init__(self, figure, *args, **kwargs):
151+
super(FigureCanvasWebAggCore, self).__init__(figure, *args, **kwargs)
152+
super(DOMWidget, self).__init__(*args, **kwargs)
153+
self._uid = uuid().hex
154+
self.on_msg(self._handle_message)
155+
156+
def _handle_message(self, object, message, buffers):
157+
# The 'supports_binary' message is relevant to the
158+
# websocket itself. The other messages get passed along
159+
# to matplotlib as-is.
115160

116-
class FigureManagerNbAgg(FigureManagerWebAgg):
117-
ToolbarCls = NavigationIPy
118-
119-
def __init__(self, canvas, num):
120-
self._shown = False
121-
FigureManagerWebAgg.__init__(self, canvas, num)
122-
123-
def display_js(self):
124-
# XXX How to do this just once? It has to deal with multiple
125-
# browser instances using the same kernel (require.js - but the
126-
# file isn't static?).
127-
display(Javascript(FigureManagerNbAgg.get_javascript()))
128-
129-
def show(self):
130-
if not self._shown:
131-
self.display_js()
132-
self._create_comm()
133-
else:
134-
self.canvas.draw_idle()
135-
self._shown = True
136-
137-
def reshow(self):
138-
"""
139-
A special method to re-show the figure in the notebook.
140-
141-
"""
142-
self._shown = False
143-
self.show()
144-
145-
@property
146-
def connected(self):
147-
return bool(self.web_sockets)
148-
149-
@classmethod
150-
def get_javascript(cls, stream=None):
151-
if stream is None:
152-
output = io.StringIO()
161+
# Every message has a "type" and a "figure_id".
162+
message = json.loads(message)
163+
if message['type'] == 'closing':
164+
self._closed = True
165+
elif message['type'] == 'supports_binary':
166+
self.supports_binary = message['value']
167+
elif message['type'] == 'initialized':
168+
_, _, w, h = self.figure.bbox.bounds
169+
self.manager.resize(w, h)
170+
self.send_json('refresh')
153171
else:
154-
output = stream
155-
super(FigureManagerNbAgg, cls).get_javascript(stream=output)
156-
with io.open(os.path.join(
157-
os.path.dirname(__file__),
158-
"web_backend",
159-
"nbagg_mpl.js"), encoding='utf8') as fd:
160-
output.write(fd.read())
161-
if stream is None:
162-
return output.getvalue()
163-
164-
def _create_comm(self):
165-
comm = CommSocket(self)
166-
self.add_web_socket(comm)
167-
return comm
168-
169-
def destroy(self):
170-
self._send_event('close')
171-
# need to copy comms as callbacks will modify this list
172-
for comm in list(self.web_sockets):
173-
comm.on_close()
174-
self.clearup_closed()
175-
176-
def clearup_closed(self):
177-
"""Clear up any closed Comms."""
178-
self.web_sockets = set([socket for socket in self.web_sockets
179-
if socket.is_open()])
180-
181-
if len(self.web_sockets) == 0:
182-
self.canvas.close_event()
172+
self.manager.handle_json(message)
183173

184-
def remove_comm(self, comm_id):
185-
self.web_sockets = set([socket for socket in self.web_sockets
186-
if not socket.comm.comm_id == comm_id])
174+
def send_json(self, content):
175+
self.send({'data': json.dumps(content)})
187176

177+
def send_binary(self, blob):
178+
# The comm is ascii, so we always send the image in base64
179+
# encoded data URL form.
180+
data = b64encode(blob)
181+
if six.PY3:
182+
data = data.decode('ascii')
183+
data_uri = "data:image/png;base64,{0}".format(data)
184+
self.send({'data': data_uri})
188185

189-
class FigureCanvasNbAgg(FigureCanvasWebAggCore):
190186
def new_timer(self, *args, **kwargs):
191187
return TimerTornado(*args, **kwargs)
192188

@@ -197,6 +193,31 @@ def stop_event_loop(self):
197193
FigureCanvasBase.stop_event_loop_default(self)
198194

199195

196+
class FigureManagerNbAgg(FigureManagerWebAgg):
197+
ToolbarCls = NavigationIPy
198+
199+
def __init__(self, canvas, num):
200+
FigureManagerWebAgg.__init__(self, canvas, num)
201+
toolitems = []
202+
for name, tooltip, image, method in self.ToolbarCls.toolitems:
203+
if name is None:
204+
toolitems.append(['', '', '', ''])
205+
else:
206+
toolitems.append([name, tooltip, image, method])
207+
canvas._toolbar_items = toolitems
208+
self.web_sockets = [self.canvas]
209+
210+
def show(self):
211+
if self.canvas._closed:
212+
self.canvas._closed = False
213+
display(self.canvas)
214+
else:
215+
self.canvas.draw_idle()
216+
217+
def destroy(self):
218+
self._send_event('close')
219+
220+
200221
def new_figure_manager(num, *args, **kwargs):
201222
"""
202223
Create a new figure manager instance
@@ -229,76 +250,46 @@ def closer(event):
229250
return manager
230251

231252

232-
class CommSocket(object):
253+
def nbinstall(overwrite=False, user=True):
233254
"""
234-
Manages the Comm connection between IPython and the browser (client).
235-
236-
Comms are 2 way, with the CommSocket being able to publish a message
237-
via the send_json method, and handle a message with on_message. On the
238-
JS side figure.send_message and figure.ws.onmessage do the sending and
239-
receiving respectively.
240-
255+
Copies javascript dependencies to the '/nbextensions' folder in
256+
your IPython directory.
257+
258+
Parameters
259+
----------
260+
261+
overwrite : bool
262+
If True, always install the files, regardless of what mayœ already be
263+
installed. Defaults to False.
264+
user : bool
265+
Whether to install to the user's .ipython/nbextensions directory.
266+
Otherwise do a system-wide install
267+
(e.g. /usr/local/share/jupyter/nbextensions). Defaults to False.
241268
"""
242-
def __init__(self, manager):
243-
self.supports_binary = None
244-
self.manager = manager
245-
self.uuid = str(uuid())
246-
# Publish an output area with a unique ID. The javascript can then
247-
# hook into this area.
248-
display(HTML("<div id=%r></div>" % self.uuid))
249-
try:
250-
self.comm = Comm('matplotlib', data={'id': self.uuid})
251-
except AttributeError:
252-
raise RuntimeError('Unable to create an IPython notebook Comm '
253-
'instance. Are you in the IPython notebook?')
254-
self.comm.on_msg(self.on_message)
255-
256-
manager = self.manager
257-
self._ext_close = False
258-
259-
def _on_close(close_message):
260-
self._ext_close = True
261-
manager.remove_comm(close_message['content']['comm_id'])
262-
manager.clearup_closed()
263-
264-
self.comm.on_close(_on_close)
265-
266-
def is_open(self):
267-
return not (self._ext_close or self.comm._closed)
268-
269-
def on_close(self):
270-
# When the socket is closed, deregister the websocket with
271-
# the FigureManager.
272-
if self.is_open():
273-
try:
274-
self.comm.close()
275-
except KeyError:
276-
# apparently already cleaned it up?
277-
pass
278-
279-
def send_json(self, content):
280-
self.comm.send({'data': json.dumps(content)})
281-
282-
def send_binary(self, blob):
283-
# The comm is ascii, so we always send the image in base64
284-
# encoded data URL form.
285-
data = b64encode(blob)
286-
if six.PY3:
287-
data = data.decode('ascii')
288-
data_uri = "data:image/png;base64,{0}".format(data)
289-
self.comm.send({'data': data_uri})
290-
291-
def on_message(self, message):
292-
# The 'supports_binary' message is relevant to the
293-
# websocket itself. The other messages get passed along
294-
# to matplotlib as-is.
295-
296-
# Every message has a "type" and a "figure_id".
297-
message = json.loads(message['content']['data'])
298-
if message['type'] == 'closing':
299-
self.on_close()
300-
self.manager.clearup_closed()
301-
elif message['type'] == 'supports_binary':
302-
self.supports_binary = message['value']
303-
else:
304-
self.manager.handle_json(message)
269+
if (check_nbextension('matplotlib') or
270+
check_nbextension('matplotlib', True)):
271+
return
272+
273+
# Make a temporary directory so we can wrap mpl.js in a requirejs define().
274+
tempdir = mkdtemp()
275+
path = os.path.join(os.path.dirname(__file__), "web_backend")
276+
shutil.copy2(os.path.join(path, "nbagg_mpl.js"), tempdir)
277+
278+
with open(os.path.join(path, 'mpl.js')) as fid:
279+
contents = fid.read()
280+
281+
with open(os.path.join(tempdir, 'mpl.js'), 'w') as fid:
282+
fid.write('define(["jquery"], function($) {\n')
283+
fid.write(contents)
284+
fid.write('\nreturn mpl;\n});')
285+
286+
install_nbextension(
287+
tempdir,
288+
overwrite=overwrite,
289+
symlink=False,
290+
destination='matplotlib',
291+
verbose=0,
292+
**({'user': user} if version_info >= (3, 0, 0, '') else {})
293+
)
294+
295+
#nbinstall()

lib/matplotlib/backends/backend_webagg_core.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import io
1919
import json
2020
import os
21-
import time
21+
import datetime
2222
import warnings
2323

2424
import numpy as np
@@ -501,6 +501,7 @@ def get_javascript(cls, stream=None):
501501
with io.open(os.path.join(
502502
os.path.dirname(__file__),
503503
"web_backend",
504+
"js",
504505
"mpl.js"), encoding='utf8') as fd:
505506
output.write(fd.read())
506507

@@ -530,7 +531,7 @@ def get_javascript(cls, stream=None):
530531

531532
@classmethod
532533
def get_static_file_path(cls):
533-
return os.path.join(os.path.dirname(__file__), 'web_backend')
534+
return os.path.join(os.path.dirname(__file__), 'web_backend', 'js')
534535

535536
def _send_event(self, event_type, **kwargs):
536537
payload = {'type': event_type}

0 commit comments

Comments
 (0)