Package csb :: Package io :: Module plots
[frames] | no frames]

Source Code for Module csb.io.plots

  1  """ 
  2  Plotting facilities, based on Python's MPL library. 
  3   
  4  The L{Chart} object is a facade which provides intuitive access to MPL's plotting 
  5  objects. The following examples show a typical use of L{Chart}: 
  6   
  7      1. Instantiation 
  8       
  9      >>> chart = Chart()                     # a chart with a single plot     
 10      >>> chart = Chart(rows=2, columns=2)    # a chart with 2x2=4 plots 
 11       
 12      2. Accessing plots (equivalent to MPL's subplots) 
 13   
 14      >>> chart.plots[0]                      # first plot (at row=0, column=0)  
 15      Plot (matplotlib.axes.AxesSubplot)     
 16      >>> chart.plots[0, 1] 
 17      Plot (matplotlib.axes.AxesSubplot)      # plot at row=0, column=1 
 18       
 19      3. Plotting 
 20       
 21      >>> chart.plots[0].hist(...) 
 22      >>> chart.plots[0].set_title('T') 
 23           
 24      >>> chart.plots[1].scatter(...) 
 25      >>> chart.plots[1].set_xlabel('X') 
 26       
 27      4. Using the GUI 
 28       
 29      >>> chart.show() 
 30      >>> chart.hide() 
 31       
 32      5. Saving as image 
 33       
 34      >>> chart.save(filename, format=chart.formats.PDF) 
 35       
 36  If using the GUI, do not forget to dispose the chart at the end: 
 37   
 38      >>> chart.dispose() 
 39       
 40  or simply use the chart in a context manager: 
 41   
 42      >>> with Chart() as chart: 
 43              chart... 
 44  """ 
 45   
 46  import time 
 47  import csb.core 
 48   
 49  from abc import ABCMeta, abstractmethod 
 50   
 51  from threading import Thread, Event 
 52   
 53  from matplotlib.figure import Figure 
 54  from matplotlib.backends.backend_agg import FigureCanvasAgg 
