Skip to content

Commit

Permalink
Implement MetricTable module (#290)
Browse files Browse the repository at this point in the history
* Implement MetricTable module

Wrap gsm::Report_MetricTable() into a Shiny module. This adds these features:
- Basic interactivity from gt (column resizers, highlight on hover, sorting, pagination).
- Pass in a row to highlight (to show the selection).
- Return the selected row when the user clicks.

The module is mostly generalized to work for any gt table, but full generalization will come in a future PR (when we use it for a second table).

Closes #187.

* More implementation of gtIO.

* Update WORDLIST.

* Use fixed gsm.

The DESCRIPTION should get switched back to @dev before this gets merged, but this will allow tests to pass.

* Use dev gsm.
  • Loading branch information
jonthegeek authored Oct 17, 2024
1 parent 73f8b0f commit 5f8219a
Show file tree
Hide file tree
Showing 31 changed files with 580 additions and 442 deletions.
3 changes: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ Suggests:
devtools,
here,
shinytest2,
stringr,
testthat (>= 3.0.0),
usethis,
withr
Remotes:
gsm=Gilead-BioStats/gsm@v2.1.0
gsm=Gilead-BioStats/gsm@dev
Config/testthat/edition: 3
Encoding: UTF-8
Language: en-US
Expand Down
36 changes: 0 additions & 36 deletions R/htmlDependency.R
Original file line number Diff line number Diff line change
Expand Up @@ -32,42 +32,6 @@ htmlDependency_Stylesheet <- function(
)
}

#' HighlightTableRow JavaScript
#'
#' Attach `highlightTableRow.js` to an app or other HTML exactly once.
#'
#' @returns An `html_dependency` object (see [htmltools::htmlDependency()]),
#' which is attached to the Shiny app exactly once, regardless how many times
#' it is added.
#' @keywords internal
htmlDependency_HighlightTableRow <- function() {
htmltools::htmlDependency(
name = "HighlightTableRow",
version = "1.0.0",
src = "js",
package = "gsm.app",
script = "highlightTableRow.js"
)
}

#' TableClick JavaScript
#'
#' Attach `tableClick.js` to an app or other HTML exactly once.
#'
#' @returns An `html_dependency` object (see [htmltools::htmlDependency()]),
#' which is attached to the Shiny app exactly once, regardless how many times
#' it is added.
#' @keywords internal
htmlDependency_TableClick <- function() {
htmltools::htmlDependency(
name = "TableClick",
version = "1.0.0",
src = "js",
package = "gsm.app",
script = "tableClick.js"
)
}

#' DetectCardClicks JavaScript
#'
#' Attach `detectCardClicks.js` to an app or other HTML exactly once.
Expand Down
38 changes: 7 additions & 31 deletions R/mod_MetricDetails_Server.R
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,6 @@ mod_MetricDetails_Server <- function(
}) %>%
bindCache(rctv_strMetricID())

rctv_dfResults_AnalysisOutput <- reactive({
rctv_dfResults_Latest() %>%
dplyr::arrange("GroupID") %>%
dplyr::select(
"GroupID", "Numerator", "Denominator", "Metric",
"Score", "Flag", "MetricID"
)
}) %>%
bindCache(rctv_strMetricID())

rctv_dfBounds_byMetricID <- reactive({
filter_byMetricID(dfBounds, rctv_strMetricID())
}) %>%
Expand All @@ -53,7 +43,12 @@ mod_MetricDetails_Server <- function(
rctv_strBarValueGroup <- reactive(NULL)
rctv_strBarScoreGroup <- reactive(NULL)
rctv_strTimeSeriesGroup <- reactive(NULL)
rctv_strAnalysisOutputGroup <- reactive(NULL)
rctv_strAnalysisOutputGroup <- mod_MetricTable_Server(
"analysis_output",
rctv_dfResults = rctv_dfResults_byMetricID,
dfGroups = dfGroups,
rctv_strSiteID = rctv_strSiteID
)

# Outputs ----
rctv_strSelectedGroupID <- reactive({
Expand Down Expand Up @@ -102,26 +97,7 @@ mod_MetricDetails_Server <- function(
outputOptions(output, "time_series", suspendWhenHidden = FALSE)
rctv_strTimeSeriesGroup()
},
"Analysis Output" = {
output$results <- renderUI({
gsm::Report_MetricTable(
rctv_dfResults_AnalysisOutput(),
dfGroups,
strGroupLevel = "Site"
) %>%
HTML()
})
observe({
shinyjs::runjs(
sprintf(
"highlightTableRow('analysis_output_table', '%s');",
rctv_strSiteID()
)
)
})
shinyjs::runjs("tableClick('analysis_output_table');")
rctv_strAnalysisOutputGroup()
}
"Analysis Output" = rctv_strAnalysisOutputGroup()
)
})

