TL; DR: try our shiny new nutritional search engine. Feedback welcome.

“In the middle of our life’s journey, I found myself in a dark wood.” So starts Dante’s Inferno. My midlife doesn’t feel remotely as bleak, but for reasons that will be best left untold, I had to almost completely strike two nutrients from my diet: sodium and sugar. This started a thorough examination of the body of knowledge contained in nutritional labels that, in the US and many other countries, are mandatory on most packaged foods.

Despite the wisdom thus accrued, shopping frustration was inevitable. For instance, available cold breakfasts can be roughly classifieds as granolas, which are too high in sugar, and cereals, which are too high in sodium. After a long search that also involved loving family members, we found that the intersection of no sugar and no sodium cereals contained all of two products available in our favorite grocery stores. It seems natural to think that a better solution to this problem must be e-commerce, with its enormous selection and powerful search features. While the selection is indeed enormous – roughly 1M items on Amazon alone – nutritional content search is still at most a figment of Jeff Bezos imagination. Free text searches for [low sodium low sugar cereal], despite recent advances in NLP, resulted only in contempt for my own profession. If one replaces searching with browsing, no matter how time consuming, nutritional information is sometimes available, in the form of pictures of the packaging, but not on every site and not very consistently. There has to be a better way. So I decided to write something myself.

## Implementation

Now a little on the making of this shiny app. It is pretty run-of-the mill, with the only twist of some dynamic UI elements. The app is organized into three files, ui.R, server.R and global.R. The latter can be used to create objects shared by the other two, and since we have some data-dependent UI elements, that’s where we read in the dataset.

In the global.R file, we just read in the data, change the col names to something more readable and turn integer cols into numeric ones, as all these data are continuous in reality:

food_data = read.table("sr28abbr/ABBREV.txt", sep = "^", quote = "~")

names(food_data) =
c("food_code", "food_desc", "water", "energy", "protein", "fat", "ash", "carbohydrate_plus_fiber", "fiber", "sugar", "calcium", "iron", "magnesium", "phosphorus", "potassium", "sodium", "zinc", "copper", "manganese", "selenium", "vitamin_c", "thiamin", "riboflavin", "niacin", "pantothenic_acid", "vitamin_b6", "folate_total", "folic_acid", "food_folate", "folate", "choline", "vitamin_b12", "vitamin_a", "vitamin_a_retinol", "retinol", "alpha_carotene", "beta_carotene", "beta_cryptoxanthin", "lycopene", "lutein", "vitamin_e", "vitamin_d", "vitamin_d_iu", "vitamin_k", "saturated_fatty_acids", "monounsaturated_fatty_acids", "polyunsaturated_fatty_acids", "cholesterol", "first_household_weight", "description_household_weight_1", "second_household_weight", "description_household_weight_2", "refuse")

food_data$food_desc = paste0( "<a href=\"http://google.com/search?q=", map(as.character(food_data$food_desc), URLencode), "\">",
food_data$food_desc, "</a>") food_data %>% map(function(x) if(is.integer(x)) as.numeric(x) else x) %>% data.frame -> food_data On the server side, a couple of helper functions help filling in some empty values coming from the UI, to avoid downstream errors, and help parse text field containing comma-separated R expressions, to be fed as arguments to dplyr verbs: default = function(x, value, condition = is.null) if(condition(x)) value else x parse_field = function(field) { as.list( parse(text = paste0("list(", field, ")"))[[1]][-1])} Let’s get the server started shinyServer(function(input, output) { Then we wrap the generic dplyr verb into a function that will pick the right one based on a string, parse the argument and feed them to it. This is a bridge between text inputs and actual code: verb = function(data, name, val) { get(paste0(name, "_"))( data, .dots = parse_field(default(input[[name]], val, function(x) is.null(x) || x == "")))} The main output is a table, a simple derivative of the main data set, which is obtained in one of two possible ways, one is for the sodium-focused search and the other for the advanced search: output$food_data =
DT::renderDataTable({
switch(
input$search_type, The processing for the low sodium search consists of pattern matching the user input on the food_desc column, then filtering on sodium, sodium/energy and sodium/protein in a cascade based on thresholds obtained from the UI, entered by the user: Low Sodium = { food_data %>% filter( grepl( x = food_desc, pattern = default(input$food_type, ""),
ignore.case = TRUE)) %>%
filter(sodium <= default(input$sodium, 120)) %>% filter(sodium/energy <= default(input$sodium_energy, 0.6)) %>%
filter(sodium/protein <= default(input$sodium_protein, 19)) %>% select_(.dots = c("food_desc", "sodium", colnames(food_data)))}, The advanced processing is also a cascade of dplyr verbs, 4 of the most commonly used, fed user inputs as arguments or some sensible defaults. Advanced = { food_data %>% verb("mutate", "") %>% verb("filter", "TRUE") %>% verb("arrange", "food_code") %>% verb("select", paste(names(food_data), collapse = ","))}) }, escape = FALSE) }) Switching to the UI side, we define a couple of specialized input elements. The first is a slider input with data-dependent min, max and starting values: sodium_quantiles = quantile(food_data$sodium, na.rm = TRUE)
sodium_energy_quantiles =
quantile(food_data$sodium/(food_data$energy + 0.01), na.rm = TRUE)
sodium_protein_quantiles =
quantile(food_data$sodium/(food_data$protein + 0.01), na.rm = TRUE)

sodium_slider_input =
function(inputId, label, quantiles)
sliderInput(
inputId,
label,
round(quantiles[["0%"]], 2),
round(quantiles[["75%"]], 2),
round(quantiles[["25%"]], 2))

The second is a text input used for dplyr verb arguments, which have as name and id the verb itself and a different placeholder for each verb (the placeholder is what is displayed in an empty text input):

ti =
function(name, placeholder)
textInput(name, name, "", "100%", placeholder)

Now we can start defining the actual UI. We pick a fluid page because we think fluid layouts best adapt to a variety of screen sizes. We adopt a simple sidebar layout, with inputs on the left and the data output on the right:

shinyUI(
fluidPage(
titlePanel("Nutritional Food Search"),
p(a(href="http://www.ars.usda.gov/Services/docs.htm?docid=25700", "Dataset"),
"from USDA"),

The first input element is a selection between the two types of search:

sidebarLayout(
sidebarPanel(
selectInput("search_type", "Search type", c("Low Sodium", "Advanced")),

Then we have two conditional panels, which appear only if a condition, written in JavaScript, is satisfied. The alternative to writing JavaScript was to build UI elements on the server side, where the output of the previous selection is available in R. I feel this is a choice between a rock and a hard place. It may be that my knowledge of shiny is not advanced enough or a design flaw. Multiple available examples point to the latter, but I don’t know how hard it would be to fix it. The first of the two panels is for the sodium-focused search and contains three sliders which provide values later used in filtering the main dataset:

conditionalPanel(
'input.search_type == "Low Sodium"',
p("Simple search for low sodium foods. Minimize your sodium intake per amount of food, energy or protein"),
textInput("food_type", "Food type", "", placeholder = "Partial food name, e.g. 'cheese' or empty"),
sodium_slider_input(
"sodium",
"max sodium per weight mg/100 g",
sodium_quantiles),
sodium_slider_input(
"sodium_energy",
"max sodium per energy mg/kcal",
sodium_energy_quantiles),
sodium_slider_input(
"sodium_protein",
"max sodium per protein mg/g",
sodium_protein_quantiles)),

The second panel has a text input for each verb, which will be converted into the .dots argument and passed to the appropriate function:

conditionalPanel(