Drill down function for graphs embedded in Dash app

I am working on making a dashboard for an hr dataset and am trying to have one the bar charts be able to drill down on the column that the user clicks on. Is there any way to do this in Dash?
Here is what my current graph looks like.
48%20AM
I am thinking that I might have to embed a link through a click element on each column of the bar chart in order to make it “drill down” into each columns element but I am not sure whether there is an easier way to do this or not.

Here is my current code that I used to make the graph inside my dashboard: @app.callback(Output(‘tot_hours’,‘figure’),
[Input(‘month_slider’,‘value’)])
def tot_hors(month_slider):
data=[
go.Bar(
y=df.loc[df[‘ed_code’].isin([‘E02’,‘E04’,‘E03’,‘E01’,‘E05’]),‘hours’].groupby(df[‘month’]).sum(),
x=df[‘month2’].unique(),
name=‘Worked’,
text=df.loc[df[‘ed_code’].isin([‘E02’,‘E04’,‘E03’,‘E01’,‘E05’]),‘hours’].groupby(df[‘month’]).sum(),
marker=dict(color=’#42A5B3’),
showlegend=True
),
go.Bar(
y=df.loc[df[‘ed_code’].isin([‘E20’,‘E12’,‘E11’,‘E14’,‘E13’]),‘hours’].groupby(df[‘month’]).sum(),
x=df[‘month2’].unique(),
name=‘Sick’,
text=df.loc[df[‘ed_code’].isin([‘E20’,‘E12’,‘E11’,‘E14’,‘E13’]),‘hours’].groupby(df[‘month’]).sum(),
marker=dict(color=’#D15A86’),
showlegend=True
)

]
layout= go.Layout(
    title='Total Hours',
    legend=dict(x=-.1,y=1.1),
    hovermode='closest',
    barmode='stack'
)
figure = {'data':data,'layout':layout}
return figure

Thanks in advance!

Yeah, I think that’s the easiest way to do this right now. I’ve seen a few examples that have succkessfully done this. Output('my-graph', 'figure'), [Input('my-graph', 'clickData')]. The trick is how you drill back out: One way I’ve seen someone do this is by embedding a second scatter trace with an annotation in the corner of the graph with text that says “Back”. Clicking on that item will refresh the figure to the original graph. Since it’s on the same figure, it will fire the same update on clickData

1 Like

Ok! Awesome! Thanks for your help!

If I understood correctly that’s a good idea only for 1 step drill down. What do you recommend for many steps?

I tried to use html.Button with @app.callback as input and output with the same figure as in ‘clickData’, but get an error:

You have already assigned a callback to the output
with ID "****" and property "figure". An output can only have
a single callback function. Try combining your inputs and
callback functions together into one function.

I implemented this kind of Drill Down. Here is an example:

"data": [
                {
                    'x': progress_demo2,
                    'y': cls_lev2_short,
                    'text': [str(i) + "%" for i in progress_demo2],
                    'textposition': 'inside',
                    'marker': {
                        'color': 'rgb(158,202,225)',
                        'line': dict(
                            color='rgb(8,48,107)',
                            width=1.5)
                    },
                    'opacity': 0.6,
                    'orientation': 'h',
                    'hoverinfo': 'y',
                    'type': 'bar',
                    'showlegend': False
                },
                {
                    'x': progress_demo,
                    'y': cls_heads_short,
                    'text': [str(i) + "%" for i in progress_demo],
                    'textposition': 'inside',
                    'marker': {
                        'color': 'rgb(158,202,225)',
                        'line': dict(
                            color='rgb(8,48,107)',
                            width=1.5)
                    },
                    'opacity': 0.6,
                    'orientation': 'h',
                    'hoverinfo': 'y',
                    'type': 'bar',
                    'visible': 'legendonly',
                    'name': 'Back',
                },
            ],
            "layout": {
                'height': 650,
                'margin': {
                    't': 50,
                    'l': 390
                },
                'yaxis': {
                    'type': "category",
                    'categoryorder': "category descending"
                },
                'bargap': 0.15,
                'legend': {
                        'x': -1.02,
                        'y': 1.02
                    }
            }

The problem is how to make other trace invisible after “Back” pressed. Can anyone suggest?

Thanks!

1 Like

I don’t fully understand what you mean but I really like the visual aspect you gave to your project. Would you share your View file with me ?

1 Like

Visual aspect you can find in post above in figure definition.

Finally I’ve done it through html.Button functionality and using local variables in session environment to save number of previous button clicks and compare it with n_clicks.

Here is multilevel DrillDown with two buttons “Back to top” and “Back to previous level”:

4 Likes

Hey, I’m glad you succeed to find a solution to your problem.

Sorry, my question was not clear, I was not talking about your graph but about the “side Menu” to navigate throw your views (I guess). The 2 “buttons” at the top of your view and the four at the top left.

I’m new to web development, and I don’t know how to build those navigations things. For the moment I’m dealing with with those ugly links to navigate through my web-app :stuck_out_tongue:

dash%20app%20navigation

Oh, I’m new to web dev too.
For navigation I use Tabs in Dash Core Components

1 Like

Thanks man, I thought I knew the documentation by heart but obviously I missed this…

Here is a track from my favourite album of 2018 to correct my mistake :wink:

1 Like

Hey iTem, drilldown is looking good.
Can you share the code what you have used to get this drilldown.

2 Likes

Sure,
but I should warn you about that it’s maybe not optimal code, because of I’m new to web development. And I’m using Dash app inside Django app, that’s why I’m using request.session() instead of Flask session. And only after I’ve noticed that Dash Core Components has build-in Store component for that. I think better use this component.

@app.callback(
    Output('Stats_for_top_classes', 'figure'),
    [Input('Stats_for_top_classes', 'clickData'),
     Input('but_top_lev', 'n_clicks'),
     Input('but_prev_lev', 'n_clicks')])

def update_figure_for_next_level(clickData, n_clicks_top, n_clicks_prev):
    # Create local variables in session environment:
        ## prev_clicks_top - number of previous clicks 'Back to top' button
        ## prev_clicks_prev - number of previous clicks 'Back to previous' button
        ## cur_level - current level of hierarchy
        ## selected_parent_cls_id - cls_id of current selection
        ## demo_mode - switch for data source

    if (n_clicks_top == 0) & (n_clicks_prev == 0):
        request.session["cur_level"] = request.session.get("cur_level", 0)
        request.session["selected_parent_cls_id"] = request.session.get(
            "selected_parent_cls_id", 1)
        request.session["demo_mode"] = request.session.get("demo_mode", 1)
    if n_clicks_top == 0:
        request.session["prev_clicks_top"] = 0
    if n_clicks_prev == 0:
        request.session["prev_clicks_prev"] = 0

    # Button "Back to top" was pressed
    if n_clicks_top > request.session["prev_clicks_top"]:
        cls_hds = Class.objects.filter(cls_cls_id__isnull=True)
        cls_heads = cls_hds.values_list('code', 'name')
        cls_heads = [' - '.join(cls_head) for cls_head in cls_heads]
        cls_heads_short = []

        for i in cls_heads:
            if len(i) > 50:
                cls_short = i[:50] + '<br>' + i[50:]
                cls_heads_short.append(cls_short)
            else:
                cls_heads_short.append(i)

        if request.session["demo_mode"] == 1:  # Demo data
            progress_demo = [random.randint(0, 100) for _ in range(len(cls_heads))]
        else:  # Real data
            progress_demo = np.array([get_count_accept_obj_for_class(i) for i in cls_hds]) / \
                np.array([get_count_all_obj_for_class(i) for i in cls_hds]) * 100

        figure = fig_for_stat_top(progress_demo, cls_heads_short)

        request.session["prev_clicks_top"] = n_clicks_top
        request.session["cur_level"] = 0

    # Button "Back to previous level" was pressed
    elif n_clicks_prev > request.session["prev_clicks_prev"]:
        if request.session["cur_level"] > 0:
            request.session["cur_level"] -= 1

            # Calculate our 'grandpa'
            selected_grandpa_cls_id = Class.objects.filter(
                cls_id=request.session["selected_parent_cls_id"]).values_list('cls_cls_id')
            request.session["selected_parent_cls_id"] = selected_grandpa_cls_id[0][0]

            # Get all our 'brothers'
            cls_cur_lv = Class.objects.filter(cls_cls_id=selected_grandpa_cls_id[0][0])
            cls_cur_lev = cls_cur_lv.values_list('code', 'name')
            cls_cur_lev = [' - '.join(cls_cur_lev) for cls_cur_lev in cls_cur_lev]

            # Transform long name classes
            cls_cur_lev_short = []
            for i in cls_cur_lev:
                if len(i) > 50:
                    cls_short = i[:50] + '<br>' + i[50:]
                    cls_cur_lev_short.append(cls_short)
                else:
                    cls_cur_lev_short.append(i)

            if request.session["demo_mode"] == 1:  # Demo data
                progress_demo_for_cur = [random.randint(0, 100) for _ in range(len(cls_cur_lev))]
            else:  # Real data
                progress_demo_for_cur = np.array([get_count_accept_obj_for_class(i) for i in cls_cur_lv]) / \
                    np.array([get_count_all_obj_for_class(i) for i in cls_cur_lv]) * 100

            figure = fig_for_stat_top(progress_demo_for_cur, cls_cur_lev_short)

        else:  # We are at top level
            # Data loading
            cls_hds = Class.objects.filter(cls_cls_id__isnull=True)
            cls_heads = cls_hds.values_list('code', 'name')
            cls_heads = [' - '.join(cls_head) for cls_head in cls_heads]
            cls_heads_short = []

            for i in cls_heads:
                if len(i) > 50:
                    cls_short = i[:50] + '<br>' + i[50:]
                    cls_heads_short.append(cls_short)
                else:
                    cls_heads_short.append(i)

            if request.session["demo_mode"] == 1:  # Demo data
                progress_demo = [random.randint(0, 100) for _ in range(len(cls_heads))]
            else:
                progress_demo = np.array([get_count_accept_obj_for_class(i) for i in cls_hds]) / \
                    np.array([get_count_all_obj_for_class(i) for i in cls_hds]) * 100

            figure = fig_for_stat_top(progress_demo, cls_heads_short)

        request.session["prev_clicks_prev"] = n_clicks_prev

    else:  # Buttons weren't pressed, but clickData was triggered
        try:  # To avoid TypeError: 'NoneType' object is not subscriptable
            # Get selected parent
            selected_parent = clickData["points"][0]['y']
            selected_parent_code = selected_parent[:selected_parent.find(" - ")]
            selected_parent_cls_id = Class.objects.filter(code=selected_parent_code).values_list('cls_id')

            # Check for existing some child
            if Class.objects.filter(cls_cls_id=selected_parent_cls_id[0][0]).exists():
                request.session["cur_level"] += 1
                request.session["selected_parent_cls_id"] = selected_parent_cls_id[0][0]
                # If child exists, then get all childs
                cls_lv2 = Class.objects.filter(cls_cls_id=selected_parent_cls_id[0][0])
                cls_lev2 = cls_lv2.values_list('code', 'name')
                cls_lev2 = [' - '.join(cls_lev2) for cls_lev2 in cls_lev2]

                # Transform long name classes
                cls_lev2_short = []
                for i in cls_lev2:
                    if len(i) > 50:
                        cls_short = i[:50] + '<br>' + i[50:]
                        cls_lev2_short.append(cls_short)
                    else:
                        cls_lev2_short.append(i)

                if request.session["demo_mode"] == 1:  # Demo data
                    progress_demo2 = [random.randint(0, 100) for _ in range(len(cls_lev2))]
                else:  # Real data
                    progress_demo2 = np.array([get_count_accept_obj_for_class(i) for i in cls_lv2]) / \
                        np.array([get_count_all_obj_for_class(i) for i in cls_lv2]) * 100

            else:
                cls_lv2 = Class.objects.filter(cls_cls_id=request.session["selected_parent_cls_id"])
                cls_lev2 = cls_lv2.values_list('code', 'name')
                cls_lev2 = [' - '.join(cls_lev2) for cls_lev2 in cls_lev2]

                # Transform long name classes
                cls_lev2_short = []
                for i in cls_lev2:
                    if len(i) > 50:
                        cls_short = i[:50] + '<br>' + i[50:]
                        cls_lev2_short.append(cls_short)
                    else:
                        cls_lev2_short.append(i)

                if request.session["demo_mode"] == 1:  # Demo data
                    progress_demo2 = [random.randint(0, 100) for _ in range(len(cls_lev2))]
                else:  # Real data
                    progress_demo2 = np.array([get_count_accept_obj_for_class(i) for i in cls_lv2]) / \
                        np.array([get_count_all_obj_for_class(i) for i in cls_lv2]) * 100

            figure = fig_for_stat_top(progress_demo2, cls_lev2_short)

        except TypeError:
            # Data loading
            cls_hds = Class.objects.filter(cls_cls_id__isnull=True)
            cls_heads = cls_hds.values_list('code', 'name')
            cls_heads = [' - '.join(cls_head) for cls_head in cls_heads]
            cls_heads_short = []

            for i in cls_heads:
                if len(i) > 50:
                    cls_short = i[:50] + '<br>' + i[50:]
                    cls_heads_short.append(cls_short)
                else:
                    cls_heads_short.append(i)

            if request.session["demo_mode"] == 1:  # Demo data
                progress_demo = [random.randint(0, 100) for _ in range(len(cls_heads))]
            else:
                progress_demo = np.array([get_count_accept_obj_for_class(i) for i in cls_hds]) / \
                    np.array([get_count_all_obj_for_class(i) for i in cls_hds]) * 100

            figure = fig_for_stat_top(progress_demo, cls_heads_short)

    return figure
3 Likes

I can’t figure out the logic for moving back and forth in my drill down
I have a 1 step drill down, with a back button.

When my app is loaded for the first time I can click on the graph it then triggers the clickData and it takes me to the step 1 of drill down and I can also jump back to the original graph using the back button’s n_clicks but now if I want to go ahead again I can’t as the condition is if n_clicks: return original graph() and once I click the back button the above condition is always true

Is it possible to reset the n_clicks value?

code:

 @app.callback(
     Output(component_id='figure card', component_property='figure'),   #plot output
     Input(component_id='figure card', component_property='clickData'), #generate different plot from the input of clickData
     Input(component_id='back button', component_property='n_clicks')   #button to go back to the original plot
 )
def update_graph(col_name,n_clicks):
      if n_clicks:
           return generate_original_graph()
      else:
           return generate_drill_down(col_name)

Hi @atharvakatre

It’s actually not necessary to reset the n_clicks value. You can do this by knowing which input triggered the callback. See more info on how to do this here: Advanced Callbacks | Dash for Python Documentation | Plotly.

The callback could look something like:


def update_graph(col_name, n_clicks):
    ctx = dash.callback_context
    trigger_id = ctx.triggered[0]["prop_id"].split(".")[0]    
    
    if trigger_id == "figure card":
        return generate_original_graph()
    else:
        return generate_drill_down(col_name)
1 Like

Hi @iTem ,
can you Please share your code repository or guide me in which file which functions , urls and callback function needs to be added as i am new to Django.

1 Like