Expand Down
10 changes: 1 addition & 9 deletions R/mod_MetricDetails_UI.R
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,7 @@ mod_MetricDetails_UI <- function(id) {
),
bslib::nav_panel(
"Analysis Output",
div(
id = "analysis_output_table",
class = "card mb-3",
style = "margin-top: 4px;",
div(
class = "card-body",
uiOutput(ns("results"))
)
)
mod_MetricTable_UI(ns("analysis_output"))
)
)
}
72 changes: 72 additions & 0 deletions R/mod_MetricTable_Server.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#' Metric Table Module Server
#'
#' @inheritParams shared-params
#' @returns A [shiny::reactive()] with the id of the selected group.
#' @keywords internal
mod_MetricTable_Server <- function(
id,
rctv_dfResults,
dfGroups,
rctv_strSiteID
) {
moduleServer(id, function(input, output, session) {
output$table <- gt::render_gt({
req(rctv_dfResults())
tbl <- gsm::Report_MetricTable(
rctv_dfResults(),
dfGroups = dfGroups,
strGroupLevel = "Site"
)
# Hack to fix `Enrolled` sorting. See
# https://github.com/Gilead-BioStats/gsm/issues/1895
tbl$`_data`$Enrolled <- as.integer(tbl$`_data`$Enrolled)

tbl %>%
gt::opt_interactive(
use_resizers = TRUE,
use_highlight = TRUE,
use_compact_mode = TRUE,
use_text_wrapping = FALSE,
use_page_size_select = TRUE
) %>%
gt::tab_options(
table.background.color = "transparent",
column_labels.background.color = "transparent"
) %>%
gt::opt_row_striping()
})

observe({
session$sendCustomMessage(
"gtSetSelectID",
list(
id = session$ns("table"),
selectID = rctv_strSiteID()
)
)
})

# Reactive value to store the selected row
selected_row <- reactiveVal("None")

# Whenever the table changes, re-bind click events.
observeEvent(rctv_dfResults(), {
session$sendCustomMessage("gtBindClick", list(id = session$ns("table")))
})

# Observe table selections
observe({
req(rctv_dfResults())
if (length(input$table) > 0) {
selected_row(input$table)
} else {
selected_row("None")
}
})

# Return the selected row data
return(reactive({
selected_row()
}))
})
}
84 changes: 84 additions & 0 deletions R/mod_MetricTable_UI.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#' Metric Table Module UI
#'
#' @inheritParams shared-params
#' @returns A [bslib::card()] with an optional title and a
#' [gsm::Report_MetricTable()].
#' @keywords internal
mod_MetricTable_UI <- function(id) {
ns <- NS(id)
bslib::card(
id = id,
full_screen = TRUE,
class = "MetricTable",
gtIO(ns("table"))
)
}

#' gt Table Input and Output
#'
#' @param id
#'
#' @return An [htmltools::tagList()] containing the dependencies needed to use
#' gt as both an input and an output, and a [shiny::htmlOutput()] with class
#' "gtIO".
#' @keywords internal
gtIO <- function(id) {
htmltools::tagList(
htmlDependency_gtIO(),
shiny::htmlOutput(id, class = "gtIO")
)
}

