--- title: "Interactive graphics with {ggformula}" subtitle: "Getting Started" date: last-modified author: Randall Pruim engine: knitr toc: true vignette: > %\VignetteIndexEntry{Interactive graphics with {ggformula}} %\VignetteEngine{quarto::html} %\VignetteEncoding{UTF-8} execute: engine: knitr eval: true echo: true knitr: opts_chunk: collapse: true comment: '#>' fig-width: 6 fig-height: 4 number-sections: true number-depth: 3 embed-resources: true --- # Interactive geoms, scales, and facets ```{r} #| label: setup #| include: false set.seed(1234) library(ggformula) library(dplyr) library(patchwork) ``` ## 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. * hover actions * easy tooltips using `tooltip = ` * restyling elements upon hover * selection of multiple elements at once * linking items across multiple plots * custom javascript to be applied when clicking on a graphical element with `onclick = `. 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](https://www.ardata.fr/ggiraph-book/shiny.html). ## 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. * `tooltip` provides the text (or HTML) to be displayed in the tooltip. * `data_id` provides and identifier for a selection. ```{r} #| label: load-ggformula library(ggformula) theme_set(theme_bw()) ``` ```{r} #| label: cars-scatter 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. ```{r} #| label: cars-scatter-tooltip 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) ``` ## 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. ```{r} #| label: diamonds-bargraph data(diamonds) diamonds_bargraph <- diamonds |> gf_bar_interactive( ~color, fill = ~cut, tooltip = ~ after_stat(count), data_id = ~ as.character(cut) ) diamonds_bargraph |> gf_girafe() ``` :::{.callout-note} ### `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()`.^[Technically, we are seeing the data after both the statistic and the scale have been applied. See below.] ```{r} #| label: layer_data diamonds_bargraph |> layer_data() |> head(3) ``` Some things to notice: * There is one row for each of the bars. * We have access to some new columns that provide information about each of our bars: `count`, `ymin`, `ymax`, `xmin`, `xmax`. These have been computed by the stat. * We can see that our `tooltip` and `data_id` also appear here. * We no longer have access to columns like `color` (it has been mapped to `x` with values 1, 2, 3, ...) or `cut` (instead we have `fill` which shows that actual color being used to fill the bars). These values are computed by scales in a third stage of data transformation. ::: :::{.callout-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 @sec-finer-control. ::: ### Finer control {#sec-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. ```{r} #| label: diamonds-colgraph-1 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() ``` 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. ```{r} #| label: diamonds-col-graph-2 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() ``` #### 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. ```{r} #| label: diamonds-bargraph2 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 ) diamonds_bargraph_2 |> gf_girafe() ``` :::{.callout-note} ### `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, * `start = glue::glue("color: {color}; cut: {cut}"),` creates a column named `tooltip` that contains the text identifying the color and cut of a diamond. * `after_stat = glue::glue("cut: {tooltip}; count = {count}"))` uses `tooltip` value created at the start stage to create a new `tooltip` column after the stat has been calculated and `count` is available. If you have not encountered `stage()` before, you can learn more in the [documentation for `stage()`](https://ggplot2.tidyverse.org/reference/aes_eval.html). ::: ## Interactive scales Interactive scales can be used inside `gf_refine()` and generate interactive guides (legends, axes, etc.). ```{r} #| label: interactive-scales 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 . ## 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. ```{r} #| label: faceting 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() ``` :::{.callout-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. ```{r} #| label: diamonds-bargraph4 diamonds_bargraph_4 |> layer_data() |> slice_sample(n=4) ``` ::: ## Interactive themes {ggiraph} provides 3 intereactive elements for use in interactive themes: * `element_text_interactive()` * `element_rect_interactive()` * `element_line_interactive()` These are drop-in replacements for their non-interactive counterparts. * They can be used with `gf_theme()` to add interactive theme elements to an individual plot, as was done the previous example; * They can be used with `set_girafe_defaults()`; or * They can be included in custom theme functions. {ggformula} provides `theme_facets_interactive()` for adding interactive elements for faceting to a theme of your choice. ```{r} #| label: theme-facets-interactive 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 @sec-customizing to see how to improve the hover interaction. ## 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. ```{r} #| label: patchwork 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) ``` ## 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. ### Alerts ```{r} #| label: js-alert 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() ``` ### Opening another webpage In the example below, we use this to open a webpage with related information. ```{r} #| label: js-window-open 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() ``` # Customizing girafe animations {#sec-customizing} 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: * `fonts = list(...)` * `opts_sizing = opts_sizing(...)` * `opts_tooltip = opts_tooltip(...)` * `opts_hover = opts_hover(...)` * `opts_hover_key = opts_hover_key(...)` * `opts_hover_inv = opts_hover_inv(...)` * `opts_hover_theme = opts_hover_theme(...)` * `opts_selection = opts_selection(...)` * `opts_selection_inv = opts_selection_inv(...)` * `opts_selection_key = opts_selection_key(...)` * `opts_selection_theme = opts_selection_theme(...)` * `opts_zoom = opts_zoom(...)` * `opts_toolbar = opts_toolbar(...)` 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. ## 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`: color for points, etc. * `stroke`: color for lines, text, etc. * `background-color`: background color for text * `fill`: fill color for points, rectangles, etc. * `border-style`, `border-width`, `border-color`: border properties for rectangles, can be combined as in `border: 5px solid red;` * `width`, `height`: size (of tooltip, for example) * `padding`: space around content * `opacity`: opacity (a number between 0 and 1) :::{.callout-warning} ### 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. ::: :::{.callout-warning} ### 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. ::: ## Hover options ### 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 * `fill`: background color * `stroke`: color * `stroke-width`: border width * `r`: circle radius * `opacity`: opacity (a number between 0 and 1) We can use opacity to improve our interactive facets. ```{r} #| label: hover-css 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. ### `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`. ```{r} #| label: girafe-css 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()`. ```{r} #| label: 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;" ) ) ) ) ``` ### 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()`. :::{.callout-tip} ### 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. ::: ```{r} #| label: weather-hover-inv 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) ) ) ``` ## Tooltip options ### Position `opts_tooltip()` has three arguments for determining the position of the tooltip: * `use_cursor_pos` -- a logical indicating whether the tooltip position is relative to the cursor position (default) or to the upper left corner of the plot. * `offx`, `offy` -- the number of pixels to offset the tooltip from this base position. ```{r} #| label: tooltip-position cars_scatter |> gf_girafe( options = list( opts_tooltip(offx = 0, offy = -30, use_cursor_pos = FALSE) ) ) ``` ### 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. ```{r} #| label: autocoloring 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;" ) ) ) ``` :::{.callout-warning} ### 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. ::: ## Zoom options We can enable panning and zooming by choosing a value of `max` greater than 1 in `opts_zoom()`. ```{r} #| label: zoom cars_scatter |> gf_girafe( options = list(opts_zoom(max = 5)) ) ``` ## Global options We can set options globally using `set_girafe_defaults()` ```{r} #| label: 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) ) ) ``` :::{.callout-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. :::