Interactive graphics with {ggformula}

Getting Started

Randall Pruim

2025-08-27

1 Interactive geoms, scales, and facets

1.1 Interactive geoms

The {ggiraph} package provides a number of interactive geoms. {ggformula} makes these available via gf_*_interactive() functions. Interactive geoms provide several types of interaction.

Each type of interaction can be customized for both behavior and style.

Although not discussed here, {ggiraph} transforms graphics into reactive objects, making various events and selections available for shiny apps.

1.2 First example: Scatter plot with tooltips

Interactive geoms provide two new aesthetics that can be used used to help identify or provide additional information about individual points or sets of points.

library(ggformula)
theme_set(theme_bw())
data(mtcars)
mtcars2 <- mtcars |>
  tibble::rownames_to_column(var = "carname")

cars_scatter <-
  mtcars2 |>
  gf_point_interactive(
    wt ~ drat,
    color = ~mpg,
    tooltip = ~carname, # show carname when hovering on a point
    data_id = ~carname, # unique identifier -- selection is a single point
    hover_nearest = TRUE,
    size = 3
  )

# to display the graph with interactive compontents enabled, use
# gf_girafe() to convert it to an HTML widget.

gf_girafe(cars_scatter)

In the previous example, data_id was a unique identifier for the points. In the next example, data_id identifies groups of cars (those with the same number of cylinders). The hover text identifies both the car name and the number of cylinders.

cars_scatter_tooltip <-
  mtcars2 |>
  gf_point_interactive(
    qsec ~ disp,
    tooltip = ~ glue::glue("{carname} ({cyl} cylinders)"),
    data_id = ~cyl,
    size = 3,
    hover_nearest = TRUE
  )

gf_girafe(cars_scatter_tooltip)

1.3 Dealing with stats in interactive bar graphs

Many graphics – bar graphs, histograms, boxplots, density plots, etc. – are built not from our original data but from some transformation of the data. Understanding the role of data transformations computed by statistics and scales can be important to creating graphics.

Here is an example where tooltips allow us to see how many items are represented in each segment of a stacked bar plot.

data(diamonds)
diamonds_bargraph <-
  diamonds |>
  gf_bar_interactive(
    ~color,
    fill = ~cut,
    tooltip = ~ after_stat(count),
    data_id = ~ as.character(cut)
  )

diamonds_bargraph |> gf_girafe()

after_stat()

By default, when we map variables, the mapping uses the original data we provide for the layer. When a layer is created from summarised data (bars defined by counts, boxplots defined by 5-number summaries, densities for density plots, etc.), we have the option to do mapping after the summarising statistic as been applied. That’s what after_stat() is doing here.

We don’t have a column named count in our data, but the stat used by gf_bar() and gf_bar_interactive() computes these counts (and some additional things), storing the counts in a column called count, which is only available after the statistic has been computed. tooltip = ~ after_stat(count) creates the tooltip variable after count has been calculated.

In examples like this, it can be handy to know what data are available after the summarising statistic has been applied. We can inspect this with layer_data().1

diamonds_bargraph |> layer_data() |> head(3)
#>   count prop x flipped_aes      fill data_id PANEL group tooltip    y ymin ymax
#> 1   163    1 1       FALSE #440154FF    Fair     1     1     163 6775 6612 6775
#> 2   224    1 2       FALSE #440154FF    Fair     1     2     224 9797 9573 9797
#> 3   312    1 3       FALSE #440154FF    Fair     1     3     312 9542 9230 9542
#>   xmin xmax colour linewidth linetype alpha width
#> 1 0.55 1.45     NA       0.5        1    NA   0.9
#> 2 1.55 2.45     NA       0.5        1    NA   0.9
#> 3 2.55 3.45     NA       0.5        1    NA   0.9

Some things to notice:

Warning

Note that in the previous graphic, the count displayed when hovering is the count for a single segment, not for all of the segments that change color. The latter is determined by data_id, which after_stat(count) does not know about. If this is not desireable, we will need to refine either tooltip or data_id to make them match. See Section 1.3.1.

1.3.1 Finer control

There are several ways to get finer control over what is highlited and what information appears in the tooltip when we hover.

Method 1: Summarising before plotting

Sometimes it is better to summarise the data ourselves before creating a graphic. gf_col_interactive() is similar to gf_bar_interacive(), but is designed to work with data that have already been summarised.

library(dplyr)
diamonds |>
  group_by(color, cut) |>
  summarise(count = n()) |>
  gf_col_interactive(
    count ~ color,
    fill = ~cut,
    tooltip = ~ glue::glue("color: {color}, cut: {cut}, count: {count}"),
    data_id = ~ glue::glue("{cut} - {color}")
  ) |>
  gf_girafe()
