diff --git a/doc/users/whats_new.rst b/doc/users/whats_new.rst index b7a2f890662d..64299430243c 100644 --- a/doc/users/whats_new.rst +++ b/doc/users/whats_new.rst @@ -70,6 +70,17 @@ minimum and maximum colorbar extensions. plt.show() +Figures are picklable +--------------------- + +Philip Elson added an experimental feature to make figures picklable +for quick and easy short-term storage of plots. Pickle files +are not designed for long term storage, are unsupported when restoring a pickle +saved in another matplotlib version and are insecure when restoring a pickle +from an untrusted source. Having said this, they are useful for short term +storage for later modification inside matplotlib. + + Set default bounding box in matplotlibrc ------------------------------------------ diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 232ac22618cf..deb568e3dcaa 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1068,6 +1068,7 @@ def tk_window_focus(): 'matplotlib.tests.test_mathtext', 'matplotlib.tests.test_mlab', 'matplotlib.tests.test_patches', + 'matplotlib.tests.test_pickle', 'matplotlib.tests.test_rcparams', 'matplotlib.tests.test_simplification', 'matplotlib.tests.test_spines', diff --git a/lib/matplotlib/_pylab_helpers.py b/lib/matplotlib/_pylab_helpers.py index f6b61868cecf..6901e0482170 100644 --- a/lib/matplotlib/_pylab_helpers.py +++ b/lib/matplotlib/_pylab_helpers.py @@ -14,7 +14,7 @@ def error_msg(msg): class Gcf(object): """ - Manage a set of integer-numbered figures. + Singleton to manage a set of integer-numbered figures. This class is never instantiated; it consists of two class attributes (a list and a dictionary), and a set of static @@ -131,6 +131,7 @@ def set_active(manager): if m != manager: Gcf._activeQue.append(m) Gcf._activeQue.append(manager) Gcf.figs[manager.num] = manager + atexit.register(Gcf.destroy_all) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 6ce12b083748..afe5ec9e9e64 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -104,6 +104,13 @@ def __init__(self): self.y_isdata = True # with y self._snap = None + def __getstate__(self): + d = self.__dict__.copy() + # remove the unpicklable remove method, this will get re-added on load + # (by the axes) if the artist lives on an axes. + d['_remove_method'] = None + return d + def remove(self): """ Remove the artist from the figure if possible. The effect @@ -123,7 +130,7 @@ def remove(self): # the _remove_method attribute directly. This would be a protected # attribute if Python supported that sort of thing. The callback # has one parameter, which is the child to be removed. - if self._remove_method != None: + if self._remove_method is not None: self._remove_method(self) else: raise NotImplementedError('cannot remove artist') diff --git a/lib/matplotlib/axes.py b/lib/matplotlib/axes.py index 6c7e833e09dd..2b504444326e 100644 --- a/lib/matplotlib/axes.py +++ b/lib/matplotlib/axes.py @@ -154,9 +154,8 @@ def set_default_color_cycle(clist): DeprecationWarning) -class _process_plot_var_args: +class _process_plot_var_args(object): """ - Process variable length arguments to the plot command, so that plot commands like the following are supported:: @@ -172,6 +171,14 @@ def __init__(self, axes, command='plot'): self.command = command self.set_color_cycle() + def __getstate__(self): + # note: it is not possible to pickle a itertools.cycle instance + return {'axes': self.axes, 'command': self.command} + + def __setstate__(self, state): + self.__dict__ = state.copy() + self.set_color_cycle() + def set_color_cycle(self, clist=None): if clist is None: clist = rcParams['axes.color_cycle'] @@ -332,7 +339,7 @@ def _grab_next_args(self, *args, **kwargs): for seg in self._plot_args(remaining[:isplit], kwargs): yield seg remaining=remaining[isplit:] - + class Axes(martist.Artist): """ @@ -352,9 +359,10 @@ class Axes(martist.Artist): _shared_x_axes = cbook.Grouper() _shared_y_axes = cbook.Grouper() - + def __str__(self): return "Axes(%g,%g;%gx%g)" % tuple(self._position.bounds) + def __init__(self, fig, rect, axisbg = None, # defaults to rc axes.facecolor frameon = True, @@ -489,6 +497,15 @@ def __init__(self, fig, rect, self._ycid = self.yaxis.callbacks.connect('units finalize', self.relim) + def __setstate__(self, state): + self.__dict__ = state + # put the _remove_method back on all artists contained within the axes + for container_name in ['lines', 'collections', 'tables', 'patches', + 'texts', 'images']: + container = getattr(self, container_name) + for artist in container: + artist._remove_method = container.remove + def get_window_extent(self, *args, **kwargs): """ get the axes bounding box in display space; *args* and @@ -1599,13 +1616,13 @@ def _process_unit_info(self, xdata=None, ydata=None, kwargs=None): if xdata is not None: # we only need to update if there is nothing set yet. if not self.xaxis.have_units(): - self.xaxis.update_units(xdata) + self.xaxis.update_units(xdata) #print '\tset from xdata', self.xaxis.units if ydata is not None: # we only need to update if there is nothing set yet. if not self.yaxis.have_units(): - self.yaxis.update_units(ydata) + self.yaxis.update_units(ydata) #print '\tset from ydata', self.yaxis.units # process kwargs 2nd since these will override default units @@ -8770,7 +8787,15 @@ def __init__(self, fig, *args, **kwargs): # _axes_class is set in the subplot_class_factory self._axes_class.__init__(self, fig, self.figbox, **kwargs) - + def __reduce__(self): + # get the first axes class which does not inherit from a subplotbase + axes_class = filter(lambda klass: (issubclass(klass, Axes) and + not issubclass(klass, SubplotBase)), + self.__class__.mro())[0] + r = [_PicklableSubplotClassConstructor(), + (axes_class,), + self.__getstate__()] + return tuple(r) def get_geometry(self): """get the subplot geometry, eg 2,2,3""" @@ -8852,6 +8877,21 @@ def subplot_class_factory(axes_class=None): # This is provided for backward compatibility Subplot = subplot_class_factory() + +class _PicklableSubplotClassConstructor(object): + """ + This stub class exists to return the appropriate subplot + class when __call__-ed with an axes class. This is purely to + allow Pickling of Axes and Subplots.""" + def __call__(self, axes_class): + # create a dummy object instance + subplot_instance = _PicklableSubplotClassConstructor() + subplot_class = subplot_class_factory(axes_class) + # update the class to the desired subplot class + subplot_instance.__class__ = subplot_class + return subplot_instance + + docstring.interpd.update(Axes=martist.kwdoc(Axes)) docstring.interpd.update(Subplot=martist.kwdoc(Axes)) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 43db8b9b7f9f..db658e060b73 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -595,7 +595,6 @@ class Ticker: formatter = None - class Axis(artist.Artist): """ diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index f8ffd36ef7bd..2cebf1f4c925 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -52,6 +52,6 @@ def do_nothing(*args, **kwargs): pass matplotlib.verbose.report('backend %s version %s' % (backend,backend_version)) - return new_figure_manager, draw_if_interactive, show + return backend_mod, new_figure_manager, draw_if_interactive, show diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 9c585d0c7d6f..da2cff90f38f 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -385,7 +385,6 @@ def post_processing(image, dpi): image) - def new_figure_manager(num, *args, **kwargs): """ Create a new figure manager instance @@ -396,7 +395,14 @@ def new_figure_manager(num, *args, **kwargs): FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*args, **kwargs) - canvas = FigureCanvasAgg(thisFig) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasAgg(figure) manager = FigureManagerBase(canvas, num) return manager diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index eeb91713d6f4..3694d521542f 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -397,10 +397,17 @@ def new_figure_manager(num, *args, **kwargs): # called by backends/__init__.py """ Create a new figure manager instance """ - if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) + if _debug: print('%s.%s()' % (_fn_name())) FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*args, **kwargs) - canvas = FigureCanvasCairo(thisFig) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasCairo(figure) manager = FigureManagerBase(canvas, num) return manager diff --git a/lib/matplotlib/backends/backend_cocoaagg.py b/lib/matplotlib/backends/backend_cocoaagg.py index 7a21f079833a..e4743cec9eda 100644 --- a/lib/matplotlib/backends/backend_cocoaagg.py +++ b/lib/matplotlib/backends/backend_cocoaagg.py @@ -35,12 +35,21 @@ mplBundle = NSBundle.bundleWithPath_(os.path.dirname(__file__)) + def new_figure_manager(num, *args, **kwargs): FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass( *args, **kwargs ) - canvas = FigureCanvasCocoaAgg(thisFig) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasCocoaAgg(figure) return FigureManagerCocoaAgg(canvas, num) + ## Below is the original show() function: #def show(): # for manager in Gcf.get_all_fig_managers(): diff --git a/lib/matplotlib/backends/backend_emf.py b/lib/matplotlib/backends/backend_emf.py index 580798f641f0..cf112b848b40 100644 --- a/lib/matplotlib/backends/backend_emf.py +++ b/lib/matplotlib/backends/backend_emf.py @@ -688,7 +688,14 @@ def new_figure_manager(num, *args, **kwargs): # main-level app (egg backend_gtk, backend_gtkagg) for pylab FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*args, **kwargs) - canvas = FigureCanvasEMF(thisFig) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasEMF(figure) manager = FigureManagerEMF(canvas, num) return manager diff --git a/lib/matplotlib/backends/backend_fltkagg.py b/lib/matplotlib/backends/backend_fltkagg.py index fe31d0f81191..13900b7c9071 100644 --- a/lib/matplotlib/backends/backend_fltkagg.py +++ b/lib/matplotlib/backends/backend_fltkagg.py @@ -78,6 +78,13 @@ def new_figure_manager(num, *args, **kwargs): """ FigureClass = kwargs.pop('FigureClass', Figure) figure = FigureClass(*args, **kwargs) + return new_figure_manager_given_figure(num, figure) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ window = Fltk.Fl_Double_Window(10,10,30,30) canvas = FigureCanvasFltkAgg(figure) window.end() diff --git a/lib/matplotlib/backends/backend_gdk.py b/lib/matplotlib/backends/backend_gdk.py index 7d7f7e323d20..a94ca1fd11d0 100644 --- a/lib/matplotlib/backends/backend_gdk.py +++ b/lib/matplotlib/backends/backend_gdk.py @@ -422,11 +422,15 @@ def new_figure_manager(num, *args, **kwargs): """ FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*args, **kwargs) - canvas = FigureCanvasGDK(thisFig) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasGDK(figure) manager = FigureManagerBase(canvas, num) - # equals: - #manager = FigureManagerBase (FigureCanvasGDK (Figure(*args, **kwargs), - # num) return manager diff --git a/lib/matplotlib/backends/backend_gtk.py b/lib/matplotlib/backends/backend_gtk.py index b7199890225a..7cebe9877c04 100644 --- a/lib/matplotlib/backends/backend_gtk.py +++ b/lib/matplotlib/backends/backend_gtk.py @@ -90,10 +90,15 @@ def new_figure_manager(num, *args, **kwargs): """ FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*args, **kwargs) - canvas = FigureCanvasGTK(thisFig) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasGTK(figure) manager = FigureManagerGTK(canvas, num) - # equals: - #manager = FigureManagerGTK(FigureCanvasGTK(Figure(*args, **kwargs), num) return manager diff --git a/lib/matplotlib/backends/backend_gtk3agg.py b/lib/matplotlib/backends/backend_gtk3agg.py index da14f356ba0a..fcf9a7914f2d 100644 --- a/lib/matplotlib/backends/backend_gtk3agg.py +++ b/lib/matplotlib/backends/backend_gtk3agg.py @@ -78,7 +78,14 @@ def new_figure_manager(num, *args, **kwargs): """ FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*args, **kwargs) - canvas = FigureCanvasGTK3Agg(thisFig) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasGTK3Agg(figure) manager = FigureManagerGTK3Agg(canvas, num) return manager diff --git a/lib/matplotlib/backends/backend_gtk3cairo.py b/lib/matplotlib/backends/backend_gtk3cairo.py index 21abae2a4762..bf3e34d09142 100644 --- a/lib/matplotlib/backends/backend_gtk3cairo.py +++ b/lib/matplotlib/backends/backend_gtk3cairo.py @@ -45,7 +45,14 @@ def new_figure_manager(num, *args, **kwargs): """ FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*args, **kwargs) - canvas = FigureCanvasGTK3Cairo(thisFig) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasGTK3Cairo(figure) manager = FigureManagerGTK3Cairo(canvas, num) return manager diff --git a/lib/matplotlib/backends/backend_gtkagg.py b/lib/matplotlib/backends/backend_gtkagg.py index c8f359f8c0f6..5abc56b3612d 100644 --- a/lib/matplotlib/backends/backend_gtkagg.py +++ b/lib/matplotlib/backends/backend_gtkagg.py @@ -33,6 +33,7 @@ def _get_toolbar(self, canvas): toolbar = None return toolbar + def new_figure_manager(num, *args, **kwargs): """ Create a new figure manager instance @@ -40,10 +41,18 @@ def new_figure_manager(num, *args, **kwargs): if DEBUG: print('backend_gtkagg.new_figure_manager') FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*args, **kwargs) - canvas = FigureCanvasGTKAgg(thisFig) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasGTKAgg(figure) return FigureManagerGTKAgg(canvas, num) if DEBUG: print('backend_gtkagg.new_figure_manager done') + class FigureCanvasGTKAgg(FigureCanvasGTK, FigureCanvasAgg): filetypes = FigureCanvasGTK.filetypes.copy() filetypes.update(FigureCanvasAgg.filetypes) diff --git a/lib/matplotlib/backends/backend_gtkcairo.py b/lib/matplotlib/backends/backend_gtkcairo.py index 616a3dc8ba71..baa51c54e438 100644 --- a/lib/matplotlib/backends/backend_gtkcairo.py +++ b/lib/matplotlib/backends/backend_gtkcairo.py @@ -26,7 +26,14 @@ def new_figure_manager(num, *args, **kwargs): if _debug: print('backend_gtkcairo.%s()' % fn_name()) FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*args, **kwargs) - canvas = FigureCanvasGTKCairo(thisFig) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasGTKCairo(figure) return FigureManagerGTK(canvas, num) diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index 7e402c302392..694baf8ec886 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -233,9 +233,20 @@ def new_figure_manager(num, *args, **kwargs): """ if not _macosx.verify_main_display(): import warnings - warnings.warn("Python is not installed as a framework. The MacOSX backend may not work correctly if Python is not installed as a framework. Please see the Python documentation for more information on installing Python as a framework on Mac OS X") + warnings.warn("Python is not installed as a framework. The MacOSX " + "backend may not work correctly if Python is not " + "installed as a framework. Please see the Python " + "documentation for more information on installing " + "Python as a framework on Mac OS X") FigureClass = kwargs.pop('FigureClass', Figure) figure = FigureClass(*args, **kwargs) + return new_figure_manager_given_figure(num, figure) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ canvas = FigureCanvasMac(figure) manager = FigureManagerMac(canvas, num) return manager diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 4cd830829eff..2e3e7bccf9ef 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2171,7 +2171,14 @@ def new_figure_manager(num, *args, **kwargs): # main-level app (egg backend_gtk, backend_gtkagg) for pylab FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*args, **kwargs) - canvas = FigureCanvasPdf(thisFig) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasPdf(figure) manager = FigureManagerPdf(canvas, num) return manager diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 2af23224f145..6ca5df558e37 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -944,7 +944,14 @@ def shouldstroke(self): def new_figure_manager(num, *args, **kwargs): FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*args, **kwargs) - canvas = FigureCanvasPS(thisFig) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasPS(figure) manager = FigureManagerPS(canvas, num) return manager diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 6acca884e6a0..8aae6b18bfde 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -69,9 +69,16 @@ def new_figure_manager( num, *args, **kwargs ): Create a new figure manager instance """ FigureClass = kwargs.pop('FigureClass', Figure) - thisFig = FigureClass( *args, **kwargs ) - canvas = FigureCanvasQT( thisFig ) - manager = FigureManagerQT( canvas, num ) + thisFig = FigureClass(*args, **kwargs) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasQT(figure) + manager = FigureManagerQT(canvas, num) return manager diff --git a/lib/matplotlib/backends/backend_qt4.py b/lib/matplotlib/backends/backend_qt4.py index 45fe444743d8..ff72226bb694 100644 --- a/lib/matplotlib/backends/backend_qt4.py +++ b/lib/matplotlib/backends/backend_qt4.py @@ -72,9 +72,16 @@ def new_figure_manager( num, *args, **kwargs ): """ Create a new figure manager instance """ - thisFig = Figure( *args, **kwargs ) - canvas = FigureCanvasQT( thisFig ) - manager = FigureManagerQT( canvas, num ) + thisFig = Figure(*args, **kwargs) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasQT(figure) + manager = FigureManagerQT(canvas, num) return manager diff --git a/lib/matplotlib/backends/backend_qt4agg.py b/lib/matplotlib/backends/backend_qt4agg.py index c47584da4a6f..20cd7ad60480 100644 --- a/lib/matplotlib/backends/backend_qt4agg.py +++ b/lib/matplotlib/backends/backend_qt4agg.py @@ -23,9 +23,17 @@ def new_figure_manager( num, *args, **kwargs ): if DEBUG: print('backend_qtagg.new_figure_manager') FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass( *args, **kwargs ) - canvas = FigureCanvasQTAgg( thisFig ) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasQTAgg(figure) return FigureManagerQT( canvas, num ) + class NavigationToolbar2QTAgg(NavigationToolbar2QT): def _get_canvas(self, fig): return FigureCanvasQTAgg(fig) diff --git a/lib/matplotlib/backends/backend_qtagg.py b/lib/matplotlib/backends/backend_qtagg.py index e7a3d89910b8..000d7bd4a3db 100644 --- a/lib/matplotlib/backends/backend_qtagg.py +++ b/lib/matplotlib/backends/backend_qtagg.py @@ -23,8 +23,16 @@ def new_figure_manager( num, *args, **kwargs ): if DEBUG: print('backend_qtagg.new_figure_manager') FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass( *args, **kwargs ) - canvas = FigureCanvasQTAgg( thisFig ) - return FigureManagerQTAgg( canvas, num ) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasQTAgg(figure) + return FigureManagerQTAgg(canvas, num) + class NavigationToolbar2QTAgg(NavigationToolbar2QT): def _get_canvas(self, fig): diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 0b9b7ab51701..ae83208d4d9f 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1151,10 +1151,18 @@ class FigureManagerSVG(FigureManagerBase): def new_figure_manager(num, *args, **kwargs): FigureClass = kwargs.pop('FigureClass', Figure) thisFig = FigureClass(*args, **kwargs) - canvas = FigureCanvasSVG(thisFig) + return new_figure_manager_given_figure(num, thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasSVG(figure) manager = FigureManagerSVG(canvas, num) return manager + svgProlog = u"""\ = 8.5: diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index c70b25e78bb5..da1dfc16e71e 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -1456,6 +1456,14 @@ def new_figure_manager(num, *args, **kwargs): FigureClass = kwargs.pop('FigureClass', Figure) fig = FigureClass(*args, **kwargs) + return new_figure_manager_given_figure(num, fig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + fig = figure frame = FigureFrameWx(num, fig) figmgr = frame.get_figure_manager() if matplotlib.is_interactive(): @@ -1463,6 +1471,7 @@ def new_figure_manager(num, *args, **kwargs): return figmgr + class FigureFrameWx(wx.Frame): def __init__(self, num, fig): # On non-Windows platform, explicitly set the position - fix diff --git a/lib/matplotlib/backends/backend_wxagg.py b/lib/matplotlib/backends/backend_wxagg.py index fa498ba0f3d4..c48dd28222bd 100644 --- a/lib/matplotlib/backends/backend_wxagg.py +++ b/lib/matplotlib/backends/backend_wxagg.py @@ -121,14 +121,20 @@ def new_figure_manager(num, *args, **kwargs): FigureClass = kwargs.pop('FigureClass', Figure) fig = FigureClass(*args, **kwargs) - frame = FigureFrameWxAgg(num, fig) + + return new_figure_manager_given_figure(num, fig) + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + frame = FigureFrameWxAgg(num, figure) figmgr = frame.get_figure_manager() if matplotlib.is_interactive(): figmgr.frame.Show() return figmgr - # # agg/wxPython image conversion functions (wxPython >= 2.8) # diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 7026614ea84e..922aa9f748f8 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -266,6 +266,15 @@ def __init__(self, *args): self._cid = 0 self._func_cid_map = {} + def __getstate__(self): + # We cannot currently pickle the callables in the registry, so + # return an empty dictionary. + return {} + + def __setstate__(self, state): + # re-initialise an empty callback registry + self.__init__() + def connect(self, s, func): """ register *func* to be called when a signal *s* is generated @@ -375,7 +384,7 @@ class silent_list(list): """ override repr when returning a list of matplotlib artists to prevent long, meaningless output. This is meant to be used for a - homogeneous list of a give type + homogeneous list of a given type """ def __init__(self, type, seq=None): self.type = type @@ -385,7 +394,15 @@ def __repr__(self): return '' % (len(self), self.type) def __str__(self): - return '' % (len(self), self.type) + return repr(self) + + def __getstate__(self): + # store a dictionary of this SilentList's state + return {'type': self.type, 'seq': self[:]} + + def __setstate__(self, state): + self.type = state['type'] + self.extend(state['seq']) def strip_math(s): 'remove latex formatting from mathtext' @@ -1879,6 +1896,27 @@ def is_math_text(s): return even_dollars + +class _NestedClassGetter(object): + # recipe from http://stackoverflow.com/a/11493777/741316 + """ + When called with the containing class as the first argument, + and the name of the nested class as the second argument, + returns an instance of the nested class. + """ + def __call__(self, containing_class, class_name): + nested_class = getattr(containing_class, class_name) + + # make an instance of a simple object (this one will do), for which we + # can change the __class__ later on. + nested_instance = _NestedClassGetter() + + # set the class of the instance, the __init__ will never be called on + # the class but the original state will be set later on by pickle. + nested_instance.__class__ = nested_class + return nested_instance + + # Numpy > 1.6.x deprecates putmask in favor of the new copyto. # So long as we support versions 1.6.x and less, we need the # following local version of putmask. We choose to make a diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index e476c5a539b3..537e8714b4ac 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -185,6 +185,12 @@ docstring.interpd.update(colorbar_doc=colorbar_doc) +def _set_ticks_on_axis_warn(*args, **kw): + # a top level function which gets put in at the axes' + # set_xticks set_yticks by _patch_ax + warnings.warn("Use the colorbar set_ticks() method instead.") + + class ColorbarBase(cm.ScalarMappable): ''' Draw a colorbar in an existing axes. @@ -277,7 +283,7 @@ def __init__(self, ax, cmap=None, # The rest is in a method so we can recalculate when clim changes. self.config_axis() self.draw_all() - + def _extend_lower(self): """Returns whether the lower limit is open ended.""" return self.extend in ('both', 'min') @@ -285,13 +291,12 @@ def _extend_lower(self): def _extend_upper(self): """Returns whether the uper limit is open ended.""" return self.extend in ('both', 'max') - + def _patch_ax(self): - def _warn(*args, **kw): - warnings.warn("Use the colorbar set_ticks() method instead.") - - self.ax.set_xticks = _warn - self.ax.set_yticks = _warn + # bind some methods to the axes to warn users + # against using those methods. + self.ax.set_xticks = _set_ticks_on_axis_warn + self.ax.set_yticks = _set_ticks_on_axis_warn def draw_all(self): ''' diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 7abcbb91f7e6..77f8cfda4e71 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -847,6 +847,13 @@ def __init__(self, ax, *args, **kwargs): self.collections.append(col) self.changed() # set the colors + def __getstate__(self): + state = self.__dict__.copy() + # the C object Cntr cannot currently be pickled. This isn't a big issue + # as it is not actually used once the contour has been calculated + state['Cntr'] = None + return state + def legend_elements(self, variable_name='x', str_format=str): """ Return a list of artist and labels suitable for passing through diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index ce8a11f6945d..264838a5b8e5 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -33,6 +33,7 @@ import matplotlib.cbook as cbook from matplotlib import docstring +from matplotlib import __version__ as _mpl_version from operator import itemgetter import os.path @@ -1131,11 +1132,73 @@ def _gci(self): return im return None + def __getstate__(self): + state = self.__dict__.copy() + # the axobservers cannot currently be pickled. + # Additionally, the canvas cannot currently be pickled, but this has + # the benefit of meaning that a figure can be detached from one canvas, + # and re-attached to another. + for attr_to_pop in ('_axobservers', 'show', 'canvas', '_cachedRenderer') : + state.pop(attr_to_pop, None) + + # add version information to the state + state['__mpl_version__'] = _mpl_version + + # check to see if the figure has a manager and whether it is registered + # with pyplot + if self.canvas is not None and self.canvas.manager is not None: + manager = self.canvas.manager + import matplotlib._pylab_helpers + if manager in matplotlib._pylab_helpers.Gcf.figs.viewvalues(): + state['_restore_to_pylab'] = True + return state + + def __setstate__(self, state): + version = state.pop('__mpl_version__') + restore_to_pylab = state.pop('_restore_to_pylab', False) + + if version != _mpl_version: + import warnings + warnings.warn("This figure was saved with matplotlib version %s " + "and is unlikely to function correctly." % + (version, )) + + self.__dict__ = state + + # re-initialise some of the unstored state information + self._axobservers = [] + self.canvas = None + + if restore_to_pylab: + # lazy import to avoid circularity + import matplotlib.pyplot as plt + import matplotlib._pylab_helpers as pylab_helpers + allnums = plt.get_fignums() + num = max(allnums) + 1 if allnums else 1 + mgr = plt._backend_mod.new_figure_manager_given_figure(num, self) + + # XXX The following is a copy and paste from pyplot. Consider + # factoring to pylab_helpers + + if self.get_label(): + mgr.set_window_title(self.get_label()) + + # make this figure current on button press event + def make_active(event): + pylab_helpers.Gcf.set_active(mgr) + + mgr._cidgcf = mgr.canvas.mpl_connect('button_press_event', + make_active) + + pylab_helpers.Gcf.set_active(mgr) + self.number = num + + plt.draw_if_interactive() + def add_axobserver(self, func): 'whenever the axes state change, ``func(self)`` will be called' self._axobservers.append(func) - def savefig(self, *args, **kwargs): """ Save the current figure. diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index 73850af48dac..dd18254a93a0 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -113,6 +113,16 @@ def __init__(self, marker=None, fillstyle='full'): self.set_marker(marker) self.set_fillstyle(fillstyle) + def __getstate__(self): + d = self.__dict__.copy() + d.pop('_marker_function') + return d + + def __setstate__(self, statedict): + self.__dict__ = statedict + self.set_marker(self._marker) + self._recache() + def _recache(self): self._path = Path(np.empty((0,2))) self._transform = IdentityTransform() diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index b03cddcbfbf8..082f427b9e5e 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -1617,7 +1617,6 @@ def pprint_styles(klass): """ return _pprint_styles(klass._style_list) - @classmethod def register(klass, name, style): """ @@ -1687,9 +1686,6 @@ def __init__(self): """ super(BoxStyle._Base, self).__init__() - - - def transmute(self, x0, y0, width, height, mutation_size): """ The transmute method is a very core of the @@ -1701,8 +1697,6 @@ def transmute(self, x0, y0, width, height, mutation_size): """ raise NotImplementedError('Derived must override') - - def __call__(self, x0, y0, width, height, mutation_size, aspect_ratio=1.): """ @@ -1728,7 +1722,15 @@ def __call__(self, x0, y0, width, height, mutation_size, else: return self.transmute(x0, y0, width, height, mutation_size) - + def __reduce__(self): + # because we have decided to nest thes classes, we need to + # add some more information to allow instance pickling. + import matplotlib.cbook as cbook + return (cbook._NestedClassGetter(), + (BoxStyle, self.__class__.__name__), + self.__dict__ + ) + class Square(_Base): """ @@ -2296,9 +2298,6 @@ def get_bbox(self): return transforms.Bbox.from_bounds(self._x, self._y, self._width, self._height) - - - from matplotlib.bezier import split_bezier_intersecting_with_closedpath from matplotlib.bezier import get_intersection, inside_circle, get_parallels from matplotlib.bezier import make_wedged_bezier2 @@ -2359,7 +2358,7 @@ class _Base(object): points. This base class defines a __call__ method, and few helper methods. """ - + class SimpleEvent: def __init__(self, xy): self.x, self.y = xy @@ -2401,7 +2400,6 @@ def insideB(xy_display): return path - def _shrink(self, path, shrinkA, shrinkB): """ Shrink the path by fixed size (in points) with shrinkA and shrinkB @@ -2441,6 +2439,15 @@ def __call__(self, posA, posB, shrinked_path = self._shrink(clipped_path, shrinkA, shrinkB) return shrinked_path + + def __reduce__(self): + # because we have decided to nest thes classes, we need to + # add some more information to allow instance pickling. + import matplotlib.cbook as cbook + return (cbook._NestedClassGetter(), + (ConnectionStyle, self.__class__.__name__), + self.__dict__ + ) class Arc3(_Base): @@ -2771,7 +2778,6 @@ def connect(self, posA, posB): {"AvailableConnectorstyles": _pprint_styles(_style_list)} - class ArrowStyle(_Style): """ :class:`ArrowStyle` is a container class which defines several @@ -2867,8 +2873,6 @@ class and must be overriden in the subclasses. It receives raise NotImplementedError('Derived must override') - - def __call__(self, path, mutation_size, linewidth, aspect_ratio=1.): """ @@ -2901,7 +2905,15 @@ def __call__(self, path, mutation_size, linewidth, return path_mutated, fillable else: return self.transmute(path, mutation_size, linewidth) - + + def __reduce__(self): + # because we have decided to nest thes classes, we need to + # add some more information to allow instance pickling. + import matplotlib.cbook as cbook + return (cbook._NestedClassGetter(), + (ArrowStyle, self.__class__.__name__), + self.__dict__ + ) class _Curve(_Base): @@ -3048,7 +3060,6 @@ def __init__(self): _style_list["-"] = Curve - class CurveA(_Curve): """ An arrow with a head at its begin point. @@ -3087,7 +3098,6 @@ def __init__(self, head_length=.4, head_width=.2): beginarrow=False, endarrow=True, head_length=head_length, head_width=head_width ) - #_style_list["->"] = CurveB _style_list["->"] = CurveB @@ -3109,11 +3119,9 @@ def __init__(self, head_length=.4, head_width=.2): beginarrow=True, endarrow=True, head_length=head_length, head_width=head_width ) - #_style_list["<->"] = CurveAB _style_list["<->"] = CurveAB - class CurveFilledA(_Curve): """ An arrow with filled triangle head at the begin. diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 37c9494a1329..d47f303db97b 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -18,206 +18,211 @@ ScaledTranslation, blended_transform_factory, BboxTransformToMaxOnly import matplotlib.spines as mspines -class PolarAxes(Axes): - """ - A polar graph projection, where the input dimensions are *theta*, *r*. - Theta starts pointing east and goes anti-clockwise. +class PolarTransform(Transform): """ - name = 'polar' - - class PolarTransform(Transform): - """ - The base polar transform. This handles projection *theta* and - *r* into Cartesian coordinate space *x* and *y*, but does not - perform the ultimate affine transformation into the correct - position. - """ - input_dims = 2 - output_dims = 2 - is_separable = False - - def __init__(self, axis=None, use_rmin=True): - Transform.__init__(self) - self._axis = axis - self._use_rmin = use_rmin - - def transform(self, tr): - xy = np.empty(tr.shape, np.float_) - if self._axis is not None: - if self._use_rmin: - rmin = self._axis.viewLim.ymin - else: - rmin = 0 - theta_offset = self._axis.get_theta_offset() - theta_direction = self._axis.get_theta_direction() + The base polar transform. This handles projection *theta* and + *r* into Cartesian coordinate space *x* and *y*, but does not + perform the ultimate affine transformation into the correct + position. + """ + input_dims = 2 + output_dims = 2 + is_separable = False + + def __init__(self, axis=None, use_rmin=True): + Transform.__init__(self) + self._axis = axis + self._use_rmin = use_rmin + + def transform(self, tr): + xy = np.empty(tr.shape, np.float_) + if self._axis is not None: + if self._use_rmin: + rmin = self._axis.viewLim.ymin else: rmin = 0 - theta_offset = 0 - theta_direction = 1 - - t = tr[:, 0:1] - r = tr[:, 1:2] - x = xy[:, 0:1] - y = xy[:, 1:2] - - t *= theta_direction - t += theta_offset - - if rmin != 0: - r = r - rmin - mask = r < 0 - x[:] = np.where(mask, np.nan, r * np.cos(t)) - y[:] = np.where(mask, np.nan, r * np.sin(t)) - else: - x[:] = r * np.cos(t) - y[:] = r * np.sin(t) + theta_offset = self._axis.get_theta_offset() + theta_direction = self._axis.get_theta_direction() + else: + rmin = 0 + theta_offset = 0 + theta_direction = 1 + + t = tr[:, 0:1] + r = tr[:, 1:2] + x = xy[:, 0:1] + y = xy[:, 1:2] + + t *= theta_direction + t += theta_offset + + if rmin != 0: + r = r - rmin + mask = r < 0 + x[:] = np.where(mask, np.nan, r * np.cos(t)) + y[:] = np.where(mask, np.nan, r * np.sin(t)) + else: + x[:] = r * np.cos(t) + y[:] = r * np.sin(t) - return xy - transform.__doc__ = Transform.transform.__doc__ + return xy + transform.__doc__ = Transform.transform.__doc__ - transform_non_affine = transform - transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__ + transform_non_affine = transform + transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__ - def transform_path(self, path): - vertices = path.vertices - if len(vertices) == 2 and vertices[0, 0] == vertices[1, 0]: - return Path(self.transform(vertices), path.codes) - ipath = path.interpolated(path._interpolation_steps) - return Path(self.transform(ipath.vertices), ipath.codes) - transform_path.__doc__ = Transform.transform_path.__doc__ + def transform_path(self, path): + vertices = path.vertices + if len(vertices) == 2 and vertices[0, 0] == vertices[1, 0]: + return Path(self.transform(vertices), path.codes) + ipath = path.interpolated(path._interpolation_steps) + return Path(self.transform(ipath.vertices), ipath.codes) + transform_path.__doc__ = Transform.transform_path.__doc__ - transform_path_non_affine = transform_path - transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__ + transform_path_non_affine = transform_path + transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__ - def inverted(self): - return PolarAxes.InvertedPolarTransform(self._axis, self._use_rmin) - inverted.__doc__ = Transform.inverted.__doc__ + def inverted(self): + return PolarAxes.InvertedPolarTransform(self._axis, self._use_rmin) + inverted.__doc__ = Transform.inverted.__doc__ - class PolarAffine(Affine2DBase): - """ - The affine part of the polar projection. Scales the output so - that maximum radius rests on the edge of the axes circle. - """ - def __init__(self, scale_transform, limits): - """ - *limits* is the view limit of the data. The only part of - its bounds that is used is ymax (for the radius maximum). - The theta range is always fixed to (0, 2pi). - """ - Affine2DBase.__init__(self) - self._scale_transform = scale_transform - self._limits = limits - self.set_children(scale_transform, limits) - self._mtx = None - - def get_matrix(self): - if self._invalid: - limits_scaled = self._limits.transformed(self._scale_transform) - yscale = limits_scaled.ymax - limits_scaled.ymin - affine = Affine2D() \ - .scale(0.5 / yscale) \ - .translate(0.5, 0.5) - self._mtx = affine.get_matrix() - self._inverted = None - self._invalid = 0 - return self._mtx - get_matrix.__doc__ = Affine2DBase.get_matrix.__doc__ - - class InvertedPolarTransform(Transform): - """ - The inverse of the polar transform, mapping Cartesian - coordinate space *x* and *y* back to *theta* and *r*. - """ - input_dims = 2 - output_dims = 2 - is_separable = False - - def __init__(self, axis=None, use_rmin=True): - Transform.__init__(self) - self._axis = axis - self._use_rmin = use_rmin - - def transform(self, xy): - if self._axis is not None: - if self._use_rmin: - rmin = self._axis.viewLim.ymin - else: - rmin = 0 - theta_offset = self._axis.get_theta_offset() - theta_direction = self._axis.get_theta_direction() + +class PolarAffine(Affine2DBase): + """ + The affine part of the polar projection. Scales the output so + that maximum radius rests on the edge of the axes circle. + """ + def __init__(self, scale_transform, limits): + """ + *limits* is the view limit of the data. The only part of + its bounds that is used is ymax (for the radius maximum). + The theta range is always fixed to (0, 2pi). + """ + Affine2DBase.__init__(self) + self._scale_transform = scale_transform + self._limits = limits + self.set_children(scale_transform, limits) + self._mtx = None + + def get_matrix(self): + if self._invalid: + limits_scaled = self._limits.transformed(self._scale_transform) + yscale = limits_scaled.ymax - limits_scaled.ymin + affine = Affine2D() \ + .scale(0.5 / yscale) \ + .translate(0.5, 0.5) + self._mtx = affine.get_matrix() + self._inverted = None + self._invalid = 0 + return self._mtx + get_matrix.__doc__ = Affine2DBase.get_matrix.__doc__ + + +class InvertedPolarTransform(Transform): + """ + The inverse of the polar transform, mapping Cartesian + coordinate space *x* and *y* back to *theta* and *r*. + """ + input_dims = 2 + output_dims = 2 + is_separable = False + + def __init__(self, axis=None, use_rmin=True): + Transform.__init__(self) + self._axis = axis + self._use_rmin = use_rmin + + def transform(self, xy): + if self._axis is not None: + if self._use_rmin: + rmin = self._axis.viewLim.ymin else: rmin = 0 - theta_offset = 0 - theta_direction = 1 + theta_offset = self._axis.get_theta_offset() + theta_direction = self._axis.get_theta_direction() + else: + rmin = 0 + theta_offset = 0 + theta_direction = 1 - x = xy[:, 0:1] - y = xy[:, 1:] - r = np.sqrt(x*x + y*y) - theta = np.arccos(x / r) - theta = np.where(y < 0, 2 * np.pi - theta, theta) + x = xy[:, 0:1] + y = xy[:, 1:] + r = np.sqrt(x*x + y*y) + theta = np.arccos(x / r) + theta = np.where(y < 0, 2 * np.pi - theta, theta) - theta -= theta_offset - theta *= theta_direction + theta -= theta_offset + theta *= theta_direction - r += rmin + r += rmin - return np.concatenate((theta, r), 1) - transform.__doc__ = Transform.transform.__doc__ + return np.concatenate((theta, r), 1) + transform.__doc__ = Transform.transform.__doc__ - def inverted(self): - return PolarAxes.PolarTransform(self._axis, self._use_rmin) - inverted.__doc__ = Transform.inverted.__doc__ + def inverted(self): + return PolarAxes.PolarTransform(self._axis, self._use_rmin) + inverted.__doc__ = Transform.inverted.__doc__ - class ThetaFormatter(Formatter): - """ - Used to format the *theta* tick labels. Converts the native - unit of radians into degrees and adds a degree symbol. - """ - def __call__(self, x, pos=None): - # \u00b0 : degree symbol - if rcParams['text.usetex'] and not rcParams['text.latex.unicode']: - return r"$%0.0f^\circ$" % ((x / np.pi) * 180.0) - else: - # we use unicode, rather than mathtext with \circ, so - # that it will work correctly with any arbitrary font - # (assuming it has a degree sign), whereas $5\circ$ - # will only work correctly with one of the supported - # math fonts (Computer Modern and STIX) - return u"%0.0f\u00b0" % ((x / np.pi) * 180.0) - - class RadialLocator(Locator): - """ - Used to locate radius ticks. - Ensures that all ticks are strictly positive. For all other - tasks, it delegates to the base - :class:`~matplotlib.ticker.Locator` (which may be different - depending on the scale of the *r*-axis. - """ - def __init__(self, base): - self.base = base +class ThetaFormatter(Formatter): + """ + Used to format the *theta* tick labels. Converts the native + unit of radians into degrees and adds a degree symbol. + """ + def __call__(self, x, pos=None): + # \u00b0 : degree symbol + if rcParams['text.usetex'] and not rcParams['text.latex.unicode']: + return r"$%0.0f^\circ$" % ((x / np.pi) * 180.0) + else: + # we use unicode, rather than mathtext with \circ, so + # that it will work correctly with any arbitrary font + # (assuming it has a degree sign), whereas $5\circ$ + # will only work correctly with one of the supported + # math fonts (Computer Modern and STIX) + return u"%0.0f\u00b0" % ((x / np.pi) * 180.0) + + +class RadialLocator(Locator): + """ + Used to locate radius ticks. + + Ensures that all ticks are strictly positive. For all other + tasks, it delegates to the base + :class:`~matplotlib.ticker.Locator` (which may be different + depending on the scale of the *r*-axis. + """ + def __init__(self, base): + self.base = base - def __call__(self): - ticks = self.base() - return [x for x in ticks if x > 0] + def __call__(self): + ticks = self.base() + return [x for x in ticks if x > 0] - def autoscale(self): - return self.base.autoscale() + def autoscale(self): + return self.base.autoscale() - def pan(self, numsteps): - return self.base.pan(numsteps) + def pan(self, numsteps): + return self.base.pan(numsteps) - def zoom(self, direction): - return self.base.zoom(direction) + def zoom(self, direction): + return self.base.zoom(direction) - def refresh(self): - return self.base.refresh() + def refresh(self): + return self.base.refresh() - def view_limits(self, vmin, vmax): - vmin, vmax = self.base.view_limits(vmin, vmax) - return 0, vmax + def view_limits(self, vmin, vmax): + vmin, vmax = self.base.view_limits(vmin, vmax) + return 0, vmax + +class PolarAxes(Axes): + """ + A polar graph projection, where the input dimensions are *theta*, *r*. + + Theta starts pointing east and goes anti-clockwise. + """ + name = 'polar' def __init__(self, *args, **kwargs): """ @@ -653,6 +658,17 @@ def drag_pan(self, button, key, x, y): scale = r / startr self.set_rmax(p.rmax / scale) + +# to keep things all self contained, we can put aliases to the Polar classes +# defined above. This isn't strictly necessary, but it makes some of the +# code more readable (and provides a backwards compatible Polar API) +PolarAxes.PolarTransform = PolarTransform +PolarAxes.PolarAffine = PolarAffine +PolarAxes.InvertedPolarTransform = InvertedPolarTransform +PolarAxes.ThetaFormatter = ThetaFormatter +PolarAxes.RadialLocator = RadialLocator + + # These are a couple of aborted attempts to project a polar plot using # cubic bezier curves. diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 15a309307dab..1e64ae8697b6 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -94,7 +94,7 @@ def _backend_selection(): ## Global ## from matplotlib.backends import pylab_setup -new_figure_manager, draw_if_interactive, _show = pylab_setup() +_backend_mod, new_figure_manager, draw_if_interactive, _show = pylab_setup() @docstring.copy_dedent(Artist.findobj) def findobj(o=None, match=None): @@ -102,6 +102,7 @@ def findobj(o=None, match=None): o = gcf() return o.findobj(match) + def switch_backend(newbackend): """ Switch the default backend. This feature is **experimental**, and @@ -115,10 +116,10 @@ def switch_backend(newbackend): Calling this command will close all open windows. """ close('all') - global new_figure_manager, draw_if_interactive, _show + global _backend_mod, new_figure_manager, draw_if_interactive, _show matplotlib.use(newbackend, warn=False, force=True) from matplotlib.backends import pylab_setup - new_figure_manager, draw_if_interactive, _show = pylab_setup() + _backend_mod, new_figure_manager, draw_if_interactive, _show = pylab_setup() def show(*args, **kw): @@ -312,22 +313,17 @@ class that will be passed on to :meth:`new_figure_manager` in the if edgecolor is None : edgecolor = rcParams['figure.edgecolor'] allnums = get_fignums() + next_num = max(allnums) + 1 if allnums else 1 figLabel = '' if num is None: - if allnums: - num = max(allnums) + 1 - else: - num = 1 + num = next_num elif is_string_like(num): figLabel = num allLabels = get_figlabels() if figLabel not in allLabels: if figLabel == 'all': warnings.warn("close('all') closes all existing figures") - if len(allLabels): - num = max(allnums) + 1 - else: - num = 1 + num = next_num else: inum = allLabels.index(figLabel) num = allnums[inum] @@ -363,6 +359,7 @@ def make_active(event): draw_if_interactive() return figManager.canvas.figure + def gcf(): "Return a reference to the current figure." diff --git a/lib/matplotlib/tests/baseline_images/test_pickle/multi_pickle.png b/lib/matplotlib/tests/baseline_images/test_pickle/multi_pickle.png new file mode 100644 index 000000000000..d60ff2275d48 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_pickle/multi_pickle.png differ diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py new file mode 100644 index 000000000000..b2b3d9588dcd --- /dev/null +++ b/lib/matplotlib/tests/test_pickle.py @@ -0,0 +1,157 @@ +from __future__ import print_function + +import numpy as np + +import matplotlib +matplotlib.use('tkagg') + +from matplotlib.testing.decorators import cleanup, image_comparison +import matplotlib.pyplot as plt + +from nose.tools import assert_equal, assert_not_equal + +# cpickle is faster, pickle gives better exceptions +import cPickle as pickle +import pickle + +from cStringIO import StringIO + + +def recursive_pickle(obj, nested_info='top level object', memo=None): + """ + Pickle the object's attributes recursively, storing a memo of the object + which have already been pickled. + + If any pickling issues occur, a pickle.Pickle error will be raised with details. + + This is not a completely general purpose routine, but will be useful for + debugging some pickle issues. HINT: cPickle is less verbose than Pickle + + + """ + if memo is None: + memo = {} + + if id(obj) in memo: + return + + # put this object in the memo + memo[id(obj)] = obj + + # start by pickling all of the object's attributes/contents + + if isinstance(obj, list): + for i, item in enumerate(obj): + recursive_pickle(item, memo=memo, nested_info='list item #%s in (%s)' % (i, nested_info)) + else: + if isinstance(obj, dict): + state = obj + elif hasattr(obj, '__getstate__'): + state = obj.__getstate__() + if not isinstance(state, dict): + state = {} + elif hasattr(obj, '__dict__'): + state = obj.__dict__ + else: + state = {} + + for key, value in state.iteritems(): + recursive_pickle(value, memo=memo, nested_info='attribute "%s" in (%s)' % (key, nested_info)) + +# print(id(obj), type(obj), nested_info) + + # finally, try picking the object itself + try: + pickle.dump(obj, StringIO())#, pickle.HIGHEST_PROTOCOL) + except (pickle.PickleError, AssertionError), err: + print(pickle.PickleError('Pickling failed with nested info: [(%s) %s].' + '\nException: %s' % (type(obj), + nested_info, + err))) + # re-raise the exception for full traceback + raise + + +@cleanup +def test_simple(): + fig = plt.figure() + # un-comment to debug + recursive_pickle(fig, 'figure') + pickle.dump(fig, StringIO(), pickle.HIGHEST_PROTOCOL) + + ax = plt.subplot(121) +# recursive_pickle(ax, 'ax') + pickle.dump(ax, StringIO(), pickle.HIGHEST_PROTOCOL) + + ax = plt.axes(projection='polar') +# recursive_pickle(ax, 'ax') + pickle.dump(ax, StringIO(), pickle.HIGHEST_PROTOCOL) + +# ax = plt.subplot(121, projection='hammer') +# recursive_pickle(ax, 'figure') +# pickle.dump(ax, StringIO(), pickle.HIGHEST_PROTOCOL) + + +@image_comparison(baseline_images=['multi_pickle'], + extensions=['png']) +def test_complete(): + fig = plt.figure('Figure with a label?') + + plt.suptitle('Can you fit any more in a figure?') + + # make some arbitrary data + x, y = np.arange(8), np.arange(10) + data = u = v = np.linspace(0, 10, 80).reshape(10, 8) + v = np.sin(v * -0.6) + + plt.subplot(3,3,1) + plt.plot(range(10)) + + plt.subplot(3, 3, 2) + plt.contourf(data, hatches=['//', 'ooo']) +# plt.colorbar() # sadly, colorbar is currently failing. This might be an easy fix once + # its been identified what the problem is. (lambda functions in colorbar) + + plt.subplot(3, 3, 3) + plt.pcolormesh(data) +# cb = plt.colorbar() + + plt.subplot(3, 3, 4) + plt.imshow(data) + + plt.subplot(3, 3, 5) + plt.pcolor(data) + + plt.subplot(3, 3, 6) + plt.streamplot(x, y, u, v) + + plt.subplot(3, 3, 7) + plt.quiver(x, y, u, v) + + plt.subplot(3, 3, 8) + plt.scatter(x, x**2, label='$x^2$') +# plt.legend() + + plt.subplot(3, 3, 9) + plt.errorbar(x, x * -0.5, xerr=0.2, yerr=0.4) + + + result_fh = StringIO() +# recursive_pickle(fig, 'figure') + pickle.dump(fig, result_fh, pickle.HIGHEST_PROTOCOL) + + plt.close('all') + + # make doubly sure that there are no figures left + assert_equal(plt._pylab_helpers.Gcf.figs, {}) + + # wind back the fh and load in the figure + result_fh.seek(0) + fig = pickle.load(result_fh) + + # make sure there is now a figure manager + assert_not_equal(plt._pylab_helpers.Gcf.figs, {}) + + assert_equal(fig.get_label(), 'Figure with a label?') + + \ No newline at end of file diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index c6f45119570d..a827cf9b8145 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -133,33 +133,31 @@ from matplotlib import transforms as mtransforms +class _DummyAxis(object): + def __init__(self): + self.dataLim = mtransforms.Bbox.unit() + self.viewLim = mtransforms.Bbox.unit() -class TickHelper: - axis = None - class DummyAxis: - def __init__(self): - self.dataLim = mtransforms.Bbox.unit() - self.viewLim = mtransforms.Bbox.unit() - - def get_view_interval(self): - return self.viewLim.intervalx + def get_view_interval(self): + return self.viewLim.intervalx - def set_view_interval(self, vmin, vmax): - self.viewLim.intervalx = vmin, vmax + def set_view_interval(self, vmin, vmax): + self.viewLim.intervalx = vmin, vmax - def get_data_interval(self): - return self.dataLim.intervalx + def get_data_interval(self): + return self.dataLim.intervalx - def set_data_interval(self, vmin, vmax): - self.dataLim.intervalx = vmin, vmax + def set_data_interval(self, vmin, vmax): + self.dataLim.intervalx = vmin, vmax +class TickHelper(object): def set_axis(self, axis): self.axis = axis def create_dummy_axis(self): if self.axis is None: - self.axis = self.DummyAxis() + self.axis = _DummyAxis() def set_view_interval(self, vmin, vmax): self.axis.set_view_interval(vmin, vmax) @@ -176,7 +174,6 @@ class Formatter(TickHelper): """ Convert the tick location to a string """ - # some classes want to see all the locs to help format # individual ones locs = [] @@ -214,6 +211,7 @@ def fix_minus(self, s): """ return s + class IndexFormatter(Formatter): """ format the position x to the nearest i-th label where i=int(x+0.5) @@ -239,6 +237,7 @@ def __call__(self, x, pos=None): 'Return the format for tick val *x* at position *pos*' return '' + class FixedFormatter(Formatter): 'Return fixed strings for tick labels' def __init__(self, seq): @@ -260,6 +259,7 @@ def get_offset(self): def set_offset_string(self, ofs): self.offset_string = ofs + class FuncFormatter(Formatter): """ User defined function for formatting @@ -283,6 +283,7 @@ def __call__(self, x, pos=None): 'Return the format for tick val *x* at position *pos*' return self.fmt % x + class OldScalarFormatter(Formatter): """ Tick location is a plain old number. diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index bac5d963b3ca..7e1b20df2848 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -91,6 +91,17 @@ def __init__(self): # computed for the first time. self._invalid = 1 + def __getstate__(self): + d = self.__dict__.copy() + # turn the weakkey dictionary into a normal dictionary + d['_parents'] = dict(self._parents.iteritems()) + return d + + def __setstate__(self, data_dict): + self.__dict__ = data_dict + # turn the normal dictionary back into a WeakKeyDict + self._parents = WeakKeyDictionary(self._parents) + def __copy__(self, *args): raise NotImplementedError( "TransformNode instances can not be copied. " + @@ -1275,12 +1286,19 @@ def __init__(self, child): be replaced with :meth:`set`. """ assert isinstance(child, Transform) - Transform.__init__(self) self.input_dims = child.input_dims self.output_dims = child.output_dims self._set(child) self._invalid = 0 + + def __getstate__(self): + # only store the child + return {'child': self._child} + + def __setstate__(self, state): + # re-initialise the TransformWrapper with the state's child + self.__init__(state['child']) def __repr__(self): return "TransformWrapper(%r)" % self._child