Не удается установить группу флажков Bokeh для обновления графика

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

Мне удалось встроить его в Jupyter, и я написал функцию обратного вызова, которая выполняет код. Я могу создать новый ColumnDataSource из установленного флажка. Однако график просто не обновляется.

Я просмотрел все сообщения здесь, которые мог, и просмотрел все учебные пособия, которые смог найти. У большинства из них просто есть обратный вызов обновления, который создает новый источник, а затем устанавливает новый источник графа, который, как я полагаю, должен обновлять граф. Я также видел варианты, когда люди назначают его как oldsource.data = newsource.data. У меня это тоже не работает.

Мне интересно, есть ли какие-либо неотъемлемые ограничения при встраивании в Jupyter Notebook, для которых мне нужен Javascript, или ограничения на то, как можно обновлять источники. Или, может быть, я просто упускаю что-то очень очевидное? Код ниже:

import os
import pandas as pd
import numpy as np
import bokeh.plotting as bk
import bokeh.layouts as ly
import bokeh.models as md
import bokeh.colors as cl
import bokeh.palettes as plet
from bokeh.io import curdoc
from bokeh.io import show as io_show
from bokeh.models.widgets import CheckboxGroup, Select, Button
from bokeh.plotting import output_file, show, figure, output_notebook, reset_output, curdoc

data_list = ["one", "two", "three", "four", "five", "six"]
data_list2 = ["one", "two"]
data_fac = [1, 2, 3, 4, 5, 6]
data_fac_dict = dict(zip(data_list,data_fac))
data_x = np.linspace(0,100)

df = pd.DataFrame(columns = data_list)

def modify_doc(doc):

    def make_data(data_list):
    #Make new source with appropriate datasets
        df = pd.DataFrame(columns = data_list)

        for case in data_list:
            df[case] = data_x * data_fac_dict[case]

        result = md.ColumnDataSource(df)

        return result

    #Make colors
    list_colors = plet.Dark2[len(data_list)]
    dict_colors = dict(zip(data_list,list_colors))

    #Default source with one datapoint
    src = make_data(["one"])

    print(src.data.keys())

    #Plot graphs
    p = bk.figure()
    for case in src.data.keys():
        if case != "index":
            p.line(source = src, x = 'index', y = case, color = dict_colors[case])  
            print("plotting loop")


    def update(attr,old,new):
    #Callback    
        print("update triggered")
        selection = list()

        for i in wg_chk.active:
            selection.append(data_list[i])

        src = make_data(selection)

        print(selection)


    wg_chk = CheckboxGroup(labels = data_list, active = [0]*len(data_list))        
    wg_chk.on_change('active', update) 

    layout = ly.row(wg_chk,p)
    doc.add_root(layout)

bk.show(modify_doc, notebook_url='localhost:8888')

ОБНОВЛЕНИЕ №1

Я изменил код в обратном вызове, чтобы создать соответствующий фрейм данных, затем создал dict, используя ColumnDataSource.from_df, затем установил src.data равным ему, как показано ниже. По-прежнему не работает. Я использовал печать, чтобы убедиться, что у data_new есть правильные ключи.

df_new = make_df(selection)
data_new = md.ColumnDataSource.from_df(df_new)
src.data = data_new

Для наглядности я использую новейшую версию Bokeh и Python на сегодняшний день (Bokeh 1.0.2, Python 3.7.1).

ОБНОВЛЕНИЕ №2

Согласно комментариям, я заранее сгенерировал все необходимые глифы, поэтому они, по сути, являются «слотами для данных», а не генерируются по запросу для любого количества наборов данных. Поскольку они теперь постоянны, это позволяет мне легко включать и выключать их с помощью свойства .visible. Теперь у меня есть шесть «слотов» для данных, которые должны быть нанесены на график с соответствующими глифами, и я добавил функцию в обратном вызове для обновления соответствующих источников данных (в данном случае, изменив линейную кривую на квадратичную). Я также обновил Bokeh до последней версии (1.3.4). Обратите внимание, что это специально встраивается в Jupyter Notebook.

Вот код для справки:

import os
import pandas as pd
import numpy as np
import bokeh.plotting as bk
import bokeh.layouts as ly
import bokeh.models as md
import bokeh.colors as cl
import bokeh.palettes as plet

from bokeh.io import curdoc
from bokeh.io import show as io_show
from bokeh.models.widgets import CheckboxGroup, Select, Button, RadioGroup
from bokeh.plotting import output_file, show, figure, output_notebook, reset_output, curdoc

data_list = ["one", "two", "three", "four", "five", "six"]
data_list2 = ["one", "two"]
data_fac = [1, 2, 3, 4, 5, 6]
data_fac_dict = dict(zip(data_list,data_fac))
data_x = np.linspace(0,100)

df = pd.DataFrame(columns = data_list)
for case in data_list:
    df[case] = data_x * data_fac_dict[case] + np.power(data_x, 3) * data_fac_dict[case]