#> `summarise()` has grouped output by 'color'. You can override using the
#> `.groups` argument.

Our use of data_id here limits the hover highlighting to one bar segment (defined by cut and color). If we omit data_id in the previous example, we still get the hover text, but the bar segment we are hovering on does not change color.

diamonds |>
  group_by(color, cut) |>
  summarise(count = n()) |>
  gf_col_interactive(
    count ~ color,
    fill = ~cut,
    tooltip = ~ glue::glue("color: {color}, cut: {cut}, count: {count}")
  ) |>
  gf_girafe()
#> `summarise()` has grouped output by 'color'. You can override using the
#> `.groups` argument.

Method 2: Using stage()

Alternatively, we might prefer to let gf_bar_interactive() take care of the data summarising for us, but still have finer control over the highliting and tooltip text.

diamonds_bargraph_2 <-
  diamonds |>
  gf_bar_interactive(
    ~color,
    fill = ~cut,
    tooltip = ~ stage(
      start = glue::glue("color: {color}; cut: {cut}"),
      after_stat = glue::glue("{tooltip}; count = {count}")
    ),
    data_id = ~ glue::glue("{cut} -- {color}"),
    size = 3
  )
#> Warning in (function (mapping = NULL, data = NULL, stat = "count", position =
#> "stack", : Ignoring unknown parameters: `size`
diamonds_bargraph_2 |>
  gf_girafe()

stage()

As mentioned above, when ggplot2 graphics are built, the data goes through a sequence of transformations. We can do mapping at three stages along the way.

  1. start: The process begins with the original data that we provide. By default, this is where mapping happens.

  2. after_stat: The starting data are transformed by a statistic (or stat) that computes any summaries of the data.

  3. after_scale: The after_stat data is further transformed by scales that compute the specific positions, colors, etc. that are used.

The use of stage() allows us to map the same quantity to different values at different stages in this process. In our example,

If you have not encountered stage() before, you can learn more in the documentation for stage().

1.4 Interactive scales

Interactive scales can be used inside gf_refine() and generate interactive guides (legends, axes, etc.).

diamonds_bargraph_3 <-
  diamonds_bargraph |>
  gf_refine(
    scale_fill_viridis_d_interactive(
      begin = 0.1,
      end = 0.7,
      option = "D",
      data_id = function(breaks) as.character(breaks),
      tooltip = function(breaks) glue::glue("break: {as.character(breaks)}")
    )
  )

diamonds_bargraph_3 |>
  gf_girafe()

By themselves, interactive scales are not that interesting. But key selections can be turned into reactive values for use in things like shiny apps. See https://www.ardata.fr/ggiraph-book/shiny.html.

1.5 Interactive faceting

Interactive faceting requires three things:

  1. The use of gf_facet_wrap_interacive() or gf_facet_grid_interactive(), in place of gf_facet_wrap() or gf_facet_grid();
  2. The use of an interactive labeller (labeller = gf_labeller_interactive()) to create the labels; and
  3. A theme that enables facet text and/or strips to be interactive.
diamonds_bargraph_4 <-
  diamonds_bargraph_3 |>
  gf_theme(
    strip.text = element_text_interactive(),
    strip.background = element_rect_interactive()
  ) |>
  gf_facet_wrap_interactive(
    ~clarity, # or vars(clarity)
    interactive_on = "both",
    ncol = 2,
    labeller = gf_labeller_interactive(
      tooltip = ~ paste("this is clarity", clarity),
      data_id = ~clarity
    )
  ) 

  diamonds_bargraph_4 |> gf_girafe()

Warning

Now that we have added facets, we again have the situation where the counts displayed are for an individual bar segment, even though segments in other facets are also being highlited. This is because faceting further partitions the data before the stat is applied. This is required so that each facet knows the size of the bar segments to display.

Note the PANEL and group columns in the layer data.

diamonds_bargraph_4 |> layer_data() |> slice_sample(n=4)
#>   count prop x flipped_aes      fill data_id PANEL group tooltip    y ymin ymax
#> 1   608    1 3       FALSE #43BF71FF   Ideal     3    31     608  608    0  608
#> 2    32    1 6       FALSE #482576FF    Fair     4     6      32 1169 1137 1169
#> 3   202    1 7       FALSE #1E9C89FF Premium     4    28     202  434  232  434
#> 4   209    1 7       FALSE #1E9C89FF Premium     3    28     209  452  243  452
#>   xmin xmax colour linewidth linetype alpha width
#> 1 2.55 3.45     NA       0.5        1    NA   0.9
#> 2 5.55 6.45     NA       0.5        1    NA   0.9
#> 3 6.55 7.45     NA       0.5        1    NA   0.9
#> 4 6.55 7.45     NA       0.5        1    NA   0.9

