2

I have a data frame of two columns and several rows. I want each row of the data frame to be represented as a choice in a selectInput(). The two elements of each row are supposed to be separated by |. These separators should be exactly one below each other in the selectInput(). Furthermore, when a choice is selected only the first element corresponding to the first column should be shown.

My idea was to use the number of chars to achieve the alignment. However, different chars have different sizes. That's why this appraoch unfortunately doesn't work. See the following example.

library(shiny) 
library(stringi)

a <- c("Veronica", "Paul", "Elisabeth", "Mike", "Katy", "Tim")
b <- c(50015, 23010, 86812, 55497, 32309, 67631)
data <- data.frame(a, b)

ui <- fluidPage(
  selectInput("selectID", 
              label = "Test Label:", 
              choices = as.list(c("", paste(data[, 1], stri_dup(intToUtf8(160), max(nchar(data[, 1])) - nchar(data[, 1])), "|", data[, 2]))))
)

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

shinyApp(ui, server)

Additionally, when I select the first row for example, only Veronica should be shown. At the moment, I do not have an idea how to achieve this.

By the way, I saw this kind of dropdown menu in an Access database. So maybe shiny or a shiny related package offers this kind of functionality and I just didn't find it. In any case, I appreciate any help.

Parfait
  • 104,375
  • 17
  • 94
  • 125

1 Answers1

4

You're almost there. To display a different value to the selection of a selectInput, you have to name the elements of the choices vector. The padding doesn't show up inside the dropdown because the default behaviour of HTML is to collapse white space. So you need to tweak the CSS that formats the dropdown. That's what the tags$head(...) is for.

I've tidied up your derivation of the choice list and added a textOutput to demonstrate the difference between the items displayed by the selectInput and its return value.

library(shiny) 
library(stringr)

a <- c("Veronica", "Paul", "Elisabeth", "Mike", "Katy", "Tim")
b <- c(50015, 23010, 86812, 55497, 32309, 67631)
data <- data.frame(a, b)
maxWidth <- max(str_length(data[, 1]))
choiceList <- data[, 1]
names(choiceList) <- paste0(str_pad(data[, 1], width=maxWidth, side="right"), "|", data[, 2])

ui <- fluidPage(
  tags$head(tags$style(".option {font-family: Monospace; white-space: pre; }")),
  selectInput("selectID", 
                label = "Test Label:", 
                choices = choiceList
                ),
    textOutput("selection")
)

server <- function(input, output, session) {
    output$selection <- renderText({ input$selectID })
}

shinyApp(ui, server)

enter image description here enter image description here

Note that the derivation of the choices for the selectInput is static. If you want that to be dynamic, you need to move the code to populate it inside the server function and wrap it in an observe or observeEvent. You'd probably need to make data reactive as well.

Edit: dealing with proportional fonts

The link in my comment below gives the key to a solution using shinyWidgets. My guess that using tags$span() would help was wrong because all the arguments to tags$span() are taken as the content of the tag rather than as arguments to the tag. So we need to construct the necessary HTML manually.

For convenience, I've added the a variable containing the necessary HTML to the data frame. The rowwise() is necessary to limit the concatenation to the current row rather than the entire data frame. I assume that the "|" separator is no longer required.

Pick the width of the col1 class to be "long enough" or calculate it on the fly.

library(shiny)
library(shinyWidgets)
library(tidyverse)

a <- c("Veronica", "Paul", "Elisabeth", "Mike", "Katy", "Tim")
b <- c(50015, 23010, 86812, 55497, 32309, 67631)
data <- data.frame(a, b) %>% 
  rowwise() %>% 
  mutate(dropdownText=HTML(paste0("<span class='col1'>", a, "</span><span class='col2'>", b, "</span>"))) %>% 
  ungroup()

ui <- fluidPage(
  tags$head(
    tags$style(".col1 {min-width: 150px; display: inline-block; }"),
    tags$style(".col2 {min-width: auto;  display: inline-block; }")
  ),
  pickerInput("selectID", 
              label = "Test Label:", 
              choices = data$a,
              choicesOpt=list(content=data$dropdownText)
  ),
  textOutput("selection")
)

server <- function(input, output, session) {
  output$selection <- renderText({ input$selectID })
}

shinyApp(ui, server)

There are undoubtedly better (ie more precise) ways of specifying the CSS classes and selectors, but I don't know enough CSS to know what they are.

enter image description here

Edit 2

To display only the first column in the selectInput but both columns in the dropdown, change the tags$head() to

  tags$head(
    tags$style(".col1 {min-width: 150px; display: inline-block; }"),
    tags$style(".col2 {min-width: auto;  display: inline-block; }"),
    tags$style(".filter-option-inner-inner .col2 {min-width: auto; 
               display: inline-block; visibility: hidden; }")
  )

The third element overrides the visibility of elements styled with thecol2 style when they are children of elements with the filter-option-inner-inner style. You can see how various elements of the UI are styled by opening the app in a browser, right-clicking anywhere on the page and selecting "Inspect" or similar.

enter image description here

Limey
  • 10,234
  • 2
  • 12
  • 32
  • Ah. I've just noted your comment about the alignment of the separators in the dropdown list. Sorry. That's trickier with a proportional font. Are you willing to use a monospace font? – Limey Jul 31 '21 at 10:06
  • Thanks for your help. I would like to have proportional font. Do you have an idea how to achieve this? And could you please be a bit more precise about the last paragraph? – Fire Salamander Jul 31 '21 at 13:25
  • Alignment is difficult when using a proportional font because counting characters and padding with spaces is no longer sufficient. You need to use HTML and/or CSS to get the alignment. I don't know how to pass HTML to a `selectInput`. However, the `showContent` parameter to `pickerOptions()` in the `shinyWidgets` [package](https://github.com/dreamRs/shinyWidgets) makes me think `shinyWidgets::pickerInput` has possibilities... – Limey Jul 31 '21 at 15:21
  • Yes. I reckon you could adapt the answer to [this question](https://stackoverflow.com/questions/47741321/adding-country-flag-to-pickerinput-shinywidgets) to get what you want, using a couple of `tags$span`s in place of the `tags$img` in the defintion of `choicesOpt`. Once you figure out the right HTML/CSS, of course... – Limey Jul 31 '21 at 15:33
  • That's a very nice solution, thanks a lot. Do you think it's possible to show only the first column entry after the selection? For example, when the first choice is selected, only "Veronica" is shown but not "50015". – Fire Salamander Jul 31 '21 at 18:35
  • See edit 2 above. You wrote "... could you please be a bit more precise about the last paragraph?". Which last paragraph? I've lost track... – Limey Aug 01 '21 at 11:00
  • Edit 2 is exactly what I meant. Thank you very much. I am aware of the "Inspect" feature but it seems to me that one needs to know a little HTML and CSS to use it. So you helped me a lot. – Fire Salamander Aug 01 '21 at 16:17