Using glyph in Shiny

glyph plots work in Shiny apps. glyphOutput() puts a plot in the page and renderGlyph() draws it from the server. When the user brushes points, clicks a mark or clicks a legend entry, the server gets the row numbers of what they selected.

This article is built around a small app that comes with glyph. You brush points in a scatterplot of mtcars, and a table lists the cars you selected. Run it with:

shiny::runApp(system.file("examples", "shiny-brush", package = "glyph"))

The example app: a scatterplot of car weight against miles per gallon, with a grey brush rectangle over nine light, efficient cars. Below it, the text '9 cars selected.' and a table of those nine cars.

shiny is not installed with glyph. Install it with install.packages("shiny") before you try this.

The app

This is the whole app:

# Brush points in the scatterplot to list the cars in a table.
#
# Run it with:
#   shiny::runApp(system.file("examples", "shiny-brush", package = "glyph"))

library(shiny)
library(glyph)

cars <- mtcars[, c("mpg", "cyl", "wt", "hp")]
cars$cyl <- as.integer(cars$cyl)
cars$hp <- as.integer(cars$hp)

ui <- fluidPage(
  titlePanel("Brushing cars"),
  p("Drag across the plot to select cars. Press Escape, or click",
    "Clear selection, to start again."),
  glyphOutput("plot", height = "400px"),
  textOutput("count"),
  tableOutput("rows")
)

server <- function(input, output, session) {
  output$plot <- renderGlyph(
    glyph(cars, x = wt, y = mpg) |>
      mark_point(color = factor(cyl)) |>
      interact(brush = TRUE, tooltip = TRUE)
  )

  # Row numbers of `cars`, or NULL when nothing is selected.
  selected <- reactive(input$plot_selected)

  output$count <- renderText({
    n <- length(selected())
    if (is.null(selected())) "No cars selected."
    else if (n == 1) "1 car selected."
    else paste(n, "cars selected.")
  })

  output$rows <- renderTable({
    req(selected())
    cars[selected(), ]
  }, rownames = TRUE)
}

shinyApp(ui, server)

The rest of this section goes through it.

Placing the plot

glyphOutput("plot", height = "400px") reserves space for the plot in the page. The width is 100% by default. The plot is drawn to fit the space it is given.

Drawing the plot

renderGlyph() takes the same pipeline you would print at the console:

output$plot <- renderGlyph(
  glyph(cars, x = wt, y = mpg) |>
    mark_point(color = factor(cyl)) |>
    interact(brush = TRUE, tooltip = TRUE)
)

It also takes a layout from compose(), or a widget from render(). If the expression returns NULL, the output is left empty.

The plot needs something the user can select with. Here that is interact(brush = TRUE). Without it, input$plot_selected never changes.

Reading the selection

The selection arrives as input$plot_selected: the output’s name followed by _selected. It holds the row numbers of the data frame given to glyph(), so cars[input$plot_selected, ] gives the selected rows. When nothing is selected it is NULL. The table uses req() to show nothing until there is a selection.

In a Shiny module, use glyphOutput(ns("plot")) in the module’s UI, and the selection arrives in the module server as input$plot_selected.

When the server hears about it

The value changes when the user does one of these:

Each new selection replaces the last one.

A redraw also clears the selection, and sends NULL. The plot is redrawn when renderGlyph() runs again, for example because a reactive value it uses has changed. It is also redrawn when its output changes size, for example when the browser window is resized.

So keep input$plot_selected out of the plot’s own renderGlyph(). A plot that depends on its own selection is redrawn each time the user selects something, and the selection is lost straight away. Use the selection in other outputs, as the example does with its table.

What the row numbers count

The numbers are positions in the data frame, like those from which(). They are not row names.

They count every row of the data frame, including rows that aren’t drawn. A mark leaves out rows with a missing x or y value, and warns that it did. Those rows are never selected, but the rows after them keep their numbers. A computed mapping such as x = log(wt) doesn’t change the numbers either.

df <- data.frame(x = c(1, NA, 3, 4), y = c(2, 5, 1, 3))
glyph(df, x = x, y = y) |> mark_point() |> interact(brush = TRUE)
# Brushing every point gives c(1L, 3L, 4L).

Selecting a box, violin, histogram bin or polygon selects every row it summarises. A sina mark draws a point for each row, so each point is one row.

A mark that has its own data, such as mark_line(data = trend, x = x, y = fit), draws rows of another data frame. It adds nothing to the value.

Facets and layouts

A facet() grid splits one data frame into panels, so its value is one vector. Each panel has its own brush, and the value holds the rows selected in any of them.

A compose() layout can show different data in each panel. Its value is a list with one entry per panel, in the order the panels were given. If compose() was called with names, the entries have those names:

output$plots <- renderGlyph(
  compose(
    cars = glyph(mtcars, x = wt, y = mpg) |>
      mark_point() |>
      interact(brush = TRUE),
    flowers = glyph(iris, x = Sepal.Length, y = Petal.Length) |>
      mark_point() |>
      interact(brush = TRUE)
  )
)

Here input$plots_selected is NULL until something is selected. After the user brushes a few cars it is, for example, list(cars = c(3L, 18L, 20L), flowers = NULL). Each entry holds row numbers of that panel’s data, so mtcars[input$plots_selected$cars, ] gives the cars. A panel with no selection is NULL. When every panel is back to NULL, so is the whole value. A layout nested inside another gives a nested list of the same shape.

With linked_selections = TRUE, a selection in one panel highlights the matching rows in the others. Only the selection itself is reported. A panel that only highlights another panel’s selection reports NULL.

Limits

Some things are not done yet: