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