Animation combining Scattermapbox and datashader (animating from layout?)

I am generating a set of Datashader images used as layers and I would like to make an animation out of it. The data are stored in a xarray.Dataset, I generate a density map for each timestep and I would like to play it. I have tried to follow the different examples online but none seem to produce a result. here part of the code

from datetime import timedelta, datetime
import numpy as np
import xarray as xr
import datashader as DS
import plotly.graph_objects as go
from colorcet import fire
from datashader import transfer_functions as tf
from pyproj import Proj

## Control the resolution of the images in degrees for Scattermapbox
p=Proj("epsg:27700", preserve_units=False)
resolution=[0.0001*3,0.0001*10,0.0001*50,0.0001*100]#30,100,500,1000m
resolution_M=[20,30,50,100]
center_lat,center_lon=55.8,-5.38

x,y=p(center_lon,center_lat)
res_h,res_v=[],[]
for res in resolution_M:
    lon,lat=(p(x+res,y+res, inverse=True))
    res_h.append(lon-center_lon)
    res_v.append(lat-center_lat)

def mk_frame(subds,res_h,res_v,resolution_M,r=-1):
    '''
    compute each frame with datashader
    '''    
    subds=subds.where(np.logical_and(subds.lat<90,subds.lon<180), drop=True) #filter netcdf NaN
    V_arc,H_arc=subds.lat.max()-subds.lat.values.min(),subds.lon.values.max()-subds.lon.values.min()
    
    #compute the canvas
    cvs = DS.Canvas(plot_width=int(H_arc//res_h[r]), \
                plot_height=int(V_arc//res_v[r]))

    agg = cvs.points(subds, x='lon',
                y='lat',
                agg=DS.count('mass'))
    coords_lat, coords_lon = agg.coords['lat'].values, agg.coords['lon'].values
    coordinates=[[coords_lon[0], coords_lat[0]],
                       [coords_lon[-1], coords_lat[0]],
                       [coords_lon[-1], coords_lat[-1]],
                       [coords_lon[0], coords_lat[-1]]]
    return tf.shade(agg, cmap=fire, span=[0,1],how='linear').to_pil(), agg.values.max(), agg.values.min(), coordinates

## make a fake dataset
T=25
start_time,end_time=datetime(year=2022, month=1,day=1),datetime(year=2022, month=1,day=T)
pts=5000
ds= xr.Dataset({
     'time': ('time', np.arange(start_time,end_time, timedelta(days=1)),
     'trajectory': ('trajectory', np.arange(pts)),
     'lat': (('time','trajectory'), center_lat +(2*np.random.rand(T,5000)-1)),
     'lon': (('time','trajectory'), center_lon +(2*np.random.rand(T,5000)-1)),
     'mass': (('time','trajectory'), np.random.rand(T,5000))),
})
# init fig dictionary
fig_dic={
    'data':[{'type':'scattermapbox'},{'type':'scatter',
                   'x':[None], 'y':[None],'marker':go.scatter.Marker(
                        colorscale=fire,
                        cmax=1,
                        cmin=0,
                        showscale=True,
                        ),
                    'showlegend':False},],
    'layout':dict(
            height=800,
            width=640,
            hovermode='closest',
            showlegend=False,
            coloraxis_colorbar=dict(title='ng/L'),
            mapbox=dict(
                bearing=0,
                center=dict(
                    lat=center_lat,
                    lon=center_lon,
                ),
                pitch=0,
                zoom=8.5,
                style="carto-darkmatter",
            )),
    'frame':[]
    }
                
#buttons
fig_dic["layout"]["updatemenus"] = [
    {
        "buttons": [
            {
                "args": [None, {"frame": {"duration": 500, "redraw": False},
                                "fromcurrent": True, "transition": {"duration": 300,
                                                                    "easing": "quadratic-in-out"}}],
                "label": "Play",
                "method": "animate"
            },
            {
                "args": [[None], {"frame": {"duration": 0, "redraw": False},
                                  "mode": "immediate",
                                  "transition": {"duration": 0}}],
                "label": "Pause",
                "method": "animate"
            }
        ],
        "direction": "left",
        "pad": {"r": 10, "t": 87},
        "showactive": False,
        "type": "buttons",
        "x": 0.1,
        "xanchor": "right",
        "y": 0,
        "yanchor": "top"
    }
]
#slider
sliders_dict = {
    "active": 0,
    "yanchor": "top",
    "xanchor": "left",
    "currentvalue": {
        "font": {"size": 20},
        "prefix": "date:",
        "visible": True,
        "xanchor": "right"
    },
    "transition": {"duration": 300, "easing": "cubic-in-out"},
    "pad": {"b": 10, "t": 50},
    "len": 0.9,
    "x": 0.1,
    "y": 0,
    "steps": []
}


# make frames

for time in ds.time.values:
    img,cmax,cmin, coordinates=mk_frame(ds.loc[dict(time=time)].drop('time'),res_h,res_v,resolution_M,r=-1)
    frame={'layout':{'mapbox':
                        dict(layers=[
                        {
                            "below": 'traces',
                            "sourcetype": "image",
                            "source": img,
                            "coordinates": coordinates
                        }])
                    }
          }
    
    # slider steps
    slider_step = {"args": [
        [time],
        {"frame": {"duration": 300, "redraw": False},
         "mode": "immediate",
         "transition": {"duration": 300}}],
        "label": datetime.utcfromtimestamp(time.astype(int)*1e-9).strftime('%H:%M-%d/%m/%y'),
        "method": "animate"}
    sliders_dict["steps"].append(slider_step)
    
fig_dic["layout"]["sliders"] = [sliders_dict]
    
fig2=go.Figure(fig_dic)

Thanks

@boorhin
Looking at your code I noticed that:

  • the frames is missing the name. Each frame must have a name in order to connect it by name with the corresponding slider step
  • Your frames are not appended to a list of frames
  • you did not pass the list of frames to the fig

I created an example of scattermapbox and animated the layout. Each frame updates the layout.mapbox.center lat. It’s just a simple example to illustrate how the animation must be defined.

import plotly.graph_objects as go

colors = ['#800080', '#FF7F50', '#DC143C', '#FFD700', '#D2691E', '#008B8B', '#4169E1']
fig= go.Figure(go.Scattermapbox(
        lat=[45.5017, 45.4215, 46.8139, 46.3430,  43.6532, 43.6532  ],  
        lon=[-73.5673, -75.6972, -71.2080,  -72.5421,-79.3832, -79.8711 ], 
        mode='markers',
        marker_size=[40, 18, 35, 23,   27, 30, 14],   
        marker_color= colors
                    
    ))
clat_init=38

fig.update_layout(
    autosize=True,
    hovermode='closest',
 
    mapbox =dict(
        bearing=0,
        #accesstoken=mapboxt,
        center=dict(
            lat=clat_init,
            lon=-73
        ),
        style='carto-positron',
        pitch=0,
        zoom=5))

frames = []
nframes=15
for k in range(nframes):
    frames.append(go.Frame(layout = dict(mapbox=dict(center_lat=clat_init+k)),
                           name=f"{k}")) #it's very IMPORTANT to give a name to each frame
                                         #to connect it by name with the slider_step
fig.update(frames=frames) 

def frame_args(fr_duration, tr_duration=0):
    return dict(
            frame_duration=fr_duration,
            mode= "immediate",
            fromcurrent= True,
            transition_duration= tr_duration
    )
###############
sliders = [
            dict(
                pad=dict(b= 10, t= 60),
                len= 0.9,
                x= 0.1,
                y= 0,
                steps= [
                    dict(
                        args= [[fr.name], frame_args(fr_duration=100)],
                        label= f"{k}",
                        method= "animate",
                    )for k, fr in enumerate(fig.frames)
                ]
            )
        ]
fig.update_layout(updatemenus = [
            dict(
                buttons=[
                    dict(
                        args= [None,
                               frame_args(fr_duration=100)],
                        label= "&#9654;", # play symbol
                        method= "animate",
                    ),
                ],
                direction= "left",
                pad = dict(r= 10, t= 70),
                type= "buttons",
                x= 0.1,
                y= 0,
                active=0
            )
         ],
         sliders=sliders)

Following this example, you should define your frames correspondingly and assign a name to each one, to be used in the slider step definition, too.
I suggest to assign the name as in my example, but in your case k, from name=f"{k}" appears as follows:

frames=[]
for k, time in enumerate(ds.time.values):
    img,cmax,cmin, coordinates=mk_frame(ds.loc[dict(time=time)].drop('time'),res_h,res_v,resolution_M,r=-1)
   frames.append()

My animation works, but I don’t know if your special case that involve datashader will work, too.

1 Like

thanks a lot, very useful. I lost the append of frame in the simplification of the example.
I have an error message called failed to initialize webGL. Which could be the result of another issue

I got it to work on firefox but not on chromium