55 56 57 -class Backend(Thread):
58 """ 59 Abstract class defining the behavior of all Chart GUI backends. 60 61 Each backend is a 'daemon' that runs in a new thread and handles 62 all GUI requests from any L{Chart} instance. A backend service must 63 behave as a singleton - only one service of a given kind may exist at a 64 given point in time. L{Chart} clients will request GUI operations 65 on specific figures, the Backend therefore must keep track of all 66 windows opened, as well as the figure-to-window mapping. 67 """ 68 69 __metaclass__ = ABCMeta 70 _instances = {} 71 72 @staticmethod
73 - def get(backend, started=True):
74 """ 75 Backend factory, ensures one instance per subclass. 76 77 @param backend: one of the L{Backend} subclasses 78 @type backend: type 79 @param started: if True, ensure that the service is running 80 @type started: bool 81 82 @return: an instance of the backend. The returned service 83 instance may need to be started. 84 @rtype: L{Backend} 85 """ 86 87 if not issubclass(backend, Backend): 88 raise TypeError(backend) 89 90 if backend in Backend._instances: 91 instance = Backend._instances[backend] 92 else: 93 instance = backend() 94 95 if started and not instance.started: 96 instance.start() 97 return instance
98 99 @staticmethod
100 - def query(backend):
101 """ 102 @param backend: one of the L{Backend} subclasses 103 @type backend: type 104 105 @return: True if a service of type C{backend} is running 106 @rtype: bool 107 """ 108 109 if not issubclass(backend, Backend): 110 raise TypeError(backend) 111 112 if backend in Backend._instances: 113 instance = Backend._instances[backend] 114 return instance.started 115 116 else: 117 return False
118
119 - def __init__(self):
120 121 name = self.__class__ 122 if name in Backend._instances: 123 raise RuntimeError('Backend {0} has already been initialized'.format(name)) 124 else: 125 Backend._instances[name] = self 126 127 super(Backend, self).__init__() 128 129 self._figures = {} 130 self._started = Event() 131 self._running = Event() 132 133 self.setDaemon(True)
134 135 @property
136 - def started(self):
137 """ 138 True if the service had been started 139 @rtype: bool 140 """ 141 return self._started.isSet()
142 143 @property
144 - def running(self):
145 """ 146 True if the service had been started and is currently running 147 @rtype: bool 148 """ 149 return self._running.isSet()
150 151 @abstractmethod
152 - def _initapp(self):
153 """ 154 Create an instance of the GUI application. 155 """ 156 pass
157 158 @abstractmethod
159 - def _mainloop(self):
160 """ 161 Enter the GUI main loop. 162 """ 163 pass
164 165 @abstractmethod
166 - def _exit(self):
167 """ 168 Delete all frames, exit the GUI main loop and perform any cleanup 169 needed in order to unblock the thread that started the main loop. 170 """ 171 pass
172 173 @abstractmethod
174 - def _add(self, figure):
175 """ 176 Handle an 'Add new figure' event 177 """ 178 pass
179 180 @abstractmethod
181 - def _show(self, figure):
182 """ 183 Handle a 'Show existing figure' event 184 """ 185 pass
186 187 @abstractmethod
188 - def _resize(self, figure):
189 """ 190 Handle a 'Resize existing figure' event 191 """ 192 pass
193 194 @abstractmethod
195 - def _hide(self, figure):
196 """ 197 Handle a 'Hide existing figure' event 198 """ 199 pass
200 201 @abstractmethod
202 - def _destroy(self, figure):
203 """ 204 Handle a 'Delete existing figure' event 205 """ 206 pass
207 208 @abstractmethod
209 - def _invoke(self, callable, *args):
210 """ 211 Pass a GUI message: invoke C{callable} in a thread-safe way 212 """ 213 pass
214
215 - def invoke(self, callable, *args):
216 """ 217 Invoke an asynchronous GUI operation (in a thread-safe way) 218 """ 219 if not self._running.isSet(): 220 raise RuntimeError('The backend service is not running') 221 else: 222 self._invoke(callable, *args)
223
224 - def add(self, figure):
225 """ 226 Add a new figure. 227 """ 228 self.invoke(self._add, figure)
229
230 - def show(self, figure):
231 """ 232 Show existing figure. 233 """ 234 self.invoke(self._show, figure)
235
236 - def resize(self, figure):
237 """ 238 Resize existing figure. 239 """ 240 self.invoke(self._resize, figure)
241
242 - def hide(self, figure):
243 """ 244 Hide existing figure. 245 """ 246 self.invoke(self._hide, figure)
247
248 - def destroy(self, figure, wait=False):
249 """ 250 Destroy existing figure. If C{wait} is True, make sure the asynchronous 251 figure deletion is complete before returning from the method. 252 """ 253 has_figure = (figure in self._figures) 254 self.invoke(self._destroy, figure) 255 256 if has_figure and wait: 257 while figure in self._figures: 258 pass
259
260 - def start(self):
261 """ 262 Start the Backend service. This method can be called only once. 263 """ 264 try: 265 super(Backend, self).start() 266 267 while not self._running.isSet(): 268 time.sleep(0.05) 269 270 except BaseException: 271 raise RuntimeError("Failed to start the backend service")
272
273 - def run(self):
274 """ 275 Main service method, automatically called by C{start}. 276 """ 277 self._started.set() 278 279 self._initapp() 280 self._running.set() 281 282 self._mainloop() 283 284 self._running.clear() 285 self._started.clear()
286
287 - def stop(self):
288 """ 289 Stop the Backend service. The Backend object can be safely 290 disposed afterwards. 291 """ 292 self._exit() 293 self._figures = {} 294 self.join() 295 self._running.clear() 296 self._started.clear() 297 del Backend._instances[self.__class__]
298
299 - def client_disposed(self, client):
300 """ 301 Fired when a client is being deleted. Will stop the service if no 302 active clients are remaining. 303 """ 304 if self._figures is None or len(self._figures) == 0: 305 self.stop()
306
307 - def __del__(self):
308 if self._started.isSet(): 309 self.stop()
310
311 -class WxBackendImpl(Backend):
312 """ 313 WxPython L{Backend} implementor. 314 315 @note: not meant to be instantiated directly, use L{Backend.get} instead. 316 """ 317 318 _wxapp = None 319
320 - def __init__(self):
321 322 import wx 323 from matplotlib.backends.backend_wx import FigureFrameWx 324 325 self._wx = wx 326 self._FigureFrameWx = FigureFrameWx 327 328 super(WxBackendImpl, self).__init__()
329 330 @property
331 - def _app(self):
332 if WxBackendImpl._wxapp is None: 333 WxBackendImpl._wxapp = self._wx.PySimpleApp() 334 return WxBackendImpl._wxapp
335
336 - def _initapp(self):
337 338 dummy = self._app 339 frame = self._wx.Frame(None) 340 frame.Show() 341 frame.Hide()
342
343 - def _mainloop(self):
344 345 self._app.MainLoop()
346
347 - def _add(self, figure):
348 349 wx = self._wx 350 FigureFrameWx = self._FigureFrameWx 351 352 if figure not in self._figures: 353 354 frame = FigureFrameWx(figure._figure_number, figure) 355 frame.Show() 356 frame.Bind(wx.EVT_ACTIVATE, lambda e: e.GetEventObject().Layout()) 357 frame.Bind(wx.EVT_CLOSE, lambda e: self.invoke(self._hide, figure)) 358 359 self._figures[figure] = frame
360
361 - def _show(self, figure):
362 363 if figure not in self._figures: 364 self._add(figure) 365 366 self._figures[figure].Show()
367
368 - def _resize(self, figure):
369 370 if figure in self._figures: 371 372 frame = self._figures[figure] 373 374 w = figure.get_figwidth() * figure.get_dpi() 375 h = figure.get_figheight() * figure.get_dpi() 376 377 size = self._wx.Size(w, h) 378 frame.canvas.SetInitialSize(size) 379 frame.GetSizer().Fit(frame)
380
381 - def _hide(self, figure):
382 383 if figure in self._figures: 384 self._figures[figure].Hide()
385
386 - def _destroy(self, figure):
387 388 if figure in self._figures: 389 frame = self._figures[figure] 390 if not frame.IsBeingDeleted(): 391 frame.Destroy() 392 del self._figures[figure]
393
394 - def _invoke(self, callable, *args):
395 396 wx = self._wx 397 wx.CallAfter(callable, *args)
398
399 - def _exit(self):
400 401 for frame in self._figures.values(): 402 if not frame.IsBeingDeleted(): 403 frame.Destroy() 404 self._app.Exit()
405
406 -class Backends(object):
407 """ 408 Enumeration of chart backends. 409 """ 410 411 WX_WIDGETS = WxBackendImpl
412
413 414 -class PlotsCollection(object):
415 """ 416 A list-like collection of all plots in the chart (0-based). 417 """ 418
419 - def __init__(self, figure, rows=1, columns=1):
420 421 assert rows >= 1 and columns >= 1 422 423 self._plots = [] 424 self._figure = figure 425 self._rows = int(rows) 426 self._columns = int(columns) 427 428 for dummy in range(self._rows * self._columns): 429 self._plots.append(None)
430 431 @property
432 - def _active_plots(self):
433 return [p for p in self._plots if p is not None]
434
435 - def _add(self, index=1):
436 437 assert 0 <= index < len(self._plots) 438 439 plot = self._figure.add_subplot(self._rows, self._columns, index + 1) 440 self._plots[index] = plot 441 442 return plot
443
444 - def __getitem__(self, location):
445 446 if isinstance(location, tuple): 447 row, col = location 448 i = row * self._columns + col 449 else: 450 i = int(location) 451 452 if not (0 <= i < len(self._plots)): 453 raise IndexError("No such plot: {0}".format(location)) 454 455 if self._plots[i] is None: 456 return self._add(i) 457 else: 458 return self._plots[i]
459
460 - def __len__(self):
461 return len(self._active_plots)
462
463 - def __iter__(self):
464 return iter(self._active_plots)
465
466 467 -class Chart(object):
468 """ 469 Simple and clean facade to Matplotlib's plotting API. 470 471 A chart instance abstracts a plotting device, on which one or 472 multiple related plots can be drawn. Charts can be exported as images, or 473 visualized interactively. Each chart instance will always open in its own 474 GUI window, and this window will never block the execution of the rest of 475 the program, or interfere with other L{Chart}s. 476 The GUI can be safely opened in the background and closed infinite number 477 of times, as long as the client program is still running. 478 479 By default, a chart contains a single plot: 480 481 >>> chart.plot 482 matplotlib.axes.AxesSubplot 483 >>> chart.plot.hist(...) 484 485 If C{rows} and C{columns} are defined, the chart will contain 486 C{rows} x C{columns} number of plots (equivalent to MPL's sub-plots). 487 Each plot can be assessed by its index: 488 489 >>> chart.plots[0] 490 first plot 491 492 or by its position in the grid: 493 494 >>> chart.plots[0, 1] 495 plot at row=0, column=1 496 497 @param number: chart number; by default this a L{Chart.AUTONUMBER} 498 @type number: int or None 499 @param title: chart master title 500 @type title: str 501 @param rows: number of rows in the chart window 502 @type rows: int 503 @param columns: number of columns in the chart window 504 @type columns: int 505 506 @note: additional arguments are passed directly to Matplotlib's Figure 507 constructor. 508 """ 509 510 AUTONUMBER = None 511 512 _serial = 0 513 514
515 - def __init__(self, number=None, title='', rows=1, columns=1, backend=Backends.WX_WIDGETS, *fa, **fk):
516 517 if number == Chart.AUTONUMBER: 518 Chart._serial += 1 519 number = Chart._serial 520 521 if rows < 1: 522 rows = 1 523 if columns < 1: 524 columns = 1 525 526 self._rows = int(rows) 527 self._columns = int(columns) 528 self._number = int(number) 529 self._title = str(title) 530 self._figure = Figure(*fa, **fk) 531 self._figure._figure_number = self._number 532 self._figure.suptitle(self._title) 533 self._beclass = backend 534 self._hasgui = False 535 self._plots = PlotsCollection(self._figure, self._rows, self._columns) 536 self._canvas = FigureCanvasAgg(self._figure) 537 538 formats = [ (f.upper(), f) for f in self._canvas.get_supported_filetypes() ] 539 self._formats = csb.core.Enum.create('OutputFormats', **dict(formats))
540
541 - def __getitem__(self, i):
542 if i in self._plots: 543 return self._plots[i] 544 else: 545 raise KeyError('No such plot number: {0}'.format(i))
546
547 - def __enter__(self):
548 return self
549
550 - def __exit__(self, *a, **k):
551 self.dispose()
552 553 @property
554 - def _backend(self):
555 return Backend.get(self._beclass, started=True)
556 557 @property
558 - def _backend_started(self):
559 return Backend.query(self._beclass)
560 561 @property
562 - def title(self):
563 """ 564 Chart title 565 @rtype: str 566 """ 567 return self._title
568 569 @property
570 - def number(self):
571 """ 572 Chart number 573 @rtype: int 574 """ 575 return self._number
576 577 @property
578 - def plots(self):
579 """ 580 Index-based access to the plots in this chart 581 @rtype: L{PlotsCollection} 582 """ 583 return self._plots
584 585 @property
586 - def plot(self):
587 """ 588 First plot 589 @rtype: matplotlib.AxesSubplot 590 """ 591 return self._plots[0]
592 593 @property
594 - def rows(self):
595 """ 596 Number of rows in this chart 597 @rtype: int 598 """ 599 return self._rows
600 601 @property
602 - def columns(self):
603 """ 604 Number of columns in this chart 605 @rtype: int 606 """ 607 return self._columns
608 609 @property
610 - def width(self):
611 """ 612 Chart's width in inches 613 @rtype: int 614 """ 615 return self._figure.get_figwidth()
616 @width.setter
617 - def width(self, inches):
618 self._figure.set_figwidth(inches) 619 if self._backend_started: 620 self._backend.resize(self._figure)
621 622 @property
623 - def height(self):
624 """ 625 Chart's height in inches 626 @rtype: int 627 """ 628 return self._figure.get_figheight()
629 @height.setter
630 - def height(self, inches):
631 self._figure.set_figheight(inches) 632 if self._backend_started: 633 self._backend.resize(self._figure)
634 635 @property
636 - def dpi(self):
637 """ 638 Chart's DPI 639 @rtype: int 640 """ 641 return self._figure.get_dpi()
642 @dpi.setter
643 - def dpi(self, dpi):
644 self._figure.set_dpi(dpi) 645 self._backend.resize(self._figure)
646 647 @property
648 - def formats(self):
649 """ 650 Supported output file formats 651 @rtype: L{csb.core.enum} 652 """ 653 return self._formats
654
655 - def show(self):
656 """ 657 Show the GUI window (non-blocking). 658 """ 659 if not self._hasgui: 660 self._backend.add(self._figure) 661 self._hasgui = True 662 663 self._backend.show(self._figure)
664
665 - def hide(self):
666 """ 667 Hide (but do not dispose) the GUI window. 668 """ 669 self._backend.hide(self._figure)
670
671 - def dispose(self):
672 """ 673 Dispose the GUI interface. Must be called at the end if any 674 chart.show() calls have been made. Automatically called if using 675 the chart in context manager ("with" statement). 676 677 @note: Failing to call this method if show() has been called at least 678 once may cause backend-related errors. 679 """ 680 if self._backend_started: 681 682 service = self._backend 683 684 if service and service.running: 685 service.destroy(self._figure, wait=True) 686 service.client_disposed(self)
687
688 - def save(self, file, format='png', crop=False, dpi=None, *a, **k):
689 """ 690 Save all plots to an image. 691 692 @param file: destination file name 693 @type file: str 694 @param format: output image format; see C{chart.formats} for enumeration 695 @type format: str or L{csb.core.EnumItem} 696 @param crop: if True, crop the image (equivalent to MPL's bbox=tight) 697 @type crop: bool 698 699 @note: additional arguments are passed directly to MPL's savefig method 700 """ 701 if 'bbox_inches' in k: 702 bbox = k['bbox_inches'] 703 del k['bbox_inches'] 704 else: 705 if crop: 706 bbox = 'tight' 707 else: 708 bbox = None 709 710 self._canvas.print_figure(file, format=str(format), bbox_inches=bbox, dpi=dpi, *a, **k)
711