1

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!

Joe Flip
  • 1,076
  • 4
  • 21
  • 37
  • If you want to run real Python code in response to events, that is the primary purpose of a Bokeh server application http://docs.bokeh.org/en/latest/docs/user_guide/server.html – bigreddot Nov 02 '21 at 16:18
  • @bigreddot Looks complicated but promising! Could you provide an example? – Joe Flip Nov 02 '21 at 16:26
  • not complicated think to do that. with server side, with taptool you could create and update new plots. – kağan hazal koçdemir Nov 02 '21 at 18:13
  • Would you consider posting an answer using the toy example @kağanhazalkoçdemir ? I would really appreciate it! – Joe Flip Nov 02 '21 at 19:15

2 Answers2

1

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 :)

-1

There are two ways to do that. This is basic example. First, you could use Tap event to do that and create a function to get information from glyph. Second, you could directly connect source to function.

from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.events import Tap
from bokeh.models import TapTool, ColumnDataSource

def tapfunc():
    print(source.selected.indices)

def sourcefunc(attr, old, new):
    print(source.selected)

source = ColumnDataSource(data={
    'x': [1,2,3,4,5],
    'y': [6,7,8,9,10]
})

p = figure(width=400, height=400)
circles = p.circle(x='x', y='y', source=source, size=20, color="navy", alpha=0.5)

p.add_tools(TapTool())
p.on_event(Tap, tapfunc)

source.selected.on_change('indices', sourcefunc)

curdoc().add_root(p)

selected return a list a selected values index. so, you should add index to your source. You could use with pandas for index. For more information about selection check here. So in function you could create a new figure and glyph (line etc.) and update it. Here, very good example. You could pull and run it from your pc.

  • Thanks for the answer! Could you clarify a bit though or use the example I used for the question or something with class methods? The code you provided throws errors and I'm not sure how to fix it. Also you say there are two ways to do it? Which one is this? Thanks! – Joe Flip Nov 03 '21 at 18:39
  • both. there are two connections as you can see. on_event connect Tap, and second the source. – kağan hazal koçdemir Nov 03 '21 at 18:53
  • if you provide example of data, i could check your code. – kağan hazal koçdemir Nov 03 '21 at 18:54
  • Can you use the working example I provided in the question? I'm trying to keep it as simple as possible. Thanks! – Joe Flip Nov 04 '21 at 17:01
  • Your example doesn't work and I'm not sure how to fix it. Could you debug it so I can figure out how to adapt it please? Thanks! (Sorry, I downvoted just to get your attention! I'll undo it.) – Joe Flip Nov 12 '21 at 17:04