color-conventions

The single source of truth for color in The Societal Mirror

0.1 Purpose

Use these standards when adding or reviewing any chart, table, or tinted panel in The Societal Mirror. Color in a Mirror chart is an encoding, not a decoration: every hue comes from the ourmirror palette functions or a named constant in shared_code_26.R, never from a bare hex sprinkled into a chunk. This page is the inventory of what those functions return and where each color is used, so a reviewer can tell at a glance whether a page is palette-compliant.

0.2 Standard

1 Color Conventions

Color in a Mirror chart is an encoding, not a decoration. Every hue comes from an ourmirror palette function or a named constant in shared_code_26.R — never from a bare hex typed into a chunk.

This skill is for authoring new charts. To convert a page that already has hard-coded colors, use color-migration.

1.1 The four rules

  1. Never hardcode a hex. Call a palette function or a named constant. Exactly three bare hex literals are sanctioned in the entire code base (catalogued below). Don’t add a fourth.
  2. Pick the palette by data type. Categorical for unordered groups, sequential for ordered values, stoplight for good→bad tiers, brand for fixed accents.
  3. Let the palette pick label ink. Segment and bar labels take their color from stoplight_ink() or the $ink returned alongside $fill — never hand-tune label contrast.
  4. Tint by survey. Every panel carries its survey family’s background tint. The tint is what visually distinguishes member from centre from admin pages.

This is the detailed catalogue behind Core Rule 3 in quarto-file-standards.

1.2 Choosing a palette

Start here. The data type decides the function:

Your data Function Returns
Unordered categories (≤4) mrr_pal_categorical(n) or scale_fill_mirror_d() character vector
One hue, light → dark (size tiers, ramps) mrr_pal_sequential(hue, n) character vector
Polarity axis (No…Yes, Lack…Enough) mrr_pal_stoplight(n, labels) list of $fill + $ink
Fixed brand accents shambhala_palette_function()[["Green"]] named character vector
Single-fill bar chart dark_bar_green constant one hex
Structural (gridlines, axis text, white/black) leave to mirror_theme()

If a chart needs more than 4 unordered categories, collapse the tail into “Other” rather than extending the categorical palette.

1.3 The palette functions

All outputs below are verified against the installed ourmirror package.

1.3.1 shambhala_palette_function() — brand palette

Underpins every derived palette.

Name Hex Role
Light Text #2E3C45 Axes / secondary text
Header Text #232D34 Website headers
Dark Text #101417 Primary text / dark ink
Grid #DCE0E3 Gridlines / neutral grey
Green #5AA678 Brand green (chart fills)
Crimson #D16471 Brand crimson
Yellow #F1C95B Brand yellow
Blue #587AA7 Brand blue

1.3.2 mrr_pal_categorical(n = NULL) — unordered groups

Fixed order: green, blue, crimson, yellow. Max 4.

mrr_pal_categorical()   # "#5AA678" "#587AA7" "#D16471" "#F1C95B"
mrr_pal_categorical(3)  # "#5AA678" "#587AA7" "#D16471"

# In a plot, prefer the scale wrapper:
ggplot(...) + scale_fill_mirror_d()

1.3.3 mrr_pal_stoplight(n = 4, labels = NULL) — polarity tiers

The most-used palette in the project. Returns a list with $fill and a contrast-matched $ink.

mrr_pal_stoplight(n = 3)
#> $fill  "#C25B60" "#ECB552" "#5AA678"
#> $ink   "white"   "#101417" "#101417"

mrr_pal_stoplight(n = 4)
#> $fill  "#C25B60" "#E8A24A" "#F1C95B" "#5AA678"
#> $ink   "white"   "#101417" "#101417" "#101417"

Pass labels to get a named vector you can index by response value — this is the robust form when factor order doesn’t match pole order:

stoplight <- mrr_pal_stoplight(n = 3, labels = c("Lack", "Some", "Enough"))
#> $fill    Lack      Some    Enough
#>       "#C25B60" "#ECB552" "#5AA678"
#> $ink    "white"  "#101417" "#101417"

response_order <- names(stoplight$fill)

ggplot(...) +
  geom_col(aes(fill = response)) +
  scale_fill_manual(
    values = stoplight$fill,
    breaks = response_order,
    limits = response_order
  ) +
  geom_text(aes(label = pct, color = response), show.legend = FALSE) +
  scale_color_manual(values = stoplight$ink, guide = "none")

