How to pause/edit reactiveTimer

TLDR: how to create a timer object to refresh parts across shiny app, this should be a module I can import across the app to keep all things in sync with the ability to pause/edit the interval.

I'm writing a shiny app. In this app I have multiple parts that I want to be updated on an interval.

I'm looking for a way to create a timer object I can use to refresh different parts at a given interval and also a way to pause/change the interval of the reactiveTimer.

I came up with the idea of using a common timer object (setInterval) in a module (using rhino and box) which I import in other modules and use with shiny$observeEvent(timer()....

This is the simple timer all it does is trigger every 500ms. Now I want to add the ability to pause and edit the interval.

# Set up a common reactive timer to trigger every 500 ms
common_timer <- shiny$reactiveTimer(interval = 1500)

#' @export
timer <- function() {
    return(common_timer())
}

I tried a few different things like making an R6 class to handle the state of my timer (active/inactive); returns timer object or NULL. But I can't get anything to work properly. My R6 class works but when I pause the timer I can't get it to restart again.

Here is some of my code. Here is an example of how I use the timer for getting some data into my app.

box::use(shiny)

#' @export
live_data <- shiny$reactiveVal(NULL)

#' @export
date_interval <- shiny$reactiveValues(
    start_date = lubridate::as_datetime("2019-01-01"),
    end_date = lubridate::as_datetime("2019-01-28")
)

box::use(lubridate)

box::use(./common_timer[ timer ])
box::use(./modules/helpers)
box::use(./modules/cpp)

#' @export
call_data <- function(session, side_bar_options) {
    shiny$observeEvent(timer$get_timer, {
        currency <- shiny$isolate(side_bar_options$currency())

        dt <- helpers$read_market_data(
            file = "./data/kucoin_btc-usdt-hourly.csv",
            symbols = currency,
            from = date_interval$start_date,
            to = date_interval$end_date
        )

        live_data(dt)

        # bump the dates by 10 days
        date_interval$start_date <- date_interval$start_date + lubridate$days(1)
        date_interval$end_date <- date_interval$end_date + lubridate$days(1)
    })
}

And here is where I want to pause and then after add ability to edit the interval for the timer.

box::use(shiny)

#' @export
ui <- function(id, width = 2) {
    ns <- shiny$NS(id)
    shiny$sidebarPanel(
        shiny$tags$h4("Side bar"),
        shiny$selectInput(
            inputId = ns("select_currency"),
            label = "Currency:",
            choices = c("BTC/USDT", "XMR/BTC"),
            selected = "BTC/USDT"
        ),
        shiny$selectInput(
            inputId = ns("supplementary_plot"),
            label = "Supplementary Plot:",
            choices = c("RSI", "MACD", "Volume")
        ),
        shiny$actionButton(
            inputId = ns("toggle_timer"),
            label = "Start/Pause"
        ),
        width = width
    )
}

box::use(../logic/common_timer[ timer ])

#' @export
server <- function(id, live_data) {
    shiny$moduleServer(id, \(input, output, session) {
        ns <- session$ns

        shiny$observeEvent(input$toggle_timer, {
            timer$toggle_timer()
        })

        supplementary_plot <- shiny$reactive({
            input$supplementary_plot
        })

        currency <- shiny$reactive({
            input$select_currency
        })

        return(list(
            currency = currency,
            supplementary_plot = supplementary_plot
        ))
    })
}

Any help would be appreciated. Thank you very much.

Is this useful ?


library(shiny)

ui <- fluidPage(
  numericInput("ms","millisec",
               value=1000,
               min=100,
               max=2000,
               step=100),
  checkboxInput("chk","on/off",value=TRUE),
  verbatimTextOutput("display")
)

# to see more decimal points
op <- options(digits.secs = 6)

server <- function(input, output, session) {
  
  mytimer <- reactive({
    if(!isTruthy(input$chk)){
      return(NULL)
    } else{
      invalidateLater(millis=req(input$ms))
    }
    Sys.time()
  })
  
  last_timer_value <- reactiveVal(NULL)
  
  observeEvent(mytimer(),
               {if(!identical(last_timer_value,
                             mytimer())){
                 if(isTruthy(mytimer()))
                   last_timer_value(mytimer())
               }})
  
  output$display <- renderText({
    capture.output(req(last_timer_value()))
  })
}

shinyApp(ui, server)

That indeed demonstrates the functionality I'm after thank you @nirgrahamuk! However, I'm looking to get the timer object into an independent module, a standalone file I can import into others and use it there to trigger refreshes. Can you help me to modularise timer?

I tried myself and again the app ticks but it doesn't allow me to modify the timer object. I think I'm not correctly passing the options to the server function of the timer.

The reason I need it as a separate module is I want to then import it to various other files with other parts of my app and trigger refreshes on those all together.

I'm still learning so thank you for the help. Here is what I have modularised so far from the code you gave me.

main.R

box::use(shiny)

ui <- shiny$fluidPage(
  shiny$numericInput(
        "ms",
        "millisec",
        value=1000,
        min=100,
        max=2000,
        step=100
    ),
    shiny$checkboxInput("chk", "on/off", value = TRUE),
    shiny$verbatimTextOutput("display")
)

# to see more decimal points
op <- options(digits.secs = 6)

box::use(./suggested_timer)

server <- function(input, output, session) {
    timer_options <- shiny$reactive({
        list(
            timer_ms = input$ms,
            timer_active = input$chk
        )
    })

    last_timer_value <- suggested_timer$server(timer_options)
    output$display <- shiny$renderText({
        capture.output(shiny$req(last_timer_value()))
    })


}

shiny$shinyApp(ui, server)

suggested_timer.R

box::use(shiny)

#' @export
server <- function(timer_options) {
    options <- shiny$isolate(timer_options())
    mytimer <- shiny$reactive({
            if(!shiny$isTruthy(options$timer_active)) {
            return(NULL)
        } else{
            shiny$invalidateLater(millis = shiny$req(options$timer_ms))
        }

        Sys.time()
    })

    last_timer_value <- shiny$reactiveVal(NULL)

    shiny$observeEvent(mytimer(), {
        if(!identical(last_timer_value, mytimer())) {
            if(shiny$isTruthy(mytimer())) {
                last_timer_value(mytimer())
            }
        }
    })

    return(last_timer_value)
}

Here a stackoverflow crosspost can be found.

I made the fewest modifications possible to my original example to demonstrate such a thing; and crossposted the solution to stackoverflow.

library(shiny)

usesTimerModUI <- function(id) {
  ns <- NS(id)
  tagList(
    div(style="border:1px solid black;",
    h4(ns("")),
    verbatimTextOutput(ns("display"))
  ))
}

usesTimerModServer <- function(id,source_timer) {
  moduleServer(
    id,
    function(input, output, session) {
      stopifnot(is.reactive(source_timer))
      
      output$display <- renderText({
        capture.output(req(source_timer()))
      })
    }
  )
}

ui <- fluidPage(
  numericInput("ms","millisec",
               value=1000,
               min=100,
               max=2000,
               step=100),
  checkboxInput("chk","on/off",value=TRUE),
  hr(),
  usesTimerModUI("m1"),
  br(),
  usesTimerModUI("m2")

)

# to see more decimal points
op <- options(digits.secs = 6)

server <- function(input, output, session) {
  
  mytimer <- reactive({
    if(!isTruthy(input$chk)){
      return(NULL)
    } else{
      invalidateLater(millis=req(input$ms))
    }
    Sys.time()
  })
  
  last_timer_value <- reactiveVal(NULL)
  
  observeEvent(mytimer(),
               {if(!identical(last_timer_value,
                              mytimer())){
                 if(isTruthy(mytimer()))
                   last_timer_value(mytimer())
               }})
  
  usesTimerModServer(id="m1",
                     source_timer = last_timer_value)
  usesTimerModServer(id="m2",
                     source_timer = last_timer_value)
  

}

shinyApp(ui, server)

TLDR: I got something working (pausable/editable interval timer reactive that I can use in other modules of my app for synching everything together)! However, I can't reduce it to a minimal example outside of rhino framework/project. I would appreciate for someone more experienced to review my shiny code and point out my stupid mistakes. Finally I don't understand HOW the timer I got working works - if someone can explain/correct my code I would appreciate it greatly. I would tip you in bitcoin if you provide an address and help :slight_smile:

got something working; kind of

I was able to get something working in my app. But the honest truth is I don't understand how the whole thing works and it only works in my rhino project.

If someone can review my code and let me know any corrections/improvements to make I would greatly appreciate it. I will try to pare down my code to as minimal an example as possible. However, I don't understand everything thus I cannot provide a truely minimal example.

I tried to pare down my code to a mininal example that works but I couldn't get it to work as a minimal example.

I get this error if I run the minimal version (below) in a separate project without the rhino framework.

Listening on http://127.0.0.1:3003
Warning: Error in paste: environments cannot be coerced to other types
  62: paste
  61: shiny$NS
  60: ui [#2]
   3: runApp
   2: print.shiny.appobj
   1: <Anonymous>
^C

With rhino I run the whole app as such:

shiny::runApp(
    port = 3003,
    launch.browser = FALSE
)

And that script calls:

# Rhino / shinyApp entrypoint. Do not edit.
rhino::app()

If I try to run this minimal code as such then I get the error I showed above.

Nonetheless the point was to demonstrate my timer that can be paused and edited. I was able to get it to work, but it ONLY works in my rhino app.

Someone please help I'm a bit lost.

minimal code example

Note this is not working outside of rhino app.

main.R

box::use(shiny)

box::use(./side_bar)
box::use(./call_data[ call_data, date_interval ])

#' @export
ui <- function(id) {
    ns <- shiny$NS(id)
    shiny$fluidPage(
        shiny$titlePanel("Real time trading"),
        # we will render the subtitle from date_interval in server
        shiny$tags$h4(shiny$textOutput(ns("sub_title"))),
        shiny$sidebarLayout(
            side_bar$ui(ns("side_bar")),
            shiny$mainPanel(
                shiny$tabsetPanel(
                    shiny$tabPanel(
                        "Hello World",
                        shiny$tags$h1("Hello World")
                    )
                ),
                width = 10
            )
        )
    )
}

#' @export
server <- function(id) {
    shiny$moduleServer(id, \(input, output, session) {
        ns <- session$ns

        # -----------------------
        # concat the subtitle
        output$sub_title <- shiny$renderText({
            paste("From", date_interval$start_date, "to", date_interval$end_date)
        })

        # -----------------------
        side_bar_options <- side_bar$server("side_bar")

        # -----------------------
        call_data(session, side_bar_options)
    })
}

shiny$shinyApp(ui, server, options = list(port = 3003, launch.browser = FALSE))

common_timer.R

box::use(shiny)
box::use(R6)

timer_func <- function(timer_opts) {
    shiny$observe({
        print(timer_opts$interval())
        print(timer_opts$active())
    })

    mytimer <- shiny$reactive({
        millis <- shiny$req(timer_opts$interval())
        shiny$invalidateLater(millis = millis)

        active <- shiny$req(timer_opts$active())
        if (!shiny$isTruthy(active)) {
            return(NULL)
        }

        return(Sys.time())
    })

    return(mytimer)
}

box::use(./side_bar[ timer_opts ])

#' @export
timer <- timer_func(timer_opts)

call_data.R

box::use(shiny)

#' @export
date_interval <- shiny$reactiveValues(
    start_date = lubridate::as_datetime("2019-01-01"),
    end_date = lubridate::as_datetime("2019-01-28")
)

box::use(lubridate)

box::use(./common_timer[ timer ])

#' @export
call_data <- function(session, side_bar_options) {
    shiny$observeEvent(timer(), {
        date_interval$start_date <- date_interval$start_date + lubridate$days(1)
        date_interval$end_date <- date_interval$end_date + lubridate$days(1)
    })
}

side_bar.R

box::use(shiny)

#' @export
ui <- function(id, width = 2) {
    ns <- shiny$NS(id)
    shiny$sidebarPanel(
        shiny$tags$h4("Side bar"),
        shiny$selectInput(
            inputId = ns("select_currency"),
            label = "Currency:",
            choices = c("BTC/USDT", "XMR/BTC"),
            selected = "BTC/USDT"
        ),
        shiny$selectInput(
            inputId = ns("supplementary_plot"),
            label = "Supplementary Plot:",
            choices = c("RSI", "MACD", "Volume")
        ),
        shiny$actionButton(
            inputId = ns("timer_toggle"),
            label = "Start/Pause"
        ),
        shiny$numericInput(
            ns("timer_interval"),
            "millisec",
            value = 300,
            min = 1,
            max = Inf
        ),
        width = width
    )
}

#' @export
timer_opts <- list(
    interval = shiny$reactiveVal(300),
    active = shiny$reactiveVal(TRUE)
)

#' @export
server <- function(id, live_data) {
    shiny$moduleServer(id, \(input, output, session) {
        shiny$observeEvent(input$timer_toggle, {
            timer_opts$active(!timer_opts$active())
        })

        shiny$observeEvent(input$timer_interval, {
            timer_opts$interval(input$timer_interval)
        })

        supplementary_plot <- shiny$reactive({
            input$supplementary_plot
        })

        currency <- shiny$reactive({
            input$select_currency
        })

        return(list(
            currency = currency,
            supplementary_plot = supplementary_plot
        ))
    })
}

This topic was automatically closed 54 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.