📣 Preserving UI State, like Zoom, in dcc.Graph with uirevision with Dash

@mbkupfer here’s what I meant with the countries:

from random import random
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

app = dash.Dash(__name__)
app.layout = html.Div([
    dcc.Dropdown(
        id='countries',
        multi=True,
        options=[
            {'label': 'France', 'value': 'France'},
            {'label': 'Germany', 'value': 'Germany'},
            {'label': 'UK', 'value': 'UK'}
        ],
        value=['France', 'Germany', 'UK']
    ),
    dcc.Graph(id='graph')
])


@app.callback(Output('graph', 'figure'), [Input('countries', 'value')])
def update_graph(countries):
    traces = [dict(
        type='bar',
        x=range(10),
        y=[random() for _ in range(10)],
        uid=country,
        name=country
    ) for country in countries]

    return dict(data=traces, layout={'legend': {'uirevision': True}})


if __name__ == '__main__':
    app.run_server(debug=True)

By using the country as uid, you can hide the 2nd or 3rd trace from the legend, then delete the first trace from the dropdown, and the same country will still be hidden in the legend. Without uid it would be the same trace index that was hidden. Note though that if you delete a hidden trace via dropdown then later bring it back, we’ll lose memory that you had previously hidden it.

As to changing tabs resetting the UI - I’m afraid that’s expected. plotly.js stores this UI state on the DOM element and when you switch to a different tab that DOM element is destroyed, only to be recreated when you come back. I suppose you could pre-render all the tabs and just hide the inactive ones, rather than removing them from the DOM; that might be bad for initial load time, though if done right (just using a multi-output on style props?) it could be good for switching performance. Other than that all I can think of is manually tracking these changes via restyleData etc… but the pain and complication associated with that is why we added uirevision in the first place!

2 Likes

I see what you mean. My main confusion came from the visible: legendonly part as I thought we had to set it, but it appears from your example that this is not the case and that it is done implicitly.

As for the tabs part, thanks for clarifying what is going on with the DOM. Would it make a difference though if I’m not using callbacks to generate my tabs, rather they are all created decoratively in the children properties of each tab. Wouldn’t they already be pre-rendered in this case, or is there more abstraction to how dcc.Tabs works?

Ah I actually hadn’t looked in detail at how we implemented dcc.Tabs but it turns out only the selected tab is rendered, so previously-visible tab contents are destroyed and removed from the DOM. Might be interesting to investigate adding a mode for this, something like Tabs.inactive_mode: 'destroy' | 'hide', but it’s not available now.

3 Likes

hey alexcjhonson, I was working with plotly in angularjs, can you help me with applying this uirevision in that?

AFAIK there’s nothing different about how this will work in Angular - provide an unchanging (but truthy) uirevision if you want user-initiated changes to persist across redraws, and a different value when you want to reset them. But I haven’t used it in Angular myself. Is there a specific problem you’re encountering?

I am not able to find how to apply uirevision in angular, it is all in react. I am a beginner and having a hard time preserving the state of the graph

Are you using angular-plotly.js? That wrapper uses the Plotly.react method to update a plot, so it should work fine with uirevision. The react method doesn’t have anything to do with the React framework other than sharing a name.

For the most part, all you need to do is set the uirevision attribute of the plot’s layout. Set it to something constant and truthy, and you should see user-initiated zoom/pan etc preserved when you update the plot. Once you have that working, if you need more fine-grained control you can explore changing layout.uirevision and/or using the derived versions like layout.yaxis.uirevision etc.

@alexcjohnson i am trying to plot a graph where the data points for the graph are fetched from database at a interval of 2 second and that are updated on graph, so that we can see that data point moving( data points are actually the lat,lon of cars) making it near to real time.
The problem is that everytime when the data points are updates the window is also refreshed, means the window is refreshed at every 2 second and hence we can’t zoom, pan the window as it will go to the same state after 2 second.
Is there a way in which the UI state is preserved even when the data points are getting updated?
i tried uirevision, but it’s not working.

@vararp that’s exactly the kind of situation uirevision is meant for - you should be able to pick some constant value and provide that same value as layout.uirevision on every update. If that’s not working, can you post some code so we can get to the bottom of it?

