-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathvis-detection_bubble_plot.r
325 lines (295 loc) · 11 KB
/
vis-detection_bubble_plot.r
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
#'
#' Make bubble plots showing the number of fish detected across a defined set
#' of receiver locations.
#'
#' @inheritParams summarize_detections
#' @param map An optional sp, sf, or terra::SpatVect spatial object that
#' can by plotted with using `plot` to be included as the background for the
#' plot. If NULL, then the example Great Lakes polygon
#' object (`data(great_lakes_polygon)`) will be used. Map CRS must be in
#' EPSG:4326 or conversion will be attempted.
#'
#' @param out_file An optional character string with the name (including
#' extension) of output file created. File extension will determine type of
#' file written. For example, `"BubblePlot.png"` will write a png
#' file to the working directory. If `NULL` (default) then the plot will
#' be printed to the default plot device. Supported extensions:
#' png, jpeg, bmp, and tiff.
#'
#' @param background_xlim A two-element numeric vector that defines minimum and
#' maximum extents of the viewable plot area along the x-axis (i.e.,
#' latitude).
#'
#' @param background_ylim A two-element numeric vector that defines minimum and
#' maximum extents of the viewable plot area along the y-axis (i.e.,
#' longitude).
#'
#' @param symbol_radius Radius of each "bubble" on the plot in units of percent
#' of x-axis scale. Default value = 1 (i.e., 1 percent of x-axis).
#'
#' @param col_grad A two-element character vector indicating the start and end
#' colors of the gradient scale used to color-code "bubbles".
#'
#' @param scale_loc An optional 4-element numeric vector, to be passed to
#' plotrix::color.legend, indicating the plotting location of the legend in
#' the same units as `map`. Elements in the vector are the lower left
#' and upper right coordinates of the rectangle of colors
#' (i.e., c(xleft, ybottom, xright, ytop)). If `scale_loc` = NULL
#' (default), the legend is plotted along the left edge of the plot.
#'
#' @details Data are summarized using [summarize_detections].
#'
#' @details If `receiver_locs` is specified (not NULL) then the plot will
#' show all receivers in `receiver_locs` including any that detected
#' none of the transmitters in `det`. Although this is helpful to view
#' locations where fish were *not* detected, the user will usually want
#' to take care to include only receivers that were in the water during the
#' period of interest. If you are using a glatos receiver locations file to
#' specify location for plotting, you will likely want to filter the receiver
#' data by depoyment and receovery dates to exclude deployments that occured
#' outside of the period of interest.
#'
#' @details "col_grad" is used in a call to [colorRampPalette][colorRamp],
#' which will accept a vector containing any two colors return by
#' [colors][grDevices::colors] as character strings.
#'
#' @return A data frame produced by
#' `glatos::summarize_detections(det, location_col = location_col,
#' receiver_locs = receiver_locs, summ_type = "location")`
#'
#' @return If not out_file is specified, then an image is printed to the
#' default plot device. If out_file is specified, then an image of
#' specified type is written to `out_file`.
#'
#' @seealso [summarize_detections()]
#'
#' @author T. R. Binder, edited by A. Dini
#'
#' @examples
#'
#' # get path to example detection file
#' det_file <- system.file("extdata", "walleye_detections.csv",
#' package = "glatos"
#' )
#' det <- read_glatos_detections(det_file)
#'
#' # call with defaults
#' detection_bubble_plot(det, map = great_lakes_polygon)
#'
#' # change symbol size and color
#' detection_bubble_plot(det, symbol_radius = 2, col_grad = c("grey90", "grey10"))
#'
#' # Add all receivers
#'
#' # get path to example receiver file
#' rec_file <- system.file("extdata", "sample_receivers.csv",
#' package = "glatos"
#' )
#' rec <- read_glatos_receivers(rec_file)
#'
#' detection_bubble_plot(det, receiver_locs = rec)
#'
#'
#' #' #Subset receivers to include on receivers that were deployed during the
#' #' detection interval.
#'
#' # get path to example receiver file
#' rec_file <- system.file("extdata", "sample_receivers.csv",
#' package = "glatos"
#' )
#' rec <- read_glatos_receivers(rec_file)
#'
#' first <- min(det$detection_timestamp_utc) # time of first detection
#' last <- max(det$detection_timestamp_utc) # time of last detection
#'
#' # Subset receiver deployments oustide the detection period.
#' # !is.na(rec$recover_date_time) eliminates receivers that have been
#' # deployed but not yet recovered.
#' plot_rec <- rec[rec$deploy_date_time < last &
#' rec$recover_date_time > first &
#' !is.na(rec$recover_date_time), ]
#'
#' detection_bubble_plot(det, receiver_locs = plot_rec)
#'
#' @export
detection_bubble_plot <- function(det, location_col = "glatos_array",
receiver_locs = NULL,
map = NULL,
out_file = NULL,
background_ylim = c(41.3, 49.0),
background_xlim = c(-92.45, -75.87),
symbol_radius = 1,
col_grad = c("white", "red"),
scale_loc = NULL) {
# Declare global variables for NSE & R CMD check
great_lakes_polygon <- NULL
# Check that the specified columns appear in the det data frame
missingCols <- setdiff(
c(
"animal_id", "detection_timestamp_utc",
"deploy_lat", "deploy_long", location_col
),
names(det)
)
if (length(missingCols) > 0) {
stop(
paste0(
"det is missing the following ",
"column(s):\n", paste0(" '", missingCols, "'",
collapse = "\n"
)
),
call. = FALSE
)
}
# convert sp to sf
if (!is.null(map) & inherits(map, "Spatial")) {
map <- sf::st_as_sf(map)
message("Converted sp object to sf")
}
# convert terra::SpatVector to sf
if (!is.null(map) & inherits(map, "SpatVector")) {
map <- sf::st_as_sf(map)
message("Converted terra SpatVector to sf")
}
# Check CRS == 4326 and attempt conversion if not
if (!is.null(map) & sf::st_crs(map)$input != "EPSG:4326") {
map <- sf::st_transform(map, 4326)
message("Converted map to EPSG:4326")
}
if (is.null(map)) {
utils::data("great_lakes_polygon", envir = environment())
map <- great_lakes_polygon
rm(great_lakes_polygon)
} # example in glatos package (sf object)
# Check that timestamp is of class 'POSIXct'
if (!("POSIXct" %in% class(det$detection_timestamp_utc))) {
stop(paste0("Column detection_timestamp_utc in det data frame must be of
class 'POSIXct'."),
call. = FALSE
)
}
# Call glatos::detection_summary to create summary data.
det_summ <- glatos::summarize_detections(det,
location_col = location_col,
receiver_locs = receiver_locs,
summ_type = "location"
)
# Re-order the summaries so that sites with detections plot on top of sites
# without. Makes it easier to see detected locations when they are close
# enough together that the bubbles overlap
det_summ <- det_summ[order(det_summ$num_fish), ]
# Create labs with degrees symbol for plots
xlabs <- round(seq(
from = background_xlim[1], to = background_xlim[2],
length.out = 5
), 2)
ylabs <- round(seq(
from = background_ylim[1], to = background_ylim[2],
length.out = 5
), 2)
# Define the color palette used to color-code the bubbles
color <- c(colorRampPalette(col_grad)(101))
# Calculate great circle distance in meters of x and y limits.
# needed to determine aspect ratio of the output
linear_x <- geodist::geodist_vec(
x1 = background_xlim[1], y1 = background_ylim[1],
x2 = background_xlim[2], y2 = background_ylim[1], measure = "haversine"
)
linear_y <- geodist::geodist_vec(
x1 = background_xlim[1], y1 = background_ylim[1],
x2 = background_xlim[1], y2 = background_ylim[2], measure = "haversine"
)
# aspect ratio of image
figRatio <- linear_y / linear_x
# get file extension
file_type <- ifelse(is.null(out_file), NA, tools::file_ext(out_file))
# check file extension is supported
ext_supp <- c(NA, "png", "jpeg", "png", "bmp", "tiff")
if (!(tolower(file_type) %in% ext_supp)) {
stop(paste0("Image type '", file_type, "' is not supported."),
call. = FALSE
)
}
if (!is.na(file_type) & tolower(file_type) == "png") {
png(out_file, height = 1000 * figRatio, width = 1000, pointsize = 28)
}
if (!is.na(file_type) & tolower(file_type) == "jpeg") {
jpeg(out_file, height = 1000 * figRatio, width = 1000, pointsize = 28)
}
if (!is.na(file_type) & tolower(file_type) == "bmp") {
bmp(out_file, height = 1000 * figRatio, width = 1000, pointsize = 28)
}
if (!is.na(file_type) & tolower(file_type) == "tiff") {
tiff(out_file, height = 1000 * figRatio, width = 1000, pointsize = 28)
}
if (is.null(out_file)) {
dev.new(noRStudioGD = TRUE, height = 7 * figRatio, width = 7)
}
# Set margins
par(mar = c(1, 0, 0, 2), oma = c(3, 5, 1, 0))
# Plot background image
plot(sf::st_geometry(map),
xlim = background_xlim, ylim = background_ylim, axes = T,
xaxs = "i", lwd = 1.5, xaxt = "n", yaxt = "n", col = "White",
bg = "WhiteSmoke"
)
# Plot the bubbles
symbols(det_summ$mean_lon, det_summ$mean_lat,
circles = rep(
(background_xlim[2] -
background_xlim[1]) * symbol_radius / 100,
length(det_summ$mean_lon)
),
add = T, inches = FALSE,
bg = color[round(det_summ$num_fish
/ max(det_summ$num_fish) * 100, 0) + 1],
fg = "black", lwd = 3
)
# Add 'X' to bubbles with no detections
if (any(det_summ$num_fish == 0)) {
with(
det_summ[det_summ$num_fish == 0, ],
text(mean_lon, mean_lat,
"X",
cex = 0.6 * symbol_radius
)
)
}
if (is.null(scale_loc)) {
# Calculate the location to plot the color scale
scale_loc <- c(
par("usr")[1] + (par("usr")[2] - par("usr")[1]) * 0.02,
par("usr")[3] + ((par("usr")[4] - par("usr")[3]) * 0.25),
par("usr")[1] + ((par("usr")[2] - par("usr")[1]) * 0.03),
par("usr")[4] - ((par("usr")[4] - par("usr")[3]) * 0.25)
)
}
# Add color legend
# explore options for doing this without an extra plotrix package https://stackoverflow.com/questions/13355176/gradient-legend-in-base
plotrix::color.legend(scale_loc[1], scale_loc[2], scale_loc[3], scale_loc[4],
paste0(" ", round(
seq(from = 1, to = max(det_summ$num_fish), length.out = 6),
0
)), color,
gradient = "y", family = "sans", cex = 0.75, align = "rb"
)
# Add x-axis and title
axis(1, at = xlabs, labels = paste0(format(xlabs, 4), intToUtf8(176)), cex.axis = 1)
mtext("Longitude", side = 1, line = 2.5, cex = 1)
# Add y-axis and title
axis(2,
at = ylabs, labels = paste0(format(ylabs, 4), intToUtf8(176)), cex.axis = 1,
las = 1
)
mtext("Latitude", side = 2, line = 4, cex = 1)
box()
if (!is.na(file_type)) dev.off() # Close plot device
if (!is.na(file_type)) {
message(paste0(
"Image files were written to the following directory:\n",
getwd(), "\n"
))
}
return(det_summ)
}