R Shiny:在 plotOutput 中创建非反应性背景

R Shiny: create non-reactive background in plotOutput

我正在尝试构建一个闪亮的应用程序,我可以在其中以交互方式更改情节。我希望情节在几毫秒内发生变化,因为变化只包括添加一些点,这实际上是可能的。

可重现的例子包含了这个想法的抽象。第一个示例绘制了一个散点图,我可以交互式地更改点数。这基本上立即发生。我将把这部分情节称为“反应层”。

library(shiny)

ui <- fluidPage(
  
  sliderInput(inputId = "slider_input", label = "Reactive values:", min = 1, max = 100, value = 10),
  plotOutput(outputId = "plotx")
  
)

quick_server <- function(input, output, session){
    
  output$plotx <- renderPlot({
    
    # reactive layer
    plot(
      x = sample(x = -4:4, size = input$slider_input, replace = T), 
      y = sample(x = -4:4, size = input$slider_input, replace = T)
      )
    
  })
  
}

shinyApp(ui = ui, server = quick_server)

问题是我想交互更改的图总是包含一个“慢速非反应层”,其中包含许多不反应且从不改变的数据点。由于此数据集的大小和 renderPlot() 总是重新绘制它,我交互更改“反应层”的速度急剧下降。

library(shiny)

ui <- fluidPage(
  
  sliderInput(inputId = "slider_input", label = "Reactive values:", min = 1, max = 100, value = 10),
  plotOutput(outputId = "plotx")
  
)

slow_server <- function(input, output, session){
  
  base_data <- reactiveVal(value = data.frame(x = rnorm(n = 200000), y = rnorm(n = 200000)))
  
  output$plotx <- renderPlot({
    
    # slow non reactive layer
    plot(x = base_data()$x, y = base_data()$y)
    
    # reactive layer
    points(
      x = sample(x = -4:4, size = input$slider_input, replace = T),
      y = sample(x = -4:4, size = input$slider_input, replace = T), 
      col = "red", 
      cex = 5, 
      pch = 19
      )
    
  })
  
}

shinyApp(ui = ui, server = slow_server)

由于基础数据和生成的图(这里是点云)永远不会改变,所以 renderPlot() 总是重新绘制所有内容(都是“图层”),这很烦人。有没有办法“隔离”从非反应性数据集创建的图?这样就不会重新绘制非反应性层?

我已经尝试使用 ggplot2 并创建一个稳定层

big_data_frame <- data.frame(x = rnorm(n = 200000), y = rnorm(n = 200000))

steady_layer <- reactiveVal(value = geom_point(data = big_data_frame, mapping = aes(x = x, y = y))

然后创建这样的情节

output$plotx <- renderPlot({

  small_df <- 
   data.frame(
      x = sample(x = -4:4, size = input$slider_input, replace = T),
      y = sample(x = -4:4, size = input$slider_input, replace = T)
    )
  
   ggplot() + 
    steady_layer() +
    geom_point(data = small_df, mapping = aes(x = x, y = y) 
    
})


但这并没有帮助,因为重新绘制过程需要时间而不是创建 ggplot 图层本身。

虽然我可以想象解决方案可能是创建一个大图的 .png 并将其用作 HTML 的背景和 output$plotx 的 CSS 通过使其成为反应式 UI 我还没有成功地操纵 HTML 和 CSS。

感谢任何帮助。提前致谢!

您需要了解 renderPlot 的工作原理。它首先使用R png 函数创建一个png,然后将其发送到客户端浏览器。当绘图的数据发生变化时,将重新创建此 png。因此,不可能重新绘制 png 的一部分。所以给一个已经存在的plot加点总是会用到slow points + new points的时间,所以在目前的闪亮机制下,不重新计算这些慢点是不可能的。一个可能的选择是使用 plotlyproxy 。它由 HTML 和 Javascript 组成,所以是的,您可以通过部分更新来完成。详见link,此处不再赘述。根据我的个人经验,它的速度并不像您想要的那样快milliseconds-level。

所以在这里我有一个聪明的技巧给你:为什么我们在同一个情节上更新?我们可以用一个图作为背景,它很慢,但我们只渲染一次,我们再也不会碰它了。然后我们更新另一个只有几个点的图,我们将这个图堆叠在慢图的顶部。

方法如下:

  1. 添加一些 CSS 技巧来进行堆叠
  2. 渲染慢情节
  3. 使用透明渲染快速绘图
library(shiny)
library(ggplot2)
ui <- fluidPage(
    
    sliderInput(inputId = "slider_input", label = "Reactive values:", min = 1, max = 100, value = 10),
    div(
        class = "large-plot",
        plotOutput(outputId = "plot_bg"),
        plotOutput(outputId = "plotx")
    ),
    tags$style(
        "
        .large-plot {
            position: relative;
        }
        #plot_bg {
            position: absolute;
        }        
        #plotx {
            position: absolute;
        }
        "
    )
    
    
)

slow_server <- function(input, output, session){
    
    base_data <- reactiveVal(value = data.frame(x = rnorm(n = 200000), y = rnorm(n = 200000)))
    
    output$plot_bg <- renderPlot({
        ggplot(base_data()) +
            geom_point(aes(x,y)) +
            scale_x_continuous(breaks = -4:4) +
            scale_y_continuous(breaks = -4:4) +
            xlim(-5, 5) +
            ylim(-5, 5)
    })
    output$plotx <- renderPlot({
        data.frame(
            x = sample(x = -4:4, size = input$slider_input, replace = T),
            y = sample(x = -4:4, size = input$slider_input, replace = T)
        ) %>% 
            ggplot() +
                geom_point(aes(x,y), color = "red", size = 3) + 
                scale_x_continuous(breaks = -4:4) +
                scale_y_continuous(breaks = -4:4) +
            theme(
                panel.background = element_rect(fill = "transparent"),
                plot.background = element_rect(fill = "transparent", color = NA), 
                panel.grid.major = element_blank(), 
                panel.grid.minor = element_blank(), 
                legend.background = element_rect(fill = "transparent"), 
                legend.box.background = element_rect(fill = "transparent")
            )+
            xlim(-5, 5) +
            ylim(-5, 5)
    }, bg="transparent")
    
}

shinyApp(ui = ui, server = slow_server)