Changing sunburst plot zoom in/out behaviour

For my needs, i want sunburst plot to zoom in/out on single click ONLY . Out of the box, no matter if you double-click or single click a segment on sunburst plot - it will zoom in/out.

I want to change this behavior such that, it only zooms in/out on a single click and not respond to double click.
I believe it would need some sort of java script - but i have no idea on where to started on this.

A nudge in the right direction could really help me, please.

Bump…

@jinnyzor @adamschroeder

Hello @ptser,

Could you provide a sample app that we could work with?

I’m not sure if we can disable it for double-click, since it is in essence two single clicks. Haha.

hi @ptser
I agree with @jinnyzor . I doubt that we can disable that. But share with us what you have so far. I can ask one of my colleague engineers if we can’t figure it out here on the forum.

Ok so here is my working.

First capture double click at user side(See dash py code below), after that capture click event on plotly sunburst plot (see javascript code below) - in this event, evaluate if user doubled clicked or single clicked, if it is double clicked - return false (the plotly graph doesn’t update i.e. the segments doesn’t zoom in / animate) otherwise true (the segments zoom in / do their animation)

Below is the code to capture double clicks - i wrote client side callback to capture double clicks. This piece of code just toggles a dummy graph based on double or single click.

import dash
from dash import dcc
from dash import html
from dash import ClientsideFunction
from dash.dependencies import Input, Output, State
import plotly.graph_objects as go


app = dash.Dash(__name__)

def dummy_graph():
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=[1, 2, 3], y=[1, 2, 3], name=f'Trace 1'))
    fig.add_trace(go.Scatter(x=[4, 5, 6], y=[1, 2, 3], name=f'Trace 2'))
    fig.add_trace(go.Scatter(x=[7, 8, 9], y=[1, 2, 3], name=f'Trace 3'))
    fig.add_trace(go.Scatter(x=[10, 11, 12], y=[1, 2, 3], name=f'Trace 4'))
    fig.add_trace(go.Scatter(x=[13, 14, 15], y=[1, 2, 3], name=f'Trace 5'))
    fig.add_trace(go.Scatter(x=[13, 14, 15], y=[1, 2, 3], name=f'Trace 6'))
    fig.add_trace(go.Scatter(x=[16, 17, 18], y=[1, 2, 3], name=f'Trace 7'))
    fig.add_trace(go.Scatter(x=[19, 20, 21], y=[1, 2, 3], name=f'Trace 8'))
    fig.add_trace(go.Scatter(x=[22, 23, 24], y=[1, 2, 3], name=f'Trace 9'))
    fig.add_trace(go.Scatter(x=[25, 26, 27], y=[1, 2, 3], name=f'Trace 10'))
    fig.update_layout(showlegend=True,
                      legend=dict(itemclick = False))
    return fig


app.layout = html.Div([
    html.Button('Click Me', id='output-double-click'),  # Add a button to the layout
    html.Div(id='output-message'),
    dcc.Graph(id='graph'),
    dcc.Store(id='last_click',data=False),
    dcc.Store(id='dc_trig'),
])

app.clientside_callback(
    """
   
    function(n_clicks, data)
    {
        let CLICK_TIME = (new Date()).getTime();
        if(CLICK_TIME - data < 500)
        {
            console.log('double click');
            return [(new Date()).getTime(), new Boolean(true)];
        }
        else
        {
            LAST_CLICK = CLICK_TIME
            console.log('single click');
            return [LAST_CLICK, new Boolean(false)];
        }
        console.log(data);
        return [(new Date()).getTime(), new Boolean(false)];
    }
    """,
    [Output('last_click', 'data'),Output('dc_trig', 'data')],
    [Input('output-double-click', 'n_clicks'),State('last_click', 'data')],
    prevent_initial_call=True
)

@app.callback(
    Output("graph", "figure"),
    [Input('dc_trig', 'data')],
    prevent_initial_call=True
)
def dc_trigger_plot(data):
    if(data == True):
      #  print("Double Clicked!.")
        fig = dummy_graph()
        return fig
    else:
        return {}

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

double_click

The part of capturing double clicks works well.

Onto the second part - capture click event on plotly sunburst figure , is something i’m struggling with.
I found some JS code (linked below), which is for plotly.js - where plotly sunbrust click event is captured, and based on what is returned (true or false) - it either zooms in or not. I can’t replicate the same behavior in dash python.

See plotly js code here : https://codepen.io/etpinard/pen/GRKNyNO

My question : how do I capture ‘plotly_sunburstclick’ in dash python?

@jinnyzor @adamschroeder

Hi @ptser
This is a challenging one. I have less experience with the clientside callback, but I’ll try again later today. And if I can’t get it, I’ll ask my colleague Engineer, who might know better.

1 Like

hey. I hope you had success with the code - anything you could share?.

Any information from your colleagues engineer that could be of any help to me ?

@adamschroeder

hi @ptser

Nothing yet. I’m not able to get this to work. I’ll start a private messaging chain with you to see if we can figure this out together. And if we do, we can post here.

1 Like

@ptser,

Might be easier to use a ctrl+click instead.

Check this out:

import dash
from dash import dcc
from dash import html
from dash import ClientsideFunction
from dash.dependencies import Input, Output, State
import plotly.graph_objects as go

app = dash.Dash(__name__)


def dummy_graph():
    fig = go.Figure()
    fig.add_trace(go.Sunburst(labels=['Root', 'A', 'B', 'a', 'aa', 'b', 'bb'],
                              parents=['', 'Root', 'Root', 'A', 'A', 'B', 'B']))
    return fig


app.layout = html.Div([
    html.Button('Click Me', id='output-double-click'),  # Add a button to the layout
    html.Div(id='output-message'),
    dcc.Graph(id='graph', figure=dummy_graph()),
    dcc.Store(id='last_click', data=False),
    dcc.Store(id='dc_trig'),
])

app.clientside_callback(
    """
    function(f, id)
    {
        setTimeout(() => {
        window.Plotly.react(document.querySelector(`#${id} .js-plotly-plot`), f.data, f.layout)
        .then(gd => {
          gd.on('plotly_sunburstclick', (event) => {
            console.log(event)
              if (event.event.ctrlKey) {
                return false
              }
          })
        })
        }, 300)
        return window.dash_clientside.no_update
    }
    """,
    Output('graph', 'id'),
    Input('graph', 'figure'),
    State('graph', 'id')
)


@app.callback(
    Output("graph", "figure"),
    [Input('dc_trig', 'data')],
    prevent_initial_call=True
)
def dc_trigger_plot(data):
    if (data == True):
        #  print("Double Clicked!.")
        fig = dummy_graph()
        return fig
    else:
        return {}


if __name__ == '__main__':
    app.run_server(debug=True, port=8050)
2 Likes

Here is a little more fun:

import dash
from dash import html, Input, Output, State, dcc
import plotly.graph_objects as go
import json

app = dash.Dash(__name__)


def dummy_graph():
    fig = go.Figure()
    fig.add_trace(go.Sunburst(labels=['Root', 'A', 'B', 'a', 'aa', 'b', 'bb'],
                              parents=['', 'Root', 'Root', 'A', 'A', 'B', 'B']))
    return fig


app.layout = html.Div([
    html.Div(id='output-message'),
    dcc.Graph(id='graph', figure=dummy_graph()),
])

app.clientside_callback(
    """
    function(f, id)
    {
        setTimeout(() => {
        window.Plotly.react(document.querySelector(`#${id} .js-plotly-plot`), f.data, f.layout)
        .then(gd => {
          gd.on('plotly_sunburstclick', (click_event) => {
            console.log(click_event)
              if (click_event.event.ctrlKey) {
                new_points = []
                keys = ["curveNumber", "pointNumber","currentPath","root","entry","percentRoot","percentEntry",
                           "percentParent","parent","label"]
                click_event.points.forEach((point) => {
                    new_object = {}
                    keys.forEach((key) => {new_object[key] = point[key]})
                    new_points.push(new_object)})
                get_setProps(document.querySelector(`#${id}`))
                .setProps({clickData: {points: new_points, ctrlKey: true}})
                return false
              }
          })
        })
        }, 300)
        return window.dash_clientside.no_update
    }
    """,
    Output('graph', 'id'),
    Input('graph', 'figure'),
    State('graph', 'id')
)

@app.callback(
    Output('output-message', 'children'),
    Input('graph', 'clickData')
)
def showClickData(d):
    return json.dumps(d)


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

Put this in your js file:

function get_setProps(dom, traverseUp = 0) {
    const key = Object.keys(dom).find(key=>{
        return key.startsWith("__reactFiber$") // react 17+
            || key.startsWith("__reactInternalInstance$"); // react <17
    });
    const domFiber = dom[key];
    if (domFiber == null) return null;

    // react <16
    if (domFiber._currentElement) {
        let compFiber = domFiber._currentElement._owner;
        for (let i = 0; i < traverseUp; i++) {
            compFiber = compFiber._currentElement._owner;
        }
        return compFiber;
    }

    // react 16+
    const GetCompFiber = fiber=>{
        //return fiber._debugOwner; // this also works, but is __DEV__ only
        let parentFiber = fiber.return;
        while (typeof parentFiber.type == "string") {
            parentFiber = parentFiber.return;
        }
        return parentFiber;
    };
    let compFiber = GetCompFiber(domFiber);
    for (let i = 0; i < traverseUp; i++) {
        compFiber = GetCompFiber(compFiber);
    }
    if (compFiber.stateNode) {
        if (Object.keys(compFiber.stateNode.props).includes('setProps')) {
            return compFiber.stateNode.props
        }
        if (Object.keys(compFiber.stateNode).includes('setProps')) {
            return compFiber.stateNode
        }
    }
    for (let i = 0; i < 30; i++) {
        compFiber = GetCompFiber(compFiber);
        if (compFiber.stateNode) {
            if (Object.keys(compFiber.stateNode.props).includes('setProps')) {
                return compFiber.stateNode.props
            }
            if (Object.keys(compFiber.stateNode).includes('setProps')) {
                return compFiber.stateNode
            }
        }
    }
    throw new Error("this is not the dom you are looking for")
}

@jinnyzor thank you for your solution. Im not adept in JS stuff, can you give a brief explanation of what the code inside JS file is doing ? Just a brief explanation would be enough , rest I can just google.

Sure,

I am reassigning the clicks on the sunburst to only apply to the plot when you are not holding the ctrl key. Otherwise, it stops the event from triggering the plot.

@jinnyzor Im talking about the code inside JS-file, get_setProps() function. What does it have to do with sunburst ctrl-key event. It has something to do with react fiber, but I dont know what that is - never worked in react.

In other words, what’s the difference between the code you provided with JS get_setProps function and the one without it (the one i selected as the solution) ? Whats the added benefit or functionality of get_setProps() ?

Oh, get_setProps is a custom JS function which allows for directly manipulating the React props, Dash can see the results.

If the ctrl key is not pushed, I leave the event alone. However, if the ctrl key is pressed, I capture the event and then take the data from it to set the clickData for wherever you clicked, but this time I also added that the ctrlKey was pressed. On the dash server, you could use this event to then trigger a modal being opened if you wanted.

Basically, it gives some flexibility and the ability to directly manipulate Dash component props from JS only functions.

1 Like