在没有 R/shiny 的 plotlyProxy 的情况下更新 plotly plot

Update plotly plot without R/shiny's plotlyProxy

提问人:Nik_D 提问时间:11/16/2023 最后编辑:ismirsehregalNik_D 更新时间:11/16/2023 访问量:49

问:

我正在开发一个 R/shiny 应用程序,用户应该在其中绘制随时间推移的轨迹。根据其他 帖子,我可以混合使用闪亮和情节拼凑出一个工作示例,但是在闪亮的服务器上部署时速度很慢。

据我了解,plotly 交互式绘图速度很快,因为 plotly 在客户端工作,而具有闪亮的交互式绘图必须在客户端和服务器之间传输数据。事实上,plotly 中的手绘选项没有我在自己的尝试中遇到的滞后。

我想通过最终在客户端用 JavaScript 完成所有数据处理步骤来加快我的应用程序,然后在不先将数据发送到闪亮的服务器的情况下更新绘图。目前,我正在为后者而苦苦挣扎——如何仅使用 JavaScript 更新绘图背后的数据?

示例应用:

#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, adapted from
      //https://stackoverflow.com/questions/71922585/find-position-of-all-unique-elements-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;
      }
      


      //Adapted from https://stackoverflow.com/questions/64309573/freehand-drawing-ggplot-how-to-improve-and-or-format-for-plotly
      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];
            
            //Only retain the path for further processing
            var path = output.path;
            
            //Transform path into x/y coordinates
            //Points in the path start with 'M' or 'L' - use this to split the string
            var path = path.replace(/M|L/g, ',');
            var path = path.split(',');
            
            //Remove empty first element
            var path = path.slice(1);
            
            //Define as numeric
            var path = path.map(Number);
            
            
            
            
            //x values are saved at odd indices, y values at even indices
            //Create x indices
            var xIndex = [];
            xIndex.length = (path.length / 2);
            
            for (let i =0; i <= path.length - 1; i+= 2) {
            xIndex.push(i);
            }
            


           //Create y indices
            var yIndex = [];
            yIndex.length = (path.length / 2);
            
            for (let i =1; i <= path.length; i+= 2) {
            yIndex.push(i);
            }
            
            
            
            
            //Now, separate path into x and y values
            var xPath = xIndex.map(i=>path[i]);
            var yPath = yIndex.map(i=>path[i]);
            
            //Only allow integer values for x axis
            var xPath = xPath.map(item => Math.round(item))

            //After rounding to integers, x values might be duplicated
            //Retain only one value per x coordinate
            var yPath = uniqueIndices(xPath).map(i=>yPath[i])
            var xPath = uniqueIndices(xPath).map(i=>xPath[i]);






            //Current approach sends these values to shiny, then adds a new trace via plotlyProxy
            Shiny.setInputValue('shinyX', xPath);
            Shiny.setInputValue('shinyY', yPath);
            
            
            //Remove previous shapes so that only the most recent line is visible
            gd.update_layout(gd.layout.shapes = output)
          });
        };
  }
  "
  
  
  
  
  #Plotly plot
  output$myPlot <- renderPlotly({
    plot_ly(type = "scatter", mode = "markers") %>% 
      
      plotly::layout(
        dragmode = "drawopenpath",
        newshape = list(line = list(color = "gray50")),
        
        #x axis
        xaxis = list(title = "x",
                     fixedrange = TRUE,
                     range = c(-0.1, 10.1),
                     showgrid = FALSE, zeroline = FALSE, showline = TRUE, mirror = TRUE),
        
        #y axis
        yaxis = list(title = "y",
                     fixedrange = TRUE,
                     range = c(1.1, 5.1),
                     showgrid = FALSE, zeroline = FALSE, showline = TRUE, mirror = TRUE)) %>% 
      
    #Call js
    onRender(js) 

  }) #End render plotly
  
  
  
  
  
  
  
  #Create plotlyProxy object 
  myPlotProxy <- plotlyProxy("myPlot", session)
  
  
  
  
  
  
  ################################################################################
  #Observe drawn user input
  observe({    
    
    #Ensure that user input is not NULL
    if(!is.null(input$shinyY))
    {
      #Delete existing traces
      plotlyProxyInvoke(myPlotProxy, "deleteTraces", {
        list(0) 
      })
      
      
      #Add user-drawn line based on new coordinates
      plotlyProxyInvoke(myPlotProxy, "addTraces", {
        list(list(x = input$shinyX,
                  y = input$shinyY,
                  hoverinfo = "none",
                  mode = "lines+markers"))
      })
      
    } #End condition that user input must not be NULL
  }) #End observe for drawn user input
} #End server function





