I have built a web app to make it easier to quickly filter plots in the SEOSAW network based on plot metadata and attributes of the plot. I built the app using Shiny , which offers a neat solution for creating simple HTML5 web apps in R.
I’ve pasted the code for the app below. The app is actually fairly simple. It has a sidebar with a bunch of sliders and dropdown checkbox options to filter a dataframe of plot metadata. The main panel has a map displaying the plot locations, with the points optionally shaded according to one of the fields of plot metadata. The map is built using leaflet , and pulls background tiles from mapbox . Below the map is a table showing the selected plots with their metadata values.
# Packages
library(shiny)
library(dplyr)
library(sf)
library(leaflet)
library(shinyWidgets)
library(DT)
library(scico)
# Import data
plots_clean_sf <- readRDS("plots_clean_sf.rds")
species <- readRDS("species.rds")
# Country names lookup
africa_lookup <- readRDS("africa_lookup.rds")
# Column names lookup
column_lookup <- readRDS("column_lookup.rds")
# Construct mapbox URL
mbox_base <- "https://api.mapbox.com/"
mbox_id <- "styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token="
mbox_token <- "redacted"
mapbox_url <- paste0(mbox_base, mbox_id, mbox_token)
# Define some functions for inputs to cut down on code replication
pickerInputFunc <- function(id, name, choices, rem_na = FALSE) {
out <- list(
pickerInput(id,
column_lookup[[name]]$html,
choices,
options = list(`actions-box` = TRUE, `live-search` = TRUE),
selected = choices, multiple = TRUE)
)
if (rem_na == TRUE) {
out[[2]] <- checkboxInput(paste0(id, "NA"),
label = paste("Include NA values?"), value = TRUE)
}
return(out)
}
sliderInputFunc <- function(id, name, x, rem_na = FALSE) {
lo <- floor(min(x, na.rm = TRUE))
hi <- ceiling(max(x, na.rm = TRUE))
out <- list(
numericRangeInput(id,
column_lookup[[name]]$html,
min = lo,
max = hi,
value = c(lo, hi)
)
)
if (rem_na == TRUE) {
out[[2]] <- checkboxInput(paste0(id, "NA"),
label = paste("Include NA values?"), value = TRUE)
}
return(out)
}
# UI
ui <- fluidPage(
tags$head(
tags$style(HTML(".leaflet-container { background: white; border-radius: 5px; border: 1px solid black; }"))
),
titlePanel(
tagList(span("SEOSAW plot data explorer",
span(actionButton('more_info', 'More information'),
style = "position: absolute; right: 2em;")
)
),
windowTitle = "SEOSAW plot data explorer"),
sidebarLayout(
sidebarPanel(
style = "overflow-y: auto; height: 90vh;",
selectInput("pointHiSel", "Shade points",
c("None", unname(unlist(lapply(column_lookup, "[[", "label")))),
selected = "None"),
pickerInput("speciesSel", "Species", unique(species$species),
options = list(`actions-box` = TRUE, `live-search` = TRUE),
selected = unique(species$species), multiple = TRUE),
pickerInputFunc("siteSel", "site", unique(plots_clean_sf$site)),
pickerInputFunc("country_iso3Sel", "country_iso3", africa_lookup),
pickerInputFunc("prinvSel", "prinv", unique(plots_clean_sf$prinv)),
pickerInputFunc("permanentSel", "permanent", unique(plots_clean_sf$permanent)),
pickerInputFunc("plot_shapeSel", "plot_shape", unique(plots_clean_sf$plot_shape)),
pickerInputFunc("teow_biomeSel", "teow_biome", unique(plots_clean_sf$teow_biome), rem_na = TRUE),
pickerInputFunc("whites_veg_minorSel", "whites_veg_minor", unique(plots_clean_sf$whites_veg_minor), rem_na = TRUE),
sliderInputFunc("plot_areaSel", "plot_area", plots_clean_sf$plot_area),
sliderInputFunc("longitudeSel", "longitude", plots_clean_sf$longitude),
sliderInputFunc("latitudeSel", "latitude", plots_clean_sf$latitude),
sliderInputFunc("elevationSel", "elevation", plots_clean_sf$elevation, rem_na = TRUE),
sliderInputFunc("min_diam_threshSel", "min_diam_thresh", plots_clean_sf$min_diam_thresh, rem_na = TRUE),
sliderInputFunc("ba_haSel", "ba_ha", plots_clean_sf$ba_ha),
sliderInputFunc("agb_haSel", "agb_ha", plots_clean_sf$agb_ha, rem_na = TRUE),
sliderInputFunc("n_stems_ge5Sel", "n_stems_ge5", plots_clean_sf$n_stems_ge5),
sliderInputFunc("richnessSel", "richness", plots_clean_sf$richness),
sliderInputFunc("n_censusSel", "n_census", plots_clean_sf$n_census),
sliderInputFunc("bio1Sel", "bio1", plots_clean_sf$bio1, rem_na = TRUE),
sliderInputFunc("bio12Sel", "bio12", plots_clean_sf$bio12, rem_na = TRUE),
sliderInputFunc("travel_time_citySel", "travel_time_city", plots_clean_sf$travel_time_city, rem_na = TRUE),
sliderInputFunc("forest_heightSel", "forest_height", plots_clean_sf$forest_height, rem_na = TRUE),
sliderInputFunc("soil_org_c_densitSel", "soil_org_c_densit", plots_clean_sf$soil_org_c_densit, rem_na = TRUE),
sliderInputFunc("soil_sandSel", "soil_sand", plots_clean_sf$soil_sand, rem_na = TRUE)
),
mainPanel(
leafletOutput("mapOutput"),
pickerInput("tableColSel", "Select columns",
choices = unname(unlist(lapply(column_lookup, "[[", "label"))),
selected = unlist(unname(lapply(column_lookup[c(
"plot_id", "country_iso3", "prinv", "permanent", "plot_area",
"plot_shape", "min_diam_thresh", "n_census", "agb_ha",
"ba_ha", "n_stems_ge5", "richness")], "[[", "label"))),
multiple = TRUE,
options = list(`actions-box` = TRUE, `live-search` = TRUE)),
DTOutput("tableOutput")
)
)
)
# Server
server <- function(input, output, session) {
plotsFil <- reactive({
plots_clean_sf %>%
filter(
plot_id %in% unique(species$plot_id[species$species %in% input$speciesSel]),
site %in% na_if(input$siteSel, "NA"),
country_iso3 %in% na_if(input$country_iso3Sel, "NA"),
prinv %in% na_if(input$prinvSel, "NA"),
permanent %in% na_if(input$permanentSel, "NA"),
plot_shape %in% na_if(input$plot_shapeSel, "NA"),
teow_biome %in% na_if(input$teow_biomeSel, "NA"),
whites_veg_minor %in% na_if(input$whites_veg_minorSel, "NA"),
between(plot_area, input$plot_areaSel[1],input$plot_areaSel[2]) | is.na(plot_area),
between(longitude, input$longitudeSel[1],input$longitudeSel[2]) | is.na(longitude),
between(latitude, input$latitudeSel[1],input$latitudeSel[2]) | is.na(latitude),
between(min_diam_thresh, input$min_diam_threshSel[1],input$min_diam_threshSel[2]) | is.na(min_diam_thresh),
between(ba_ha, input$ba_haSel[1], input$ba_haSel[2]) | is.na(ba_ha),
between(agb_ha, input$agb_haSel[1], input$agb_haSel[2]) | is.na(agb_ha),
between(n_stems_ge5, input$n_stems_ge5Sel[1], input$n_stems_ge5Sel[2]) | is.na(n_stems_ge5),
between(richness, input$richnessSel[1], input$richnessSel[2]) | is.na(richness),
between(n_census, input$n_censusSel[1], input$n_censusSel[2]) | is.na(n_census),
between(bio1, input$bio1Sel[1], input$bio1Sel[2]) | is.na(bio1),
between(bio12, input$bio12Sel[1], input$bio12Sel[2]) | is.na(bio12),
between(travel_time_city, input$travel_time_citySel[1], input$travel_time_citySel[2]) | is.na(travel_time_city),
between(elevation, input$elevationSel[1], input$elevationSel[2]) | is.na(elevation),
between(forest_height, input$forest_heightSel[1], input$forest_heightSel[2]) | is.na(forest_height),
between(soil_org_c_densit, input$soil_org_c_densitSel[1], input$soil_org_c_densitSel[2]) | is.na(soil_org_c_densit),
between(soil_sand, input$soil_sandSel[1], input$soil_sandSel[2]) | is.na(soil_sand)
) %>%
filter(if (!input$teow_biomeSelNA) !is.na(teow_biome) else TRUE) %>%
filter(if (!input$whites_veg_minorSelNA) !is.na(whites_veg_minor) else TRUE) %>%
filter(if (!input$min_diam_threshSelNA) !is.na(min_diam_thresh) else TRUE) %>%
filter(if (!input$bio1SelNA) !is.na(bio1) else TRUE) %>%
filter(if (!input$bio12SelNA) !is.na(bio12) else TRUE) %>%
filter(if (!input$travel_time_citySelNA) !is.na(travel_time_city) else TRUE) %>%
filter(if (!input$elevationSelNA) !is.na(elevation) else TRUE) %>%
filter(if (!input$forest_heightSelNA) !is.na(forest_height) else TRUE) %>%
filter(if (!input$soil_org_c_densitSelNA) !is.na(soil_org_c_densit) else TRUE) %>%
filter(if (!input$soil_sandSelNA) !is.na(soil_sand) else TRUE)
})
output$mapOutput <- renderLeaflet({
leaflet() %>%
addTiles(urlTemplate = mapbox_url,
options = tileOptions(
maxZoom = 18
)
) %>%
setView(lng = 30, lat = -15, zoom = 4)
})
toListen <- reactive({
list(
input$speciesSel,
input$tableColSel,
input$pointHiSel,
input$siteSel,
input$country_iso3Sel,
input$prinvSel,
input$plot_areaSel,
input$permanentSel,
input$plot_shapeSel,
input$teow_biomeSel,
input$teow_biomeSelNA,
input$whites_veg_minorSel,
input$whites_veg_minorSelNA,
input$longitudeSel,
input$latitudeSel,
input$elevationSel,
input$elevationSelNA,
input$min_diam_threshSel,
input$min_diam_threshSelNA,
input$ba_haSel,
input$agb_haSel,
input$n_stems_ge5Sel,
input$richnessSel,
input$n_censusSel,
input$bio1Sel,
input$bio1SelNA,
input$bio12Sel,
input$bio12SelNA,
input$travel_time_citySel,
input$travel_time_citySelNA,
input$forest_heightSel,
input$forest_heightSelNA,
input$soil_org_c_densitSel,
input$soil_org_c_densitSelNA,
input$soil_sandSel,
input$soil_sandSelNA
)
})
observeEvent(toListen(), {
leafletProxy("mapOutput") %>%
clearMarkers() %>%
clearControls()
if (nrow(plotsFil()) > 0) {
if (input$pointHiSel != "None") {
if (is.numeric(plotsFil()[[names(column_lookup)[
unname(unlist(lapply(column_lookup, "[[", "label"))) == input$pointHiSel]]])) {
pal <- colorNumeric(
palette = scico(n = 100, palette = "imola"),
domain = plotsFil()[[names(column_lookup)[
unname(unlist(lapply(column_lookup, "[[", "label"))) == input$pointHiSel]]],
na.color = "darkgrey"
)
} else {
pal <- colorFactor(
palette = scico(n = length(unique(plotsFil()[[names(column_lookup)[
unname(unlist(lapply(column_lookup, "[[", "label"))) == input$pointHiSel]]])),
palette = "imola"),
domain = plotsFil()[[names(column_lookup)[
unname(unlist(lapply(column_lookup, "[[", "label"))) == input$pointHiSel]]]
)
}
leafletProxy("mapOutput") %>%
addCircleMarkers(data = plotsFil(),
popup = ~label,
radius = 4, color = "black", opacity = 1, weight = 1,
fillOpacity = 1,
fillColor = ~pal(plotsFil()[[names(column_lookup)[
unname(unlist(lapply(column_lookup, "[[", "label"))) == input$pointHiSel]]])) %>%
addLegend(position = "bottomright", pal = pal,
values = plotsFil()[[names(column_lookup)[
unname(unlist(lapply(column_lookup, "[[", "label"))) == input$pointHiSel]]],
title = unname(unlist(lapply(column_lookup, "[[", "html")))[
unname(unlist(lapply(column_lookup, "[[", "label"))) == input$pointHiSel],
opacity = 1)
} else {
leafletProxy("mapOutput") %>%
addCircleMarkers(data = plotsFil(),
popup = ~label,
radius = 4, color = "black", opacity = 1, weight = 1,
fillOpacity = 1, fillColor = "tomato")
}
}
})
observeEvent(toListen(), {
plots_df <- plotsFil() %>%
st_drop_geometry() %>%
dplyr::select(names(column_lookup)[
unlist(lapply(column_lookup, "[[", "label")) %in% input$tableColSel])
names(plots_df) <- unlist(lapply(column_lookup, "[[", "label"))[
match(names(plots_df), names(column_lookup))]
output$tableOutput <- renderDT({
datatable(plots_df, rownames = FALSE,
options=list(autoWidth = TRUE, scrollX = TRUE)
)
})
})
observeEvent(input$more_info, {
showModal(modalDialog(
title = "",
HTML(paste0(
tags$p("This app is designed to provide quick filtering of the plot data in the SEOSAW network, based on various plot attributes and metadata."),
tags$p("For more information on SEOSAW, visit: ",
tags$a(href = "https://seosaw.github.io", "https://seosaw.github.io", target="_blank")
),
tags$p("Created by John L. Godlee (",
tags$a(href = "mailto:john.godlee@ed.ac.uk", "john.godlee@ed.ac.uk"),
")"))),
easyClose = TRUE,
footer = NULL
))
})
}
shinyApp(ui, server)