# Academic Figures Monochrome

> Produces journal-ready, print-safe academic figures in a monochrome house standard where series are separated by marker shape, dash pattern and texture rather than by colour, the legend sits outside the plot area at the top or the bottom and never inside it, and colour, where it is genuinely needed, is restricted to a dark navy and a dark wine red used sparingly. Covers event studies, coefficient plots, group trend plots, distributions, binned scatters for regression discontinuity, hatched bar charts, and grey-ramp maps, with working Stata and matplotlib code, caption and note requirements, and the rule that every figure is regenerated by a script rather than edited by hand. Use this skill whenever someone needs a figure for a paper, thesis, proposal or slide deck, asks for an event-study or coefficient plot, asks to make a chart publication quality, says their figure is too colourful or unreadable in print, or wants an existing chart restyled to a journal standard. Trigger also on vague requests such as "mak

- Skill: `ingridleiria/academic-figures-monochrome` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ingridleiria/academic-figures-monochrome`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ingridleiria/academic-figures-monochrome/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: ingridleiria (https://skillmd.com/u/ingridleiria)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ingridleiria/academic-figures-monochrome

---


# Academic Figures, Monochrome

A figure in an academic document has to survive three conditions at once. It will be printed in black and white by at least one referee, because many still print. It will be read by someone who cannot distinguish red from green, which is roughly one man in twelve. And it will be looked at for about five seconds before the reader decides whether to work at it or move on. Colour-coded series fail the first two conditions completely, and a figure with a legend sitting on top of the data fails the third.

The failure this prevents is specific and extremely common: a figure that is perfectly legible on the author's screen and unreadable everywhere else. Four coloured lines become four indistinguishable grey lines on the printer. A shaded confidence band in a light tint becomes a grey smear that obscures the point estimates it was meant to accompany. A legend placed in the top-right corner covers the two post-treatment coefficients that carry the result. The author never sees any of this, because the author never prints it.

The cost is not aesthetic. It is that the figure is usually carrying the paper's main identification evidence. An event-study plot is where a referee decides whether to believe the parallel trends assumption, and a referee who cannot read it does not conclude that the design is fine. They conclude that the pre-period is unconvincing, and that comment costs a revision cycle.

## When to use this, and when not to

Use it for any figure destined for an academic document: an article, a thesis chapter, a referee response, a proposal, a poster, or a seminar deck built from the same results. Use it when restyling an existing figure, when a coauthor's figure does not match the rest of the paper, and when setting up a project so that every figure produced later is consistent by default, which is the cheapest time to do it.

Do not use it for a table, which is `academic-tables-booktabs` for typography and `regression-table-production` or `descriptive-statistics-tables` for content. The decision between a table and a figure is worth stating: use a figure when the reader needs to see a shape, a trend, or the relationship among many estimates at once, and a table when the reader needs to read exact numbers. Eight specifications with one coefficient each is a coefficient plot; two specifications with nine coefficients each is a table.

Do not use it to decide what to estimate or plot, which is `econometrician`, or to decide whether the pre-period is convincing, which is `identification-defense`. This skill makes the evidence legible; it does not judge it.

Do not use it for slide-only graphics with no paper behind them, where `conference-presentation-deck` sets the different constraints of a projected image seen for thirty seconds from twelve metres away.

## What you need before starting

**The estimates or data to be plotted, from a stored output rather than a screen.** A saved estimates file, a results matrix, a CSV of coefficients, or the analysis dataset. Missing: do not rebuild numbers by typing them. Go back to the estimation script and have it write the values to a file, which takes ten minutes and removes an entire class of error.

**The one message the figure carries.** In a sentence. Missing: write the sentence first. A figure without a stated message becomes a figure with three messages, and the reader receives none of them. The test: if you cannot say what the reader should conclude in one sentence, the figure is not ready to be drawn.

**The units of the vertical axis, as the reader should interpret them.** Percentage points, log points, standard deviations of the outcome, counts per thousand. Missing: take them from the specification and state them; an axis labelled "coefficient" is uninterpretable and is the most common labelling failure.

**The confidence level and the clustering.** These go in the note, always. Missing: get them from the estimation output rather than assuming ninety-five percent.

**The final printed width.** A single-column figure at about 8 cm and a full-width figure at about 16 cm need different font sizes and different numbers of series. Missing: assume full text width in a single-column layout, around 13 cm, and set fonts so that axis labels are 8 to 10 point at that size.

**The output format the document needs.** Vector for LaTeX, meaning PDF or EPS; high-resolution raster for word processors that handle vector badly, meaning PNG at 300 dots per inch or better. Missing: produce both, which costs one extra line.

**The document's font.** Missing: use a serif face matching the body text where the journal uses serif, otherwise the document's sans face. One family per figure and the same family across every figure in the document.

## The method

The house standard that these steps apply is set out in full in the section immediately following, and the two are meant to be read together.

1. **Write the one-sentence message, then choose the figure type from it.** The mapping is mechanical and should not be agonised over. Effects over event time is an event study. Many single coefficients compared is a coefficient plot. Two or three group means over calendar time is a trend plot. A distribution comparison is a density or histogram pair. A discontinuity is a binned scatter with fitted lines on each side. A composition or a small set of category comparisons is a hatched bar chart. Geography is a grey-ramp choropleth. If the message needs two figure types, it is two figures.

2. **Count the series.** Four or fewer, proceed. Five or more, split into small multiples or cut. Decide this before writing any plotting code, because retrofitting a five-series figure into panels after it is built takes longer than starting again.

3. **Assign the separation channels before assigning any grey.** Line order is solid, long dash, dot, dash-dot. Marker order is filled circle, hollow square, filled triangle, hollow diamond. Bar hatch order is solid grey, diagonal, cross, dotted. Only after these are assigned does grey value vary, and it varies in the same order as the series ordering so the two channels reinforce each other rather than fighting.

4. **Decide whether colour is needed at all, and default to no.** The test to apply, honestly: name the element that would be lost without colour, and say why shape and pattern cannot carry it. Two acceptable answers exist in practice. One series must be picked out of many for the reader's attention, in which case the highlighted series takes navy and everything else is grey. Two categories carry an opposed meaning that the reader must not confuse, such as gains against losses, in which case navy and wine are used and the shapes still differ. Every other answer means no colour.

5. **Place the legend outside, above or below, in one row.** Or drop it entirely and label the lines directly at their right-hand ends, which is better whenever the lines do not converge there. Where a legend entry describes a confidence band as well as a line, say so in the note rather than adding a second legend row.

6. **Set the axes to be interpretable without the text.** The vertical axis label carries the outcome and its units. The horizontal axis label carries the variable and, for event studies, states that zero is the reference period. Do not truncate a vertical axis that includes zero in a plot of effects, and where a truncated axis is unavoidable, say so in the note. Tick labels are in the reader's units, not in scientific notation.

7. **Draw the reference lines.** A horizontal line at zero on every plot of effects, a vertical dashed line at the treatment date or the cutoff, thin and dark grey so they read as furniture rather than as data.

8. **Set the fonts for the final printed size, not for the screen.** Test by exporting at the intended width and viewing at one hundred percent. Axis and tick labels at 8 to 10 point at final size, legend the same or one point smaller, no text smaller than 7 point anywhere.

9. **Run the greyscale test before delivering.** Convert the exported file to greyscale and look at it. This is not optional and it is not a formality; it is the check that the entire standard exists to pass. Anything that becomes ambiguous is fixed at the pattern level, never by making a grey darker, because darkening greys is how figures end up with four indistinguishable near-blacks.

10. **Write the caption and the note.** The caption states what is plotted, the sample, the period, and the estimator. The note states the confidence level, the clustering, the reference period, the bandwidth if there is one, and the source. The standard: a reader who sees only the figure, its caption and its note should not need the body text to understand what they are looking at.

11. **Save the script beside the output and regenerate rather than edit.** Every figure is produced by a script in the project. The check is the same one used for tables: delete the exported file, rerun the script, confirm it comes back identical. A figure that has been touched in a graphics editor cannot pass this and will eventually diverge from the results.

## The house standard

This is the standard the method above applies, stated once in full. Everything else in the skill implements it.

**Monochrome first.** Every figure is designed to work in black, greys and white. Colour is not the starting point that gets removed for print; absence of colour is the starting point, and colour is added only where a specific reason survives scrutiny.

**Series are separated by marker shape, dash pattern and texture, not by colour.** Line series differ first by dash pattern, then by grey value. Point series differ by marker shape and by fill, meaning solid against hollow. Bars and areas differ by hatch texture and by grey value. A figure passes only if it remains readable when every ink is converted to the same black.

**The legend sits outside the plot area, at the top or the bottom, never inside it.** Inside the plot area, a legend either covers data or forces the axes to be padded until the data is small. Above or below, in one row where it fits, it costs a few millimetres and covers nothing. Where two or three series are clearly separable, direct labels at the ends of the lines are better than a legend and should be preferred.

**Where colour is genuinely needed, it is restricted to a dark navy and a dark wine red, used sparingly.** Sparingly means one or two elements in a figure, for a reason that can be stated: a single highlighted series against grey context, a treated unit against a donor pool, one region of a map. Both chosen colours are dark enough to remain distinguishable from each other and from mid-grey when converted to greyscale, which ordinary saturated red and blue are not. Never use colour as the only channel carrying information; the dash pattern or marker must still differ, so that the figure survives the conversion.

| Role | Value |
| Primary series | #000000 black |
| Second series | #404040 dark grey |
| Third series | #7F7F7F mid grey |
| Fourth series | #A6A6A6 light grey |
| Accent one, sparingly | #1F3864 dark navy |
| Accent two, sparingly | #7B1E28 dark wine red |
| Confidence bands | grey fill at 20 to 30 percent opacity, or capped whiskers |
| Reference lines, zero, cutoff, treatment date | thin, dark grey, dashed |

Background is white. No chart border box, no gridlines except optionally faint horizontal ones where the reader must compare levels across a wide plot, no three-dimensional effects, no gradients, no drop shadows.

**Four series is the practical limit.** Beyond four, greys and dash patterns stop being reliably distinguishable in print. The correct response to a fifth series is not a fifth grey; it is small multiples, meaning a panel of subplots sharing axes, or a reduction of the question the figure is answering.

**No title inside the figure file.** The document's caption carries the title and the note. A title baked into the image duplicates the caption and cannot be edited by a copy editor.

## Standard figure types

**Event study.** Event time on the horizontal axis with the reference period marked and normalised to zero, point estimates as markers with capped vertical confidence intervals, a horizontal line at zero, a vertical dashed line between the last pre-period and the first post-period, endpoint coefficients binned and labelled as such. Pre-period coefficients are the identification evidence and get exactly the same visual weight as post-period ones; shrinking or greying them is a misrepresentation. Where the pre-period test has little power, that belongs in the note, and `identification-defense` explains why it matters.

**Coefficient plot.** Estimates from several specifications, outcomes or subgroups on a common scale, with specification labels on the vertical axis reading top to bottom in a meaningful order, markers for point estimates, horizontal whiskers for intervals, and a vertical line at zero. Sort by something the reader can name, never by magnitude alone unless magnitude is the message.

**Trends by group.** Group means over calendar time, treatment date marked with a vertical dashed line, series separated by dash pattern, direct labels at the line ends in preference to a legend. Where group sizes differ greatly, say so in the note, because a noisy line from a small group looks like a finding.

**Distributions.** Kernel densities or histograms by group, separated by dash pattern for densities and by hatch for histograms, with a vertical line at any threshold that matters. Two groups at most on one panel; three or more go to small multiples.

**Binned scatter for regression discontinuity.** Bins as hollow circles with the bin count stated in the note, fitted lines estimated separately on each side using the same polynomial order as the estimation, the cutoff as a vertical dashed line, and the bandwidth stated. Do not fit a line across the cutoff and do not use a smoother whose settings differ from the estimation.

**Bar charts.** Hatched and grey, never coloured, with the bars ordered meaningfully and the axis starting at zero. Error bars where the quantities are estimated, and no error bars where they are population counts, since spurious whiskers on a census are a claim about uncertainty that does not exist.

**Maps.** A grey ramp choropleth with at most five classes, the class breaks stated and justified in the note, boundaries thin and mid grey, no basemap, no labels other than those the argument needs, and the legend outside. Where one region must be picked out, navy fill for that region and grey for everything else.

## Implementation, Stata

Set the style once for the project in a preferences do-file, then call the same globals from every figure script. This is what makes twelve figures in a thesis look like one document rather than twelve.

```stata
*--- figure_prefs.do : run once at the top of every figure script ---
graph set window fontface "Times New Roman"

