Source code for utils.plotUtils

# Copyright (C) 2012  VT SuperDARN Lab
# Full license can be found in LICENSE.txt
"""
.. module:: plotUtils
   :synopsis: Plotting utilities (maps, colormaps, ...)
*********************
**Module**: utils.plotUtils
*********************
Basic plotting tools

**Functions**:
  * :func:`utils.plotUtils.mapObj`: Create empty map 
  * :func:`utils.plotUtils.genCmap`: generate a custom colormap
  * :func:`utils.plotUtils.drawCB`: draw a colorbar
  * :func:`utils.plotUtils.curvedEarthAxes`: Plot axes in (R, Theta) coordinates with lower limit at R = Earth radius
  * :func:`utils.plotUtils.addColorbar`: Colorbar for :func:`curvedEarthAxes`

"""
from mpl_toolkits import basemap


################################################################################
################################################################################
[docs]class mapObj(basemap.Basemap): """This class wraps arround :class:`mpl_toolkits.basemap.Basemap` (<http://tinyurl.com/d4rzmfo>) **Members**: * **coords** (str): map coordinate system ('geo', 'mag', 'mlt'). * all members of :class:`mpl_toolkits.basemap.Basemap` (<http://tinyurl.com/d4rzmfo>) **Methods**: * all methods of :class:`mpl_toolkits.basemap.Basemap` (<http://tinyurl.com/d4rzmfo>) **Example**: :: # Create the map myMap = utils.mapObj(boundinglat=30, coords='mag') # Plot the geographic and geomagnetic North Poles # First convert from lat/lon to map projection coordinates... x, y = myMap(0., 90., coords='geo') # ...then plot myMap.scatter(x, y, zorder=2, color='r') # Convert to map projection... x, y = myMap(0., 90., coords='mag') # ...and plot myMap.scatter(x, y, zorder=2, color='g') .. note:: Once the map is created, all plotting calls will be assumed to already be in the map's declared coordinate system given by **coords**. """ def __init__(self, datetime=None, coords='geo', projection='stere', resolution='c', dateTime=None, lat_0=None, lon_0=None, boundinglat=None, width=None, height=None, fillContinents='.8', fillOceans='None', fillLakes=None, coastLineWidth=0., grid=True, gridLabels=True, showCoords=True, **kwargs): """Create empty map **Args**: * **[width, height]**: width and height in m from the (lat_0, lon_0) center * **[lon_0]**: center meridian (default is -70E) * **[lat_0]**: center latitude (default is -90E) * **[boundingLat]**: bounding latitude (default it +/-20) * **[grid]**: show/hide parallels and meridians grid * **[fill_continents]**: continent color. Default is 'grey' * **[fill_water]**: water color. Default is 'None' * **[coords]**: 'geo' * **[showCoords]**: display coordinate system name in upper right corner * **[dateTime]** (datetime.datetime): necessary for MLT plots if you want the continents to be plotted * **[kwargs]**: See <http://tinyurl.com/d4rzmfo> for more keywords **Returns**: * **map**: a Basemap object (<http://tinyurl.com/d4rzmfo>) **Example**: :: myMap = mapObj(lat_0=50, lon_0=-95, width=111e3*60, height=111e3*60) written by Sebastien, 2013-02 """ from models import aacgm import numpy as np from pylab import text import math from copy import deepcopy self._coordsDict = {'mag': 'AACGM', 'geo': 'Geographic', 'mlt': 'MLT'} if coords is 'mlt': print 'MLT coordinates not implemented yet.' return # Add an extra member to the Basemap class if coords is not None and coords not in self._coordsDict: print 'Invalid coordinate system given in coords ({}): setting "geo"'.format(coords) coords = 'geo' self.coords = coords # Set map projection limits and center point depending on hemisphere selection if lat_0 is None: lat_0 = 90. if boundinglat: lat_0 = math.copysign(lat_0, boundinglat) if lon_0 is None: lon_0 = -100. if self.coords == 'mag': _, lon_0, _ = aacgm.aacgmConv(0., lon_0, 0., 0) if boundinglat: width = height = 2*111e3*( abs(lat_0 - boundinglat) ) # Initialize map super(mapObj, self).__init__(self, projection=projection, resolution=resolution, lat_0=lat_0, lon_0=lon_0, width=width, height=height, **kwargs) # Add continents if coords is not 'mlt' or dateTime is not None: _ = self.drawcoastlines(linewidth=coastLineWidth) # self.drawmapboundary(fill_color=fillOceans) _ = self.fillcontinents(color=fillContinents, lake_color=fillLakes) # Add coordinate spec if showCoords: _ = text(self.urcrnrx, self.urcrnry, self._coordsDict[coords]+' coordinates', rotation=-90., va='top', fontsize=8) # draw parallels and meridians. if grid: parallels = np.arange(-80.,81.,20.) out = self.drawparallels(parallels, color='.6', zorder=10) # label parallels on map if gridLabels: lablon = int(self.llcrnrlon/10)*10 rotate_label = lablon - lon_0 if lat_0 >= 0 else lon_0 - lablon + 180. x,y = basemap.Basemap.__call__(self, lablon*np.ones(parallels.shape), parallels) for ix,iy,ip in zip(x,y,parallels): if not self.xmin <= ix <= self.xmax: continue if not self.ymin <= iy <= self.ymax: continue _ = text(ix, iy, r"{:3.0f}$^\circ$".format(ip), rotation=rotate_label, va='center', ha='center', zorder=10, color='.4') # label meridians on bottom and left meridians = np.arange(-180.,181.,20.) if gridLabels: merLabels = [False,False,False,True] else: merLabels = [False,False,False,False] # draw meridians out = self.drawmeridians(meridians, labels=merLabels, color='.6', zorder=10) def __call__(self, x, y, inverse=False, coords=None): from models import aacgm from copy import deepcopy import numpy as np import inspect if coords is not None and coords not in self._coordsDict: print 'Invalid coordinate system given in coords ({}): setting "{}"'.format(coords, self.coords) coords = None if coords and coords != self.coords: trans = coords+'-'+self.coords if trans in ['geo-mag','mag-geo']: flag = 0 if trans == 'geo-mag' else 1 try: nx, ny = len(x), len(y) xt = np.array(x) yt = np.array(y) shape = xt.shape y, x, _ = aacgm.aacgmConvArr(list(yt.flatten()), list(xt.flatten()), [0.]*nx, flag) x = np.array(x).reshape(shape) y = np.array(y).reshape(shape) except TypeError as e: y, x, _ = aacgm.aacgmConv(y, x, 0., flag) if self.coords is 'geo': return basemap.Basemap.__call__(self, x, y, inverse=inverse) elif self.coords is 'mag': try: callerFile, _, callerName = inspect.getouterframes(inspect.currentframe())[1][1:4] except: return basemap.Basemap.__call__(self, x, y, inverse=inverse) if isinstance(y, float) and abs(y) == 90.: return basemap.Basemap.__call__(self, x, y, inverse=inverse) if 'mpl_toolkits' in callerFile and callerName is '_readboundarydata': if not inverse: try: nx, ny = len(x), len(y) x = np.array(x) y = np.array(y) shape = x.shape yout, xout, _ = aacgm.aacgmConvArr(list(y.flatten()), list(x.flatten()), [0.]*nx, 0) xout = np.array(xout).reshape(shape) yout = np.array(yout).reshape(shape) except TypeError: yout, xout, _ = aacgm.aacgmConv(y, x, 0., 0) return basemap.Basemap.__call__(self, xout, yout, inverse=inverse) else: return basemap.Basemap.__call__(self, x, y, inverse=inverse) else: return basemap.Basemap.__call__(self, x, y, inverse=inverse) elif self.coords is 'mlt': print 'Not implemented' callerFile, _, callerName = inspect.getouterframes(inspect.currentframe())[1][1:4] def _readboundarydata(self, name, as_polygons=False): from models import aacgm from copy import deepcopy import _geoslib import numpy as np if self.coords is 'mag': nPts = len(self._boundarypolyll.boundary[:, 0]) lats, lons, _ = aacgm.aacgmConvArr(list(self._boundarypolyll.boundary[:, 1]), list(self._boundarypolyll.boundary[:, 0]), [0.]*nPts, 1) b = np.asarray([lons,lats]).T oldgeom = deepcopy(self._boundarypolyll) newgeom = _geoslib.Polygon(b).fix() self._boundarypolyll = newgeom out = basemap.Basemap._readboundarydata(self, name, as_polygons=as_polygons) self._boundarypolyll = oldgeom return out else: return basemap.Basemap._readboundarydata(self, name, as_polygons=as_polygons) ################################################################################ ################################################################################
[docs]def genCmap(param, scale, colors='lasse', lowGray=False): """Generates a colormap and returns the necessary components to use it **Args**: * **param** (str): the parameter being plotted ('velocity' and 'phi0' are special cases, anything else gets the same color scale) * **scale** (list): a list with the [min,max] values of the color scale * **[colors]** (str): a string indicating which colorbar to use, valid inputs are 'lasse', 'aj'. default = 'lasse' * **[lowGray]** (boolean): a flag indicating whether to plot low velocities (|v| < 15 m/s) in gray. default = False **Returns**: * **cmap** (`matplotlib.colors.ListedColormap <http://matplotlib.org/api/colors_api.html?highlight=listedcolormap#matplotlib.colors.ListedColormap>`_): the colormap generated. This then gets passed to the mpl plotting function (e.g. scatter, plot, LineCollection, etc.) * **norm** (`matplotlib.colors.BoundaryNorm <http://matplotlib.org/api/colors_api.html?highlight=matplotlib.colors.boundarynorm#matplotlib.colors.BoundaryNorm>`_): the colormap index. This then gets passed to the mpl plotting function (e.g. scatter, plot, LineCollection, etc.) * **bounds** (list): the boundaries of each of the colormap segments. This can be used to manually label the colorbar, for example. **Example**: :: cmap,norm,bounds = genCmap('velocity', [-200,200], colors='aj', lowGray=True) Written by AJ 20120820 """ import matplotlib,numpy import matplotlib.pyplot as plot #the MPL colormaps we will be using cmj = matplotlib.cm.jet cmpr = matplotlib.cm.prism #check for a velocity plot if(param == 'velocity'): #check for what color scale we want to use if(colors == 'aj'): if(not lowGray): #define our discrete colorbar cmap = matplotlib.colors.ListedColormap([cmpr(.142),cmpr(.125),cmpr(.11),cmpr(.1),\ cmpr(.175),cmpr(.158),cmj(.32),cmj(.37)]) else: cmap = matplotlib.colors.ListedColormap([cmpr(.142),cmpr(.125),cmpr(.11),cmpr(.1),'.6',\ cmpr(.175),cmpr(.158),cmj(.32),cmj(.37)]) else: if(not lowGray): #define our discrete colorbar cmap = matplotlib.colors.ListedColormap([cmj(.9),cmj(.8),cmj(.7),cmj(.65),\ cmpr(.142),cmj(.45),cmj(.3),cmj(.1)]) else: cmap = matplotlib.colors.ListedColormap([cmj(.9),cmj(.8),cmj(.7),cmj(.65),'.6',\ cmpr(.142),cmj(.45),cmj(.3),cmj(.1)]) #define the boundaries for color assignments bounds = numpy.round(numpy.linspace(scale[0],scale[1],7)) if(lowGray): bounds[3] = -15. bounds = numpy.insert(bounds,4,15.) bounds = numpy.insert(bounds,0,-50000.) bounds = numpy.append(bounds,50000.) norm = matplotlib.colors.BoundaryNorm(bounds, cmap.N) elif(param == 'phi0'): #check for what color scale we want to use if(colors == 'aj'): #define our discrete colorbar cmap = matplotlib.colors.ListedColormap([cmpr(.142),cmpr(.125),cmpr(.11),cmpr(.1),\ cmpr(.18),cmpr(.16),cmj(.32),cmj(.37)]) else: #define our discrete colorbar cmap = matplotlib.colors.ListedColormap([cmj(.9),cmj(.8),cmj(.7),cmj(.65),\ cmpr(.142),cmj(.45),cmj(.3),cmj(.1)]) #define the boundaries for color assignments bounds = numpy.linspace(scale[0],scale[1],9) norm = matplotlib.colors.BoundaryNorm(bounds, cmap.N) elif(param == 'grid'): #check what color scale we want to use if(colors == 'aj'): #define our discrete colorbar cmap = matplotlib.colors.ListedColormap([cmpr(.175),cmpr(.17),cmj(.32),cmj(.37),\ cmpr(.142),cmpr(.13),cmpr(.11),cmpr(.10)]) else: #define our discrete colorbar cmap = matplotlib.colors.ListedColormap([cmj(.1),cmj(.3),cmj(.45),cmpr(.142),\ cmj(.65),cmj(.7),cmj(.8),cmj(.9)]) #define the boundaries for color assignments bounds = numpy.round(numpy.linspace(scale[0],scale[1],8)) bounds = numpy.append(bounds,50000.) norm = matplotlib.colors.BoundaryNorm(bounds, cmap.N) #if its a non-velocity plot else: #check what color scale we want to use if(colors == 'aj'): #define our discrete colorbar cmap = matplotlib.colors.ListedColormap([cmpr(.175),cmpr(.158),cmj(.32),cmj(.37),\ cmpr(.142),cmpr(.13),cmpr(.11),cmpr(.10)]) else: #define our discrete colorbar cmap = matplotlib.colors.ListedColormap([cmj(.1),cmj(.3),cmj(.45),cmpr(.142),\ cmj(.65),cmj(.7),cmj(.8),cmj(.9)]) #define the boundaries for color assignments bounds = numpy.round(numpy.linspace(scale[0],scale[1],8)) bounds = numpy.append(bounds,50000.) norm = matplotlib.colors.BoundaryNorm(bounds, cmap.N) cmap.set_bad('w',1.0) cmap.set_over('w',1.0) cmap.set_under('.6',1.0) return cmap,norm,bounds ################################################################################ ################################################################################
[docs]def drawCB(fig,coll,cmap,norm,map=False,pos=[0,0,1,1]): """manually draws a colorbar on a figure. This can be used in lieu of the standard mpl colorbar function if you need the colorbar in a specific location. See :func:`pydarn.plotting.rti.plotRti` for an example of its use. **Args**: * **fig** (`matplotlib.figure.Figure <http://matplotlib.org/api/figure_api.html#matplotlib.figure.Figure>`_): the figure being drawn on. * **coll** (`matplotlib.collections.Collection <http://matplotlib.org/api/collections_api.html?highlight=collection#matplotlib.collections.Collection>`_: the collection using this colorbar * **cmap** (`matplotlib.colors.ListedColormap <http://matplotlib.org/api/colors_api.html?highlight=listedcolormap#matplotlib.colors.ListedColormap>`_): the colormap being used * **norm** (`matplotlib.colors.BoundaryNorm <http://matplotlib.org/api/colors_api.html?highlight=matplotlib.colors.boundarynorm#matplotlib.colors.BoundaryNorm>`_): the colormap index being used * **[map]** (boolean): a flag indicating the we are drawing the colorbar on a figure with a map plot * **[pos]** (list): the position of the colorbar. format = [left,bottom,width,height] **Returns**: **Example**: :: cmap,norm,bounds = genCmap('velocity', [-200,200], colors='aj', lowGray=True) Written by AJ 20120820 """ import matplotlib,numpy import matplotlib.pyplot as plot if not map: #create a new axes for the colorbar cax = fig.add_axes(pos) #set the colormap and boundaries for the collection #of plotted items if(isinstance(coll,list)): for c in coll: c.set_cmap(cmap) c.set_norm(norm) cb = plot.colorbar(c,cax=cax,drawedges=True) else: coll.set_cmap(cmap) coll.set_norm(norm) cb = plot.colorbar(coll,cax=cax,drawedges=True) else: if(isinstance(coll,list)): for c in coll: c.set_cmap(cmap) c.set_norm(norm) cb = fig.colorbar(c,location='right',drawedges=True) else: coll.set_cmap(cmap) coll.set_norm(norm) cb = fig.colorbar(coll,location='right',pad="5%",drawedges=True) cb.ax.tick_params(axis='y',direction='out') return cb ################################################################################ ################################################################################
[docs]def curvedEarthAxes(rect=111, fig=None, minground=0., maxground=2000, minalt=0, maxalt=500, Re=6371., nyticks=5, nxticks=4): """ Create curved axes in ground-range and altitude **Args**: * [**rect**]: subplot spcification * [**fig**]: A pylab.figure object (default to gcf) * [**maxground**]: maximum ground range [km] * [**minalt**]: lowest altitude limit [km] * [**maxalt**]: highest altitude limit [km] * [**Re**]: Earth radius **Returns**: * **ax**: matplotlib.axes object containing formatting * **aax**: matplotlib.axes object containing data **Example**: :: import numpy as np from utils import plotUtils ax, aax = plotUtils.curvedEarthAxes() th = np.linspace(0, ax.maxground/ax.Re, 50) r = np.linspace(ax.Re+ax.minalt, ax.Re+ax.maxalt, 20) Z = exp( -(r - 300 - ax.Re)**2 / 100**2 ) * np.cos(th[:, np.newaxis]/th.max()*4*np.pi) x, y = np.meshgrid(th, r) im = aax.pcolormesh(x, y, Z.T) ax.grid() written by Sebastien, 2013-04 """ from matplotlib.transforms import Affine2D, Transform import mpl_toolkits.axisartist.floating_axes as floating_axes from matplotlib.projections import polar from mpl_toolkits.axisartist.grid_finder import FixedLocator, DictFormatter import numpy as np from pylab import gcf ang = maxground / Re minang = minground / Re angran = ang - minang angle_ticks = [(0, "{:.0f}".format(minground))] while angle_ticks[-1][0] < angran: tang = angle_ticks[-1][0] + 1./nxticks*angran angle_ticks.append( ( tang, "{:.0f}".format((tang-minang)*Re) ) ) grid_locator1 = FixedLocator([v for v, s in angle_ticks]) tick_formatter1 = DictFormatter(dict(angle_ticks)) altran = maxalt - minalt alt_ticks = [(minalt+Re, "{:.0f}".format(minalt))] while alt_ticks[-1][0] < Re+maxalt: alt_ticks.append( ( 1./nyticks*altran+alt_ticks[-1][0], "{:.0f}".format(altran*1./nyticks+alt_ticks[-1][0]-Re) ) ) _ = alt_ticks.pop() grid_locator2 = FixedLocator([v for v, s in alt_ticks]) tick_formatter2 = DictFormatter(dict(alt_ticks)) tr_rotate = Affine2D().rotate(np.pi/2-ang/2) tr_shift = Affine2D().translate(0, Re) tr = polar.PolarTransform() + tr_rotate grid_helper = floating_axes.GridHelperCurveLinear(tr, extremes=(0, angran, Re+minalt, Re+maxalt), grid_locator1=grid_locator1, grid_locator2=grid_locator2, tick_formatter1=tick_formatter1, tick_formatter2=tick_formatter2, ) if not fig: fig = gcf() ax1 = floating_axes.FloatingSubplot(fig, rect, grid_helper=grid_helper) # adjust axis ax1.axis["left"].label.set_text(r"ALt. [km]") ax1.axis["bottom"].label.set_text(r"Ground range [km]") ax1.invert_xaxis() ax1.minground = minground ax1.maxground = maxground ax1.minalt = minalt ax1.maxalt = maxalt ax1.Re = Re fig.add_subplot(ax1, transform=tr) # create a parasite axes whose transData in RA, cz aux_ax = ax1.get_aux_axes(tr) aux_ax.patch = ax1.patch # for aux_ax to have a clip path as in ax ax1.patch.zorder=0.9 # but this has a side effect that the patch is # drawn twice, and possibly over some other # artists. So, we decrease the zorder a bit to # prevent this. return ax1, aux_ax ################################################################################ ################################################################################
[docs]def addColorbar(mappable, ax): """ Append colorbar to axes **Args**: * **mappable**: a mappable object * **ax**: an axes object **Returns**: * **cbax**: colorbar axes object .. note:: This is mostly useful for axes created with :func:`curvedEarthAxes`. written by Sebastien, 2013-04 """ from mpl_toolkits.axes_grid1 import SubplotDivider, LocatableAxes, Size import matplotlib.pyplot as plt fig1 = ax.get_figure() divider = SubplotDivider(fig1, *ax.get_geometry(), aspect=True) # axes for colorbar cbax = LocatableAxes(fig1, divider.get_position()) h = [Size.AxesX(ax), # main axes Size.Fixed(0.1), # padding Size.Fixed(0.2)] # colorbar v = [Size.AxesY(ax)] _ = divider.set_horizontal(h) _ = divider.set_vertical(v) _ = ax.set_axes_locator(divider.new_locator(nx=0, ny=0)) _ = cbax.set_axes_locator(divider.new_locator(nx=2, ny=0)) _ = fig1.add_axes(cbax) _ = cbax.axis["left"].toggle(all=False) _ = cbax.axis["top"].toggle(all=False) _ = cbax.axis["bottom"].toggle(all=False) _ = cbax.axis["right"].toggle(ticklabels=True, label=True) _ = plt.colorbar(mappable, cax=cbax) return cbax ################################################################################ ################################################################################
[docs]def textHighlighted(xy, text, color='k', fontsize=None, xytext=(0,0), zorder=None, text_alignment=(0,0), xycoords='data', textcoords='offset points', **kwargs): """Plot highlighted annotation (with a white lining) **Belongs to**: :class:`rbspFp` **Args**: * **xy**: position of point to annotate * **text**: text to show * **[xytext]**: text position * **[zorder]**: text zorder * **[color]**: text color * **[fontsize]**: text font size * **[xycoords]**: xy coordinate (see `matplotlib.pyplot.annotate<http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.annotate>`) * **[textcoords]**: text coordinate (see `matplotlib.pyplot.annotate<http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.annotate>`) """ import matplotlib as mp from pylab import gca ax = gca() text_path = mp.text.TextPath( (0,0), text, size=fontsize, **kwargs) p1 = mp.patches.PathPatch(text_path, ec="w", lw=4, fc="w", alpha=0.7, zorder=zorder, transform=mp.transforms.IdentityTransform()) p2 = mp.patches.PathPatch(text_path, ec="none", fc=color, zorder=zorder, transform=mp.transforms.IdentityTransform()) offsetbox2 = mp.offsetbox.AuxTransformBox(mp.transforms.IdentityTransform()) offsetbox2.add_artist(p1) offsetbox2.add_artist(p2) ab = mp.offsetbox.AnnotationBbox(offsetbox2, xy, xybox=xytext, xycoords=xycoords, boxcoords=textcoords, box_alignment=text_alignment, frameon=False) ab.set_zorder(zorder) ax.add_artist(ab)