Building Shiny Dashboards

Learn how to build a shiny dashboard in R to help users analyze, visualize, and understand their data.

26 min read Ben Hayes

Table of Contents


Introduction

Shiny apps provide interactivity with your data analysis and visualizations. A popular and growing subset of Shiny apps includes Shiny Dashboards which offer one-stop destinations for data analysis, summary, and visualization. There are two primary types of Shiny Dashboards: shiny dashboards and flex dashboards. While this post will focus on flex dashboards, you should become familiar with both as there are tradeoffs in choosing one over the other. A few of the key differences between shiny dashboards and flex dashboards are significant and make them largely incompatible. Shiny dashboards are stored in .R files while flex dashboards support the .Rmd extension. This means flex dashboards can contain code chunks. Another difference is that flex dashboards have simplified server and ui code (as the chunks handle the processing and functions handle the layout).

To get a better sense of what Shiny Dashboards (or apps in general) are capable of, visit the Shiny gallery here: https://shiny.rstudio.com/gallery/. To learn more about building and deploying Shiny apps, visit the Shiny homepage here: https://shiny.rstudio.com/

The Shiny Dashboard that we will be building today will help the City of Pittsburgh better understand non-traffic citations. Screenshots of the various pages in the dashboard can be seen in the images here:


Understanding the Data

The City of Pittsburgh publishes data on non-traffic citations in the area. The data is available to download in CSV format from the Western PA Regional Data Center - Non-Traffic Citations. The WPRDC’s description of the data set is provided below:

Non-traffic citations (NTCs, also known as "summary offenses") document low-level criminal offenses where a law enforcement officer or other authorized official issued a citation in lieu of arrest. These citations normally include a fine. In Pennsylvania, NTCs often include a notice to appear before a magistrate if the person does not provide a guilty plea. Offenses that normally result in a citation include disorderly conduct, loitering, harassment and retail theft. This data set only contains information reported by City of Pittsburgh Police. It does not contain incidents that solely involve other police departments operating within the city (for example, campus police or Port Authority police).

The data set contains over 5000 non-traffic citations (rows) each with an age, gender, race, neighborhood, offense, and date/time (columns). In the example below, we do a little bit of feature extraction/engineering with the date/time field. The results of that process are then used in the configurable filters.


Forming Questions

Now that we know what is contained within the data set, we can begin to form questions that we might answer using the data. A few example questions that may come up include:

  • How do age, race, and gender affect issuance of non-traffic citations?
  • Do certain neighborhoods have more non-traffic citations?
  • Are the number of non-traffic citations increasing, decreasing, or remaining the same over time?
  • Do non-traffic citations occur more frequently on weekdays or weekends? During the day or at night?

Since our shiny dashboard will have reactivity and interactivity, the user can drive the data exploration by applying different combinations of filters. This is one of the key advantages of shiny dashboards.


Cleaning the Data

Prior to building the dashboard, we must clean the data so that it is usable for import, analysis, and initial visualization.

 1```{r load_prep_data}
 2
 3# Read in non-traffic citations data from the data folder
 4df_citations_raw <- read.csv("./data/pittsburgh_nontraffic_citations.csv")
 5
 6# Clean the citation data
 7df_citations_clean <- df_citations_raw %>%
 8  select(-X_id, -ZONE, -INCIDENTTRACT, -COUNCIL_DISTRICT, -PUBLIC_WORKS_DIVISION) %>%  # Deselect unnecessary columns
 9  rename("key" = PK,
10         "gender" = GENDER,
11         "race" = RACE,
12         "age" = AGE,
13         "citedtime" = CITEDTIME,
14         "incident_location" = INCIDENTLOCATION,
15         "offense" = OFFENSES,
16         "neighborhood" = NEIGHBORHOOD,
17         "Lat" = Y,
18         "Long" = X
19         ) %>%
20  mutate(
21    # Change gender to full text
22    gender = ifelse(gender == "M", "Male", "Female"),
23    
24    # Change race to full text
25    race = case_when(
26      race == "A" ~ "Asian",
27      race == "B" ~ "African American",
28      race == "H" ~ "Hispanic",
29      race == "I" ~ "Unknown",
30      race == "O" ~ "Other",
31      race == "W" ~ "White",
32      TRUE ~ as.character(race)
33    ),
34    
35    # Create date/time variables
36    citation_date = ymd_hms(citedtime), # Parse citedtime using lubridate::ymd_hms()
37    year = year(citation_date),
38    month = month(citation_date),
39    hour_of_day = hour(citation_date),
40    am_pm = am(citation_date),
41    am_pm = ifelse(am_pm == TRUE, "AM", "PM"),
42    
43    # Create day of week variable
44    day_of_week = wday(citation_date),
45    day_of_week_full = case_when(
46      day_of_week == 1 ~ "Sunday",
47      day_of_week == 2 ~ "Monday",
48      day_of_week == 3 ~ "Tuesday",
49      day_of_week == 4 ~ "Wednesday",
50      day_of_week == 5 ~ "Thursday",
51      day_of_week == 6 ~ "Friday",
52      day_of_week == 7 ~ "Saturday"
53    ),
54    # Create month full name variable
55    month_name = case_when(
56      month == 1 ~ "January",
57      month == 2 ~ "February",
58      month == 3 ~ "March",
59      month == 4 ~ "April",
60      month == 5 ~ "May",
61      month == 6 ~ "June",
62      month == 7 ~ "July",
63      month == 8 ~ "August",
64      month == 9 ~ "September",
65      month == 10 ~ "October",
66      month == 11 ~ "November",
67      month == 12 ~ "December"
68    )
69    
70  ) %>%
71  select(-citedtime)  # Deselect unnecessary columns
72
73# Change factor orders
74df_citations_clean$day_of_week_full <- factor(df_citations_clean$day_of_week_full, 
75                                              levels = c('Sunday', 
76                                                         'Monday', 
77                                                         'Tuesday', 
78                                                         'Wednesday', 
79                                                         'Thursday', 
80                                                         'Friday', 
81                                                         'Saturday'))
82df_citations_clean$month_name <- factor(df_citations_clean$month_name, 
83                                        levels = c('January',
84                                                   'February',
85                                                   'March',
86                                                   'April',
87                                                   'May',
88                                                   'June',
89                                                   'July',
90                                                   'August',
91                                                   'September',
92                                                   'October',
93                                                   'November',
94                                                   'December'))
95```

