0

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

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
hm8
  • 1,381
  • 3
  • 21
  • 41

1 Answers1

1

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.

  1. A Points graph.
  2. A RadioButtonGroup selector.
  3. 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)
SanguineL
  • 1,189
  • 1
  • 7
  • 18
  • This has been super helpful!! One question though - can you figure out a way I can get the title of the table to switch between "df_a" and "df_b", depending on what radio button is selected? The title can be set with `.opts(title = ...)`, and I've tried a few ideas, but I can't seem to get anything that updates properly... – hm8 Jul 21 '23 at 20:27
  • 1
    @hm8 Updated the answer. Hope it is what you are looking for. – SanguineL Jul 24 '23 at 13:22