This vignette shows how to render interactive plots and tabular results in Shiny using the visOmopResults package and functions that build on it. Specifically, we will demonstrate:
Small/static tables → gt
, for
compact, fixed results such as cohort counts or
characteristics.
Large/dynamic tables → DT
or
reactable
, for large results or those that require
sorting/filtering to interpret.
Plots → build with ggplot2
and
(optionally) wrap with plotly::ggplotly()
for interactivity
in shiny.
Load packages and mock data.
library(shiny)
library(bslib)
library(sortable)
library(shinyWidgets)
library(gt)
library(DT)
library(reactable)
library(plotly)
library(dplyr)
library(visOmopResults)
library(IncidencePrevalence)
library(CohortCharacteristics)
library(shinycssloaders)
# Mock results in visOmopResults
data <- visOmopResults::data
# Remove global options (just in case we have them from previous work)
setGlobalPlotOptions(style = NULL, type = NULL)
setGlobalTableOptions(style = NULL, type = NULL)
We will use 3 different mock results, which are:
Baseline characteristics from the
CohortCharacteristics
package
Incidence results from the
IncidencePrevalence
package
Large scale characterisation, which is not a
<summarised_result>
The Shiny app has three panels, one for each result. All allow filtering by sex strata and provide panel-specific visualization options:
- Baseline characteristics: shows a gt
table with controls for headers, groups, and hidden columns..
- Large Scale characteristics: renders as a
datatable
or reactable
, with options to group
or hide columns.
- Incidence: displays a static ggplot
or interactive plotly
plot, with options for colouring,
faceting, and ribbons.
ui <- bslib::page_navbar(
title = "visOmopResults for Shiny",
window_title = "visOmopResults • Shiny",
collapsible = TRUE,
# Baseline Characteristics (GT table)
bslib::nav_panel(
title = "Baseline Characteristics",
icon = icon("users-gear"),
bslib::layout_sidebar(
sidebar = bslib::sidebar(
title = "Filters",
shinyWidgets::pickerInput(
inputId = "summarise_characteristics_sex",
label = "Sex",
choices = c("overall", "Male", "Female"),
selected = "overall",
multiple = TRUE
),
width = 320,
position = "left",
open = TRUE
),
bslib::card(
full_screen = TRUE,
bslib::card_header("Table layout"),
bslib::layout_sidebar(
sidebar = bslib::sidebar(
title = "Arrange columns",
sortable::bucket_list(
header = NULL,
group_name = "col-buckets",
orientation = "horizontal",
add_rank_list(
text = "None",
labels = c("variable_name", "variable_level", "estimate_name"),
input_id = "summarise_characteristics_table_none"
),
add_rank_list(
text = "Header",
labels = c("sex"),
input_id = "summarise_characteristics_table_header"
),
add_rank_list(
text = "Group columns",
labels = c("cdm_name", "cohort_name"),
input_id = "summarise_characteristics_table_group_column"
),
add_rank_list(
text = "Hide",
labels = "table_name",
input_id = "summarise_characteristics_table_hide"
)
),
position = "right",
width = 400,
open = FALSE
),
# GT output
gt::gt_output("summarise_characteristics_table") |>
shinycssloaders::withSpinner(type = 4)
)
)
)
),
# Large Scale Characterisation (DT / reactable)
bslib::nav_panel(
title = "Large Scale Characterisation",
icon = icon("table"),
bslib::layout_sidebar(
sidebar = bslib::sidebar(
# title = "Display options",
shinyWidgets::pickerInput(
inputId = "large_scale_sex",
label = "Sex",
choices = c("overall", "Male", "Female"),
selected = "overall",
multiple = TRUE
),
radioButtons(
"large_engine",
"Renderer",
choices = c("DT", "reactable"),
inline = TRUE
),
sortable::bucket_list(
header = NULL,
group_name = "col-buckets",
orientation = "horizontal",
add_rank_list(
text = "None",
labels = c("variable_name", "variable_level", "estimate_name"),
input_id = "large_scale_none"
),
add_rank_list(
text = "Group columns",
labels = c("cdm_name", "cohort_name"),
input_id = "large_scale_group_column"
),
add_rank_list(
text = "Hide",
labels = character(),
input_id = "large_scale_hide"
)
),
width = 320
),
bslib::card(
full_screen = TRUE,
bslib::card_header("Cohort characteristics (large-scale)"),
conditionalPanel(
"input.large_engine == 'DT'",
DTOutput("large_dt") |> shinycssloaders::withSpinner(type = 4)
),
conditionalPanel(
"input.large_engine == 'reactable'",
reactableOutput("large_reactable") |> shinycssloaders::withSpinner(type = 4)
)
)
)
),
# Incidence (ggplot → plotly)
bslib::nav_panel(
title = "Incidence",
icon = icon("chart-line"),
bslib::layout_sidebar(
sidebar = bslib::sidebar(
title = "Plot options",
shinyWidgets::pickerInput(
"incidence_sex",
"Sex strata",
choices = c("overall", "Male", "Female"),
selected = "overall",
multiple = TRUE
),
shinyWidgets::pickerInput(
inputId = "facet",
label = "Facet",
selected = "sex",
multiple = TRUE,
choices = c("cdm_name", "incidence_start_date", "sex", "outcome_cohort_name"),
),
shinyWidgets::pickerInput(
inputId = "colour",
label = "Colour",
selected = "outcome_cohort_name",
multiple = TRUE,
choices = c("cdm_name", "incidence_start_date", "sex", "outcome_cohort_name")
),
checkboxInput("inc_ribbon", "Show ribbon (CI)", TRUE),
checkboxInput("interactive", "Interactive Plot", TRUE),
width = 320
),
bslib::card(
full_screen = TRUE,
bslib::card_header("Incidence over time"),
uiOutput("incidence_plot", height = "520px") |> shinycssloaders::withSpinner(type = 4)
)
)
)
)
The server filters results by the selected sex and creates a
gt
table using the tableCharacteristics()
function from the CohortCharacteristics
package. This
function is built on visOmopResults, which ensures
consistent styling and supports arguments to define headers, group
columns, and hide columns.
If you have your own <summarised_result>
table,
which don’t has a dedicated table function, you can instead use
visOmopTable()
to generate a gt
table in
Shiny. This allows you to group estimates and configure header, group,
and hidden column options in a similar way.
These results are not in <summarised_result>
format, as shown below:
data$large_scale_characteristics
#> # A tibble: 952 × 8
#> cdm_name cohort_name sex variable_name variable_level concept_id count
#> <chr> <chr> <chr> <chr> <chr> <chr> <int>
#> 1 my_duckdb_da… denominato… over… Acute allerg… -inf to -366 4084167 113
#> 2 my_duckdb_da… denominato… over… Acute bacter… -inf to -366 4294548 607
#> 3 my_duckdb_da… denominato… over… Acute bronch… -inf to -366 260139 2303
#> 4 my_duckdb_da… denominato… over… Acute cholec… -inf to -366 198809 29
#> 5 my_duckdb_da… denominato… over… Acute viral … -inf to -366 4112343 2388
#> 6 my_duckdb_da… denominato… over… Alzheimer's … -inf to -366 378419 15
#> 7 my_duckdb_da… denominato… over… Anemia -inf to -366 439777 73
#> 8 my_duckdb_da… denominato… over… Angiodysplas… -inf to -366 4310024 281
#> 9 my_duckdb_da… denominato… over… Appendicitis -inf to -366 440448 125
#> 10 my_duckdb_da… denominato… over… Atopic derma… -inf to -366 133834 54
#> # ℹ 942 more rows
#> # ℹ 1 more variable: percentage <dbl>
In this case, we use visTable()
to generate tables as
either a datatable
or a reactable
, depending
on the user’s choice in the UI. The table type is specified with the
type argument.
For both table types, we pass the UI-selected columns to
groupColumn
and hide.
We do not generate a
header for this result, as it would require restructuring the estimates
into a single “estimate_value” column.
The look and behaviour of the tables can be customised through the style argument. Available options can be explored with:
tableStyle("datatable")
tableStyle("reactable")
In this vignette, we modify the datatable
style in the
server code so filters appear at the top of the table instead of the
default bottom.
For incidence results, we use the plotIncidence()
function from the IncidencePrevalence
package. This
function creates a ggplot
object, which can be rendered as
a static plot with plotOutput
or as an interactive plot
with plotlyOutput
. Users can also select which columns to
use for colouring and faceting, and whether to display confidence
interval ribbons.
For other results—whether <summarised_reuslt>
class or not—you can generate plots in a similar way by using the
plotting functions available in visOmopResults.
Note: Both
CohortCharacteristics
andIncidencePrevalence
functions for plotting and tabulation are built onvisOmopResults
, which means they share a consistent interface and style.
server <- function(input, output, session) {
# Baseline (GT)
output$summarise_characteristics_table <- gt::render_gt({
data$summarised_characteristics |>
# filter results by sex
filterStrata(sex %in% input$summarise_characteristics_sex) |>
# create GT table
CohortCharacteristics::tableCharacteristics(
header = input$summarise_characteristics_table_header,
groupColumn = input$summarise_characteristics_table_group_column,
hide = input$summarise_characteristics_table_hide,
type = "gt"
)
})
# Large scale characteristics
getLargeScaleResults <- reactive({
data$large_scale_characteristics |>
filter(.data$sex %in% input$large_scale_sex)
})
# To render as DT
output$large_dt <- renderDT({
getLargeScaleResults() |>
visTable(
hide = input$large_scale_hide,
groupColumn = input$large_scale_group_column,
type = "datatable",
style = list(
filter = "top",
searchHighlight = TRUE,
rownames = FALSE
)
)
})
# To render as reactable
output$large_reactable <- reactable::renderReactable({
getLargeScaleResults() |>
visTable(
hide = input$large_scale_hide,
groupColumn = input$large_scale_group_column,
type = "reactable",
style = "default"
)
})
# Incidence
getIncidencePlot <- reactive({
data$incidence |>
filterStrata(sex %in% input$incidence_sex) |>
plotIncidence(
colour = input$colour,
facet = input$facet,
ribbon = input$inc_ribbon
) +
theme(axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1))
})
output$incidence_plot <- renderUI({
plt <- getIncidencePlot()
if (input$interactive) {
ggplotly(plt)
} else {
renderPlot(plt)
}
})
}
To run the Shiny app, copy the code chunks provided in this vignette
into a script named app.R
, and add the
following line at the end:
You can find the complete code run the ShinyApp here.