Clientside callback to filter data and update graph

I’ve been trying to update a figure from a clientside callback (because I have 5 figures in a page, performance issues). I’m using the example here Clientside Callbacks | Dash for Python Documentation | Plotly :

from dash import Dash, dcc, html, Input, Output
import pandas as pd
import json
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = 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(
        {'pop' : 'Population', 'lifeExp': 'Life Expectancy', 'gdpPercap': 'GDP per Capita'},
        'pop',
        id='clientside-graph-indicator'
    ),
    'Country',
    dcc.Dropdown(available_countries, 'Canada', id='clientside-graph-country'),
    'Graph scale',
    dcc.RadioItems(
        ['linear', 'log'],
        'linear',
        id='clientside-graph-scale'
    ),
    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')
)

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

My problem is that I don’t want to filter the data and then store it ( dff = df[df['country'] == country] in this example)
I would like to store the data as it is, then filter it on the clientside callback, something like ( dff = df[df['country'] == 'Canada'] ) and then assign x and y values. Is this possible and if so, how should I store the data? I guess storing it as JSON array of objects is more suitable than x & y lists. Thanks!

Hi,

It is possible to do it, the limitation is mostly the data size as explained here. It shouldn’t be a problem for your particular example though…

You can use the pd.DataFrame.to_dict method to do the conversion and Dash will serialize to JSON. The choice of orient is a bit arbitrary since you will manipulate the object in JS (and not convert back to a pandas dataframe), but I would stick with orient="records" for convenience.

Your clientside callback would look more or less like this (you might need to handle nulls and make minor adjustments):

app.clientside_callback(
    """
    function(data, scale, indicator, country) {
        filtered_data = data.filter(d => d["country"] == country)
        return {
            'data': [{
                x: filtered_data.map(d=> d["year"]),
                y: filtered_data.map(d=> d[indicator]),
                mode: "markers"
             }],
            'layout': {
                 'yaxis': {'type': scale}
             }
        }
    }
    """,
    Output('clientside-graph', 'figure'),
    Input('clientside-figure-store', 'data'),
    Input('clientside-graph-scale', 'value'),
    Input('clientside-graph-indicator', 'value'),
    Input('clientside-graph-country', 'value')
)

Hope this helps!

1 Like

I think you also need to specify the trace type in that data array. For instance type: "Scatter"

2 Likes

Hi!
Thanks for the answer. It helped a lot to put me in the right direction. I had to use forEach() to define x and y since I store the entire df as JSON (df.to_dict('records')), then it worked.

var x_array = [];
var y_array = [];
var filtered_data = data.filter(d => d["country"] == country);
filtered_data.forEach((arr)=>{x_array.push(arr.year)});
filtered_data.forEach((arr)=>{y_array.push(arr.indicator)});

return {
            'data': [{
                x: x_array,
                y: y_array,
                mode: "markers"
             }]
}

Thanks a lot!

1 Like