Clientside Callbacks

Sometimes callbacks can incur a significant overhead, especially when they:
- receive and/or return very large quantities of data (transfer time)
- are called very often (network latency, queuing, handshake)
- are part of a callback chain that requires multiple roundtrips
between the browser and Dash

When the overhead cost of a callback becomes too great and no
other optimization is possible, the callback can be modified to be run
directly in the browser instead of a making a request to Dash.

The syntax for the callback is almost exactly the same; you use
Input and Output as you normally would when declaring a callback,
but you also define a JavaScript function as the first argument to the
@app.callback decorator.

For example, the following callback:

@app.callback(
    Output('out-component', 'value'),
    [Input('in-component1', 'value'), Input('in-component2', 'value')]
)
def large_params_function(largeValue1, largeValue2):
    largeValueOutput = someTransform(largeValue1, largeValue2)

    return largeValueOutput

Can be rewritten to use JavaScript like so:

from dash.dependencies import Input, Output

app.clientside_callback(
    """
    function(largeValue1, largeValue2) {
        return someTransform(largeValue1, largeValue2);
    }
    """,
    Output('out-component', 'value'),
    [Input('in-component1', 'value'), Input('in-component2', 'value')]
)

You also have the option of defining the function in a .js file in
your assets/ folder. To achieve the same result as the code above,
the contents of the .js file would look like this:

window.dash_clientside = Object.assign({}, window.dash_clientside, {
    clientside: {
        large_params_function: function(largeValue1, largeValue2) {
            return someTransform(largeValue1, largeValue2);
        }
    }
});

In Dash, the callback would now be written as:

from dash.dependencies import ClientsideFunction, Input, Output

app.clientside_callback(
    ClientsideFunction(
        namespace='clientside',
        function_name='large_params_function'
    ),
    Output('out-component', 'value'),
    [Input('in-component1', 'value'), Input('in-component2', 'value')]
)

A simple example

Below are two examples of using clientside callbacks to update a
graph in conjunction with a dcc.Store component. In these
examples, we update a dcc.Store component on the backend; to
create and display the graph, we have a clientside callback in the
frontend that adds some extra information about the layout that we
specify using the radio buttons under “Graph scale”.

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

import json

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv')

available_countries = df['country'].unique()

app.layout = html.Div([
    dcc.Graph(
        id='clientside-graph'
    ),
    dcc.Store(
        id='clientside-figure-store',
        data=[{
            'x': df[df['country'] == 'Canada']['year'],
            'y': df[df['country'] == 'Canada']['pop']
        }]
    ),
    'Indicator',
    dcc.Dropdown(
        id='clientside-graph-indicator',
        options=[
            {'label': 'Population', 'value': 'pop'},
            {'label': 'Life Expectancy', 'value': 'lifeExp'},
            {'label': 'GDP per Capita', 'value': 'gdpPercap'}
        ],
        value='pop'
    ),
    'Country',
    dcc.Dropdown(
        id='clientside-graph-country',
        options=[
            {'label': country, 'value': country}
            for country in available_countries
        ],
        value='Canada'
    ),
    'Graph scale',
    dcc.RadioItems(
        id='clientside-graph-scale',
        options=[
            {'label': x, 'value': x} for x in ['linear', 'log']
        ],
        value='linear'
    ),
    html.Hr(),
    html.Details([
        html.Summary('Contents of figure storage'),
        dcc.Markdown(
            id='clientside-figure-json'
        )
    ])
])


@app.callback(
    Output('clientside-figure-store', 'data'),
    [Input('clientside-graph-indicator', 'value'),
     Input('clientside-graph-country', 'value')]
)
def update_store_data(indicator, country):
    dff = df[df['country'] == country]
    return [{
        'x': dff['year'],
        'y': dff[indicator],
        'mode': 'markers'
    }]


app.clientside_callback(
    """
    function(data, scale) {
        return {
            'data': data,
            'layout': {
                 'yaxis': {'type': scale}
             }
        }
    }
    """,
    Output('clientside-graph', 'figure'),
    [Input('clientside-figure-store', 'data'),
     Input('clientside-graph-scale', 'value')]
)


