diego-ellis-soto
commited on
Commit
Β·
ea5f4e5
1
Parent(s):
aedb415
Ready for huggingface!
Browse files- California_academy_logo.png +0 -0
- R/.DS_Store +0 -0
- app.R β R/old_poc/app_20250110.R +96 -67
- app_old.R β R/old_poc/app_old.R +0 -0
- R/old_poc/app_works_no_shinydashboard.R +1022 -0
- R/setup.R +97 -0
- README.md +5 -0
- Reimagining_San_Francisco.png +0 -0
- UC Berkeley_logo.png +0 -0
- app_shinydashboards.R +1008 -0
- rsconnect/shinyapps.io/diego-ellis-soto/RSF_Biodiversity_Access.dcf +2 -3
California_academy_logo.png
ADDED
![]() |
R/.DS_Store
CHANGED
Binary files a/R/.DS_Store and b/R/.DS_Store differ
|
|
app.R β R/old_poc/app_20250110.R
RENAMED
@@ -1,3 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# Get working directory, perhaps shiny apps is not receiving the data and the www?
|
2 |
# rsconnect::setAccountInfo(name='diego-ellis-soto', token='A47BE3C9E4B9EBCDFEC889AF31F64154', secret='g2Q2rxeYCiwlH81EkPXcCGsiHMgdyhTznJRmHtea')
|
3 |
# deployApp()
|
@@ -7,7 +12,7 @@
|
|
7 |
|
8 |
# Optimize some calculations? Shorten
|
9 |
|
10 |
-
|
11 |
|
12 |
|
13 |
|
@@ -17,7 +22,7 @@
|
|
17 |
# University of California Berkeley, ESPM
|
18 |
# California Academy of Sciences
|
19 |
###############################################################################
|
20 |
-
|
21 |
library(shiny)
|
22 |
library(leaflet)
|
23 |
library(mapboxapi)
|
@@ -31,57 +36,29 @@ library(data.table) # for fread
|
|
31 |
library(mapview) # for mapview objects
|
32 |
library(sjPlot) # for plotting lm model coefficients
|
33 |
library(sjlabelled) # optional if needed for sjPlot
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
#
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
#
|
44 |
-
|
45 |
-
getwd()
|
46 |
-
osm_greenspace <- st_read("data/greenspaces_osm_nad83.shp", quiet = TRUE) %>%
|
47 |
-
st_transform(4326)
|
48 |
-
if (!"name" %in% names(osm_greenspace)) {
|
49 |
-
osm_greenspace$name <- "Unnamed Greenspace"
|
50 |
-
}
|
51 |
-
|
52 |
-
# -- NDVI Raster
|
53 |
-
ndvi <- rast("data/SF_EastBay_NDVI_Sentinel_10.tif")
|
54 |
-
|
55 |
-
# -- GBIF data
|
56 |
-
# Load what is basically inter_gbif !!!!!
|
57 |
-
# load("data/sf_gbif.Rdata") # => sf_gbif
|
58 |
-
load('data/gbif_census_ndvi_anno.Rdata')
|
59 |
-
vect_gbif <- vect(sf_gbif)
|
60 |
-
# -- Precomputed CBG data
|
61 |
-
load('data/cbg_vect_sf.Rdata')
|
62 |
-
if (!"unique_species" %in% names(cbg_vect_sf)) {
|
63 |
-
cbg_vect_sf$unique_species <- cbg_vect_sf$n_species
|
64 |
-
}
|
65 |
-
if (!"n_observations" %in% names(cbg_vect_sf)) {
|
66 |
-
cbg_vect_sf$n_observations <- cbg_vect_sf$n
|
67 |
-
}
|
68 |
-
if (!"median_inc" %in% names(cbg_vect_sf)) {
|
69 |
-
cbg_vect_sf$median_inc <- cbg_vect_sf$medincE
|
70 |
-
}
|
71 |
-
if (!"ndvi_mean" %in% names(cbg_vect_sf)) {
|
72 |
-
cbg_vect_sf$ndvi_mean <- cbg_vect_sf$ndvi_sentinel
|
73 |
-
}
|
74 |
-
|
75 |
-
# -- Hotspots/Coldspots
|
76 |
-
biodiv_hotspots <- st_read("data/hotspots.shp", quiet = TRUE) %>% st_transform(4326)
|
77 |
-
biodiv_coldspots <- st_read("data/coldspots.shp", quiet = TRUE) %>% st_transform(4326)
|
78 |
|
79 |
# ------------------------------------------------
|
80 |
# 3) UI
|
81 |
# ------------------------------------------------
|
82 |
ui <- fluidPage(
|
83 |
-
|
84 |
|
|
|
|
|
|
|
|
|
|
|
85 |
fluidRow(
|
86 |
column(
|
87 |
width = 12, align = "center",
|
@@ -91,7 +68,8 @@ ui <- fluidPage(
|
|
91 |
height = "120px", style = "margin:10px;"),
|
92 |
tags$img(src = "Reimagining_San_Francisco.png",
|
93 |
height = "120px", style = "margin:10px;")
|
94 |
-
)
|
|
|
95 |
),
|
96 |
|
97 |
fluidRow(
|
@@ -126,7 +104,9 @@ ui <- fluidPage(
|
|
126 |
)
|
127 |
),
|
128 |
br(),
|
129 |
-
|
|
|
|
|
130 |
tabsetPanel(
|
131 |
|
132 |
# 1) Isochrone Explorer
|
@@ -180,32 +160,40 @@ ui <- fluidPage(
|
|
180 |
column(12,
|
181 |
br(),
|
182 |
uiOutput("bioScoreBox"),
|
|
|
183 |
uiOutput("closestGreenspaceUI")
|
184 |
)
|
185 |
),
|
186 |
|
187 |
br(),
|
188 |
-
DTOutput("dataTable"),
|
189 |
|
|
|
190 |
br(),
|
191 |
fluidRow(
|
192 |
column(12,
|
193 |
-
plotOutput("bioSocPlot", height = "400px")
|
194 |
)
|
195 |
),
|
196 |
|
|
|
|
|
197 |
br(),
|
198 |
fluidRow(
|
199 |
column(12,
|
200 |
-
plotOutput("collectionPlot", height = "
|
201 |
)
|
202 |
)
|
203 |
)
|
204 |
)
|
205 |
),
|
206 |
|
|
|
|
|
207 |
#br.?
|
208 |
-
|
|
|
|
|
209 |
"GBIF Summaries",
|
210 |
sidebarLayout(
|
211 |
sidebarPanel(
|
@@ -226,11 +214,16 @@ ui <- fluidPage(
|
|
226 |
DTOutput("classTable"),
|
227 |
br(),
|
228 |
h3("Observations vs. Species Richness"),
|
229 |
-
plotOutput("obsVsSpeciesPlot", height = "
|
230 |
p("This plot displays the relationship between the number of observations and the species richness. Use this visualization to understand data coverage and biodiversity trends.")
|
231 |
)
|
232 |
)
|
233 |
-
),
|
|
|
|
|
|
|
|
|
|
|
234 |
fluidRow(
|
235 |
column(
|
236 |
width = 12,
|
@@ -317,7 +310,7 @@ ui <- fluidPage(
|
|
317 |
# )
|
318 |
# )
|
319 |
# )
|
320 |
-
|
321 |
|
322 |
# ------------------------------------------------
|
323 |
# 4) Server
|
@@ -955,7 +948,7 @@ server <- function(input, output, session) {
|
|
955 |
geom_point(aes(y = EstimatedPopulation / 1000), color = "red", size = 3) +
|
956 |
labs(
|
957 |
x = "Isochrone (Mode-Time)",
|
958 |
-
y = "
|
959 |
title = "Biodiversity & Socioeconomic Summary"
|
960 |
) +
|
961 |
theme_minimal(base_size = 14) +
|
@@ -997,12 +990,13 @@ server <- function(input, output, session) {
|
|
997 |
st_drop_geometry() %>%
|
998 |
group_by(institutionCode) %>%
|
999 |
summarize(count = n(), .groups = "drop") %>%
|
1000 |
-
arrange(desc(count))
|
|
|
1001 |
|
1002 |
-
ggplot(df_code, aes(x = reorder(
|
1003 |
geom_bar(stat = "identity", fill = "darkorange", alpha = 0.7) +
|
1004 |
labs(
|
1005 |
-
x = "Institution Code",
|
1006 |
y = "Number of Records",
|
1007 |
title = "GBIF Records by Institution Code (Isochrone Union)"
|
1008 |
) +
|
@@ -1028,21 +1022,55 @@ server <- function(input, output, session) {
|
|
1028 |
map_s@map
|
1029 |
})
|
1030 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1031 |
# ------------------------------------------------
|
1032 |
# Additional Plot: n_observations vs n_species
|
1033 |
# ------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1034 |
output$obsVsSpeciesPlot <- renderPlot({
|
1035 |
-
|
1036 |
-
ggplot(
|
1037 |
geom_point(color = "blue", alpha = 0.6) +
|
1038 |
labs(
|
1039 |
-
x = "Number of Observations
|
1040 |
-
y = "
|
1041 |
-
title = "Data Availability vs. Species Richness"
|
1042 |
) +
|
1043 |
theme_minimal(base_size = 14)
|
1044 |
})
|
1045 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1046 |
# ------------------------------------------------
|
1047 |
# Additional Plot: Linear model of n_species ~ n_observations + median_inc + ndvi_mean
|
1048 |
# ------------------------------------------------
|
@@ -1073,9 +1101,10 @@ server <- function(input, output, session) {
|
|
1073 |
}
|
1074 |
|
1075 |
shinyApp(ui, server)
|
1076 |
-
|
1077 |
# library(profvis)
|
1078 |
#
|
1079 |
# profvis({
|
1080 |
# shinyApp(ui, server)
|
1081 |
-
# })
|
|
|
|
1 |
+
# truncate the name
|
2 |
+
# Geocoder shiny all -> Adapt !!!
|
3 |
+
|
4 |
+
|
5 |
+
|
6 |
# Get working directory, perhaps shiny apps is not receiving the data and the www?
|
7 |
# rsconnect::setAccountInfo(name='diego-ellis-soto', token='A47BE3C9E4B9EBCDFEC889AF31F64154', secret='g2Q2rxeYCiwlH81EkPXcCGsiHMgdyhTznJRmHtea')
|
8 |
# deployApp()
|
|
|
12 |
|
13 |
# Optimize some calculations? Shorten
|
14 |
|
15 |
+
# Look at code human facets or relate social vulnerabiltiy income
|
16 |
|
17 |
|
18 |
|
|
|
22 |
# University of California Berkeley, ESPM
|
23 |
# California Academy of Sciences
|
24 |
###############################################################################
|
25 |
+
require(shinyjs)
|
26 |
library(shiny)
|
27 |
library(leaflet)
|
28 |
library(mapboxapi)
|
|
|
36 |
library(mapview) # for mapview objects
|
37 |
library(sjPlot) # for plotting lm model coefficients
|
38 |
library(sjlabelled) # optional if needed for sjPlot
|
39 |
+
require(bslib)
|
40 |
+
require(shinycssloaders)
|
41 |
+
source('R/setup.R')
|
42 |
+
# Global theme definition
|
43 |
+
theme <- bs_theme(
|
44 |
+
bootswatch = "flatly",
|
45 |
+
base_font = font_google("Roboto"),
|
46 |
+
heading_font = font_google("Roboto Slab"),
|
47 |
+
bg = "#f8f9fa",
|
48 |
+
fg = "#212529"
|
49 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
|
51 |
# ------------------------------------------------
|
52 |
# 3) UI
|
53 |
# ------------------------------------------------
|
54 |
ui <- fluidPage(
|
55 |
+
theme = theme, # Introduce a theme from bslib
|
56 |
|
57 |
+
# For dynamically show and hide a 'Calculating' message
|
58 |
+
useShinyjs(), # Initialize shinyjs
|
59 |
+
div(id = "loading", style = "display:none; font-size: 20px; color: red;", "Calculating..."),
|
60 |
+
titlePanel("San Francisco Biodiversity Access Decision Support Tool"),
|
61 |
+
p('Explore your local biodiversity and your access to it!'),
|
62 |
fluidRow(
|
63 |
column(
|
64 |
width = 12, align = "center",
|
|
|
68 |
height = "120px", style = "margin:10px;"),
|
69 |
tags$img(src = "Reimagining_San_Francisco.png",
|
70 |
height = "120px", style = "margin:10px;")
|
71 |
+
),
|
72 |
+
theme=bs_theme(bootswatch='yeti')
|
73 |
),
|
74 |
|
75 |
fluidRow(
|
|
|
104 |
)
|
105 |
),
|
106 |
br(),
|
107 |
+
# fluidRow(
|
108 |
+
# column(
|
109 |
+
# width = 6 , # quitar
|
110 |
tabsetPanel(
|
111 |
|
112 |
# 1) Isochrone Explorer
|
|
|
160 |
column(12,
|
161 |
br(),
|
162 |
uiOutput("bioScoreBox"),
|
163 |
+
br(),
|
164 |
uiOutput("closestGreenspaceUI")
|
165 |
)
|
166 |
),
|
167 |
|
168 |
br(),
|
169 |
+
DTOutput("dataTable") %>% withSpinner(type = 8, color = "#337ab7"),
|
170 |
|
171 |
+
br(),
|
172 |
br(),
|
173 |
fluidRow(
|
174 |
column(12,
|
175 |
+
plotOutput("bioSocPlot", height = "400px") %>% withSpinner(type = 8, color = "#337ab7")
|
176 |
)
|
177 |
),
|
178 |
|
179 |
+
br(),
|
180 |
+
br(),
|
181 |
br(),
|
182 |
fluidRow(
|
183 |
column(12,
|
184 |
+
plotOutput("collectionPlot", height = "400px") %>% withSpinner(type = 8, color = "#f39c12")
|
185 |
)
|
186 |
)
|
187 |
)
|
188 |
)
|
189 |
),
|
190 |
|
191 |
+
|
192 |
+
# ), # end of column wifth
|
193 |
#br.?
|
194 |
+
# column(
|
195 |
+
# width=6,
|
196 |
+
tabPanel(
|
197 |
"GBIF Summaries",
|
198 |
sidebarLayout(
|
199 |
sidebarPanel(
|
|
|
214 |
DTOutput("classTable"),
|
215 |
br(),
|
216 |
h3("Observations vs. Species Richness"),
|
217 |
+
plotOutput("obsVsSpeciesPlot", height = "300px"),
|
218 |
p("This plot displays the relationship between the number of observations and the species richness. Use this visualization to understand data coverage and biodiversity trends.")
|
219 |
)
|
220 |
)
|
221 |
+
) %>% withSpinner(type = 8, color = "#337ab7")
|
222 |
+
),
|
223 |
+
# )
|
224 |
+
|
225 |
+
# ),
|
226 |
+
|
227 |
fluidRow(
|
228 |
column(
|
229 |
width = 12,
|
|
|
310 |
# )
|
311 |
# )
|
312 |
# )
|
313 |
+
|
314 |
|
315 |
# ------------------------------------------------
|
316 |
# 4) Server
|
|
|
948 |
geom_point(aes(y = EstimatedPopulation / 1000), color = "red", size = 3) +
|
949 |
labs(
|
950 |
x = "Isochrone (Mode-Time)",
|
951 |
+
y = "Unique Species (Blue) \n | Population (Red) (thousands)",
|
952 |
title = "Biodiversity & Socioeconomic Summary"
|
953 |
) +
|
954 |
theme_minimal(base_size = 14) +
|
|
|
990 |
st_drop_geometry() %>%
|
991 |
group_by(institutionCode) %>%
|
992 |
summarize(count = n(), .groups = "drop") %>%
|
993 |
+
arrange(desc(count)) %>%
|
994 |
+
mutate(truncatedCode = substr(institutionCode, 1, 5)) # Shorter version of the names
|
995 |
|
996 |
+
ggplot(df_code, aes(x = reorder(truncatedCode, -count), y = count)) + # replaced institutionCode with trunacedCode
|
997 |
geom_bar(stat = "identity", fill = "darkorange", alpha = 0.7) +
|
998 |
labs(
|
999 |
+
x = "Institution Code (Truncoded)",
|
1000 |
y = "Number of Records",
|
1001 |
title = "GBIF Records by Institution Code (Isochrone Union)"
|
1002 |
) +
|
|
|
1022 |
map_s@map
|
1023 |
})
|
1024 |
|
1025 |
+
|
1026 |
+
|
1027 |
+
|
1028 |
+
|
1029 |
+
|
1030 |
+
|
1031 |
+
|
1032 |
+
|
1033 |
# ------------------------------------------------
|
1034 |
# Additional Plot: n_observations vs n_species
|
1035 |
# ------------------------------------------------
|
1036 |
+
|
1037 |
+
# Make it reactive: obsVsSpeciesPlot updates dynamically based on user-selected class_filter or family_filter.
|
1038 |
+
|
1039 |
+
filtered_data <- reactive({
|
1040 |
+
data <- cbg_vect_sf
|
1041 |
+
if (input$class_filter != "All") {
|
1042 |
+
data <- data[data$class == input$class_filter, ]
|
1043 |
+
}
|
1044 |
+
if (input$family_filter != "All") {
|
1045 |
+
data <- data[data$family == input$family_filter, ]
|
1046 |
+
}
|
1047 |
+
data
|
1048 |
+
})
|
1049 |
+
|
1050 |
output$obsVsSpeciesPlot <- renderPlot({
|
1051 |
+
data <- filtered_data()
|
1052 |
+
ggplot(data, aes(x = log(n_observations + 1), y = log(unique_species + 1))) +
|
1053 |
geom_point(color = "blue", alpha = 0.6) +
|
1054 |
labs(
|
1055 |
+
x = "Log(Number of Observations)",
|
1056 |
+
y = "Log(Species Richness)",
|
1057 |
+
title = "Filtered Data Availability vs. Species Richness"
|
1058 |
) +
|
1059 |
theme_minimal(base_size = 14)
|
1060 |
})
|
1061 |
|
1062 |
+
# output$obsVsSpeciesPlot <- renderPlot({
|
1063 |
+
# # A simple scatter plot of n_observations vs. n_species from cbg_vect_sf
|
1064 |
+
# ggplot(cbg_vect_sf, aes(x = log(n_observations+1), y = log(unique_species+1)) ) +
|
1065 |
+
# geom_point(color = "blue", alpha = 0.6) +
|
1066 |
+
# labs(
|
1067 |
+
# x = "Number of Observations (n_observations)",
|
1068 |
+
# y = "Number of Species (n_species)",
|
1069 |
+
# title = "Data Availability vs. Species Richness"
|
1070 |
+
# ) +
|
1071 |
+
# theme_minimal(base_size = 14)
|
1072 |
+
# })
|
1073 |
+
|
1074 |
# ------------------------------------------------
|
1075 |
# Additional Plot: Linear model of n_species ~ n_observations + median_inc + ndvi_mean
|
1076 |
# ------------------------------------------------
|
|
|
1101 |
}
|
1102 |
|
1103 |
shinyApp(ui, server)
|
1104 |
+
# run_with_themer(shinyApp(ui, server))
|
1105 |
# library(profvis)
|
1106 |
#
|
1107 |
# profvis({
|
1108 |
# shinyApp(ui, server)
|
1109 |
+
# })
|
1110 |
+
|
app_old.R β R/old_poc/app_old.R
RENAMED
File without changes
|
R/old_poc/app_works_no_shinydashboard.R
ADDED
@@ -0,0 +1,1022 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
###############################################################################
|
2 |
+
# Shiny App: San Francisco Biodiversity Access Decision Support Tool
|
3 |
+
# Author: Diego Ellis Soto, et al.
|
4 |
+
# University of California Berkeley, ESPM
|
5 |
+
# California Academy of Sciences
|
6 |
+
###############################################################################
|
7 |
+
require(shinyjs)
|
8 |
+
library(shiny)
|
9 |
+
library(leaflet)
|
10 |
+
library(mapboxapi)
|
11 |
+
library(tidyverse)
|
12 |
+
library(tidycensus)
|
13 |
+
library(sf)
|
14 |
+
library(DT)
|
15 |
+
library(RColorBrewer)
|
16 |
+
library(terra)
|
17 |
+
library(data.table) # for fread
|
18 |
+
library(mapview) # for mapview objects
|
19 |
+
library(sjPlot) # for plotting lm model coefficients
|
20 |
+
library(sjlabelled) # optional if needed for sjPlot
|
21 |
+
require(bslib)
|
22 |
+
require(shinycssloaders)
|
23 |
+
|
24 |
+
source('R/setup.R') # Ensure this script loads necessary data objects
|
25 |
+
|
26 |
+
# Define your Mapbox token securely
|
27 |
+
mapbox_token <- "pk.eyJ1Ijoia3dhbGtlcnRjdSIsImEiOiJjbHc3NmI0cDMxYzhyMmt0OXBiYnltMjVtIn0.Thtu6WqIhOfin6AykskM2g"
|
28 |
+
|
29 |
+
# Global theme definition
|
30 |
+
theme <- bs_theme(
|
31 |
+
bootswatch = "flatly",
|
32 |
+
base_font = font_google("Roboto"),
|
33 |
+
heading_font = font_google("Roboto Slab"),
|
34 |
+
bg = "#f8f9fa",
|
35 |
+
fg = "#212529"
|
36 |
+
)
|
37 |
+
|
38 |
+
# ------------------------------------------------
|
39 |
+
# 3) UI
|
40 |
+
# ------------------------------------------------
|
41 |
+
ui <- fluidPage(
|
42 |
+
theme = theme, # Introduce a theme from bslib
|
43 |
+
|
44 |
+
# For dynamically show and hide a 'Calculating' message
|
45 |
+
useShinyjs(), # Initialize shinyjs
|
46 |
+
div(id = "loading", style = "display:none; font-size: 20px; color: red;", "Calculating..."),
|
47 |
+
|
48 |
+
titlePanel("San Francisco Biodiversity Access Decision Support Tool"),
|
49 |
+
p('Explore your local biodiversity and your access to it!'),
|
50 |
+
|
51 |
+
fluidRow(
|
52 |
+
column(
|
53 |
+
width = 12, align = "center",
|
54 |
+
tags$img(src = "www/UC Berkeley_logo.png",
|
55 |
+
height = "120px", style = "margin:10px;"),
|
56 |
+
tags$img(src = "www/California_academy_logo.png",
|
57 |
+
height = "120px", style = "margin:10px;"),
|
58 |
+
tags$img(src = "www/Reimagining_San_Francisco.png",
|
59 |
+
height = "120px", style = "margin:10px;")
|
60 |
+
),
|
61 |
+
theme=bs_theme(bootswatch='yeti')
|
62 |
+
),
|
63 |
+
|
64 |
+
fluidRow(
|
65 |
+
column(
|
66 |
+
width = 12,
|
67 |
+
br(),
|
68 |
+
tags$b("App Summary (Fill out with RSF data working group):"),
|
69 |
+
p("
|
70 |
+
This application allows users to either click on a map or geocode an address
|
71 |
+
to generate travel-time isochrones across multiple transportation modes
|
72 |
+
(e.g., pedestrian, cycling, driving, driving during traffic).
|
73 |
+
It retrieves socio-economic data from precomputed Census variables, calculates NDVI,
|
74 |
+
and summarizes biodiversity records from GBIF. Users can explore information
|
75 |
+
related to biodiversity in urban environments, including greenspace coverage,
|
76 |
+
population estimates, and species diversity within each isochrone.
|
77 |
+
"),
|
78 |
+
|
79 |
+
tags$b("Created by:"),
|
80 |
+
p(strong("Diego Ellis Soto", "Carl Boettiger, Rebecca Johnson, Christopher J. Schell")),
|
81 |
+
|
82 |
+
p("Contact Information: ", strong("[email protected]"))
|
83 |
+
)
|
84 |
+
),
|
85 |
+
|
86 |
+
br(),
|
87 |
+
|
88 |
+
# Tabbed Interface
|
89 |
+
tabsetPanel(
|
90 |
+
# 1) Isochrone Explorer Tab
|
91 |
+
tabPanel("Isochrone Explorer",
|
92 |
+
sidebarLayout(
|
93 |
+
sidebarPanel(
|
94 |
+
radioButtons(
|
95 |
+
"location_choice",
|
96 |
+
"Select how to choose your location:",
|
97 |
+
choices = c("Address (Geocode)" = "address",
|
98 |
+
"Click on Map" = "map_click"),
|
99 |
+
selected = "map_click"
|
100 |
+
),
|
101 |
+
|
102 |
+
conditionalPanel(
|
103 |
+
condition = "input.location_choice == 'address'",
|
104 |
+
mapboxGeocoderInput(
|
105 |
+
inputId = "geocoder",
|
106 |
+
placeholder = "Search for an address",
|
107 |
+
access_token = mapbox_token
|
108 |
+
)
|
109 |
+
),
|
110 |
+
|
111 |
+
checkboxGroupInput(
|
112 |
+
"transport_modes",
|
113 |
+
"Select Transportation Modes:",
|
114 |
+
choices = list("Driving" = "driving",
|
115 |
+
"Walking" = "walking",
|
116 |
+
"Cycling" = "cycling",
|
117 |
+
"Driving with Traffic"= "driving-traffic"),
|
118 |
+
selected = c("driving", "walking")
|
119 |
+
),
|
120 |
+
|
121 |
+
checkboxGroupInput(
|
122 |
+
"iso_times",
|
123 |
+
"Select Isochrone Times (minutes):",
|
124 |
+
choices = list("5" = 5, "10" = 10, "15" = 15),
|
125 |
+
selected = c(5, 10)
|
126 |
+
),
|
127 |
+
|
128 |
+
actionButton("generate_iso", "Generate Isochrones"),
|
129 |
+
actionButton("clear_map", "Clear")
|
130 |
+
),
|
131 |
+
|
132 |
+
mainPanel(
|
133 |
+
leafletOutput("isoMap", height = 600),
|
134 |
+
|
135 |
+
fluidRow(
|
136 |
+
column(12,
|
137 |
+
br(),
|
138 |
+
uiOutput("bioScoreBox"),
|
139 |
+
br(),
|
140 |
+
uiOutput("closestGreenspaceUI")
|
141 |
+
)
|
142 |
+
),
|
143 |
+
|
144 |
+
br(),
|
145 |
+
DTOutput("dataTable") %>% withSpinner(type = 8, color = "#337ab7"),
|
146 |
+
|
147 |
+
br(),
|
148 |
+
br(),
|
149 |
+
fluidRow(
|
150 |
+
column(12,
|
151 |
+
plotOutput("bioSocPlot", height = "400px") %>% withSpinner(type = 8, color = "#337ab7")
|
152 |
+
)
|
153 |
+
),
|
154 |
+
|
155 |
+
br(),
|
156 |
+
br(),
|
157 |
+
br(),
|
158 |
+
fluidRow(
|
159 |
+
column(12,
|
160 |
+
plotOutput("collectionPlot", height = "400px") %>% withSpinner(type = 8, color = "#f39c12")
|
161 |
+
)
|
162 |
+
)
|
163 |
+
)
|
164 |
+
)
|
165 |
+
),
|
166 |
+
|
167 |
+
# 2) GBIF Summaries Tab
|
168 |
+
tabPanel(
|
169 |
+
"GBIF Summaries",
|
170 |
+
sidebarLayout(
|
171 |
+
sidebarPanel(
|
172 |
+
selectInput(
|
173 |
+
"class_filter",
|
174 |
+
"Select a GBIF Class to Summarize:",
|
175 |
+
choices = c("All", sort(unique(sf_gbif$class))),
|
176 |
+
selected = "All"
|
177 |
+
),
|
178 |
+
selectInput(
|
179 |
+
"family_filter",
|
180 |
+
"Filter by Family (optional):",
|
181 |
+
choices = c("All", sort(unique(sf_gbif$family))),
|
182 |
+
selected = "All"
|
183 |
+
)
|
184 |
+
),
|
185 |
+
mainPanel(
|
186 |
+
DTOutput("classTable"),
|
187 |
+
br(),
|
188 |
+
h3("Observations vs. Species Richness"),
|
189 |
+
plotOutput("obsVsSpeciesPlot", height = "300px"),
|
190 |
+
p("This plot displays the relationship between the number of observations and the species richness. Use this visualization to understand data coverage and biodiversity trends.")
|
191 |
+
)
|
192 |
+
)
|
193 |
+
) %>% withSpinner(type = 8, color = "#337ab7")
|
194 |
+
),
|
195 |
+
|
196 |
+
# Additional Information and Next Steps
|
197 |
+
fluidRow(
|
198 |
+
column(
|
199 |
+
width = 12,
|
200 |
+
tags$b("Reimagining San Francisco (Fill out with CAS):"),
|
201 |
+
p("Reimagining San Francisco is an initiative aimed at integrating ecological, social,
|
202 |
+
and technological dimensions to shape a sustainable future for the Bay Area.
|
203 |
+
This collaboration unites diverse stakeholders to explore innovations in urban planning,
|
204 |
+
conservation, and community engagement. The Reimagining San Francisco Data Working Group has been tasked with identifying and integrating multiple sources of socio-ecological biodiversity information in a co-development framework."),
|
205 |
+
|
206 |
+
tags$b("Why Biodiversity Access Matters (Polish this):"),
|
207 |
+
p("Ensuring equitable access to biodiversity is essential for human well-being,
|
208 |
+
ecological resilience, and global policy decisions related to conservation.
|
209 |
+
Areas with higher biodiversity can support ecosystem services including pollinators, moderate climate extremes,
|
210 |
+
and provide cultural, recreational, and health benefits to local communities.
|
211 |
+
Recognizing that cities are particularly complex socio-ecological systems facing both legacies of sociocultural practices as well as current ongoing dynamic human activities and pressures.
|
212 |
+
Incorporating multiple facets of biodiversity metrics alongside variables employed by city planners, human geographers, and decision-makers into urban planning will allow a more integrative lens in creating a sustainable future for cities and their residents."),
|
213 |
+
|
214 |
+
tags$b("How We Calculate Biodiversity Access Percentile:"),
|
215 |
+
p("Total unique species found within the user-generated isochrone.
|
216 |
+
We then compare that value to the distribution of unique species counts across all census block groups,
|
217 |
+
converting that comparison into a percentile ranking (Polish this, look at the 15 Minute city).
|
218 |
+
A higher percentile indicates greater biodiversity within the chosen area,
|
219 |
+
relative to other parts of the city or region.")
|
220 |
+
),
|
221 |
+
|
222 |
+
tags$b("Next Steps:"),
|
223 |
+
tags$ul(
|
224 |
+
tags$li("Add impervious surface"),
|
225 |
+
tags$li("National walkability score"),
|
226 |
+
tags$li("Social vulnerability score"),
|
227 |
+
tags$li("NatureServe biodiversity maps"),
|
228 |
+
tags$li("Calculate cold-hotspots within aggregation of H6 bins instead of by census block group: Ask Carl"),
|
229 |
+
tags$li("Species range maps"),
|
230 |
+
tags$li("Add common name GBIF"),
|
231 |
+
tags$li("Partner orgs"),
|
232 |
+
tags$li("Optimize speed -> store variables -> H-ify the world?"),
|
233 |
+
tags$li("Brainstorm and co-develop the biodiversity access score"),
|
234 |
+
tags$li("For the GBIF summaries, add an annotated GBIF_sf with environmental variables so we can see landcover type association across the biodiversity within the isochrone.")
|
235 |
+
)
|
236 |
+
)
|
237 |
+
)
|
238 |
+
|
239 |
+
# ------------------------------------------------
|
240 |
+
# 4) Server
|
241 |
+
# ------------------------------------------------
|
242 |
+
server <- function(input, output, session) {
|
243 |
+
|
244 |
+
chosen_point <- reactiveVal(NULL)
|
245 |
+
|
246 |
+
# ------------------------------------------------
|
247 |
+
# Leaflet Base + Hide Overlays
|
248 |
+
# ------------------------------------------------
|
249 |
+
output$isoMap <- renderLeaflet({
|
250 |
+
pal_cbg <- colorNumeric("YlOrRd", cbg_vect_sf$medincE)
|
251 |
+
|
252 |
+
pal_rich <- colorNumeric("YlOrRd", domain = cbg_vect_sf$unique_species)
|
253 |
+
# 2) Color palette for data availability
|
254 |
+
pal_data <- colorNumeric("Blues", domain = cbg_vect_sf$n_observations)
|
255 |
+
|
256 |
+
leaflet() %>%
|
257 |
+
addTiles(group = "Street Map (Default)") %>%
|
258 |
+
addProviderTiles(providers$Esri.WorldImagery, group = "Satellite (ESRI)") %>%
|
259 |
+
addProviderTiles(providers$CartoDB.Positron, group = "CartoDB.Positron") %>%
|
260 |
+
|
261 |
+
addPolygons(
|
262 |
+
data = cbg_vect_sf,
|
263 |
+
group = "Income",
|
264 |
+
fillColor = ~pal_cbg(medincE),
|
265 |
+
fillOpacity = 0.6,
|
266 |
+
color = "white",
|
267 |
+
weight = 1,
|
268 |
+
label=~medincE,
|
269 |
+
highlightOptions = highlightOptions(
|
270 |
+
weight = 5,
|
271 |
+
color = "blue",
|
272 |
+
fillOpacity = 0.5,
|
273 |
+
bringToFront = TRUE
|
274 |
+
),
|
275 |
+
labelOptions = labelOptions(
|
276 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
277 |
+
textsize = "12px",
|
278 |
+
direction = "auto"
|
279 |
+
)
|
280 |
+
) %>%
|
281 |
+
|
282 |
+
addPolygons(
|
283 |
+
data = osm_greenspace,
|
284 |
+
group = "Greenspace",
|
285 |
+
fillColor = "darkgreen",
|
286 |
+
fillOpacity = 0.3,
|
287 |
+
color = "green",
|
288 |
+
weight = 1,
|
289 |
+
label = ~name,
|
290 |
+
highlightOptions = highlightOptions(
|
291 |
+
weight = 5,
|
292 |
+
color = "blue",
|
293 |
+
fillOpacity = 0.5,
|
294 |
+
bringToFront = TRUE
|
295 |
+
),
|
296 |
+
labelOptions = labelOptions(
|
297 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
298 |
+
textsize = "12px",
|
299 |
+
direction = "auto",
|
300 |
+
noHide = FALSE # Labels appear on hover
|
301 |
+
)
|
302 |
+
) %>%
|
303 |
+
|
304 |
+
addPolygons(
|
305 |
+
data = biodiv_hotspots,
|
306 |
+
group = "Hotspots (KnowBR)",
|
307 |
+
fillColor = "firebrick",
|
308 |
+
fillOpacity = 0.2,
|
309 |
+
color = "firebrick",
|
310 |
+
weight = 2,
|
311 |
+
label = ~GEOID,
|
312 |
+
highlightOptions = highlightOptions(
|
313 |
+
weight = 5,
|
314 |
+
color = "blue",
|
315 |
+
fillOpacity = 0.5,
|
316 |
+
bringToFront = TRUE
|
317 |
+
),
|
318 |
+
labelOptions = labelOptions(
|
319 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
320 |
+
textsize = "12px",
|
321 |
+
direction = "auto"
|
322 |
+
)
|
323 |
+
) %>%
|
324 |
+
|
325 |
+
addPolygons(
|
326 |
+
data = biodiv_coldspots,
|
327 |
+
group = "Coldspots (KnowBR)",
|
328 |
+
fillColor = "navyblue",
|
329 |
+
fillOpacity = 0.2,
|
330 |
+
color = "navyblue",
|
331 |
+
weight = 2,
|
332 |
+
label = ~GEOID,
|
333 |
+
highlightOptions = highlightOptions(
|
334 |
+
weight = 5,
|
335 |
+
color = "blue",
|
336 |
+
fillOpacity = 0.5,
|
337 |
+
bringToFront = TRUE
|
338 |
+
),
|
339 |
+
labelOptions = labelOptions(
|
340 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
341 |
+
textsize = "12px",
|
342 |
+
direction = "auto"
|
343 |
+
)
|
344 |
+
) %>%
|
345 |
+
|
346 |
+
# Add richness and nobs
|
347 |
+
# -- Richness layer
|
348 |
+
addPolygons(
|
349 |
+
data = cbg_vect_sf,
|
350 |
+
group = "Species Richness",
|
351 |
+
fillColor = ~pal_rich(unique_species),
|
352 |
+
fillOpacity = 0.6,
|
353 |
+
color = "white",
|
354 |
+
weight = 1,
|
355 |
+
label =~unique_species,
|
356 |
+
popup = ~paste0(
|
357 |
+
"<strong>GEOID: </strong>", GEOID,
|
358 |
+
"<br><strong>Species Richness: </strong>", unique_species,
|
359 |
+
"<br><strong>Observations: </strong>", n_observations,
|
360 |
+
"<br><strong>Median Income: </strong>", median_inc,
|
361 |
+
"<br><strong>Mean NDVI: </strong>", ndvi_mean
|
362 |
+
)
|
363 |
+
) %>%
|
364 |
+
|
365 |
+
# -- Data Availability layer
|
366 |
+
addPolygons(
|
367 |
+
data = cbg_vect_sf,
|
368 |
+
group = "Data Availability",
|
369 |
+
fillColor = ~pal_data(n_observations),
|
370 |
+
fillOpacity = 0.6,
|
371 |
+
color = "white",
|
372 |
+
weight = 1,
|
373 |
+
label =~n_observations,
|
374 |
+
popup = ~paste0(
|
375 |
+
"<strong>GEOID: </strong>", GEOID,
|
376 |
+
"<br><strong>Observations: </strong>", n_observations,
|
377 |
+
"<br><strong>Species Richness: </strong>", unique_species,
|
378 |
+
"<br><strong>Median Income: </strong>", median_inc,
|
379 |
+
"<br><strong>Mean NDVI: </strong>", ndvi_mean
|
380 |
+
)
|
381 |
+
) %>%
|
382 |
+
|
383 |
+
|
384 |
+
setView(lng = -122.4194, lat = 37.7749, zoom = 12) %>%
|
385 |
+
addLayersControl(
|
386 |
+
baseGroups = c("Street Map (Default)", "Satellite (ESRI)", "CartoDB.Positron"),
|
387 |
+
overlayGroups = c("Income", "Greenspace","Species Richness", "Data Availability",
|
388 |
+
"Hotspots (KnowBR)", "Coldspots (KnowBR)"),
|
389 |
+
options = layersControlOptions(collapsed = FALSE)
|
390 |
+
) %>%
|
391 |
+
hideGroup("Income") %>%
|
392 |
+
hideGroup("Greenspace") %>%
|
393 |
+
hideGroup("Hotspots (KnowBR)") %>%
|
394 |
+
hideGroup("Coldspots (KnowBR)") %>%
|
395 |
+
hideGroup("Species Richness") %>%
|
396 |
+
hideGroup("Data Availability")
|
397 |
+
})
|
398 |
+
|
399 |
+
|
400 |
+
# ------------------------------------------------
|
401 |
+
# Observe map clicks (location_choice = 'map_click')
|
402 |
+
# ------------------------------------------------
|
403 |
+
observeEvent(input$isoMap_click, {
|
404 |
+
req(input$location_choice == "map_click")
|
405 |
+
click <- input$isoMap_click
|
406 |
+
if (!is.null(click)) {
|
407 |
+
chosen_point(c(lon = click$lng, lat = click$lat))
|
408 |
+
|
409 |
+
# Provide feedback with coordinates
|
410 |
+
showNotification(
|
411 |
+
paste0("Map clicked at Longitude: ", round(click$lng, 5),
|
412 |
+
", Latitude: ", round(click$lat, 5)),
|
413 |
+
type = "message"
|
414 |
+
)
|
415 |
+
|
416 |
+
# Update the map with a marker
|
417 |
+
leafletProxy("isoMap") %>%
|
418 |
+
clearMarkers() %>%
|
419 |
+
addCircleMarkers(
|
420 |
+
lng = click$lng, lat = click$lat,
|
421 |
+
radius = 6, color = "firebrick",
|
422 |
+
label = "Map Click Location"
|
423 |
+
)
|
424 |
+
}
|
425 |
+
})
|
426 |
+
|
427 |
+
# ------------------------------------------------
|
428 |
+
# Observe geocoder input
|
429 |
+
# ------------------------------------------------
|
430 |
+
observeEvent(input$geocoder, {
|
431 |
+
req(input$location_choice == "address")
|
432 |
+
geocode_result <- input$geocoder
|
433 |
+
if (!is.null(geocode_result)) {
|
434 |
+
# Extract coordinates
|
435 |
+
xy <- geocoder_as_xy(geocode_result)
|
436 |
+
|
437 |
+
# Update the chosen_point reactive value
|
438 |
+
chosen_point(c(lon = xy[1], lat = xy[2]))
|
439 |
+
|
440 |
+
# Provide feedback with the geocoded address and coordinates
|
441 |
+
showNotification(
|
442 |
+
paste0("Address geocoded to Longitude: ", round(xy[1], 5),
|
443 |
+
", Latitude: ", round(xy[2], 5)),
|
444 |
+
type = "message"
|
445 |
+
)
|
446 |
+
|
447 |
+
# Update the map with a marker
|
448 |
+
leafletProxy("isoMap") %>%
|
449 |
+
clearMarkers() %>%
|
450 |
+
addCircleMarkers(
|
451 |
+
lng = xy[1], lat = xy[2],
|
452 |
+
radius = 6, color = "navyblue",
|
453 |
+
label = "Geocoded Address"
|
454 |
+
) %>%
|
455 |
+
flyTo(lng = xy[1], lat = xy[2], zoom = 13)
|
456 |
+
}
|
457 |
+
})
|
458 |
+
|
459 |
+
# ------------------------------------------------
|
460 |
+
# Observe clearing of map
|
461 |
+
# ------------------------------------------------
|
462 |
+
observeEvent(input$clear_map, {
|
463 |
+
# Reset the chosen point
|
464 |
+
chosen_point(NULL)
|
465 |
+
|
466 |
+
# Clear all markers and isochrones from the map
|
467 |
+
leafletProxy("isoMap") %>%
|
468 |
+
clearMarkers() %>%
|
469 |
+
# clearShapes() %>%
|
470 |
+
clearGroup("Isochrones") %>%
|
471 |
+
clearGroup("NDVI Raster")
|
472 |
+
|
473 |
+
# Optional: Reset any other reactive values if needed
|
474 |
+
showNotification("Map cleared. You can select a new location.")
|
475 |
+
})
|
476 |
+
|
477 |
+
# ------------------------------------------------
|
478 |
+
# Generate Isochrones
|
479 |
+
# ------------------------------------------------
|
480 |
+
isochrones_data <- eventReactive(input$generate_iso, {
|
481 |
+
|
482 |
+
leafletProxy("isoMap") %>%
|
483 |
+
clearGroup("Isochrones") %>%
|
484 |
+
clearGroup("NDVI Raster")
|
485 |
+
|
486 |
+
# If user selected address:
|
487 |
+
if (input$location_choice == "address") {
|
488 |
+
if (is.null(input$geocoder)) {
|
489 |
+
showNotification("Please use the geocoder to select an address.", type = "error")
|
490 |
+
return(NULL)
|
491 |
+
}
|
492 |
+
|
493 |
+
# Coordinates are already set via the geocoder observer
|
494 |
+
# No need to geocode again
|
495 |
+
}
|
496 |
+
|
497 |
+
pt <- chosen_point()
|
498 |
+
if (is.null(pt)) {
|
499 |
+
showNotification("No location selected! Provide an address or click the map.", type = "error")
|
500 |
+
return(NULL)
|
501 |
+
}
|
502 |
+
if (length(input$transport_modes) == 0) {
|
503 |
+
showNotification("Select at least one transportation mode.", type = "error")
|
504 |
+
return(NULL)
|
505 |
+
}
|
506 |
+
if (length(input$iso_times) == 0) {
|
507 |
+
showNotification("Select at least one isochrone time.", type = "error")
|
508 |
+
return(NULL)
|
509 |
+
}
|
510 |
+
|
511 |
+
location_sf <- st_as_sf(
|
512 |
+
data.frame(lon = pt["lon"], lat = pt["lat"]),
|
513 |
+
coords = c("lon","lat"), crs = 4326
|
514 |
+
)
|
515 |
+
|
516 |
+
iso_list <- list()
|
517 |
+
for (mode in input$transport_modes) {
|
518 |
+
for (t in input$iso_times) {
|
519 |
+
iso <- tryCatch({
|
520 |
+
mb_isochrone(location_sf, time = as.numeric(t), profile = mode,
|
521 |
+
access_token = mapbox_token)
|
522 |
+
}, error = function(e) {
|
523 |
+
showNotification(paste("Isochrone error:", mode, t, e$message), type = "error")
|
524 |
+
NULL
|
525 |
+
})
|
526 |
+
if (!is.null(iso)) {
|
527 |
+
iso$mode <- mode
|
528 |
+
iso$time <- t
|
529 |
+
iso_list <- append(iso_list, list(iso))
|
530 |
+
}
|
531 |
+
}
|
532 |
+
}
|
533 |
+
if (length(iso_list) == 0) {
|
534 |
+
showNotification("No isochrones generated.", type = "warning")
|
535 |
+
return(NULL)
|
536 |
+
}
|
537 |
+
|
538 |
+
all_iso <- do.call(rbind, iso_list) %>% st_transform(4326)
|
539 |
+
all_iso
|
540 |
+
})
|
541 |
+
|
542 |
+
# ------------------------------------------------
|
543 |
+
# Plot Isochrones + NDVI
|
544 |
+
# ------------------------------------------------
|
545 |
+
observeEvent(isochrones_data(), {
|
546 |
+
iso_data <- isochrones_data()
|
547 |
+
req(iso_data)
|
548 |
+
|
549 |
+
iso_data$iso_group <- paste(iso_data$mode, iso_data$time, sep = "_")
|
550 |
+
pal <- colorRampPalette(brewer.pal(8, "Set2"))
|
551 |
+
cols <- pal(nrow(iso_data))
|
552 |
+
|
553 |
+
for (i in seq_len(nrow(iso_data))) {
|
554 |
+
poly_i <- iso_data[i, ]
|
555 |
+
leafletProxy("isoMap") %>%
|
556 |
+
addPolygons(
|
557 |
+
data = poly_i,
|
558 |
+
group = "Isochrones",
|
559 |
+
color = cols[i],
|
560 |
+
weight = 2,
|
561 |
+
fillOpacity = 0.4,
|
562 |
+
label = paste0(poly_i$mode, " - ", poly_i$time, " mins")
|
563 |
+
)
|
564 |
+
}
|
565 |
+
|
566 |
+
iso_union <- st_union(iso_data)
|
567 |
+
iso_union_vect <- vect(iso_union)
|
568 |
+
ndvi_crop <- crop(ndvi, iso_union_vect)
|
569 |
+
ndvi_mask <- mask(ndvi_crop, iso_union_vect)
|
570 |
+
ndvi_vals <- values(ndvi_mask)
|
571 |
+
ndvi_vals <- ndvi_vals[!is.na(ndvi_vals)]
|
572 |
+
|
573 |
+
if (length(ndvi_vals) > 0) {
|
574 |
+
ndvi_pal <- colorNumeric("YlGn", domain = range(ndvi_vals, na.rm = TRUE), na.color = "transparent")
|
575 |
+
|
576 |
+
leafletProxy("isoMap") %>%
|
577 |
+
addRasterImage(
|
578 |
+
x = ndvi_mask,
|
579 |
+
colors = ndvi_pal,
|
580 |
+
opacity = 0.7,
|
581 |
+
project = TRUE,
|
582 |
+
group = "NDVI Raster"
|
583 |
+
) %>%
|
584 |
+
addLegend(
|
585 |
+
position = "bottomright",
|
586 |
+
pal = ndvi_pal,
|
587 |
+
values = ndvi_vals,
|
588 |
+
title = "NDVI"
|
589 |
+
)
|
590 |
+
}
|
591 |
+
|
592 |
+
leafletProxy("isoMap") %>%
|
593 |
+
addLayersControl(
|
594 |
+
baseGroups = c("Street Map (Default)", "Satellite (ESRI)", "CartoDB.Positron"),
|
595 |
+
overlayGroups = c("Income", "Greenspace",
|
596 |
+
"Hotspots (KnowBR)", "Coldspots (KnowBR)",
|
597 |
+
"Isochrones", "NDVI Raster"),
|
598 |
+
options = layersControlOptions(collapsed = FALSE)
|
599 |
+
)
|
600 |
+
})
|
601 |
+
|
602 |
+
# ------------------------------------------------
|
603 |
+
# socio_data Reactive + Summaries
|
604 |
+
# ------------------------------------------------
|
605 |
+
socio_data <- reactive({
|
606 |
+
iso_data <- isochrones_data()
|
607 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
608 |
+
return(data.frame())
|
609 |
+
}
|
610 |
+
|
611 |
+
acs_wide <- cbg_vect_sf %>%
|
612 |
+
mutate(
|
613 |
+
population = popE,
|
614 |
+
med_income = medincE
|
615 |
+
)
|
616 |
+
|
617 |
+
hotspot_union <- st_union(biodiv_hotspots)
|
618 |
+
coldspot_union <- st_union(biodiv_coldspots)
|
619 |
+
|
620 |
+
results <- data.frame()
|
621 |
+
|
622 |
+
# Calculate distance to coldspot and hotspots
|
623 |
+
for (i in seq_len(nrow(iso_data))) {
|
624 |
+
poly_i <- iso_data[i, ]
|
625 |
+
|
626 |
+
dist_hot <- st_distance(poly_i, hotspot_union)
|
627 |
+
dist_cold <- st_distance(poly_i, coldspot_union)
|
628 |
+
dist_hot_km <- round(as.numeric(min(dist_hot)) / 1000, 3)
|
629 |
+
dist_cold_km <- round(as.numeric(min(dist_cold)) / 1000, 3)
|
630 |
+
|
631 |
+
inter_acs <- st_intersection(acs_wide, poly_i)
|
632 |
+
|
633 |
+
vect_acs_wide <- vect(acs_wide)
|
634 |
+
vect_poly_i <- vect(poly_i)
|
635 |
+
inter_acs <- intersect(vect_acs_wide, vect_poly_i)
|
636 |
+
inter_acs = st_as_sf(inter_acs)
|
637 |
+
|
638 |
+
pop_total <- 0
|
639 |
+
inc_str <- "N/A"
|
640 |
+
if (nrow(inter_acs) > 0) {
|
641 |
+
inter_acs$area <- st_area(inter_acs)
|
642 |
+
inter_acs$area_num <- as.numeric(inter_acs$area)
|
643 |
+
inter_acs$area_ratio <- inter_acs$area_num / as.numeric(st_area(inter_acs))
|
644 |
+
inter_acs$weighted_pop <- inter_acs$population * inter_acs$area_ratio
|
645 |
+
|
646 |
+
pop_total <- round(sum(inter_acs$weighted_pop, na.rm = TRUE))
|
647 |
+
|
648 |
+
w_income <- sum(inter_acs$med_income * inter_acs$area_num, na.rm = TRUE) /
|
649 |
+
sum(inter_acs$area_num, na.rm = TRUE)
|
650 |
+
if (!is.na(w_income) && w_income > 0) {
|
651 |
+
inc_str <- paste0("$", formatC(round(w_income, 2), format = "f", big.mark = ","))
|
652 |
+
}
|
653 |
+
}
|
654 |
+
|
655 |
+
# Intersection with greenspace
|
656 |
+
vec_osm_greenspace <- vect(osm_greenspace)
|
657 |
+
inter_gs <- intersect(vec_osm_greenspace, vect_poly_i)
|
658 |
+
inter_gs = st_as_sf(inter_gs)
|
659 |
+
|
660 |
+
gs_area_m2 <- 0
|
661 |
+
if (nrow(inter_gs) > 0) {
|
662 |
+
gs_area_m2 <- sum(st_area(inter_gs))
|
663 |
+
}
|
664 |
+
iso_area_m2 <- as.numeric(st_area(poly_i))
|
665 |
+
gs_area_m2 <- as.numeric(gs_area_m2)
|
666 |
+
gs_percent <- ifelse(iso_area_m2 > 0, 100 * gs_area_m2 / iso_area_m2, 0)
|
667 |
+
|
668 |
+
# NDVI Calculation
|
669 |
+
poly_vect <- vect(poly_i)
|
670 |
+
ndvi_crop <- crop(ndvi, poly_vect)
|
671 |
+
ndvi_mask <- mask(ndvi_crop, poly_vect)
|
672 |
+
ndvi_vals <- values(ndvi_mask)
|
673 |
+
ndvi_vals <- ndvi_vals[!is.na(ndvi_vals)]
|
674 |
+
mean_ndvi <- ifelse(length(ndvi_vals) > 0, round(mean(ndvi_vals, na.rm=TRUE), 3), NA)
|
675 |
+
|
676 |
+
# Intersection with GBIF data
|
677 |
+
inter_gbif <- intersect(vect_gbif, vect_poly_i)
|
678 |
+
inter_gbif <- st_as_sf(inter_gbif)
|
679 |
+
|
680 |
+
inter_gbif_acs <- sf_gbif %>%
|
681 |
+
mutate(
|
682 |
+
income = medincE,
|
683 |
+
ndvi = ndvi_sentinel
|
684 |
+
)
|
685 |
+
|
686 |
+
if (nrow(inter_gbif) > 0) {
|
687 |
+
inter_gbif_acs <- inter_gbif_acs[inter_gbif_acs$GEOID %in% inter_gbif$GEOID, ]
|
688 |
+
}
|
689 |
+
|
690 |
+
n_records <- nrow(inter_gbif)
|
691 |
+
n_species <- length(unique(inter_gbif$species))
|
692 |
+
|
693 |
+
n_birds <- length(unique(inter_gbif$species[inter_gbif$class == "Aves"]))
|
694 |
+
n_mammals <- length(unique(inter_gbif$species[inter_gbif$class == "Mammalia"]))
|
695 |
+
n_plants <- length(unique(inter_gbif$species[inter_gbif$class %in%
|
696 |
+
c("Magnoliopsida","Liliopsida","Pinopsida","Polypodiopsida",
|
697 |
+
"Equisetopsida","Bryopsida","Marchantiopsida") ]))
|
698 |
+
|
699 |
+
iso_area_km2 <- round(iso_area_m2 / 1e6, 3)
|
700 |
+
|
701 |
+
row_i <- data.frame(
|
702 |
+
Mode = tools::toTitleCase(poly_i$mode),
|
703 |
+
Time = poly_i$time,
|
704 |
+
IsochroneArea_km2 = iso_area_km2,
|
705 |
+
DistToHotspot_km = dist_hot_km,
|
706 |
+
DistToColdspot_km = dist_cold_km,
|
707 |
+
EstimatedPopulation = pop_total,
|
708 |
+
MedianIncome = inc_str,
|
709 |
+
MeanNDVI = ifelse(!is.na(mean_ndvi), mean_ndvi, "N/A"),
|
710 |
+
GBIF_Records = n_records,
|
711 |
+
GBIF_Species = n_species,
|
712 |
+
Bird_Species = n_birds,
|
713 |
+
Mammal_Species = n_mammals,
|
714 |
+
Plant_Species = n_plants,
|
715 |
+
Greenspace_m2 = round(gs_area_m2, 2),
|
716 |
+
Greenspace_percent = round(gs_percent, 2),
|
717 |
+
stringsAsFactors = FALSE
|
718 |
+
)
|
719 |
+
results <- rbind(results, row_i)
|
720 |
+
}
|
721 |
+
|
722 |
+
iso_union <- st_union(iso_data)
|
723 |
+
vect_iso <- vect(iso_union)
|
724 |
+
inter_all_gbif <- intersect(vect_gbif, vect_iso)
|
725 |
+
inter_all_gbif <- st_as_sf(inter_all_gbif)
|
726 |
+
|
727 |
+
union_n_species <- length(unique(inter_all_gbif$species))
|
728 |
+
rank_percentile <- round(100 * ecdf(cbg_vect_sf$unique_species)(union_n_species), 1)
|
729 |
+
attr(results, "bio_percentile") <- rank_percentile
|
730 |
+
|
731 |
+
# Closest Greenspace from ANY part of the isochrone
|
732 |
+
dist_mat <- st_distance(iso_union, osm_greenspace) # 1 x N matrix
|
733 |
+
if (length(dist_mat) > 0) {
|
734 |
+
min_dist <- min(dist_mat)
|
735 |
+
min_idx <- which.min(dist_mat)
|
736 |
+
gs_name <- osm_greenspace$name[min_idx]
|
737 |
+
attr(results, "closest_greenspace") <- gs_name
|
738 |
+
} else {
|
739 |
+
attr(results, "closest_greenspace") <- "None"
|
740 |
+
}
|
741 |
+
|
742 |
+
results
|
743 |
+
})
|
744 |
+
|
745 |
+
# ------------------------------------------------
|
746 |
+
# Render main summary table
|
747 |
+
# ------------------------------------------------
|
748 |
+
output$dataTable <- renderDT({
|
749 |
+
df <- socio_data()
|
750 |
+
if (nrow(df) == 0) {
|
751 |
+
return(DT::datatable(data.frame("Message" = "No isochrones generated yet.")))
|
752 |
+
}
|
753 |
+
DT::datatable(
|
754 |
+
df,
|
755 |
+
colnames = c(
|
756 |
+
"Mode" = "Mode",
|
757 |
+
"Time (min)" = "Time",
|
758 |
+
"Area (kmΒ²)" = "IsochroneArea_km2",
|
759 |
+
"Dist. Hotspot (km)" = "DistToHotspot_km",
|
760 |
+
"Dist. Coldspot (km)" = "DistToColdspot_km",
|
761 |
+
"Population" = "EstimatedPopulation",
|
762 |
+
"Median Income" = "MedianIncome",
|
763 |
+
"Mean NDVI" = "MeanNDVI",
|
764 |
+
"GBIF Records" = "GBIF_Records",
|
765 |
+
"Unique Species" = "GBIF_Species",
|
766 |
+
"Bird Species" = "Bird_Species",
|
767 |
+
"Mammal Species" = "Mammal_Species",
|
768 |
+
"Plant Species" = "Plant_Species",
|
769 |
+
"Greenspace (mΒ²)" = "Greenspace_m2",
|
770 |
+
"Greenspace (%)" = "Greenspace_percent"
|
771 |
+
),
|
772 |
+
options = list(pageLength = 10, autoWidth = TRUE),
|
773 |
+
rownames = FALSE
|
774 |
+
)
|
775 |
+
})
|
776 |
+
|
777 |
+
# ------------------------------------------------
|
778 |
+
# Biodiversity Access Score + Closest Greenspace
|
779 |
+
# ------------------------------------------------
|
780 |
+
output$bioScoreBox <- renderUI({
|
781 |
+
df <- socio_data()
|
782 |
+
if (nrow(df) == 0) return(NULL)
|
783 |
+
|
784 |
+
percentile <- attr(df, "bio_percentile")
|
785 |
+
if (is.null(percentile)) percentile <- "N/A"
|
786 |
+
else percentile <- paste0(percentile, "th Percentile")
|
787 |
+
|
788 |
+
wellPanel(
|
789 |
+
HTML(paste0("<h2>Biodiversity Access Score: ", percentile, "</h2>"))
|
790 |
+
)
|
791 |
+
})
|
792 |
+
|
793 |
+
output$closestGreenspaceUI <- renderUI({
|
794 |
+
df <- socio_data()
|
795 |
+
if (nrow(df) == 0) return(NULL)
|
796 |
+
gs_name <- attr(df, "closest_greenspace")
|
797 |
+
if (is.null(gs_name)) gs_name <- "None"
|
798 |
+
|
799 |
+
tagList(
|
800 |
+
strong("Closest Greenspace (from any part of the Isochrone):"),
|
801 |
+
p(gs_name)
|
802 |
+
)
|
803 |
+
})
|
804 |
+
|
805 |
+
# ------------------------------------------------
|
806 |
+
# Secondary table: user-selected CLASS & FAMILY
|
807 |
+
# ------------------------------------------------
|
808 |
+
output$classTable <- renderDT({
|
809 |
+
iso_data <- isochrones_data()
|
810 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
811 |
+
return(DT::datatable(data.frame("Message" = "No isochrones generated yet.")))
|
812 |
+
}
|
813 |
+
|
814 |
+
iso_union <- st_union(iso_data)
|
815 |
+
# inter_gbif <- st_intersection(sf_gbif, iso_union)
|
816 |
+
|
817 |
+
vect_iso <- vect(iso_union)
|
818 |
+
inter_gbif <- intersect(vect_gbif, vect_iso)
|
819 |
+
inter_gbif = st_as_sf(inter_gbif)
|
820 |
+
|
821 |
+
# Add a quick ACS intersection for mean income & NDVI if needed
|
822 |
+
acs_wide <- cbg_vect_sf %>% mutate(
|
823 |
+
income = median_inc,
|
824 |
+
ndvi = ndvi_mean
|
825 |
+
)
|
826 |
+
# this can be skipped !
|
827 |
+
# inter_gbif_acs <- st_intersection(inter_gbif, acs_wide)
|
828 |
+
|
829 |
+
inter_gbif_acs = sf_gbif |> dplyr::mutate(income = medincE,
|
830 |
+
ndvi = ndvi_sentinel)#We can do this because we preannotated ndvi and us census information
|
831 |
+
|
832 |
+
if (input$class_filter != "All") {
|
833 |
+
inter_gbif_acs <- inter_gbif_acs[ inter_gbif_acs$class == input$class_filter, ]
|
834 |
+
}
|
835 |
+
if (input$family_filter != "All") {
|
836 |
+
inter_gbif_acs <- inter_gbif_acs[ inter_gbif_acs$family == input$family_filter, ]
|
837 |
+
}
|
838 |
+
|
839 |
+
if (nrow(inter_gbif_acs) == 0) {
|
840 |
+
return(DT::datatable(data.frame("Message" = "No records for that combination in the isochrone.")))
|
841 |
+
}
|
842 |
+
|
843 |
+
species_counts <- inter_gbif_acs %>%
|
844 |
+
st_drop_geometry() %>%
|
845 |
+
group_by(species) %>%
|
846 |
+
summarize(
|
847 |
+
n_records = n(),
|
848 |
+
mean_income = round(mean(income, na.rm=TRUE), 2),
|
849 |
+
mean_ndvi = round(mean(ndvi, na.rm=TRUE), 3),
|
850 |
+
.groups = "drop"
|
851 |
+
) %>%
|
852 |
+
arrange(desc(n_records))
|
853 |
+
|
854 |
+
DT::datatable(
|
855 |
+
species_counts,
|
856 |
+
colnames = c("Species", "Number of Records", "Mean Income", "Mean NDVI"),
|
857 |
+
options = list(pageLength = 10),
|
858 |
+
rownames = FALSE
|
859 |
+
)
|
860 |
+
})
|
861 |
+
|
862 |
+
# ------------------------------------------------
|
863 |
+
# Ggplot: Biodiversity & Socioeconomic Summary
|
864 |
+
# ------------------------------------------------
|
865 |
+
output$bioSocPlot <- renderPlot({
|
866 |
+
df <- socio_data()
|
867 |
+
if (nrow(df) == 0) return(NULL)
|
868 |
+
|
869 |
+
df_plot <- df %>%
|
870 |
+
mutate(IsoLabel = paste0(Mode, "-", Time, "min"))
|
871 |
+
|
872 |
+
ggplot(df_plot, aes(x = IsoLabel)) +
|
873 |
+
geom_col(aes(y = GBIF_Species), fill = "steelblue", alpha = 0.7) +
|
874 |
+
geom_line(aes(y = EstimatedPopulation / 1000, group = 1), color = "red", size = 1) +
|
875 |
+
geom_point(aes(y = EstimatedPopulation / 1000), color = "red", size = 3) +
|
876 |
+
labs(
|
877 |
+
x = "Isochrone (Mode-Time)",
|
878 |
+
y = "Unique Species (Blue) | Population (Red) (Thousands)",
|
879 |
+
title = "Biodiversity & Socioeconomic Summary"
|
880 |
+
) +
|
881 |
+
theme_minimal(base_size = 14) +
|
882 |
+
theme(
|
883 |
+
axis.text.x = element_text(angle = 45, hjust = 1, size = 12),
|
884 |
+
axis.text.y = element_text(size = 12),
|
885 |
+
axis.title.x = element_text(size = 14),
|
886 |
+
axis.title.y = element_text(size = 14),
|
887 |
+
plot.title = element_text(hjust = 0.5, size = 16, face = "bold")
|
888 |
+
)
|
889 |
+
})
|
890 |
+
|
891 |
+
# ------------------------------------------------
|
892 |
+
# Bar plot: GBIF records by institutionCode
|
893 |
+
# ------------------------------------------------
|
894 |
+
output$collectionPlot <- renderPlot({
|
895 |
+
iso_data <- isochrones_data()
|
896 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
897 |
+
plot.new()
|
898 |
+
title("No GBIF records found in this isochrone.")
|
899 |
+
return(NULL)
|
900 |
+
}
|
901 |
+
|
902 |
+
iso_union <- st_union(iso_data)
|
903 |
+
# inter_gbif <- st_intersection(sf_gbif, iso_union)
|
904 |
+
|
905 |
+
vect_iso <- vect(iso_union)
|
906 |
+
inter_gbif <- intersect(vect_gbif, vect_iso)
|
907 |
+
inter_gbif = st_as_sf(inter_gbif)
|
908 |
+
|
909 |
+
if (nrow(inter_gbif) == 0) {
|
910 |
+
plot.new()
|
911 |
+
title("No GBIF records found in this isochrone.")
|
912 |
+
return(NULL)
|
913 |
+
}
|
914 |
+
|
915 |
+
df_code <- inter_gbif %>%
|
916 |
+
st_drop_geometry() %>%
|
917 |
+
group_by(institutionCode) %>%
|
918 |
+
summarize(count = n(), .groups = "drop") %>%
|
919 |
+
arrange(desc(count)) %>%
|
920 |
+
mutate(truncatedCode = substr(institutionCode, 1, 5)) # Shorter version of the names
|
921 |
+
|
922 |
+
ggplot(df_code, aes(x = reorder(truncatedCode, -count), y = count)) + # replaced institutionCode with truncatedCode
|
923 |
+
geom_bar(stat = "identity", fill = "darkorange", alpha = 0.7) +
|
924 |
+
labs(
|
925 |
+
x = "Institution Code (Truncated)",
|
926 |
+
y = "Number of Records",
|
927 |
+
title = "GBIF Records by Institution Code (Isochrone Union)"
|
928 |
+
) +
|
929 |
+
theme_minimal(base_size = 14) +
|
930 |
+
theme(
|
931 |
+
axis.text.x = element_text(angle = 45, hjust = 1, size = 12),
|
932 |
+
axis.text.y = element_text(size = 12),
|
933 |
+
axis.title.x = element_text(size = 14),
|
934 |
+
axis.title.y = element_text(size = 14),
|
935 |
+
plot.title = element_text(hjust = 0.5, size = 16, face = "bold")
|
936 |
+
)
|
937 |
+
})
|
938 |
+
|
939 |
+
# ------------------------------------------------
|
940 |
+
# Additional Section: mapview for species richness vs. data availability
|
941 |
+
# ------------------------------------------------
|
942 |
+
output$mapNUI <- renderUI({
|
943 |
+
map_n <- mapview(cbg_vect_sf, zcol = "n", layer.name="Data Availability (n)")
|
944 |
+
map_n@map
|
945 |
+
})
|
946 |
+
|
947 |
+
output$mapSpeciesUI <- renderUI({
|
948 |
+
map_s <- mapview(cbg_vect_sf, zcol = "n_species", layer.name="Species Richness (n_species)")
|
949 |
+
map_s@map
|
950 |
+
})
|
951 |
+
|
952 |
+
|
953 |
+
|
954 |
+
|
955 |
+
# ------------------------------------------------
|
956 |
+
# Additional Plot: n_observations vs n_species
|
957 |
+
# ------------------------------------------------
|
958 |
+
|
959 |
+
# Make it reactive: obsVsSpeciesPlot updates dynamically based on user-selected class_filter or family_filter.
|
960 |
+
|
961 |
+
filtered_data <- reactive({
|
962 |
+
data <- cbg_vect_sf
|
963 |
+
if (input$class_filter != "All") {
|
964 |
+
data <- data[data$class == input$class_filter, ]
|
965 |
+
}
|
966 |
+
if (input$family_filter != "All") {
|
967 |
+
data <- data[data$family == input$family_filter, ]
|
968 |
+
}
|
969 |
+
data
|
970 |
+
})
|
971 |
+
|
972 |
+
output$obsVsSpeciesPlot <- renderPlot({
|
973 |
+
data <- filtered_data()
|
974 |
+
if (nrow(data) == 0) {
|
975 |
+
plot.new()
|
976 |
+
title("No data available for selected filters.")
|
977 |
+
return(NULL)
|
978 |
+
}
|
979 |
+
|
980 |
+
ggplot(data, aes(x = log(n_observations + 1), y = log(unique_species + 1))) +
|
981 |
+
geom_point(color = "blue", alpha = 0.6) +
|
982 |
+
labs(
|
983 |
+
x = "Log(Number of Observations + 1)",
|
984 |
+
y = "Log(Species Richness + 1)",
|
985 |
+
title = "Data Availability vs. Species Richness"
|
986 |
+
) +
|
987 |
+
theme_minimal(base_size = 14) +
|
988 |
+
theme(
|
989 |
+
axis.text.x = element_text(size = 12),
|
990 |
+
axis.text.y = element_text(size = 12),
|
991 |
+
axis.title.x = element_text(size = 14),
|
992 |
+
axis.title.y = element_text(size = 14),
|
993 |
+
plot.title = element_text(hjust = 0.5, size = 16, face = "bold")
|
994 |
+
)
|
995 |
+
})
|
996 |
+
|
997 |
+
# ------------------------------------------------
|
998 |
+
# [Optional: Linear Model Plot (Commented Out)]
|
999 |
+
# ------------------------------------------------
|
1000 |
+
# Uncomment and adjust if needed
|
1001 |
+
# output$lmCoefficientsPlot <- renderPlot({
|
1002 |
+
# df_lm <- cbg_vect_sf %>%
|
1003 |
+
# filter(!is.na(n_observations),
|
1004 |
+
# !is.na(unique_species),
|
1005 |
+
# !is.na(median_inc),
|
1006 |
+
# !is.na(ndvi_mean))
|
1007 |
+
#
|
1008 |
+
# if (nrow(df_lm) < 5) {
|
1009 |
+
# plot.new()
|
1010 |
+
# title("Not enough data for linear model.")
|
1011 |
+
# return(NULL)
|
1012 |
+
# }
|
1013 |
+
#
|
1014 |
+
# fit <- lm(unique_species ~ n_observations + median_inc + ndvi_mean, data = df_lm)
|
1015 |
+
#
|
1016 |
+
# p <- plot_model(fit, show.values = TRUE, value.offset = .3, title = "LM Coefficients: n_species ~ n_observations + median_inc + ndvi_mean")
|
1017 |
+
# print(p)
|
1018 |
+
# })
|
1019 |
+
}
|
1020 |
+
|
1021 |
+
# Run the Shiny app
|
1022 |
+
shinyApp(ui, server)
|
R/setup.R
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# setup
|
2 |
+
|
3 |
+
# ------------------------------------------------
|
4 |
+
# 1) API Keys
|
5 |
+
# ------------------------------------------------
|
6 |
+
mapbox_token <- "pk.eyJ1Ijoia3dhbGtlcnRjdSIsImEiOiJjbHc3NmI0cDMxYzhyMmt0OXBiYnltMjVtIn0.Thtu6WqIhOfin6AykskM2g"
|
7 |
+
mb_access_token(mapbox_token, install = FALSE)
|
8 |
+
|
9 |
+
# ------------------------------------------------
|
10 |
+
# 2) Load Data
|
11 |
+
# ------------------------------------------------
|
12 |
+
# -- Greenspace
|
13 |
+
getwd()
|
14 |
+
osm_greenspace <- st_read("data/greenspaces_osm_nad83.shp", quiet = TRUE) %>%
|
15 |
+
st_transform(4326)
|
16 |
+
if (!"name" %in% names(osm_greenspace)) {
|
17 |
+
osm_greenspace$name <- "Unnamed Greenspace"
|
18 |
+
}
|
19 |
+
|
20 |
+
# -- NDVI Raster
|
21 |
+
ndvi <- rast("data/SF_EastBay_NDVI_Sentinel_10.tif")
|
22 |
+
|
23 |
+
# -- GBIF data
|
24 |
+
# Load what is basically inter_gbif !!!!!
|
25 |
+
# load("data/sf_gbif.Rdata") # => sf_gbif
|
26 |
+
load('data/gbif_census_ndvi_anno.Rdata')
|
27 |
+
vect_gbif <- vect(sf_gbif)
|
28 |
+
# -- Precomputed CBG data
|
29 |
+
load('data/cbg_vect_sf.Rdata')
|
30 |
+
if (!"unique_species" %in% names(cbg_vect_sf)) {
|
31 |
+
cbg_vect_sf$unique_species <- cbg_vect_sf$n_species
|
32 |
+
}
|
33 |
+
if (!"n_observations" %in% names(cbg_vect_sf)) {
|
34 |
+
cbg_vect_sf$n_observations <- cbg_vect_sf$n
|
35 |
+
}
|
36 |
+
if (!"median_inc" %in% names(cbg_vect_sf)) {
|
37 |
+
cbg_vect_sf$median_inc <- cbg_vect_sf$medincE
|
38 |
+
}
|
39 |
+
if (!"ndvi_mean" %in% names(cbg_vect_sf)) {
|
40 |
+
cbg_vect_sf$ndvi_mean <- cbg_vect_sf$ndvi_sentinel
|
41 |
+
}
|
42 |
+
|
43 |
+
# -- Hotspots/Coldspots
|
44 |
+
biodiv_hotspots <- st_read("data/hotspots.shp", quiet = TRUE) %>% st_transform(4326)
|
45 |
+
biodiv_coldspots <- st_read("data/coldspots.shp", quiet = TRUE) %>% st_transform(4326)
|
46 |
+
|
47 |
+
|
48 |
+
#
|
49 |
+
# # Community Organizations shapefile
|
50 |
+
# # For now simulate
|
51 |
+
#
|
52 |
+
# # Define San Francisco bounding box coordinates
|
53 |
+
# sf_bbox <- st_bbox(c(
|
54 |
+
# xmin = -122.5247, # Western longitude
|
55 |
+
# ymin = 37.7045, # Southern latitude
|
56 |
+
# xmax = -122.3569, # Eastern longitude
|
57 |
+
# ymax = 37.8334 # Northern latitude
|
58 |
+
# ), crs = st_crs(4326)) # WGS84 CRS
|
59 |
+
#
|
60 |
+
# # Convert bounding box to polygon
|
61 |
+
# sf_boundary <- st_as_sfc(sf_bbox) %>% st_make_valid()
|
62 |
+
#
|
63 |
+
# # Transform boundary to projected CRS for accurate buffering (EPSG:3310)
|
64 |
+
# sf_boundary_proj <- st_transform(sf_boundary, 3310)
|
65 |
+
#
|
66 |
+
# # Set seed for reproducibility
|
67 |
+
# set.seed(123)
|
68 |
+
#
|
69 |
+
# # Simulate 20 random points within San Francisco boundary
|
70 |
+
# community_points <- st_sample(sf_boundary_proj, size = 20, type = "random")
|
71 |
+
#
|
72 |
+
# # Convert to sf object with POINT geometry and assign unique names
|
73 |
+
# community_points_sf <- st_sf(
|
74 |
+
# NAME = paste("Community Org", 1:20),
|
75 |
+
# geometry = community_points
|
76 |
+
# )
|
77 |
+
# # Select first 3 points to buffer
|
78 |
+
# buffered_points_sf <- community_points_sf[1:3, ] %>%
|
79 |
+
# st_buffer(dist = 100) # Buffer distance in meters
|
80 |
+
#
|
81 |
+
# # Update the NAME column to indicate buffered areas
|
82 |
+
# buffered_points_sf$NAME <- paste(buffered_points_sf$NAME, "Area")
|
83 |
+
# community_points_sf <- st_transform(community_points_sf, 4326)
|
84 |
+
# buffered_points_sf <- st_transform(buffered_points_sf, 4326)
|
85 |
+
#
|
86 |
+
# # Combine points and polygons into one sf object
|
87 |
+
# community_orgs <- bind_rows(
|
88 |
+
# community_points_sf,
|
89 |
+
# buffered_points_sf
|
90 |
+
# )
|
91 |
+
#
|
92 |
+
# # View the combined dataset
|
93 |
+
# print(community_orgs)
|
94 |
+
#
|
95 |
+
# community_points_only <- community_orgs %>% filter(st_geometry_type(geometry) == "POINT")
|
96 |
+
# community_polygons_only <- community_orgs %>% filter(st_geometry_type(geometry) == "POLYGON")
|
97 |
+
#
|
README.md
CHANGED
@@ -8,6 +8,11 @@ It further calculates a summary table of the GBIF data located within the isochr
|
|
8 |
|
9 |
# Next steps: Optimize preanno of sf gbif and cbg
|
10 |
|
|
|
|
|
11 |
# Public transport ddata
|
12 |
|
|
|
|
|
13 |
# Show difference on the day
|
|
|
|
8 |
|
9 |
# Next steps: Optimize preanno of sf gbif and cbg
|
10 |
|
11 |
+
Add Imp Surf, Walking Scores, SVI to cbg_sf
|
12 |
+
|
13 |
# Public transport ddata
|
14 |
|
15 |
+
Calculate accessability matrix for SF
|
16 |
+
|
17 |
# Show difference on the day
|
18 |
+
|
Reimagining_San_Francisco.png
ADDED
![]() |
UC Berkeley_logo.png
ADDED
![]() |
app_shinydashboards.R
ADDED
@@ -0,0 +1,1008 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
###############################################################################
|
2 |
+
# Shiny App: San Francisco Biodiversity Access Decision Support Tool
|
3 |
+
# Author: Diego Ellis Soto, et al.
|
4 |
+
# University of California Berkeley, ESPM
|
5 |
+
# California Academy of Sciences
|
6 |
+
###############################################################################
|
7 |
+
require(shinyjs)
|
8 |
+
library(shiny)
|
9 |
+
library(shinydashboard)
|
10 |
+
library(leaflet)
|
11 |
+
library(mapboxapi)
|
12 |
+
library(tidyverse)
|
13 |
+
library(tidycensus)
|
14 |
+
library(sf)
|
15 |
+
library(DT)
|
16 |
+
library(RColorBrewer)
|
17 |
+
library(terra)
|
18 |
+
library(data.table) # for fread
|
19 |
+
library(mapview) # for mapview objects
|
20 |
+
library(sjPlot) # for plotting lm model coefficients
|
21 |
+
library(sjlabelled) # optional if needed for sjPlot
|
22 |
+
library(bslib)
|
23 |
+
library(shinycssloaders)
|
24 |
+
|
25 |
+
source('R/setup.R') # Ensure this script loads necessary data objects
|
26 |
+
|
27 |
+
# Define your Mapbox token securely
|
28 |
+
mapbox_token <- "pk.eyJ1Ijoia3dhbGtlcnRjdSIsImEiOiJjbHc3NmI0cDMxYzhyMmt0OXBiYnltMjVtIn0.Thtu6WqIhOfin6AykskM2g"
|
29 |
+
|
30 |
+
# Global theme definition using a green-themed bootswatch
|
31 |
+
theme <- bs_theme(
|
32 |
+
bootswatch = "minty", # 'minty' is a light green-themed bootswatch
|
33 |
+
base_font = font_google("Roboto"),
|
34 |
+
heading_font = font_google("Roboto Slab"),
|
35 |
+
bg = "#f0fff0", # Honeydew background
|
36 |
+
fg = "#2e8b57" # SeaGreen foreground
|
37 |
+
)
|
38 |
+
|
39 |
+
# UI
|
40 |
+
ui <- dashboardPage(
|
41 |
+
skin = "green", # shinydashboard skin color
|
42 |
+
dashboardHeader(title = "SF Biodiversity Access Tool"),
|
43 |
+
dashboardSidebar(
|
44 |
+
sidebarMenu(
|
45 |
+
menuItem("Isochrone Explorer", tabName = "isochrone", icon = icon("map-marker-alt")),
|
46 |
+
menuItem("GBIF Summaries", tabName = "gbif", icon = icon("table")),
|
47 |
+
menuItem("Community Science", tabName = "community_science", icon = icon("users")),
|
48 |
+
menuItem("About", tabName = "about", icon = icon("info-circle"))
|
49 |
+
)
|
50 |
+
),
|
51 |
+
dashboardBody(
|
52 |
+
theme = theme, # Apply the custom theme
|
53 |
+
useShinyjs(), # Initialize shinyjs
|
54 |
+
# Loading message
|
55 |
+
div(id = "loading", style = "display:none; font-size: 20px; color: red;", "Calculating..."),
|
56 |
+
|
57 |
+
# Tab Items
|
58 |
+
tabItems(
|
59 |
+
# Isochrone Explorer Tab
|
60 |
+
tabItem(tabName = "isochrone",
|
61 |
+
fluidRow(
|
62 |
+
box(
|
63 |
+
title = "Controls", status = "success", solidHeader = TRUE, width = 4,
|
64 |
+
radioButtons(
|
65 |
+
"location_choice",
|
66 |
+
"Select Location Method:",
|
67 |
+
choices = c("Address (Geocode)" = "address",
|
68 |
+
"Click on Map" = "map_click"),
|
69 |
+
selected = "map_click"
|
70 |
+
),
|
71 |
+
|
72 |
+
conditionalPanel(
|
73 |
+
condition = "input.location_choice == 'address'",
|
74 |
+
mapboxGeocoderInput(
|
75 |
+
inputId = "geocoder",
|
76 |
+
placeholder = "Search for an address",
|
77 |
+
access_token = mapbox_token
|
78 |
+
)
|
79 |
+
),
|
80 |
+
|
81 |
+
checkboxGroupInput(
|
82 |
+
"transport_modes",
|
83 |
+
"Select Transportation Modes:",
|
84 |
+
choices = list("Driving" = "driving",
|
85 |
+
"Walking" = "walking",
|
86 |
+
"Cycling" = "cycling",
|
87 |
+
"Driving with Traffic"= "driving-traffic"),
|
88 |
+
selected = c("driving", "walking")
|
89 |
+
),
|
90 |
+
|
91 |
+
checkboxGroupInput(
|
92 |
+
"iso_times",
|
93 |
+
"Select Isochrone Times (minutes):",
|
94 |
+
choices = list("5" = 5, "10" = 10, "15" = 15),
|
95 |
+
selected = c(5, 10)
|
96 |
+
),
|
97 |
+
|
98 |
+
actionButton("generate_iso", "Generate Isochrones", icon = icon("play")),
|
99 |
+
actionButton("clear_map", "Clear", icon = icon("times"))
|
100 |
+
),
|
101 |
+
box(
|
102 |
+
title = "Map", status = "success", solidHeader = TRUE, width = 8,
|
103 |
+
leafletOutput("isoMap", height = 600)
|
104 |
+
)
|
105 |
+
),
|
106 |
+
fluidRow(
|
107 |
+
box(
|
108 |
+
title = "Biodiversity Access Score", status = "success", solidHeader = TRUE, width = 6,
|
109 |
+
uiOutput("bioScoreBox")
|
110 |
+
),
|
111 |
+
box(
|
112 |
+
title = "Closest Greenspace", status = "success", solidHeader = TRUE, width = 6,
|
113 |
+
uiOutput("closestGreenspaceUI")
|
114 |
+
)
|
115 |
+
),
|
116 |
+
fluidRow(
|
117 |
+
box(
|
118 |
+
title = "Summary Data", status = "success", solidHeader = TRUE, width = 12,
|
119 |
+
DTOutput("dataTable") %>% withSpinner(type = 8, color = "#28a745")
|
120 |
+
)
|
121 |
+
),
|
122 |
+
fluidRow(
|
123 |
+
box(
|
124 |
+
title = "Biodiversity & Socioeconomic Summary", status = "success", solidHeader = TRUE, width = 12,
|
125 |
+
plotOutput("bioSocPlot", height = "400px") %>% withSpinner(type = 8, color = "#28a745")
|
126 |
+
)
|
127 |
+
),
|
128 |
+
fluidRow(
|
129 |
+
box(
|
130 |
+
title = "GBIF Records by Institution", status = "success", solidHeader = TRUE, width = 12,
|
131 |
+
plotOutput("collectionPlot", height = "400px") %>% withSpinner(type = 8, color = "#28a745")
|
132 |
+
)
|
133 |
+
)
|
134 |
+
),
|
135 |
+
|
136 |
+
# GBIF Summaries Tab
|
137 |
+
tabItem(tabName = "gbif",
|
138 |
+
fluidRow(
|
139 |
+
box(
|
140 |
+
title = "Filters", status = "success", solidHeader = TRUE, width = 4,
|
141 |
+
selectInput(
|
142 |
+
"class_filter",
|
143 |
+
"Select a GBIF Class to Summarize:",
|
144 |
+
choices = c("All", sort(unique(sf_gbif$class))),
|
145 |
+
selected = "All"
|
146 |
+
),
|
147 |
+
selectInput(
|
148 |
+
"family_filter",
|
149 |
+
"Filter by Family (optional):",
|
150 |
+
choices = c("All", sort(unique(sf_gbif$family))),
|
151 |
+
selected = "All"
|
152 |
+
)
|
153 |
+
),
|
154 |
+
box(
|
155 |
+
title = "Data Summary", status = "success", solidHeader = TRUE, width = 8,
|
156 |
+
DTOutput("classTable")
|
157 |
+
)
|
158 |
+
),
|
159 |
+
fluidRow(
|
160 |
+
box(
|
161 |
+
title = "Observations vs. Species Richness", status = "success", solidHeader = TRUE, width = 12,
|
162 |
+
plotOutput("obsVsSpeciesPlot", height = "300px") %>% withSpinner(type = 8, color = "#28a745"),
|
163 |
+
p("This plot displays the relationship between the number of observations and the species richness. Use this visualization to understand data coverage and biodiversity trends.")
|
164 |
+
)
|
165 |
+
)
|
166 |
+
),
|
167 |
+
# Community Science Tab
|
168 |
+
tabItem(tabName = "community_science",
|
169 |
+
fluidRow(
|
170 |
+
box(
|
171 |
+
title = "Partner Community Organizations", status = "success", solidHeader = TRUE, width = 12,
|
172 |
+
leafletOutput("communityMap", height = 600)
|
173 |
+
)
|
174 |
+
),
|
175 |
+
fluidRow(
|
176 |
+
box(
|
177 |
+
title = "Community Organizations Data", status = "success", solidHeader = TRUE, width = 12,
|
178 |
+
DTOutput("communityTable") %>% withSpinner(type = 8, color = "#28a745")
|
179 |
+
)
|
180 |
+
)
|
181 |
+
),
|
182 |
+
|
183 |
+
# About Tab
|
184 |
+
tabItem(tabName = "about",
|
185 |
+
fluidRow(
|
186 |
+
box(
|
187 |
+
title = "App Summary", status = "success", solidHeader = TRUE, width = 12,
|
188 |
+
tags$b("App Summary (Fill out with RSF data working group):"),
|
189 |
+
p("
|
190 |
+
This application allows users to either click on a map or geocode an address
|
191 |
+
to generate travel-time isochrones across multiple transportation modes
|
192 |
+
(e.g., pedestrian, cycling, driving, driving during traffic).
|
193 |
+
It retrieves socio-economic data from precomputed Census variables, calculates NDVI,
|
194 |
+
and summarizes biodiversity records from GBIF. Users can explore information
|
195 |
+
related to biodiversity in urban environments, including greenspace coverage,
|
196 |
+
population estimates, and species diversity within each isochrone.
|
197 |
+
"),
|
198 |
+
|
199 |
+
tags$b("Created by:"),
|
200 |
+
p(strong("Diego Ellis Soto", "Carl Boettiger, Rebecca Johnson, Christopher J. Schell")),
|
201 |
+
|
202 |
+
p("Contact Information: ", strong("[email protected]"))
|
203 |
+
)
|
204 |
+
),
|
205 |
+
fluidRow(
|
206 |
+
box(
|
207 |
+
title = "Reimagining San Francisco", status = "success", solidHeader = TRUE, width = 12,
|
208 |
+
tags$b("Reimagining San Francisco (Fill out with CAS):"),
|
209 |
+
p("Reimagining San Francisco is an initiative aimed at integrating ecological, social,
|
210 |
+
and technological dimensions to shape a sustainable future for the Bay Area.
|
211 |
+
This collaboration unites diverse stakeholders to explore innovations in urban planning,
|
212 |
+
conservation, and community engagement. The Reimagining San Francisco Data Working Group has been tasked with identifying and integrating multiple sources of socio-ecological biodiversity information in a co-development framework.")
|
213 |
+
)
|
214 |
+
),
|
215 |
+
fluidRow(
|
216 |
+
box(
|
217 |
+
title = "Why Biodiversity Access Matters", status = "success", solidHeader = TRUE, width = 12,
|
218 |
+
p("Ensuring equitable access to biodiversity is essential for human well-being,
|
219 |
+
ecological resilience, and global policy decisions related to conservation.
|
220 |
+
Areas with higher biodiversity can support ecosystem services including pollinators, moderate climate extremes,
|
221 |
+
and provide cultural, recreational, and health benefits to local communities.
|
222 |
+
Recognizing that cities are particularly complex socio-ecological systems facing both legacies of sociocultural practices as well as current ongoing dynamic human activities and pressures.
|
223 |
+
Incorporating multiple facets of biodiversity metrics alongside variables employed by city planners, human geographers, and decision-makers into urban planning will allow a more integrative lens in creating a sustainable future for cities and their residents.")
|
224 |
+
)
|
225 |
+
),
|
226 |
+
fluidRow(
|
227 |
+
box(
|
228 |
+
title = "How We Calculate Biodiversity Access Percentile", status = "success", solidHeader = TRUE, width = 12,
|
229 |
+
p("Total unique species found within the user-generated isochrone.
|
230 |
+
We then compare that value to the distribution of unique species counts across all census block groups,
|
231 |
+
converting that comparison into a percentile ranking (Polish this, look at the 15 Minute city).
|
232 |
+
A higher percentile indicates greater biodiversity within the chosen area,
|
233 |
+
relative to other parts of the city or region.")
|
234 |
+
)
|
235 |
+
),
|
236 |
+
fluidRow(
|
237 |
+
box(
|
238 |
+
title = "Next Steps", status = "success", solidHeader = TRUE, width = 12,
|
239 |
+
tags$ul(
|
240 |
+
tags$li("Add impervious surface"),
|
241 |
+
tags$li("National walkability score"),
|
242 |
+
tags$li("Social vulnerability score"),
|
243 |
+
tags$li("NatureServe biodiversity maps"),
|
244 |
+
tags$li("Calculate cold-hotspots within aggregation of H6 bins instead of by census block group: Ask Carl"),
|
245 |
+
tags$li("Species range maps"),
|
246 |
+
tags$li("Add common name GBIF"),
|
247 |
+
tags$li("Partner orgs"),
|
248 |
+
tags$li("Optimize speed -> store variables -> H-ify the world?"),
|
249 |
+
tags$li("Brainstorm and co-develop the biodiversity access score"),
|
250 |
+
tags$li("For the GBIF summaries, add an annotated GBIF_sf with environmental variables so we can see landcover type association across the biodiversity within the isochrone.")
|
251 |
+
)
|
252 |
+
)
|
253 |
+
)
|
254 |
+
)
|
255 |
+
)
|
256 |
+
)
|
257 |
+
)
|
258 |
+
|
259 |
+
# ------------------------------------------------
|
260 |
+
# Server
|
261 |
+
# ------------------------------------------------
|
262 |
+
server <- function(input, output, session) {
|
263 |
+
|
264 |
+
chosen_point <- reactiveVal(NULL)
|
265 |
+
|
266 |
+
# ------------------------------------------------
|
267 |
+
# Leaflet Base + Hide Overlays
|
268 |
+
# ------------------------------------------------
|
269 |
+
output$isoMap <- renderLeaflet({
|
270 |
+
pal_cbg <- colorNumeric("YlOrRd", cbg_vect_sf$medincE)
|
271 |
+
|
272 |
+
pal_rich <- colorNumeric("YlOrRd", domain = cbg_vect_sf$unique_species)
|
273 |
+
# Color palette for data availability
|
274 |
+
pal_data <- colorNumeric("Blues", domain = cbg_vect_sf$n_observations)
|
275 |
+
|
276 |
+
leaflet() %>%
|
277 |
+
addTiles(group = "Street Map (Default)") %>%
|
278 |
+
addProviderTiles(providers$Esri.WorldImagery, group = "Satellite (ESRI)") %>%
|
279 |
+
addProviderTiles(providers$CartoDB.Positron, group = "CartoDB.Positron") %>%
|
280 |
+
|
281 |
+
addPolygons(
|
282 |
+
data = cbg_vect_sf,
|
283 |
+
group = "Income",
|
284 |
+
fillColor = ~pal_cbg(medincE),
|
285 |
+
fillOpacity = 0.6,
|
286 |
+
color = "white",
|
287 |
+
weight = 1,
|
288 |
+
label=~GEOID,
|
289 |
+
highlightOptions = highlightOptions(
|
290 |
+
weight = 5,
|
291 |
+
color = "blue",
|
292 |
+
fillOpacity = 0.5,
|
293 |
+
bringToFront = TRUE
|
294 |
+
),
|
295 |
+
labelOptions = labelOptions(
|
296 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
297 |
+
textsize = "12px",
|
298 |
+
direction = "auto"
|
299 |
+
)
|
300 |
+
) %>%
|
301 |
+
|
302 |
+
addPolygons(
|
303 |
+
data = osm_greenspace,
|
304 |
+
group = "Greenspace",
|
305 |
+
fillColor = "darkgreen",
|
306 |
+
fillOpacity = 0.3,
|
307 |
+
color = "green",
|
308 |
+
weight = 1,
|
309 |
+
label = ~name,
|
310 |
+
highlightOptions = highlightOptions(
|
311 |
+
weight = 5,
|
312 |
+
color = "blue",
|
313 |
+
fillOpacity = 0.5,
|
314 |
+
bringToFront = TRUE
|
315 |
+
),
|
316 |
+
labelOptions = labelOptions(
|
317 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
318 |
+
textsize = "12px",
|
319 |
+
direction = "auto",
|
320 |
+
noHide = FALSE # Labels appear on hover
|
321 |
+
)
|
322 |
+
) %>%
|
323 |
+
|
324 |
+
addPolygons(
|
325 |
+
data = biodiv_hotspots,
|
326 |
+
group = "Hotspots (KnowBR)",
|
327 |
+
fillColor = "firebrick",
|
328 |
+
fillOpacity = 0.2,
|
329 |
+
color = "firebrick",
|
330 |
+
weight = 2,
|
331 |
+
label = ~GEOID,
|
332 |
+
highlightOptions = highlightOptions(
|
333 |
+
weight = 5,
|
334 |
+
color = "blue",
|
335 |
+
fillOpacity = 0.5,
|
336 |
+
bringToFront = TRUE
|
337 |
+
),
|
338 |
+
labelOptions = labelOptions(
|
339 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
340 |
+
textsize = "12px",
|
341 |
+
direction = "auto"
|
342 |
+
)
|
343 |
+
) %>%
|
344 |
+
|
345 |
+
addPolygons(
|
346 |
+
data = biodiv_coldspots,
|
347 |
+
group = "Coldspots (KnowBR)",
|
348 |
+
fillColor = "navyblue",
|
349 |
+
fillOpacity = 0.2,
|
350 |
+
color = "navyblue",
|
351 |
+
weight = 2,
|
352 |
+
label = ~GEOID,
|
353 |
+
highlightOptions = highlightOptions(
|
354 |
+
weight = 5,
|
355 |
+
color = "blue",
|
356 |
+
fillOpacity = 0.5,
|
357 |
+
bringToFront = TRUE
|
358 |
+
),
|
359 |
+
labelOptions = labelOptions(
|
360 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
361 |
+
textsize = "12px",
|
362 |
+
direction = "auto"
|
363 |
+
)
|
364 |
+
) %>%
|
365 |
+
|
366 |
+
# Add Species Richness Layer
|
367 |
+
addPolygons(
|
368 |
+
data = cbg_vect_sf,
|
369 |
+
group = "Species Richness",
|
370 |
+
fillColor = ~pal_rich(unique_species),
|
371 |
+
fillOpacity = 0.6,
|
372 |
+
color = "white",
|
373 |
+
weight = 1,
|
374 |
+
label = ~unique_species,
|
375 |
+
popup = ~paste0(
|
376 |
+
"<strong>GEOID: </strong>", GEOID,
|
377 |
+
"<br><strong>Species Richness: </strong>", unique_species,
|
378 |
+
"<br><strong>Observations: </strong>", n_observations,
|
379 |
+
"<br><strong>Median Income: </strong>", median_inc,
|
380 |
+
"<br><strong>Mean NDVI: </strong>", ndvi_mean
|
381 |
+
)
|
382 |
+
) %>%
|
383 |
+
|
384 |
+
# Add Data Availability Layer
|
385 |
+
addPolygons(
|
386 |
+
data = cbg_vect_sf,
|
387 |
+
group = "Data Availability",
|
388 |
+
fillColor = ~pal_data(n_observations),
|
389 |
+
fillOpacity = 0.6,
|
390 |
+
color = "white",
|
391 |
+
weight = 1,
|
392 |
+
label = ~n_observations,
|
393 |
+
popup = ~paste0(
|
394 |
+
"<strong>GEOID: </strong>", GEOID,
|
395 |
+
"<br><strong>Observations: </strong>", n_observations,
|
396 |
+
"<br><strong>Species Richness: </strong>", unique_species,
|
397 |
+
"<br><strong>Median Income: </strong>", median_inc,
|
398 |
+
"<br><strong>Mean NDVI: </strong>", ndvi_mean
|
399 |
+
)
|
400 |
+
) %>%
|
401 |
+
|
402 |
+
setView(lng = -122.4194, lat = 37.7749, zoom = 12) %>%
|
403 |
+
addLayersControl(
|
404 |
+
baseGroups = c("Street Map (Default)", "Satellite (ESRI)", "CartoDB.Positron"),
|
405 |
+
overlayGroups = c("Income", "Greenspace",
|
406 |
+
"Hotspots (KnowBR)", "Coldspots (KnowBR)",
|
407 |
+
"Species Richness", "Data Availability",
|
408 |
+
"Isochrones", "NDVI Raster"),
|
409 |
+
options = layersControlOptions(collapsed = FALSE)
|
410 |
+
) %>%
|
411 |
+
hideGroup("Income") %>%
|
412 |
+
hideGroup("Greenspace") %>%
|
413 |
+
hideGroup("Hotspots (KnowBR)") %>%
|
414 |
+
hideGroup("Coldspots (KnowBR)") %>%
|
415 |
+
hideGroup("Species Richness") %>%
|
416 |
+
hideGroup("Data Availability")
|
417 |
+
})
|
418 |
+
|
419 |
+
|
420 |
+
# ------------------------------------------------
|
421 |
+
# Observe map clicks (location_choice = 'map_click')
|
422 |
+
# ------------------------------------------------
|
423 |
+
observeEvent(input$isoMap_click, {
|
424 |
+
req(input$location_choice == "map_click")
|
425 |
+
click <- input$isoMap_click
|
426 |
+
if (!is.null(click)) {
|
427 |
+
chosen_point(c(lon = click$lng, lat = click$lat))
|
428 |
+
|
429 |
+
# Provide feedback with coordinates
|
430 |
+
showNotification(
|
431 |
+
paste0("Map clicked at Longitude: ", round(click$lng, 5),
|
432 |
+
", Latitude: ", round(click$lat, 5)),
|
433 |
+
type = "message"
|
434 |
+
)
|
435 |
+
|
436 |
+
# Update the map with a marker
|
437 |
+
leafletProxy("isoMap") %>%
|
438 |
+
clearMarkers() %>%
|
439 |
+
addCircleMarkers(
|
440 |
+
lng = click$lng, lat = click$lat,
|
441 |
+
radius = 6, color = "firebrick",
|
442 |
+
label = "Map Click Location"
|
443 |
+
)
|
444 |
+
}
|
445 |
+
})
|
446 |
+
|
447 |
+
# ------------------------------------------------
|
448 |
+
# Observe geocoder input
|
449 |
+
# ------------------------------------------------
|
450 |
+
observeEvent(input$geocoder, {
|
451 |
+
req(input$location_choice == "address")
|
452 |
+
geocode_result <- input$geocoder
|
453 |
+
if (!is.null(geocode_result)) {
|
454 |
+
# Extract coordinates
|
455 |
+
xy <- geocoder_as_xy(geocode_result)
|
456 |
+
|
457 |
+
# Update the chosen_point reactive value
|
458 |
+
chosen_point(c(lon = xy[1], lat = xy[2]))
|
459 |
+
|
460 |
+
# Provide feedback with the geocoded address and coordinates
|
461 |
+
showNotification(
|
462 |
+
paste0("Address geocoded to Longitude: ", round(xy[1], 5),
|
463 |
+
", Latitude: ", round(xy[2], 5)),
|
464 |
+
type = "message"
|
465 |
+
)
|
466 |
+
|
467 |
+
# Update the map with a marker
|
468 |
+
leafletProxy("isoMap") %>%
|
469 |
+
clearMarkers() %>%
|
470 |
+
addCircleMarkers(
|
471 |
+
lng = xy[1], lat = xy[2],
|
472 |
+
radius = 6, color = "navyblue",
|
473 |
+
label = "Geocoded Address"
|
474 |
+
) %>%
|
475 |
+
flyTo(lng = xy[1], lat = xy[2], zoom = 13)
|
476 |
+
}
|
477 |
+
})
|
478 |
+
|
479 |
+
# ------------------------------------------------
|
480 |
+
# Observe clearing of map
|
481 |
+
# ------------------------------------------------
|
482 |
+
observeEvent(input$clear_map, {
|
483 |
+
# Reset the chosen point
|
484 |
+
chosen_point(NULL)
|
485 |
+
|
486 |
+
# Clear all markers and isochrones from the map, but keep other layers
|
487 |
+
leafletProxy("isoMap") %>%
|
488 |
+
clearMarkers() %>%
|
489 |
+
clearGroup("Isochrones") %>%
|
490 |
+
clearGroup("NDVI Raster")
|
491 |
+
|
492 |
+
# Provide feedback to the user
|
493 |
+
showNotification("Map cleared. You can select a new location.", type = "message")
|
494 |
+
})
|
495 |
+
|
496 |
+
# ------------------------------------------------
|
497 |
+
# Generate Isochrones
|
498 |
+
# ------------------------------------------------
|
499 |
+
isochrones_data <- eventReactive(input$generate_iso, {
|
500 |
+
|
501 |
+
leafletProxy("isoMap") %>%
|
502 |
+
clearGroup("Isochrones") %>%
|
503 |
+
clearGroup("NDVI Raster")
|
504 |
+
|
505 |
+
# Validate inputs
|
506 |
+
pt <- chosen_point()
|
507 |
+
if (is.null(pt)) {
|
508 |
+
showNotification("No location selected! Provide an address or click the map.", type = "error")
|
509 |
+
return(NULL)
|
510 |
+
}
|
511 |
+
if (length(input$transport_modes) == 0) {
|
512 |
+
showNotification("Select at least one transportation mode.", type = "error")
|
513 |
+
return(NULL)
|
514 |
+
}
|
515 |
+
if (length(input$iso_times) == 0) {
|
516 |
+
showNotification("Select at least one isochrone time.", type = "error")
|
517 |
+
return(NULL)
|
518 |
+
}
|
519 |
+
|
520 |
+
location_sf <- st_as_sf(
|
521 |
+
data.frame(lon = pt["lon"], lat = pt["lat"]),
|
522 |
+
coords = c("lon","lat"), crs = 4326
|
523 |
+
)
|
524 |
+
|
525 |
+
iso_list <- list()
|
526 |
+
for (mode in input$transport_modes) {
|
527 |
+
for (t in input$iso_times) {
|
528 |
+
iso <- tryCatch({
|
529 |
+
mb_isochrone(location_sf, time = as.numeric(t), profile = mode,
|
530 |
+
access_token = mapbox_token)
|
531 |
+
}, error = function(e) {
|
532 |
+
showNotification(paste("Isochrone error:", mode, t, e$message), type = "error")
|
533 |
+
NULL
|
534 |
+
})
|
535 |
+
if (!is.null(iso)) {
|
536 |
+
iso$mode <- mode
|
537 |
+
iso$time <- t
|
538 |
+
iso_list <- append(iso_list, list(iso))
|
539 |
+
}
|
540 |
+
}
|
541 |
+
}
|
542 |
+
if (length(iso_list) == 0) {
|
543 |
+
showNotification("No isochrones generated.", type = "warning")
|
544 |
+
return(NULL)
|
545 |
+
}
|
546 |
+
|
547 |
+
all_iso <- do.call(rbind, iso_list) %>% st_transform(4326)
|
548 |
+
all_iso
|
549 |
+
})
|
550 |
+
|
551 |
+
# ------------------------------------------------
|
552 |
+
# Plot Isochrones + NDVI
|
553 |
+
# ------------------------------------------------
|
554 |
+
observeEvent(isochrones_data(), {
|
555 |
+
iso_data <- isochrones_data()
|
556 |
+
req(iso_data)
|
557 |
+
|
558 |
+
iso_data$iso_group <- paste(iso_data$mode, iso_data$time, sep = "_")
|
559 |
+
pal <- colorRampPalette(brewer.pal(8, "Set2"))
|
560 |
+
cols <- pal(nrow(iso_data))
|
561 |
+
|
562 |
+
for (i in seq_len(nrow(iso_data))) {
|
563 |
+
poly_i <- iso_data[i, ]
|
564 |
+
leafletProxy("isoMap") %>%
|
565 |
+
addPolygons(
|
566 |
+
data = poly_i,
|
567 |
+
group = "Isochrones",
|
568 |
+
color = cols[i],
|
569 |
+
weight = 2,
|
570 |
+
fillOpacity = 0.4,
|
571 |
+
label = paste0(poly_i$mode, " - ", poly_i$time, " mins")
|
572 |
+
)
|
573 |
+
}
|
574 |
+
|
575 |
+
iso_union <- st_union(iso_data)
|
576 |
+
iso_union_vect <- vect(iso_union)
|
577 |
+
ndvi_crop <- crop(ndvi, iso_union_vect)
|
578 |
+
ndvi_mask <- mask(ndvi_crop, iso_union_vect)
|
579 |
+
ndvi_vals <- values(ndvi_mask)
|
580 |
+
ndvi_vals <- ndvi_vals[!is.na(ndvi_vals)]
|
581 |
+
|
582 |
+
if (length(ndvi_vals) > 0) {
|
583 |
+
ndvi_pal <- colorNumeric("YlGn", domain = range(ndvi_vals, na.rm = TRUE), na.color = "transparent")
|
584 |
+
|
585 |
+
leafletProxy("isoMap") %>%
|
586 |
+
addRasterImage(
|
587 |
+
x = ndvi_mask,
|
588 |
+
colors = ndvi_pal,
|
589 |
+
opacity = 0.7,
|
590 |
+
project = TRUE,
|
591 |
+
group = "NDVI Raster"
|
592 |
+
) %>%
|
593 |
+
addLegend(
|
594 |
+
position = "bottomright",
|
595 |
+
pal = ndvi_pal,
|
596 |
+
values = ndvi_vals,
|
597 |
+
title = "NDVI"
|
598 |
+
)
|
599 |
+
}
|
600 |
+
|
601 |
+
# Ensure other layers remain
|
602 |
+
leafletProxy("isoMap") %>%
|
603 |
+
addLayersControl(
|
604 |
+
baseGroups = c("Street Map (Default)", "Satellite (ESRI)", "CartoDB.Positron"),
|
605 |
+
overlayGroups = c("Income", "Greenspace",
|
606 |
+
"Hotspots (KnowBR)", "Coldspots (KnowBR)",
|
607 |
+
"Species Richness", "Data Availability",
|
608 |
+
"Isochrones", "NDVI Raster"),
|
609 |
+
options = layersControlOptions(collapsed = FALSE)
|
610 |
+
)
|
611 |
+
})
|
612 |
+
|
613 |
+
# ------------------------------------------------
|
614 |
+
# socio_data Reactive + Summaries
|
615 |
+
# ------------------------------------------------
|
616 |
+
socio_data <- reactive({
|
617 |
+
iso_data <- isochrones_data()
|
618 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
619 |
+
return(data.frame())
|
620 |
+
}
|
621 |
+
|
622 |
+
acs_wide <- cbg_vect_sf %>%
|
623 |
+
mutate(
|
624 |
+
population = popE,
|
625 |
+
med_income = medincE
|
626 |
+
)
|
627 |
+
|
628 |
+
hotspot_union <- st_union(biodiv_hotspots)
|
629 |
+
coldspot_union <- st_union(biodiv_coldspots)
|
630 |
+
|
631 |
+
results <- data.frame()
|
632 |
+
|
633 |
+
# Calculate distance to coldspot and hotspots
|
634 |
+
for (i in seq_len(nrow(iso_data))) {
|
635 |
+
poly_i <- iso_data[i, ]
|
636 |
+
|
637 |
+
dist_hot <- st_distance(poly_i, hotspot_union)
|
638 |
+
dist_cold <- st_distance(poly_i, coldspot_union)
|
639 |
+
dist_hot_km <- round(as.numeric(min(dist_hot)) / 1000, 3)
|
640 |
+
dist_cold_km <- round(as.numeric(min(dist_cold)) / 1000, 3)
|
641 |
+
|
642 |
+
inter_acs <- st_intersection(acs_wide, poly_i)
|
643 |
+
|
644 |
+
vect_acs_wide <- vect(acs_wide)
|
645 |
+
vect_poly_i <- vect(poly_i)
|
646 |
+
inter_acs <- intersect(vect_acs_wide, vect_poly_i)
|
647 |
+
inter_acs = st_as_sf(inter_acs)
|
648 |
+
|
649 |
+
pop_total <- 0
|
650 |
+
inc_str <- "N/A"
|
651 |
+
if (nrow(inter_acs) > 0) {
|
652 |
+
inter_acs$area <- st_area(inter_acs)
|
653 |
+
inter_acs$area_num <- as.numeric(inter_acs$area)
|
654 |
+
inter_acs$area_ratio <- inter_acs$area_num / as.numeric(st_area(inter_acs))
|
655 |
+
inter_acs$weighted_pop <- inter_acs$population * inter_acs$area_ratio
|
656 |
+
|
657 |
+
pop_total <- round(sum(inter_acs$weighted_pop, na.rm = TRUE))
|
658 |
+
|
659 |
+
w_income <- sum(inter_acs$med_income * inter_acs$area_num, na.rm = TRUE) /
|
660 |
+
sum(inter_acs$area_num, na.rm = TRUE)
|
661 |
+
if (!is.na(w_income) && w_income > 0) {
|
662 |
+
inc_str <- paste0("$", formatC(round(w_income, 2), format = "f", big.mark = ","))
|
663 |
+
}
|
664 |
+
}
|
665 |
+
|
666 |
+
# Intersection with greenspace
|
667 |
+
vec_osm_greenspace <- vect(osm_greenspace)
|
668 |
+
inter_gs <- intersect(vec_osm_greenspace, vect_poly_i)
|
669 |
+
inter_gs = st_as_sf(inter_gs)
|
670 |
+
|
671 |
+
gs_area_m2 <- 0
|
672 |
+
if (nrow(inter_gs) > 0) {
|
673 |
+
gs_area_m2 <- sum(st_area(inter_gs))
|
674 |
+
}
|
675 |
+
iso_area_m2 <- as.numeric(st_area(poly_i))
|
676 |
+
gs_area_m2 <- as.numeric(gs_area_m2)
|
677 |
+
gs_percent <- ifelse(iso_area_m2 > 0, 100 * gs_area_m2 / iso_area_m2, 0)
|
678 |
+
|
679 |
+
# NDVI Calculation
|
680 |
+
poly_vect <- vect(poly_i)
|
681 |
+
ndvi_crop <- crop(ndvi, poly_vect)
|
682 |
+
ndvi_mask <- mask(ndvi_crop, poly_vect)
|
683 |
+
ndvi_vals <- values(ndvi_mask)
|
684 |
+
ndvi_vals <- ndvi_vals[!is.na(ndvi_vals)]
|
685 |
+
mean_ndvi <- ifelse(length(ndvi_vals) > 0, round(mean(ndvi_vals, na.rm=TRUE), 3), NA)
|
686 |
+
|
687 |
+
# Intersection with GBIF data
|
688 |
+
inter_gbif <- intersect(vect_gbif, vect_poly_i)
|
689 |
+
inter_gbif <- st_as_sf(inter_gbif)
|
690 |
+
|
691 |
+
inter_gbif_acs <- sf_gbif %>%
|
692 |
+
mutate(
|
693 |
+
income = medincE,
|
694 |
+
ndvi = ndvi_sentinel
|
695 |
+
)
|
696 |
+
|
697 |
+
if (nrow(inter_gbif) > 0) {
|
698 |
+
inter_gbif_acs <- inter_gbif_acs[inter_gbif_acs$GEOID %in% inter_gbif$GEOID, ]
|
699 |
+
}
|
700 |
+
|
701 |
+
n_records <- nrow(inter_gbif)
|
702 |
+
n_species <- length(unique(inter_gbif$species))
|
703 |
+
|
704 |
+
n_birds <- length(unique(inter_gbif$species[inter_gbif$class == "Aves"]))
|
705 |
+
n_mammals <- length(unique(inter_gbif$species[inter_gbif$class == "Mammalia"]))
|
706 |
+
n_plants <- length(unique(inter_gbif$species[inter_gbif$class %in%
|
707 |
+
c("Magnoliopsida","Liliopsida","Pinopsida","Polypodiopsida",
|
708 |
+
"Equisetopsida","Bryopsida","Marchantiopsida") ]))
|
709 |
+
|
710 |
+
iso_area_km2 <- round(iso_area_m2 / 1e6, 3)
|
711 |
+
|
712 |
+
row_i <- data.frame(
|
713 |
+
Mode = tools::toTitleCase(poly_i$mode),
|
714 |
+
Time = poly_i$time,
|
715 |
+
IsochroneArea_km2 = iso_area_km2,
|
716 |
+
DistToHotspot_km = dist_hot_km,
|
717 |
+
DistToColdspot_km = dist_cold_km,
|
718 |
+
EstimatedPopulation = pop_total,
|
719 |
+
MedianIncome = inc_str,
|
720 |
+
MeanNDVI = ifelse(!is.na(mean_ndvi), mean_ndvi, "N/A"),
|
721 |
+
GBIF_Records = n_records,
|
722 |
+
GBIF_Species = n_species,
|
723 |
+
Bird_Species = n_birds,
|
724 |
+
Mammal_Species = n_mammals,
|
725 |
+
Plant_Species = n_plants,
|
726 |
+
Greenspace_m2 = round(gs_area_m2, 2),
|
727 |
+
Greenspace_percent = round(gs_percent, 2),
|
728 |
+
stringsAsFactors = FALSE
|
729 |
+
)
|
730 |
+
results <- rbind(results, row_i)
|
731 |
+
}
|
732 |
+
|
733 |
+
iso_union <- st_union(iso_data)
|
734 |
+
vect_iso <- vect(iso_union)
|
735 |
+
inter_all_gbif <- intersect(vect_gbif, vect_iso)
|
736 |
+
inter_all_gbif <- st_as_sf(inter_all_gbif)
|
737 |
+
|
738 |
+
union_n_species <- length(unique(inter_all_gbif$species))
|
739 |
+
rank_percentile <- round(100 * ecdf(cbg_vect_sf$unique_species)(union_n_species), 1)
|
740 |
+
attr(results, "bio_percentile") <- rank_percentile
|
741 |
+
|
742 |
+
# Closest Greenspace from ANY part of the isochrone
|
743 |
+
dist_mat <- st_distance(iso_union, osm_greenspace) # 1 x N matrix
|
744 |
+
if (length(dist_mat) > 0) {
|
745 |
+
min_dist <- min(dist_mat)
|
746 |
+
min_idx <- which.min(dist_mat)
|
747 |
+
gs_name <- osm_greenspace$name[min_idx]
|
748 |
+
attr(results, "closest_greenspace") <- gs_name
|
749 |
+
} else {
|
750 |
+
attr(results, "closest_greenspace") <- "None"
|
751 |
+
}
|
752 |
+
|
753 |
+
results
|
754 |
+
})
|
755 |
+
|
756 |
+
# ------------------------------------------------
|
757 |
+
# Render main summary table
|
758 |
+
# ------------------------------------------------
|
759 |
+
output$dataTable <- renderDT({
|
760 |
+
df <- socio_data()
|
761 |
+
if (nrow(df) == 0) {
|
762 |
+
return(DT::datatable(data.frame("Message" = "No isochrones generated yet.")))
|
763 |
+
}
|
764 |
+
DT::datatable(
|
765 |
+
df,
|
766 |
+
colnames = c(
|
767 |
+
"Mode" = "Mode",
|
768 |
+
"Time (min)" = "Time",
|
769 |
+
"Area (kmΒ²)" = "IsochroneArea_km2",
|
770 |
+
"Dist. Hotspot (km)" = "DistToHotspot_km",
|
771 |
+
"Dist. Coldspot (km)" = "DistToColdspot_km",
|
772 |
+
"Population" = "EstimatedPopulation",
|
773 |
+
"Median Income" = "MedianIncome",
|
774 |
+
"Mean NDVI" = "MeanNDVI",
|
775 |
+
"GBIF Records" = "GBIF_Records",
|
776 |
+
"Unique Species" = "GBIF_Species",
|
777 |
+
"Bird Species" = "Bird_Species",
|
778 |
+
"Mammal Species" = "Mammal_Species",
|
779 |
+
"Plant Species" = "Plant_Species",
|
780 |
+
"Greenspace (mΒ²)" = "Greenspace_m2",
|
781 |
+
"Greenspace (%)" = "Greenspace_percent"
|
782 |
+
),
|
783 |
+
options = list(pageLength = 10, autoWidth = TRUE),
|
784 |
+
rownames = FALSE
|
785 |
+
)
|
786 |
+
})
|
787 |
+
|
788 |
+
# ------------------------------------------------
|
789 |
+
# Biodiversity Access Score + Closest Greenspace
|
790 |
+
# ------------------------------------------------
|
791 |
+
output$bioScoreBox <- renderUI({
|
792 |
+
df <- socio_data()
|
793 |
+
if (nrow(df) == 0) return(NULL)
|
794 |
+
|
795 |
+
percentile <- attr(df, "bio_percentile")
|
796 |
+
if (is.null(percentile)) percentile <- "N/A"
|
797 |
+
else percentile <- paste0(percentile, "th Percentile")
|
798 |
+
|
799 |
+
wellPanel(
|
800 |
+
HTML(paste0("<h2>Biodiversity Access Score: ", percentile, "</h2>"))
|
801 |
+
)
|
802 |
+
})
|
803 |
+
|
804 |
+
output$closestGreenspaceUI <- renderUI({
|
805 |
+
df <- socio_data()
|
806 |
+
if (nrow(df) == 0) return(NULL)
|
807 |
+
gs_name <- attr(df, "closest_greenspace")
|
808 |
+
if (is.null(gs_name)) gs_name <- "None"
|
809 |
+
|
810 |
+
tagList(
|
811 |
+
strong("Closest Greenspace (from any part of the Isochrone):"),
|
812 |
+
p(gs_name)
|
813 |
+
)
|
814 |
+
})
|
815 |
+
|
816 |
+
# ------------------------------------------------
|
817 |
+
# Secondary table: user-selected CLASS & FAMILY
|
818 |
+
# ------------------------------------------------
|
819 |
+
output$classTable <- renderDT({
|
820 |
+
iso_data <- isochrones_data()
|
821 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
822 |
+
return(DT::datatable(data.frame("Message" = "No isochrones generated yet.")))
|
823 |
+
}
|
824 |
+
|
825 |
+
iso_union <- st_union(iso_data)
|
826 |
+
vect_iso <- vect(iso_union)
|
827 |
+
inter_gbif <- intersect(vect_gbif, vect_iso)
|
828 |
+
inter_gbif = st_as_sf(inter_gbif)
|
829 |
+
|
830 |
+
inter_gbif_acs = sf_gbif %>%
|
831 |
+
mutate(
|
832 |
+
income = medincE,
|
833 |
+
ndvi = ndvi_sentinel
|
834 |
+
)
|
835 |
+
|
836 |
+
if (input$class_filter != "All") {
|
837 |
+
inter_gbif_acs <- inter_gbif_acs[ inter_gbif_acs$class == input$class_filter, ]
|
838 |
+
}
|
839 |
+
if (input$family_filter != "All") {
|
840 |
+
inter_gbif_acs <- inter_gbif_acs[ inter_gbif_acs$family == input$family_filter, ]
|
841 |
+
}
|
842 |
+
|
843 |
+
if (nrow(inter_gbif_acs) == 0) {
|
844 |
+
return(DT::datatable(data.frame("Message" = "No records for that combination in the isochrone.")))
|
845 |
+
}
|
846 |
+
|
847 |
+
species_counts <- inter_gbif_acs %>%
|
848 |
+
st_drop_geometry() %>%
|
849 |
+
group_by(species) %>%
|
850 |
+
summarize(
|
851 |
+
n_records = n(),
|
852 |
+
mean_income = round(mean(income, na.rm=TRUE), 2),
|
853 |
+
mean_ndvi = round(mean(ndvi, na.rm=TRUE), 3),
|
854 |
+
.groups = "drop"
|
855 |
+
) %>%
|
856 |
+
arrange(desc(n_records))
|
857 |
+
|
858 |
+
DT::datatable(
|
859 |
+
species_counts,
|
860 |
+
colnames = c("Species", "Number of Records", "Mean Income", "Mean NDVI"),
|
861 |
+
options = list(pageLength = 10),
|
862 |
+
rownames = FALSE
|
863 |
+
)
|
864 |
+
})
|
865 |
+
|
866 |
+
# ------------------------------------------------
|
867 |
+
# Ggplot: Biodiversity & Socioeconomic Summary
|
868 |
+
# ------------------------------------------------
|
869 |
+
output$bioSocPlot <- renderPlot({
|
870 |
+
df <- socio_data()
|
871 |
+
if (nrow(df) == 0) return(NULL)
|
872 |
+
|
873 |
+
df_plot <- df %>%
|
874 |
+
mutate(IsoLabel = paste0(Mode, "-", Time, "min"))
|
875 |
+
|
876 |
+
ggplot(df_plot, aes(x = IsoLabel)) +
|
877 |
+
geom_col(aes(y = GBIF_Species), fill = "steelblue", alpha = 0.7) +
|
878 |
+
geom_line(aes(y = EstimatedPopulation / 1000, group = 1), color = "red", size = 1) +
|
879 |
+
geom_point(aes(y = EstimatedPopulation / 1000), color = "red", size = 3) +
|
880 |
+
labs(
|
881 |
+
x = "Isochrone (Mode-Time)",
|
882 |
+
y = "Unique Species (Blue) | Population (Red) (Thousands)",
|
883 |
+
title = "Biodiversity & Socioeconomic Summary"
|
884 |
+
) +
|
885 |
+
theme_minimal(base_size = 14) +
|
886 |
+
theme(
|
887 |
+
axis.text.x = element_text(angle = 45, hjust = 1, size = 12),
|
888 |
+
axis.text.y = element_text(size = 12),
|
889 |
+
axis.title.x = element_text(size = 14),
|
890 |
+
axis.title.y = element_text(size = 14),
|
891 |
+
plot.title = element_text(hjust = 0.5, size = 16, face = "bold")
|
892 |
+
)
|
893 |
+
})
|
894 |
+
|
895 |
+
# ------------------------------------------------
|
896 |
+
# Bar plot: GBIF records by institutionCode
|
897 |
+
# ------------------------------------------------
|
898 |
+
output$collectionPlot <- renderPlot({
|
899 |
+
iso_data <- isochrones_data()
|
900 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
901 |
+
plot.new()
|
902 |
+
title("No GBIF records found in this isochrone.")
|
903 |
+
return(NULL)
|
904 |
+
}
|
905 |
+
|
906 |
+
iso_union <- st_union(iso_data)
|
907 |
+
vect_iso <- vect(iso_union)
|
908 |
+
inter_gbif <- intersect(vect_gbif, vect_iso)
|
909 |
+
inter_gbif = st_as_sf(inter_gbif)
|
910 |
+
|
911 |
+
if (nrow(inter_gbif) == 0) {
|
912 |
+
plot.new()
|
913 |
+
title("No GBIF records found in this isochrone.")
|
914 |
+
return(NULL)
|
915 |
+
}
|
916 |
+
|
917 |
+
df_code <- inter_gbif %>%
|
918 |
+
st_drop_geometry() %>%
|
919 |
+
group_by(institutionCode) %>%
|
920 |
+
summarize(count = n(), .groups = "drop") %>%
|
921 |
+
arrange(desc(count)) %>%
|
922 |
+
mutate(truncatedCode = substr(institutionCode, 1, 5)) # Shorter version of the names
|
923 |
+
|
924 |
+
ggplot(df_code, aes(x = reorder(truncatedCode, -count), y = count)) +
|
925 |
+
geom_bar(stat = "identity", fill = "darkorange", alpha = 0.7) +
|
926 |
+
labs(
|
927 |
+
x = "Institution Code (Truncated)",
|
928 |
+
y = "Number of Records",
|
929 |
+
title = "GBIF Records by Institution Code (Isochrone Union)"
|
930 |
+
) +
|
931 |
+
theme_minimal(base_size = 14) +
|
932 |
+
theme(
|
933 |
+
axis.text.x = element_text(angle = 45, hjust = 1, size = 12),
|
934 |
+
axis.text.y = element_text(size = 12),
|
935 |
+
axis.title.x = element_text(size = 14),
|
936 |
+
axis.title.y = element_text(size = 14),
|
937 |
+
plot.title = element_text(hjust = 0.5, size = 16, face = "bold")
|
938 |
+
)
|
939 |
+
})
|
940 |
+
|
941 |
+
# ------------------------------------------------
|
942 |
+
# Additional Plot: n_observations vs n_species
|
943 |
+
# ------------------------------------------------
|
944 |
+
|
945 |
+
# Make it reactive: obsVsSpeciesPlot updates dynamically based on user-selected class_filter or family_filter.
|
946 |
+
|
947 |
+
filtered_data <- reactive({
|
948 |
+
data <- cbg_vect_sf
|
949 |
+
if (input$class_filter != "All") {
|
950 |
+
data <- data[data$class == input$class_filter, ]
|
951 |
+
}
|
952 |
+
if (input$family_filter != "All") {
|
953 |
+
data <- data[data$family == input$family_filter, ]
|
954 |
+
}
|
955 |
+
data
|
956 |
+
})
|
957 |
+
|
958 |
+
output$obsVsSpeciesPlot <- renderPlot({
|
959 |
+
data <- filtered_data()
|
960 |
+
if (nrow(data) == 0) {
|
961 |
+
plot.new()
|
962 |
+
title("No data available for selected filters.")
|
963 |
+
return(NULL)
|
964 |
+
}
|
965 |
+
|
966 |
+
ggplot(data, aes(x = log(n_observations + 1), y = log(unique_species + 1))) +
|
967 |
+
geom_point(color = "blue", alpha = 0.6) +
|
968 |
+
labs(
|
969 |
+
x = "Log(Number of Observations + 1)",
|
970 |
+
y = "Log(Species Richness + 1)",
|
971 |
+
title = "Data Availability vs. Species Richness"
|
972 |
+
) +
|
973 |
+
theme_minimal(base_size = 14) +
|
974 |
+
theme(
|
975 |
+
axis.text.x = element_text(size = 12),
|
976 |
+
axis.text.y = element_text(size = 12),
|
977 |
+
axis.title.x = element_text(size = 14),
|
978 |
+
axis.title.y = element_text(size = 14),
|
979 |
+
plot.title = element_text(hjust = 0.5, size = 16, face = "bold")
|
980 |
+
)
|
981 |
+
})
|
982 |
+
|
983 |
+
# ------------------------------------------------
|
984 |
+
# [Optional: Linear Model Plot (Commented Out)]
|
985 |
+
# ------------------------------------------------
|
986 |
+
# Uncomment and adjust if needed
|
987 |
+
# output$lmCoefficientsPlot <- renderPlot({
|
988 |
+
# df_lm <- cbg_vect_sf %>%
|
989 |
+
# filter(!is.na(n_observations),
|
990 |
+
# !is.na(unique_species),
|
991 |
+
# !is.na(median_inc),
|
992 |
+
# !is.na(ndvi_mean))
|
993 |
+
#
|
994 |
+
# if (nrow(df_lm) < 5) {
|
995 |
+
# plot.new()
|
996 |
+
# title("Not enough data for linear model.")
|
997 |
+
# return(NULL)
|
998 |
+
# }
|
999 |
+
#
|
1000 |
+
# fit <- lm(unique_species ~ n_observations + median_inc + ndvi_mean, data = df_lm)
|
1001 |
+
#
|
1002 |
+
# p <- plot_model(fit, show.values = TRUE, value.offset = .3, title = "LM Coefficients: n_species ~ n_observations + median_inc + ndvi_mean")
|
1003 |
+
# print(p)
|
1004 |
+
# })
|
1005 |
+
}
|
1006 |
+
|
1007 |
+
# Run the Shiny app
|
1008 |
+
shinyApp(ui, server)
|
rsconnect/shinyapps.io/diego-ellis-soto/RSF_Biodiversity_Access.dcf
CHANGED
@@ -1,13 +1,12 @@
|
|
1 |
name: RSF_Biodiversity_Access
|
2 |
-
title:
|
3 |
username: diego-ellis-soto
|
4 |
account: diego-ellis-soto
|
5 |
server: shinyapps.io
|
6 |
hostUrl: https://api.shinyapps.io/v1
|
7 |
appId: 13693040
|
8 |
-
bundleId:
|
9 |
url: https://diego-ellis-soto.shinyapps.io/RSF_Biodiversity_Access/
|
10 |
version: 1
|
11 |
asMultiple: FALSE
|
12 |
asStatic: FALSE
|
13 |
-
ignoredFiles: app_old.R
|
|
|
1 |
name: RSF_Biodiversity_Access
|
2 |
+
title: SF_biodiv_access_shiny
|
3 |
username: diego-ellis-soto
|
4 |
account: diego-ellis-soto
|
5 |
server: shinyapps.io
|
6 |
hostUrl: https://api.shinyapps.io/v1
|
7 |
appId: 13693040
|
8 |
+
bundleId: 9633861
|
9 |
url: https://diego-ellis-soto.shinyapps.io/RSF_Biodiversity_Access/
|
10 |
version: 1
|
11 |
asMultiple: FALSE
|
12 |
asStatic: FALSE
|
|