Your app: explore a dataset

Putting it all together, step by step

We build the app that will go online on Day 2. The user:

  1. chooses a dataset (or uploads their own) and loads it
  2. chooses the X and Y variables among the columns of that dataset
  3. clicks to update the plot and the summary

This is the app we are going to build. Try it: load a dataset, choose the variables and click Update analysis.

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 800
## file: ui.R
library(shiny)

ui <- fluidPage(
  titlePanel("Explore a dataset"),
  sidebarLayout(
    sidebarPanel(
      selectInput(
        "dataset",
        "Choose a dataset:",
        choices = c("rock", "pressure", "cars",
                    "Upload data" = "upload")              # NEW option
      ),
      # NEW: shown only when "Upload data" is selected
      conditionalPanel(
        condition = "input.dataset == 'upload'",
        fileInput("file", "Select a CSV file", accept = ".csv"),
        radioButtons("sep", "Column separator",
                     choices = c(";" = ";", "," = ","))
      ),
      actionButton("load", "Load dataset"),
      hr(),
      uiOutput("x_ui"),
      uiOutput("y_ui"),
      actionButton("analyze", "Update analysis")
    ),
    mainPanel(
      plotOutput("graph"),
      verbatimTextOutput("summary")
    )
  )
)

## file: server.R
library(shiny)

server <- function(input, output, session) {

  # 1. LOAD THE DATASET
  dataInput <- eventReactive(input$load, {
    switch(input$dataset,
           rock = rock,
           pressure = pressure,
           cars = cars,
           upload = {                                           # NEW
             req(input$file)                                    # NEW: wait for a file
             read.csv(input$file$datapath, sep = input$sep)     # NEW
           })
  })

  # 2. VARIABLE NAMES
  vars <- reactive({
    names(dataInput())
  })

  # 3. DYNAMIC UI
  output$x_ui <- renderUI({
    selectInput("x", "X variable:",
                choices = vars(),
                selected = vars()[1])
  })

  output$y_ui <- renderUI({
    selectInput("y", "Y variable:",
                choices = vars(),
                selected = vars()[min(2, length(vars()))])
  })

  # 4. DATA FOR THE ANALYSIS
  newdata <- eventReactive(input$analyze, {
    df <- dataInput()
    df[, c(input$x, input$y), drop = FALSE]
  })

  # 5. OUTPUTS
  output$graph <- renderPlot({
    df <- newdata()
    x <- df[[1]]
    y <- df[[2]]
    if (is.factor(x) || is.character(x)) {
      boxplot(y ~ x, xlab = names(df)[1], ylab = names(df)[2])
    } else {
      plot(x, y, xlab = names(df)[1], ylab = names(df)[2], pch = 19)
    }
  })

  output$summary <- renderPrint({
    summary(newdata())
  })
}

We build it in four steps. Each step is a working app: run it, check it, then move on. The files of each step are in apps/ (05-explore-step1 … 05-explore-step4).

flowchart LR
    DS["input$dataset"] -.-> D
    L["input$load"] ==>|"trigger"| D["dataInput()<br/>eventReactive"]
    D --> V["vars()<br/>reactive"]
    V --> XU["output$x_ui / y_ui<br/>renderUI"]
    XU --> X["input$x / input$y"]
    X -.-> N
    A["input$analyze"] ==>|"trigger"| N["newdata()<br/>eventReactive"]
    D -.-> N
    N --> G["output$graph"]
    N --> S["output$summary"]
    style L fill:#d9534f,color:#fff
    style A fill:#d9534f,color:#fff
    style DS fill:#2b6c8e,color:#fff
    style X fill:#2b6c8e,color:#fff
    style D fill:#f0ad4e,color:#fff
    style V fill:#f0ad4e,color:#fff
    style N fill:#f0ad4e,color:#fff
    style XU fill:#8fd36f,color:#fff
    style G fill:#8fd36f,color:#fff
    style S fill:#8fd36f,color:#fff

Step 1 · Load a dataset

A dataset is loaded with an eventReactive(). The first rows are printed to check that the data are correct. Choose a dataset and click the button:

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 330
## file: ui.R
library(shiny)

