You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

多标签页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的书签功能可以保存应用的输入状态,但默认无法保存上传的文件,需要做以下适配:

  • 捕获动态输入:因为您的输入(如x1y1sheet1)是动态生成的,需要确保这些输入ID能被书签机制识别。可以在生成输入时,通过setBookmarkInclude主动添加这些动态ID,或者在server函数开头设置bookmarkStore = "url"
  • 序列化上传数据:将上传的Excel数据转换为字符串(比如转成CSV或RDS格式后用base64enc编码),存储到书签的URL中。恢复时解码并重新生成data_list
  • 示例修改要点
    # 启用书签
    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)
        )
      }
    })
    
    注意:如果Excel文件较大,书签URL会变得很长,可能超出浏览器限制,这种情况适合小文件场景。

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

火山引擎 最新活动