################################################################################
#Execute app
shinyApp(ui, server)

上面的例子使用 plotly 的 drawopenpath 来画一条线。路径转换为 x/y 坐标,我希望 x 坐标是整数值,例如 0、1、2 等。目前,这些坐标被发送到闪亮的服务器,然后使用 plotlyProxy 更新绘图。

我怀疑这种来回使应用程序变得如此缓慢,即绘图和绘图更新之间有 0.5 到 1 秒的延迟。在本地运行应用时不会发生这种情况。那么,有没有办法在不先向闪亮的服务器发送数据的情况下用新值更新绘图呢?我玩过update_layout,但无法让它工作。任何帮助将不胜感激。

JavaScript r 闪亮 的情节

评论

0赞 ismirsehregal 11/16/2023
我只是运行您的示例应用程序,它似乎一点也不慢?但是,如果您想避免将坐标传递给 shiny,则需要在调用中直接使用 plotly 的 JS 函数在这里,您可以找到类似的方法。此外,你可能想要检查 {profvis} 包来分析你的应用。onRender
0赞 Nik_D 11/16/2023
嗨,该应用程序在我的计算机上本地运行时确实运行良好;只有当将其部署到闪亮的服务器上时,才会发生延迟。非常感谢您提出的问题,我将检查是否可以在我的示例中使用此方法。
0赞 ismirsehregal 11/24/2023
根据我的意见,您是否能够为此制定解决方案?
1赞 Nik_D 11/24/2023
事实上,我根据您在链接问题中的示例(以及相当多的试验和错误)得到了一个可行的解决方案。我会在周末晚些时候添加它,那时我有更多的时间。;)

答:

1赞 Nik_D 11/26/2023 #1

多亏了@ismirsehregal的建议以及链接问题及其他问题中的有用示例,我设法构建了一个不需要闪亮的应用程序来处理输入和更新用户绘制的线条。事实上,当将这个应用程序部署到我闪亮的服务器时,对用户输入的滞后反应已经消失了!

新代码如下所示。我以前没有任何使用 Javascript 的经验,所以这段代码可能会让更有经验的用户哭泣。但是,它确实适用于我的目的,并且可能对其他人有所帮助,所以请耐心等待。;)

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



