You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在R Shiny中不借助plotlyProxy用JS更新Plotly图表?

问题

我正在开发一款R Shiny应用,支持用户绘制时间轨迹。结合Shiny与Plotly实现了可用示例,但部署到Shiny Server后运行缓慢。本地运行无延迟,但部署后因当前方案需要将数据传输至服务器,再通过plotlyProxy更新图表,导致0.5-1秒的延迟。我希望在客户端JavaScript完成所有数据处理后,直接更新Plotly图表,无需先将数据发送至Shiny服务器,目前卡在如何仅用JavaScript更新Plotly图表的底层数据这一步。

示例应用代码:

# Load packages
library(plotly)
library(shiny)
library(htmlwidgets)

################################################################################
# UI function
ui <- fixedPage(
  plotlyOutput("myPlot", width = 500, height = 250)
) # End UI

################################################################################
# Server function
server <- function(input, output, session) {
  
  js <- "
function(el, x){
      var id = el.getAttribute('id');
      var gd = document.getElementById(id);
      
      // Function to find unique indices in array
      function uniqueIndices(arr) {
      const seen = new Set();
      const indices = [];
      for (const [i, n] of arr.entries()) {
              if (!seen.has(n)) {
                  seen.add(n);
                  indices.push(i);
              }
          }
          return indices;
      }

      // Attach draw event after plot loads
      Plotly.update(id).then(attach);
        function attach() {
          gd.on('plotly_relayout', function(evt) {
            // Save most recent shape from plotly
            var output = gd.layout.shapes[0];
            if(!output) return; // Exit if no shape exists
            
            // Only retain the path for further processing
            var path = output.path;
            
            // Transform path into x/y coordinates
            var path = path.replace(/M|L/g, ',');
            var path = path.split(',');
            path = path.slice(1); // Remove empty first element
            path = path.map(Number); // Convert to numeric
            
            // Separate x and y values
            var xPath = [];
            var yPath = [];
            for(let i = 0; i < path.length; i += 2){
              xPath.push(path[i]);
              yPath.push(path[i+1]);
            }
            
            // Round x values to integers
            xPath = xPath.map(item => Math.round(item));
            
            // Remove duplicate x values, retain corresponding y values
            const uniqueXIndices = uniqueIndices(xPath);
            xPath = uniqueXIndices.map(i => xPath[i]);
            yPath = uniqueXIndices.map(i => yPath[i]);
            
            // Current approach sends data to Shiny server
            Shiny.setInputValue('shinyX', xPath);
            Shiny.setInputValue('shinyY', yPath);
            
            // Remove previous shapes
            gd.layout.shapes = [output];
            Plotly.relayout(gd, gd.layout);
          });
        };
  }
  "
  
  # Plotly plot
  output$myPlot <- renderPlotly({
    plot_ly(type = "scatter", mode = "markers") %>% 
      plotly::layout(
        dragmode = "drawopenpath",
        newshape = list(line = list(color = "gray50")),
        xaxis = list(title = "x",
                     fixedrange = TRUE,
                     range = c(-0.1, 10.1),
                     showgrid = FALSE, zeroline = FALSE, showline = TRUE, mirror = TRUE),
        yaxis = list(title = "y",
                     fixedrange = TRUE,
                     range = c(1.1, 5.1),
                     showgrid = FALSE, zeroline = FALSE, showline = TRUE, mirror = TRUE)) %>% 
      onRender(js) 
  }) # End render plotly
  
  # Create plotlyProxy object 
  myPlotProxy <- plotlyProxy("myPlot", session)
  
  # Observe drawn user input
  observe({    
    if(!is.null(input$shinyY))
    {
      # Delete existing traces
      plotlyProxyInvoke(myPlotProxy, "deleteTraces", list(0))
      
      # Add user-drawn line
      plotlyProxyInvoke(myPlotProxy, "addTraces", 
                        list(list(x = input$shinyX,
                                  y = input$shinyY,
                                  hoverinfo = "none",
                                  mode = "lines+markers")))
    }
  }) # End observe
} # End server function

# Execute app
shinyApp(ui, server)
解决方案

