rstudio / DT

R Interface to the jQuery Plug-in DataTables
https://rstudio.github.io/DT/
Other
595 stars 182 forks source link

Column filters not updating #587

Open dcleere opened 6 years ago

dcleere commented 6 years ago

Hi there,

My issue is identical to what was reported in StackOverflow here - https://stackoverflow.com/questions/45547670/datatable-filters-update-on-the-fly

When I filter by one column, the factors in the remaining columns include all possibilities, regardless of whether or not it is relevant to the first filter. Can the column filtering only include categories that remain in the data following previous filtering, and not all categories in the data?

I do not want to use selectInput as a workaround.

Thanks!

rfineman commented 5 years ago

Any progress on this front? Facing the same issue.

shrektan commented 4 years ago

Sorry for the late reply but there's no short plan on this feature yet. However, there's a new extension SearchPanes that is able to do that (but it's different from the filter and only works for the client-side processing mode). This new extension will be available when #756 gets merged.

AhmedKhaled945 commented 2 years ago

@shrektan Hello, is this still the case?

yihui commented 2 years ago

@AhmedKhaled945 I think it should be possible now with @wholmes105 and @mikmart's recent work.

remotes::install_github('rstudio/DT')

Mikko has provided an example here: https://github.com/rstudio/DT/blob/ddcc9e91ffa00924ccffeb6ceee14dfb2e14c0b6/inst/examples/DT-updateFilters/app.R#L46

You can update the filters with the filtered data, which you can obtain from input$tableId_rows_all (https://rstudio.github.io/DT/shiny.html). It should work for both client-side and server-side processing.

That said, the SearchPanes extension may also work, but it would only work for the client-side processing mode as @shrektan mentioned above.

AhmedKhaled945 commented 2 years ago

@yihui This is the example found, tried to tweak it to implement dependent filters, i think there is a way but somehow it is not working as expected, it gives wrong factor levels on the last 2 columns and it revert filtering when i filter using the sliders,

library(shiny)
library(DT)

tbl <- data.frame(
    num1 = seq(-2, 2) / 1,
    num2 = seq(-2, 2) / 10,
    num3 = seq(-2, 2) / 100,
    num4 = replace(seq(-2, 2) / 100, 1, Inf),
    dttm = round(Sys.time()) + seq(-2, 2) * 3600,
    fct1 = factor(c("A", rep("B", 4)), levels = c("A", "B", "C")),
    fct2 = factor(c(rep("A", 4), "B"), levels = c("A", "B", "C"))
)

ui <- fluidPage(

    DTOutput("table"),
)

server <- function(input, output, session) {
    output$table <- renderDT(datatable(tbl, filter = "top"))
    namess <- names(tbl)

    tbl_slice <- eventReactive(input$table_rows_all, ignoreNULL = T, {

        print(input$table_rows_all)

        tbl[input$table_rows_all, ]
    })

    proxy <- dataTableProxy("table")
    observeEvent(tbl_slice(), {
        #replaceData(proxy, tbl_slice())
        updateFilters(proxy, tbl_slice())
    })
}

shinyApp(ui, server)
wholmes105 commented 2 years ago

@AhmedKhaled945 In the new updateFilters() as originally written, the data argument was immediately converted into a list containing the new limits of the filters - you could give updateFilters() a list containing just the new limits you wanted instead of the data in your datatable and you would get the same result. I haven't had the opportunity to test this with @mikmart's updates, but in the worst case, you could construct a dummy data.frame containing whatever numeric ranges, factor levels, etc. you want and feed that to updateFilters() in place of the original data to get the desired effect.

AhmedKhaled945 commented 2 years ago

so the format of the data argument should be something like this

list(col1 = factors of col1, col2 = (lower_limit, upper_limit)  for a range, ...etc)  

and so on?

wholmes105 commented 2 years ago

It could be; remember that any data.frame is also a list. The idea behind updateFilters's creation was that the function would calculate the new limits for you, but also that you could change the limits of those filters by changing what you fed to the data argument.

For example, if I want to update a datatable with the following data

data.frame(letters)

there's nothing stopping me from using this data for the filters instead

data.frame(c(letters, LETTERS))

Doing this would include options in the filters that do not appear in the datatable.

Alternatively,

data.frame(letters[-1:10])

I could deliberately make filters that do not display all possible values as options. Note in the latter case that the filters do not filter the data if left empty, meaning the non-selectable values would still be visible to the user in the datatable until they used the relevant column filter, and the filtered values would reappear if the filter was cleared.

mikmart commented 2 years ago

I can confirm that with the recent changes, too, you can pass just the desired limits in a list. Ideally you wouldn't have to fiddle with them manually too much, though.

I made a little "real world" app focusing on this dependent filtering feature. Using droplevels() here is key to get just the values appearing in the filtered data in the updated filters.

library(shiny)
library(DT)
library(dplyr)

# Adverse events data from a fake clinical trial
adae <- haven::read_xpt("https://github.com/RConsortium/submissions-pilot1-to-fda/raw/main/m5/datasets/rconsortiumpilot1/analysis/adam/datasets/adae.xpt")

tbl <- adae %>% 
  transmute(
    USUBJID = factor(USUBJID),
    AESEQ = as.integer(AESEQ),
    AEDECOD = factor(AEDECOD),
    AESOC = factor(AESOC)
  )

ui <- fluidPage(
  DTOutput("table")
)

server <- function(input, output, session) {
  output$table <- renderDT(tbl, filter = "top")
  filtered <- reactive(droplevels(tbl[input$table_rows_all, ]))

  proxy <- dataTableProxy("table")
  observeEvent(filtered(), {
    updateFilters(proxy, filtered())
  })
}

shinyApp(ui, server)

The slider input resetting that @AhmedKhaled945 mentioned is evident here, too. I think what's happening is: You first set a filter range on the slider, which subsets the data. But then in the new data the slider range is set to the full range of the data. I believe such a setting is currently interpreted as "no filter" which then results in the reset to full data. I'm not sure how you would go about getting around that.

Another annoyance is only being able to select one level from factor filters. That also makes sense: you pick one, which results in the filter being applied straight away. Then your data only contains that one value for that column, so that's the only one available in the select control now. I think to work around that, you'd need to somehow delay sending the filter input from the select control until it's lost focus, or something along those lines.

So yeah, I think this would be a useful feature, but I don't think we're quite there yet with the current functionality.

wholmes105 commented 2 years ago

But then in the new data the slider range is set to the full range of the data. I believe such a setting is currently interpreted as "no filter" which then results in the reset to full data. I'm not sure how you would go about getting around that.

That's correct; as I mentioned earlier, the column filters are only active if they are not empty; take the following as an example:

factor1 = factor(c('a', 'b', 'c'))
factor2 = factor(c('b', 'c', 'd'))

If we updated the filter for factor1 using factor2's data, then level a would still appear if we did not actually select a value in the column filter. Before updateFilters(), the only way to deal with this was to write custom code or remake the table from scratch.

Another annoyance is only being able to select one level from factor filters.

Perhaps I just misunderstood you, but you can select multiple levels in a factor filter sequentially. Using iris as an example, you can select setosa and then versicolor as options from the Species column; as long as the filters themselves haven't been updated to exclude the original values, all the old options will still be there.

datatable(iris, filter = 'top')
mikmart commented 2 years ago

Ah sorry for the confusion @wholmes105. With everything after the R code for the app, I was specifically referring to this example app with adverse event data, and dependent filters using input$table_rows_all.

wholmes105 commented 2 years ago

... I was specifically referring to this example app with adverse event data, and dependent filters using input$table_rows_all.

Ah, ok. In that case, I'd recommend using the functionality shiny already has built in for reactive delays. debounce and throttle will delay responses to reactive events for a pre-defined length of time, and you could combine observeEvent with actionButton if you wanted to only update the filters on command rather than on the fly.

mikmart commented 2 years ago

I thought about this a bit more, and one thought that came to mind was to only update filters for columns that don't already have a filter set. I updated the server for my example app to accomodate that:

server <- function(input, output, session) {
  output$table <- renderDT(tbl, filter = "top")
  filtered <- reactive(tbl[input$table_rows_all, ])

  proxy <- dataTableProxy("table")
  observe({
    updates <- as.list(filtered())

    # Don't update filters on columns that are already filtered
    is_filtered_col <- (input$table_search_columns != "")
    if (!length(is_filtered_col)) return()

    updates[!is_filtered_col] <- droplevels(filtered()[!is_filtered_col])
    updates[is_filtered_col] <- tbl[is_filtered_col]

    updateFilters(proxy, updates)
  })
}

It solves some of the issues with the initial approach, like the sliders resetting and only being able to select one option. However, this is not without issues either. If you set filters on multiple columns, and then go to change one that already has a filter, you get the full range of options available again. Here's a gif illustrating what's happening:

Gif

That doesn't seem like how a feature like this should work. When opening a filter control, I would expect to be able to pick options as if the currently open filter had nothing set, but all other filters in the table were applied. That's not something that can be achieved at the moment on the R side, since there's no input event for a user opening a filter. Just for them changing one.

Could we add the currently open filter control to the information DT passes to the server input object?

mikmart commented 2 years ago

I tried to see what it would take to implement this "show options that would be available if all other filters would be applied" approach. It's incredibly hacky (and realisticly unusable as it is), but it works correctly -- if not well.

gif

Here's the full code:

library(shiny)
library(DT)
library(dplyr)

# Adverse events data from a fake clinical trial
adae <- haven::read_xpt("https://github.com/RConsortium/submissions-pilot1-to-fda/raw/main/m5/datasets/rconsortiumpilot1/analysis/adam/datasets/adae.xpt")

tbl <- adae %>% 
  transmute(
    USUBJID = factor(USUBJID),
    AESEQ = as.integer(AESEQ),
    AEDECOD = factor(AEDECOD),
    AESOC = factor(AESOC)
  )

ui <- fluidPage(
  DTOutput("table"),
  DTOutput("table_shadow"),
  tags$script(HTML(
    "
    // Set hooks to tell R which filter is focused
    function focusHook() {
      var $filterRow = $('#table thead tr:last');
      $filterRow.find('input[type=\"search\"]').each(function(i) {
        $(this).focus(function() {
          Shiny.setInputValue('table_search_columns_focus', i + 1);
        });
      });
    }

    // Don't know how to tell when DT is ready
    $(function() { setTimeout(focusHook, 500) });
    "
  ))
)

server <- function(input, output, session) {
  output$table <- renderDT(tbl, filter = "top", options = list(pageLength = 5))

  # Need somewhere to perform a modified search without disturbing real table
  output$table_shadow <- renderDT(tbl, filter = "top", options = list(
    pageLength = 2, dom = "ti"
  ))

  # Search shadow table with current search without focused filter
  shadow_proxy <- dataTableProxy("table_shadow")
  observeEvent(input$table_search_columns_focus, {
    focused <- req(input$table_search_columns_focus)
    # Leading empty search is for rownames which isn't inlucded in input
    search_cols <- c("", replace(input$table_search_columns, focused, ""))
    updateSearch(shadow_proxy, keywords = list(columns = search_cols))
  })

  # Update focused filter with options from searched shadow table
  # Also reset others so they don't get stuck disabled & unfocusable
  proxy <- dataTableProxy("table")
  observe({
    focused <- req(input$table_search_columns_focus)
    shadow_rows <- tbl[req(input$table_shadow_rows_all), ]
    updates <- as.list(tbl) # lapply(tbl, function(...) NULL)
    updates[focused] <- droplevels(shadow_rows[focused])
    updateFilters(proxy, updates)
  })
}

shinyApp(ui, server)

The two "missing technologies" to make this approach viable are:

  1. An input that tells which filter control the user is currently interacting with. I worked around this with some crude JS.
  2. A way to perform a modified search on the R side. A silly work-around here was to use a second "shadow" table to perform the search on the client side.

The first should be fairly straightforward to add to DT, the second I think a bit more involved. The search is actually already currently done on the server side within this function:

https://github.com/rstudio/DT/blob/a121b0b6cd568ab9400d0e307cb8e1fd2baa9a77/R/shiny.R#L586-L587

The parts relevant to the search function would need to be extracted and parameterized in an accessible manner.

mikmart commented 2 years ago

I think I figured out a much cleaner way to do this. It's still a bit involved, and does need access to the internal server search functions, but the API for the end user could just be a single function. I made a branch with a PoC version, where I called that function linkColumnFilters().

Here's my demo app updated to use it:

library(shiny)
library(DT) # remotes::install_github("mikmart/DT@link-filters")
library(dplyr)

# Adverse events data from a fake clinical trial
file <- file.path(tempdir(), "adae.xpt")
if (!file.exists(file)) {
  url <- "https://github.com/RConsortium/submissions-pilot1-to-fda/raw/main/m5/datasets/rconsortiumpilot1/analysis/adam/datasets/adae.xpt"
  curl::curl_download(url, file, quiet = FALSE)
}
adae <- haven::read_xpt(file)

tbl <- adae %>% 
  transmute(
    USUBJID = factor(USUBJID),
    AESEQ = as.integer(AESEQ),
    AEDECOD = factor(AEDECOD),
    AESOC = factor(AESOC)
  )

ui <- fluidPage(
  DTOutput("table")
)

server <- function(input, output, session) {
  output$table <- renderDT(tbl, filter = "top")
  linkColumnFilters(dataTableProxy("table"), tbl)
}

shinyApp(ui, server)

@AhmedKhaled945 would you like to try it out to see if it does what you expect? You'd need to install my branch:

remotes::install_github("mikmart/DT@link-filters")
mikmart commented 2 years ago

On second thought, maybe the "all-in-one" linkColumnFilters() is not such a good idea. One glaring issue is that it creates reactive observers, meaning that it should basically never be called from another observer. That's pretty much the exact opposite of how all other proxy functions in DT are used: you almost always want them inside observers to be useful.

It would be handy to wrap the full functionality in a single call, but for now I'm not sure how it should be presented.

In the meanwhile, maybe we could just export the searching functions from DT and let users implement their own solutions? Here's a POC branch for that -- it exports doColumnSearch() and doGlobalSearch() that can be used to get the filtered indices from data, given the search strings that DT uses. And here's what my demo app would look like with that:

library(shiny)
library(DT) # remotes::install_github("mikmart/DT@export-search")
library(dplyr)

# Adverse events data from a fake clinical trial
file <- file.path(tempdir(), "adae.xpt")
if (!file.exists(file)) {
  url <- "https://github.com/RConsortium/submissions-pilot1-to-fda/raw/main/m5/datasets/rconsortiumpilot1/analysis/adam/datasets/adae.xpt"
  curl::curl_download(url, file, quiet = FALSE)
}
adae <- haven::read_xpt(file)

tbl <- adae %>% 
  transmute(
    USUBJID = factor(USUBJID),
    AESEQ = as.integer(AESEQ),
    AEDECOD = factor(AEDECOD),
    AESOC = factor(AESOC)
  )

ui <- fluidPage(
  DTOutput("table")
)

server <- function(input, output, session) {
  output$table <- renderDT(tbl, filter = "top")

  filterable_sets <- eventReactive(input$table_search_columns, {
    # Get separately filtered indices
    fi <- Map(doColumnSearch, tbl, input$table_search_columns)

    # Find what rows others leave available
    ai <- lapply(seq_along(fi), function(j) Reduce(intersect, fi[-j]))

    # Get the corresponding data
    lapply(Map(`[`, tbl, ai), function(x) {
      if (is.factor(x)) droplevels(x) else x
    })
  })

  proxy <- dataTableProxy("table")
  observeEvent(filterable_sets(), {
    updateFilters(proxy, filterable_sets())
  })
}

shinyApp(ui, server)

It's not quite as efficient as with linkColumnFilters() above, because here we always redo all searches even when only one search string changes. But at least it shows a simple version can be implemented without too much hassle.

wholmes105 commented 2 years ago

@mikmart If you can already track when the user interacts with a filter and you can return what row indices of the original data get returned by that interaction, it sounds like that's everything the user needs to implement the feature; if they put whatever filter was interacted with last in a reactive value (input$table_search_columns_focus in the app below), they could monitor that with observeEvent() and call updateFilters() every time the value changes. As long as they feed the original data (orig_data in the app) and the updated data after the filters are applied (filtered_data), they shouldn't have any problems. The one thing I'm not 100% sure on is whether it would be safer to use the original data and call both replaceData and updateFilters for each active filter, which would be slower, or use the filtered data and just updateFilters as outlined below. In either case, the bulk of the code would be the same.

That said, I notice that input$table_search_columns_focus updates when the user begins interacting with one of the column filters; for this to work, it would need to update when the user stopped interacting with the filter, after they'd updated that filter's value. I would think that replacing the focus event with onfocusout would solve that, but w3 says it might not work as intended for several major browsers, including Chrome (it didn't work when I tested it in Chrome, Edge, or Firefox).

After watching the input tags' parent divs in the console, I think we might be able to hack it by watching for display: none to appear and disappear in their style arguments, but I can't help but wonder if there's an easier way.

library(shiny)
library(DT)
library(shinyjs)

ui = fluidPage(
  useShinyjs(), # required to use other shinyjs functions

  actionButton('reset_filter_order', 'Reset Filter Order'),
  DT::dataTableOutput('test_table'),
  verbatimTextOutput('test_text'),
  verbatimTextOutput('test_text2'),

  # This is borrowed from @mikmart to track the datatable
  # It monitors the filter row and returns the index of whichever filter was interacted with last
  tags$script(HTML(
    "
    // Set hooks to tell R which filter is focused
    function focusHook() {
      var $filterRow = $('#test_table thead tr:last');
      $filterRow.find('input[type=\"search\"]').each(function(i) {
        $(this).focus(function() {
          Shiny.setInputValue('table_search_columns_focus', i + 1);
        });
      });
    }

    // Don't know how to tell when DT is ready
    $(function() { setTimeout(focusHook, 500) });
    "
  ))
)

server = function(input, output) {
  # The pre-filtered dataset would need to be remembered to understand what 
  # the data would look like if the filters were applied sequentially rather than simultaneously
  orig_data = iris

  # Make a copy of the original data to respond to user filters
  filtered_data = reactiveVal(orig_data)

  # Since exporting the displayed row indices is not part of DT 0.21, this app just drops the first 10 rows
  # to demonstrate the ability to update the row indices
  displayed_row_indices = -(1:10)

  # A proxy object to update the datatable
  test_table_proxy = dataTableProxy('test_table')

  # Whenever the user interacts with a new filter, append that to a vector noting the order of filters to be applied
  filter_order = reactiveVal(c())
  observeEvent(input$table_search_columns_focus, {
    filter_order(unique(c(
      filter_order(),
      input$table_search_columns_focus
    )))

  })

  # A dummy table
  output$test_table = DT::renderDataTable({
    datatable(
      orig_data,
      filter = 'top'
    )
  })

  # Show what the user has selected
  output$test_text = renderText({
    input$table_search_columns_focus
  })

  output$test_text2 = renderText({
    filter_order()
  })

  # Reset filter_order()
  observeEvent(input$reset_filter_order, {
    filter_order(c())

    filtered_data(orig_data)
  })

  # When a column filter is updated, update the filters
  observeEvent(input$table_search_columns_focus, {

    filtered_data(filtered_data()[displayed_row_indices,])

    test_table_proxy %>% 
      updateFilters(filtered_data())
  })

}

shinyApp(ui, server)
mikmart commented 2 years ago

You can use .focusout() to find when a filter element loses focus. It's just that selectize inputs also move the focus within the filter cell, so you need an additional check to see if you're moving outside the td. Here's an updated version of the JS:

// Set hooks to tell R which filter is focused
function focusHook() {
  var $filters = $('#table thead tr:last td:has(input)');
  $filters.each(function(i) {
    $(this).focusin(function() {
      Shiny.setInputValue('table_search_columns_focus', i + 1);
    });
    $(this).focusout(function(event) {
      // Focus moved to an element outside the filter `td`
      if (!this.contains(event.relatedTarget)) {
        Shiny.setInputValue('table_search_columns_focus', null);
      }
    });
  });
}

// Don't know how to tell when DT is ready
$(function() { setTimeout(focusHook, 500) });

(Although as you'll note in this case I updated it to set the input to NULL when a filter loses focus, to match the semantics.)

However, I don't really see how this would be enough to solve the problem. For example if you set some filters, then go to edit one that you set before, there's no guarantee that you've at any point in the past had a filter state that could be used to get the values you'd need to update the options with.

mikmart commented 2 years ago

Just to summarize my understanding of this feature so far:

Desired behaviour: Limit filtering options based on currently applied filters.

Potential solution: updateFilters() with currently filtered data from input$tableId_rows_all.

Solution: Don't limit filter options based on their own values.

Solution: Re-perform search with current filter not included. If you do this for all filters, you don't need to know which is current.

AhmedKhaled945 commented 2 years ago

@mikmart @wholmes105 That is a lot of help, thanks everyone, well i have another bottleneck, i am not working on the main branch, i am working on https://github.com/LukasK13/DT, a branch that was developed to give dropdown edit capability on cell level, by some edits i added numerics and dates, so i have 2 separate branches that contains 2 functionalities that i really need, how do you think i can resolve this, is there a way to automate it, And in general, aren't you planning on adding select input capability in the edit types in datatable?

Thanks in advance.

mikmart commented 2 years ago

@AhmedKhaled945 you could make your own fork, add the repositories you want to use as remotes and git cherry-pick what you want to include. Unfortunately that's definitely a very manual process.

AhmedKhaled945 commented 2 years ago

Okay thank you, appreciate the help

mikmart commented 2 years ago

@yihui how would you feel about exporting the server side searching functions to help with this feature? If that's something you'd consider, I'd be happy to add documentation and some tests to my branch and submit a PR.

yihui commented 2 years ago

@mikmart I didn't read the full discussion above, but I'm okay with exporting whatever functions you need. Thanks!

mikmart commented 2 years ago

Okay, thanks! I'll put something together then.

DavidBlairs commented 1 year ago

Hello @mikmart. Your solution to this problem has worked nicely for myself but I'm finding that it conflicts with updateSearch. I think its because of circular reactives, causing the eventReactive to trigger twice after running updateSearch. You wouldn't by any chance have a suggestion on how to prevent this? Thanks in advance

mikmart commented 1 year ago

@DavidBlairs I'm not sure I follow. Would you be able to give a reproducible example?

DavidBlairs commented 1 year ago

@mikmart In the example below, I've added your code for updating filters and also added a button labelled "click" that sets a numeric filter. The problem is that when the filter is set, it disappears instantly. This is only the case for numeric and date columns.

library(shiny)
library(DT)

ui <- fluidPage(
  actionButton("mybttn", "click"),
  DTOutput("mytable")
)

server <- function(input, output, session) {
  data(iris) # Load the default iris dataset

  output$mytable <- DT::renderDT(
    datatable(
      iris[, c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width", "Species")],
      filter = "top"
    )
  )

  # update filter dropdowns
  filterable_sets <- eventReactive(input$mytable_search_columns, {
    # Get separate filtered indices
    fi <- Map(doColumnSearch, iris[, c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width", "Species")], input$mytable_search_columns)
    # Find what rows others leave available
    ai <- lapply(seq_along(fi), function(j) Reduce(intersect, fi[-j]))
    # Get the corresponding data
    lapply(
      lapply(
        Map(`[`, iris[, c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width", "Species")], ai),
        function(x) {
          if (is.factor(x)) droplevels(x) else x
        }
      ),
      sort
    )
  })

  # update the columns filters
  observeEvent(filterable_sets(), {
    updateFilters(proxy, filterable_sets())
  })

  observe({
    print(input$mytable_search_columns)
  })

  proxy <- DT::dataTableProxy("mytable")

  observeEvent(input$mybttn, {
    updateSearch(proxy, keywords = list(global = NULL, columns = c("", "", "", "3.0 ... 6.0", "")))
  })
}

shinyApp(ui, server)
mikmart commented 8 months ago

Thanks @DavidBlairs. Looks like the problem is caused by updateSearch() setting the string input value in the search field, but not actually updating the values on the slider. (You can observe that behaviour in your example app by commenting out the updateFilters().) Then when updateFilters() gets triggered by the updated search string, it restores the old values from the slider into the search field. I suspect a fix to updateSearch() would resolve the bad interaction with updateFilters(). But unfortunately I don't know how to approach that one at the moment.

wholmes105 commented 8 months ago

I think this might be a shiny logic problem interacting with a DT bug. I inserted a few extra print() messages into the example above to see what reactive events were being called: filterable_sets() gets called once when the app loads and once when a column is updated, but it gets called twice when the test button is clicked: the first time sets it to the intended value, and the second time sets it to the default value with all the column filters cleared.

The odd thing here is that input$mytable_search_columns is only recorded being changed once, and the second change is missed entirely. I set the priority of the calls to observeEvent() to see if that might help, but that didn't seem to have any effect. Changing filterable_sets() from a reactive value to a function you can call manually might be a workaround in this case, but it won't solve the underlying issue.

One last thing I noticed that might be relevant is that, whenever I filtered columns one at a time, the column I filtered always had a value of 150 (the full length of the table) in filterable_sets() while the other columns all had shorter values. Could this be causing some unintended behavior with updateSearch() or updateFilters()?

library(shiny)
library(DT)

ui <- fluidPage(
  actionButton("mybttn", "click"),
  DTOutput("mytable")
)

server <- function(input, output, session) {
  data(iris) # Load the default iris dataset

  output$mytable <- DT::renderDT(
    datatable(
      iris[, c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width", "Species")],
      filter = "top"
    )
  )

  # update filter dropdowns
  filterable_sets <- eventReactive(input$mytable_search_columns, {
    print('filterable_sets() updated')

    # Get separate filtered indices
    fi <- Map(doColumnSearch, iris[, c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width", "Species")], input$mytable_search_columns)
    # Find what rows others leave available
    ai <- lapply(seq_along(fi), function(j) Reduce(intersect, fi[-j]))
    # Get the corresponding data
    lapply(
      lapply(
        Map(`[`, iris[, c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width", "Species")], ai),
        function(x) {
          if (is.factor(x)) droplevels(x) else x
        }
      ),
      sort
    )
  })

  # update the columns filters
  observeEvent(filterable_sets(), {
    # # print('column filters updated:', length(filterable_sets()[[1]]), 'rows available')
    print(paste0('column filters updated: (', paste(sapply(filterable_sets(), length), collapse = ', '), ') rows available'))

    updateFilters(proxy, filterable_sets())
  }, priority = -1)

  observe({
    req(input$mytable_search_columns)
    print(input$mytable_search_columns)
  })

  proxy <- DT::dataTableProxy("mytable")

  observeEvent(input$mybttn, {
    print('input$mybttn clicked')
    updateSearch(proxy, keywords = list(global = NULL, columns = c("", "", "", "3.0 ... 6.0", "")))
  }, priority = 1)
}

shinyApp(ui, server)
mikmart commented 7 months ago

@DavidBlairs your issue is caused by #1110 and fixed by #1111, which is now merged.