Revision: 4767
          http://matplotlib.svn.sourceforge.net/matplotlib/?rev=4767&view=rev
Author:   mdboom
Date:     2007-12-18 09:46:01 -0800 (Tue, 18 Dec 2007)

Log Message:
-----------
Preliminary version of "adding new scales and projections" document.
(In reST, since that appears to be where mpl documentation is
heading.)

Added Paths:
-----------
    branches/transforms/doc/
    branches/transforms/doc/devel/
    branches/transforms/doc/devel/add_new_projection.rst

Added: branches/transforms/doc/devel/add_new_projection.rst
===================================================================
--- branches/transforms/doc/devel/add_new_projection.rst                        
        (rev 0)
+++ branches/transforms/doc/devel/add_new_projection.rst        2007-12-18 
17:46:01 UTC (rev 4767)
@@ -0,0 +1,192 @@
+===============================================
+Adding new scales and projections to matplotlib
+===============================================
+
+.. ::author Michael Droettboom
+
+Matplotlib supports the addition of new transformations that transform
+the data before it is displayed.  Separable transformations, that work
+on a single dimension are called "scales", and non-separable
+transformations, that take data in two or more dimensions as input are
+called "projections".
+
+This document is intended for developers and advanced users who need
+to add more scales and projections to matplotlib.
+
+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]
+
+Creating a new scale
+====================
+
+Adding a new scale consists of defining a subclass of ``ScaleBase``,
+that brings together the following elements:
+
+  - A transformation from data space into plot space.
+
+  - An inverse of that transformation.  For example, this is used to
+    convert mouse positions 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.
+
+  - 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.
+
+  - 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.
+
+First define a new subclass of ``ScaleBase``::
+
+    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'
+
+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.
+
+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``).::
+
+        class MercatorLatitudeTransform(Transform):
+            input_dims = 1
+            output_dims = 1
+
+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.
+
+``MercatorLatitudeTransform`` has a simple constructor that takes and
+stores the *threshold* for the Mercator projection (to limit its range
+to prevent plotting to infinity).::
+
+            def __init__(self, thresh):
+                Transform.__init__(self)
+                self.thresh = thresh
+
+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.::
+
+            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


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

-------------------------------------------------------------------------
SF.Net email is sponsored by:
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