Search code examples
pythoncallbackplotly-dash

Including a select all feature in dash plotly callback - python


I've got a plotly bar chart that's connected to a callback to allow filtering. Using below, filtering is done on the Type column.

Issue: The real data I actually filter on contains thousands of items. I want to show all data initially but don't want to visualise all individual items in the dropdown bar (because there are too many). Therefore, I'm aiming to incorporate a Select All feature.

I don't think the current approach can be manipulated but I'm open to new ideas.

Question: If Select All is chosen from the dropdown bar, it should be the only visible icon. You can't have all the items and something else. Where an individual type(s) is chosen initially, if Select All is subsequently selected, it should drop the individual item from the drop down bar.

Example below. Start with B, then Select All is chosen so B should be dropped from the dropdown bar.

Further, I doubt this is a function at all, but if Select All is in place, then individual type(s) cannot be added to the dropdown bar.

import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output
import dash_bootstrap_components as dbc
import plotly.express as px
import plotly.graph_objs as go
import pandas as pd

df = pd.DataFrame({
       'Type': ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'],
       })

N = 300
df = pd.concat([df] * N, ignore_index=True)

df['TIMESTAMP'] = pd.date_range(start='2024/01/01 07:36', end='2024/01/30 08:38', periods=len(df))

df['DATE'], df['TIME'] = zip(*[(d.date(), d.time()) for d in df['TIMESTAMP']])
df['DATE'] = pd.to_datetime(df['DATE'], format='%Y-%m-%d')


external_stylesheets = [dbc.themes.SPACELAB, dbc.icons.BOOTSTRAP]

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


filter_box = html.Div(children=[
    html.Div(children=[
        dcc.Dropdown(
            id = 'Type',
            options = [
                {'label': x, 'value': x} for x in df['Type'].unique()
            ] + [
                {'label': 'Select All', 'value': 'all'}
            ],
            value = 'all',
            multi = True,
            clearable = True,
            style = {'display': 'inline-block','margin':'0.1rem'}
        ),       
    ], className = "vstack gap-1 h-100",            
    )
])

app.layout = dbc.Container([
    dbc.Row([
        dbc.Col([
            dbc.Row([
                dbc.Col(html.Div(filter_box), 
                        ),
            ]),
        ]),
        dbc.Col([
            dbc.Row([
                dcc.Graph(id = 'date-bar-chart'),
            ]),
        ])
    ])
], fluid = True)

@app.callback(
    Output('date-bar-chart', 'figure'),
    [Input('Type', 'value'), 
    ])     

def chart(value_type):

    if 'all' in value_type:
          value_type = ['all']
    else:
          value_type = value_type

    if value_type == 'all':
        dff = df

    elif value_type == ['all']:
        dff = df

    else:
        dff = df[df['Type'].isin(value_type)]

    df_count = dff.groupby(['DATE','Type'])['DATE'].count().reset_index(name = 'counts')

    if df_count.empty == True:
        type_fig = go.Figure()

    else:
        df_count = df_count

        type_fig = px.bar(x = df_count['DATE'], 
                      y = df_count['counts'],
                      color = df_count['Type']
                      )

    return type_fig

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

enter image description here


Solution

  • Your selection processing is OK, but you are missing an update of the Dropdown component.

    Also, in the case of adding something else to Select all option, you probably want to only purge the selection only, but keep the graph unchanged. It can be noted, that there are opposite cases, that end up with the similar selection value, e.g. ['A', 'all']. To distinguish them you need to track the previous state, which can be implemented with dcc.Store component.

    These sum up as the following changes:

    Additional imports
    from dash import no_update
    from dash.dependencies import State
    
    Some constants and an auxiliary chart creation method:
    SELECT_ALL = 'all'  # to avoid string literals in the code
    
    def make_figure(*, select_all=True, selected_values=None):
    
        if select_all:
            dff = df  # use source data frame
        
        elif selected_values is None:
            return go.Figure()  # result is empty without filter values
    
        else:
            dff = df[df['Type'].isin(selected_values)]  # filtered
    
        df_count = dff.groupby(['DATE', 'Type'])['DATE'].count().reset_index(
            name='counts')
    
        if df_count.empty:
            return go.Figure()
    
        return px.bar(x=df_count['DATE'],
                      y=df_count['counts'],
                      color=df_count['Type'])
    
    Layout update:
    filter_box = html.Div(children=[
        dcc.Store(id='session_select_all_types', data=True, storage_type='session'),
        html.Div(children=[
            dcc.Dropdown(
    ...
    
    app.layout = dbc.Container([
        ...
        # Initialize with all types selected
        dcc.Graph(id='date-bar-chart',
                  figure=make_figure(select_all=True)),
    
    Callback:
    @app.callback(
        Output('date-bar-chart', 'figure'),
        Output('Type', 'value'),
        Output('session_select_all_types', 'data'),
        Input('Type', 'value'),
        State('session_select_all_types', 'data'))
    def chart(value_type, select_all_types_previous):
    
        select_all_types = SELECT_ALL in value_type
    
        if select_all_types:
            if select_all_types_previous is True:
                return (no_update,     # you don't want to recreate a valid figure
                        [SELECT_ALL],  # force it to have an only Select all value
                        no_update)     # already True
    
            value_type = [SELECT_ALL]
    
        figure = make_figure(select_all=select_all_types,
                             selected_values=value_type)
    
        return (figure,
                value_type,        # update the selection
                select_all_types)  # update the flag in the session storage