3
3
"""
4
4
from __future__ import division , print_function
5
5
6
- import cStringIO
7
6
import datetime
8
7
import errno
8
+ import io
9
9
import json
10
10
import os
11
11
import random
@@ -104,10 +104,23 @@ class FigureCanvasWebAgg(backend_agg.FigureCanvasAgg):
104
104
105
105
def __init__ (self , * args , ** kwargs ):
106
106
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
111
124
112
125
def show (self ):
113
126
# show the figure window
@@ -117,7 +130,7 @@ def draw(self):
117
130
# TODO: Do we just queue the drawing here? That's what Gtk does
118
131
renderer = self .get_renderer ()
119
132
120
- self .png_is_old = True
133
+ self ._png_is_old = True
121
134
122
135
backend_agg .RendererAgg .lock .acquire ()
123
136
try :
@@ -128,67 +141,75 @@ def draw(self):
128
141
self .manager .refresh_all ()
129
142
130
143
def draw_idle (self ):
131
- if self .pending_draw is None :
144
+ if self ._pending_draw is None :
132
145
ioloop = tornado .ioloop .IOLoop .instance ()
133
- self .pending_draw = ioloop .add_timeout (
146
+ self ._pending_draw = ioloop .add_timeout (
134
147
datetime .timedelta (milliseconds = 50 ),
135
148
self ._draw_idle_callback )
136
149
137
150
def _draw_idle_callback (self ):
138
151
try :
139
152
self .draw ()
140
153
finally :
141
- self .pending_draw = None
154
+ self ._pending_draw = None
142
155
143
156
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.
145
161
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 )
149
165
150
- if not self .force_full :
166
+ if not self ._force_full :
151
167
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 )
155
171
156
172
diff = buffer != last_buffer
157
173
output = np .where (diff , buffer , 0 )
158
174
else :
159
175
output = buffer
160
176
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
+
163
183
# TODO: We should write a new version of write_png that
164
184
# handles the differencing inline
165
185
_png .write_png (
166
186
output .tostring (),
167
187
output .shape [1 ], output .shape [0 ],
168
- self .png_buffer )
188
+ self ._png_buffer )
169
189
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 ()
175
196
176
197
def get_renderer (self ):
177
198
l , b , w , h = self .figure .bbox .bounds
178
199
key = w , h , self .figure .dpi
179
200
try :
180
- self ._lastKey , self .renderer
201
+ self ._lastKey , self ._renderer
181
202
except AttributeError :
182
203
need_new_renderer = True
183
204
else :
184
205
need_new_renderer = (self ._lastKey != key )
185
206
186
207
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 )
189
210
self ._lastKey = key
190
211
191
- return self .renderer
212
+ return self ._renderer
192
213
193
214
def handle_event (self , event ):
194
215
type = event ['type' ]
@@ -201,8 +222,10 @@ def handle_event(self, event):
201
222
# off by 1
202
223
button = event ['button' ] + 1
203
224
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.
206
229
if button == 2 :
207
230
button = 3
208
231
@@ -223,7 +246,7 @@ def handle_event(self, event):
223
246
# TODO: Be more suspicious of the input
224
247
getattr (self .toolbar , event ['name' ])()
225
248
elif type == 'refresh' :
226
- self .force_full = True
249
+ self ._force_full = True
227
250
self .draw_idle ()
228
251
229
252
def send_event (self , event_type , ** kwargs ):
@@ -248,9 +271,6 @@ def __init__(self, canvas, num):
248
271
249
272
self .web_sockets = set ()
250
273
251
- self .canvas = canvas
252
- self .num = num
253
-
254
274
self .toolbar = self ._get_toolbar (canvas )
255
275
256
276
def show (self ):
@@ -279,16 +299,9 @@ def resize(self, w, h):
279
299
280
300
281
301
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 ]) + [
290
303
('Download' , 'Download plot' , 'filesave' , 'download' )
291
- )
304
+ ]
292
305
293
306
def _init_toolbar (self ):
294
307
self .message = ''
@@ -331,7 +344,7 @@ def get(self, fignum):
331
344
tpl = fd .read ()
332
345
333
346
fignum = int (fignum )
334
- manager = Gcf () .get_fig_manager (fignum )
347
+ manager = Gcf .get_fig_manager (fignum )
335
348
336
349
t = tornado .template .Template (tpl )
337
350
self .write (t .generate (
@@ -341,7 +354,7 @@ def get(self, fignum):
341
354
class Download (tornado .web .RequestHandler ):
342
355
def get (self , fignum , format ):
343
356
self .fignum = int (fignum )
344
- manager = Gcf () .get_fig_manager (self .fignum )
357
+ manager = Gcf .get_fig_manager (self .fignum )
345
358
346
359
# TODO: Move this to a central location
347
360
mimetypes = {
@@ -357,25 +370,25 @@ def get(self, fignum, format):
357
370
358
371
self .set_header ('Content-Type' , mimetypes .get (format , 'binary' ))
359
372
360
- buffer = cStringIO . StringIO ()
373
+ buffer = io . BytesIO ()
361
374
manager .canvas .print_figure (buffer , format = format )
362
375
self .write (buffer .getvalue ())
363
376
364
377
class WebSocket (tornado .websocket .WebSocketHandler ):
365
378
def open (self , fignum ):
366
379
self .fignum = int (fignum )
367
- manager = Gcf () .get_fig_manager (self .fignum )
380
+ manager = Gcf .get_fig_manager (self .fignum )
368
381
manager .add_web_socket (self )
369
382
l , b , w , h = manager .canvas .figure .bbox .bounds
370
383
manager .resize (w , h )
371
384
self .on_message ('{"type":"refresh"}' )
372
385
373
386
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 )
375
388
376
389
def on_message (self , message ):
377
390
message = json .loads (message )
378
- canvas = Gcf () .get_fig_manager (self .fignum ).canvas
391
+ canvas = Gcf .get_fig_manager (self .fignum ).canvas
379
392
canvas .handle_event (message )
380
393
381
394
def send_event (self , event_type , ** kwargs ):
@@ -384,7 +397,7 @@ def send_event(self, event_type, **kwargs):
384
397
self .write_message (json .dumps (payload ))
385
398
386
399
def send_image (self ):
387
- canvas = Gcf () .get_fig_manager (self .fignum ).canvas
400
+ canvas = Gcf .get_fig_manager (self .fignum ).canvas
388
401
diff = canvas .get_diff_image ()
389
402
self .write_message (diff , binary = True )
390
403
@@ -416,8 +429,11 @@ def initialize(cls):
416
429
417
430
app = cls ()
418
431
432
+ # This port selection algorithm is borrowed, more or less
433
+ # verbatim, from IPython.
419
434
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.
421
437
422
438
The first 5 ports will be sequential, and the remaining n-5 will be
423
439
randomly selected in the range [port-2*n, port+2*n].
@@ -429,8 +445,7 @@ def random_ports(port, n):
429
445
430
446
success = None
431
447
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' ]):
434
449
try :
435
450
app .listen (port )
436
451
except socket .error as e :
0 commit comments