Cartopy

Source code for cartopy.mpl_integration.geoaxes

# (C) British Crown Copyright 2011 - 2012, Met Office
#
# This file is part of cartopy.
#
# cartopy is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cartopy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with cartopy.  If not, see <http://www.gnu.org/licenses/>.
"""
This module defines the :class:`GeoAxes` class, for use with matplotlib.

When a matplotlib figure contains a GeoAxes the plotting commands can transform
plot results from source coordinates to the GeoAxes' target projection.

""" 

import matplotlib.axes
from matplotlib.image import imread
import matplotlib.transforms as mtransforms
import matplotlib.patches as mpatches
import matplotlib.path as mpath
import matplotlib.collections as mcollections
import numpy
import shapely.geometry

#import cartopy.cartesian
import cartopy
import cartopy.crs as ccrs
import cartopy.img_transform
import cartopy.mpl_integration.patch as patch


import matplotlib
assert matplotlib.__version__ >= '1.2', 'Cartopy can only work with matplotlib 1.2 or greater.'


_colors = {
          'land': numpy.array((240, 240, 220)) / 256.,
          'sea': numpy.array((152, 183, 226)) / 256.,
          }

_PATH_TRANSFORM_CACHE = {}
"""
Stores a mapping between (hash(path), hash(self.source_projection), hash(self.target_projection))
and the resulting transformed path.

Provides a significant performance boost for contours which, at matplotlib 1.2.0 called
transform_path_non_affine twice unnecessarily.

"""

_GEOMETRY_TO_PATH_CACHE = {}
"""
Caches a map between (hash(geometries), hash(crs)) to projected paths.
This provides a significant boost when producing multiple maps of the same projection.
"""

_NATURAL_EARTH_GEOM_CACHE = {}
"""
Caches a mapping between (name, category, resolution) and a tuple of the resulting geometries.
Provides a significant performance benefit (when combined with object id caching 
in GeoAxes.add_geometries) when producing multiple maps of the same projection.
"""


# XXX call this InterCRSTransform
class InterProjectionTransform(mtransforms.Transform):
    """Transforms coordinates from the source_projection to the target_projection."""
    input_dims = 2
    output_dims = 2
    is_separable = False
    has_inverse = True


    def __init__(self, source_projection, target_projection):
        """
        Create the transform object from the given projections.
        
        Args:
        
            * source_projection - A :class:`~cartopy.crs.CRS`.
            * target_projection - A :class:`~cartopy.crs.CRS`.
            
        """
        # assert target_projection is cartopy.crs.Projection
        # assert source_projection is cartopy.crs.CRS
        self.source_projection = source_projection
        self.target_projection = target_projection
        mtransforms.Transform.__init__(self)

    def __repr__(self):
        return '< InterProjectionTransform %s -> %s >' % (self.source_projection, self.target_projection)

    def transform_non_affine(self, xy):
        """
        Transforms from source to target coordinates.
        
        Args:
        
            * xy - An (n,2) array of points in source coordinates.
            
        Returns:
        
            * An (n,2) array of transformed points in target coordinates.
            
        """
        if isinstance(xy, numpy.ndarray):
            return self.target_projection.transform_points(self.source_projection, xy[:, 0], xy[:, 1])[:, 0:2]
        else:
            x, y = xy
            x, y = self.target_projection.transform_point(x, y, self.source_projection)
            return x, y

    def transform_path_non_affine(self, path):
        """
        Transforms from source to target coordinates.
        
        Caches results, so subsequent calls with the same *path* argument
        (and the same source and target projections) are faster.
        
        Args:
        
            * path - A matplotlib :class:`~matplotlib.path.Path` object with
                     vertices in source coordinates.
            
        Returns
        
            * A matplotlib :class:`~matplotlib.path.Path` with vertices
              in target coordinates.
            
        """
        orig_id = (hash(path), hash(self.source_projection), hash(self.target_projection))
        result = _PATH_TRANSFORM_CACHE.get(orig_id, None)
        if result is not None:
            return result
        
        bypass = self.source_projection == self.target_projection
        if bypass:
            projection = self.source_projection
            if isinstance(projection, ccrs._CylindricalProjection):
                x = path.vertices[:, 0]
                x_limits = projection.x_limits
                bypass = x.min() >= x_limits[0] and x.max() <= x_limits[1]
        if bypass:
            return path

        if path.vertices.shape == (1, 2):
            return mpath.Path(self.transform(path.vertices))

        transformed_geoms = []
        for geom in patch.path_to_geos(path):
            transformed_geoms.append(self.target_projection.project_geometry(geom, self.source_projection))

        if not transformed_geoms:
            result = mpath.Path(numpy.empty([0, 2]))
        else:
            paths = patch.geos_to_path(transformed_geoms)
            if not paths:
                return mpath.Path(numpy.empty([0, 2]))
            points, codes = zip(*[patch.path_segments(path, curves=False, simplify=False) for path in paths])
            result = mpath.Path(numpy.concatenate(points, 0), numpy.concatenate(codes))
        
        # store the result in the cache for future performance boosts    
        _PATH_TRANSFORM_CACHE[orig_id] = result
        
        return result

    def inverted(self):
        """Return a matplotlib :class:`~matplotlib.transforms.Transform` from target to source coordinates."""
        return InterProjectionTransform(self.target_projection, self.source_projection)


