#' Plots sub-annual cycles
#'
#' For a list of Fields, plots the sub-annual cycles of the selected layers. Automatically splits data into separate panels based on Source, Quantity,
#' Sire and/or Region, but instead these can be distinguished by aesthetics - see the \code{col.by}, \code{linetype.by} and \code{alpha.by} arguments.
#' Summary or aggregate function can optionally be applied and plotted on top.
#'
#' @param fields The data to plot. Can be a Field or a list of Fields.
#' @param layers A list of strings specifying which layers to plot. Defaults to all layers.
#' @param gridcells A list of gridcells to be plotted (either in different panels or the same panel). For formatting of this argument see \code{selectGridcells}.
#' Leave empty or NULL to plot all gridsizrecells (but note that if this involves too many gridcells the code will stop)
#' @param title A character string to override the default title.
#' @param subtitle A character string to override the default subtitle.
#' @param x.label,y.label Character strings (or expressions) for the x and y axes (optional)
#' @param col.by,linetype.by,alpha.by Character strings defining the aspects of the data which which should be used to set the colour, line type and alpha (transparency).
#' Can meaningfully take the values "Layer", "Source", "Site", "Region" or "Quantity".
#' NOTE SPECIAL DEFAULT CASE: By default, \code{col.by} is set to "Year" which means that the years are plotted according to a colour gradient, and all other aspects of the
#' the data are distinguished by different facet panels. To change this behaviour and colour the lines according to something different, set the "col.by" argument to one of the
#' strings suggested above.
#' @param cols,linetypes,alphas A vector of colours, line types, or alpha values (respectively) to control the aesthetics of the lines.
#' Only "cols" makes sense without a corresponding "xxx.by" argument (see above). The vectors can/should be named to match particular col/linetype/alpha values
#' to particular Layers/Sources/Sites/Quantities/Regions.
#' @param col.labels,linetype.labels,alpha.labels A vector of character strings which are used as the labels for the lines. Must have the same length as the
#' number of Sources/Layers/Sites/Quantities in the plot. The vectors can/should be named to match particular col/linewtype/alpha values to particular Layers/Sources/Sites/Quantities/Region.
#' @param linewidth Numeric (as ggplot2), width of the lines on the plot, consistent with ggplot2. Note the width is doubled for the aggregate/summary line.
#' @param size Numeric, size of the points for the aggregate/summary data, consistent with ggplot2.
#' @param summary.function A function to summarise (aggregate) across year and plot on top. Obvious choice is \code{mean}, but there is flexibility to anything that operates on
#' a vector of numerics - eg median, a 95th percentile, standard deviation.
#' @param summary.function.label An optional character string to give a pretty label to the summary function legend.
#' @param text.multiplier A number specifying an overall multiplier for the text on the plot.
#' Make it bigger if the text is too small on large plots and vice-versa.
#' @param plot Boolean, if FALSE return a data.table with the final data instead of the ggplot object. This can be useful for inspecting the structure of the facetting columns, amongst other things.
#' @param ... Arguments passed to \code{ggplot2::facet_wrap()}. See the ggplot2 documentation for full details but the following are particularly useful.
#' \itemize{
#' \item{"nrow"}{The number of rows of facets}
#' \item{"ncol"}{The number of columns of facets}
#' \item{"scales"}{Whether the scales (ie. x and y ranges) should be fixed for all facets. Options are "fixed" (same scales on all facets, default)
#' "free" (all facets can their x and y ranges), "free_x" and "free_y" (only x and y ranges can vary, respectively).}
#' \item{"labeller"}{A function to define the labels for the facets. This is a little tricky, please look to the ggplot2 documentation}
#' }
#'
#' @details
#'
#' Note that like all \code{DGVMTools} plotting functions, \code{plotSubannual} splits the data into separate panels using the \code{ggplot2::facet_wrap()}. If you want to 'grid' the facets
#' using \code{ggplot2::facet_grid()} you can do so afterwards. 'gridding the facets' implies the each column and row of facets vary by one specific aspect.
#' For example you might have one column for each Source, and one row for each "Quantity".
#'
#'
#'
#' @return Returns either a ggplot2 object or a data.table (depending on the 'plot' argument)
#' @author Matthew Forrest \email{matthew.forrest@@senckenberg.de}
#' @export
plotSubannual <- function(fields, # can be a Field or a list of Fields
layers = NULL,
gridcells = NULL,
title = NULL,
subtitle = NULL,
cols = NULL,
col.by = "Year",
col.labels = waiver(),
linetypes = NULL,
linetype.by = NULL,
linetype.labels = waiver(),
alphas = NULL,
alpha.by = NULL,
alpha.labels = waiver(),
linewidth = 0.5,
size = 3 ,
y.label = NULL,
x.label = NULL,
summary.function,
summary.function.label = deparse(substitute(summary.function)),
text.multiplier = NULL,
plot = TRUE,
...) {
Quantity = Month = Source = Value = Year = PlotGroup = StatsGroup = NULL
### CHECK INPUTS AND HANDLE SPECIAL CASES
# set a flag for the special case of colouring by year
special_case_year_colour <- FALSE
if(identical(col.by,"Year")) {
special_case_year_colour <- TRUE
if(!missing(cols) || !is.null(cols)) {
warning("'cols' argument ignored since colouring by Year and therefore using a continuous scale")
cols <- NULL
}
}
### SANITISE FIELDS, LAYERS AND DIMENSIONS
## 1. FIELDS - check the input Field objects (and if it is a single Field put it into a one-item list)
fields <- santiseFieldsForPlotting(fields)
if(is.null(fields)) return(NULL)
## 2. LAYERS - check the number of layers
layers <- santiseLayersForPlotting(fields, layers)
if(is.null(layers)) return(NULL)
## 3. DIMENSIONS - check the dimensions (require that all fields the same dimensions) and then that one and only subannual dimension is present )
# check the Fields for consistent dimensions
dim.names <- santiseDimensionsForPlotting(fields)
if(is.null(dim.names)) return(NULL)
# get the subannual dimensions and check one and only one
subannual.dimension <- dim.names[which(dim.names %in% c("Day", "Month", "Season")) ]
if(length(subannual.dimension) == 0) stop("No subannual dims found for plotSubannual ")
else if((length(subannual.dimension) > 1) ) stop(paste0("Multiple subannual dimensions found in plotSubannual: ", paste(subannual.dimension)))
### PREPARE AND CHECK DATA FOR PLOTTING
final.fields <- trimFieldsForPlotting(fields, layers, gridcells = gridcells)
### MERGE DATA FOR PLOTTING INTO ONE BIG DATA.TABLE
# MF TODO maybe make some clever checks on these switches
if("Lon" %in% dim.names & "Lat" %in% dim.names) add.Site <- TRUE
else add.Site <- FALSE
add.Region <- TRUE
# MF TODO: Consider adding add.TimePeriod?
data.toplot <- mergeFieldsForPlotting(final.fields, add.Quantity = TRUE, add.Site = add.Site, add.Region = TRUE)
### CHECK IS YEAR IS PRESENT
if(!("Year" %in% names(data.toplot))) {
special_case_year_colour <- FALSE
if(missing(col.by)) col.by = NULL
}
### XX. FACETTING
# all column names, used a lot below
all.columns <- names(data.toplot)
# check the "xxx.by" arguments
if(!missing(col.by) && !is.null(col.by) && !col.by %in% all.columns) stop(paste("Colouring by", col.by, "requested, but that is not available, so failing."))
if(!missing(linetype.by) && !is.null(linetype.by) && !linetype.by %in% all.columns) stop(paste("Setting linetypes by", linetype.by, "requested, but that is not available, so failing."))
if(!missing(alpha.by) && !is.null(alpha.by) && !alpha.by %in% all.columns) stop(paste("Setting alphas by", alpha.by, "requested, but that is not available, so failing."))
# ar first assume facetting by everything except for...
dontFacet <- c("Value", "Time", "Year", "Month", "Season", "Day", "Lon", "Lat", col.by, linetype.by, alpha.by)
facet.vars <- all.columns[!all.columns %in% dontFacet]
# then remove facets with only one unique value
for(this.facet in facet.vars) {
if(length(unique(data.toplot[[this.facet]])) == 1) facet.vars <- facet.vars[!facet.vars == this.facet]
}
### LEGEND ENTRY ORDERING
# ## Fix order of items in legend(s) by making them factors with levels corresponding to the order of the input fields
# all.sources <- list()
# # first loop across the fields
# for(this.field in fields) {
# all.sources <- append(all.sources, this.field@source@name)
# }
# if("Source" %in% names(data.toplot)) data.toplot[, Source := factor(Source, levels = unique(all.sources))]
### 7. MAKE THE Y-AXIS LABEL
if(is.null(y.label)) {
y.label <- makeYAxis(final.fields)
}
### MATCH LAYER COLOUR
# if cols is not specified and plots are to be coloured by Layers, look up line colours from Layer meta-data
if(missing(cols) && !is.null(col.by) && col.by == "Layer"){
# check the defined Layers present in the Fields and make a unique list
# maybe also here check which one are actually in the layers to plot, since we have that information
all.layers.defined <- list()
for(object in fields){
all.layers.defined <- append(all.layers.defined, object@source@defined.layers)
}
all.layers.defined <- unique(all.layers.defined)
all.layers <- unique(as.character(data.toplot[["Layer"]]))
cols <- matchLayerCols(all.layers, all.layers.defined)
}
#### MAKE LIST OF QUANTITIES AND UNITS FOR Y LABEL
### DEPRECATED BY CALL TO makeYAxis() above, but some functionality might be needed to keep for now.
# unit.str <- list()
# quant.str <- list()
# id.str <- list()
# for(this.field in final.fields) {
#
# # pull out the unit string, id and full name string
# quant <- this.field@quant
# unit.str <- append(unit.str, quant@units)
# quant.str <- append(quant.str, quant@name)
# id.str <- append(id.str, quant@id)
#
# }
#
# # check the units
# if(length(unique(unit.str)) == 1){
# unit.str <- unique(unit.str)
# }
# else {
# unit.str <- paste(unique(unit.str), collapse = ", ")
# warning("Quants to be plotted have non-identical units in plotSeasonal().")
# }
#
# # check the strings
# if(length(unique(quant.str)) == 1){ quant.str <- unique(quant.str) }
# else{ quant.str <- paste(id.str, sep = ", ", collapse = ", ") }
### MAKE A DESCRIPTIVE TITLE IF ONE HAS NOT BEEN SUPPLIED
if(missing(title) || missing(subtitle)) {
titles <- makePlotTitle(final.fields)
if(missing(title)) title <- titles[["title"]]
else if(is.null(title)) title <- waiver()
if(missing(subtitle)) subtitle <- titles[["subtitle"]]
else if(is.null(subtitle)) subtitle <- waiver()
}
### MAKE THE GROUPS - these are columns for ggplot which are essentially interaction terms to group the data
# The plot group
# include Year only if not colouring by year
if(special_case_year_colour) interaction_list <- c()
else {
if("Year" %in% names(data.toplot)) interaction_list <- c("Year")
else interaction_list <- c()
}
# include the other columns if required for an aesthetic
if(!is.null(col.by)) {
interaction_list <- append(interaction_list, col.by) # special case for col.by because if missing is taken to be "Year"
}
if(!missing(linetype.by) && !is.null(linetype.by)) interaction_list <- append(interaction_list, linetype.by)
if(!missing(alpha.by) && !is.null(alpha.by)) interaction_list <- append(interaction_list, alpha.by)
# and now make the interaction column
data.toplot[, PlotGroup := interaction(.SD), .SDcols = interaction_list]
# The Stats Group - this is much easier since we every possible combination must have it's own stat
stats_SDcols <- names(data.toplot) %in% c("Source", "Quantity", "Layer", "Region", "Site")
data.toplot[, StatsGroup := interaction(.SD), .SDcols = stats_SDcols]
###### MAKE THE PLOT #####
# return the data if plot = FALSE
if(!plot) return(data.toplot)
# make the "symbols" for the ggplot2 call. A bit of a pain -since they ggplot2 folks took away aes_string()- but what can you do...
col.sym <- if(is.character(col.by)) ensym(col.by) else NULL
alpha.sym <- if(is.character(alpha.by)) ensym(alpha.by) else NULL
linetype.sym <- if(is.character(linetype.by)) ensym(linetype.by) else NULL
# build the basic plot
p <- ggplot(as.data.frame(data.toplot), aes(x = .data[[subannual.dimension]], y = Value, group = PlotGroup,
col = !! col.sym,
alpha = !! alpha.sym,
linetype = !! linetype.sym))
# build arguments for aesthetics to geom_line/geom_line and/or fixed arguments outside
geom_args <- list()
# col, alpha and linetyp
if(!is.null(cols) && is.null(col.by)) geom_args[["colour"]] <- cols
if(!is.null(alphas) && is.null(alpha.by)) geom_args[["alpha"]] <- alphas
if(!is.null(linetypes) && is.null(linetype.by)) geom_args[["linetype"]] <- linetypes
# line width if a fixed value for all
geom_args[["linewidth"]] <- linewidth
# call geom_line (with fixed aesthetics define above)
p <- p + do.call(geom_line, geom_args)
# add scales for the defined aesthetics
# colour scale is a special case for two reasons,
# 1. if col.by not variable provided, then the special case is activated and colour by year (and hence we have a continuous colour scale)
# 2. if no colours provided, use a viridis pallete to override ggplot2
if(special_case_year_colour) p <- p + viridis::scale_color_viridis(name = "Year")
else if (!is.null(col.by) & !is.null(cols)) p <- p + scale_color_manual(values=cols, labels=col.labels)
else p <- p + viridis::scale_colour_viridis(discrete=TRUE, option = "C", end = 0.9 )
# these are simply defined by the arguments, no special cases
if(!is.null(linetype.by) & !is.null(linetypes)) p <- p + scale_linetype_manual(values=linetypes, labels=linetype.labels)
if(!is.null(alpha.by) & !is.null(alphas)) p <- p + scale_alpha_manual(values=alphas, labels=alpha.labels)
# set the theme to theme_bw, simplest way to set the background to white
p <- p + theme_bw()
# make scale a bit bigger - consider removing for purity?? this can easy be controlled by the user
p <- p + theme(legend.key.size = unit(2, 'lines'))
# if chosen, plot the average year
if(!missing(summary.function)) {
# if is the special case where we colour by year
if(special_case_year_colour) {
# NOTE in this case we always use black
# special case to add a line type scale if it wasn't already added
# honestly not sure exactly why this works and many other things I tried didn't work
if(missing(linetype.by) || is.null(linetype.by)) {
p <- p + stat_summary(aes(group=col.by, linetype = "dummy string"), fun=summary.function, geom="line", color="black", linewidth = linewidth * 2)
p <- p + scale_linetype_manual(values=c("dummy string"="solid"), labels = c("dummy string" = summary.function.label), name = element_blank())
}
# if linetypes are already specified
else{
p <- p + stat_summary(aes(group=StatsGroup, linetype = .data[[linetype.by]]), fun=summary.function, geom="line", color="black", linewidth = linewidth * 2)
# title the legend
p <- p + labs(linetype=summary.function.label)
}
}
#if *not* the special case where we colour by year
else {
# add the stats
p <- p + stat_summary(aes(group=StatsGroup, fill = get(col.by)), fun=summary.function, geom="point", color="black", shape = 21, size = size)
# colour the points appropriately
if(!is.null(cols)) p <- p + scale_fill_manual(values = cols, name = summary.function.label, labels = col.labels)
else p <- p + viridis::scale_fill_viridis(name = summary.function.label, labels = col.labels, discrete = TRUE, option = "C", end = 0.9 )
p <- p + labs(shape=summary.function.label )
# also add linetype if necessary
if(!missing(linetype.by) && !is.null(linetype.by)) {
p <- p + stat_summary(aes(group=StatsGroup, linetype = .data[[linetype.by]]), fun=summary.function, geom="line", color="black", linewidth = linewidth * 2)
}
}
}
# set the x-axis
if(subannual.dimension == "Month") p <- p + scale_x_continuous(breaks = 1:12, labels = c("Jan", "Feb", "Mar", "Apr", "May", "Jun","Jul", "Aug", "Sep","Oct","Nov","Dec"))
# set the title
p <- p + labs(title = title, subtitle = subtitle, y = y.label, x = subannual.dimension)
p <- p + theme(plot.title = element_text(hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5))
# set legend
p <- p + theme(legend.position = "right", legend.key.size = unit(2, 'lines'))
# facet if necessary
if(length(facet.vars) > 0) p <- p + facet_wrap(facet.vars, ...)
# overall text multiplier
if(!is.null(text.multiplier)) p <- p + theme(text = element_text(size = theme_get()$text$size * text.multiplier))
return(p)
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.