How to make an animation frame update data and layout simultaneously

Hi there,

I’m using plotly to create an interactive timeline and have run into the following issue:

When using animation frames similar to the example here I am unable to update both the data and the layout (i.e. xaxis range) in the same frame.

I know that the data in the frame is correct because if I pause the visual it will show the correct data and axis range. However, when I click play again, the data begins to update while the layout axis range does not.

I have attached my code below, any help would be appreciated as I have been pulling my hair out for days trying to figure this out.

figure = {

    'data':[{'x':[df['Date']], 'y':[df['High']], 'mode':'lines'}],

    'layout':{
        'title':'blah',
        'font' : {'family' : 'Courier New, monospace', 'color' : '#FFF'},

        'plot_bgcolor' : '#000',
        'paper_bgcolor' : '#000',

        'hovermode' : 'closest',
        'hoverdistance' : 50,
        'dragmode' : 'pan',

        'sliders': {
            'args' : ['transition', {'duration':0, 'easing':'linear'}],
            'initialValue':'1/1/2015',
            'plotlycommand':'animate',
            'values': df['Date'],
            'visible':True
        },

        'updatemenus' : [{
            'buttons': [
                {
                    'args' : [None, {'frame': {'duration':1, 'redraw': False},
                                'fromcurrent':True, 'transition':{'duration':0, 'easing':'linear'}}],
                    'label':'Play',
                    'method':'animate'
                },
                {
                    'args' : [[None], {'frame': {'duration':0, 'redraw': False},
                                'mode':'immediate', 'transition':{'duration':0}}],
                    'label':'Pause',
                    'method':'animate'
                }
            ],
            'direction': 'left',
            'showactive':False,
            'type': 'buttons',
            'xanchor':'right',
            'yanchor':'top',
            'x':0,
            'y':0
        }],

        'xaxis' : {
            'title' : 'Date',
            'titlefont' : {'color' : 'white'},
            'tickcolor' : '#FFF',
            'tickfont' : {'color':'white'},
            'showgrid' : False,
            'zeroline' : False,
            'type' : 'date'
        },

        'yaxis' : {
            'title' : 'blah',
            'titlefont' : {'color':'white'},
            'tickcolor' : '#FFF',
            'tickfont' : {'color':'white'},
            'range':[-5,5]
        }
    },

    'frames':[]
}

sliders_dict = {
    'active': 0,
    'yanchor': 'top',
    'xanchor': 'left',
    'currentvalue': {
        'font': {'size': 20},
        'prefix': 'Date:',
        'visible': True,
        'xanchor': 'right'
    },
    'transition': {'duration': 0, 'easing': 'linear'},
    'pad': {'b': 10, 't': 50},
    'len': 0.9,
    'x': 0.1,
    'y': 0,
    'steps': []
}

for ii in range(len(df)):

    if ii < 90:
        days = ii
    else:
        days = 90

    data_by_month = df.iloc[ii-days:ii]

    data_dict = {
        'x' : list(data_by_month['Date']),
        'y' : list(data_by_month['High']),
        'mode':'lines',
        'fillcolor':'#000',
        'marker': {'color':'#FFF'},
        'text':'Shooting Incident'
    }

    frame = {'data':[data_dict], 'layout':{'xaxis': {'range':[pd.to_datetime(df['Date'][ii-(days)]), pd.to_datetime(df['Date'][ii] + pd.Timedelta(days=45))], 'type':'date', 'autorange':False}}, 'name':str(df['Date'][ii])}

    figure['frames'].append(frame)

    slider_step = {
        'args': [[str(df['Date'][ii])], {'frame': {'duration':0, 'redraw':False},
        'mode':'immediate', 'transition': {'duration':0}}],
        'label':str(df['Date'][ii]),
        'method':'animate'
    }
    sliders_dict['steps'].append(slider_step)

figure['layout']['sliders'] = [sliders_dict]
1 Like

Hi @jbent15,

Could you include some sample data for the df variable?

-Jon

1 Like

Hi Jon,

Oops, I forgot to include the code that generates df. Here it is:

peak = False
lineData=[]
for ii in range(1097):
    if peak:
        lineData.append(0)
        peak = False
    else:
        det = np.random.rand()
        if (det > .75):
            lineData.append(np.random.randint(-5,5))
            peak = True
        else:
            lineData.append(0)

df = pd.DataFrame(data={'Date':pd.date_range(start='1/1/2015', end='1/1/2018'), 'High':lineData})

Hi @jbent15,

It does look like there is some kind of bug going on here. I noticed that when I move the slider around using the mouse everything seems to update alright. And if I hit β€œPlay” after moving the slider around it also seems to work properly. But when the animation starts by itself things get messed up.

Another experiment I tried was disabling autoplay in iplot using the auto_play=False argument to plotly.offline.iplot. When I do this in the classic notebook, and then press play to start the animation manually, everything seems to work alright. So as far as I can tell it has something to do with starting the animation immediately.

I don’t have time right now, but it would be helpful to simplify the example a bit so that we can try to reproduce it in Plotly.js directly. The idea would be to remove all of the extra styling info, and trim it down to 5-10 frames. If you’re interested in simplifying it a bit I could convert it to JavaScript and create the Plotly.js issue.

