Revision: 4803
          http://matplotlib.svn.sourceforge.net/matplotlib/?rev=4803&view=rev
Author:   mdboom
Date:     2008-01-07 13:15:58 -0800 (Mon, 07 Jan 2008)

Log Message:
-----------
Provide heavily-documented examples for adding new scales and
projections.
Fix various bugs related to non-rectangular clipping.
Remove MercatorLatitude scale from core and put it in an example.

Modified Paths:
--------------
    branches/transforms/doc/devel/add_new_projection.rst
    branches/transforms/lib/matplotlib/axes.py
    branches/transforms/lib/matplotlib/patches.py
    branches/transforms/lib/matplotlib/projections/__init__.py
    branches/transforms/lib/matplotlib/projections/geo.py
    branches/transforms/lib/matplotlib/projections/polar.py
    branches/transforms/lib/matplotlib/scale.py

Added Paths:
-----------
    branches/transforms/examples/custom_projection_example.py
    branches/transforms/examples/custom_scale_example.py

Modified: branches/transforms/doc/devel/add_new_projection.rst
===================================================================
--- branches/transforms/doc/devel/add_new_projection.rst        2008-01-06 
18:28:17 UTC (rev 4802)
+++ branches/transforms/doc/devel/add_new_projection.rst        2008-01-07 
21:15:58 UTC (rev 4803)
@@ -4,190 +4,99 @@
 
 .. ::author Michael Droettboom
 
-Matplotlib supports the addition of new transformations that transform
-the data before it is displayed.  In ``matplotlib`` nomenclature,
-separable transformations, working on a single dimension, are called
-"scales", and non-separable transformations, that take handle data in
-two or more dimensions at a time, are called "projections".
+Matplotlib supports the addition of custom procedures that transform
+the data before it is displayed.
 
+There is an important distinction between two kinds of
+transformations.  Separable transformations, working on a single
+dimension, are called "scales", and non-separable transformations,
+that handle data in two or more dimensions at a time, are called
+"projections".
+
 From the user's perspective, the scale of a plot can be set with
-``set_xscale`` and ``set_yscale``.  Choosing the projection
-currently has no *standardized* method. [MGDTODO]
+``set_xscale()`` and ``set_yscale()``.  Projections can be chosen using
+the ``projection`` keyword argument to the ``plot()`` or ``subplot()``
+functions::
 
+    plot(x, y, projection="custom")
+
 This document is intended for developers and advanced users who need
-to add more scales and projections to matplotlib.
+to create new scales and projections for matplotlib.  The necessary
+code for scales and projections can be included anywhere: directly
+within a plot script, in third-party code, or in the matplotlib source
+tree itself.
 
 
 Creating a new scale
 ====================
 
-Adding a new scale consists of defining a subclass of ``ScaleBase``,
-that brings together the following elements:
+Adding a new scale consists of defining a subclass of ``ScaleBase``
+(in the ``matplotlib.scale`` module), that includes the following
+elements:
 
-  - A transformation from data space into plot space.
+  - A transformation from data coordinates into display coordinates.
 
-  - An inverse of that transformation.  For example, this is used to
-    convert mouse positions back into data space.
+  - An inverse of that transformation.  This is used, for example, to
+    convert mouse positions from screen space back into data space.
 
-  - A function to limit the range of the axis to acceptable values.  A
-    log scale, for instance, would prevent the range from including
-    values less than or equal to zero.
+  - A function to limit the range of the axis to acceptable values
+    (``limit_range_for_scale()``).  A log scale, for instance, would
+    prevent the range from including values less than or equal to
+    zero.
 
   - Locators (major and minor) that determine where to place ticks in
     the plot, and optionally, how to adjust the limits of the plot to
-    some "good" values.
+    some "good" values.  Unlike ``limit_range_for_scale()``, which is
+    always enforced, the range setting here is only used when
+    automatically setting the range of the plot.
 
   - Formatters (major and minor) that specify how the tick labels
     should be drawn.
 
-There are a number of ``Scale`` classes in ``scale.py`` that may be
-used as starting points for new scales.  As an example, this document
-presents adding a new scale ``MercatorLatitudeScale`` which can be
-used to plot latitudes in a Mercator_ projection.  For simplicity,
-this scale assumes that it has a fixed center at the equator.  The
-code presented here is a simplification of actual code in
-``matplotlib``, with complications added only for the sake of
-optimization removed.
+Once the class is defined, it must be registered with ``matplotlib``
+so that the user can select it.
 
-First define a new subclass of ``ScaleBase``::
+A full-fledged and heavily annotated example is in
+``examples/custom_scale_example.py``.  There are also some ``Scale``
+classes in ``scale.py`` that may be used as starting points.
 
-    class MercatorLatitudeScale(ScaleBase):
-        """
-        Scales data in range -pi/2 to pi/2 (-90 to 90 degrees) using
-        the system used to scale latitudes in a Mercator projection.
 
-        The scale function:
-          ln(tan(y) + sec(y))
+Creating a new projection
+=========================
 
-        The inverse scale function:
-          atan(sinh(y))
+Adding a new projection consists of defining a subclass of ``Axes``
+(in the ``matplotlib.axes`` module), that includes the following
+elements:
 
-        Since the Mercator scale tends to infinity at +/- 90 degrees,
-        there is user-defined threshold, above and below which nothing
-        will be plotted.  This defaults to +/- 85 degrees.
+  - A transformation from data coordinates into display coordinates.
 
-        source:
-        http://en.wikipedia.org/wiki/Mercator_projection
-        """
-        name = 'mercator_latitude'
+  - An inverse of that transformation.  This is used, for example, to
+    convert mouse positions from screen space back into data space.
 