1.6 Interactive themes

{ggiraph} provides 3 intereactive elements for use in interactive themes:

These are drop-in replacements for their non-interactive counterparts.

diamonds_bargraph_3 |>
  gf_theme(theme_facets_interactive(theme_minimal())) |>
  gf_facet_wrap_interactive(
    ~clarity, # or vars(clarity)
    interactive_on = "both",
    ncol = 2,
    labeller = gf_labeller_interactive(
      tooltip = ~ paste("this is clarity", clarity),
      data_id = ~clarity
    )
  ) |>
  gf_girafe()

We’ll return to this example in Section 2 to see how to improve the hover interaction.

1.7 Interacting with multiple plots using {patchwork}

If we use {patchwork} to arrange multiple plots into a grid, selecting points in one plot will highlight them in both.

library(patchwork)


cars_scatter_2 <-
  mtcars2 |>
  gf_point_interactive(
    disp ~ qsec,
    color = ~mpg,
    tooltip = ~carname,
    data_id = ~carname,
    hover_nearest = TRUE,
    size = 3
  )

gf_girafe(cars_scatter / cars_scatter_2)

1.8 Click actions with JavaScript

If you know some JavaScript, you can create click actions for interactive plot elements by passing the JavaScript that should be executed as onclick. In this section we include just two example uses of JavaScript.

1.8.1 Alerts

  mtcars2 |>
  gf_point_interactive(
    wt ~ drat,
    color = ~mpg,
    data_id = ~carname, 
    onclick = ~glue::glue('alert("Here is some info for {carname} ...")'),
    size = 3
  ) |>
    gf_girafe()

1.8.2 Opening another webpage

In the example below, we use this to open a webpage with related information.

  mtcars2 |>
  gf_point_interactive(
    wt ~ drat,
    color = ~ mpg,
    data_id = ~ carname, 
    tooltip = ~ carname,
    onclick = ~ glue::glue('window.open("https://en.wikipedia.org/w/index.php?search={carname}")'),
    size = 3
  ) |>
    gf_girafe()

2 Customizing girafe animations

We can customize the interactive features of our plots in one of two ways:

  1. Setting options = list( ... ) in the call to gf_girafe(), or
  2. Using set_girafe_defaults( ... ).

In either case we replace ... with calls to one or more of the following:

Girafe animations are produced in SVG (scalable vector graphics) format. We can customize how SVGs appear using CSS (cascading style sheets). So many of these functions are utilities to help us create the correct CSS. Some options require the user to provide some CSS as a string of semi-colon separated key-value pairs, where key-value pairs are separated by colons. But for many options we can avoid writing CSS directly using these helper functions.

2.1 CSS styling

The style of many interactive elements is determined by a character string containing CSS styling. Each CSS declaration includes a property name and an associated value. Property names and values are separated by colons and name-value pairs always end with a semicolon. Spaces can be added around delimeters to improve readability. For example color:gray; text-align:center;".

Common CSS properties include:

Color keys

Notice that the names of the keys for setting color vary among the various kinds of elements. To make things more confusing, text elements have both stroke and fill. Text will often look better if the stroke is removed, unless is is large enough to have substantial space within the stroke.

Don’t include curly braces

If you are familiar with CSS, you might be tempted to wrap your CSS string in curly braces. gf_girafe() takes care of adding those for you, so don’t include them in your string.

2.2 Hover options

2.2.1 Hover CSS

Use opts_hover to style hovered data elements, opts_hover_inv to style non-hovered data elements, and opts_hover_key to style hovered guide elements. Common CSS properties for styling these elements include

We can use opacity to improve our interactive facets.

diamonds_bargraph_3 |>
  gf_theme(theme_facets_interactive(theme_minimal())) |>
  gf_facet_wrap_interactive(
    ~clarity, # or vars(clarity)
    interactive_on = "both",
    ncol = 2,
    labeller = gf_labeller_interactive(
      tooltip = ~ paste("this is clarity", clarity),
      data_id = ~clarity
    )
  ) |>
  gf_girafe(
    options = list(
      opts_hover("fill:red; opacity: 0.5")
    )
  )

This still leaves room for some improvement as our hover option is affecting both the strip rectangle and the strip text.

2.2.2 girafe_css()

Sometimes we need finer control over what gets styled by our css. For example, when using interactive facets or gf_label_interactive(), the interactive elements include both text and rectangles, which we may wish to style differently. girafe_css() provides this finer control. The css argument provides a starting point which can be overridden with the subsequent arguments: text, point, line, area (used for rects, polygons, and paths), and image.

