Plotly的 rangeslider小部件在显示所选范围时,无法同时显示整个数据集

编程语言 2026-07-08

我想创建一个Shiny应用,用来显示时间序列数据,包含若干选择器和一个日期范围输入控件。此外,我还希望在使用绘图底部的范围滑块时,相应地更新日期范围输入控件。以下代码正在处理这一切:

library(shiny)
library(plotly)

xls_plot <- structure(list(mid_date = structure(c(20106,
20106, 20113, 20113, 20120, 20120, 20127, 20127, 20134, 20134,
20141, 20141, 20148, 20148, 20155, 20155, 20162, 20162, 20169,
20169, 20176, 20176, 20183, 20183, 20190, 20190, 20197, 20197,
20204, 20204, 20211, 20211, 20218, 20218, 20225, 20225, 20232,
20232, 20239, 20239, 20246, 20246, 20253, 20253, 20260, 20260,
20267, 20267, 20274, 20274, 20281, 20281, 20288, 20288, 20295,
20295, 20302, 20302, 20309, 20309, 20316, 20316, 20323, 20323,
20330, 20330, 20337, 20337, 20344, 20344, 20351, 20351, 20358,
20358, 20365, 20365, 20372, 20372, 20379, 20379, 20386, 20386,
20393, 20393, 20400, 20400, 20407, 20407, 20414, 20414, 20421,
20421, 20428, 20428, 20435, 20435), class = "Date"), concentration = c(1.39, 1.45, 0.69, 0.87, 1.29,
1.35, 1.48, 1.46, 1.3, 1.37, 0.1, 0.1, 1.53, 1.39, 1.58, 1.76,
0.82, 0.84, 1.28, 1.05, 1.13, 1.11, 0.99, 0.96, 0.37, 0.42, 0.42,
0.35, 0.41, 0.56, 0.51, 0.42, 0.33, 0.51, 0.39, 0.34, 0.22, 0.25,
0.22, 0.28, 0.25, 0.27, 0.44, 0.52, 0.15, 0.24, 0.17, 0.12, 0.13,
0.13, 0.15, 0.31, 0.11, 0.1, 0.18, 0.1, 0.1, 0.1, 0.23, 0.24,
0.27, 1.06, 0.15, 0.1, 0.13, 1.06, 0.16, 0.11, 0.18, 0.27, 0.27,
0.33, 0.49, 0.49, 0.1, 0.37, 0.38, 0.44, 0.36, 0.36, 0.23, 0.22,
0.32, 0.25, 0.36, 0.42, 0.48, 0.59, 0.1, 0.42, 0.1, 0.33, 1.08,
0.26, 0.49, 0.45)), row.names = c(NA, -96L), class = c("tbl_df",
"tbl", "data.frame")) %>%
  mutate(pollutant = "B", station = "51R001") %>%
  group_by(station, pollutant) %>%
  mutate(index = factor(dplyr::cur_group_id()))


stations <- unique(xls_plot$station)
pollutants <- unique(xls_plot$pollutant)
start_date <- min(xls_plot$mid_date)
end_date <- max(xls_plot$mid_date)

ui <- fluidPage(
  sidebarLayout(
    sidebarPanel(
      selectizeInput(
        inputId = "stations",
        label = "Station",
        choices = c("", stations),
        multiple = TRUE
      ),
      selectizeInput(
        inputId = "pollutants",
        label = "Polluant",
        choices = c("", pollutants),
        multiple = FALSE
      ),
      dateRangeInput(
        inputId = "dates",
        label = "Dates",
        start = start_date,
        end = end_date,
        min = NULL,
        max = NULL,
        format = "dd/mm/yyyy",
        startview = "month",
        weekstart = 1,
        language = "fr",
        separator = " à ",
        width = NULL,
        autoclose = TRUE
      ),
      width = 2),
    # Show a plot of the generated distribution
    mainPanel(
      plotlyOutput(outputId = "plot"),
      width = 10)
  )
)

