Modify CustomJS for HoverTool.callback and the CSS for the HoverTool

Hi all,

I’m currently learning about the HoverTool and have some questions.

Regarding the CustomJS callback: I’ve drawn some circles and applied the HoverTool, but I want the tooltips to appear only when I hover near the center of each circle. I tried changing the anchor="center_center", but it didn’t work. So, I came up with an idea to use CustomJS instead.

from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool, CustomJS


data = {"x": [1, 2, 3, 4, 5], "y": [2, 5, 8, 2, 7], "radius": [0.5, 0.4, 0.3, 0.2, 0.1]}

source = ColumnDataSource(data=data)

p = figure(tools="", title="HoverTool")

circles = p.circle("x", "y", radius="radius", source=source, fill_alpha=0.6)

hover = HoverTool(
    tooltips=[("index", "$index"), ("(x,y)", "($x, $y)")], anchor="center_center"
)
p.add_tools(hover)

hover.callback = CustomJS(
    args=dict(hover=hover, source=source),
    code="""
    var indices = cb_data.index.indices;
    if (indices.length !== 0){
        let data_x
        let data_y
        for (let i = 0; i < indices.length; i++){
            const glyph_id = indices[i];
            data_x = source.data.x[glyph_id];
            data_y = source.data.y[glyph_id];
        }
        let x = cb_data.geometry.x;
        let y = cb_data.geometry.y;
        if (Math.abs(x-data_x)<0.25&&Math.abs(y-data_y)<0.25){
            console.log("Permitted!");
            cb_obj.visible = true;
        }else{
            cb_obj.visible = false;
        }
       
    }
""",
)

show(p)

I set cb_obj.visible = false when the conditions aren’t met, but the tooltips keep appearing.

Regarding Changing the CSS: I haven’t found a way to directly change the CSS of the tooltips using a stylesheet. Currently, I’m modifying it using a MutationObserver in JavaScript.

function ModifyHoverTool() {
    const observer = new MutationObserver((mutationsList) => {
        mutationsList.forEach((mutation) => {
            if (mutation.type === "childList") {
                mutation.addedNodes.forEach((node) => {
                    if (
                        node.nodeType === 1 &&
                        node.classList.contains(
                            "bk-Tooltip",
                            "bk-show-arrow",
                            "bk-non-interactive"
                        ) &&
                        (node.classList.contains("bk-right") ||
                            node.classList.contains("bk-above") ||
                            node.classList.contains("bk-below") ||
                            node.classList.contains("bk-left"))
                    ) {
                        node.style.background = "transparent";
                        node.style.border = "none";

                        const vwToPx = (window.innerWidth * 1.2) / 100;
                        const vhToPx = (window.innerWidth * 1.2) / 100;


                        if (node.classList.contains("bk-right")) {
                            node.style.left = `${
                                parseFloat(node.style.left) - vwToPx
                            }px`;
                        } else if (node.classList.contains("bk-left")) {
                            node.style.left = `${
                                parseFloat(node.style.left) + vwToPx
                            }px`;
                        } else if (node.classList.contains("bk-above")) {
                            node.style.top = `${
                                parseFloat(node.style.top) + vhToPx
                            }px`;
                        } else if (node.classList.contains("bk-below")) {
                            node.style.top = `${
                                parseFloat(node.style.top) - vhToPx
                            }px`;
                        }

                        Array.from(
                            node.shadowRoot.querySelector(".bk-tooltip-content")
                                .firstElementChild.children
                        ).forEach((childDiv) => {
                            Object.assign(childDiv.style, {
                                height: "100%",
                                width: "100%",
                                display: "block",
                                border: "1px solid black",
                                margin: "2px",
                                background: "white",
                                borderRadius: "4px",
                            });
                        });
                    }
                });
            }
        });
    });

    observer.observe(document.body, { childList: true, subtree: true });
}

ModifyHoverTool();

Is there a better way to do it using Python only or CustomJS callback ?

Appreciate any help.

The easy workaround would be to make a second renderer (‘dum’) with alphas of 0 driven by the same CDS, and assign that renderer to the Hovertool:

from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, HoverTool, CustomJS


data = {"x": [1, 2, 3, 4, 5], "y": [2, 5, 8, 2, 7], "radius": [0.5, 0.4, 0.3, 0.2, 0.1]}

source = ColumnDataSource(data=data)

p = figure(tools="", title="HoverTool")

circles = p.circle("x", "y", radius="radius", source=source, fill_alpha=0.6)
dum = p.circle('x','y',radius = 0.2, source = source, fill_alpha=0,line_alpha=0)

hover = HoverTool(renderers=[dum],
    tooltips=[("index", "$index"), ("(x,y)", "($x, $y)")], anchor="center_center"
)
p.add_tools(hover)

show(p)

htool

2 Likes

That’s a great idea—I’ll definitely try it later! If you have any other suggestions, feel free to share. :slight_smile:

Hit-testing for glyphs is always all-or-nothing. There’s definitely no mechanism at all to have a hover tool only be active over “part” of a glyph, so @gmerritt123’s idea with hidden glyphs is probably the best possible approach.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.