Search code examples
pythonbokeh

Open new plot with `bokeh` TapTool


I have a class Collection that holds a bunch of other class objects Thing that all have the same attributes with different values. The Collection.plot(x, y) method makes a scatter plot of the x values vs. the y values of all the collected Thing objects like so:

from bokeh.plotting import figure, show
from bokeh.models import TapTool

class Thing:
    def __init__(self, foo, bar, baz):
        self.foo = foo
        self.bar = bar
        self.baz = baz
    
    def plot(self):

        # Plot all data for thing
        fig = figure()
        fig.circle([1,2,3], [self.foo, self.bar, self.baz])
        return fig

class Collection:
    def __init__(self, things):
        self.things = things

    def plot(self, x, y):

        # Configure plot
        title = '{} v {}'.format(x, y)
        fig = figure(title=title, tools=['pan', 'tap'])
        taptool = fig.select(type=TapTool)
        taptool.callback = RUN_THING_PLOT_ON_CLICK()

        # Plot data
        xdata = [getattr(th, x) for th in self.things]
        ydata = [getattr(th, y) for th in self.things]
        fig.circle(xdata, ydata)

        return fig

Then I would make a scatter plot of all four Thing sources' 'foo' vs. 'baz' values with:

A = Thing(2, 4, 6)
B = Thing(3, 6, 9)
C = Thing(7, 2, 5)
D = Thing(9, 2, 1)
X = Collection([A, B, C, D])
X.plot('foo', 'baz')

What I would like to have happen here is have each point on the scatter plot able to be clicked. On click, it would run the plot method for the given Thing, making a separate plot of all its 'foo', 'bar', and 'baz' values.

Any ideas on how this can be accomplished?

I know I can just load ALL the data for all the objects into a ColumnDataSource and make the plot using this toy example, but in my real use case the Thing.plot method does a lot of complicated calculations and may be plotting thousands of points. I really need it to actually run the Thing.plot method and draw the new plot. Is that feasible?

Alternatively, could I pass the Collection.plot method a list of all the Thing.plot pre-drawn figures to then display on click?

Using Python>=3.6 and bokeh>=2.3.0. Thank you very much!


Solution

  • I edited your code and sorry i returned too late.

    enter image description here

    from bokeh.plotting import figure, show
    from bokeh.models import TapTool, ColumnDataSource
    from bokeh.events import Tap
    from bokeh.io import curdoc
    from bokeh.layouts import Row
    
    
    class Thing:
        def __init__(self, foo, bar, baz):
            self.foo = foo
            self.bar = bar
            self.baz = baz
    
        def plot(self):
            # Plot all data for thing
            t_fig = figure(width=300, height=300)
            t_fig.circle([1, 2, 3], [self.foo, self.bar, self.baz])
            return t_fig
    
    
    def tapfunc(self):
        selected_=[]
        '''
        here we get selected data. I select by name (foo, bar etc.) but also x/y works. There is a loop because taptool
        has a multiselect option. All selected names adds to selected_
        '''
        for i in range(len(Collection.source.selected.indices)):
            selected_.append(Collection.source.data['name'][Collection.source.selected.indices[i]])
        print(selected_)  # your selected data
    
        # now create a graph according to selected_. I use only first item of list. But you can use differently.
        if Collection.source.selected.indices:
            if selected_[0] == "foo":
                A = Thing(2, 4, 6).plot()
                layout.children = [main, A]
            elif selected_[0] == "bar":
                B = Thing(3, 6, 9).plot()
                layout.children = [main, B]
            elif selected_[0] == 'baz':
                C = Thing(7, 2, 5).plot()
                layout.children = [main, C]
    
    class Collection:
        # Columndata source. Also could be added in __init__
        source = ColumnDataSource(data={
                'x': [1, 2, 3, 4, 5],
                'y': [6, 7, 8, 9, 10],
                'name': ['foo', 'bar', 'baz', None, None]
            })
        def __init__(self):
            pass
    
        def plot(self):
            # Configure plot
            TOOLTIPS = [
                ("(x,y)", "(@x, @y)"),
                ("name", "@name"),
            ]
            fig = figure(width=300, height=300, tooltips=TOOLTIPS)
            # Plot data
            circles = fig.circle(x='x', y='y', source=self.source, size=10)
            fig.add_tools(TapTool())
            fig.on_event(Tap, tapfunc)
            return fig
    
    
    main = Collection().plot()
    
    layout = Row(children=[main])
    
    curdoc().add_root(layout)
    

    The problem is when you select something every time Thing class creates a new figure. It's not recommended. So, you could create all graphs and make them visible/invisible as your wishes OR you could change the source of the graph. You could find lots of examples about changing graph source and making them visible/invisible. I hope it works for you :)