Динамическое добавление и удаление легенд Bokeh - PullRequest
0 голосов
/ 10 апреля 2020

Я пытаюсь разработать относительно сложное приложение для построения графиков, в котором есть огромный выбор данных для построения. Используя выпадающие списки, пользователь может выбрать, какие линии он хотел бы построить. Я разработал значительно упрощенную версию кода (показанную ниже), чтобы проиллюстрировать, на что похоже мое приложение.

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. Это потому, что если я просто скрываю строки, производительность программы ухудшается, учитывая, что в реальном наборе данных так много параметров строк.

Проблема: Я хочу добавить легенду на график, который содержит только записи для глифов линий, которые в данный момент находятся на графике (в реальном наборе данных слишком много параметров линий, чтобы все они были видны в легенде в любое время.) У меня возникли проблемы с поиском любые ресурсы, которые помогут с этим: для большинства приложений что-то вроде этого достаточно, но это не работает с тем, как я определил линии, которые я строю.

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)

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

, но я не могу понять, как эффективно удалять макеты легенд из графика после их добавления. Прочитав исходный код для add_layout, я понял, что вы можете получить список макетов в данном месте, используя что-то вроде 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')

Проверяя атрибуты графика, это, кажется, добавляет и удаляет легенды и линии правильно, но заставляет график полностью останавливаться визуально обновление.

Кто-нибудь знает, как выполнить sh такое поведение? Возможно, я даже не добавляю легенду правильным образом, но я не мог понять, как еще добавить их, когда строки определены как объекты Glyph.

Ответы [ 2 ]

0 голосов
/ 14 апреля 2020

После долгих guish я наконец-то понял ( эта ссылка была полезной). @ Евгений Пахомов был прав в том, что тот факт, что я удалил строки и легенды в моем исходном коде, был проблемой. Вместо этого ключ должен был инициализировать новую строку только тогда, когда пользователь запросил построить новое максимальное количество линий. Во всех остальных случаях вы можете просто отредактировать источник данных существующих строк. Это позволяет программе избегать построения и скрытия всех линий, когда пользователь хочет построить только один или два из общих параметров.

Вместо удаления и повторного преобразования условных обозначений вы можете установить его пустым на каждое обновление, а затем добавляйте записи по мере необходимости.

Следующий код работал для меня в 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)
0 голосов
/ 11 апреля 2020

Basi c Глифы обеспечивают большую гибкость по сравнению с классами диаграммы / модели. Здесь можно использовать глиф basi c line (не Line).

В приведенном ниже коде я добавляю глифы basi c на график. Я сохраняю глифы в словаре, который можно использовать позже (поскольку OP сказал, что это сложное приложение, я уверен, что это будет использовано позже). Я прокомментировал создание ColumnDataSource, так как оно будет доступно через data_source.data соответствующих глифов (теперь сохранено в словаре).

Кроме того, поскольку сейчас мы создаем строки одну за другой, цвет должен быть предусмотрен для разных линий. Я использовал функцию bokeh.palette для генерации нескольких цветов. Подробнее об этом можно прочитать здесь

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