-This class must have a member ``name`` that defines the string used to
-select the scale.  For example,
-``gca().set_yscale("mercator_latitude")`` would be used to select the
-Mercator latitude scale.
+  - Transformations for the gridlines, ticks and ticklabels.  Custom
+    projections will often need to place these elements in special
+    locations, and ``matplotlib`` has a facility to help with doing so.
 
-Next define two nested classes: one for the data transformation and
-one for its inverse.  Both of these classes must be subclasses of
-``Transform`` (defined in ``transforms.py``).::
+  - Setting up default values (overriding ``cla()``), since the
+    defaults for a rectilinear axes may not be appropriate.
 
-        class MercatorLatitudeTransform(Transform):
-            input_dims = 1
-            output_dims = 1
+  - Defining the shape of the axes, for example, an elliptical axes,
+    that will be used to draw the background of the plot and for
+    clipping any data elements.
 
-There are two class-members that must be defined.  ``input_dims`` and
-``output_dims`` specify number of input dimensions and output
-dimensions to the transformation.  These are used by the
-transformation framework to do some error checking and prevent
-incompatible transformations from being connected together.  When
-defining transforms for a scale, which are by definition separable and
-only have one dimension, these members should always be 1.
+  - Defining custom locators and formatters for the projection.  For
+    example, in a geographic projection, it may be more convenient to
+    display the grid in degrees, even if the data is in radians.
 
-``MercatorLatitudeTransform`` has a simple constructor that takes and
-stores the *threshold* for the Mercator projection (to limit its range
-to prevent plotting to infinity).::
+  - 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 ``polar.py``.
 
-            def __init__(self, thresh):
-                Transform.__init__(self)
-                self.thresh = thresh
+  - Any additional methods for additional convenience or features.
 
-The ``transform`` method is where the real work happens: It takes an N
-x 1 ``numpy`` array and returns a transformed copy.  Since the range
-of the Mercator scale is limited by the user-specified threshold, the
-input array must be masked to contain only valid values.
-``matplotlib`` will handle masked arrays and remove the out-of-range
-data from the plot.  Importantly, the transformation should return an
-array that is the same shape as the input array, since these values
-need to remain synchronized with values in the other dimension.::
+Once the class is defined, it must be registered with ``matplotlib``
+so that the user can select it.
 
-            def transform(self, a):
-                masked = ma.masked_where((a < -self.thresh) | (a > 
self.thresh), a)
-                return ma.log(ma.abs(ma.tan(masked) + 1.0 / ma.cos(masked)))
-
-Lastly for the transformation class, define a method to get the
-inverse transformation::
-
-            def inverted(self):
-                return 
MercatorLatitudeScale.InvertedMercatorLatitudeTransform(self.thresh)
-
-The inverse transformation class follows the same pattern, but
-obviously the mathematical operation performed is different::
-
-        class InvertedMercatorLatitudeTransform(Transform):
-            input_dims = 1
-            output_dims = 1
-
-            def __init__(self, thresh):
-                Transform.__init__(self)
-                self.thresh = thresh
-
-            def transform(self, a):
-                return npy.arctan(npy.sinh(a))
-
-            def inverted(self):
-                return 
MercatorLatitudeScale.MercatorLatitudeTransform(self.thresh)
-
-Now we're back to methods for the ``MercatorLatitudeScale`` class.
-Any keyword arguments passed to ``set_xscale`` and ``set_yscale`` will
-be passed along to the scale's constructor.  In the case of
-``MercatorLatitudeScale``, the ``thresh`` keyword argument specifies
-the degree at which to crop the plot data.  The constructor also
-creates a local instance of the ``Transform`` class defined above,
-which is made available through its ``get_transform`` method::
-
-        def __init__(self, axis, **kwargs):
-            thresh = kwargs.pop("thresh", (85 / 180.0) * npy.pi)
-            if thresh >= npy.pi / 2.0:
-                raise ValueError("thresh must be less than pi/2")
-            self.thresh = thresh
-            self._transform = self.MercatorLatitudeTransform(thresh)
-
-        def get_transform(self):
-            return self._transform
-
-The ``limit_range_for_scale`` method must be provided to limit the
-bounds of the axis to the domain of the function.  In the case of
-Mercator, the bounds should be limited to the threshold that was
-passed in.  Unlike the autoscaling provided by the tick locators, this
-range limiting will always be adhered to, whether the axis range is set
-manually, determined automatically or changed through panning and
-zooming::
-
-        def limit_range_for_scale(self, vmin, vmax, minpos):
-            return max(vmin, -self.thresh), min(vmax, self.thresh)
-
-Lastly, the ``set_default_locators_and_formatters`` method sets up the
-locators and formatters to use with the scale.  It may be that the new
-scale requires new locators and formatters.  Doing so is outside the
-scope of this document, but there are many examples in ``ticker.py``.
-The Mercator example uses a fixed locator from -90 to 90 degrees and a
-custom formatter class to put convert the radians to degrees and put a
-degree symbol after the value::
-
-        def set_default_locators_and_formatters(self, axis):
-            class DegreeFormatter(Formatter):
-                def __call__(self, x, pos=None):
-                    # \u00b0 : degree symbol
-                    return u"%d\u00b0" % ((x / npy.pi) * 180.0)
-
-            deg2rad = npy.pi / 180.0
-            axis.set_major_locator(FixedLocator(
-                    npy.arange(-90, 90, 10) * deg2rad))
-            axis.set_major_formatter(DegreeFormatter())
-            axis.set_minor_formatter(DegreeFormatter())
-
-Now that the Scale class has been defined, it must be registered so
-that ``matplotlib`` can find it::
-
-       register_scale(MercatorLatitudeScale)
-
-.. _Mercator: http://en.wikipedia.org/wiki/Mercator_projection
\ No newline at end of file
+A full-fledged and heavily annotated example is in
+``examples/custom_projection_example.py``.  The polar plot
+functionality in ``polar.py`` may also be interest.

