Skip to content

Publication-Ready Figures with neuro_py

Build cleaner, sharper neuroscience figures that drop directly into manuscripts with the right physical size and typography.

This tutorial shows how to combine: - set_plotting_defaults(workflow) to match journal/document typography - set_size(width, fraction, subplots, ratio) to get exact final figure dimensions

By the end, you’ll have reusable plotting patterns for single-panel, multi-panel, and workflow-specific exports (nature, word, latex).

import matplotlib.pyplot as plt
import numpy as np
from neuro_py.plotting.figure_helpers import set_plotting_defaults, set_size

SAVE_FIG = False

Available Workflows

Workflow Font Font Size Target
"nature" Helvetica / Arial 7pt Nature, Science, Cell, Neuron
"word" Times New Roman 11pt Word / Google Doc manuscripts
"latex" CMU Serif / Latin Modern 10pt LaTeX article submissions

Available Width Presets

Preset Width (pt) Width (mm) Use case
nature_single 255 90mm Nature single column
nature_double 510 180mm Nature double column
science_single 162 57mm Science single column
science_double 343 121mm Science double column
science_triple 521 184mm Science full width
cell_single 241 85mm Cell/Neuron single column
cell_1p5 323 114mm Cell/Neuron 1.5 column
cell_double 493 174mm Cell/Neuron double column
single_col 255 90mm Generic single column
double_col 510 180mm Generic double column
textwidth 418 147mm LaTeX article textwidth
thesis 427 151mm Thesis textwidth
beamer 307 108mm Beamer presentation

1. Basic Usage — Nature Workflow

set_plotting_defaults("nature")

# Single-column figure sized for Nature
fig, ax = plt.subplots(figsize=set_size("nature_single"), dpi=200)

x = np.linspace(0, 2 * np.pi, 300)
signal_a = np.sin(x)
signal_b = np.cos(x)

ax.plot(x, signal_a, label="Condition A", lw=1.6)
ax.plot(x, signal_b, label="Condition B", lw=1.6)
ax.fill_between(x, signal_a, signal_b, alpha=0.12, color="0.35", linewidth=0)

ax.set_xlabel("Time (s)")
ax.set_ylabel("Amplitude")
ax.set_title("Example single-column panel", loc="left", fontweight="bold")
ax.axhline(0, color="0.7", lw=0.8, zorder=0)
ax.margins(x=0)
ax.legend(frameon=False, ncol=2, loc="upper right")

fig.tight_layout()
if SAVE_FIG:
    fig.savefig("nature_single.svg", bbox_inches="tight")
plt.show()

png


2. Multi-Panel Figure — Double Column

set_plotting_defaults("nature")

# set_size adjusts height automatically for subplot grids
fig, axes = plt.subplots(
    1, 2, figsize=set_size("nature_double", subplots=(1, 2)), dpi=200
)

np.random.seed(42)
t = np.linspace(0, 1, 200)
spikes = np.random.poisson(5, size=(20, 200))

# Panel A — raster plot
for i, train in enumerate(spikes):
    spike_times = t[train > 0]
    axes[0].vlines(spike_times, i + 0.5, i + 1.5, linewidth=0.5, alpha=0.9)
axes[0].set_xlabel("Time (s)")
axes[0].set_ylabel("Neuron")
axes[0].set_title("A  Spike Raster", loc="left", fontweight="bold")

# Panel B — firing rate with smoothed trend
rate = spikes.mean(axis=0)
smooth = np.convolve(rate, np.ones(15) / 15, mode="same")
axes[1].plot(t, rate, alpha=0.35, lw=1.0, label="Raw")
axes[1].plot(t, smooth, lw=1.8, label="Smoothed")
axes[1].set_xlabel("Time (s)")
axes[1].set_ylabel("Firing Rate (Hz)")
axes[1].set_title("B  Population Rate", loc="left", fontweight="bold")
axes[1].legend(frameon=False, loc="upper right")

for a in axes:
    a.axvline(0.5, ls="--", color="0.75", lw=0.8)
    a.margins(x=0)

fig.tight_layout()
if SAVE_FIG:
    fig.savefig("nature_double_panel.svg", bbox_inches="tight")
plt.show()

png

The subplots=(1, 2) argument to set_size adjusts the figure height so that each panel has a golden ratio aspect ratio rather than the whole figure.


3. Using fraction — Half-Width Figures

set_plotting_defaults("nature")

# Half of a double column — useful for inset-style figures
fig, ax = plt.subplots(figsize=set_size("nature_double", fraction=0.5), dpi=200)

np.random.seed(0)
data = [np.random.normal(loc, 0.5, 50) for loc in [1, 2, 3]]
bp = ax.boxplot(data, tick_labels=["Pre", "Task", "Post"], patch_artist=True)

for patch, face in zip(bp["boxes"], ["#88CCEE", "#CC6677", "#44AA99"]):
    patch.set_facecolor(face)
    patch.set_alpha(0.5)

