2025, Oct 21 13:00

How to size Seaborn/Matplotlib figures for LaTeX \textwidth: match fonts, reduce clutter, save vector PDFs

Fix cramped Seaborn/Matplotlib plots at LaTeX \textwidth: set rcParams font sizes, use LaTeX text, save as PDF, and tune marker size for readable figures.

When you size seaborn figures to match a LaTeX \textwidth and still end up with plots that look cramped, it’s frustrating. The common symptom is a figure that appears visually smaller than expected, with dense markers and labels, and which only looks “right” after doubling the figure size. The goal, however, is to keep the figure at the target width and maintain font sizes consistent with the main text of the paper.

Reproducing the issue

The setup below targets a 7-inch layout width and arranges three panels side by side. The plot mixes a stripplot with a boxplot, shares the y-axis, and rotates x-tick labels — very typical for publication graphics. Notice that label sizing is referenced but not explicitly set beforehand.

import matplotlib.pyplot as plt
import seaborn as sns
page_width = 7.00925
metrics = ['test_1', 'test_2', 'test_3']
panel_titles = ['Test 1', 'Test 2', 'Test 3']
canvas, axarr = plt.subplots(1, 3, figsize=(page_width, 3), sharey=True)
for idx, (target_col, ttl) in enumerate(zip(metrics, panel_titles)):
    pane = axarr[idx]
    # Stripplot
    sns.stripplot(
        x='diagnosis_grouped', y=target_col, data=plot_frame, ax=pane,
        palette='viridis', alpha=0.7, jitter=True
    )
    # Boxplot
    sns.boxplot(
        x='group', y=target_col, data=plot_frame, ax=pane, showfliers=False,
        width=0.3,
        boxprops=dict(facecolor='none', edgecolor='black'),
        medianprops=dict(color='black'),
        whiskerprops=dict(color='black'),
        capprops=dict(color='black')
    )
    pane.set_title(ttl, fontsize=label_size)
    pane.set_xlabel('Group', fontsize=label_size)
    if idx == 0:
        pane.set_ylabel('Score', fontsize=label_size)
    else:
        pane.set_ylabel('')
    pane.tick_params(axis='x', rotation=20, labelsize=label_size)
    pane.tick_params(axis='y', labelsize=label_size)
plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.savefig('/path/figure_1.eps', dpi=800)

What’s really happening

Two separate factors drive the visual impression. First, increasing figsize makes the same data occupy more horizontal space, so points and boxes naturally spread out and look less congested. Second, without explicitly aligning font sizes to the target document, axis labels and ticks may look off even when the figure width is correct. Once fonts are set deliberately, the same figure width reads as intended on the page. Rendering text with LaTeX further ensures that labels and math match the document’s typography.

The fix

Keep the intended width. Set a concrete label size to match your document (for example, 11). Enable LaTeX text rendering to keep typography consistent. Saving to a vector format such as PDF with tight bounding box helps preserve sharpness and whitespace control. If the points still feel crowded, reduce marker size in sns.stripplot via the size parameter.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib import rcParams
# Use LaTeX for all text rendering
rcParams.update({"text.usetex": True})
# Set font sizes to match the manuscript
rcParams.update({
    "font.size": 11,
    "axes.labelsize": 11,
    "axes.titlesize": 11,
    "xtick.labelsize": 11,
    "ytick.labelsize": 11,
    "legend.fontsize": 11,
})
label_size = 11
page_width_in = 7.00925
panel_height_in = 3
frame_metrics = ['test_1', 'test_2', 'test_3']
frame_titles = ['Test 1', 'Test 2', 'Test 3']
board, panels = plt.subplots(1, 3, figsize=(page_width_in, panel_height_in), sharey=True)
for j, (col_name, title_txt) in enumerate(zip(frame_metrics, frame_titles)):
    slot = panels[j]
    sns.stripplot(
        x='diagnosis_grouped', y=col_name, data=plot_frame, ax=slot,
        palette='viridis', alpha=0.7, jitter=True
        # size=4  # optionally reduce dot size if it looks cramped
    )
    sns.boxplot(
        x='group', y=col_name, data=plot_frame, ax=slot, showfliers=False,
        width=0.3,
        boxprops=dict(facecolor='none', edgecolor='black'),
        medianprops=dict(color='black'),
        whiskerprops=dict(color='black'),
        capprops=dict(color='black')
    )
    slot.set_title(title_txt, fontsize=label_size)
    slot.set_xlabel('Group', fontsize=label_size)
    if j == 0:
        slot.set_ylabel('Score', fontsize=label_size)
    else:
        slot.set_ylabel('')
    slot.tick_params(axis='x', rotation=20, labelsize=label_size)
    slot.tick_params(axis='y', labelsize=label_size)
plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.savefig('/path/figure_1.pdf', bbox_inches='tight')
plt.show()

Why this matters for publication workflows

Figures prepared for LaTeX need consistent sizing and typography to blend into the layout. Aligning fonts programmatically avoids guesswork, and LaTeX-based text rendering prevents mismatches between the plot labels and the document’s text. Exporting to PDF keeps vector sharpness for print and electronic versions and ensures that scaling in the final manuscript doesn’t degrade readability.

Takeaways

Size the canvas to the target width, then set font sizes explicitly so labels match the manuscript. Use LaTeX text rendering to keep typography consistent and save to PDF with a tight bounding box for clean output. If the visual density is still high at the correct width, reduce marker size in the strip plot rather than inflating the figure. This way you keep the figure faithful to \textwidth without compromising legibility or layout.

The article is based on a question from StackOverflow by RBG and an answer by Thomas Beck.