动态添加和删除散景图例

Dynamically adding and removing Bokeh legends

我正在尝试开发一个相对复杂的绘图应用程序,它有大量 select 数据要绘制。使用下拉菜单,用户可以 select 他们想要绘制的线条。我开发了一个大大简化的代码版本(如下所示)来说明我的应用程序是什么样的。

import bokeh.plotting.figure as bk_figure
import random
import numpy as np
from bokeh.io import show
from bokeh.layouts import row, column, widgetbox
from bokeh.models import ColumnDataSource, Legend, LegendItem, Line
from bokeh.models.widgets import MultiSelect
from bokeh.io import output_notebook # enables plot interface in J notebook
from bokeh.application import Application
from bokeh.application.handlers import FunctionHandler

global x, ys

output_notebook()

plot = bk_figure(plot_width=950, plot_height=800, title="Legend Test Plot"\
        , x_axis_label="X Value", y_axis_label="Y Value")
lines = ['0','1','2']
line_select = MultiSelect(title='Line Select', value = [lines[0]],options=lines)

x = np.linspace(0,10,10)
ys = []
#generates three different lines
for i in range(len(lines)):
    ys.append(x*i)

#add line 0 to plot initially
source = ColumnDataSource(data={'x':x,'y':ys[0]})
glyph = Line(x='x',y='y')
glyph = plot.add_glyph(source,glyph)

def change_line(attr,old,new):

    #remove old lines
    render_copy = list(plot.renderers)
    for line in render_copy:
        plot.renderers.remove(line)

    legend_items = []

    #add selected lines to plot
    for i,line in enumerate(line_select.value):
        y = ys[int(line)]
        source = ColumnDataSource(data={'x':x,'y':y})
        glyph = Line(x='x',y='y')
        glyph = plot.add_glyph(source,glyph)

line_select.on_change('value',change_line)

layout = column(line_select,plot)

def modify_doc(doc):
    doc.add_root(row(layout,width=800))
    doc.title = "PlumeDataVis"

handler = FunctionHandler(modify_doc)
app = Application(handler)
show(app)

我决定在绘图中动态添加和删除线条字形,因为它们是在 MultiSelect 中 selected 的。这是因为如果我简单地隐藏线条,程序的性能会受到影响,因为真实数据集中有太多线条选项。

问题: 我想在图中添加一个图例,它只包含当前图中线字形的条目(真实数据集中有太多的线选项,以至于它们在图例中始终可见。)我在寻找任何资源来帮助解决这个问题时遇到了问题:对于大多数应用程序,像 this 这样的东西就足够了,但这不适用于我定义我正在绘制的线条的方式。

我一直在添加图例 manually,例如:

#add line 0 to plot initially
source = ColumnDataSource(data={'x':x,'y':ys[0]})
glyph = Line(x='x',y='y')
glyph = plot.add_glyph(source,glyph)

#create first legend
legend_item = [LegendItem(label=lines[0],\
                        renderers=[glyph])]
legend = Legend(items=legend_item)
plot.add_layout(legend,place='right')

但我不知道如何在添加图例布局后有效地从图中删除它们。在阅读了 add_layout 的 source code 之后,我意识到您可以使用 getattr(plot,'right') 之类的方法获取给定位置的布局列表。为了使用它,我将 change_line 函数替换为以下内容:

def change_line(attr,old,new):

    #remove old lines
    render_copy = list(plot.renderers)
    for line in render_copy:
        plot.renderers.remove(line)

    #remove old legend
    right_attrs_copy = list(getattr(plot,'right'))
    for legend in right_attrs_copy:
        getattr(plot,'right').remove(legend)

    legend_items = []

    #add selected lines to plot
    for i,line in enumerate(line_select.value):
        y = ys[int(line)]
        source = ColumnDataSource(data={'x':x,'y':y})
        glyph = Line(x='x',y='y')
        glyph = plot.add_glyph(source,glyph)

        legend_items.append(LegendItem(label='line '+str(line),\
                        renderers=[glyph]))


    #create legend
    legend = Legend(items=legend_items)
    plot.add_layout(legend,place='right')

检查绘图的属性,这似乎可以正确添加和删除图例和线条,但它会导致绘图完全停止视觉更新。

有谁知道如何实现这种行为?有可能我什至没有以正确的方式添加图例,但当线条被定义为 Glyph 对象时,我无法弄清楚如何添加它们。

与 chart/model 类 相比,基本字形提供了更大的灵活性。这里可以使用基本的 line(不是 Line)字形。

在下面的代码中,我将基本字形添加到图表中。我将字形保存在字典中,稍后可以操作(正如 OP 所说,它是一个复杂的应用程序,我相信稍后会用到)。我已经评论了 ColumnDataSource 的创建,因为它可以通过相应字形的 data_source.data 访问(现在保存在字典中)。

另外,由于我们现在是一条一条地创建线条,因此需要为不同的线条提供颜色。我使用 bokeh.palette 函数生成多种颜色。更多信息请阅读 here

