Selection highlighting

Linking the selection of a particular annotator to highlight the associated curve (line_alpha=1) is sort of working, but I can’t seem to get the previously selected lines to go back to being unhighlighted (line_alpha=.5) when selecting the next line… Any tips?

from holonote.annotate import Annotator
from holonote.app import PanelWidgets
from holonote.app.tabulator import AnnotatorTable
import holoviews as hv; hv.extension('bokeh')
import panel as pn; pn.extension('tabulator')
import numpy as np
import xarray as xr

# Annotator
annotator = Annotator({"height": float, "width": float}, fields=["type"], groupby="type")
color_dim = hv.dim("type").categorize(
    categories={"A": "red", "B": "orange", "C": "cyan"}, default="grey"
)
annotator.style.color = color_dim
annotator.style.alpha = .5
panel_widgets = PanelWidgets(annotator)
table_widget = AnnotatorTable(annotator)
annotator_widgets = pn.WidgetBox(panel_widgets, table_widget, horizontal=True)

# Data
width, height, frame = 30, 40, 50
frames = np.arange(frame)
data = np.random.random((width, height, frame))

da = xr.DataArray(
    data,
    dims=["width", "height", "frame"],
    coords={
        "width": np.arange(width),
        "height": np.arange(height),
        "frame": frames
    },
    name="data"
)

# App
def sel_frame(iframe):
    return hv.Image(da.sel(frame=iframe)).opts(title='Image')
    
frame_slider = pn.widgets.DiscreteSlider(options=list(frames), value=frames[0])
dyn_img = hv.DynamicMap(pn.bind(sel_frame, frame_slider))

ts = pn.pane.HoloViews(hv.Curve([]).opts(xlim = (frames[0], frames[-1]),
    title='Create an annotation in the image'))

def plot_ts(event):
    curves = {}
    df = annotator.df
    for i, row in df.iterrows():
        group = f'{row['type']}'
        label = f'{i[:6]}'
        h1, h2, w1, w2 = row[["start[height]", "end[height]", "start[width]", "end[width]"]]
        da_sel = da.sel(height=slice(h1, h2), width=slice(w1, w2))
        curve = hv.Curve(da_sel.mean(["height", "width"]), 'frame', group=group, label=label)
        curve = curve.opts(subcoordinate_y=True,
                           color=panel_widgets.colormap[group],
                          line_alpha=.5)
        curves[(group, label)] = curve
    ts.object = (hv.Overlay(curves, kdims=['annotation'])).opts(
        title='Timeseries', xlim = (frames[0], frames[-1]),
        show_legend=False,)

annotator.on_event(plot_ts)

annotator.set_regions(height=(5,15), width=(5,15))
annotator.add_annotation(type='A')
annotator.set_regions(height=(25,30), width=(25,30))
annotator.add_annotation(type='B')

def highlight_selected(idx):
    if len(idx) == 1:
        idx = idx[0]
        # highlight alpha of selected curve
        row = annotator.df.loc[idx]
        group = f'{row['type']}'
        label = f'{idx[:6]}'
        ts.object = ts.object.opts(hv.opts.Curve(f'{group}.{label}', line_alpha=1), hv.opts.Curve(line_alpha=.5))

pn.bind(highlight_selected, annotator.param.selected_indices, watch=True)
pn.Column(annotator_widgets, pn.Row(pn.Column(annotator * dyn_img, frame_slider), ts))

Simplified the example to remove holonote:

# note, in the real case, we don't know a prior how many curves there will be
df = pd.DataFrame({'A': np.random.random(4)}, index=list(np.arange(4)))
table = pn.widgets.Tabulator(df, disabled=True)

curves = {}
for i, row in df.iterrows():
    group = f'Group{i}'
    label = f'Label{i}'
    curves[(group, label)] = hv.Curve(np.random.random(10), 'frame', 'val', group=group, label=label).opts(
        subcoordinate_y=True,
    line_alpha=.5)
plot = hv.Overlay(curves).opts(show_legend=False)
pn_plot = pn.pane.HoloViews(plot)

def show_selected(event):
    idx = df.iloc[event.row].name
    group = f'Group{idx}'
    label = f'Label{idx}'
    pn_plot.object = pn_plot.object.opts(hv.opts.Curve(line_alpha=.5), hv.opts.Curve(f'{group}.{label}', line_alpha=1))
    
table.on_click(show_selected)
pn.Row(table,  pn_plot)

Thanks for simplifying it.

One way is clearing all the opts and re-applying them.

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

hv.extension("bokeh")

DEFAULT_CURVE_OPTS = hv.opts.Curve(subcoordinate_y=True, line_alpha=0.5)

# note, in the real case, we don't know a prior how many curves there will be
df = pd.DataFrame({"A": np.random.random(4)}, index=list(np.arange(4)))
table = pn.widgets.Tabulator(df, disabled=True)

curves = {}
for i, row in df.iterrows():
    group = f"Group{i}"
    label = f"Label{i}"
    curves[(group, label)] = hv.Curve(
        np.random.random(10), "frame", "val", group=group, label=label
    ).opts(DEFAULT_CURVE_OPTS)
plot = hv.Overlay(curves).opts(show_legend=False)
pn_plot = pn.pane.HoloViews(plot)


