This vignette provides a deep dive into the design of the
teal.slice
package. It is intended for advanced developers.
If you want to merely use teal.slice
, the Filter panel
for developers vignette should satisfy all your needs.
Introduction
The teal.slice
package is composed of multiple classes,
whose primary purpose is to provide a shiny
module for
managing and displaying filters. The top class in the structure is
FilteredData
. Other modules in the app can interact with
FilteredData
to make filter calls and obtain filtered
data.
The FilteredData
object contains one or more
FilteredDataset
objects. While FilteredData
manages the entire filter panel, each FilteredDataset
is
responsible for filtering a single data set.
FilteredDataset
contains one or more
FilterStates
objects, each corresponding to one data
structure like a data.frame
, a
SummarizedExperiment
, or a
MultiAssayExperiment
. FilterStates
holds a
collection of FilterState
objects, each representing a
single filter condition applied on one variable.
FilterStates
can also hold FilterStateExpr
objects. This is a variation of FilterStates
that focuses
on the filter expression regardless of the underlying data. The
expression can refer to one or several variables.
FilteredData
, FilteredDataset
,
FilterStates
, and
FilterState
/FilterStateExpr
are all
R6
classes and objects of those classes form a hierarchy
but there is no inheritance between them.
Each FilterState
/FilterStateExpr
object
contains one teal_slice
object. teal_slice
stores all information necessary to define a filter. It is not an
R6
class, it does not have any innate methods or behaviors
and does not produce any shiny
modules. Its sole purpose is
to store filter information.
Initialization
As part of the teal
workflow, FilteredData
,
FilteredDataset
, and FilterStates
are created
instantly when the data is loaded and remain unchanged. One
FilteredData
object is initialized on a list of objects
(e.g. data.frame
, MAE
). A
FilteredDataset
is initialized for each data set. One or
more FilterStates
are initialized, depending on the type of
data set.
On the other hand, a FilterState
is initialized each
time a new filter is added. The values of the FilterState
can be changed, and it can also be removed and added again.
The key mechanism in the new filter panel is in
FilterStates
class. One can think of
FilterStates
as equivalent to a single, possibly compound,
subset call made on one data structure. While FilterState
represents a logical predicate on one vector, e.g
SEX == "F"
, FilterStates
will compose all
predicates of its member FilterState
objects into a call to
a subsetting function,
e.g. data <- subset(data, SEX == "F" & RACE == "CAUCASIAN")
.
In the case of a data.frame
, a single
dplyr::filter
call is sufficient to subset the whole data
set. A MultiAssayExperiment
on the other hand contains
patient data in the @colData
slot and multiple experiments
in the @ExperimentList
slot, and all of these objects have
to be filtered by separate subsetting calls. Therefore, subclasses of
FilterStates
exist to handle different kinds of data
structures and they use different subsetting functions.
Class Description
This section provides a detailed description of all classes that make up the filter panel structure.
FilteredData
FilteredData
is an object responsible for managing the
filter panel. It sits on top of the class structure and handles the
shiny
modules of the subclasses.
FilteredData
provides several API methods that can be
used to access reproducible subsetting calls and the resulting filtered
data. It also allows external modules to manage filter states through
functions such as get_filter_state
,
set_filter_state
, remove_filter_state
, and
clear_filter_state
.
FilteredDataset
FilteredDataset
is a class that keeps unfiltered data
and returns filtered data based on the filter call derived from the
contained FilterStates
. FilteredDataset
class
objects are initialized by FilteredData
, one for each data
set. FilteredDataset
contains a single data object and
one-to-many FilterStates
depending on the type of that
object. FilteredDataset
stores data set attributes, joins
keys to other data sets, and also combines and executes the subsetting
calls taken from FilterStates
.
The following FilteredDataset
child classes are
currently implemented:
-
DataframeFilteredDataset
fordata.frame
. -
MAEFilteredDataset
forMultiAssayExperiment
. -
DefaultFilteredDataset
for all remaining (unsupported) classes - this subclass is different to the others in that it provides no filtering, its only purpose is to hold and return an object for which filtering is not supported.
FilterStates
When the app starts, FilteredDataset
initializes one or
more FilterStates
objects, one for each component of the
underlying data set. Every FilterStates
object is
responsible for making one subset call. For example, a
MAEFilteredDataset
will create one
FilterStates
for its colData
and one
FilterStates
for each of its experiments. Every
FilterStates
will return a separate subsetting call, which
will be used to subset the entire MultiAssayExperiment
.
The following FilteredStates
child classes are currently
implemented:
-
DFFilterStates
fordata.frame
; usesdplyr::filter
to filter on columns. -
MAEFilterStates
forMultiAssayExperiment
; usesMultiAssayExperiment::subsetByColData
to filter on columns of theDataFrame
in the@colData
slot. -
SEFilterStates
forSummarizedExperiment
; uses thesubset
method forSummarizedExperiment
to filter on columns ofDataFrames
in the@colData
and@rowData
slots. -
MatrixFilterStates
formatrix
; uses the[
operator to filter on columns.
FilterStates
serves two shiny
-related
purposes:
ui/srv_add
allows adding aFilterState
for a selected variable. The variables included in the module are the filterable column names of the provided data set. Selecting a variable adds aFilterState
to thereactiveVal
stored in theprivate$state_list
private field. The subtype of the createdFilterState
is automatically determined based on the class of the selected column.ui/srv_active
displays cards of the currently existingFilterState
objects. EveryFilterState
object serves a remove button andFilterStates
reacts to clicking that button by removing the respectiveFilterState
fromprivate$state_list
and destroying its observers.ui/srv_active
also contains a remove button that removes allFilterState
objects within thisFilterStates
.
FilterState
This class controls a single filter card and returns a condition call
that depends on the selection state. A FilterState
is
initialized each time FilterStates
adds a new filter.
Different classes of data require different handling of choices so
several FilterState
subclasses exist and each of them
presents a different user interface and generates a different subsetting
call. A FilterState
is created as follows:
-
FilterStates
createsteal_slice
withdataname
based on the parent data set andvarname
based on the selected variable - the
teal_slice
is wrapped inteal_slices
and passed toFilterStates$set_filter_state
-
FilterStates$set_filter_state_impl
is called -
FilterStates$set_filter_state_impl
callsinit_filter_state
, passing the appropriate variable asx
-
init_filter_states
is a generic function that dispatchesx
,teal_slice
, and other arguments to the respective child class constructor:
-
LogicalFilterState
forlogical
variables. Presents a checkbox group. Call example:!variable
. -
RangeFilterState
fornumeric
variables. Presents an interactive plot as well as two numeric inputs. Selection is always two values that represent inclusive interval limits. Call example:variable >= selection[1] & variable <= selection[2]
-
DateFilterState
forDate
variables. Presents two date inputs. Selection is two values that determine inclusive interval limits. Call example:variable >= selection[1] & variable <= selection[2]
. -
DatetimeFilterState
forPOSIXct
andPOSIXlt
variables. Similar toDateFilterState
. -
ChoicesFilterState
forcharacter
andfactor
values. Additionally, all classes will be passed toChoicesFilterState
if their number of unique values is lower than that ingetOption("teal.threshold_slider_vs_checkboxgroup")
. Presents either a checkbox group or a drop-down menu. Depending on settings, allows either only one or any number of values to be selected. Call examples:variable == selection
,variable %in% selection
. -
EmptyFilterState
for variables that contain only missing values. Does not return calls.
All child classes handle missing values, and
RangedFilterState
also handles infinite values.
The FilterState
constructor also takes the
extract_type
argument, which determines how the call is
constructed extract_type
can be unspecified,
"matrix"
or "list"
and its value corresponds
to the type of the variable prefix in the returned condition call. If
extract_type
is "list"
, the variable in the
condition call is <dataname>$<varname>
, while
for "matrix"
it would be
<dataname>[, "<varname>"]
.
FilterStateExpr
Similarly to FilterState
, FilterStateExpr
controls a single filter card and returns logical expression. However,
while FilterState
generates the call based on the selection
state, in FilterStateExpr
the call must be specified
manually and it must be a valid R expression.
teal_slice
teal_slice
is a simple object for storing filter
information. It can be thought of as a quantum of information. A
teal_slice
object is passed directly to
FilterState$initialize
and is stored inside of the
FilterState
to keep the current state of the filter.
Technically, all values used to generate a call are in
teal_slice
. FilterState
can be described as a
wrapper around teal_slice
that provides additional methods
to handle filter state. It also contains the actual data (a single
column).
While teal_slice
is not an R6
object and
does not encode any behaviors, it is implemented around a
reactiveValues
object to allow shared state in advanced
teal
applications.
See ?teal_slice
for a detailed explanation.
Making a reproducible filter call
Overview
The diagram above presents the filter panel classes and their
responsibilities when composing filter calls. Code is generated by
nested execution of get_call
methods.
FilteredData$get_call
calls
FilteredDataset$get_call
, which calls
FilterStates$get_call
which in turn calls
FilterState$get_call
.
FilterState$get_call()
returns a single subsetting
expression (logical predicate).
FilterStates$get_call()
returns a single complete
subsetting call by extracting expressions from all
FilterState
objects and combining them with the
&
operator.
FilteredDataset$get_call()
returns a list of calls
extracted from all FilterStates
objects.
FilteredData$get_call(<dataname>)
returns a list
of calls extracted from the specified FilteredDataset
.
Example
Calling datasets$get_call(<dataname>)
in
teal
modules executes a chain of calls in all filter panel
classes. Consider the following scenario:
FilteredData
contains three data sets:ADSL
,ADTTE
,MAE
, each stored in its ownFiteredDataset
objectADSL
is adata.frame
so it can be filtered with a singledplyr::filter
call. This data set is stored inDataframeFilteredDataset
, which holds a singleFilterStates
object.FilterStates
constructs adplyr::filter
call based on theFilterState
objects present in itsprivate$state_list
.When
FilterStates$set_filter_state
adds a newteal_slice
, aFilterState
is created and added toprivate$state_list
inFilterStates
.FilterStates
gathers logical expressions from all of itsFilterState
objects and composes them into adplyr::filter(ADSL, ...)
call.Two new filters have been added:
SEX
andAGE
. This causes initialization of appropriateFilterState
subclasses:ChoiceFilterState
andRangeFilterState
. EachFilterState
produces a subsetting expression:SEX == "F"
andAGE >= 20 & AGE <= 60
. The expressions are combined with&
and passed todplyr::filter
, producingADSL <- dplyr::filter(ADSL, SEX == "F" & AGE >= 20 & AGE <= 60)
.DataframeFilteredDataset
puts this call in a list and returns it toFilteredData
.ADTTE
is also adata.frame
so theFilteredDataset
that stores it works much the same as the one forADSL
. The one difference is that thedplyr::filter
call forADTTE
is followed by adplyr::inner_join
call to merge the filtering result with the parent data set (ADSL
) so that key columns remain consistent. Note that this is only done whenjoin_keys
is specified - otherwiseADTTE
would be treated as a separate data set and filtered independently.The
MAE
data set is aMultiAssayExperiment
, which contains multiple sub-objects which can be filtered on. One of them isADSL
-like patient data, stored as aDataFrame
inMAE@colData
, and others are experiments, typicallySummarizedExperiment
objects, stored inMAE@ExperimentList
, which can be extracted usingMAE[["experiment name"]]
. Therefore,MAEFilteredDataset
has multipleFilterStates
objects: one for subject data and one for each experiment.A
MAEFilterStates
object is initialized for subject data and for this objectMultiAssayExperiment::subsetByColData
function is applied.MultiAssayExperiment::subsetByColData
has two arguments:x
(data) andy
(conditions). Since all filter expressions are passed to one argument,MAEFilterStates
only has onestate_list
, just likeDFFilterStates
. Adding new filters triggers the same actions as described in (4).A
SummarizedExperiment
is more complicated as observations can be filtered based on itsrowData
andcolData
slots, both containDataFrame
s. Subsetting is done by a dedicated S4subset
method, which takes two key arguments:subset
takes logical expressions that will be applied torowData
, andselect
takes logical expressions that will be applied tocolData
.teal_slice
objects that specify filters in aSummarizedExperiment
must contain anarg
element, either"subset"
or"select"
, to reflect which slot of the experiment they refer to. TheSEFilterStates
gathers logical expressions of its memberFilterState
objects, groups them by thearg
element, and builds a call sosubset
with two combined logical expressions passed to thesubset
andselect
arguments.
Filter panel modules
The FilteredData
object uses the
filter_panel_ui
and filter_panel_srv
methods
to put up a filter panel that can be used in any application. In
teal
applications it will be placed on the right-hand side.
The filter panel module does not return anything. Data, subsetting
calls, and filter states are accessible by specific public methods:
get_data
, get_call
, and
get_filter_state
, respectively. Typically, the filter panel
consists of three modules:
-
ui/srv_overview
displays observation counts filtered vs unfiltered data -
ui/srv_active
displays active filter cards, which are created byFilterState
objects -
ui/srv_add
allows for adding filters
FilteredData
does not handle data sets directly because
they may be of different types, rather, it calls respective methods in
lower-level classes.
When a new filter is added using the “Add Filter Variable” module in
FilterStates
, a new FilterState
object is
initialized and added to private$state_list
.
FilterStates$srv_active
observes
private$state_list
(which is a reactiveVal
)
and when the state of the list changes (a filter is added or removed),
it calls FilterState$server
and uses renderUI
to display FilterState$ui
.