Animated Choroplethmapbox?

I’m wondering if it is possible to animate a Choroplethmapbox heat map? If tried to stitch together code based on animating scatter plots but substituting Choroplethmapbox in place of Scatter elements.

Here’s what I have written. It never finishes execution. I’m using plotly v4.6.

Thanks for any help!

-Chris

from urllib.request import urlopen
import json
with urlopen('https://raw.githubusercontent.com/plotly/datasets/master/geojson-counties-fips.json') as response:
    counties = json.load(response)

state = "New York"
us_county_df = df[(df["Country/Region"] == "United States") & \
                  (df["Province/State"] == state) & \
                  (df.Date > pd.datetime(2020, 3, 21))]
days = np.sort(us_county_df.Date.unique())

fig_frames = []
for day in days:
    day_df = us_county_df[us_county_df.Date == days[0]]
    fig_frames.append([go.Choroplethmapbox(geojson=counties, locations=day_df.FIPS,
                                           z=day_df.Confirmed, colorscale="Viridis",
                                           marker_opacity=0.5, marker_line_width=0)])


day_df = us_county_df[us_county_df.Date == days[0]]
fig = go.Figure(
    data=[go.Choroplethmapbox(geojson=counties, locations=day_df.FIPS,
                              z=day_df.Confirmed, colorscale="Viridis",
                              marker_opacity=0.5, marker_line_width=0)],
    layout=go.Layout(
        mapbox_style="carto-positron",
        mapbox_zoom=3,
        mapbox_center = {"lat": 37.0902, "lon": -95.7129},
        title="Start Title",
        updatemenus=[dict(type="buttons",
                          buttons=[dict(label="Play",
                                        method="animate",
                                        args=[None])])]
    ),
    frames=fig_frames
)
1 Like

For posterity’s sake I’ll share my finished code.

The primary source which helped me was a forum post on a similar issue which I stumbled across while looking for answers to another problem: Animation with slider not moving when pressing 'play'

I also refactored my code to follow the Slider and Buttons example here: https://plotly.com/python/animations/

I’ll note that the reference documentation is pretty slim for how to get sliders and buttons to work together. There’s no explanation for why the example code I mentioned actually works and the Python reference documentation for updatemenus, slider and frames leaves a lot unsaid. After much trial and error, here’s my final code.

import pandas as pd
import numpy as np
import plotly, plotly.graph_objects as go
import datetime as dt
from urllib.request import urlopen
import json

# read US county geojson file
with open(data_path +"us_county_geo.json") as f:
  counties_json = json.load(f)

def numpy_dt64_to_str(dt64):
    day_timestamp_dt = (dt64 - np.datetime64('1970-01-01T00:00:00Z')) / np.timedelta64(1, 's')
    day_dt = dt.datetime.utcfromtimestamp(day_timestamp_dt)
    return day_dt.strftime("%b %d")

plot_var = "ConfirmedPerDate"
us_df = df[(df["Country/Region"] == "United States") & \
           (df.Date > pd.datetime(2020, 3, 21))]
days = np.sort(us_df.Date.unique())
plot_df = us_df[us_df.Date == days[-1]]

fig_data =go.Choroplethmapbox(geojson=counties_json, locations=plot_df.FIPS, 
                              z=np.log10(plot_df[plot_var]),
                              zmin=0,
                              zmax=np.log10(us_df[plot_var].max()),
                              customdata=plot_df[plot_var],
                              name="",
                              text=plot_df.County.astype(str) + ", " + \
                                   plot_df["Province/State"].astype(str),
                              hovertemplate="%{text}<br>Cases: %{customdata}",
                              colorbar=dict(outlinewidth=1,
                                            outlinecolor="#333333",
                                            len=0.9,
                                            lenmode="fraction",
                                            xpad=30,
                                            xanchor="right",
                                            bgcolor=None,
                                            title=dict(text="Cases",
                                                       font=dict(size=14)),
                                            tickvals=[0,1,2,3,4,5,6],
                                            ticktext=["1", "10", "100", "1K", "10K", "100K", "1M"],
                                            tickcolor="#333333",
                                            tickwidth=2,
                                            tickfont=dict(color="#333333",
                                                          size=12)),
                              colorscale="ylorrd", #ylgn
                              #reversescale=True,
                              marker_opacity=0.7,
                              marker_line_width=0)

token = open(data_path + ".mapbox_token").read()
fig_layout = go.Layout(mapbox_style="light",
                       mapbox_zoom=3,
                       mapbox_accesstoken=token,
                       mapbox_center={"lat": 37.0902, "lon": -95.7129},
                       margin={"r":0,"t":0,"l":0,"b":0},
                       plot_bgcolor=None)

