0

Trying to find an easy solution to change the order of x-axis and other parameters in ggplot2 using the user input in a Shiny app.

This is the code for the app

library(shiny)
library(openxlsx)
library(ggplot2)
library(plotly)
library(ggbeeswarm)
library(stringr)
library(dplyr)
library(ggpubr)

# Define UI
ui <- fluidPage(
  titlePanel("Upload and Visualize tabular Data"),
  sidebarLayout(
    sidebarPanel(
      width = 3,  # Reduce the width of the side panel
      tags$h4("Upload input files"),
      fluidRow(
        column(12, fileInput("data_file", "Choose dataset (.xlsx)")),
      ),
      fluidRow(
        column(12, fileInput("annotation_file", "Choose annotation file (.xlsx)")),
      ),
      hr(),  # Add another horizontal line as a split
      tags$h4("Filter your data"),
      fluidRow(
        column(12, uiOutput("filter_inputs"))
      ),
      hr(),  # Add another horizontal line as a split
      tags$h4("Select graph parameters"),
      fluidRow(
        column(12, selectInput("x_param", "X axis variable", "")),
      ),
      fluidRow(
        column(12, selectInput("data_column", "Y axis Variable", "")),
      ),
      fluidRow(
        column(12, selectInput("fill_param", "Color boxplots by:", "")),
      ),
      fluidRow(
        column(12, selectInput("facet_param", "Choose parameter to create facets:", "")),
      ),
      hr(),  # Add another horizontal line as a split
      tags$h4("Download data"),
      fluidRow(
        column(12, downloadButton("download_data", "Download Filtered Data")),
      ),
      fluidRow(
        column(12, downloadButton("download_plot", "Download Plot")),
      )
    ),
    mainPanel(
      plotOutput("graph")
    )
  )
)


# Define server
server <- function(input, output, session) {
  # Read dataset and annotation files
  dataset <- reactive({
    req(input$data_file)
    read.xlsx(input$data_file$datapath, rowNames = TRUE)
  })
  
  annotation <- reactive({
    req(input$annotation_file)
    read.xlsx(input$annotation_file$datapath, rowNames = TRUE)
  })
  
  # Update the filter input widgets based on the annotation file
  observe({
    col_names <- names(annotation())
    filter_inputs <- lapply(col_names, function(col) {
      #if (is.numeric(annotation()[[col]])) {
      #  numericInput(inputId = paste0("filter_", col), label = col, value = "")
      #} else {
        selectizeInput(inputId = paste0("filter_", col), label = col, choices = c("", unique(annotation()[[col]])), 
                       multiple = TRUE, selected = "")
      #}
    })
    output$filter_inputs <- renderUI({ filter_inputs })
    
    # Update the choices for x_param, fill_param, and facet_param based on the annotation
    updateSelectInput(session, "x_param", choices = col_names, selected = "")
    updateSelectInput(session, "fill_param", choices = c("none", col_names), selected = "none")
    updateSelectInput(session, "facet_param", choices = c("none", col_names), selected = "none")
  })
  
  # Merge the datasets based on the row names (unique identifier)
  merged_data <- reactive({
    merge(annotation(), dataset(), by = 'row.names', all = TRUE)
  })
  
 
  # Update the choices for data_column based on the dataset
  observe({
    req(dataset())
    col_names <- names(dataset())
    updateSelectInput(session, "data_column", choices = col_names)
  })
  
  
  # Filter the data based on the selected filter columns
  filtered_data <- reactive({
    data <- merged_data()
    
    for (col in names(annotation())) {
      if (!is.null(input[[paste0("filter_", col)]])) {
        filter_values <- input[[paste0("filter_", col)]]
        
        if (length(filter_values) > 0) {
          col_escaped <- if (grepl("[[:punct:]]", col)) paste0("`", col, "`") else col
          
          if (is.numeric(annotation()[[col]])) {
            data <- data[data[[col_escaped]] %in% filter_values, ]
          } else {
            data <- data[grepl(paste0("^(", paste(filter_values, collapse = "|"), ")$"), data[[col_escaped]]), ]
          }
        }
      }
    }
    
    data
  })
  
  # Create the graph using ggplot2 and render with plotly
  output$graph <- renderPlot({
    req(input$x_param, input$fill_param, input$facet_param, input$data_column)
    
    facets <- input$facet_param %>% 
      str_replace_all(",", "+") %>%
      rlang::parse_exprs()
    
  p <- ggplot(filtered_data(), aes_string(x = input$x_param, y = col_escaped(input$data_column)))
  
  if(input$fill_param != "none") {
    p = p + geom_boxplot(aes_string(fill=input$fill_param), alpha = 0.25, outlier.shape = NA) +
      geom_point(aes_string(fill=input$fill_param), col="black", size = 2, position = position_jitterdodge(jitter.width=0.2))
  }
  else {
    p = p + geom_boxplot(outlier.shape = NA) +
      geom_quasirandom(size=2)
  }
  
    #geom_boxplot(aes_string(fill=input$fill_param), outlier.shape = NA) + 
  
  if(input$facet_param != "none"){
    p = p + facet_grid(cols = vars(!!!facets)) + 
      theme_bw() + 
      theme(axis.text.x=element_text(size=15), axis.text.y = element_text(size=12), 
            axis.title.y = element_text(size=18), strip.text.x = element_text(size = 15),
            legend.title = element_text(size=15), legend.text = element_text(size=12))+
      theme(axis.text.x = element_text(angle = 45, hjust=1))+
      xlab("")
  }
  else{
    p =   p + theme_bw() +
      theme(axis.text.x=element_text(size=15), axis.text.y = element_text(size=12), 
            axis.title.y = element_text(size=18), strip.text.x = element_text(size = 15),
            legend.title = element_text(size=15), legend.text = element_text(size=12))+
      theme(axis.text.x = element_text(angle = 45, hjust=1))+
      xlab("")
  }
  print (p)  
    
  }, height = 750)
  
  # Function to escape column names with special characters
  col_escaped <- function(col) {
    if (grepl("[[:punct:]]", col)) {
      return(paste0("`", col, "`"))
    } else {
      return(col)
    }
  }
  
  # Download the filtered data as a CSV file
  output$download_data <- downloadHandler(
    filename = function() {
      "filtered_data.csv"
    },
    content = function(file) {
      write.csv(filtered_data(), file)
    }
  )
  
  # Download the plot as a PNG image
  output$download_plot <- downloadHandler(
    filename = function() {
      "plot.png"
    },
    content = function(file) {
      ggsave(file, plot = p, width = 12, height = 8, dpi = 300, device = "png")
    }
  )
}

