Basemap in 3D

Even though many people don’t like them, maps with 3d elements can be created using basemap and the matplotlib mplot3d toolkit.

Creating a basic map

The most important thing to know when starting with 3d matplotlib plots is that the Axes3D class has to be used. To add geographical data to the map, the method add_collection3d will be used:

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.basemap import Basemap

map = Basemap()

fig = plt.figure()
ax = Axes3D(fig)

'''
ax.azim = 270
ax.elev = 90
ax.dist = 5
'''

ax.add_collection3d(map.drawcoastlines(linewidth=0.25))
ax.add_collection3d(map.drawcountries(linewidth=0.35))

plt.show()
  • The ax variable is in this example, an Axes3D instance. All the methods will be used from this instance, so they need to support 3D operations, which doesn’t occur in many cases on the basemap methods
  • The commented block shows how to rotate the resulting map so the view is better
  • To draw lines, just use the add_collection3d method with the output of any of the basemap methods that return an matplotlib.patches.LineCollection object, such as drawcountries
_images/plotting_3d_basic.png

Basic usage, the axis rotation is the one by default

_images/plotting_3d_basic2.png

The axis rotation is set so the map is watched from the z axis, like when drawing it in 2D

Filling the polygons

Unfortunately, the basemap fillcontinents method doesn’t return an object supported by add_collection3d (PolyCollection, LineColleciton, PatchCollection), but a list of matplotlib.patches.Polygon objects.

The solution, of course, is to create a list of PolyCollection:

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.basemap import Basemap
from matplotlib.collections import PolyCollection

map = Basemap()

fig = plt.figure()
ax = Axes3D(fig)

ax.azim = 270
ax.elev = 50
ax.dist = 8

ax.add_collection3d(map.drawcoastlines(linewidth=0.25))
ax.add_collection3d(map.drawcountries(linewidth=0.35))

polys = []
for polygon in map.landpolygons:
    polys.append(polygon.get_coords())


lc = PolyCollection(polys, edgecolor='black',
                    facecolor='#DDDDDD', closed=False)

ax.add_collection3d(lc)


plt.show()
  • The coast lines and the countries are drawn as in the previous example
  • To create the PolyCollection, the polygons are needed, but the Basemap object has it in the field landpolygons. (There are others for the rest of the included polygons, such as the countries)
  • For each of the polygons, the coordinates can be retrieved as a list of floats using the get_coords method. (he are _geoslib.Polygon objects)
  • Once a list of coordinates list is created, the PoilyCollection can be built
  • The PolyCollection is added using add_collection3d, as we did with the lines
  • If the original polygons are added using fillcontinents, matplotlib says that doesn’t has the methods to convert it to 3D
_images/plotting_3d_fill.png

Adding 3D bars

Creating a 3D map hasn’t got sense if no 3D data is drawn on it. The Axes3D class has the bar3d method that draws 3D bars. It can be added on the map using the 3rd dimension:

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.basemap import Basemap
from matplotlib.collections import PolyCollection
import numpy as np

map = Basemap(llcrnrlon=-20,llcrnrlat=0,urcrnrlon=15,urcrnrlat=50,)

fig = plt.figure()
ax = Axes3D(fig)

ax.set_axis_off()
ax.azim = 270
ax.dist = 7

polys = []
for polygon in map.landpolygons:
    polys.append(polygon.get_coords())


lc = PolyCollection(polys, edgecolor='black',
                    facecolor='#DDDDDD', closed=False)

ax.add_collection3d(lc)
ax.add_collection3d(map.drawcoastlines(linewidth=0.25))
ax.add_collection3d(map.drawcountries(linewidth=0.35))

lons = np.array([-13.7, -10.8, -13.2, -96.8, -7.99, 7.5, -17.3, -3.7])
lats = np.array([9.6, 6.3, 8.5, 32.7, 12.5, 8.9, 14.7, 40.39])
cases = np.array([1971, 7069, 6073, 4, 6, 20, 1, 1])
deaths = np.array([1192, 2964, 1250, 1, 5, 8, 0, 0])
places = np.array(['Guinea', 'Liberia', 'Sierra Leone','United States', 'Mali', 'Nigeria', 'Senegal', 'Spain'])

x, y = map(lons, lats)

ax.bar3d(x, y, np.zeros(len(x)), 2, 2, deaths, color= 'r', alpha=0.8)

plt.show()
  • The map is zoomed to fit the needs of the Ebola cases dataset
  • The axes are eliminated with the method set_axis_off
  • The bar3d needs the x, y and z positions, plus the delta x, y and z. To be properly drawn, the z position must be 0, and the delta z, the final value
_images/plotting_3d_bars.png