Hi all,

I've created a custom projection for drawing Lambert plots on two
adjacent hemispheres, copied closely from the
custom_projection_example [1], and attached here.

The basics are working I think, but the axes objects have too much
whitespace around them, and I can't immediately work out what approach
to take to reduce it to something reasonable. Can anyone point me in
the right direction? Ultimately I'm hoping to be able to put several
of these plots together in a figure, which is currently not practical.

(Also, pcolor doesn't work as I expect, but I need to play around with
that a bit more before I know what question I need to ask of the
list.)

Thanks,

Angus.

[1] 
http://matplotlib.sourceforge.net/examples/api/custom_projection_example.html
-- 
AJC McMorland
Post-doctoral research fellow
Neurobiology, University of Pittsburgh
from matplotlib.axes import Axes
from matplotlib import cbook, docstring
from matplotlib.patches import Patch, cbook, transforms, artist
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, PolarAxes
from matplotlib.projections.geo import GeoAxes, LambertAxes
import matplotlib.spines as mspines
import matplotlib.axis as maxis
from copy import copy
from split_lambert_transforms import SplitLambertTransform

import numpy as np

class TwoCircle(Patch):
    """
    A scale-free ellipse.
    """
    def __str__(self):
        return "TwoCircle(%s, %s; %s)" % (self.center[0], self.center[1],
                                          self.radius)

    @docstring.dedent_interpd
    def __init__(self, xy, radius, **kwargs):
        """
        xy : array_like
          center of two circles
        radius : scalar
          size of each circle
          
        Valid kwargs are:
        %(Patch)s
        """
        Patch.__init__(self, **kwargs)

        self.center = xy
        self.radius = radius
        self.width = 2 * radius
        self.height = 2. * radius
        path = copy(Path.unit_circle())
        n_pts = path.vertices.shape[0]
        path.vertices = np.tile(path.vertices, [2,1])
        path.vertices[:n_pts,0] -= 1
        path.vertices[n_pts:,0] += 1
        path.codes = np.tile(path.codes, [2])
        self._path = path
        # Note: This cannot be calculated until this is added to an Axes
        self._patch_transform = transforms.IdentityTransform()

    def _recompute_transform(self):
        """NOTE: This cannot be called until after this has been added
                 to an Axes, otherwise unit conversion will fail. This
                 makes it very important to call the accessor method and
                 not directly access the transformation member variable.
        """
        center = (self.convert_xunits(self.center[0]),
                  self.convert_yunits(self.center[1]))
        width = self.convert_xunits(self.width)
        height = self.convert_yunits(self.height)
        self._patch_transform = transforms.Affine2D() \
            .scale(width * 0.5, height * 0.5) \
            .translate(*center)

    def get_path(self):
        """
        Return the vertices of the rectangle
        """
        return self._path

    def get_patch_transform(self):
        self._recompute_transform()
        return self._patch_transform

    def contains(self,ev):
        if ev.x is None or ev.y is None: return False,{}
        x, y = self.get_transform().inverted().transform_point((ev.x, ev.y))
        r = self.radius
        return (((x - r) * (x - r) + y * y) <= r*r) or \
            (((x + r) * (x + r) + y * y) <= r*r), {}

