quarto-file-standards
Standards derived from the quarto-file-standards skill
0.1 Purpose
Use these standards when authoring, reviewing, or refactoring any .qmd page in The Societal Mirror project. Captures the house style for Quarto file structure, chunk organization, helper-function placement, theme/palette compliance, and .llms.md export behavior.
0.2 Standard
1 Quarto File Standards
These standards derive from the Quarto documentation:
They are also based on comparing a well-structured page (leadership_and_service_roles.qmd, ~950 lines, 17 chunks) against an anti-pattern page. Follow the good pattern; don’t replicate the bad one.
1.1 The Shape of a Good Page
A .qmd page should look like one thin helper library at the top, then short consumer chunks that call it.
┌─ YAML header
├─ Setup chunk (echo=FALSE, include=FALSE)
│ source(shared_code_26.R, shared_functions_26.R, shared_sdb_code_26.R)
├─ Helper chunk (define plot_* / table_* / calc_* once, with roxygen docs)
├─ Narrative markdown
├─ Chunk: data prep + call plot_xxx(q_var)
├─ Narrative markdown
├─ Chunk: data prep + call plot_xxx(other_var)
└─ ...
One question → one short chunk. Each chunk does prep + call helper, not prep + helper-definition + call.
1.2 Core Rules
1.2.2 2. Always use ourmirror::mirror_theme() — never theme_set() or custom themes
Project CLAUDE.md requires this. If you want a background tint, mirror_theme() + theme(plot.background=…) on top — don’t replace it.
1.2.3 3. Always use shambhala_palette_function() — never hardcoded hex colors
No "#2E8B57", "#C62828", "#F9A825" sprinkled through chunks. Colors come from the palette function or named constants (dark_bar_green, member_background_tint, centre_background_tint, sdb_background_tint) that already exist in shared_code_26.R. See color-conventions for the full catalogue and which palette to pick for a given data type.
1.2.4 4. Define helpers once, in one helper chunk, with roxygen docs
#| label: helpers
#| include: false
#' Plot a one-level summary of a categorical question
#'
#' @param .data tibble filtered to respondents answering this question
#' @param q_var bare column name of the question
#' @param title chart title
plot_1_level <- function(.data, q_var, title) { ... }If you catch yourself writing html_safe <- function(...) or apply_named_recode <- function(...) in a second chunk, stop: move it to the helper chunk.
1.2.5 5. Use Quarto semantic divs, not custom HTML or flat H1 stacks
::: panel-tabsetto group related views of the same question::: {.callout-note icon=false appearance="simple"}for quotes and side notes::: column-screen-insetfor wide charts##/###headers for hierarchy — avoid a chain of 20+ flat#headers
1.2.6 6. Use Quarto cross-reference labels
#| label: fig-belonging-by-generation
#| fig-cap: "Sense of belonging by generation"
#| label: tbl-capacity-counts
#| tbl-cap: "Capacity counts by role"
fig-… and tbl-… prefixes enable @fig-belonging-by-generation cross-refs and produce proper “Figure 1” numbering.
1.2.7 7. Suppress chunk output globally, opt in explicitly
At the top of every page:
knitr::opts_chunk$set(echo = FALSE, message = FALSE, warning = FALSE)Don’t repeat echo=FALSE, message=FALSE, warning=FALSE on every chunk.
1.2.8 8. Every chart’s subtitle carries N
labs(subtitle = str_glue("N = {scales::number(n, big_mark = ',')}"))1.2.9 9. Tidyverse idioms (from project CLAUDE.md)
- Native pipe
|>— never%>% .by =grouping — nevergroup_by() |> ungroup()join_by()— neverby = c("a" = "b")- On joins expected to be 1:1, add
multiple = "error"/unmatched = "error"so a bad key fails loudly instead of silently fanning out or dropping rows reframe()— for summaries that return more than one row per group (e.g.reframe(q = quantile(x, c(.25, .5, .75)), .by = group));summarise()is for one row per groupmap() |> list_rbind()— nevermap_dfr()stringr— nevergrepl/gsub/substr/nchar/tolower/paste0to_factor()from labelled — prefer overhaven::as_factor(.x, levels="labels") |> as.character()chains- Data masking in helpers: embrace
{ var }for bare column arguments,.data[["name"]]for character column names, andacross(where(...), ...)for multiple columns — don’teval(parse(text = ...))or paste column names into strings - Prefix a helper’s non-standard first data argument with a dot (
function(.data, ...)) so it doesn’t collide with tidy-eval names
1.2.10 10. Prefer static ggplot + mirror_theme() over raw girafe widgets by default
Interactive charts have a real cost: they bloat HTML, and every girafe() widget vanishes from the .llms.md export that screen readers and LLMs consume. Use ggiraph intentionally when tooltips add value, not reflexively for every chart.
1.3 Anti-Patterns to Refuse
When reviewing or refactoring, flag any of these — they are the signature of the centre_survey.qmd anti-pattern:
| Smell | Why it’s bad |
|---|---|
| Same helper function defined in 10+ chunks | Copy-paste drift; every edit has to be applied N times |
Banner comments like # --------------------- every 20 lines |
Signals the chunk is trying to be its own file; break it up instead |
dir.create("images", ...) with no PNG write downstream |
Dead code |
| Hardcoded hex colors in chart code | Breaks palette cohesion and dark-mode-ready theming |
Custom theme_set(...) at file top |
Overrides mirror_theme() silently |
Flat sequence of 20+ # headers |
Should be ## under a few # sections, or tabsets |
haven::as_factor(.x, levels = "labels") \|> as.character() |
Use to_factor() / to_character() from labelled |
Every question rendered as a girafe() widget |
HTML balloons; .llms.md loses the charts entirely |
# USER SETTINGS / # HELPER FUNCTIONS blocks inside every chunk |
That’s a script template, not a Quarto page; hoist to the helper chunk |
1.4 Quick Diagnostic
If you’re unsure whether a page follows the standard, grep it:
# Should be 0 or very small:
grep -c "^html_safe <- function" page.qmd # helper redefinitions
grep -c "^apply_named_recode <- function" page.qmd
grep -c "^# -----" page.qmd # banner dividers
grep -cE '#[0-9A-Fa-f]{6}' page.qmd # hardcoded hex colors
# Should be > 0:
grep -c "mirror_theme()" page.qmd
grep -c "shambhala_palette_function" page.qmd
grep -cE "(panel-tabset|callout-note)" page.qmdA page with zero mirror_theme() calls and dozens of helper redefinitions is not conforming to project standards, regardless of whether it renders.
1.5 When Refactoring an Offending Page
The refactor path is usually: 1. Extract the repeated helpers into one helper chunk (or, if reusable across pages, into back-end/shared_functions_26.R). 2. Replace every girafe/custom theme block with a call to one parametric plot function that uses mirror_theme() + shambhala_palette_function(). 3. Collapse per-question USER SETTINGS blocks into rows of a tribble() that drives the helpers. 4. Replace flat # header stacks with ## + ::: panel-tabset. 5. Delete dead dir.create() calls and unused imports.
Expect a well-run refactor to reduce line count by roughly 4× and bring the page into theme/palette compliance as a side effect.