ui <- fluidPage(
  titlePanel("Explore a dataset"),
  sidebarLayout(
    sidebarPanel(
      selectInput(
        "dataset",
        "Choose a dataset:",
        choices = c("rock", "pressure", "cars")
      ),
      actionButton("load", "Load dataset")
    ),
    mainPanel(
      verbatimTextOutput("preview")
    )
  )
)

## file: server.R
library(shiny)

server <- function(input, output, session) {

  # 1. LOAD THE DATASET (only when "Load dataset" is clicked)
  dataInput <- eventReactive(input$load, {
    switch(input$dataset,
           rock = rock,
           pressure = pressure,
           cars = cars)
  })

  # check that the data arrived
  output$preview <- renderPrint({
    head(dataInput())
  })
}

The code:

#| '!! 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: 330
## file: ui.R
library(shiny)

ui <- fluidPage(
  titlePanel("Explore a dataset"),
  sidebarLayout(
    sidebarPanel(
      selectInput(
        "dataset",
        "Choose a dataset:",
        choices = c("rock", "pressure", "cars")
      ),
      actionButton("load", "Load dataset")
    ),
    mainPanel(
      verbatimTextOutput("preview")
    )
  )
)

## file: server.R
library(shiny)

server <- function(input, output, session) {

  # 1. LOAD THE DATASET (only when "Load dataset" is clicked)
  dataInput <- eventReactive(input$load, {
    switch(input$dataset,
           rock = rock,
           pressure = pressure,
           cars = cars)
  })

  # check that the data arrived
  output$preview <- renderPrint({
    head(dataInput())
  })
}

Step 2 · Menus built from the data

Now the app offers two menus, X and Y, whose choices are the columns of the loaded dataset. Load rock, then cars: the menus change with the data. Before the first click the slots are empty, because vars() needs dataInput(), which waits for the button.

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 420
## file: ui.R
library(shiny)

ui <- fluidPage(
  titlePanel("Explore a dataset"),
  sidebarLayout(
    sidebarPanel(
      selectInput(
        "dataset",
        "Choose a dataset:",
        choices = c("rock", "pressure", "cars")
      ),
      actionButton("load", "Load dataset"),
      hr(),
      uiOutput("x_ui"),   # NEW: empty slot, the server will put a selectInput here
      uiOutput("y_ui")    # NEW
    ),
    mainPanel(
      verbatimTextOutput("preview")
    )
  )
)

## file: server.R
library(shiny)

server <- function(input, output, session) {

  # 1. LOAD THE DATASET
  dataInput <- eventReactive(input$load, {
    switch(input$dataset,
           rock = rock,
           pressure = pressure,
           cars = cars)
  })

  # 2. VARIABLE NAMES (NEW)
  vars <- reactive({
    names(dataInput())
  })

  # 3. DYNAMIC UI (NEW): the choices depend on the loaded data
  output$x_ui <- renderUI({
    selectInput("x", "X variable:",
                choices = vars(),
                selected = vars()[1])
  })

  output$y_ui <- renderUI({
    selectInput("y", "Y variable:",
                choices = vars(),
                selected = vars()[min(2, length(vars()))])
  })

  output$preview <- renderPrint({
    cat("x =", input$x, " | y =", input$y, "\n\n")
    head(dataInput())
  })
}

How does it work? The X and Y menus cannot be written in ui.R because their choices depend on the column names of the uploaded dataset:

  • in ui.R we reserve an empty slot with uiOutput()
  • in server.R, renderUI() creates the selectInput() and puts it in that slot
# ui.R
1uiOutput("x_ui")
1
An empty slot for a piece of UI that the server will create
# server.R
1vars <- reactive({
  names(dataInput())
})

2output$x_ui <- renderUI({
3  selectInput("x", "X variable:",
              choices = vars(),
              selected = vars()[1])
})
1
The column names, as a reactive expression: they change when new data are loaded
2
renderUI() is the partner of uiOutput()
3
A normal selectInput(), with ID x: from now on the server can read input$x, like any other input

The complete code:

#| '!! 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: 420
## file: ui.R
library(shiny)

ui <- fluidPage(
  titlePanel("Explore a dataset"),
  sidebarLayout(
    sidebarPanel(
      selectInput(
        "dataset",
        "Choose a dataset:",
        choices = c("rock", "pressure", "cars")
      ),
      actionButton("load", "Load dataset"),
      hr(),
      uiOutput("x_ui"),   # NEW: empty slot, the server will put a selectInput here
      uiOutput("y_ui")    # NEW
    ),
    mainPanel(
      verbatimTextOutput("preview")
    )
  )
)