#' gt Input-Output Dependencies
#'
#' Attach CSS and JavaScript necessary for gtIO to an app or other HTML exactly
#' once.
#'
#' @returns An [htmltools::tagList()] of `html_dependency` objects (see
#' [htmltools::htmlDependency()]), so that each will be attached to the Shiny
#' app exactly once, regardless how many times they are added.
#' @keywords internal
htmlDependency_gtIO <- function() {
htmltools::tagList(
htmlDependency_gtIOjs(),
htmlDependency_gtIOInput(),
htmlDependency_Stylesheet("gtIOStyle.css")
)
}

#' gt JavaScript
#'
#' Attach `gtIO.js` to an app or other HTML exactly once.
#'
#' @returns An `html_dependency` object (see [htmltools::htmlDependency()]),
#' which is attached to the Shiny app exactly once, regardless how many times
#' it is added.
#' @keywords internal
htmlDependency_gtIOjs <- function() {
htmltools::htmlDependency(
name = "gtIO",
version = "0.0.1",
src = "js",
package = "gsm.app",
script = "gtIO.js"
)
}


#' gt Input JavaScript
#'
#' Attach `gtIOInput.js` to an app or other HTML exactly once.
#'
#' @returns An `html_dependency` object (see [htmltools::htmlDependency()]),
#' which is attached to the Shiny app exactly once, regardless how many times
#' it is added.
#' @keywords internal
htmlDependency_gtIOInput <- function() {
htmltools::htmlDependency(
name = "gtShinyInput",
version = "0.0.1",
src = "inputs",
package = "gsm.app",
script = "gtIOInput.js"
)
}
2 changes: 0 additions & 2 deletions R/out_Sidebar.R
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ out_Sidebar <- function(
tagListSidebar,
shinyjs::useShinyjs(),
htmlDependency_Default_Stylesheet(),
htmlDependency_HighlightTableRow(),
htmlDependency_TableClick(),
out_StudyInformation(lStudy = lStudy),
out_Inputs(
chrMetrics = chrMetrics,
Expand Down
4 changes: 2 additions & 2 deletions inst/WORDLIST
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ Analytics
CMD
DetectCardClicks
GroupID
HighlightTableRow
KRI
MetricID
MetricIDs
Expand All @@ -13,7 +12,6 @@ Param
Rmd
ScatterPlot
ScatterPlotSet
TableClick
UI
chrWords
df
Expand All @@ -24,7 +22,9 @@ dfMetrics
dfResults
gsm
gsmApp
gtIO
io
lMetric
magrittr
namespaced
pkgdown
Expand Down
59 changes: 59 additions & 0 deletions inst/inputs/gtIOInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Shiny InputBinding for a gtIO Module.
*/
var gtIOBinding = new Shiny.InputBinding();

$.extend(gtIOBinding, {
/**
* Finds the gtIO element within the given scope.
*
* @param {HTMLElement} scope - The scope in which to search for the gtIO
* element.
* @returns {jQuery} The jQuery object containing the gtIO element.
*/
find: function(scope) {
return $(scope).find('.gtIO');
},
/**
* Gets the value of the selected select ID from the gtIO element.
*
* @param {HTMLElement} el - The element containing the gtIO.
* @returns {string|null} The selected select ID, or null if no select is
* selected.
*/
getValue: function(el) {
return $(el).data('selectID');
},
/**
* Sets the selected select ID for the gtIO element.
*
* @param {HTMLElement} el - The element containing the gtIO.
* @param {string} value - The new selected select ID.
*/
setValue: function(el, value) {
$(el).data('selectID', value);
},
/**
* Update Shiny when a gtIO element changes.
*
* @param {HTMLElement} el - The element containing the gtIO.
* @param {function} callback - The callback to trigger when the gtIO
* element changes.
*/
subscribe: function(el, callback) {
$(el).on('gtIO-value-changed', function(event) {
callback();
});
},
/**
* Unsubscribes from custom events for the gtIO element.
*
* @param {HTMLElement} el - The element containing the gtIO.
*/
unsubscribe: function(el) {
$(el).off('gtIO-value-changed');
}
});

// Register the input binding with Shiny
Shiny.inputBindings.register(gtIOBinding, 'gsm.app.gtIOBinding');
Loading

0 comments on commit 5f8219a

Please sign in to comment.