global GOPT      graphregion(color(white) margin(medium)) ///
                 plotregion(color(white) lstyle(none)) ///
                 ysize(3.2) xsize(5.2) scale(1.0)
global GLEG      legend(position(6) ring(1) rows(1) region(lstyle(none)) size(small))
global GREY1     black
global GREY2     "gs6"
global GREY3     "gs9"
global GREY4     "gs12"
global NAVY      "26 56 100"      // #1F3864, use sparingly
global WINE      "123 30 40"      // #7B1E28, use sparingly
```

An event-study plot, with coefficients written to a file by the estimation script rather than typed:

```stata
*--- fig2_event_study.do ---
do "$code/figure_prefs.do"
use "$out/eventstudy_coefs.dta", clear     // vars: etime b se
gen lo = b - 1.96*se
gen hi = b + 1.96*se

twoway (rcap hi lo etime, lcolor("$GREY2") lwidth(medthin)) ///
       (scatter b etime, mcolor(black) msymbol(O) msize(medium)) ///
       , ///
       yline(0, lpattern(solid) lcolor("$GREY3") lwidth(thin)) ///
       xline(-0.5, lpattern(dash) lcolor("$GREY3") lwidth(thin)) ///
       xlabel(-5(1)5, labsize(small)) ///
       ylabel(-4(2)6, labsize(small) angle(horizontal) format(%3.0f)) ///
       xtitle("Years relative to rollout (-1 is the reference period)", size(small)) ///
       ytitle("Effect on employment (percentage points)", size(small)) ///
       legend(off) $GOPT

