Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2f9c992

Browse files
committed
Address documentation and smaller issues raised by @pelson in matplotlib#1426.
1 parent 31479e6 commit 2f9c992

File tree

8 files changed

+102
-61
lines changed

8 files changed

+102
-61
lines changed

lib/matplotlib/animation.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,9 @@ class Animation(object):
523523
'''
524524
def __init__(self, fig, event_source=None, blit=False):
525525
self._fig = fig
526+
# Disables blitting for backends that don't support it. This
527+
# allows users to request it if available, but still have a
528+
# fallback that works if it is not.
526529
self._blit = blit and fig.canvas.supports_blit
527530

528531
# These are the basics of the animation. The frame sequence represents

lib/matplotlib/backends/backend_webagg.py

Lines changed: 69 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
"""
44
from __future__ import division, print_function
55

6-
import cStringIO
76
import datetime
87
import errno
8+
import io
99
import json
1010
import os
1111
import random
@@ -104,10 +104,23 @@ class FigureCanvasWebAgg(backend_agg.FigureCanvasAgg):
104104

105105
def __init__(self, *args, **kwargs):
106106
backend_agg.FigureCanvasAgg.__init__(self, *args, **kwargs)
107-
self.png_buffer = cStringIO.StringIO()
108-
self.png_is_old = True
109-
self.force_full = True
110-
self.pending_draw = None
107+
108+
# A buffer to hold the PNG data for the last frame. This is
109+
# retained so it can be resent to each client without
110+
# regenerating it.
111+
self._png_buffer = io.BytesIO()
112+
113+
# Set to True when the renderer contains data that is newer
114+
# than the PNG buffer.
115+
self._png_is_old = True
116+
117+
# Set to True by the `refresh` message so that the next frame
118+
# sent to the clients will be a full frame.
119+
self._force_full = True
120+
121+
# Set to True when a drawing is in progress to prevent redraw
122+
# messages from piling up.
123+
self._pending_draw = None
111124

112125
def show(self):
113126
# show the figure window
@@ -117,7 +130,7 @@ def draw(self):
117130
# TODO: Do we just queue the drawing here? That's what Gtk does
118131
renderer = self.get_renderer()
119132

120-
self.png_is_old = True
133+
self._png_is_old = True
121134

122135
backend_agg.RendererAgg.lock.acquire()
123136
try:
@@ -128,67 +141,75 @@ def draw(self):
128141
self.manager.refresh_all()
129142

130143
def draw_idle(self):
131-
if self.pending_draw is None:
144+
if self._pending_draw is None:
132145
ioloop = tornado.ioloop.IOLoop.instance()
133-
self.pending_draw = ioloop.add_timeout(
146+
self._pending_draw = ioloop.add_timeout(
134147
datetime.timedelta(milliseconds=50),
135148
self._draw_idle_callback)
136149

137150
def _draw_idle_callback(self):
138151
try:
139152
self.draw()
140153
finally:
141-
self.pending_draw = None
154+
self._pending_draw = None
142155

143156
def get_diff_image(self):
144-
if self.png_is_old:
157+
if self._png_is_old:
158+
# The buffer is created as type uint32 so that entire
159+
# pixels can be compared in one numpy call, rather than
160+
# needing to compare each plane separately.
145161
buffer = np.frombuffer(
146-
self.renderer.buffer_rgba(), dtype=np.uint32)
147-
buffer = buffer.reshape(
148-
(self.renderer.height, self.renderer.width))
162+
self._renderer.buffer_rgba(), dtype=np.uint32)
163+
buffer.shape = (
164+
self._renderer.height, self._renderer.width)
149165

150-
if not self.force_full:
166+
if not self._force_full:
151167
last_buffer = np.frombuffer(
152-
self.last_renderer.buffer_rgba(), dtype=np.uint32)
153-
last_buffer = last_buffer.reshape(
154-
(self.renderer.height, self.renderer.width))
168+
self._last_renderer.buffer_rgba(), dtype=np.uint32)
169+
last_buffer.shape = (
170+
self._renderer.height, self._renderer.width)
155171

156172
diff = buffer != last_buffer
157173
output = np.where(diff, buffer, 0)
158174
else:
159175
output = buffer
160176

161-
self.png_buffer.reset()
162-
self.png_buffer.truncate()
177+
# Clear out the PNG data buffer rather than recreating it
178+
# each time. This reduces the number of memory
179+
# (de)allocations.
180+
self._png_buffer.truncate()
181+
self._png_buffer.seek(0)
182+
163183
# TODO: We should write a new version of write_png that
164184
# handles the differencing inline
165185
_png.write_png(
166186
output.tostring(),
167187
output.shape[1], output.shape[0],
168-
self.png_buffer)
188+
self._png_buffer)
169189

170-
self.renderer, self.last_renderer = \
171-
self.last_renderer, self.renderer
172-
self.force_full = False
173-
self.png_is_old = False
174-
return self.png_buffer.getvalue()
190+
# Swap the renderer frames
191+
self._renderer, self._last_renderer = \
192+
self._last_renderer, self._renderer
193+
self._force_full = False
194+
self._png_is_old = False
195+
return self._png_buffer.getvalue()
175196

176197
def get_renderer(self):
177198
l, b, w, h = self.figure.bbox.bounds
178199
key = w, h, self.figure.dpi
179200
try:
180-
self._lastKey, self.renderer
201+
self._lastKey, self._renderer
181202
except AttributeError:
182203
need_new_renderer = True
183204
else:
184205
need_new_renderer = (self._lastKey != key)
185206

186207
if need_new_renderer:
187-
self.renderer = backend_agg.RendererAgg(w, h, self.figure.dpi)
188-
self.last_renderer = backend_agg.RendererAgg(w, h, self.figure.dpi)
208+
self._renderer = backend_agg.RendererAgg(w, h, self.figure.dpi)
209+
self._last_renderer = backend_agg.RendererAgg(w, h, self.figure.dpi)
189210
self._lastKey = key
190211

191-
return self.renderer
212+
return self._renderer
192213

193214
def handle_event(self, event):
194215
type = event['type']
@@ -201,8 +222,10 @@ def handle_event(self, event):
201222
# off by 1
202223
button = event['button'] + 1
203224

204-
# The right mouse button pops up a context menu, which doesn't
205-
# work very well, so use the middle mouse button instead
225+
# The right mouse button pops up a context menu, which
226+
# doesn't work very well, so use the middle mouse button
227+
# instead. It doesn't seem that it's possible to disable
228+
# the context menu in recent versions of Chrome.
206229
if button == 2:
207230
button = 3
208231

@@ -223,7 +246,7 @@ def handle_event(self, event):
223246
# TODO: Be more suspicious of the input
224247
getattr(self.toolbar, event['name'])()
225248
elif type == 'refresh':
226-
self.force_full = True
249+
self._force_full = True
227250
self.draw_idle()
228251

229252
def send_event(self, event_type, **kwargs):
@@ -248,9 +271,6 @@ def __init__(self, canvas, num):
248271

249272
self.web_sockets = set()
250273

251-
self.canvas = canvas
252-
self.num = num
253-
254274
self.toolbar = self._get_toolbar(canvas)
255275

256276
def show(self):
@@ -279,16 +299,9 @@ def resize(self, w, h):
279299

280300

281301
class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2):
282-
toolitems = (
283-
('Home', 'Reset original view', 'home', 'home'),
284-
('Back', 'Back to previous view', 'back', 'back'),
285-
('Forward', 'Forward to next view', 'forward', 'forward'),
286-
(None, None, None, None),
287-
('Pan', 'Pan axes with left mouse, zoom with right', 'move', 'pan'),
288-
('Zoom', 'Zoom to rectangle', 'zoom_to_rect', 'zoom'),
289-
(None, None, None, None),
302+
toolitems = list(backend_bases.NavigationToolbar2.toolitems[:6]) + [
290303
('Download', 'Download plot', 'filesave', 'download')
291-
)
304+
]
292305

293306
def _init_toolbar(self):
294307
self.message = ''
@@ -331,7 +344,7 @@ def get(self, fignum):
331344
tpl = fd.read()
332345

333346
fignum = int(fignum)
334-
manager = Gcf().get_fig_manager(fignum)
347+
manager = Gcf.get_fig_manager(fignum)
335348

336349
t = tornado.template.Template(tpl)
337350
self.write(t.generate(
@@ -341,7 +354,7 @@ def get(self, fignum):
341354
class Download(tornado.web.RequestHandler):
342355
def get(self, fignum, format):
343356
self.fignum = int(fignum)
344-
manager = Gcf().get_fig_manager(self.fignum)
357+
manager = Gcf.get_fig_manager(self.fignum)
345358

346359
# TODO: Move this to a central location
347360
mimetypes = {
@@ -357,25 +370,25 @@ def get(self, fignum, format):
357370

358371
self.set_header('Content-Type', mimetypes.get(format, 'binary'))
359372

360-
buffer = cStringIO.StringIO()
373+
buffer = io.BytesIO()
361374
manager.canvas.print_figure(buffer, format=format)
362375
self.write(buffer.getvalue())
363376

364377
class WebSocket(tornado.websocket.WebSocketHandler):
365378
def open(self, fignum):
366379
self.fignum = int(fignum)
367-
manager = Gcf().get_fig_manager(self.fignum)
380+
manager = Gcf.get_fig_manager(self.fignum)
368381
manager.add_web_socket(self)
369382
l, b, w, h = manager.canvas.figure.bbox.bounds
370383
manager.resize(w, h)
371384
self.on_message('{"type":"refresh"}')
372385

373386
def on_close(self):
374-
Gcf().get_fig_manager(self.fignum).remove_web_socket(self)
387+
Gcf.get_fig_manager(self.fignum).remove_web_socket(self)
375388

376389
def on_message(self, message):
377390
message = json.loads(message)
378-
canvas = Gcf().get_fig_manager(self.fignum).canvas
391+
canvas = Gcf.get_fig_manager(self.fignum).canvas
379392
canvas.handle_event(message)
380393

381394
def send_event(self, event_type, **kwargs):
@@ -384,7 +397,7 @@ def send_event(self, event_type, **kwargs):
384397
self.write_message(json.dumps(payload))
385398

386399
def send_image(self):
387-
canvas = Gcf().get_fig_manager(self.fignum).canvas
400+
canvas = Gcf.get_fig_manager(self.fignum).canvas
388401
diff = canvas.get_diff_image()
389402
self.write_message(diff, binary=True)
390403

@@ -416,8 +429,11 @@ def initialize(cls):
416429

417430
app = cls()
418431

432+
# This port selection algorithm is borrowed, more or less
433+
# verbatim, from IPython.
419434
def random_ports(port, n):
420-
"""Generate a list of n random ports near the given port.
435+
"""
436+
Generate a list of n random ports near the given port.
421437
422438
The first 5 ports will be sequential, and the remaining n-5 will be
423439
randomly selected in the range [port-2*n, port+2*n].
@@ -429,8 +445,7 @@ def random_ports(port, n):
429445

430446
success = None
431447
cls.port = rcParams['webagg.port']
432-
# TODO: Configure port_retrues
433-
for port in random_ports(cls.port, 50):
448+
for port in random_ports(cls.port, rcParams['webagg.port_retries']):
434449
try:
435450
app.listen(port)
436451
except socket.error as e:

lib/matplotlib/backends/web_static/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
onkeydown="key_event(event, 'key_press')"
88
onkeyup="key_event(event, 'key_release')">
99
<div id="mpl-div">
10-
<canvas id="mpl-canvas" width="800" height="600"
10+
<canvas id="mpl-canvas"
11+
class="mpl-canvas"
1112
onmousedown="mouse_event(event, 'button_press')"
1213
onmouseup="mouse_event(event, 'button_release')"
1314
onmousemove="mouse_event(event, 'motion_notify')">
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1+
.mpl-canvas {
2+
width:800px;
3+
height:600px;
4+
}
5+
16
.mpl-message {
27
float:right;
38
vertical-align:text-center;
49
}
510

611
.mpl-toolbar-spacer {
7-
padding:8px
12+
padding:8px;
813
}
914

1015
.mpl-toolbar-button {
11-
padding:0px
12-
margin:0px
16+
padding:0px;
17+
margin:0px;
1318
}

lib/matplotlib/rcsetup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from matplotlib.colors import is_color_like
2121

2222
#interactive_bk = ['gtk', 'gtkagg', 'gtkcairo', 'fltkagg', 'qtagg', 'qt4agg',
23-
# 'tkagg', 'wx', 'wxagg', 'cocoaagg', 'web']
23+
# 'tkagg', 'wx', 'wxagg', 'cocoaagg', 'webagg']
2424
# The capitalized forms are needed for ipython at present; this may
2525
# change for later versions.
2626

@@ -389,6 +389,7 @@ def __call__(self, s):
389389
'backend.qt4' : ['PyQt4', validate_qt4],
390390
'webagg.port' : [8888, validate_int],
391391
'webagg.open_in_browser' : [True, validate_bool],
392+
'webagg.port_retries' : [50, validate_int],
392393
'toolbar' : ['toolbar2', validate_toolbar],
393394
'datapath' : [None, validate_path_exists], # handled by _get_data_path_cached
394395
'interactive' : [False, validate_bool],

matplotlibrc.template

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,13 @@ backend : %(backend)s
4141
# "pyqt" and "pyside". The "pyqt" setting has the side effect of
4242
# forcing the use of Version 2 API for QString and QVariant.
4343

44-
# The port to use for the web server in the WebAgg backend
44+
# The port to use for the web server in the WebAgg backend.
4545
# webagg.port : 8888
4646

47+
# If webagg.port is unavailable, a number of other random ports will
48+
# be tried until one that is available is found.
49+
# webagg.port_retries : 50
50+
4751
# When True, open the webbrowser to the plot that is shown
4852
# webagg.open_in_browser : True
4953

setup.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
check_for_qt, check_for_qt4, check_for_pyside, check_for_cairo, \
4949
check_provide_pytz, check_provide_dateutil,\
5050
check_for_dvipng, check_for_ghostscript, check_for_latex, \
51-
check_for_pdftops, options, build_png, build_tri, check_provide_six
51+
check_for_pdftops, options, build_png, build_tri, check_provide_six, \
52+
check_for_tornado
5253

5354

5455
packages = [
@@ -197,6 +198,7 @@ def chop_package(fname):
197198
check_for_qt4()
198199
check_for_pyside()
199200
check_for_cairo()
201+
check_for_tornado()
200202

201203
print_raw("")
202204
print_raw("OPTIONAL DATE/TIMEZONE DEPENDENCIES")

setupext.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,16 @@ def check_for_cairo():
454454
print_status("Cairo", cairo.version)
455455
return True
456456

457+
def check_for_tornado():
458+
try:
459+
import tornado
460+
except ImportError:
461+
print_status("Tornado (webagg)", "no")
462+
return False
463+
else:
464+
print_status("Tornado (webagg)", tornado.version)
465+
return True
466+
457467
def check_provide_pytz():
458468
if options['provide_pytz'] is True:
459469
print_status("pytz", "matplotlib will provide")

0 commit comments

Comments
 (0)