Plotly maps with Matplotlib Basemap in Python/v3

An IPython Notebook showing how to make an interactive world map using plotly and Maplotlib Basemap


Note: this page is part of the documentation for version 3 of Plotly.py, which is not the most recent version.
See our Version 4 Migration Guide for information about how to upgrade.

This notebook comes in response to this Rhett Allain tweet.

Version Check

In [1]:
import plotly
plotly.__version__
Out[1]:
'1.2.6'

Next, if you have a plotly account as well as a credentials file set up on your machine, singing in to Plotly's servers is done automatically while importing plotly.plotly. Import the plotly graph objects (in particular Contour) to help build our figure:

In [2]:
import plotly.plotly as py
from plotly.graph_objs import *

Data with this notebook will be taken from a NetCDF file, so import netcdf class from the scipy.io module, along with numpy:

In [4]:
import numpy as np           
from scipy.io import netcdf  

Finally, import the Matplotlib Basemap Toolkit, its installation instructions can found here.

In [5]:
from mpl_toolkits.basemap import Basemap

Get the Data

The data is taken from NOAA Earth System Research Laboratory.

Unfortunately, this website does not allow to code your output demand and/or use wget to download the data.

That said, the data used for this notebook can be downloaded in a only a few clicks:

  • Select Air Temperature in Varaibles
  • Select Surface in Analysis level?
  • Select Jul | 1 and Jul | 31
  • Enter 2014 in the Enter Year of last day of range field
  • Select Anomaly in Plot type?
  • Select All in Region of globe
  • Click on Create Plot

Then on the following page, click on Get a copy of the netcdf data file used for the plot to download the NetCDF on your machine.

Note that the data represents the average daily surface air temperature anomaly (in deg. C) for July 2014 with respect to 1981-2010 climatology.

Now, import the NetCDF file into this IPython session. The following was inspired by this earthpy blog post.

In [6]:
# Path the downloaded NetCDF file (different for each download)
f_path = '/home/etienne/Downloads/compday.Bo3cypJYyE.nc'

# Retrieve data from NetCDF file
with netcdf.netcdf_file(f_path, 'r') as f:
    lon = f.variables['lon'][::]    # copy as list
    lat = f.variables['lat'][::-1]  # invert the latitude vector -> South to North
    air = f.variables['air'][0,::-1,:]  # squeeze out the time dimension, 
                                        # invert latitude index

The values lon start a 0 degrees and increase eastward to 360 degrees. So, the air array is centered about the Pacific Ocean. For a better-looking plot, shift the data so that it is centered about the 0 meridian:

In [7]:
# Shift 'lon' from [0,360] to [-180,180], make numpy array
tmp_lon = np.array([lon[n]-360 if l>=180 else lon[n] 
                   for n,l in enumerate(lon)])  # => [0,180]U[-180,2.5]

i_east, = np.where(tmp_lon>=0)  # indices of east lon
i_west, = np.where(tmp_lon<0)   # indices of west lon
lon = np.hstack((tmp_lon[i_west], tmp_lon[i_east]))  # stack the 2 halves

# Correspondingly, shift the 'air' array
tmp_air = np.array(air)
air = np.hstack((tmp_air[:,i_west], tmp_air[:,i_east]))

Make Contour Graph Object

Very simply,

In [8]:
trace1 = Contour(
    z=air,
    x=lon,
    y=lat,
    colorscale="RdBu",
    zauto=False,  # custom contour levels
    zmin=-5,      # first contour level
    zmax=5        # last contour level  => colorscale is centered about 0
)

Get Coastlines and Country boundaries with Basemap

The Basemap module includes data for drawing coastlines and country boundaries onto world maps. Adding coastlines and/or country boundaries on a matplotlib figure is done with the .drawcoaslines() or .drawcountries() Basemap methods.

Next, we will retrieve the Basemap plotting data (or polygons) and convert them to longitude/latitude arrays (inspired by this stackoverflow post) and then package them into Plotly Scatter graph objects .

In other words, the goal is to plot each continuous coastline and country boundary lines as 1 Plolty scatter line trace.

In [10]:
# Make shortcut to Basemap object, 
# not specifying projection type for this example
m = Basemap() 

# Make trace-generating function (return a Scatter object)
def make_scatter(x,y):
    return Scatter(
        x=x,
        y=y,
        mode='lines',
        line=Line(color="black"),
        name=' '  # no name on hover
    )

# Functions converting coastline/country polygons to lon/lat traces
def polygons_to_traces(poly_paths, N_poly):
    ''' 
    pos arg 1. (poly_paths): paths to polygons
    pos arg 2. (N_poly): number of polygon to convert
    '''
    traces = []  # init. plotting list 

    for i_poly in range(N_poly):
        poly_path = poly_paths[i_poly]
        
        # get the Basemap coordinates of each segment
        coords_cc = np.array(
            [(vertex[0],vertex[1]) 
             for (vertex,code) in poly_path.iter_segments(simplify=False)]
        )
        
        # convert coordinates to lon/lat by 'inverting' the Basemap projection
        lon_cc, lat_cc = m(coords_cc[:,0],coords_cc[:,1], inverse=True)
        
        # add plot.ly plotting options
        traces.append(make_scatter(lon_cc,lat_cc))
     
    return traces

# Function generating coastline lon/lat traces
def get_coastline_traces():
    poly_paths = m.drawcoastlines().get_paths() # coastline polygon paths
    N_poly = 91  # use only the 91st biggest coastlines (i.e. no rivers)
    return polygons_to_traces(poly_paths, N_poly)

# Function generating country lon/lat traces
def get_country_traces():
    poly_paths = m.drawcountries().get_paths() # country polygon paths
    N_poly = len(poly_paths)  # use all countries
    return polygons_to_traces(poly_paths, N_poly)

# Get list of of coastline and country lon/lat traces
traces_cc = get_coastline_traces()+get_country_traces()

Make a Figure Object and Plot!

Package the Contour trace with the coastline and country traces. Note that the Contour trace must be placed before the coastline and country traces in order to make all traces visible. Layout options are set in a Layout object:

In [13]:
data = Data([trace1]+traces_cc)

title = u"Average daily surface air temperature anomalies [\u2103]<br> \
in July 2014 with respect to 1981-2010 climatology"

anno_text = "Data courtesy of \
<a href='http://www.esrl.noaa.gov/psd/data/composites/day/'>\
NOAA Earth System Research Laboratory</a>"

axis_style = dict(
    zeroline=False,
    showline=False,
    showgrid=False,
    ticks='',
    showticklabels=False,
)

layout = Layout(
    title=title,
    showlegend=False,
    hovermode="closest",        # highlight closest point on hover
    xaxis=XAxis(
        axis_style,
        range=[lon[0],lon[-1]]  # restrict y-axis to range of lon
    ),
    yaxis=YAxis(
        axis_style,
    ),
    annotations=Annotations([
        Annotation(
            text=anno_text,
            xref='paper',
            yref='paper',
            x=0,
            y=1,
            yanchor='bottom',
            showarrow=False
        )
    ]),
    autosize=False,
    width=1000,
    height=500,
)

Package data and layout in a Figure object and send it to plotly:

In [14]:
fig = Figure(data=data, layout=layout)

py.iplot(fig, filename="maps", width=1000)

See this graph in full screen here.

Reference

See our online documentation page or our User Guide.