Interactive Tooltip Tables

tutorial data visualization ggplot2 purrr

How to include tables in your {ggiraph} tooltips.

Kyle Cuilla
09-30-2022

About

In this tutorial, I’ll show you how to add tables to interactive {ggiraph} tooltips like the one I created below using the {kableExtra} and {gt}/{gtExtras} packages.

Show code
knitr::include_graphics("https://raw.githubusercontent.com/kcuilla/USgasprices/main/imgs/gas_map_demo.gif")

Source: US Gas Prices Shiny App

As an added bonus, I’ll show you a trick on how to apply conditional formatters from {gtExtras} to the tooltips by parsing the raw HTML content of the table.

Interactive Tooltips

{ggiraph} is an amazing package that makes any {ggplot2} graphic interactive.

The example below, which comes from the package site, shows how easy it is to make a {ggplot2} interactive:

Show code
library(ggplot2)
library(ggiraph)
library(dplyr)

# load mtcars dataset
data <- mtcars %>% dplyr::select(qsec, wt, disp, mpg, hp, cyl)
data$car <- row.names(data)

# default ggiraph tooltip
gg_point <- ggplot2::ggplot(data = data) +
  ggiraph::geom_point_interactive(aes(
    x = wt,
    y = qsec,
    color = disp,
    data_id = car,
    # display car in the tooltip
    tooltip = car
  )) +
  ggplot2::theme_minimal()

# pass through girafe to activate interactivity
ggiraph::girafe(ggobj = gg_point)

If you hover your mouse over the data points on the chart, you will see the car name within the tooltip. But what if we wanted to add more info to the tooltip such as the car’s mpg, hp, and number of cyl? How would we do that?

Well if you’ve made it this far, you probably already know the answer: tables! How do we do that exactly? I’ll explain step-by-step below.

Using {kableExtra} to create the table for the tooltip

The first thing we need to do is to design our table. In this example, we’ll use the {kableExtra} package to build the table.

Later, I will also show you how to use the {gt} and {gtExtras} packages.

Here’s a preview of a simple table built with {kableExtra} with the columns that we need:

Show code
library(kableExtra)
library(dplyr)

table <- data %>%
  dplyr::select(car, mpg, hp, cyl) %>% 
  kableExtra::kbl(row.names = FALSE)

table
car mpg hp cyl
Mazda RX4 21.0 110 6
Mazda RX4 Wag 21.0 110 6
Datsun 710 22.8 93 4
Hornet 4 Drive 21.4 110 6
Hornet Sportabout 18.7 175 8
Valiant 18.1 105 6
Duster 360 14.3 245 8
Merc 240D 24.4 62 4
Merc 230 22.8 95 4
Merc 280 19.2 123 6
Merc 280C 17.8 123 6
Merc 450SE 16.4 180 8
Merc 450SL 17.3 180 8
Merc 450SLC 15.2 180 8
Cadillac Fleetwood 10.4 205 8
Lincoln Continental 10.4 215 8
Chrysler Imperial 14.7 230 8
Fiat 128 32.4 66 4
Honda Civic 30.4 52 4
Toyota Corolla 33.9 65 4
Toyota Corona 21.5 97 4
Dodge Challenger 15.5 150 8
AMC Javelin 15.2 150 8
Camaro Z28 13.3 245 8
Pontiac Firebird 19.2 175 8
Fiat X1-9 27.3 66 4
Porsche 914-2 26.0 91 4
Lotus Europa 30.4 113 4
Ford Pantera L 15.8 264 8
Ferrari Dino 19.7 175 6
Maserati Bora 15.0 335 8
Volvo 142E 21.4 109 4

If we replace ‘car’ with our table in the tooltip option of ggiraph::geom_point_interactive(), the full table will appear when hovering over each point on the plot.

Our table is showing within the tooltip, but this isn’t quite what we want. Instead, we want to show the values that are relevant for each specific car.

Show code
gg_point <- ggplot2::ggplot(data = data) +
  ggiraph::geom_point_interactive(aes(
    x = wt,
    y = qsec,
    color = disp,
    data_id = car,
    tooltip = table
  )) +
  ggplot2::theme_minimal()

girafe(ggobj = gg_point)

To fix this, we need to create a column within our dataset that contains a table for each row. We can write a function that will loop through each car and add its corresponding data from the mpg, hp, and cyl columns using the {purrr} package.

Creating a table for each observation

We’ll start by creating a simple function that filters our dataset based on the car, selects the columns we need for our table, and builds the table with {kableExtra}. This is the same code we used to build our tables in the previous section, the only difference is that we’re adding a parameter to filter on the car before building our table.