class SplitLambertAxes(Axes):
    """
    A custom class for the split Lambert projection, an equal-area map
    projection using one circle for each hemisphere.
    """
    name = 'split_lambert'
    resolution = 75

    def __init__(self, *args, **kwargs):
        Axes.__init__(self, *args, **kwargs)
        self.set_aspect(1., adjustable='box', anchor='C')
        self.cla()

    def _init_axis(self):
        self.xaxis = maxis.XAxis(self) # xaxis == theta == latitude
        self.yaxis = maxis.YAxis(self) # yaxis == phi == longitude
        
        # Do not register xaxis or yaxis with spines -- as done in
        # Axes._init_axis() -- until SplitLambertAxes.xaxis.cla() works.
        # self.spines['split_lambert'].register_axis(self.yaxis)
        self._update_transScale()

    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_theta_grid(np.pi/4.)
        self.set_phi_grid(np.pi/2.)
        self.set_phi_grid_ends(np.pi/8.)

        # Turn off minor ticking altogether
        self.xaxis.set_minor_locator(NullLocator())
        self.yaxis.set_minor_locator(NullLocator())

        # Do not display ticks
        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, 0, np.pi)
        Axes.set_ylim(self, 0, 2 * np.pi)

    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 SplitLambertTransform class.
        # Needs to transform theta, phi into x,y on a unit rectangle
        # (i.e. into axis co-ordinates).
        self.transProjection = SplitLambertTransform(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.  Only need to shift origin (0,0), to (0.5, 0.5).
        self.transAffine = Affine2D().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
        
        self._xaxis_pretransform = \
            Affine2D() \
            .scale(1.0, 2 * np.pi) \
            .translate(0.0, np.pi/4.)
        self._xaxis_transform = \
            self._xaxis_pretransform + \
            self.transData

        self._yaxis_pretransform = \
            Affine2D().scale(np.pi/2. - 1e-8, 1.0).translate(0.0, 0.)           
        self._yaxis_transform = \
            self._yaxis_pretransform + \
            self.transData

    def get_xaxis_transform(self, which='grid'):
        """
        Override this method to provide a transformation for the
        x-axis grid and ticks.
        """
        assert which in ['tick1','tick2','grid']
        return self._xaxis_transform

    def get_yaxis_transform(self,which='grid'):
        """
        Override this method to provide a transformation for the
        y-axis grid and ticks.
        """
        assert which in ['tick1','tick2','grid']
        return self._yaxis_transform
    
    def _gen_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.
        """
        return TwoCircle((0.5, 0.5), 0.5)

    def _gen_axes_spines(self):
        # circular_spine(axes, center, radius)
        return {'top':mspines.Spine.circular_spine( \
                self, (0.25, 0.5), 0.25),
                'bottom':mspines.Spine.circular_spine( \
                self, (0.75, 0.5), 0.25)}

    # 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, 0, np.pi)
        Axes.set_ylim(self, 0, 2.0 * np.pi)
    set_ylim = set_xlim

    def format_coord(self, theta, phi):
        """
        Override this method to change how the values are displayed in
        the status bar.
        """
        # U03D1 = theta, U03D5 = phi
        return u'\u03D1 %f, \u03D5 %f' % (theta, phi)

    class RadianFormatter(Formatter):
        """
        This is a custom formatter. Returns values as factors of pi.
        """
        def __init__(self, round_to=0.01):
            self._round_to = round_to

        def __call__(self, x, pos=None):
            pix = x / np.pi
            n_dp = len(str(self._round_to).split('.')[-1])
            rads = round(pix / self._round_to) * self._round_to
            # U03C0 = lowercase pi
            format_str = "%0." + "%0d" % n_dp + "f" + u'\u03C0'
            return format_str % rads

    def set_theta_grid(self, rads):
        """
        Set the number of radians between each theta grid,
        i.e. circle of latitude

        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 radians.
        number = 0 #(np.pi / rads) + 1
        self.xaxis.set_major_locator(
            FixedLocator(
                np.linspace(0, np.pi, number, True)[1:-1]))
        self.xaxis.set_major_formatter(self.RadianFormatter(0.01))

    def set_phi_grid(self, rads):
        """
        Set the number of radians between each phi 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 radians.
        number = 0 # (2 * np.pi / rads) + 1
        self.yaxis.set_major_locator(
            FixedLocator(
                np.linspace(0, 2 * np.pi, number, True)[1:-1]))
        self.yaxis.set_major_formatter(self.RadianFormatter(0.01))

    def set_phi_grid_ends(self, rads):
        """
        Set the theta(s) at which to stop drawing the phi grids.

        Often, in geographic projections, you wouldn't want to draw
        longitude gridlines near the poles.  This allows the user to
        specify the location 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.
        """
        phi_cap = rads #* np.pi
        self._yaxis_pretransform \
            .clear() \
            .scale(np.pi/2. - rads - 1e-8, 1.0) \
            .translate(rads, 0.)
                    #phi_cap * 2.0) \


    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 register the projection with matplotlib so the user can select
# it.
register_projection(SplitLambertAxes)

# Now make a simple example using the custom projection.

# import matplotlib.pyplot as plt
# ax = plt.subplot(111, projection="split_lambert")
# H = np.pi
# #p = plt.plot([0, 3/8.*H, 3/8.*H, 11/8.*H],
# #             [0,      0,   H/6.,    H/6.], "o--")
# p = plt.plot([0, H], [0, 0], 'o-')

# plt.show()
import numpy as np
from matplotlib.projections import register_projection, PolarAxes
from matplotlib.transforms import Affine2D, Affine2DBase, Bbox, \
    BboxTransformTo, IdentityTransform, Transform, TransformWrapper
from matplotlib.path import Path

class SplitLambertTransform(Transform):
    """
    The base Lambert transform.
    
    Converts theta, phi 3-d direction data co-ordinates to x, y into
    (2,1) rectangle axis co-ordinates.
    """
    input_dims = 2
    output_dims = 2
    is_separable = False

    def __init__(self, resolution):
        '''Create a new Split Lambert transform.
        
        Resolution is the number of steps to interpolate between each
        input line segment to approximate its path in curved Lambert space.
        '''
        Transform.__init__(self)
        self._resolution = resolution
    
    def _fix_theta(self, theta):
        return np.abs(np.pi - np.abs(np.pi - theta % (2 * np.pi)))
    
    def _fix_phi(self, phi):
        return phi % (2 * np.pi)
    
    def transform(self, thph):
        """
        Override the transform method to implement the custom transform.
        
        The input and output are Nx2 numpy arrays.
        
        Corrects all theta values to 0 < pi, and phi inputs to 0 < phi < 2 * pi.
        
        Theta is angle from (0,0,1) i.e. up axis,
        and phi is angle from (0,1,0) i.e. right.
        """
        # get values
        theta = self._fix_theta(thph[:, 0:1])
        phi = self._fix_phi(thph[:,1:2])
        
        # sort into flipped and not
        top_hemi = np.atleast_1d((theta < (np.pi / 2.)).squeeze())
        
        # flip theta and phi values for top hemisphere
        theta[top_hemi] = np.pi - theta[top_hemi]
        phi[top_hemi] = (np.pi - phi[top_hemi]) % (2 * np.pi)
        
        # 3d -> 2d polar co-ordinates
        rho = (2 * np.sin((np.pi - theta)/2.))
        rho /= 4 * np.sqrt(2.)
        psi = phi % (2 * np.pi)
        tr = np.concatenate((psi, rho), 1)

        # 2d polar -> 2d cartesian co-ordinates
        polarTrans = PolarAxes.PolarTransform()
        xy = polarTrans.transform(tr)

        xy[top_hemi, 0] -= 0.25
        xy[~top_hemi, 0] += 0.25
        return xy

        # 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):
        ipath = self.interpolate_path(path) # interpolate in data-space
        
        nvert = self.transform(ipath.vertices) # transform interpolated pts
        ncode = np.array([y for x,y in ipath.iter_segments(simplify=False)])
        nx = nvert[:,0]
        flip = [x + 1 for x in np.nonzero(np.diff(nx > 0))]
        ncode[flip] = Path.MOVETO
        npath = Path(nvert, ncode)
        return npath

    def interpolate_path(self, path):
        """
        Returns a new path resampled to length N x steps.  Does not
        currently handle interpolating curves.
        """
        steps = self._resolution
        if steps == 1:
            return self

        vertices = circular_interpolation(path.vertices, steps)
        codes = path.codes
        if codes is not None:
            new_codes = Path.LINETO * np.ones(((len(codes) - 1) * steps + 1, ))
            new_codes[0::steps] = codes
        else:
            new_codes = None
        return Path(vertices, new_codes)

    transform_path_non_affine = transform_path

    def inverted(self):
        return InvertedSplitLambertTransform()
    inverted.__doc__ = Transform.inverted.__doc__

def circular_interpolation(a, steps):
    '''
    special case for interpolation of circular co-ordinates
    i.e. mod 2 * pi
    '''
    if steps == 1:
        return a

    steps = np.floor(steps)
    new_length = ((len(a) - 1) * steps) + 1
    new_shape = list(a.shape)
    new_shape[0] = new_length
    result = np.zeros(new_shape, a.dtype)

    result[0] = a[0]
    a0 = a[0:-1]
    a1 = a[1:  ]
    # problem is:
    # * only with phi dimension
    # * when shortest distance between a0 and a1 goes through
    #   2n * np.pi (for n E N), rather than just difference
       
    delta = (a1 - a0)
    ow = np.abs(delta[:,1]) > np.pi # go other way on these ones
    delta[ow,1] = (2 * np.pi - np.abs(delta[ow,1])) * np.sign(delta[ow,1]) * -1
    delta /= steps
    
    for i in range(1, int(steps)):
        result[i::steps] = delta * i + a0
    result[steps::steps] = a1

    return result
    
class InvertedSplitLambertTransform(Transform):
    input_dims = 2
    output_dims = 2
    is_separable = False
    
    def transform(self, xy):
        x = xy[:, 0:1]
        y = xy[:, 1:2]
        
        # work out which are in the top hemisphere
        top_hemi = np.atleast_1d((x < 0.).squeeze())
        # then collapse them all into one circle
        xy[top_hemi, 0] += 0.25
        xy[~top_hemi, 0] -= 0.25

        # convert to polar co-ordinates
        polarInvTrans = PolarAxes.InvertedPolarTransform()
        
        tr = polarInvTrans.transform(xy)
        # replace psi nans
        tr[:,0:1][np.isnan(tr[:,0:1])] = 0.

        # convert 2d polar -> 3d polar
        rho = tr[:,1:2]
        psi = tr[:,0:1]
        theta = np.pi - 2 * np.arcsin(2 * np.sqrt(2.) * rho)
        phi = psi
        
        # flip theta and phi values for top hemisphere
        theta[top_hemi] = np.pi - theta[top_hemi]
        phi[top_hemi] = (np.pi - phi[top_hemi]) % (2 * np.pi)
            
        return np.concatenate((theta, phi), 1)
    transform.__doc__ = Transform.transform.__doc__

    def inverted(self):
        # The inverse of the inverse is the original transform... ;)
        return SplitLambertTransform()
    inverted.__doc__ = Transform.inverted.__doc__

    
def test_split_lambert_transform():
    slt = SplitLambertTransform()
    tp = np.array([[0, 0],
                   [np.pi, 0],
                   [np.pi/2., 0.],
                   [np.pi/2., np.pi]])
    answers = np.array([[-0.25, 0],
                        [0.25, 0],
                        [0.5, 0],
                        [0, 0]])
    assert(np.allclose(slt.transform(tp), answers))
    islt = slt.inverted()
    print "********"
    assert(np.allclose(islt.transform(answers), tp))
    
------------------------------------------------------------------------------
Learn how Oracle Real Application Clusters (RAC) One Node allows customers
to consolidate database storage, standardize their database environment, and, 
should the need arise, upgrade to a full multi-node Oracle RAC database 
without downtime or disruption
http://p.sf.net/sfu/oracle-sfdevnl
_______________________________________________
Matplotlib-users mailing list
Matplotlib-users@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/matplotlib-users

Reply via email to