Consider the following code that creates a Points plot that changes which DataFrame it is plotting based on a RadioButton.
import pandas as pd
import panel as pn
import holoviews as hv
hv.extension('bokeh')
df_a = pd.DataFrame(index = ['a','b', 'c', 'd'], data = {'x':range(4), 'y':range(4)})
df_b = pd.DataFrame(index = ['w','x', 'y', 'z'], data = {'x':range(4), 'y':range(3,-1,-1)})
radio_button = pn.widgets.RadioButtonGroup(options=['df_a', 'df_b'])
@pn.depends(option = radio_button.param.value)
def update_plot(option):
if option == 'df_a':
points = hv.Points(data=df_a, kdims=['x', 'y'])
if option == 'df_b':
points = hv.Points(data=df_b, kdims=['x', 'y'])
points = points.opts(size = 10, tools = ['tap'])
return points
pn.Column(radio_button, hv.DynamicMap(update_plot))
What I would like to add is functionality where when one of the points is tapped, a table to the right is filled in with location information from the corresponding DataFrame (i.e. if the lower left point is tapped when df_a
is selected, the data at df_a.loc['a']
should be printed in a table.
I’ve tried a few things, but I can’t find a good way that 1) Updates the table on new clicks and 2) doesn’t reset the zoom level whenever the RadioButton selection is switched.
Number 2 is particularly important for my actual purpose (this is an extremely stripped down version).
This is a complicated question, mostly because holoviews is a more advanced library, but after a bunch of digging, I figured it out.
We've got three main objects in our program.
Points
graph.RadioButtonGroup
selector.Table
view.Both the Points
and the Table
views are to update dynamically based on the RadioButtonGroup
, and the Table
view is also going to update based on the selected Point
.
So, we're going to need two Stream
objects. One, a Selection1D()
, so we know when a Point
is selected. Two, a custom Stream based on the RadioButtonGroup
. But we'll get to that in a second.
We've got your imports...
import holoviews as hv
from holoviews import streams
import panel as pn
import pandas as pd
from bokeh.models import RadioButtonGroup
hv.extension('bokeh')
And your given data.
df_a = pd.DataFrame(index = ['a','b', 'c', 'd'], data = {'x':range(4), 'y':range(4)})
df_b = pd.DataFrame(index = ['w','x', 'y', 'z'], data = {'x':range(4), 'y':range(3,-1,-1)})
Also, for convenience, let's make a dictionary based on the names, so we can easily reference the data from the RadioButtonGroup
. And I'll make a variable to keep track of the current DataFrame
dfs = {'df_a': df_a, 'df_b': df_b}
current_df = df_a #this will change
Here's where we define our DynamicMaps
and the RadioButtonGroup
. Stream
s included too. I added some comments so it is more clear.
radio_button_group = RadioButtonGroup(labels=['df_a', 'df_b'], active=0)
#STREAMS HERE. VERY IMPORTANT
selection_stream = streams.Selection1D() #updates when Point selected
selected_df = streams.Stream.define('selected_df', df=current_df) #updates when RadioButtonGroup selection changes.
def radio_button_callback(attr, old, new): #attr not used. old is previous value of radio_button
global current_df
current_df = dfs[list(dfs.keys())[new]] #set current_df
dynamic_map.event(df=current_df) #these events update the map and table.
dynamic_table.event(df=current_df)
selection_stream.source = dynamic_map
radio_button_group.on_change("active", radio_button_callback) #trigger for callback
#keywords must be called index and df.
def update_table(index=current_df.index, df=current_df):
if index == []: #this happens when the plot is clicked but no Point is selected.
index = [x for x in range(len(current_df.index))]
selected_df = current_df.iloc[index]
return hv.Table(selected_df)
def update_plot(index=0, df=current_df):
points = hv.Points(data=df)
return points.opts(size = 10, tools = ['tap'])
dynamic_map = hv.DynamicMap(update_plot, streams=[selection_stream, selected_df()])
dynamic_table = hv.DynamicMap(update_table, streams=[selection_stream, selected_df()])
And finally, add your Panel
formatting.
column_layout = pn.Column(radio_button_group, dynamic_map)
row_layout = pn.Row(dynamic_table, column_layout)
Final Result: Streamable Link
Please let me know if this is not the intended result, or if I am missing something important. Also, I forgot to record this, but the DynamicMap
keeps the scale of the Points
graph the same, even if you change the DataFrame
, fulfilling your second requirement.
Hope this helped!
--
EDIT AFTER COMMENT
To have a dynamically updating title, the easiest method is to probably define another variable current_df_index
(set it to 0 of course).
Then, in the method radio_button_callback
, add current_df_index
to the global variables, and set it to new
.
current_df_index = new
Finally, in the update_table
method, let's change the return statement to a variable instead, assign the title, and then return.
table = hv.Table(selected_df)
table.opts(title=list(dfs.keys())[current_df_index])
return table
--
Here is the full updated code.
import holoviews as hv
from holoviews import streams
import panel as pn
import pandas as pd
from bokeh.models import RadioButtonGroup
hv.extension('bokeh')
df_a = pd.DataFrame(index = ['a','b', 'c', 'd'], data = {'x':range(4), 'y':range(4)})
df_b = pd.DataFrame(index = ['w','x', 'y', 'z'], data = {'x':range(4), 'y':range(3,-1,-1)})
dfs = {'df_a': df_a, 'df_b': df_b}
current_df = df_a #this will change
current_df_index = 0
radio_button_group = RadioButtonGroup(labels=['df_a', 'df_b'], active=0)
#STREAMS HERE. VERY IMPORTANT
selection_stream = streams.Selection1D() #updates when Point selected
selected_df = streams.Stream.define('selected_df', df=current_df) #updates when RadioButtonGroup selection changes.
def radio_button_callback(attr, old, new): #attr not used. old is previous value of radio_button
global current_df, current_df_index
current_df = dfs[list(dfs.keys())[new]] #set current_df
current_df_index = new
dynamic_map.event(df=current_df) #these events update the map and table.
dynamic_table.event(df=current_df)
selection_stream.source = dynamic_map
radio_button_group.on_change("active", radio_button_callback) #trigger for callback
#keywords must be called index and df.
def update_table(index=current_df.index, df=current_df):
if index == []: #this happens when the plot is clicked but no Point is selected.
index = [x for x in range(len(current_df.index))]
selected_df = current_df.iloc[index]
table = hv.Table(selected_df)
table.opts(title=list(dfs.keys())[current_df_index])
return table
def update_plot(index=0, df=current_df):
points = hv.Points(data=df)
return points.opts(size = 10, tools = ['tap'])
dynamic_map = hv.DynamicMap(update_plot, streams=[selection_stream, selected_df()])
dynamic_table = hv.DynamicMap(update_table, streams=[selection_stream, selected_df()])
column_layout = pn.Column(radio_button_group, dynamic_map)
row_layout = pn.Row(dynamic_table, column_layout)