✊🏿 Black Lives Matter. Please consider donating to Black Girls Code today.
⚾️ It's finally Baseball season! Root for the home team... & Register for our Sports Analytics Webinar!

clickData only recording clicks after dropdown is used

Hi,
I was solving another issue (thanks @pdh for the help) and we came across this strange thing with selectedData and clickData.

If I don’t have dropdowns in the dashboard, the events in the barchart work fine. However, if I have a dropdown, when I load the page and click in the graph it doesn’t record the clicked point. I need to first filter using the dropdown and from that moment it starts recording the clicks, it doesn’t matter if I clear the dropdown filter after, it keeps working after the first time. When I click it does go into the callback, but it just returns an empty object.

Here’s the items2.txt file and below is my code.

At the end of this post you can see the output in the different scenarios. It shows how clickData is empty until the dropdown is used for the first time.

Any idea on why this is happening?

Thanks!

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


df = pd.read_csv("item2.txt", sep=',')
cols = df.columns
cols = cols.map(lambda x: x.replace(' ', '_') if isinstance(x, (str, unicode)) else x)
df.columns = cols

app = dash.Dash()
app.css.append_css({'external_url': 'https://codepen.io/chriddyp/pen/bWLwgP.css'})
app.config['suppress_callback_exceptions']=False

app.layout = html.Div(
    html.Div(
    children=[
        #First row
        html.Div(
        children=[
            # Location Dropdown
            html.Div(
            children=[
                html.Label('Location'),
                dcc.Dropdown(id='location', 
                    options=[{'label': i, 'value': i} for i in df.Location.unique()], 
                )
            ], className="one columns"),
            
             # Site Dropdown
            html.Div(
            children=[    
                html.Label('Site'),
                dcc.Dropdown(id='site', 
                    options=[{'label': i, 'value': i} for i in df.Site.unique()],
                )
            ], className="one columns"),
    
           # Items
            html.Div(
            children=[    
                html.Label('Items'),
                html.Div(id='items_container')
                    #df.shape[0])
            ], className="one columns"),

           # Quantity
            html.Div(
            children=[    
                html.Label('Quantity'),
                html.Div(id='quantity_container')
            ], className="one columns"),

            html.Div(
            children=[
                html.Label('Placeholder'),
                html.Div(id='placeholder')
            ], className="eight columns", style={'width':'100%'}),


        ]), 

        html.Div(
        children=[
            html.Div(
            children=[    
                # BarChart with stock by type
                html.Div(id='bar_current_stock_by_type_container',
                    children=[dcc.Graph(id='bar_stock_by_type', clickData={'points': []}
                    )] 
                ),
            ], className="four columns"),
        ]),    
        
        html.Div(
        children=[
            html.Div(
            children=[
                # Table with data
                html.Div(id='table-container')
            ], className="twelve columns"),
        ])     
        
    ])
)

def get_filtered_df(location, site, clickData):
    
    if location is None and site is None:
       dff = df
    elif site is None:
        dff = df[df.Location == location]
    elif location is None:
        dff = df[df.Site == site]    
    else:
        dff = df[(df.Location == location) & (df.Site == site)]
  
    print (clickData["points"])
    
    if clickData["points"] != []:
        dataselected = clickData["points"][0]["y"]
        print dataselected
        for item in dff.Stock_Type:        
            if item == dataselected:
                dff2 = dff[(dff.Stock_Type == dataselected)]               
    else:
        dff2=dff
    return dff2


def generate_table(df, max_rows=10):
    return html.Table(
        [html.Tr([html.Th(col) for col in df.columns])] +
        [html.Tr([html.Td(df.iloc[i][col]) for col in df.columns]) 
        for i in range(min(len(df), max_rows))], id='full_table'
    )


def generate_bar_stock_by_type(df, byStockType):

    x = byStockType.sum()['Inventory_Value']/1000
    line = go.Scatter(
        x=x,    
        y=x.index,    
        name='Inventory value (k)'
    )

    x = byStockType.count()['Current_Balance']
    bar = go.Bar(
        x=x,          
        y=x.index,     
        orientation='h',
        name='Num Stock Units'
    )

    graph_stock = dcc.Graph(
        id='bar_stock_by_type',
        figure={'data': [bar,line]},  
        clickData={'points': [], 'range': None},
        config={'displayModeBar': True},
        style={}
    )
    return graph_stock