graph export "$fig/fig2_event_study.pdf", replace
graph export "$fig/fig2_event_study.png", replace width(2400)
```

Two group trends, separated by dash pattern with the legend below the plot:

```stata
*--- fig1_trends.do ---
do "$code/figure_prefs.do"
use "$out/group_means.dta", clear           // vars: year mean_treat mean_comp

twoway (line mean_treat year, lpattern(solid) lcolor(black) lwidth(medthick)) ///
       (line mean_comp  year, lpattern(dash)  lcolor("$GREY3") lwidth(medthick)) ///
       , ///
       xline(2016, lpattern(shortdash) lcolor("$GREY3") lwidth(thin)) ///
       xlabel(2012(2)2022, labsize(small)) ///
       ylabel(, labsize(small) angle(horizontal)) ///
       xtitle("Year", size(small)) ///
       ytitle("Firm registrations per 1,000 residents", size(small)) ///
       legend(order(1 "Treated municipalities" 2 "Never treated") ///
              position(6) ring(1) rows(1) region(lstyle(none)) size(small)) ///
       $GOPT

graph export "$fig/fig1_trends.pdf", replace
```

A hatched bar chart, where texture rather than grey does the separating work:

```stata
graph bar (mean) share, over(category, label(labsize(small))) ///
      over(period) asyvars ///
      bar(1, color(gs14) lcolor(black) lwidth(thin)) ///
      bar(2, color(gs14) lcolor(black) lwidth(thin) fintensity(inten30)) ///
      ytitle("Share of firms", size(small)) ///
      legend(position(6) ring(1) rows(1) region(lstyle(none)) size(small)) ///
      $GOPT