Building the Shiny App

Markdown / Libraries

As mentioned above, flexdashboards use RMarkdown as the format and rely less on clearly defined server/ui components. In order to have our document output a shiny app, we must specify that in the document information at the top of the file. Here, specifying the runtime as ‘shiny’ tells R to execute a shiny app instead of knitting to HTML, PDF, or another file type. A few notable libraries are flexdashboard, shiny, and leaflet. These are needed for the dashboard framework, shiny reactivity, and leaflet map visualization.

 1---
 2title: "Pittsburgh Non-Traffic Citations"
 3runtime: shiny
 4output: 
 5  flexdashboard::flex_dashboard:
 6    orientation: row
 7    vertical_layout: fill
 8---
 9
10```{r setup, include=FALSE}
11library(flexdashboard)
12library(shiny)
13library(plyr)
14library(dplyr)
15library(tidyr)
16library(plotly)
17library(DT)
18library(leaflet)
19library(lubridate)
20
21pdf(NULL)
22```

Reactivity

Shiny is designed in a way that minimizes the amount of user design experience and event handling that we are responsible for and lets you focus on the logic behind your app, charts, and data manipulations. One task that we are responsible for is making our data reflect changes in the user’s inputs. Shiny tackles this by allowing us to create a function that returns a dataframe. Within the function, we can select, filter, and/or group our data as needed - tidyverse packages are recommended here but not required. To accomplish this, the Shiny framework provides a function called reactive() that lets us specify what data to return. Other functions within our app (e.g., ggplot()) can call this function and it will act as a dataframe.

 1```{r reactive}
 2# Create reative dataframe to filter by inputs
 3df_citations_reactive <- reactive({
 4  
 5  # Apply inputs as filter criteria
 6  df <- df_citations_clean %>% 
 7    filter(neighborhood %in% input$neighborhood_select) %>%
 8    filter(gender %in% input$gender_checkbox) %>%
 9    filter(race %in% input$race_select) %>%
10    filter(age >= input$age_slider[1] & age <= input$age_slider[2]) %>%
11    filter(citation_date >= input$date_range[1] & citation_date <= input$date_range[2]) %>%
12    filter(day_of_week %in% input$day_of_week_select) %>%
13    filter(am_pm %in% input$am_pm_checkbox)
14  
15  return(df)
16})
17```

Once we establish how the data will be filtered, we should define our input widgets. Shiny comes with many inputs out of the box that can be further configured to meet our needs (slider, date range, checkbox, dropdown, etc.). Notice how the sidebar is declared using a simple keyword (‘Sidebar’) followed by a class definition and 37 equal signs. This indicates that the sidebar panel will be global/persist across all pages. More on pages later.

 1Sidebar {.sidebar}
 2=====================================
 3
 4#### **Location Filters**
 5
 6```{r loc_inputs}
 7selectInput("neighborhood_select",
 8            "Neighborhood of Citation:",
 9            choices = sort(levels(df_citations_clean$neighborhood)),
10            multiple = TRUE,
11            selectize = TRUE,
12            selected = c("East Liberty", "Mount Washington", "Friendship", "Point Breeze", "Shadyside", 
13                         "Bloomfield", "Central Oakland", "North Oakland", "West Oakland")
14            )
15```
16
17<hr/>
18
19#### **Individual Filters**
20
21```{r indiv_inputs}
22checkboxGroupInput("gender_checkbox",
23                   "Gender:",
24                   choices = sort(unique(df_citations_clean$gender)),
25                   selected = sort(unique(df_citations_clean$gender)),
26                   inline = T
27                   )
28
29selectInput("race_select",
30            "Race:",
31            choices = sort(unique(df_citations_clean$race)),
32            multiple = TRUE,
33            selectize = TRUE,
34            selected = sort(unique(df_citations_clean$race))
35            )
36
37sliderInput("age_slider",
38            "Age:",
39            min = min(df_citations_clean$age, na.rm = T),
40            max = max(df_citations_clean$age, na.rm = T),
41            value = c(min(df_citations_clean$age, na.rm = T), max(df_citations_clean$age, na.rm = T)),
42            step = 1
43            )
44```
45
46<hr/>
47
48#### **Date/Time Filters**
49
50```{r date_time_inputs}
51dateRangeInput("date_range",
52               "Incident Date:",
53               min = min(df_citations_clean$citation_date),
54               start = min(df_citations_clean$citation_date),
55               format = "yyyy-mm-dd"
56               )  # No value provided for max or end so today's date is used (default)
57
58selectInput("day_of_week_select",
59            "Incident Day of Week:",
60            choices = list(Sunday = 1, Monday = 2, Tuesday = 3, Wednesday = 4, 
61                           Thursday = 5, Friday = 6, Saturday = 7), # Use named list to sort values (Sun, Mon, etc.)
62            selected = c(1, 2, 3, 4, 5, 6, 7),
63            multiple = T,
64            selectize = T
65            )
66
67checkboxGroupInput("am_pm_checkbox",
68                   "AM/PM:",
69                   choices = sort(unique(df_citations_clean$am_pm)),
70                   selected = sort(unique(df_citations_clean$am_pm)),
71                   inline = T
72                   )
73```

