This commit is contained in:
2025-11-01 15:18:03 -07:00
parent dd40446f8c
commit 1ee5d2fb06
10 changed files with 114 additions and 15 deletions

View File

@@ -0,0 +1 @@
UTF-8

View File

@@ -0,0 +1 @@
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 466 KiB

View File

@@ -9,11 +9,12 @@ import os
from pathlib import Path
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point
from shapely.geometry import Point, box
import matplotlib.pyplot as plt
from matplotlib_scalebar.scalebar import ScaleBar
import matplotlib.patheffects as pe
from sqlalchemy import create_engine
from matplotlib import patches
ROOT = Path('/home/dadams/Repos/colorado_spills')
ANALYSIS_DIR = ROOT / 'analysis' / 'new analysis Aug 2025'
@@ -127,9 +128,42 @@ def try_query_counties(engine) -> gpd.GeoDataFrame | None:
def get_colorado_boundary(spills_gdf: gpd.GeoDataFrame) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame | None]:
"""Return Colorado boundary and optional counties. Prefer PostGIS; fallback to convex-hull from points."""
engine = get_engine()
"""Return Colorado boundary and optional counties. Prefer local shapefile, then PostGIS; fallback to convex-hull from points."""
counties = None
# 1) Prefer local shapefile if present
shp_dir = ROOT / 'colorado_shape' / 'USA_Census_Counties_7412386151545161096'
shp_path = shp_dir / 'dtl_cnty.shp'
if shp_path.exists():
try:
gdf = gpd.read_file(shp_path)
# Try common Colorado filters
col_candidates = [
("STATEFP", "08"),
("STATE_FIPS", "08"),
("STATE", "CO"),
("STATE_NAME", "Colorado"),
]
cg = gdf
for col, val in col_candidates:
if col in gdf.columns:
if val == "08":
cg = gdf[gdf[col].astype(str) == "08"]
else:
cg = gdf[gdf[col].astype(str).str.upper() == str(val).upper()]
if not cg.empty:
break
if cg.empty:
cg = gdf # If filtering failed, use all (last resort)
counties = cg.to_crs(4326)
# Dissolve to single state boundary
boundary = counties.dissolve().reset_index(drop=True)
return boundary[["geometry"]], counties[["geometry"]]
except Exception:
pass
# 2) PostGIS
engine = get_engine()
if engine is not None:
boundary = try_query_boundary(engine)
if boundary is not None and not boundary.empty:
@@ -155,6 +189,36 @@ def style_axes(ax):
for spine in ax.spines.values():
spine.set_visible(False)
def get_cities_gdf():
"""Major Colorado cities for reference with lat/lon (EPSG:4326)."""
cities = [
{"name": "Denver", "lat": 39.7392, "lon": -104.9903},
{"name": "Colorado Springs", "lat": 38.8339, "lon": -104.8214},
{"name": "Fort Collins", "lat": 40.5853, "lon": -105.0844},
{"name": "Pueblo", "lat": 38.2544, "lon": -104.6091},
{"name": "Grand Junction", "lat": 39.0639, "lon": -108.5506},
{"name": "Greeley", "lat": 40.4233, "lon": -104.7091},
{"name": "Boulder", "lat": 40.01499, "lon": -105.2705},
{"name": "Durango", "lat": 37.2753, "lon": -107.8801},
]
df = pd.DataFrame(cities)
gdf = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df['lon'], df['lat']), crs='EPSG:4326')
return gdf
def plot_cities(ax, cities_gdf_3857):
"""Plot city points and labels on given axis (coordinates in EPSG:3857)."""
x = cities_gdf_3857.geometry.x.values
y = cities_gdf_3857.geometry.y.values
ax.scatter(x, y, s=12, facecolor='white', edgecolor='black', linewidth=0.6, zorder=6)
for i, row in cities_gdf_3857.iterrows():
ax.annotate(
row['name'],
(row.geometry.x, row.geometry.y),
xytext=(4, 3), textcoords='offset points',
fontsize=8, color='black', zorder=6,
path_effects=[pe.withStroke(linewidth=2.5, foreground='white')]
)
def map_points(spills: gpd.GeoDataFrame, boundary: gpd.GeoDataFrame, counties: gpd.GeoDataFrame | None):
# Project to Web Mercator for plotting
@@ -171,6 +235,10 @@ def map_points(spills: gpd.GeoDataFrame, boundary: gpd.GeoDataFrame, counties: g
# Plot points with alpha and small size to avoid overplotting
spills_3857.plot(ax=ax, markersize=1.2, alpha=0.35, color='#2c7fb8')
# Cities
cities = get_cities_gdf().to_crs(3857)
plot_cities(ax, cities)
# Tighten to boundary
minx, miny, maxx, maxy = boundary_3857.total_bounds
ax.set_xlim(minx, maxx)
@@ -193,8 +261,39 @@ def map_points(spills: gpd.GeoDataFrame, boundary: gpd.GeoDataFrame, counties: g
pass
ax.set_title(title, fontsize=12, pad=10)
# Front Range inset (approx bounding box DenverBoulderFort Collins corridor)
# Define bbox in EPSG:4326 and project to 3857
fr_minx, fr_miny, fr_maxx, fr_maxy = -106.0, 39.0, -104.5, 40.9
bbox_poly_3857 = gpd.GeoSeries([box(fr_minx, fr_miny, fr_maxx, fr_maxy)], crs='EPSG:4326').to_crs(3857)
bxmin, bymin, bxmax, bymax = bbox_poly_3857.total_bounds
# Locator rectangle on main map
rect = patches.Rectangle((bxmin, bymin), bxmax - bxmin, bymax - bymin,
linewidth=1.2, edgecolor='#333333', facecolor='none', zorder=7)
ax.add_patch(rect)
# Inset axes: [left, bottom, width, height] in figure fraction
inset_ax = fig.add_axes([0.62, 0.62, 0.32, 0.32])
boundary_3857.plot(ax=inset_ax, color='#f5f5f5', edgecolor='#888888', linewidth=0.6)
if counties is not None and not counties.empty:
counties_inset_3857 = counties.to_crs(3857)
counties_inset_3857.boundary.plot(ax=inset_ax, color='#aaaaaa', linewidth=0.25)
spills_3857.plot(ax=inset_ax, markersize=1.2, alpha=0.35, color='#2c7fb8')
# Cities on inset
cities_inset = get_cities_gdf().to_crs(3857)
plot_cities(inset_ax, cities_inset)
inset_ax.set_xlim(bxmin, bxmax)
inset_ax.set_ylim(bymin, bymax)
style_axes(inset_ax)
# Thin frame around inset
for spine in inset_ax.spines.values():
spine.set_visible(True)
spine.set_linewidth(0.6)
spine.set_edgecolor('#444444')
out_png = OUT / 'spill_map_points.png'
fig.savefig(out_png, dpi=300)
# Tight crop to reduce whitespace
fig.savefig(out_png, dpi=300, bbox_inches='tight', pad_inches=0.05)
plt.close(fig)
return out_png
@@ -219,6 +318,10 @@ def map_hex(spills: gpd.GeoDataFrame, boundary: gpd.GeoDataFrame, counties: gpd.
cb = fig.colorbar(hb, ax=ax, fraction=0.028, pad=0.01)
cb.set_label('Spill count')
# Cities
cities = get_cities_gdf().to_crs(3857)
plot_cities(ax, cities)
# Scale bar and north arrow
ax.add_artist(ScaleBar(1, units='m', location='lower left', box_alpha=0.7))
add_north_arrow(ax)
@@ -228,20 +331,12 @@ def map_hex(spills: gpd.GeoDataFrame, boundary: gpd.GeoDataFrame, counties: gpd.
ax.set_xlim(minx, maxx)
ax.set_ylim(miny, maxy)
style_axes(ax)
# Dynamic title from available year fields
title = 'Spill Density (Hexbin)'
year_cols = [c for c in spills.columns if c.lower() in ('year', 'yr')]
if year_cols:
try:
yrs = spills[year_cols[0]].dropna().astype(int)
if not yrs.empty:
title = f"Spill Density (Hexbin), {yrs.min()}{yrs.max()}"
except Exception:
pass
ax.set_title(title, fontsize=12, pad=10)
# No title per manuscript layout; keep figure clean
out_pdf = OUT / 'spill_map_hex.pdf'
out_png = OUT / 'spill_map_hex.png'
fig.savefig(out_pdf)
fig.savefig(out_png, dpi=300, bbox_inches='tight', pad_inches=0.05)
plt.close(fig)
return out_pdf