Show and Tell - Drill Down functionality in Dash using callback_context

Hello community,

Recently I was looking into adding drill down capabilities in my Dash App.
For those unaware about drill down analysis, it enables the user to have a deeper look into the data.

I found some similar questions posted online before
Some were still unanswered:

Then I found this old thread on the forum - Drill down function for graphs embedded in Dash app
You’ll find some smart ideas discussed here with a few examples to achieve drill down. But the thread is old and the examples were a bit complex.

So I thought about sharing a MWE for others looking into it in future.

In this example I am showcasing just a single level drill down to keep it simple but with few modifications multi -level drill down can be achieved.
There’s a back button for going back to the original figure. The back button is shown only on the level two of the drill down and hides on the original bottom level.
drill down

Code:

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

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

# creating a dummy sales dataframe
product_sales = {'vendors':['VANS','VANS','VANS','VANS','NIKE','NIKE','NIKE','ADIDAS','ADIDAS','CONVERSE','CONVERSE','CONVERSE'],
                 'products': ['Tshirts','Sneakers','Caps','Clothing','Sports Outfit','Sneakers','Caps','Accessories','Bags','Sneakers','Accessories','Tshirts'],
                 'units sold': [2,15,3,8,37,13,7,4,12,7,8,2]
                 }
product_sales_df = pd.DataFrame(product_sales)

# all vendors sales pie chart
def sales_pie():
    df = product_sales_df.groupby('vendors').sum().reset_index()
    fig = px.pie(df, names='vendors',
                 values='units sold', hole=0.4)
    fig.update_layout(template='presentation', title='Sales distribution per Vendor')
    return fig

# creating app layout
app.layout = dbc.Container([
    dbc.Card([
            dbc.Button('🡠', id='back-button', outline=True, size="sm",
                        className='mt-2 ml-2 col-1', style={'display': 'none'}),
            dbc.Row(
                dcc.Graph(
                        id='graph',
                        figure=sales_pie()
                    ), justify='center'
            )
    ], className='mt-3')
])

#Callback
@app.callback(
    Output('graph', 'figure'),
    Output('back-button', 'style'), #to hide/unhide the back button
    Input('graph', 'clickData'),    #for getting the vendor name from graph
    Input('back-button', 'n_clicks')
)
def drilldown(click_data,n_clicks):

    # using callback context to check which input was fired
    ctx = dash.callback_context
    trigger_id = ctx.triggered[0]["prop_id"].split(".")[0]

    if trigger_id == 'graph':

        # get vendor name from clickData
        if click_data is not None:
            vendor = click_data['points'][0]['label']

            if vendor in product_sales_df.vendors.unique():
                # creating df for clicked vendor
                vendor_sales_df = product_sales_df[product_sales_df['vendors'] == vendor]

                # generating product sales bar graph
                fig = px.bar(vendor_sales_df, x='products',
                             y='units sold', color='products')
                fig.update_layout(title='<b>{} product sales<b>'.format(vendor),
                                  showlegend=False, template='presentation')
                return fig, {'display':'block'}     #returning the fig and unhiding the back button

            else:
                return sales_pie(), {'display': 'none'}     #hiding the back button

    else:
        return sales_pie(), {'display':'none'}

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

Thank you :slightly_smiling_face:

11 Likes

Very nice! Thanks for sharing this.

1 Like

Hey there!

I have a question. I use your example to create this kind of drilldown graph. But I had kind of problem with dropdown menu.
I have also dropdown menu and whenever I change something in dropdown menu my chart backs to initial stage. Can I somehow force behaviour that:
If my chart is drilled down → Update dropdown menu to only those values that are linked with drilled down chart, and whenever user change something in dropdown menu it won’t go back to previous chart (before drill down).

I am trying 2nd day how to handle my case :smiley: For now I am trying to have a Context object in which I keep information about if is drilled down or not.

Can anyone help :slight_smile: ?

Here is my code:

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