def modify_doc(doc):

    #Make colors
    list_colors = plet.Dark2[len(data_list)]
    dict_colors = dict(zip(data_list,list_colors))


    p = bk.figure()

    def make_line(case):
        line = p.line(x = 'index', y = case, source = src_store[case], color = dict_colors[case])
        return line

    #Make six sources, make one line per source, and set them to invisible
    src_store = dict()
    list_lines = dict()

    for case in data_list:
        src_store[case] = md.ColumnDataSource(df[[case]])
        list_lines[case] = make_line(case)
        list_lines[case].visible = False

    #First checkbox defaults to ticked, so let's show it by default.
    list_lines["one"].visible = True

    def modify_data(order):
    #Modify the data and update the six sources' data with it
        df = pd.DataFrame(columns = data_list)
        src_store_new = dict()
        data_new = dict()

        for case in data_list:
            df[case] = data_x * data_fac_dict[case] + np.power(data_x,order) * data_fac_dict[case]
            data_new[case] = md.ColumnDataSource.from_df(df[[case]])
            src_store[case].data = data_new[case]

    def update(attr,old,new):
    #Callback    
        print("update triggered")

        #Get selection of lines to display
        selection = list()
        for i in wg_chk.active:
            selection.append(data_list[i])

        #Set visibility according to selection
        for case in data_list:
            list_lines[case].visible = case in selection

        #Get line multiplier from radio buttons and update sources
        order = wg_rad.active + 1
        modify_data(order)

        print(selection)

    wg_rad = RadioGroup(labels=["x*0", "x*1"], active = 0)
    wg_chk = CheckboxGroup(labels = data_list, active = [0]*len(data_list))

    wg_chk.on_change('active', update)
    wg_rad.on_change('active', update)

    layout = ly.row(ly.column(wg_chk,wg_rad),p)
    doc.add_root(layout)

bk.show(modify_doc, notebook_url='localhost:8888')

person Mike Kryjak    schedule 08.10.2019    source источник
comment
FYI 1.0.2 почти год. Самая новая версия Bokeh на сегодня - 1.3.4.   -  person bigreddot    schedule 08.10.2019


Ответы (1)


Когда вы рисуете глиф Боке, этот глифовый объект имеет связанный источник данных. Если вы хотите обновить глиф, вам необходимо обновить существующий источник данных, т.е. изменить его, установив его свойство .data. Приведенный выше код этого не делает. Он создает новый источник данных, который ни к чему не привязан и не настроен, а затем немедленно выбрасывает его (это локальная переменная в функции, поскольку ничто не хранит ссылку на нее, она исчезает, когда функция завершается).

Вам необходимо обновить любой существующий источник данных, который вы использовали изначально:

source.data = new_data  # plain python dict

И, по крайней мере, с Bokeh 1.3.4 new_data должен быть обычным словарем Python. Не поддерживается "перенос" .data значения из одной CDS в другую:

source1.data = source2.data  # BAD! WILL NOT WORK

Попытка сделать это, вероятно, вызовет явную ошибку в ближайшем будущем. На ColumnDataSource есть from_df статический метод, который можно использовать для преобразования DataFrames в правильный вид dict.

person bigreddot    schedule 08.10.2019
comment
Спасибо - я видел, как вы ответили на другой пост с похожей проблемой, но я не мог принять решение. Я попробовал то, что вы предложили, но не смог заставить его работать - см. Редактирование в основном сообщении. - person Mike Kryjak; 08.10.2019
comment
Глифы настроены для просмотра определенных столбцов в CDS. Вы всегда добавляете только один линейный глиф, который настраивается только для просмотра одного столбца. Добавление новых столбцов не будет иметь никакого эффекта, потому что вы никогда не создаете никаких глифов для просмотра этих столбцов. Похоже, вы должны создать одну CDS для всех данных и глифы для каждого столбца впереди, а затем использовать обратный вызов, чтобы просто переключать, какие из них видны. Это будет намного проще. См., Например, github.com/bokeh/bokeh/blob/master/ примеры / app / line_on_off.py - person bigreddot; 08.10.2019
comment
Спасибо - также за подсказку о версии Bokeh, я там явно что-то упустил. Теперь о последней версии. Я попробую ваше предложение завтра. Я думаю, что неправильно понимаю нечто фундаментальное, связанное с обратными вызовами - я предполагал, что если источник изменится, часть кода будет повторно запущена. На этом основании я подумал, что создам новые глифы. Не могли бы вы объяснить, что именно изменяется или запускается повторно после обратного вызова? Например, если я сделаю глифы в обратном вызове, можно ли их когда-нибудь добавить в сюжет? Если источник можно обновить без обратного вызова, почему другой код не следует? - person Mike Kryjak; 08.10.2019
comment
Вы можете добавлять / удалять глифы в обратных вызовах (хотя я обычно советую избегать этого, если это возможно, поскольку обновление данных - лучшая практика). Однако ваш update обратный вызов выше, который является единственным кодом, который выполняется при изменении состояния флажка active, не добавляет и не удаляет никаких глифов. Единственное, что выполняется в ответ на события или изменения свойств, - это обратные вызовы, которые вы выбираете для указания с помощью on_change или on_event. - person bigreddot; 08.10.2019
comment
Большое спасибо за помощь. Я реализовал как предварительную генерацию всех необходимых глифов, так и переключение невидимости и обновление источников с помощью метода from_df, и все это прекрасно работает .. Я обновлю сообщение для справки. - person Mike Kryjak; 09.10.2019