Thanks,
-Jon

Hi @jmmease,

After following along with what you tried I’m still unable to get the functionality that I want. I’m trying to get both the data and the x-axis range to move when I click play. Currently, If i move the slider around manually I can see that both the data and layout update, and that means that the individual frames do contain the correct information. However, when I click play at any point, the data begins to update but the layout does not, as the x-axis does not change to follow the data. I hypothesize that this is a bug since the layout information in the frame is correct but it does not get rendered when the frames are being played.

Some other things to note: I am using jupyter notebook to run the code, I am using plotly.offline for the plot, and I am running plotly version 3.7.1

If you think that this is not a bug but rather poor coding on my part, I would be happy to try to simplify the example in python and pass that on to you.

Let me know what the best way to approach this would be.

Thanks,
Joey

Hi @jbent15,

Yes, I expect there is probably a bug going on here. So the best way forward would be to make a simple example that shows the problem that we could show the plotly.js team. Thanks!

-Jon

I have a similar problem using plotly via R shiny where I animate the data and then the axis, but the axis then reverts to the original scale for some reason. I have posted this on the plotly gihub, but thought I’d add it here also.

library(shiny)
library(plotly)
library(dplyr)

ui <- fluidPage(
	plotlyOutput("plot")
)

gendata <- function(){
	cat("gendata\n")
	ndata <- 10
	d <- tibble(text=LETTERS[1:ndata], f=1, x=runif(ndata)) %>% mutate(r = rank(x))
	rbind(mutate(d, x=-1), d, mutate(d, x=-1)) %>%
		arrange(text)
}

server <- function(input, output, session){

	origdata <- gendata()

	axrange <- function(x){
		c(-0.1*runif(1), 1+0.1*runif(1))
	}

	my <- reactiveValues(
		data = origdata,
		xrange = c(-0.1, 1.1)
		)

	speed = 1000 # redraw interval in milliseconds

	output$plot <- renderPlotly({
		isolate({
			cat("renderPlotly\n")
			plot_ly() %>%
				add_trace(x=my$data$x, y=my$data$r, frame=my$data$f, line=list(width=20, simplify=FALSE), opacity=0.3, color=I("orange"), type="scatter", mode="lines", name="Rank") %>%
				add_trace(x=my$data$x + 0.02, y=my$data$r, frame=my$data$f, text=my$data$text, type="scatter", mode="text", showlegend=FALSE) %>%
				layout(xaxis=list(range=my$xrange)) %>%
				animation_opts(frame=speed, transition=speed, redraw=FALSE, mode="afterall")
		})
	})

	proxy <- plotlyProxy("plot", session=session, deferUntilFlush=FALSE)

	# https://shiny.rstudio.com/reference/shiny/0.14/reactiveTimer.html
	autoInvalidate <- reactiveTimer(speed)

	observe({
		autoInvalidate()
	})

	observeEvent(autoInvalidate(), {
		# req(NULL)
		cat("observeEvent autoInvalidate()\n")
		my$data <- gendata() %>% mutate(f = my$data$f + 1)
		# print(head(my$data))
		# https://plot.ly/javascript/plotlyjs-function-reference/#plotlyanimate
		data <- list(list(
			x = my$data$x,
			y = my$data$r,
			line=list(width=20, simplify=FALSE, color=sample(c(I("red"), I("green"), I("blue")), 1)),
			frame = my$data$f
		),
		list(
			x = my$data$x + 0.02,
			y = my$data$r,
			text = my$data$text,
			frame = my$data$f
		))
		cat("animate scatter\n")
		plotlyProxyInvoke(proxy, "animate",
						  # frameOrGroupNameOrFrameList
						  list(
						  	data = data,
						  	traces = as.list(as.integer(0:1))
						  	# ,
						  	# layout = list()
						  ),
						  # animationAttributes
						  list(
						  	frame=as.list(rep(list(duration=speed), length(0:1))),
						  	transition=as.list(rep(list(duration=speed), length(0:1)))
						  )
		)# plotlyProxyInvoke
		my$data$f <- my$data$f + 1
		my$xrange <- axrange(my$data$x)
		data <- list(list(
			frame = my$data$f
		),
		list(
			frame = my$data$f
		))
		cat("animate axis\n")
		plotlyProxyInvoke(proxy, "animate",
						  # frameOrGroupNameOrFrameList
						  list(
						  	# data = data,
						  	# traces = as.list(as.integer(0:1)),
						  	layout = list(xaxis=list(range=my$xrange))
						  ),
						  # animationAttributes
						  list(
						  	frame=list(duration=speed),
						  	transition=list(duration=speed)
						  )
		) # plotlyProxyInvoke
		# https://rdrr.io/cran/plotly/src/inst/examples/shiny/proxy_relayout/app.R
		# plotlyProxyInvoke(proxy, "relayout",
		# 				  #update
		# 				  list(xaxis=list(range=my$xrange))
		)
	}) # observeEvent

}

shinyApp(ui, server)