Skip to content
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

slider snaps from edge in RangedFilterState #189

Merged
merged 35 commits into from
Feb 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f4e7681
tweak docs and format
Feb 3, 2023
93e47d4
disable validation in set_selected method
Feb 3, 2023
a9a5413
prettify initial selection
Feb 3, 2023
bb2177f
move finding actual limits section to format method
Feb 3, 2023
41963e1
remove redundant validation
Feb 3, 2023
ce72bb3
modify ui_inputs display
Feb 3, 2023
2fa8619
add range validation to format method
Feb 3, 2023
7479efc
remove commented code
Feb 7, 2023
69c6151
update unit tests
Feb 7, 2023
aaf73c3
modify init method of RangeFiltrerState"
Feb 8, 2023
6fae8a7
restore set_selected method in parent class
Feb 8, 2023
f4fc325
waive selection validation in RangedFilterState
Feb 8, 2023
a0da45f
modify initial slider setup
Feb 8, 2023
6ea198f
minor code tweaks
Feb 8, 2023
b486897
adjust unit tests
Feb 8, 2023
fa107da
rearrange unit tests
Feb 8, 2023
f96f03a
Merge branch 'main' into 184_slider_moves@main
mhallal1 Feb 9, 2023
9e6fc27
merge main branch
Feb 9, 2023
362dca5
clean up
Feb 9, 2023
028fb94
adjust slider to programmatic selection
Feb 9, 2023
8a16200
initiate slider with private() not
Feb 9, 2023
c7d30d7
add function contain_interval with docs and unit tests
Feb 10, 2023
9493e91
use contain_interval for adjusting selection
Feb 10, 2023
7c615b9
modify conditions in observers in RangeFilterState
Feb 10, 2023
5bccb2b
adjust unit tests for filter api
Feb 10, 2023
878a98b
amend documentation
Feb 10, 2023
848fdc2
amend NEWS
Feb 10, 2023
470f0b6
remove leftover assertion
Feb 10, 2023
27910b1
round slider properties in RangeFilterState
Feb 10, 2023
c6621eb
Merge 27910b1d8bf8aad1de07ee692573b32b0e444ef3 into 4abdf2b97e252998b…
chlebowa Feb 10, 2023
efec0de
[skip actions] Restyle files
github-actions[bot] Feb 10, 2023
168fe4e
linter
Feb 10, 2023
e71af10
spelling
Feb 10, 2023
fc8ae9f
patch check
Feb 10, 2023
02ac57a
improve docs of internals
Feb 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
### Bug fixes

* Fixed an error where the `RangeFilterState` produced an error when using `bootstrap 4`.
* Fixed a bug that caused the range slider to omit values selected programmatically through the filter API.

### Miscellaneous