Show code
make_table <- function(name) {
  data %>%
    # filter by car name
    dplyr::filter(car == name) %>% 
    dplyr::select(car, mpg, hp, cyl) %>% 
    kableExtra::kbl(row.names = FALSE) 
}

Now that we have our function, we can use purrr::map() to iterate over each car in the dataset and store the tables in a column called ‘table’.

When we look at the updated dataset, we can see that the table column contains the raw HTML that is used to create the tables in the {kableExtra} package.

Show code
library(purrr)

df <- data %>% 
  dplyr::mutate(table = purrr::map(car, make_table)) %>% 
  dplyr::select(car, qsec, wt, disp, table)

head(df)
                                car  qsec    wt disp
Mazda RX4                 Mazda RX4 16.46 2.620  160
Mazda RX4 Wag         Mazda RX4 Wag 17.02 2.875  160
Datsun 710               Datsun 710 18.61 2.320  108
Hornet 4 Drive       Hornet 4 Drive 19.44 3.215  258
Hornet Sportabout Hornet Sportabout 17.02 3.440  360
Valiant                     Valiant 20.22 3.460  225
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          table
Mazda RX4                   <table>\n <thead>\n  <tr>\n   <th style="text-align:left;"> car </th>\n   <th style="text-align:right;"> mpg </th>\n   <th style="text-align:right;"> hp </th>\n   <th style="text-align:right;"> cyl </th>\n  </tr>\n </thead>\n<tbody>\n  <tr>\n   <td style="text-align:left;"> Mazda RX4 </td>\n   <td style="text-align:right;"> 21 </td>\n   <td style="text-align:right;"> 110 </td>\n   <td style="text-align:right;"> 6 </td>\n  </tr>\n</tbody>\n</table>
Mazda RX4 Wag           <table>\n <thead>\n  <tr>\n   <th style="text-align:left;"> car </th>\n   <th style="text-align:right;"> mpg </th>\n   <th style="text-align:right;"> hp </th>\n   <th style="text-align:right;"> cyl </th>\n  </tr>\n </thead>\n<tbody>\n  <tr>\n   <td style="text-align:left;"> Mazda RX4 Wag </td>\n   <td style="text-align:right;"> 21 </td>\n   <td style="text-align:right;"> 110 </td>\n   <td style="text-align:right;"> 6 </td>\n  </tr>\n</tbody>\n</table>
Datsun 710                <table>\n <thead>\n  <tr>\n   <th style="text-align:left;"> car </th>\n   <th style="text-align:right;"> mpg </th>\n   <th style="text-align:right;"> hp </th>\n   <th style="text-align:right;"> cyl </th>\n  </tr>\n </thead>\n<tbody>\n  <tr>\n   <td style="text-align:left;"> Datsun 710 </td>\n   <td style="text-align:right;"> 22.8 </td>\n   <td style="text-align:right;"> 93 </td>\n   <td style="text-align:right;"> 4 </td>\n  </tr>\n</tbody>\n</table>
Hornet 4 Drive       <table>\n <thead>\n  <tr>\n   <th style="text-align:left;"> car </th>\n   <th style="text-align:right;"> mpg </th>\n   <th style="text-align:right;"> hp </th>\n   <th style="text-align:right;"> cyl </th>\n  </tr>\n </thead>\n<tbody>\n  <tr>\n   <td style="text-align:left;"> Hornet 4 Drive </td>\n   <td style="text-align:right;"> 21.4 </td>\n   <td style="text-align:right;"> 110 </td>\n   <td style="text-align:right;"> 6 </td>\n  </tr>\n</tbody>\n</table>
Hornet Sportabout <table>\n <thead>\n  <tr>\n   <th style="text-align:left;"> car </th>\n   <th style="text-align:right;"> mpg </th>\n   <th style="text-align:right;"> hp </th>\n   <th style="text-align:right;"> cyl </th>\n  </tr>\n </thead>\n<tbody>\n  <tr>\n   <td style="text-align:left;"> Hornet Sportabout </td>\n   <td style="text-align:right;"> 18.7 </td>\n   <td style="text-align:right;"> 175 </td>\n   <td style="text-align:right;"> 8 </td>\n  </tr>\n</tbody>\n</table>
Valiant                     <table>\n <thead>\n  <tr>\n   <th style="text-align:left;"> car </th>\n   <th style="text-align:right;"> mpg </th>\n   <th style="text-align:right;"> hp </th>\n   <th style="text-align:right;"> cyl </th>\n  </tr>\n </thead>\n<tbody>\n  <tr>\n   <td style="text-align:left;"> Valiant </td>\n   <td style="text-align:right;"> 18.1 </td>\n   <td style="text-align:right;"> 105 </td>\n   <td style="text-align:right;"> 6 </td>\n  </tr>\n</tbody>\n</table>

Now, when we feed the table column into the tooltip, we should get a single table for each car on the plot!

Show code
gg_point <- ggplot2::ggplot(data = df) +
  ggiraph::geom_point_interactive(aes(
    x = wt,
    y = qsec,
    color = disp,
    data_id = car,
    tooltip = table
  )) +
  ggplot2::theme_minimal()

ggiraph::girafe(ggobj = gg_point)

Customizing the tooltip

We can further customize the appearance of the tooltip tables by using styles from the {kableExtra} package.

In order to do that, we just need to modify the function we used to create the tables for each car and apply the styles as shown below:

Show code
make_table <- function(name) {
  data %>%
    # filter by car name
    dplyr::filter(car == name) %>% 
    dplyr::select(car, mpg, hp, cyl) %>% 
    kableExtra::kbl(row.names = FALSE) %>%
    # change the font family and increase font size
    kableExtra::kable_styling(font_size = 24, html_font = "Courier New") %>% 
    # increase the width of the columns, make the text blue and bold, apply white background
    kableExtra::column_spec(1:4, width = "3em", bold = T, color = "blue", background = "white")
}

df <- data %>% 
  dplyr::mutate(table = purrr::map(car, make_table)) %>% 
  dplyr::select(car, qsec, wt, disp, table)

And then call the table within our chart using the same method as before:

Show code
gg_point <- ggplot2::ggplot(data = df) +
  ggiraph::geom_point_interactive(aes(
    x = wt,
    y = qsec,
    color = disp,
    data_id = car,
    tooltip = table
  )) +
  ggplot2::theme_minimal()

ggiraph::girafe(ggobj = gg_point)

Using {gt} & {gtExtras}

In addition to the {kableExtra} package, we can also use the {gt} and {gtExtras} packages to build tables for our tooltip.

For this example, we are going to build a {gt} table that displays the most populous city in each U.S. city (based on the 2010 U.S. Census). The dataset comes from the {usmap} package, which we will also use to build a U.S. map in the next section.

Here is what the full {gt} table looks like with a theme applied from the {gtExtras} package:

Show code
library(gt)
library(gtExtras)
library(usmap)

# load city population dataset from {usmap}
cities_t <- usmap::usmap_transform(citypop) %>%
    # remove DC from dataset
    dplyr::filter(!state %in% c('District of Columbia')) %>%
    # sort by state
    dplyr::arrange(state)

gt_table <- cities_t %>% 
    dplyr::arrange(state) %>%
    dplyr::select(state, city = most_populous_city, city_pop) %>% 
    # create a {gt} table
    gt::gt() %>% 
    # add comma delimeters to the city_pop column
    gt::fmt_number(columns = city_pop, decimals = 0) %>%
    # adjust column widths
    gt::cols_width(everything() ~ px(120)) %>%
    # apply the espn theme from {gtExtras}
    gtExtras::gt_theme_espn() %>%
    # add a title and subtitle to the table
    gt::tab_header(title = "Most Populous City in Each State", subtitle = "Source: US Census 2010") 

gt_table
Most Populous City in Each State
Source: US Census 2010
state city city_pop
Alabama Birmingham 212,237
Alaska Anchorage 291,826
Arizona Phoenix 1,445,632
Arkansas Little Rock 193,524
California Los Angeles 3,792,621
Colorado Denver 600,158
Connecticut Bridgeport 144,229
Delaware Wilmington 70,851
Florida Jacksonville 880,619
Georgia Atlanta 420,003
Hawaii Honolulu 337,256
Idaho Boise 205,671
Illinois Chicago 2,695,598
Indiana Indianapolis 820,445
Iowa Des Moines 215,472
Kansas Wichita 382,368
Kentucky Louisville 597,337
Louisiana New Orleans 343,829
Maine Portland 66,194
Maryland Baltimore 620,961
Massachusetts Boston 617,594
Michigan Detroit 713,777
Minnesota Minneapolis 382,578
Mississippi Jackson 173,514
Missouri Kansas City 459,787
Montana Billings 104,170
Nebraska Omaha 466,893
Nevada Las Vegas 583,756
New Hampshire Manchester 109,565
New Jersey Newark 277,140
New Mexico Albuquerque 545,852
New York New York City 8,175,133
North Carolina Charlotte 731,424
North Dakota Fargo 105,549
Ohio Columbus 879,170
Oklahoma Oklahoma City 579,999
Oregon Portland 583,776
Pennsylvania Philadelphia 1,526,006
Rhode Island Providence 178,042
South Carolina Charleston 129,272
South Dakota Sioux Falls 153,888
Tennessee Nashville 660,388
Texas Houston 2,099,451
Utah Salt Lake City 186,440
Vermont Burlington 42,417
Virginia Virginia Beach 437,994
Washington Seattle 608,660
West Virginia Charleston 51,400
Wisconsin Milwaukee 594,833
Wyoming Cheyenne 59,466

Extracting the HTML content from a {gt} table

An important thing to note here is that if we were to apply a {gt} table, such as the one above, directly to {ggiraph}, it would not appear in our tooltip. If you remember earlier when we were using the {kableExtra} package, the tooltip column we created for our tables contained the raw HTML of the table. That is because, by default, {kableExtra} gives you the HTML content that was used to create the table. The {gt} package, however, does not do this by default. Thankfully, though, there is a way of extracting the HTML content of the table using the gt::as_raw_html() function. We can do this by simply piping the table we created directly into the gt::as_raw_html() function as shown below:

Show code
# get HTML content from {gt} table
gt_table_html <- gt_table %>%
    gt::as_raw_html() 

Now that we have the HTML content of our {gt} table, we can follow the same steps as we did above with our {kableExtra} tables to create a table for each row, or state, in the dataset:

Show code
make_table <- function(name) {
  cities_t %>% 
    # filter by state name
    dplyr::filter(state == name) %>%
    dplyr::arrange(state) %>%
    dplyr::select(state, city = most_populous_city, city_pop) %>% 
    gt::gt() %>% 
    gt::fmt_number(columns = city_pop, decimals = 0) %>%
    gt::cols_width(everything() ~ px(120)) %>%
    gtExtras::gt_theme_espn() %>%
    gt::tab_header(title = "Most Populous City in Each State", subtitle = "Source: US Census 2010") %>%
    # get HTML content of table
    gt::as_raw_html()
}

cities_t <- cities_t %>%
  dplyr::mutate(tooltip = purrr::map(state, make_table))

gg_map <- usmap::plot_usmap(fill = "white", alpha = 0.25) +
        ggiraph::geom_point_interactive(
          data = cities_t, 
          ggplot2::aes(
            x = x,
            y = y,
            size = city_pop,
            tooltip = tooltip,
            data_id = state
          ),
          color = "purple",
          alpha = 0.8
        ) +
  scale_size_continuous(range = c(1, 16),
                        label = scales::comma) +
  labs(title = "Most Populous City in Each State",
       subtitle = "Source: US Census 2010",
       size = "City Population") +
  theme(legend.position = "right")

ggiraph::girafe(ggobj = gg_map)

Using conditional formatters from {gtExtras}

Let’s say that we wanted to add a column to our table that shows a horizontal bar chart for each city’s population. We can do so by adding gtExtras::gt_color_rows() to our table as shown below:

Show code
cities_t <- usmap_transform(citypop) %>%
  dplyr::filter(!state %in% c('District of Columbia','Alaska','Hawaii')) %>%
  dplyr::arrange(state)

gt_table <- cities_t %>% 
    dplyr::arrange(state) %>%
    dplyr::select(state, city = most_populous_city, city_pop) %>% 
    gt::gt() %>% 
    gt::fmt_number(columns = city_pop, decimals = 0) %>%
    # add horizontal bar chart to values based on relative population size
    gtExtras::gt_plt_bar(city_pop, keep_column = TRUE) %>%
    gtExtras::gt_theme_espn() %>%
    gt::tab_header(title = "Most Populous City in Each State", subtitle = "Source: US Census 2010")

gt_table
Most Populous City in Each State
Source: US Census 2010
state city city_pop city_pop
Alabama Birmingham 212,237
Arizona Phoenix 1,445,632
Arkansas Little Rock 193,524
California Los Angeles 3,792,621
Colorado Denver 600,158
Connecticut Bridgeport 144,229
Delaware Wilmington 70,851
Florida Jacksonville 880,619
Georgia Atlanta 420,003
Idaho Boise 205,671
Illinois Chicago 2,695,598
Indiana Indianapolis 820,445
Iowa Des Moines 215,472
Kansas Wichita 382,368
Kentucky Louisville 597,337
Louisiana New Orleans 343,829
Maine Portland 66,194
Maryland Baltimore 620,961
Massachusetts Boston 617,594
Michigan Detroit 713,777
Minnesota Minneapolis 382,578
Mississippi Jackson 173,514
Missouri Kansas City 459,787
Montana Billings 104,170
Nebraska Omaha 466,893
Nevada Las Vegas 583,756
New Hampshire Manchester 109,565
New Jersey Newark 277,140
New Mexico Albuquerque 545,852
New York New York City 8,175,133
North Carolina Charlotte 731,424
North Dakota Fargo 105,549
Ohio Columbus 879,170
Oklahoma Oklahoma City 579,999
Oregon Portland 583,776
Pennsylvania Philadelphia 1,526,006
Rhode Island Providence 178,042
South Carolina Charleston 129,272
South Dakota Sioux Falls 153,888
Tennessee Nashville 660,388
Texas Houston 2,099,451
Utah Salt Lake City 186,440
Vermont Burlington 42,417
Virginia Virginia Beach 437,994
Washington Seattle 608,660
West Virginia Charleston 51,400
Wisconsin Milwaukee 594,833
Wyoming Cheyenne 59,466

