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