Skip to content

Commit f9feefe

Browse files
committed
Merge pull request #3559 from pelson/nbagg_enhancments
ENH : improve nbagg backend
2 parents ee2dde7 + d37e613 commit f9feefe

File tree

8 files changed

+509
-57
lines changed

8 files changed

+509
-57
lines changed

lib/matplotlib/backends/backend_nbagg.py

+82-17
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,45 @@
11
"""Interactive figures in the IPython notebook"""
2+
# Note: There is a notebook in
3+
# lib/matplotlib/backends/web_backend/nbagg_uat.ipynb to help verify
4+
# that changes made maintain expected behaviour.
5+
26
from base64 import b64encode
7+
from contextlib import contextmanager
38
import json
49
import io
510
import os
611
import six
712
from uuid import uuid4 as uuid
813

14+
import tornado.ioloop
15+
916
from IPython.display import display, Javascript, HTML
1017
from IPython.kernel.comm import Comm
1118

19+
from matplotlib import rcParams
1220
from matplotlib.figure import Figure
21+
from matplotlib.backends import backend_agg
1322
from matplotlib.backends.backend_webagg_core import (FigureManagerWebAgg,
1423
FigureCanvasWebAggCore,
1524
NavigationToolbar2WebAgg)
16-
from matplotlib.backend_bases import ShowBase, NavigationToolbar2
25+
from matplotlib.backend_bases import (ShowBase, NavigationToolbar2,
26+
TimerBase, FigureCanvasBase)
1727

1828

1929
class Show(ShowBase):
2030
def __call__(self, block=None):
21-
import matplotlib._pylab_helpers as pylab_helpers
31+
from matplotlib._pylab_helpers import Gcf
2232
from matplotlib import is_interactive
2333

24-
managers = pylab_helpers.Gcf.get_all_fig_managers()
34+
managers = Gcf.get_all_fig_managers()
2535
if not managers:
2636
return
2737

28-
interactive = is_interactive()
29-
3038
for manager in managers:
3139
manager.show()
32-
if not interactive and manager in pylab_helpers.Gcf._activeQue:
33-
pylab_helpers.Gcf._activeQue.remove(manager)
40+
41+
if not is_interactive() and manager in Gcf._activeQue:
42+
Gcf._activeQue.remove(manager)
3443

3544

3645
show = Show()
@@ -49,19 +58,18 @@ def draw_if_interactive():
4958
def connection_info():
5059
"""
5160
Return a string showing the figure and connection status for
52-
the backend.
61+
the backend. This is intended as a diagnostic tool, and not for general
62+
use.
5363
5464
"""
55-
# TODO: Make this useful!
56-
import matplotlib._pylab_helpers as pylab_helpers
65+
from matplotlib._pylab_helpers import Gcf
5766
result = []
58-
for manager in pylab_helpers.Gcf.get_all_fig_managers():
67+
for manager in Gcf.get_all_fig_managers():
5968
fig = manager.canvas.figure
6069
result.append('{} - {}'.format((fig.get_label() or
6170
"Figure {0}".format(manager.num)),
6271
manager.web_sockets))
63-
result.append('Figures pending show: ' +
64-
str(len(pylab_helpers.Gcf._activeQue)))
72+
result.append('Figures pending show: {}'.format(len(Gcf._activeQue)))
6573
return '\n'.join(result)
6674

6775

@@ -96,7 +104,8 @@ def __init__(self, canvas, num):
96104

97105
def display_js(self):
98106
# XXX How to do this just once? It has to deal with multiple
99-
# browser instances using the same kernel.
107+
# browser instances using the same kernel (require.js - but the
108+
# file isn't static?).
100109
display(Javascript(FigureManagerNbAgg.get_javascript()))
101110

102111
def show(self):
@@ -108,6 +117,10 @@ def show(self):
108117
self._shown = True
109118

110119
def reshow(self):
120+
"""
121+
A special method to re-show the figure in the notebook.
122+
123+
"""
111124
self._shown = False
112125
self.show()
113126

@@ -140,6 +153,49 @@ def destroy(self):
140153
for comm in self.web_sockets.copy():
141154
comm.on_close()
142155

