Search code examples
pythonmatplotlibplotlygeojsoncontour

Adding GeoJSON contours as layers on Plotly Density_Mapbox


I would like to add a weather contour on top of a plotly density_mapbox map, but am unsure of the necessary steps.

First, I created a matplotlib contour plot to visualize the data.

Then, I used geojsoncontour to create a geojson file from said matplotlib contour plot of the contours.

What I would like to do now, is plot the contours in the same map as the density_mapbox.

geojson and .csv files containing data can be found here.

Concerning the .csv file, 'Rand_Data' is the data that goes into the density_mapbox plot, 'Rain_in' is the data used to generate the contours.

link to data: https://github.com/jkiefn1/Contours_and_plotly

Creating the Mapbox:

# Create the static figure
fig = px.density_mapbox(df
                        ,lat='lat'
                        ,lon='long'
                        ,z='Rand_Data'
                        ,hover_data={
                                     'lat':True # remove from hover data
                                     ,'long':True # remove from hover data
                                     ,col:True
                                    }
                        ,center=dict(lat=38.5, lon=-96)
                        ,zoom=3
                        ,radius=30
                        ,opacity=0.5
                        ,mapbox_style='open-street-map'
                        ,color_continuous_scale='inferno'
                       )

fig.show()

enter image description here

Creating the matplotlib contour plot and generating the geojson file

# Load in the DataFrame
path = r'/Users/joe_kiefner/Desktop/Sample_Data.csv'
df = pd.read_csv(path, index_col=[0])
data = []

# Define rain levels to be contours in geojson
levels = [0.25,0.5,1,2.5,5,10]

colors = ['royalblue',  'cyan',  'lime',  'yellow', 'red']
vmin   = 0
vmax   = 1
cm     = branca.colormap.LinearColormap(colors, vmin=vmin, vmax=vmax).to_step(len(levels))


x_orig = (df.long.values.tolist())
y_orig = (df.lat.values.tolist())
z_orig = np.asarray(df['Rain_in'].values.tolist())


x_arr          = np.linspace(np.min(x_orig), np.max(x_orig), 500)
y_arr          = np.linspace(np.min(y_orig), np.max(y_orig), 500)
x_mesh, y_mesh = np.meshgrid(x_arr, y_arr)

xscale = df.long.max() - df.long.min()
yscale = df.lat.max() - df.lat.min()

scale = np.array([xscale, yscale])


z_mesh = griddata((x_orig, y_orig), z_orig, (x_mesh, y_mesh), method='linear')


sigma = [5, 5]
z_mesh = sp.ndimage.filters.gaussian_filter(z_mesh, sigma, mode='nearest')

# Create the contour
contourf = plt.contourf(x_mesh, y_mesh, z_mesh, levels, alpha=0.9, colors=colors, 
                        linestyles='none', vmin=vmin, vmax=vmax)

# Convert matplotlib contourf to geojson
geojson = geojsoncontour.contourf_to_geojson(
    contourf=contourf,
    min_angle_deg=3,
    ndigits=2,
    unit='in',
    stroke_width=1,
    fill_opacity=0.3)
d = json.loads(geojson)
len_features=len(d['features'])
if not data:
    data.append(d)
else:
    for i in range(len(d['features'])):
         data[0]['features'].append(d['features'][i])
            
with open('/path/to/Sample.geojson', 'w') as f:
   dump(geojson, f)

enter image description here


Solution

    • there are two core options
      1. add as layers https://plotly.com/python/mapbox-layers/
      2. add as choropleth traces https://plotly.com/python/mapbox-county-choropleth/
      3. layers-legend - same as layers option with addition of creation of a legend by adding additional traces to figure
    • both these options are coded up below. change value of OPTION to switch between them
    • layers means there is no legend or hover text
    • choropleth these are present, moved colorbar so it does not overlap legend. More beatification of legend and hover text requires...
    import json, requests
    import pandas as pd
    import geopandas as gpd
    import plotly.express as px
    
    txt = requests.get(
        "https://raw.githubusercontent.com/jkiefn1/Contours_and_plotly/main/Sample.geojson"
    ).text
    js = json.loads(json.loads(txt))
    
    df = pd.read_csv(
        "https://raw.githubusercontent.com/jkiefn1/Contours_and_plotly/main/Sample_Data.csv"
    )
    
    col = "Rand_Data"
    fig = px.density_mapbox(
        df,
        lat="lat",
        lon="long",
        z="Rand_Data",
        hover_data={
            "lat": True,  # remove from hover data
            "long": True,  # remove from hover data
            col: True,
        },
        center=dict(lat=38.5, lon=-96),
        zoom=3,
        radius=30,
        opacity=0.5,
        mapbox_style="open-street-map",
        color_continuous_scale="inferno",
    )
    
    OPTION = "layers-legend"
    if OPTION[0:6]=="layers":
        fig.update_traces(legendgroup="weather").update_layout(
            mapbox={
                "layers": [
                    {
                        "source": f,
                        "type": "fill",
                        "color": f["properties"]["fill"],
                        "opacity": f["properties"]["fill-opacity"],
                    }
                    for f in js["features"]
                ],
            }
        )
        
        if OPTION=="layers-legend":
            # create a dummy figure to create a legend for the geojson
            dfl = pd.DataFrame(js["features"])
            dfl = pd.merge(
                dfl["properties"].apply(pd.Series),
                dfl["geometry"].apply(pd.Series)["coordinates"].apply(len).rename("len"),
                left_index=True,
                right_index=True,
            )
            figl = px.bar(
                dfl.loc[dfl["len"].gt(0)],
                color="title",
                x="fill",
                y="fill-opacity",
                color_discrete_map={cm[0]: cm[1] for cm in dfl.loc[:, ["title", "fill"]].values},
            ).update_traces(visible="legendonly")
    
            fig.add_traces(figl.data).update_layout(
                xaxis={"visible": False}, yaxis={"visible": False}, coloraxis={"colorbar":{"y":.25}}
            )
    else:
        gdf = gpd.GeoDataFrame.from_features(js)
        gdf = gdf.loc[~gdf.geometry.is_empty]
        cmap = {
            list(d.values())[0]: list(d.values())[1]
            for d in gdf.loc[:, ["title", "fill"]].apply(dict, axis=1).tolist()
        }
        fig2 = px.choropleth_mapbox(
            gdf,
            geojson=gdf.geometry,
            locations=gdf.index,
            color="title",
            color_discrete_map=cmap,
            opacity=.3
        )
        fig.add_traces(fig2.data).update_layout(coloraxis={"colorbar":{"y":.25}})
        
        
    fig
    

    layers

    enter image description here

    traces

    enter image description here