如何在 Shiny 中创建可点击的直方图?
Posted
技术标签:
【中文标题】如何在 Shiny 中创建可点击的直方图?【英文标题】:How to create a clickable histogram in Shiny? 【发布时间】:2022-01-04 17:52:18 【问题描述】:我想在shiny
中创建一个可点击的直方图,但不知道是否可行。
几个月前,我看到了一个可点击的火山图,它为您提供了您点击的内容的表格。
来源:https://2-bitbio.com/2017/12/clickable-volcano-plots-in-shiny.html
我发现的关于创建可点击直方图的最接近的帖子是Click to get coordinates from multiple histogram in shiny
但是,我不想获取坐标。我想要数据框的行名。
有了这个数据框,我每次点击直方图中的一个条时可以得到行名吗?
mtcars <- mtcars %>%
select("hp")
mtcars <- as.matrix(mtcars)
闪亮的一个例子(但不可点击):
library(shiny)
library(ggplot2)
library(scales)
library(dplyr)
ui <- fluidPage(
titlePanel("Histogram"),
sidebarLayout(
sidebarPanel(
),
mainPanel(
plotOutput("hist"),
)
)
)
mtcars <- mtcars %>%
select("hp")
mtcars <- as.matrix(mtcars)
server <- function(input, output)
output$hist <- renderPlot(
pp <- qplot(mtcars, geom = "histogram", bins = 10, xlab="values",
ylab="Frequency", main="Histogram",
fill=I("red"), col=I("black"), alpha=I(0.4))
pp + scale_x_continuous(breaks=pretty(mtcars, n=10))
)
shinyApp(ui = ui, server = server)
有人知道怎么做吗?
提前非常感谢!
问候
【问题讨论】:
【参考方案1】:这是一个很好的问题,它的挑战在于 qplot/ggplot 图表是静态图像。下面的app.r
是我将如何做的一个例子。我很想看看其他方法。
本质上:
-
创建一个数字序列,这些数字既可用作直方图中的断点,也可用作数据框中的间隔。这些是基于用户输入的,但您可以对它们进行硬编码。
根据值所在的间隔为数据帧中的每一行分配一个“bin”值。
记录用户点击事件的 x 坐标,并根据同一组间隔为其分配一个“bin”值。
对您的数据框进行子集化,并仅保留数据的“bin”值与用户点击事件的 x 坐标的“bin”值匹配的记录。
否则,如果你愿意走 d3 路线,你可以探索 R Views 发布的something like this。
#Load libraries ----------------------------------------------------
library(shiny)
library(ggplot2)
library(scales)
library(dplyr)
# Prepare data -----------------------------------------------------
df <- mtcars
df <- cbind(model = rownames(df), data.frame(df, row.names = NULL)) # setting the rownames as the first column
dm <- df$hp %>% as.matrix()
# UI function ------------------------------------------------------
ui <- fluidPage(
titlePanel("Histogram"),
sidebarLayout(
sidebarPanel(
tags$h5("I added the below text output only to demonstrate shiny's way for tracking user interaction on static plots. You can click, double-click, or click & drag (i.e. brushing). These functions are AWESOME when exploring scatterplots."),
tags$h3("Chart click and brushing"),
verbatimTextOutput("info"),
tags$h5("Now I'm applying the below UI inputs to the `vec` and `breaks` arguments in `findInterval()` and `qplot()` respectively; I'm using `findInterval()` to bin the values in the dataframe AND to bin the x-value of the user's click event input on the chart. Then we can return the dataframe rows with the same bin values as the x-value of the click input."),
sliderInput("seq_from_to"
, label = h3("Sequence 'From' and 'To'")
, min = 0
, max = 500
, value = c(50, 350)
),
sliderInput("seq_by"
, label = h3("Sequence 'By'")
, min = 25
, max = 200
, value = 50
, step = 5)
),
mainPanel(
plotOutput("hist",
click = "plot_click",
dblclick = "plot_dblclick",
hover = "plot_hover",
brush = "plot_brush"),
dataTableOutput("table")
)
)
)
# Server function --------------------------------------------------
server <- function(input, output)
# Render Histogram Plot
output$hist <- renderPlot(
# Using the same `qplot` function but inserting the user inputs to set the breaks values in the plot
pp <- qplot(dm
, geom = "histogram"
, breaks = seq(from = input$seq_from_to[1], to = input$seq_from_to[2], by = input$seq_by)
, xlab = "values"
, ylab = "Frequency"
, main = "Histogram"
, fill = I("red")
, col = I("black")
, alpha = I(0.4)
)
# Also using the user inputs to set the breaks values for the x-axis
pp + scale_x_continuous(breaks = seq(from = input$seq_from_to[1], to = input$seq_from_to[2], by = input$seq_by))
)
# This is purely explanatory to help show how shiny can read user interaction on qplot/ggplot objects
# It's taken from the Shiny docs here: https://shiny.rstudio.com/articles/plot-interaction.html
output$info <- renderText(
# Retain the x and y coords of the user click event data
xy_str <- function(e)
if(is.null(e)) return("NULL\n")
paste0("x=", round(e$x, 1), " y=", round(e$y, 1), "\n")
# Retain the x and y range coords of click & drag (brush) data
xy_range_str <- function(e)
if(is.null(e)) return("NULL\n")
paste0("xmin=", round(e$xmin, 1), " xmax=", round(e$xmax, 1),
" ymin=", round(e$ymin, 1), " ymax=", round(e$ymax, 1))
# Paste this together so we can read it in the UI function for demo purposes
paste0(
"click: ", xy_str(input$plot_click),
"dblclick: ", xy_str(input$plot_dblclick),
"hover: ", xy_str(input$plot_hover),
"brush: ", xy_range_str(input$plot_brush)
)
)
# Back to the story. Set a listener to trigger when one of the following is updated:
toListen <- reactive(list(
input$plot_click # user clicks on the plot
, input$seq_from_to # user updates the range slider
, input$seq_by # user updates the number input
)
)
# When one of those events are triggered, update the datatable output
observeEvent(toListen(),
# Save the user click event data
click_data <- input$plot_click
print(click_data) # during your app preview, you can watch the R Console to see what click data is accessible
# Assign bin values to each row using the intervals that are set by the user input
df$bin <- findInterval(dm, vec = seq(from = input$seq_from_to[1], to = input$seq_from_to[2], by = input$seq_by))
# Similarly assign a bin value to the click event based on what interval the x values falls within
click_data$x_bin <- findInterval(click_data$x, vec = seq(from = input$seq_from_to[1], to = input$seq_from_to[2], by = input$seq_by))
# Lastly, subset the df to only those records within the same interval as the click event x-value
df_results <- subset(df, bin == click_data$x_bin)
# Select what values to view in the table
df_results <- df_results %>% select(model, hp)
# And push these back out to the UI
output$table <- renderDataTable(df_results,
options = list(
pageLength = 5
)
)
)
shinyApp(ui = ui, server = server)
【讨论】:
非常感谢!你的回答很完整。以防万一,你知道我怎样才能显示刷子的桌子吗?除了“点击选项”之外,我也想拥有那个选项。我在想brushedPoints
,但我需要 x 和 y,而我没有 y。 (shiny.rstudio.com/reference/shiny/0.12.0/brushedPoints.html)【参考方案2】:
嗯,有人回答了。由于我花时间把它放在一起,这里是另一个潜在的解决方案。
library(shiny)
library(ggplot2)
library(scales)
library(dplyr)
library(DescTools) # added for Closest()
ui <- fluidPage(
titlePanel("Histogram"),
sidebarLayout(
sidebarPanel(
),
mainPanel(
plotOutput("hist", click = 'plot_click'), # added plot_click
verbatimTextOutput("x_value"), # added queues for interactivity
verbatimTextOutput("selected_rows") # added table for bin values
)
)
)
# this can be a dataframe or matrix for qplot or ggplot
# (not sure if there was another reason you had this code?)
# mtcars <- mtcars %>%
# select("hp") # if you only want hp
# mtcars <- as.matrix(mtcars) # I suggest making row names a column
# to keep 2 columns
pp <- ggplot(mtcars) +
geom_histogram(aes(x = hp),
bins = 10,
fill = "red",
color = "black",
alpha = .4) +
labs(x = "values",
y = "Frequency",
title = "Histogram")
# extract data from plot to find where each value falls within the histogram bins
# I kept the pkg name, function in more than one library
bd <- ggplot_build(ggplot2::last_plot())$data[[1]]
# add the assigned bin number to the mtcars frame; used for filtering matches
mtcars$bins <- lapply(mtcars$hp,
function(y)
which(bd$x == Closest(bd$x, y))
) %>% unlist()
server <- function(input, output)
output$hist <- renderPlot(
# moved the plot outside of server, so that global variables could be created
# pp <- qplot(mtcars[,"hp"], geom = "histogram", bins = 10, xlab="values",
# ylab = "Frequency", main = "Histogram",
# fill = I("red"), col = I("black"), alpha = I(0.4))
# scale_x_continuous(breaks=pretty(mtcars, n=10)) # can't use this
pp
)
# # Print the name of the x value # added all that's below with server()
output$x_value <- renderPrint(
if (is.null(input$plot_click$x)) return()
# find the closest bin center to show where the user clicked on the histogram
cBin <- which(bd$x == Closest(bd$x, input$plot_click$x))
paste0("You selected bin ", cBin) # print out selected value based on bin center
)
# Print the rows of the data frame which match the x value
output$selected_rows <- renderPrint(
if (is.null(input$plot_click$x)) return()
# find the closest bin center to show where the user clicked on the histogram
cBin <- which(bd$x == Closest(bd$x, input$plot_click$x))
mtcars %>% filter(bins == cBin)
# mtcars
)
shinyApp(ui = ui, server = server)
【讨论】:
【参考方案3】:以防万一有人在这篇文章结束时寻找一种包含brushedPoints
的方法...灵感来自post,我找到了一种方法!
代码:
#Load libraries ----------------------------------------------------
library(shiny)
library(ggplot2)
library(scales)
library(dplyr)
# Prepare data -----------------------------------------------------
df <- mtcars
df <- cbind(model = rownames(df), data.frame(df, row.names = NULL)) # setting the rownames as the first column
breaks_data = pretty(mtcars$hp, n=10)
my_breaks = seq(min(breaks_data), to=max(breaks_data), by=30)
# UI function ------------------------------------------------------
ui <- fluidPage(
titlePanel("Histogram"),
sidebarLayout(
sidebarPanel(
actionButton("draw_plot", "Draw the plot")
),
mainPanel(
plotOutput("hist",
brush = brushOpts("plot_brush", resetOnNew = T, direction = "x")),
dataTableOutput("table"),
)
)
)
# Server function --------------------------------------------------
server <- function(input, output)
observeEvent(input$plot_brush,
info_plot <- brushedPoints(df, input$plot_brush)
output$table <- renderDataTable(info_plot)
)
# If the user didn't choose to see the plot, it won't appear.
output$hist <- renderPlot(
df %>% ggplot(aes(hp)) +
geom_histogram(alpha=I(0.4), col = I("black"), fill = I("red"), bins=10) +
labs(x = "values",
y = "Frequency",
title = "Histogram") +
scale_x_continuous(breaks = my_breaks)
)
shinyApp(ui = ui, server = server)
【讨论】:
以上是关于如何在 Shiny 中创建可点击的直方图?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 facet_grid 将计数标签添加到直方图的每个条形?