Pages / Plots

Once the sidebar has been filled out with the input filters, we will now start making the individual pages. All of our charts and figures will be contained within pages. Each page is defined similarly to the sidebar panel. In this case, the title is specified above the line of equal signs. In flex dashboards, pages appear at the top in the NavBar section but in shiny dashboards, by default, they appear in the sidebar.

For the first time, we also encounter the ‘Row’ keyword. Since we specified the orientation in the document header as ‘row’ we can declare new rows in the body. Each row is then populated with the charts and valueboxes listed in the ‘Row’ section. In this example, we have two rows on this page - one that contains tabs for each chart.

 1Who Is Being Cited?
 2=====================================
 3
 4Row 
 5-------------------------------------
 6
 7###
 8
 9```{r}
10renderValueBox({
11  # Get count of citations total
12  count_citations <- nrow(df_citations_clean)
13  valueBox("# of Non-Traffic Citations", value = count_citations, color = "black")  # No icon since color is black
14})
15```
16
17###
18
19```{r}
20renderValueBox({
21  # Get count of citations after applying filters
22  count_citations_reactive <- paste0(nrow(df_citations_reactive()), ' (', round(nrow(df_citations_reactive()) * 100 / nrow(df_citations_clean), 1), '%)')
23  valueBox("# Non-Traffic Citations Matching Filter Criteria", value = count_citations_reactive, icon="fa-filter", color = "grey")
24})
25```
26
27###
28
29```{r}
30renderValueBox({
31  # Calculate average age among remaining citations
32  avg_age <- round(mean(df_citations_reactive()$age), 2)
33  valueBox("Average Age (years)", value = avg_age, icon="fa-hashtag", color = "green")
34})
35```
36
37###
38
39```{r}
40renderValueBox({
41  # Calculate percent that are male
42  pct_male <- round(length(which(df_citations_reactive()$gender == 'Male')) / nrow(df_citations_reactive()), 4) * 100
43  valueBox("Percent Male", value = paste0(pct_male, '%'), icon="fa-percent", color = "steelblue")
44})
45```
46
47Row {.tabset .tabset-fade}
48-----------------------------------------------------------------------
49
50### Non-Traffic Citations by Gender
51
52```{r}
53renderPlotly({
54  dat <- df_citations_reactive()
55  ggplot(data = dat, aes(x = gender)) + 
56    geom_bar(stat="Count") +
57    labs(x = "Gender", y = "Count", title = "Non-Traffic Citations by Gender")
58})
59```
60
61### Non-Traffic Citations by Race
62
63```{r}
64renderPlotly({
65  dat <- df_citations_reactive()
66  ggplot(data = dat, aes(x = race)) + 
67    geom_bar(stat="Count") +
68    labs(x = "Race", y = "Count", title = "Non-Traffic Citations by Race")
69})
70```
71
72### Non-Traffic Citations by Age
73
74```{r}
75renderPlotly({
76  dat <- df_citations_reactive()
77  ggplot(data = dat, aes(x = age)) + 
78    geom_bar(stat="Count") +
79    labs(x = "Age", y = "Count", title = "Non-Traffic Citations by Race")
80})
81```

Within the ‘Who Is Being Cited?’ page, the charts will be displayed beneath the value boxes in individual tabs.

Adding a second page is as simple as adding the first page. The code below adds interactive charts which focus on time data.

  1When Are People Being Cited?
  2=====================================
  3
  4Row 
  5-------------------------------------
  6
  7###
  8
  9```{r}
 10renderValueBox({
 11  count_citations <- nrow(df_citations_clean)
 12  valueBox("# of Non-Traffic Citations", 
 13           value = count_citations, 
 14           color = "black")  # No icon since color is black
 15})
 16```
 17
 18###
 19
 20```{r}
 21renderValueBox({
 22  count_citations_reactive <- paste0(nrow(df_citations_reactive()), ' (', round(nrow(df_citations_reactive()) * 100 / nrow(df_citations_clean), 1), '%)')
 23  valueBox("# Non-Traffic Citations Matching Filter Criteria", 
 24           value = count_citations_reactive, 
 25           icon="fa-filter", 
 26           color = "grey")
 27})
 28```
 29
 30###
 31
 32```{r}
 33renderValueBox({
 34  # Get month with most non-traffic citations
 35  max_month <- df_citations_reactive() %>%
 36    group_by(month_name) %>%
 37    summarize(
 38      n = n()
 39    ) %>%
 40    arrange(desc(n))
 41  valueBox("has the most non-traffic citations", 
 42           value = as.character(max_month$month_name[1]), 
 43           icon="fa-calendar", 
 44           color = "green")
 45})
 46```
 47
 48###
 49
 50```{r}
 51renderValueBox({
 52  # Get day with most non-traffic citations
 53  max_day <- df_citations_reactive() %>%
 54    group_by(day_of_week_full) %>%
 55    summarize(
 56      n = n()
 57    ) %>%
 58    arrange(desc(n))
 59  
 60  # Conditionally change the icon for the value box
 61  day_icon = 'fa-moon-o'
 62  
 63  if (length(which(df_citations_reactive()$am_pm == 'AM')) >= length(which(df_citations_reactive()$am_pm == 'PM'))) {
 64    day_icon = 'fa-sun-o'
 65  }
 66  
 67  valueBox("has the most non-traffic citations", 
 68           value = as.character(max_day$day_of_week_full[1]), 
 69           icon=day_icon, 
 70           color = "steelblue")
 71})
 72```
 73
 74
 75Row {.tabset .tabset-fade}
 76-----------------------------------------------------------------------
 77
 78### Non-Traffic Citations by Month
 79
 80```{r}
 81renderPlotly({
 82  dat <- df_citations_reactive()
 83  ggplot(data = dat, aes(x = month_name)) + 
 84    geom_bar(stat = "Count") +
 85    labs(x = "Month", y = "Count", title = "Non-Traffic Citations by Month")
 86})
 87```
 88
 89### Non-Traffic Citations by Day of Week
 90
 91```{r}
 92renderPlotly({
 93  dat <- df_citations_reactive()
 94  ggplot(data = dat, aes(x = day_of_week_full)) + 
 95    geom_bar(stat="Count") +
 96    labs(x = "Day of Week", y = "Count", title = "Non-Traffic Citations by Day of Week")
 97})
 98```
 99
