0

I'm trying to create a Dash App that has a scatterplot and timeseries, where the user can select data points on either graph and they highlight on both. This is similar to the "Crossfiltering" example in the Dash documentation (https://dash.plotly.com/interactive-graphing) but one key difference is that I'm looking for the union of each graph's selection rather than the intersection.

To further complicate - I have a second callback linked to a dropdown and "next" button with the intent that both can be used to change the underlying dataset used in the graphs by filtering on the "ID" column in the original dataset. I've set it up to store the filtered dataframe as a JSON object in a DCC.Store called "asset_df" that can then be pulled as an input for the callback that updates the graphs.

In its current state:

  1. App loads and plots the dataset for the first ID in the list - this works as intended
  2. Clicking the "Next" button or selecting a difference ID from the dropdown updates both graphs with the new dataset - this works as intended
  3. Selecting data on either graph highlights those points on both graphs - this is where it breaks

It doesn't return a callback error or any error messages, but the graphs don't get updated with the selected points highlighted. I believe part of the issue is that the callback for updating the graphs seems to be firing twice, with the second firing returning a blank dataset for "selectedData" for both graphs.

Questions I'm hoping the community can help me with:

- Am I collecting/storing/recalling the asset_df correctly using the DCC.Store and my callback inputs/outputs?

- Why is the callback linked to the display_selected_data function getting called twice when a selection is made on either plot?

If you see any other issues with the code (I'm a beginner so no doubt there are many) please let me know, especially if they may be contributing to the issue described above.

Thank you!

dataset available here: SampleData

from jupyter_dash import JupyterDash
from dash import Dash, dcc, html, dash_table, ctx
import numpy as np
import pandas as pd
import json
from dash.dependencies import Input, Output
import plotly.express as px
from base64 import b64encode
import io
import collections


df_raw = pd.read_csv(PATH_TO_DATA)
df_raw.set_index('PCTimeStamp', inplace=True, drop=False)
asset_col = "Asset"
asset_list = df_raw[asset_col].unique().tolist()

X2_col = "X2_Variable"
timestamp_col = "PCTimeStamp"
Y1_col = "Y1_Variable"




app = JupyterDash(__name__)


app.layout = html.Div([
    html.Button('Next',
                id='next_button',
                n_clicks=0),
    dcc.Dropdown(asset_list,
                 value=asset_list[0],
                 id='dropdown'),
    dcc.Graph(id='scatter',
                  config={'displayModeBar': True}),
    dcc.Graph(id='timeseries',
                  config={'displayModeBar': True}),
    dcc.Store(id='asset_df')
])

def get_figure(df, x_col, y_col, asset_col, selectedpoints):
    if x_col == 'PCTimeStamp':
        fig = px.scatter(df, x=df[x_col], y=df[y_col], text=df.index, color=df[asset_col])
        fig.update_traces(selectedpoints=selectedpoints,
                          customdata=df.index, mode='markers+lines',
                          line_color='grey',
                          marker={'color': 'grey', 'size': 5},
                          unselected={'marker': {'opacity': 0.3, 'color': 'grey'}, 'textfont': {'color': 'grey'}},
                          selected={'marker': {'opacity': 1.0, 'color': 'yellow'}, 'textfont': {'color': 'yellow'}})
    elif x_col == X2_col:
        fig = px.scatter(df, x=df[x_col], y=df[y_col], text=df.index, color=df[asset_col])
        fig.update_traces(selectedpoints=selectedpoints,
                          customdata=df.index, mode='markers',
                          marker={'color': 'grey', 'size': 10},
                          unselected={'marker': {'opacity': 0.3, 'color': 'grey'}, 'textfont': {'color': 'grey'}},
                          selected={'marker': {'opacity': 1.0, 'color': 'yellow'}, 'textfont': {'color': 'yellow'}})
    else:
        print("something's wrong...")

    fig.update_layout(margin={'l': 20, 'r': 0, 'b': 15, 't': 5}, dragmode='select', hovermode=False)

    return fig


@app.callback(
    Output('asset_df', 'data'),
    Output('next_button', 'n_clicks'),
    Output('dropdown','value'),
    Input('dropdown', 'value'),
    Input('next_button', 'n_clicks'),
    prevent_initial_call=False
)
def create_asset_df(value, n_clicks):
    starting_pos=0
    if "next_button" == ctx.triggered_id:
        new_position = n_clicks
        n_clicks = n_clicks
    elif "dropdown" == ctx.triggered_id:
        new_position = asset_list.index(value)
        n_clicks = new_position
    else:
        new_position = starting_pos
        n_clicks = 0

    df_asset = df_raw[df_raw[asset_col] == asset_list[new_position]]
    df_asset = df_asset[[asset_col, X2_col, Y1_col, timestamp_col]]
    df_json = df_asset.to_json()
    
    return df_json, n_clicks, asset_list[new_position]
    
@app.callback(
    Output('scatter', 'figure'),
    Output('timeseries', 'figure'),
    Input('scatter', 'selectedData'),
    Input('timeseries', 'selectedData'),
    Input('asset_df', 'data'),
    prevent_initial_call=False
)
def display_selected_data(selection1, selection2, df_json):
    print("selection1:")
    print(selection1)

    print("selection2:")
    print(selection2)

    df_asset = pd.read_json(df_json)
    print("df_asset:")
    print(df_asset)
    for selected_data in [selection1, selection2]:
        if selected_data and selected_data['points']:
            selectedpoints = np.union1d(selectedpoints,
                                            [p['customdata'] for p in selected_data['points']])  
      
    print('selectedpoints:')
    print(selectedpoints)

    fig1 = get_figure(df_asset, X2_col , Y1_col, asset_col, selectedpoints)
    fig2 = get_figure(df_asset, timestamp_col, Y1_col, asset_col, selectedpoints)
    
    return fig1,fig2

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

0 Answers0