Added: branches/transforms/examples/custom_projection_example.py
===================================================================
--- branches/transforms/examples/custom_projection_example.py                   
        (rev 0)
+++ branches/transforms/examples/custom_projection_example.py   2008-01-07 
21:15:58 UTC (rev 4803)
@@ -0,0 +1,473 @@
+from matplotlib.axes import Axes
+from matplotlib import cbook
+from matplotlib.patches import Circle
+from matplotlib.path import Path
+from matplotlib.ticker import Formatter, Locator, NullLocator, FixedLocator, 
NullFormatter
+from matplotlib.transforms import Affine2D, Affine2DBase, Bbox, \
+    BboxTransformTo, IdentityTransform, Transform, TransformWrapper
+from matplotlib.projections import register_projection
+
+# This example projection class is rather long, but it is designed to
+# illustrate many features, not all of which will be used every time.
+# It is also common to factor out a lot of these methods into common
+# code used by a number of projections with similar characteristics
+# (see geo.py).
+
+class HammerAxes(Axes):
+    """
+    A custom class for the Aitoff-Hammer projection, an equal-area map
+    projection.
+
+    http://en.wikipedia.org/wiki/Hammer_projection
+    """
+    # The projection must specify a name.  This will be used be the
+    # user to select the projection, i.e. ``subplot(111,
+    # projection='hammer')``.
+    name = 'hammer'
+
+    # The number of interpolation steps when converting from straight
+    # lines to curves.  (See ``transform_path``).
+    RESOLUTION = 75
+
+    def __init__(self, *args, **kwargs):
+        Axes.__init__(self, *args, **kwargs)
+        self.set_aspect(0.5, adjustable='box', anchor='C')
+        self.cla()
+
+    def cla(self):
+        """
+        Override to set up some reasonable defaults.
+        """
+        # Don't forget to call the base class
+        Axes.cla(self)
+
+        # Set up a default grid spacing
+        self.set_longitude_grid(30)
+        self.set_latitude_grid(15)
+        self.set_longitude_grid_ends(75)
+
+        # Turn off minor ticking altogether
+        self.xaxis.set_minor_locator(NullLocator())
+        self.yaxis.set_minor_locator(NullLocator())
+
+        # Do not display ticks -- we only want gridlines and text
+        self.xaxis.set_ticks_position('none')
+        self.yaxis.set_ticks_position('none')
+
+        # The limits on this projection are fixed -- they are not to
+        # be changed by the user.  This makes the math in the
+        # transformation itself easier, and since this is a toy
+        # example, the easier, the better.
+        Axes.set_xlim(self, -npy.pi, npy.pi)
+        Axes.set_ylim(self, -npy.pi / 2.0, npy.pi / 2.0)
+
+    def cla(self):
+        """
+        Initialize the Axes object to reasonable defaults.
+        """
+        Axes.cla(self)
+
+        self.set_longitude_grid(30)
+        self.set_latitude_grid(15)
+        self.set_longitude_grid_ends(75)
+        self.xaxis.set_minor_locator(NullLocator())
+        self.yaxis.set_minor_locator(NullLocator())
+        self.xaxis.set_ticks_position('none')
+        self.yaxis.set_ticks_position('none')
+
+        # self.grid(rcParams['axes.grid'])
+
+        Axes.set_xlim(self, -npy.pi, npy.pi)
+        Axes.set_ylim(self, -npy.pi / 2.0, npy.pi / 2.0)
+
+    def _set_lim_and_transforms(self):
+        """
+        This is called once when the plot is created to set up all the
+        transforms for the data, text and grids.
+        """
+        # There are three important coordinate spaces going on here:
+        #
+        #    1. Data space: The space of the data itself
+        #
+        #    2. Axes space: The unit rectangle (0, 0) to (1, 1)
+        #       covering the entire plot area.
+        #
+        #    3. Display space: The coordinates of the resulting image,
+        #       often in pixels or dpi/inch.
+
+        # This function makes heavy use of the Transform classes in
+        # ``lib/matplotlib/transforms.py.`` For more information, see
+        # the inline documentation there.
+
+        # The goal of the first two transformations is to get from the
+        # data space (in this case longitude and latitude) to axes
+        # space.  It is separated into a non-affine and affine part so
+        # that the non-affine part does not have to be recomputed when
+        # a simple affine change to the figure has been made (such as
+        # resizing the window or changing the dpi).
+
+        # 1) The core transformation from data space into
+        # rectilinear space defined in the HammerTransform class.
+        self.transProjection = self.HammerTransform(self.RESOLUTION)
+
+        # 2) The above has an output range that is not in the unit
+        # rectangle, so scale and translate it so it fits correctly
+        # within the axes.  The peculiar calculations of xscale and
+        # yscale are specific to a Aitoff-Hammer projection, so don't
+        # worry about them too much.
+        xscale = 2.0 * npy.sqrt(2.0) * npy.sin(0.5 * npy.pi)
+        yscale = npy.sqrt(2.0) * npy.sin(0.5 * npy.pi)
+        self.transAffine = Affine2D() \
+            .scale(0.5 / xscale, 0.5 / yscale) \
+            .translate(0.5, 0.5)
+
+        # 3) This is the transformation from axes space to display
+        # space.
+        self.transAxes = BboxTransformTo(self.bbox)
+
+        # Now put these 3 transforms together -- from data all the way
+        # to display coordinates.  Using the '+' operator, these
+        # transforms will be applied "in order".  The transforms are
+        # automatically simplified, if possible, by the underlying
+        # transformation framework.
+        self.transData = \
+            self.transProjection + \
+            self.transAffine + \
+            self.transAxes
+
+        # The main data transformation is set up.  Now deal with
+        # gridlines and tick labels.
+
+        # Longitude gridlines and ticklabels.  The input to these
+        # transforms are in display space in x and axes space in y.
+        # Therefore, the input values will be in range (-xmin, 0),
+        # (xmax, 1).  The goal of these transforms is to go from that
+        # space to display space.  The tick labels will be offset 4
+        # pixels from the equator.
+        self._xaxis_pretransform = \
+            Affine2D() \
+            .scale(1.0, npy.pi) \
+            .translate(0.0, -npy.pi)
+        self._xaxis_transform = \
+            self._xaxis_pretransform + \
+            self.transData
+        self._xaxis_text1_transform = \
+            Affine2D().scale(1.0, 0.0) + \
+            self.transData + \
+            Affine2D().translate(0.0, 4.0)
+        self._xaxis_text2_transform = \
+            Affine2D().scale(1.0, 0.0) + \
+            self.transData + \
+            Affine2D().translate(0.0, -4.0)
+
+        # Now set up the transforms for the latitude ticks.  The input to
+        # these transforms are in axes space in x and display space in
+        # y.  Therefore, the input values will be in range (0, -ymin),
+        # (1, ymax).  The goal of these transforms is to go from that
+        # space to display space.  The tick labels will be offset 4
+        # pixels from the edge of the axes ellipse.
+        yaxis_stretch = Affine2D().scale(npy.pi * 2.0, 1.0).translate(-npy.pi, 
0.0)
+        yaxis_space = Affine2D().scale(1.0, 1.1)
+        self._yaxis_transform = \
+            yaxis_stretch + \
+            self.transData
+        yaxis_text_base = \
+            yaxis_stretch + \
+            self.transProjection + \
+            (yaxis_space + \
+             self.transAffine + \
+             self.transAxes)
+        self._yaxis_text1_transform = \
+            yaxis_text_base + \
+            Affine2D().translate(-8.0, 0.0)
+        self._yaxis_text2_transform = \
+            yaxis_text_base + \
+            Affine2D().translate(8.0, 0.0)
+
+    def get_xaxis_transform(self):
+        """
+        Override this method to provide a transformation for the
+        x-axis grid and ticks.
+        """
+        return self._xaxis_transform
+
+    def get_xaxis_text1_transform(self, pixelPad):
+        """
+        Override this method to provide a transformation for the
+        x-axis tick labels.
+
+        Returns a tuple of the form (transform, valign, halign)
+        """
+        return self._xaxis_text1_transform, 'bottom', 'center'
+
+    def get_xaxis_text2_transform(self, pixelPad):
+        """
+        Override this method to provide a transformation for the
+        secondary x-axis tick labels.
+
+        Returns a tuple of the form (transform, valign, halign)
+        """
+        return self._xaxis_text2_transform, 'top', 'center'
+
+    def get_yaxis_transform(self):
+        """
+        Override this method to provide a transformation for the
+        y-axis grid and ticks.
+        """
+        return self._yaxis_transform
+
+    def get_yaxis_text1_transform(self, pixelPad):
+        """
+        Override this method to provide a transformation for the
+        y-axis tick labels.
+
+        Returns a tuple of the form (transform, valign, halign)
+        """
+        return self._yaxis_text1_transform, 'center', 'right'
+
+    def get_yaxis_text2_transform(self, pixelPad):
+        """
+        Override this method to provide a transformation for the
+        secondary y-axis tick labels.
+
+        Returns a tuple of the form (transform, valign, halign)
+        """
+        return self._yaxis_text2_transform, 'center', 'left'
+
+    def get_axes_patch(self):
+        """
+        Override this method to define the shape that is used for the
+        background of the plot.  It should be a subclass of Patch.
+
+        In this case, it is a Circle (that may be warped by the axes
+        transform into an ellipse).  Any data and gridlines will be
+        clipped to this shape.
+        """
+        return Circle((0.5, 0.5), 0.5)
+
+    # Prevent the user from applying scales to one or both of the
+    # axes.  In this particular case, scaling the axes wouldn't make
+    # sense, so we don't allow it.
+    def set_xscale(self, *args, **kwargs):
+        if args[0] != 'linear':
+            raise NotImplementedError
+        Axes.set_xscale(self, *args, **kwargs)
+
+    def set_yscale(self, *args, **kwargs):
+        if args[0] != 'linear':
+            raise NotImplementedError
+        Axes.set_yscale(self, *args, **kwargs)
+
+    # Prevent the user from changing the axes limits.  In our case, we
+    # want to display the whole sphere all the time, so we override
+    # set_xlim and set_ylim to ignore any input.  This also applies to
+    # interactive panning and zooming in the GUI interfaces.
+    def set_xlim(self, *args, **kwargs):
+        Axes.set_xlim(self, -npy.pi, npy.pi)
+        Axes.set_ylim(self, -npy.pi / 2.0, npy.pi / 2.0)
+    set_ylim = set_xlim
+
+    def format_coord(self, long, lat):
+        """
+        Override this method to change how the values are displayed in
+        the status bar.
+
+        In this case, we want them to be displayed in degrees N/S/E/W.
+        """
+        long = long * (180.0 / npy.pi)
+        lat = lat * (180.0 / npy.pi)
+        if lat >= 0.0:
+            ns = 'N'
+        else:
+            ns = 'S'
+        if long >= 0.0:
+            ew = 'E'
+        else:
+            ew = 'W'
+        # \u00b0 : degree symbol
+        return u'%f\u00b0%s, %f\u00b0%s' % (abs(lat), ns, abs(long), ew)
+
+    class DegreeFormatter(Formatter):
+        """
+        This is a custom formatter that converts the native unit of
+        radians into (truncated) degrees and adds a degree symbol.
+        """
+        def __init__(self, round_to=1.0):
+            self._round_to = round_to
+
+        def __call__(self, x, pos=None):
+            degrees = (x / npy.pi) * 180.0
+            degrees = round(degrees / self._round_to) * self._round_to
+            # \u00b0 : degree symbol
+            return u"%d\u00b0" % degrees
+
+    def set_longitude_grid(self, degrees):
+        """
+        Set the number of degrees between each longitude grid.
+
+        This is an example method that is specific to this projection
+        class -- it provides a more convenient interface to set the
+        ticking than set_xticks would.
+        """
+        # Set up a FixedLocator at each of the points, evenly spaced
+        # by degrees.
+        number = (360.0 / degrees) + 1
+        self.xaxis.set_major_locator(
+            FixedLocator(
+                npy.linspace(-npy.pi, npy.pi, number, True)[1:-1]))
+        # Set the formatter to display the tick labels in degrees,
+        # rather than radians.
+        self.xaxis.set_major_formatter(self.DegreeFormatter(degrees))
+
+    def set_latitude_grid(self, degrees):
+        """
+        Set the number of degrees between each longitude grid.
+
+        This is an example method that is specific to this projection
+        class -- it provides a more convenient interface than
+        set_yticks would.
+        """
+        # Set up a FixedLocator at each of the points, evenly spaced
+        # by degrees.
+        number = (180.0 / degrees) + 1
+        self.yaxis.set_major_locator(
+            FixedLocator(
+                npy.linspace(-npy.pi / 2.0, npy.pi / 2.0, number, True)[1:-1]))
+        # Set the formatter to display the tick labels in degrees,
+        # rather than radians.
+        self.yaxis.set_major_formatter(self.DegreeFormatter(degrees))
+
+    def set_longitude_grid_ends(self, degrees):
+        """
+        Set the latitude(s) at which to stop drawing the longitude grids.
+
+        Often, in geographic projections, you wouldn't want to draw
+        longitude gridlines near the poles.  This allows the user to
+        specify the degree at which to stop drawing longitude grids.
+
+        This is an example method that is specific to this projection
+        class -- it provides an interface to something that has no
+        analogy in the base Axes class.
+        """
+        longitude_cap = degrees * (npy.pi / 180.0)
+        # Change the xaxis gridlines transform so that it draws from
+        # -degrees to degrees, rather than -pi to pi.
+        self._xaxis_pretransform \
+            .clear() \
+            .scale(1.0, longitude_cap * 2.0) \
+            .translate(0.0, -longitude_cap)
+
+    def get_data_ratio(self):
+        """
+        Return the aspect ratio of the data itself.
+
+        This method should be overridden by any Axes that have a
+        fixed data ratio.
+        """
+        return 1.0
+
+    # Interactive panning and zooming is not supported with this projection,
+    # so we override all of the following methods to disable it.
+    def can_zoom(self):
+        """
+        Return True if this axes support the zoom box
+        """
+        return False
+    def start_pan(self, x, y, button):
+        pass
+    def end_pan(self):
+        pass
+    def drag_pan(self, button, key, x, y):
+        pass
+
+    # Now, the transforms themselves.
+
+    class HammerTransform(Transform):
+        """
+        The base Hammer transform.
+        """
+        input_dims = 2
+        output_dims = 2
+        is_separable = False
+
+        def __init__(self, resolution):
+            """
+            Create a new Hammer transform.  Resolution is the number of steps
+            to interpolate between each input line segment to approximate its
+            path in curved Hammer space.
+            """
+            Transform.__init__(self)
+            self._resolution = resolution
+
+        def transform(self, ll):
+            """
+            Override the transform method to implement the custom transform.
+
+            The input and output are Nx2 numpy arrays.
+            """
+            longitude = ll[:, 0:1]
+            latitude  = ll[:, 1:2]
+
+            # Pre-compute some values
+            half_long = longitude / 2.0
+            cos_latitude = npy.cos(latitude)
+            sqrt2 = npy.sqrt(2.0)
+
+            alpha = 1.0 + cos_latitude * npy.cos(half_long)
+            x = (2.0 * sqrt2) * (cos_latitude * npy.sin(half_long)) / alpha
+            y = (sqrt2 * npy.sin(latitude)) / alpha
+            return npy.concatenate((x, y), 1)
+
+        # This is where things get interesting.  With this projection,
+        # straight lines in data space become curves in display space.
+        # This is done by interpolating new values between the input
+        # values of the data.  Since ``transform`` must not return a
+        # differently-sized array, any transform that requires
+        # changing the length of the data array must happen within
+        # ``transform_path``.
+        def transform_path(self, path):
+            vertices = path.vertices
+            ipath = path.interpolated(self._resolution)
+            return Path(self.transform(ipath.vertices), ipath.codes)
+
+        def inverted(self):
+            return HammerAxes.InvertedHammerTransform(self._resolution)
+        inverted.__doc__ = Transform.inverted.__doc__
+
+    class InvertedHammerTransform(Transform):
+        input_dims = 2
+        output_dims = 2
+        is_separable = False
+
+        def __init__(self, resolution):
+            Transform.__init__(self)
+            self._resolution = resolution
+
+        def transform(self, xy):
+            x = xy[:, 0:1]
+            y = xy[:, 1:2]
+
+            quarter_x = 0.25 * x
+            half_y = 0.5 * y
+            z = npy.sqrt(1.0 - quarter_x*quarter_x - half_y*half_y)
+            longitude = 2 * npy.arctan((z*x) / (2.0 * (2.0*z*z - 1.0)))
+            latitude = npy.arcsin(y*z)
+            return npy.concatenate((longitude, latitude), 1)
+        transform.__doc__ = Transform.transform.__doc__
+
+        def inverted(self):
+            # The inverse of the inverse is the original transform... ;)
+            return HammerAxes.HammerTransform(self._resolution)
+        inverted.__doc__ = Transform.inverted.__doc__
+
+# Now register the projection with matplotlib so the user can select
+# it.
+register_projection(HammerAxes)
+
+# Now make a simple example using the custom projection.
+from pylab import *
+
+subplot(111, projection="hammer")
+grid(True)
+
+show()

