Skip to content

Commit f21fe77

Browse files
committed
Revert "Merge pull request #5754 from blink1073/ipython-widget"
This reverts commit 9c100c9, reversing changes made to 6bed1be. The reason for doing this is that ipywidgets is still moving much too fast for Matplotlib to keep up with. The original work done in #5754 was done against ipywidgets v4, as of now the final touches are being put on v7. We are reverting back to our old inject-into-the-DOM method from `nbagg`, which is what is used by `%matplotlib notebook`. For embedding as a 'proper' widget use ipympl (github.com/matplotlib/jupyter-wigets) which will release on a schedule that matches the upstream jupyter ecosystem. closes #7695
1 parent 4c33d97 commit f21fe77

File tree

10 files changed

+362
-197
lines changed

10 files changed

+362
-197
lines changed

lib/matplotlib/__init__.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,16 +1373,6 @@ def tk_window_focus():
13731373
return rcParams['tk.window_focus']
13741374

13751375

1376-
# Jupyter extension paths
1377-
def _jupyter_nbextension_paths():
1378-
return [{
1379-
'section': 'notebook',
1380-
'src': 'backends/web_backend/js',
1381-
'dest': 'matplotlib',
1382-
'require': 'matplotlib/extension'
1383-
}]
1384-
1385-
13861376
default_test_modules = [
13871377
'matplotlib.tests',
13881378
'matplotlib.sphinxext.tests',

lib/matplotlib/backend_bases.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2188,8 +2188,7 @@ def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None,
21882188
origfacecolor = self.figure.get_facecolor()
21892189
origedgecolor = self.figure.get_edgecolor()
21902190

2191-
if dpi != 'figure':
2192-
self.figure.dpi = dpi
2191+
self.figure.dpi = dpi
21932192
self.figure.set_facecolor(facecolor)
21942193
self.figure.set_edgecolor(edgecolor)
21952194

lib/matplotlib/backends/backend_nbagg.py

Lines changed: 149 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,23 @@
33
# lib/matplotlib/backends/web_backend/nbagg_uat.ipynb to help verify
44
# that changes made maintain expected behaviour.
55

6+
import datetime
67
from base64 import b64encode
78
import json
89
import io
9-
from tempfile import mkdtemp
10-
import shutil
1110
import os
1211
import six
1312
from uuid import uuid4 as uuid
1413

15-
from IPython.display import display, HTML
16-
from IPython import version_info
14+
import tornado.ioloop
15+
16+
from IPython.display import display, Javascript, HTML
1717
try:
1818
# Jupyter/IPython 4.x or later
19-
from ipywidgets import DOMWidget
20-
from traitlets import Unicode, Bool, Float, List, Any
21-
from notebook.nbextensions import install_nbextension, check_nbextension
19+
from ipykernel.comm import Comm
2220
except ImportError:
2321
# Jupyter/IPython 3.x or earlier
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
22+
from IPython.kernel.comm import Comm
2723

2824
from matplotlib import rcParams, is_interactive
2925
from matplotlib._pylab_helpers import Gcf
@@ -33,6 +29,13 @@
3329
from matplotlib.backend_bases import (
3430
_Backend, FigureCanvasBase, NavigationToolbar2)
3531
from matplotlib.figure import Figure
32+
from matplotlib import is_interactive
33+
from matplotlib.backends.backend_webagg_core import (FigureManagerWebAgg,
34+
FigureCanvasWebAggCore,
35+
NavigationToolbar2WebAgg,
36+
TimerTornado)
37+
from matplotlib.backend_bases import (ShowBase, NavigationToolbar2,
38+
FigureCanvasBase)
3639

3740

3841
def connection_info():
@@ -65,7 +68,6 @@ def connection_info():
6568
'zoom_to_rect': 'fa fa-square-o icon-check-empty',
6669
'move': 'fa fa-arrows icon-move',
6770
'download': 'fa fa-floppy-o icon-save',
68-
'export': 'fa fa-file-picture-o icon-picture',
6971
None: None
7072
}
7173

@@ -77,154 +79,161 @@ class NavigationIPy(NavigationToolbar2WebAgg):
7779
_FONT_AWESOME_CLASSES[image_file], name_of_method)
7880
for text, tooltip_text, image_file, name_of_method
7981
in (NavigationToolbar2.toolitems +
80-
(('Download', 'Download plot', 'download', 'download'),
81-
('Export', 'Export plot', 'export', 'export')))
82+
(('Download', 'Download plot', 'download', 'download'),))
8283
if image_file in _FONT_AWESOME_CLASSES]
8384