100### Non-Traffic Citations by Time of Day
101
102```{r}
103renderPlotly({
104  dat <- df_citations_reactive()
105  ggplot(data = dat, aes(x = am_pm)) + 
106    geom_bar(stat="Count") +
107    labs(x = "Time of Day", y = "Count", title = "Non-Traffic Citations by Time of Day")
108})
109```
110
111### Non-Traffic Citations Over Time
112
113```{r}
114renderPlotly({
115  dat <- df_citations_reactive()
116  ggplot(data = dat, aes(x = citation_date)) + 
117    geom_line(stat="Count") +
118    labs(x = "Time", y = "Count", title = "Non-Traffic Citations by Over Time")
119})
120```

Following the same pattern, we can include a third page - this time focusing on location data. The code below will add a page with a reactive leaflet plot. The setView() function will center the map on the Pittsburgh area.

 1Where Are Citations Occurring?
 2=====================================
 3
 4Row 
 5-------------------------------------
 6
 7###
 8
 9```{r}
10renderValueBox({
11  count_citations <- nrow(df_citations_clean)
12  valueBox("# of Non-Traffic Citations", 
13           value = count_citations, 
14           color = "black")  # No icon since color is black
15})
16```
17
18###
19
20```{r}
21renderValueBox({
22  count_citations_reactive <- paste0(nrow(df_citations_reactive()), ' (', round(nrow(df_citations_reactive()) * 100 / nrow(df_citations_clean), 1), '%)')
23  valueBox("# Non-Traffic Citations Matching Filter Criteria", 
24           value = count_citations_reactive, 
25           icon="fa-filter", 
26           color = "grey")
27})
28```
29
30###
31
32```{r}
33renderValueBox({
34  # Get neighborhood with most non-traffic citations
35  max_neighborhood <- df_citations_reactive() %>%
36    group_by(neighborhood) %>%
37    summarize(
38      n = n()
39    ) %>%
40    arrange(desc(n))
41  valueBox("has the most non-traffic citations", 
42           value = as.character(max_neighborhood$neighborhood[1]), 
43           icon="fa-map-marker", 
44           color = "green")
45})
46```
47
48###
49
50```{r}
51renderValueBox({
52  # Get neighborhood with most non-traffic citations
53  min_neighborhood <- df_citations_reactive() %>%
54    group_by(neighborhood) %>%
55    summarize(
56      n = n()
57    ) %>%
58    arrange(n)
59  valueBox("has the least non-traffic citations", 
60           value = as.character(min_neighborhood$neighborhood[1]), 
61           icon="fa-map-marker", 
62           color = "steelblue")
63})
64```
65
66Row {.tabset .tabset-fade}
67-----------------------------------------------------------------------
68
69### Map of Non-Traffic Citations in Pittsburgh
70
71```{r}
72renderLeaflet({
73  leaflet(df_citations_reactive()) %>%
74    addTiles() %>%
75    setView(lat = 40.45, lng = -79.95, zoom = 13) %>% 
76    addCircles(~Long, 
77               ~Lat,
78               popup = ~as.character(paste0("<b>", key, "</b>: ", offense)))
79})
80```
81
82### Non-Traffic Citations by Neighborhood
83
84```{r}
85renderPlotly({
86  dat <- df_citations_reactive()
87  ggplot(data = dat, aes(x = neighborhood)) + 
88    geom_bar(stat = "Count") +
89    labs(x = "Neighborhood", y = "Count", title = "Non-Traffic Citations by Neighborhood") +
90    theme(axis.text.x = element_text(angle = 35, hjust = 1))
91})
92```