Added: branches/transforms/examples/custom_scale_example.py
===================================================================
--- branches/transforms/examples/custom_scale_example.py                        
        (rev 0)
+++ branches/transforms/examples/custom_scale_example.py        2008-01-07 
21:15:58 UTC (rev 4803)
@@ -0,0 +1,165 @@
+from matplotlib import scale as mscale
+from matplotlib import transforms as mtransforms
+
+class MercatorLatitudeScale(mscale.ScaleBase):
+    """
+    Scales data in range -pi/2 to pi/2 (-90 to 90 degrees) using
+    the system used to scale latitudes in a Mercator projection.
+
+    The scale function:
+      ln(tan(y) + sec(y))
+
+    The inverse scale function:
+      atan(sinh(y))
+
+    Since the Mercator scale tends to infinity at +/- 90 degrees,
+    there is user-defined threshold, above and below which nothing
+    will be plotted.  This defaults to +/- 85 degrees.
+
+    source:
+    http://en.wikipedia.org/wiki/Mercator_projection
+    """
+
+    # The scale class must have a member ``name`` that defines the
+    # string used to select the scale.  For example,
+    # ``gca().set_yscale("mercator")`` would be used to select this
+    # scale.
+    name = 'mercator'
+
+
+    def __init__(self, axis, **kwargs):
+        """
+        Any keyword arguments passed to ``set_xscale`` and
+        ``set_yscale`` will be passed along to the scale's
+        constructor.
+
+        thresh: The degree above which to crop the data.
+        """
+        mscale.ScaleBase.__init__(self)
+        thresh = kwargs.pop("thresh", (85 / 180.0) * npy.pi)
+        if thresh >= npy.pi / 2.0:
+            raise ValueError("thresh must be less than pi/2")
+        self.thresh = thresh
+
+    def get_transform(self):
+        """
+        Override this method to return a new instance that does the
+        actual transformation of the data.
+
+        The MercatorLatitudeTransform class is defined below as a
+        nested class of this one.
+        """
+        return self.MercatorLatitudeTransform(self.thresh)
+
+    def set_default_locators_and_formatters(self, axis):
+        """
+        Override to set up the locators and formatters to use with the
+        scale.  This is only required if the scale requires custom
+        locators and formatters.  Writing custom locators and
+        formatters is rather outside the scope of this example, but
+        there are many helpful examples in ``ticker.py``.
+
+        In our case, the Mercator example uses a fixed locator from
+        -90 to 90 degrees and a custom formatter class to put convert
+        the radians to degrees and put a degree symbol after the
+        value::
+        """
+        class DegreeFormatter(Formatter):
+            def __call__(self, x, pos=None):
+                # \u00b0 : degree symbol
+                return u"%d\u00b0" % ((x / npy.pi) * 180.0)
+
+        deg2rad = npy.pi / 180.0
+        axis.set_major_locator(FixedLocator(
+                npy.arange(-90, 90, 10) * deg2rad))
+        axis.set_major_formatter(DegreeFormatter())
+        axis.set_minor_formatter(DegreeFormatter())
+
+    def limit_range_for_scale(self, vmin, vmax, minpos):
+        """
+        Override to limit the bounds of the axis to the domain of the
+        transform.  In the case of Mercator, the bounds should be
+        limited to the threshold that was passed in.  Unlike the
+        autoscaling provided by the tick locators, this range limiting
+        will always be adhered to, whether the axis range is set
+        manually, determined automatically or changed through panning
+        and zooming.
+        """
+        return max(vmin, -self.thresh), min(vmax, self.thresh)
+
+    class MercatorLatitudeTransform(mtransforms.Transform):
+        # There are two value members that must be defined.
+        # ``input_dims`` and ``output_dims`` specify number of input
+        # dimensions and output dimensions to the transformation.
+        # These are used by the transformation framework to do some
+        # error checking and prevent incompatible transformations from
+        # being connected together.  When defining transforms for a
+        # scale, which are, by definition, separable and have only one
+        # dimension, these members should always be set to 1.
+        input_dims = 1
+        output_dims = 1
+        is_separable = True
+
+        def __init__(self, thresh):
+            mtransforms.Transform.__init__(self)
+            self.thresh = thresh
+
+        def transform(self, a):
+            """
+            This transform takes an Nx1 ``numpy`` array and returns a
+            transformed copy.  Since the range of the Mercator scale
+            is limited by the user-specified threshold, the input
+            array must be masked to contain only valid values.
+            ``matplotlib`` will handle masked arrays and remove the
+            out-of-range data from the plot.  Importantly, the
+            ``transform`` method *must* return an array that is the
+            same shape as the input array, since these values need to
+            remain synchronized with values in the other dimension.
+            """
+            masked = ma.masked_where((a < -self.thresh) | (a > self.thresh), a)
+            if masked.mask.any():
+                return ma.log(npy.abs(ma.tan(masked) + 1.0 / ma.cos(masked)))
+            else:
+                return npy.log(npy.abs(npy.tan(a) + 1.0 / npy.cos(a)))
+
+        def inverted(self):
+            """
+            Override this method so matplotlib knows how to get the
+            inverse transform for this transform.
+            """
+            return 
MercatorLatitudeScale.InvertedMercatorLatitudeTransform(self.thresh)
+
+    class InvertedMercatorLatitudeTransform(mtransforms.Transform):
+        input_dims = 1
+        output_dims = 1
+        is_separable = True
+
+        def __init__(self, thresh):
+            mtransforms.Transform.__init__(self)
+            self.thresh = thresh
+
+        def transform(self, a):
+            return npy.arctan(npy.sinh(a))
+
+        def inverted(self):
+            return MercatorLatitudeScale.MercatorLatitudeTransform(self.thresh)
+
+# Now that the Scale class has been defined, it must be registered so
+# that ``matplotlib`` can find it.
+mscale.register_scale(MercatorLatitudeScale)
+
+from pylab import *
+import numpy as npy
+
+t = arange(-180.0, 180.0, 0.1)
+s = t / 360.0 * npy.pi
+
+plot(t, s, '-', lw=2)
+gca().set_yscale('mercator')
+
+xlabel('Longitude')
+ylabel('Latitude')
+title('Mercator: Projection of the Oppressor')
+grid(True)
+
+show()