## file: server.R
library(shiny)

server <- function(input, output, session) {

  # 1. LOAD THE DATASET
  dataInput <- eventReactive(input$load, {
    switch(input$dataset,
           rock = rock,
           pressure = pressure,
           cars = cars)
  })

  # 2. VARIABLE NAMES (NEW)
  vars <- reactive({
    names(dataInput())
  })

  # 3. DYNAMIC UI (NEW): the choices depend on the loaded data
  output$x_ui <- renderUI({
    selectInput("x", "X variable:",
                choices = vars(),
                selected = vars()[1])
  })

  output$y_ui <- renderUI({
    selectInput("y", "Y variable:",
                choices = vars(),
                selected = vars()[min(2, length(vars()))])
  })

  output$preview <- renderPrint({
    cat("x =", input$x, " | y =", input$y, "\n\n")
    head(dataInput())
  })
}

Step 3 · The analysis, on demand

A second button, analyze, and a second eventReactive() that freezes the selected columns when it is clicked. The plot and the summary use only these frozen data.

Try it: load cars, choose the variables, click Update analysis. Now change the variables without clicking: the plot does not move until you click again.

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 720
## file: ui.R
library(shiny)

ui <- fluidPage(
  titlePanel("Explore a dataset"),
  sidebarLayout(
    sidebarPanel(
      selectInput(
        "dataset",
        "Choose a dataset:",
        choices = c("rock", "pressure", "cars")
      ),
      actionButton("load", "Load dataset"),
      hr(),
      uiOutput("x_ui"),
      uiOutput("y_ui"),
      actionButton("analyze", "Update analysis")   # NEW: second button
    ),
    mainPanel(
      plotOutput("graph"),                         # NEW
      verbatimTextOutput("summary")                # NEW
    )
  )
)

## file: server.R
library(shiny)

server <- function(input, output, session) {

  # 1. LOAD THE DATASET
  dataInput <- eventReactive(input$load, {
    switch(input$dataset,
           rock = rock,
           pressure = pressure,
           cars = cars)
  })

  # 2. VARIABLE NAMES
  vars <- reactive({
    names(dataInput())
  })

  # 3. DYNAMIC UI
  output$x_ui <- renderUI({
    selectInput("x", "X variable:",
                choices = vars(),
                selected = vars()[1])
  })

  output$y_ui <- renderUI({
    selectInput("y", "Y variable:",
                choices = vars(),
                selected = vars()[min(2, length(vars()))])
  })

  # 4. DATA FOR THE ANALYSIS (NEW): frozen until "Update analysis" is clicked
  newdata <- eventReactive(input$analyze, {
    df <- dataInput()
    df[, c(input$x, input$y), drop = FALSE]
  })

  # 5. OUTPUTS (NEW)
  output$graph <- renderPlot({
    df <- newdata()
    x <- df[[1]]
    y <- df[[2]]
    if (is.factor(x) || is.character(x)) {
      boxplot(y ~ x, xlab = names(df)[1], ylab = names(df)[2])
    } else {
      plot(x, y, xlab = names(df)[1], ylab = names(df)[2], pch = 19)
    }
  })

  output$summary <- renderPrint({
    summary(newdata())
  })
}

How does it work? In the server:

1newdata <- eventReactive(input$analyze, {
  df <- dataInput()
2  df[, c(input$x, input$y), drop = FALSE]
})
1
Runs only when Update analysis is clicked
2
Keep only the two chosen columns. input$x and input$y are text (the column names), so we can use them to select columns

The plot adapts to the type of X: a boxplot if X is categorical, a scatterplot if it is numeric.

The complete code:

#| '!! 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: 720
## file: ui.R
library(shiny)

ui <- fluidPage(
  titlePanel("Explore a dataset"),
  sidebarLayout(
    sidebarPanel(
      selectInput(
        "dataset",
        "Choose a dataset:",
        choices = c("rock", "pressure", "cars")
      ),
      actionButton("load", "Load dataset"),
      hr(),
      uiOutput("x_ui"),
      uiOutput("y_ui"),
      actionButton("analyze", "Update analysis")   # NEW: second button
    ),
    mainPanel(
      plotOutput("graph"),                         # NEW
      verbatimTextOutput("summary")                # NEW
    )
  )
)

