diff --git a/doc/api/api_changes.rst b/doc/api/api_changes.rst index a1f6682170fc..2b118428fad4 100644 --- a/doc/api/api_changes.rst +++ b/doc/api/api_changes.rst @@ -11,6 +11,20 @@ help figure out possible sources of the changes you are experiencing. For new features that were added to matplotlib, please see :ref:`whats-new`. +Changes in 1.2.x +================ +* Use of :func:`matplotlib.projections.projection_factory` is now deprecated + in favour of axes class identification using + :func:`matplotlib.projections.process_projection_requirements` followed by + direct axes class invocation (at the time of writing, this is done by + :meth:`matplotlib.figure.Figure.add_axes`, + :meth:`matplotlib.figure.Figure.add_subplot` and + :meth:`matplotlib.figure.Figure.gca`. + This change means that third party objects can expose themselves as + matplotlib axes by providing a ``_as_mpl_axes`` method (see + :ref:`adding-new-scales` for more detail). + + Changes in 1.1.x ================ diff --git a/doc/devel/add_new_projection.rst b/doc/devel/add_new_projection.rst index 5efbead7fc40..8704a2542cdc 100644 --- a/doc/devel/add_new_projection.rst +++ b/doc/devel/add_new_projection.rst @@ -71,8 +71,9 @@ in :mod:`matplotlib.scale` that may be used as starting points. Creating a new projection ========================= -Adding a new projection consists of defining a subclass of -:class:`matplotlib.axes.Axes`, that includes the following elements: +Adding a new projection consists of defining a projection axes which +subclasses :class:`matplotlib.axes.Axes` and includes the following +elements: - A transformation from data coordinates into display coordinates. @@ -98,11 +99,28 @@ Adding a new projection consists of defining a subclass of - Set up interactive panning and zooming. This is left as an "advanced" feature left to the reader, but there is an example of this for polar plots in :mod:`matplotlib.projections.polar`. - + - Any additional methods for additional convenience or features. -Once the class is defined, it must be registered with matplotlib -so that the user can select it. +Once the projection axes is defined, it can be used in one of two ways: + + - By defining the class attribute ``NAME``, the projection axes can be + registered with :func:`matplotlib.projections.register_projection` + and subsequently simply invoked by name:: + + plt.axes(projection=NAME) + + - For more complex, parameterisable projections, a generic "projection" + object may be defined which includes the method ``_as_mpl_axes``. + ``_as_mpl_axes`` should take no arguments and return the projection's + axes subclass and a dictionary of additional arguments to pass to the + subclass' ``__init__`` method. Subsequently a parameterised projection + can be initialised with:: + + plt.axes(projection=MyProjection(param1=param1_value)) + + where MyProjection is an object which implements a ``_as_mpl_axes`` method. + A full-fledged and heavily annotated example is in :file:`examples/api/custom_projection_example.py`. The polar plot diff --git a/lib/matplotlib/axes.py b/lib/matplotlib/axes.py index 3f418a30aed6..545ff665aaa5 100644 --- a/lib/matplotlib/axes.py +++ b/lib/matplotlib/axes.py @@ -938,15 +938,11 @@ def hold(self, b=None): Set the hold state. If *hold* is *None* (default), toggle the *hold* state. Else set the *hold* state to boolean value *b*. - Examples: - - * toggle hold: - >>> hold() - * turn hold on: - >>> hold(True) - * turn hold off - >>> hold(False) - + Examples:: + + hold() # toggle hold + hold(True) # turn hold on + hold(False) # turn hold off When hold is True, subsequent plot commands will be added to the current axes. When hold is False, the current axes and @@ -3461,12 +3457,11 @@ def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs): Return value is a :class:`matplotlib.patches.Polygon` instance. - Examples: - - * draw a gray rectangle from *y* = 0.25-0.75 that spans the - horizontal extent of the axes + Examples:: - >>> axhspan(0.25, 0.75, facecolor='0.5', alpha=0.5) + # draw a gray rectangle from *y* = 0.25-0.75 that spans the + # horizontal extent of the axes + axhspan(0.25, 0.75, facecolor='0.5', alpha=0.5) Valid kwargs are :class:`~matplotlib.patches.Polygon` properties: @@ -3517,12 +3512,11 @@ def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs): Return value is the :class:`matplotlib.patches.Polygon` instance. - Examples: - - * draw a vertical green translucent rectangle from x=1.25 to 1.55 that - spans the yrange of the axes + Examples:: - >>> axvspan(1.25, 1.55, facecolor='g', alpha=0.5) + # draw a vertical green translucent rectangle from x=1.25 to 1.55 + # that spans the yrange of the axes + axvspan(1.25, 1.55, facecolor='g', alpha=0.5) Valid kwargs are :class:`~matplotlib.patches.Polygon` properties: @@ -6872,7 +6866,7 @@ def pcolor(self, *args, **kwargs): y = np.arange(3) X, Y = meshgrid(x,y) - is equivalent to: + is equivalent to:: X = array([[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], @@ -7412,11 +7406,11 @@ def hist(self, x, bins=10, range=None, normed=False, weights=None, """ call signature:: - def hist(x, bins=10, range=None, normed=False, weights=None, + hist(x, bins=10, range=None, normed=False, weights=None, cumulative=False, bottom=None, histtype='bar', align='mid', orientation='vertical', rwidth=None, log=False, color=None, label=None, - **kwargs): + **kwargs) Compute and draw the histogram of *x*. The return value is a tuple (*n*, *bins*, *patches*) or ([*n0*, *n1*, ...], *bins*, @@ -8439,7 +8433,7 @@ def label_outer(self): _subplot_classes = {} def subplot_class_factory(axes_class=None): - # This makes a new class that inherits from SubclassBase and the + # This makes a new class that inherits from SubplotBase and the # given axes_class (which is assumed to be a subclass of Axes). # This is perhaps a little bit roundabout to make a new class on # the fly like this, but it means that a new Subplot class does diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 87d819c1260e..dbc08c447734 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -27,8 +27,8 @@ from legend import Legend from transforms import Affine2D, Bbox, BboxTransformTo, TransformedBbox -from projections import projection_factory, get_projection_names, \ - get_projection_class +from projections import get_projection_names, get_projection_class, \ + process_projection_requirements from matplotlib.blocking_input import BlockingMouseInput, BlockingKeyMouseInput import matplotlib.cbook as cbook @@ -41,11 +41,18 @@ class AxesStack(Stack): """ - Specialization of the Stack to handle all - tracking of Axes in a Figure. This requires storing - key, (ind, axes) pairs. The key is based on the args and kwargs - used in generating the Axes. ind is a serial number for tracking - the order in which axes were added. + Specialization of the Stack to handle all tracking of Axes in a Figure. + This stack stores ``key, (ind, axes)`` pairs, where: + + * **key** should be a hash of the args and kwargs + used in generating the Axes. + * **ind** is a serial number for tracking the order + in which axes were added. + + The AxesStack is a callable, where ``ax_stack()`` returns + the current axes. Alternatively the :meth:`current_key_axes` will + return the current key and associated axes. + """ def __init__(self): Stack.__init__(self) @@ -74,9 +81,14 @@ def _entry_from_axes(self, e): return (k, (ind, e)) def remove(self, a): + """Remove the axes from the stack.""" Stack.remove(self, self._entry_from_axes(a)) - + def bubble(self, a): + """ + Move the given axes, which must already exist in the + stack, to the top. + """ return Stack.bubble(self, self._entry_from_axes(a)) def add(self, key, a): @@ -107,12 +119,22 @@ def add(self, key, a): self._ind += 1 return Stack.push(self, (key, (self._ind, a))) - def __call__(self): + def current_key_axes(self): + """ + Return a tuple of ``(key, axes)`` for the active axes. + + If no axes exists on the stack, then returns ``(None, None)``. + + """ if not len(self._elements): - return self._default + return self._default, self._default else: - return self._elements[self._pos][1][1] + key, (index, axes) = self._elements[self._pos] + return key, axes + def __call__(self): + return self.current_key_axes()[1] + def __contains__(self, a): return a in self.as_list() @@ -681,6 +703,8 @@ def add_axes(self, *args, **kwargs): """ if not len(args): return + # shortcut the projection "key" modifications later on, if an axes + # with the exact args/kwargs exists, return it immediately. key = self._make_key(*args, **kwargs) ax = self._axstack.get(key) if ax is not None: @@ -692,18 +716,19 @@ def add_axes(self, *args, **kwargs): assert(a.get_figure() is self) else: rect = args[0] - ispolar = kwargs.pop('polar', False) - projection = kwargs.pop('projection', None) - if ispolar: - if projection is not None and projection != 'polar': - raise ValueError( - "polar=True, yet projection='%s'. " + - "Only one of these arguments should be supplied." % - projection) - projection = 'polar' - - a = projection_factory(projection, self, rect, **kwargs) - + projection_class, kwargs, key = \ + process_projection_requirements(self, *args, **kwargs) + + # check that an axes of this type doesn't already exist, if it + # does, set it as active and return it + ax = self._axstack.get(key) + if ax is not None and isinstance(ax, projection_class): + self.sca(ax) + return ax + + # create the new axes using the axes class given + a = projection_class(self, rect, **kwargs) + self._axstack.add(key, a) self.sca(a) return a @@ -711,13 +736,21 @@ def add_axes(self, *args, **kwargs): @docstring.dedent_interpd def add_subplot(self, *args, **kwargs): """ - Add a subplot. Examples: + Add a subplot. Examples:: fig.add_subplot(111) - fig.add_subplot(1,1,1) # equivalent but more general - fig.add_subplot(212, axisbg='r') # add subplot with red background - fig.add_subplot(111, polar=True) # add a polar subplot - fig.add_subplot(sub) # add Subplot instance sub + + # equivalent but more general + fig.add_subplot(1,1,1) + + # add subplot with red background + fig.add_subplot(212, axisbg='r') + + # add a polar subplot + fig.add_subplot(111, projection='polar') + + # add Subplot instance sub + fig.add_subplot(sub) *kwargs* are legal :class:`!matplotlib.axes.Axes` kwargs plus *projection*, which chooses a projection type for the axes. @@ -741,41 +774,34 @@ def add_subplot(self, *args, **kwargs): if len(args) == 1 and isinstance(args[0], int): args = tuple([int(c) for c in str(args[0])]) - + if isinstance(args[0], SubplotBase): + a = args[0] assert(a.get_figure() is self) - key = self._make_key(*args, **kwargs) + key = self._make_key(*args[1:], **kwargs) else: - kwargs = kwargs.copy() - ispolar = kwargs.pop('polar', False) - projection = kwargs.pop('projection', None) - if ispolar: - if projection is not None and projection != 'polar': - raise ValueError( - "polar=True, yet projection='%s'. " + - "Only one of these arguments should be supplied." % - projection) - projection = 'polar' - - projection_class = get_projection_class(projection) - - # Remake the key without projection kwargs: - key = self._make_key(*args, **kwargs) + projection_class, kwargs, key = \ + process_projection_requirements(self, *args, **kwargs) + + # try to find the axes with this key in the stack ax = self._axstack.get(key) + if ax is not None: if isinstance(ax, projection_class): + # the axes already existed, so set it as active & return self.sca(ax) return ax else: - self._axstack.remove(ax) # Undocumented convenience behavior: # subplot(111); subplot(111, projection='polar') # will replace the first with the second. # Without this, add_subplot would be simpler and # more similar to add_axes. - + self._axstack.remove(ax) + a = subplot_class_factory(projection_class)(self, *args, **kwargs) + self._axstack.add(key, a) self.sca(a) return a @@ -1033,24 +1059,40 @@ def gca(self, **kwargs): """ Return the current axes, creating one if necessary - The following kwargs are supported + The following kwargs are supported for ensuring the returned axes + adheres to the given projection etc., and for axes creation if + the active axes does not exist: %(Axes)s - """ - ax = self._axstack() - if ax is not None: - ispolar = kwargs.get('polar', False) - projection = kwargs.get('projection', None) - if ispolar: - if projection is not None and projection != 'polar': - raise ValueError( - "polar=True, yet projection='%s'. " + - "Only one of these arguments should be supplied." % - projection) - projection = 'polar' - - projection_class = get_projection_class(projection) - if isinstance(ax, projection_class): - return ax + + .. note:: + When specifying kwargs to ``gca`` to find the pre-created active + axes, they should be equivalent in every way to the kwargs which + were used in its creation. + + """ + ckey, cax = self._axstack.current_key_axes() + # if there exists an axes on the stack see if it maches + # the desired axes configuration + if cax is not None: + + # if no kwargs are given just return the current axes + # this is a convenience for gca() on axes such as polar etc. + if not kwargs: + return cax + + # if the user has specified particular projection detail + # then build up a key which can represent this + else: + # we don't want to modify the original kwargs + # so take a copy so that we can do what we like to it + kwargs_copy = kwargs.copy() + projection_class, _, key = \ + process_projection_requirements(self, **kwargs_copy) + # if the cax matches this key then return the axes, otherwise + # continue and a new axes will be created + if key == ckey and isinstance(cax, projection_class): + return cax + return self.add_subplot(111, **kwargs) def sca(self, a): @@ -1081,7 +1123,7 @@ def savefig(self, *args, **kwargs): savefig(fname, dpi=None, facecolor='w', edgecolor='w', orientation='portrait', papertype=None, format=None, - transparent=False, bbox_inches=None, pad_inches=0.1): + transparent=False, bbox_inches=None, pad_inches=0.1) Save the current figure. diff --git a/lib/matplotlib/projections/__init__.py b/lib/matplotlib/projections/__init__.py index cbdd114d524f..7b999a39cddf 100644 --- a/lib/matplotlib/projections/__init__.py +++ b/lib/matplotlib/projections/__init__.py @@ -46,6 +46,7 @@ def get_projection_names(self): def register_projection(cls): projection_registry.register(cls) + def get_projection_class(projection=None): """ Get a projection class from its name. @@ -61,6 +62,7 @@ def get_projection_class(projection=None): except KeyError: raise ValueError("Unknown projection '%s'" % projection) + def projection_factory(projection, figure, rect, **kwargs): """ Get a new projection instance. @@ -74,10 +76,56 @@ def projection_factory(projection, figure, rect, **kwargs): Any other kwargs are passed along to the specific projection constructor being used. + + .. deprecated:: + + This routine is deprecated in favour of getting the projection + class directly with :func:`get_projection_class` and initialising it + directly. Will be removed in version 1.3. + """ return get_projection_class(projection)(figure, rect, **kwargs) + +def process_projection_requirements(figure, *args, **kwargs): + """ + Handle the args/kwargs to for add_axes/add_subplot/gca, + returning:: + + (axes_proj_class, proj_class_kwargs, proj_stack_key) + + Which can be used for new axes initialization/identification. + + .. note:: **kwargs** is modified in place. + + """ + ispolar = kwargs.pop('polar', False) + projection = kwargs.pop('projection', None) + if ispolar: + if projection is not None and projection != 'polar': + raise ValueError( + "polar=True, yet projection=%r. " + "Only one of these arguments should be supplied." % + projection) + projection = 'polar' + + if isinstance(projection, basestring) or projection is None: + projection_class = get_projection_class(projection) + elif hasattr(projection, '_as_mpl_axes'): + projection_class, extra_kwargs = projection._as_mpl_axes() + kwargs.update(**extra_kwargs) + else: + raise TypeError('projection must be a string, None or implement a ' + '_as_mpl_axes method. Got %r' % projection) + + # Make the key without projection kwargs, this is used as a unique + # lookup for axes instances + key = figure._make_key(*args, **kwargs) + + return projection_class, kwargs, key + + def get_projection_names(): """ Get a list of acceptable projection names. diff --git a/lib/matplotlib/tests/test_tightlayout.py b/lib/matplotlib/tests/test_tightlayout.py index 3e6d7861d109..a846b56bb21b 100644 --- a/lib/matplotlib/tests/test_tightlayout.py +++ b/lib/matplotlib/tests/test_tightlayout.py @@ -121,3 +121,6 @@ def test_tight_layout6(): h_pad=0.5) +if __name__=='__main__': + import nose + nose.runmodule(argv=['-s','--with-doctest'], exit=False)