---
name: scientific-figure-conventions
source: https://app.decimal.ai/s/scientific-figure-conventions@1/SKILL.md
source_sha256: 36633c921c24
---

# Scientific figure conventions

## Contract
When you write matplotlib/seaborn code for a journal-submission figure, emit code that already
encodes the journal's house style: the Okabe-Ito categorical palette, the exact per-journal column
width in inches, the per-journal panel-label case, perceptually-uniform colormaps, vector export, and
sans-serif fonts. Apply this whenever the task names a journal (Nature / Science / Cell) or says
"publication", "manuscript", "submission", or "journal figure". Do not substitute library defaults.

## Rules (the complete spec)

### R1 — Categorical colors: the Okabe-Ito palette, in this order
For every set of categorical series (lines, bars, groups, hues), assign colors from this exact
8-value list, in order, cycling if there are more than 8 series:

```
#E69F00  #56B4E9  #009E73  #F0E442  #0072B2  #D55E00  #CC79A7  #000000
```

(orange, sky blue, bluish green, yellow, blue, vermillion, reddish purple, black.) Never fall back to
matplotlib's default cycle (`C0`/`C1`/`C2` = blue/orange/green), `tab10`, or seaborn's `colorblind`
theme. Set it explicitly per-artist (`color=okabe_ito[i]`) or globally
(`plt.rcParams['axes.prop_cycle'] = plt.cycler(color=okabe_ito)`).

### R2 — Figure width: the journal's exact column width (1 in = 25.4 mm)
Set the `figsize` **width** to the target journal's column width. Height is free (choose by aspect);
width is fixed:

| Journal | Single column | Double column |
|---|---|---|
| Nature  | 89 mm = **3.50 in** | 183 mm = **7.20 in** |
| Science | 55 mm = **2.17 in** | 175 mm = **6.89 in** |
| Cell    | 85 mm = **3.35 in** | 178 mm = **7.01 in** |

Never use the matplotlib default (6.4 in) or a round number like 6, 8, or 10. A "single-column" or
unspecified-width request defaults to that journal's single-column value.

### R3 — Panel labels: case depends on the journal
Multi-panel figures get bold per-panel letters. The **case** is journal-specific:
- **Nature** → lowercase `a`, `b`, `c`, … (bold)
- **Science** and **Cell** → uppercase `A`, `B`, `C`, … (bold)

Render with `fontweight='bold'` (or `weight='bold'`), typically via
`ax.text(-0.15, 1.05, 'a', transform=ax.transAxes, fontweight='bold')`.

### R4 — Continuous colormaps
- **Sequential** (one-directional magnitude): `viridis` or `cividis`. (`plasma` is also acceptable
  perceptually-uniform.) **Never** `jet` or `rainbow`.
- **Diverging** (data spanning negative and positive, e.g. log2 fold-change, correlation): `RdBu_r`
  or `PuOr`, **centered at 0** (`center=0` for seaborn, or symmetric `vmin=-vmax` for matplotlib).
- Always include a labeled colorbar for image/heatmap plots.

### R5 — Export format
- **Plots / line art** (graphs): vector **PDF** or **EPS**. If rasterized, **600+ DPI**.
- **Images** (microscopy, photos): **TIFF** or **PNG** at **≥ 300 DPI**.
- **Never JPEG** for any scientific figure — it introduces compression artifacts.

### R6 — Fonts
Sans-serif, specifically **Arial** or **Helvetica**. Sizes tuned for final print size: axis labels
7–9 pt, tick labels 6–8 pt, panel labels 8–12 pt bold. Set via
`rcParams['font.family'] = 'sans-serif'` and `rcParams['font.sans-serif'] = ['Arial', 'Helvetica']`.

### R7 — Spines
Remove the **top** and **right** spines (`ax.spines['top'].set_visible(False)` and same for
`'right'`, or `sns.despine()`). Keep left and bottom.

### R8 — Statistical rigor (when the plot summarizes a distribution)
Show error bars and state their meaning (SD, SEM, or 95% CI) in the caption; report sample size `n`;
mark significance with `*`/`**`/`***`; overlay individual data points where feasible rather than
showing summary statistics alone.

## Worked examples (BEFORE = base default → AFTER = conforming)

### R1 — categorical colors
```python
# BEFORE: matplotlib default cycle (C0 blue, C1 orange, C2 green)
ax.plot(x, a, label='Control')
ax.plot(x, b, label='Treated')

# AFTER: Okabe-Ito, in order
okabe_ito = ['#E69F00', '#56B4E9', '#009E73', '#F0E442',
             '#0072B2', '#D55E00', '#CC79A7', '#000000']
ax.plot(x, a, color=okabe_ito[0], label='Control')   # #E69F00
ax.plot(x, b, color=okabe_ito[1], label='Treated')   # #56B4E9
```

### R2 — figure width (Nature single column)
```python
# BEFORE: round, arbitrary size
fig, ax = plt.subplots(figsize=(8, 5))

# AFTER: Nature single column = 89 mm = 3.50 in
fig, ax = plt.subplots(figsize=(3.5, 2.5))
```