# Run the Shiny app
shinyApp(ui, server)

The way the app works is straight forward, users upload an Annotation and a Dataset file where the first column is the sample ID. The app uses the column names of the annotation file to create filters so users can filter their data. Those column names will also be available for 3 parameters: x-axis, color of boxplots and facets. The columns of the dataset file will be used for the Y axis variable.

The app works perfectly but I had some requests to be able to change the order of X-axis and the order of the facets in the plot. I know I should use factor and levels in the ggplot to change the order from alphabetically to the order I want but I do not really know how to do that within the Selectinput choices.

So basically, depending on the order of the choices made in the "Filter your data" section, ggplot would use that order in the x-axis and in the facet.

I can provide mor info if my request is not explicit enough.

Thank you

UPDATE: I managed to do something that could work by adding this piece of code:

 # Define a reactive expression to store the selected values for x_param
  selected_x_values <- reactive({
    req(input$x_param)
    unique(filtered_data()[[input$x_param]])
  })
  

I then changed the plot part like this:

# Create the graph using ggplot2 and render the plot
  
  output$graph <- renderPlot({
    req(input$x_param, input$fill_param, input$facet_param, input$data_column, selected_x_values())
    
    facets <- input$facet_param %>% 
      str_replace_all(",", "+") %>%
      rlang::parse_exprs()
    
    
  p <- ggplot(filtered_data(), aes_string(x = factor(filtered_data()[[input$x_param]], levels = selected_x_values()), y = col_escaped(input$data_column)))
  
 (...)

The good news is that I can make a plot. The bad news is that I still cannot have the order right. I know the first part in the update is showing the selected_x_values but somehow the order is still coming alphabetically.

I still need some help here.

UPDATE2:

Answer in the comment below!

JMarchante
  • 123
  • 1
  • 4
  • 11

1 Answers1

0

I find it amusing how this became a Monologue but I still managed to get the solution after so much thought.

The solution comes from this piece:

  # Filter the data based on the selected filter columns
  filtered_data <- reactive({
    data <- merged_data()
    
    for (col in names(annotation())) {
      if (!is.null(input[[paste0("filter_", col)]])) {
        filter_values <- input[[paste0("filter_", col)]]
        
        if (length(filter_values) > 0) {
          col_escaped <- if (grepl("[[:punct:]]", col)) paste0("`", col, "`") else col
          
          if (is.numeric(annotation()[[col]])) {
            data <- data[data[[col_escaped]] %in% filter_values, ]
          } else {
            data[[col_escaped]] <- factor(data[[col_escaped]], levels = filter_values)
            data <- data[complete.cases(data), ]  # Remove rows with NAs after factor conversion
          }
        }
      }
    }
    
    data
  })
  

Basically, I knew I had to play with factor and levels to have the order of the axis working in ggplot. I just did not know how to apply that from the choices in the filter section. The solution is actually really simple, I just converted the columns during the filter/subset as factors and assigned the levels as the choices from the filters. After that, I just had to add a line to remove all the values that did not have a level assigned as they were being plotted as "NA" and the filter/subset was not working.

It now works perfectly and if anyone is willing to use the app to plot tabular data, please feel free to do it and let me know how you would change it/improve it :)

Thank you everyone that view the question and actually thought about it.

JMarchante
  • 123
  • 1
  • 4
  • 11