Search code examples
javascriptreactjsplotlyplotly.jssunburst-diagram

Plotly.js Sunburst chart state change determination


I have a react app where I'm using plotly.js to display a sunburst chart. I have other actions/components that change depending on the "zoom state" of the sunburst chart. E.g. if I'm all the way zoomed out, the burst chart shows 4 concentric circles. If I click on the second circle from the center, it zooms in and there are 3 concentric circles. I thought that I could determine the state from the click handler, but that doesn't seem to be straightforward. The state I'm interested in is essentially what things are visible, and what the depth is (how many concentric circles).

I'd hoped to find some sort of call back or something that would reflect the state, but all I've found is a click handler for sunburst chart (cf https://github.com/plotly/react-plotly.js/blob/master/README.md#event-handler-props - onSunburstClick, https://community.plotly.com/t/capturing-sunburst-on-click-events/32171)

The problem is, it's hard to deduce the state based on the click. Clicking on a segment changes the state in different ways. If I click on the outmost segment, it doesn't change the state at all. If I click on the innermost circle, it could zoom out or in, depending on the segments currently shown in the innermost circle.

Someone has an example on codepen based on this code:

Plotly.newPlot('graph', [{
  type: 'sunburst',
  parents: ['', 'Root', 'Root', 'A'],
  labels: ['Root', 'A', 'B', 'a']
}]).then(gd => {
  gd.on('plotly_sunburstclick', console.log)
})

If you go there and click on 'A', you end up in the state where 'A' is the center circle, and 'a' is the outer circle. If you click on 'A' again, then you end up with 'root' as the center circle. To the click handler, both clicks look essentially the same, but the resulting state (which is the part I care about) is different. I've tried some heuristics in my case to deduce whether it's zooming in or out, but nothing I've tried works consistently.

I've searched for solutions, but so far the closest thing I've found is the click handler, which after some effort doesn't solve my issue. If there's already an answer, that'd be great, but please don't be too quick mark a question as duplicate without being sure that it really is already solved. I've seen a lot of questions marked duplicate where the referred answer doesn't actually address the new question, meanwhile discussion and any attempts to answer the actual question are effectively shut down.

I should add that I'm not necessarily wedded to using plotly.js, if there are other react friendly libraries that would solve my problem, I'm open to suggestions.


Solution

  • The click event in this context eventually triggers what behaves like a "switch".

    By default, we see the root node and all the children of the hierarchy.

    • Clicking on a ring that has at least one child (like 'A') filters out its parent ('Root') and siblings ('B') and put that ring visually as the new root, and we only see the hierarchy under that ring (ie. the ring filter switch is on and the property nextLevel is set accordingly in the event data to 'A', like "A is the new root").

    • Clicking on that ring again - now the innermost circle - switches the filter back off, the parent and the siblings reappear and nextLevel is set to 'Root'.

    • Clicking on a ring that has no children (like 'a' or 'B') has no effect, and thus nextLevel is undefined in this case.

    Actually the property nextLevel is what reflects a state change.

    Now in order to have a better understanding on what's going on in the click handler, you might want to check currentpath (or for consistency, a slightly tweaked version of it) and the presence/value of nextLevel :

    Plotly.newPlot('graph', [{
      type: 'sunburst',
      parents: ['', 'Root', 'Root', 'A'],
      labels: ['Root', 'A', 'B', 'a']
    }]).then(gd => {
      gd.on('plotly_sunburstclick', data => {
        const pt = data.points[0]
        const path = ((pt.currentPath ?? '/') + pt.label).replace('Root', '')
        console.log('path:', path)                // path of the clicked element 
        console.log('nextLevel:', data.nextLevel) // label of the new root if any
      })
    })