### R3 — panel labels (Nature vs Cell)
```python
# BEFORE: uppercase used for everything
ax1.text(-0.15, 1.05, 'A', transform=ax1.transAxes, fontweight='bold')  # wrong for Nature

# AFTER (Nature → lowercase, bold)
ax1.text(-0.15, 1.05, 'a', transform=ax1.transAxes, fontweight='bold')
ax2.text(-0.15, 1.05, 'b', transform=ax2.transAxes, fontweight='bold')

# AFTER (Cell or Science → uppercase, bold)
ax1.text(-0.15, 1.05, 'A', transform=ax1.transAxes, fontweight='bold')
```

### R4 — colormaps
```python
# BEFORE (sequential): jet
im = ax.imshow(M, cmap='jet')

# AFTER (sequential): perceptually uniform
im = ax.imshow(M, cmap='viridis')
fig.colorbar(im, ax=ax, label='Expression (AU)')

# BEFORE (diverging): default, not centered
sns.heatmap(corr, cmap='coolwarm')

# AFTER (diverging): RdBu_r centered at 0
sns.heatmap(corr, cmap='RdBu_r', center=0)
```

### R5 — export
```python
# BEFORE: JPEG at low DPI
fig.savefig('figure1.jpg', dpi=150)

# AFTER: vector for a plot
fig.savefig('figure1.pdf')                 # or .eps
# AFTER: raster image panel
fig.savefig('micrograph.tiff', dpi=300)    # TIFF/PNG, ≥300 DPI, never JPEG
```

### R6 / R7 — fonts and spines
```python
# BEFORE: default DejaVu Sans, all four spines
# (no font set; box stays closed)

# AFTER
import matplotlib as mpl
mpl.rcParams['font.family'] = 'sans-serif'
mpl.rcParams['font.sans-serif'] = ['Arial', 'Helvetica']
mpl.rcParams['axes.labelsize'] = 9
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
```

### R8 — statistics
```python
# BEFORE: bare bars, no uncertainty
ax.bar(groups, means)

# AFTER: error bars + individual points + significance
ax.bar(groups, means, yerr=sems, capsize=3, color=okabe_ito[:len(groups)])
ax.scatter(jittered_x, individual_points, color='black', alpha=0.3, s=8)
ax.text(1.5, ymax * 1.05, '***', ha='center')   # state "mean ± SEM, n=…" in caption
```

## Edge cases & exceptions
- **More than 8 categorical series:** cycle the Okabe-Ito list back to the start, and add a redundant
  encoding (line style or marker) so series stay distinguishable beyond color.
- **Width given but no journal:** if the task says "single column" with no journal, default to Nature
  89 mm (3.50 in); if it says "full page" / "full width", use that journal's double-column value.
- **Diverging vs sequential:** the deciding factor is whether the data spans both signs (correlations
  −1..1, log2 fold-changes) → diverging centered at 0; one-directional magnitude → sequential.
- **Bar charts:** start the y-axis at 0 unless there is a scientific reason not to (truncated bar
  axes mislead).
- **Grayscale safety:** because reviewers may print in grayscale, pair color with redundant encoding
  (markers/line styles) for line plots.
- **Plotly/other libs:** the same K applies — Okabe-Ito hexes, exact width, never-JPEG export
  (`scale=3` ≈ 300 DPI for raster export).

## Do / Don't
- **Never** use `cmap='jet'` or `'rainbow'`. **Always** use `viridis`/`cividis` (sequential) or
  `RdBu_r`/`PuOr` centered at 0 (diverging).
- **Never** save a figure as `.jpg`/`.jpeg`. **Always** PDF/EPS for plots, TIFF/PNG (≥300 DPI) for
  images.
- **Never** leave the matplotlib default color cycle. **Always** set Okabe-Ito explicitly.
- **Never** use uppercase panel labels for Nature. **Always** lowercase for Nature, uppercase for
  Science/Cell, both bold.
- **Never** use `figsize=(6.4, …)` or a round 8/10. **Always** the journal's exact column width in
  inches.
- **Never** leave the top/right spines on. **Always** remove them.

## Common mistakes (the base's wrong defaults)
1. Uses matplotlib's blue/orange/green default cycle instead of Okabe-Ito.
2. Picks a round `figsize` like `(8, 6)` or `(10, 6)` instead of the journal column width.
3. Uses `jet`/`coolwarm`/`rainbow` for heatmaps; forgets to center diverging maps at 0.
4. Saves as `.png` or `.jpg` for a line plot instead of vector PDF/EPS.
5. Uses uppercase `A, B, C` panel labels for Nature (Nature wants lowercase).
6. Leaves all four spines; never sets a sans-serif font.
7. Plots bare summary bars with no error bars, n, or individual points.

## Quick checklist
- [ ] Categorical colors = Okabe-Ito 8-hex, in order
- [ ] `figsize` width = journal column width (Nature 3.50/7.20, Science 2.17/6.89, Cell 3.35/7.01 in)
- [ ] Panel labels: Nature lowercase, Science/Cell uppercase, bold
- [ ] Sequential cmap viridis/cividis; diverging RdBu_r/PuOr centered at 0; never jet/rainbow
- [ ] Export vector PDF/EPS for plots, TIFF/PNG ≥300 DPI for images, never JPEG
- [ ] Sans-serif Arial/Helvetica; top & right spines removed
- [ ] Error bars + n + significance + individual points when summarizing distributions