As you can see, the size of each bar is relative to the overall distribution of population sizes within the column. This would be something fun to add to our tooltip, but look what happens when we do using the same method as before:

Show code
make_table <- function(name) {
  cities_t %>% 
    # filter by state name
    dplyr::filter(state == name) %>%
    dplyr::arrange(state) %>%
    dplyr::select(state, city = most_populous_city, city_pop) %>% 
    gt::gt() %>% 
    gt::fmt_number(columns = city_pop, decimals = 0) %>%
    # add horizontal bar chart to values based on relative population size
    gtExtras::gt_plt_bar(city_pop, keep_column = TRUE) %>%
    gtExtras::gt_theme_espn() %>%
    gt::tab_header(title = "Most Populous City in Each State", subtitle = "Source: US Census 2010") %>%
    # get HTML content of table
    gt::as_raw_html()
}

cities_t <- cities_t %>%
  dplyr::mutate(tooltip = purrr::map(state, make_table))

gg_map <- usmap::plot_usmap(fill = "white", alpha = 0.25) +
        ggiraph::geom_point_interactive(
          data = cities_t, 
          ggplot2::aes(
            x = x,
            y = y,
            size = city_pop,
            tooltip = tooltip,
            data_id = state
          ),
          color = "purple",
          alpha = 0.8
        ) +
  scale_size_continuous(range = c(1, 16),
                        label = scales::comma) +
  labs(title = "Most Populous City in Each State",
       subtitle = "Source: US Census 2010",
       size = "City Population") +
  theme(legend.position = "right")

ggiraph::girafe(ggobj = gg_map)

Did you notice in the map above that all of the purple bar charts were exactly the same length regardless of which state you hovered over? That’s because gtExtras::gt_plt_bar() determines the length of each horizontal bar based on how that value compares to other values within the column. But, since we filter each state BEFORE building our {gt} table, gtExtras::gt_plt_bar() only sees one value within the column and assigns it the same length regardless if the value is 1 or 10,000 because it has no other value to compare it with.

You may be wondering why we didn’t apply our dplyr::filter() after building our {gt} table instead of before, and the reason is simply because we can’t. Once we pass data through a {gt} table, it gets converted to a gt_tbl object and is no longer compatible with dplyr functions. However, through some HTML-parsing trickery outlined in the next section, we can still filter our {gt} table thanks to the extracted HTML content via gt::as_raw_html().

Extracting HTML content from {gt} tables

HTML table basics

Before diving in to the HTML output from {gt} tables, it may help to understand the basic structure of HTML tables.

Below is a simple example of a table created with HTML. Every HTML table starts with <table> and ends with </table>. Within the table, the names of the columns are defined in table header, or <th> cells which appear as <th>Column Name</th>. Each row in the table starts with <tr> and the data values are stored within <td>Value</td>.

Show code
"<table>
  <tr>
    <th>Column 1</th>
  </tr>
  <tbody>
    <tr>
      <td>Row 1</td>
    </tr>
    <tr>
      <td>Row 2</td>
    </tr>
  </tbody>
</table>"
Column 1
Row 1
Row 2

There are many additional options within HTML tables, such as a table title (<caption>), a table footer (<tfoot>), and styling elements that contain CSS code.

However, it’s not necessary to know all of that, because all we’re looking for are the names of the states within the table. And given the info above, we know the states will be contained within a row (<tr>) followed by a data cell (<td>) containing the state name, such as: <tr><td>California.

Extracting the head of the table

I mentioned that we will be filtering the part of the table that contains the data for each state so that we can capture the correct size of the horizontal bar charts based on the state’s population. However, before we do that, we need to extract the head of table first. Once we have the HTML content for the head of the table, we can append the HTML content for each one of the states to it so that we can have a complete HTML table for each state.

To get the HTML content for the head of the table, we can convert the output to a character vector and use strsplit() to split the vector at the point when reach <tr><td which marks the start of the rows that contain our state data. When we run this, it splits our table before each row and stores it within a list. Since we have 48 continental states within our dataset plus the header of the table (remember, even the table headers in an HTML table start with <tr>), our list will contain 49 elements in total:

