NEON Woody Vegetation Visualization

A simple project to download, organize, and interactively view NEON data. This was a fun project to get practice accesing data through APIs and building interactive apps.

  • Interactive App link
  • Code Repository link

My code for this project is shown in a reproducible example below.

Part 1. Download Data

Load Libraries

R version: 4.1.2

library(neonUtilities);  library(ggplot2)
 library(dplyr); library(shinyr); library(shinythemes)

Download Data using NEON API

library(neonUtilities);

# load my neon token as an object called NEON_TOKEN
source('./neon_token.R')

# instructions for getting a NEON API token can be found here: https://www.neonscience.org/resources/learning-hub/tutorials/neon-api-tokens-tutorial

veglist = loadByProduct(dpID="DP1.10098.001",
                        site = "all",
                        package = "basic",
                        check.size = F,
                        token = NEON_TOKEN)
saveRDS(veglist, './veglist.Rdata')

Part 2. Organize Data

Load datasets of interest and select columns of interest

veglist = readRDS('./veglist.Rdata')

veg_ind = veglist$vst_apparentindividual %>%
  dplyr::select(siteID, plotID, height, individualID, plantStatus,
                stemDiameter, date, eventID)

veg_loc = veglist$vst_mappingandtagging %>%
  dplyr::select(domainID, siteID, plotID, individualID, taxonID, scientificName) %>%
  unique()

plot_info = veglist$vst_perplotperyear %>%
  select(eventID, siteID, plotID, plotType) %>%
  unique()

rm(veglist) # unload the large file now that it's no longer needed

Join Datasets

veg = veg_ind %>%
  inner_join(veg_loc, by = c("individualID", "siteID", "plotID")) %>%
  inner_join(plot_info, by = c("siteID", "plotID", "eventID"))

veg %>% head()
##   siteID   plotID height            individualID           plantStatus
## 1   BART BART_044    5.6 NEON.PLA.D01.BART.05332 Live, disease damaged
## 2   BART BART_037   11.5 NEON.PLA.D01.BART.05273 Live, disease damaged
## 3   BART BART_037    1.8 NEON.PLA.D01.BART.05262 Live, disease damaged
## 4   BART BART_044    0.8 NEON.PLA.D01.BART.05424                  Live
## 5   BART BART_044   10.4 NEON.PLA.D01.BART.05415 Live, disease damaged
## 6   BART BART_044    1.1 NEON.PLA.D01.BART.05319                  Live
##   stemDiameter       date       eventID domainID taxonID
## 1          5.8 2015-08-26 vst_BART_2015      D01    FAGR
## 2         10.3 2015-08-26 vst_BART_2015      D01    FAGR
## 3          1.6 2015-08-26 vst_BART_2015      D01    FAGR
## 4          1.4 2015-08-26 vst_BART_2015      D01    TSCA
## 5         12.5 2015-08-26 vst_BART_2015      D01    FAGR
## 6          1.2 2015-08-26 vst_BART_2015      D01   PICEA
##                   scientificName plotType
## 1        Fagus grandifolia Ehrh.    tower
## 2        Fagus grandifolia Ehrh.    tower
## 3        Fagus grandifolia Ehrh.    tower
## 4 Tsuga canadensis (L.) Carrière    tower
## 5        Fagus grandifolia Ehrh.    tower
## 6                      Picea sp.    tower

Filter Data

veg = veg %>%
  # filter for Live trees
  filter(substr(plantStatus, 1,4) == "Live") %>%
  # filter for tree measurements (which start at 10cm)
  filter(stemDiameter > 10) %>%
  filter(is.na(height) == F & is.na(stemDiameter) == F) %>%
  # remove outliers
  group_by(taxonID) %>%
  mutate(height_lower =  boxplot(height, plot=FALSE)$stats[1],
         height_upper = boxplot(height, plot=FALSE)$stats[5],
         diam_lower =  boxplot(stemDiameter, plot=FALSE)$stats[1],
         diam_upper = boxplot(stemDiameter, plot=FALSE)$stats[5]) %>%
  filter(height > height_lower & height < height_upper,
         stemDiameter > diam_lower & stemDiameter < diam_upper) %>%
  dplyr::select(-height_lower, -height_upper, -diam_lower, -diam_upper) %>%
  ungroup()

# Select columns
veg = veg %>%
  dplyr::select(siteID, stemDiameter, height, plantStatus, scientificName, taxonID, date, individualID)

Get the 20 Most Commmon Species

# Find top 20 species by observation count
species = veg %>%
  group_by(taxonID, scientificName) %>%
  summarize(obs_count = n()) %>%
  arrange(desc(obs_count)) %>%
  head(20) %>%
  select(taxonID, scientificName) %>%
  ungroup() %>%
  mutate(num = row_number()) %>%
  as.data.frame()

Write Output Files for Shiny App

saveRDS(veg, './veg_data_for_shiny.Rdata')
saveRDS(species, './species.Rdata')

Part 3. The Shiny app

Server Function

server = function(input, output) {
  selection = reactive({
    as.numeric(input$optionNum)
  })
  selected_data <- reactive({
    df <- veg %>%
      filter(taxonID == species$taxonID[as.numeric(input$optionNum)]) %>%
      dplyr::select(individualID, siteID,scientificName, plantStatus, date, stemDiameter, height)

    if(!input$Live){
      df = df %>% filter(plantStatus != "Live")
    }
    if(!input$Live_DD){
      df = df %>% filter(plantStatus != "Live, disease damaged")
    }
    if(!input$Live_PD){
      df = df %>% filter(plantStatus != "Live, physically damaged")
    }
    if(!input$Live_BB){
      df = df %>% filter(plantStatus != "Live, broken bole")
    }
    if(!input$Live_ID){
      df = df %>% filter(plantStatus != "Live, insect damaged")
    }
    if(!input$Live_OD){
      df = df %>% filter(plantStatus != "Live,  other damage")
    }

    print(df)
    df
  })


  output$table <- DT::renderDataTable({
    table <- selected_data()
  })

  output$plot1 <- renderPlot({
    ggplot(data = selected_data(),
           aes(x = stemDiameter, y = height)) +
      theme_bw() +
      geom_point(aes(color = siteID), alpha = 0.4) +
      geom_smooth(color = "black",size = 0.5, se = F) +
      xlab("Stem Diameter [cm]") +
      ylab("Tree Height [m]")  +
      # ggtitle(paste("Selected Species:",
      #               species$scientificName[selection()])) +
      labs(colour = "NEON Site ID")+
      theme(legend.position = "right",
            axis.title = element_text(size = 16))
  })

  output$result <- renderText({
    paste("You have selected ", species$scientificName[selection()])
  })
}

User Interface Function

ui = fluidPage(
  theme = shinytheme("readable"),
  h3("NEON Woody Plant Vegetation Structure Data"),
  p("This app allows for quick visualization of tree height and diameter data for the 20 most common species in the NEON woody vegetation structure dataset. The data have been filtered to remove dead trees and obvious outliers for height and diameter. "),
  HTML("<p style='color:#808080'>NEON (National Ecological Observatory Network). Woody plant vegetation structure, RELEASE-2021 (DP1.10098.001). https://doi.org/10.48443/e3qn-xw47. Dataset accessed from https://data.neonscience.org on November 24, 2021 </p>"),
  p("\n"),
  fluidRow(column(width = 12, selectInput(inputId = "optionNum",
                                          label = "Choose a Species Group",
                                          choices = input_options,
                                          selected = 1))),
  fluidRow(column(width = 1, checkboxInput("Live", "Live", TRUE)),
           column(width = 2, checkboxInput("Live_DD", "Live, disease damaged", TRUE,)),
           column(width = 2, checkboxInput("Live_PD", "Live, physically damaged", TRUE)),
           column(width = 2, checkboxInput("Live_BB", "Live, broken bole", TRUE)),
           column(width = 2, checkboxInput("Live_ID", "Live, insect damaged", TRUE)),
           column(width = 2, checkboxInput("Live_OD", "Live,  other damage", TRUE))),

  mainPanel(tabsetPanel(type = "tabs",
                        tabPanel(title ="Plot",  plotOutput('plot1')),
                        tabPanel("Data Table",  DT::dataTableOutput('table')))
  )
)

App Setup

# load saved datasets
veg = readRDS('./veg_data_for_shiny.Rdata')
species = readRDS('./species.Rdata')

# format input options
input_options = species$num
names(input_options) = species$scientificName

Run the App

shinyApp(ui = ui, server = server)