Modified: branches/transforms/lib/matplotlib/axes.py
===================================================================
--- branches/transforms/lib/matplotlib/axes.py  2008-01-06 18:28:17 UTC (rev 
4802)
+++ branches/transforms/lib/matplotlib/axes.py  2008-01-07 21:15:58 UTC (rev 
4803)
@@ -550,6 +550,10 @@
 
         self.bbox = mtransforms.TransformedBbox(self._position, 
fig.transFigure)
         #these will be updated later as data is added
+       self.dataLim = mtransforms.Bbox.unit()
+        self.viewLim = mtransforms.Bbox.unit()
+        self.transScale = 
mtransforms.TransformWrapper(mtransforms.IdentityTransform())
+
         self._set_lim_and_transforms()
 
     def _set_lim_and_transforms(self):
@@ -558,8 +562,6 @@
         transScale, transData, transLimits and transAxes
         transformations.
         """
-       self.dataLim = mtransforms.Bbox.unit()
-        self.viewLim = mtransforms.Bbox.unit()
         self.transAxes = mtransforms.BboxTransformTo(self.bbox)
 
         # Transforms the x and y axis separately by a scale factor

Modified: branches/transforms/lib/matplotlib/patches.py
===================================================================
--- branches/transforms/lib/matplotlib/patches.py       2008-01-06 18:28:17 UTC 
(rev 4802)
+++ branches/transforms/lib/matplotlib/patches.py       2008-01-07 21:15:58 UTC 
(rev 4803)
@@ -371,6 +371,7 @@
         self._width = width
         self._height = height
         self._rect_transform = transforms.IdentityTransform()
+        self._update_patch_transform()
     __init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd
 
     def get_path(self):
@@ -862,6 +863,7 @@
         self.angle = angle
         self._path = Path.unit_circle()
         self._patch_transform = transforms.IdentityTransform()
+        self._recompute_transform()
 
     def _recompute_transform(self):
         center = (self.convert_xunits(self.center[0]),

Modified: branches/transforms/lib/matplotlib/projections/__init__.py
===================================================================
--- branches/transforms/lib/matplotlib/projections/__init__.py  2008-01-06 
18:28:17 UTC (rev 4802)
+++ branches/transforms/lib/matplotlib/projections/__init__.py  2008-01-07 
21:15:58 UTC (rev 4803)
@@ -26,7 +26,11 @@
     AitoffAxes,
     HammerAxes,
     LambertAxes)
+)
 
+def register_projection(cls):
+    projection_registry.register(cls)
+
 def get_projection_class(projection):
     if projection is None:
         projection = 'rectilinear'

Modified: branches/transforms/lib/matplotlib/projections/geo.py
===================================================================
--- branches/transforms/lib/matplotlib/projections/geo.py       2008-01-06 
18:28:17 UTC (rev 4802)
+++ branches/transforms/lib/matplotlib/projections/geo.py       2008-01-07 
21:15:58 UTC (rev 4803)
@@ -51,19 +51,13 @@
         Axes.set_ylim(self, -npy.pi / 2.0, npy.pi / 2.0)
 
     def _set_lim_and_transforms(self):
-       self.dataLim = Bbox.unit()
-        self.viewLim = Bbox.unit()
-        self.transAxes = BboxTransformTo(self.bbox)
-
-        # Transforms the x and y axis separately by a scale factor
-        # It is assumed that this part will have non-linear components
-        self.transScale = TransformWrapper(IdentityTransform())
-
         # A (possibly non-linear) projection on the (already scaled) data
         self.transProjection = self._get_core_transform(self.RESOLUTION)
 
         self.transAffine = self._get_affine_transform()
 
+        self.transAxes = BboxTransformTo(self.bbox)
+
         # The complete data transformation stack -- from data all the
         # way to display coordinates
         self.transData = \

Modified: branches/transforms/lib/matplotlib/projections/polar.py
===================================================================
--- branches/transforms/lib/matplotlib/projections/polar.py     2008-01-06 
18:28:17 UTC (rev 4802)
+++ branches/transforms/lib/matplotlib/projections/polar.py     2008-01-07 
21:15:58 UTC (rev 4803)
@@ -186,8 +186,6 @@
         self.yaxis.set_ticks_position('none')
 
     def _set_lim_and_transforms(self):
-       self.dataLim = Bbox.unit()
-        self.viewLim = Bbox.unit()
         self.transAxes = BboxTransformTo(self.bbox)
 
         # Transforms the x and y axis separately by a scale factor

Modified: branches/transforms/lib/matplotlib/scale.py
===================================================================
--- branches/transforms/lib/matplotlib/scale.py 2008-01-06 18:28:17 UTC (rev 
4802)
+++ branches/transforms/lib/matplotlib/scale.py 2008-01-07 21:15:58 UTC (rev 
4803)
@@ -307,94 +307,11 @@
         return self._transform
 
 
-class MercatorLatitudeScale(ScaleBase):
-    """
-    Scales data in range -pi/2 to pi/2 (-90 to 90 degrees) using
-    the system used to scale latitudes in a Mercator projection.
 