[docs]class GeoAxes(matplotlib.axes.Axes): """ A subclass of :class:`matplotlib.axes.Axes` which represents a map :class:`~cartopy.crs.Projection`. This class replaces the matplotlib :class:`~matplotlib.axes.Axes` class when created with the *projection* keyword. For example: # Set up a standard map for latlon data. geo_axes = pyplot.axes(projection=cartopy.crs.PlateCarree()) # Set up an OSGB map. geo_axes = pyplot.subplot(2, 2, 1, projection=cartopy.crs.OSGB()) When a source projection is provided to one of it's plotting methods, using the *transform* keyword, the standard matplotlib plot result is transformed from source coordinates to the target projection. For example: # Plot latlon data on an OSGB map. pyplot.axes(projection=cartopy.crs.OSGB()) pyplot.contourf(x, y, data, transform=cartopy.crs.PlateCarree()) """ def __init__(self, *args, **kwargs): """ Create a GeoAxes object using standard matplotlib :class:`~matplotlib.axes.Axes` args and keywords. Kwargs: * map_projection - The target :class:`~cartopy.crs.Projection` of this Axes object. All other args and keywords are passed straight through to :class:`matplotlib.axes.Axes`. """ self.projection = kwargs.pop('map_projection') super(GeoAxes, self).__init__(*args, **kwargs) self._gridliners = [] self.img_factories = [] self._done_img_factory = False
[docs] def add_image(self, factory, *args, **kwargs): """ Adds an image "factory" to the Axes. Any image "factory" added, will be asked to retrieve an image with associated metadata for a given bounding box at draw time. The advantage of this approach is that the limits of the map do not need to be known when adding the image factory, but can be deferred until everything which can effect the limits has been added. Currently an image "factory" is just an object with a ``image_for_domain`` method. Examples of image factories are :class:`cartopy.io.img_nest.NestedImageCollection` and :class:`cartopy.io.image_tiles.GoogleTiles`. """ # XXX TODO: Needs working on self.img_factories.append([factory, args, kwargs])
@matplotlib.axes.allow_rasterization def draw(self, renderer=None, inframe=False): """ Extends the standard behaviour of :func:`matplotlib.axes.Axes.draw`. Draws grid lines and image factory results before invoking standard matplotlib drawing. A global range is used if no limits have yet been set. """ # if no data has been added, and no extents set, then make the map global if self.ignore_existing_data_limits and self._autoscaleXon and self._autoscaleYon: self.set_global() self.ignore_existing_data_limits = True for gl in self._gridliners: gl.do_gridlines(background_patch=self.background_patch) self._gridliners = [] # XXX This interface needs a tidy up: # image drawing on pan/zoom; # caching the resulting image; # buffering the result by 10%...; if not self._done_img_factory: for factory, args, kwargs in self.img_factories: img, extent, origin = factory.image_for_domain(self._get_extent_geom(factory.crs), args[0]) self.imshow(img, extent=extent, origin=origin, transform=factory.crs, *args[1:], **kwargs) self._done_img_factory = True return matplotlib.axes.Axes.draw(self, renderer=renderer, inframe=inframe) def __str__(self): return '< GeoAxes: %s >' % self.projection def cla(self): """Clears the current axes and adds boundary lines.""" result = matplotlib.axes.Axes.cla(self) self.xaxis.set_visible(False) self.yaxis.set_visible(False) self.autoscale_view(tight=True) self.set_aspect('equal') pre_bounary = self.ignore_existing_data_limits self._boundary() self.ignore_existing_data_limits = pre_bounary # XXX consider a margin - but only when the map is not global... # self._xmargin = 0.15 # self._ymargin = 0.15 return result def format_coord(self, x, y): """Return a string formatted for the matplotlib GUI status bar.""" lon, lat = ccrs.Geodetic().transform_point(x, y, self.projection) ns = 'N' if lat >= 0.0 else 'S' ew = 'E' if lon >= 0.0 else 'W' return u'%.4g, %.4g (%f\u00b0%s, %f\u00b0%s)' % (x, y, abs(lat), ns, abs(lon), ew)
[docs] def coastlines(self, resolution='110m', color='black', **kwargs): """ Adds coastal **outlines** to the current axes from the Natural Earth "coastline" shapefile collection. Kwargs: * resolution - a named resolution to use from the Natural Earth dataset. Currently can be one of "110m", "50m", and "10m". .. note:: Currently no clipping is done on the coastlines before adding them to the axes. This means, if very high resolution coastlines are being used, performance is likely to be severely effected. This should be resolved transparently by v0.5. """ kwargs = kwargs.copy() kwargs.setdefault('edgecolor', color) kwargs.setdefault('facecolor', 'none') return self.natural_earth_shp(name='coastline', resolution=resolution, category='physical', **kwargs)
[docs] def natural_earth_shp(self, name='land', resolution='110m', category='physical', **kwargs): """ Adds the geometries from the specified Natural Earth shapefile to the Axes as a :class:`~matplotlib.collections.PathCollection`. ``**kwargs`` are passed through to the :class:`~matplotlib.collections.PathCollection` constructor. Returns the created :class:`~matplotlib.collections.PathCollection`. .. note:: Currently no clipping is done on the geometries before adding them to the axes. This means, if very high resolution geometries are being used, performance is likely to be severely effected. This should be resolved transparently by v0.5. """ import cartopy.io.shapereader as shapereader kwargs.setdefault('edgecolor', 'face') kwargs.setdefault('facecolor', _colors['land']) key = (name, category, resolution) if key not in _NATURAL_EARTH_GEOM_CACHE: coastline_path = shapereader.natural_earth(resolution=resolution, category=category, name=name) geoms = tuple(shapereader.Reader(coastline_path).geometries()) # put the geoms in the cache _NATURAL_EARTH_GEOM_CACHE[key] = geoms else: geoms = _NATURAL_EARTH_GEOM_CACHE[key] return self.add_geometries(geoms, ccrs.Geodetic(), **kwargs)
[docs] def add_geometries(self, geoms, crs, **collection_kwargs): """ Add the given shapely geometries (in the given crs) to the axes as a :class:`~matplotlib.collections.PathCollection`. """ oid = (hash(geoms), hash(crs), hash(self.projection)) if oid in _GEOMETRY_TO_PATH_CACHE: paths = _GEOMETRY_TO_PATH_CACHE[oid] else: paths = [] for geom in geoms: paths.extend(patch.geos_to_path(self.projection.project_geometry(geom, crs))) _GEOMETRY_TO_PATH_CACHE[oid] = paths c = mcollections.PathCollection(paths, transform=self.projection, **collection_kwargs) self.add_collection(c, autolim=False) return c # def gshhs(self, outline_color='k', land_fill='green', ocean_fill='None', resolution='coarse', domain=None): # import cartopy.gshhs as gshhs # from matplotlib.collections import PatchCollection # # if ocean_fill is not None and land_fill is None: # land_fill = self.outline_patch.get_facecolor() # # if domain is None: # domain = self.ll_boundary_poly() # # paths = [] # for points in gshhs.read_gshhc(gshhs.fnames[resolution], poly=True, domain=domain): # # XXX Sometimes we only want to do lines... # poly = shapely.geometry.Polygon(points[::-1, :]) # projected = self.projection.project_polygon(poly) # # paths.extend(patch.geos_to_path(projected)) # # if ocean_fill is not None: # self.outline_patch.set_facecolor(ocean_fill) # # collection = PatchCollection([mpatches.PathPatch(pth) for pth in paths], # edgecolor=outline_color, # facecolor=land_fill, # zorder=2, # ) # self.add_collection(collection, autolim=False)
[docs] def get_extent(self, crs=None): """ Get the extent (x0, x1, y0, y1) of the map in the given coordinate system. If no crs is given, the returned extents' coordinate system will be assumed to be the Geodetic version of this axes' projection. """ p = self._get_extent_geom(crs) r = p.bounds x1, y1, x2, y2 = r return x1, x2, y1, y2
def _get_extent_geom(self, crs=None): # Perform the calculations for get_extent(), which just repackages it. x1, x2 = self.get_xlim() y1, y2 = self.get_ylim() if x1 == 0 and y1 == 0 and x2 == 1 and y2 == 1: x1, x2 = self.projection.x_limits y1, y2 = self.projection.y_limits domain_in_crs = shapely.geometry.LineString([[x1, y1], [x2, y1], [x2, y2], [x1, y2], [x1, y1]]) if self.projection != crs: domain_in_crs = self.projection.project_geometry(domain_in_crs, crs) return domain_in_crs
[docs] def set_extent(self, extents, crs=None): """ Set the extent (x0, x1, y0, y1) of the map in the given coordinate system. If no crs is given, the extents' coordinate system will be assumed to be the Geodetic version of this axes' projection. """ # TODO: Implement the same semantics as plt.xlim and # plt.ylim - allowing users to set None for a minimum and/or maximum value x1, x2, y1, y2 = extents domain_in_crs = shapely.geometry.LineString([[x1, y1], [x2, y1], [x2, y2], [x1, y2], [x1, y1]]) r = self.projection.project_geometry(domain_in_crs, crs) x1, y1, x2, y2 = r.bounds self.set_xlim([x1, x2]) self.set_ylim([y1, y2])
[docs] def set_global(self): """ Set the extent of the Axes to the limits of the projection. .. note:: In some cases where the projection has a limited sensible range the ``set_global`` method does not actually make the whole globe visible. Instead, the most appropriate extents will be used (e.g. Ordnance Survey UK will set the extents to be around the British Isles. """ self.set_xlim(self.projection.x_limits) self.set_ylim(self.projection.y_limits) # def geod_circle_meters(self, lon_0, lat_0, radius, npts=80, **kwargs): # # radius is in meters # geod = self.projection.as_geodetic() # # az = numpy.linspace(0, 360, npts) # lats = numpy.zeros(npts) + lat_0 # lons = numpy.zeros(npts) + lon_0 # distances = numpy.zeros(npts) + radius # # lons, lats, _reverse_az = geod.fwd(lons, lats, az, distances, radians=False) # ll = numpy.concatenate([lons[:, None], lats[:, None]], 1) # from matplotlib.patches import Polygon # poly = Polygon(ll, transform=cartopy.prj.PlateCarree(), **kwargs) # self.add_patch(poly) # return poly # # def gshhs_line(self, outline_color='k', domain=None, resolution='low', **kwargs): # # domain is a shapely geometry (Polygon or MultiPolygon) # import cartopy.gshhs as gshhs ## import cartopy.spherical as spherical # from matplotlib.collections import PatchCollection, LineCollection # # paths = [] # # projection = self.projection # # if domain is None: # domain = self.map_domain(ccrs.PlateCarree()) # # for points in gshhs.read_gshhc(gshhs.fnames[resolution], poly=False, domain=domain): # paths.extend(patch.geos_to_path(shapely.geometry.LineString(points))) # ## slinestring = shapely.geometry.LineString(points) ## projected = projection.project_geometry(slinestring) ## paths.extend(patch.geos_to_path(projected)) # # collection = PatchCollection([mpatches.PathPatch(pth) for pth in paths], # edgecolor=outline_color, facecolor='none', # transform=ccrs.PlateCarree(), # **kwargs # ) # # self.add_collection(collection, autolim=False)
[docs] def stock_img(self, name='ne_shaded'): """ Add a standard image to the map. Currently, the only (and default) option is a downsampled version of the Natural Earth shaded relief raster. """ # XXX Turn into a dictionary (inside the method)? # if img_name == 'bluemarble': # source_proj = ccrs.PlateCarree() # fname = '/data/local/dataZoo/cartography/raster/blue_marble_720_360.png' ## fname = '/data/local/dataZoo/cartography/raster/blue_marble_2000_1000.jpg' # img_origin = 'lower' # img = imread(fname) # img = img[::-1] # return self.imshow(img, origin=img_origin, transform=source_proj, extent=[-180, 180, -90, 90]) # elif img_name == 'bm_high': # source_proj = ccrs.PlateCarree() # fname = '/data/local/dataZoo/cartography/raster/blue_marble_2000_1000.jpg' # img_origin = 'lower' # img = imread(fname) # return self.imshow(img, origin=img_origin, transform=source_proj, extent=[-180, 180, -90, 90]) if name == 'ne_shaded': import os source_proj = ccrs.PlateCarree() fname = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'raster', 'natural_earth', '50-natural-earth-1-downsampled.png') img_origin = 'lower' img = imread(fname) img = img[::-1] return self.imshow(img, origin=img_origin, transform=source_proj, extent=[-180, 180, -90, 90]) else: raise ValueError('Unknown stock image %r.' % name)
def imshow(self, img, *args, **kwargs): """ Add the "transform" keyword to :func:`~matplotlib.pyplot.imshow'. Extra kwargs: transform - a :class:`~cartopy.crs.Projection`. regrid_shape - default is (750, 375). But may be changed to "auto" in the future... extent = (left, right, bottom, top) - transform coordinates for the extent of the source image. target_extent = (left, right, bottom, top) - native coordinates for the extent of the desired image. origin - default is changed to 'lower' update_datalim - flag whether the image should affect the data limits (default: True) """ transform = kwargs.pop('transform', None) regrid_shape = kwargs.pop('regrid_shape', (750, 375)) update_datalim = kwargs.pop('update_datalim', True) kwargs.setdefault('origin', 'lower') same_projection = isinstance(transform, ccrs.Projection) and self.projection == transform if not update_datalim: data_lim = self.dataLim.frozen().get_points() view_lim = self.viewLim.frozen().get_points() if transform is None or transform == self.transData or same_projection: if isinstance(transform, ccrs.Projection): transform = transform._as_mpl_transform(self) result = matplotlib.axes.Axes.imshow(self, img, *args, **kwargs) else: extent = kwargs.pop('extent', None) if not isinstance(transform, ccrs.Projection): raise ValueError('Expected a projection subclass. Cannot handle a %s in imshow.' % type(transform)) # XXX adaptive resolution depending on incoming img? img, extent = cartopy.img_transform.warp_array(img, source_proj=transform, source_extent=extent, target_proj=self.projection, target_res=regrid_shape, target_extent=self.get_extent(self.projection), ) # as a workaround to a matplotlib limitation, turn any images which are RGB with a mask into # RGBA images with an alpha channel. if isinstance(img, numpy.ma.MaskedArray) and img.shape[2:3] == (3, ): old_img = img img = numpy.zeros(img.shape[:2] + (4, )) img[:, :, 0:3] = old_img # put an alpha channel in if the image was masked img[:, :, 3] = ~ numpy.any(old_img.mask, axis=2) result = matplotlib.axes.Axes.imshow(self, img, *args, extent=extent, **kwargs) # clip the image. This does not work as the patch moves with mouse movement, but the clip path doesn't # This could definitely be fixed in matplotlib # if result.get_clip_path() in [None, self.patch]: # # image does not already have clipping set, clip to axes patch # result.set_clip_path(self.outline_patch) if not update_datalim: self.dataLim.set_points(data_lim) self.viewLim.set_points(view_lim) return result
[docs] def gridlines(self, crs=None, **kwargs): """ Automatically adds gridlines to the axes, in the given coordinate system, at draw time. ``**kwargs`` - are passed through to the created :class:`matplotlib.collections.Collection` allowing control of colors and linewidths etc. """ if crs is None: crs = ccrs.PlateCarree() from cartopy.mpl_integration.gridliner import Gridliner gl = Gridliner(self, crs=crs, collection_kwargs=kwargs) self._gridliners.append(gl) return gl
def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'): # generate some axes spines, as some Axes super class machinery requires them. Just make them invisible spines = matplotlib.axes.Axes._gen_axes_spines(self, locations=locations, offset=offset, units=units) for spine in spines.itervalues(): spine.set_visible(False) return spines def _boundary(self): """ Adds the map's boundary. Note: The boundary is not the axes.patch, which provides rectilinear clipping for all of the map's artists. The axes.patch will have its visibility set to False inside GeoAxes.gca() """ import cartopy.mpl_integration.patch as p path, = p.geos_to_path(self.projection.boundary) # from matplotlib.collections import PatchCollection self.sct = sct = SimpleClippedTransform(self.transScale + self.transLimits, self.transAxes) # XXX Should be exactly one path... collection = mpatches.PathPatch(path, facecolor='none', edgecolor='k', zorder=1000, # transform=self.transData, transform=sct, clip_on=False, ) self.outline_patch = collection # XXX autolim = False self.add_patch(collection) # put a color patch for background color # XXX Should be exactly one path... collection = mpatches.PathPatch(path, facecolor='w', edgecolor='none', zorder= -1, transform=sct, clip_on=False, ) self.background_patch = collection # XXX autolim = False self.add_patch(collection) self.patch.set_facecolor((1, 1, 1, 0)) self.patch.set_edgecolor((0.5, 0.5, 0.5)) self.patch.set_linewidth(0.0) # mpl 1.2.0rc2 compatibility. To be removed once 1.2 is released def contour(self, *args, **kwargs): """ Add the "transform" keyword to :func:`~matplotlib.pyplot.contour'. Extra kwargs: transform - a :class:`~cartopy.crs.Projection`. """ t = kwargs.get('transform', None) # Keep this bit - even at mpl v1.2 if t is None: t = self.projection if hasattr(t, '_as_mpl_transform'): kwargs['transform'] = t._as_mpl_transform(self) return matplotlib.axes.Axes.contour(self, *args, **kwargs) # mpl 1.2.0rc2 compatibility. To be removed once 1.2 is released def contourf(self, *args, **kwargs): """ Add the "transform" keyword to :func:`~matplotlib.pyplot.contourf'. Extra kwargs: transform - a :class:`~cartopy.crs.Projection`. """ t = kwargs.get('transform', None) # Keep this bit - even at mpl v1.2 if t is None: t = self.projection if hasattr(t, '_as_mpl_transform'): kwargs['transform'] = t._as_mpl_transform(self) return matplotlib.axes.Axes.contourf(self, *args, **kwargs) # mpl 1.2.0rc2 compatibility. To be removed once 1.2 is released def scatter(self, *args, **kwargs): """ Add the "transform" keyword to :func:`~matplotlib.pyplot.scatter'. Extra kwargs: transform - a :class:`~cartopy.crs.Projection`. """ t = kwargs.get('transform', None) # Keep this bit - even at mpl v1.2 if t is None: t = self.projection if hasattr(t, '_as_mpl_transform'): kwargs['transform'] = t._as_mpl_transform(self) return matplotlib.axes.Axes.scatter(self, *args, **kwargs) # mpl 1.2.0rc2 compatibility. To be removed once 1.2 is released def pcolormesh(self, *args, **kwargs): """ A temporary, modified duplicate of :func:`~matplotlib.pyplot.pcolormesh'. This function contains a workaround for a matplotlib issue and will be removed once the issue has been resolved. https://github.com/matplotlib/matplotlib/pull/1314 """ import warnings import numpy as np import numpy.ma as ma import matplotlib as mpl import matplotlib.cbook as cbook import matplotlib.colors as mcolors import matplotlib.cm as cm from matplotlib import docstring import matplotlib.transforms as transforms import matplotlib.artist as artist from matplotlib.artist import allow_rasterization import matplotlib.backend_bases as backend_bases import matplotlib.path as mpath import matplotlib.mlab as mlab import matplotlib.collections as mcoll if not self._hold: self.cla() alpha = kwargs.pop('alpha', None) norm = kwargs.pop('norm', None) cmap = kwargs.pop('cmap', None) vmin = kwargs.pop('vmin', None) vmax = kwargs.pop('vmax', None) shading = kwargs.pop('shading', 'flat').lower() antialiased = kwargs.pop('antialiased', False) kwargs.setdefault('edgecolors', 'None') X, Y, C = self._pcolorargs('pcolormesh', *args) Ny, Nx = X.shape # convert to one dimensional arrays if shading != 'gouraud': C = ma.ravel(C[0:Ny-1, 0:Nx-1]) # data point in each cell is value at # lower left corner else: C = C.ravel() X = X.ravel() Y = Y.ravel() coords = np.zeros(((Nx * Ny), 2), dtype=float) coords[:, 0] = X coords[:, 1] = Y collection = mcoll.QuadMesh( Nx - 1, Ny - 1, coords, antialiased=antialiased, shading=shading, **kwargs) collection.set_alpha(alpha) collection.set_array(C) if norm is not None: assert(isinstance(norm, mcolors.Normalize)) collection.set_cmap(cmap) collection.set_norm(norm) collection.set_clim(vmin, vmax) collection.autoscale_None() self.grid(False) # Transform from native to data coordinates? t = collection._transform if (not isinstance(t, mtransforms.Transform) and hasattr(t, '_as_mpl_transform')): t = t._as_mpl_transform(self.axes) if t and any(t.contains_branch_seperately(self.transData)): trans_to_data = t - self.transData pts = np.vstack([X, Y]).T.astype(np.float) transformed_pts = trans_to_data.transform(pts) X = transformed_pts[..., 0] Y = transformed_pts[..., 1] # XXX Not a mpl 1.2 thing... no_inf = (X != np.inf) & (Y != np.inf) X = X[no_inf] Y = Y[no_inf] minx = np.amin(X) maxx = np.amax(X) miny = np.amin(Y) maxy = np.amax(Y) corners = (minx, miny), (maxx, maxy) self.update_datalim( corners) self.autoscale_view() self.add_collection(collection) return collection # mpl 1.2.0rc2 compatibility. To be removed once 1.2 is released def pcolor(self, *args, **kwargs): """ A temporary, modified duplicate of :func:`~matplotlib.pyplot.pcolor'. This function contains a workaround for a matplotlib issue and will be removed once the issue has been resolved. https://github.com/matplotlib/matplotlib/pull/1314 """ t = kwargs.get('transform', None) if t is None: kwargs['transform'] = self.projection import warnings import numpy as np import numpy.ma as ma import matplotlib as mpl import matplotlib.cbook as cbook import matplotlib.colors as mcolors import matplotlib.cm as cm from matplotlib import docstring import matplotlib.transforms as transforms import matplotlib.artist as artist from matplotlib.artist import allow_rasterization import matplotlib.backend_bases as backend_bases import matplotlib.path as mpath import matplotlib.mlab as mlab import matplotlib.collections as mcoll if not self._hold: self.cla() alpha = kwargs.pop('alpha', None) norm = kwargs.pop('norm', None) cmap = kwargs.pop('cmap', None) vmin = kwargs.pop('vmin', None) vmax = kwargs.pop('vmax', None) shading = kwargs.pop('shading', 'flat') X, Y, C = self._pcolorargs('pcolor', *args) Ny, Nx = X.shape # convert to MA, if necessary. C = ma.asarray(C) X = ma.asarray(X) Y = ma.asarray(Y) mask = ma.getmaskarray(X)+ma.getmaskarray(Y) xymask = mask[0:-1,0:-1]+mask[1:,1:]+mask[0:-1,1:]+mask[1:,0:-1] # don't plot if C or any of the surrounding vertices are masked. mask = ma.getmaskarray(C)[0:Ny-1,0:Nx-1]+xymask newaxis = np.newaxis compress = np.compress ravelmask = (mask==0).ravel() X1 = compress(ravelmask, ma.filled(X[0:-1,0:-1]).ravel()) Y1 = compress(ravelmask, ma.filled(Y[0:-1,0:-1]).ravel()) X2 = compress(ravelmask, ma.filled(X[1:,0:-1]).ravel()) Y2 = compress(ravelmask, ma.filled(Y[1:,0:-1]).ravel()) X3 = compress(ravelmask, ma.filled(X[1:,1:]).ravel()) Y3 = compress(ravelmask, ma.filled(Y[1:,1:]).ravel()) X4 = compress(ravelmask, ma.filled(X[0:-1,1:]).ravel()) Y4 = compress(ravelmask, ma.filled(Y[0:-1,1:]).ravel()) npoly = len(X1) xy = np.concatenate((X1[:,newaxis], Y1[:,newaxis], X2[:,newaxis], Y2[:,newaxis], X3[:,newaxis], Y3[:,newaxis], X4[:,newaxis], Y4[:,newaxis], X1[:,newaxis], Y1[:,newaxis]), axis=1) verts = xy.reshape((npoly, 5, 2)) C = compress(ravelmask, ma.filled(C[0:Ny-1,0:Nx-1]).ravel()) linewidths = (0.25,) if 'linewidth' in kwargs: kwargs['linewidths'] = kwargs.pop('linewidth') kwargs.setdefault('linewidths', linewidths) if shading == 'faceted': edgecolors = 'k', else: edgecolors = 'none' if 'edgecolor' in kwargs: kwargs['edgecolors'] = kwargs.pop('edgecolor') ec = kwargs.setdefault('edgecolors', edgecolors) # aa setting will default via collections to patch.antialiased # unless the boundary is not stroked, in which case the # default will be False; with unstroked boundaries, aa # makes artifacts that are often disturbing. if 'antialiased' in kwargs: kwargs['antialiaseds'] = kwargs.pop('antialiased') if 'antialiaseds' not in kwargs and ec.lower() == "none": kwargs['antialiaseds'] = False collection = mcoll.PolyCollection(verts, **kwargs) collection.set_alpha(alpha) collection.set_array(C) if norm is not None: assert(isinstance(norm, mcolors.Normalize)) collection.set_cmap(cmap) collection.set_norm(norm) collection.set_clim(vmin, vmax) collection.autoscale_None() self.grid(False) x = X.compressed() y = Y.compressed() # Transform from native to data coordinates? t = collection._transform if (not isinstance(t, mtransforms.Transform) and hasattr(t, '_as_mpl_transform')): t = t._as_mpl_transform(self.axes) if t and any(t.contains_branch_seperately(self.transData)): trans_to_data = t - self.transData pts = np.vstack([x, y]).T.astype(np.float) transformed_pts = trans_to_data.transform(pts) x = transformed_pts[..., 0] y = transformed_pts[..., 1] # XXX Not a mpl 1.2 thing... no_inf = (x != np.inf) & (y != np.inf) x = x[no_inf] y = y[no_inf] minx = np.amin(x) maxx = np.amax(x) miny = np.amin(y) maxy = np.amax(y) corners = (minx, miny), (maxx, maxy) self.update_datalim( corners) self.autoscale_view() self.add_collection(collection) return collection # alias GeoAxes - NOTE: THIS WAS NOT IN v0.4.0rc1
GenericProjectionAxes = GeoAxes """(To be removed in v0.5) An alias to the :class:`GeoAxes` class.""" class SimpleClippedTransform(mtransforms.Transform): """ Transforms the values using a pre transform, clips them, then post transforms them. This transform should not be widely used, but is useful for transforming a background patch and clipping the patch to a desired extent. """ input_dims = 2 output_dims = 2 has_inverse = True def __init__(self, pre_clip_transform, post_clip_transform, xclip=(0, 1), yclip=(0, 1)): """ Create the transform. Args: * pre_clip_transform - A :class:`matplotlib.transforms.Transform`. * post_clip_transform - A :class:`matplotlib.transforms.Transform`. * xclip - Defaults to (0,1). * yclip - Defaults to (0,1). """ mtransforms.Transform.__init__(self) self.pre_clip_transform = pre_clip_transform self.post_clip_transform = post_clip_transform self.x_clips = xclip self.y_clips = yclip def transform_non_affine(self, values): """ Transforms from source to target coordinates. Args: * value - An (n,2) array of points in source coordinates. Returns: * An (n,2) array of transformed points in target coordinates. """ new_vals = self.pre_clip_transform.transform(values) x, y = new_vals[:, 0:1], new_vals[:, 1:2] numpy.clip(x, self.x_clips[0], self.x_clips[1], x) numpy.clip(y, self.y_clips[0], self.y_clips[1], y) # XXX support ma's? return self.post_clip_transform.transform(new_vals) def inverted(self): """Return a matplotlib :class:`~matplotlib.transforms.Transform` from target to source coordinates.""" return (self.pre_clip_transform + self.post_clip_transform).inverted()