Geometric Transformations Applied to GeoDataFrames During Plotting in Prettymaps
Prettymaps applies translate, scale, and rotate transformations to GeoDataFrames through the transform_gdfs function in draw.py before rendering maps with Matplotlib.
Before any map elements appear on screen, the prettymaps library aggressively manipulates spatial data. The plot() API automatically projects, aggregates, affinely transforms, and re-projects GeoDataFrames (GDFs) fetched from OpenStreetMap. This pipeline ensures that users can shift, stretch, and rotate entire city layouts with simple numeric parameters.
The transform_gdfs Pipeline in prettymaps/draw.py
All geometric transformations happen inside transform_gdfs, located at lines 75‑87 of [prettymaps/draw.py](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/draw.py). The function follows a strict six-step workflow:
- Project to planar CRS — convert from latitude/longitude to local meter-based coordinates
- Aggregate geometries — pack every layer into nested
GeometryCollectionobjects - Apply affine operations — translate, scale, and rotate the unified collection
- De-aggregate — split transformed geometries back to their original layers
- Re-project to geographic CRS — return to
EPSG:4326for downstream compatibility
This design guarantees that all layers move together as a rigid body, preventing misalignment between streets, buildings, and water features.
Why Projection Precedes Transformation
The library relies on osmnx.projection.project_gdf to switch each GDF to a planar coordinate reference system before any affine work:
gdfs = {
name: ox.projection.project_gdf(gdf) if len(gdf) > 0 else gdf
for name, gdf in gdfs.items()
}
Planar coordinates (meters) are required because shapely.affinity operations assume Euclidean space. Latitude/longitude degrees vary in physical size depending on location, which would distort scaling and rotation.
Core Affine Operations: translate, scale, rotate
Once aggregated into a single GeometryCollection, the geometries pass through three shapely.affinity functions in sequence:
| Operation | Shapely Call | Purpose |
|---|---|---|
| Translate | shapely.affinity.translate(collection, x, y) |
Move east/west (x) and north/south (y) in meters |
| Scale | shapely.affinity.scale(collection, scale_x, scale_y) |
Stretch horizontally (scale_x) and vertically (scale_y) |
| Rotate | shapely.affinity.rotate(collection, rotation) |
Pivot around the origin by rotation degrees counter-clockwise |
The rotation angle uses degrees, not radians, matching user expectations and Matplotlib conventions.
Aggregation and De-aggregation Mechanics
To apply uniform transforms across layers, prettymaps nests collections:
collection = GeometryCollection([
GeometryCollection(list(gdf.geometry))
for gdf in gdfs.values()
])
After transformation, geometries are reassigned using their original order:
gdfs[layer].geometry = list(collection.geoms[i].geoms)
This preserves layer names and GeoDataFrame structure while permitting mathematical operations that treat the entire map as one geometric object.
User-Facing API: Parameters in plot()
The high-level plot() function exposes transformations directly. According to the docstring and implementation in draw.py, the signature includes:
def plot(...,
x: float = 0,
y: float = 0,
scale_x: float = 1,
scale_y: float = 1,
rotation: float = 0,
...):
These parameters feed directly into transform_gdfs:
gdfs = transform_gdfs(gdfs, x, y, scale_x, scale_y, rotation, logging=logging)
Users never need to import transform_gdfs directly unless building custom workflows.
Translation Example: Shifting a City Boundary
import prettymaps as pm
# Shift Porto Alegre 500m east, 200m north
pm.plot(
query="Porto Alegre, Brazil",
x=500,
y=200,
scale_x=1,
scale_y=1,
rotation=0,
layers={"streets": {}},
style={"streets": {"ec": "#222", "fc": "#ddd"}},
)
The x=500, y=200 arguments move the entire rendered map without changing the underlying OSM query.
Rotation Example: Diamond-Oriented Park View
pm.plot(
query="Central Park, New York, USA",
rotation=45, # degrees counter-clockwise
layers={"waterway": {}},
style={"waterway": {"fc": "#aaddff"}},
)
Setting rotation=45 pivots the dataset around the planar origin, producing an angled perspective of the park's reservoirs and paths.
Direct Access to transform_gdfs for Advanced Pipelines
For workflows requiring inspection or intermediate processing, import transform_gdfs alongside get_gdfs from fetch.py:
from prettymaps.draw import transform_gdfs, get_gdfs
# Fetch raw OSM layers
raw_gdfs = get_gdfs(
query="Amsterdam, Netherlands",
layers={"streets": {}, "waterway": {}},
radius=None,
dilate=None,
rotation=0,
logging=False,
)
# Apply custom affine transforms
transformed = transform_gdfs(
raw_gdfs,
x=-300, # shift west
y=150, # shift north
scale_x=0.8, # compress width
scale_y=0.8, # compress height
rotation=30, # tilt
)
# Access transformed GeoDataFrame directly
streets = transformed["streets"]
print(streets.total_bounds) # check new extents
This pattern separates data acquisition from spatial manipulation, enabling metric computation or multi-stage filtering before最终渲染.
CRS Handling: The Re-projection Step
After transformation, transform_gdfs forces every layer back to EPSG:4326:
gdfs[layer] = ox.projection.project_gdf(gdfs[layer], to_crs="EPSG:4326")
This step ensures compatibility with:
- Hill-shade generation (operates on geographic coordinates)
- Background layer creation in
draw_background - External data fusion requiring latitude/longitude
The round-trip projection—geographic → planar → geographic—adds minimal overhead because OSMnx caches CRS transformations internally.
Source Code Architecture
Understanding these files clarifies how geometric transformations fit into the broader pipeline:
| File | Transformation Role |
|---|---|
prettymaps/draw.py |
Contains transform_gdfs and orchestrates the full draw sequence |
prettymaps/fetch.py |
Provides get_gdfs for raw OSM retrieval; outputs feed into transformation |
prettymaps/utils.py |
Supplies execution-time logging via log_execution_time decorator |
prettymaps/__init__.py |
Exposes public plot API that triggers the transformation chain |
Summary
- Geometric transformations in prettymaps happen automatically through
transform_gdfsindraw.py - The pipeline projects, aggregates, transforms, de-aggregates, and re-projects GeoDataFrames as a unified workflow
- Users control translate (x, y), scale (scale_x, scale_y), and rotate via simple parameters in
plot() - Planar CRS is mandatory for accurate affine math; the library handles this transparently
- Direct
transform_gdfsaccess enables preprocessing for advanced cartographic workflows
Frequently Asked Questions
What coordinate units does prettymaps use for translation?
Prettymaps expects meters for the x and y translation parameters. These values apply after the library projects GeoDataFrames to a planar CRS, where one unit equals one meter locally. Passing geographic degrees would produce massive, unintended shifts.
Can I apply different transformations to individual layers?
No—the transform_gdfs implementation deliberately treats all layers as a rigid body. To transform layers independently, you must extract GDFs manually with get_gdfs, modify geometries separately, and pass them to lower-level drawing functions rather than plot().
Why does prettymaps re-project to EPSG:4326 after transformation?
Downstream operations including hill-shade calculation and background generation in draw.py assume latitude/longitude coordinates. The re-projection step maintains compatibility with these routines while keeping the internal transformation math in reliable planar units.
Does rotation affect the bounding box used for fetching OSM data?
No—rotation happens after data fetching. The rotation parameter in plot() passes through to transform_gdfs, which runs on already-retrieved geometries. If you need a rotated query boundary for large rotations, you must manually expand the search radius or use dilate.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →