Search code examples
pythonplotlymapboxplotly-python

Plotly create inset choropleth (Mapbox ideally) of Alaska and Hawaii


Is there a way to create additional choropleth (ideally Mapbox choropleths, but I'd settle for Plotly's standard choropleth function) maps and layer them on top of a base choropleth so that I can easily show data related to Alaska and Hawaii with the continental US?

Something like this image

Don't believe my existing code is necessarily helpful, but here's how I build my base map (removed my custom style so that anyone should be able to generate).

fig = px.choropleth_mapbox(
    df_mar,
    geojson=puma,
    locations="stpuma",
    color="inter_pct",
    range_color=(0,25),
    color_continuous_scale="Viridis",
    labels={"inter_pct": "Marriage (%)"},
    center={"lat": 37.0902, "lon": -95.7129},
    zoom=4.2,
    opacity=1.0,
    mapbox_style="white-bg"
)
fig.update_layout(
    coloraxis_colorbar=dict(
        bgcolor="rgba(22,33,49,1)",
        title="Marriage,<br>Percent Share",
        titlefont=dict(
            color="rgba(255,255,255,1)"
        ),
        tickfont=dict(
            color="rgba(255,255,255,1)"
        ),
    ),
    margin=dict(
        l=50,
        r=50,
        b=50,
        t=50,
        pad=4
    ),
    paper_bgcolor = "rgba(8,18,23,1)",
    plot_bgcolor = "rgba(8,18,23,1)",
    showlegend = True,
    annotations = [
        dict(
            x=-0.025,
            y=-0.04,
            xref='paper',
            yref='paper',
            text='Source: Census ACS 5 2015-2019',
            showarrow = False,
            font=dict(
                color="rgba(255,255,255,1)"
            ),
            bgcolor="rgba(8,18,23,1)",
        )
    ]
)
fig.update_traces(
    marker_line_width=0,
    below="waterway"
)
fig.show(width=1920, height=1080)

Solution

    • it can be achieved with mapbox
    • key concept is modify geometry to meet you layout requirements. Hence have done https://shapely.readthedocs.io/en/stable/manual.html#affine-transformations to move Alaska and Hawaii
    • also found that Alaska geometry was give issues given it crosses date line. Hence clipped geometry after transformation as well
    • for good measure added layers to indicate geometry has been manipulated
    • included maritime boundary to demonstrate how it can be extended to additional geometries
    import geopandas as gpd
    import shapely.geometry
    import numpy as np
    import plotly.express as px
    import requests, io
    from pathlib import Path
    from zipfile import ZipFile
    import urllib
    import pandas as pd
    from shapely.affinity import affine_transform as T
    
    # US geometry
    urls = [
        "https://www2.census.gov/geo/tiger/GENZ2018/shp/cb_2018_us_state_5m.zip",
        "https://maritimeboundaries.noaa.gov/downloads/USMaritimeLimitsAndBoundariesSHP.zip",
    ]
    gdfs = {}
    for url in urls:
        f = Path.cwd().joinpath(urllib.parse.urlparse(url).path.split("/")[-1])
    
        if not f.exists():
            r = requests.get(url, stream=True, headers={"User-Agent": "XY"})
            with open(f, "wb") as fd:
                for chunk in r.iter_content(chunk_size=128):
                    fd.write(chunk)
            zfile = ZipFile(f)
            zfile.extractall(f.stem)
    
        gdfs[f.stem] = gpd.read_file(
            list(f.parent.joinpath(f.stem).glob("*.shp"))[0]
        )  # .to_crs("EPSG:4326")
    
    gdf2 = gdfs["cb_2018_us_state_5m"]
    gdf2 = gdf2.set_index("STUSPS", drop=False)
    gdf2["color"] = gdf2["STATEFP"].astype(int)
    
    
    # move alaska and hawaii using affine transform
    t = {"AK": [0.6, 0, 0, 0.6, -20, -15], "HI": [3, 0, 0, 3, 385, -42]}
    clip = (-179, 15, 0, 150)
    gdf3 = gdf2.copy()
    gdf3.loc[t.keys(), "geometry"] = gdf3.loc[t.keys(), ["geometry"]].apply(
        lambda g: shapely.ops.clip_by_rect(T(g["geometry"], t[g.name]), *clip), axis=1
    )
    
    gdf_m = gdfs["USMaritimeLimitsAndBoundariesSHP"]
    gdf_m = gdf_m.dissolve("REGION")
    tm = {"Alaska": "AK", "Hawaiian Islands": "HI"}
    gdf_m.loc[tm.keys(), "geometry"] = gdf_m.loc[tm.keys(), ["geometry"]].apply(
        lambda g: shapely.ops.clip_by_rect(
            T(g["geometry"], t[tm[g.name]]), *gdf3.loc[tm[g.name]].geometry.bounds
        ),
        axis=1,
    )
    
    
    px.choropleth_mapbox(
        gdf3,
        geojson=gdf3.geometry.__geo_interface__,
        locations=gdf3.index,
        color="color",
        hover_name="NAME",
    ).update_layout(
        mapbox={
            "style": "carto-positron",
            "center": {"lon": -98, "lat": 33},
            "zoom": 2.5,
            "layers": [
                {
                    "source": shapely.geometry.box(
                        *gdf3.loc[box].geometry.bounds
                    ).__geo_interface__,
                    "type": "fill",
                    "color": "blue",
                    "opacity": 0.1,
                }
                for box in t.keys()
            ]
            + [
                {
                    "source": gdf_m.geometry.__geo_interface__,
                    "type": "line",
                    "color": "blue",
                    "line": {"width": 1},
                }
            ],
        },
        margin={"l": 0, "r": 0, "t": 0, "b": 0},
    )
    

    enter image description here