Conceptual approach for using picklists, Selection1D points and hv.Table.apply

Hello holoviews community!

I have a model with multiple plots (central map with points, and some additional bar charts) where the main map dataset is driven by a hv.DynamicMap (returning a hv.Table) based on several picklists, and then the bar charts are generated using hv_table.apply() with a selectionstream from the map.

It works beautifully UNLESS I lasso a set of points and then change the picklists for the main map; in which case the Selection1D stream based on the lasso tool will hit index bound issues (e.g. I select 200 points on the map with filter1=“A”, but changing filter1–>“B” results in an hv_table with only 150 rows; so now my bar charts which have index slicing “cached” from filter1=“A” + Selection1D stream crash because the indices are misaligned).

I can think of two rough pseudo-code approaches to attempt next:

  • if I could clear the selection index programmatically when any of the pn.widget.Select objects that hv_table depends on change, that would likely solve the issues, but:
  1. it is not clear how I’d clear the selection index (Can I change which points are selected programmatically? - #2 by sbi_vm)
  2. how could I ensure that the theoretical “clear_index()” function triggers before the dynamic map updates trigger for each pn.widget.Select event?
  • Instead of filtering my main geodataframe with a DynamicMap, maybe I could just use the dynamicmap to set the alpha=0.0 for the “out of scope” points? so that the index structure is consistent between the permutations of pd.widget values, but just the visualization changes? …but then a bunch of complexity gets pushed to all the related legends/labels/color maps I think…

Am I missing something obvious here? Anyone else have a suggestion for handling the overlap of selection streams and widgets–>DynamicMap ?

Here is a toy example: making a selection on dataset “A” then switching to dataset “B” causes issues:

import numpy as np
import holoviews as hv
from holoviews import dim
from holoviews import streams
import panel as pn
import pandas as pd
import random

hv.extension('bokeh')

data_sets = {}

cat_grp = ["apple", "banana", "pear"]

data_sets["A"] = pd.DataFrame(data=[(np.random.random(),np.random.random(),random.choice(cat_grp)) for i in range(2000)],columns=['x','y','fruit'])
data_sets["B"] = pd.DataFrame(data=[(np.random.random(),np.random.random(),random.choice(cat_grp)) for i in range(20)],columns=['x','y','fruit'])

dataset_selector =  pn.widgets.Select(options=list(data_sets.keys()),
                                                    name="Dataset",
                                                    value="A",
                                                    width=250)

@pn.depends(ds = dataset_selector.param.value)
def set_selector(ds):
    return hv.Table(data_sets[ds],kdims=['x','y'],vdims=['fruit'])
    
hv_table = hv.DynamicMap(set_selector)

points = hv_table.apply(hv.Points).opts(tools=['lasso_select'])

sel = streams.Selection1D(source=points)

def clear_sel(event):
    print(f"old index was: {sel.index}")
    sel.event(index=[])
    print(f"new index is: {sel.index}")
    
def set_sel(event):
    print(f"old index was: {sel.index}")
    sel.event(index=[1,2,3])
    print(f"new index is: {sel.index}")
    
def summary_table_sel(table,index):
    if len(index)>0:
        selected_df = table.data.iloc[index]
    else:
        selected_df = table.data
    
    agg_table = selected_df.groupby('fruit').agg({'x':'count'}).reset_index()
    agg_table.rename(columns={'x':'# of records'},inplace=True)
    
    return hv.Table(agg_table,kdims=['fruit'],vdims=['# of records'])

summary_table = hv_table.apply(summary_table_sel,streams=[sel])

plt = points.opts(color='k', marker='x', size=10)
clr_btn = pn.widgets.Button(name='Clear Selection', button_type='primary')
clr_btn.on_click(callback = lambda event: clear_sel(event=event))

set_btn = pn.widgets.Button(name='Set Selection', button_type='primary')
set_btn.on_click(callback = lambda event: set_sel(event=event))

layout = pn.Row(plt,pn.Column(dataset_selector,clr_btn,set_btn),summary_table)
layout

It looks like I can manually click “Clear Selection” before switching A–>B and it prevents the crash, but the plot doesn’t update to show that the selection was cleared. (also I’d have to figure out how to trigger the clear_sel() function as the first event trigger off the dataset_selector change…)

Looks like I can trigger the back-end index clear if the first watcher I register is the clear function:

dataset_selector =  pn.widgets.Select(options=list(data_sets.keys()),
                                                    name="Dataset",
                                                    value="A",
                                                    width=250)
def clear_sel(event):
    print(f"old index was: {sel.index}")
    sel.event(index=[])
    print(f"new index is: {sel.index}")
    
dataset_selector.param.watch(lambda event:clear_sel(event), 'value')

…and it prevents the crash from index bounds, but the selected points on the plot itself don’t reflect that programmatic selection change.