šŸ“£ Preserving UI State, like Zoom, in dcc.Graph with uirevision with Dash

Yep, uirevision is implemented entirely on the javascript side, so it’ll work just as well from R as from Python. Just add uirevision=TRUE to your graph’s figure$layout.

1 Like

Thank you :slight_smile:

If I have a figure with a go.Heatmap trace plus some other overlayed traces, how can I make the heatmap always visible while still allowing the user to toggle the visibility of the other traces via the clickable legend? The unwanted scenario is when the heatmap gets hidden when another trace gets double-clicked to show that other trace in isolation. Thanks!

Thanks for bringing this up! I’d call that behavior a bug -> https://github.com/plotly/plotly.js/issues/4389

A post was split to a new topic: Uirevision and animation slider

Hi @alexcjohnson I’m facing the same issue. I’m using uirevision to preserve the UI state in graph. I am also using dcc.Tabs… When I change the tab and come back to the original tab, the graph is getting reset to the default state. Could you please help me in this?

1 Like

@balagdivya I don’t know that we have a good solution for this use case yet but it’s come up before. There may be a way by not actually removing the graph when you switch tabs, just hiding it via CSS. But that could be really awkward depending on how many tabs you have and how complex they are. I’ve made an issue to look into a natural solution https://github.com/plotly/dash-core-components/issues/806

Hi @alexcjohnson, any update on the issue https://github.com/plotly/dash-core-components/issues/806 ??

A post was split to a new topic: Trigger callback when legend is clicked

Hey @crosenth, what do you mean that ā€œthe user MUST update the figureā€? I am having the issue where I am updating some traces of the figure, but the view still resets to where it was when I triggered the callback the first time the callback was called.

Hi @jvgomez, what I meant to say is the user must ā€œinteractā€ the figure in order to preserve . This feature is meant to preserve the user’s interactions. If you are adding traces to the plot it will reset unless you also interact (Zoom in, out, autoscale, etc).

There used to be a way to preserve a plots’ axes without interaction as described in this Issue (https://github.com/plotly/dash-core-components/issues/321) but I suspect it was accidentally squashed by a PR back in 2018.

@crosenth Not sure I follow. I have the case for example that I manually move the camera and click on the figure that triggers a callback that modifies one of the existing traces. So far so good.

Then I move the camera again, I click again on the figure to trigger the callback, and the camera resets to the prose it had on the previous click. So I understand user interactions are not preserved. Is this supposed to be fixed with uirevision? Because i try and didn’t work.

@jvgomez - That should work, but maybe there is an issue with 3D interactions. Any chance you create a really simple, small, reproducible example with dummy data? That’ll help us create the bug report.

@crosenth I tried to reproduce the issue with the following code which represents my real application. Basically, a callback creates a figure and updates it. This callback is triggered when we click in a point on the figure and updates one of the traces to highlight the point clicked.

import json

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

import plotly
import plotly.graph_objects as go


external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
data = [1,2,3,4,5]

# Layout
app.layout = html.Div([
    # Invisible div for data caching
    html.Div(id='point-data', style={'display': 'none'}),

    # 3D  graph div
    html.Div([
        dcc.Graph(id='3d-graph', style={'height': '60vh'})
    ]),
])

# Callbacks
@app.callback(Output('3d-graph', 'figure'),
              [Input('point-data', 'children')],
              [State('3d-graph', 'figure')])
def on_figure_update(cache_data, fig):
    # On start, create the figure with a placeholder trace for future updates
    if not fig:
        trace1 = go.Scatter3d(x=data, y=data, z=data,
                            mode="markers", marker=dict(size=3, color='blue'),
                            name="trace1")
        trace2 = go.Scatter3d(x=[], y=[], z=[],
                            mode="markers", marker=dict(size=5, color='red'),
                            name="trace2")

        # This is causing the camera issue!
        scene = dict(camera=dict(
                                up=dict(x=0, y=-1, z=0),
                                center=dict(x=0, y=0, z=0),
                                eye=dict(x=1, y=-1, z=1))
            )
        layout = go.Layout(scene=scene)
        fig = go.Figure(data=[trace1, trace2], layout=layout)
        fig['layout']['uirevision'] = 'Do not change'
    else:
        # Update figure
        if not cache_data:
            return fig
        idx = json.loads(cache_data)
        fig['data'][1]['x'] = [data[idx]]
        fig['data'][1]['y'] = [data[idx]]
        fig['data'][1]['z'] = [data[idx]]

    return fig


# Clicking on the 3D map
@app.callback(Output('point-data', 'children'),
              [Input('3d-graph', 'clickData')])
def on_3d_click(clickData):
    if clickData is None:
        return ''

    return json.dumps(clickData['points'][0]['pointNumber'])

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

So I realized that the default camera is causing the issue. The code above produces strange behavior of resetting camera to a previous position (I think it is the camera on the first callback), as shown in the image:
optimized

If you remove the custom layout when creating the figure, the behavior seems to be mostly OK. There are still small issues such as the first time the figure updates the camera returns to the initial position, or when zoom is changed, the camera readjusts it a bit when updated:

optimized

For my application the camera change is relatively important, any suggestion on how to deal with it?

Thanks!

1 Like

Hello everyone,

I am having an issue with the uirevision parameter in my live chart. I believe I am using it correctly however I cannot maintain zoom or panning. every other second the UI resets back to the default and I lose zoom and chart position. Any suggestions are much appreciated. My end goal is to have a live candlestick chart similar to trading view :slight_smile:
thank you

import dash
from dash.dependencies import Output, Input
import dash_core_components as dcc
import dash_html_components as html
import plotly
import plotly.graph_objs as go
from collections import deque
from binance.client import Client
from binance.websockets import BinanceSocketManager
import time
from datetime import datetime
import pandas as pd


data = pd.DataFrame(columns=['Open', "High", "Low", "Close", 'Date'])
y_candle = []
index = 0

app = dash.Dash(__name__)
app.layout = html.Div([dcc.Graph(id='live-graph', animate=True, style={'height': '1080px'}),
                       dcc.Interval(id='graph-update', interval=1000, n_intervals=0)])


@app.callback(Output('live-graph', 'figure'),
              [Input('graph-update', 'n_intervals')])


def update_graph_scatter(n):

    candle_graph = go.Candlestick(
        x=data['Date'],
        open=data['Open'],
        high=data['High'],
        low=data['Low'],
        close=data['Close'],
        name='candles')


    return dict(data=[candle_graph], layout=go.Layout(title='BTC/USDT',
                                                      showlegend=True, xaxis=dict(range=[data["Date"].iloc[0], data["Date"].iloc[-1]]),
                                                      yaxis=dict(range=[min(data["Low"]), max(data["High"])]), autosize=True,
                                                      uirevision=True, template="plotly_dark"))


def handle_message(msg):
    global index
    timestamp = msg['T'] / 1000
    new_time = datetime.fromtimestamp(timestamp)
    new_time.strftime('%Y-%m-%d %H:%M:%S')

    y_candle.append(msg['p'])




    if (len(y_candle) % 100 == 0) and (len(y_candle) > 0):
        temp = {}
        temp['Open'] = y_candle[0]
        temp['High'] = max(y_candle)
        temp['Low'] = min(y_candle)
        temp['Close'] = y_candle[-1]
        temp['Date'] = new_time
        data.loc[index] = temp
        index += 1

        #df.append(pd.Series(temp), ignore_index=True)
        #print(df)
        y_candle.clear()




if __name__ == '__main__':


    bm = BinanceSocketManager(client)
    conn_key = bm.start_trade_socket('BTCUSDT', handle_message)


    bm.start()
    time.sleep(10)
    app.run_server(debug=True)
    #
    #
    # bm.stop_socket(conn_key)



I believe this is a bug in Plotly.js, or a combination of multiple bugs. I’ve logged them as https://github.com/plotly/plotly.js/issues/5050 and https://github.com/plotly/plotly.js/issues/5005 and https://github.com/plotly/plotly.js/issues/5004

1 Like

Ah ok I appreciate the feedback.

Thank you

A post was split to a new topic: Vue & uirevision

Hi Everyone,

I am building a streaming application. So, every 5 minutes my data is refreshed in the database and I am pulling the updated data from the database in the dash application. So, my customer wants to update the data tables but not the graphs because they will be seeing a trend if there are abnormal behaviour in the lines.

I have tried using uirevision but it not working. Can someone help in this? Thanks in advance

#call back for All Plating Lines

@app.callback(
    dash.dependencies.Output('table', 'children'),
    [dash.dependencies.Input('lineSelect', 'value'),
    dash.dependencies.Input('refresh-table', 'n_intervals'),])

def filter_table(input,num):
    if num == 99999999999:
        raise PreventUpdate
    else:
        print ("The code is inside the function")
        df2 = pd.read_sql_query(query_3, engine)
        df2['location'] = df2['location'].replace(regex='AmpsActual',value='')
        df2['line'] = np.where(df2.location.str[3] == '1' ,df2.line+' 1',df2.line)
        df2['line'] = np.where(df2.location.str[3] == '2' ,df2.line+' 2',df2.line)

        tableData = df2.pivot_table('alarmflag',['line','linespeed'],'location',aggfunc = 'first')
        tableData = pd.DataFrame(tableData.to_records())
        tableData.columns = [hdr.replace("('alarmflag',","").replace(")","") for hdr in tableData.columns]

        td1 = tableData
        td1['linespeed'] = td1['linespeed'].round(1)
        td1['linespeed'] = td1['linespeed'].astype(str)


        for j in td1.columns:
            if j not in ['line','linespeed']:
                td1[j] = j + td1[j]

        sort0_cols = td1.columns[td1.columns.str.contains(pat="EC")  ]
        sort1_cols = td1.columns[td1.columns.str.contains(pat="Ni")  ]
        sort2_cols = td1.columns[td1.columns.str.contains(pat="Ag")  ]
        sort3_cols = td1.columns[td1.columns.str.contains(pat="Sn")  ]
        sort4_cols = td1.columns[(~td1.columns.str.contains(pat="EC") & ~td1.columns.str.contains(pat="Ni") &
                                  ~td1.columns.str.contains(pat="Ag") & ~td1.columns.str.contains(pat="Sn") &
                                  ~td1.columns.str.contains(pat="line") ) ]
        td1 = pd.concat([td1['line'],td1['linespeed'],td1[sort0_cols],td1[sort1_cols],td1[sort2_cols],td1[sort3_cols],td1[sort4_cols]],axis=1)
        td1['new'] = td1.apply(lambda x: ','.join(x.dropna()),axis=1)
        td2 = td1['new'].apply(lambda x:pd.Series(x.split(',')))
        td2.rename(columns = {0:'line'},inplace=True)

        df = pd.DataFrame(data = td2)
        dfFiltered = pd.DataFrame()
        temp = input

        dfFiltered = df[df.line.isin(temp)]

        return html.Div([
            dash_table.DataTable(
                id='tab',
                columns = [{"name": i, "id": i} for i in dfFiltered.columns],
                data = dfFiltered.to_dict('records'),

                style_cell={'maxWidth': 0,
                            'overflow': 'hidden',
                            'textOverflow': 'ellipsis',
                            'textAlign': 'center',
                            'font-family': 'Verdana',
                            'margin':'10px',
                            'font-weight': 'bold',
                            },
                style_header = {'display': 'none'},
                style_data_conditional = ([{'if':{'filter_query': '{{{col}}} is blank'.format(col=col),
                                                  'column_id' : col},
                                            'backgroundColor': 'white',
                                            'color': 'white' } for col in dfFiltered.columns] +
                                          [{'if':{'filter_query': '{{{col}}} contains "."'.format(col=col),
                                                  'column_id' : col},
                                            'backgroundColor': '#CC0000',
                                            'color': 'white' } for col in dfFiltered.columns] +
                                          [{'if':{'filter_query': '{{{col}}} contains "`"'.format(col=col),
                                                  'column_id' : col},
                                            'backgroundColor': 'white',
                                            'color': 'green' } for col in dfFiltered.columns] +
                                          [{'if':{'filter_query': '{{{col}}} contains " "'.format(col=col),
                                                  'column_id' : col},
                                            'backgroundColor': '#D3D3D3',
                                            'color': 'black' } for col in dfFiltered.columns]
                                           +  [{'if':{
                                                   'column_id' : 1},
                                             'backgroundColor': '#D3D3D3',
                                             'color': 'black' }]
                                          ),
                )
            ,   dcc.Graph(id="graph3", style={"width": "50%", "display": "inline-block"}),
            dcc.Graph(id="graph4", style={"width": "50%", "display": "inline-block"}),])


#call back for selection on All Plating Lines

@app.callback(
    [Output('graph3', 'figure'),Output('graph4', 'figure')],
    [Input('tab', 'derived_virtual_data'),Input('tab', 'active_cell')])

def show_graph(rows, selection):

    dff = pd.DataFrame(rows)

    if selection is None:
        n_intervals = 0
        return {}, {}
    else:
        n_intervals = 99999999999
        loc = dff.iloc[selection['row']][selection['column']-1]
        loc = loc.replace('.','').replace('`','').replace(' ','').replace('AmpsActual','')
        input1 = loc + 'AmpsActual'
        t1 = dff.iloc[selection['row']]['line']
        input2 = t2 = t1[:3]
        t1 = 'USMLXLIUP_' + t2

        connection_string_details()
        cursor = cnxn_1.cursor()

        query_4 = """Select COLUMN_NAME from information_schema.columns where TABLE_NAME = '{}'
        and TABLE_SCHEMA = 'ppqa' and
        (COLUMN_NAME like '{}%' or COLUMN_NAME = 'DateTime' )
        order by ORDINAL_POSITION"""

        table_columns = pd.read_sql_query(query_4.format(t1,loc), cnxn_1)



        col_list = [i for i in table_columns['COLUMN_NAME']]
        col_string = ', '.join([str(i) for i in col_list])

        query_5 = """SELECT {},  '{}' as line, NOW() as lastrefreshdate
                FROM ppqa.{}
                where DateTime >= now() - interval 24 hour
                order by DateTime"""

        df = pd.read_sql_query(query_5.format(col_string,t2,t1), cnxn_1)

        if len(df) == 0:
            return {}, {}
        else:
            df['data point i'] = df.index + 1
            filename = t2
            volt_col = input1.replace('Amp','Volt')

            tolerance_inputs = pd.read_excel("C:/Users/ISLAMS/Downloads/Work/Project Fusion/Files/"+filename +".xlsx", sheet_name = 'Sheet1')

            current_tolerance = int(tolerance_inputs["Current Tolerance for " + input1].loc[0])
            voltage_tolerance = int(tolerance_inputs["Voltage Tolerance for Standard Deviation for " + volt_col].loc[0])

            standard_dev_15_rows = df[df['data point i'] <= 15][volt_col].std()

            df['rolling_average']  = df[volt_col].rolling(15).mean()

            def func_voltage_mu_o(df):
                if df['data point i'] <= 15:
                    return (df[volt_col])
                else:
                    return (df['rolling_average'])

            df['voltage_mu_o'] = df.apply(func_voltage_mu_o, axis = 1)

            if input1.startswith("Ni"):
                Voltage_SD = 0.045
            elif input1.startswith("Sn"):
                Voltage_SD = 0.050
            elif input1.startswith("ECln"):
                Voltage_SD = 0.055
            elif input1.startswith("Ag"):
                Voltage_SD = 0.055
            else:
                Voltage_SD = 0.055

            df['voltage_sigma'] =  Voltage_SD


            fig=go.Figure()
            fig.add_trace(go.Scatter(x=df['DateTime'],y=df[loc+'AmpsSetpoint'],name = 'Current Set Point',line=dict(color='black',width=4,dash='dash')))
            fig.add_trace(go.Scatter(x=df['DateTime'],y=df[input1],name = 'Actual Current',line=dict(color='black',width=4)))
            fig.add_trace(go.Scatter(x=df['DateTime'],y=df[loc+'AmpsSetpoint']+df[loc+'AmpsSetpoint']*current_tolerance/100,name = 'Upper Limit',line=dict(color='red',width=4)))
            fig.add_trace(go.Scatter(x=df['DateTime'],y=df[loc+'AmpsSetpoint']-df[loc+'AmpsSetpoint']*current_tolerance/100,name = 'Lower Limit',line=dict(color='red',width=4)))
            fig.update_layout(title= input2+' '+input1[:5]+ ' : Current Vs Time',xaxis_title = 'Time',yaxis_title = 'Current (Amp)')
            fig['layout']['uirevision'] = 'Do not change'

            fig2=go.Figure()
            fig2.add_trace(go.Scatter(x=df['DateTime'],y=df[loc+'VoltsActual'],name = 'Actual Voltage',line=dict(color='black',width=4)))
            fig2.add_trace(go.Scatter(x=df['DateTime'],y=df['voltage_mu_o'] + df['voltage_sigma'] * voltage_tolerance,name = 'Voltage Red UCL',line=dict(color='red',width=4)))
            fig2.add_trace(go.Scatter(x=df['DateTime'],y=df['voltage_mu_o'] - df['voltage_sigma'] * voltage_tolerance,name = 'Voltage Red LCL',line=dict(color='red',width=4)))
            fig2.update_layout(title= input2+' '+input1[:5]+ ' : Voltage Vs Time',xaxis_title = 'Time',yaxis_title = 'Voltage (Volt)')
            fig2['layout']['uirevision'] = 'Do not change'

            return fig, fig2

@app.callback(
    Output('refresh-table','n_intervals'),
    Input('tab', 'active_cell'))

def stop( selection):
    if selection is None:
        n_intervals = 0
        return n_intervals
    else:
        n_intervals = 99999999999
        return n_intervals

Hi all,
I am also facing a problem with uirevision. In my application, which is based on a 3D scatter plot, I set uirevision to preserve the UI state but I found a case where it does not work: when I click on a legend trace, the camera change to a prior position.

Any idea of I can fix that?

Thank you.