## file: server.R
library(shiny)

server <- function(input, output, session) {

  # 1. LOAD THE DATASET
  dataInput <- eventReactive(input$load, {
    switch(input$dataset,
           rock = rock,
           pressure = pressure,
           cars = cars)
  })

  # 2. VARIABLE NAMES
  vars <- reactive({
    names(dataInput())
  })

  # 3. DYNAMIC UI
  output$x_ui <- renderUI({
    selectInput("x", "X variable:",
                choices = vars(),
                selected = vars()[1])
  })

  output$y_ui <- renderUI({
    selectInput("y", "Y variable:",
                choices = vars(),
                selected = vars()[min(2, length(vars()))])
  })

  # 4. DATA FOR THE ANALYSIS (NEW): frozen until "Update analysis" is clicked
  newdata <- eventReactive(input$analyze, {
    df <- dataInput()
    df[, c(input$x, input$y), drop = FALSE]
  })

  # 5. OUTPUTS (NEW)
  output$graph <- renderPlot({
    df <- newdata()
    x <- df[[1]]
    y <- df[[2]]
    if (is.factor(x) || is.character(x)) {
      boxplot(y ~ x, xlab = names(df)[1], ylab = names(df)[2])
    } else {
      plot(x, y, xlab = names(df)[1], ylab = names(df)[2], pch = 19)
    }
  })

  output$summary <- renderPrint({
    summary(newdata())
  })
}

Step 4 · Upload your own data

Last step: a fourth option, Upload data, with value "upload": choices = c("rock", "pressure", "cars", "Upload data" = "upload"). When it is selected, a file input and the choice of the column separator appear.

Test files: example.csv (separator ,) and example-semicolon.csv (separator ;).

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 800
## file: ui.R
library(shiny)

ui <- fluidPage(
  titlePanel("Explore a dataset"),
  sidebarLayout(
    sidebarPanel(
      selectInput(
        "dataset",
        "Choose a dataset:",
        choices = c("rock", "pressure", "cars",
                    "Upload data" = "upload")              # NEW option
      ),
      # NEW: shown only when "Upload data" is selected
      conditionalPanel(
        condition = "input.dataset == 'upload'",
        fileInput("file", "Select a CSV file", accept = ".csv"),
        radioButtons("sep", "Column separator",
                     choices = c(";" = ";", "," = ","))
      ),
      actionButton("load", "Load dataset"),
      hr(),
      uiOutput("x_ui"),
      uiOutput("y_ui"),
      actionButton("analyze", "Update analysis")
    ),
    mainPanel(
      plotOutput("graph"),
      verbatimTextOutput("summary")
    )
  )
)

## file: server.R
library(shiny)

server <- function(input, output, session) {

  # 1. LOAD THE DATASET
  dataInput <- eventReactive(input$load, {
    switch(input$dataset,
           rock = rock,
           pressure = pressure,
           cars = cars,
           upload = {                                           # NEW
             req(input$file)                                    # NEW: wait for a file
             read.csv(input$file$datapath, sep = input$sep)     # NEW
           })
  })

  # 2. VARIABLE NAMES
  vars <- reactive({
    names(dataInput())
  })

  # 3. DYNAMIC UI
  output$x_ui <- renderUI({
    selectInput("x", "X variable:",
                choices = vars(),
                selected = vars()[1])
  })

  output$y_ui <- renderUI({
    selectInput("y", "Y variable:",
                choices = vars(),
                selected = vars()[min(2, length(vars()))])
  })

  # 4. DATA FOR THE ANALYSIS
  newdata <- eventReactive(input$analyze, {
    df <- dataInput()
    df[, c(input$x, input$y), drop = FALSE]
  })

  # 5. OUTPUTS
  output$graph <- renderPlot({
    df <- newdata()
    x <- df[[1]]
    y <- df[[2]]
    if (is.factor(x) || is.character(x)) {
      boxplot(y ~ x, xlab = names(df)[1], ylab = names(df)[2])
    } else {
      plot(x, y, xlab = names(df)[1], ylab = names(df)[2], pch = 19)
    }
  })

  output$summary <- renderPrint({
    summary(newdata())
  })
}

