In a Shiny application, using ggiraph to make a ggplot2 plot interactive, using geom_col_interactive() or similar to add tooltips to a plot element, tooltip appears on hover, but if the user clicks the plot element, the tooltip disappears.
library(ggplot2)
library(ggiraph)
p <- mtcars |>
ggplot() +
aes(x = wt, y = mpg) +
geom_point_interactive(
aes(tooltip = rownames(mtcars))
)
girafe(ggobj = p)
Tooltip appears on hover, but if you click on points, tooltip disappears. I need the tooltip to keep visible even if the user clicks the hovered element.
In opts_tooltip() I've set the delay_mouseout = 2000 argument, which kinda does what I need, but not quite as it just delaying the time the tooltip takes to disappear.
ggiraph tooltip is a single <div> element that gets updated with a new content, location and opacity when hovering over interactive plot elements.
<div class="tooltip_svg_mtcars" style="position: absolute; opacity: 0; left: 333px; top: 281px;">Merc 450SL</div>
I think one of the easiest options is to just clone that same element, make it visible through opacity and perhaps register a handler or two to hide / remove it later.
When it comes to JavaScript, the following example is somewhat crude and probably non-idiomatic proof of concept, though WorksOnMyMachine(tm) and doesn't seem to interfere much with ggiraph client-side. Using d3 as it's already in the ggiraph JS bundle.
function toggleTooltip(elem, canvas_id){
const tooltip = d3.select("#tooltip_" + elem.id);
if ( tooltip.empty() ) {
d3.select("div.tooltip_" + canvas_id)
.clone(true)
.attr("id", "tooltip_" + elem.id)
.style("opacity", "0.9")
.style("pointer-events", "auto")
.on("click", function() {
d3.select(this).remove();
});
} else {
tooltip.remove();
}
}
toggleTooltip(elem, canvas_id) is a click handler for interactive geoms. It first checks for a matching tooltip and removes it if found. If there's no match, the original is cloned. At that point, the tooltip's location and content have already been updated by ggiraph's JS, so we just need to adjust its opacity and set an id to link tooltips to plot items. Toggling is all set now, but to allow closing tooltips by clicking the tooltip itself, we need to adjust the pointer-events style from none and add click handler for the cloned element.
canvas_id argument is plot's SVG ID, set via girafe(..., canvas_id) when rendering the plot, make sure to use unique id for each plot.
After clicking on few interactive plot elements there are now several div.tooltip_svg_mtcarss:
<div class="tooltip_svg_mtcars" style="position: absolute; opacity: 0; left: 333px; top: 281px;">Merc 450SL</div>
<div class="tooltip_svg_mtcars" style="position: absolute; opacity: 0.9; left: 273px; top: 221px; pointer-events: auto;" id="tooltip_svg_mtcars_e4">Hornet 4 Drive</div>
<div class="tooltip_svg_mtcars" style="position: absolute; opacity: 0.9; left: 272px; top: 174px; pointer-events: auto;" id="tooltip_svg_mtcars_e8">Merc 240D</div>
ggirpah JS operates only with the first one (TooltipHandler.applyOn()), for others the id includes plot element id, e.g. tooltip_svg_mtcars_e4 is built from svg_mtcars_e4 of
<circle id="svg_mtcars_e4" cx="206.65" cy=... title="Hornet 4 Drive" onclick="toggleTooltip(this, 'svg_mtcars');"></circle>
Complete example:
library(ggplot2)
library(ggiraph)
library(shiny)
ui <- fluidPage(
tags$script(HTML('
function toggleTooltip(elem, canvas_id){
const tooltip = d3.select("#tooltip_" + elem.id);
if ( tooltip.empty() ) {
d3.select("div.tooltip_" + canvas_id)
.clone(true)
.attr("id", "tooltip_" + elem.id)
.style("opacity", "0.9")
.style("pointer-events", "auto")
.on("click", function() {
d3.select(this).remove();
});
} else {
tooltip.remove();
}
}
' )),
girafeOutput("gg")
)
server <- function(input, output) {
output$gg <- renderGirafe({
canvas_id <- "svg_mtcars"
gg <- mtcars |>
ggplot() +
aes(x = wt, y = mpg) +
geom_point_interactive(
aes(tooltip = rownames(mtcars)),
onclick = glue::glue('toggleTooltip(this, "{canvas_id}");'),
color = "gray70", size = 3, alpha = .8,
) +
theme_minimal()
girafe(ggobj = gg, canvas_id = canvas_id)
})
}
shinyApp(ui = ui, server = server)

For reference, D3 bundled with the current ggiraph release is v5.16.0 and its behaviour can be slightly different than described in the latest doc. rev.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With