ax.set_ylabel("Firing Rate (Hz)")
ax.set_title("Half-width summary panel", loc="left", fontweight="bold")
ax.grid(axis="y", alpha=0.2, lw=0.5)

fig.tight_layout()
if SAVE_FIG:
    fig.savefig("nature_half.svg", bbox_inches="tight")
plt.show()

png

In practice, fraction is the main knob for fitting figures to your layout. A good workflow is to start with fraction=1.0 and reduce until axis labels occupy a comfortable proportion of the figure width:

# Start here and adjust until it looks right
fig, ax = plt.subplots(figsize=set_size("nature_double", fraction=0.6))

The font sizes stay fixed — only the figure dimensions change — so reducing fraction makes labels appear relatively larger compared to the plot area.

set_plotting_defaults("nature")

# Tip: adjust fraction until axis labels fill the figure naturally
fig, ax = plt.subplots(figsize=set_size("nature_double", fraction=0.35), dpi=200)

np.random.seed(0)
data = [np.random.normal(loc, 0.5, 50) for loc in [1, 2, 3]]
bp = ax.boxplot(data, tick_labels=["Pre", "Task", "Post"], patch_artist=True)

for patch, face in zip(bp["boxes"], ["#88CCEE", "#CC6677", "#44AA99"]):
    patch.set_facecolor(face)
    patch.set_alpha(0.5)

ax.set_ylabel("Firing Rate (Hz)")
ax.set_title("Half-width summary panel", loc="left", fontweight="bold")
ax.grid(axis="y", alpha=0.2, lw=0.5)

fig.tight_layout()
if SAVE_FIG:
    fig.savefig("nature_half.svg", bbox_inches="tight")
plt.show()

png


4. Overriding the Aspect Ratio

set_plotting_defaults("nature")

# Square figure — useful for place field maps, correlation matrices
fig, axes = plt.subplots(
    1, 2, figsize=set_size("nature_double", ratio=1.0, subplots=(1, 2)), dpi=200
)

np.random.seed(1)
place_field = np.random.rand(20, 20)
corr_matrix = np.corrcoef(np.random.rand(10, 50))

# Panel A — place field heatmap
im1 = axes[0].imshow(place_field, cmap="hot", aspect="equal")
axes[0].set_xlabel("Position X (cm)")
axes[0].set_ylabel("Position Y (cm)")
axes[0].set_title("A  Place Field", loc="left", fontweight="bold")
plt.colorbar(im1, ax=axes[0], label="Rate (Hz)")

# Panel B — correlation matrix
im2 = axes[1].imshow(corr_matrix, cmap="RdBu_r", vmin=-1, vmax=1, aspect="equal")
axes[1].set_title("B  Correlation Matrix", loc="left", fontweight="bold")
plt.colorbar(im2, ax=axes[1], label="Correlation")

fig.tight_layout()
if SAVE_FIG:
    fig.savefig("nature_square.svg")
plt.show()

png

The default golden ratio (~0.618) is ideal for line plots, but ratio=1.0 gives square panels which is better for spatial maps and matrices.


5. Word Workflow

set_plotting_defaults("word")

# single_col maps to nature_single width — good default for Word figures
fig, ax = plt.subplots(figsize=set_size("single_col"), dpi=200)

np.random.seed(3)
epochs = ["Pre-sleep", "Task", "Post-sleep"]
means = [2.1, 8.4, 3.7]
sems = [0.3, 0.6, 0.4]

ax.bar(epochs, means, yerr=sems, capsize=3, color=["#0072B2", "#D55E00", "#009E73"])
ax.set_ylabel("Firing Rate (Hz)")
ax.set_title("Mean Firing Rate by Epoch")

if SAVE_FIG:
    fig.savefig("word_bar.svg")
plt.show()

png


6. LaTeX Workflow

set_plotting_defaults("latex")

# textwidth matches \textwidth in a standard article class document
fig, ax = plt.subplots(figsize=set_size("textwidth"), dpi=200)

np.random.seed(5)
t = np.linspace(0, 2, 500)
theta = np.cumsum(np.random.randn(500)) * 0.1
power = np.abs(np.fft.rfft(theta)) ** 2
freqs = np.fft.rfftfreq(500, d=1 / 250)

ax.semilogy(freqs[1:], power[1:])
ax.set_xlabel("Frequency (Hz)")
ax.set_ylabel("Power ($\\mu V^2$/Hz)")
ax.set_title("LFP Power Spectrum")
ax.set_xlim(0, 100)

if SAVE_FIG:
    fig.savefig("latex_spectrum.svg")
plt.show()

png

For half-width figures in a LaTeX document (e.g. two figures side by side with \includegraphics[width=0.5\textwidth]):

fig, ax = plt.subplots(figsize=set_size("textwidth", fraction=0.5))

7. Comparing Workflows Side by Side

To make font differences visible in a single figure, this demo applies workflow-specific font families directly to each panel (instead of relying only on global style settings).