fig_layout["updatemenus"] = [dict(type="buttons",
                                  buttons=[dict(label="Play",
                                                method="animate",
                                                args=[None,
                                                      dict(frame=dict(duration=1000,
                                                                      redraw=True),
                                                           fromcurrent=True)]),
                                           dict(label="Pause",
                                                method="animate",
                                                args=[[None],
                                                      dict(frame=dict(duration=0,
                                                                      redraw=True),
                                                           mode="immediate")])],
                                  direction="left",
                                  pad={"r": 10, "t": 35},
                                  showactive=False,
                                  x=0.1,
                                  xanchor="right",
                                  y=0,
                                  yanchor="top")]

sliders_dict = dict(active=len(days) - 1,
                    visible=True,
                    yanchor="top",
                    xanchor="left",
                    currentvalue=dict(font=dict(size=20),
                                      prefix="Date: ",
                                      visible=True,
                                      xanchor="right"),
                    pad=dict(b=10,
                             t=10),
                    len=0.875,
                    x=0.125,
                    y=0,
                    steps=[])

fig_frames = []
for day in days:
    plot_df = us_df[us_df.Date == day]
    frame = go.Frame(data=[go.Choroplethmapbox(locations=plot_df.FIPS,
                                               z=np.log10(plot_df[plot_var]),
                                               customdata=plot_df[plot_var],
                                               name="",
                                               text=plot_df.County.astype(str) + ", " + \
                                                    plot_df["Province/State"].astype(str),
                                               hovertemplate="%{text}<br>%{customdata}")],
                     name=numpy_dt64_to_str(day))
    fig_frames.append(frame)

    slider_step = dict(args=[[numpy_dt64_to_str(day)],
                             dict(mode="immediate",
                                  frame=dict(duration=300,
                                             redraw=True))],
                       method="animate",
                       label=numpy_dt64_to_str(day))
    sliders_dict["steps"].append(slider_step)

fig_layout.update(sliders=[sliders_dict])

# Plot the figure 
fig=go.Figure(data=fig_data, layout=fig_layout, frames=fig_frames)
fig.show(renderer="browser")
3 Likes

awesome, but can you share the datasets too?

I created this account just to say thank you! I was struggling so much with this!

1 Like

same i just made an account just to thank this guy for this animation OMG

Are you able to share the ‘us_county_geo.json’? I have applied the code to my problem but when I get to the last step fig.show() I get the following error: "TypeError: Object of type GeoDataFrame is not JSON serializable. I suspect I need to convert my geopandas dataframe to a simpler json format but knowing how ‘us_county_geo.json’ looks would help

Fortunately, the JSON file you are interested in is stored in my Git repo:
https://raw.githubusercontent.com/buckeye17/seecovid/master/data_clean/us_states_geo.json

According to my code, it looks like I downloaded this file from here (not sure which link on the page I used): GeoJSON and KML data for the United States - Eric Celeste

I am not sure if this helps (if not, just ignore): I had been struggling with a similar problem until I understood that Plotly Choropleth expects the GeoJson file with a specific structure. I was using TopoJson and it didn’t work. I learned that I had to use a different Json file that must of type FeatureCollection and contain ids that match ids in the dataframe.

When I used a GeoDataframe (from Geopandas), this hint helped me:

The geojson argument expects a dictionary and you are passing a string. (…) turn your GeoJSON string to a dictionary.

