flowchart TD A[User Input] --> B[Reactive Expression] B --> C[Render Function] C --> D[Output Object] D --> E[UI Display] F[Data Processing] --> B G[Server Logic] --> C H[UI Definition] --> E C --> I[renderPlot] C --> J[renderTable] C --> K[renderText] C --> L[renderUI] style A fill:#e1f5fe style C fill:#f3e5f5 style E fill:#e8f5e8
Key Takeaways
- Complete Output Ecosystem: Master all output types - plots, tables, text, downloads, and custom displays for versatile applications
- Interactive Visualization Power: Transform static outputs into engaging interactive experiences using plotly, DT, and htmlwidgets
- Performance-Optimized Rendering: Implement efficient rendering strategies that handle large datasets and complex visualizations smoothly
- Professional Presentation: Create polished, publication-ready displays with proper formatting, styling, and user experience design
- Modular Display Architecture: Build reusable output components that scale across different applications and use cases
Introduction
Shiny’s output system is where your data analysis transforms into compelling visual stories that users can explore and understand. While inputs capture user intentions, outputs deliver insights through plots, tables, interactive visualizations, and dynamic content that respond to user interactions in real-time.
This comprehensive guide covers Shiny’s complete output ecosystem, from basic text displays to sophisticated interactive dashboards. You’ll learn to create professional-quality visualizations, implement responsive data tables, integrate cutting-edge plotting libraries, and optimize performance for complex displays. By mastering these output techniques, you’ll build applications that not only analyze data but present it in ways that drive understanding and decision-making.
Whether you’re building business dashboards, research tools, or data exploration platforms, understanding Shiny’s output capabilities is essential for creating applications that truly serve your users’ needs.
Understanding Shiny’s Output Architecture
Before diving into specific output types, it’s crucial to understand how Shiny’s output system works and how it integrates with the reactive programming model you’ve already learned.
The Output Creation Process
Every Shiny output follows a consistent three-step pattern that connects server-side processing with user interface display:
Step 1: UI Declaration
In your UI, you declare where outputs will appear using output functions:
# UI side - declaring output locations
fluidPage(
plotOutput("my_plot"), # Declares a plot area
tableOutput("my_table"), # Declares a table area
textOutput("my_summary") # Declares a text area
)
Step 2: Server Rendering
In your server function, you create the actual content using render functions:
# Server side - generating output content
<- function(input, output) {
server $my_plot <- renderPlot({
output# Plot generation code
})
$my_table <- renderTable({
output# Table generation code
})
$my_summary <- renderText({
output# Text generation code
}) }
Step 3: Reactive Updates
Shiny automatically updates outputs when their dependencies change, creating the responsive experience users expect.
Output Function Pairs
Each output type consists of a UI function and corresponding render function that work together:
Output Type | UI Function | Render Function | Purpose |
---|---|---|---|
Plots | plotOutput() |
renderPlot() |
Static plots (ggplot2, base R) |
Interactive Plots | plotlyOutput() |
renderPlotly() |
Interactive visualizations |
Tables | tableOutput() |
renderTable() |
Simple data tables |
Data Tables | DT::dataTableOutput() |
DT::renderDataTable() |
Interactive tables |
Text | textOutput() |
renderText() |
Single text values |
Formatted Text | htmlOutput() |
renderUI() |
HTML formatted content |
Downloads | downloadButton() |
downloadHandler() |
File downloads |
Explore All Output Types Hands-On
Explore the complete output ecosystem with hands-on experimentation:
- Test every output type - Switch between plots, tables, text, and interactive displays
- Adjust parameters - See how different settings affect rendering and performance
- Compare approaches - Understand when to use each output type for maximum impact
- Experience downloads - Test different export formats and generation methods
- Monitor performance - Observe how data size affects rendering speed
Key Learning: Each output type serves specific purposes and user needs. Understanding these differences helps you choose the right display method for any data communication scenario.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 1000
#| editorHeight: 300
# Complete Output Type Explorer
# Interactive demonstration of all Shiny output types
library(shiny)
library(bsicons)
library(ggplot2)
library(DT)
library(plotly)
# This solves the issue of the download button not working from Chromium when this app is deployed as Shinylive
downloadButton <- function(...) {
tag <- shiny::downloadButton(...)
tag$attribs$download <- NULL
tag
}
ui <- fluidPage(
theme = bslib::bs_theme(version = 5, bootswatch = "cosmo"),
tags$head(
tags$style(HTML("
.output-control-panel {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.output-demo-area {
background: white;
border: 2px solid #007bff;
border-radius: 8px;
padding: 25px;
margin-bottom: 20px;
min-height: 400px;
}
.performance-metrics {
background: #e7f3ff;
border: 1px solid #007bff;
border-radius: 6px;
padding: 15px;
margin-top: 15px;
}
.output-selector {
margin-bottom: 20px;
}
.metric-item {
margin: 8px 0;
font-size: 0.9em;
}
"))
),
div(class = "container-fluid",
h2(bs_icon("display"), "Complete Output Type Explorer",
class = "text-center mb-4"),
p("Discover all Shiny output types and understand when to use each one",
class = "text-center lead text-muted mb-4"),
fluidRow(
# Control Panel
column(4,
div(class = "output-control-panel",
h4(bs_icon("sliders"), "Output Configuration"),
# Output Type Selection
div(class = "output-selector",
selectInput("output_type", "Output Type:",
choices = list(
"Static Plot (ggplot2)" = "static_plot",
"Interactive Plot (plotly)" = "plotly",
"Data Table (DT)" = "datatable",
"Simple Table" = "simple_table",
"Text Output" = "text",
"HTML Output" = "html",
"Verbatim Output" = "verbatim"
),
selected = "static_plot")
),
# Dataset Selection
selectInput("dataset", "Sample Dataset:",
choices = list(
"Motor Trends Cars (mtcars)" = "mtcars",
"Iris Flowers" = "iris",
"Economics Data" = "economics",
"Diamonds" = "diamonds_sample"
),
selected = "mtcars"),
# Data Size Control
sliderInput("data_size", "Sample Size:",
min = 10, max = 500, value = 100, step = 10),
conditionalPanel(
condition = "input.output_type == 'static_plot' || input.output_type == 'plotly'",
h5("Plot Configuration:"),
selectInput("plot_type", "Plot Type:",
choices = list("Scatter" = "scatter", "Histogram" = "histogram",
"Box Plot" = "boxplot", "Line" = "line")),
checkboxInput("show_trends", "Show Trend Lines", FALSE)
),
conditionalPanel(
condition = "input.output_type == 'datatable' || input.output_type == 'simple_table'",
h5("Table Configuration:"),
checkboxInput("show_pagination", "Enable Pagination", TRUE),
numericInput("page_length", "Rows per Page:", value = 10, min = 5, max = 50)
),
hr(),
# Quick Presets
h5("Quick Presets:"),
div(
actionButton("preset_dashboard", "Dashboard View",
class = "btn-outline-primary btn-sm mb-2 w-100"),
actionButton("preset_analysis", "Analysis View",
class = "btn-outline-success btn-sm mb-2 w-100"),
actionButton("preset_report", "Report View",
class = "btn-outline-info btn-sm w-100")
)
),
# Performance Metrics
div(class = "performance-metrics",
h5(bs_icon("speedometer2"), "Performance Metrics"),
div(class = "metric-item",
strong("Render Time: "), textOutput("render_time", inline = TRUE)),
div(class = "metric-item",
strong("Data Points: "), textOutput("data_points", inline = TRUE)),
div(class = "metric-item",
strong("Memory Usage: "), textOutput("memory_usage", inline = TRUE)),
div(class = "metric-item",
strong("User Interactions: "), textOutput("interaction_count", inline = TRUE))
)
),
# Main Display Area
column(8,
div(class = "output-demo-area",
h4(textOutput("output_title", inline = TRUE)),
p(textOutput("output_description"), class = "text-muted"),
# Dynamic Output Area
uiOutput("dynamic_output"),
# Output-specific controls
conditionalPanel(
condition = "input.output_type == 'static_plot'",
hr(),
div(
downloadButton("download_plot", "Download Plot", class = "btn-outline-secondary btn-sm"),
actionButton("refresh_plot", "Refresh", class = "btn-outline-primary btn-sm")
)
),
conditionalPanel(
condition = "input.output_type == 'datatable' || input.output_type == 'simple_table'",
hr(),
div(
downloadButton("download_data", "Download Data", class = "btn-outline-secondary btn-sm"),
actionButton("select_random", "Random Selection", class = "btn-outline-info btn-sm")
)
)
)
)
)
)
)
server <- function(input, output, session) {
# Reactive values for tracking
values <- reactiveValues(
render_start = NULL,
interaction_count = 0
)
# Get selected dataset
selected_data <- reactive({
req(input$dataset, input$data_size)
values$render_start <- Sys.time()
base_data <- switch(input$dataset,
"mtcars" = mtcars,
"iris" = iris,
"economics" = ggplot2::economics,
"diamonds_sample" = ggplot2::diamonds[sample(nrow(ggplot2::diamonds), 1000), ]
)
req(nrow(base_data) > 0)
if (nrow(base_data) > input$data_size) {
base_data[sample(nrow(base_data), input$data_size), ]
} else {
base_data
}
})
# Titles + Descriptions
output$output_title <- renderText({
switch(input$output_type,
"static_plot" = "Static Plot (ggplot2)",
"plotly" = "Interactive Plot (plotly)",
"datatable" = "Interactive Data Table (DT)",
"simple_table" = "Simple Data Table",
"text" = "Text Output",
"html" = "HTML Formatted Output",
"verbatim" = "Verbatim Console Output"
)
})
output$output_description <- renderText({
switch(input$output_type,
"static_plot" = "High-quality static visualizations using ggplot2. Best for publications and reports.",
"plotly" = "Interactive visualizations with zoom, pan, and hover capabilities. Great for data exploration.",
"datatable" = "Feature-rich interactive tables with sorting, filtering, and searching. Perfect for data analysis.",
"simple_table" = "Basic HTML tables for simple data display. Lightweight and fast rendering.",
"text" = "Simple text display for summaries, statistics, and computed values.",
"html" = "Rich HTML content with formatting, links, and styling capabilities.",
"verbatim" = "Preformatted text output, ideal for showing code, model summaries, or console output."
)
})
# Dynamic output container
output$dynamic_output <- renderUI({
switch(input$output_type,
"static_plot" = plotOutput("demo_static_plot", height = "400px"),
"plotly" = plotlyOutput("demo_plotly", height = "400px"),
"datatable" = DT::dataTableOutput("demo_datatable"),
"simple_table" = tableOutput("demo_simple_table"),
"text" = div(textOutput("demo_text"), style = "font-size: 1.2em; padding: 20px;"),
"html" = htmlOutput("demo_html"),
"verbatim" = verbatimTextOutput("demo_verbatim")
)
})
# ---- Static Plot ----
create_static_plot <- reactive({
data <- selected_data()
req(nrow(data) > 0)
numeric_cols <- names(data)[sapply(data, is.numeric)]
req(length(numeric_cols) >= 1)
x_var <- numeric_cols[1]
y_var <- if (length(numeric_cols) >= 2) numeric_cols[2] else numeric_cols[1]
if (input$dataset == "iris") {
p <- ggplot(data, aes(x = Sepal.Length, y = Sepal.Width, color = Species)) +
geom_point(size = 2, alpha = 0.7)
} else {
p <- switch(input$plot_type,
"scatter" = ggplot(data, aes_string(x = x_var, y = y_var)) + geom_point(),
"histogram" = ggplot(data, aes_string(x = x_var)) + geom_histogram(bins = 20),
"boxplot" = ggplot(data, aes(x = factor(1), y = !!sym(y_var))) + geom_boxplot(),
"line" = if ("date" %in% names(data)) {
ggplot(data, aes_string(x = "date", y = y_var)) + geom_line()
} else {
ggplot(data, aes_string(x = x_var, y = y_var)) + geom_line()
}
)
if (input$show_trends && input$plot_type == "scatter") {
p <- p + geom_smooth(method = "lm", se = FALSE)
}
}
p + theme_minimal()
})
output$demo_static_plot <- renderPlot({
create_static_plot()
})
# ---- Plotly ----
output$demo_plotly <- renderPlotly({
data <- selected_data()
req(nrow(data) > 0)
numeric_cols <- names(data)[sapply(data, is.numeric)]
req(length(numeric_cols) >= 1)
x_var <- numeric_cols[1]
y_var <- if (length(numeric_cols) >= 2) numeric_cols[2] else numeric_cols[1]
if (input$dataset == "iris") {
plot_ly(data, x = ~Sepal.Length, y = ~Sepal.Width, color = ~Species,
type = "scatter", mode = "markers")
} else {
plot_ly(data, x = ~get(x_var), y = ~get(y_var),
type = "scatter", mode = "markers")
}
})
# ---- DT Table ----
output$demo_datatable <- DT::renderDataTable({
data <- selected_data()
req(nrow(data) > 0)
DT::datatable(data,
options = list(
pageLength = input$page_length,
scrollX = TRUE,
dom = 'Bfrtip',
buttons = c('copy', 'csv', 'excel')
),
extensions = 'Buttons'
)
})
# ---- Simple Table ----
output$demo_simple_table <- renderTable({
data <- selected_data()
req(nrow(data) > 0)
if (input$show_pagination) head(data, input$page_length) else data
}, striped = TRUE, hover = TRUE, bordered = TRUE)
# ---- Text ----
output$demo_text <- renderText({
data <- selected_data()
req(nrow(data) > 0)
paste("Dataset", input$dataset, "has", nrow(data), "rows and", ncol(data), "columns.")
})
# ---- HTML ----
output$demo_html <- renderUI({
data <- selected_data()
req(nrow(data) > 0)
numeric_cols <- names(data)[sapply(data, is.numeric)]
if (length(numeric_cols) == 0) {
HTML("<p>No numeric columns available.</p>")
} else {
stats_html <- lapply(numeric_cols[1:min(3, length(numeric_cols))], function(col) {
HTML(paste0(
"<h5>", col, "</h5>",
"<ul>",
"<li>Mean: ", round(mean(data[[col]], na.rm = TRUE), 2), "</li>",
"<li>Min: ", round(min(data[[col]], na.rm = TRUE), 2), "</li>",
"<li>Max: ", round(max(data[[col]], na.rm = TRUE), 2), "</li>",
"</ul>"
))
})
do.call(tagList, stats_html)
}
})
# ---- Verbatim ----
output$demo_verbatim <- renderPrint({
data <- selected_data()
req(nrow(data) > 0)
summary(data)
})
# ---- Metrics ----
output$render_time <- renderText({
if (!is.null(values$render_start)) {
render_time <- as.numeric(difftime(Sys.time(), values$render_start, units = "secs"))
paste0(round(render_time, 2), " sec")
} else {
"0 sec"
}
})
output$data_points <- renderText({
data <- selected_data()
req(nrow(data) > 0)
prettyNum(nrow(data) * ncol(data), big.mark = ",")
})
output$memory_usage <- renderText({
data <- selected_data()
req(nrow(data) > 0)
paste0(round(object.size(data) / 1024^2, 2), " MB")
})
output$interaction_count <- renderText({
as.character(values$interaction_count)
})
observe({
input$output_type
input$dataset
input$data_size
input$plot_type
values$interaction_count <- isolate(values$interaction_count) + 1
})
# ---- Presets ----
observeEvent(input$preset_dashboard, {
updateSelectInput(session, "output_type", selected = "plotly")
updateSelectInput(session, "dataset", selected = "mtcars")
updateSliderInput(session, "data_size", value = 200)
updateSelectInput(session, "plot_type", selected = "scatter")
updateCheckboxInput(session, "show_trends", value = TRUE)
})
observeEvent(input$preset_analysis, {
updateSelectInput(session, "output_type", selected = "datatable")
updateSelectInput(session, "dataset", selected = "iris")
updateSliderInput(session, "data_size", value = 150)
updateCheckboxInput(session, "show_pagination", value = TRUE)
updateNumericInput(session, "page_length", value = 15)
})
observeEvent(input$preset_report, {
updateSelectInput(session, "output_type", selected = "static_plot")
updateSelectInput(session, "dataset", selected = "economics")
updateSliderInput(session, "data_size", value = 300)
updateSelectInput(session, "plot_type", selected = "line")
})
# ---- Download handlers ----
output$download_plot <- downloadHandler(
filename = function() {
paste("plot_", Sys.Date(), ".png", sep = "")
},
content = function(file) {
ggsave(file, plot = create_static_plot(), width = 10, height = 6, dpi = 300)
}
)
output$download_data <- downloadHandler(
filename = function() {
paste("data_", Sys.Date(), ".csv", sep = "")
},
content = function(file) {
write.csv(selected_data(), file, row.names = FALSE)
}
)
# ---- Extra buttons ----
observeEvent(input$refresh_plot, {
values$render_start <- Sys.time()
})
observeEvent(input$select_random, {
current_data <- selected_data()
req(nrow(current_data) > 0)
random_size <- sample(10:min(100, nrow(current_data)), 1)
updateSliderInput(session, "data_size", value = random_size)
})
}
shinyApp(ui = ui, server = server)
Text and Formatted Output
Text outputs form the foundation of user communication in Shiny applications, providing summaries, statistics, and dynamic feedback.
Basic Text Output
The simplest output type displays single text values or computed statistics:
# UI
textOutput("simple_text")
# Server
$simple_text <- renderText({
outputpaste("Current time:", Sys.time())
})
# UI
textOutput("data_summary")
# Server
$data_summary <- renderText({
output<- mtcars
data paste("Dataset contains", nrow(data),
"observations and", ncol(data), "variables")
})
# UI
sliderInput("n_rows", "Number of rows:", 1, 100, 50),
textOutput("filtered_summary")
# Server
$filtered_summary <- renderText({
output<- head(mtcars, input$n_rows)
filtered_data paste("Showing", nrow(filtered_data), "of", nrow(mtcars), "total rows")
})
HTML and Formatted Output
For richer formatting, use htmlOutput()
and renderUI()
to include HTML tags, styling, and complex layouts:
# UI
htmlOutput("formatted_summary")
# Server
$formatted_summary <- renderUI({
output<- summary(mtcars$mpg)
data_stats
HTML(paste(
"<h4>Miles Per Gallon Summary</h4>",
"<ul>",
"<li><strong>Mean:</strong>", round(mean(mtcars$mpg), 2), "</li>",
"<li><strong>Median:</strong>", round(median(mtcars$mpg), 2), "</li>",
"<li><strong>Range:</strong>", round(min(mtcars$mpg), 2), "-",
round(max(mtcars$mpg), 2), "</li>",
"</ul>"
)) })
Verbatim Text Output
For displaying code, statistical output, or preformatted text, use verbatimTextOutput()
:
# UI
verbatimTextOutput("model_summary")
# Server
$model_summary <- renderPrint({
output<- lm(mpg ~ wt + hp, data = mtcars)
model summary(model)
})
textOutput()
: Single values, simple strings, computed statisticshtmlOutput()
: Formatted text with HTML tags, styled contentverbatimTextOutput()
: Code, model summaries, preformatted console output
Plot Outputs and Static Visualization
Plots are often the centerpiece of data applications, transforming numbers into visual insights that users can immediately understand.
Base R and ggplot2 Integration
Shiny seamlessly integrates with R’s plotting ecosystem through renderPlot()
:
library(ggplot2)
# UI
plotOutput("ggplot_example", height = "400px")
# Server
$ggplot_example <- renderPlot({
outputggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl))) +
geom_point(size = 3, alpha = 0.7) +
geom_smooth(method = "lm", se = FALSE) +
theme_minimal() +
labs(title = "Fuel Efficiency vs Weight",
x = "Weight (1000 lbs)",
y = "Miles per Gallon",
color = "Cylinders") +
theme(text = element_text(size = 12))
})
# UI
plotOutput("base_plot")
# Server
$base_plot <- renderPlot({
outputplot(mtcars$wt, mtcars$mpg,
xlab = "Weight (1000 lbs)",
ylab = "Miles per Gallon",
main = "Fuel Efficiency vs Weight",
col = rainbow(length(unique(mtcars$cyl)))[factor(mtcars$cyl)],
pch = 16, cex = 1.2)
legend("topright", legend = unique(mtcars$cyl),
col = rainbow(length(unique(mtcars$cyl))), pch = 16,
title = "Cylinders")
})
# UI
selectInput("plot_type", "Plot Type:",
choices = list("Scatter" = "scatter",
"Box" = "box",
"Histogram" = "hist")),
plotOutput("advanced_plot")
# Server
$advanced_plot <- renderPlot({
output<- ggplot(mtcars, aes(x = wt, y = mpg))
base_plot
switch(input$plot_type,
"scatter" = base_plot +
geom_point(aes(color = factor(cyl)), size = 3) +
geom_smooth(method = "lm"),
"box" = ggplot(mtcars, aes(x = factor(cyl), y = mpg)) +
geom_boxplot(fill = "lightblue", alpha = 0.7) +
geom_jitter(width = 0.2),
"hist" = ggplot(mtcars, aes(x = mpg)) +
geom_histogram(bins = 15, fill = "steelblue", alpha = 0.7)
+ theme_minimal()
) })
Plot Customization and Styling
Control plot appearance and responsiveness with these techniques:
# Responsive plot sizing
plotOutput("responsive_plot",
height = "auto", # Automatic height adjustment
width = "100%") # Full width
# High-resolution plots for publication
$publication_plot <- renderPlot({
output# Your ggplot2 code here
res = 96, height = 600, width = 800) # High DPI settings
},
# Click and hover interactions
plotOutput("interactive_base_plot",
click = "plot_click",
hover = "plot_hover",
brush = "plot_brush")
Handle plot interactions in the server:
# Server - handling plot clicks
observeEvent(input$plot_click, {
<- nearPoints(mtcars, input$plot_click)
clicked_point if(nrow(clicked_point) > 0) {
showModal(modalDialog(
title = "Selected Car",
paste("Car:", rownames(clicked_point)[1],
"MPG:", clicked_point$mpg[1],
"Weight:", clicked_point$wt[1])
))
} })
Interactive Visualizations with Plotly
Plotly transforms static plots into interactive explorations, allowing users to zoom, pan, hover, and drill down into data details.
Basic Plotly Integration
Converting ggplot2 to interactive plotly is remarkably simple:
library(plotly)
# UI
plotlyOutput("plotly_basic")
# Server
$plotly_basic <- renderPlotly({
output<- ggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl))) +
p geom_point(size = 3) +
theme_minimal() +
labs(title = "Interactive Fuel Efficiency Plot")
ggplotly(p) # Convert ggplot to plotly
})
Master Advanced Plotly Features
Explore sophisticated interactive visualization techniques:
- Build custom interactions - Create plots with click, hover, and selection events
- Apply advanced styling - Master custom themes, annotations, and layouts
- Combine plot types - Mix scatter, line, bar, and surface plots effectively
- Add animations - Create compelling animated visualizations for time-series data
- Handle plot events - Capture user interactions for dynamic app behavior
Key Learning: Advanced plotly features transform static data into engaging, explorable stories that reveal insights through interaction rather than passive observation.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 1100
#| editorHeight: 300
# Advanced Plotly Workshop
# Comprehensive demonstration of plotly features and interactions
library(shiny)
library(bsicons)
library(plotly)
library(dplyr)
# Global context variable - set based on deployment environment
# Options: "shinylive" or "shinyserver" (on production server)
APP_CONTEXT <- "shinylive"
ui <- fluidPage(
theme = bslib::bs_theme(version = 5, bootswatch = "cosmo"),
tags$head(
tags$style(HTML("
.workshop-panel {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.plot-container {
background: white;
border: 2px solid #007bff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.event-info {
background: #e7f3ff;
border: 1px solid #007bff;
border-radius: 6px;
padding: 15px;
margin-top: 15px;
font-family: monospace;
font-size: 0.9em;
}
.feature-tabs .nav-tabs {
margin-bottom: 20px;
}
"))
),
div(class = "container-fluid",
h2(bs_icon("graph-up"), "Advanced Plotly Workshop",
class = "text-center mb-4"),
p("Master interactive visualization techniques with hands-on plotly exploration",
class = "text-center lead text-muted mb-4"),
fluidRow(
# Configuration Panel
column(4,
div(class = "workshop-panel",
h4(bs_icon("gear"), "Plot Configuration"),
# Plot Type Selection
selectInput("plot_feature", "Feature to Explore:",
choices = list(
"Interactive Scatter Plot" = "scatter",
"Custom Hover Information" = "hover",
"3D Visualization" = "3d",
"Animated Time Series" = "animation",
"Multiple Plot Types" = "combo",
"Geographic Mapping" = "geo"
),
selected = "scatter"),
# Data Configuration
selectInput("data_source", "Data Source:",
choices = list(
"Motor Trends Cars" = "mtcars",
"Iris Flowers" = "iris",
"Economics Time Series" = "economics",
"Sample Geographic Data" = "geo_data"
),
selected = "mtcars"),
conditionalPanel(
condition = "input.plot_feature == 'scatter'",
h5("Scatter Plot Options:"),
checkboxInput("color_by_group", "Color by Group", TRUE),
checkboxInput("size_by_value", "Size by Value", FALSE),
checkboxInput("add_trendline", "Add Trend Line", FALSE)
),
conditionalPanel(
condition = "input.plot_feature == '3d'",
h5("3D Options:"),
selectInput("z_variable", "Z-Axis Variable:",
choices = NULL), # Will be populated dynamically
sliderInput("marker_size_3d", "Marker Size:",
min = 2, max = 15, value = 5)
),
conditionalPanel(
condition = "input.plot_feature == 'animation'",
h5("Animation Options:"),
sliderInput("animation_speed", "Animation Speed (ms):",
min = 200, max = 2000, value = 800, step = 100),
checkboxInput("loop_animation", "Loop Animation", TRUE)
),
hr(),
# Styling Options
h5("Styling & Theme:"),
selectInput("plot_theme", "Theme:",
choices = list(
"Default" = "default",
"Dark" = "plotly_dark",
"White" = "plotly_white",
"Minimal" = "simple_white"
)),
selectInput("color_palette", "Color Palette:",
choices = list(
"Viridis" = "viridis",
"Set1" = "set1",
"Pastel" = "pastel",
"Dark2" = "dark2"
)),
hr(),
# Quick Examples
h5("Quick Examples:"),
div(
actionButton("example_basic", "Basic Interactive",
class = "btn-outline-primary btn-sm mb-2 w-100"),
actionButton("example_advanced", "Advanced Features",
class = "btn-outline-success btn-sm mb-2 w-100"),
actionButton("example_dashboard", "Dashboard Style",
class = "btn-outline-info btn-sm w-100")
)
),
# Event Information Panel
div(class = "event-info",
h5(bs_icon("cursor"), "Plot Interactions"),
div(
strong("Last Click: "), textOutput("click_info", inline = TRUE),
br(),
strong("Hover Point: "), textOutput("hover_info", inline = TRUE),
br(),
strong("Selected Points: "), textOutput("selection_info", inline = TRUE),
br(),
strong("Zoom Level: "), textOutput("zoom_info", inline = TRUE)
)
)
),
# Main Plot Area
column(8,
div(class = "plot-container",
h4(textOutput("plot_title")),
p(textOutput("plot_description"), class = "text-muted"),
# Main plotly output
plotlyOutput("main_plot", height = "500px"),
# Download and export options
hr(),
div( # Conditional download button based on context
if (APP_CONTEXT == "shinylive") {
actionButton("download_help", "Download Help",
class = "btn-outline-secondary btn-sm")
} else {
downloadButton("download_plotly", "Download HTML",
class = "btn-outline-secondary btn-sm")
},
actionButton("reset_zoom", "Reset Zoom",
class = "btn-outline-primary btn-sm"),
actionButton("random_data", "Randomize Data",
class = "btn-outline-info btn-sm")
)
)
)
)
)
)
server <- function(input, output, session) {
# Reactive values for tracking interactions
values <- reactiveValues(
click_data = NULL,
hover_data = NULL,
selected_data = NULL,
zoom_data = NULL
)
# Get processed data based on selection
plot_data <- reactive({
req(input$data_source)
switch(input$data_source,
"mtcars" = mtcars %>%
mutate(car_name = rownames(mtcars),
efficiency = cut(mpg, breaks = 3, labels = c("Low", "Medium", "High"))),
"iris" = iris,
"economics" = economics,
"geo_data" = data.frame(
city = c("New York", "Los Angeles", "Chicago", "Houston", "Phoenix"),
lat = c(40.7128, 34.0522, 41.8781, 29.7604, 33.4484),
lon = c(-74.0060, -118.2437, -87.6298, -95.3698, -112.0740),
population = c(8175133, 3971883, 2695598, 2320268, 1680992),
state = c("NY", "CA", "IL", "TX", "AZ")
)
)
})
# Update Z-variable choices for 3D plots
observe({
req(input$data_source)
data <- plot_data()
numeric_cols <- names(data)[sapply(data, is.numeric)]
updateSelectInput(session, "z_variable",
choices = numeric_cols,
selected = numeric_cols[min(3, length(numeric_cols))])
})
# Change data source to geo_data when plot_feature is geo
observe({
if (input$plot_feature == "geo") {
updateSelectInput(session, "data_source", selected = "geo_data")
}
})
# If plot_feature != geo and data_source is geo_data, switch to mtcars
observe({
if (input$plot_feature != "geo" && input$data_source == "geo_data") {
updateSelectInput(session, "data_source", selected = "mtcars")
}
})
# Dynamic plot title and description
output$plot_title <- renderText({
switch(input$plot_feature,
"scatter" = "Interactive Scatter Plot with Custom Events",
"hover" = "Advanced Hover Information Display",
"3d" = "Three-Dimensional Data Exploration",
"animation" = "Animated Time Series Visualization",
"combo" = "Combined Plot Types and Subplots",
"geo" = "Geographic Data Mapping"
)
})
output$plot_description <- renderText({
switch(input$plot_feature,
"scatter" = "Click on points to see details. Try selecting multiple points by dragging.",
"hover" = "Hover over points to see custom formatted information with additional context.",
"3d" = "Rotate and zoom the 3D plot to explore data from different angles.",
"animation" = "Use the play button to animate through time. Adjust speed with the slider.",
"combo" = "Multiple plot types combined into a single interactive visualization.",
"geo" = "Interactive map showing geographic data with hover information and zoom capabilities."
)
})
# Main plot rendering
output$main_plot <- renderPlotly({
req(input$plot_feature, input$data_source)
data <- plot_data()
# Apply theme
plot_theme <- switch(input$plot_theme,
"plotly_dark" = list(plot_bgcolor = 'rgb(17,17,17)', paper_bgcolor = 'rgb(17,17,17)'),
"plotly_white" = list(plot_bgcolor = 'white', paper_bgcolor = 'white'),
"simple_white" = list(plot_bgcolor = 'white', paper_bgcolor = 'white'),
"default" = list()
)
# Generate plot based on feature selection
p <- switch(input$plot_feature,
"scatter" = create_scatter_plot(data),
"hover" = create_hover_plot(data),
"3d" = create_3d_plot(data),
"animation" = create_animated_plot(data),
"combo" = create_combo_plot(data),
"geo" = create_geo_plot(data)
)
# Apply theme and return
p %>% layout(
template = input$plot_theme,
plot_bgcolor = plot_theme$plot_bgcolor,
paper_bgcolor = plot_theme$paper_bgcolor
) %>%
event_register("plotly_click") %>%
event_register("plotly_hover") %>%
event_register("plotly_selected") %>%
event_register("plotly_relayout")
})
# Create scatter plot
create_scatter_plot <- function(data) {
if (input$data_source == "mtcars") {
p <- plot_ly(data, x = ~wt, y = ~mpg, source = "main_plot")
if (input$color_by_group) {
p <- p %>% add_markers(color = ~factor(cyl),
colors = get_color_palette(),
hovertemplate = "Weight: %{x:.2f}<br>MPG: %{y:.1f}<br>Cylinders: %{color}<extra></extra>")
} else {
p <- p %>% add_markers(marker = list(color = "#1f77b4"))
}
if (input$size_by_value) {
p <- p %>% add_markers(size = ~hp, sizes = c(10, 30))
}
if (input$add_trendline) {
fit <- lm(mpg ~ wt, data = data)
p <- p %>% add_lines(y = ~fitted(fit), line = list(color = "red", dash = "dash"),
name = "Trend Line", showlegend = TRUE)
}
p %>% layout(title = "Car Performance Analysis",
xaxis = list(title = "Weight (1000 lbs)"),
yaxis = list(title = "Miles per Gallon"))
} else {
# Handle other datasets
numeric_cols <- names(data)[sapply(data, is.numeric)]
if (length(numeric_cols) >= 2) {
plot_ly(data, x = ~get(numeric_cols[1]), y = ~get(numeric_cols[2]),
type = "scatter", mode = "markers") %>%
layout(xaxis = list(title = numeric_cols[1]),
yaxis = list(title = numeric_cols[2]))
}
}
}
# Create hover plot with rich information
create_hover_plot <- function(data) {
if (input$data_source == "mtcars") {
plot_ly(data, x = ~wt, y = ~mpg, color = ~factor(cyl),
colors = get_color_palette(),
text = ~car_name, source = "main_plot",
hovertemplate = paste(
"<b>%{text}</b><br>",
"<i>Performance Metrics</i><br>",
"Weight: %{x:.2f} tons<br>",
"Fuel Efficiency: %{y:.1f} MPG<br>",
"Cylinders: %{color}<br>",
"Efficiency Rating: %{customdata}<br>",
"<extra></extra>"
),
customdata = ~efficiency) %>%
add_markers(size = I(10)) %>%
layout(title = "Rich Hover Information Demo",
xaxis = list(title = "Weight (1000 lbs)"),
yaxis = list(title = "Miles per Gallon"))
} else if (input$data_source == "iris") {
plot_ly(data, x = ~Sepal.Length, y = ~Sepal.Width,
color = ~Species, colors = get_color_palette(),
source = "main_plot",
text = ~paste("Petal L:", Petal.Length, "| Petal W:", Petal.Width),
hovertemplate = paste(
"<b>%{color} Iris</b><br>",
"Sepal Length: %{x:.1f} cm<br>",
"Sepal Width: %{y:.1f} cm<br>",
"%{text}<br>",
"<extra></extra>"
)) %>%
add_markers() %>%
layout(title = "Iris Dataset with Rich Hover")
}
}
# Create 3D plot
create_3d_plot <- function(data) {
if (input$data_source == "mtcars" && !is.null(input$z_variable)) {
plot_ly(data, x = ~wt, y = ~mpg, z = ~get(input$z_variable),
color = ~factor(cyl), colors = get_color_palette(),
text = ~car_name,
type = "scatter3d", mode = "markers",
marker = list(size = input$marker_size_3d),
hovertemplate = paste(
"<b>%{text}</b><br>",
"Weight: %{x:.2f}<br>",
"MPG: %{y:.1f}<br>",
input$z_variable, ": %{z:.1f}<br>",
"<extra></extra>"
)) %>%
layout(title = "3D Car Performance Visualization",
scene = list(
xaxis = list(title = "Weight"),
yaxis = list(title = "MPG"),
zaxis = list(title = input$z_variable)
))
} else {
# Fallback 3D plot
plot_ly(x = rnorm(50), y = rnorm(50), z = rnorm(50),
type = "scatter3d", mode = "markers") %>%
layout(title = "Sample 3D Visualization")
}
}
# Create animated plot
create_animated_plot <- function(data) {
if (input$data_source == "economics") {
# Create sample animated data
years <- rep(1990:2020, each = 20)
months <- rep(1:12, length.out = length(years))
animated_data <- data.frame(
year = years,
month = months,
date = as.Date(paste(years, months, "01", sep = "-")),
value = cumsum(rnorm(length(years), 0, 10)) + 100,
category = sample(c("A", "B", "C"), length(years), replace = TRUE)
)
plot_ly(animated_data, x = ~month, y = ~value,
frame = ~year, color = ~category,
colors = get_color_palette(),
type = "scatter", mode = "markers+lines") %>%
animation_opts(frame = input$animation_speed,
transition = input$animation_speed / 2,
redraw = FALSE) %>%
layout(title = "Animated Time Series",
xaxis = list(title = "Month"),
yaxis = list(title = "Value"))
} else {
# Simple animated scatter
plot_ly(mtcars, x = ~wt, y = ~mpg, frame = ~cyl,
type = "scatter", mode = "markers") %>%
animation_opts(frame = input$animation_speed) %>%
layout(title = "Animated by Cylinder Count")
}
}
# Create combination plot
create_combo_plot <- function(data) {
if (input$data_source == "mtcars") {
# Create subplot
p1 <- plot_ly(data, x = ~wt, y = ~mpg, type = "scatter", mode = "markers",
name = "MPG vs Weight") %>%
layout(xaxis = list(title = "Weight"), yaxis = list(title = "MPG"))
p2 <- plot_ly(data, x = ~hp, type = "histogram", name = "HP Distribution") %>%
layout(xaxis = list(title = "Horsepower"), yaxis = list(title = "Count"))
subplot(p1, p2, nrows = 2, shareY = FALSE) %>%
layout(title = "Combined Visualization: Scatter + Histogram")
}
}
# Create geographic plot
create_geo_plot <- function(data) {
if (input$data_source == "geo_data") {
plot_ly(data, lat = ~lat, lon = ~lon,
color = ~population, size = ~population,
colors = get_color_palette(),
type = "scattermapbox",
hovertemplate = paste(
"<b>%{text}</b><br>",
"Population: %{marker.size:,}<br>",
"State: %{customdata}<br>",
"<extra></extra>"
),
text = ~city, customdata = ~state) %>%
layout(title = "US Cities Population Map",
mapbox = list(
style = "open-street-map",
center = list(lat = 39.8283, lon = -98.5795),
zoom = 3
))
}
}
# Get color palette
get_color_palette <- function() {
switch(input$color_palette,
"viridis" = viridis::viridis(8),
"set1" = RColorBrewer::brewer.pal(8, "Set1"),
"pastel" = RColorBrewer::brewer.pal(8, "Pastel1"),
"dark2" = RColorBrewer::brewer.pal(8, "Dark2")
)
}
# Event handling
observe({
click_data <- event_data("plotly_click", source = "main_plot")
values$click_data <- click_data
})
observe({
hover_data <- event_data("plotly_hover", source = "main_plot")
values$hover_data <- hover_data
})
observe({
selected_data <- event_data("plotly_selected", source = "main_plot")
values$selected_data <- selected_data
})
observe({
relayout_data <- event_data("plotly_relayout", source = "main_plot")
values$zoom_data <- relayout_data
})
# Display event information
output$click_info <- renderText({
if (!is.null(values$click_data)) {
paste("Point (", round(values$click_data$x, 2), ",",
round(values$click_data$y, 2), ")")
} else {
"None"
}
})
output$hover_info <- renderText({
if (!is.null(values$hover_data)) {
paste("Point (", round(values$hover_data$x, 2), ",",
round(values$hover_data$y, 2), ")")
} else {
"None"
}
})
output$selection_info <- renderText({
if (!is.null(values$selected_data)) {
paste(nrow(values$selected_data), "points selected")
} else {
"None"
}
})
output$zoom_info <- renderText({
if (!is.null(values$zoom_data) && "xaxis.range[0]" %in% names(values$zoom_data)) {
"Custom zoom applied"
} else {
"Default view"
}
})
# Example presets
observeEvent(input$example_basic, {
updateSelectInput(session, "plot_feature", selected = "scatter")
updateSelectInput(session, "data_source", selected = "mtcars")
updateCheckboxInput(session, "color_by_group", value = TRUE)
updateCheckboxInput(session, "size_by_value", value = FALSE)
updateSelectInput(session, "plot_theme", selected = "default")
})
observeEvent(input$example_advanced, {
updateSelectInput(session, "plot_feature", selected = "hover")
updateSelectInput(session, "data_source", selected = "iris")
updateSelectInput(session, "plot_theme", selected = "plotly_white")
updateSelectInput(session, "color_palette", selected = "viridis")
})
observeEvent(input$example_dashboard, {
updateSelectInput(session, "plot_feature", selected = "combo")
updateSelectInput(session, "data_source", selected = "mtcars")
updateSelectInput(session, "plot_theme", selected = "simple_white")
updateSelectInput(session, "color_palette", selected = "set1")
})
# Action buttons
observeEvent(input$reset_zoom, {
plotlyProxy("main_plot") %>%
plotlyProxyInvoke("relayout", list(
"xaxis.autorange" = TRUE,
"yaxis.autorange" = TRUE
))
})
observeEvent(input$random_data, {
# Clear interaction data
values$click_data <- NULL
values$hover_data <- NULL
values$selected_data <- NULL
# Actually randomize the data source and settings for a visible effect
random_sources <- c("mtcars", "iris", "economics", "geo_data")
new_source <- sample(random_sources, 1)
# Update the data source
updateSelectInput(session, "data_source", selected = new_source)
# Randomize some plot settings too
random_features <- c("scatter", "hover", "3d", "combo")
updateSelectInput(session, "plot_feature", selected = sample(random_features, 1))
# Randomize theme and colors
themes <- c("default", "plotly_dark", "plotly_white", "simple_white")
updateSelectInput(session, "plot_theme", selected = sample(themes, 1))
palettes <- c("viridis", "set1", "pastel", "dark2")
updateSelectInput(session, "color_palette", selected = sample(palettes, 1))
# Show notification about what changed
showNotification(
paste("Randomized to:", new_source, "data with random settings!"),
type = "message",
duration = 10
)
})
# Context-aware download handling
if (APP_CONTEXT == "shinylive") {
# Shinylive: Show guidance notification
observeEvent(input$download_help, {
showNotification(
HTML(paste0(
bs_icon("camera"), " <strong>Tip:</strong> Use the camera icon in the plot toolbar above to download as PNG!<br>",
bs_icon("tools"), " The toolbar also has zoom, pan, and selection tools."
)),
type = "message", duration = 5
)
})
} else {
# Shiny Server: Actual download with HTML export + notification
output$download_plotly <- downloadHandler(
filename = function() { paste("plotly_plot_", Sys.Date(), ".html", sep = "") },
content = function(file) {
data <- plot_data()
req(nrow(data) > 0)
# Recreate the current plot for download
p <- switch(input$plot_feature,
"scatter" = create_scatter_plot(data),
"hover" = create_hover_plot(data),
"3d" = create_3d_plot(data),
"animation" = create_animated_plot(data),
"combo" = create_combo_plot(data),
"geo" = create_geo_plot(data)
)
if (!is.null(p)) {
# Apply current theme
p <- p %>% layout(template = input$plot_theme)
tryCatch({
# Save as self-contained HTML (works in shiny server)
htmlwidgets::saveWidget(p, file, selfcontained = TRUE)
# Show notification about PNG option
showNotification(
HTML(paste0(
bs_icon("check-circle"), " <strong>HTML file downloaded!</strong><br>",
bs_icon("camera"), " Tip: You can also use the camera icon in the plot to save as PNG directly."
)),
type = "success", duration = 10
)
}, error = function(e) {
showNotification(
"HTML export failed. Use camera icon for PNG export.",
type = "warning", duration = 4
)
})
}
}
)
}
}
shinyApp(ui = ui, server = server)
Advanced Plotly Features
Create sophisticated interactive visualizations with custom hover information and animations:
$plotly_custom <- renderPlotly({
output<- plot_ly(mtcars,
p x = ~wt, y = ~mpg, color = ~factor(cyl),
type = "scatter", mode = "markers",
hovertemplate = paste(
"<b>%{text}</b><br>",
"Weight: %{x:.2f} tons<br>",
"MPG: %{y:.1f}<br>",
"Cylinders: %{color}<br>",
"<extra></extra>"
),text = rownames(mtcars)) %>%
layout(title = "Car Performance Data",
xaxis = list(title = "Weight (1000 lbs)"),
yaxis = list(title = "Miles per Gallon"))
p })
$plotly_3d <- renderPlotly({
outputplot_ly(mtcars,
x = ~wt, y = ~hp, z = ~mpg,
color = ~factor(cyl),
type = "scatter3d", mode = "markers",
marker = list(size = 5)) %>%
layout(title = "3D Car Performance",
scene = list(
xaxis = list(title = "Weight"),
yaxis = list(title = "Horsepower"),
zaxis = list(title = "MPG")
)) })
# Assuming we have time-series data
$plotly_animated <- renderPlotly({
output# Create sample time-series data
<- data.frame(
time_data year = rep(2018:2022, each = 32),
car = rep(rownames(mtcars), 5),
mpg = rep(mtcars$mpg, 5) + rnorm(160, 0, 1),
wt = rep(mtcars$wt, 5) + rnorm(160, 0, 0.1)
)
plot_ly(time_data,
x = ~wt, y = ~mpg,
frame = ~year,
color = ~car,
type = "scatter", mode = "markers") %>%
animation_opts(frame = 1000, transition = 500) %>%
layout(title = "Animated Car Performance Over Time")
})
Plotly Event Handling
Capture user interactions with plotly plots for advanced functionality:
# UI
plotlyOutput("interactive_plotly"),
verbatimTextOutput("plotly_click_info")
# Server
$interactive_plotly <- renderPlotly({
outputplot_ly(mtcars, x = ~wt, y = ~mpg,
source = "cars_plot") %>% # Important: set source
add_markers()
})
# Capture click events
$plotly_click_info <- renderPrint({
output<- event_data("plotly_click", source = "cars_plot")
click_data if(!is.null(click_data)) {
paste("Clicked point - X:", click_data$x, "Y:", click_data$y)
else {
} "Click on a point to see details"
} })
Data Tables and Interactive Tables
Tables present detailed data that users can sort, filter, and explore. Shiny offers multiple approaches from simple displays to feature-rich interactive tables.
Basic Table Output
For simple data display without interaction:
# UI
tableOutput("simple_table")
# Server
$simple_table <- renderTable({
outputhead(mtcars, 10)
striped = TRUE, hover = TRUE, bordered = TRUE) },
DT Package for Interactive Tables
The DT package transforms basic tables into powerful data exploration tools:
library(DT)
# UI
::dataTableOutput("dt_basic")
DT
# Server
$dt_basic <- DT::renderDataTable({
output
mtcarsoptions = list(
}, pageLength = 10,
scrollX = TRUE,
searchHighlight = TRUE
))
$dt_advanced <- DT::renderDataTable({
output
mtcarsoptions = list(
}, pageLength = 15,
scrollX = TRUE,
dom = 'Bfrtip', # Add buttons
buttons = list(
list(extend = 'csv', filename = 'car_data'),
list(extend = 'excel', filename = 'car_data'),
list(extend = 'pdf', filename = 'car_data')
),columnDefs = list(
list(targets = c(0, 1), className = 'dt-center'),
list(targets = '_all', className = 'dt-nowrap')
)extensions = 'Buttons') ),
$dt_editable <- DT::renderDataTable({
output
mtcarseditable = TRUE, options = list(
}, pageLength = 10,
scrollX = TRUE
))
# Handle edits
observeEvent(input$dt_editable_cell_edit, {
<- input$dt_editable_cell_edit
info # Process the edit
showNotification(paste("Cell edited: Row", info$row,
"Column", info$col, "New value:", info$value))
})
Custom Table Formatting
Create professional-looking tables with conditional formatting:
$formatted_table <- DT::renderDataTable({
output::datatable(mtcars, options = list(pageLength = 10)) %>%
DT::formatStyle(
DT'mpg',
backgroundColor = DT::styleInterval(c(15, 25),
c('red', 'yellow', 'green')),
color = 'white'
%>%
) ::formatStyle(
DT'hp',
background = DT::styleColorBar(range(mtcars$hp), 'lightblue'),
backgroundSize = '100% 90%',
backgroundRepeat = 'no-repeat',
backgroundPosition = 'center'
%>%
) ::formatRound(c('mpg', 'wt'), 1)
DT })
Go beyond basic examples with comprehensive interactive table configuration:
While the DT examples show core functionality, mastering the full feature set requires understanding how different options work together. Interactive configuration helps you discover the perfect combination for your specific use case.
Try the DT Configuration Playground →
Test every DT feature in real-time, see live code generation, and understand how professional table configurations enhance your output displays with advanced search, styling, and interaction patterns.
Professional Multi-Output Integration
Create sophisticated multi-output dashboards with integrated components:
- Combine output types - Integrate plots, tables, and text into cohesive displays
- Connect interactions - See how plot selections update tables and summaries
- Apply consistent styling - Create professional, branded display layouts
- Test responsiveness - Experience how displays adapt to different screen sizes
- Generate reports - Export complete displays as professional documents
Key Learning: Professional applications integrate multiple output types seamlessly, where each component enhances the others to create comprehensive data exploration experiences.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 1200
# Professional Data Display Builder
# Integrated dashboard showing multiple output types working together
library(shiny)
library(bsicons)
library(ggplot2)
library(plotly)
library(DT)
library(dplyr)
# This solves the issue of the download button not working from Chromium when this app is deployed as Shinylive
downloadButton <- function(...) {
tag <- shiny::downloadButton(...)
tag$attribs$download <- NULL
tag
}
ui <- fluidPage(
theme = bslib::bs_theme(version = 5, bootswatch = "cosmo"),
tags$head(
tags$style(HTML("
.dashboard-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 30px;
text-align: center;
}
.control-panel {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
position: sticky;
top: 20px;
}
.display-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.display-card h4 {
color: #495057;
border-bottom: 2px solid #e9ecef;
padding-bottom: 10px;
margin-bottom: 20px;
}
.summary-stats {
background: #e7f3ff;
border: 1px solid #007bff;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
}
.stat-item {
display: inline-block;
margin: 0 15px 10px 0;
padding: 8px 12px;
background: white;
border-radius: 4px;
border: 1px solid #dee2e6;
}
.selection-info {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
padding: 10px;
margin-top: 10px;
font-size: 0.9em;
}
.export-section {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin-top: 20px;
}
"))
),
# Dashboard Header
div(class = "dashboard-header",
h1(bs_icon("layout-three-columns"), "Professional Data Display Builder"),
p("Experience how multiple output types work together in professional applications",
class = "lead")
),
fluidRow(
# Control Panel
column(3,
div(class = "control-panel",
h4(bs_icon("gear"), "Dashboard Controls"),
# Dataset Selection
selectInput("dataset_choice", "Dataset:",
choices = list(
"Motor Trends Cars" = "mtcars",
"Iris Flowers" = "iris",
"Economics Data" = "economics"
),
selected = "mtcars"),
# Display Mode
selectInput("display_mode", "Display Mode:",
choices = list(
"Executive Summary" = "executive",
"Detailed Analysis" = "detailed",
"Comparison View" = "comparison"
),
selected = "executive"),
# Filter Controls
conditionalPanel(
condition = "input.dataset_choice == 'mtcars'",
h5("Filter Options:"),
sliderInput("mpg_filter", "MPG Range:",
min = 10, max = 35, value = c(10, 35)),
checkboxGroupInput("cyl_filter", "Cylinders:",
choices = list("4" = 4, "6" = 6, "8" = 8),
selected = c(4, 6, 8))
),
conditionalPanel(
condition = "input.dataset_choice == 'iris'",
h5("Filter Options:"),
checkboxGroupInput("species_filter", "Species:",
choices = list("Setosa" = "setosa",
"Versicolor" = "versicolor",
"Virginica" = "virginica"),
selected = c("setosa", "versicolor", "virginica"))
),
hr(),
# Display Options
h5("Display Options:"),
checkboxInput("show_plot", "Show Visualization", TRUE),
checkboxInput("show_table", "Show Data Table", TRUE),
checkboxInput("show_summary", "Show Summary Stats", TRUE),
hr(),
# Quick Presets
h5("Quick Presets:"),
div(
actionButton("preset_overview", "Overview",
class = "btn-outline-primary btn-sm mb-2 w-100"),
actionButton("preset_detailed", "Detailed",
class = "btn-outline-success btn-sm mb-2 w-100"),
actionButton("preset_minimal", "Minimal",
class = "btn-outline-secondary btn-sm w-100")
)
)
),
# Main Display Area
column(9,
# Summary Statistics
conditionalPanel(
condition = "input.show_summary",
div(class = "summary-stats",
h4(bs_icon("graph-up"), "Dataset Overview"),
uiOutput("summary_statistics"),
div(class = "selection-info",
textOutput("selection_summary"))
)
),
# Main Visualization
conditionalPanel(
condition = "input.show_plot",
div(class = "display-card",
h4(bs_icon("bar-chart"), "Interactive Visualization"),
p("Click on points to see details. Selection will update the table below.",
class = "text-muted"),
plotlyOutput("main_visualization", height = "400px"),
div(class = "selection-info",
textOutput("plot_interaction_info"))
)
),
# Data Table
conditionalPanel(
condition = "input.show_table",
div(class = "display-card",
h4(bs_icon("table"), "Interactive Data Table"),
p("Search, sort, and filter the data. Table updates based on plot selections.",
class = "text-muted"),
DT::dataTableOutput("interactive_table")
)
),
# Export and Report Section
div(class = "export-section",
h5(bs_icon("download"), "Export Options"),
p("Generate professional reports and export data in various formats. Note: Use the camera icon in the plot toolbar to download as PNG.",
class = "text-muted"),
fluidRow(
column(3, downloadButton("export_data", "Export Data",
class = "btn-outline-primary btn-sm w-100")),
column(3, actionButton("export_plot", "Export Plot Help",
class = "btn-outline-success btn-sm w-100")),
column(3, downloadButton("export_report", "Generate Report",
class = "btn-outline-info btn-sm w-100")),
column(3, actionButton("refresh_dashboard", "Refresh All",
class = "btn-outline-secondary btn-sm w-100"))
)
)
)
)
)
server <- function(input, output, session) {
# Reactive values for tracking selections and interactions
values <- reactiveValues(
selected_points = NULL,
plot_click = NULL,
table_selection = NULL
)
# Get filtered dataset
filtered_data <- reactive({
req(input$dataset_choice)
base_data <- switch(input$dataset_choice,
"mtcars" = mtcars %>%
mutate(car_name = rownames(mtcars)),
"iris" = iris,
"economics" = economics
)
# Apply filters
if (input$dataset_choice == "mtcars") {
base_data <- base_data %>%
filter(mpg >= input$mpg_filter[1] & mpg <= input$mpg_filter[2]) %>%
filter(cyl %in% input$cyl_filter)
} else if (input$dataset_choice == "iris") {
base_data <- base_data %>%
filter(Species %in% input$species_filter)
}
base_data
})
# Summary statistics
output$summary_statistics <- renderUI({
data <- filtered_data()
if (input$dataset_choice == "mtcars") {
stats <- list(
"Total Cars" = nrow(data),
"Avg MPG" = round(mean(data$mpg), 1),
"Avg HP" = round(mean(data$hp), 0),
"Avg Weight" = round(mean(data$wt), 2)
)
} else if (input$dataset_choice == "iris") {
stats <- list(
"Total Flowers" = nrow(data),
"Species Count" = length(unique(data$Species)),
"Avg Sepal Length" = round(mean(data$Sepal.Length), 1),
"Avg Petal Length" = round(mean(data$Petal.Length), 1)
)
} else {
stats <- list(
"Total Records" = nrow(data),
"Date Range" = paste(range(data$date), collapse = " to "),
"Variables" = ncol(data)
)
}
stat_elements <- lapply(names(stats), function(name) {
div(class = "stat-item",
strong(name, ": "), stats[[name]])
})
do.call(tagList, stat_elements)
})
# Main visualization
output$main_visualization <- renderPlotly({
data <- filtered_data()
if (input$dataset_choice == "mtcars") {
p <- plot_ly(data, x = ~wt, y = ~mpg, color = ~factor(cyl),
text = ~car_name, source = "main_plot",
hovertemplate = paste(
"<b>%{text}</b><br>",
"Weight: %{x:.2f} tons<br>",
"MPG: %{y:.1f}<br>",
"Cylinders: %{color}<br>",
"<extra></extra>"
)) %>%
add_markers(size = I(8)) %>%
layout(title = "Car Performance Analysis",
xaxis = list(title = "Weight (1000 lbs)"),
yaxis = list(title = "Miles per Gallon"),
showlegend = TRUE)
} else if (input$dataset_choice == "iris") {
p <- plot_ly(data, x = ~Sepal.Length, y = ~Sepal.Width,
color = ~Species, source = "main_plot",
hovertemplate = paste(
"<b>%{color} Iris</b><br>",
"Sepal Length: %{x:.1f} cm<br>",
"Sepal Width: %{y:.1f} cm<br>",
"<extra></extra>"
)) %>%
add_markers(size = I(8)) %>%
layout(title = "Iris Species Comparison",
xaxis = list(title = "Sepal Length (cm)"),
yaxis = list(title = "Sepal Width (cm)"))
} else {
p <- plot_ly(data, x = ~date, y = ~unemploy,
type = "scatter", mode = "lines+markers",
source = "main_plot") %>%
layout(title = "Economic Trends Over Time",
xaxis = list(title = "Date"),
yaxis = list(title = "Unemployment"))
}
p %>% event_register("plotly_click") %>%
event_register("plotly_selected")
})
# Interactive data table
output$interactive_table <- DT::renderDataTable({
data <- filtered_data()
# Highlight selected rows if any
selected_rows <- NULL
if (!is.null(values$selected_points)) {
# Find matching rows based on plot selection
if (input$dataset_choice == "mtcars") {
selected_rows <- which(data$car_name %in% values$selected_points$text)
}
}
DT::datatable(data,
selection = list(mode = "multiple", selected = selected_rows),
options = list(
pageLength = 10,
scrollX = TRUE,
searchHighlight = TRUE,
dom = 'Bfrtip',
buttons = list(
list(extend = 'csv', filename = 'filtered_data'),
list(extend = 'excel', filename = 'filtered_data')
)
),
extensions = 'Buttons') %>%
DT::formatRound(columns = which(sapply(data, is.numeric)), digits = 2)
})
# Handle plot interactions
observe({
click_data <- event_data("plotly_click", source = "main_plot")
if (!is.null(click_data)) {
values$plot_click <- click_data
}
})
observe({
selected_data <- event_data("plotly_selected", source = "main_plot")
if (!is.null(selected_data)) {
values$selected_points <- selected_data
}
})
# Display interaction information
output$selection_summary <- renderText({
data <- filtered_data()
paste("Showing", nrow(data), "records after filtering")
})
output$plot_interaction_info <- renderText({
if (!is.null(values$plot_click)) {
paste("Last clicked point: (", round(values$plot_click$x, 2), ",",
round(values$plot_click$y, 2), ")")
} else if (!is.null(values$selected_points)) {
paste(nrow(values$selected_points), "points selected")
} else {
"Click or select points to see details"
}
})
# When display_mode selection changes, update visibility of elements
observeEvent(input$display_mode, {
if (input$display_mode == "executive") {
updateCheckboxInput(session, "show_plot", value = TRUE)
updateCheckboxInput(session, "show_table", value = FALSE)
updateCheckboxInput(session, "show_summary", value = TRUE)
} else if (input$display_mode == "detailed") {
updateCheckboxInput(session, "show_plot", value = TRUE)
updateCheckboxInput(session, "show_table", value = TRUE)
updateCheckboxInput(session, "show_summary", value = TRUE)
} else {
updateCheckboxInput(session, "show_plot", value = TRUE)
updateCheckboxInput(session, "show_table", value = FALSE)
updateCheckboxInput(session, "show_summary", value = FALSE)
}
})
# Preset configurations
observeEvent(input$preset_overview, {
updateSelectInput(session, "display_mode", selected = "executive")
updateCheckboxInput(session, "show_plot", value = TRUE)
updateCheckboxInput(session, "show_table", value = FALSE)
updateCheckboxInput(session, "show_summary", value = TRUE)
})
observeEvent(input$preset_detailed, {
updateSelectInput(session, "display_mode", selected = "detailed")
updateCheckboxInput(session, "show_plot", value = TRUE)
updateCheckboxInput(session, "show_table", value = TRUE)
updateCheckboxInput(session, "show_summary", value = TRUE)
})
observeEvent(input$preset_minimal, {
updateSelectInput(session, "display_mode", selected = "comparison")
updateCheckboxInput(session, "show_plot", value = TRUE)
updateCheckboxInput(session, "show_table", value = FALSE)
updateCheckboxInput(session, "show_summary", value = FALSE)
})
# Export handlers
output$export_data <- downloadHandler(
filename = function() {
paste("dashboard_data_", Sys.Date(), ".csv", sep = "")
},
content = function(file) {
write.csv(filtered_data(), file, row.names = FALSE)
}
)
output$export_report <- downloadHandler(
filename = function() {
paste("dashboard_report_", Sys.Date(), ".html", sep = "")
},
content = function(file) {
# Create a simple HTML report
data <- filtered_data()
report_content <- paste0(
"<html><head><title>Dashboard Report</title>",
"<style>body{font-family: Arial, sans-serif; margin: 40px;}",
"h1{color: #007bff;} table{border-collapse: collapse; width: 100%;}",
"th, td{border: 1px solid #ddd; padding: 8px; text-align: left;}",
"th{background-color: #f2f2f2;}</style></head><body>",
"<h1>Data Dashboard Report</h1>",
"<p>Generated on: ", Sys.Date(), "</p>",
"<h2>Dataset: ", input$dataset_choice, "</h2>",
"<p>Total records: ", nrow(data), "</p>",
"<h2>Data Summary</h2>",
"<table>",
"<tr><th>Variable</th><th>Type</th><th>Summary</th></tr>"
)
# Add summary for each column
for (col in names(data)[1:min(5, ncol(data))]) {
col_summary <- if (is.numeric(data[[col]])) {
paste("Mean:", round(mean(data[[col]], na.rm = TRUE), 2))
} else {
paste("Unique values:", length(unique(data[[col]])))
}
report_content <- paste0(report_content,
"<tr><td>", col, "</td><td>", class(data[[col]])[1],
"</td><td>", col_summary, "</td></tr>")
}
report_content <- paste0(report_content,
"</table>",
"<p><em>Report generated by Professional Data Display Builder</em></p>",
"</body></html>")
writeLines(report_content, file)
}
)
observeEvent(input$export_plot, {
showNotification(
HTML(paste0(
bs_icon("camera"), " <strong>Tip:</strong> Use the camera icon in the plot toolbar above to download as PNG!<br>",
bs_icon("tools"), " The toolbar also has zoom, pan, and selection tools."
)),
type = "message", duration = 5
)
})
# Refresh dashboard
observeEvent(input$refresh_dashboard, {
values$selected_points <- NULL
values$plot_click <- NULL
values$table_selection <- NULL
showNotification("Dashboard refreshed", type = "success", duration = 2)
})
}
shinyApp(ui = ui, server = server)
Download Outputs and File Generation
Enable users to export data, reports, and visualizations from your application.
Basic Download Handler
# UI
downloadButton("download_data", "Download Data",
class = "btn-primary")
# Server
$download_data <- downloadHandler(
outputfilename = function() {
paste("car_data_", Sys.Date(), ".csv", sep = "")
},content = function(file) {
write.csv(mtcars, file, row.names = TRUE)
} )
Multiple File Format Downloads
Offer users choice in download formats:
# UI
selectInput("download_format", "Choose format:",
choices = list("CSV" = "csv", "Excel" = "xlsx", "RDS" = "rds")),
downloadButton("download_flexible", "Download Data")
# Server
$download_flexible <- downloadHandler(
outputfilename = function() {
paste("data_export.", input$download_format, sep = "")
},content = function(file) {
switch(input$download_format,
"csv" = write.csv(mtcars, file, row.names = FALSE),
"xlsx" = openxlsx::write.xlsx(mtcars, file),
"rds" = saveRDS(mtcars, file)
)
} )
# UI
downloadButton("download_plot", "Download Plot")
# Server
$download_plot <- downloadHandler(
outputfilename = function() {
paste("plot_", Sys.Date(), ".png", sep = "")
},content = function(file) {
ggsave(file, plot = ggplot(mtcars, aes(x = wt, y = mpg)) +
geom_point() + theme_minimal(),
width = 10, height = 6, dpi = 300)
} )
# UI
downloadButton("download_report", "Generate Report")
# Server
$download_report <- downloadHandler(
outputfilename = function() {
paste("analysis_report_", Sys.Date(), ".html", sep = "")
},content = function(file) {
# Create temporary R Markdown file
<- file.path(tempdir(), "report.Rmd")
temp_report
# Write R Markdown content
writeLines(c(
"---",
"title: 'Car Data Analysis Report'",
"output: html_document",
"---",
"",
"```{r echo=FALSE}",
"library(ggplot2)",
"data(mtcars)",
"```",
"",
"## Summary Statistics",
"```{r echo=FALSE}",
"summary(mtcars)",
"```",
"",
"## Visualization",
"```{r echo=FALSE}",
"ggplot(mtcars, aes(x = wt, y = mpg)) + geom_point() + theme_minimal()",
"```"
), temp_report)
# Render the report
::render(temp_report, output_file = file)
rmarkdown
} )
Custom and Advanced Output Types
Beyond standard outputs, Shiny supports custom HTML widgets and specialized visualization libraries.
HTML Widgets Integration
Shiny seamlessly integrates with the htmlwidgets ecosystem:
library(leaflet)
library(networkD3)
# Interactive Maps
$map <- renderLeaflet({
outputleaflet() %>%
addTiles() %>%
addMarkers(lng = -74.0059, lat = 40.7128,
popup = "New York City")
})
# Network Visualizations
$network <- renderForceNetwork({
output# Create sample network data
<- data.frame(
nodes name = c("A", "B", "C", "D"),
group = c(1, 1, 2, 2)
)<- data.frame(
links source = c(0, 1, 2),
target = c(1, 2, 3),
value = c(1, 1, 1)
)
forceNetwork(Links = links, Nodes = nodes,
Source = "source", Target = "target",
Value = "value", NodeID = "name",
Group = "group")
})
Dynamic UI Generation
Create outputs that generate UI elements dynamically:
# UI
uiOutput("dynamic_content")
# Server
$dynamic_content <- renderUI({
output<- input$n_plots # Assume this comes from a slider
n_plots
<- lapply(1:n_plots, function(i) {
plot_outputs plotOutput(paste0("plot_", i), height = "300px")
})
do.call(tagList, plot_outputs)
})
# Generate the individual plots
observe({
<- input$n_plots
n_plots
for(i in 1:n_plots) {
local({
<- paste0("plot_", i)
plot_id <- renderPlot({
output[[plot_id]] <- mtcars[sample(nrow(mtcars), 10), ]
sample_data plot(sample_data$wt, sample_data$mpg,
main = paste("Plot", i))
})
})
} })
Performance Optimization for Complex Outputs
As your applications grow more sophisticated, optimizing output performance becomes crucial for user experience.
Efficient Data Processing
Optimize data preparation for complex outputs:
# Use reactive expressions to cache expensive computations
<- reactive({
processed_data # Expensive data processing
heavy_computation(raw_data())
})
# Use debouncing for responsive inputs
<- reactive({
processed_data_debounced $filter_text # Trigger
inputinvalidateLater(500) # Wait 500ms before updating
# Your processing code
%>% debounce(500) })
Conditional Rendering
Render outputs only when necessary:
# Conditional plot rendering
$conditional_plot <- renderPlot({
outputreq(input$show_plot) # Only render if checkbox is checked
if(nrow(filtered_data()) > 0) {
ggplot(filtered_data(), aes(x = x, y = y)) + geom_point()
else {
} # Return empty plot for no data
ggplot() + theme_void() +
labs(title = "No data available for current filters")
} })
Large Dataset Handling
Manage large datasets efficiently:
# Server-side processing for large tables
$large_table <- DT::renderDataTable({
output# Your large dataset
big_data server = TRUE, options = list(
}, processing = TRUE,
pageLength = 25,
searchDelay = 500
))
# Pagination for plots
$paginated_plots <- renderUI({
output<- 20
page_size <- input$plot_page %||% 1
current_page
<- (current_page - 1) * page_size + 1
start_idx <- min(current_page * page_size, nrow(data))
end_idx
<- data[start_idx:end_idx, ]
current_data
plotOutput("current_page_plot")
})
Common Issues and Solutions
Issue 1: Plots Not Displaying or Appearing Blank
Problem: Plots render without errors but show empty or blank output.
Solution:
Check data availability and plot generation:
# Add debugging and data validation
$debug_plot <- renderPlot({
outputreq(input$data_source) # Ensure input exists
<- get_data()
data req(nrow(data) > 0) # Ensure data has rows
# Add debugging output
print(paste("Data dimensions:", nrow(data), "x", ncol(data)))
# Your plot code with error handling
tryCatch({
ggplot(data, aes(x = x, y = y)) + geom_point()
error = function(e) {
}, # Return informative error plot
ggplot() + theme_void() +
labs(title = paste("Plot Error:", e$message))
}) })
Issue 2: Tables Not Updating Reactively
Problem: Data table doesn’t refresh when underlying data changes.
Solution:
Ensure proper reactive dependencies:
# Problematic approach
$static_table <- DT::renderDataTable({
output# This won't update
static_data
})
# Correct reactive approach
$reactive_table <- DT::renderDataTable({
outputfiltered_data() # Properly reactive data source
options = list(
}, pageLength = 10,
searching = TRUE
))
# Force table updates when needed
observeEvent(input$refresh_data, {
# Trigger data refresh
refresh_data_source()
# Optional: Use DT proxy for efficient updates
::replaceData(DT::dataTableProxy("reactive_table"),
DTfiltered_data())
})
Issue 3: Download Handlers Not Working
Problem: Download buttons don’t trigger file downloads or produce errors.
Solution:
Debug download handler implementation:
# Add error handling and debugging
$debug_download <- downloadHandler(
outputfilename = function() {
paste("data_", Sys.Date(), ".csv", sep = "")
},content = function(file) {
tryCatch({
<- get_current_data()
data_to_download
# Validate data exists
if(is.null(data_to_download) || nrow(data_to_download) == 0) {
stop("No data available for download")
}
write.csv(data_to_download, file, row.names = FALSE)
error = function(e) {
}, # Log error for debugging
cat("Download error:", e$message, "\n")
# Create error file
writeLines(paste("Error generating download:", e$message), file)
})
} )
- Use
req()
to validate inputs before expensive computations - Cache intermediate results with reactive expressions
- Implement conditional rendering for complex outputs
- Use server-side processing for large datasets in DT tables
- Add loading indicators for slow-rendering outputs
Common Questions About Shiny Outputs
Use plotly when you need interactivity that enhances understanding - zooming into detailed data, hovering for additional information, or allowing users to toggle data series. Plotly is excellent for exploratory dashboards where users need to drill down into data. However, stick with regular ggplot2 for simple displays, static reports, or when plot performance is critical. Plotly adds overhead and complexity that isn’t always necessary.
Enable server-side processing with server = TRUE
in your DT options, which processes data on the R server rather than sending everything to the browser. Implement pagination with reasonable page sizes (25-50 rows), add search delays to prevent excessive filtering, and consider pre-aggregating data when possible. For extremely large datasets, implement custom filtering logic that limits results before sending to DT.
Use R Markdown with parameterized reports within your download handler. Create a template .Rmd file that accepts parameters from your Shiny app, then use rmarkdown::render()
to generate HTML, PDF, or Word documents. This approach allows you to include dynamic content, formatted tables, plots, and narrative text in a professional report format that users can save and share.
Use relative sizing (width = "100%"
, height = "auto"
), implement flexible plot dimensions that adjust to container size, and use Bootstrap-compatible layout functions. For DT tables, enable horizontal scrolling with scrollX = TRUE
. Consider using CSS media queries for custom styling that adapts to different screen sizes, and test your applications on mobile devices during development.
renderTable()
creates static HTML tables suitable for small datasets and simple display needs. DT::renderDataTable()
creates interactive tables with sorting, filtering, pagination, and search capabilities. Use renderTable() for summary statistics or small reference tables, and DT for data exploration, large datasets, or when users need to interact with the data directly.
Test Your Understanding
Which UI-Server function pairs are correctly matched for creating different types of outputs?
plotOutput()
withrenderPlotly()
,textOutput()
withrenderText()
plotlyOutput()
withrenderPlot()
,tableOutput()
withDT::renderDataTable()
plotlyOutput()
withrenderPlotly()
,DT::dataTableOutput()
withDT::renderDataTable()
htmlOutput()
withrenderTable()
,plotOutput()
withrenderUI()
- Each output type requires a specific UI and render function pair
- The UI function declares where output appears, render function creates the content
- Library-specific outputs (like plotly, DT) require their own function pairs
C) plotlyOutput()
with renderPlotly()
, DT::dataTableOutput()
with DT::renderDataTable()
Correct function pairs must match in both name and purpose:
- plotlyOutput() ↔︎ renderPlotly(): For interactive plotly visualizations
- DT::dataTableOutput() ↔︎ DT::renderDataTable(): For interactive data tables
- plotOutput() ↔︎ renderPlot() (for static plots)
- textOutput() ↔︎ renderText() (for simple text)
- htmlOutput() ↔︎ renderUI() (for HTML content)
Option A mixes plotly render with regular plot UI, Option B reverses the plotly functions, and Option D pairs unrelated functions.
Complete this code to optimize a data table for large datasets:
$optimized_table <- DT::renderDataTable({
output
large_dataset_______ = TRUE, options = list(
}, _______ = TRUE,
pageLength = _______,
_______ = 500
))
Fill in the blanks for optimal performance with large datasets.
- Think about where data processing should happen for large datasets
- Consider what helps indicate to users that processing is happening
- What’s a reasonable number of rows to display per page?
- How can you prevent excessive search queries?
$optimized_table <- DT::renderDataTable({
output
large_datasetserver = TRUE, options = list(
}, processing = TRUE,
pageLength = 25,
searchDelay = 500
))
Key optimizations explained:
server = TRUE
: Processes data on R server instead of sending all data to browserprocessing = TRUE
: Shows loading indicator during data processingpageLength = 25
: Reasonable page size that balances usability with performancesearchDelay = 500
: Waits 500ms after user stops typing before filtering, preventing excessive queries
You’re building a dashboard that needs to display both summary statistics and detailed data exploration. Users should be able to click on plot points to see related information. Which approach provides the best user experience?
- Use separate static ggplot2 plots with reactive text summaries
- Use plotly with custom hover info and event handling for point clicks
- Use base R plots with click coordinates to filter a separate data table
- Create multiple static plots showing different data subsets
- Consider what happens when users interact with the visualization
- Think about the smoothest way to connect plot interactions with detailed information
- Which approach provides the most seamless exploration experience?
B) Use plotly with custom hover info and event handling for point clicks
This approach provides the optimal user experience because:
# Optimal implementation
$interactive_viz <- renderPlotly({
outputplot_ly(data, x = ~x, y = ~y,
source = "main_plot",
hovertemplate = "Custom info: %{text}<extra></extra>",
text = ~detailed_info) %>%
add_markers()
})
# Handle clicks seamlessly
observeEvent(event_data("plotly_click", source = "main_plot"), {
<- event_data("plotly_click", source = "main_plot")
clicked_data # Update related outputs or show detailed information
})
Why this is best:
- Immediate feedback: Hover shows information without clicking
- Seamless interaction: Click events can trigger detailed views
- Professional feel: Smooth animations and transitions
- Data exploration: Users can zoom, pan, and explore naturally
Options A and D lack interactivity, while C requires more complex coordinate handling.
Conclusion
Mastering Shiny’s output system transforms your applications from simple data processors into compelling interactive experiences that users genuinely want to explore. You’ve learned to create everything from basic text displays to sophisticated interactive visualizations, each serving specific purposes in your application’s storytelling arsenal.
The techniques covered in this guide - from plotly integration and DT table customization to download handlers and performance optimization - form the foundation for building professional-grade applications. Understanding when to use each output type, how to optimize performance, and how to create seamless user interactions will serve you well as you build increasingly sophisticated Shiny applications.
Your journey through Shiny’s output ecosystem prepares you to create applications that not only analyze data effectively but present insights in ways that drive understanding and decision-making for your users.
Next Steps
Based on what you’ve learned about Shiny outputs, here are the recommended paths for advancing your Shiny development skills:
Immediate Next Steps (Complete These First)
- Styling and Custom Themes in Shiny - Learn to create beautiful, branded applications that showcase your outputs professionally
- Responsive Design for Shiny Apps - Ensure your outputs look great on all devices and screen sizes
- Practice Exercise: Enhance your first Shiny app by replacing basic outputs with interactive plotly visualizations and DT tables
Building on Your Foundation (Choose Your Path)
For Advanced Visualization Focus:
For Data Management Focus:
For Production Applications:
Long-term Goals (2-4 Weeks)
- Build a comprehensive dashboard that integrates multiple output types effectively
- Create a data exploration application with advanced interactive features
- Develop a reporting system that generates downloadable documents with embedded visualizations
- Contribute to the Shiny community by sharing innovative output techniques or visualizations
Explore More Articles
Here are more articles from the same category to help you dive deeper into the topic.
Reuse
Citation
@online{kassambara2025,
author = {Kassambara, Alboukadel},
title = {Shiny {Output} {Types} and {Visualization:} {Complete}
{Display} {Guide}},
date = {2025-05-23},
url = {https://www.datanovia.com/learn/tools/shiny-apps/ui-design/output-displays.html},
langid = {en}
}