Political Cartography: People Vote, Not Land

Standard choropleth maps create a massive visual distortion in election reporting. By coloring administrative boundaries, large, sparsely populated rural areas dominate the viewer's perception. This geographical "spread" visually overrepresents certain parties, even if their actual voter count in those massive districts is relatively small.

The core thesis of this project is simple: People vote, not land.

The Geometric Solution: Anamorphosis

To break this visual illusion, I built an analytical pipeline that calculates a Non-contiguous Cartogram. By distorting the geometry so that the physical area of a district is strictly proportional to its number of voters, the true democratic weight is revealed.

  • Metropolises expand to show their density.
  • Rural districts shrink, revealing their lower population density.

The Architecture & Pipeline

The project is structured into a clean data engineering and visualization pipeline, designed for performance and precision.

1. Spatial Processing (processor.py)

Handling spatial distortion requires strict coordinate reference system (CRS) management. The pipeline projects the raw EPSG:4326 data into a metric CRS (e.g., EPSG:3857), scales each district geometry around its centroid based on the voter weight, and re-projects it for the frontend.

# Simplified Anamorphosis Scaling Logic
def calculate_cartogram_geometries(gdf: gpd.GeoDataFrame, weight_col: str) -> gpd.GeoDataFrame:
    """
    Scales district polygons based on voter count to create a non-contiguous cartogram.
    """
    # Project to metric for accurate area calculations
    gdf_metric = gdf.to_crs(epsg=3857)
    
    # Calculate scale factor: (District Voters / Average Voters)
    mean_voters = gdf_metric[weight_col].mean()
    gdf_metric['scale_factor'] = gdf_metric[weight_col] / mean_voters
    
    # Scale geometries around their individual centroids
    scaled_geoms = gdf_metric.apply(
        lambda row: row.geometry.scale(
            xfact=row['scale_factor'], 
            yfact=row['scale_factor'], 
            origin=row.geometry.centroid
        ), 
        axis=1
    )
    
    gdf_metric.geometry = scaled_geoms
    # Re-project for web mapping engine
    return gdf_metric.to_crs(epsg=4326)

Try It Live

The app is deployed and running — explore the data directly below or open it full screen.