The following notebook gives one example of how to make a decent looking chart with filled bathymetric contours using matplotlib. Decent being in the eye of the beholder. This builds on discussion from the 27 Oct 2014 PyHOGS meeting, plus earlier meetings on colormaps with matplotlib.

Aim

Using Matlab it's hard to directly align desired contour intervals with the desired color, and this becomes very tricky if your contour intervals are not evenly spaced. It turns out there are easy ways to do this with matplotlib.

Colormap

I'd like a colormap that goes from white (shallow water) to dark blue (deep water), that has a preponderance of colors for shallow intervals for resolving large bathymetric changes close to land.

Nonuniform contour intervals

There is an easy way to implement this by built-in scaling functions that scale the data to the colormap. Taking the log of the value to be shown in color is one way to deal with data with large ranges, but I used the built-in method instead.

When using plt.colorbar(), the contour intervals can be shown two ways, either with uniform graphical spacing regardless of the actual value (the default) colorbar(pc, spacing='uniform') or by scaling the colorbar ticks based on the values of the contour levels colorbar(spacing='proportional') Proportional spacing looks better for this example.

Implementation

Import and define functions


In [1]:
%matplotlib inline
from __future__ import division # to remove integer division (e.g. 2/5=0)
from __future__ import unicode_literals # to remove unicode/ascii distinction in python2

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import scipy.io as spio
import matplotlib

# Use function from JPaul/Earle
def custom_div_cmap(numcolors=11, name='custom_div_cmap',
                    mincol='blue', midcol='white', maxcol='red'):
    """ Create a custom diverging colormap with three colors
    
    Default is blue to white to red with 11 colors.  Colors can be specified
    in any way understandable by matplotlib.colors.ColorConverter.to_rgb()
    """

    from matplotlib.colors import LinearSegmentedColormap 
    
    cmap = LinearSegmentedColormap.from_list(name=name, 
                                             colors =[mincol, midcol, maxcol],
                                             N=numcolors)
    return cmap

# make negative contours, normally dashed by default, be solid
matplotlib.rcParams['contour.negative_linestyle'] = 'solid'

Load in bathymetric data from the western subtropical Pacific, of Luzon Island


In [2]:
# load bathymetry data from matlab data file
BB = spio.loadmat('../../data/bathy.mat');
blon = BB['blon']
blat = BB['blat']
bdepth = BB['bdepth']
Blon,Blat = np.meshgrid(blon,blat)

Setup contour intervals and colormaps


In [3]:
# set up contour levels and color map, note that (min, max) of bdepth is (-6066, 2411)
blevels = [-8000, -7000, -6000, -5000, -4000, -3000, -2000, -1500, -1000, -500, -200, -100, 0]
N = len(blevels)-1
cmap1 = plt.cm.get_cmap('Blues_r',N)

cmap2 = custom_div_cmap(N, mincol='DarkBlue', midcol='CornflowerBlue' ,maxcol='w')

Now start plotting. First do a naive plot to show why the straightforward approach doesn't give a good looking example.


In [4]:
fig, ax = plt.subplots(1,1, figsize=(10,6))
ax.set_aspect(1/np.cos(np.average(blat)*np.pi/180)) # set the aspect ratio for a local cartesian grid
pc = plt.contourf(Blon,Blat,bdepth, vmin=-8000, vmax=0, levels=blevels, cmap=cmap2, extend='min')
#plt.colorbar(pc, ticks=blevels, spacing='proportional') # spacing='proportional' for scaling the tick marks by their value
plt.colorbar(pc, ticks=blevels, spacing='uniform') # spacing='uniform' for making tick marks be equidistant on the page

# extract the coastlines, needs to use contour object from contour, because contourf breaks up
# the contours and so the filled polygons are not the entire landmass
#pc = plt.contour(Blon,Blat,bdepth, levels=blevels, colors=None)
#j0 = blevels.index(0)
#b0s = pc.collections[j0].get_paths()
pc = plt.contour(Blon,Blat,bdepth, levels=[0], colors='0.8')
b0s = pc.collections[0].get_paths()

for i in range(len(b0s)):
    b0x = b0s[i].vertices[:,0]
    b0y = b0s[i].vertices[:,1]
    plt.fill(b0x,b0y, '0.8', linestyle='solid') # plot land as filled gray


Now make a non-linear colormap using BoundaryNorm() and make it pretty.


In [5]:
blevels = [-8000, -7000, -6000, -5000, -4000, -3000, -2000, -1500, -1000, -500, -200, -100, 0]
# make lots of shallow levels, for showing the good color resolution
#blevels = [-8000, -7000, -6000, -5000, -4000, -3000, -2000, -1500, -1200, -1000, -800, -600, -400, -200, -100, 0]
N = len(blevels)-1

from matplotlib.colors import BoundaryNorm

bnorm = BoundaryNorm(blevels, ncolors=N, clip=True)

fig, ax = plt.subplots(1,1, figsize=(10,6))
ax.set_aspect(1/np.cos(np.average(blat)*np.pi/180)) # set the aspect ratio for a local cartesian grid
pc = plt.contourf(Blon,Blat,bdepth, vmin=-8000, vmax=0, levels=blevels, norm=bnorm, cmap=cmap2, extend='min')
plt.colorbar(pc, ticks=blevels, spacing='uniform')

# extract the coastlines
pc = plt.contour(Blon,Blat,bdepth, levels=[0], colors='0.7', linewidth=0)
b0s = pc.collections[0].get_paths()
for i in range(len(b0s)):
    b0x = b0s[i].vertices[:,0]
    b0y = b0s[i].vertices[:,1]
    plt.fill(b0x,b0y, '0.7', linestyle='solid') # plot land as filled gray

# plot specific isobath(s)
pc = plt.contour(Blon,Blat,bdepth, levels=[-1000], colors='k')