1

Say I have the following code, which switches between two holoviews Points plots based on a Radio Button. If you click on one of the points, a corresponding timeseries plot pops up to the right.

import pandas as pd 
import holoviews as hv 
import panel as pn 
import numpy as np

hv.extension('bokeh')

####Create example data - just copy and past this part####
##########################################################
df = pd.DataFrame(data = {'id':['w', 'x', 'y', 'z'], 'value':[1,2,3,4], 'x':range(4), 'y':range(4)})

#create another dataset, same as the example but with the values flipped
df_qc = df.copy()
df_qc['value'] = df_qc['value'].to_list()[::-1]

#create timeseries for each ID
df_w = pd.DataFrame(data = {'id':['w']*5, 'hour':range(5), 'value':np.random.random(5)})
df_x = pd.DataFrame(data = {'id':['x']*5, 'hour':range(5), 'value':np.random.random(5)})
df_y = pd.DataFrame(data = {'id':['y']*5, 'hour':range(5), 'value':np.random.random(5)})
df_z = pd.DataFrame(data = {'id':['z']*5, 'hour':range(5), 'value':np.random.random(5)})
df_ts = pd.concat([df_w, df_x, df_y, df_z])
df_ts = df_ts.set_index(['id', 'hour'])

#create another set of timeseries, same as the first but with values flipped
df_ts_qc = df_ts.copy()
for id in df_ts_qc.index.unique('id'):
    df_ts_qc.loc[id, 'value'] = df_ts_qc.loc[id, 'value'].to_list()[::-1]

##########################################################


def plot_points(df, df_ts):
    points = hv.Points(data=df, kdims=['x', 'y'], vdims = ['id', 'value'])
    
    stream = hv.streams.Selection1D(source=points).rename(index="index")

    empty_curve = hv.Curve(df_ts.loc['w']).opts(visible = False)
    def tap_station_curve(index):
        if not index:
            curve = empty_curve
        elif index:
            id = df.iloc[index[0]]['id']
            curve = hv.Curve(df_ts.loc[id], label = str(id))
        return curve

    ts_curve = hv.DynamicMap(tap_station_curve, kdims=[], streams=[stream])

    point_options = hv.opts.Points(size = 10, color = 'value', tools = ['tap'])
    panel = pn.Row((points).opts(point_options), ts_curve)
    
    return panel

radio_button = pn.widgets.RadioButtonGroup(options=['df', 'df_qc'])

@pn.depends(radio_button.param.value)
def update_plot(option):
    if option == 'df':
        plot = plot_points(df, df_ts)
    else:
        plot = plot_points(df_qc, df_ts_qc)
    return plot

pn.Column(radio_button, update_plot)

It works fine, but there is one thing I'd like to change. Right now, when I switch between df and df_qc with the Radio button, the zoom level/axis limits reset. If the user has zoomed in on the plot before switching, I want that zoom level/axis limits to stay constant when switching to the other Points plot.

I assume there is some way to do this with storing the current axis limits, and then setting the axis limits to the old one when switching plots...but I can't quite figure it out.

Thanks!

hm8
  • 1,381
  • 3
  • 21
  • 41

1 Answers1

1

Panel is smart about how it handles a HoloViews DynamicMap, updating only the bits that need to change, so you can get it to work by turning that function into a DynamicMap:

import pandas as pd 
import holoviews as hv 
import panel as pn 

hv.extension('bokeh')

df = pd.DataFrame(data = {'value':[1,2,3,4], 'x':range(4), 'y':range(4)})

df_qc = df.copy()
df_qc['value'] = df_qc['value'].to_list()[::-1]

point_options = hv.opts.Points(size = 10, color = 'value')

def plot_points(df):
    points = hv.Points(data=df, kdims=['x', 'y'], vdims = ['value'])
    points = points.opts(point_options)
    return points

radio_button = pn.widgets.RadioButtonGroup(options=['df', 'df_qc'], value='df_qc')

@pn.depends(option=radio_button)
def update_plot(option):
    return plot_points(df if option == 'df' else df_qc)

pn.Column(radio_button, hv.DynamicMap(update_plot))

There may be a more direct way, but this should do it!

James A. Bednar
  • 3,195
  • 1
  • 9
  • 13
  • This does work! But unfortunately, its a situation where me trying to make a "minimal reproducible example" lead to me making it a little too simple. In reality, my "plot_points" function returns a panel object, which DynamicMap does not seem to like being passed as an argument. I've updated my original post. – hm8 Apr 18 '23 at 20:16
  • Excellent MRE, btw! Without it I wouldn't have attempted a solution. In your updated example, I think you should be able to do the same thing by having two separate DynamicMaps, one for each of your two plots, both depending on the same widget. But I don't have time to try to trace out how to separate the logic for the two plots in this more complicated case. If it's not clear how to do that, hopefully someone else can chime in. – James A. Bednar Apr 18 '23 at 21:07
  • I'm still struggling with this. I understand that I could have two `update_plots` functions, one for each of my plots, and then do something like `pn.Column(radio_button, hv.DynamicMap(update_plot1), hv.DynamicMap(update_plot2))`. But the issue is that plot2 is linked to a tap event in plot1. So I think they both need to be updated in the same `plot_points` function, as I have it. I've tried having plot_points return the two plots as a tuple so I can pass the first one into a DynamicMap in the final `pn.Column` call, but that broke things... – hm8 Jun 01 '23 at 18:01