@app.callback(
    Output('clientside-figure-json', 'children'),
    [Input('clientside-figure-store', 'data')]
)
def generated_figure_json(data):
    return '```\n'+json.dumps(data, indent=2)+'\n```'


if __name__ == '__main__':
    app.run_server(debug=True)
Indicator
Country
Graph scale

Contents of figure storage

None

Note that, in this example, we are manually creating the figure
dictionary by extracting the relevant data from the
dataframe. This is what gets stored in our dcc.Store component;
expand the “Contents of figure storage” above to see exactly what
is used to construct the graph.

Using Plotly Express to generate a figure

Plotly Express enables you to create one-line declarations of
figures. When you create a graph with, for example,
plotly_express.Scatter, you get a dictionary as a return
value. This dictionary is in the same shape as the figure
argument to a dcc.Graph component. (See
here for
more information about the shape of figures.)

We can rework the example above to use Plotly Express.

import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import json

import plotly.express as px

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv')

available_countries = df['country'].unique()

app.layout = html.Div([
    dcc.Graph(
        id='clientside-graph-px'
    ),
    dcc.Store(
        id='clientside-figure-store-px'
    ),
    'Indicator',
    dcc.Dropdown(
        id='clientside-graph-indicator-px',
        options=[
            {'label': 'Population', 'value': 'pop'},
            {'label': 'Life Expectancy', 'value': 'lifeExp'},
            {'label': 'GDP per Capita', 'value': 'gdpPercap'}
        ],
        value='pop'
    ),
    'Country',
    dcc.Dropdown(
        id='clientside-graph-country-px',
        options=[
            {'label': country, 'value': country}
            for country in available_countries
        ],
        value='Canada'
    ),
    'Graph scale',
    dcc.RadioItems(
        id='clientside-graph-scale-px',
        options=[
            {'label': x, 'value': x} for x in ['linear', 'log']
        ],
        value='linear'
    ),
    html.Hr(),
    html.Details([
        html.Summary('Contents of figure storage'),
        dcc.Markdown(
            id='clientside-figure-json-px'
        )
    ])
])


@app.callback(
    Output('clientside-figure-store-px', 'data'),
    [Input('clientside-graph-indicator-px', 'value'),
     Input('clientside-graph-country-px', 'value')]
)
def update_store_data(indicator, country):
    dff = df[df['country'] == country]
    return px.scatter(dff, x='year', y=str(indicator))


app.clientside_callback(
    """
    function(figure, scale) {
        if(figure === undefined) {
            return {'data': [], 'layout': {}};
        }
        const fig = Object.assign({}, figure, {
            'layout': {
                ...figure.layout,
                'yaxis': {
                    ...figure.layout.yaxis, type: scale
                }
             }
        });
        return fig;
    }
    """,
    Output('clientside-graph-px', 'figure'),
    [Input('clientside-figure-store-px', 'data'),
     Input('clientside-graph-scale-px', 'value')]
)


@app.callback(
    Output('clientside-figure-json-px', 'children'),
    [Input('clientside-figure-store-px', 'data')]
)
def generated_px_figure_json(data):
    return '```\n'+json.dumps(data, indent=2)+'\n```'


if __name__ == '__main__':
    app.run_server(debug=True)
Indicator
Country
Graph scale

Contents of figure storage

None

Again, you can expand the “Contents of figure storage” section
above to see what gets generated. You may notice that this is
quite a bit more extensive than the previous example; in
particular, a layout is already defined. So, instead of creating
a layout as we did previously, we have to mutate the existing
layout in our JavaScript code.


Note: There are a few limitations to keep in mind:

  1. Clientside callbacks execute on the browser’s main thread and wil block
    rendering and events processing while being executed.
  2. Dash does not currently support asynchronous clientside callbacks and will
    fail if a Promise is returned.
  3. Clientside callbacks are not possible if you need to refer to global
    variables on the server or a DB call is required.