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
- 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.
- Pick the palette by data type. Categorical for unordered groups, sequential for ordered values, stoplight for good→bad tiers, brand for fixed accents.
- Let the palette pick label ink. Segment and bar labels take their color from
stoplight_ink()or the$inkreturned alongside$fill— never hand-tune label contrast. - 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 look1.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 rampStill supported (thin wrapper over mrr_pal_sequential), but prefer mrr_pal_sequential() for new work.
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_tint — shared_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.qmdA 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.