```

Stata's fill textures are limited compared with matplotlib's hatching, and ggplot2 has none at all without an extra package. Where more than two textures are needed, either move the figure to matplotlib or reduce the number of categories, which is usually the better answer anyway.

## Implementation, Python

Put the style and the helpers in one module imported by every figure script.

```python
# plotting.py : the house standard, imported by every figure script
import matplotlib as mpl
import matplotlib.pyplot as plt

GREY = ["#000000", "#404040", "#7F7F7F", "#A6A6A6"]
NAVY = "#1F3864"   # accent one, sparingly
WINE = "#7B1E28"   # accent two, sparingly

DASHES  = ["solid", (0, (6, 2)), (0, (1, 1.5)), (0, (6, 2, 1, 2))]
MARKERS = ["o", "s", "^", "D"]
FILLS   = ["full", "none", "full", "none"]
HATCHES = ["", "///", "xxx", "..."]

mpl.rcParams.update({
    "font.family":       "serif",
    "font.serif":        ["Times New Roman", "DejaVu Serif"],
    "font.size":         9,
    "axes.labelsize":    9,
    "axes.titlesize":    9,
    "xtick.labelsize":   8,
    "ytick.labelsize":   8,
    "legend.fontsize":   8,
    "axes.spines.top":   False,
    "axes.spines.right": False,
    "axes.edgecolor":    "#404040",
    "axes.linewidth":    0.8,
    "figure.facecolor":  "white",
    "axes.facecolor":    "white",
    "axes.grid":         False,
    "legend.frameon":    False,
    "savefig.bbox":      "tight",
    "savefig.dpi":       400,
})

def legend_below(ax, ncol=None, handles=None, labels=None):
    """Legend outside the plot area, centred below it. Never inside."""
    h, l = (handles, labels) if handles else ax.get_legend_handles_labels()
    ax.legend(h, l, loc="upper center", bbox_to_anchor=(0.5, -0.18),
              ncol=ncol or len(l), frameon=False, handlelength=2.8)

def save(fig, stem):
    fig.savefig(f"figures/{stem}.pdf")
    fig.savefig(f"figures/{stem}.png")
    plt.close(fig)
```

An event-study plot, reading coefficients from the estimation output:

```python
# fig2_event_study.py
import pandas as pd
import matplotlib.pyplot as plt
from plotting import GREY, legend_below, save

