Build a Data Explorer Shiny App
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
- Build a Sales Dashboard with Shiny — A more advanced dashboard project with KPIs and multiple visualizations
- Getting Started with Shiny — Core Shiny concepts for building your first app
- Organising Shiny Apps with Modules — Scale your apps with reusable components