#' Civilytics brand colors #' #' A named character vector of all Civilytics brand colors, matching the CSS #' custom properties in the Civilytics design system (`--cv-*` variables). #' Includes full ramps for navy, ember, and violet, plus supporting hues #' (teal, plum, moss, brass) and semantic status colors. #' #' @format A named character vector of hex color codes. #' @export #' #' @examples #' civilytics_colors["ink"] #' civilytics_colors[c("navy_600", "ember_600", "teal_600")] civilytics_colors <- c( # Neutrals — warm paper -> civic ink paper = "#FAF7F2", paper_2 = "#F2EDE4", paper_3 = "#E6DFD1", rule = "#D6CEBD", rule_strong = "#B8AE97", ink = "#0E1A2B", ink_2 = "#2B3A52", ink_3 = "#5A6A82", ink_4 = "#8C97AB", # Navy — civic authority (primary brand blue) navy_900 = "#0E1A2B", navy_800 = "#132339", navy_700 = "#1A2E4A", navy_600 = "#22406A", navy_500 = "#2E5590", navy_400 = "#4A74B0", navy_300 = "#7A9BCA", navy_200 = "#B3C6E0", navy_100 = "#DDE6F2", navy_50 = "#EEF3FA", # Ember — warm orange accent ember_900 = "#451A00", ember_800 = "#6B2B00", ember_700 = "#923D00", ember_600 = "#C25311", ember_500 = "#DB6C25", ember_400 = "#EA8A49", ember_300 = "#F2A976", ember_200 = "#F8C8A3", ember_100 = "#FBE0C6", ember_50 = "#FDF1E4", # Violet — extended supporting violet_900 = "#19102E", violet_800 = "#2A1B4D", violet_700 = "#3D2A6B", violet_600 = "#5C3A8A", violet_500 = "#7556A8", violet_400 = "#9A7EC2", violet_300 = "#BDA8DA", violet_200 = "#DCD0EC", violet_100 = "#F1ECF8", # Supporting — muted editorial hues for data viz teal_600 = "#1F6F70", teal_300 = "#7CB3B3", teal_100 = "#D3E6E6", plum_600 = "#6B3A5E", plum_300 = "#B392A7", plum_100 = "#E7DAE1", moss_600 = "#4A6B2F", moss_300 = "#9CB47D", moss_100 = "#DEE8CF", brass_600 = "#B8751C", brass_100 = "#F9E6C8", # Semantic status colors success = "#4A6B2F", warning = "#9A5F18", danger = "#A6271D", info = "#2E5590" ) #' Civilytics visualization palettes #' #' Named list of curated color palettes for data visualization. Qualitative #' palettes use distinct hues for categorical data; sequential palettes ramp #' through a single hue for ordered data; diverging palettes fan out from a #' neutral midpoint. #' #' @format A named list of character vectors of hex color codes. #' #' @section Qualitative (categorical data): #' \describe{ #' \item{`qual`}{7 distinct hues: navy, ember, plum, violet, red, teal, ink-3} #' \item{`qual_warm`}{Warm-leaning: ember, red, magenta, plum, violet} #' \item{`qual_cool`}{Cool-leaning: navy, violet, plum, teal} #' } #' #' @section Sequential (ordered/continuous data): #' \describe{ #' \item{`seq_ember`}{Light to dark ember (9 stops)} #' \item{`seq_navy`}{Light to dark navy (9 stops)} #' \item{`seq_violet`}{Light to dark violet (9 stops)} #' \item{`seq_paper_ink`}{Paper through ember/plum to ink (8 stops, good for heatmaps)} #' } #' #' @section Diverging (data with a meaningful midpoint): #' \describe{ #' \item{`div_navy_ember`}{Navy <-> paper <-> ember (9 stops)} #' \item{`div_violet_ember`}{Violet <-> paper <-> ember (9 stops)} #' } #' #' @export #' #' @examples #' names(civilytics_palettes) #' civilytics_palettes[["qual"]] civilytics_palettes <- list( # -- Qualitative -- qual = c( "#22406A", "#C25311", "#6B3A5E", "#3D2A6B", "#A6271D", "#1F6F70", "#5A6A82" ), qual_warm = c( "#C25311", "#A6271D", "#923D00", "#B8366B", "#6B3A5E", "#DB6C25", "#3D2A6B" ), qual_cool = c( "#22406A", "#3D2A6B", "#6B3A5E", "#1F6F70", "#2E5590", "#7556A8", "#5A6A82" ), # -- Sequential -- seq_ember = c( "#FDF1E4", "#FBE0C6", "#F8C8A3", "#F2A976", "#EA8A49", "#DB6C25", "#C25311", "#923D00", "#451A00" ), seq_navy = c( "#EEF3FA", "#DDE6F2", "#B3C6E0", "#7A9BCA", "#4A74B0", "#2E5590", "#22406A", "#1A2E4A", "#0E1A2B" ), seq_violet = c( "#F1ECF8", "#DCD0EC", "#BDA8DA", "#9A7EC2", "#7556A8", "#5C3A8A", "#3D2A6B", "#2A1B4D", "#19102E" ), seq_paper_ink = c( "#FAF7F2", "#F8C8A3", "#EA8A49", "#C25311", "#A6271D", "#6B3A5E", "#3D2A6B", "#0E1A2B" ), # -- Diverging -- div_navy_ember = c( "#0E1A2B", "#22406A", "#4A74B0", "#B3C6E0", "#FAF7F2", "#F8C8A3", "#EA8A49", "#C25311", "#6B2B00" ), div_violet_ember = c( "#19102E", "#3D2A6B", "#7556A8", "#BDA8DA", "#FAF7F2", "#F8C8A3", "#EA8A49", "#C25311", "#6B2B00" ) ) #' Get colors from a Civilytics palette #' #' Returns a character vector of hex codes from a named Civilytics palette. #' For qualitative palettes, colors beyond the palette length recycle with a #' warning. For sequential and diverging palettes, colors are interpolated #' via [grDevices::colorRampPalette()]. #' #' @param name Character. Palette name. See `names(civilytics_palettes)`. #' @param n Integer or `NULL`. Number of colors to return. If `NULL`, returns #' all defined stops. #' @param reverse Logical. Reverse the palette order. Default `FALSE`. #' #' @return A character vector of hex color codes. #' @export #' #' @examples #' civilytics_palette() # all 7 qualitative colors #' civilytics_palette("seq_navy", n = 5) # 5-stop navy ramp #' civilytics_palette("div_navy_ember", n = 11, reverse = TRUE) civilytics_palette <- function(name = "qual", n = NULL, reverse = FALSE) { pal <- civilytics_palettes[[name]] if (is.null(pal)) { stop( "'", name, "' is not a valid palette. Choose from: ", paste(names(civilytics_palettes), collapse = ", "), call. = FALSE ) } if (reverse) pal <- rev(pal) if (is.null(n)) return(pal) if (startsWith(name, "qual")) { if (n > length(pal)) { warning( "Requested ", n, " colors from '", name, "' palette (max ", length(pal), "); recycling.", call. = FALSE ) pal <- rep_len(pal, n) } return(pal[seq_len(n)]) } grDevices::colorRampPalette(pal)(n) } #' Civilytics palette function (closure) #' #' Returns a closure `function(n)` suitable for passing to #' [discrete_scale()] or similar scale constructors. #' #' @param name Character. Palette name. See `names(civilytics_palettes)`. #' @param reverse Logical. Reverse the palette order. Default `FALSE`. #' #' @return A function that takes integer `n` and returns `n` hex color codes. #' @export #' #' @examples #' pal_fn <- civilytics_pal("qual") #' pal_fn(4) civilytics_pal <- function(name = "qual", reverse = FALSE) { function(n) civilytics_palette(name, n = n, reverse = reverse) } #' Civilytics color scale for ggplot2 #' #' Applies a Civilytics brand palette to the `colour` aesthetic. #' #' @param palette Character. Palette name. Defaults to `"qual"`. #' @param discrete Logical. `TRUE` (default) for categorical data; `FALSE` #' for a continuous gradient via [scale_color_gradientn()]. #' @param reverse Logical. Reverse the palette order. Default `FALSE`. #' @param ... Additional arguments passed to the ggplot2 scale function. #' #' @return A ggplot2 scale object. #' @export #' #' @examples #' library(ggplot2) #' ggplot(mpg, aes(displ, hwy, colour = class)) + #' geom_point() + #' scale_color_civilytics() #' #' ggplot(mpg, aes(displ, hwy, colour = cty)) + #' geom_point() + #' scale_color_civilytics("seq_navy", discrete = FALSE) scale_color_civilytics <- function(palette = "qual", discrete = TRUE, reverse = FALSE, ...) { if (discrete) { discrete_scale( "colour", palette = civilytics_pal(palette, reverse = reverse), ... ) } else { pal <- civilytics_palette(palette, reverse = reverse) scale_color_gradientn( colours = grDevices::colorRampPalette(pal)(256), ... ) } } #' Civilytics fill scale for ggplot2 #' #' Applies a Civilytics brand palette to the `fill` aesthetic. #' #' @param palette Character. Palette name. Defaults to `"qual"`. #' @param discrete Logical. `TRUE` (default) for categorical data; `FALSE` #' for a continuous gradient via [scale_fill_gradientn()]. #' @param reverse Logical. Reverse the palette order. Default `FALSE`. #' @param ... Additional arguments passed to the ggplot2 scale function. #' #' @return A ggplot2 scale object. #' @export #' #' @examples #' library(ggplot2) #' ggplot(mpg, aes(class, fill = class)) + #' geom_bar() + #' scale_fill_civilytics() #' #' ggplot(faithfuld, aes(waiting, eruptions, fill = density)) + #' geom_tile() + #' scale_fill_civilytics("seq_ember", discrete = FALSE) scale_fill_civilytics <- function(palette = "qual", discrete = TRUE, reverse = FALSE, ...) { if (discrete) { discrete_scale( "fill", palette = civilytics_pal(palette, reverse = reverse), ... ) } else { pal <- civilytics_palette(palette, reverse = reverse) scale_fill_gradientn( colours = grDevices::colorRampPalette(pal)(256), ... ) } }