import bokeh.plotting.figure as bk_figure
import random
import numpy as np
from bokeh.io import show
from bokeh.layouts import row, column, widgetbox
from bokeh.models import ColumnDataSource, Legend, LegendItem, Line
from bokeh.models.widgets import MultiSelect
from bokeh.io import output_notebook # enables plot interface in J notebook
from bokeh.application import Application
from bokeh.application.handlers import FunctionHandler
import bokeh.palettes

#change the number as per the max number of glyphs in system
palette = bokeh.palettes.inferno(5)

global x, ys

output_notebook()

plot = bk_figure(plot_width=950, plot_height=800, title="Legend Test Plot"\
        , x_axis_label="X Value", y_axis_label="Y Value")
lines = ['0','1','2']
line_select = MultiSelect(title='Line Select', value = [lines[0]],options=lines)

x = np.linspace(0,10,10)
ys = []
#generates three different lines
for i in range(len(lines)):
    ys.append(x*i)

linedict = {}    

#add line 0 to plot initially
#source = ColumnDataSource(data={'x':x,'y':ys[0]})
#glyph = Line(x='x',y='y')
#glyph = plot.add_glyph(source,glyph)
l1 = plot.line(x = x, y= ys[0], legend=str(0), color = palette[0])
linedict[str(0)] = l1


def change_line(attr,old,new):

    #remove old lines
    render_copy = list(plot.renderers)
    for line in render_copy:
        plot.renderers.remove(line)

    legend_items = []

    #add selected lines to plot
    for i,line in enumerate(line_select.value):
        y = ys[int(line)]
        #source = ColumnDataSource(data={'x':x,'y':y})
        l1 = plot.line(x = x, y= y, legend=line, color = palette[i])
        #linedict[line] = l1
        glyph = Line(x='x',y='y', legend=line, color = palette[i])
        glyph = plot.add_glyph(source,glyph)

line_select.on_change('value',change_line)

layout = column(line_select,plot)

def modify_doc(doc):
    doc.add_root(row(layout,width=800))
    doc.title = "PlumeDataVis"

handler = FunctionHandler(modify_doc)
app = Application(handler)
show(app)

千辛万苦终于弄明白了( link 很有帮助)。 @Eugene Pakhomov 是正确的,因为我在初始代码中删除了线条和图例是一个问题。相反,关键是仅当用户请求绘制新的最大行数时才初始化新行。在所有其他情况下,您只需编辑现有行的 data_source。这允许程序避免在用户只想绘制总选项中的一个或两个时绘制和隐藏所有线条。

您可以在每次更新时将图例设置为空,然后根据需要添加条目,而不是删除和重新制作图例。

以下代码在 Jupyter Notebook 运行 bokeh 1.4.0 中为我工作:

from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Legend, LegendItem, Line
from bokeh.models.widgets import MultiSelect
from bokeh.io import output_notebook
from bokeh.application import Application
from bokeh.application.handlers import FunctionHandler
from bokeh.palettes import Category10 as palette

output_notebook()

plot = bk_figure(plot_width=750, plot_height=600, title="Legend Test Plot"\
        , x_axis_label="X Value", y_axis_label="Y Value")
lines = ['0','1','2']
line_select = MultiSelect(title='Line Select', value = [lines[0]],options=lines)

x = np.linspace(0,10,10)

ys = []
#generates three different lines with 0,1, and 2 slope
for i in range(len(lines)):
    ys.append(x*i)

#add line 0 to plot initially
source = ColumnDataSource(data={'x':x,'y':ys[0]})
glyph = Line(x='x',y='y')
glyph = plot.add_glyph(source,glyph)

#intialize Legend
legend = Legend(items=[LegendItem(label=lines[0],renderers=[glyph])])
plot.add_layout(legend)

def change_line(attr,old,new):
    plot.legend.items = [] #reset the legend

    #add selected lines to plot
    for i,line in enumerate(line_select.value):
        line_num = int(line)
        color = palette[10][i]

        #if i lines have already been plotted in the past, just edit an existing line
        if i < len(plot.renderers):
            #edit the existing line's data source
            plot.renderers[i]._property_values['data_source'].data = {'x':x, 'y':ys[line_num]}

            #Add a new legend entry
            plot.legend.items.append(LegendItem(label=line,renderers=[plot.renderers[i]]))

        #otherwise, initialize an entirely new line
        else:
            #create a new glyph with a new data source
            source = ColumnDataSource(data={'x':x,'y':ys[line_num]})
            glyph = Line(x='x',y='y',line_color=color)
            glyph = plot.add_glyph(source,glyph)

            #Add a new legend entry
            plot.legend.items.append(LegendItem(label=line,renderers=[plot.renderers[i]]))

    #'Remove' all extra lines by making them contain no data
    #instead of outright deleting them, which Bokeh dislikes
    for extra_line_num in range(i+1,len(plot.renderers)):
        plot.renderers[extra_line_num]._property_values['data_source'].data = {'x':[],'y':[]}


line_select.on_change('value',change_line)

layout = column(line_select,plot)

def modify_doc(doc):
    doc.add_root(row(layout,width=800))
    doc.title = "PlumeDataVis"

handler = FunctionHandler(modify_doc)
app = Application(handler)
show(app)