3

The python Basemap has two contents plotted in it: A multiline shapefile (IL_State_ln) and a scatterplot of some random points within the basemap extent. My interest is in generating a legend that gives information about both shapefile and the scatter points. For now, I am only able to include the points in the legend and not the shapefile.

Checking the Basemap API documentation does not provide any information, as the function readshapefile() does not seem to have any label argument.

Could you please help me include the shapefile indicator in the legend like in the ArcGIS maps?

Here is my code:

import numpy as np    
from matplotlib import pyplot as plt
from mpl_toolkits.basemap import Basemap

fname = "DATA/GIS/IL_State_ln"

m = Basemap(llcrnrlon=-92.,llcrnrlat=36.8,urcrnrlon=-86.5,urcrnrlat=43.,
             resolution='i', projection='tmerc', lat_0 = 36.5, lon_0 = -91.8)

m.readshapefile(fname, 'mygeom')

x,y = m([-90., -91.2, -88.], [38., 37.7, 42.])
m.scatter(x,y, marker='o', label="Points")

plt.legend(loc=3)
plt.show()

I'm using Python 3.5, matplotlib 2.0 and basemap 1.0.8.

SereneWizard
  • 166
  • 3
  • 14

1 Answers1

16

The idea to create a legend entry would be to draw the shapes as polygons, which can then be added to the legend.
Therfore we would first deactivate drawbounds, m.readshapefile(fn, 'shf', drawbounds = False). We can then create a matplotlib.patches.Polygon from the shapefile and add it to the axes, plt.gca().add_artist(polygon).

The legend can then be updated using this polygon

handles, labels = plt.gca().get_legend_handles_labels()
handles.extend([polygon])  
labels.extend(["Name of the shape"])                     
plt.legend(handles=handles, labels=labels)

Here is now some code in action, which produces the following images. It uses the ne_10m_admin_0_countries file.

enter image description here

from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
import numpy as np

m = Basemap(llcrnrlon=-10,llcrnrlat=35,urcrnrlon=35,urcrnrlat=60.,
             resolution='i', projection='tmerc', lat_0 = 48.9, lon_0 = 15.3)

m.drawcoastlines()
m.drawcountries(zorder=0, color=(.9,.9,.9), linewidth=1)

fn = r"ne_10m_admin_0_countries\ne_10m_admin_0_countries"
m.readshapefile(fn, 'shf', drawbounds = False)

#Madrid
x,y = m([-3.703889],[40.4125])
m.plot(x,y, marker="o", color="blue", label="Madrid", ls="")

# some countries
countries = ['Switzerland', 'Ireland', "Belgium"]
colors= {'Switzerland':"red", 'Ireland':"orange", 'Belgium' : "purple"}
shapes = {}
for info, shape in zip(m.shf_info, m.shf):
    if info['NAME'] in countries:
        p= Polygon(np.array(shape), True, facecolor= colors[info['NAME']], 
                   edgecolor='none', alpha=0.7, zorder=2)
        shapes.update({info['NAME'] : p})

for country in countries:
    plt.gca().add_artist(shapes[country]) 


# create legend, by first getting the already present handles, labels
handles, labels = plt.gca().get_legend_handles_labels()
# and then adding the new ones
handles.extend([shapes[c] for c in countries])  
labels.extend(countries)                     
plt.legend(handles=handles, labels=labels, framealpha=1.)

plt.show()

Now because we already have a polygon with the shape, why not make the legend a bit more fancy, by directly plotting the shape into the legend. This can be done as follows.

enter image description here

from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
import numpy as np

m = Basemap(llcrnrlon=-10,llcrnrlat=35,urcrnrlon=35,urcrnrlat=60.,
             resolution='i', projection='tmerc', lat_0 = 48.9, lon_0 = 15.3)

m.drawcoastlines()

fn = r"ne_10m_admin_0_countries\ne_10m_admin_0_countries"
m.readshapefile(fn, 'shf', drawbounds = False)

#Madrid
x,y = m([-3.703889],[40.4125])
m.plot(x,y, marker="o", color="blue", label="Madrid", ls="")

countries = ['Switzerland', 'Ireland', "Belgium"]
colors= {'Switzerland':"red", 'Ireland':"orange", 'Belgium' : "purple"}
shapes = {}
for info, shape in zip(m.shf_info, m.shf):
    if info['NAME'] in countries:
        p= Polygon(np.array(shape), True, facecolor= colors[info['NAME']], 
                   edgecolor='none', alpha=0.7, zorder=2)
        shapes.update({info['NAME'] : p})

for country in countries:
    plt.gca().add_artist(shapes[country]) 


class PolygonN(object):
    def legend_artist(self, legend, orig_handle, fontsize, handlebox):
        x0, y0 = handlebox.xdescent, handlebox.ydescent
        width, height = handlebox.width, handlebox.height
        aspect= height/float(width)
        verts = orig_handle.get_xy()
        minx, miny = verts[:,0].min(), verts[:,1].min()
        maxx, maxy = verts[:,0].max(), verts[:,1].max()
        aspect= (maxy-miny)/float((maxx-minx))
        nvx = (verts[:,0]-minx)*float(height)/aspect/(maxx-minx)-x0
        nvy = (verts[:,1]-miny)*float(height)/(maxy-miny)-y0

        p = Polygon(np.c_[nvx, nvy])
        p.update_from(orig_handle)
        p.set_transform(handlebox.get_transform())

        handlebox.add_artist(p)
        return p

handles, labels = plt.gca().get_legend_handles_labels()
handles.extend([shapes[c] for c in countries])  
labels.extend(countries)     
plt.legend(handles=handles, labels=labels, handleheight=3, handlelength=3, framealpha=1.,
           handler_map={Polygon: PolygonN()} )

plt.show()
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • I have a question. What is the use of the variable *zorder*? Or why is it used here? – SereneWizard Mar 07 '17 at 17:14
  • Very good question. `zorder` determines the order by which layers are drawn, the higher the zorder to more on top lives the layer. It's like sheets of paper on the desk - the lowest one has the lowest zorder. However, there is no reason to use it here and leaving it out makes the shapes live behind the coastlines which might be looking better. – ImportanceOfBeingErnest Mar 07 '17 at 17:42
  • A question clicked in my mind. Instead of using *matplotlib.patches.Polygon()*, couldn't we use *shapely.geometry.Polygon()* here? The purpose is still to get the polygon in the legend. – SereneWizard Mar 08 '17 at 02:30
  • I would guess that you can use a `shapely.geometry.Polygon` from which you could create a `descartes.patch.PolygonPatch`. This patch could be added to the matplotlib canvas using `plt.gca().add_patch()`. However I cannot test it and it's also not clear to me what the benefit of that would be. – ImportanceOfBeingErnest Mar 08 '17 at 07:27