Show code
# the code used to create our dataset and HTML table:
cities_t <- usmap_transform(citypop) %>%
  dplyr::filter(!state %in% c('District of Columbia','Alaska','Hawaii')) %>%
  dplyr::arrange(state)

gt_table_html <- cities_t %>% 
    dplyr::arrange(state) %>%
    dplyr::select(state, city = most_populous_city, city_pop) %>% 
    gt::gt() %>% 
    gt::fmt_number(columns = city_pop, decimals = 0) %>%
    gtExtras::gt_plt_bar(city_pop, keep_column = TRUE) %>%
    gtExtras::gt_theme_espn() %>%
    gt::tab_header(title = "Most Populous City in Each State", subtitle = "Source: US Census 2010") %>%
    gt::as_raw_html()
Show code
length(strsplit(as.character(gt_table_html), "<tr><td")[[1]])
[1] 49

So, based on what we described above, the head of the table will be contained within the first element of our list, while the data for the states will be contained in the other elements.

Let’s store the head of the table as table_head so that we can append the HTML for the states to it later:

Show code
table_head <- strsplit(as.character(gt_table_html), "<tr><td")[[1]][1]
table_head
[1] "<table style=\"font-family: Lato, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', 'Fira Sans', 'Droid Sans', Arial, sans-serif; display: table; border-collapse: collapse; margin-left: auto; margin-right: auto; color: #333333; font-size: 16px; font-weight: normal; font-style: normal; background-color: #FFFFFF; width: auto; border-top-style: solid; border-top-width: 3px; border-top-color: #FFFFFF; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #A8A8A8; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3;\">\n  <thead style=\"\">\n    <tr>\n      <td colspan=\"4\" style=\"background-color: #FFFFFF; text-align: left; border-bottom-color: #FFFFFF; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; color: #333333; font-size: 24px; font-weight: initial; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; border-bottom-width: 0; font-weight: normal;\" style>Most Populous City in Each State</td>\n    </tr>\n    <tr>\n      <td colspan=\"4\" style=\"background-color: #FFFFFF; text-align: left; border-bottom-color: #FFFFFF; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; color: #333333; font-size: 85%; font-weight: initial; padding-top: 0; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; border-top-color: #FFFFFF; border-top-width: 0; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; font-weight: normal;\" style>Source: US Census 2010</td>\n    </tr>\n  </thead>\n  <thead style=\"border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3;\">\n    <tr>\n      <th style=\"color: #333333; background-color: #FFFFFF; font-size: 80%; font-weight: bolder; text-transform: uppercase; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: left;\" rowspan=\"1\" colspan=\"1\" scope=\"col\">state</th>\n      <th style=\"color: #333333; background-color: #FFFFFF; font-size: 80%; font-weight: bolder; text-transform: uppercase; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: left;\" rowspan=\"1\" colspan=\"1\" scope=\"col\">city</th>\n      <th style=\"color: #333333; background-color: #FFFFFF; font-size: 80%; font-weight: bolder; text-transform: uppercase; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;\" rowspan=\"1\" colspan=\"1\" scope=\"col\">city_pop</th>\n      <th style=\"color: #333333; background-color: #FFFFFF; font-size: 80%; font-weight: bolder; text-transform: uppercase; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: left;\" rowspan=\"1\" colspan=\"1\" scope=\"col\">city_pop</th>\n    </tr>\n  </thead>\n  <tbody style=\"border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3;\">\n    "
Show code
/* CSS code to prevent HTML output from truncating in output */ 
pre code {
  white-space: pre-wrap;
}

Extracting the body of the table

The data for the states are stored within elements 2 through 49. Before creating the table, we sorted the states in alphabetical order, so the first state that appears in our HTML should be Alabama. There’s a lot of style content within the HTML output shown below, but if you look close enough, you should be able to see the state name (Alabama), city (Birmingham), and population (212,237).

Show code
strsplit(as.character(gt_table_html), "<tr><td")[[1]][2]
[1] " style=\"padding-top: 7px; padding-bottom: 7px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #F6F7F7; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;\">Alabama</td>\n<td style=\"padding-top: 7px; padding-bottom: 7px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #F6F7F7; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;\">Birmingham</td>\n<td style=\"padding-top: 7px; padding-bottom: 7px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #F6F7F7; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;\">212,237</td>\n<td style=\"padding-top: 7px; padding-bottom: 7px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #F6F7F7; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;\"><?xml version='1.0' encoding='UTF-8' ?><svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' width='198.43pt' height='14.17pt' viewBox='0 0 198.43 14.17'><defs>  <style type='text/css'><![CDATA[    .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle {      fill: none;      stroke: #000000;      stroke-linecap: round;      stroke-linejoin: round;      stroke-miterlimit: 10.00;    }    .svglite text {      white-space: pre;    }  ]]></style></defs><rect width='100%' height='100%' style='stroke: none; fill: none;'/><defs>  <clipPath id='cpMC4wMHwxOTguNDN8MC4wMHwxNC4xNw=='>    <rect x='0.00' y='0.00' width='198.43' height='14.17' />  </clipPath></defs><g clip-path='url(#cpMC4wMHwxOTguNDN8MC4wMHwxNC4xNw==)'><rect x='8.78' y='1.77' width='4.47' height='10.63' style='stroke-width: 1.07; stroke: none; stroke-linecap: square; stroke-linejoin: miter; fill: #A020F0;' /><line x1='8.78' y1='14.17' x2='8.78' y2='0.0000000000000018' style='stroke-width: 2.13; stroke-linecap: butt;' /></g></svg></td></tr>\n    "

