quarto-file-standards
Standards derived from the quarto-file-standards skill
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.
2 Standard
These standards come from Quarto documentation found here:
They are also based comparing a well-structured page against an anti-pattern page. Follow the good pattern; don’t replicate the other one.
3 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.
4 Core Rules
4.2 2. Always use ourmirror::mirror_theme() — never theme_set() or custom themes
For a background tint, mirror_theme() + theme(plot.background=…) on top — don’t replace it.
4.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, leader_background_tint, sdb_background_tint) that already exist in shared_code_26.R.
4.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.
4.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
4.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.
4.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.
4.8 8. Every chart’s subtitle carries N
labs(subtitle = str_glue("N = {scales::number(n, big_mark = ',')}"))4.9 9. Tidyverse idioms
- 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
4.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.
5 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 |
6 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.
7 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.