How does it work? In ui.R, the file input and the separator are wrapped in a conditionalPanel():

1conditionalPanel(
2  condition = "input.dataset == 'upload'",
  fileInput("file", "Select a CSV file", accept = ".csv"),
  radioButtons("sep", "Column separator",
               choices = c(";" = ";", "," = ","))
)
1
Shows its content only when the condition is true
2
The condition is written in JavaScript, not in R: input.dataset with a dot, and 'upload' in quotes

In the server, a fourth case in the switch() of dataInput() must be added. The curly brackets hold more than one line of code:

upload = {
1  req(input$file)
2  read.csv(input$file$datapath, sep = input$sep)
}
1
req() = required: if no file has been uploaded yet, stop here silently, without an error
2
Read the file from the place where Shiny saved it, with the separator chosen by the user

The complete code:

#| '!! 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: 800
## file: ui.R
library(shiny)

ui <- fluidPage(
  titlePanel("Explore a dataset"),
  sidebarLayout(
    sidebarPanel(
      selectInput(
        "dataset",
        "Choose a dataset:",
        choices = c("rock", "pressure", "cars",
                    "Upload data" = "upload")              # NEW option
      ),
      # NEW: shown only when "Upload data" is selected
      conditionalPanel(
        condition = "input.dataset == 'upload'",
        fileInput("file", "Select a CSV file", accept = ".csv"),
        radioButtons("sep", "Column separator",
                     choices = c(";" = ";", "," = ","))
      ),
      actionButton("load", "Load dataset"),
      hr(),
      uiOutput("x_ui"),
      uiOutput("y_ui"),
      actionButton("analyze", "Update analysis")
    ),
    mainPanel(
      plotOutput("graph"),
      verbatimTextOutput("summary")
    )
  )
)

## file: server.R
library(shiny)

server <- function(input, output, session) {

  # 1. LOAD THE DATASET
  dataInput <- eventReactive(input$load, {
    switch(input$dataset,
           rock = rock,
           pressure = pressure,
           cars = cars,
           upload = {                                           # NEW
             req(input$file)                                    # NEW: wait for a file
             read.csv(input$file$datapath, sep = input$sep)     # NEW
           })
  })

  # 2. VARIABLE NAMES
  vars <- reactive({
    names(dataInput())
  })

  # 3. DYNAMIC UI
  output$x_ui <- renderUI({
    selectInput("x", "X variable:",
                choices = vars(),
                selected = vars()[1])
  })

  output$y_ui <- renderUI({
    selectInput("y", "Y variable:",
                choices = vars(),
                selected = vars()[min(2, length(vars()))])
  })

  # 4. DATA FOR THE ANALYSIS
  newdata <- eventReactive(input$analyze, {
    df <- dataInput()
    df[, c(input$x, input$y), drop = FALSE]
  })

  # 5. OUTPUTS
  output$graph <- renderPlot({
    df <- newdata()
    x <- df[[1]]
    y <- df[[2]]
    if (is.factor(x) || is.character(x)) {
      boxplot(y ~ x, xlab = names(df)[1], ylab = names(df)[2])
    } else {
      plot(x, y, xlab = names(df)[1], ylab = names(df)[2], pch = 19)
    }
  })

  output$summary <- renderPrint({
    summary(newdata())
  })
}

Put it in your project

Copy ui.R and server.R of apps/05-explore-step4 into your project my-shiny-app, replacing the old ones, and run it with Run App.

This is the app that goes online on Day 2.

Exercise 1 · Make it yours

Pick one or more:

  1. Add iris to the datasets (remember to add the case in dataInput())
  2. Use the wrong separator on purpose with example.csv: what happens to the menus? Why?
  3. Add a tableOutput() that shows the first 6 rows of newdata()
  4. Show the title of the plot with the names of the two variables (main = paste(...))
  1. selectInput(): choices = c("rock", "pressure", "cars", "iris", "Upload data" = "upload"); dataInput(): add iris = iris to the switch()
  2. With the wrong separator read.csv() reads a single column with a long name: the menus offer only one variable
  3. ui.R: tableOutput("rows"); server.R: output$rows <- renderTable({ head(newdata()) })
  4. plot(x, y, ..., main = paste(names(df)[2], "by", names(df)[1]))