In order to pull the HTML content for each of the remaining states in our dataset, we will need to create a for loop that will go through each element in our list, extract the HTML content, and append it to the table_head we created in the previous section and store it in a vector called html_tables.

A couple quick things to note are when we use strsplit() to split the HTML on <tr><td, strsplit() actually will remove the <tr><td during the split. So, in order to add it back in, we can just paste it before the split. The other thing is we will need to add </tbody></table> to the end of the table body to tell the HTML to close the body and table so that the table can be created.

Show code
table_body <- c()
for (i in 2:49) {
  table_body[i - 1] <-
    paste0("<tr><td",
           strsplit(as.character(gt_table_html), "<tr><td")[[1]][i],
           "</tbody></table>")
  html_tables <- paste0(table_head, table_body)
}

Adding the tables to our tooltip

To use the HTML tables we created for each state, we will need to create a column containing the code for the HTML within our dataset so that we can call it within the tooltip of ggiraph::geom_point_interactive() just as we did in prior sections.

Now, when we hover over each state, you can see that our bar charts are displaying properly!

Show code
cities_t <- cities_t %>%
  dplyr::mutate(tooltip = data.frame(html_tables))

gg_map <- usmap::plot_usmap(fill = "white", alpha = 0.25) +
        ggiraph::geom_point_interactive(
          data = cities_t, 
          ggplot2::aes(
            x = x,
            y = y,
            size = city_pop,
            tooltip = tooltip$html_tables,
            data_id = state
          ),
          color = "purple",
          alpha = 0.8
        ) +
  scale_size_continuous(range = c(1, 16),
                        label = scales::comma) +
  labs(title = "Most Populous City in Each State",
       subtitle = "Source: US Census 2010",
       size = "City Population") +
  theme(legend.position = "right")

ggiraph::girafe(ggobj = gg_map)

Anoter example of using conditional formatters from {gtExtras} in interactive tooltips

Now that we went over step-by-step on how to add conditional formatters from {gtExtras} to our tooltips, I’ll quickly share another example of how we can create an interactive choropleth map with {ggiraph} and match the color of the state on the map, which pertains to the state’s city with the largest population, to the color of the population within our {gt} table.

Here is the same table we created in the previous section but with gtExtras::gt_color_rows() applied to the city_pop column:

Show code
cities_t <- usmap_transform(citypop) %>%
  dplyr::filter(!state %in% c('District of Columbia','Alaska','Hawaii')) %>%
  dplyr::arrange(state)

gt_table <- cities_t %>% 
    dplyr::arrange(state) %>%
    dplyr::select(state, city = most_populous_city, city_pop) %>% 
    gt::gt() %>% 
    gt::fmt_number(columns = city_pop, decimals = 0) %>%
    gt::cols_width(everything() ~ px(140)) %>% 
    gtExtras::gt_color_rows(city_pop, palette = "ggsci::blue_material") %>%
    gtExtras::gt_theme_espn() %>%
    gt::tab_header(title = "Most Populous City in Each State", subtitle = "Source: US Census 2010")