diamonds_bargraph_3 |>
  gf_theme(theme_facets_interactive(theme_minimal())) |>
  gf_facet_wrap_interactive(
    ~clarity, # or vars(clarity)
    interactive_on = "both",
    ncol = 2,
    labeller = gf_labeller_interactive(
      tooltip = ~ paste("this is clarity", clarity),
      data_id = ~clarity
    )
  ) |>
  gf_girafe(
    options = list(
      opts_hover(
        css = girafe_css(
          css = "fill:red; opacity:0.7; stroke:black; stroke-width:3px;",
          text = "stroke:none; fill:white; opacity:0.9;"
        )
      )
    )
  )

Here is another example, this time using gf_label_interactive().

mtcars2[1:6, ] |>
  gf_label_interactive(qsec ~ disp, label = ~carname, data_id = ~carname) |>
  gf_girafe(
    options = list(
      opts_hover(
        css = girafe_css(
          css = "fill:yellow;",
          text = "stroke:none; fill:red;"
        )
      )
    )
  )

2.2.3 Key and inverse hovering

Styling hovering on a guide element (part of the legend or key) is handled with opts_hover_key() in the same way we used opts_hover(). We can also style the non-selected elements with opts_hover_inv().

Use low opacity in non-selected elements to make highlighted elements stand out.

The use of low opacity in non-hovered elements can be used to highlight the selected elements.

mosaicData::Weather |>
  gf_line_interactive(
    high_temp ~ date,
    color = ~city,
    show.legend = FALSE,
    tooltip = ~city,
    data_id = ~city
  ) |>
  gf_facet_wrap_interactive(
    ~year,
    ncol = 1,
    scales = "free_x",
    labeller = gf_labeller_interactive(
      data_id = ~year,
      tooltip = ~ glue::glue("This is the year {year}")
    )
  ) |>
  gf_theme(theme_facets_interactive()) |>
  gf_girafe(
    options = list(
      opts_hover_inv(css = "opacity:0.2;"),
      opts_hover(css = "stroke-width:2;", nearest_distance = 40),
      opts_tooltip(use_cursor_pos = FALSE, offx = 0, offy = -30)
    )
  )

2.3 Tooltip options

2.3.1 Position

opts_tooltip() has three arguments for determining the position of the tooltip:

cars_scatter |>
  gf_girafe(
    options = list(
      opts_tooltip(offx = 0, offy = -30, use_cursor_pos = FALSE)
    )
  )

2.3.2 Autocoloring

If we set use_fill = TRUE, then the fil color of the tooltip will match the color of the plot element it is associated to.

diamonds_bargraph_3 |>
  gf_girafe(
    options = list(
      opts_tooltip(
        use_fill = TRUE,
        offx = 0,
        offy = 0,
        use_cursor_pos = FALSE,
        css = "border: 2px solid black; color: aliceblue; border-radius: 4px; padding: 6px;"
      )
    )
  )

A downside of use_fill = TRUE

Depending on your color scheme, you may find it hard to find a text color that works well over all the different fill colors.

2.4 Zoom options

We can enable panning and zooming by choosing a value of max greater than 1 in opts_zoom().

cars_scatter |>
  gf_girafe(
    options = list(opts_zoom(max = 5))
  )

2.5 Global options

We can set options globally using set_girafe_defaults()

set_girafe_defaults(
  # set colors for
  opts_hover = opts_hover(
    css = "fill:yellow;stroke:black;stroke-width:3px;r:10px;"
  ),
  opts_hover_inv = opts_hover_inv(css = "opacity:0.5"),
  # allow zooming/panning up to 4x size
  opts_zoom = opts_zoom(min = 1, max = 4),
  opts_tooltip = opts_tooltip(
    css = "padding: 2px; border: 4px solid navy; background-color: steelblue; color: white; border-radius: 8px"
  ),
  opts_sizing = opts_sizing(rescale = TRUE),
  opts_toolbar = opts_toolbar(
    saveaspng = FALSE,
    position = "bottom",
    delay_mouseout = 5000
  )
)

cars_scatter |>
  gf_girafe()

cars_scatter |>
  gf_girafe(
    options = list(
      opts_tooltip(offx = 0, offy = -25, use_cursor_pos = FALSE)
    )
  )

Warning

Notice that using opts_tooltip() in the options argument of gf_girafe() not only changes offx, offy, and use_cursor_pos, but also causes css to revert to the package defaults rather than to the session defaults we set.


  1. Technically, we are seeing the data after both the statistic and the scale have been applied. See below.↩︎