xls_plot_filtered <- NULL
xmin_filtered <- NULL
xmax_filtered <- NULL
yRange <- NULL
server <- function(input, output, session, ...) {
  reactivedata <- reactive({
    xls_plot_filtered <<- xls_plot %>%
      filter(station %in% input$stations) %>%
      filter(pollutant %in% input$pollutants)

    xmin_filtered <<- min(xls_plot_filtered$mid_date)
    xmax_filtered <<- max(xls_plot_filtered$mid_date)
    yRange <<- range(xls_plot_filtered$concentration, na.rm = TRUE)

    if (all(!is.null(req(input$dates))))
      xls_plot_filtered %>% filter(mid_date>=input$dates[1] & mid_date<input$dates[2])
    else
      xls_plot_filtered
  })

  output$plot <- renderPlotly({
    req(input$stations, input$pollutants)
    if (identical(input$stations, "") || identical(input$pollutants, "")) return(NULL)

    xls_plot_filtered <<- reactivedata()

    plot_ly(xls_plot_filtered, x = ~mid_date, y = ~concentration, showlegend = FALSE) %>%
      add_bars(color = ~index) %>%
      rangeslider() %>%
      layout(
        barmode = "stack",
        title = "COV",
        xaxis = list(
          title = "Date",
          rangeslider = list(type = "date")),
        yaxis = list(title = "Concentration [µg/m³]", showline = TRUE),
        legend = list(itemclick = FALSE, itemdoubleclick = FALSE))
  })

  observeEvent(event_data("plotly_relayout"), {
    d <- event_data("plotly_relayout")
    xmin <- if (length(d[["xaxis.range[0]"]])) d[["xaxis.range[0]"]] else d[["xaxis.range"]][1]
    xmax <- if (length(d[["xaxis.range[1]"]])) d[["xaxis.range[1]"]] else d[["xaxis.range"]][2]
    if (is.null(xmin) || is.null(xmax)) return(NULL)

    # compute the y-range based on the new x-range
    idx <- with(xls_plot_filtered, xmin <= mid_date & mid_date <= xmax)
    yrng <- extendrange(xls_plot_filtered$concentration[idx])
    yrng[0] <- 0.

    # Update daterange selector accordingly to selection
    updateDateRangeInput(session, "dates", label = NULL, start = xmin,
                         end = xmax, min = NULL, max = NULL)

    plotlyProxy("plot", session) %>%
      plotlyProxyInvoke("relayout", list(yaxis = list(range = yrng)))
  })

  observeEvent(event_data("plotly_doubleclick"), {
    # Update daterange selector accordingly to selection
    updateDateRangeInput(session, "dates", label = NULL, start = xmin_filtered,
                         end = xmax_filtered, min = NULL, max = NULL)

    plotlyProxy("plot", session) %>%
      plotlyProxyInvoke("relayout", list(yaxis = list(range = yRange)))
  })
}

app <- shinyApp(ui, server)

然而,用于更新输入日期范围控件的observeEvent代码会导致rangeslider小部件无法显示数据的完整范围,而只显示所选的窗口:rangeslider restricted to selection

而它被正确显示为:

范围滑块显示为整体并带有选定区域

如果把下面的代码注释掉

updateDateRangeInput(session, "dates", label = NULL, start = xmin,
                         end = xmax, min = NULL, max = NULL)

在仍然更新输入日期范围控件的前提下,如何修改我的代码以让它重新正常工作?谢谢

解决方案

如果你有两个Shiny输入可以相互更新,除非这两个值彼此同构,否则改变其中一个输入将导致无限循环。 这个演示通过在短时间内“禁用”另一个输入来处理这种情况。1

你可以将这个很棒的演示从 @tomjemmett 改编到你的用例中,以便 dateRangeInputplotly_relayout 能够安全地互相更新。为清晰起见,界面进行了简化。

# R 4.6.0
library(shiny)  # shiny_1.13.0 
library(plotly) # plotly_4.12.0

