Ally Alfie
Ally Alfie

Reputation: 141

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

Upvotes: 0

Views: 486

Answers (2)

philippjfr
philippjfr

Reputation: 4080

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

Upvotes: 2

bigreddot
bigreddot

Reputation: 34568

I can't really speak to the Panel abstractions on top of Bokeh, but it's worth mentionfing that actual Bokeh sliders cannot have complicated things such a ColumnDataSource as the options value. Only simple types such a numbers, strings etc. As it happens, this:

ColumnDataSource(id='5489', ...)

Is the "repr" for a CDS, i.e. if you try to print a CDS that the string that is generated. So my guess is that Panel is somewhere converting the CDS to its string representation, and passing that string to the underlying Bokeh Select widge. That is not going to do what you want of course.

There may be a better Panel-specific way to do things, but one approach is to get the underlying Bokeh Select widget, then call js_on_change to add a CustomJS callback that updates the data. There are lots of examples you an emulate in in the JavaScript Callbacks chapter of the docs.

Upvotes: 0

Related Questions