How to set fixed size for 'basemap' subplot when iterating through rows?

995 views Asked by At

Please forgive me as this is my first go at a Python 'Project'.

A quick overview: I am trying to produce maps (MatPlotLib figures) by iterating through a GeoPandas dataframe of a GeoJSON file containing the boundaries of active subdivision phases in order to show the progress of construction of the individual lots in each subdivision phase. We normally did this manually in GIS, but I figured I'd take a stab at automating some, or all, of the process. I am also attempting to do this without using ESRI's python functionality and would prefer to keep it that way for stability for future use as ESRI can move around quite a bit.

I am able to iterate through the geodataframe and produce a map (figure) that is zoomed to the extent of the subdivision phase boundary, however, it is clipping the background aerial imagery basemap to the minimum binding box of the subdivision phase that I have been using to set the 'zoom' of the figure.

Example of what I am aiming to produce. This was made in ArcGIS Pro:

Example of what I am aiming to produce. This was made in ArcGIS Pro.

What I am able to make in Python: What I am able to make in Python.

I am unable to get the basemap to be a constant size that fills a standard landscape letter page whilst still being correctly zoomed in to the extent of the subdivision phase. My output jpeg is sized correctly, but the aerial imagery basemap is continuously croppped to the extent of the subdivision phase boundary leaving large borders around the figure.

import geopandas as gpd # extension to Pandas to work with geodata
import urllib.request, json # download from the web
import os.path # work with the local file system
from shapely.geometry import Point # basic functions to work with vector geometries
import matplotlib as mpl # plotting
from matplotlib import pyplot as plt # some matplotlib convenience functions
import contextily as ctx # simple free basemaps
import csv # read .csv files
import time # time tracking for processes
from matplotlib.patches import Patch # build legend items
from matplotlib.lines import Line2D # build legend items
from matplotlib import figure # change size of figures

start_time = time.time() # Keep track of execution time

# Build Legend Elements
legend_elements = [Patch(facecolor='green', edgecolor='r', alpha=0.5, 
                         label='Completed Lots'),
                   Patch(facecolor='red', edgecolor='black', alpha=0.5, 
                         label='Lots Under Construction'),
                   Patch(facecolor='none', edgecolor='r',
                         label='Subdivision Boundary')]

for index, row in phases.iterrows():
    name = row['NAME']
    print(name)
    lots_complete_select = lots_complete[(lots_complete['SUBDIVISION'] == row['NAME'])]
    print('Completed Lots: ' + str(len(lots_complete_select)))
    lots_uc_select = lots_uc[(lots_uc['SUBDIVISION'] == row['NAME'])]
    print('Lots Under Construction: ' + str(len(lots_uc_select)))
    phase_select = phases[(phases['NAME'] == row['NAME'])]
    lots_select = lots[(lots['SUBDIVISION'] == row['NAME'])]
    print('Lots in subdivision: ' + str(len(lots_select)))
    
    minx, miny, maxx, maxy = (lots_select).total_bounds # Set zoom to lots for area of interest
    
    map_time = time.time()
    print('Building map...')
    fig, ax = plt.subplots() # Create a figure with multiple plots within it
    ax.set_aspect('equal')
    ax.axis('off') # Turn off axes

    ax.set_xlim(minx, maxx) # Apply x zoom level
    ax.set_ylim(miny, maxy) # Apply y zoom level
    
    streets.plot(ax=ax, linewidth=1.2, color='black') # Plot Streets
    
    lots_complete_select.plot(ax=ax, linewidth=0.5, color='green', edgecolor='red', alpha=0.5) # Plot completed lots
    
    lots_uc_select.plot(ax=ax, linewidth=0.5, color='red', edgecolor='black', alpha=0.5) # Plot U.C. lots
    
    lots.plot(ax=ax, linewidth=0.7, color='none', edgecolor='black') # Plot lot lines
    
    phase_select.plot(ax=ax, linewidth=4, color='none', edgecolor='black') # Plot Active Subdivision Phases
    phase_select.plot(ax=ax, linewidth=2, color='none', edgecolor='red') # Plot Active Subdivision Phases
    phase_select.plot(ax=ax, linewidth=1, color='none', edgecolor='white') # Plot Active Subdivision Phases
    
    ctx.add_basemap(ax, crs=streets.crs.to_string(), source=ctx.providers.Esri.WorldImagery, 
                    attribution='City of Pflugerville GIS Services') # Change basemap
        
    ax.set_title((name + ' Residential Construction'), fontsize=10, fontweight ="bold") # Set title
    
    ax.legend(handles=legend_elements, prop={'size': 6}, title='Legend', framealpha=1, fancybox=False, 
              edgecolor='black', loc='upper left') # Add legend
                
    plt.savefig(save_path + name + '.jpg', edgecolor='black', orientation='landscape', papertype='letter', 
                dpi=300) # Save map
        
    print('Map saved.')
    print('Done building map.')
    print("--- %s Seconds ---" % (time.time() - map_time) + '\n')

print('Done configuring maps.')
print("--- %s Minutes ---" % ((time.time() - start_time)/60))

I have tried using figsize=(11, 8.5) in various places around the code to no effect.

Any input would be greatly appreciated. Please let me know if there is anything that needs to be changed/clarified.

Also, if anyone is familiar with labelling line features, such as streets, in Python, is there a way of labelling streets in Python in a manner similar to what is seen in the first image?

1

There are 1 answers

3
martinfleis On BEST ANSWER

If you want to have a fixed area which, say, extends the bounding box by 10m, you have to set xlim and ylim extended. You are explicitly specifying both to bounding box.

margin = 10
ax.set_xlim(minx - margin, maxx + margin)
ax.set_ylim(miny - margin, maxy + margin)

Furthermore, to remove white boundaries around your plot, you can set bbox_inches to tight in saving.

plt.savefig(save_path + name + '.jpg', edgecolor='black', orientation='landscape', papertype='letter', 
                dpi=300, bbox_inches='tight') # Save map