In the code chunk below, we declare the fourth page which features a DT::datatable. For the datatable output, we specify options/styling. For example, we shade morning citations in yellow and evening citations in blue.

 1Data Table
 2=====================================
 3
 4Row 
 5-------------------------------------
 6
 7###
 8
 9```{r}
10renderValueBox({
11  count_citations <- nrow(df_citations_clean)
12  valueBox("# of Non-Traffic Citations", 
13           value = count_citations, 
14           color = "black")  # No icon since color is black
15})
16```
17
18###
19
20```{r}
21renderValueBox({
22  count_citations_reactive <- paste0(nrow(df_citations_reactive()), ' (', round(nrow(df_citations_reactive()) * 100 / nrow(df_citations_clean), 1), '%)')
23  valueBox("# Non-Traffic Citations Matching Filter Criteria", 
24           value = count_citations_reactive, 
25           icon="fa-filter", 
26           color = "grey")
27})
28```
29
30Row 
31-------------------------------------
32
33### Data Table
34
35```{r render_datatable}
36DT::renderDataTable({
37  dat <- df_citations_reactive() %>%
38    select(-year, -month, -day_of_week, -day_of_week_full, -month_name, -hour_of_day)  # Deselect unwanted columns
39  
40  DT::datatable(dat,
41                rownames = F,
42                colnames = c("Key", "CCR", "Gender", "Race", "Age", "Address", "Offense", 
43                             "Neighborhood", "Latitude", "Longitude", "Date/Time", "AM/PM"),
44                extensions = 'Buttons',
45                options = list(pageLength = 10, 
46                               scrollX = T,
47                               filter = "top",
48                               dom = 'Bfrtip',
49                               buttons = c('csv', 'copy', 'print')
50
51                )) %>%
52    formatStyle(columns = seq(1, 21, 1), fontSize = "9pt") %>%  # Reduce fontsize for all columns
53    formatRound(columns = c('Lat', 'Long'), 5) %>%  # Round latitude and longitude to 5 decimal places, still outputs full value
54    formatStyle(
55      'gender',
56      target = 'cell',
57      backgroundColor = styleEqual(c('Male', 'Female'), c('lightblue', 'pink'))
58    ) %>%
59    formatStyle(
60      'am_pm',
61      target = 'cell',
62      backgroundColor = styleEqual(c('AM', 'PM'), c('yellow', 'steelblue'))
63    )
64})
65```

Making it Shine

If you are familiar with shiny dashboards (as opposed to flex dashboards), then you are probably wondering where the server and ui components have gone. Do not fret, we still have them, but they are much, much simpler to write now.

1```{r}
2ui <- fluidPage(
3)
4
5server <- function(input, output, session) {
6}
7
8shinyApp(ui, server)
9```

Deploying the Shiny App

Since Shiny apps are ‘apps’ and not reports, there needs to be a server to host the app. RStudio provides free hosting for Shiny apps via shinyapps.io. Certain limitations are in place for free accounts but for industrial apps, typically the organization or client will pay for hosting.

Sign up for a free account on http://www.shinyapps.io/ to get started. Once your account is created, follow the instructions to link your account to your RStudio / Shiny App. After you link your Shinyapps.io account to your Shiny app, you can publish the app and it will be hosted, accessible within minutes.

You can explore the Shiny app here on Shinyapps.io: benh.shinyapps.io/pittsburgh_non-traffic_citations/.


Bonus: Traditional Shiny Dashboard (Added: November 2018)

In order to provide a contrast between the flexdashboard and shinydashboards, I’ve provided a more traditional Shiny app. The second application helps visualize Pittsburgh’s trees by neighborhood, condition, and species. Features included are dynamic/draggable panels for inputs and charts, and user interaction with the geographic figures (both points and polygons). The code below is divided into two sections as you would typically find in a Shiny app: one for the UI and one for the server.

User Interface