d = pd.read_csv("output/eventstudy_coefs.csv")   # etime, b, se
d["lo"] = d.b - 1.96 * d.se
d["hi"] = d.b + 1.96 * d.se

fig, ax = plt.subplots(figsize=(5.2, 3.2))
ax.axhline(0, color=GREY[2], lw=0.8)
ax.axvline(-0.5, color=GREY[2], lw=0.8, ls=(0, (5, 3)))
ax.vlines(d.etime, d.lo, d.hi, color=GREY[1], lw=1.0)
ax.hlines(d.lo, d.etime - 0.12, d.etime + 0.12, color=GREY[1], lw=1.0)
ax.hlines(d.hi, d.etime - 0.12, d.etime + 0.12, color=GREY[1], lw=1.0)
ax.plot(d.etime, d.b, ls="none", marker="o", ms=4.5,
        mfc="black", mec="black")

ax.set_xlabel("Years relative to rollout (-1 is the reference period)")
ax.set_ylabel("Effect on employment (percentage points)")
ax.set_xticks(range(-5, 6))
save(fig, "fig2_event_study")
```

Multiple series separated by dash pattern and marker, with the legend below:

```python
# fig1_trends.py
import pandas as pd
import matplotlib.pyplot as plt
from plotting import GREY, DASHES, MARKERS, FILLS, legend_below, save

d = pd.read_csv("output/group_means.csv")        # year, group, mean
groups = ["Treated", "Not yet treated", "Never treated"]

fig, ax = plt.subplots(figsize=(5.2, 3.2))
for k, g in enumerate(groups):
    s = d[d["group"] == g]
    ax.plot(s["year"], s["mean"], color=GREY[k], ls=DASHES[k], lw=1.4,
            marker=MARKERS[k], ms=4, fillstyle=FILLS[k], label=g)

ax.axvline(2016, color=GREY[2], lw=0.8, ls=(0, (5, 3)))
ax.set_xlabel("Year")
ax.set_ylabel("Firm registrations per 1,000 residents")
legend_below(ax, ncol=3)
save(fig, "fig1_trends")
```

Bars separated by hatch texture rather than by colour:

```python
# fig4_shares.py
import numpy as np, matplotlib.pyplot as plt
from plotting import GREY, HATCHES, legend_below, save

cats  = ["Micro", "Small", "Medium", "Large"]
pre   = [0.42, 0.31, 0.19, 0.08]
post  = [0.36, 0.33, 0.22, 0.09]
x, w  = np.arange(len(cats)), 0.38

fig, ax = plt.subplots(figsize=(5.2, 3.0))
ax.bar(x - w/2, pre,  w, facecolor="white", edgecolor="black",
       hatch=HATCHES[0], linewidth=0.8, label="2015")
ax.bar(x + w/2, post, w, facecolor="white", edgecolor="black",
       hatch=HATCHES[1], linewidth=0.8, label="2022")
ax.set_xticks(x, cats)
ax.set_ylabel("Share of firms")
legend_below(ax, ncol=2)
save(fig, "fig4_shares")
```

The single highlighted series, the one case where an accent colour earns its place:

```python
# fig5_donor_pool.py : treated unit in navy, donor pool in light grey
for unit in donors:
    ax.plot(t, paths[unit], color=GREY[3], lw=0.7, alpha=0.8)
ax.plot(t, paths["treated"], color=NAVY, lw=1.8, ls="solid",
        marker="o", ms=3.5, label="Treated region")
ax.plot(t, synthetic, color="black", lw=1.4, ls=(0, (6, 2)),
        label="Synthetic comparison")
legend_below(ax, ncol=2)
```

Note what makes that last block acceptable under the standard: the navy is one element, it is doing a job that grey cannot do because the donor pool is already grey, and the two labelled series still differ in dash pattern, so the figure survives greyscale conversion with only a loss of emphasis rather than a loss of information.

## Implementation, R with ggplot2

Same standard, same three channels, a third toolchain. Put the theme and the scales in one file sourced by every figure script, exactly as `plotting.py` and the Stata preferences do-file do.

```r
# plotting.R : the house standard, sourced by every figure script
library(ggplot2)

GREY    <- c("#000000", "#404040", "#7F7F7F", "#A6A6A6")
NAVY    <- "#1F3864"   # accent one, sparingly
WINE    <- "#7B1E28"   # accent two, sparingly

DASHES  <- c("solid", "22", "11", "4212")   # ggplot2 hex dash strings
SHAPES  <- c(16, 15, 17, 18)                # solid circle, square, triangle, diamond
HOLLOW  <- c(1, 0, 2, 5)                    # the hollow equivalents

