Reputation: 141
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?
Upvotes: 0
Views: 486
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
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