def show_selected(event):
    idx = df.iloc[event.row].name
    group = f"Group{idx}"
    label = f"Label{idx}"
    hv_obj = pn_plot.object
    hv_obj.opts.clear()
    hv_obj.opts(DEFAULT_CURVE_OPTS)
    pn_plot.object = hv_obj.opts(
        hv.opts.Curve(line_alpha=0.5), hv.opts.Curve(f"{group}.{label}", line_alpha=1)
    )


table.on_click(show_selected)
pn.Row(table, pn_plot).show()

1 Like

Rather than nuking all the options, we can target it with a little cache

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

hv.extension("bokeh")

# note, in the real case, we don't know a prior how many curves there will be
df = pd.DataFrame({"A": np.random.random(4)}, index=list(np.arange(4)))
table = pn.widgets.Tabulator(df, disabled=True)

curves = {}
for i, row in df.iterrows():
    group = f"Group{i}"
    label = f"Label{i}"
    curves[(group, label)] = hv.Curve(
        np.random.random(10), "frame", "val", group=group, label=label
    ).opts(subcoordinate_y=True, line_alpha=0.5)
plot = hv.Overlay(curves).opts(show_legend=False)
pn_plot = pn.pane.HoloViews(plot)


cached_group_label = set([])


def show_selected(event):
    idx = df.iloc[event.row].name
    group = f"Group{idx}"
    label = f"Label{idx}"
    group_label = f"{group}.{label}"
    if cached_group_label:
        last_group_label = cached_group_label.pop()
        reset_opts = hv.opts.Curve(last_group_label, line_alpha=0.5)
    else:
        reset_opts = hv.opts.Curve()
    cached_group_label.add(group_label)
    pn_plot.object = pn_plot.object.opts(
        reset_opts,
        hv.opts.Curve(group_label, line_alpha=1),
    )


table.on_click(show_selected)
pn.Row(table, pn_plot).show()

A solution to the more complicated original example based on @ahuang11 's targeted approach. I changed it a bit so that deselecting all entries also resets the alpha of all. This doesn’t highlight multiple selected curves, just the most recently clicked, which I think is reasonable enough. Thanks for the help!

from holonote.annotate import Annotator
from holonote.app import PanelWidgets
from holonote.app.tabulator import AnnotatorTable
import holoviews as hv; hv.extension('bokeh')
import panel as pn; pn.extension('tabulator')
import numpy as np
import xarray as xr

# Annotator
annotator = Annotator({"height": float, "width": float}, fields=["type"], groupby="type")
color_dim = hv.dim("type").categorize(
    categories={"A": "red", "B": "orange", "C": "cyan"}, default="grey"
)
annotator.style.color = color_dim
annotator.style.alpha = .5
panel_widgets = PanelWidgets(annotator)
table_widget = AnnotatorTable(annotator)
annotator_widgets = pn.WidgetBox(panel_widgets, table_widget, horizontal=True)

# Data
width, height, frame = 30, 40, 50
frames = np.arange(frame)
data = np.random.random((width, height, frame))

da = xr.DataArray(
    data,
    dims=["width", "height", "frame"],
    coords={
        "width": np.arange(width),
        "height": np.arange(height),
        "frame": frames
    },
    name="data"
)

# App
def sel_frame(iframe):
    return hv.Image(da.sel(frame=iframe)).opts(title='Image')
    
frame_slider = pn.widgets.DiscreteSlider(options=list(frames), value=frames[0])
dyn_img = hv.DynamicMap(pn.bind(sel_frame, frame_slider))

ts = pn.pane.HoloViews(hv.Curve([]).opts(xlim = (frames[0], frames[-1]),
    title='Create an annotation in the image'))

def plot_ts(event):
    curves = {}
    df = annotator.df
    for i, row in df.iterrows():
        group = f'{row['type']}'
        label = f'{i[:6]}'
        h1, h2, w1, w2 = row[["start[height]", "end[height]", "start[width]", "end[width]"]]
        da_sel = da.sel(height=slice(h1, h2), width=slice(w1, w2))
        curve = hv.Curve(da_sel.mean(["height", "width"]), 'frame', group=group, label=label)
        curve = curve.opts(subcoordinate_y=True,
                           color=panel_widgets.colormap[group],
                          line_alpha=.5)
        curves[(group, label)] = curve
    ts.object = (hv.Overlay(curves, kdims=['annotation'])).opts(
        title='Timeseries', xlim = (frames[0], frames[-1]),
        show_legend=False,)

annotator.on_event(plot_ts)

annotator.set_regions(height=(5,15), width=(5,15))
annotator.add_annotation(type='A')
annotator.set_regions(height=(25,30), width=(25,30))
annotator.add_annotation(type='B')

cached_group_label = set([])

def highlight_selected(idx):
    if cached_group_label:
        last_group_label = cached_group_label.pop()
        reset_opts = hv.opts.Curve(last_group_label, line_alpha=0.5)
    else:
        reset_opts = hv.opts.Curve()
    
    # highlight alpha of selected curve
    if len(idx):
        idx = idx[-1]
        row = annotator.df.loc[idx]
        group = f'{row['type']}'
        label = f'{idx[:6]}'
        group_label = f"{group}.{label}"
        cached_group_label.add(group_label)
        ts.object = ts.object.opts(reset_opts, hv.opts.Curve(group_label, line_alpha=1))
    else:
        ts.object = ts.object.opts(reset_opts)

pn.bind(highlight_selected, annotator.param.selected_indices, watch=True)
pn.Column(annotator_widgets, pn.Row(pn.Column(annotator * dyn_img, frame_slider), ts))
1 Like