Scrollbar legend in hvplot

I have a holoviews scatter plot with many items in the legend which only a limited number of items are visible in the plot. I want to find a way to be able to see all items in the legends either by being able to scroll through legend items or reshaping the legend presentation. This is a sample piece of code that I have:

import pandas as pd
import numpy as np
import holoviews as hv
hv.extension('bokeh')


df = pd.DataFrame({'index': np.arange(0, 1000), 
                                 'A': np.random.randint(1,40, 1000), 
                                 'B': np.random.uniform(-1, 1, 1000)})


scat = df.hvplot(x='index', y='B', kind='scatter', by='A', size=10)
            
        
scat.opts(width=900, height=500, 
                legend_opts={"location": "top_left", "click_policy": "hide", "title_text_font_size": "10px", 
                             "label_text_font_size": "10px", "glyph_height": 10, "spacing": 1,  
                             "background_fill_color": 'blue', "background_fill_alpha":0.1})
1 Like

Hi @esarvestani

Welcome to the community.

Unfortunately the bokeh backend does not provide any easy to use solution for a legend with many items.

I think your best option for bokeh is to create a workaround/ custom legend from the ideas in Scrollable Legend View - Community Support - Bokeh Discourse. How depends on your requirements. If you are running in a notebook or deploying to a server a solution could be to use Panel to create the component to list the legend items and make them clickable.

Alternatively you can check out the plotly backend. It might provide the functionality you need.

1 Like

This is just one example of how to create a tool that could be useful for exploration

import pandas as pd
import numpy as np
import holoviews as hv
import hvplot.pandas
import random
import string
import random

hv.extension("bokeh")

LEGEND_ITEMS = 1000


def get_colors(n):
    return [
        "#" + "".join([random.choice("ABCDEF0123456789") for i in range(6)])
        for _ in range(0, n)
    ]


def get_data():
    return pd.DataFrame(
        {
            "x": np.random.uniform(-1, 1, LEGEND_ITEMS),
            "y": np.random.uniform(-1, 1, LEGEND_ITEMS),
            "category": [
                "".join(random.choices(string.ascii_lowercase, k=5))
                for _ in range(0, LEGEND_ITEMS)
            ],
            "color": get_colors(LEGEND_ITEMS),
        }
    ).sort_values(["category", "x"])


def get_options(df):
    return list(df["category"].unique())


def get_plot(df, items=None):
    if not items:
        return pd.DataFrame(columns=["x", "y"]).hvplot(kind="scatter", responsive=True)

    df = df[df["category"].isin(items)]
    scat = df.hvplot(
        x="x",
        y="y",
        kind="scatter",
        by="category",
        size=50,
        responsive=True,
        xlim=(-1, 1),
        ylim=(-1, 1),
        color="color",
    )
    scat.opts(
        legend_opts={
            "location": "top_left",
            "click_policy": "hide",
            "title_text_font_size": "10px",
            "label_text_font_size": "10px",
            "glyph_height": 10,
            "spacing": 1,
            "background_fill_color": "blue",
            "background_fill_alpha": 0.1,
        },
    )
    return scat


df = get_data()

import panel as pn

pn.extension()

options = get_options(df)
items = pn.widgets.MultiSelect(
    value=options[:5], options=options, sizing_mode="stretch_height", name="Category"
)
get_plot = pn.bind(get_plot, df=df, items=items)
plot = pn.panel(hv.DynamicMap(get_plot), sizing_mode="stretch_both", loading=True)


pn.Row(plot, items, sizing_mode="stretch_both").servable()

You can run it via

panel serve name_of_script.py
2 Likes