diff --git a/doc/users/whats_new.rst b/doc/users/whats_new.rst index a6fce09ef9ff..b8c458142344 100644 --- a/doc/users/whats_new.rst +++ b/doc/users/whats_new.rst @@ -100,6 +100,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 d8e014c845e0..33fe760d19b1 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1085,6 +1085,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..afef43f917f1 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 @@ -132,6 +132,7 @@ def set_active(manager): 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 d5375189ea49..68699a211342 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -103,6 +103,13 @@ def __init__(self): self._gid = None 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 @@ -122,7 +129,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 71bf4ca86a11..da78d16d9061 100644 --- a/lib/matplotlib/axes.py +++ b/lib/matplotlib/axes.py @@ -153,9 +153,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:: @@ -171,6 +170,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'] @@ -281,7 +288,7 @@ def _plot_args(self, tup, kwargs): linestyle, marker, color = _process_plot_format(tup[-1]) tup = tup[:-1] elif len(tup) == 3: - raise ValueError, 'third arg must be a format string' + raise ValueError('third arg must be a format string') else: linestyle, marker, color = None, None, None kw = {} @@ -354,6 +361,7 @@ class Axes(martist.Artist): 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, @@ -488,6 +496,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 @@ -1472,7 +1489,7 @@ def _update_line_limits(self, line): return line_trans = line.get_transform() - + if line_trans == self.transData: data_path = path @@ -1491,8 +1508,8 @@ def _update_line_limits(self, line): else: data_path = trans_to_data.transform_path(path) else: - # for backwards compatibility we update the dataLim with the - # coordinate range of the given path, even though the coordinate + # for backwards compatibility we update the dataLim with the + # coordinate range of the given path, even though the coordinate # systems are completely different. This may occur in situations # such as when ax.transAxes is passed through for absolute # positioning. @@ -1502,7 +1519,7 @@ def _update_line_limits(self, line): updatex, updatey = line_trans.contains_branch_seperately( self.transData ) - self.dataLim.update_from_path(data_path, + self.dataLim.update_from_path(data_path, self.ignore_existing_data_limits, updatex=updatex, updatey=updatey) @@ -2199,11 +2216,11 @@ def ticklabel_format(self, **kwargs): cb = False else: cb = True - raise NotImplementedError, "comma style remains to be added" + raise NotImplementedError("comma style remains to be added") elif style == '': sb = None else: - raise ValueError, "%s is not a valid style value" + raise ValueError("%s is not a valid style value") try: if sb is not None: if axis == 'both' or axis == 'x': @@ -3706,9 +3723,9 @@ def hlines(self, y, xmin, xmax, colors='k', linestyles='solid', xmax = np.resize( xmax, y.shape ) if len(xmin)!=len(y): - raise ValueError, 'xmin and y are unequal sized sequences' + raise ValueError('xmin and y are unequal sized sequences') if len(xmax)!=len(y): - raise ValueError, 'xmax and y are unequal sized sequences' + raise ValueError('xmax and y are unequal sized sequences') verts = [ ((thisxmin, thisy), (thisxmax, thisy)) for thisxmin, thisxmax, thisy in zip(xmin, xmax, y)] @@ -3785,9 +3802,9 @@ def vlines(self, x, ymin, ymax, colors='k', linestyles='solid', ymax = np.resize( ymax, x.shape ) if len(ymin)!=len(x): - raise ValueError, 'ymin and x are unequal sized sequences' + raise ValueError('ymin and x are unequal sized sequences') if len(ymax)!=len(x): - raise ValueError, 'ymax and x are unequal sized sequences' + raise ValueError('ymax and x are unequal sized sequences') Y = np.array([ymin, ymax]).T @@ -4768,7 +4785,7 @@ def make_iterable(x): if len(height) == 1: height *= nbars else: - raise ValueError, 'invalid orientation: %s' % orientation + raise ValueError('invalid orientation: %s' % orientation) if len(linewidth) < nbars: linewidth *= nbars @@ -4826,7 +4843,7 @@ def make_iterable(x): bottom = [bottom[i] - height[i]/2. for i in xrange(len(bottom))] else: - raise ValueError, 'invalid alignment: %s' % align + raise ValueError('invalid alignment: %s' % align) args = zip(left, bottom, width, height, color, edgecolor, linewidth) for l, b, w, h, c, e, lw in args: @@ -5701,7 +5718,7 @@ def computeConfInterval(data, med, iq, bootstrap): else: x = [x[:,i] for i in xrange(nc)] else: - raise ValueError, "input x can have no more than 2 dimensions" + raise ValueError("input x can have no more than 2 dimensions") if not hasattr(x[0], '__len__'): x = [x] col = len(x) @@ -7069,10 +7086,10 @@ def _pcolorargs(self, funcname, *args): Nx = X.shape[-1] Ny = Y.shape[0] - if len(X.shape) <> 2 or X.shape[0] == 1: + if len(X.shape) != 2 or X.shape[0] == 1: x = X.reshape(1,Nx) X = x.repeat(Ny, axis=0) - if len(Y.shape) <> 2 or Y.shape[1] == 1: + if len(Y.shape) != 2 or Y.shape[1] == 1: y = Y.reshape(Ny, 1) Y = y.repeat(Nx, axis=1) if X.shape != Y.shape: @@ -8815,7 +8832,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 + not_subplotbase = lambda c: issubclass(c, Axes) and \ + not issubclass(c, SubplotBase) + axes_class = [c for c in self.__class__.mro() if not_subplotbase(c)][0] + r = [_PicklableSubplotClassConstructor(), + (axes_class,), + self.__getstate__()] + return tuple(r) def get_geometry(self): """get the subplot geometry, eg 2,2,3""" @@ -8897,6 +8922,22 @@ 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 3723845e7b5d..2533a51b1581 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -597,7 +597,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..e221989a6dbd 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..966e618fa408 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -26,15 +26,15 @@ def _fn_name(): return sys._getframe(1).f_code.co_name try: - import cairo + import cairo except ImportError: - raise ImportError("Cairo backend requires that pycairo is installed.") + raise ImportError("Cairo backend requires that pycairo is installed.") _version_required = (1,2,0) if cairo.version_info < _version_required: - raise ImportError ("Pycairo %d.%d.%d is installed\n" - "Pycairo %d.%d.%d or later is required" - % (cairo.version_info + _version_required)) + raise ImportError ("Pycairo %d.%d.%d is installed\n" + "Pycairo %d.%d.%d or later is required" + % (cairo.version_info + _version_required)) backend_version = cairo.version del _version_required @@ -183,27 +183,27 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) if ismath: - self._draw_mathtext(gc, x, y, s, prop, angle) + self._draw_mathtext(gc, x, y, s, prop, angle) else: - ctx = gc.ctx - ctx.new_path() - ctx.move_to (x, y) - ctx.select_font_face (prop.get_name(), - self.fontangles [prop.get_style()], - self.fontweights[prop.get_weight()]) - - size = prop.get_size_in_points() * self.dpi / 72.0 - - ctx.save() - if angle: - ctx.rotate (-angle * np.pi / 180) - ctx.set_font_size (size) - if sys.version_info[0] < 3: - ctx.show_text (s.encode("utf-8")) - else: - ctx.show_text (s) - ctx.restore() + ctx = gc.ctx + ctx.new_path() + ctx.move_to (x, y) + ctx.select_font_face (prop.get_name(), + self.fontangles [prop.get_style()], + self.fontweights[prop.get_weight()]) + + size = prop.get_size_in_points() * self.dpi / 72.0 + + ctx.save() + if angle: + ctx.rotate (-angle * np.pi / 180) + ctx.set_font_size (size) + if sys.version_info[0] < 3: + ctx.show_text (s.encode("utf-8")) + else: + ctx.show_text (s) + ctx.restore() def _draw_mathtext(self, gc, x, y, s, prop, angle): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) @@ -215,28 +215,28 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx.save() ctx.translate(x, y) if angle: - ctx.rotate (-angle * np.pi / 180) + ctx.rotate (-angle * np.pi / 180) for font, fontsize, s, ox, oy in glyphs: - ctx.new_path() - ctx.move_to(ox, oy) - - fontProp = ttfFontProperty(font) - ctx.save() - ctx.select_font_face (fontProp.name, - self.fontangles [fontProp.style], - self.fontweights[fontProp.weight]) - - size = fontsize * self.dpi / 72.0 - ctx.set_font_size(size) - ctx.show_text(s.encode("utf-8")) - ctx.restore() + ctx.new_path() + ctx.move_to(ox, oy) + + fontProp = ttfFontProperty(font) + ctx.save() + ctx.select_font_face (fontProp.name, + self.fontangles [fontProp.style], + self.fontweights[fontProp.weight]) + + size = fontsize * self.dpi / 72.0 + ctx.set_font_size(size) + ctx.show_text(s.encode("utf-8")) + ctx.restore() for ox, oy, w, h in rects: - ctx.new_path() - ctx.rectangle (ox, oy, w, h) - ctx.set_source_rgb (0, 0, 0) - ctx.fill_preserve() + ctx.new_path() + ctx.rectangle (ox, oy, w, h) + ctx.set_source_rgb (0, 0, 0) + ctx.fill_preserve() ctx.restore() @@ -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()' % (_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..5c9c04938874 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..50da4fea706f 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..7a2a7f258f2c 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..c0790c6591bb 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..8336ddaecf8e 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..c05e0266631a 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..2b1fefc04d4b 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..aa7c42a68027 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..2cd41ba720eb 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..a8fb1b758fed 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 f2baf9ba5deb..06f9b1d30156 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2182,7 +2182,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_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 9909f6b09223..cd5d520022ac 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -598,6 +598,7 @@ class GraphicsContextPgf(GraphicsContextBase): def draw_if_interactive(): pass + def new_figure_manager(num, *args, **kwargs): """ Create a new figure manager instance @@ -608,7 +609,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 = FigureCanvasPgf(thisFig) + return new_figure_manager_given_figure(thisFig) + + +def new_figure_manager_given_figure(num, figure): + """ + Create a new figure manager instance for the given figure. + """ + canvas = FigureCanvasPgf(figure) manager = FigureManagerPgf(canvas, num) return manager diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 2af23224f145..d604028c554c 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..0e1836a81acf 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..f64bfa4245ff 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..07017818e853 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..91a9cd47133e 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..aa117ca9bacb 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..3c61d1e412bd 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..5b301a045f04 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..0cbfb85dc0b6 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -152,6 +152,90 @@ def __call__(self, s): if self.is_missing(s): return self.missingval return int(s) + +class _BoundMethodProxy(object): + ''' + Our own proxy object which enables weak references to bound and unbound + methods and arbitrary callables. Pulls information about the function, + class, and instance out of a bound method. Stores a weak reference to the + instance to support garbage collection. + + @organization: IBM Corporation + @copyright: Copyright (c) 2005, 2006 IBM Corporation + @license: The BSD License + + Minor bugfixes by Michael Droettboom + ''' + def __init__(self, cb): + try: + try: + self.inst = ref(cb.im_self) + except TypeError: + self.inst = None + self.func = cb.im_func + self.klass = cb.im_class + except AttributeError: + self.inst = None + self.func = cb + self.klass = None + + def __getstate__(self): + d = self.__dict__.copy() + # de-weak reference inst + inst = d['inst'] + if inst is not None: + d['inst'] = inst() + return d + + def __setstate__(self, statedict): + self.__dict__ = statedict + inst = statedict['inst'] + # turn inst back into a weakref + if inst is not None: + self.inst = ref(inst) + + def __call__(self, *args, **kwargs): + ''' + Proxy for a call to the weak referenced object. Take + arbitrary params to pass to the callable. + + Raises `ReferenceError`: When the weak reference refers to + a dead object + ''' + if self.inst is not None and self.inst() is None: + raise ReferenceError + elif self.inst is not None: + # build a new instance method with a strong reference to the instance + if sys.version_info[0] >= 3: + mtd = types.MethodType(self.func, self.inst()) + else: + mtd = new.instancemethod(self.func, self.inst(), self.klass) + else: + # not a bound method, just return the func + mtd = self.func + # invoke the callable and return the result + return mtd(*args, **kwargs) + + def __eq__(self, other): + ''' + Compare the held function and instance with that held by + another proxy. + ''' + try: + if self.inst is None: + return self.func == other.func and other.inst is None + else: + return self.func == other.func and self.inst() == other.inst() + except Exception: + return False + + def __ne__(self, other): + ''' + Inverse of __eq__. + ''' + return not self.__eq__(other) + + class CallbackRegistry: """ Handle registering and disconnecting for a set of signals and @@ -190,73 +274,6 @@ def ondrink(x): `"Mindtrove" blog `_. """ - class BoundMethodProxy(object): - ''' - Our own proxy object which enables weak references to bound and unbound - methods and arbitrary callables. Pulls information about the function, - class, and instance out of a bound method. Stores a weak reference to the - instance to support garbage collection. - - @organization: IBM Corporation - @copyright: Copyright (c) 2005, 2006 IBM Corporation - @license: The BSD License - - Minor bugfixes by Michael Droettboom - ''' - def __init__(self, cb): - try: - try: - self.inst = ref(cb.im_self) - except TypeError: - self.inst = None - self.func = cb.im_func - self.klass = cb.im_class - except AttributeError: - self.inst = None - self.func = cb - self.klass = None - - def __call__(self, *args, **kwargs): - ''' - Proxy for a call to the weak referenced object. Take - arbitrary params to pass to the callable. - - Raises `ReferenceError`: When the weak reference refers to - a dead object - ''' - if self.inst is not None and self.inst() is None: - raise ReferenceError - elif self.inst is not None: - # build a new instance method with a strong reference to the instance - if sys.version_info[0] >= 3: - mtd = types.MethodType(self.func, self.inst()) - else: - mtd = new.instancemethod(self.func, self.inst(), self.klass) - else: - # not a bound method, just return the func - mtd = self.func - # invoke the callable and return the result - return mtd(*args, **kwargs) - - def __eq__(self, other): - ''' - Compare the held function and instance with that held by - another proxy. - ''' - try: - if self.inst is None: - return self.func == other.func and other.inst is None - else: - return self.func == other.func and self.inst() == other.inst() - except Exception: - return False - - def __ne__(self, other): - ''' - Inverse of __eq__. - ''' - return not self.__eq__(other) - def __init__(self, *args): if len(args): warnings.warn( @@ -266,6 +283,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 @@ -279,7 +305,7 @@ def connect(self, s, func): cid = self._cid self._func_cid_map[s][func] = cid self.callbacks.setdefault(s, dict()) - proxy = self.BoundMethodProxy(func) + proxy = _BoundMethodProxy(func) self.callbacks[s][cid] = proxy return cid @@ -375,7 +401,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 +411,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 +1913,41 @@ 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 + + +class _InstanceMethodPickler(object): + """ + Pickle cannot handle instancemethod saving. _InstanceMethodPickler + provides a solution to this. + """ + def __init__(self, instancemethod): + """Takes an instancemethod as its only argument.""" + self.parent_obj = instancemethod.im_self + self.instancemethod_name = instancemethod.im_func.__name__ + + def get_instancemethod(self): + return getattr(self.parent_obj, self.instancemethod_name) + + # 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 e600828d9712..1f67d676d4b2 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. @@ -287,11 +293,10 @@ def _extend_upper(self): 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): ''' @@ -531,16 +536,13 @@ def _ticker(self): intv = self._values[0], self._values[-1] else: intv = self.vmin, self.vmax - locator.create_dummy_axis() - formatter.create_dummy_axis() + locator.create_dummy_axis(minpos=intv[0]) + formatter.create_dummy_axis(minpos=intv[0]) locator.set_view_interval(*intv) locator.set_data_interval(*intv) formatter.set_view_interval(*intv) formatter.set_data_interval(*intv) - # the dummy axis is expecting a minpos - locator.axis.get_minpos = lambda : intv[0] - formatter.axis.get_minpos = lambda : intv[0] b = np.array(locator()) ticks = self._locate(b) inrange = (ticks > -0.001) & (ticks < 1.001) diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index ceee95a37145..916a608afc27 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -851,6 +851,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 6dc6e0361529..b6a23aab420f 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -17,12 +17,15 @@ import numpy as np -from matplotlib import rcParams, docstring +from matplotlib import rcParams +from matplotlib import docstring +from matplotlib import __version__ as _mpl_version import matplotlib.artist as martist from matplotlib.artist import Artist, allow_rasterization import matplotlib.cbook as cbook + from matplotlib.cbook import Stack, iterable from matplotlib import _image @@ -114,7 +117,6 @@ def add(self, key, a): a_existing = self.get(key) if a_existing is not None: Stack.remove(self, (key, a_existing)) - import warnings warnings.warn( "key %s already existed; Axes is being replaced" % key) # I don't think the above should ever happen. @@ -1170,11 +1172,74 @@ 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 getattr(self.canvas, 'manager', None) 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/image.py b/lib/matplotlib/image.py index 0c50fa460d54..7fbf3d963139 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -1075,10 +1075,10 @@ def get_window_extent(self, renderer=None): else: raise ValueError("unknown type of bbox") - def contains(self, mouseevent): """Test whether the mouse event occured within the image.""" - if callable(self._contains): return self._contains(self,mouseevent) + if callable(self._contains): + return self._contains(self, mouseevent) if not self.get_visible():# or self.get_figure()._renderer is None: return False,{} @@ -1137,7 +1137,7 @@ def make_image(self, renderer, magnification=1.0): numrows, numcols = self._A.shape[:2] if not self.interp_at_native and widthDisplay==numcols and heightDisplay==numrows: - im.set_interpolation(0) + im.set_interpolation(0) # resize viewport to display rx = widthDisplay / numcols diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 0b4738a214d7..dd464273e808 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -236,7 +236,6 @@ def __init__(self, parent, handles, labels, self.legendHandles = [] self._legend_title_box = None - self._handler_map = handler_map localdict = locals() @@ -386,7 +385,6 @@ def _set_artist_props(self, a): a.set_axes(self.axes) a.set_transform(self.get_transform()) - def _set_loc(self, loc): # find_offset function will be provided to _legend_box and # _legend_box will draw itself at the location of the return @@ -415,7 +413,7 @@ def _findoffset_best(self, width, height, xdescent, ydescent, renderer): return ox+xdescent, oy+ydescent def _findoffset_loc(self, width, height, xdescent, ydescent, renderer): - "Heper function to locate the legend using the location code" + "Helper function to locate the legend using the location code" if iterable(self._loc) and len(self._loc)==2: # when loc is a tuple of axes(or figure) coordinates. @@ -433,10 +431,8 @@ def draw(self, renderer): "Draw everything that belongs to the legend" if not self.get_visible(): return - renderer.open_group('legend') - fontsize = renderer.points_to_pixels(self._fontsize) # if mode == fill, set the width of the legend_box to the @@ -463,7 +459,6 @@ def draw(self, renderer): renderer.close_group('legend') - def _approx_text_height(self, renderer=None): """ Return the approximate height of the text. This is used to place @@ -577,7 +572,6 @@ def _init_legend_box(self, handles, labels): # is an instance of offsetbox.TextArea which contains legend # text. - text_list = [] # the list of text instances handle_list = [] # the list of text instances @@ -589,14 +583,13 @@ def _init_legend_box(self, handles, labels): labelboxes = [] handleboxes = [] - # The approximate height and descent of text. These values are # only used for plotting the legend handle. descent = 0.35*self._approx_text_height()*(self.handleheight - 0.7) # 0.35 and 0.7 are just heuristic numbers. this may need to be improbed height = self._approx_text_height() * self.handleheight - descent # each handle needs to be drawn inside a box of (x, y, w, h) = - # (0, -descent, width, height). And their corrdinates should + # (0, -descent, width, height). And their coordinates should # be given in the display coordinates. # The transformation of each handle will be automatically set @@ -604,7 +597,6 @@ def _init_legend_box(self, handles, labels): # default trasnform (eg, Collections), you need to # manually set their transform to the self.get_transform(). - legend_handler_map = self.get_legend_handler_map() for orig_handle, lab in zip(handles, labels): @@ -632,12 +624,8 @@ def _init_legend_box(self, handles, labels): handlebox) handle_list.append(handle) - - - handleboxes.append(handlebox) - if len(handleboxes) > 0: # We calculate number of rows in each column. The first diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 777270e68e49..c5f285f75452 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -154,10 +154,9 @@ def get_xdata(self, legend, xdescent, ydescent, width, height, fontsize): return xdata, xdata_marker - class HandlerNpointsYoffsets(HandlerNpoints): def __init__(self, numpoints=None, yoffsets=None, **kw): - HandlerNpoints.__init__(self,numpoints=numpoints, **kw) + HandlerNpoints.__init__(self, numpoints=numpoints, **kw) self._yoffsets = yoffsets def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize): @@ -169,9 +168,6 @@ def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize): return ydata - - - class HandlerLine2D(HandlerNpoints): """ Handler for Line2D instances @@ -179,7 +175,6 @@ class HandlerLine2D(HandlerNpoints): def __init__(self, marker_pad=0.3, numpoints=None, **kw): HandlerNpoints.__init__(self, marker_pad=marker_pad, numpoints=numpoints, **kw) - def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): @@ -198,7 +193,6 @@ def create_artists(self, legend, orig_handle, legline.set_drawstyle('default') legline.set_marker("") - legline_marker = Line2D(xdata_marker, ydata[:len(xdata_marker)]) self.update_prop(legline_marker, orig_handle, legend) #legline_marker.update_from(orig_handle) @@ -217,7 +211,6 @@ def create_artists(self, legend, orig_handle, return [legline, legline_marker] - class HandlerPatch(HandlerBase): """ Handler for Patches @@ -250,7 +243,6 @@ def create_artists(self, legend, orig_handle, return [p] - class HandlerLineCollection(HandlerLine2D): """ Handler for LineCollections @@ -285,7 +277,6 @@ def create_artists(self, legend, orig_handle, return [legline] - class HandlerRegularPolyCollection(HandlerNpointsYoffsets): """ Handler for RegularPolyCollections. @@ -340,11 +331,9 @@ def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): - xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent, width, height, fontsize) - ydata = self.get_ydata(legend, xdescent, ydescent, width, height, fontsize) @@ -362,6 +351,7 @@ def create_artists(self, legend, orig_handle, return [p] + class HandlerPathCollection(HandlerRegularPolyCollection): """ Handler for PathCollections, which are used by scatter @@ -410,8 +400,6 @@ def get_err_size(self, legend, xdescent, ydescent, width, height, fontsize): return xerr_size, yerr_size - - def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): @@ -431,7 +419,6 @@ def create_artists(self, legend, orig_handle, xerr_size, yerr_size = self.get_err_size(legend, xdescent, ydescent, width, height, fontsize) - legline_marker = Line2D(xdata_marker, ydata_marker) # when plotlines are None (only errorbars are drawn), we just @@ -452,7 +439,6 @@ def create_artists(self, legend, orig_handle, newsz = legline_marker.get_markersize()*legend.markerscale legline_marker.set_markersize(newsz) - handle_barlinecols = [] handle_caplines = [] @@ -501,7 +487,6 @@ def create_artists(self, legend, orig_handle, return artists - class HandlerStem(HandlerNpointsYoffsets): """ Handler for Errorbars @@ -516,7 +501,6 @@ def __init__(self, marker_pad=0.3, numpoints=None, self._bottom = bottom - def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize): if self._yoffsets is None: ydata = height*(0.5*legend._scatteryoffsets + 0.5) @@ -525,7 +509,6 @@ def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize): return ydata - def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index 73850af48dac..0579c3e67565 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/offsetbox.py b/lib/matplotlib/offsetbox.py index 8a6030100b80..2f96dee80796 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -131,7 +131,6 @@ def _get_aligned_offsets(hd_list, height, align="baseline"): return height, descent, offsets - class OffsetBox(martist.Artist): """ The OffsetBox is a simple container artist. The child artist are meant @@ -144,6 +143,24 @@ def __init__(self, *args, **kwargs): self._children = [] self._offset = (0, 0) + def __getstate__(self): + state = martist.Artist.__getstate__(self) + + # pickle cannot save instancemethods, so handle them here + from cbook import _InstanceMethodPickler + import inspect + + offset = state['_offset'] + if inspect.ismethod(offset): + state['_offset'] = _InstanceMethodPickler(offset) + return state + + def __setstate__(self, state): + self.__dict__ = state + from cbook import _InstanceMethodPickler + if isinstance(self._offset, _InstanceMethodPickler): + self._offset = self._offset.get_instancemethod() + def set_figure(self, fig): """ Set the figure @@ -343,7 +360,7 @@ def get_extent_offsets(self, renderer): class HPacker(PackerBase): """ The HPacker has its children packed horizontally. It automatically - adjust the relative positions of children in the drawing time. + adjusts the relative positions of children at draw time. """ def __init__(self, pad=None, sep=None, width=None, height=None, align="baseline", mode="fixed", @@ -364,12 +381,10 @@ def __init__(self, pad=None, sep=None, width=None, height=None, super(HPacker, self).__init__(pad, sep, width, height, align, mode, children) - def get_extent_offsets(self, renderer): """ update offset of children and return the extents of the box """ - dpicor = renderer.points_to_pixels(1.) pad = self.pad * dpicor sep = self.sep * dpicor @@ -391,7 +406,6 @@ def get_extent_offsets(self, renderer): self.height, self.align) - pack_list = [(w, xd) for w,h,xd,yd in whd_list] width, xoffsets_ = _get_packed_offsets(pack_list, self.width, sep, self.mode) @@ -406,8 +420,6 @@ def get_extent_offsets(self, renderer): zip(xoffsets, yoffsets) - - class PaddedBox(OffsetBox): def __init__(self, child, pad=None, draw_frame=False, patch_attrs=None): """ @@ -438,7 +450,6 @@ def __init__(self, child, pad=None, draw_frame=False, patch_attrs=None): self._drawFrame = draw_frame - def get_extent_offsets(self, renderer): """ update offset of childrens and return the extents of the box @@ -453,7 +464,6 @@ def get_extent_offsets(self, renderer): xd+pad, yd+pad, \ [(0, 0)] - def draw(self, renderer): """ Update the location of children if necessary and draw them @@ -517,8 +527,6 @@ def __init__(self, width, height, xdescent=0., self.dpi_transform = mtransforms.Affine2D() - - def get_transform(self): """ Return the :class:`~matplotlib.transforms.Transform` applied @@ -532,7 +540,6 @@ def set_transform(self, t): """ pass - def set_offset(self, xy): """ set offset of the container. @@ -544,14 +551,12 @@ def set_offset(self, xy): self.offset_transform.clear() self.offset_transform.translate(xy[0], xy[1]) - def get_offset(self): """ return offset of the container. """ return self._offset - def get_window_extent(self, renderer): ''' get the bounding box in display space. @@ -560,7 +565,6 @@ def get_window_extent(self, renderer): ox, oy = self.get_offset() #w, h, xd, yd) return mtransforms.Bbox.from_bounds(ox-xd, oy-yd, w, h) - def get_extent(self, renderer): """ Return with, height, xdescent, ydescent of box @@ -571,14 +575,12 @@ def get_extent(self, renderer): return self.width*dpi_cor, self.height*dpi_cor, \ self.xdescent*dpi_cor, self.ydescent*dpi_cor - def add_artist(self, a): 'Add any :class:`~matplotlib.artist.Artist` to the container box' self._children.append(a) if not a.is_transform_set(): a.set_transform(self.get_transform()) - def draw(self, renderer): """ Draw the children @@ -601,9 +603,6 @@ class TextArea(OffsetBox): of the TextArea instance is the width and height of the its child text. """ - - - def __init__(self, s, textprops=None, multilinebaseline=None, @@ -639,7 +638,6 @@ def __init__(self, s, self._multilinebaseline = multilinebaseline self._minimumdescent = minimumdescent - def set_text(self, s): "set text" self._text.set_text(s) @@ -658,14 +656,12 @@ def set_multilinebaseline(self, t): """ self._multilinebaseline = t - def get_multilinebaseline(self): """ get multilinebaseline . """ return self._multilinebaseline - def set_minimumdescent(self, t): """ Set minimumdescent . @@ -675,21 +671,18 @@ def set_minimumdescent(self, t): """ self._minimumdescent = t - def get_minimumdescent(self): """ get minimumdescent. """ return self._minimumdescent - def set_transform(self, t): """ set_transform is ignored. """ pass - def set_offset(self, xy): """ set offset of the container. @@ -701,14 +694,12 @@ def set_offset(self, xy): self.offset_transform.clear() self.offset_transform.translate(xy[0], xy[1]) - def get_offset(self): """ return offset of the container. """ return self._offset - def get_window_extent(self, renderer): ''' get the bounding box in display space. @@ -717,7 +708,6 @@ def get_window_extent(self, renderer): ox, oy = self.get_offset() #w, h, xd, yd) return mtransforms.Bbox.from_bounds(ox-xd, oy-yd, w, h) - def get_extent(self, renderer): clean_line, ismath = self._text.is_math_text(self._text._text) _, h_, d_ = renderer.get_text_width_height_descent( @@ -733,7 +723,6 @@ def get_extent(self, renderer): self._baseline_transform.clear() - if len(info) > 1 and self._multilinebaseline: d_new = 0.5 * h - 0.5 * (h_ - d_) self._baseline_transform.translate(0, d - d_new) @@ -754,7 +743,6 @@ def get_extent(self, renderer): return w, h, 0., d - def draw(self, renderer): """ Draw the children @@ -765,7 +753,6 @@ def draw(self, renderer): bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) - class AuxTransformBox(OffsetBox): """ Offset Box with the aux_transform . Its children will be @@ -780,7 +767,6 @@ class AuxTransformBox(OffsetBox): children. Furthermore, the extent of the children will be calculated in the transformed coordinate. """ - def __init__(self, aux_transform): self.aux_transform = aux_transform OffsetBox.__init__(self) @@ -805,7 +791,6 @@ def get_transform(self): Return the :class:`~matplotlib.transforms.Transform` applied to the children """ - return self.aux_transform + \ self.ref_offset_transform + \ self.offset_transform @@ -816,7 +801,6 @@ def set_transform(self, t): """ pass - def set_offset(self, xy): """ set offset of the container. @@ -828,14 +812,12 @@ def set_offset(self, xy): self.offset_transform.clear() self.offset_transform.translate(xy[0], xy[1]) - def get_offset(self): """ return offset of the container. """ return self._offset - def get_window_extent(self, renderer): ''' get the bounding box in display space. @@ -844,7 +826,6 @@ def get_window_extent(self, renderer): ox, oy = self.get_offset() #w, h, xd, yd) return mtransforms.Bbox.from_bounds(ox-xd, oy-yd, w, h) - def get_extent(self, renderer): # clear the offset transforms @@ -856,7 +837,6 @@ def get_extent(self, renderer): bboxes = [c.get_window_extent(renderer) for c in self._children] ub = mtransforms.Bbox.union(bboxes) - # adjust ref_offset_tansform self.ref_offset_transform.translate(-ub.x0, -ub.y0) @@ -866,7 +846,6 @@ def get_extent(self, renderer): return ub.width, ub.height, 0., 0. - def draw(self, renderer): """ Draw the children @@ -878,7 +857,6 @@ def draw(self, renderer): bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) - class AnchoredOffsetbox(OffsetBox): """ An offset box placed according to the legend location @@ -887,7 +865,6 @@ class AnchoredOffsetbox(OffsetBox): the offset box is anchored against its parent axes. You may explicitly specify the bbox_to_anchor. """ - zorder = 5 # zorder of the legend def __init__(self, loc, @@ -927,7 +904,6 @@ def __init__(self, loc, bbox_transform : with which the bbox_to_anchor will be transformed. """ - super(AnchoredOffsetbox, self).__init__(**kwargs) self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform) @@ -955,9 +931,6 @@ def __init__(self, loc, self.patch.set_boxstyle("square",pad=0) self._drawFrame = frameon - - - def set_child(self, child): "set the child to be anchored" self._child = child @@ -970,7 +943,6 @@ def get_children(self): "return the list of children" return [self._child] - def get_extent(self, renderer): """ return the extent of the artist. The extent of the child @@ -982,7 +954,6 @@ def get_extent(self, renderer): return w+2*pad, h+2*pad, xd+pad, yd+pad - def get_bbox_to_anchor(self): """ return the bbox that the legend will be anchored @@ -997,9 +968,6 @@ def get_bbox_to_anchor(self): return TransformedBbox(self._bbox_to_anchor, transform) - - - def set_bbox_to_anchor(self, bbox, transform=None): """ set the bbox that the child will be anchored. @@ -1024,7 +992,6 @@ def set_bbox_to_anchor(self, bbox, transform=None): self._bbox_to_anchor_transform = transform - def get_window_extent(self, renderer): ''' get the bounding box in display space. @@ -1034,7 +1001,6 @@ def get_window_extent(self, renderer): ox, oy = self.get_offset(w, h, xd, yd, renderer) return Bbox.from_bounds(ox-xd, oy-yd, w, h) - def _update_offset_func(self, renderer, fontsize=None): """ Update the offset func which depends on the dpi of the @@ -1053,10 +1019,9 @@ def _offset(w, h, xd, yd, renderer, fontsize=fontsize, self=self): bbox_to_anchor, borderpad) return x0+xd, y0+yd - + self.set_offset(_offset) - def update_frame(self, bbox, fontsize=None): self.patch.set_bounds(bbox.x0, bbox.y0, bbox.width, bbox.height) @@ -1086,8 +1051,6 @@ def draw(self, renderer): self.get_child().set_offset((px, py)) self.get_child().draw(renderer) - - def _get_anchored_bbox(self, loc, bbox, parentbbox, borderpad): """ return the position of the bbox anchored at the parentbbox @@ -1141,7 +1104,6 @@ def __init__(self, s, loc, pad=0.4, borderpad=0.5, prop=None, **kwargs): **kwargs) - class OffsetImage(OffsetBox): def __init__(self, arr, zoom=1, @@ -1176,7 +1138,6 @@ def __init__(self, arr, OffsetBox.__init__(self) - def set_data(self, arr): self._data = np.asarray(arr) self.image.set_data(self._data) @@ -1205,8 +1166,6 @@ def get_zoom(self): # self.offset_transform.clear() # self.offset_transform.translate(xy[0], xy[1]) - - def get_offset(self): """ return offset of the container. @@ -1224,7 +1183,6 @@ def get_window_extent(self, renderer): ox, oy = self.get_offset() return mtransforms.Bbox.from_bounds(ox-xd, oy-yd, w, h) - def get_extent(self, renderer): if self._dpi_cor: # True, do correction @@ -1239,26 +1197,21 @@ def get_extent(self, renderer): return w, h, 0, 0 - - def draw(self, renderer): """ Draw the children """ - self.image.draw(renderer) - #bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) - from matplotlib.text import _AnnotationBase + class AnnotationBbox(martist.Artist, _AnnotationBase): """ Annotation-like class, but with offsetbox instead of Text. """ - zorder = 3 def __str__(self): @@ -1328,7 +1281,6 @@ def __init__(self, offsetbox, xy, self.patch.set(**bboxprops) self._drawFrame = frameon - def contains(self,event): t,tinfo = self.offsetbox.contains(event) #if self.arrow_patch is not None: @@ -1339,7 +1291,6 @@ def contains(self,event): return t,tinfo - def get_children(self): children = [self.offsetbox, self.patch] if self.arrow_patch: @@ -1379,7 +1330,6 @@ def update_positions(self, renderer): if self.arrow_patch: self.arrow_patch.set_mutation_scale(mutation_scale) - def _update_position_xybox(self, renderer, xy_pixel): "Update the pixel positions of the annotation text and the arrow patch." @@ -1437,8 +1387,6 @@ def _update_position_xybox(self, renderer, xy_pixel): patchA = d.pop("patchA", self.patch) self.arrow_patch.set_patchA(patchA) - - def draw(self, renderer): """ Draw the :class:`Annotation` object to the given *renderer*. @@ -1466,7 +1414,6 @@ def draw(self, renderer): self.offsetbox.draw(renderer) - class DraggableBase(object): """ helper code for a draggable artist (legend, offsetbox) @@ -1635,7 +1582,7 @@ def finalize_offset(self): if __name__ == "__main__": - + import matplotlib.pyplot as plt fig = plt.figure(1) fig.clf() ax = plt.subplot(121) @@ -1672,7 +1619,6 @@ def finalize_offset(self): ) ax.add_artist(ann) - + plt.draw() plt.show() - diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 793052567fb8..70fe014f5f32 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -1629,7 +1629,6 @@ def pprint_styles(klass): """ return _pprint_styles(klass._style_list) - @classmethod def register(klass, name, style): """ @@ -1699,9 +1698,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 @@ -1713,8 +1709,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.): """ @@ -1740,6 +1734,14 @@ 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): @@ -2308,9 +2310,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 @@ -2413,7 +2412,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 @@ -2454,6 +2452,15 @@ def __call__(self, posA, posB, 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): """ @@ -2783,7 +2790,6 @@ def connect(self, posA, posB): {"AvailableConnectorstyles": _pprint_styles(_style_list)} - class ArrowStyle(_Style): """ :class:`ArrowStyle` is a container class which defines several @@ -2879,8 +2885,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.): """ @@ -2914,6 +2918,14 @@ def __call__(self, path, mutation_size, linewidth, 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): @@ -3060,7 +3072,6 @@ def __init__(self): _style_list["-"] = Curve - class CurveA(_Curve): """ An arrow with a head at its begin point. @@ -3099,7 +3110,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 @@ -3121,11 +3131,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 126c59289a42..60702ff841ad 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -18,201 +18,209 @@ 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_non_affine(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_non_affine(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_non_affine.__doc__ = Transform.transform_non_affine.__doc__ + return xy + transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__ - def transform_path_non_affine(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_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__ + def transform_path_non_affine(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_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_non_affine(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__ + + def __getstate__(self): + return {} + + +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_non_affine(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_non_affine.__doc__ = Transform.transform_non_affine.__doc__ + return np.concatenate((theta, r), 1) + transform_non_affine.__doc__ = Transform.transform_non_affine.__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): """ Create a new Polar Axes for a polar plot. @@ -451,8 +459,10 @@ def set_yscale(self, *args, **kwargs): self.yaxis.set_major_locator( self.RadialLocator(self.yaxis.get_major_locator())) - set_rscale = Axes.set_yscale - set_rticks = Axes.set_yticks + def set_rscale(self, *args, **kwargs): + return Axes.set_yscale(self, *args, **kwargs) + def set_rticks(self, *args, **kwargs): + return Axes.set_yticks(self, *args, **kwargs) @docstring.dedent_interpd def set_thetagrids(self, angles, labels=None, frac=None, fmt=None, @@ -647,6 +657,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 dcd46f87beb6..effc065083f2 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..1e15b6d94fd4 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..5655b6e7e68f --- /dev/null +++ b/lib/matplotlib/tests/test_pickle.py @@ -0,0 +1,199 @@ +from __future__ import print_function + +import numpy as np + +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 depth_getter(obj, + current_depth=0, + depth_stack=None, + nest_info='top level object'): + """ + Returns a dictionary mapping: + + id(obj): (shallowest_depth, obj, nest_info) + + for the given object (and its subordinates). + + This, in conjunction with recursive_pickle, can be used to debug + pickling issues, although finding others is sometimes a case of + trial and error. + + """ + if depth_stack is None: + depth_stack = {} + + if id(obj) in depth_stack: + stack = depth_stack[id(obj)] + if stack[0] > current_depth: + del depth_stack[id(obj)] + else: + return depth_stack + + depth_stack[id(obj)] = (current_depth, obj, nest_info) + + if isinstance(obj, (list, tuple)): + for i, item in enumerate(obj): + depth_getter(item, current_depth=current_depth+1, + depth_stack=depth_stack, + nest_info='list/tuple item #%s in (%s)' % (i, nest_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(): + depth_getter(value, current_depth=current_depth+1, + depth_stack=depth_stack, + nest_info='attribute "%s" in (%s)' % (key, nest_info)) + + # for instancemethod picklability (and some other issues), uncommenting + # the following may be helpful +# print([(name, dobj.__class__) for name, dobj in state.iteritems()], ': ', nest_info, ';', type(obj)) + + return depth_stack + + +def recursive_pickle(top_obj): + """ + Recursively pickle all of the given objects subordinates, starting with + the deepest first. **Very** handy for debugging pickling issues, but + also very slow (as it literally pickles each object in turn). + + Handles circular object references gracefully. + + """ + objs = depth_getter(top_obj) + # sort by depth then by nest_info + objs = sorted(objs.itervalues(), key=lambda val: (-val[0], val[2])) + + for _, obj, location in objs: +# print('trying %s' % location) + try: + pickle.dump(obj, StringIO(), pickle.HIGHEST_PROTOCOL) + except Exception, err: + print(obj) + print('Failed to pickle %s. \n Type: %s. Traceback follows:' % (location, type(obj))) + raise + + +@cleanup +def test_simple(): + fig = plt.figure() + # un-comment to debug +# recursive_pickle(fig) + pickle.dump(fig, StringIO(), pickle.HIGHEST_PROTOCOL) + + ax = plt.subplot(121) + pickle.dump(ax, StringIO(), pickle.HIGHEST_PROTOCOL) + + ax = plt.axes(projection='polar') + plt.plot(range(10), label='foobar') + plt.legend() + +# recursive_pickle(fig) + 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?', figsize=(10, 6)) + + 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() + + plt.subplot(3, 3, 3) + plt.pcolormesh(data) + + + 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(loc='upper left') + + plt.subplot(3, 3, 9) + plt.errorbar(x, x * -0.5, xerr=0.2, yerr=0.4) + + ###### plotting is done, now test its pickle-ability ######### + + # Uncomment to debug any unpicklable objects. This is slow (~200 seconds). +# recursive_pickle(fig) + + result_fh = StringIO() + 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?') + + +def test_no_pyplot(): + # tests pickle-ability of a figure not created with pyplot + + import pickle as p + from matplotlib.backends.backend_pdf import FigureCanvasPdf as fc + from matplotlib.figure import Figure + + fig = Figure() + can = fc(fig) + ax = fig.add_subplot(1, 1, 1) + ax.plot([1, 2, 3], [1, 2, 3]) + + # Uncomment to debug any unpicklable objects. This is slow so is not + # uncommented by default. +# recursive_pickle(fig) + pickle.dump(fig, StringIO(), pickle.HIGHEST_PROTOCOL) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index c6f45119570d..620223b16c0b 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -133,33 +133,37 @@ from matplotlib import transforms as mtransforms +class _DummyAxis(object): + def __init__(self, minpos=0): + self.dataLim = mtransforms.Bbox.unit() + self.viewLim = mtransforms.Bbox.unit() + self._minpos = minpos + + def get_view_interval(self): + return self.viewLim.intervalx -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 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_minpos(self): + return self._minpos - 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): + axis = None + def set_axis(self, axis): self.axis = axis - def create_dummy_axis(self): + def create_dummy_axis(self, **kwargs): if self.axis is None: - self.axis = self.DummyAxis() + self.axis = _DummyAxis(**kwargs) def set_view_interval(self, vmin, vmax): self.axis.set_view_interval(vmin, vmax) @@ -176,7 +180,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 +217,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 +243,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 +265,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 +289,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 f884881708bb..2b12d0d79ed9 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -86,7 +86,7 @@ def __init__(self, shorthand_name=None): other than to improve the readability of ``str(transform)`` when DEBUG=True. """ - # Parents are stored in a WeakKeyDictionary, so that if the + # Parents are stored in a WeakValueDictionary, so that if the # parents are deleted, references from the children won't keep # them alive. self._parents = WeakValueDictionary() @@ -101,6 +101,17 @@ def __str__(self): # either just return the name of this TransformNode, or it's repr return self._shorthand_name or repr(self) + 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 WeakValueDictionary + self._parents = WeakValueDictionary(self._parents) + def __copy__(self, *args): raise NotImplementedError( "TransformNode instances can not be copied. " + @@ -1398,7 +1409,6 @@ 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 @@ -1412,6 +1422,14 @@ def __eq__(self, other): def __str__(self): return str(self._child) + 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