Pretty tooltips for 'ggplot2'
Posted on July 3, 2022
by Stéphane Laurent
Below is a simple example of a ‘ggplot2’ graphic with tooltips in a Shiny application. The tooltips are constructed with the help of the qTip2 JavaScript library.
library(shiny)
library(ggplot2)
library(shinyjs)
set.seed(666)
dat <- data.frame(
x = rnorm(10),
y = rnorm(10),
f = gl(2, 5, labels = c("A", "B"))
)
qTipTemplate <- "
$('#hoverinfo').qtip({
overwrite: true,
content: {
text: $('#tooltiptext').clone()
},
position: {
my: '%s',
at: '%s',
target: [%s, %s],
container: $('#ggplot')
},
show: {
ready: true
},
hide: {
event: false,
inactive: 4000
},
style: {
classes: 'myqtip'
}
});
"
ui <- basicPage(
useShinyjs(),
tags$head(
tags$link(rel = "stylesheet", href = "jquery.qtip.css"),
tags$script(src = "jquery.qtip.js"),
tags$style("
.myqtip {
font-size: 15px;
line-height: 18px;
background-color: rgba(245, 245, 245, 0.8);
border-color: rgb(54, 57, 64);
}")
),
div(
id = "tooltiptext", style = "display: none;"
),
br(),
div(
style = "position: relative;",
plotOutput("ggplot", hover = hoverOpts("plot_hover")),
div(id = "hoverinfo", style = "position: absolute;")
)
)
server <- function(input, output, session) {
output$ggplot <- renderPlot(
ggplot(dat, aes(x = x, y = y)) +
geom_point(size = 6) +
theme(
panel.background = element_rect(fill = "#FFCF9E"),
axis.title = element_text(size = 19),
axis.text = element_text(size = 16)
)
)
observeEvent(input[["plot_hover"]], {
if(is.null(hover <- input[["plot_hover"]])) {
return(NULL)
}
point <- nearPoints(dat, hover, threshold = 15, maxpoints = 1L)
if(nrow(point) == 0L) {
return(NULL)
}
left_pct <-
(point[["x"]] - hover$domain$left) /
(hover$domain$right - hover$domain$left)
top_pct <-
(hover$domain$top - point[["y"]]) /
(hover$domain$top - hover$domain$bottom)
left_px <-
(hover$range$left + left_pct * (hover$range$right - hover$range$left)) /
hover$img_css_ratio$x
top_px <-
(hover$range$top + top_pct * (hover$range$bottom - hover$range$top)) /
hover$img_css_ratio$y
pos <- ifelse(left_pct < 0.5,
ifelse(top_pct < 0.5,
"top left",
"bottom left"
),
ifelse(top_pct < 0.5,
"top right",
"bottom right"
)
)
tooltip <- paste0(
"<b> x: </b>", formatC(point[["x"]]), "<br/>",
"<b> y: </b>", formatC(point[["y"]]), "<br/>",
"<b> f: </b>", as.character(point[["f"]])
)
runjs(
paste0(
sprintf(
"$('#tooltiptext').html('%s');", tooltip
),
sprintf(qTipTemplate, pos, pos, left_px, top_px)
)
)
})
}
shinyApp(ui, server)
We can easily set some colors with the help of CSS classes:
set.seed(666)
dat <- data.frame(
x = c(rnorm(10, 0), rnorm(10, 2), rnorm(10, 4)),
y = c(rnorm(10, 0), rnorm(10, 2), rnorm(10, 4)),
f = gl(3, 10, labels = c("A", "B", "C"))
)
qTipTemplate <- "
$('#hoverinfo').qtip({
overwrite: true,
content: {
text: $('#tooltiptext').clone()
},
position: {
my: '%s',
at: '%s',
target: [%s, %s],
container: $('#ggplot')
},
show: {
ready: true
},
hide: {
event: false,
inactive: 4000
},
style: {
classes: 'myqtip %s'
}
});
"
ui <- basicPage(
useShinyjs(),
tags$head(
tags$link(rel = "stylesheet", href = "jquery.qtip.css"),
tags$script(src = "jquery.qtip.js"),
tags$style("
.myqtip {
font-size: 15px;
line-height: 18px;
color: white;
border-color: rgb(54, 57, 64);
}
.darkred {
background-color: rgba(139, 0, 0, 0.8);
}
.darkgreen {
background-color: rgba(0, 100, 0, 0.8);
}
.darkblue {
background-color: rgba(0, 0, 139, 0.8);
}
")
),
div(
id = "tooltiptext", style = "display: none;"
),
br(),
div(
style = "position: relative;",
plotOutput("ggplot", hover = hoverOpts("plot_hover")),
div(id = "hoverinfo", style = "position: absolute;")
)
)
server <- function(input, output, session) {
output$ggplot <- renderPlot(
ggplot(dat, aes(x = x, y = y, color = f)) + geom_point(size = 4) +
scale_color_manual(
values = c("darkred", "darkgreen", "darkblue")
) +
theme(
panel.background = element_rect(fill = "gainsboro"),
axis.title = element_text(size = 19),
axis.text = element_text(size = 16),
legend.title = element_text(size = 18),
legend.text = element_text(size = 15),
legend.key.size = unit(4, "points")
)
)
observeEvent(input[["plot_hover"]], {
if(is.null(hover <- input[["plot_hover"]])) {
return(NULL)
}
point <- nearPoints(dat, hover, threshold = 15, maxpoints = 1L)
if(nrow(point) == 0L) {
return(NULL)
}
left_pct <-
(point[["x"]] - hover$domain$left) /
(hover$domain$right - hover$domain$left)
top_pct <-
(hover$domain$top - point[["y"]]) /
(hover$domain$top - hover$domain$bottom)
left_px <-
(hover$range$left + left_pct * (hover$range$right - hover$range$left)) /
hover$img_css_ratio$x
top_px <-
(hover$range$top + top_pct * (hover$range$bottom - hover$range$top)) /
hover$img_css_ratio$y
pos <- ifelse(left_pct < 0.5,
ifelse(top_pct < 0.5,
"top left",
"bottom left"
),
ifelse(top_pct < 0.5,
"top right",
"bottom right"
)
)
f <- as.character(point[["f"]])
color <- switch(f,
A = "darkred",
B = "darkgreen",
C = "darkblue"
)
tooltip <- paste0(
"<b> x: </b>", formatC(point[["x"]]), "<br/>",
"<b> y: </b>", formatC(point[["y"]]), "<br/>",
"<b> f: </b>", f
)
runjs(
paste0(
sprintf(
"$('#tooltiptext').html('%s');", tooltip
),
sprintf(qTipTemplate, pos, pos, left_px, top_px, color)
)
)
})
}
shinyApp(ui, server)
Let’s do something a bit more complicated now, with a continuous color scale. The idea is to define a CSS class for each point.
library(shiny)
library(shinyjs)
library(ggplot2)
library(viridisLite)
set.seed(666L)
n <- 200L
dat <- data.frame(
x = 1:n,
y = runif(n)
)
# font color given background color:
black_or_white <- function(color) {
rgb <- col2rgb(color)
if(rgb[1]*0.299 + rgb[2]*0.587 + rgb[3]*0.114 > 186)
"black" # background color is light
else
"white" # background color is dark
}
ramp <- colorRamp(viridis(255L))
bkgs <- rgb(ramp(dat$y), maxColorValue = 255)
colors <- vapply(bkgs, black_or_white, character(1L))
classes <- sprintf("color%03d", 1:n)
dat$class <- classes
CSSclass <- function(i) {
paste0(
".", classes[i], " {",
"color: ", colors[i], "; ",
"background-color: ", bkgs[i], ";",
"}"
)
}
CSS <- paste0(vapply(1:n, CSSclass, character(1L)), collapse = "\n")
qTipTemplate <- "
$('#hoverinfo').qtip({
overwrite: true,
content: {
text: $('#tooltiptext').clone()
},
position: {
my: '%s',
at: '%s',
target: [%s, %s],
container: $('#ggplot')
},
show: {
ready: true
},
hide: {
event: false,
inactive: 4000
},
style: {
classes: 'myqtip %s'
}
});
"
ui <- basicPage(
useShinyjs(),
tags$head(
tags$link(rel = "stylesheet", href = "jquery.qtip.css"),
tags$script(src = "jquery.qtip.js"),
tags$style("
.myqtip {
font-size: 15px;
line-height: 18px;
color: white;
border-color: rgb(54, 57, 64);
}
"),
tags$style(CSS)
),
div(
id = "tooltiptext", style = "display: none;"
),
br(),
div(
style = "position: relative;",
plotOutput("ggplot", hover = hoverOpts("plot_hover")),
div(id = "hoverinfo", style = "position: absolute;")
)
)
server <- function(input, output, session) {
output$ggplot <- renderPlot(
ggplot(dat, aes(x = x, y = y, color = y)) + geom_point(size = 3) +
scale_color_viridis_c() +
theme(
panel.background = element_rect(fill = "gainsboro"),
axis.title = element_text(size = 19),
axis.text = element_text(size = 16),
legend.title = element_text(size = 18)
)
)
observeEvent(input[["plot_hover"]], {
if(is.null(hover <- input[["plot_hover"]])) {
return(NULL)
}
point <- nearPoints(dat, hover, threshold = 15, maxpoints = 1L)
if(nrow(point) == 0L) {
return(NULL)
}
left_pct <-
(point[["x"]] - hover$domain$left) /
(hover$domain$right - hover$domain$left)
top_pct <-
(hover$domain$top - point[["y"]]) /
(hover$domain$top - hover$domain$bottom)
left_px <-
(hover$range$left + left_pct * (hover$range$right - hover$range$left)) /
hover$img_css_ratio$x
top_px <-
(hover$range$top + top_pct * (hover$range$bottom - hover$range$top)) /
hover$img_css_ratio$y
pos <- ifelse(left_pct < 0.5,
ifelse(top_pct < 0.5,
"top left",
"bottom left"
),
ifelse(top_pct < 0.5,
"top right",
"bottom right"
)
)
cssClass <- point[["class"]]
tooltip <- paste0(
"<b> x: </b>", formatC(point[["x"]]), "<br/>",
"<b> y: </b>", formatC(point[["y"]]), "<br/>"
)
runjs(
paste0(
sprintf(
"$('#tooltiptext').html('%s');", tooltip
),
sprintf(qTipTemplate, pos, pos, left_px, top_px, cssClass)
)
)
})
}
shinyApp(ui, server)