Build a Data Explorer Shiny App

· 6 min read · Updated March 13, 2026 · intermediate
shiny interactive data-exploration ggplot2 dt

A data explorer app lets users interact with datasets without writing code. They can upload their own data, apply filters, sort columns, and generate visualizations—all through a clean web interface. In this guide, you’ll build a complete data explorer from scratch using Shiny.

What You Will Build

By the end of this guide, you’ll have an app with:

  • File upload capability (CSV, Excel)
  • Dynamic column selection
  • Filtering by any column value
  • Sortable data table with pagination
  • Histogram and scatter plot visualizations
  • Download filtered data

Setting Up Your Project

First, create a new Shiny project in RStudio or via the console:

# Install required packages if needed
install.packages(c("shiny", "ggplot2", "DT", "readr", "dplyr"))

# Create a new Shiny app
shiny::shinyApp(ui = ui, server = server)

Create a new folder called data-explorer and save your app as app.R.

Building the UI

The user interface needs to handle file input, sidebar controls for filtering, and the main area for displaying results:

library(shiny)
library(DT)
library(ggplot2)
library(readr)
library(dplyr)

ui <- fluidPage(
  titlePanel("Interactive Data Explorer"),
  
  sidebarLayout(
    sidebarPanel(
      fileInput("file", "Upload CSV File",
                accept = c("text/csv", "text/comma-separated-values", ".csv")),
      checkboxInput("header", "Has Header", TRUE),
      selectInput("xvar", "X Variable", choices = NULL),
      selectInput("yvar", "Y Variable", choices = NULL),
      selectInput("fillvar", "Fill Variable", choices = NULL),
      actionButton("update", "Apply Changes", class = "btn-primary")
    ),
    
    mainPanel(
      tabsetPanel(
        tabPanel("Data Table", DTOutput("table")),
        tabPanel("Histogram", plotOutput("hist")),
        tabPanel("Scatter Plot", plotOutput("scatter"))
      )
    )
  )
)

The sidebarPanel holds all your controls, while the mainPanel uses a tabsetPanel to switch between views.

Building the Server

The server function handles reactivity—whenever a user changes an input, the outputs update automatically:

server <- function(input, output, session) {
  
  # Reactive value to store the uploaded data
  data <- reactive({
    req(input$file)
    read_csv(input$file$datapath, col_names = input$header)
  })
  
  # Update select inputs when data changes
  observeEvent(data(), {
    updateSelectInput(session, "xvar", choices = names(data()))
    updateSelectInput(session, "yvar", choices = names(data()))
    updateSelectInput(session, "fillvar", choices = c("None", names(data())))
  })
  
  # Filtered data based on user selection
  filtered_data <- eventReactive(input$update, {
    req(data())
    data()
  })
  
  # Render the data table
  output$table <- renderDT({
    datatable(filtered_data(), options = list(pageLength = 10))
  })
  
  # Render histogram
  output$hist <- renderPlot({
    req(input$xvar, filtered_data())
    ggplot(filtered_data(), aes(x = .data[[input$xvar]])) +
      geom_histogram(bins = 30, fill = "steelblue", alpha = 0.7) +
      labs(x = input$xvar, y = "Count") +
      theme_minimal()
  })
  
  # Render scatter plot
  output$scatter <- renderPlot({
    req(input$xvar, input$yvar, filtered_data())
    fill_val <- if (input$fillvar == "None") NULL else input$fillvar
    
    p <- ggplot(filtered_data(), aes(x = .data[[input$xvar]], y = .data[[input$yvar]]))
    if (!is.null(fill_val)) {
      p <- p + aes(fill = .data[[fill_val]]) + geom_point(alpha = 0.6)
    } else {
      p <- p + geom_point(alpha = 0.6)
    }
    p + labs(x = input$xvar, y = input$yvar) + theme_minimal()
  })
}

The key is using reactive() for values that change and eventReactive() for actions that should only trigger on specific events like button clicks.