gt_table
Most Populous City in Each State
Source: US Census 2010
state city city_pop
Alabama Birmingham 212,237
Arizona Phoenix 1,445,632
Arkansas Little Rock 193,524
California Los Angeles 3,792,621
Colorado Denver 600,158
Connecticut Bridgeport 144,229
Delaware Wilmington 70,851
Florida Jacksonville 880,619
Georgia Atlanta 420,003
Idaho Boise 205,671
Illinois Chicago 2,695,598
Indiana Indianapolis 820,445
Iowa Des Moines 215,472
Kansas Wichita 382,368
Kentucky Louisville 597,337
Louisiana New Orleans 343,829
Maine Portland 66,194
Maryland Baltimore 620,961
Massachusetts Boston 617,594
Michigan Detroit 713,777
Minnesota Minneapolis 382,578
Mississippi Jackson 173,514
Missouri Kansas City 459,787
Montana Billings 104,170
Nebraska Omaha 466,893
Nevada Las Vegas 583,756
New Hampshire Manchester 109,565
New Jersey Newark 277,140
New Mexico Albuquerque 545,852
New York New York City 8,175,133
North Carolina Charlotte 731,424
North Dakota Fargo 105,549
Ohio Columbus 879,170
Oklahoma Oklahoma City 579,999
Oregon Portland 583,776
Pennsylvania Philadelphia 1,526,006
Rhode Island Providence 178,042
South Carolina Charleston 129,272
South Dakota Sioux Falls 153,888
Tennessee Nashville 660,388
Texas Houston 2,099,451
Utah Salt Lake City 186,440
Vermont Burlington 42,417
Virginia Virginia Beach 437,994
Washington Seattle 608,660
West Virginia Charleston 51,400
Wisconsin Milwaukee 594,833
Wyoming Cheyenne 59,466

And here is a choropleth map created with {ggplot2} and {ggriaph} without the interactive tooltip activated:

Show code
states_map <- ggplot2::map_data("state")
cities_t$state <- tolower(cities_t$state)

gg_map <- ggplot(cities_t, aes(map_id = state)) +
  ggiraph::geom_map_interactive(
    aes(
      fill = city_pop,
      data_id = state
    ),
    color = "white",
    map = states_map
  ) +
  expand_limits(x = states_map$long, y = states_map$lat) +
  ggsci::scale_fill_material("blue",
                             label = scales::comma) +
  labs(title = "Most Populous City in Each State",
       subtitle = "Source: US Census 2010",
       fill = "City Population") +
  theme_void()

gg_map

By following the same steps in the previous section, we can extract the HTML content from our {gt} table and build our tooltip that contains the same shade of blue for each state that is seen on the map.

Show code
# get HTML content from the {gt} table
gt_table_html <- gt_table %>%
  gt::as_raw_html()

# extract HTML content in the head of the table
table_head <- strsplit(as.character(gt_table_html), "<tr><td")[[1]][1]

# extract HTML content from the body of the table for each state
table_body <- c()
for (i in 2:49) {
  table_body[i - 1] <-
    paste0("<tr><td",
           strsplit(as.character(gt_table_html), "<tr><td")[[1]][i],
           "</tbody></table>")
  html_tables <- paste0(table_head, table_body)
}

# add the HTML tables to our dataset
cities_t <- cities_t %>%
  dplyr::mutate(tooltip = data.frame(html_tables))

gg_map <- ggplot(cities_t, aes(map_id = state)) +
  ggiraph::geom_map_interactive(
    aes(
      fill = city_pop,
      data_id = state,
      tooltip = tooltip$html_tables
    ),
    color = "white",
    map = states_map
  ) +
  expand_limits(x = states_map$long, y = states_map$lat) +
  ggsci::scale_fill_material("blue",
                             label = scales::comma) +
  labs(title = "Most Populous City in Each State",
       subtitle = "Source: US Census 2010",
       fill = "City Population") +
  theme_void()

ggiraph::girafe(ggobj = gg_map, width_svg = 5, height_svg = 3)

Display table in stable form

If you don’t want the tables to follow the cursor as you hover, you can place them in a stable position by setting use_cursor_pos to FALSE and adjusting the position of where you want the table to be displayed by utilizing the offx and offy options within opts_tooltip() of {ggiraph}:

Show code
ggiraph::girafe(
  ggobj = gg_map,
  options = list(opts_tooltip(
    offx = 50,
    offy = 425,
    use_cursor_pos = FALSE
  )),
  width_svg = 5,
  height_svg = 3
)






Other table-making packages

In this tutorial, we’ve gone through how to build {kableExtra}, {gt}/{gtExtras} tables and place them within {ggiraph} tooltips. Because we need the raw HTML of the table output in order for {ggiraph} to use the table as a tooltip, that limits the types of table-building packages we can use. For example, tables built with {reactable}/{reactablefmtr} are not compatible with {ggiraph} because their output is in JSON format. Thankfully, the {kableExtra} and {gt}/{gtExtras} packages are highly flexible and should give you all the customization options you need for your tooltips.

Citation

For attribution, please cite this work as

Cuilla (2022, Sept. 30). UNCHARTED DATA: Interactive Tooltip Tables. Retrieved from https://uncharteddata.netlify.app/posts/2022-09-30-interactive-tooltip-tables/

BibTeX citation

@misc{cuilla2022interactive,
  author = {Cuilla, Kyle},
  title = {UNCHARTED DATA: Interactive Tooltip Tables},
  url = {https://uncharteddata.netlify.app/posts/2022-09-30-interactive-tooltip-tables/},
  year = {2022}
}