Change ColumnDataSource columns and get '...nonexistent field...'

Hi, I am using bokeh server to show some data. The main function is to filter different columns to show, but when it is updated, I get:[bokeh] attempted to retrieve property array for nonexistent field 'asds#asd'' in browser console. Why it doesn’t work?


import re

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from bokeh.layouts import column, row, layout
from bokeh.plotting import curdoc, figure
from bokeh.models import ColumnDataSource, Select



def load_source(col):
    data = {
        'time': pd.date_range('2021-10-1T00', periods=10, freq='D'),
        'asds#asd': np.linspace(0, 10, 10),
        'bas&da': np.linspace(10, 20, 10),
        'cas#da': np.linspace(20, 30, 10),
        'ds&fsf': np.linspace(30, 40, 10)
    }
    df = pd.DataFrame(data)
    df.set_index('time', drop=True, inplace=True)
    filtered_data = df.filter(regex=re.compile(r'time|' + col), axis=1)
    # print(filtered_data)
    return ColumnDataSource(data=filtered_data)

def gen_colors(n):
    color_arr = plt.cm.Set3(np.linspace(0, 1, n)).tolist()
    for i in color_arr:
        i[0] = int(i[0]*255)
        i[1] = int(i[1]*255)
        i[2] = int(i[2]*255)
        i[3] = float(i[3]*1.0)
        i = tuple(i)
    return color_arr

def make_plot(source, title):
    p1 = figure(width=1000, height=600, title=title, x_axis_type='datetime') 
    color_list = gen_colors(len(source.column_names))
    for i, col in enumerate(source.column_names):
        if col != 'time':
            p1.line(
                x='time', y=col, source=source, 
                line_width=2, legend_label=col, color=color_list[i]
            )
    return p1
    
def update_plot(attr, old, new):
    col = col_select.value
    plot.title.text = col

    src = load_source(col)
    print(source.data, '\n')
    # source.data.update(src.data)
    source.data = dict(src.data)
    print(source.data)

init_col = '#'
col_select = Select(title='type', value=init_col, width=200, 
    options=['#', '&'])

source = load_source(init_col)
plot = make_plot(source, init_col)

col_select.on_change('value', update_plot)

curdoc().add_root(row(plot, col_select))

Because you configured the line glyphs up front to look for those column names (and that never changes, they always look for those column names) but then you delete those columns from the data source in your update callback.

A much simpler approach would be this: Add all the data, and lines for all the data, once, up front. Then have a very simple callback that simply toggles the visibility of the lines as appropriate.

Thank you! I follow your advice and add some other code to avoid the legend of the whole columns taking too much space by refering(Updating legend label using callback - #4 by p-himik). Now it’s great!

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from bokeh.layouts import column, row, layout
from bokeh.plotting import curdoc, figure
from bokeh.models import ColumnDataSource, Select, Legend, LegendItem



def load_source():
    data = {
        'time': pd.date_range('2021-10-1T00', periods=10, freq='D'),
        'asdsxasd': np.linspace(0, 10, 10),
        'basyda': np.linspace(10, 20, 10),
        'casxda': np.linspace(20, 30, 10),
        'dsyfsf': np.linspace(30, 40, 10)
    }
    df = pd.DataFrame(data)
    df.set_index('time', drop=True, inplace=True)
    return ColumnDataSource(data=df), df

def gen_colors(n):
    color_arr = plt.cm.Set3(np.linspace(0, 1, n)).tolist()
    for i in color_arr:
        i[0] = int(i[0]*255)
        i[1] = int(i[1]*255)
        i[2] = int(i[2]*255)
        i[3] = float(i[3]*1.0)
        i = tuple(i)
    return color_arr

def make_plot(source, title):
    p1 = figure(width=1000, height=600, title=title, x_axis_type='datetime') 
    color_list = gen_colors(len(source.column_names))
    leg_list = []
    for i, col in enumerate(source.column_names):
        if col != 'time':
            temp_name = p1.line(
                x='time', y=col, source=source, line_width=2,
                color=color_list[i], visible=False, name=col
            )
            leg_list.append(LegendItem(label=col, renderers=[temp_name]))
    return p1, leg_list
    

init_col = 'origin'

col_select = Select(title='type', value=init_col, width=200, 
    options=['origin', 'x', 'y'])

source, df = load_source()
plot, legend_list = make_plot(source, init_col)

legend = Legend(items=legend_list)

def update_plot(attr, old, new):
    col = col_select.value
    plot.title.text = col
    leg_list = []
    for i in df.columns:
        if col in i:
            plot.select_one(i).visible = True
            leg_list.append(LegendItem(label=i, renderers=[plot.select_one(i)]))
        else:
            plot.select_one(i).visible = False

    legend.items = leg_list
col_select.on_change('value', update_plot)


plot.add_layout(legend)
curdoc().add_root(row(plot, col_select))
1 Like