diff --git a/DESCRIPTION b/DESCRIPTION index aa08b33..6b79f11 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -2,7 +2,7 @@ Package: consequencestools Type: Package Title: Provides Support Tools for Data Analysis and Visualization from the Ultimate Consequences Database -Version: 0.0.0.9010 +Version: 0.0.0.9011 Authors@R: person( "Carwil", "Bjork-James", @@ -28,6 +28,7 @@ Imports: assertthat, dplyr, forcats, + ggplot2, grDevices, incase, lubridate, @@ -42,6 +43,7 @@ Imports: tidyselect, transcats (>= 0.0.0.9002), utils, + waffle (>= 1.0.2), zoo Depends: R (>= 3.5) diff --git a/NAMESPACE b/NAMESPACE index d8d9695..dd1e510 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -29,11 +29,14 @@ export(clean_pt_variables) export(col2hex) export(combine_dates) export(combine_names) +export(complete_x_values) export(count_events_in_month) export(count_ongoing_events) export(count_range_by) export(department_name_from_id) export(displayed_date_string) +export(domain_filename) +export(domain_filename_es) export(estimated_date) export(estimated_date_string) export(event_counts_by) @@ -54,6 +57,8 @@ export(id_for_event) export(id_for_municipality) export(id_for_municipality_2) export(id_for_president) +export(make_waffle_chart) +export(make_waffle_chart_tall) export(muni_counts_by) export(muni_list_with_counts_by) export(municipality_name_from_id) @@ -66,6 +71,7 @@ export(perp_pal) export(province_name_from_id) export(quasilogical_as_binary) export(red_pal) +export(relabel_pres_admin) export(rename_anexo_columns) export(render_age) export(render_age_es) @@ -77,6 +83,8 @@ export(repair_name_line_es) export(sep_pal) export(set_maximums) export(share_of_largest_n) +export(sorted_by_most_frequent) +export(sorted_by_widest_appearance) export(standard_filter) export(str_equivalent) export(str_equivalent_list) @@ -85,6 +93,7 @@ export(sv_pal) export(top_values_string) export(truncate_event_list) export(truncate_muni_list) +export(waffle_counts) export(wide_event_outcomes_reactable) export(wide_event_outcomes_table) import(dplyr) @@ -113,6 +122,22 @@ importFrom(forcats,fct_collapse) importFrom(forcats,fct_explicit_na) importFrom(forcats,fct_na_value_to_level) importFrom(forcats,fct_relevel) +importFrom(ggplot2,aes) +importFrom(ggplot2,coord_equal) +importFrom(ggplot2,element_blank) +importFrom(ggplot2,element_line) +importFrom(ggplot2,element_text) +importFrom(ggplot2,facet_wrap) +importFrom(ggplot2,geom_vline) +importFrom(ggplot2,ggplot) +importFrom(ggplot2,guide_legend) +importFrom(ggplot2,scale_fill_manual) +importFrom(ggplot2,scale_x_continuous) +importFrom(ggplot2,scale_x_discrete) +importFrom(ggplot2,scale_y_continuous) +importFrom(ggplot2,scale_y_discrete) +importFrom(ggplot2,theme) +importFrom(ggplot2,theme_minimal) importFrom(grDevices,col2rgb) importFrom(grDevices,colorRamp) importFrom(grDevices,rgb) @@ -124,7 +149,9 @@ importFrom(reactable,colDef) importFrom(reactablefmtr,nytimes) importFrom(rlang,"!!!") importFrom(rlang,.data) +importFrom(rlang,enquo) importFrom(rlang,list2) +importFrom(rlang,quo_name) importFrom(rlang,sym) importFrom(stringi,stri_trans_general) importFrom(stringr,str_c) @@ -141,4 +168,5 @@ importFrom(tidyselect,any_of) importFrom(tidyselect,last_col) importFrom(transcats,translated_join_vars) importFrom(utils,head) +importFrom(waffle,geom_waffle) importFrom(zoo,as.Date.yearmon) diff --git a/R/consequencestools-package.R b/R/consequencestools-package.R index 2e42bb0..d49642a 100644 --- a/R/consequencestools-package.R +++ b/R/consequencestools-package.R @@ -22,6 +22,22 @@ #' @importFrom forcats fct_explicit_na #' @importFrom forcats fct_na_value_to_level #' @importFrom forcats fct_relevel +#' @importFrom ggplot2 aes +#' @importFrom ggplot2 coord_equal +#' @importFrom ggplot2 element_blank +#' @importFrom ggplot2 element_line +#' @importFrom ggplot2 element_text +#' @importFrom ggplot2 facet_wrap +#' @importFrom ggplot2 geom_vline +#' @importFrom ggplot2 ggplot +#' @importFrom ggplot2 guide_legend +#' @importFrom ggplot2 scale_fill_manual +#' @importFrom ggplot2 scale_x_continuous +#' @importFrom ggplot2 scale_x_discrete +#' @importFrom ggplot2 scale_y_continuous +#' @importFrom ggplot2 scale_y_discrete +#' @importFrom ggplot2 theme +#' @importFrom ggplot2 theme_minimal #' @importFrom grDevices col2rgb #' @importFrom grDevices colorRamp #' @importFrom grDevices rgb @@ -31,7 +47,9 @@ #' @importFrom reactablefmtr nytimes #' @importFrom rlang !!! #' @importFrom rlang .data +#' @importFrom rlang enquo #' @importFrom rlang list2 +#' @importFrom rlang quo_name #' @importFrom rlang sym #' @importFrom stringi stri_trans_general #' @importFrom stringr str_c @@ -48,6 +66,7 @@ #' @importFrom tidyselect last_col #' @importFrom transcats translated_join_vars #' @importFrom utils head +#' @importFrom waffle geom_waffle #' @importFrom zoo as.Date.yearmon ## usethis namespace: end NULL diff --git a/R/filename-tools-subset.R b/R/filename-tools-subset.R new file mode 100644 index 0000000..0963b81 --- /dev/null +++ b/R/filename-tools-subset.R @@ -0,0 +1,42 @@ +#' Generate protest domain filename +#' +#' @param protest_domain The protest domain name +#' @return The corresponding filename for the domain page, +#' relative to the root of the website. +#' @export +#' +#' @examples +#' domain_filename("Rural land") +domain_filename <- function(protest_domain){ + # No files for overlapping domains like "Rural land, Partisan politics" + name <- stringr::str_replace_all(protest_domain, "\\s+", "-") %>% + stringr::str_to_lower() %>% + stringi::stri_trans_general("Latin-ASCII") + filename <- str_glue("/domain/{name}.html") + return(if_else(stringr::str_detect(protest_domain, ","), + "", # No files for overlapping domains + filename + )) +} + +#' Generate Spanish domain filename +#' +#' @param protest_domain The protest domain name in Spanish +#' @return The corresponding filename for the Spanish domain page, +#' relative to the root of the website. +#' +#' @export +#' +#' @examples +#' domain_filename_es("Campesino") +domain_filename_es <- function(protest_domain){ + name <- stringr::str_replace_all(protest_domain, "\\s+", "-") %>% + stringr::str_to_lower() %>% + stringi::stri_trans_general("Latin-ASCII") + filename <- str_glue("/dominio/{name}.html") + + return(if_else(stringr::str_detect(protest_domain, ","), + "", # No files for overlapping domains + filename + )) +} diff --git a/R/relabel-pres-admin.R b/R/relabel-pres-admin.R new file mode 100644 index 0000000..98c4a86 --- /dev/null +++ b/R/relabel-pres-admin.R @@ -0,0 +1,52 @@ +#' Relabel presidential administration using presidency_name_table +#' +#' Replaces pres_admin values with corresponding values from a specified column +#' in presidency_name_table (e.g., presidency_year, presidency_initials, etc.) +#' +#' @param dataframe The input dataframe containing a pres_admin column +#' @param output_column The column from presidency_name_table to use for new labels +#' (unquoted column name) +#' @param .input_column The column from presidency_name_table to match against +#' pres_admin (default "presidency") +#' @param keep_original Logical indicating whether to keep the original pres_admin +#' column as pres_admin_original (default TRUE) +#' +#' @return The dataframe with pres_admin relabeled according to output_column +#' +#' @export +#' +#' @examples +#' def <- assign_presidency_levels(deaths_aug24) +#' def_labeled <- relabel_pres_admin(def, presidency_year) +#' def_labeled <- relabel_pres_admin(def, presidency_initials) +#' def_labeled <- relabel_pres_admin(def, presidency_fullname_es, keep_original = TRUE) +relabel_pres_admin <- function(dataframe, output_column, .input_column="presidency", keep_original = TRUE) { + output_col_name <- rlang::ensym(output_column) + input_col_name <- rlang::ensym(.input_column) + + result <- dataframe %>% + left_join(select(presidency_name_table, {{input_col_name}}, {{output_column}}), + by = join_by(pres_admin == {{input_col_name}})) + + if (keep_original) { + result <- result %>% + rename(pres_admin_original = pres_admin) + } else { + result <- result %>% + select(-pres_admin) + } + + result <- result %>% + rename(pres_admin = {{output_column}}) + + if(length(unique(presidency_name_table[[rlang::as_name(output_col_name)]])) < nrow(presidency_name_table)) { + warning("Some new values are ambiguous. pres_admin not returned as a factor.") + } else { + result <- result %>% + # Make pres_admin a factor in the order specified in presidency_name_table + mutate(pres_admin = factor(pres_admin, + levels = presidency_name_table[[rlang::as_name(output_col_name)]])) + } + + return(result) +} diff --git a/R/sort-var-description.R b/R/sort-var-description.R new file mode 100644 index 0000000..ad326ad --- /dev/null +++ b/R/sort-var-description.R @@ -0,0 +1,129 @@ +#' Sort variable description by widest appearance across groups +#' +#' Reorders the levels and colors in a variable description list based on how +#' widely each level appears across a grouping variable. Levels that appear in +#' more groups are ranked higher. For example, sorting protest domains by how +#' many presidential administrations each domain appears in. +#' +#' @param var_description A list containing variable metadata with elements: +#' \itemize{ +#' \item \code{r_variable}: Name of the variable in the dataframe +#' \item \code{levels}: Character vector of level names +#' \item \code{colors}: Named vector of colors for each level +#' \item \code{levels_es}: (Optional) Spanish translations of levels +#' \item \code{colors_es}: (Optional) Spanish-named color vector +#' } +#' @param def The dataframe containing the data to analyze +#' @param grouping_var The variable to group by when calculating width of +#' appearance (e.g., pres_admin, year). Use unquoted variable name. +#' +#' @return A modified variable description list with levels and colors reordered +#' by widest appearance. Includes a new \code{$ranked} element with the +#' sorted levels. +#' +#' @export +#' @examples +#' \dontrun{ +#' # Sort protest domains by how many presidential administrations they appear in +#' deaths <- assign_protest_domain_levels(deaths_aug24) +#' pd_sorted <- sorted_by_widest_appearance( +#' var_description = lev$protest_domain, +#' def = deaths, +#' grouping_var = pres_admin +#' ) +#' } +sorted_by_widest_appearance <- function(var_description, def, grouping_var) { + var_name <- var_description$r_variable + var_description$ranked <- + count(def, {{grouping_var}}, !!sym(var_name)) %>% # 2-var frequency table + count(!!sym(var_name)) %>% # frequency table of number of + # values of the grouping variable, with deaths in each value of the variable + arrange(desc(n)) %>% # ranked downward + pull(!!sym(var_name)) # vector of the groups + new_var_description <- var_description + + # Get the new order based on ranked + new_order <- match(new_var_description$ranked, var_description$levels) + + # Reorder levels + new_var_description$levels <- var_description$ranked + + # Reorder levels_es if it exists + if (!is.null(var_description$levels_es)) { + new_var_description$levels_es <- var_description$levels_es[new_order] + } + + # Reorder colors + new_var_description$colors <- var_description$colors[new_order] + + # Reorder colors_es if it exists + if (!is.null(var_description$colors_es)) { + new_var_description$colors_es <- var_description$colors_es[new_order] + } + + new_var_description +} + + +#' Sort variable description by frequency +#' +#' Reorders the levels and colors in a variable description list based on +#' the frequency of each level in the data. More frequent levels are ranked +#' higher. +#' +#' @param var_description A list containing variable metadata with elements: +#' \itemize{ +#' \item \code{r_variable}: Name of the variable in the dataframe +#' \item \code{levels}: Character vector of level names +#' \item \code{colors}: Named vector of colors for each level +#' \item \code{levels_es}: (Optional) Spanish translations of levels +#' \item \code{colors_es}: (Optional) Spanish-named color vector +#' } +#' @param def The dataframe containing the data to analyze +#' @param grouping_var Optional grouping variable (not used, included for +#' consistency with \code{sorted_by_widest_appearance}) +#' +#' @return A modified variable description list with levels and colors reordered +#' by frequency. Includes a new \code{$ranked} element with the sorted levels. +#' +#' @export +#' @examples +#' \dontrun{ +#' deaths <- assign_protest_domain_levels(deaths_aug24) +#' # Sort protest domains by total frequency +#' pd_sorted <- sorted_by_most_frequent( +#' var_description = lev$protest_domain, +#' def = deaths +#' ) +#' } +sorted_by_most_frequent <- function(var_description, def, grouping_var = NULL) { + # Note: grouping_var parameter is not used but included for API consistency + + var_name <- var_description$r_variable + var_description$ranked <- + count(def, !!sym(var_name)) %>% # frequency table for the variable in question + arrange(desc(n)) %>% # ranked downward + pull(!!sym(var_name)) # vector of the groups + new_var_description <- var_description + + # Get the new order based on ranked + new_order <- match(new_var_description$ranked, var_description$levels) + + # Reorder levels + new_var_description$levels <- var_description$ranked + + # Reorder levels_es if it exists + if (!is.null(var_description$levels_es)) { + new_var_description$levels_es <- var_description$levels_es[new_order] + } + + # Reorder colors + new_var_description$colors <- var_description$colors[new_order] + + # Reorder colors_es if it exists + if (!is.null(var_description$colors_es)) { + new_var_description$colors_es <- var_description$colors_es[new_order] + } + + new_var_description +} diff --git a/R/waffle-plot-functions.R b/R/waffle-plot-functions.R new file mode 100644 index 0000000..a15ebcc --- /dev/null +++ b/R/waffle-plot-functions.R @@ -0,0 +1,336 @@ +#' Helper function to create counts dataframe for waffle charts +#' +#' @param dataframe The input dataframe containing the data. +#' @param x_var The variable to facet the waffle chart by (e.g., year). +#' @param fill_var The variable to color the waffle chart blocks by (e.g., +#' state_responsibility). +#' @param fill_var_description A list containing the title, levels, and colors +#' for the fill variable. Can be taken from our level description list, lev. +#' +#' @return A dataframe with counts of occurrences for each combination of x_var +#' and fill_var. +#' +#' @export +#' @examples +#' waffle_counts(dataframe = assign_state_responsibility_levels(deaths_aug24, simplify = TRUE), +#' x_var = year, fill_var = state_responsibility, +#' fill_var_description = lev$state_responsibility) +waffle_counts <- function(dataframe, x_var, + fill_var, fill_var_description) { + # Ensure the fill variable is properly factored with the correct levels + dataframe <- dataframe %>% + mutate(across({{ fill_var }}, + ~factor(., levels = fill_var_description$levels))) + + # Create the counts dataframe + counts_df <- dataframe %>% + filter(!is.na(year)) %>% + filter(!is.na({{ x_var }})) %>% + filter({{ x_var }} != "Unknown") %>% + filter(!is.na({{ fill_var }})) %>% + dplyr::count({{ x_var }}, {{ fill_var }}) + + return(counts_df) +} + +#' New helper function to add null/missing x values +#' +#' @param counts_df The counts dataframe from waffle_counts +#' @param x_var The variable to facet the waffle chart by (e.g., year). +#' @param fill_var The variable to color the waffle chart blocks by (e.g., +#' state_responsibility). +#' @param all_levels Optional: All possible levels for the x variable (needed +#' for pres_admin) +#' @param .verbose Logical indicating whether to print debug information +#' @return The counts dataframe with missing x values added +#' +#' @export +#' +#' @examples +#' \donttest{ +#' deaths <- assign_levels(deaths_aug24, "standard", .simplify=TRUE) %>% +#' dplyr::filter(year != 2012) # Remove 2012 +#' waffle_counts_df <- waffle_counts(deaths, +#' x_var = year, +#' fill_var = protest_domain, +#' fill_var_description = lev$protest_domain) +#' waffle_counts_df_completed <- complete_x_values(waffle_counts_df, +#' x_var = year, +#' fill_var = protest_domain, +#' all_levels = NULL, +#' .verbose = FALSE) +#' } +complete_x_values <- function(counts_df, x_var, fill_var, + all_levels = NULL, .verbose = FALSE) { + x_var_name <- quo_name(enquo(x_var)) + existing_values <- unique(counts_df[[x_var_name]]) + + null_x_values <- c() + range_of_x_levels <- c() + + # Handle year variable + if (x_var_name == "year") { + min_val <- min(existing_values) + max_val <- max(existing_values) + range_of_x_levels <- min_val:max_val + null_x_values <- setdiff(range_of_x_levels, existing_values) + + if (.verbose) { + print(paste("Null values: ", paste(null_x_values, collapse = ", "))) + } + } + + # Handle pres_admin variable (requires all_levels) + if (x_var_name == "pres_admin") { + if (is.null(all_levels)) { + stop("all_levels must be provided for pres_admin variable") + } + + min_idx <- min(match(existing_values, all_levels)) + max_idx <- max(match(existing_values, all_levels)) + range_of_x_levels <- all_levels[min_idx:max_idx] + null_x_values <- setdiff(range_of_x_levels, existing_values) + } + + # Add null rows if any missing values found + if (length(null_x_values) > 0) { + null_rows <- tibble( + {{ x_var }} := null_x_values, + {{ fill_var }} := " ", + n = 1 + ) + counts_df <- bind_rows(counts_df, null_rows) + } + + # Re-factor x variable with complete range of levels + if (length(range_of_x_levels) > 0) { + counts_df <- counts_df %>% + mutate(across(1, ~factor(., levels = range_of_x_levels))) + } + + if (.verbose) { + print(utils::str(counts_df)) + } + + return(counts_df) +} + +#' Create a waffle chart with facets +#' +#' Creates a waffle chart from an overall dataset, both calculating +#' the relevant counts and formatting the chart. +#' +#' @param dataframe The input dataframe containing the data. +#' @param x_var The variable to facet the waffle chart by (e.g., year +#' or pres_admin). This will name the facets. +#' @param fill_var The variable to color the waffle chart blocks by (e.g., +#' state_responsibility). +#' @param fill_var_description A list containing the title, levels, and colors +#' for the fill variable. Can be taken from our level description list, lev. +#' @param n_columns Number of columns for the facet wrap. +#' @param waffle_width Number of rows in each waffle chart (default 10). +#' @param complete_x Logical indicating whether to complete missing x values +#' with a single null block. Only implemented for year and pres_admin. +#' @param lang Language for the legend labels ("en" or "es"). Default is "en". +#' Only affects the legend and not the x-axis label, which should be +#' handled before calling the function. +#' @param .verbose Logical indicating whether to print debug information. +#' +#' @return A ggplot object representing the waffle chart. +#' @export +#' +#' @examples +#' \dontrun{ +#' deaths <- assign_levels(deaths_aug24, "standard", .simplify = TRUE) +#' make_waffle_chart(deaths, +#' x_var = pres_admin, +#' fill_var = state_responsibility, +#' fill_var_description = state_resp, +#' complete_x = TRUE, n_columns = 7) +#' } +make_waffle_chart <- function(dataframe, x_var, fill_var, + fill_var_description, + n_columns = 5, + waffle_width = 10, + complete_x = FALSE, + lang = "en", + .verbose = FALSE) { + # Get the color palette from the corresponding description variable + fill_colors <- fill_var_description$colors + fill_legend <- fill_colors + if (lang=="es" & "colors_es" %in% names(fill_var_description)){ + fill_legend <- fill_var_description$colors_es + fill_var_description$title <- fill_var_description$title_es + } + + x_var_name <- quo_name(enquo(x_var)) + range_of_x_levels <- list() + if(is.factor(dataframe[[x_var_name]])){ + all_levels <- levels(dataframe[[x_var_name]]) + range_of_x_levels <- all_levels + } + + counts_df <- waffle_counts(dataframe, {{x_var}}, {{fill_var}}, + {{fill_var_description}}) + + # Complete with null blocks if requested + if (complete_x) { + # code only implemented for these two variables for now + if (x_var_name %in% c("year", "pres_admin")){ + counts_df <- complete_x_values(counts_df, {{x_var}}, {{fill_var}}, + all_levels = range_of_x_levels, + .verbose = .verbose) + + # Add null to the color set but not to the legend + fill_colors <- c(fill_colors, ' ' = "white") + fill_legend <- c(fill_legend, ' ' = "white") + } + } + + # Create the plot + ggplot(counts_df, aes(fill = {{ fill_var }}, values = n)) + + waffle::geom_waffle(color = "white", size = .25, n_rows = waffle_width, + flip = TRUE, na.rm = TRUE) + + facet_wrap(ggplot2::vars({{ x_var }}), ncol = n_columns, + labeller = ggplot2::label_wrap_gen(20), + strip.position = "bottom") + + scale_x_discrete() + + scale_y_continuous(breaks = c(0.5, 5.5, 10.5), + labels = function(x) (x-0.5) * waffle_width, # make this multiplier the same as n_rows + expand = c(0,0)) + + coord_equal() + + scale_fill_manual(name = fill_var_description$title, + values = fill_colors, + limits = names(fill_colors), + labels = names(fill_legend), # Language specific + breaks = names(fill_colors)) + + theme_minimal(base_family = "Roboto Condensed") + + theme(panel.grid = element_blank(), axis.ticks.y = element_line(), + legend.text = element_text(size = 12), + axis.text = element_text(size = 12), + strip.text.x = element_text(size = 12)) + + ggplot2::guides(fill = guide_legend(reverse = TRUE)) +} + +#' Create a tall waffle chart with facets +#' +#' Creates a tall-oriented waffle chart from an overall dataset, both calculating +#' the relevant counts and formatting the chart. Facets are arranged vertically. +#' +#' @param dataframe The input dataframe containing the data. +#' @param x_var The variable to facet the waffle chart by (e.g., year +#' or pres_admin). This will name the facets. +#' @param fill_var The variable to color the waffle chart blocks by (e.g., +#' state_responsibility). +#' @param fill_var_description A list containing the title, levels, and colors +#' for the fill variable. Can be taken from our level description list, lev. +#' @param n_columns Number of rows for the facet wrap (displayed vertically). +#' @param complete_x Logical indicating whether to complete missing x values +#' with a single null block. Only implemented for year and pres_admin. +#' @param lang Language for the legend labels ("en" or "es"). Default is "en". +#' Only affects the legend and not the x-axis label, which should be +#' handled before calling the function. +#' @param .verbose Logical indicating whether to print debug information. +#' +#' @return A ggplot object representing the tall waffle chart. +#' @export +#' +#' @examples +#' \dontrun{ +#' deaths <- assign_levels(deaths_aug24, "standard", .simplify = TRUE) +#' make_waffle_chart_tall(deaths, +#' x_var = pres_admin, +#' fill_var = state_responsibility, +#' fill_var_description = lev$state_responsibility, +#' complete_x = TRUE, n_columns = 7) +#' make_waffle_chart_tall(deaths, +#' x_var = pres_admin, fill_var = state_responsibility, +#' fill_var_description = lev$state_responsibility, +#' complete_x = FALSE, n_columns = 6) +#' make_waffle_chart_tall(deaths, +#' x_var = pres_admin, fill_var = state_responsibility, +#' fill_var_description = lev$state_responsibility, +#' complete_x = FALSE, n_columns = 6, lang="es") +#' } +make_waffle_chart_tall <- function(dataframe, x_var, fill_var, fill_var_description, + n_columns = 5, + complete_x = FALSE, + lang = "en", + .verbose = FALSE) { + # Get the color palette from the corresponding description variable + fill_colors <- fill_var_description$colors + fill_legend <- fill_colors + if (lang=="es" & "colors_es" %in% names(fill_var_description)){ + fill_legend <- fill_var_description$colors_es + fill_var_description$title <- fill_var_description$title_es + } + + x_var_name <- quo_name(enquo(x_var)) + range_of_x_levels <- list() + if(is.factor(dataframe[[x_var_name]])){ + all_levels <- levels(dataframe[[x_var_name]]) + range_of_x_levels <- all_levels + } + + counts_df <- waffle_counts(dataframe, {{x_var}}, {{fill_var}}, + {{fill_var_description}}) + + # Complete with null blocks if requested + if (complete_x) { + # code only implemented for these two variables for now + if (x_var_name %in% c("year", "pres_admin")){ + counts_df <- complete_x_values(counts_df, {{x_var}}, {{fill_var}}, + all_levels = range_of_x_levels, + .verbose = .verbose) + + # Add null to the color set but not to the legend + fill_colors <- c(fill_colors, ' ' = "white") + fill_legend <- c(fill_legend, ' ' = "white") + } + } + + # Define waffle chart parameters + waffle_width <- 5 + + legend_orientation <- "horizontal" + if ((nrow(counts_df) / n_columns) <= 1) { + legend_orientation <- "vertical" + } + + # Create the plot + ggplot(counts_df, aes(fill = {{ fill_var }}, values = n)) + + # Remove flip=TRUE to make bars build left to right + geom_waffle(color = "white", size = .25, n_rows = waffle_width, na.rm = TRUE) + + # Change strip.position to "left" and use nrow instead of ncol + facet_wrap(ggplot2::vars({{ x_var }}), nrow = n_columns, + labeller = ggplot2::label_wrap_gen(20), + strip.position = "left", + dir = "v") + + scale_x_continuous(breaks = c(0.5, 5.5, 10.5, 15.5, 20.5, 25.5), + labels = function(x) (x-0.5) * waffle_width, + expand = c(0,0)) + + # Use scale_y_discrete() instead of scale_x_discrete() + scale_y_discrete() + + coord_equal() + + scale_fill_manual(name = fill_var_description$title, + values = fill_colors, + breaks = names(fill_colors), + labels = names(fill_legend), # Language specific + guide = guide_legend(reverse = FALSE)) + + theme_minimal(base_family = "Roboto Condensed") + + # Change axis.ticks.y to axis.ticks.x + theme(panel.grid = element_blank(), + axis.ticks.x = element_line(), + legend.text = element_text(size = 12), + axis.text = element_text(size = 12), + legend.position = "top", + legend.direction = legend_orientation, + # Change strip.text.x to strip.text.y + strip.text.y.left = element_text(size = 14, + angle = 0, hjust = 0), + strip.placement = "outside", + plot.margin = ggplot2::margin(5, 20, 5, 35)) + + # add a 1pt solid line at x=0.5 and 1pt grey line at x =20.5 + geom_vline(xintercept = 0.5, color = "black", linewidth = 0.5) + + geom_vline(xintercept = 20.5, color = "darkgrey", linewidth = 0.25) +} diff --git a/data-raw/level-variables.R b/data-raw/level-variables.R index 828b0d4..0ce8a39 100644 --- a/data-raw/level-variables.R +++ b/data-raw/level-variables.R @@ -3,6 +3,7 @@ lev <- list() president <- list() president$title <- "Presidential Administration" +president$title_es <- "Administración presidencial" president$r_variable <- "pres_admin" president$levels <- c( "Hernán Siles Zuazo", "Víctor Paz Estenssoro", "Jaime Paz Zamora", @@ -35,6 +36,7 @@ usethis::use_data(president, overwrite=TRUE) location_precision <- list() location_precision$title <- "Location Precision" +location_precision$title_es <- "Precisión de la ubicación" location_precision$r_variable <- "location_precision" location_precision$levels <- c( "address", "poi_small", "intersection", @@ -47,11 +49,12 @@ lev$location_precision <- location_precision state_resp <- list() state_resp$title <- "State Responsibility" +state_resp$title_es <- "Responsabilidad del Estado" state_resp$r_variable <- "state_responsibility" state_resp$levels <- c("Perpetrator", "Victim", "Involved", "Separate", "Unintentional", "Unknown") state_resp$colors <- c( Perpetrator = "forestgreen", - Victim = "#cd6600", # "darkorange3", + Victim = "#8B1A1A", # "firebrick4", Involved = "#90ee90", # "lightgreen", Separate = "#eeb422", # "goldenrod2", Unintentional = "darkgray", @@ -59,7 +62,7 @@ state_resp$colors <- c( state_resp$colors_es <- c( Perpetrador = "forestgreen", - Victima = "#cd6600", # "darkorange3", + Victima = "#8B1A1A", # "firebrick4", Involucrado = "#90ee90", # "lightgreen", Separado = "#eeb422", # "goldenrod2", "No Intencional" = "darkgray", @@ -124,11 +127,12 @@ assign_protest_domain.colors <- function() { protest_domains <- list() protest_domains$title <- "Protest Domain" +protest_domains$title_es <- "Dominio de protesta" protest_domains$r_variable <- "protest_domain" protest_domains$levels <- protest_domain.grouped protest_domains$colors <- assign_protest_domain.colors() -lev$protest_domain <- protest_domain +lev$protest_domain <- protest_domains usethis::use_data(protest_domains, overwrite=TRUE) # Affiliations @@ -244,6 +248,7 @@ assign_affiliation.colors <- function() { affiliations <- list() affiliations$title <- "Affiliation" +affiliations$title_es <- "Afiliación" affiliations$r_variable <- "affiliation" affiliations$levels <- affiliation.grouped affiliations$levels_es <- affiliation.grouped_es @@ -253,14 +258,17 @@ usethis::use_data(affiliations, overwrite = TRUE) lev$dec_affiliation <- affiliations lev$dec_affiliation$title <- "Deceased Affiliation" +lev$dec_affiliation$title_es <- "Afiliación del fallecido" lev$dec_affiliation$r_variable <- "dec_affiliation" lev$perp_affiliation <- affiliations lev$perp_affiliation$title <- "Perpetrator Affiliation" +lev$perp_affiliation$title_es <- "Afiliación del perpetrador" lev$perp_affiliation$r_variable <- "perp_affiliation" departments <- list() departments$title <- "Department" +departments$title_es <- "Departamento" departments$r_variable <- "department" departments$levels <- c("Beni", "Chuquisaca", "Cochabamba", "La Paz", "Oruro", "Pando", "Potosí", "Santa Cruz", "Tarija", "Unknown") diff --git a/data/affiliations.rda b/data/affiliations.rda index 64852dc..b5f2617 100644 Binary files a/data/affiliations.rda and b/data/affiliations.rda differ diff --git a/data/departments.rda b/data/departments.rda index eb912e8..2eeb09a 100644 Binary files a/data/departments.rda and b/data/departments.rda differ diff --git a/data/president.rda b/data/president.rda index 7732de9..89df703 100644 Binary files a/data/president.rda and b/data/president.rda differ diff --git a/data/protest_domains.rda b/data/protest_domains.rda index b76ba6e..11d3ce6 100644 Binary files a/data/protest_domains.rda and b/data/protest_domains.rda differ diff --git a/data/state_resp.rda b/data/state_resp.rda index 614a1c6..fdcf2ee 100644 Binary files a/data/state_resp.rda and b/data/state_resp.rda differ diff --git a/man/complete_x_values.Rd b/man/complete_x_values.Rd new file mode 100644 index 0000000..c41339a --- /dev/null +++ b/man/complete_x_values.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/waffle-plot-functions.R +\name{complete_x_values} +\alias{complete_x_values} +\title{New helper function to add null/missing x values} +\usage{ +complete_x_values( + counts_df, + x_var, + fill_var, + all_levels = NULL, + .verbose = FALSE +) +} +\arguments{ +\item{counts_df}{The counts dataframe from waffle_counts} + +\item{x_var}{The variable to facet the waffle chart by (e.g., year).} + +\item{fill_var}{The variable to color the waffle chart blocks by (e.g., +state_responsibility).} + +\item{all_levels}{Optional: All possible levels for the x variable (needed +for pres_admin)} + +\item{.verbose}{Logical indicating whether to print debug information} +} +\value{ +The counts dataframe with missing x values added +} +\description{ +New helper function to add null/missing x values +} +\examples{ +\donttest{ +deaths <- assign_levels(deaths_aug24, "standard", .simplify=TRUE) \%>\% + dplyr::filter(year != 2012) # Remove 2012 +waffle_counts_df <- waffle_counts(deaths, + x_var = year, + fill_var = protest_domain, + fill_var_description = lev$protest_domain) +waffle_counts_df_completed <- complete_x_values(waffle_counts_df, + x_var = year, + fill_var = protest_domain, + all_levels = NULL, + .verbose = FALSE) +} +} diff --git a/man/domain_filename.Rd b/man/domain_filename.Rd new file mode 100644 index 0000000..fb3c4d2 --- /dev/null +++ b/man/domain_filename.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/filename-tools-subset.R +\name{domain_filename} +\alias{domain_filename} +\title{Generate protest domain filename} +\usage{ +domain_filename(protest_domain) +} +\arguments{ +\item{protest_domain}{The protest domain name} +} +\value{ +The corresponding filename for the domain page, + relative to the root of the website. +} +\description{ +Generate protest domain filename +} +\examples{ +domain_filename("Rural land") +} diff --git a/man/domain_filename_es.Rd b/man/domain_filename_es.Rd new file mode 100644 index 0000000..1251f28 --- /dev/null +++ b/man/domain_filename_es.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/filename-tools-subset.R +\name{domain_filename_es} +\alias{domain_filename_es} +\title{Generate Spanish domain filename} +\usage{ +domain_filename_es(protest_domain) +} +\arguments{ +\item{protest_domain}{The protest domain name in Spanish} +} +\value{ +The corresponding filename for the Spanish domain page, + relative to the root of the website. +} +\description{ +Generate Spanish domain filename +} +\examples{ +domain_filename_es("Campesino") +} diff --git a/man/make_waffle_chart.Rd b/man/make_waffle_chart.Rd new file mode 100644 index 0000000..eb75ba5 --- /dev/null +++ b/man/make_waffle_chart.Rd @@ -0,0 +1,60 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/waffle-plot-functions.R +\name{make_waffle_chart} +\alias{make_waffle_chart} +\title{Create a waffle chart with facets} +\usage{ +make_waffle_chart( + dataframe, + x_var, + fill_var, + fill_var_description, + n_columns = 5, + waffle_width = 10, + complete_x = FALSE, + lang = "en", + .verbose = FALSE +) +} +\arguments{ +\item{dataframe}{The input dataframe containing the data.} + +\item{x_var}{The variable to facet the waffle chart by (e.g., year +or pres_admin). This will name the facets.} + +\item{fill_var}{The variable to color the waffle chart blocks by (e.g., +state_responsibility).} + +\item{fill_var_description}{A list containing the title, levels, and colors +for the fill variable. Can be taken from our level description list, lev.} + +\item{n_columns}{Number of columns for the facet wrap.} + +\item{waffle_width}{Number of rows in each waffle chart (default 10).} + +\item{complete_x}{Logical indicating whether to complete missing x values +with a single null block. Only implemented for year and pres_admin.} + +\item{lang}{Language for the legend labels ("en" or "es"). Default is "en". +Only affects the legend and not the x-axis label, which should be +handled before calling the function.} + +\item{.verbose}{Logical indicating whether to print debug information.} +} +\value{ +A ggplot object representing the waffle chart. +} +\description{ +Creates a waffle chart from an overall dataset, both calculating +the relevant counts and formatting the chart. +} +\examples{ +\dontrun{ +deaths <- assign_levels(deaths_aug24, "standard", .simplify = TRUE) +make_waffle_chart(deaths, + x_var = pres_admin, + fill_var = state_responsibility, + fill_var_description = state_resp, + complete_x = TRUE, n_columns = 7) +} +} diff --git a/man/make_waffle_chart_tall.Rd b/man/make_waffle_chart_tall.Rd new file mode 100644 index 0000000..d3560b2 --- /dev/null +++ b/man/make_waffle_chart_tall.Rd @@ -0,0 +1,65 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/waffle-plot-functions.R +\name{make_waffle_chart_tall} +\alias{make_waffle_chart_tall} +\title{Create a tall waffle chart with facets} +\usage{ +make_waffle_chart_tall( + dataframe, + x_var, + fill_var, + fill_var_description, + n_columns = 5, + complete_x = FALSE, + lang = "en", + .verbose = FALSE +) +} +\arguments{ +\item{dataframe}{The input dataframe containing the data.} + +\item{x_var}{The variable to facet the waffle chart by (e.g., year +or pres_admin). This will name the facets.} + +\item{fill_var}{The variable to color the waffle chart blocks by (e.g., +state_responsibility).} + +\item{fill_var_description}{A list containing the title, levels, and colors +for the fill variable. Can be taken from our level description list, lev.} + +\item{n_columns}{Number of rows for the facet wrap (displayed vertically).} + +\item{complete_x}{Logical indicating whether to complete missing x values +with a single null block. Only implemented for year and pres_admin.} + +\item{lang}{Language for the legend labels ("en" or "es"). Default is "en". +Only affects the legend and not the x-axis label, which should be +handled before calling the function.} + +\item{.verbose}{Logical indicating whether to print debug information.} +} +\value{ +A ggplot object representing the tall waffle chart. +} +\description{ +Creates a tall-oriented waffle chart from an overall dataset, both calculating +the relevant counts and formatting the chart. Facets are arranged vertically. +} +\examples{ +\dontrun{ +deaths <- assign_levels(deaths_aug24, "standard", .simplify = TRUE) +make_waffle_chart_tall(deaths, + x_var = pres_admin, + fill_var = state_responsibility, + fill_var_description = lev$state_responsibility, + complete_x = TRUE, n_columns = 7) + make_waffle_chart_tall(deaths, + x_var = pres_admin, fill_var = state_responsibility, + fill_var_description = lev$state_responsibility, + complete_x = FALSE, n_columns = 6) + make_waffle_chart_tall(deaths, + x_var = pres_admin, fill_var = state_responsibility, + fill_var_description = lev$state_responsibility, + complete_x = FALSE, n_columns = 6, lang="es") +} +} diff --git a/man/relabel_pres_admin.Rd b/man/relabel_pres_admin.Rd new file mode 100644 index 0000000..448d0b6 --- /dev/null +++ b/man/relabel_pres_admin.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/relabel-pres-admin.R +\name{relabel_pres_admin} +\alias{relabel_pres_admin} +\title{Relabel presidential administration using presidency_name_table} +\usage{ +relabel_pres_admin( + dataframe, + output_column, + .input_column = "presidency", + keep_original = TRUE +) +} +\arguments{ +\item{dataframe}{The input dataframe containing a pres_admin column} + +\item{output_column}{The column from presidency_name_table to use for new labels +(unquoted column name)} + +\item{.input_column}{The column from presidency_name_table to match against +pres_admin (default "presidency")} + +\item{keep_original}{Logical indicating whether to keep the original pres_admin +column as pres_admin_original (default TRUE)} +} +\value{ +The dataframe with pres_admin relabeled according to output_column +} +\description{ +Replaces pres_admin values with corresponding values from a specified column +in presidency_name_table (e.g., presidency_year, presidency_initials, etc.) +} +\examples{ +def <- assign_presidency_levels(deaths_aug24) +def_labeled <- relabel_pres_admin(def, presidency_year) +def_labeled <- relabel_pres_admin(def, presidency_initials) +def_labeled <- relabel_pres_admin(def, presidency_fullname_es, keep_original = TRUE) +} diff --git a/man/sorted_by_most_frequent.Rd b/man/sorted_by_most_frequent.Rd new file mode 100644 index 0000000..2ea8097 --- /dev/null +++ b/man/sorted_by_most_frequent.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/sort-var-description.R +\name{sorted_by_most_frequent} +\alias{sorted_by_most_frequent} +\title{Sort variable description by frequency} +\usage{ +sorted_by_most_frequent(var_description, def, grouping_var = NULL) +} +\arguments{ +\item{var_description}{A list containing variable metadata with elements: +\itemize{ + \item \code{r_variable}: Name of the variable in the dataframe + \item \code{levels}: Character vector of level names + \item \code{colors}: Named vector of colors for each level + \item \code{levels_es}: (Optional) Spanish translations of levels + \item \code{colors_es}: (Optional) Spanish-named color vector +}} + +\item{def}{The dataframe containing the data to analyze} + +\item{grouping_var}{Optional grouping variable (not used, included for +consistency with \code{sorted_by_widest_appearance})} +} +\value{ +A modified variable description list with levels and colors reordered + by frequency. Includes a new \code{$ranked} element with the sorted levels. +} +\description{ +Reorders the levels and colors in a variable description list based on +the frequency of each level in the data. More frequent levels are ranked +higher. +} +\examples{ +\dontrun{ +deaths <- assign_protest_domain_levels(deaths_aug24) +# Sort protest domains by total frequency +pd_sorted <- sorted_by_most_frequent( + var_description = lev$protest_domain, + def = deaths +) +} +} diff --git a/man/sorted_by_widest_appearance.Rd b/man/sorted_by_widest_appearance.Rd new file mode 100644 index 0000000..4e72e46 --- /dev/null +++ b/man/sorted_by_widest_appearance.Rd @@ -0,0 +1,45 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/sort-var-description.R +\name{sorted_by_widest_appearance} +\alias{sorted_by_widest_appearance} +\title{Sort variable description by widest appearance across groups} +\usage{ +sorted_by_widest_appearance(var_description, def, grouping_var) +} +\arguments{ +\item{var_description}{A list containing variable metadata with elements: +\itemize{ + \item \code{r_variable}: Name of the variable in the dataframe + \item \code{levels}: Character vector of level names + \item \code{colors}: Named vector of colors for each level + \item \code{levels_es}: (Optional) Spanish translations of levels + \item \code{colors_es}: (Optional) Spanish-named color vector +}} + +\item{def}{The dataframe containing the data to analyze} + +\item{grouping_var}{The variable to group by when calculating width of +appearance (e.g., pres_admin, year). Use unquoted variable name.} +} +\value{ +A modified variable description list with levels and colors reordered + by widest appearance. Includes a new \code{$ranked} element with the + sorted levels. +} +\description{ +Reorders the levels and colors in a variable description list based on how +widely each level appears across a grouping variable. Levels that appear in +more groups are ranked higher. For example, sorting protest domains by how +many presidential administrations each domain appears in. +} +\examples{ +\dontrun{ +# Sort protest domains by how many presidential administrations they appear in +deaths <- assign_protest_domain_levels(deaths_aug24) +pd_sorted <- sorted_by_widest_appearance( + var_description = lev$protest_domain, + def = deaths, + grouping_var = pres_admin +) +} +} diff --git a/man/waffle_counts.Rd b/man/waffle_counts.Rd new file mode 100644 index 0000000..3f8a76f --- /dev/null +++ b/man/waffle_counts.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/waffle-plot-functions.R +\name{waffle_counts} +\alias{waffle_counts} +\title{Helper function to create counts dataframe for waffle charts} +\usage{ +waffle_counts(dataframe, x_var, fill_var, fill_var_description) +} +\arguments{ +\item{dataframe}{The input dataframe containing the data.} + +\item{x_var}{The variable to facet the waffle chart by (e.g., year).} + +\item{fill_var}{The variable to color the waffle chart blocks by (e.g., +state_responsibility).} + +\item{fill_var_description}{A list containing the title, levels, and colors +for the fill variable. Can be taken from our level description list, lev.} +} +\value{ +A dataframe with counts of occurrences for each combination of x_var + and fill_var. +} +\description{ +Helper function to create counts dataframe for waffle charts +} +\examples{ +waffle_counts(dataframe = assign_state_responsibility_levels(deaths_aug24, simplify = TRUE), + x_var = year, fill_var = state_responsibility, + fill_var_description = lev$state_responsibility) +} diff --git a/tests/testthat/_snaps/relabel-pres-admin.md b/tests/testthat/_snaps/relabel-pres-admin.md new file mode 100644 index 0000000..c5afbeb --- /dev/null +++ b/tests/testthat/_snaps/relabel-pres-admin.md @@ -0,0 +1,52 @@ +# relabel_pres_admin works + + Code + def_labeled + Output + # A tibble: 670 x 52 + event_title unconfirmed year month day later_year later_month later_day + + 1 Shooting at S~ NA 1984 10 26 NA NA NA + 2 Shooting at S~ NA 1984 10 26 NA NA NA + 3 Shooting at S~ NA 1984 10 26 NA NA NA + 4 Shooting at S~ NA 1984 10 26 NA NA NA + 5 Killing of ha~ NA 1984 10 28 NA NA NA + 6 Huayllani roa~ NA 1985 6 3 NA NA NA + 7 Huayllani roa~ NA 1985 6 3 NA NA NA + 8 Huayllani roa~ TRUE 1985 6 3 NA NA NA + 9 COB education~ NA 1986 4 8 NA NA NA + 10 COB education~ NA 1986 4 8 1986 4 10 + # i 660 more rows + # i 44 more variables: dec_firstname , dec_surnames , id_indiv , + # dec_age , dec_alt_age , dec_gender , dec_residence , + # dec_nationality , dec_affiliation , dec_spec_affiliation , + # dec_title , cause_death , munition , weapon , + # va_notes , perp_category , perp_group , + # perp_pol_stalemate , dec_pol_stalemate , ... + +--- + + Code + def_labeled2 + Output + # A tibble: 670 x 52 + event_title unconfirmed year month day later_year later_month later_day + + 1 Shooting at S~ NA 1984 10 26 NA NA NA + 2 Shooting at S~ NA 1984 10 26 NA NA NA + 3 Shooting at S~ NA 1984 10 26 NA NA NA + 4 Shooting at S~ NA 1984 10 26 NA NA NA + 5 Killing of ha~ NA 1984 10 28 NA NA NA + 6 Huayllani roa~ NA 1985 6 3 NA NA NA + 7 Huayllani roa~ NA 1985 6 3 NA NA NA + 8 Huayllani roa~ TRUE 1985 6 3 NA NA NA + 9 COB education~ NA 1986 4 8 NA NA NA + 10 COB education~ NA 1986 4 8 1986 4 10 + # i 660 more rows + # i 44 more variables: dec_firstname , dec_surnames , id_indiv , + # dec_age , dec_alt_age , dec_gender , dec_residence , + # dec_nationality , dec_affiliation , dec_spec_affiliation , + # dec_title , cause_death , munition , weapon , + # va_notes , perp_category , perp_group , + # perp_pol_stalemate , dec_pol_stalemate , ... + diff --git a/tests/testthat/test-filename-tools-subset.R b/tests/testthat/test-filename-tools-subset.R new file mode 100644 index 0000000..17c6220 --- /dev/null +++ b/tests/testthat/test-filename-tools-subset.R @@ -0,0 +1,26 @@ +test_that("domain_filename works", { + domain_list <- c("Gas wars", "Economic policies", "Labor", "Education", "Mining", + "Coca", "Peasant", "Rural land", + "Ethno-ecological", "Urban land", "Drug trade", "Contraband", + "Municipal governance", "Local development", "National governance", + "Partisan politics", "Disabled", "Guerrilla", "Paramilitary", + "Unknown") + desired_links <- c("/domain/gas-wars.html", "/domain/economic-policies.html", + "/domain/labor.html", "/domain/education.html", "/domain/mining.html", + "/domain/coca.html", "/domain/peasant.html", "/domain/rural-land.html", + "/domain/ethno-ecological.html", "/domain/urban-land.html", + "/domain/drug-trade.html", "/domain/contraband.html", + "/domain/municipal-governance.html", + "/domain/local-development.html", "/domain/national-governance.html", + "/domain/partisan-politics.html", "/domain/disabled.html", + "/domain/guerrilla.html", + "/domain/paramilitary.html", "/domain/unknown.html") + expect_equal(domain_filename(domain_list), desired_links) +}) +test_that("domain_filename handles commas", { + expect_equal(domain_filename("Rural land, Partisan politics"), "") + expect_equal(domain_filename_es("Campesino, Politica partidaria"), "") +}) + + + diff --git a/tests/testthat/test-relabel-pres-admin.R b/tests/testthat/test-relabel-pres-admin.R new file mode 100644 index 0000000..76a6e0a --- /dev/null +++ b/tests/testthat/test-relabel-pres-admin.R @@ -0,0 +1,14 @@ +test_that("relabel_pres_admin works", { + def <- deaths_aug24 + def_labeled <- relabel_pres_admin(def, presidency_year, keep_original = TRUE) + expect_equal(ncol(def_labeled), ncol(def) + 1) + expect_true("pres_admin_original" %in% colnames(def_labeled)) + expect_equal(def_labeled$pres_admin_original, def$pres_admin) + expect_snapshot(def_labeled) + + def_labeled2 <- relabel_pres_admin(def, presidency_initials) %>% suppressWarnings() + expect_equal(ncol(def_labeled2), ncol(def) + 1) + expect_true(all(unique(def_labeled2$pres_admin) %in% presidency_name_table$presidency_initials)) + + expect_snapshot(def_labeled2) +}) diff --git a/tests/testthat/test-sort-var-description.R b/tests/testthat/test-sort-var-description.R new file mode 100644 index 0000000..7abffa2 --- /dev/null +++ b/tests/testthat/test-sort-var-description.R @@ -0,0 +1,224 @@ +# tests/testthat/test-sort-var-description.R + +test_that("sorted_by_widest_appearance reorders levels correctly", { + # Create sample data + test_data <- tibble::tibble( + year = c(2010, 2010, 2011, 2011, 2012, 2013), + category = c("A", "B", "A", "C", "A", "B") + ) + + # Create sample var_description + test_desc <- list( + r_variable = "category", + levels = c("A", "B", "C"), + colors = c("A" = "#FF0000", "B" = "#00FF00", "C" = "#0000FF") + ) + + # Apply function + result <- sorted_by_widest_appearance(test_desc, test_data, year) + + # A appears in 3 years, B in 2 years, C in 1 year + expect_equal(as.character(result$ranked), c("A", "B", "C")) + expect_equal(result$levels, c("A", "B", "C")) + expect_equal(names(result$colors), c("A", "B", "C")) + expect_equal(as.character(result$colors), c("#FF0000", "#00FF00", "#0000FF")) +}) + +test_that("sorted_by_widest_appearance handles tied ranks", { + test_data <- tibble::tibble( + year = c(2010, 2010, 2011, 2011), + category = c("A", "B", "A", "B") + ) + + test_desc <- list( + r_variable = "category", + levels = c("A", "B"), + colors = c("A" = "#FF0000", "B" = "#00FF00") + ) + + result <- sorted_by_widest_appearance(test_desc, test_data, year) + + # Both A and B appear in 2 years + expect_equal(length(result$ranked), 2) + expect_true(all(c("A", "B") %in% as.character(result$ranked))) +}) + +test_that("sorted_by_widest_appearance preserves Spanish levels", { + test_data <- tibble::tibble( + year = c(2010, 2010, 2011, 2011, 2012), + category = c("A", "B", "A", "C", "A") + ) + + test_desc <- list( + r_variable = "category", + levels = c("A", "B", "C"), + levels_es = c("Categoría A", "Categoría B", "Categoría C"), + colors = c("A" = "#FF0000", "B" = "#00FF00", "C" = "#0000FF") + ) + + result <- sorted_by_widest_appearance(test_desc, test_data, year) + + # Check that Spanish levels are reordered to match English + expect_equal(result$levels, c("A", "B", "C")) + expect_equal(result$levels_es, c("Categoría A", "Categoría B", "Categoría C")) +}) + +test_that("sorted_by_widest_appearance preserves Spanish colors", { + test_data <- tibble::tibble( + year = c(2010, 2010, 2011, 2012), + category = c("B", "A", "B", "B") + ) + + test_desc <- list( + r_variable = "category", + levels = c("A", "B"), + levels_es = c("Categoría A", "Categoría B"), + colors = c("A" = "#FF0000", "B" = "#00FF00"), + colors_es = c("Categoría A" = "#FF0000", "Categoría B" = "#00FF00") + ) + + result <- sorted_by_widest_appearance(test_desc, test_data, year) + + # B appears in 3 years, A in 1 year + expect_equal(result$levels, c("B", "A")) + expect_equal(result$levels_es, c("Categoría B", "Categoría A")) + expect_equal(names(result$colors_es), c("Categoría B", "Categoría A")) + expect_equal(as.character(result$colors_es), c("#00FF00", "#FF0000")) +}) + +test_that("sorted_by_most_frequent reorders by frequency", { + test_data <- tibble::tibble( + category = c("A", "A", "A", "B", "B", "C") + ) + + test_desc <- list( + r_variable = "category", + levels = c("C", "B", "A"), + colors = c("C" = "#0000FF", "B" = "#00FF00", "A" = "#FF0000") + ) + + result <- sorted_by_most_frequent(test_desc, test_data) + + # A has 3, B has 2, C has 1 + expect_equal(as.character(result$ranked), c("A", "B", "C")) + expect_equal(result$levels, c("A", "B", "C")) + expect_equal(names(result$colors), c("A", "B", "C")) + expect_equal(as.character(result$colors), c("#FF0000", "#00FF00", "#0000FF")) +}) + +test_that("sorted_by_most_frequent ignores grouping_var parameter", { + test_data <- tibble::tibble( + year = c(2010, 2010, 2011), + category = c("A", "B", "A") + ) + + test_desc <- list( + r_variable = "category", + levels = c("A", "B"), + colors = c("A" = "#FF0000", "B" = "#00FF00") + ) + + result1 <- sorted_by_most_frequent(test_desc, test_data) + result2 <- sorted_by_most_frequent(test_desc, test_data, grouping_var = year) + + # Both should give same result (A appears 2x, B appears 1x) + expect_equal(result1$ranked, result2$ranked) + expect_equal(result1$levels, result2$levels) +}) + +test_that("sorted_by_most_frequent preserves Spanish translations", { + test_data <- tibble::tibble( + category = c("C", "C", "C", "A", "A", "B") + ) + + test_desc <- list( + r_variable = "category", + levels = c("A", "B", "C"), + levels_es = c("Categoría A", "Categoría B", "Categoría C"), + colors = c("A" = "#FF0000", "B" = "#00FF00", "C" = "#0000FF"), + colors_es = c("Categoría A" = "#FF0000", + "Categoría B" = "#00FF00", + "Categoría C" = "#0000FF") + ) + + result <- sorted_by_most_frequent(test_desc, test_data) + + # C has 3, A has 2, B has 1 + expect_equal(result$levels, c("C", "A", "B")) + expect_equal(result$levels_es, c("Categoría C", "Categoría A", "Categoría B")) + expect_equal(names(result$colors_es), + c("Categoría C", "Categoría A", "Categoría B")) +}) + +test_that("functions work with real package data", { + skip_if_not(exists("deaths_aug24"), "deaths_aug24 not available") + skip_if_not(exists("lev"), "lev not available") + skip_if_not(exists("assign_levels"), "assign_levels not available") + + deaths_filtered <- assign_levels(deaths_aug24, "standard", .simplify = TRUE) + + # Test sorted_by_widest_appearance with real data + if ("protest_domain" %in% names(lev)) { + result_wide <- sorted_by_widest_appearance( + lev$protest_domain, + deaths_filtered, + pres_admin + ) + + expect_true("ranked" %in% names(result_wide)) + expect_equal(length(result_wide$levels), length(result_wide$colors)) + if (!is.null(result_wide$levels_es)) { + expect_equal(length(result_wide$levels), length(result_wide$levels_es)) + } + } + + # Test sorted_by_most_frequent with real data + if ("protest_domain" %in% names(lev)) { + result_freq <- sorted_by_most_frequent( + lev$protest_domain, + deaths_filtered + ) + + expect_true("ranked" %in% names(result_freq)) + expect_equal(length(result_freq$levels), length(result_freq$colors)) + } +}) + +test_that("functions handle single-level variables", { + test_data <- tibble::tibble( + year = c(2010, 2011, 2012), + category = c("A", "A", "A") + ) + + test_desc <- list( + r_variable = "category", + levels = c("A"), + colors = c("A" = "#FF0000") + ) + + result_wide <- sorted_by_widest_appearance(test_desc, test_data, year) + result_freq <- sorted_by_most_frequent(test_desc, test_data) + + expect_equal(length(result_wide$levels), 1) + expect_equal(length(result_freq$levels), 1) + expect_equal(result_wide$levels, "A") + expect_equal(result_freq$levels, "A") +}) + +test_that("functions preserve all color values", { + test_data <- tibble::tibble( + category = c("B", "C", "A") + ) + + test_desc <- list( + r_variable = "category", + levels = c("A", "B", "C"), + colors = c("A" = "#FF0000", "B" = "#00FF00", "C" = "#0000FF") + ) + + result <- sorted_by_most_frequent(test_desc, test_data) + + # Check all colors are present (even if reordered) + expect_setequal(as.character(result$colors), + c("#FF0000", "#00FF00", "#0000FF")) +}) diff --git a/tests/testthat/test-waffle-plot-functions.R b/tests/testthat/test-waffle-plot-functions.R new file mode 100644 index 0000000..e15ea23 --- /dev/null +++ b/tests/testthat/test-waffle-plot-functions.R @@ -0,0 +1,210 @@ +# Tests for waffle plot functions + +# Setup test data and descriptions +setup_test_data <- function() { + # Create sample fill variable description + fill_desc <- list( + title = "Test Category", + levels = c("Category A", "Category B", "Category C"), + colors = c("Category A" = "#FF0000", + "Category B" = "#00FF00", + "Category C" = "#0000FF") + ) + + # Create sample dataframe + test_df <- tibble::tibble( + year = c(2010, 2010, 2012, 2012, 2014, 2014), + pres_admin = factor(c("Admin1", "Admin1", "Admin2", "Admin2", "Admin3", "Admin3"), + levels = c("Admin1", "Admin2", "Admin3", "Admin4")), + category = factor(c("Category A", "Category B", "Category A", + "Category C", "Category B", "Category C"), + levels = c("Category A", "Category B", "Category C")), + value = c(5, 3, 4, 2, 6, 1) + ) + + list(df = test_df, fill_desc = fill_desc) +} + +test_that("waffle_counts filters and counts correctly", { + test_data <- setup_test_data() + + result <- waffle_counts(test_data$df, year, category, test_data$fill_desc) + + # Should have 6 rows (one for each year-category combination) + expect_equal(nrow(result), 6) + + # Should have 3 columns: year, category, n + expect_equal(ncol(result), 3) + + # Category should be properly factored + expect_true(is.factor(result$category)) + expect_equal(levels(result$category), test_data$fill_desc$levels) +}) + +test_that("waffle_counts filters out NA and Unknown values", { + test_data <- setup_test_data() + + # Add problematic rows + df_with_nas <- test_data$df %>% + dplyr::bind_rows( + tibble::tibble(year = NA, pres_admin = "Admin1", category = "Category A", value = 1), + tibble::tibble(year = 2015, pres_admin = "Unknown", category = "Category A", value = 1), + tibble::tibble(year = 2015, pres_admin = "Admin1", category = NA, value = 1) + ) + + result <- waffle_counts(df_with_nas, pres_admin, category, test_data$fill_desc) + + # Should only have the original 6 rows + expect_equal(nrow(result), 6) + + # Should not contain NA or "Unknown" + expect_false(any(is.na(result$pres_admin))) + expect_false(any(result$pres_admin == "Unknown")) + expect_false(any(is.na(result$category))) +}) + +test_that("waffle_counts works on standard dataset", { + deaths <- deaths_aug24 %>% + assign_state_responsibility_levels(simplify = TRUE) + result <- waffle_counts(deaths, year, state_responsibility, + lev$state_responsibility) + + # Should have more than 0 rows + expect_gt(nrow(result), 0) + + # Should have full range of values + expect_equal(sort(unique(result$year)), + sort(unique(na.omit(deaths$year)))) + expect_equal(sort(unique(result$state_responsibility)), + sort(unique(deaths$state_responsibility))) + + # Should have 3 columns: year, state_responsibility, n + expect_equal(names(result), c("year", "state_responsibility", "n")) + + # state_responsibility should be properly factored + expect_true(is.factor(result$state_responsibility)) + expect_equal(levels(result$state_responsibility), lev$state_responsibility$levels) +}) + +test_that("make_waffle_chart returns a ggplot object", { + test_data <- setup_test_data() + + result <- make_waffle_chart(test_data$df, year, category, + test_data$fill_desc, complete_x = FALSE) + + expect_s3_class(result, "gg") + expect_s3_class(result, "ggplot") +}) + +test_that("complete_x_values adds missing years", { + test_data <- setup_test_data() + + # Create data with gaps + df_with_gaps <- test_data$df %>% + dplyr::filter(year != 2012) # Remove 2012 + + counts_df <- waffle_counts(df_with_gaps, year, category, test_data$fill_desc) + + completed_counts <- complete_x_values(counts_df, year, category, + all_levels = NULL, .verbose = FALSE) + + # Check that 2012 was added + expect_true(2012 %in% completed_counts$year) +}) + +test_that("complete_x_values fixes missing years in standard data", { + deaths <- assign_levels(deaths_aug24, "standard", .simplify=TRUE) + + # Create data with gaps + df_with_gaps <- deaths %>% + dplyr::filter(year != 2012) # Remove 2012 + + waffle_counts_df <- waffle_counts(df_with_gaps, + x_var = year, + fill_var = protest_domain, + fill_var_description = lev$protest_domain) + + expect_equal(nrow(waffle_counts_df[waffle_counts_df$year == 2012, ]), 0) + + waffle_counts_df_completed <- complete_x_values(waffle_counts_df, + x_var = year, + fill_var = protest_domain, + all_levels = NULL, + .verbose = FALSE) + expect_true(2012 %in% waffle_counts_df_completed$year) + + result <- make_waffle_chart(df_with_gaps, + x_var = year, + fill_var = protest_domain, + lev$protest_domain, complete_x = FALSE) + + # Check that the plot data doesn't include 2012 + plot_data <- ggplot2::ggplot_build(result)$data[[1]] + expect_false(2012 %in% plot_data$plot$data$year) + + result_completed <- make_waffle_chart(df_with_gaps, + x_var = year, + fill_var = protest_domain, + lev$protest_domain, complete_x = TRUE) + # Check that the plot data includes 2012 + plot_data_completed <- ggplot2::ggplot_build(result_completed) + expect_true(2012 %in% plot_data_completed$plot$data$year) +}) + +test_that("make_waffle_chart with complete_x=TRUE fills pres_admin gaps", { + deaths <- assign_levels(deaths_aug24, "standard", .simplify=TRUE) + + # verify that there are already missinng values + count(deaths, pres_admin, .drop=FALSE) %>% filter(n==0) %>% nrow() -> num_missing + expect_gt(num_missing, 0) + pres_admin_missing <- count(deaths, pres_admin, .drop=FALSE) %>% + filter(n==0) %>% pull(pres_admin) + + # Verify it's missing if not completed + result <- make_waffle_chart(deaths, pres_admin, protest_domain, + lev$protest_domain, complete_x = FALSE) + plot_data <- ggplot2::ggplot_build(result)$data[[1]] + expect_false(any(pres_admin_missing %in% plot_data$plot$data$pres_admin)) + + # This should add a null block for our missing president(s) + result_completed <- make_waffle_chart(deaths, pres_admin, protest_domain, + lev$protest_domain, complete_x = TRUE) + plot_data_completed <- ggplot2::ggplot_build(result_completed) + expect_true(any(pres_admin_missing %in% plot_data_completed$plot$data$pres_admin)) +}) + +test_that("make_waffle_chart_tall returns a ggplot object", { + test_data <- setup_test_data() + + result <- make_waffle_chart_tall(test_data$df, year, category, + test_data$fill_desc) + + expect_s3_class(result, "gg") + expect_s3_class(result, "ggplot") +}) + +test_that("make_waffle_chart handles different waffle_width values", { + test_data <- setup_test_data() + + result1 <- make_waffle_chart(test_data$df, year, category, + test_data$fill_desc, waffle_width = 5) + result2 <- make_waffle_chart(test_data$df, year, category, + test_data$fill_desc, waffle_width = 15) + + expect_s3_class(result1, "ggplot") + expect_s3_class(result2, "ggplot") +}) + +test_that("complete_x verbose mode produces output", { + test_data <- setup_test_data() + + df_with_gaps <- test_data$df %>% + dplyr::filter(year != 2012) + + # Should print verbose output + expect_output( + make_waffle_chart(df_with_gaps, year, category, + test_data$fill_desc, complete_x = TRUE, .verbose = TRUE), + "Null values" + ) +}) diff --git a/vignettes/articles/event-heat-map.Rmd b/vignettes/articles/event-heat-map.Rmd index 38ad4d5..2616259 100644 --- a/vignettes/articles/event-heat-map.Rmd +++ b/vignettes/articles/event-heat-map.Rmd @@ -85,7 +85,7 @@ create_calendar_heatmap <- function(year) { plot_month <- function(month_num) { month_data <- df %>% filter(month == month_num) - ggplot(month_data, aes(x = wday, y = week, fill = events)) + + ggplot2::ggplot(month_data, aes(x = wday, y = week, fill = events)) + geom_tile(color = "white") + scale_fill_gradient(low = "white", high = "red") + scale_x_continuous(breaks = 1:7, labels = c("S", "M", "T", "W", "T", "F", "S")) +