The first piece of code captures the UI function which handles the layout of different components. In addition to the UI, there are a few lines that set the application up (in order to load libraries, define functions, etc.).

  1# Load data libraries
  2library(plyr)
  3library(dplyr)
  4library(tidyr)
  5library(rgdal)
  6library(httr)
  7library(htmltools)
  8library(jsonlite)
  9library(lubridate)
 10
 11# Load visualization libraries
 12library(shiny)
 13library(shinyjs)
 14library(shinythemes)
 15library(plotly)
 16library(DT)
 17library(ggplot2)
 18library(leaflet)
 19library(leaflet.extras)
 20
 21
 22###############
 23# Set-up Code #
 24###############
 25
 26# Create function that takes in a URL and returns a data frame from the API
 27grabData <- function(URL) {
 28  
 29  # Create request and extract content as text
 30  req = RETRY("GET", URLencode(URL))
 31  data = content(req, "text")
 32  
 33  # Use gsub to replace NaN with NA
 34  data_clean <- gsub('NaN', 'NA', data, perl = TRUE)
 35  
 36  # Return a dataframe
 37  data.frame(fromJSON(data_clean)$result$records)
 38}
 39
 40
 41# Define function to get unique values to use as choices/default values in select inputs
 42grabUniqueValues <- function(field_name) {
 43  # Construct URL
 44  URL <- paste0("https://data.wprdc.org/api/action/datastore_search_sql?sql=SELECT%20DISTINCT(%22", field_name, "%22)%20from%20%221515a93c-73e3-4425-9b35-1cd11b2196da%22")
 45  
 46  #Grab data after encoding URL
 47  c(grabData(URLencode(URL)))
 48}
 49
 50# Define function to get max values to use as max/min in select inputs
 51grabMaxMin <- function(field_name) {
 52  # Construct URL (probably a way to do this in one query but this works)
 53  maxURL <- paste0("https://data.wprdc.org/api/action/datastore_search_sql?sql=SELECT%20MAX(%22", field_name, "%22)%20from%20%221515a93c-73e3-4425-9b35-1cd11b2196da%22")
 54  minURL <- paste0("https://data.wprdc.org/api/action/datastore_search_sql?sql=SELECT%20MIN(%22", field_name, "%22)%20from%20%221515a93c-73e3-4425-9b35-1cd11b2196da%22")
 55  
 56  max <- grabData(URLencode(maxURL))$max
 57  min <- grabData(URLencode(minURL))$min
 58  
 59  return(c(min,max))
 60}
 61
 62# Get unique/max/min values
 63neighborhoods_all <- sort(grabUniqueValues("neighborhood")$neighborhood)
 64conditions_all <- sort(grabUniqueValues("condition")$condition)
 65diameter_maxmin <- grabMaxMin("diameter_base_height")
 66stem_maxmin <- grabMaxMin("stems")
 67
 68# Could not find pittsburgh neighborhood polygon data API that let me specify multiple neighborhoods.
 69# Grabbing geojson from arcgis endpoint
 70pittsburgh_neighborhood_gj <- readOGR("http://pghgis-pittsburghpa.opendata.arcgis.com/datasets/dbd133a206cc4a3aa915cb28baa60fd4_0.geojson")
 71
 72
 73##########
 74#   UI   #
 75##########
 76
 77ui <- fluidPage(
 78   useShinyjs(),
 79   navbarPage("Pittsburgh Trees", 
 80              theme = shinytheme("united"),
 81              tabPanel("Map", # Output Map
 82              
 83                div(class="mymap",
 84                  tags$head(
 85                    includeCSS("./css/custom_style.css")
 86                  ),
 87                  leafletOutput("tree_map", width = "100%", height = "100%"),
 88                  absolutePanel(
 89                    id = "neighborhood-chart",
 90                    fixed = TRUE,
 91                    draggable = TRUE,
 92                    top = 64,
 93                    left = 55,
 94                    right = "auto",
 95                    bottom = "auto",
 96                    width = 500,
 97                    height = "auto",
 98                    
 99                    h3("Neighborhood Stats:"),
100                    h5("Click a neighborhood polygon to see neighborhood stats."),
101                    
102                    htmlOutput("neighborhood_tree_count"),
103                    htmlOutput("scientific_name_count"),
104                    br(), # line break
105                    plotOutput("stems_plot"),
106                    plotOutput("value_plot")
107                  ),
108                  absolutePanel(
109                    id = "filters",
110                    fixed = TRUE,
111                    draggable = TRUE, 
112                    top = 64, 
113                    left = "auto", 
114                    right = 20, 
115                    bottom = "auto",
116                    width = 330, 
117                    height = "auto",
118                    
119                    h3("Filters:"),
120                    selectInput("neighborhood_select",
121                                "Neighborhoods:",
122                                choices = neighborhoods_all,
123                                multiple = TRUE,
124                                selectize = TRUE,
125                                selected = sort(c("East Liberty", 
126                                                  "Mount Washington", 
127                                                  "Friendship", 
128                                                  "Point Breeze", 
129                                                  "Shadyside", 
130                                                  "Bloomfield", 
131                                                  "Central Oakland", 
132                                                  "North Oakland", 
133                                                  "West Oakland",
134                                                  "Squirrel Hill North",
135                                                  "Squirrel Hill South",
136                                                  "Upper Hill",
137                                                  "Terrace Village",
138                                                  "Middle Hill",
139                                                  "Polish Hill",
140                                                  "Lower Lawrenceville",
141                                                  "Upper Lawrenceville",
142                                                  "South Shore",
143                                                  "North Shore",
144                                                  "Hazelwood",
145                                                  "Bluff",
146                                                  "Garfield",
147                                                  "Stanton Heights",
148                                                  "Highland Park",
149                                                  "Glen Hazel",
150                                                  "Morningside",
151                                                  "Central Lawrenceville",
152                                                  "Strip District",
153                                                  "Central Business District",
154                                                  "Crawford-Roberts",
155                                                  "Friendship",
156                                                  "Greenfield",
157                                                  "Bedford Dwellings",
158                                                  "South Oakland",
159                                                  "Larimer"))
160                    ),
161                    selectInput("condition_select",
162                                "Tree Condition:",
163                                choices = conditions_all,
164                                multiple = TRUE,
165                                selectize = TRUE,
166                                selected = conditions_all  # Select all by default
167                    ),
168                    sliderInput("diameter_range",
169                                "Diameter Base Height (ft):",
170                                min = diameter_maxmin[1],
171                                max = diameter_maxmin[2],
172                                value = c(diameter_maxmin[1], diameter_maxmin[2]) # Select min, max by default
173                    ),
174                    sliderInput("stems_range",
175                                "Stem Count:",
176                                min = stem_maxmin[1],
177                                max = stem_maxmin[2],
178                                value = c(stem_maxmin[1], stem_maxmin[2])
179                    ),
180                    actionButton('ignore_overhead_utils',
181                                 'Exclude Trees Under Utilities',
182                                 icon = icon("minus"),
183                                 style = "margin: 12px; margin-left: 0px;"
184                    ),
185                    actionButton('reset_filters',
186                                 'Reset Filters',
187                                 icon = icon("refresh")
188                    )
189                  )
190                )
191              ), # End map tabPanel
192            tabPanel("Table", # Output Data Table
193              fluidPage(
194                downloadButton('download_tree_data', "Download Data"),
195                tags$div(tags$br()), # Add HTML <br/> tag to space items
196                DT::dataTableOutput("tree_table"))
197            ) # End table tabPanel
198
199   ) # End navbarPage
200) # End UI

Server

