Search code examples
pythoncolorsplotlychoroplethplotly-express

Plotly express choropleth map custom color_continuous_scale


I am trying to create a custom coloring for an animated choropleth map. I am using Plotly express and my dataframe looks like this.

where I am plotting the values on each region (region code=K_KRAJ, region name=N_KRAJ) and my animation is over the variables.

The values are in percentages so the min is 0 and max is 1. I want to divide the colors into 6 parts with exactly the midpoints as written here in color_continous_scale

fig = px.choropleth(df_anim,
             locations="K_KRAJ",
             featureidkey="properties.K_KRAJ",
             geojson=regions_json,
             color="value",
             hover_name="N_KRAJ",
             color_continuous_scale=[(0.0, "#e5e5e5"),   (0.0001, "#e5e5e5"),
                                     (0.0001, "#ffe5f0"),   (0.0075, "#ffe5f0"),
                                     (0.0075, "#facfdf"), (0.01, "#facfdf"),
                                     (0.01, "#f3b8ce"),  (0.025, "#f3b8ce"),
                                     (0.025, "#eca2bf"), (0.05, "#eca2bf"),
                                     (0.05, "#e37fb1"), (1, "#e37fb1")
                                    ],
             animation_frame="variable"
                   )
fig.update_geos(fitbounds="locations", visible=False)
fig.show()

Unfortunately, that creates a wrong map like this

instead of a map like this

the second map which is almost correct was created using the largest value as 100% and mathematically finding the midpoints. Even though this is very close to being correct, there can always be numerical mistakes and I would rather use the code shown above if it worked correctly.

the almost correct one was created like this (max value was 0.06821107602623269)

color_continuous_scale=[(0.0, "#e5e5e5"),   (0.001449275362, "#e5e5e5"), # 0.01% , 0.0001
                        (0.01449275362, "#ffe5f0"),   (0.1086956522, "#ffe5f0"), # 0.75% , 0.0075
                        (0.1086956522, "#facfdf"), (0.1449275362, "#facfdf"), # 1% , 0.01
                        (0.1449275362, "#f3b8ce"),  (0.3623188406, "#f3b8ce"), # 2.5% , 0.025
                        (0.3623188406, "#eca2bf"), (0.7246376812, "#eca2bf"), # 5% , 0.05
                        (0.7246376812, "#e37fb1"), (1, "#e37fb1") # 6.9% , 0.069
                                    ],

And even best if someone knew how to change the numbers in the colorscale which is shown in the images on the right from numbers to percentages (0.05 -> 5%)

If I add range_color=(0, 1) it adds the correct colors but then there is a useless colorbar on the right.


Solution

    • color_continuous_scale is a Plotly Express construct not limited to choropleths. Hence technique presented is how to build a color scale
    • I cannot find a repeatable source of Czech region geometry, hence code below does not work as an MWE without you have geometry in your own downloads folder
    • core solution
      • given you want six bins, start by using pd.cut() and get the bin edges
      • with this scale them to be between 0 and 1 to work with color scales
      • construct colorscale with hard edges
    edges = pd.cut(df_anim["value"], bins=5, retbins=True)[1]
    edges = edges[:-1] / edges[-1]
    colors = ["#e5e5e5", "#ffe5f0", "#facfdf", "#f3b8ce", "#eca2bf", "#e37fb1"]
    
    cc_scale = (
        [(0, colors[0])]
        + [(e, colors[(i + 1) // 2]) for i, e in enumerate(np.repeat(edges, 2))]
        + [(1, colors[5])]
    )
    
    from pathlib import Path
    import geopandas as gpd
    import pandas as pd
    import numpy as np
    import plotly.express as px
    
    # simulate source data
    gdf = gpd.read_file(
        list(Path.home().joinpath("Downloads/WGS84").glob("*KRAJ*.shp"))[0]
    ).set_crs("epsg:4326")
    gdf["geometry"] = gdf.to_crs(gdf.estimate_utm_crs()).simplify(2000).to_crs(gdf.crs)
    regions_json = gdf.__geo_interface__
    
    df = (
        pd.json_normalize(regions_json["features"])
        .pipe(lambda d: d.loc[:, [c.strip() for c in d.columns if c[0:3] == "pro"]])
        .rename(columns={"properties.ID": "K_KRAJ", "properties.NAZEV_NUTS": "N_KRAJ"})
    )
    df_anim = df.merge(
        pd.DataFrame(
            {"variable": [f"REL{n1}{n2}" for n1 in range(15, 21) for n2 in ["06", "12"]]}
        ),
        how="cross",
    ).pipe(lambda d: d.assign(value=np.random.uniform(0, 0.003, len(d))))
    # end data simulation
    
    edges = pd.cut(df_anim["value"], bins=5, retbins=True)[1]
    edges = edges[:-1] / edges[-1]
    colors = ["#e5e5e5", "#ffe5f0", "#facfdf", "#f3b8ce", "#eca2bf", "#e37fb1"]
    
    cc_scale = (
        [(0, colors[0])]
        + [(e, colors[(i + 1) // 2]) for i, e in enumerate(np.repeat(edges, 2))]
        + [(1, colors[5])]
    )
    
    fig = px.choropleth(
        df_anim,
        locations="K_KRAJ",
        featureidkey="properties.ID", ### ! changed !
        geojson=regions_json,
        color="value",
        hover_name="N_KRAJ",
        color_continuous_scale=cc_scale,
        animation_frame="variable",
    )
    fig.update_geos(fitbounds="locations", visible=False)
    

    enter image description here