Thanks. Adding go.Choroplethmapbox(geojson=eval(gpd_farm[‘geometry’].to_json()) as per

https://gis.stackexchange.com/questions/424860/problem-plotting-geometries-in-choropleth-map-using-plotly/436649#436649 worked for me. But, now it plots but doesn’t show the polygons and colors on the Choropleth.

This is my full code:

import pandas as pd
import numpy as np
import plotly, plotly.graph_objects as go
import datetime as dt
from urllib.request import urlopen
import json
geo_df_forecasts = gpd_farm.merge(df_forecast_covers, on="Paddock").assign(lat=lambda d: d.geometry.centroid.y, 
                                                        lon=lambda d: d.geometry.centroid.x).set_index('Paddock')
geo_df_forecasts['Cover'] = geo_df_forecasts['Cover'].astype(float).astype(int)
plot_var = 'Cover'
days = np.sort(geo_df_forecasts.Day.unique())
mapboxtoken = 'pk.eyJ1IjoiYnJ5YW50amFpbWVyIiwiYSI6ImNrdG5xbng2dDA1aXkydnJ6cGprcjQzYW8ifQ.qZIczC4AQt0rFKr-pib_fw'
mapboxstyle="mapbox://styles/mapbox/satellite-v9"
fig_data =go.Choroplethmapbox(geojson=eval(gpd_farm['geometry'].to_json()), 
                              locations=geo_df_forecasts.index, 
                              z=geo_df_forecasts[plot_var],
                              zmin=1000,
                              zmax=4000,
                              customdata=geo_df_forecasts[plot_var],
                              name="",
                              text=geo_df_forecasts.index.astype(str),
                              hovertemplate="%{text}<br>Cover (kg DM/ha): %{customdata}",
                              colorbar=dict(
                                            bgcolor=None,
                                            title=dict(text="Cover",
                                                       font=dict(size=14)),
                                            tickvals=[1000,1500,2000,2500,3000,3500,4000],
                                            ticktext=["1000", "1500", "2000", "2500", "3000", "3500", "4000"],
                                            ),
                              colorscale="spectral", #ylgn
                              marker_opacity=0.7,
                              marker_line_width=0)

fig_layout = go.Layout(mapbox_style=mapboxstyle,
                       mapbox_zoom=13.3,
                       mapbox_accesstoken=mapboxtoken,
                       mapbox_center={"lat": centre_lat, "lon": centre_long},
                       margin={"r":0,"t":0,"l":0,"b":0},
                       plot_bgcolor=None)

fig_layout["updatemenus"] = [dict(type="buttons",
                                  buttons=[dict(label="Play",
                                                method="animate",
                                                args=[None,
                                                      dict(frame=dict(duration=1000,
                                                                      redraw=True),
                                                           fromcurrent=True)]),
                                           dict(label="Pause",
                                                method="animate",
                                                args=[[None],
                                                      dict(frame=dict(duration=0,
                                                                      redraw=True),
                                                           mode="immediate")])],
                                  direction="left",
                                  pad={"r": 10, "t": 35},
                                  showactive=False,
                                  x=0.1,
                                  xanchor="right",
                                  y=0,
                                  yanchor="top")]

sliders_dict = dict(active=len(days) - 1,
                    visible=True,
                    yanchor="top",
                    xanchor="left",
                    currentvalue=dict(font=dict(size=20),
                                      prefix="Date: ",
                                      visible=True,
                                      xanchor="right"),
                    pad=dict(b=10,
                             t=10),
                    len=0.875,
                    x=0.125,
                    y=0,
                    steps=[])

fig_frames = []
for day in days:
    plot_df = geo_df_forecasts[geo_df_forecasts.Day == day]
    frame = go.Frame(data=[go.Choroplethmapbox(locations=plot_df.index,
                                               z=plot_df[plot_var],
                                               customdata=plot_df[plot_var],
                                               name="",
                                               text=plot_df.index.astype(str),
                                               hovertemplate="%{text}<br>Cover (kg DM/ha): %{customdata}")],
                     name=day.astype(str))
    fig_frames.append(frame)
    slider_step = dict(args=[[day.astype(str)],
                             dict(mode="immediate",
                                  frame=dict(duration=300,
                                             redraw=True))],
                       method="animate",
                       label=day.astype(str))
    sliders_dict["steps"].append(slider_step)
fig_layout.update(sliders=[sliders_dict])
fig=go.Figure(data=fig_data, layout=fig_layout, frames=fig_frames)
fig.show()

And geo_df_forecasts looks like this:

| |geometry |Area |Cover |Day |lat |lon|
|---|---|---|---|---|---|---|
|Paddock |||||||
|W2 |POLYGON ((175.49454 -40.34541, 175.49481 -40.3... |2.84 |2024 |0 |-40.35 |175.50|
|W2 |POLYGON ((175.49454 -40.34541, 175.49481 -40.3... |2.84 |2043 |1 |-40.35 |175.50|
|W2 |POLYGON ((175.49454 -40.34541, 175.49481 -40.3... |2.84 |2062 |2 |-40.35 |175.50|
|W2 |POLYGON ((175.49454 -40.34541, 175.49481 -40.3... |2.84 |2081 |3 |-40.35 |175.50|
|W2 |POLYGON ((175.49454 -40.34541, 175.49481 -40.3... |2.84 |2101 |4 |-40.35 |175.50|

gpd_farm looks like this:

Paddock 	geometry
0 	W1 	POLYGON ((175.49382 -40.34594, 175.49415 -40.3...
1 	W2 	POLYGON ((175.49454 -40.34541, 175.49481 -40.3...
2 	W3 	POLYGON ((175.49544 -40.34480, 175.49567 -40.3...
3 	W4 	POLYGON ((175.49627 -40.34416, 175.49647 -40.3...
4 	W5 	POLYGON ((175.49685 -40.34373, 175.49701 -40.3...

Any ideas? Help is much appreciated