################################################################################
#UI function
ui <- fixedPage(
  
  plotlyOutput("myPlot", width = 500, height = 250),
  
  
  #Temporary grey line when actively drawing (will disappear when users stop drawing by setting color to transparent later)
  #see https://stackoverflow.com/questions/77538320/plotly-drawopenpath-color-of-finished-line-vs-drawing-in-process
  tags$head(
    tags$style(HTML("
      .select-outline {
        stroke: rgb(150, 150, 150) !important;
        }"))),
  
) #End UI






################################################################################
#Server function
server <- function(input, output, session) {
  
  js <- "
function(el, x){

      //Function to find unique indices in array, adapted from
      //https://stackoverflow.com/questions/71922585/find-position-of-all-unique-elements-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;
      }
      

      //Function for linear interpolation between points given as two arrays 
      //adapted from https://stackoverflow.com/questions/59264119/how-do-i-interpolate-the-value-in-certain-data-point-by-array-of-known-points
      const interpolate = (xarr, yarr, xpoint) => {
        const   xa = [...xarr].reverse().find(x => x<=xpoint),
                xb = xarr.find(x => x>= xpoint),      
                ya = yarr[xarr.indexOf(xa)],      
                yb =yarr[xarr.indexOf(xb)]  
        return yarr[xarr.indexOf(xpoint)] || ya+(xpoint-xa)*(yb-ya)/(xb-xa)
            };





      //Define eligible integer x values for the user-drawn line
      var xLimits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
      
      //y values are empty at first and will be filled with user-drawn coordinates
      var yValues = new Array(xLimits.length).fill('');
      


      var id = el.getAttribute('id');
      var gd = document.getElementById(id);
      

      //Main function
      //Adapted from https://stackoverflow.com/questions/64309573/freehand-drawing-ggplot-how-to-improve-and-or-format-for-plotly
      Plotly.update(id).then(attach);
        function attach() {

          //Check changes following relayout event
          gd.on('plotly_relayout', function(evt) {
          
            //Save most recent shape from plotly
            var output = gd.layout.shapes[0];
            
            //Only retain the path for further processing
            var path = output.path;
            
            //Transform path into x/y coordinates
            //Points in the path start with 'M' or 'L' - use this to split the string
            path = path.replace(/M|L/g, ',');
            path = path.split(',');
            
            //Remove empty first element
            path = path.slice(1);
            
            //Define as numeric
            path = path.map(Number);
            
 
            
            
            //x values are saved at odd indices, y values at even indices
            //Create x indices
            var xIndex = [];
            xIndex.length = (path.length / 2);
            
            for (let i =0; i <= path.length - 1; i+= 2) {
            xIndex.push(i);
            }
            


           //Create y indices
            var yIndex = [];
            yIndex.length = (path.length / 2);
            
            for (let i =1; i <= path.length; i+= 2) {
            yIndex.push(i);
            }
            
            
            //Now, separate path into x and y values
            var xPath = xIndex.map(i=>path[i]);
            var yPath = yIndex.map(i=>path[i]);


            //Only allow integer values for x axis
            xPath = xPath.map(item => Math.round(item));

            //After rounding to integers, x values might be duplicated
            //Retain only one value per x coordinate
            yPath = uniqueIndices(xPath).map(i=>yPath[i]);
            xPath = uniqueIndices(xPath).map(i=>xPath[i]);


            //Create 2 new placeholders for x/y coordinates
            //The goal is to take the existing path and fill 'holes' in it: The SVG path does not necessarily
            //have x/y coordinates for all values on the x axis. If e.g. a straight line from x1 = 1 to x2 = 5
            //is enough, the SVG path will only include these end points, but no points in between. To get a full
            //set of x/y coordinates, interpolation will be used
            var placeholderX = [];
            var placeholderY = [];

            
            //Interpolation function was defined above
            //The loop covers the whole range of the x axis, but the interpolate function returns only values
            //for the given x/y values at the i-th value of the x axis limits. For instance, if the axis limits are
            //0:10, and the input coordinates for x are 3, 6, 7, then the function will return values for 3:7
            for(let i = xLimits[0]; i < xLimits.length; i++) {
            placeholderY.push(interpolate(xPath, yPath, i));
            }

            //Save corresponding x values after interpolation
            //The loop above returns y values, while this loop creates an equal number of x values
            //It loops from the smallest value in xPath to the largest and saves all integer values inbetween
            //Since users can draw from left to right or right to left, the starting point for the loop is always
            //the minimum of the x coordinates and the end point is the maximum
            for(let i = Math.min(...[xPath[1], xPath[xPath.length - 1]]); 
                i < Math.max(...[xPath[1], xPath[xPath.length - 1]]);     i++) {
            placeholderX.push(i)
            }

            //Last index seems to be missing; add it
            placeholderX.push(1 + Math.max(...placeholderX));


            //Ensure that input has at least 2 entries so that a line can be drawn (not only a point)
            if(placeholderX.length > 1) {
            
             //Loop over entries in prepared, interpolated placeholder
             //The interpolated y coordinates are added to the final dataset at the position determined by the x coordinate placeholder
                for(let i = Math.min(...placeholderX); 
                    i <= Math.max(...placeholderX); i++) {
                yValues[i] = placeholderY[i];
                    }
            }
 

            //Remove previous shapes so that only the most recent line is visible
            Plotly.deleteTraces(gd, 0);
            Plotly.addTraces(gd, {x: xLimits, y: yValues, 
                                  line: {color: 'red'},
                                  hoverinfo: 'none',
                                  mode: 'lines+markers',
                                  }, 0);
            gd.update_layout(gd.layout.shapes = output);
          });
        };
  }
  " #End JS part
  
  
  
  
  
  
  #Plotly plot
  output$myPlot <- renderPlotly({
    plot_ly(type = "scatter", mode = "markers") %>% 
      
      plotly::layout(
        dragmode = "drawopenpath",
        newshape = list(line = list(color = "transparent")),
        
        #x axis
        xaxis = list(title = "x",
                     fixedrange = TRUE,
                     range = c(-0.1, 10.1),
                     showgrid = FALSE, zeroline = FALSE, showline = TRUE, mirror = TRUE),
        
        #y axis
        yaxis = list(title = "y",
                     fixedrange = TRUE,
                     range = c(1.1, 5.1),
                     showgrid = FALSE, zeroline = FALSE, showline = TRUE, mirror = TRUE)) %>% 
      
      #Call js
      onRender(js) 
    
  }) #End render plotly
  
  
} #End server function





################################################################################
#Execute app
shinyApp(ui, server)

我在之前的尝试中遇到的一个问题是,在通过 addTraces() 将处理后的坐标添加到绘图中后,plotly 的 drawopenpath 中的线仍然可见。我在这里劫持了我另一个问题的答案,这有助于通过使完成的线不可见来回避这个问题。我想那里有更优雅的解决方案,但我对这里的行为完全满意。:)