input_data <- structure(list(mid_date = structure(c(20106, 20106, 20113, 20113, 20120, 20120, 20127, 20127, 20134, 20134, 20141, 20141, 20148, 20148, 20155, 20155, 20162, 20162, 20169, 20169, 20176, 20176, 20183, 20183, 20190, 20190, 20197, 20197, 20204, 20204, 20211, 20211, 20218, 20218, 20225, 20225, 20232, 20232, 20239, 20239, 20246, 20246, 20253, 20253, 20260, 20260, 20267, 20267, 20274, 20274, 20281, 20281, 20288, 20288, 20295, 20295, 20302, 20302, 20309, 20309, 20316, 20316, 20323, 20323, 20330, 20330, 20337, 20337, 20344, 20344, 20351, 20351, 20358, 20358, 20365, 20365, 20372, 20372, 20379, 20379, 20386, 20386, 20393, 20393, 20400, 20400, 20407, 20407, 20414, 20414, 20421, 20421, 20428, 20428, 20435, 20435), class = "Date"), concentration = c(1.39, 1.45, 0.69, 0.87, 1.29, 1.35, 1.48, 1.46, 1.3, 1.37, 0.1, 0.1, 1.53, 1.39, 1.58, 1.76, 0.82, 0.84, 1.28, 1.05, 1.13, 1.11, 0.99, 0.96, 0.37, 0.42, 0.42, 0.35, 0.41, 0.56, 0.51, 0.42, 0.33, 0.51, 0.39, 0.34, 0.22, 0.25, 0.22, 0.28, 0.25, 0.27, 0.44, 0.52, 0.15, 0.24, 0.17, 0.12, 0.13, 0.13, 0.15, 0.31, 0.11, 0.1, 0.18, 0.1, 0.1, 0.1, 0.23, 0.24, 0.27, 1.06, 0.15, 0.1, 0.13, 1.06, 0.16, 0.11, 0.18, 0.27, 0.27, 0.33, 0.49, 0.49, 0.1, 0.37, 0.38, 0.44, 0.36, 0.36, 0.23, 0.22, 0.32, 0.25, 0.36, 0.42, 0.48, 0.59, 0.1, 0.42, 0.1, 0.33, 1.08, 0.26, 0.49, 0.45), pollutant = c("B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B", "B"), station = c("51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001", "51R001"), index = structure(c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), levels = "1", class = "factor")), groups = structure(list( station = "51R001", pollutant = "B", .rows = structure(list( 1:96), ptype = integer(0), class = c("vctrs_list_of", "vctrs_vctr", "list"))), row.names = c(NA, -1L), class = c("tbl_df", "tbl", "data.frame"), .drop = TRUE), row.names = c(NA, -96L), class = c("grouped_df", "tbl_df", "tbl", "data.frame"))

ui <- fluidPage(
    dateRangeInput(
      inputId = "dates",
      label = "Dates",
      start = min(input_data$mid_date),
      end = max(input_data$mid_date),
      format = "dd/mm/yyyy",
      weekstart = 1,
      separator = " - "
    ),
    plotlyOutput(outputId = "plotly")
)

server <- function(input, output, session, ...) {

  output$plotly <- renderPlotly({
    plot_ly(
      input_data,
      x = ~ mid_date,
      y = ~ concentration,
      showlegend = FALSE
    ) |>
      add_bars(color = ~ index) |>
      rangeslider() |>
      layout(
        barmode = "stack",
        xaxis = list(title = "Date", rangeslider = list(type = "date")),
        yaxis = list(title = "Concentration [µg/m³]", showline = TRUE),
        legend = list(itemclick = FALSE, itemdoubleclick = FALSE)
      )
  })

  # wrap event_data once so we can bindEvent to it like a normal input
  relayout_data <- reactive(event_data("plotly_relayout"))

  # keep track of whether we are allowed to update the inputs. Default both to TRUE
  allow_update <- reactiveValues(dates = TRUE, plot = TRUE)

  # create reactives that will fire when the input's change. They will return
  # * NULL, if this slider cannot be updated currently
  # * The current system time otherwise (a monotonically increasing value)
  update_plot  <- reactive(if (allow_update$plot)  Sys.time()) |>
    bindEvent(relayout_data())

  update_dates <- reactive(if (allow_update$dates) Sys.time()) |>
    bindEvent(input$dates)

  # observe the event of these update_*() reactives changing. We need to make sure to not  
  # bind to the event when on initialisation, otherwise we will get stuck in a loop
  # plotly updates dateRangeInput
  observe({
    d <- relayout_data()

    new_start <- d[["xaxis.range"]][1] |> as.Date()
    new_end <- d[["xaxis.range"]][2] |> as.Date()

    if (is.null(new_start) || is.null(new_end)) return()

    allow_update$dates <- FALSE
    updateDateRangeInput(session, "dates", start = new_start, end = new_end)
  }) |>
    bindEvent(update_plot(), ignoreInit = TRUE)

  # dateRangeInput updates plotly
  observe({
    allow_update$plot <- FALSE
    plotlyProxy("plotly", session) |>
      plotlyProxyInvoke(
        "relayout",
        list("xaxis.range" = as.character(input$dates))
      )
  }) |>
    bindEvent(update_dates(), ignoreInit = TRUE)

  # in order to re-enabled the inputs, we want to wait a short period of time. create a 
  # monotonically increasing reactive which is triggered by the allow_update reactiveValues
  last_update_trigger <- reactive(Sys.time()) |>
    bindEvent(allow_update$dates, allow_update$plot) |>
    debounce(100)

  # when the last_update_trigger is fired, re-enable the inputs
  observe({
    allow_update$plot <- TRUE
    allow_update$dates <- TRUE
  }) |>
    bindEvent(last_update_trigger())
}

shinyApp(ui, server)

结果

编辑1