84-
def export(self):
85-
buf = io.BytesIO()
86-
self.canvas.figure.savefig(buf, format='png', dpi='figure')
87-
# Figure width in pixels
88-
pwidth = self.canvas.figure.get_figwidth()*self.canvas.figure.get_dpi()
89-
# Scale size to match widget on HiPD monitors
90-
width = pwidth/self.canvas._dpi_ratio
91-
data = "<img src='data:image/png;base64,{0}' width={1}/>"
92-
data = data.format(b64encode(buf.getvalue()).decode('utf-8'), width)
93-
display(HTML(data))
94-
95-
96-
class FigureCanvasNbAgg(DOMWidget, FigureCanvasWebAggCore):
97-
_view_module = Unicode("matplotlib", sync=True)
98-
_view_name = Unicode('MPLCanvasView', sync=True)
99-
_toolbar_items = List(sync=True)
100-
_closed = Bool(True)
101-
_id = Unicode('', sync=True)
102-
103-
# Must declare the superclass private members.
104-
_png_is_old = Bool()
105-
_force_full = Bool()
106-
_current_image_mode = Unicode()
107-
_dpi_ratio = Float(1.0)
108-
_is_idle_drawing = Bool()
109-
_is_saving = Bool()
110-
_button = Any()
111-
_key = Any()
112-
_lastx = Any()
113-
_lasty = Any()
114-
_is_idle_drawing = Bool()
115-
116-
def __init__(self, figure, *args, **kwargs):
117-
super(FigureCanvasWebAggCore, self).__init__(figure, *args, **kwargs)
118-
super(DOMWidget, self).__init__(*args, **kwargs)
119-
self._uid = uuid().hex
120-
self.on_msg(self._handle_message)
121-
122-
def _handle_message(self, object, message, buffers):
123-
# The 'supports_binary' message is relevant to the
124-
# websocket itself. The other messages get passed along
125-
# to matplotlib as-is.
126-
127-
# Every message has a "type" and a "figure_id".
128-
message = json.loads(message)
129-
if message['type'] == 'closing':
130-
self._closed = True
131-
elif message['type'] == 'supports_binary':
132-
self.supports_binary = message['value']
133-
elif message['type'] == 'initialized':
134-
_, _, w, h = self.figure.bbox.bounds
135-
self.manager.resize(w, h)
136-
self.send_json('refresh')
137-
else:
138-
self.manager.handle_json(message)
139-
140-
def send_json(self, content):
141-
self.send({'data': json.dumps(content)})
142-
143-
def send_binary(self, blob):
144-
# The comm is ascii, so we always send the image in base64
145-
# encoded data URL form.
146-
data = b64encode(blob)
147-
if six.PY3:
148-
data = data.decode('ascii')
149-
data_uri = "data:image/png;base64,{0}".format(data)
150-
self.send({'data': data_uri})
151-
152-
def new_timer(self, *args, **kwargs):
153-
return TimerTornado(*args, **kwargs)
154-
155-
def start_event_loop(self, timeout):
156-
FigureCanvasBase.start_event_loop_default(self, timeout)
157-
158-
def stop_event_loop(self):
159-
FigureCanvasBase.stop_event_loop_default(self)
160-
16185

16286
class FigureManagerNbAgg(FigureManagerWebAgg):
16387
ToolbarCls = NavigationIPy
16488

16589
def __init__(self, canvas, num):
90+
self._shown = False
16691
FigureManagerWebAgg.__init__(self, canvas, num)
167-
toolitems = []
168-
for name, tooltip, image, method in self.ToolbarCls.toolitems:
169-
if name is None:
170-
toolitems.append(['', '', '', ''])
171-
else:
172-
toolitems.append([name, tooltip, image, method])
173-
canvas._toolbar_items = toolitems
174-
self.web_sockets = [self.canvas]
92+
93+
def display_js(self):
94+
# XXX How to do this just once? It has to deal with multiple
95+
# browser instances using the same kernel (require.js - but the
96+
# file isn't static?).
97+
display(Javascript(FigureManagerNbAgg.get_javascript()))
17598

17699
def show(self):
177-
if self.canvas._closed:
178-
self.canvas._closed = False
179-
display(self.canvas)
100+
if not self._shown:
101+
self.display_js()
102+
self._create_comm()
180103
else:
181104
self.canvas.draw_idle()
105+
self._shown = True
106+
107+
def reshow(self):
108+
"""
109+
A special method to re-show the figure in the notebook.
110+
111+
"""
112+
self._shown = False
113+
self.show()
114+
115+
@property
116+
def connected(self):
117+
return bool(self.web_sockets)
118+
119+
@classmethod
120+
def get_javascript(cls, stream=None):
121+
if stream is None:
122+
output = io.StringIO()
123+
else:
124+
output = stream
125+
super(FigureManagerNbAgg, cls).get_javascript(stream=output)
126+
with io.open(os.path.join(
127+
os.path.dirname(__file__),
128+
"web_backend",
129+
"nbagg_mpl.js"), encoding='utf8') as fd:
130+
output.write(fd.read())
131+
if stream is None:
132+
return output.getvalue()
133+
134+
def _create_comm(self):
135+
comm = CommSocket(self)
136+
self.add_web_socket(comm)
137+
return comm
182138

183139
def destroy(self):
184140
self._send_event('close')
141+
# need to copy comms as callbacks will modify this list
142+
for comm in list(self.web_sockets):
143+
comm.on_close()
144+
self.clearup_closed()
145+
146+
def clearup_closed(self):
147+
"""Clear up any closed Comms."""
148+
self.web_sockets = set([socket for socket in self.web_sockets
149+
if socket.is_open()])
150+
151+
if len(self.web_sockets) == 0:
152+
self.canvas.close_event()
153+
154+
def remove_comm(self, comm_id):
155+
self.web_sockets = set([socket for socket in self.web_sockets
156+
if not socket.comm.comm_id == comm_id])
157+
158+
159+
class FigureCanvasNbAgg(FigureCanvasWebAggCore):
160+
def new_timer(self, *args, **kwargs):
161+
return TimerTornado(*args, **kwargs)
185162