Order labels negative → positive. To reverse a battery, reverse the labels vector — never swap individual hexes.

1.3.4 mrr_stoplight_constants() — named tiers

For pages needing a named tier beyond a simple n-step ramp (e.g. an “Other/grey” residual).

Name Hex
red #C25B60
orange #E8A24A
amber #F1C95B
lightgreen #A5B769
green #5AA678
grey #DCE0E3

1.3.5 mrr_pal_sequential(hue = "Green", n, blend = "white", direction = 1) — ordered ramps

Light → dark, deepest step is the anchor hue.

mrr_pal_sequential("Green", 4)     # "#DEEDE4" "#B2D5C0" "#86BD9C" "#5AA678"
mrr_pal_sequential("#401F09", 4)   # "#D8D2CD" "#A5968C" "#725A4A" "#401F09"  (brown)
mrr_pal_sequential("Green", 4, blend = "Grid")   # legacy mrr_color_steps look

1.3.6 stoplight_ink(fills, white_min = 4) — contrast-safe label ink

The standard way bar labels get their color (~30 call sites). Pass any fills, get back white or dark ink per fill, WCAG-validated at ≥4:1.

stoplight_ink("#5AA678")   # "#101417"  (dark ink on brand green)
stoplight_ink("#C25B60")   # "white"    (white on soft crimson)

geom_text(aes(label = pct), color = stoplight_ink(dark_bar_green))

The crimson pole carrying white while the other three carry dark ink is validated, not a bug. Let $ink decide.

1.3.7 mrr_color_steps(color_number, levels, blend_number = 4) — green ramps

mrr_color_steps(5, 4)   # green_step_2: "#5AA678" "#7CB594" "#9FC4B1" "#C2D4CD"
mrr_color_steps(5, 5)   # green_step_5: 5-step ramp

Still supported (thin wrapper over mrr_pal_sequential), but prefer mrr_pal_sequential() for new work.

1.4 Constants in shared_code_26.R

Defined once in shared code, referenced across pages. Use these rather than re-deriving.

Constant Definition Value
dark_bar_green shambhala_palette_function()[["Green"]] #5AA678
member_background_tint literal #FEF7F4 pale peach
centre_background_tint literal #F4FFF5 pale mint
sdb_background_tint literal #F4F4F4 light grey
green_step_2 mrr_color_steps(5, 4) 4-step green ramp
green_step_5 mrr_color_steps(5, 5) 5-step green ramp
membership_colors shambhala_palette_function()[c("Yellow","Green","Blue")] named Non member / Member / Past member yellow, green, blue
css_default_hover girafe_css_bicolor("yellow", "red") ggiraph hover

css_default_hover is wired into set_girafe_defaults(), so it applies to every ggiraph plot project-wide — no by-name call needed in a page.

1.5 Background tints by survey family

The three tints are the most-referenced colors in the project. Every chart carries its family’s tint in both backgrounds:

Survey family Constant Hex
Member (m26, m25, m24) member_background_tint #FEF7F4
Centre/Leader (C26, L25, L24) centre_background_tint #F4FFF5
Admin/SDB sdb_background_tint #F4F4F4
theme(
  plot.background  = element_rect(fill = member_background_tint),
  panel.background = element_rect(fill = member_background_tint)
)

The constant for the centre family is centre_background_tint. There is no leader_background_tintshared_code_26.R defines only the three names above, and referencing any other tint name will fail at render with “object not found”.

1.6 Sanctioned bare hexes

Only three bare hex literals exist in the code base. Each is intentional. Nothing else should introduce a fourth.

File:Line Hex Context
centre_profile.qmd:750 #401F09 mrr_pal_sequential("#401F09", 4) brown-ramp anchor
what_do_they_value_and_need.qmd:755, 1016 #f5f0e8 gt cell_fill() pale tan — table-only, not a chart color

1.7 Before you finish

1.8 Quick diagnostic

# Should be 0 (bare hexes) — only the three sanctioned exceptions above:
grep -nE '#[0-9A-Fa-f]{6}' page.qmd

# Should be > 0 on any page that draws charts:
grep -c "shambhala_palette_function\|mrr_pal_\|dark_bar_green\|_background_tint" page.qmd

A page with bare hexes outside the sanctioned three, or one that draws charts without touching the palette functions, is not conforming — regardless of whether it renders.