theme_house <- function(base_size = 9) {
  theme_classic(base_size = base_size, base_family = "serif") +
    theme(
      panel.grid       = element_blank(),
      axis.line        = element_line(colour = "#404040", linewidth = 0.3),
      legend.position  = "bottom",          # outside the panel, never inside
      legend.title     = element_blank(),
      legend.key       = element_blank(),
      legend.margin    = margin(t = -2),
      plot.title        = element_blank(),  # the caption carries the title
      strip.background = element_blank()
    )
}

save_fig <- function(p, stem, w = 5.2, h = 3.0) {
  ggsave(paste0("figures/", stem, ".pdf"), p, width = w, height = h)
  ggsave(paste0("figures/", stem, ".png"), p, width = w, height = h, dpi = 400)
}
```

An event study, with the reference period normalised to zero and the pre-period given the same visual weight as the post-period:

```r
# fig2_event_study.R
es <- readr::read_csv("output/event_study.csv")   # k, b, lo, hi

p <- ggplot(es, aes(k, b)) +
  geom_hline(yintercept = 0, colour = GREY[2], linetype = "22", linewidth = 0.3) +
  geom_vline(xintercept = -0.5, colour = GREY[2], linetype = "22", linewidth = 0.3) +
  geom_errorbar(aes(ymin = lo, ymax = hi), width = 0.12, colour = "black",
                linewidth = 0.4) +
  geom_point(shape = SHAPES[1], size = 1.6, colour = "black") +
  labs(x = "Years relative to adoption", y = "Effect on log employment") +
  theme_house()

save_fig(p, "fig2_event_study")
```

Group trends separated by dash pattern and grey value, with the legend below and no colour at all:

```r
p <- ggplot(trends, aes(year, mean, linetype = group, colour = group)) +
  geom_line(linewidth = 0.6) +
  geom_vline(xintercept = 2018, colour = GREY[2], linetype = "22", linewidth = 0.3) +
  scale_linetype_manual(values = DASHES) +
  scale_colour_manual(values = GREY) +
  guides(linetype = guide_legend(nrow = 1)) +
  labs(x = NULL, y = "Mean registrations per 1,000 firms") +
  theme_house()