Adding Data Download

Users often want to save their filtered results. Add this to your UI and server:

# In UI, add after the sidebarPanel controls:
downloadButton("download", "Download Filtered Data")

# In server, add:
output$download <- downloadHandler(
  filename = function() {
    paste("filtered_data_", Sys.Date(), ".csv", sep = "")
  },
  content = function(file) {
    write_csv(filtered_data(), file)
  }
)

Running Your App

Run the app from RStudio by clicking “Run App” or from the console:

shiny::runApp("data-explorer")

Your data explorer is now ready. Users can upload any CSV file, explore its structure, filter views, and download results.

Understanding Reactivity

Shiny’s reactivity system is what makes your app interactive. When a user changes an input, any output that depends on that input automatically re-renders. This happens through a dependency graph that Shiny builds automatically.

The reactive() function creates a reactive value—a piece of data that changes over time. Whenever something that depends on a reactive value changes, Shiny knows to re-execute any code that uses that value. In our app, the data() reactive value depends on input$file, so whenever a user uploads a file, everything that depends on data() re-runs.

The observeEvent() function runs code only when a specific event occurs. We use it to update the dropdown menus whenever new data loads. Without observeEvent(), the menus would never update because selectInputs don’t automatically react to changes in their choices.

The eventReactive() function is similar but returns a value. We use it for the “Apply Changes” button so that filtering only happens when the user clicks the button, rather than continuously as they adjust settings. This gives users a chance to make multiple changes before seeing results.

Adding Advanced Filtering

You can enhance your data explorer with more sophisticated filtering options. Here’s how to add column-specific filters:

# Add to UI
ui <- fluidPage(
  # ... existing UI code ...
  sidebarPanel(
    # ... existing controls ...
    
    # Add dynamic filter inputs
    uiOutput("filterControls"),
    actionButton("addFilter", "Add Filter", class = "btn-secondary"),
    actionButton("clearFilters", "Clear All", class = "btn-outline")
  )
)

# Add to server
output$filterControls <- renderUI({
  req(data())
  filter_list <- lapply(seq_len(input$numFilters), function(i) {
    selectInput(paste0("filterCol", i), "Filter Column",
                choices = c("None", names(data())))
  })
  tagList(filter_list)
})

observeEvent(input$addFilter, {
  updateNumericInput(session, "numFilters",
                     value = isolate(input$numFilters + 1))
})

observeEvent(input$clearFilters, {
  updateNumericInput(session, "numFilters", value = 0)
})

This pattern uses dynamic UI generation—creating inputs on the fly based on the data structure. The isolate() function prevents infinite loops by stopping reactivity propagation.

Customizing the Appearance

Shiny apps can be styled using CSS. Add a custom stylesheet to match your brand:

ui <- fluidPage(
  tags$head(
    tags$style(HTML("
      .btn-primary {
        background-color: #2c3e50;
        border-color: #2c3e50;
      }
      .btn-primary:hover {
        background-color: #34495e;
      }
      .well {
        background-color: #ecf0f1;
      }
    "))
  ),
  # ... rest of UI
)

You can also use Bootstrap themes. The shiny package includes several built-in themes, or use the bslib package for more control:

library(bslib)

ui <- fluidPage(
  theme = bs_theme(version = 5, bootswatch = "cerulean"),
  # ... your UI components
)

Deployment Options

When your app is ready to share, you have several deployment options:

Shinyapps.io: The easiest option for beginners. Sign up at shinyapps.io, connect your account, and deploy with one click from RStudio:

rsconnect::setAccountInfo(name="your-account", token="xxx", secret="yyy")
rsconnect::deployApp("data-explorer")

RStudio Connect: For organizations needing more control. This is a paid product with enterprise features like authentication and scheduling.

Self-hosted: Deploy on your own server using Shiny Server or Docker. This gives you full control but requires more setup.

Each option has trade-offs. For personal projects or small teams, shinyapps.io is usually the best starting point.

See Also