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

问题描述 投票:0回答:1

我正在 R/shiny 中开发一个应用程序,用户应该在其中绘制随时间变化的轨迹。基于其他帖子,我可以使用shiny和plotly的组合拼凑出一个工作示例,但部署在shiny服务器上时速度很慢。

据我所知,plotly交互式绘图速度很快,因为plotly在客户端工作,而shiny交互式绘图必须在客户端和服务器之间传输数据。事实上,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 shiny plotly
1个回答
0
投票

感谢@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的线仍然可见。我劫持了我的另一个问题的答案here,这有助于通过使完成的线不可见来回避这个问题。我想那里有更优雅的解决方案,但我对这里的行为非常满意。 :)

© www.soinside.com 2019 - 2024. All rights reserved.