Shiny slider restrict reaction to releasing left mouse button
Asked Answered
C

2

15

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
Cissy answered 23/3, 2015 at 23:54 Comment(1)
Possible duplicate, also referencing debounce(): stackoverflow.com/questions/32235525Cosmic
E
6

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)
Eastbourne answered 9/12, 2017 at 7:28 Comment(2)
thanks, this is really what I need, I put it in a lazy_slider.js file as you recommendKuebbing
@greg L Thanks for this brilliant JS code. However this code does not work with Shiny 1-7Overwhelming
V
5

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)
Vaas answered 7/12, 2017 at 16:15 Comment(2)
yeah, I'm imagining if the user is interested in getting exactly what they requested, they could probably dig into the Java script generated by this code. But it's a bit out of my reach, regarding the time to investigate.Vaas
This package was written before debounce and throttle were added. Now it's easy to debounce/throttle any input - just wrap the input value in a reactive like debounce(reactive(input$slider), 500)Eastbourne

© 2022 - 2024 — McMap. All rights reserved.