-    The scale function:
-      ln(tan(y) + sec(y))
-
-    The inverse scale function:
-      atan(sinh(y))
-
-    Since the Mercator scale tends to infinity at +/- 90 degrees,
-    there is user-defined threshold, above and below which nothing
-    will be plotted.  This defaults to +/- 85 degrees.
-
-    source:
-    http://en.wikipedia.org/wiki/Mercator_projection
-    """
-    name = 'mercator_latitude'
-
-    class MercatorLatitudeTransform(Transform):
-        input_dims = 1
-        output_dims = 1
-        is_separable = True
-
-        def __init__(self, thresh):
-            Transform.__init__(self)
-            self.thresh = thresh
-
-        def transform(self, a):
-            masked = ma.masked_where((a < -self.thresh) | (a > self.thresh), a)
-            if masked.mask.any():
-                return ma.log(npy.abs(ma.tan(masked) + 1.0 / ma.cos(masked)))
-            else:
-                return npy.log(npy.abs(npy.tan(a) + 1.0 / npy.cos(a)))
-
-        def inverted(self):
-            return 
MercatorLatitudeScale.InvertedMercatorLatitudeTransform(self.thresh)
-
-    class InvertedMercatorLatitudeTransform(Transform):
-        input_dims = 1
-        output_dims = 1
-        is_separable = True
-
-        def __init__(self, thresh):
-            Transform.__init__(self)
-            self.thresh = thresh
-
-        def transform(self, a):
-            return npy.arctan(npy.sinh(a))
-
-        def inverted(self):
-            return MercatorLatitudeScale.MercatorLatitudeTransform(self.thresh)
-
-    def __init__(self, axis, **kwargs):
-        """
-        thresh: The degree above which to crop the data.
-        """
-        thresh = kwargs.pop("thresh", (85 / 180.0) * npy.pi)
-        if thresh >= npy.pi / 2.0:
-            raise ValueError("thresh must be less than pi/2")
-        self.thresh = thresh
-        self._transform = self.MercatorLatitudeTransform(thresh)
-
-    def set_default_locators_and_formatters(self, axis):
-        class DegreeFormatter(Formatter):
-            def __call__(self, x, pos=None):
-                # \u00b0 : degree symbol
-                return u"%d\u00b0" % ((x / npy.pi) * 180.0)
-
-        deg2rad = npy.pi / 180.0
-        axis.set_major_locator(FixedLocator(
-                npy.arange(-90, 90, 10) * deg2rad))
-        axis.set_major_formatter(DegreeFormatter())
-        axis.set_minor_formatter(DegreeFormatter())
-
-    def get_transform(self):
-        return self._transform
-
-    def limit_range_for_scale(self, vmin, vmax, minpos):
-        return max(vmin, -self.thresh), min(vmax, self.thresh)
-
-
 _scale_mapping = {
     'linear'            : LinearScale,
     'log'               : LogScale,
-    'symlog'            : SymmetricalLogScale,
-    'mercator_latitude' : MercatorLatitudeScale
+    'symlog'            : SymmetricalLogScale
     }
 def scale_factory(scale, axis, **kwargs):
     scale = scale.lower()


This was sent by the SourceForge.net collaborative development platform, the 
world's largest Open Source development site.

-------------------------------------------------------------------------
Check out the new SourceForge.net Marketplace.
It's the best place to buy or sell services for
just about anything Open Source.
http://ad.doubleclick.net/clk;164216239;13503038;w?http://sf.net/marketplace
_______________________________________________
Matplotlib-checkins mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/matplotlib-checkins

Reply via email to