我正在 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,但无法让它工作。任何帮助将不胜感激。
感谢@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,这有助于通过使完成的线不可见来回避这个问题。我想那里有更优雅的解决方案,但我对这里的行为非常满意。 :)