np.random.seed(7)
x = np.linspace(0, 4 * np.pi, 200)
y1 = np.sin(x) + np.random.normal(0, 0.1, 200)
y2 = np.cos(x) + np.random.normal(0, 0.1, 200)

workflows = ["nature", "word", "latex"]
demo_fonts = {
    "nature": "Helvetica",
    "word": "Times New Roman",
    "latex": "Latin Modern Roman",
}
ylim = (min(y1.min(), y2.min()) - 0.2, max(y1.max(), y2.max()) + 0.2)

fig, axes = plt.subplots(
    1, 3, figsize=set_size("nature_double", subplots=(1, 3), ratio=1), dpi=200
)

for ax, workflow in zip(axes, workflows):
    set_plotting_defaults(workflow)
    font = demo_fonts[workflow]

    ax.plot(x, y1, label="Condition A", lw=1.4)
    ax.plot(x, y2, label="Condition B", lw=1.4)
    ax.set_title(workflow.capitalize(), fontweight="bold", fontfamily=font)
    ax.set_xlabel("Time (s)", fontfamily=font)
    ax.set_ylim(*ylim)
    ax.margins(x=0)
    ax.grid(alpha=0.18, lw=0.5)

    for tick in ax.get_xticklabels() + ax.get_yticklabels():
        tick.set_fontfamily(font)

axes[0].set_ylabel("Signal", fontfamily=demo_fonts[workflows[0]])

legend = axes[-1].legend(frameon=False, loc="upper right", bbox_to_anchor=(1.02, 1.0))
for text in legend.get_texts():
    text.set_fontfamily(demo_fonts[workflows[-1]])

    fig.suptitle("Workflow style comparison on identical data", y=1.01)
fig.tight_layout()

if SAVE_FIG:
    fig.savefig("compare_workflows.svg", bbox_inches="tight")
plt.show()

png


8. Passing a Custom Width

If your journal has a specific column width not in the presets, pass the width in points directly. To convert from mm: width_pt = width_mm * 2.8346.

set_plotting_defaults("nature")

# eLife single column: 87.6 mm -> points
elife_single_pt = 87.6 * 2.8346
fig_w, fig_h = set_size(elife_single_pt)

fig, ax = plt.subplots(figsize=(fig_w, fig_h), dpi=200)
trace = np.random.default_rng(11).random(100).cumsum()
ax.plot(np.linspace(0, 1, 100), trace, lw=1.5)
ax.set_xlabel("Time (s)")
ax.set_ylabel("Cumulative Signal")
ax.set_title("Custom-width panel", loc="left", fontweight="bold")
ax.text(
    0.02,
    0.95,
    f"Size: {fig_w:.2f} x {fig_h:.2f} in",
    transform=ax.transAxes,
    va="top",
    fontsize=6,
    color="0.35",
)

fig.tight_layout()
plt.show()

png


9. Dark Theme with neuro_py_dark

For talks, posters, or dark slide decks, you can apply the provided neuro_py_dark.mplstyle file in a local style context without changing the rest of the notebook defaults.

set_plotting_defaults("dark")

fig, ax = plt.subplots(figsize=set_size("nature_single"), dpi=200)
rng = np.random.default_rng(21)
x = np.linspace(0, 10, 400)
signal = np.sin(2 * np.pi * 0.8 * x) + 0.25 * rng.normal(size=x.size)
ax.plot(x, signal, label="LFP-like trace")
signal = np.cos(2 * np.pi * 0.8 * x) + 0.25 * rng.normal(size=x.size)
ax.plot(x, signal - 3, label="LFP-like trace")
signal = np.sin(2 * np.pi * 0.8 * x) + 0.25 * rng.normal(size=x.size)
ax.plot(x, signal - 6, label="LFP-like trace")
ax.set_xlabel("Time (s)")
ax.set_ylabel("Amplitude (a.u.)")
ax.set_title("Dark-theme figure with neuro_py_dark", loc="left", fontweight="bold")
ax.margins(x=0)

fig.tight_layout()
if SAVE_FIG:
    fig.savefig("dark_style_demo.svg", bbox_inches="tight")
plt.show()

png


Quick Recipe Card

Use this as a copy/paste checklist when preparing final figures:

# 1) Pick your destination workflow
set_plotting_defaults("nature")   # or "word", "latex"

# 2) Match figure width to target layout
fig, ax = plt.subplots(figsize=set_size("nature_single"))

# 3) Multi-panel layout (height auto-adjusts)
fig, axes = plt.subplots(2, 3, figsize=set_size("nature_double", subplots=(2, 3)))

# 4) For maps/matrices, force square axes
fig, ax = plt.subplots(figsize=set_size("nature_single", ratio=1.0))

# 5) Half-width panel for insets
fig, ax = plt.subplots(figsize=set_size("nature_double", fraction=0.5))

# 6) Save vector output for publication
fig.savefig("figure.svg", bbox_inches="tight")

Tip: if labels look too small/large in your manuscript, verify that the figure is inserted at its intended physical width (not scaled in the editor).