#' @rdname bar #' #' @inheritParams bar #' #' @export hbar <- function( ..., flip = TRUE, add_text = FALSE, theme_fun = theme_bar(flip = flip, add_text = add_text) ) { bar(flip = flip, add_text = add_text, theme_fun = theme_fun, ...) } #' Simple bar chart #' #' `bar()` is a simple bar chart with some customization allowed, in particular the `theme_fun` argument for theming. `hbar()` uses `bar()` with sane defaults for a horizontal bar chart. #' #' @param df A data frame. #' @param x A quoted numeric column. #' @param y A quoted character column or coercible as a character column. #' @param group Some quoted grouping categorical column, e.g. administrative areas or population groups. #' @param facet Some quoted grouping categorical column, e.g. administrative areas or population groups. #' @param x_rm_na Remove NAs in x? #' @param y_rm_na Remove NAs in y? #' @param group_rm_na Remove NAs in group? #' @param facet_rm_na Remove NAs in facet? #' @param y_expand Multiplier to expand the y axis. #' @param add_color Add a color to bars (if no grouping). #' @param add_color_guide Should a legend be added? #' @param flip TRUE or FALSE (default). Default to TRUE or horizontal bar plot. #' @param wrap Should x-labels be wrapped? Number of characters. #' @param position Should the chart be stacked? Default to "dodge". Can take "dodge" and "stack". #' @param alpha Fill transparency. #' @param x_title The x scale title. Default to NULL. #' @param y_title The y scale title. Default to NULL. #' @param group_title The group legend title. Default to NULL. #' @param title Plot title. Default to NULL. #' @param subtitle Plot subtitle. Default to NULL. #' @param caption Plot caption. Default to NULL. #' @param width Bar width. #' @param add_text TRUE or FALSE. Add values as text. #' @param add_text_size Text size. #' @param add_text_color Text color. #' @param add_text_font_face Text font_face. #' @param add_text_threshold_display Minimum value to add the text label. #' @param add_text_suffix If percent is FALSE, should we add a suffix to the text label? #' @param add_text_expand_limit Default to adding 10\% on top of the bar. #' @param add_text_round Round the text label. #' @param theme_fun Whatever theme function. For no custom theme, use theme_fun = NULL. #' #' @inheritParams reorder_by #' #' @importFrom rlang `:=` #' #' @export bar <- function( df, x, y, group = "", facet = "", order = "none", x_rm_na = TRUE, y_rm_na = TRUE, group_rm_na = TRUE, facet_rm_na = TRUE, y_expand = 0.1, add_color = color("cat_5_main_1"), add_color_guide = TRUE, flip = FALSE, wrap = NULL, position = "dodge", alpha = 1, x_title = NULL, y_title = NULL, group_title = NULL, title = NULL, subtitle = NULL, caption = NULL, width = 0.8, add_text = FALSE, add_text_size = 4.5, add_text_color = color("dark_grey"), add_text_font_face = "bold", add_text_threshold_display = 0.05, add_text_suffix = "%", add_text_expand_limit = 1.2, add_text_round = 1, theme_fun = theme_bar( flip = flip, add_text = add_text, axis_text_x_angle = 0, axis_text_x_vjust = 0.5, axis_text_x_hjust = 0.5 ), scale_fill_fun = scale_fill_visualizer_discrete(), scale_color_fun = scale_color_visualizer_discrete() ) { #------ Checks # df is a data frame checkmate::assert_data_frame(df) # x and y and group are character checkmate::assert_character(x, len = 1) checkmate::assert_character(y, len = 1) checkmate::assert_character(group, len = 1) # x and y are columns in df checkmate::assert_choice(x, colnames(df)) checkmate::assert_choice(y, colnames(df)) if (group != "") { checkmate::assert_choice(group, colnames(df)) } # x_rm_na, y_rm_na and group_rm_na are logical scalar checkmate::assert_logical(x_rm_na, len = 1) checkmate::assert_logical(y_rm_na, len = 1) checkmate::assert_logical(group_rm_na, len = 1) checkmate::assert_logical(facet_rm_na, len = 1) # flip is a logical scalar checkmate::assert_logical(flip, len = 1) # wrap is a numeric scalar or NULL if (!is.null(wrap)) { checkmate::assert_numeric(wrap, len = 1, null.ok = TRUE) } # alpha is a numeric scalar between 0 and 1 checkmate::assert_numeric(alpha, lower = 0, upper = 1, len = 1) # add_text is a logical scalar checkmate::assert_logical(add_text, len = 1) # add_text_size is a numeric scalar checkmate::assert_numeric(add_text_size, len = 1) # add_text_font_face is a character scalar in bold plain or italic checkmate::assert_choice(add_text_font_face, c("bold", "plain", "italic")) # add_text_threshold_display is a numeric scalar checkmate::assert_numeric(add_text_threshold_display, len = 1) # add_text_suffix is a character scalar checkmate::assert_character(add_text_suffix, len = 1) # add_text_expand_limit is a numeric scalar checkmate::assert_numeric(add_text_expand_limit, len = 1) # add_text_round is a numeric scalar checkmate::assert_numeric(add_text_round, len = 1) # x and y are numeric or character if (class(df[[y]]) %notin% c("integer", "numeric")) { rlang::abort(paste0(y, " must be numeric.")) } if (!any(class(df[[x]]) %in% c("character", "factor"))) { rlang::abort(paste0(x, " must be character or factor")) } # width is a numeric scalar between 0 and 1 checkmate::assert_numeric(width, lower = 0, upper = 1, len = 1) # Check if position is stack or dodge if (position %notin% c("stack", "dodge")) { rlang::abort("Position should be either 'stack' or 'dodge'.") } #----- Data wrangling # facets over group if (group != "" && facet != "" && group == facet) { rlang::warn("'group' and 'facet' are the same identical.") } # remove NAs using base R if (x_rm_na) { df <- df[!(is.na(df[[x]])), ] } if (y_rm_na) { df <- df[!(is.na(df[[y]])), ] } if (group != "" && group_rm_na) { df <- df[!(is.na(df[[group]])), ] } if (facet != "" && facet_rm_na) { df <- df[!(is.na(df[[facet]])), ] } # reorder dir_order <- if (flip && order %in% c("x", "grouped_x")) { -1 } else if (!flip && order %in% c("x", "grouped_x")) { 1 } else if (flip) { 1 } else { -1 } group_order <- if (group != "" || (group == "" && facet == "")) { group } else if (group == "" && facet != "") { facet } df <- reorder_by( df = df, x = x, y = y, group = group_order, order = order, dir_order = dir_order ) # prepare aes if (group != "") { g <- ggplot2::ggplot( df, mapping = ggplot2::aes( x = !!rlang::sym(x), y = !!rlang::sym(y), fill = !!rlang::sym(group), color = !!rlang::sym(group) ) ) } else { g <- ggplot2::ggplot( df, mapping = ggplot2::aes( x = !!rlang::sym(x), y = !!rlang::sym(y) ) ) } # add title, subtitle, caption, x_title, y_title g <- g + ggplot2::labs( title = title, subtitle = subtitle, caption = caption, x = y_title, y = x_title, color = group_title, fill = group_title ) # width width <- width dodge_width <- width # facets if (facet != "") { if (flip) { g <- g + ggplot2::facet_grid( rows = ggplot2::vars(!!rlang::sym(facet)), scales = "free", space = "free_y" ) } else { g <- g + ggplot2::facet_grid( cols = ggplot2::vars(!!rlang::sym(facet)), scales = "free", space = "free_x" ) } } # should the graph use position_fill? if (group != "") { if (position == "stack") { g <- g + ggplot2::geom_col( alpha = alpha, width = width, position = ggplot2::position_stack() ) } else if (position == "dodge") { g <- g + ggplot2::geom_col( alpha = alpha, width = width, position = ggplot2::position_dodge2( width = dodge_width, preserve = "single" ) ) } else { g <- g + ggplot2::geom_col( alpha = alpha, width = width ) } } else { if (position == "stack") { g <- g + ggplot2::geom_col( alpha = alpha, width = width, position = ggplot2::position_stack(), fill = add_color, color = add_color ) } else if (position == "dodge") { g <- g + ggplot2::geom_col( alpha = alpha, width = width, position = ggplot2::position_dodge2( width = dodge_width, preserve = "single" ), fill = add_color, color = add_color ) } else { g <- g + ggplot2::geom_col( alpha = alpha, width = width, fill = add_color, color = add_color ) } } # wrap labels on the x scale? if (!is.null(wrap)) { g <- g + ggplot2::scale_x_discrete(labels = scales::label_wrap(wrap)) } # because a text legend should always be horizontal, especially for an horizontal bar graph if (flip) { g <- g + ggplot2::coord_flip() } # add text to bars if (flip) { hjust_flip <- -0.5 } else { hjust_flip <- 0.5 } if (flip) { vjust_flip <- 0.5 } else { vjust_flip <- -0.5 } # function for interaction interaction_f <- function(group, facet, data) { if (group == "" && facet == "") { return(NULL) } else if (group != "" && facet != "") { return(interaction(data[[group]], data[[facet]])) } else if (group != "") { return(data[[group]]) } else if (facet != "") { return(data[[facet]]) } else { return(NULL) } } # add text labels if (add_text & position == "dodge") { df <- dplyr::mutate( df, "y_threshold" := ifelse( !!rlang::sym(y) >= add_text_threshold_display, !!rlang::sym(y), NA ) ) # expand limits g <- g + ggplot2::geom_blank( data = df, ggplot2::aes( x = !!rlang::sym(x), y = !!rlang::sym(y) * add_text_expand_limit, group = interaction_f(group, facet, df) ) ) g <- g + ggplot2::geom_text( data = df, ggplot2::aes( label = ifelse( is.na(!!rlang::sym("y_threshold")), NA, paste0( round(!!rlang::sym("y_threshold"), add_text_round), add_text_suffix ) ), group = interaction_f(group, facet, df) ), hjust = hjust_flip, vjust = vjust_flip, color = add_text_color, fontface = add_text_font_face, size = add_text_size, position = ggplot2::position_dodge2(width = dodge_width) ) } else if (add_text & position == "stack") { df <- dplyr::mutate( df, "y_threshold" := ifelse( !!rlang::sym(y) >= add_text_threshold_display, !!rlang::sym(y), NA ) ) g <- g + ggplot2::geom_text( data = df, ggplot2::aes( label = ifelse( is.na(!!rlang::sym("y_threshold")), NA, paste0( round(!!rlang::sym("y_threshold"), add_text_round), add_text_suffix ) ), group = interaction_f(group, facet, df) ), hjust = hjust_flip, vjust = vjust_flip, color = add_text_color, fontface = add_text_font_face, size = add_text_size, position = ggplot2::position_dodge2(width = dodge_width) ) } # y scale tweaks g <- g + ggplot2::scale_y_continuous( # start at 0 expand = ggplot2::expansion(mult = c(0, y_expand)), # remove trailing 0 and choose accuracy of y labels labels = scales::label_number( accuracy = 0.1, drop0trailing = TRUE, big.mark = "", decimal.mark = "." ), ) # # remove guides for legend if !add_color_guide if (!add_color_guide) { g <- g + ggplot2::guides(fill = "none", color = "none") } # # add theme fun if (!is.null(theme_fun)) { g <- g + theme_fun } # # # add scale fun if (!is.null(scale_fill_fun)) { g <- g + scale_fill_fun } if (!is.null(scale_color_fun)) { g <- g + scale_color_fun } return(g) }