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 is not installed with glyph. Install it with
install.packages("shiny") before you try this.
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.
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.
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.
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.
The value changes when the user does one of these:
integer(0).interact(click = "select"). The mark’s rows are selected.
Clicking it again clears the selection.selection(type = "legend"). Clicking an entry hides its
rows, and the rows still shown are the selection. With every entry shown
again, the value is NULL.NULL.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.
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.
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.
Some things are not done yet: