Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Shiny slider restrict reaction to releasing left mouse button

I am using a Shiny application in which it may take some time to set a slider to the right value.

So while trying to set the slider to the right value (and not releasing my left mouse button!) the (i.e. my local) server observed several new values and reacts accordingly.

As the response of my server on any new value may take a few seconds I would be pleased if I could either:

  • postpone signalling the server till the release of the left mouse button, or
  • at the server side, abort any earlier responses if it receive a new value
like image 396
Pisca46 Avatar asked Mar 23 '15 23:03

Pisca46


2 Answers

Shiny's sliderInput uses Ion.RangeSlider, which has an onFinish callback for when a user releases their mouse. That sounds like just what we need.

Here's a custom input binding for a "lazy" slider input that only signals a value change when a user releases their mouse (onFinish) or when the slider is forcibly updated (onUpdate).

For the example, I just inlined code that changes the behavior of ALL sliderInputs. You'll want to move this to an external script and customize further. Also, onUpdate gets called when the slider is initialized, but before Shiny initializes input values. You have to wait until Shiny does this to call the value change callback in onUpdate. The solution I worked in isn't fantastic, but I was too lazy to find a cleaner way.

library(shiny)

ui <- fluidPage(
  tags$head(
    tags$script(HTML("
      (function() {
        var sliderInputBinding = Shiny.inputBindings.bindingNames['shiny.sliderInput'].binding;

        var lazySliderInputBinding = $.extend({}, sliderInputBinding, {
          subscribe: function(el, callback) {
            var $el = $(el);
            var slider = $el.data('ionRangeSlider');

            var handleChange = function() {
              if (!inputsInitialized) return;
              callback(!$el.data('immediate') && !$el.data('animating'));
            };

            slider.update({
              onUpdate: handleChange,
              onFinish: handleChange
            });
          },

          unsubscribe: function(el, callback) {
            var slider = $(el).data('ionRangeSlider');
            slider.update({
              onUpdate: null,
              onFinish: null
            });
          }
        });

        Shiny.inputBindings.register(lazySliderInputBinding, 'shiny.lazySliderInput');

        var inputsInitialized = false;
        $(document).one('shiny:connected', function() {
          inputsInitialized = true;
        });
      })();
    "))
  ),
  sliderInput("sliderA", "A", 0, 10, 5),
  uiOutput("sliderB"),
  verbatimTextOutput("sliderValues"),
  actionButton("resetSliders", "Reset Sliders")
)

server <- function(input, output, session) {
  observeEvent(input$resetSliders, {
    updateSliderInput(session, "sliderA", value = 5)
    updateSliderInput(session, "sliderB", value = c(4, 6))
  })

  output$sliderB <- renderUI({
    sliderInput("sliderB", "B", 0, 10, c(4, 6))
  })

  output$sliderValues <- renderPrint({
    cat(paste("Slider A =", input$sliderA), "\n")
    cat(paste("Slider B =", paste(input$sliderB, collapse = " ")))
  })
}

shinyApp(ui, server)
like image 137
greg L Avatar answered Oct 17 '22 19:10

greg L


Alright, after some digging, I think I've found the solution. It appears the cleanest way to do this, without needing to mess with Java script, would be to use the shinyCustom package, which has a useShinyCustom and customSliderInput function. With these, you can set the reactivity to "debounce" and also mess with the delay time. If you were to use these options together, you should be able to find the sweet spot for only updating the output once the slider stops moving. It won't be a perfect "on release of mouse button;" but it should be pretty darn close!

For example, you may try something like:

# Slider delay type is actually "debounce" by default 
useShinyCustom(slider_delay = "500"), # Doubles the default delay time in ms
customSliderInput("bins", #Use customSliderInput instead of sliderInput
              "Number of bins:",
              min = 1,
              max = 50,
              value = 30)
like image 30
creutzml Avatar answered Oct 17 '22 19:10

creutzml