Below you will find the server() function which handles the data processing and graphic generation via ggplot2

  1##########
  2# Server #
  3##########
  4
  5server <- function(input, output, session=session) {
  6  
  7  # Create reactive value for overhead button
  8  overhead_btn_value <- reactiveVal(0)
  9  
 10  # Toggle Include/Exclude overhead utilities button
 11  observeEvent(input$ignore_overhead_utils, {
 12    overhead_btn_value(overhead_btn_value() + 1)
 13    
 14    if (overhead_btn_value() %% 2 == 1) {
 15      updateActionButton(session, inputId = "ignore_overhead_utils", label="Include Trees Under Utilities", icon=icon('plus'))
 16    }
 17    else {
 18      updateActionButton(session, inputId = "ignore_overhead_utils", label="Exclude Trees Under Utilities", icon=icon('minus'))
 19    }
 20    print(names(input))
 21  })
 22  
 23  # Handle reset button
 24  observeEvent(input$reset_filters, {
 25    updateSelectInput(session, inputId = "neighborhood_select", selected = sort(c("East Liberty", 
 26                                                                                  "Mount Washington", 
 27                                                                                  "Friendship", 
 28                                                                                  "Point Breeze", 
 29                                                                                  "Shadyside", 
 30                                                                                  "Bloomfield", 
 31                                                                                  "Central Oakland", 
 32                                                                                  "North Oakland", 
 33                                                                                  "West Oakland",
 34                                                                                  "Squirrel Hill North",
 35                                                                                  "Squirrel Hill South",
 36                                                                                  "Upper Hill",
 37                                                                                  "Terrace Village",
 38                                                                                  "Middle Hill",
 39                                                                                  "Polish Hill",
 40                                                                                  "Lower Lawrenceville",
 41                                                                                  "Upper Lawrenceville",
 42                                                                                  "South Shore",
 43                                                                                  "North Shore",
 44                                                                                  "Hazelwood",
 45                                                                                  "Bluff",
 46                                                                                  "Garfield",
 47                                                                                  "Stanton Heights",
 48                                                                                  "Highland Park",
 49                                                                                  "Glen Hazel",
 50                                                                                  "Morningside",
 51                                                                                  "Central Lawrenceville",
 52                                                                                  "Strip District",
 53                                                                                  "Central Business District",
 54                                                                                  "Crawford-Roberts",
 55                                                                                  "Friendship",
 56                                                                                  "Greenfield",
 57                                                                                  "Bedford Dwellings",
 58                                                                                  "South Oakland",
 59                                                                                  "Larimer"))
 60                      )
 61    updateSelectInput(session, inputId = "condition_select", selected = conditions_all)
 62    updateSliderInput(session, inputId = "diameter_range", value = c(diameter_maxmin[1], diameter_maxmin[2]))
 63    updateSliderInput(session, inputId = "stems_range", value = c(stem_maxmin[1], stem_maxmin[2]))
 64    updateActionButton(session, inputId = "ignore_overhead_utils", label="Exclude Trees Under Utilities", icon=icon('minus'))
 65    overhead_btn_value(0) # Keep the reactive value in sync
 66  })
 67  
 68  # Event handler for neighborhood selection and polygon click
 69  observeEvent(
 70    { # Monitor these values
 71      input$neighborhood_select
 72      input$tree_map_shape_click
 73    }, 
 74    { # Perform these actions
 75      hood <- input$tree_map_shape_click$id
 76      hoods <- input$neighborhood_select
 77      handleCharts(hoods, hood)
 78  })
 79
 80  
 81  # Establish base URL and base queries outside of reactive functions
 82  base_url <- "https://data.wprdc.org/api/action/datastore_search_sql?sql="
 83  base_query <- "SELECT id,scientific_name,diameter_base_height,height,stems,overhead_utilities,land_use,condition,overall_benefits_dollar_value,neighborhood,latitude,longitude FROM %221515a93c-73e3-4425-9b35-1cd11b2196da%22 WHERE 1=1 "
 84  
 85  # Create reactive function and consume WPRDC API, apply user filters
 86  tree_data <- reactive({
 87    
 88    # Evaluate tree neighborhood condition
 89    if (length(input$neighborhood_select > 0)) {
 90      and_neighborhood <- paste0("AND neighborhood IN ('", paste0(input$neighborhood_select, collapse="', '"), "') ")
 91    } 
 92    else {
 93      and_neighborhood <- "" # Set to empty string to default to all
 94    }
 95    
 96    # Evaluate tree condition filter
 97    if (length(input$condition_select > 0)) {
 98      and_condition <- paste0("AND condition IN ('", paste0(input$condition_select, collapse="', '"), "') ")
 99    }
100    else {
101      and_condition <- "" # Set to empty string to default to all
102    }
103    
104    # Evaluate scientific name filter
105    if (length(input$condition_select > 0)) {
106      and_condition <- paste0("AND condition IN ('", paste0(input$condition_select, collapse="', '"), "') ")
107    }
108    else {
109      and_condition <- "" # Set to empty string to default to all
110    }
111    
112    # Range inputs will always have values so no condition needed
113    and_diameter <- paste0("AND diameter_base_height %3E%3D", input$diameter_range[1], " AND diameter_base_height %3C%3D", input$diameter_range[2])
114    and_stems <- paste0("AND stems %3E%3D", input$stems_range[1], " AND stems %3C%3D", input$stems_range[2])
115    
116    if (overhead_btn_value() %% 2 == 1) {
117      and_ignore_ovhd_utils <- paste0("AND overhead_utilities != 'Yes';")
118    } else {
119      and_ignore_ovhd_utils <- ";"
120    }
121    
122    # Concatenate URL components into single URL
123    full_URL <- paste0(base_url, base_query, and_neighborhood, and_condition, and_diameter, and_stems, and_ignore_ovhd_utils)
124    
125    # Manually replace symbol characters because URLencode did not work
126    fixed_URL <- gsub(" ", "%20", full_URL)
127    fixed_URL <- gsub("'", "%27", fixed_URL)
128
129    return_tree_data <- grabData(fixed_URL)
130  })
131  
132  
133  # Reactively filter neighborhoods
134  neighborhood_polygons <- reactive({
135    
136    if (length(input$neighborhood_select) > 0) {
137      return_polygons <- pittsburgh_neighborhood_gj %>%
138        subset(., hood %in% input$neighborhood_select) # Using subset() because it works with spatial data and don't want to mess with sf:: package
139    }
140    else { # No neighborhoods selected - default to all
141      return_polygons <- pittsburgh_neighborhood_gj
142    }
143    
144    return(return_polygons)
145  })
146  
147  
148  ####################
149  # Generate Outputs #
150  ####################
151  
152  # Generate tree map with leaflet
153  output$tree_map <- renderLeaflet({
154    leaflet() %>%
155      addTiles() %>%
156      setView(lat = 40.45, lng = -79.95, zoom = 13) %>% 
157      addMarkers(data =  tree_data(),
158                 ~longitude, 
159                 ~latitude,
160                 clusterOptions = markerClusterOptions(),
161                 popup = ~as.character(paste0("<b>Tree ID: ", id, "</b>",
162                                              "<br/>Height: ", height, 
163                                              "<br/>Condition: ", condition, 
164                                              "<br/>Scientific Name: ", scientific_name, 
165                                              "<br/>Wikipedia: ", "<a href='https://en.wikipedia.org/wiki/", scientific_name, "' target='_blank'/>Wikipedia</a>"))
166      ) %>%
167      addPolygons(data = neighborhood_polygons(),
168                  fillOpacity = 0.3,
169                  layerId = ~hood,
170                  popup = ~paste0("<b>Neighborhood:</b> ", hood)
171      )
172  })
173  
174  # Generate charts
175
176  updateCharts <- function(hood) {
177
178    # Use renderUI() to return html to inject into the absolutePanel
179    output$neighborhood_tree_count <- renderUI({
180      
181      # Load reactive data and filter by neighborhood
182      trees <- tree_data() %>%
183        filter(neighborhood == hood)
184      
185      # Get count of trees
186      tree_counts <- trees %>%
187        summarize(
188          count = n()
189        )
190
191      # Return h4 element
192      h4(paste0(hood, " has ", tree_counts$count, " trees."))
193    })
194    
195    output$scientific_name_count <- renderUI({
196      
197      # Load reactive data and filter by neighborhood
198      trees <- tree_data() %>%
199        filter(neighborhood == hood)
200      
201      # Get most common tree
202      most_common_tree <- trees %>%
203        group_by(scientific_name) %>%
204        summarize(
205          count = n()
206        ) %>%
207        arrange(count) %>%
208        filter(row_number()==1)
209
210      # Return h5 element
211      h5(paste0("The most common tree is ", most_common_tree$scientific_name, "(", most_common_tree$count, ")."))
212    })
213    
214    # Generate plot to include in the absolutePanel
215    output$stems_plot <- renderPlot({
216      
217      stems_hist <- tree_data() %>%
218        filter(neighborhood == hood) %>%
219        ggplot(aes(x=stems, fill=stems)) + geom_histogram() +
220        labs(x="Stem Count", y="Number of Trees", title="Number of Trees by Stem Count")
221   
222      # Return plot
223      stems_hist
224    })
225    
226    # Generate plot to include in the absolutePanel
227    output$value_plot <- renderPlot({
228      
229      value_condition <- tree_data() %>%
230        filter(neighborhood == hood) %>%
231        ggplot(aes(x=condition, y=overall_benefits_dollar_value, fill=condition)) + geom_violin() +
232        labs(x = "Condition", y="Dollar Value", title="Dollar Value by Condition")
233      
234      # Return plot
235      value_condition
236    })
237  }
238  
239  
240  # Define custom function to handle showing/hiding of plots
241  handleCharts <- function(hoods, hood) {
242    
243    # Show plots if selected hood is in selected neighborhoods
244    if (hood %in% hoods) {
245      updateCharts(hood)
246      shinyjs::show("neighborhood_tree_count")
247      shinyjs::show("scientific_name_count")
248      shinyjs::show("stems_plot")
249      shinyjs::show("value_plot")
250    }
251    else {
252      shinyjs::hide("scientific_name_count")
253      shinyjs::hide("stems_plot")
254      shinyjs::hide("value_plot")
255
256      output$neighborhood_tree_count <- renderUI({
257        h4("Oops. Selected neighborhood has been removed. Please click a new neighborhood.")
258      })
259    }
260  }
261  
262
263  # Generate tree table with DT
264  output$tree_table <- DT::renderDataTable({
265    dat <- tree_data()
266  
267    DT::datatable(dat, options = list(pageLength = 15, scrollX = T)) %>%
268      formatStyle(
269        'condition',
270        target = 'row',
271        backgroundColor = styleEqual(c('Critical', 
272                                       'N/A', 
273                                       'Poor', 
274                                       'Very Good', 
275                                       'Excellent'), 
276                                     c('red', 
277                                       'yellow', 
278                                       'orange', 
279                                       'cyan', 
280                                       'green'))
281      )
282  })
283    
284  
285  # Download handler
286  output$download_tree_data <- downloadHandler(
287    filename = paste("pittsburgh-tree-data", Sys.Date(), ".csv", sep=""),
288    content = function(file) {
289      write.csv(tree_data(), file)
290    }
291  )
292  
293  # Hide the plots by default
294  shinyjs::hide("neighborhood_tree_count")
295  shinyjs::hide("scientific_name_count")
296  shinyjs::hide("stems_plot")
297  shinyjs::hide("value_plot")
298
299} # End Server
300
301# Run the application 
302shinyApp(ui = ui, server = server)

You can explore the Shiny app here on Shinyapps.io: benh.shinyapps.io/pittsburgh_trees_2/.