Expand Down
49 changes: 47 additions & 2 deletions R/FilterState-utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ init_filter_state.POSIXlt <- function(x,
#' @keywords internal
#'
#' @examples
#' \dontrun{
#' \donttest{
#' check_in_range(c(3, 1), c(1, 3))
#' check_in_range(c(0, 3), c(1, 3))
#' check_in_range(
Expand Down Expand Up @@ -287,13 +287,15 @@ check_in_range <- function(subinterval, range, pre_msg = "") {
#' @keywords internal
#'
#' @examples
#' check_in_subset <- teal.slice:::check_in_subset
#' \donttest{
#' check_in_subset <- check_in_subset
#' check_in_subset(c("a", "b"), c("a", "b", "c"))
#' \dontrun{
#' check_in_subset(c("a", "b"), c("b", "c"), pre_msg = "Error: ")
#' # truncated because too long
#' check_in_subset("a", LETTERS, pre_msg = "Error: ")
#' }
#' }
check_in_subset <- function(subset, choices, pre_msg = "") {
checkmate::assert_string(pre_msg)

Expand All @@ -310,3 +312,46 @@ check_in_subset <- function(subset, choices, pre_msg = "") {
}
return(invisible(NULL))
}

#' Find containing limits for interval.
#'
#' Given an interval and a numeric vector,
#' find the smallest interval within the numeric vector that contains the interval.
#'
#' This is a helper function for `RangeFilterState` that modifies slider selection
#' so that the _subsetting call_ includes the value specified by the filter API call.
#'
#' Regardless of the underlying numeric data, the slider always presents 100 steps.
#' The ticks on the slider do not represent actual observations but rather borders between virtual bins.
#' Since the value selected on the slider is passed to `private$selected` and that in turn
#' updates the slider selection, programmatic selection of arbitrary values may inadvertently shift
#' the selection to the closest tick, thereby dropping the actual value set (if it exists in the data).
#'
#' This function purposely shifts the selection to the closest ticks whose values form an interval
#' that will contain the interval defined by the filter API call.
#'
#' @param x `numeric(2)` interval to contain
#' @param range `numeric(>=2)` vector of values to contain `x` in
#'
#' @return Numeric vector of length 2 that lies within `range`.
#'
#' @keywords internal
#'
#' @examples
#' \donttest{
#' ticks <- 1:10
#' values1 <- c(3, 5)
#' contain_interval(values1, ticks)
#' values2 <- c(3.1, 5.7)
#' contain_interval(values2, ticks)
#' values3 <- c(0, 20)
#' contain_interval(values3, ticks)
#'}
contain_interval <- function(x, range) {
checkmate::assert_numeric(x, len = 2L, any.missing = FALSE, sorted = TRUE)
checkmate::assert_numeric(range, min.len = 2L, any.missing = FALSE, sorted = TRUE)

x[1] <- Find(function(i) i <= x[1], range, nomatch = min(range), right = TRUE)
x[2] <- Find(function(i) i >= x[2], range, nomatch = max(range))
x
}
58 changes: 30 additions & 28 deletions R/FilterState.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
#' @docType class
#'
#'
#' @title Abstract class to encapsulate filter states
#' @title FilterState Abstract Class
#'
#' @description Abstract class to encapsulate filter states
#'
#' @details
#' This class is responsible for managing single filter item within
Expand Down Expand Up @@ -281,7 +283,7 @@ FilterState <- R6::R6Class( # nolint
#' @param value (`vector`)\cr
#' value(s) that come from filter selection; values are set in the
#' module server after a selection is made in the app interface;
#' values are stored in `private$selected`n which is reactive;
#' values are stored in `private$selected` which is reactive;
#' value types have to be the same as `private$choices`
#'
#' @return NULL invisibly
Expand Down Expand Up @@ -420,11 +422,12 @@ FilterState <- R6::R6Class( # nolint
varlabel = character(0),
extract_type = logical(0),

#' description
#' Adds `is.na(varname)` before existing condition calls if `keep_na` is selected.
#' Otherwise, if missings are found in the variable `!is.na` will be added
#' only if `private$na_rm = TRUE`
#' return (`call`)
# private methods ----
# @description
# Adds `is.na(varname)` before existing condition calls if `keep_na` is selected.
# Otherwise, if missings are found in the variable `!is.na` will be added
# only if `private$na_rm = TRUE`
# @return (`call`)
add_keep_na_call = function(filter_call) {
if (isTRUE(self$get_keep_na())) {
call(
Expand All @@ -443,14 +446,14 @@ FilterState <- R6::R6Class( # nolint
}
},

#' description
#' Prefixed (or not) variable
#'
#' Return variable name needed to condition call.
#' If `isTRUE(private$use_dataset)` variable is prefixed by
#' dataname to be evaluated as extracted object, for example
#' `data$var`
#' return (`name` or `call`)
# @description
# Prefixed (or not) variable
#
# Return variable name needed to condition call.
# If `isTRUE(private$use_dataset)` variable is prefixed by
# dataname to be evaluated as extracted object, for example
# `data$var`
# @return (`name` or `call`)
get_varname_prefixed = function() {
if (isTRUE(private$extract_type == "list")) {
call_extract_list(private$dataname, private$varname)
Expand All @@ -464,20 +467,19 @@ FilterState <- R6::R6Class( # nolint
}
},

#' Sets `keep_na` field according to observed `input$keep_na`
#' If `keep_na = TRUE`, `is.na(varname)` is added to the returned call.
#' Otherwise returned call excludes `NA` when executed.
# Sets `keep_na` field according to observed `input$keep_na`
# If `keep_na = TRUE` `is.na(varname)` is added to the returned call.
# Otherwise returned call excludes `NA` when executed.
observe_keep_na = function(input) {

},

#' Set choices
#'
#' Set choices is supposed to be executed once in the constructor
#' to define set/range which selection is made from.
#' parameter choices (`vector`)\cr
#' class of the vector depends on the `FilterState` class.
#' return a `NULL`
# @description
# Set choices is supposed to be executed once in the constructor
# to define set/range which selection is made from.
# parameter choices (`vector`)\cr
# class of the vector depends on the `FilterState` class.
# @return `NULL`
set_choices = function(choices) {
private$choices <- choices
invisible(NULL)
Expand Down Expand Up @@ -514,11 +516,11 @@ FilterState <- R6::R6Class( # nolint
},

# shiny modules -----
#' module with inputs
# module with inputs
ui_inputs = function(id) {
stop("abstract class")
},
#' module with inputs
# module with inputs
server_inputs = function(id) {
stop("abstract class")
},
Expand Down Expand Up @@ -566,7 +568,7 @@ FilterState <- R6::R6Class( # nolint
}
)
private$observers$keep_na <- observeEvent(
ignoreNULL = FALSE, # ignoreNULL: we don't want to ignore NULL when nothing is selected in the `selectInput`,
ignoreNULL = FALSE, # ignoreNULL: we don't want to ignore NULL when nothing is selected in the `selectInput`
ignoreInit = TRUE, # ignoreInit: should not matter because we set the UI with the desired initial state
eventExpr = input$value,
handlerExpr = {
Expand Down
21 changes: 14 additions & 7 deletions R/FilterStateChoices.R
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
ChoicesFilterState <- R6::R6Class( # nolint
"ChoicesFilterState",
inherit = FilterState,

# public methods ----

public = list(

#' @description
Expand Down Expand Up @@ -134,8 +137,13 @@ ChoicesFilterState <- R6::R6Class( # nolint
super$set_selected(value)
}
),

# private members ----

private = list(
histogram_data = data.frame(),

# private methods ----
validate_selection = function(value) {
if (!is.character(value)) {
stop(
Expand Down Expand Up @@ -174,6 +182,8 @@ ChoicesFilterState <- R6::R6Class( # nolint
values[in_choices_mask]
},

# shiny modules ----

# @description
# UI Module for `ChoicesFilterState`.
# This UI element contains available choices selection and
Expand All @@ -185,21 +195,18 @@ ChoicesFilterState <- R6::R6Class( # nolint
div(
if (length(private$choices) <= getOption("teal.threshold_slider_vs_checkboxgroup")) {
l_counts <- as.numeric(names(private$choices))
is_na_l_counts <- is.na(l_counts)
if (any(is_na_l_counts)) l_counts[is_na_l_counts] <- 0
l_counts[is.na(l_counts)] <- 0
l_freqs <- l_counts / sum(l_counts)
labels <- lapply(seq_along(private$choices), function(i) {
l_count <- l_counts[i]
l_freq <- l_count / sum(l_counts)
if (is.na(l_freq) || is.nan(l_freq)) l_freq <- 0
div(
class = "choices_state_label",
style = sprintf("width:%s%%", l_freq * 100),
style = sprintf("width:%s%%", l_freqs[i] * 100),
span(
class = "choices_state_label_text",
sprintf(
"%s (%s)",
private$choices[i],
l_count
l_counts[i]
)
)
)
Expand Down
13 changes: 11 additions & 2 deletions R/FilterStateDate.R
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
DateFilterState <- R6::R6Class( # nolint
"DateFilterState",
inherit = FilterState,

# public methods ----

public = list(

#' @description
Expand Down Expand Up @@ -63,12 +66,13 @@ DateFilterState <- R6::R6Class( # nolint
format = function(indent = 0) {
checkmate::assert_number(indent, finite = TRUE, lower = 0)

vals <- self$get_selected()
sprintf(
"%sFiltering on: %s\n%1$s Selected range: %s - %s\n%1$s Include missing values: %s",
format("", width = indent),
private$varname,
format(self$get_selected()[1], nsmall = 3),
format(self$get_selected()[2], nsmall = 3),
format(vals[1], nsmall = 3),
format(vals[2], nsmall = 3),
format(self$get_keep_na())
)
},
Expand Down Expand Up @@ -125,6 +129,9 @@ DateFilterState <- R6::R6Class( # nolint
super$set_selected(value)
}
),

# private methods ----

private = list(
validate_selection = function(value) {
if (!is(value, "Date")) {
Expand Down Expand Up @@ -173,6 +180,8 @@ DateFilterState <- R6::R6Class( # nolint
values
},

# shiny modules ----

# @description
# UI Module for `DateFilterState`.
# This UI element contains two date selections for `min` and `max`
Expand Down
18 changes: 16 additions & 2 deletions R/FilterStateDatettime.R
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
DatetimeFilterState <- R6::R6Class( # nolint
"DatetimeFilterState",
inherit = FilterState,

# public methods ----

public = list(

#' @description
Expand Down Expand Up @@ -75,12 +78,15 @@ DatetimeFilterState <- R6::R6Class( # nolint
#'
format = function(indent = 0) {
checkmate::assert_number(indent, finite = TRUE, lower = 0)


vals <- self$get_selected()
sprintf(
"%sFiltering on: %s\n%1$s Selected range: %s - %s\n%1$s Include missing values: %s",
format("", width = indent),
private$varname,
format(self$get_selected(), nsmall = 3)[1],
format(self$get_selected(), nsmall = 3)[2],
format(vals[1], nsmall = 3),
format(vals[2], nsmall = 3),
format(self$get_keep_na())
)
},
Expand Down Expand Up @@ -137,8 +143,14 @@ DatetimeFilterState <- R6::R6Class( # nolint
super$set_selected(value)
}
),

# private fields ----

private = list(
timezone = Sys.timezone(),

# private methods ----

validate_selection = function(value) {
if (!(is(value, "POSIXct") || is(value, "POSIXlt"))) {
stop(
Expand Down Expand Up @@ -187,6 +199,8 @@ DatetimeFilterState <- R6::R6Class( # nolint
values
},

# shiny modules ----

# @description
# UI Module for `DatetimeFilterState`.
# This UI element contains two date-time selections for `min` and `max`
Expand Down
Loading