0

I am struggling with Dash Clientside callbacks. I am looking to create a smooth animation, so I need the clientside callback to have the fast update rate. I have an example that seems to replicate the problem; I have a normal callback and that works as expected. When I convert the same callback to clientside, it no longer works. However, when I do a JSON.stringify to the clientside return, I see the data field updating. I do not understand the issue, though I expect it is an issue with my js. I do not know how to debug on the clientisde, so any advice for error recording would also be appreciated.

Here is the working 'normal' callback:

import dash

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



fig_test={
        'data': [
            {'x': [1, 2, 3], 'y': [4, 1, 2], 'type': 'bar', 'name': 'SF'},
            {'x': [1, 2, 3], 'y': [2, 4, 5], 'type': 'bar', 'name': u'Montréal'},
        ],
        'layout': {
            'title': 'Dash Data Visualization'
        }
    }
app = dash.Dash(__name__)
app.layout = html.Div([
    html.Button("Button 1", id="btn1"),
    dcc.Graph(id="graph", figure=fig_test),
    dcc.Slider(
             id='interval-component',
             min=0,
             max=36,
             step=1,
             value=10,
         ),
    html.Div(id="log"),
    html.Pre(
        id='structure',
        style={
            'border': 'thin lightgrey solid', 
            'overflowY': 'scroll',
            'height': '275px'
        }
    )
])

@app.callback(
    Output("graph", "figure"), 
    Input('interval-component','value'),Input("graph", "figure"),Input("btn1", "n_clicks"))
def display_structure(value, figure, btn1):
    figure['data'][0]['y'][1] = value
    return {'data': figure['data'], 'layout':figure['layout']}
app.run_server(debug=False)

Here is the same callback implemented through clientside:


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



fig_test={
        'data': [
            {'x': [1, 2, 3], 'y': [4, 1, 2], 'type': 'bar', 'name': 'SF'},
            {'x': [1, 2, 3], 'y': [2, 4, 5], 'type': 'bar', 'name': u'Montréal'},
        ],
        'layout': {
            'title': 'Dash Data Visualization'
        }
    }
app = dash.Dash(__name__)
app.layout = html.Div([
    html.Button("Button 1", id="btn1"),
    dcc.Graph(id="graph", figure=fig_test),
    dcc.Slider(
             id='interval-component',
             min=0,
             max=36,
             step=1,
             value=10,
         ),
    html.Div(id="log"),
    html.Pre(
        id='structure',
        style={
            'border': 'thin lightgrey solid', 
            'overflowY': 'scroll',
            'height': '275px'
        }
    )
])
app.clientside_callback(
    """
    function(value, figure, btn1){
        figure['data'][0]['y'][1] = value 
                
        return {'data': figure['data'], 'layout':figure['layout']};
    }
    """, Output("graph", "figure"), [Input('interval-component','value'),Input("graph", "figure"),Input("btn1", "n_clicks")])
app.run_server(debug=False)

If I implement the clientside to jsonify the output like this:

app.clientside_callback(
    """
    function(value, figure, btn1){
        figure['data'][0]['y'][1] = value 
                
        return JSON.stringify({'data': figure['data'], 'layout':figure['layout']});
    }
    """, Output("log", "children"), [Input('interval-component','value'),Input("graph", "figure"),Input("btn1", "n_clicks")])

I can see the value being updated, so I do not know what the issue is.

Connor
  • 53
  • 1
  • 7

1 Answers1

0

So I figured out the 'smooth animation' for layout updates, wherein 'extend data' is not possible and so the solution in this answer: Plotly/Dash display real time data in smooth animation is not applicable. Further it allows smooth animations of live-updated data without being dependent on the 'animate' api. This is not an exact answer to the question I asked, but addresses the concept. If you are unfamiliar with Frames, or otherwise unsure how to setup a figure, see plotly's example here: https://plotly.com/python/animations/ Psuedocode for setup:

## setup figure, 
for data in some_data:
    ##do something
    fig['frames'].append(frame)
fig1 = go.Figure(fig) 

setup a store for your frames, and a store for the frames to be passed to a clientside callback. As I was setting up a process to simulate live data acquisition, I had a timer for 'polling' and a secondary one for the animation. If you don't want a timer to as a trigger, the main concept is still the same; have some 'animate' trigger, in this case 'interval-component', to kick off the quickly refreshing secondary timer.

app.layout = html.Div([
        dcc.Store(id='frames-stored', data=fig1['frames']),
        dcc.Store(id='frames'),
        dcc.Interval(
            id='interval-component',
            interval=1*500, # in milliseconds
            n_intervals=0
        ),
        dcc.Interval(id='graph-refresher',
                     interval=1*25,
                     n_intervals=0,
                     max_intervals=50,
                     disabled=True),
    dcc.Graph(id="graph", figure=fig1),
])

now a callback to catch your 'animation' trigger and pass frames to your clientside callback:

@app.callback(
    Output("frames", "data"),Output("graph-refresher", "disabled"),
    Output("graph-refresher", "max_intervals"),Output('graph-refresher','n_intervals'),
    Input("interval-component", "n_intervals"),State("frames-stored", "data"))
def data_smoother(n_intervals,frames):
    ## can do whatever here as long as a list of frames are passed to the store
    selected_frames = frames[n_intervals]
    return selected_frames,False,'some_max',0

This callback turns on the timer for the clientside callback, and resets the max_intervals with 'some_max'. This is going to be dependent on whatever you are doing. Now the clientside callback that handles the 'animation'.

app.clientside_callback(
    """
    function(n_intervals, frames){
            return {'data':frames[parseInt(n_intervals)]['data'], 'layout':frames[parseInt(n_intervals)]['layout']};
    }
     """,Output("graph", "figure"),Input('graph-refresher','n_intervals'), State("frames", "data")) 

I hope this is useful for someone!

Connor
  • 53
  • 1
  • 7