ResidentMario / geoplot

High-level geospatial data visualization library for Python.
https://residentmario.github.io/geoplot/index.html
MIT License
1.15k stars 95 forks source link

Shared colorbar between subplots #247

Closed adamgarbo closed 3 years ago

adamgarbo commented 3 years ago

Hi @ResidentMario,

Thanks for a great repository.

A quick question regarding creating a figure with several subplots that all share a single colour bar. This was originally asked about in #163, but the discussion migrated to adding colour bar normalization functionality. Speaking of, normalizing the colour bar works great and can be applied to multiple subplots. However, when the colour bar is enabled for a single axis, it will change the size of the subplot, as shown below:

figure

Is there a preferred method of adding a shared colourbar in Geoplot that won't distort the size of an individual subplot? Perhaps, by adding it to the figure instead of an axis?

Cheers, Adam

# Normalize colourbar
norm = mpl.colors.Normalize(vmin=0, vmax=150)
cmap = mpl.cm.ScalarMappable(norm=norm, cmap='Spectral_r').cmap

# Set extents
extents = [-61, -50, 45, 58]

# Set projection
proj = gcrs.PlateCarree()
fig = plt.figure(figsize=(20,10))
ax1 = plt.subplot(131, projection=proj)
ax2 = plt.subplot(132, projection=proj)
ax3 = plt.subplot(133, projection=proj)

gplt.choropleth(
    shapefile, 
    hue='mean',
    projection=gcrs.PlateCarree(),
    edgecolor='none', 
    legend=False,
    cmap=cmap,
    norm=norm,
    ax=ax1)
ax1.add_feature(coast)
ax1.set_extent(extents)
gl = ax1.gridlines(draw_labels=True, color='black', alpha=0.25, linestyle='dotted')
gl.top_labels = False
gl.right_labels = False
gl.rotate_labels = False  
gl.xlocator = mticker.FixedLocator(np.arange(-100,-40,1))
gl.ylocator = mticker.FixedLocator(np.arange(40,85,1))
ax1.spines['bottom'].set_visible(True)
ax1.spines['top'].set_visible(True)
ax1.spines['right'].set_visible(True)
ax1.spines['left'].set_visible(True)

gplt.choropleth(
    shapefile, 
    hue='min',
    projection=gcrs.PlateCarree(),
    edgecolor='none', 
    legend=False,
    cmap=cmap,
    norm=norm,
    ax=ax2)
ax2.add_feature(coast)
ax2.set_extent(extents)
gl = ax2.gridlines(draw_labels=True, color='black', alpha=0.25, linestyle='dotted')
gl.top_labels = False
gl.right_labels = False
gl.rotate_labels = False  
gl.xlocator = mticker.FixedLocator(np.arange(-100,-40,1))
gl.ylocator = mticker.FixedLocator(np.arange(40,85,1))
ax2.spines['bottom'].set_visible(True)
ax2.spines['top'].set_visible(True)
ax2.spines['right'].set_visible(True)
ax2.spines['left'].set_visible(True)

gplt.choropleth(
    shapefile, 
    hue='max',
    projection=gcrs.PlateCarree(),
    edgecolor='none', 
    legend=True,
    cmap=cmap,
    norm=norm,
    ax=ax3)
ax3.add_feature(coast)
ax3.set_extent(extents)
gl = ax3.gridlines(draw_labels=True, color='black',  alpha=0.25, linestyle='dotted')
gl.top_labels = False
gl.right_labels = False
gl.rotate_labels = False  
gl.xlocator = mticker.FixedLocator(np.arange(-100,-40,1))
gl.ylocator = mticker.FixedLocator(np.arange(40,85,1))
ax3.spines['bottom'].set_visible(True)
ax3.spines['top'].set_visible(True)
ax3.spines['right'].set_visible(True)
ax3.spines['left'].set_visible(True)
ResidentMario commented 3 years ago

Yeah, this is not surprising to me. The legend=True parameter is called on a single plot, and the code as currently written has awareness of that one plot and one axis and not of its parent figure. So that colorbar is being squeezed onto the axis, forcing the actual plot element to shrink to accommodate it.

To fix this specific issue, I believe that you can use the following code (ref):

fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap))

Doing this automatically for you could definitely be part of the geoplot API, but it would also definitely complicated the implementation (if the code needs to manipulate the colorbar it now needs to know where to look to find it; I'd have to double up on colorbar tests account for this new boolean option; etcetera).

So far my position has been that geoplot will do everything possible to make overplotting on a single axis work great, but that setting up subplotting to work how you want it to work is up to you. This is because I think the "correct" implementation for subplotting is to do what seaborn does, which is to have a whole separate API for it.

Does setting the colorbar on the figure do the trick for you?

adamgarbo commented 3 years ago

Hi @ResidentMario,

Thanks for the quick reply. It appears that setting the colorbar to the figure will still change the size of the right-most subplot.

proj = gcrs.PlateCarree()
fig = plt.figure(figsize=(15,5))
ax1 = plt.subplot(131, projection=proj)
ax2 = plt.subplot(132, projection=proj)
ax3 = plt.subplot(133, projection=proj)
fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap))

test

However, if you add the axes as a list to the colorbar, it does appear to plot correctly:

fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), ax=(ax1,ax2,ax3))

colorbar_test

I found a good discussion on colorbars and subplots on Stack Overflow that gave me the hint to do so: https://stackoverflow.com/questions/13784201/matplotlib-2-subplots-1-colorbar

Thanks again for your help.

Cheers, Adam