javascriptpythonbokehpanel-pyviz

Code not recognizing ColumnDataSource for what it is


I want to change the data source of a simple line plot depending on what the user picks from a dropdown menu.

I have 2 dataframes, the weight and age of myself and my boyfriend.

my_weight = [60,65,70] 
my_age = [21,22,25]

d_weight = [65,70,80] 
d_age = [21,22,25]

me = pd.DataFrame(list(zip(my_weight, my_age)), 
               columns =['weight', 'age'], index=None) 

dillon = pd.DataFrame(list(zip(d_weight, d_age)), 
               columns =['weight', 'age'], index=None)

I turn these two dataframe into ColumnDataSource objects, create my plot and line, add my dropdown and jslink. There is also a demo slider to show how I can change the line_width of my line.

from bokeh.models import ColumnDataSource
from bokeh.core.properties import Any, Bool, ColumnData

pn.extension()

source = ColumnDataSource(me, name="Me")
source2 = ColumnDataSource(dillon, name="Dillon")
# print("Me: ", source.data, "Dillon: ", source2.data)

plot = figure(width=300, height=300)
myline = plot.line(x='weight', y='age', source=source, color="pink")

width_slider = pn.widgets.FloatSlider(name='Line Width', start=0.1, end=10)
width_slider.jslink(myline.glyph, value='line_width')

dropdown2 = pn.widgets.Select(name='Data', options=[source, source2])
dropdown2.jslink(myline, value='data_source')

pn.Column(dropdown2, width_slider, plot)

When I run this code, I get the error

ValueError: expected an instance of type DataSource, got ColumnDataSource(id='5489', ...) of type str

with the error occurring from the dropdown2 section of code.

Whats preventing the code from recognizing source and source2 as ColumnDataSource() objects? What is meant by got ColumnDataSource(id='5489', ...) of type str? How is it a string?

enter image description here


Solution

  • There are multiple problems here. The first is that the Select widget does not actually make complex objects available in Javascript, so callbacks that try to access those models in a JS callback will not work. The only solution therefore is to write an actual JS callback and provide the actual models as args. The secondary complication here is that the two data sources contain different columns, specifically the 'Me' ColumnDataSource contains an age column and the 'Dillon' data source contains a height column. This means you also need to update the glyph to look at those different sources. In practice this looks like this:

    import panel as pn
    
    from bokeh.plotting import figure
    from bokeh.models import ColumnDataSource, DataRange1d
    from bokeh.core.properties import Any, Bool, ColumnData
    
    source = ColumnDataSource(me, name="Me")
    source2 = ColumnDataSource(dillon, name="Dillon")
    
    plot = figure(width=300, height=300)
    myline = plot.line(x='weight', y='age', source=source, color="pink")
    
    width_slider = pn.widgets.FloatSlider(name='Line Width', start=1, end=10)
    width_slider.jslink(myline.glyph, value='line_width')
    dropdown2 = pn.widgets.Select(name='Data', options={'Me': source, 'Dillon': source2})
    
    code = """
    if (cb_obj.value == 'Me') {
      myline.data_source = source
      myline.glyph.y = {'field': 'age'}
    
    } else {
      myline.data_source = source2
      myline.glyph.y = {'field': 'height'}
    }
    """
    dropdown2.jscallback(args={'myline': myline, 'source': source, 'source2': source2}, value=code)
    

    That being said, the way I would recommend to implement this in Panel is this:

    dropdown2 = pn.widgets.Select(name='Data', options={'Me': me, 'Dillon': dillon}, value=me)
    width_slider = pn.widgets.FloatSlider(name='Line Width', start=1, end=10)
    
    @pn.depends(dropdown2)
    def plot(data):
        source = ColumnDataSource(data)
        plot = figure(width=300, height=300)
        column = 'age' if 'age' in source.data else 'height'
        myline = plot.line(x='weight', y=column, source=source, color="pink")
        width_slider.jslink(myline.glyph, value='line_width')
        return plot
    
    pn.Column(dropdown2, width_slider, plot).embed()
    

    Finally, if you're willing to give hvPlot a go, this can be further reduced to:

    import hvplot.pandas
    
    dropdown2 = pn.widgets.Select(name='Data', options={'Me': me, 'Dillon': dillon}, value=me)
    width_slider = pn.widgets.FloatSlider(name='Line Width', start=1, end=10)
    
    @pn.depends(dropdown2)
    def plot(data):
        p = data.hvplot('weight', color='pink')
        width_slider.jslink(p, value='glyph.line_width')
        return p
    
    pn.Column(dropdown2, width_slider, plot).embed()