# Items Metric
@app.callback(
    dash.dependencies.Output('items_container', 'children'),
    [dash.dependencies.Input('location', 'value'),
     dash.dependencies.Input('site', 'value'), 
     dash.dependencies.Input('bar_stock_by_type', 'clickData')
     ])
def display_metric_items(location, site, clickData):
    dff = get_filtered_df(location, site, clickData)
    
    return html.H3(dff.shape[0])


# Quantity Metric
@app.callback(
    dash.dependencies.Output('quantity_container', 'children'),
    [dash.dependencies.Input('location', 'value'),
    dash.dependencies.Input('site', 'value'),
    dash.dependencies.Input('bar_stock_by_type', 'clickData')
    ])
def display_metric_quantity(location, site, clickData):
    dff = get_filtered_df(location, site, clickData)
    
    return html.H3(dff['Current_Balance'].sum())



# Table
@app.callback(
    dash.dependencies.Output('table-container', 'children'),
    [dash.dependencies.Input('location', 'value'),
    dash.dependencies.Input('site', 'value'), 
    dash.dependencies.Input('bar_stock_by_type', 'clickData')
    ])
def display_table(location, site, clickData):
    dff = get_filtered_df(location, site, clickData)

    return generate_table(dff)


# Horizontal Bar Chart
@app.callback(
    dash.dependencies.Output('bar_current_stock_by_type_container', 'children'),
    [dash.dependencies.Input('location', 'value'),
    dash.dependencies.Input('site', 'value')])
def display_bar_current_stock_by_type(location, site):
    dff = get_filtered_df(location, site, clickData={'points': [], 'range': None})
    byStockType = dff.groupby('Stock_Type')

    return generate_bar_stock_by_type(dff, byStockType)


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

Not entirely sure why, but I think it might be do do with having a redundant pair of square braces around the initial Graph component in your layout. When I removed them it seems to work. Does that make it behave as expected?

1 Like

Thanks @nedned , that worked!!! :grin:

Why is that though? I thought all the children have square braces.

I have another question regarding this graph, if you can’t help me I’ll post it as a new question, but just to leverage this post.
When you hover over the graph, the info it shows (points hovered) is not right sometimes.

I’ve updated text2.html here so it is clearer.
E.g. Starting with your pointer on the right side of the Power Supplies bar and moving it to the left:

  1. First part it is correct, Power supplies (blue)
  2. until it reaches the point where it would cross the line chart Other, where it shows Other (orange).
  3. If you keep going slowly it shows Other (blue),
  4. then Power Supplies (orange) when it meets the point,
  5. then Cables (orange) until it “goes out” of the line chart
  6. then Other (blue).

It’d make sense that is because the line is over the bar chart, but why 3 and 6? I can’t seem to get Cables (blue) hovered, whenever it is out of the points of the line chart, it shows Other instead of Cables.

Any idea?

Thanks.

children should be able to be an element, string, number, None or a list of any combination of those. I’m not sure why removing it worked in this case. If anyone can reduce this example into a minimally small, reproducable example, I can dig in further.

The problem appears not to be inherent to the use of a list, but that the children property has a list initially in the layout, but when updated by the display_bar_current_stock_by_type, becomes just the Graph component without being wrapped in a list.

Modifying the callback so that it returns a list works, so the problem is when there’s an asymmetry.

It does look a little buggy doesn’t it. But this is outside my area of familiarity. Would probably make sense to post another thread. Could also be worth seeing if you can replicate in plain plotly.js as it seems likely this is not specific to Dash.

I’ll try to replicate the scenario with a different chart and see if it makes a difference.

I’ll post an update as soon as I have something.

It’s very strange because when I inspect the elements on the page all of the bars’ divs are in their place, doesn’t look like there is anything over the Cables bar.

I’m very new to plotly, dash, and even Python, but I’ll try to replicate it and post it as a new question.
Thanks for the help!

1 Like