要消除服务器往返通信的延迟,直接在客户端JavaScript中完成图表更新,只需修改以下部分:

核心修改思路

  1. 移除服务器通信逻辑:删除JavaScript中的Shiny.setInputValue调用,不再将数据发送到Shiny服务器
  2. 客户端直接操作Plotly图表:使用Plotly客户端API的deleteTraces和addTraces方法,直接在浏览器中更新图表数据
  3. 精简服务器代码:删除服务器端的plotlyProxy定义和对应的observe监听代码

修改后的完整代码

# Load packages
library(plotly)
library(shiny)
library(htmlwidgets)

################################################################################
# UI function
ui <- fixedPage(
  plotlyOutput("myPlot", width = 500, height = 250)
) # End UI

################################################################################
# Server function
server <- function(input, output, session) {
  
  js <- "
function(el, x){
      var id = el.getAttribute('id');
      var gd = document.getElementById(id);
      
      // Function to find unique indices in array
      function uniqueIndices(arr) {
      const seen = new Set();
      const indices = [];
      for (const [i, n] of arr.entries()) {
              if (!seen.has(n)) {
                  seen.add(n);
                  indices.push(i);
              }
          }
          return indices;
      }

      // Attach draw event after plot loads
      Plotly.update(id).then(attach);
        function attach() {
          gd.on('plotly_relayout', function(evt) {
            // Save most recent shape from plotly
            var output = gd.layout.shapes[0];
            if(!output) return; // Exit if no shape exists
            
            // Only retain the path for further processing
            var path = output.path;
            
            // Transform path into x/y coordinates
            var path = path.replace(/M|L/g, ',');
            var path = path.split(',');
            path = path.slice(1); // Remove empty first element
            path = path.map(Number); // Convert to numeric
            
            // Separate x and y values
            var xPath = [];
            var yPath = [];
            for(let i = 0; i < path.length; i += 2){
              xPath.push(path[i]);
              yPath.push(path[i+1]);
            }
            
            // Round x values to integers
            xPath = xPath.map(item => Math.round(item));
            
            // Remove duplicate x values, retain corresponding y values
            const uniqueXIndices = uniqueIndices(xPath);
            xPath = uniqueXIndices.map(i => xPath[i]);
            yPath = uniqueXIndices.map(i => yPath[i]);
            
            // --- 客户端直接更新图表,无需服务器通信 ---
            // Delete existing trace (if exists)
            if(gd.data.length > 0){
              Plotly.deleteTraces(gd, 0);
            }
            
            // Add new trace with processed data
            Plotly.addTraces(gd, [{
              x: xPath,
              y: yPath,
              hoverinfo: 'none',
              mode: 'lines+markers',
              type: 'scatter'
            }]);
            
            // Remove previous shapes, keep only current one
            gd.layout.shapes = [output];
            Plotly.relayout(gd, gd.layout);
          });
        };
  }
  "
  
  # Plotly plot
  output$myPlot <- renderPlotly({
    plot_ly(type = "scatter", mode = "markers") %>% 
      plotly::layout(
        dragmode = "drawopenpath",
        newshape = list(line = list(color = "gray50")),
        xaxis = list(title = "x",
                     fixedrange = TRUE,
                     range = c(-0.1, 10.1),
                     showgrid = FALSE, zeroline = FALSE, showline = TRUE, mirror = TRUE),
        yaxis = list(title = "y",
                     fixedrange = TRUE,
                     range = c(1.1, 5.1),
                     showgrid = FALSE, zeroline = FALSE, showline = TRUE, mirror = TRUE)) %>% 
      onRender(js) 
  }) # End render plotly
} # End server function

# Execute app
shinyApp(ui, server)

关键说明

  • 客户端直接调用Plotly.deleteTraces(gd, 0)删除原有轨迹,Plotly.addTraces(gd, [...])添加处理后的新轨迹
  • 保留了原有的数据处理逻辑(路径解析、x值取整、去重),全部在客户端完成
  • 移除了所有服务器端的通信和监听代码,彻底消除了服务器往返的延迟

内容的提问来源于stack exchange,提问作者Nik_D

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 00:22:03