How do I properly add a callback to individual traces in Plotly?

56 Views Asked by At

The code below attempts to register a callback on Plotly traces in 3 different ways.

  1. Method 'a', registers a callback on the trace object directly, before adding it to the figure.
  2. Method 'b', registers the callback after adding it to the figure by getting the object from the figure data list after it has been added.
  3. Method 'c' is just a variation of 'a' where the callback is local (ie not a member function)

Here is the code showing the 3 attempts I have made to add the callback. This should be self contained and if you run it in a Jupyter Notebook you should see 3 plots and clicking on a any point on any plot should print a message, but the only plot that seems to work is the one using callback method 'b'

import ipywidgets as widgets
import plotly.graph_objects as go

line = {'name': 'line','data': ((1,1), (2,2), (3,3)), 'color':'red', 'dash':'solid'}
squared = {'name': 'squared','data': ((1,1), (2,2**2), (3,3**2)), 'color':'blue', 'dash':'4,4'}
cubed = {'name': 'cubed','data': ((1,1), (2,2**3), (3,3**3)), 'color':'green', 'dash':'solid'}
n4 = {'name': 'n4','data': ((1,1), (2,2**4), (3,3**4)), 'color':'purple', 'dash':'solid'}
traces = (line, squared, cubed, n4)

class MyPlot:
    def __init__(self, traces, use_callback):
        self.traces = traces
        self.use_callback = use_callback
    
    def get_values(self, func, index):
        return [e[index] for e in func['data']]
    
    def callback_a(self, trace, points, state):
        print(f"in callback_a with trace = {trace}, points = {points}, state = {state}")
        
    def callback_b(self, trace, points, state):
        if len(points.point_inds) < 1:
            return
        print(f"in callback_b with trace = {trace}, points = {points}, state = {state}")
       
            
    def display(self):
        
        def callback_c(trace, points, state):
            print(f"in callback_c with trace = {trace}, points = {points}, state = {state}")
        
        fig = go.FigureWidget() 
               
        for t in traces:
            s = go.Scatter(mode="lines", name=t['name'], x=self.get_values(t, 0), y=self.get_values(t, 1), line=dict(width=2, color=t['color'], dash=t['dash']))
            if self.use_callback == 'a': s.on_click(self.callback_a)
            if self.use_callback == 'c': s.on_click(callback_c)
            fig.add_trace(s) 
            if self.use_callback == 'b': fig.data[-1].on_click(self.callback_b)
            fig.layout.title = f"Plot using callback {self.use_callback}"
            
        display(fig)
        
my_plot_a = MyPlot(traces, 'a')
my_plot_b = MyPlot(traces, 'b')
my_plot_c = MyPlot(traces, 'c')
my_plot_a.display()
my_plot_b.display()
my_plot_c.display()

As I said above, the only method that seems to work is method 'b'. This however has an undesirable "property". When I click on any point on any trace, the callbacks for each trace are called, even if I did not click on that trace. I workaround this by checking length of the points.point_inds property which is only > 0 in the desired callback. While this works, its seems unnecessary, and the desired method, 'a', seems to be the documented way to do this, however I can't seem to get it to work.

Here is the example from the Plotly doc I think I am following this example in both 'a' and 'c'. Am I doing something wrong?

enter image description here

0

There are 0 best solutions below