@alexcjohnson here the data is getting updated every 2 second and i want the UI state to be preserved when data getting updates, can you help me regarding this. I am new to plotly actually, so don’t have that much idea.

data = [
go.Scattermapbox(
lat = df.lat, lon = df.lon,
mode=‘markers’,
marker=dict(
size=12,
color= df.status
),
showlegend = False
)
]
layout = go.Layout(
autosize=True, #width = 1400, height = 900,
margin = {‘l’: 0,‘r’: 0,‘t’: 0,‘b’: 0 },
hovermode=‘closest’,
mapbox=dict(
accesstoken=mapbox_access_token,
bearing=0,
center=dict(
lat=17.9716,
lon=57.5946
),
pitch=0,
zoom=11
),
)
app.layout = html.Div(children=[
html.H1(‘xyz’),
# html.Div(style={ “height” : “100vh”}),
html.Div(‘’’
updates after every 2 second
‘’'),
dcc.Graph(
id=‘bike-status-graph’,
figure={
‘data’: data,
‘layout’: layout
},
animate = True,
style={‘height’: ‘85vh’}
),
dcc.Interval(
id=‘interval-component’,
interval=2*1000, # in milliseconds
n_intervals=0
)
])
@app.callback(Output(‘bike-status-graph’, ‘figure’),
[Input(‘interval-component’, ‘n_intervals’)])
def update_graph_live(n):
df = get_busy_bikes()
data = [
go.Scattermapbox(
lat = df.lat, lon = df.lon,
mode=‘markers’,
marker=dict(
size=6,
color=df.status
),
)
]
fig={
‘data’:data,
‘layout’:layout
}
return fig
if name == ‘main’:
app.run_server(host=‘0.0.0.0’, debug=True)

Try:

layout = go.Layout(
    uirevision=True,
    autosize=True,
    ...
)
1 Like

Hi,
I’m also new to Dash and also having trouble with losing the zoom when the data gets updated after an interval…

Can I please ask how I do this uirevision thing , just to stop losing the zoom ?

My code is like this:

app.layout = html.Div(children=[
    dcc.Graph(id='basic-interactions'),
    dcc.Interval(
        id='interval-component',
        interval=5 * 1000,  # in milliseconds
        n_intervals=0
    ),

So the post above is confusing as I don’t understand what is go.Layout or what is the difference??

Also I’m not sure if I’m supposed to se uirevision=True in the layout or in the return from the callback ?

Thanks in advance.

Whenever you construct a figure for a dcc.Graph, put uirevision=True in figure['layout'].

The way you have it in your post there’s no figure in the graph in app.layout, so I guess it’ll just be in the callback return.

Hi Alex,
Thanks for taking the time to try to set me straight…

I’m very new to this so I’m cobbling together code from the examples as best I can…

My callback returns the following…

graphdata = {
        'data': traces,
        'layout': go.Layout(
            yaxis={'title': '# my title'},
            legend=dict(
                    x=1,
                    y=1,
                    traceorder='normal',
                    font=dict(
                        family='sans-serif',
                        size=12,
                        color='#000'
                    ),
                    bgcolor='#E2E2E2',
                    bordercolor='#FFFFFF',
                    borderwidth=2
                ),
            hovermode='closest',

        )
    }

I note now that there is no figure in it at all… so … am I doing it wrong then?

Thanks

That all looks reasonable - the {data, layout} dict is the figure. Then I’m assuming your callback is something like:

@app.callback(
    Output('basic-interactions', 'figure'),
    [Input('interval-component', 'n_intervals']
)
def make_figure(n):
    graphdata = ...
    # set graphdata['layout']['uirevision'] = True
    return graphdata

Great feature!

I noticed that in order to preserve the “view” between callbacks the user MUST update the figure or else the “view” will be reset no matter what uirevision is set to. Using chriddyp’s example above, if I change the Reference from Display to Hide without zooming, etc, the “view” still resets. Is that by design or is there some way to prevent the “view” reset without necessarily updating the figure?

Hello
I have got the code you can find it below, the data generate b y random function and i can see visually a live trend, the aim is that, user can zoom in and finnd out the details, i have used Uirevision, but it does not work. can you help me how to fix it.

app.layout = html.Div([
dcc.Graph(id=‘live-graph’, animate=False,
figure=go.Figure(
data=[
go.Scatter(

x=data_DB[‘time’],

y=data_DB[‘value’],

                  )
              ],
              layout=go.Layout(
                  xaxis={#'range': ['13:00:00', '14:00:00'],
                         'autorange': plot_data['Auto_scale'],
                         'tickmode': 'auto',
                         'nticks': 4,
                         },
                  yaxis={#'range': [0 , 1],
                         'autorange': plot_data['Auto_scale'],
                         'tickmode': 'auto',
                         'nticks': 3,
                         },
                         uirevision= 'refresh'
              )
          )

          ),
dcc.Interval(
    id='graph-update',
    interval=2 * 1000,
    n_intervals=0
),
html.Label('Dropdown'),
dcc.Dropdown(id='Dropdown-list',
             options=list(Drop_down_option),
             value='',
             multi=True
             ),
html.Button('Refresh', id='refresh'),

], style={‘columnCount’: 1})

@app.callback(dash.dependencies.Output(‘refresh’, ‘n_clicks’), # update data from OPC to Plot data
[dash.dependencies.Input(‘Dropdown-list’, ‘value’),
dash.dependencies.Input(‘graph-update’, ‘n_intervals’)
],
[dash.dependencies.State(‘refresh’, ‘n_clicks’)],
)
def data_update(names,n,state):
sim_add_DB()
if plot_data[‘names’]==names:
if plot_data[‘Live_view’]:
print(‘add data’)
sim_add_live() # change to function that would add data from OPC --------------------------------------
return 1
else:
if len(names) > 0:
print(‘new data’)
plot_data[‘names’] = names
if plot_data[‘Live_view’]:
a=1
# later add function that would add data from data base that other tags have --------------------------
return 1
else:
sim_data_base() # change to function that would read data from data base ------------------------------------------------
return 1
else:
plot_data[‘names’] = []
a=2
return state

-------------------------------------------------------------------------------------------------------

@app.callback( #redraw graph
Output(‘live-graph’, ‘figure’),
[Input(‘refresh’, ‘n_clicks’)],
[State(‘live-graph’, ‘figure’)]
)
def update_graph(n_clicks,fig,refresh):
figure = go.layout{
data=[
go.Scatter(
x=[0],
y=[0]
)
],
Layout_plot = #go.Layout(
xaxis={‘autorange’: plot_data[‘Auto_scale’],
‘tickmode’: ‘auto’,
‘nticks’: 9,
},
yaxis={‘autorange’: plot_data[‘Auto_scale’],
‘tickmode’: ‘auto’,
‘nticks’: 5,
},
uirevision= refresh
)}

if n_clicks != None:
    print('refreshed')
    i = 0
    if len(plot_data['names']) > 0:
        while i < len(plot_data['names']):

            data_name = 'Data' + str(i)
            if (i == 0):
                figure = go.Figure(
                    data=[
                        go.Scatter(
                            x=list(plot_data[data_name]['time']),
                            y=list(plot_data[data_name]['value']),
                            name = plot_data['names'][i]
                        )
                    ],)

            elif plot_data['names'][i] != []:
                figure.add_trace(go.Scatter(
                    x=list(plot_data[data_name]['time']),
                    y=list(plot_data[data_name]['value']),
                    name=plot_data['names'][i]
                ))
            i = i + 1
        a=1

return figure

return {
    'data' : figure['data'],
    'Layout' :Layout_plot
}

if name == ‘main’:
#app.run_server(debug=True)
server.run()

So…it’s been a while since the original post. Is there a reason this isn’t documented in the primary docs? I’ve updated to following versions:

dash==1.6.0
dash-core-components==1.5.0
dash-daq==0.2.2
dash-html-components==1.0.1
dash-renderer==1.2.0
dash-table==4.5.0

Still seems that the uirevision argument isn’t recognized:

Failed component prop type: Invalid component prop `figure` key `uirevision` supplied to Graph.

Any thoughts on what could fix this?

uirevision should be in figure.layout, not figure

Hi there, I have only seen UI revision being implemented in Python. Is it possible to do the same in R? If that is the case, some suggestions would be greatly appreciated :slight_smile: Thanks.