Shiny Output Types and Visualization: Complete Display Guide

Master All Output Methods for Professional Interactive Applications

Learn to create compelling data displays with Shiny’s comprehensive output system. Master plots, tables, text, and interactive visualizations with practical examples and best practices for professional applications.

Tools
Author
Affiliation
Published

May 23, 2025

Modified

July 2, 2025

Keywords

shiny output types, shiny data visualization, plotly shiny integration, DT datatable shiny, interactive plots R, shiny render functions

Key Takeaways

Tip
  • 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.

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

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
server <- function(input, output) {
  output$my_plot <- renderPlot({
    # Plot generation code
  })
  
  output$my_table <- renderTable({
    # Table generation code
  })
  
  output$my_summary <- renderText({
    # 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

Interactive Demo: Master All Shiny Output Types

Explore the complete output ecosystem with hands-on experimentation:

  1. Test every output type - Switch between plots, tables, text, and interactive displays
  2. Adjust parameters - See how different settings affect rendering and performance
  3. Compare approaches - Understand when to use each output type for maximum impact
  4. Experience downloads - Test different export formats and generation methods
  5. 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
output$simple_text <- renderText({
  paste("Current time:", Sys.time())
})
# UI  
textOutput("data_summary")

# Server
output$data_summary <- renderText({
  data <- mtcars
  paste("Dataset contains", nrow(data), 
        "observations and", ncol(data), "variables")
})
# UI
sliderInput("n_rows", "Number of rows:", 1, 100, 50),
textOutput("filtered_summary")

# Server
output$filtered_summary <- renderText({
  filtered_data <- head(mtcars, input$n_rows)
  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
output$formatted_summary <- renderUI({
  data_stats <- summary(mtcars$mpg)
  
  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
output$model_summary <- renderPrint({
  model <- lm(mpg ~ wt + hp, data = mtcars)
  summary(model)
})
Choosing the Right Text Output Type
  • textOutput(): Single values, simple strings, computed statistics
  • htmlOutput(): Formatted text with HTML tags, styled content
  • verbatimTextOutput(): 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
output$ggplot_example <- renderPlot({
  ggplot(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
output$base_plot <- renderPlot({
  plot(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
output$advanced_plot <- renderPlot({
  base_plot <- ggplot(mtcars, aes(x = wt, y = mpg))
  
  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
output$publication_plot <- renderPlot({
  # 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, {
  clicked_point <- nearPoints(mtcars, input$plot_click)
  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
output$plotly_basic <- renderPlotly({
  p <- ggplot(mtcars, aes(x = wt, y = mpg, color = factor(cyl))) +
    geom_point(size = 3) +
    theme_minimal() +
    labs(title = "Interactive Fuel Efficiency Plot")
  
  ggplotly(p)  # Convert ggplot to plotly
})

Master Advanced Plotly Features

Interactive Demo: Master Advanced Plotly Features

Explore sophisticated interactive visualization techniques:

  1. Build custom interactions - Create plots with click, hover, and selection events
  2. Apply advanced styling - Master custom themes, annotations, and layouts
  3. Combine plot types - Mix scatter, line, bar, and surface plots effectively
  4. Add animations - Create compelling animated visualizations for time-series data
  5. 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:

output$plotly_custom <- renderPlotly({
  p <- plot_ly(mtcars, 
               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
})
output$plotly_3d <- renderPlotly({
  plot_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
output$plotly_animated <- renderPlotly({
  # Create sample time-series data
  time_data <- data.frame(
    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
output$interactive_plotly <- renderPlotly({
  plot_ly(mtcars, x = ~wt, y = ~mpg, 
          source = "cars_plot") %>%  # Important: set source
    add_markers()
})

# Capture click events
output$plotly_click_info <- renderPrint({
  click_data <- event_data("plotly_click", source = "cars_plot")
  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
output$simple_table <- renderTable({
  head(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
DT::dataTableOutput("dt_basic")

# Server
output$dt_basic <- DT::renderDataTable({
  mtcars
}, options = list(
  pageLength = 10,
  scrollX = TRUE,
  searchHighlight = TRUE
))
output$dt_advanced <- DT::renderDataTable({
  mtcars
}, options = 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')
output$dt_editable <- DT::renderDataTable({
  mtcars
}, editable = TRUE, options = list(
  pageLength = 10,
  scrollX = TRUE
))

# Handle edits
observeEvent(input$dt_editable_cell_edit, {
  info <- input$dt_editable_cell_edit
  # 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:

output$formatted_table <- DT::renderDataTable({
  DT::datatable(mtcars, options = list(pageLength = 10)) %>%
    DT::formatStyle(
      'mpg',
      backgroundColor = DT::styleInterval(c(15, 25), 
                                         c('red', 'yellow', 'green')),
      color = 'white'
    ) %>%
    DT::formatStyle(
      'hp',
      background = DT::styleColorBar(range(mtcars$hp), 'lightblue'),
      backgroundSize = '100% 90%',
      backgroundRepeat = 'no-repeat',
      backgroundPosition = 'center'
    ) %>%
    DT::formatRound(c('mpg', 'wt'), 1)
})
Explore Complete DT Configuration Options

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

Interactive Demo: Build Professional Data Displays

Create sophisticated multi-output dashboards with integrated components:

  1. Combine output types - Integrate plots, tables, and text into cohesive displays
  2. Connect interactions - See how plot selections update tables and summaries
  3. Apply consistent styling - Create professional, branded display layouts
  4. Test responsiveness - Experience how displays adapt to different screen sizes
  5. 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
output$download_data <- downloadHandler(
  filename = 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
output$download_flexible <- downloadHandler(
  filename = 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  
output$download_plot <- downloadHandler(
  filename = 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
output$download_report <- downloadHandler(
  filename = function() {
    paste("analysis_report_", Sys.Date(), ".html", sep = "")
  },
  content = function(file) {
    # Create temporary R Markdown file
    temp_report <- file.path(tempdir(), "report.Rmd")
    
    # 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
    rmarkdown::render(temp_report, output_file = file)
  }
)

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
output$map <- renderLeaflet({
  leaflet() %>%
    addTiles() %>%
    addMarkers(lng = -74.0059, lat = 40.7128, 
               popup = "New York City")
})

# Network Visualizations  
output$network <- renderForceNetwork({
  # Create sample network data
  nodes <- data.frame(
    name = c("A", "B", "C", "D"),
    group = c(1, 1, 2, 2)
  )
  links <- data.frame(
    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
output$dynamic_content <- renderUI({
  n_plots <- input$n_plots  # Assume this comes from a slider
  
  plot_outputs <- lapply(1:n_plots, function(i) {
    plotOutput(paste0("plot_", i), height = "300px")
  })
  
  do.call(tagList, plot_outputs)
})

# Generate the individual plots
observe({
  n_plots <- input$n_plots
  
  for(i in 1:n_plots) {
    local({
      plot_id <- paste0("plot_", i)
      output[[plot_id]] <- renderPlot({
        sample_data <- mtcars[sample(nrow(mtcars), 10), ]
        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
processed_data <- reactive({
  # Expensive data processing
  heavy_computation(raw_data())
}) 

# Use debouncing for responsive inputs
processed_data_debounced <- reactive({
  input$filter_text  # Trigger
  invalidateLater(500)  # Wait 500ms before updating
  
  # Your processing code
}) %>% debounce(500)

Conditional Rendering

Render outputs only when necessary:

# Conditional plot rendering
output$conditional_plot <- renderPlot({
  req(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
output$large_table <- DT::renderDataTable({
  big_data  # Your large dataset
}, server = TRUE, options = list(
  processing = TRUE,
  pageLength = 25,
  searchDelay = 500
))

# Pagination for plots
output$paginated_plots <- renderUI({
  page_size <- 20
  current_page <- input$plot_page %||% 1
  
  start_idx <- (current_page - 1) * page_size + 1
  end_idx <- min(current_page * page_size, nrow(data))
  
  current_data <- data[start_idx:end_idx, ]
  
  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
output$debug_plot <- renderPlot({
  req(input$data_source)  # Ensure input exists
  
  data <- get_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
output$static_table <- DT::renderDataTable({
  static_data  # This won't update
})

# Correct reactive approach
output$reactive_table <- DT::renderDataTable({
  filtered_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
  DT::replaceData(DT::dataTableProxy("reactive_table"), 
                  filtered_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
output$debug_download <- downloadHandler(
  filename = function() {
    paste("data_", Sys.Date(), ".csv", sep = "")
  },
  content = function(file) {
    tryCatch({
      data_to_download <- get_current_data()
      
      # 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)
    })
  }
)
Output Performance Best Practices
  • 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?

  1. plotOutput() with renderPlotly(), textOutput() with renderText()
  2. plotlyOutput() with renderPlot(), tableOutput() with DT::renderDataTable()
  3. plotlyOutput() with renderPlotly(), DT::dataTableOutput() with DT::renderDataTable()
  4. htmlOutput() with renderTable(), plotOutput() with renderUI()
  • 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:

output$optimized_table <- DT::renderDataTable({
  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?
output$optimized_table <- DT::renderDataTable({
  large_dataset
}, server = 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 browser
  • processing = TRUE: Shows loading indicator during data processing
  • pageLength = 25: Reasonable page size that balances usability with performance
  • searchDelay = 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?

  1. Use separate static ggplot2 plots with reactive text summaries
  2. Use plotly with custom hover info and event handling for point clicks
  3. Use base R plots with click coordinates to filter a separate data table
  4. 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
output$interactive_viz <- renderPlotly({
  plot_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"), {
  clicked_data <- event_data("plotly_click", source = "main_plot")
  # 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
Back to top

Reuse

Citation

BibTeX 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}
}
For attribution, please cite this work as:
Kassambara, Alboukadel. 2025. “Shiny Output Types and Visualization: Complete Display Guide.” May 23, 2025. https://www.datanovia.com/learn/tools/shiny-apps/ui-design/output-displays.html.