```

Two things about ggplot2 are worth knowing before you rely on it for this standard.

First, it has no native hatching. `geom_bar` fills with colour only, so a bar chart separated by texture needs the `ggpattern` package and `geom_bar_pattern`, with `pattern = "stripe"` and `pattern_angle` varied across categories, or `pattern = "none"` plus grey fills where two categories are enough. The package is not part of the tidyverse and has to be installed and recorded in `renv.lock` like any other dependency. Where you would rather not add it, the same rule applies as in Stata: reduce to two categories separated by grey value, which is usually the better figure anyway.

Second, the default palettes are colour palettes and every one of them fails the greyscale test, `viridis` included, because a perceptually uniform colour ramp is not a perceptually uniform grey ramp. Set `scale_colour_manual` and `scale_fill_manual` explicitly in every plot, or set them once through `options(ggplot2.discrete.colour = GREY, ggplot2.discrete.fill = GREY)` in `plotting.R`. A figure that inherits the default palette will look fine on screen and print as three indistinguishable mid-greys.

For maps, `sf` plus `geom_sf` with `scale_fill_grey()` and at most five classes gives the choropleth in the standard; set `na.value = "white"` and put the class breaks in the note. For small multiples, `facet_wrap` with `strip.background = element_blank()` is already in `theme_house` above.

## Caption and note

The caption is a sentence, not a label. "Figure 2: Effect of rollout on employment, event-study estimates" is a caption. "Figure 2: Event study" is not.

The note carries, in this order: the sample and period, the estimator, the confidence level, the clustering level, the reference period for an event study, the bandwidth and kernel for a discontinuity plot, the number of bins for a binned scatter, and the source. Where the vertical axis is truncated, the note says so.

A figure whose caption and note do not together explain it is an incomplete figure, and the test is to read the figure with the body text covered.

## Worked example

**Situation.** Priya Raman had a thesis chapter with nine figures, produced over two years in three different tools, going to an external examiner in five weeks. Her supervisor's only comment on the figures was that they looked inconsistent. The chapter's central exhibit was an event-study plot of a fuel subsidy reform's effect on freight volumes, with estimates for three cargo classes shown together.

**Task.** Bring nine figures to one standard, all regenerable, all readable in print, in the time available alongside the actual writing.

**Action.** The audit came first and took two hours: every figure listed with its type, its series count, its tool, and whether a script existed. Four had scripts. Three had been made in a spreadsheet and the underlying numbers were in a file nobody could locate. Two had been edited by hand after export, one of them to fix an axis label. The two hand-edited ones were the ones that mattered, because a hand edit means the figure and the results have no guaranteed relationship.

The central event-study plot was the wrong turn, and it cost three days. Priya's original showed three cargo classes as three coloured lines with three shaded confidence bands at forty percent opacity. On screen it was clear. Printed, the three bands overlapped into a single grey mass through which none of the point estimates were visible, and the three lines within it were indistinguishable. Her first repair kept the structure and changed the colours to three greys with three bands at twenty percent opacity. It was worse: the bands still overlapped, and now the lines had lost their only distinguishing channel.

The diagnosis was that the figure had too many series for its type, not that it had the wrong colours. Three series with confidence intervals is six visual objects competing in one plot area, and no palette rescues that. It was rebuilt as small multiples: three stacked panels sharing a horizontal axis and a vertical scale, one cargo class per panel, each with black markers and capped grey whiskers, a zero line, and a single vertical dashed line at the reform. The panel labels sat above each panel, so no legend was needed at all. The figure became taller and immediately readable, and the comparison across classes was easier than in the overlaid version because the shared vertical scale did the work the colours had been failing to do.

Two figures genuinely needed an accent. One was a synthetic control plot with 34 donor regions in light grey, where the treated region had to be findable; it took navy, with the synthetic comparison in black long-dash, and both remained distinguishable in greyscale by dash pattern alone. The other was a map where one province was the subject of a case discussion; that province took navy fill against a grey ramp. Nothing else in the chapter used colour.

The spreadsheet figures were rebuilt from the analysis output, which required recovering two numbers that had been typed in by hand eighteen months earlier. One of them was wrong: a mean freight volume of 4.21 in the figure against 4.12 in the analysis file, a transposition that had been in the chapter for over a year and had appeared in a conference poster.

All nine were moved into one figure script folder importing a single style module, exported to PDF at a common width of 5.2 inches, and checked in greyscale as a batch by converting the whole folder and viewing the results as thumbnails, which takes one command and catches problems the eye misses in isolation.

**Result.** Nine figures, one visual language, all regenerated from scripts, none hand-edited. Total time was six working days, three of which were the abandoned recolouring of the event study. The examiner's report commented that the figures were unusually clear, and asked one question about the synthetic control donor pool that the figure had made visible and the earlier version had hidden.

The most valuable output was not the appearance. It was the transposed 4.21, which surfaced only because rebuilding from source was required by the standard.

### A second scenario, where it goes differently

The same chapter's figures were needed for a twenty-minute conference talk six weeks later, and the standard adapts rather than being abandoned.

Three things change. The audience is at a distance, so fonts move from 9 point to the equivalent of 18 to 20 point at the projected size, and the axis tick density falls: five labelled ticks where the paper figure had eleven. The message per figure tightens further, because a slide gets thirty seconds and a page gets five minutes; the three-panel event study becomes one panel showing the cargo class the talk is about, with the other two mentioned in speech.

Colour is permitted more freely on a slide because the projector is the only medium and greyscale printing is not a constraint, but the discipline of the standard still pays: the accent is used to pick out the one series the speaker is talking about at that moment, in navy, with everything else in grey, and the build across three slides moves the navy from series to series. That is a use of colour for attention rather than for encoding, which is the distinction the standard is really about. If the colour were removed the slide would still be correct, only less directed.

What does not change: the legend stays outside the plot area, the figures are still generated by scripts, and the numbers are still the numbers from the analysis output. The slide deck imports the same style module with a second rcParams block layered on top, so the two versions cannot drift apart.

## Output

For each figure, three artefacts:

```
figures/fig2_event_study.pdf     vector, for the document
figures/fig2_event_study.png     raster at 400 dpi, for slides and drafts
code/figures/fig2_event_study.py an executable script reading from output/
```

Plus, in the document:

```
CAPTION
Figure 2: Effect of rollout on freight volumes, event-study estimates,
2012 to 2022.