156+
def clearup_closed(self):
157+
"""Clear up any closed Comms."""
158+
self.web_sockets = set([socket for socket in self.web_sockets
159+
if not socket.is_open()])
160+
161+
162+
class TimerTornado(TimerBase):
163+
def _timer_start(self):
164+
import datetime
165+
self._timer_stop()
166+
if self._single:
167+
ioloop = tornado.ioloop.IOLoop.instance()
168+
self._timer = ioloop.add_timeout(
169+
datetime.timedelta(milliseconds=self.interval),
170+
self._on_timer)
171+
else:
172+
self._timer = tornado.ioloop.PeriodicCallback(
173+
self._on_timer,
174+
self.interval)
175+
self._timer.start()
176+
177+
def _timer_stop(self):
178+
if self._timer is not None:
179+
self._timer.stop()
180+
self._timer = None
181+
182+
def _timer_set_interval(self):
183+
# Only stop and restart it if the timer has already been started
184+
if self._timer is not None:
185+
self._timer_stop()
186+
self._timer_start()
187+
188+
189+
class FigureCanvasNbAgg(FigureCanvasWebAggCore):
190+
def new_timer(self, *args, **kwargs):
191+
return TimerTornado(*args, **kwargs)
192+
193+
def start_event_loop(self, timeout):
194+
FigureCanvasBase.start_event_loop_default(self, timeout)
195+
196+
def stop_event_loop(self):
197+
FigureCanvasBase.stop_event_loop_default(self)
198+
143199

