Linking HoloViews plot selections to Panel Markdown using JS Callbacks

Hi everyone,
I would like to link a selection of a HoloViews plot to a Panel Markdown, where more information about the selection should be displayed. I also want to use a JS Callback for it, so I can create a stand-alone HTML file. I also looked through the documentation of HoloViews and Panel and it is well documented how to connect two HoloViews objects (https://holoviews.org/user_guide/Linking_Plots.html) and how to connect two Panel objects or connect a Panel widget to a HoloViews object (both descript here https://panel.holoviz.org/user_guide/Links.html). However I could not find out how to connect the other way around, from an HoloViews object to a Panel object.

Here is a mini example to make clear what I want to achieve. In this app, you can select one of the points in the left plot and according to this selection, the text to the right is changing. Instead of a panel markdown object, I used here two HoloViews text objects, but a panel markdown object would be nicer for formatting the text (I also included a panel markdown object in the example called text_markdown, if someone wants to give it a shot how to link it to the HoloViews selection).

import pandas as pd
import panel as pn
import holoviews as hv
from holoviews.plotting.links import Link
from holoviews.plotting.bokeh import LinkCallback
from bokeh.models import HoverTool

hv.extension('bokeh')
pn.extension()

# creating some artifical Data
data = {'Lon': [-134.349, -146.888,  -90.87697,  -37.8032],
        'Lat': [58.38,  61.299,  79.5146,  65.6957],
        'name': ['Lemon Creek Glacier', 'Columbia Glacier',
                 'White Glacier', 'Mittivakkat Glacier'],
        'description': ['Pure ice glacier', 'Calving glacier',
                        'Pure ice glacier', 'Ice cap']}
df = pd.DataFrame.from_dict(data)

# defining the links and callbacks between the HoloViews objects
class HeaderLink(Link):
    _requires_target = True

class HeaderCallback(LinkCallback):

    source_model = 'selected'
    source_handles = ['cds']
    on_source_changes = ['indices']

    target_model = 'glyph'
    
    source_code = """
        var inds = source_selected.indices
        if (inds.length == 0)
            target_glyph.text = 'Nothing selected!'
        else
            target_glyph.text = source_cds.data['name'][inds[0]]
    """

HeaderLink.register_callback('bokeh', HeaderCallback)

class DescriptionLink(Link):
    _requires_target = True

class DescriptionCallback(LinkCallback):

    source_model = 'selected'
    source_handles = ['cds']
    on_source_changes = ['indices']

    target_model = 'glyph'
    
    source_code = """
        var inds = source_selected.indices
        if (inds.length == 0)
            target_glyph.text = ' '
        else
            target_glyph.text = source_cds.data['description'][inds[0]]
    """

DescriptionLink.register_callback('bokeh', DescriptionCallback)

# define a hover
tooltips = [
    ('Lon', '@Lon'),
    ('Lat', '@Lat'),
    ('Name', '@name')
]
hover = HoverTool(tooltips=tooltips)

# create the actual plotting elements
points = hv.Scatter(df,
                    kdims=['Lon'],
                    vdims=['Lat', 'name', 'description']
                   ).opts(default_tools=['tap', 'reset', hover],
                          size=10,
                          xaxis=None,
                          yaxis=None)
header_text = hv.Text(0, 0.5, 'Nothing selected!'
                     ).opts(text_font_size='20pt',
                            default_tools=['reset'])
description_text = hv.Text(0, 0.25, ' '
                          ).opts(default_tools=['reset'])
text_markdown = pn.pane.Markdown("## Heading Panel markdown \n" +
                                 "Description Panel markdown\n\n" +
                                 "<b>Not linked right Now!</b>")

# connect the HoloViews objects with the links
HeaderLink(points, header_text)
DescriptionLink(points, description_text)

# insert a Link from the points to text_markdown here

# probably should contain JS code something like:
#   var inds = source_selected.indices
#   if (inds.length == 0)
#     target.text = '## Nothing selected!'
#   else
#     target.text = ('## ' + source_cds.data['name'][inds[0]] +
#                    '\n' + source_cds.data['description][inds[0]])

# put the plots together in the final app
app = pn.Row(points,
             (header_text * description_text
             ).opts(xlim=(-2, 2),
                    ylim=(0, 0.6),
                    xaxis=None,
                    yaxis=None,
                    toolbar=None),
             text_markdown)

# save as a stand-alone HTML
app.save('app.html', embed=True)

I appreciate any help with how this JS Link from the HoloViews selection to the Panel Markdown could be done. Thank you!

Hello! I just wanted to add that I am interested in this question as well and maybe some activity will help to bring this to attention to someone who knows about the topic :wink:

Update:
I managed to link the holoviews selection to the panel markdown (using a ‘linking Div’ object). In general with this workaround it is possible to link holoviews selections to all different panel components.

However, it is not possible to format the resulting text in a nice way as currently HTML tags are ignored (see hv.Div ignores HTML tags when updated with Link · Issue #5590 · holoviz/holoviews · GitHub and HTML tags are ignored with jslink · Issue #4318 · holoviz/panel · GitHub).

Here is the code of the workaround for the link:

import pandas as pd
import panel as pn
import holoviews as hv
from holoviews.plotting.links import Link
from holoviews.plotting.bokeh import LinkCallback

hv.extension('bokeh')
pn.extension()

# creating some artifical Data
data = {'Lon': [-134.349, -146.888,  -90.87697,  -37.8032],
        'Lat': [58.38,  61.299,  79.5146,  65.6957],
        'name': ['Lemon Creek Glacier', 'Columbia Glacier',
                 'White Glacier', 'Mittivakkat Glacier']}
df = pd.DataFrame.from_dict(data)

# create the plot and the text element, this two should be linked
points = hv.Scatter(df,
                    kdims=['Lon'],
                    vdims=['Lat', 'name']
                   ).opts(default_tools=['tap', 'reset', 'hover'],
                          size=10,
                          xaxis=None,
                          yaxis=None)
text_markdown = pn.pane.Markdown("# Nothing selected!")

# This Div is the workaround to connect a holoviews selection to a panel object
linking_div_hv = hv.Div(' ')
linking_div_pn = pn.panel(linking_div_hv, visible=False)

# Linking plot selection to Div_hv
class PlotDivLink(Link):
    _requires_target = True
    
class PlotDivCallback(LinkCallback):
    source_model = 'selected'
    source_handles = ['cds']
    on_source_changes = ['indices']

    target_model = 'plot'
    
    source_code = """
        var inds = source_selected.indices
        if (inds.length == 0)
            target_plot.text = 'Nothing selected!'
        else
            target_plot.text = source_cds.data['name'][inds[0]]
    """

PlotDivLink.register_callback('bokeh', PlotDivCallback)
PlotDivLink(points, linking_div_hv)

# Linking Div_pn and markdown text
code_pn_link = '''
    target.text = '# ' + source.text
'''
linking_div_pn.jslink(text_markdown,
                      code={'text': code_pn_link})

# put the plots together in the final app
app = pn.Row(points,
             linking_div_pn,
             text_markdown)

# save as a stand-alone HTML
app.save('app.html', embed=True)