多标签页Shiny应用状态保存方案咨询及堆叠图函数实现
多标签页Shiny应用状态保存方案咨询及堆叠图函数实现
您好,根据您的描述,您的Shiny应用实现了上传Excel文件后自动生成对应工作表数量的标签页,每个标签页可选择X/Y变量绘制堆叠折线图,但目前保存完整HTML无法还原应用状态,下面先整理您提供的实现代码,再针对状态保存问题给出可行方案:
堆叠折线图实现函数
您编写的plotly_stacked函数可以将宽格式数据转换为长格式,并生成多子图共享X轴的堆叠折线图,代码如下:
plotly_stacked <- function(df, x_colName, cols){ DF <- df[, cols] %>% tidyr::gather(variable, value, -x_colName ) %>% transform(id = as.integer(factor(variable))) DF$variable<- factor( DF$variable, levels = unique( DF$variable)) p <- plot_ly(data = DF, x = ~get(names(DF[1])) , y = ~value, color = ~variable, colors = "Dark2", yaxis = ~paste0( "y",sort(id, decreasing = F))) %>% add_lines() %>% layout( xaxis = list( title = ""), legend = list( orientation = "h", xanchor = "center", x = 0.5)) %>% plotly::subplot(nrows = length(unique(DF$variable)), shareX = TRUE) return(p) }
完整Shiny应用代码
您的应用实现了动态生成标签页、工作表选择、变量选择及绘图功能,完整代码如下:
# Set maximum request size options(shiny.maxRequestSize = 30 * 1024^2) # Load required libraries library(DT) library(plotly) library(shiny) library(data.table) library(readr) library(openxlsx) library(readxl) library(tidyverse) library(corrplot) library(corrr) library(psych) # UI ------------------------------------------------------------ ui <- fluidPage( titlePanel(title = "Data"), fileInput("file", "Choose .xlsx file", accept = c(".xlsx")), mainPanel( tabsetPanel(id = "tabsetPanelID") ) ) # Server ------------------------------------------------------------ server <- function(input, output, session) { # UI Elements ------------------------------------------------------------ ui_elementsUI <- function(x, y, data) { tagList( h4("Select X and Y datasets"), fluidRow( column(12, selectizeInput(inputId = x, label = "X data", choices = names(data))) ), fluidRow( column(12, selectizeInput(inputId = y, label = "Y data", choices = names(data), multiple = TRUE, selected = names(data)[3])) ) ) } # Sheet Elements ------------------------------------------------------------ sheet_elementsUI <- function(sheet) { tagList( fluidRow( column(12, selectInput(inputId = sheet, label = "Sheet", choices = excel_sheets(input$file$datapath))) ) ) } # Render Sheet Elements after File Upload ------------------------------------ n <- reactive({ req(input$file) length(excel_sheets(input$file$datapath)) }) observe({ lapply(1:n(), function(val) { output[[paste0("sheet_elements", val)]] <- renderUI({ sheet_elementsUI(paste0("sheet", val)) }) output[[paste0("ui_elements", val)]] <- renderUI({ ui_elementsUI(paste0("x", val), paste0("y", val), data_list()[[val]]()) }) output[[paste0("plot", val)]] <- renderPlotly({ plot_render()(paste0("x", val), paste0("y", val), data_list()[[val]]()) }) }) }) # Plot Rendering ------------------------------------------------------------ plot_render <- reactive({ validate( need(input$file != "", "Plots will display after choosing the .xlsx file.") ) function(x, y, data) { labels <- c("xy", paste0("xy", 2:length(input[[y]]))) labels_json <- jsonlite::toJSON(labels) js_code <- sprintf( 'function(el, x){el.on("plotly_hover", function(d) { Plotly.Fx.hover(el.id, {xval: d.xvals[0]}, %s); })}', labels_json ) plotly_stacked(df = data, x_colName = input[[x]], cols = c(input[[x]], input[[y]])) %>% layout(hovermode = "x") %>% htmlwidgets::onRender(js_code) } }) # Data Processing Function --------------------------------------------------- foo <- function(sheet) { df <- read_xlsx(input$file$datapath, sheet = input[[sheet]], na = "empty") n <- which(!is.na(as.numeric(df[[1]])))[1] df[is.na(df)] <- " " colnames(df) <- apply(df[1:(n - 1), ], 2, paste, collapse = ",") df <- df[-c(1:(n - 1)), ] names(df)[1] <- "Time" df[, 2:ncol(df)] <- apply(df[, 2:ncol(df)], 2, as.numeric) if (class(df[[1]])[1] != "POSIXct") { df$Time <- as.POSIXct(as.numeric(df$Time) * 86400, origin = "1899-12-30", tz = "UTC") } return(df) } # Generate Reactive Datasets ------------------------------------------------- generate_data <- function(sheet_name) { reactive({ req(input$file) foo(sheet_name) }) } data_list <- reactive({ lapply(paste0("sheet", 1:n()), generate_data) }) # Generate Tabs -------------------------------------------------------------- generateTabUI <- function(tab_id) { tab_name <- paste("Tab", tab_id) plot_output <- plotlyOutput(paste("plot", tab_id, sep = "")) ui_elements <- uiOutput(paste("ui_elements", tab_id, sep = "")) sheet_elements <- uiOutput(paste("sheet_elements", tab_id, sep = "")) tabPanel(tab_name, value = tab_id, sheet_elements, ui_elements, plot_output) } observe({ for (i in 1:n()) { insertTab( inputId = "tabsetPanelID", tab = generateTabUI(i) ) } }) } # Run the Shiny App ----------------------------------------------------------- shinyApp(ui = ui, server = server)
状态保存解决方案
针对您遇到的“保存HTML无法还原状态”问题,推荐以下几种方案:
1. Shiny书签功能适配(推荐)
Shiny的书签功能可以保存应用的输入状态,但默认无法保存上传的文件,需要做以下适配:
- 捕获动态输入:因为您的输入(如
x1、y1、sheet1)是动态生成的,需要确保这些输入ID能被书签机制识别。可以在生成输入时,通过setBookmarkInclude主动添加这些动态ID,或者在server函数开头设置bookmarkStore = "url"。 - 序列化上传数据:将上传的Excel数据转换为字符串(比如转成CSV或RDS格式后用
base64enc编码),存储到书签的URL中。恢复时解码并重新生成data_list。 - 示例修改要点:
注意:如果Excel文件较大,书签URL会变得很长,可能超出浏览器限制,这种情况适合小文件场景。# 启用书签 enableBookmarking("url") # 自定义书签内容,包含数据和输入状态 onBookmark(function(state) { # 序列化上传的数据 data_serialized <- lapply(data_list(), function(df) { base64enc::base64encode(serialize(df(), NULL)) }) state$values$data_serialized <- data_serialized # 保存所有动态输入 state$values$tab_count <- n() }) # 恢复书签状态 onRestore(function(state) { # 反序列化数据 restored_data <- lapply(state$values$data_serialized, function(ser) { reactive({ unserialize(base64enc::base64decode(ser)) }) }) # 重新生成标签页和输入 data_list <<- reactive({ restored_data }) # 重新生成标签页 for (i in 1:state$values$tab_count) { insertTab( inputId = "tabsetPanelID", tab = generateTabUI(i) ) } })
2. 导出绘图配置+数据
如果书签方案不适用,可以添加一个下载按钮,导出当前的绘图配置(X/Y选择、工作表选择)和处理后的数据,下次上传时读取配置自动恢复绘图状态:
- 添加下载UI:在
fluidPage中加入downloadButton("save_state", "Save App State") - 服务器端实现下载逻辑:
output$save_state <- downloadHandler( filename = function() { paste0("app_state_", Sys.Date(), ".rds") }, content = function(file) { state_list <- list( data = lapply(data_list(), function(df) df()), inputs = lapply(1:n(), function(i) { list( sheet = input[[paste0("sheet", i)]], x = input[[paste0("x", i)]], y = input[[paste0("y", i)]] ) }) ) saveRDS(state_list, file) } ) - 同时添加一个加载状态的功能,让用户上传保存的
.rds文件来恢复状态。
3. 静态HTML增强(有限效果)
如果您坚持保存为HTML,可以考虑将所有绘图对象和输入状态嵌入HTML中,但需要用htmlwidgets::saveWidget结合shiny::renderUI的静态版本,不过这种方式无法还原交互状态,只能保存当前渲染的图表。
备注:内容来源于stack exchange,提问作者ACE