144200
def new_figure_manager(num, *args, **kwargs):
145201
"""
@@ -154,7 +210,9 @@ def new_figure_manager_given_figure(num, figure):
154210
"""
155211
Create a new figure manager instance for the given figure.
156212
"""
157-
canvas = FigureCanvasWebAggCore(figure)
213+
canvas = FigureCanvasNbAgg(figure)
214+
if rcParams['nbagg.transparent']:
215+
figure.patch.set_alpha(0)
158216
manager = FigureManagerNbAgg(canvas, num)
159217
return manager
160218

@@ -173,6 +231,8 @@ def __init__(self, manager):
173231
self.supports_binary = None
174232
self.manager = manager
175233
self.uuid = str(uuid())
234+
# Publish an output area with a unique ID. The javascript can then
235+
# hook into this area.
176236
display(HTML("<div id=%r></div>" % self.uuid))
177237
try:
178238
self.comm = Comm('matplotlib', data={'id': self.uuid})
@@ -181,12 +241,17 @@ def __init__(self, manager):
181241
'instance. Are you in the IPython notebook?')
182242
self.comm.on_msg(self.on_message)
183243

244+
manager = self.manager
245+
self.comm.on_close(lambda close_message: manager.clearup_closed())
246+
247+
def is_open(self):
248+
return not self.comm._closed
249+
184250
def on_close(self):
185251
# When the socket is closed, deregister the websocket with
186252
# the FigureManager.
187-
if self.comm in self.manager.web_sockets:
188-
self.manager.remove_web_socket(self)
189253
self.comm.close()
254+
self.manager.clearup_closed()
190255

191256
def send_json(self, content):
192257
self.comm.send({'data': json.dumps(content)})

lib/matplotlib/backends/backend_webagg.py

+1-26
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from matplotlib.figure import Figure
4040
from matplotlib._pylab_helpers import Gcf
4141
from . import backend_webagg_core as core
42+
from .backend_nbagg import TimerTornado
4243

4344

4445
def new_figure_manager(num, *args, **kwargs):
@@ -96,32 +97,6 @@ def run(self):
9697
webagg_server_thread = ServerThread()
9798

9899

99-
class TimerTornado(backend_bases.TimerBase):
100-
def _timer_start(self):
101-
self._timer_stop()
102-
if self._single:
103-
ioloop = tornado.ioloop.IOLoop.instance()
104-
self._timer = ioloop.add_timeout(
105-
datetime.timedelta(milliseconds=self.interval),
106-
self._on_timer)
107-
else:
108-
self._timer = tornado.ioloop.PeriodicCallback(
109-
self._on_timer,
110-
self.interval)
111-
self._timer.start()
112-
113-
def _timer_stop(self):
114-
if self._timer is not None:
115-
self._timer.stop()
116-
self._timer = None
117-
118-
def _timer_set_interval(self):
119-
# Only stop and restart it if the timer has already been started
120-
if self._timer is not None:
121-
self._timer_stop()
122-
self._timer_start()
123-
124-
125100
class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
126101
def show(self):
127102
# show the figure window

lib/matplotlib/backends/backend_webagg_core.py

+20-13
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def show(self):
7171
show()
7272

7373
def draw(self):
74-
renderer = self.get_renderer()
74+
renderer = self.get_renderer(cleared=True)
7575

7676
self._png_is_old = True
7777

@@ -91,21 +91,25 @@ def get_diff_image(self):
9191
# The buffer is created as type uint32 so that entire
9292
# pixels can be compared in one numpy call, rather than
9393
# needing to compare each plane separately.
94-
buff = np.frombuffer(
95-
self.get_renderer().buffer_rgba(), dtype=np.uint32)
96-
buff.shape = (
97-
self._renderer.height, self._renderer.width)
94+
renderer = self.get_renderer()
95+
buff = np.frombuffer(renderer.buffer_rgba(), dtype=np.uint32)
9896

99-
if not self._force_full:
100-
last_buffer = np.frombuffer(
101-
self._last_renderer.buffer_rgba(), dtype=np.uint32)
102-
last_buffer.shape = (
103-
self._renderer.height, self._renderer.width)
97+
buff.shape = (renderer.height, renderer.width)
98+
99+
# If any pixels have transparency, we need to force a full draw
100+
# as we cannot overlay new on top of old.
101+
pixels = buff.view(dtype=np.uint8).reshape(buff.shape + (4,))
102+
some_transparency = np.any(pixels[:, :, 3] != 255)
103+
104+
output = buff
105+
106+
if not self._force_full and not some_transparency:
107+
last_buffer = np.frombuffer(self._last_renderer.buffer_rgba(),
108+
dtype=np.uint32)
109+
last_buffer.shape = (renderer.height, renderer.width)
104110

105111
diff = buff != last_buffer
106112
output = np.where(diff, buff, 0)
107-
else:
108-
output = buff
109113

110114
# Clear out the PNG data buffer rather than recreating it
111115
# each time. This reduces the number of memory
@@ -122,7 +126,7 @@ def get_diff_image(self):
122126

123127
# Swap the renderer frames
124128
self._renderer, self._last_renderer = (
125-
self._last_renderer, self._renderer)
129+
self._last_renderer, renderer)
126130
self._force_full = False
127131
self._png_is_old = False
128132
return self._png_buffer.getvalue()
@@ -147,6 +151,9 @@ def get_renderer(self, cleared=None):
147151
w, h, self.figure.dpi)
148152
self._lastKey = key
149153

154+
elif cleared:
155+
self._renderer.clear()
156+
150157
return self._renderer
151158

152159
def handle_event(self, event):

lib/matplotlib/backends/web_backend/mpl.js

+2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ mpl.figure = function(figure_id, websocket, ondownload, parent_element) {
6060
}
6161

6262
this.imageObj.onload = function() {
63+
fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);
6364
fig.context.drawImage(fig.imageObj, 0, 0);
6465
fig.waiting = false;
6566
};
@@ -322,6 +323,7 @@ mpl.figure.prototype._make_on_message_function = function(fig) {
322323
(window.URL || window.webkitURL).revokeObjectURL(
323324
fig.imageObj.src);
324325
}
326+
325327
fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(
326328
evt.data);
327329
fig.updated_canvas_event();

lib/matplotlib/backends/web_backend/nbagg_mpl.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ mpl.mpl_figure_comm = function(comm, msg) {
2525
// starts-up an IPython Comm through the "matplotlib" channel.
2626

2727
var id = msg.content.data.id;
28+
// Get hold of the div created by the display call when the Comm
29+
// socket was opened in Python.
2830
var element = $("#" + id);
2931
var ws_proxy = comm_websocket_adapter(comm)
3032

@@ -44,7 +46,7 @@ mpl.mpl_figure_comm = function(comm, msg) {
4446

4547
// Disable right mouse context menu.
4648
$(fig.rubberband_canvas).bind("contextmenu",function(e){
47-
return false;
49+
return false;
4850
});
4951

5052
};
@@ -53,12 +55,16 @@ mpl.figure.prototype.handle_close = function(fig, msg) {
5355
// Update the output cell to use the data from the current canvas.
5456
fig.push_to_output();
5557
var dataURL = fig.canvas.toDataURL();
58+
// Re-enable the keyboard manager in IPython - without this line, in FF,
59+
// the notebook keyboard shortcuts fail.
60+
IPython.keyboard_manager.enable()
5661
$(fig.parent_element).html('<img src="' + dataURL + '">');
5762
fig.send_message('closing', {});
5863
fig.ws.close()
5964
}
6065

6166
mpl.figure.prototype.push_to_output = function(remove_interactive) {
67+
// Turn the data on the canvas into data in the output cell.
6268
var dataURL = this.canvas.toDataURL();
6369
this.cell_info[1]['text/html'] = '<img src="' + dataURL + '">';
6470
}

0 commit comments

Comments
 (0)