Display dynamic sum based on selected legend items

Greetings!

I have a simple dashboard that displays costs in a go.bar plot, and updates the plot based on selecting/deselecting legend items.

Is it possible to display the total cost, based on which legend items are selected?

I’m envisioning a function that returns which traces/legends are selected, sums up numbers from the cost column in those traces, and displays the value somewhere on the plot.

An example of the behavior can be seen here, where the number of cellphone towers updates based on the selected radio type.

Thanks very much for your help!!!

Example code (from Jupyter Lab) is below and input data is here.

import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.express as px
import pandas as pd
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import plotly

# get data    
df = pd.read_csv('dummy_model.csv')

# instantiate figure with subplots
fig = make_subplots(rows=4, cols=2,
                subplot_titles=('title', 'cost', 'title', 'title'),
                horizontal_spacing = .20,
                specs=[[{"rowspan": 4}, {}],
                [None, {"rowspan": 2}],
                [None,None],
                [None,{}]])

# populate traces automatically
hide = [] # instantiate a list to store existing legend items

# All Modalities plot
for index, row in df.iterrows(): # grab all of the proposals
    show = row['proposal'] not in hide # Check if legend item has already been used
    # Hide all legend items in subsequent plots
    
    fig.add_trace(go.Scatter(name=row['proposal'], x=[row['PDA_modality']], y=[row['modality']],
                            mode='markers', 
                            marker=dict(size=16, 
                            color = 'darkgray', opacity = 0.5),
                            legendgroup=row['proposal'], 
                            showlegend=show), 
                            row=1, col=1)
    hide.append(row['proposal']) # add legend item to list of existing legend items

# Cost plot, THIS IS THE PLOT WHERE I WOULD LIKE TO DISPLAY TOTAL COST
for index, row in df.iterrows(): 
    fig.add_trace(go.Bar(name=row['proposal'], x=[row['PDA_cost']], y=[row['cost2']],
                             marker = dict(color = 'darkgray'),
                             legendgroup=row['proposal'], 
                             showlegend=False), 
                             row=1, col=2)

# Grouped modalities plot    
for index, row in df.iterrows():
    fig.add_trace(go.Scatter(name=row['proposal'], x=[row['PDA_modality']], y=[row['modality_groups']],
                             mode='markers',
                             marker = dict(size=16, color = 'darkgray', opacity = 0.5),
                             legendgroup=row['proposal'], 
                             showlegend=False),
                             row=2, col=2)

# Cohorts plot
for index, row in df.iterrows():
    fig.add_trace(go.Bar(name=row['proposal'], x=[row['PDA_modality']], y=[row['Cohorts_prosp']],
                             marker = dict(color = 'darkgray'),
                             legendgroup=row['proposal'], 
                             showlegend=False,
                             #text = [row['Cohorts_prosp']],
                             textposition='outside',),
                             row=4, col=2)
    
# configure appearance of the plots
fig.update_layout(
    barmode='stack', 
    height = 1300,
    width = 2200,
    title='Dashboard',
    xaxis_title='x-axis',
    yaxis_title='y-axis',
    legend_title='title',
    font=dict(
        family="Courier New, monospace",
        size=14,
        color="RebeccaPurple"),
    legend_title_font_size = 16)

# pass categorical axis labels to a list to hardcode the axis labels 
# unfortunately, this setting changes the order, but only if there are actual values
y_categories = df['modality_list'].tolist()
fig.update_xaxes(overwrite = True, categoryorder='array', categoryarray= ['X', 'Y', 'Z'], row = 1, col = 1)
fig.update_xaxes(overwrite = True, categoryorder='array', categoryarray= ['X', 'Y', 'Z', 'Q', 'R'], row = 1, col = 2)
fig.update_xaxes(overwrite = True, categoryorder='array', categoryarray= ['X', 'Y', 'Z'], row = 2, col = 2)
fig.update_xaxes(overwrite = True, categoryorder='array', categoryarray= ['X', 'Y', 'Z'], row = 3, col = 2)

