-
Notifications
You must be signed in to change notification settings - Fork 180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Column filters not updating #587
Comments
Any progress on this front? Facing the same issue. |
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 |
@shrektan Hello, is this still the case? |
@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: DT/inst/examples/DT-updateFilters/app.R Line 46 in ddcc9e9
You can update the filters with the filtered data, which you can obtain from That said, the SearchPanes extension may also work, but it would only work for the client-side processing mode as @shrektan mentioned above. |
@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) |
@AhmedKhaled945 In the new |
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? |
It could be; remember that any For example, if I want to update a datatable with the following data
there's nothing stopping me from using this data for the filters instead
Doing this would include options in the filters that do not appear in the datatable. Alternatively,
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. |
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 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. |
That's correct; as I mentioned earlier, the column filters are only active if they are not empty; take the following as an example:
If we updated the filter for
Perhaps I just misunderstood you, but you can select multiple levels in a factor filter sequentially. Using
|
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 |
Ah, ok. In that case, I'd recommend using the functionality shiny already has built in for reactive delays. |
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: 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 |
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. 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:
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: Lines 586 to 587 in a121b0b
The parts relevant to the search function would need to be extracted and parameterized in an accessible manner. |
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 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") |
On second thought, maybe the "all-in-one" 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 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 |
@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 ( That said, I notice that After watching the input tags' parent divs in the console, I think we might be able to hack it by watching for
|
You can use // 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 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. |
Just to summarize my understanding of this feature so far: Desired behaviour: Limit filtering options based on currently applied filters. Potential solution:
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.
|
@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 Thanks in advance. |
@AhmedKhaled945 you could make your own fork, add the repositories you want to use as remotes and |
Okay thank you, appreciate the help |
@mikmart I didn't read the full discussion above, but I'm okay with exporting whatever functions you need. Thanks! |
Okay, thanks! I'll put something together then. |
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 |
@DavidBlairs I'm not sure I follow. Would you be able to give a reproducible example? |
@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) |
Thanks @DavidBlairs. Looks like the problem is caused by |
I think this might be a The odd thing here is that 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
|
@DavidBlairs your issue is caused by #1110 and fixed by #1111, which is now merged. |
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!
The text was updated successfully, but these errors were encountered: