如何在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中完成图表更新,只需修改以下部分:
核心修改思路
- 移除服务器通信逻辑:删除JavaScript中的
Shiny.setInputValue调用,不再将数据发送到Shiny服务器 - 客户端直接操作Plotly图表:使用Plotly客户端API的
deleteTraces和addTraces方法,直接在浏览器中更新图表数据 - 精简服务器代码:删除服务器端的
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
相关产品推荐
相关产品推荐

