Linked hover between legend and glyph!

Peek 2025-06-06 02-23

from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.io import output_file
from bokeh.io import curdoc
curdoc().theme = 'night_sky'
import numpy as np

x = list(range(150))
temp = [20 + 4*np.sin(i / 15) + 0.5*np.sin(i / 2) + np.random.uniform(-0.3, 0.3) for i in x]
rain = [np.random.uniform(5, 20) if i % np.random.randint(5, 10) == 0 else np.random.uniform(0, 0.3) for i in x]
radiation = [max(0, 100 * np.sin((i % 50) / 50 * np.pi) + np.random.uniform(-10, 10)) for i in x]

source = ColumnDataSource(data=dict(x=x, temp=temp, rain=rain, radiation=radiation))

p = figure(title="🌡️ Temp, 🌧️ Rain, ☀️ Radiation", width=800, height=400,background_fill_color="#1a1a1a",)
p.xaxis.axis_label = "Time"
p.yaxis.axis_label = "Value"

# 📈 Lines
r_temp = p.line('x', 'temp', source=source, color="blue", line_width=2, name="temp")
r_rain = p.line('x', 'rain', source=source, color="red", line_width=2, name="rain")
r_rad = p.line('x', 'radiation', source=source, color="lime", line_width=2, name="radiation")

js = CustomJS(code="""
    function wait() {
        const doc = Bokeh.documents[0];
        const r1 = doc.get_model_by_name('temp');
        const r2 = doc.get_model_by_name('rain');
        const r3 = doc.get_model_by_name('radiation');

        if (!r1 || !r2 || !r3) return requestAnimationFrame(wait);

        if (document.getElementById('external-legend')) return;

        const legendDiv = document.createElement('div');
        legendDiv.id = 'external-legend';
        legendDiv.style.position = 'absolute';
        legendDiv.style.top = '80px';
        legendDiv.style.left = '690px';
        legendDiv.style.background = '#111';
        legendDiv.style.padding = '12px';
        legendDiv.style.border = '1px solid #444';
        legendDiv.style.borderRadius = '8px';
        legendDiv.style.color = '#fff';
        legendDiv.style.fontFamily = 'monospace';
        legendDiv.style.fontSize = '14px';
        legendDiv.style.zIndex = 1000;
        legendDiv.innerHTML = `
            <div data-key="temp" style="color: blue; cursor: pointer;">🔵 Temp</div>
            <div data-key="rain" style="color: red; cursor: pointer;">🔴 Rain</div>
            <div data-key="radiation" style="color: lime; cursor: pointer;">🟢 Radiation</div>
        `;
        document.body.appendChild(legendDiv);

        const lines = { temp: r1, rain: r2, radiation: r3 };

        legendDiv.querySelectorAll('[data-key]').forEach(item => {
            const key = item.dataset.key;
            item.addEventListener('mouseenter', () => {
                lines[key].glyph.line_width = 6;
                lines[key].change.emit();
            });
            item.addEventListener('mouseleave', () => {
                lines[key].glyph.line_width = 1;
                lines[key].change.emit();
            });
        });

        // Optional auto-hover
        // legendDiv.querySelector('[data-key="temp"]')?.dispatchEvent(new Event("mouseenter"));
    }

    requestAnimationFrame(wait);
""")
p.min_border_right=165
p.min_border_bottom=90;
p.styles = {'margin-top': '20px','margin-left': '20px','border-radius': '10px','box-shadow': '0 18px 20px rgba(165, 221, 253, 0.2)','padding': '5px','background-color': 'black','border': '1px solid red'}

doc = curdoc()
doc.add_root(p)
doc.js_on_event('document_ready', js)

output_file("external_legend_hover_auto.html")
show(p)
2 Likes

This is handy, thanks!

1 Like