fig.update_yaxes(overwrite = True, range = [0,50000000], row = 1, col = 2)
fig.update_yaxes(overwrite = True, range = [0,3000], row = 3, col = 2)

# save plot to html
plotly.offline.plot(fig, filename='Dummy_dashboard.html')

fig.show()

Hi @jstitlow and welcome to Plotly community!

What you are looking for can be achieved using Dash via a callback function that listens to the selectedData of the figure object.

Here’s a minimal example:

import json
import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output
import plotly.express as px

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])


data = px.data.gapminder().query("country == 'India'")
fig = px.bar(data, x='year', y='pop')

fig.update_layout(clickmode='event+select')

pop_card = dbc.Card([
    html.H3(id='card-data', className='text-center'),
    html.H5('Population', className='text-center')
])

app.layout = dbc.Container([
    html.H2('Population of India', className='text-center'),
    dbc.Row(
        dcc.Graph(
        id='basic-interactions',
        figure=fig,
        ),
        justify='center'
    ),

    dbc.Row([
        dbc.Col([
            html.H5('Selected Data'),
            html.Pre(id='selected-data'),
        ], width=3),
        dbc.Col([pop_card], width=2)
    ], justify='center'),
],fluid=True)

@app.callback(
    Output('selected-data', 'children'),
    Output('card-data', 'children'),
    Input('basic-interactions', 'selectedData'))
def display_selected_data(selectedData):
    if selectedData is not None:
        return json.dumps(selectedData, indent=2), selectedData['points'][0]['y']
    else:
        return 'None', ''

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

If you are new to Dash and couldn’t get the above code, I suggest that you can start learning Dash from here.

If you are familiar with Dash you can find more about the solution I used in the example from here Part 4. Interactive Graphing and Crossfiltering | Dash for Python Documentation | Plotly

That’s awesome, thank you @atharvakatre!!!

Do you have any insight into this import error for dash_bootstrap_components:

---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
<ipython-input-7-e92403b17124> in <module>
      3 import dash_core_components as dcc
      4 import dash_html_components as html
----> 5 import dash_bootstrap_components as dbc
      6 from dash.dependencies import Input, Output
      7 import plotly.express as px

/opt/miniconda3/lib/python3.8/site-packages/dash_bootstrap_components/__init__.py in <module>
      4 
      5 from dash_bootstrap_components import themes  # noqa
----> 6 from dash_bootstrap_components import _components
      7 from dash_bootstrap_components._components import *  # noqa
      8 from dash_bootstrap_components._table import _generate_table_from_df

/opt/miniconda3/lib/python3.8/site-packages/dash_bootstrap_components/_components/__init__.py in <module>
----> 1 from .Alert import Alert
      2 from .Badge import Badge
      3 from .Button import Button
      4 from .ButtonGroup import ButtonGroup
      5 from .Collapse import Collapse

/opt/miniconda3/lib/python3.8/site-packages/dash_bootstrap_components/_components/Alert.py in <module>
      1 # AUTO GENERATED FILE - DO NOT EDIT
      2 
----> 3 from dash.development.base_component import Component, _explicitize_args
      4 
      5 

ImportError: cannot import name '_explicitize_args' from 'dash.development.base_component' (/opt/miniconda3/lib/python3.8/site-packages/dash/development/base_component.py)

All of dash packages seem to be up to date after running:

!pip install --upgrade dash dash-core-components dash-html-components dash-renderer

