Build a Sales Dashboard with Shiny

· 7 min read · Updated March 13, 2026 · intermediate
shiny dashboard data-viz ggplot2 interactive

A sales dashboard transforms raw sales data into actionable insights. Instead of static spreadsheets, you get interactive views where stakeholders can filter by date range, product category, or region—all without touching code. In this guide, you’ll build a complete sales dashboard from scratch using Shiny and ggplot2.

What You Will Build

By the end of this guide, you’ll have a dashboard with:

  • Key performance indicators (KPIs) at the top
  • Interactive date range and category filters
  • Time series chart showing sales over time
  • Bar chart comparing product categories
  • Data table with detailed transactions

The dashboard will look professional and respond instantly to user input.

Setting Up Your Project

First, create a new Shiny project in RStudio:

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

# Load libraries
library(shiny)
library(ggplot2)
library(dplyr)
library(lubridate)

For this example, we’ll generate sample sales data. In production, you’d connect to a database or read from a CSV file.

Generating Sample Sales Data

Create a function that generates realistic sales data:

generate_sales_data <- function(n = 1000) {
  set.seed(123)
  
  categories <- c("Electronics", "Clothing", "Food & Beverage", "Home & Garden")
  regions <- c("North", "South", "East", "West")
  
  data.frame(
    date = sample(seq(as.Date("2024-01-01"), as.Date("2024-12-31"), by = "day"), n, replace = TRUE),
    category = sample(categories, n, replace = TRUE),
    region = sample(regions, n, replace = TRUE),
    product = paste0("Product ", sample(1:50, n, replace = TRUE)),
    quantity = sample(1:20, n, replace = TRUE),
    unit_price = runif(n, 10, 500) |> round(2)
  ) |>
    mutate(total_sales = quantity * unit_price)
}

sales_data <- generate_sales_data(2000)
head(sales_data)
#          date     category region   product quantity unit_price total_sales
# 1 2024-03-15 Electronics    West Product 32        7      285.43      1998.01
# 2 2024-08-22       Clothing    East Product 18       19       68.31      1297.89
# ...

This gives us realistic-looking data to visualize.

Building the Dashboard UI

The UI defines what the user sees. We’ll use a sidebar layout with filters on the left and visualizations on the right:

ui <- fluidPage(
  theme = bslib::bs_theme(version = 4, bootswatch = "minty"),
  
  titlePanel("Sales Dashboard"),
  
  sidebarLayout(
    sidebarPanel(
      h4("Filters"),
      
      dateRangeInput(
        "date_range",
        "Select Date Range:",
        start = min(sales_data$date),
        end = max(sales_data$date),
        min = min(sales_data$date),
        max = max(sales_data$date)
      ),
      
      selectInput(
        "category",
        "Product Category:",
        choices = c("All", unique(sales_data$category)),
        selected = "All"
      ),
      
      selectInput(
        "region",
        "Region:",
        choices = c("All", unique(sales_data$region)),
        selected = "All"
      ),
      
      hr(),
      helpText("Adjust filters to update all visualizations.")
    ),
    
    mainPanel(
      fluidRow(
        valueBoxOutput("total_sales_box", width = 4),
        valueBoxOutput("total_orders_box", width = 4),
        valueBoxOutput("avg_order_box", width = 4)
      ),
      
      fluidRow(
        plotOutput("sales_trend", height = "300px")
      ),
      
      fluidRow(
        plotOutput("category_breakdown", height = "300px")
      ),
      
      fluidRow(
        dataTableOutput("sales_table")
      )
    )
  )
)

The UI uses valueBoxOutput() for KPIs, plotOutput() for charts, and dataTableOutput() for the detailed table. Each output corresponds to a render function in the server.

Creating the Server Logic

The server handles all the reactive logic. When filters change, all outputs update automatically:

server <- function(input, output, session) {
  
  # Reactive filtered data
  filtered_data <- reactive({
    data <- sales_data
    
    # Filter by date range
    data <- data |>
      filter(date >= input$date_range[1] & date <= input$date_range[2])
    
    # Filter by category (if not "All")
    if (input$category != "All") {
      data <- data |> filter(category == input$category)
    }
    
    # Filter by region (if not "All")
    if (input$region != "All") {
      data <- data |> filter(region == input$region)
    }
    
    data
  })
  
  # KPI: Total Sales
  output$total_sales_box <- renderValueBox({
    total <- filtered_data() |>
      summarise(total = sum(total_sales)) |>
      pull(total)
    
    valueBox(
      value = scales::dollar(total, big.mark = ","),
      subtitle = "Total Sales",
      icon = icon("dollar-sign"),
      color = "green"
    )
  })
  
  # KPI: Total Orders
  output$total_orders_box <- renderValueBox({
    orders <- nrow(filtered_data())
    
    valueBox(
      value = scales::number(orders, big.mark = ","),
      subtitle = "Total Orders",
      icon = icon("shopping-cart"),
      color = "blue"
    )
  })
  
  # KPI: Average Order Value
  output$avg_order_box <- renderValueBox({
    avg <- filtered_data() |>
      summarise(avg = mean(total_sales)) |>
      pull(avg)
    
    valueBox(
      value = scales::dollar(avg, big.mark = ","),
      subtitle = "Avg Order Value",
      icon = icon("chart-line"),
      color = "purple"
    )
  })
  
  # Sales Trend Chart
  output$sales_trend <- renderPlot({
    filtered_data() |>
      group_by(date) |>
      summarise(daily_sales = sum(total_sales)) |>
      ggplot(aes(x = date, y = daily_sales)) +
      geom_line(color = "steelblue", linewidth = 1) +
      geom_smooth(method = "loess", se = FALSE, color = "darkred") +
      labs(
        title = "Daily Sales Over Time",
        x = "Date",
        y = "Sales ($)"
      ) +
      theme_minimal() +
      scale_y_continuous(labels = scales::dollar_format()) +
      theme(
        plot.title = element_text(hjust = 0.5, size = 14, face = "bold"),
        axis.text.x = element_text(angle = 45, hjust = 1)
      )
  })
  
  # Category Breakdown Chart
  output$category_breakdown <- renderPlot({
    filtered_data() |>
      group_by(category) |>
      summarise(category_sales = sum(total_sales)) |>
      ggplot(aes(x = reorder(category, -category_sales), y = category_sales, fill = category)) +
      geom_bar(stat = "identity") +
      labs(
        title = "Sales by Category",
        x = "Category",
        y = "Sales ($)"
      ) +
      theme_minimal() +
      scale_fill_brewer(palette = "Set2") +
      scale_y_continuous(labels = scales::dollar_format()) +
      theme(
        plot.title = element_text(hjust = 0.5, size = 14, face = "bold"),
        legend.position = "none",
        axis.text.x = element_text(angle = 45, hjust = 1)
      )
  })
  
  # Sales Data Table
  output$sales_table <- renderDataTable({
    filtered_data() |>
      head(100) |>
      mutate(
        date = format(date, "%Y-%m-%d"),
        total_sales = scales::dollar(total_sales, big.mark = ",")
      ) |>
      DT::datatable(
        options = list(
          pageLength = 10,
          dom = "tip",
          columnDefs = list(
            list(className = "dt-right", targets = 3:5)
          )
        )
      )
  })
}

The key to this code is filtered_data(). Whenever a user changes any filter, Shiny automatically recalculates the filtered dataset and updates every output that depends on it.

Running the Dashboard

Now put it all together and run the app:

# Load DT for data tables
library(DT)
library(bslib)
library(scales)

shinyApp(ui, server)

When you run this, you’ll see a fully functional dashboard. Try changing the date range or selecting different categories—watch how all the charts and KPIs update instantly.

Adding Polish

A few enhancements make the dashboard more professional:

Add a Theme

Use bslib for better styling:

ui <- fluidPage(
  theme = bs_theme(version = 4, bootswatch = "minty"),
  # ... rest of UI
)

Add Conditional Filtering

Some filters only make sense when others are selected:

observe({
  available_regions <- filtered_data() |>
    distinct(region) |>
    pull(region)
  
  updateSelectInput(
    session,
    "region",
    choices = c("All", available_regions),
    selected = input$region
  )
})

This updates the region dropdown to show only regions that have data for the selected date range and category.

Add Download Button

Let users export the filtered data:

# In UI
downloadButton("download_data", "Download Data")

# In Server
output$download_data <- downloadHandler(
  filename = function() {
    paste("sales_data_", Sys.Date(), ".csv", sep = "")
  },
  content = function(file) {
    write.csv(filtered_data(), file, row.names = FALSE)
  }
)

Complete Application Structure

For larger dashboards, organize your code into separate files:

sales-dashboard/
├── app.R              # Main app that sources other files
├── global.R           # Data loading and helper functions
├── ui.R               # UI definition
├── server.R           # Server logic
├── www/
│   └── custom.css     # Custom styling
└── data/
    └── sales_data.rds # Preprocessed data

In app.R:

# Load all components
source("global.R")
source("ui.R")
source("server.R")

shinyApp(ui, server)

This separation makes it easier to maintain and test each component.

Deployment

When you’re ready to share your dashboard, you have several options:

  • shinyapps.io: Free hosting by Posit (formerly RStudio)
  • Posit Connect: Professional hosting with authentication
  • Shiny Server: Self-hosted on your own server
  • Docker: Containerize for maximum portability

Deploying to shinyapps.io is straightforward:

library(rsconnect)
rsconnect::setAccountInfo(name = "your-account", token = "your-token", secret = "your-secret")
rsconnect::deployApp("sales-dashboard/")

Your dashboard will be live at your-account.shinyapps.io/sales-dashboard.

Summary

You built a complete sales dashboard with:

  • Three KPI value boxes showing total sales, orders, and average order value
  • Interactive date range, category, and region filters
  • Time series visualization showing daily sales trends
  • Bar chart breaking down sales by product category
  • Data table with detailed transaction records

The key concepts you learned—reactive filtering, modular output rendering, and separating UI from server—apply to any Shiny dashboard you build. These patterns scale to complex applications with dozens of inputs and outputs.

See Also