使用 `bokeh` TapTool 打开新图

Open new plot with `bokeh` TapTool

我有一个 class Collection,它包含一堆其他 class 对象 Thing,它们都具有相同的属性,但值不同。 Collection.plot(x, y) 方法绘制了 x 值与所有收集到的 Thing 对象的 y 值的散点图,如下所示:

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

然后我会制作所有四个 Thing 来源的 'foo' 与 'baz' 值的散点图:

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

我希望这里发生的是散点图上的每个点都可以单击。单击时,它将 运行 给定 Thingplot 方法,单独绘制其所有 'foo'、'bar' 和 'baz' 值。

关于如何实现这一点有什么想法吗?

我知道我可以将所有对象的所有数据加载到 ColumnDataSource 中并使用这个玩具示例制作绘图,但在我的实际用例中 Thing.plot 方法做了很多复杂的计算,可能要绘制数千个点。我真的需要它来实际 运行 Thing.plot 方法并绘制新图。可行吗?

或者,我能否将所有 Thing.plot 预绘制图形的列表传递给 Collection.plot 方法,然后在单击时显示?

使用 Python>=3.6 和散景 >=2.3.0。非常感谢!

有两种方法可以做到这一点。这是基本示例。首先,您可以使用 Tap 事件来执行此操作并创建一个函数以从字形中获取信息。其次,您可以直接将源连接到函数。

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)

已选择 return 列表 选定值索引。所以,你应该为你的来源添加索引。您可以使用 with pandas 作为索引。有关选择 check here. So in function you could create a new figure and glyph (line etc.) and update it. 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)

问题是当你 select 每次事情 class 创建一个新人物时。不推荐。因此,您可以根据自己的意愿创建所有图表并使它们 visible/invisible 或者您可以更改图表的来源。您可以找到很多关于更改图源并制作它们的示例 visible/invisible。我希望它对你有用:)