我仍然有一个问题:当发生relayout事件(通过rangeslider、日期范围输入或选择)时,如何根据所选数据来改变y 轴的范围?

--> 你可以通过对 yaxis.range 进行Plotly的重新布局来改变y 轴范围。但就你给出的示例而言,我不会让y 轴成为可变的。我只需要重新绘制,并在output$plotly内使用输入,让Plotly选择合适的范围,像这样:

# R 4.6.0
library(shiny)  # shiny_1.13.0 
library(plotly) # plotly_4.12.0

set.seed(42321)
input_data <- data.frame(
  station       = rep(c("R001", "R002", "R003"), each = 20),
  pollutant     = rep(rep(c("A", "B"), each = 10), 3),
  mid_date      = seq.Date(20100, 20159),
  concentration = sample(20, 60, replace = TRUE) / 10
) |>
  within({
    index = match(paste(station, pollutant), unique(paste(station, pollutant)))
    concentration = index * concentration
    index = factor(index)
    })

ui <- fluidPage(
    selectizeInput(
      inputId = "stations",
      label = "Station",
      selected = input_data$station[1],
      choices = unique(input_data$station),
      multiple = TRUE
    ),
    selectizeInput(
      inputId = "pollutants",
      label = "Polluant",
      choices = unique(input_data$pollutant),
      multiple = FALSE
    ),
    dateRangeInput(
      inputId = "dates",
      label = "Dates",
      start = min(input_data$mid_date),
      end = max(input_data$mid_date),
      format = "dd/mm/yyyy",
      weekstart = 1,
      separator = " - "
    ),
    plotlyOutput(outputId = "plotly")
)

server <- function(input, output, session, ...) {

  output$plotly <- renderPlotly({
    req(input$stations, input$pollutants)

    input_data |>
      filter(station %in% input$stations) |>
      filter(pollutant %in% input$pollutants) |>
      plot_ly(
        x = ~ mid_date,
        y = ~ concentration,
        showlegend = FALSE
      ) |>
        add_bars(color = ~index) |>
        rangeslider() |>
        layout(
          barmode = "stack",
          xaxis = list(title = "Date", rangeslider = list(type = "date")),
          yaxis = list(title = "Concentration [µg/m³]", showline = TRUE),
          legend = list(itemclick = FALSE, itemdoubleclick = FALSE)
        )
  })

  # wrap event_data once so we can bindEvent to it like a normal input
  # for multiple plotly-plots you might need to set the source which defaults to A
  relayout_data <- reactive(event_data("plotly_relayout"))

  # keep track of whether we are allowed to update the inputs. Default both to TRUE
  allow_update <- reactiveValues(dates = TRUE, plot = TRUE)

  # create reactives that will fire when the input's change. They will return
  # * NULL, if this slider cannot be updated currently
  # * The current system time otherwise (a monotonically increasing value)
  update_plot  <- reactive(if (allow_update$plot)  Sys.time()) |>
    bindEvent(input$`plotly_relayout-A`)

  update_dates <- reactive(if (allow_update$dates) Sys.time()) |>
    bindEvent(input$dates)

  # observe the event of these update_*() reactives changing. We need to make sure to not bind to the event when on
  # initialisation, otherwise we will get stuck in a loop
  # plotly updates dateRangeInput
  observe({
    d <- relayout_data()

    new_start <- d[["xaxis.range"]][1] |> as.Date()
    new_end <- d[["xaxis.range"]][2] |> as.Date()

    if (is.null(new_start) || is.null(new_end)) return()

    allow_update$dates <- FALSE
    updateDateRangeInput(session, "dates", start = new_start, end = new_end)
  }) |>
    bindEvent(update_plot(), ignoreInit = TRUE)

  # dateRangeInput updates plotly
  observe({
    allow_update$plot <- FALSE
    plotlyProxy("plotly", session) |>
      plotlyProxyInvoke(
        "relayout",
        list("xaxis.range" = as.character(input$dates))
      )
  }) |>
    bindEvent(update_dates(), ignoreInit = TRUE)

  # in order to re-enabled the inputs, we want to wait a short period of time. create a monotonically increasing
  # reactive which is triggered by the allow_update reactiveValues
  last_update_trigger <- reactive(Sys.time()) |>
    bindEvent(allow_update$dates, allow_update$plot) |>
    debounce(100)

  # when the last_update_trigger is fired, re-enable the inputs
  observe({
    allow_update$plot <- TRUE
    allow_update$dates <- TRUE
  }) |>
    bindEvent(last_update_trigger())
}

shinyApp(ui, server)

结果2

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章