diff --git a/DESCRIPTION b/DESCRIPTION index 34617d9..0971222 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -31,7 +31,6 @@ Imports: shinybrowser, tibble, markdown, - DT, DatabaseConnector, FeatureExtraction, SqlRender, diff --git a/NAMESPACE b/NAMESPACE index 17b5179..6a07810 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -42,12 +42,6 @@ export(mod_resultsVisualisation_ui) export(readAndParseYalm) export(run_app) importFrom(DBI,dbGetQuery) -importFrom(DT,DTOutput) -importFrom(DT,dataTableOutput) -importFrom(DT,datatable) -importFrom(DT,formatSignif) -importFrom(DT,formatStyle) -importFrom(DT,renderDataTable) importFrom(DatabaseConnector,connect) importFrom(DatabaseConnector,disconnect) importFrom(DatabaseConnector,querySql) diff --git a/R/mod_resultsVisualisation_CodeWAS.R b/R/mod_resultsVisualisation_CodeWAS.R index 1e2d839..630c6c8 100644 --- a/R/mod_resultsVisualisation_CodeWAS.R +++ b/R/mod_resultsVisualisation_CodeWAS.R @@ -8,13 +8,31 @@ #' @importFrom shiny NS tagList h4 div uiOutput tabsetPanel tabPanel downloadButton #' @importFrom htmltools hr #' @importFrom ggiraph girafeOutput -#' @importFrom DT dataTableOutput #' #' @export #' mod_resultsVisualisation_CodeWAS_ui <- function(id) { ns <- shiny::NS(id) + # this must be in sync with the columns in the reactable table + tableColumns <- c( + "Analysis Name" = "analysisName", + "Concept Code" = "conceptCode", + "Vocabulary" = "vocabularyId", + "Domain" = "domainId", + "N cases" = "nCasesYes", + "N ctrls" = "nControlsYes", + "Ratio|Mean cases" = "meanCases", + "SD cases" = "sdCases", + "Ratio|Mean ctrls" = "meanControls", + "SD ctrls" = "sdControls", + "OR" = "oddsRatio", + "mlogp" = "mlogp", + "Beta" = "beta", + "Model" = "modelType", + "Notes" = "runNotes" + ) + shiny::fluidPage( title = "CodeWAS Results", shinyFeedback::useShinyFeedback(), @@ -68,7 +86,29 @@ mod_resultsVisualisation_CodeWAS_ui <- function(id) { shiny::tabPanel( "Table", shiny::div( - style = "margin-top: 10px; margin-bottom: 10px;", + fluidRow( + column(10, + tags$div(style = "display: flex; align-items: center; gap: 15px;", + tags$label("Sort by:", style = "width: 50px; margin-bottom: 0;"), + tags$div(style = "margin-top: 15px;", + selectInput(ns("sortFirst"), label = NULL, choices = tableColumns, width = "150px", selected = "oddsRatio"), + ), + tags$div(style = "width: 50px;", + checkboxInput(ns("sortFirstDesc"), "descending", value = TRUE) + ), + tags$label("", style = "width: 20px; margin-bottom: 0; margin-left: 10px;"), + tags$div(style = "margin-top: 15px;", + selectInput(ns("sortSecond"), label = NULL, choices = tableColumns, width = "150px", selected = "mlogp"), + ), + tags$div(style = "width: 50px;", + checkboxInput(ns("sortSecondDesc"), "descending", value = TRUE) + ) + ) + ), + ) # fluidRow + ), # div + shiny::div( + style = "margin-top: 0px; margin-bottom: 10px;", shinycssloaders::withSpinner( reactable::reactableOutput(ns("codeWAStable")), proxy.height = "400px" @@ -79,10 +119,10 @@ mod_resultsVisualisation_CodeWAS_ui <- function(id) { shiny::downloadButton(ns("downloadCodeWASFiltered"), "Download filtered", icon = shiny::icon("download")), shiny::downloadButton(ns("downloadCodeWASAll"), "Download all", icon = shiny::icon("download")) ) - ) - ) - ) - ) + )# tabPanel + ) # tabsetPanel + ) # tagList + ) # fluidPage } @@ -101,7 +141,6 @@ mod_resultsVisualisation_CodeWAS_ui <- function(id) { #' @importFrom tidyr separate #' @importFrom stringr str_remove str_trunc str_wrap #' @importFrom purrr map2_chr -#' @importFrom DT dataTableOutput renderDataTable datatable formatStyle #' @importFrom ggiraph renderGirafe girafeOutput geom_point_interactive girafe opts_tooltip opts_zoom opts_sizing opts_toolbar opts_hover #' @importFrom ggrepel geom_text_repel #' @importFrom ggplot2 ggplot aes geom_vline geom_hline scale_x_continuous scale_y_continuous coord_cartesian labs scale_color_manual theme_minimal @@ -308,6 +347,8 @@ mod_resultsVisualisation_CodeWAS_server <- function(id, analysisResults) { output$codeWAStable <- reactable::renderReactable({ shiny::req(r$filteredCodeWASData) shiny::req(r$filteredCodeWASData |> nrow() > 0) + shiny::req(input$sortFirst) + shiny::req(input$sortSecond) df <- r$filteredCodeWASData |> dplyr::mutate(mlogp = round(-log10(pValue), 3)) |> @@ -324,7 +365,17 @@ mod_resultsVisualisation_CodeWAS_server <- function(id, analysisResults) { oddsRatio, mlogp, beta, modelType, runNotes ) - # browser() + df <- case_when( + input$sortFirstDesc & input$sortSecondDesc ~ + dplyr::arrange(df, desc(across(all_of(input$sortFirst))), desc(across(all_of(input$sortSecond)))), + input$sortFirstDesc & !input$sortSecondDesc ~ + dplyr::arrange(df, desc(across(all_of(input$sortFirst))), across(all_of(input$sortSecond))), + !input$sortFirstDesc & input$sortSecondDesc ~ + dplyr::arrange(df, across(all_of(input$sortFirst)), desc(across(all_of(input$sortSecond)))), + !input$sortFirstDesc & !input$sortSecondDesc ~ + dplyr::arrange(df, across(all_of(input$sortFirst)), across(all_of(input$sortSecond))), + TRUE ~ df + ) reactable::reactable( df, @@ -335,7 +386,8 @@ mod_resultsVisualisation_CodeWAS_server <- function(id, analysisResults) { defaultColDef = reactable::colDef( resizable = TRUE ), - defaultSorted = list(mlogp = "desc", oddsRatio = "desc"), + # defaultSorted = list(mlogp = "desc", oddsRatio = "desc"), + sortable = FALSE, columns = list( covariateName = reactable::colDef( name = "Covariate Name", @@ -407,6 +459,15 @@ mod_resultsVisualisation_CodeWAS_server <- function(id, analysisResults) { shiny::req(r$filteredCodeWASData) shiny::req(r$filteredCodeWASData |> nrow() > 0) + n_no_test <- sum(grepl("no test", r$filteredCodeWASData, ignore.case = TRUE)) + p_limit <- -log(0.05/(nrow(r$codeWASData) - n_no_test)) + + color_coding <- c( + "cases" = "#E41A1C", + "controls" = "#377EB8", + "n.s." = "lightgrey" + ) + df <- r$filteredCodeWASData |> dplyr::mutate(oddsRatio = ifelse(is.na(oddsRatio), 1, oddsRatio)) |> dplyr::mutate(pLog10 = -log10(pValue)) |> @@ -414,21 +475,57 @@ mod_resultsVisualisation_CodeWAS_server <- function(id, analysisResults) { dplyr::mutate(beta = log(oddsRatio)) |> dplyr::mutate(beta = ifelse(beta > 5, 5, beta)) |> dplyr::mutate(beta = ifelse(beta < -5, -5, beta)) |> - dplyr::mutate(direction = ifelse(beta > 0, "cases", "controls")) |> # n.s. = not significant + dplyr::mutate(direction = case_when( + pLog10 < p_limit ~ "n.s.", + beta > 0 ~ "cases", + beta < 0 ~ "controls", + TRUE ~ "n.s." + )) |> dplyr::select(analysisName, covariateName, conceptCode, vocabularyId, pValue, oddsRatio, direction, oddsRatio, pLog10, beta, meanCases, meanControls, modelType) |> dplyr::mutate(data_id = dplyr::row_number()) - n_no_test <- sum(grepl("no test", df$modelType, ignore.case = TRUE)) - p_limit <- -log(0.05/(nrow(r$codeWASData) - n_no_test)) - p <- ggplot2::ggplot(data = df, mapping = ggplot2::aes(x = beta, y = pLog10, color = direction)) + - # show the p-value limit - ggplot2::geom_hline(aes(yintercept = p_limit), col = "red", linetype = 'dashed') + + # draw a gray rectangle showing the wall of beta = 5 + ggplot2::geom_rect( + data = NULL, + xmin = 5.02, xmax = 10, ymin = 0, ymax = p_limit, + fill = "lightgrey", alpha = 0.02, color = "white" + ) + + ggplot2::geom_rect( + data = NULL, + xmin = -10, xmax = -5.02, ymin = 0, ymax = p_limit, + fill = "lightgrey", alpha = 0.02, color = "white" + ) + + # show the p-value and beta limits + ggplot2::geom_hline(aes(yintercept = p_limit), col = "red", linetype = 'dashed', alpha = 0.5) + + ggplot2::geom_vline(xintercept = 0, col = "red", linetype = 'dashed', alpha = 0.5) + + ggiraph::geom_point_interactive( + ggplot2::aes( + data_id = data_id, + tooltip = paste( + "Covariate: ", covariateName, "
", + "Analysis: ", analysisName, "
", + "Concept code: ", conceptCode, "
", + "Vocabulary: ", vocabularyId, "
", + "beta: ", signif(beta, digits = 3), "
", + "OR: ", signif(oddsRatio, digits = 3), "
", + "p-value: ", signif(pValue, digits = 2), "
" + ), + fill = direction + ), + hover_nearest = TRUE, + shape = 21, + color = "black", + size = 1.5, + alpha = 1.0, + stroke = 0.2 + ) + {if(input$top_10) # label the top 10 values ggrepel::geom_text_repel( data = df |> - dplyr::arrange(pValue, oddsRatio) |> + dplyr::filter(direction == "cases") |> + dplyr::arrange(desc(beta), desc(pLog10)) |> # this or arrange(desc(pLog10), desc(oddsRatio))? dplyr::slice_head(n = input$label_top_n), ggplot2::aes( label = stringr::str_wrap(stringr::str_trunc(.removeDomain(covariateName), 45), 30) @@ -443,39 +540,21 @@ mod_resultsVisualisation_CodeWAS_server <- function(id, analysisResults) { segment.linetype = "dashed", segment.alpha = 0.25 )} + - ggplot2::geom_vline(xintercept = 0, col = "red", linetype = 'dashed') + - ggiraph::geom_point_interactive( - ggplot2::aes( - data_id = data_id, - tooltip = paste("Analysis: ", analysisName, "
", - "Covariate: ", covariateName, "
", - "Concept code: ", conceptCode, "
", - "Vocabulary: ", vocabularyId, "
", - "beta: ", signif(beta, digits = 3), "
", - "OR: ", signif(oddsRatio, digits = 3), "
", - "p-value: ", signif(pValue, digits = 2), "
" - ) - ), - hover_nearest = TRUE, - size = 1.5, - alpha = 0.5, - stroke = 0.2 - ) + ggplot2::scale_x_continuous() + ggplot2::scale_y_continuous(transform = "log10", labels = function(x)round(x,1), expand = ggplot2::expansion(mult = c(0.1, 0.3))) + - ggplot2::coord_cartesian(xlim = c(-5, 5), ylim = range(df$pLog10)) + + ggplot2::coord_cartesian(xlim = c(min(df$beta) - 1, max(df$beta) + 2), ylim = range(df$pLog10)) + + ggplot2::scale_fill_manual(name = "Enriched in", values = color_coding) + #, guide = "none") + ggplot2::labs( x = "beta", y = "-log10(p-value)", - color = "Enriched in", title = paste("Multiple testing significance >", round(p_limit, 1)), subtitle = paste("-log( 0.05 / (number of covariates))") ) + - ggplot2::scale_color_manual(values = c("cases" = "#E41A1C", "controls" = "#377EB8", "n.s." = "lightgrey")) + #, guide = "none") + ggplot2::theme_minimal() + ggplot2::theme( text = ggplot2::element_text(size = 8), plot.title = ggplot2::element_text(size = 8), + plot.subtitle = ggplot2::element_text(size = 6), plot.caption = ggplot2::element_text(size = 4), axis.text.x = ggplot2::element_text(size = 7), axis.text.y = ggplot2::element_text(size = 7), diff --git a/R/mod_resultsVisualisation_TimeCodeWAS.R b/R/mod_resultsVisualisation_TimeCodeWAS.R index 0d1822a..3ecc570 100644 --- a/R/mod_resultsVisualisation_TimeCodeWAS.R +++ b/R/mod_resultsVisualisation_TimeCodeWAS.R @@ -7,13 +7,33 @@ #' #' @importFrom shiny NS tagList tags h4 uiOutput tabsetPanel tabPanel div downloadButton #' @importFrom ggiraph girafeOutput -#' @importFrom DT DTOutput #' #' @export #' mod_resultsVisualisation_TimeCodeWAS_ui <- function(id) { ns <- shiny::NS(id) + # this must be in sync with the columns in the reactable table + tableColumns <- c( + "Time ID" = "GROUP", + "Covariate Name" = "name", + "Concept Code" = "conceptCode", + "Vocabulary" = "vocabularyId", + "Analysis Name" = "analysisName", + "Domain" = "domain", + "Type" = "upIn", + "N cases" = "nCasesYes", + "N ctrls" = "nControlsYes", + "Ratio|Mean cases" = "meanCases", + "SD cases" = "sdCases", + "Ratio|Mean ctrls" = "meanControls", + "SD ctrls" = "sdControls", + "OR" = "OR", + "mlogp" = "mlogp", + "Beta" = "beta", + "Notes" = "notes" + ) + shiny::fluidPage( title = "TimeCodeWAS", shiny::tagList( @@ -123,7 +143,29 @@ mod_resultsVisualisation_TimeCodeWAS_ui <- function(id) { shiny::tabPanel( "Table", shiny::div( - style = "margin-top: 20px; margin-bottom: 10px;", + fluidRow( + column(10, + tags$div(style = "display: flex; align-items: center; gap: 15px;", + tags$label("Sort by:", style = "width: 50px; margin-bottom: 0;"), + tags$div(style = "margin-top: 15px;", + selectInput(ns("sortFirst"), label = NULL, choices = tableColumns, width = "150px", selected = "OR"), + ), + tags$div(style = "width: 50px;", + checkboxInput(ns("sortFirstDesc"), "descending", value = TRUE) + ), + tags$label("", style = "width: 20px; margin-bottom: 0; margin-left: 10px;"), + tags$div(style = "margin-top: 15px;", + selectInput(ns("sortSecond"), label = NULL, choices = tableColumns, width = "150px", selected = "mlogp"), + ), + tags$div(style = "width: 50px;", + checkboxInput(ns("sortSecondDesc"), "descending", value = TRUE) + ) + ) + ), + ) # fluidRow + ), # div + shiny::div( + style = "margin-top: 5px; margin-bottom: 10px;", shinycssloaders::withSpinner( reactable::reactableOutput(ns("reactableData")), proxy.height = "400px" @@ -154,7 +196,6 @@ mod_resultsVisualisation_TimeCodeWAS_ui <- function(id) { #' @importFrom shinyWidgets pickerInput pickerOptions chooseSliderSkin #' @importFrom ggiraph renderGirafe girafe opts_sizing opts_hover opts_selection opts_toolbar geom_point_interactive #' @importFrom ggrepel geom_text_repel -#' @importFrom DT renderDataTable datatable formatSignif formatStyle #' @importFrom dplyr filter mutate select arrange transmute left_join pull case_when if_else inner_join row_number #' @importFrom tidyr separate #' @importFrom stringr str_remove_all str_remove str_c str_wrap str_trunc str_split str_extract_all str_sub @@ -471,6 +512,8 @@ mod_resultsVisualisation_TimeCodeWAS_server <- function(id, analysisResults) { # shiny::observe({ shiny::req(input$time_period) + shiny::req(input$domain) + shiny::req(input$analysis) r$line_to_plot <- NULL }) @@ -796,7 +839,7 @@ mod_resultsVisualisation_TimeCodeWAS_server <- function(id, analysisResults) { dplyr::mutate(sdCases = round(sdCases, 3)) |> dplyr::mutate(sdControls = round(sdControls, 3))|> dplyr::mutate(beta = round(log(OR), 3)) |> - dplyr::mutate(pLog10 = round(-log10(p), 3)) |> + dplyr::mutate(mlogp = round(-log10(p), 3)) |> dplyr::mutate(OR = dplyr::case_when( OR > 10e+100 ~ Inf, OR < 10e-100 ~ -Inf, @@ -805,7 +848,19 @@ mod_resultsVisualisation_TimeCodeWAS_server <- function(id, analysisResults) { dplyr::select( GROUP, name, conceptCode, vocabularyId, code, analysisName, domain, upIn, nCasesYes, nControlsYes, meanCases, meanControls, sdCases, sdControls, - OR, pLog10, beta, notes) + OR, mlogp, beta, notes) + + df <- case_when( + input$sortFirstDesc & input$sortSecondDesc ~ + dplyr::arrange(df, desc(across(all_of(input$sortFirst))), desc(across(all_of(input$sortSecond)))), + input$sortFirstDesc & !input$sortSecondDesc ~ + dplyr::arrange(df, desc(across(all_of(input$sortFirst))), across(all_of(input$sortSecond))), + !input$sortFirstDesc & input$sortSecondDesc ~ + dplyr::arrange(df, across(all_of(input$sortFirst)), desc(across(all_of(input$sortSecond)))), + !input$sortFirstDesc & !input$sortSecondDesc ~ + dplyr::arrange(df, across(all_of(input$sortFirst)), across(all_of(input$sortSecond))), + TRUE ~ df + ) reactable::reactable( df, @@ -816,7 +871,8 @@ mod_resultsVisualisation_TimeCodeWAS_server <- function(id, analysisResults) { defaultColDef = reactable::colDef( resizable = TRUE ), - defaultSorted = list(pLog10 = "desc", OR = "desc"), + # defaultSorted = list(pLog10 = "desc", OR = "desc"), + sortable = FALSE, columns = list( GROUP = reactable::colDef(name = "Time ID", minWidth = 20, align = "right"), name = reactable::colDef( @@ -843,7 +899,7 @@ mod_resultsVisualisation_TimeCodeWAS_server <- function(id, analysisResults) { sdCases = reactable::colDef(name = "SD cases", minWidth = 13), sdControls = reactable::colDef(name = "SD ctrls", minWidth = 13), OR = reactable::colDef( name = "OR", minWidth = 25), - pLog10 = reactable::colDef(name = "mlogp", minWidth = 25), + mlogp = reactable::colDef(name = "mlogp", minWidth = 25), beta = reactable::colDef(name = "Beta", minWidth = 25), notes = reactable::colDef(name = "Notes", minWidth = 30) ), @@ -991,9 +1047,14 @@ mod_resultsVisualisation_TimeCodeWAS_server <- function(id, analysisResults) { } gg_data <- gg_data |> - left_join(items, by = c("name", "analysisName")) + left_join(items, by = c("name", "analysisName")) |> dplyr::arrange(desc(rank)) gg_plot <- ggplot2::ggplot(gg_data, ggplot2::aes(x = time_period_jittered, y = pLog10_jittered, group = data_id, fill = color_group, color = color_group)) + + ggiraph::geom_point_interactive( + data = gg_data |> dplyr::filter(color_group == "11"), + aes(size = pLog10, data_id = data_id, tooltip = label), + color = "black", shape = 21, alpha = 0.2 + ) + {if(input$connect_dots) ggplot2::geom_line(data = gg_data |> dplyr::filter(color_group != "11"), linewidth = 1) } + @@ -1015,8 +1076,9 @@ mod_resultsVisualisation_TimeCodeWAS_server <- function(id, analysisResults) { ) } + ggiraph::geom_point_interactive( + data = gg_data |> dplyr::filter(color_group != "11"), aes(size = pLog10, data_id = data_id, tooltip = label), - color = "black", shape = 21, alpha = ifelse(gg_data$color_group == "11", 0.25, 1) + color = "black", shape = 21, alpha = 1 ) + ggplot2::theme_minimal() + ggplot2::theme(