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.
- A
Points graph.
- A
RadioButtonGroup selector.
- A
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. Streams 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)