Requirement already up-to-date: dash in /opt/miniconda3/lib/python3.8/site-packages (1.20.0)
Requirement already up-to-date: dash-core-components in /opt/miniconda3/lib/python3.8/site-packages (1.16.0)
Requirement already up-to-date: dash-html-components in /opt/miniconda3/lib/python3.8/site-packages (1.1.3)
Requirement already up-to-date: dash-renderer in /opt/miniconda3/lib/python3.8/site-packages (1.9.1)
Requirement already satisfied, skipping upgrade: flask-compress in /opt/miniconda3/lib/python3.8/site-packages (from dash) (1.9.0)
Requirement already satisfied, skipping upgrade: Flask>=1.0.4 in /opt/miniconda3/lib/python3.8/site-packages (from dash) (2.0.1)
Requirement already satisfied, skipping upgrade: dash-table==4.11.3 in /opt/miniconda3/lib/python3.8/site-packages (from dash) (4.11.3)
Requirement already satisfied, skipping upgrade: future in /opt/miniconda3/lib/python3.8/site-packages (from dash) (0.18.2)
Requirement already satisfied, skipping upgrade: plotly in /opt/miniconda3/lib/python3.8/site-packages (from dash) (4.14.3)
Requirement already satisfied, skipping upgrade: brotli in /opt/miniconda3/lib/python3.8/site-packages (from flask-compress->dash) (1.0.9)
Requirement already satisfied, skipping upgrade: itsdangerous>=2.0 in /opt/miniconda3/lib/python3.8/site-packages (from Flask>=1.0.4->dash) (2.0.1)
Requirement already satisfied, skipping upgrade: Jinja2>=3.0 in /opt/miniconda3/lib/python3.8/site-packages (from Flask>=1.0.4->dash) (3.0.1)
Requirement already satisfied, skipping upgrade: click>=7.1.2 in /opt/miniconda3/lib/python3.8/site-packages (from Flask>=1.0.4->dash) (8.0.1)
Requirement already satisfied, skipping upgrade: Werkzeug>=2.0 in /opt/miniconda3/lib/python3.8/site-packages (from Flask>=1.0.4->dash) (2.0.1)
Requirement already satisfied, skipping upgrade: retrying>=1.3.3 in /opt/miniconda3/lib/python3.8/site-packages (from plotly->dash) (1.3.3)
Requirement already satisfied, skipping upgrade: six in /opt/miniconda3/lib/python3.8/site-packages (from plotly->dash) (1.15.0)
Requirement already satisfied, skipping upgrade: MarkupSafe>=2.0 in /opt/miniconda3/lib/python3.8/site-packages (from Jinja2>=3.0->Flask>=1.0.4->dash) (2.0.1)

Not sure why _explicitize_args can’t be imported, I see the function in base_component.py:

def _explicitize_args(func):
    # Python 2
    if hasattr(func, "func_code"):
        varnames = func.func_code.co_varnames
    # Python 3
    else:
        varnames = func.__code__.co_varnames

    def wrapper(*args, **kwargs):
        if "_explicit_args" in kwargs.keys():
            raise Exception("Variable _explicit_args should not be set.")
        kwargs["_explicit_args"] = list(
            set(list(varnames[: len(args)]) + [k for k, _ in kwargs.items()])
        )
        if "self" in kwargs["_explicit_args"]:
            kwargs["_explicit_args"].remove("self")
        return func(*args, **kwargs)

    # If Python 3, we can set the function signature to be correct
    if hasattr(inspect, "signature"):
        # pylint: disable=no-member
        new_sig = inspect.signature(wrapper).replace(
            parameters=inspect.signature(func).parameters.values()
        )
        wrapper.__signature__ = new_sig
    return wrapper

@jstitlow
Unfortunately I have never run into this error. As per my knowledge it shouldn’t give off any import error if the package has been correctly installed.

Did you try pip install dash-bootstrap-components --upgrade

Thanks for the response @atharvakatre! I think it was an environment error. Restarting the terminal seemed to do the trick.

Is there a way to listen to the selectedLegend item instead of selectedData?

Thanks for the inspiration @atharvakatre. Rather than listen for LegendItem clicks, I converted the legend items to dropdown menu items, and used the output to refresh plots and calculate the selected total. Really appreciate the help.
j