NOTE
Estimates from equation (2) on 1,204 firms observed annually. Markers are
point estimates and vertical lines are 95 percent confidence intervals, with
standard errors clustered at the region level (17 clusters, wild bootstrap).
Event time -1 is the reference period and is normalised to zero. Endpoint
coefficients at -5 and +5 are binned and include all earlier and later
periods respectively. Source: national business registry, authors' calculations.
```

And a one-line record per figure in the project's figure index, so a coauthor can find what made what:

| Figure | Message in one sentence | Type | Series | Colour used | Script | Output |
| 1 | Treated and comparison regions moved together until 2016 | Trends | 2 | none | fig1_trends.py | fig1_trends.pdf |
| 2 | No pre-trend; effect appears in year 1 and persists | Event study | 1 | none | fig2_event_study.py | fig2_event_study.pdf |
| 5 | The treated region separates from its synthetic comparison after 2016 | Synthetic control | 2 plus donors | navy, one series | fig5_donor_pool.py | fig5_donor_pool.pdf |

## Failure modes

**Colour as the only channel.** Recognisable instantly by printing in greyscale, and by nothing else. Fix at the pattern level, by assigning dash and marker, not by adjusting the greys.

**The legend inside the plot area.** Recognisable because the axes have been padded to make room for it, or because it sits over the data. Fix by moving it below and, where possible, deleting it in favour of direct labels.

**Too many series.** Five or more lines in one plot area, usually with a legend of five entries and a reader who cannot match them to lines. Fix with small multiples, not with a fifth grey.

**Overlapping confidence bands.** Two or more shaded bands producing a grey mass. Fix by switching to capped whiskers on point estimates, or by splitting into panels. Never both a band and whiskers in the same figure.

**The hand-edited figure.** An axis label fixed in a graphics editor, an annotation added by hand. Recognisable because deleting the file and rerunning the script does not reproduce it. Fix by moving the edit into the script, always, even when it takes twenty minutes to work out how.

**Numbers typed into a plotting script.** Recognisable because the values appear as literals rather than being read from a file. Fix by having the estimation write them out. This is the failure that produced the transposed 4.21 in the worked example.

**An axis labelled "coefficient".** Fix with the outcome and its units, taken from the specification.

**A truncated vertical axis on a plot of effects.** Makes a small effect look large, and referees notice. Fix by including zero, or by stating the truncation in the note if there is a genuine reason.

**Screen-sized fonts.** Legible at 200 percent zoom and unreadable at print size. Fix by exporting at the final width and viewing at one hundred percent before delivering.

**Pre-period coefficients de-emphasised.** Shown smaller, greyer, or without intervals, in a figure whose whole purpose is to let a reader judge the pre-period. Fix by giving them identical treatment to the post-period.

**Inconsistency across a document.** Three fonts, two aspect ratios, four marker conventions. Fix with one style module imported by every figure script, set up at the start of the project rather than at the end.

**An inherited default palette.** Recognisable in ggplot2 by the hue wheel and in matplotlib by the tab10 blues and oranges, and in both by the figure printing as a set of similar mid-greys. Fix by setting the scales explicitly in the style module, and never rely on a palette being greyscale-safe because it is described as perceptually uniform in colour.

## Edge cases

**The journal requires colour or supplies its own template.** Follow the journal, and keep the pattern separation anyway; a figure that works in both is never worse. Where the template mandates a palette, map the house roles onto it and keep the legend outside.

**A poster.** Larger fonts, more colour permitted, but the same rule that colour never carries information alone, because posters are photographed and reprinted. Keep line weights heavier and series counts lower than in the paper.

**A figure with a genuinely continuous variable to encode, such as a heat map or a choropleth.** A grey ramp works and is the default. Where the quantity is diverging around a meaningful midpoint, such as positive against negative effects, navy through white to wine is the permitted diverging scheme, with the midpoint stated in the legend and the class breaks in the note.

**More than four categories that genuinely cannot be reduced.** Small multiples first. Where that fails, order the categories and use a grey ramp with direct labels rather than a legend, accepting that the reader will compare neighbours rather than arbitrary pairs.

**Very few observations.** Do not draw a smoothed curve through eight points. Show the points, and show the fitted line only if it is the estimated relationship rather than a decoration.

**A figure that will be reproduced at a very small size, such as a four-panel composite.** Reduce to one series per panel, drop tick labels to three per axis, and increase relative font size. Test at the final size before building all four panels.

**Software that cannot produce the standard.** Some tools cannot hatch, or cannot place a legend outside. Produce the figure elsewhere rather than compromising the standard, and record which tool made which figure in the figure index.

**An inherited figure whose underlying numbers are lost.** Do not restyle it. Rebuild it from the analysis output or remove it. A figure whose provenance cannot be established should not be in a document that claims to be reproducible.

## Quality bar

- Converted to greyscale, every series in every figure is still distinguishable, by dash pattern, marker shape or texture.
- No legend sits inside a plot area; every legend is above or below, or replaced by direct labels.
- Colour, where present, is dark navy or dark wine red only, appears on at mos

…(truncated)