class Context:

    def __init__(self):
        self.figures = {}
        self._drilled = False
        self._df = None
        self._vendor = None

    def add_figure(self, key, fig):
        self.figures[key] = fig

    @property
    def drilled(self):
        return self._drilled

    @drilled.setter
    def drilled(self, drilled):
        self._drilled = drilled

    @property
    def data(self):
        return self._df

    @data.setter
    def data(self, data):
        self._df = data

    @property
    def vendor(self):
        return self._vendor

    @vendor.setter
    def vendor(self, vendor):
        self._vendor = vendor


context = Context()

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

# creating a dummy sales dataframe
product_sales = {
    "vendors": [
        "VANS",
        "VANS",
        "VANS",
        "VANS",
        "NIKE",
        "NIKE",
        "NIKE",
        "ADIDAS",
        "ADIDAS",
        "CONVERSE",
        "CONVERSE",
        "CONVERSE",
    ],
    "products": [
        "Tshirts",
        "Sneakers",
        "Caps",
        "Clothing",
        "Sports Outfit",
        "Sneakers",
        "Caps",
        "Accessories",
        "Bags",
        "Sneakers",
        "Accessories",
        "Tshirts",
    ],
    "units sold": [2, 15, 3, 8, 37, 13, 7, 4, 12, 7, 8, 2],
}
product_sales_df = pd.DataFrame(product_sales)

vendors = ["VANS", "NIKE", "ADIDAS", "CONVERSE"]
products = [
    "Tshirts",
    "Sneakers",
    "Caps",
    "Clothing",
    "Sports Outfit",
    "Accessories",
    "Bags",
]


# all vendors sales pie chart
def sales_pie(products):
    df = product_sales_df[product_sales_df["products"].isin(products)]
    df = df.groupby("vendors").sum().reset_index()
    fig = px.pie(df, names="vendors", values="units sold", hole=0.4)
    fig.update_layout(template="presentation", title="Sales distribution per Vendor")
    return fig


def product_sales_barchart(df, vendor):
    fig = px.bar(df, x="products", y="units sold", color="products")
    fig.update_layout(title="<b>{} product sales<b>".format(vendor))
    return fig


# creating app layout
app.layout = dbc.Container(
    [
        dbc.Card(
            [
                dbc.Button(
                    "🡠",
                    id="back-button",
                    outline=True,
                    size="sm",
                    className="mt-2 ml-2 col-1",
                    style={"display": "none"},
                ),
                dcc.Dropdown(
                    id="products",
                    multi=True,
                    clearable=True,
                    value=products,
                    placeholder="Select products",
                    options=[{"label": c, "value": c} for c in products],
                ),
                dbc.Row(dcc.Graph(id="graph", figure=sales_pie(products)), justify="center"),
            ],
            className="mt-3",
        )
    ]
)

# Callback
@app.callback(
    Output("graph", "figure"),
    Output("back-button", "style"),  # to hide/unhide the back button
    Input("graph", "clickData"),  # for getting the vendor name from graph
    Input("back-button", "n_clicks"),
    Input("products", "value"),
)
def drilldown(click_data, n_clicks, value):

    # using callback context to check which input was fired
    ctx = dash.callback_context
    trigger_id = ctx.triggered[0]["prop_id"].split(".")[0]
    print(ctx.triggered)
    print(trigger_id)

    sales_df = product_sales_df[product_sales_df['products'].isin(value)]
    base_fig = sales_pie(value)
    context.drilled = False

    if trigger_id == "graph":

        # get vendor name from clickData
        if click_data is not None:
            vendor = click_data["points"][0]["label"]

            if vendor in sales_df.vendors.unique():
                vendor_sales_df = sales_df[sales_df["vendors"] == vendor]

                # generating product sales bar graph
                fig = product_sales_barchart(vendor_sales_df, vendor)
                # context.drilled = True
                # context.data = vendor_sales_df
                # context.vendor = vendor

                return fig, {"display": "block"}

            #context.drilled = False
            return base_fig, {"display": "none"}

    # if trigger_id == 'products' and context.drilled:
    #     data = context.data
    #     data = data[data['products'].isin(value)]
    #
    #     fig = product_sales_barchart(data, context.vendor)
    #     return fig, {"display": "block"}
    #
    # elif trigger_id == 'products' and not context.drilled:
    #     context.drilled = False
    #     return base_fig, {"display": "none"}

    return base_fig, {"display": "none"}


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