186163

187-
def nbinstall(overwrite=False, user=True):
164+
class CommSocket(object):
188165
"""
189-
Copies javascript dependencies to the '/nbextensions' folder in
190-
your IPython directory.
191-
192-
Parameters
193-
----------
194-
195-
overwrite : bool
196-
If True, always install the files, regardless of what may already be
197-
installed. Defaults to False.
198-
user : bool
199-
Whether to install to the user's .ipython/nbextensions directory.
200-
Otherwise do a system-wide install
201-
(e.g. /usr/local/share/jupyter/nbextensions). Defaults to False.
166+
Manages the Comm connection between IPython and the browser (client).
167+
168+
Comms are 2 way, with the CommSocket being able to publish a message
169+
via the send_json method, and handle a message with on_message. On the
170+
JS side figure.send_message and figure.ws.onmessage do the sending and
171+
receiving respectively.
172+
202173
"""
203-
if (check_nbextension('matplotlib') or
204-
check_nbextension('matplotlib', True)):
205-
return
206-
207-
# Make a temporary directory so we can wrap mpl.js in a requirejs define().
208-
tempdir = mkdtemp()
209-
path = os.path.join(os.path.dirname(__file__), "web_backend")
210-
shutil.copy2(os.path.join(path, "nbagg_mpl.js"), tempdir)
211-
212-
with open(os.path.join(path, 'mpl.js')) as fid:
213-
contents = fid.read()
214-
215-
with open(os.path.join(tempdir, 'mpl.js'), 'w') as fid:
216-
fid.write('define(["jquery"], function($) {\n')
217-
fid.write(contents)
218-
fid.write('\nreturn mpl;\n});')
219-
220-
install_nbextension(
221-
tempdir,
222-
overwrite=overwrite,
223-
symlink=False,
224-
destination='matplotlib',
225-
verbose=0,
226-
**({'user': user} if version_info >= (3, 0, 0, '') else {})
227-
)
174+
def __init__(self, manager):
175+
self.supports_binary = None
176+
self.manager = manager
177+
self.uuid = str(uuid())
178+
# Publish an output area with a unique ID. The javascript can then
179+
# hook into this area.
180+
display(HTML("<div id=%r></div>" % self.uuid))
181+
try:
182+
self.comm = Comm('matplotlib', data={'id': self.uuid})
183+
except AttributeError:
184+
raise RuntimeError('Unable to create an IPython notebook Comm '
185+
'instance. Are you in the IPython notebook?')
186+
self.comm.on_msg(self.on_message)
187+
188+
manager = self.manager
189+
self._ext_close = False
190+
191+
def _on_close(close_message):
192+
self._ext_close = True
193+
manager.remove_comm(close_message['content']['comm_id'])
194+
manager.clearup_closed()
195+
196+
self.comm.on_close(_on_close)
197+
198+
def is_open(self):
199+
return not (self._ext_close or self.comm._closed)
200+
201+
def on_close(self):
202+
# When the socket is closed, deregister the websocket with
203+
# the FigureManager.
204+
if self.is_open():
205+
try:
206+
self.comm.close()
207+
except KeyError:
208+
# apparently already cleaned it up?
209+
pass
210+
211+
def send_json(self, content):
212+
self.comm.send({'data': json.dumps(content)})
213+
214+
def send_binary(self, blob):
215+
# The comm is ascii, so we always send the image in base64
216+
# encoded data URL form.
217+
data = b64encode(blob)
218+
if six.PY3:
219+
data = data.decode('ascii')
220+
data_uri = "data:image/png;base64,{0}".format(data)
221+
self.comm.send({'data': data_uri})
222+
223+
def on_message(self, message):
224+
# The 'supports_binary' message is relevant to the
225+
# websocket itself. The other messages get passed along
226+
# to matplotlib as-is.
227+
228+
# Every message has a "type" and a "figure_id".
229+
message = json.loads(message['content']['data'])
230+
if message['type'] == 'closing':
231+
self.on_close()
232+
self.manager.clearup_closed()
233+
elif message['type'] == 'supports_binary':
234+
self.supports_binary = message['value']
235+
else:
236+
self.manager.handle_json(message)
228237

229238

230239
@_Backend.export

lib/matplotlib/backends/backend_webagg_core.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import io
1919
import json
2020
import os
21-
import datetime
21+
import time
2222
import warnings
2323

2424
import numpy as np
@@ -480,7 +480,6 @@ def get_javascript(cls, stream=None):
480480
with io.open(os.path.join(
481481
os.path.dirname(__file__),
482482
"web_backend",
483-
"js",
484483
"mpl.js"), encoding='utf8') as fd:
485484
output.write(fd.read())
486485

0 commit comments

Comments
 (0)