Black Lives Matter. Please consider donating to Black Girls Code today.
https://www.blackgirlscode.com

Plotly sunburst and callbacks

First of all thank you for developing Dash! Its awesome!
I have developed a few fairly simple apps and would now like to integrate a sunburst chart. There is now a Dash_Sunburst component developed as an example of how to create custom components.
There is also the recently added sunburst trace in plotly.js and plotly python.
I was assuming the plotly version of the chart embedded in a dcc.Graph component would be the one to go with for new apps. However, i am finding that i only get ‘clickData’ callbacks to fire if a leaf is clicked.
Id like to crossfilter a table with children of a clicked parent, but am not sure this is possible with the plotly sunburst. Any advice would be appreciated.
Thanks!

I ran into the same problem, hopefully this gets changed in the future. For now, I was able to find a workaround because ‘hoverData’ callbacks are generated by all regions of the sunburst.

My workaround: I wrapped the sunburst’s dcc.Graph component inside an html.Div, used the ‘n_clicks’ of the Div to trigger my callback, and used State to bring the sunburst hoverData into the callback function. The callback should be triggered by any click inside the graph area, and then hoverData will be brought in based on where the mouse was when it was clicked.

from dash.dependencies import Input, Output, State

@app.callback(
    Output(component_id='output_id', component_property='output_property'),
    [Input(component_id='html_div_id', component_property='n_clicks')],
    [State(component_id='sunburst_id', component_property='hoverData')]
)
def callback_func(n_clicks, hoverData):
    #function code
2 Likes

Brilliant! Thanks for sharing your solution.

Hi,
Thanks for this wonderful solution. While using n_clicks and the hoverData property, I am not able to perform a callback when I click on the leaf nodes (the outermost nodes).How do I handle this issue?

Hi, I ran into the same issue as you. To get around this, I added an extra ring of blank (label='') leaves to my sunburst, and then made the leaves transparent (if you’re creating your sunburst using the go.Sunburst function, this can be done by adding leaf={'opacity': 0} to your function call).

Additionally, I came up with a slightly better version of the original solution I posted. Instead of using hoverData, the ‘figure’ property of the dcc.Graph component can be accessed in the callback, and used to get the id of the currently selected sunburst sector:

from dash.dependencies import Input, Output, State

@app.callback(
    Output(component_id='output_id', component_property='output_property'),
    [Input(component_id='html_div_id', component_property='n_clicks')],
    [State(component_id='dcc_graph_id', component_property='figure')]
)
def callback_func(n_clicks, figure):
    selected_id = figure['data'][0]['level']
    # do stuff

Hi, Thanks for the quick reply but the problem still persists while using the dcc.Graph component - I think even while accessing the figure property, we need to add an extra ring of blank leaves.

An alternative that worked for me was getting the hover_data when using both sunburst clickData and html_div n_clicks property as input

from dash.dependencies import Input, Output, State

@app.callback(Output(“html-output-div”,“children”),
[Input(“html-div”,“n_clicks”),Input(“sunburst-graph”,“clickData”)],
[State(“sunburst-graph”,“hoverData”)])
def update(n_clicks,clickData,hoverData):
if ‘label’ in hoverData[‘points’][0]:
label= hoverData[‘points’][0][‘label’]
else:
current_path=hoverData[‘points’][0][‘currentPath’]
if current_path=="/":
label=0
#your code

I’m also looking for this fix … but I can’t get the express tool to work with a third parent-level. Here is what we need as an option in sunburst …
click-leaf

Hey,

Stumbled upon your solution because I need to generate secondary plots based on what the user has selected in the sunburst. However, this solution doesn’t work for me as intended. Maybe you know why?

It’s actually quite silly, I only seem to get an update to my output when I click OUTSIDE the plot region. The update I receive is correct regarding the state of the sunburst, but actually clicking on the plot does not seem to trigger an output.

I’m just trying to display what the current outer selection of the sunburst is at this point.

app.layout = html.Div([
    
    html.Div(id='sunburst_div', children=[
        dcc.Graph(id='sunburst_graph',
                  figure=fig)
        ]),
    html.Div([
        html.H3(id='text_test')
        ], id='text_div')
    
    ], id='app_div')

@app.callback(
    Output(component_id='text_test', component_property='children'),
    [Input(component_id='sunburst_div', component_property='n_clicks')],
    [State(component_id='sunburst_graph', component_property='figure')]
)
def callback_func(n_clicks, figure):
    try:
        selected_id = figure['data'][0]['level']
        if selected_id not in df1['id'].unique():
            selected_id = 'All'
    except:
        selected_id = 'All'
    return(selected_id)

Hey @abalter85

I think the input needs to be the clickData on the sunburst chart itself rather than the div.

Check out this example: Drill down with a sunburst chart

And welcome to the Dash community :slight_smile:

Hey @AnnMarieW! Thanks so much for the response.

The example was very helpful. I have played with clickData before and I ran into a problem that I’m able to recreate when I use your code. That problem occurs when the user clicks the center circle in order to go back a level in the sunburst.

So if I have 4 levels: Levels 0 through Level 3… And I click down into level 0, then level 1, then level 2. At this point, the sunburst displays level 2 in the center of the sunburst, with level 3 as leaves. clickData is reporting level 2 correctly.

If I click level 2 (center circle) in order to go back to level 1, the clickData does not reflect this change. Now level 1 is shown as the inner circle, but the click data still reports level 2, since that was the click which executed the change.

Hey again @AnnMarieW. I think I was able to solve it by adding:

        if percentEntry == 1:
            click_path = clickData["points"][0]["currentPath"].strip('/').replace('/', ' - ')
            if click_path == '':
                click_path = 'All'

It seems to be working now and I’m very grateful for you pointing me in the right direction. Thanks so much! And thanks for the welcome to the community :slight_smile:

Hey @abalter85 Glad you solved it!

I used percentEntry too when going back to the root, but in a slightly different way. Mine works, but I like your solution better :wink: Thanks for sharing!