33"""
44from __future__ import division , print_function
55
6- import cStringIO
76import datetime
87import errno
8+ import io
99import json
1010import os
1111import 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
281301class 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 :
0 commit comments