NMR Spectrum (Nuclear Magnetic Resonance) in Seaborn (Python)

The same plot in 14 other libraries — Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, plotnine, Pygal; R: ggplot2; Julia: Makie.jl; JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts, MUI X Charts. Compare all 15 side by side: NMR Spectrum (Nuclear Magnetic Resonance) in Python, R, Julia and JavaScript.

An NMR spectrum plots signal intensity versus chemical shift (in ppm) to reveal the electronic environment of atomic nuclei in a molecule. It is the primary analytical tool in organic chemistry for determining molecular structure, with peak positions indicating functional groups and splitting patterns (multiplets) revealing connectivity between neighboring atoms. The x-axis is conventionally reversed (high ppm on the left) and peaks appear as sharp signals rising from a baseline.

NMR Spectrum (Nuclear Magnetic Resonance) rendered with Seaborn

Renders

Python source (Seaborn)

""" anyplot.ai
spectrum-nmr: NMR Spectrum (Nuclear Magnetic Resonance)
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 91/100 | Updated: 2026-06-03
"""

import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Imprint palette — canonical order, first series always #009E73
IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233"]

peak_palette = {"CH₂ quartet": IMPRINT[0], "OH singlet": IMPRINT[1], "CH₃ triplet": IMPRINT[2], "TMS": IMPRINT[3]}

# Seaborn theme — warm off-white / near-black surfaces, serif font
sns.set_theme(
    style="ticks",
    rc={
        "figure.facecolor": PAGE_BG,
        "axes.facecolor": PAGE_BG,
        "axes.edgecolor": INK_SOFT,
        "axes.labelcolor": INK,
        "text.color": INK,
        "xtick.color": INK_SOFT,
        "ytick.color": INK_SOFT,
        "grid.color": INK,
        "grid.alpha": 0.15,
        "legend.facecolor": ELEVATED_BG,
        "legend.edgecolor": INK_SOFT,
        "font.family": "serif",
    },
)

# Data — synthetic 1H NMR spectrum of ethanol (Lorentzian peak shapes)
np.random.seed(42)
ppm = np.linspace(-0.5, 5.5, 6000)
width = 0.012

spectrum = np.zeros_like(ppm)

# TMS reference at 0 ppm
w_tms = 0.01
spectrum += 0.4 * w_tms**2 / ((ppm - 0.0) ** 2 + w_tms**2)

# CH3 triplet at 1.18 ppm (1:2:1 ratio, J = 0.07 ppm)
j_ch3 = 0.07
spectrum += 0.5 * width**2 / ((ppm - (1.18 - j_ch3)) ** 2 + width**2)
spectrum += 1.0 * width**2 / ((ppm - 1.18) ** 2 + width**2)
spectrum += 0.5 * width**2 / ((ppm - (1.18 + j_ch3)) ** 2 + width**2)

# CH2 quartet at 3.69 ppm (1:3:3:1 ratio, J = 0.07 ppm)
j_ch2 = 0.07
spectrum += 0.25 * width**2 / ((ppm - (3.69 - 1.5 * j_ch2)) ** 2 + width**2)
spectrum += 0.75 * width**2 / ((ppm - (3.69 - 0.5 * j_ch2)) ** 2 + width**2)
spectrum += 0.75 * width**2 / ((ppm - (3.69 + 0.5 * j_ch2)) ** 2 + width**2)
spectrum += 0.25 * width**2 / ((ppm - (3.69 + 1.5 * j_ch2)) ** 2 + width**2)

# OH singlet at 2.61 ppm (slightly broad due to proton exchange)
w_oh = 0.025
spectrum += 0.35 * w_oh**2 / ((ppm - 2.61) ** 2 + w_oh**2)

spectrum += np.random.normal(0, 0.003, len(ppm))
spectrum = np.clip(spectrum, 0, None)

# DataFrame with region labels (vectorized with np.select)
df = pd.DataFrame({"Chemical Shift (ppm)": ppm, "Intensity": spectrum})
conditions = [
    (ppm >= 3.5) & (ppm <= 3.9),
    (ppm >= 2.4) & (ppm <= 2.8),
    (ppm >= 1.0) & (ppm <= 1.4),
    (ppm >= -0.1) & (ppm <= 0.1),
]
choices = ["CH₂ quartet", "OH singlet", "CH₃ triplet", "TMS"]
df["Region"] = np.select(conditions, choices, default="Baseline")

# Plot — 3200×1800 px canvas (figsize=(8,4.5) × dpi=400)
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)

# Baseline (thin, muted — plotted separately)
sns.lineplot(
    data=df[df["Region"] == "Baseline"],
    x="Chemical Shift (ppm)",
    y="Intensity",
    color=INK_SOFT,
    linewidth=0.6,
    ax=ax,
    legend=False,
)

# Signal peaks — single hue-based lineplot (idiomatic seaborn)
hue_order = ["CH₂ quartet", "OH singlet", "CH₃ triplet", "TMS"]
sns.lineplot(
    data=df[df["Region"] != "Baseline"],
    x="Chemical Shift (ppm)",
    y="Intensity",
    hue="Region",
    hue_order=hue_order,
    palette=peak_palette,
    linewidth=2.2,
    ax=ax,
)

# Rug marks at peak centers (distinctive seaborn feature)
sns.rugplot(
    data=pd.DataFrame({"Chemical Shift (ppm)": [0.0, 1.18, 2.61, 3.69]}),
    x="Chemical Shift (ppm)",
    height=0.04,
    linewidth=1.8,
    color=INK_SOFT,
    ax=ax,
)

# Semi-transparent fill under each peak region
for region in hue_order:
    mask = df["Region"] == region
    ax.fill_between(
        df.loc[mask, "Chemical Shift (ppm)"], df.loc[mask, "Intensity"], alpha=0.12, color=peak_palette[region]
    )

# NMR convention: high ppm on left
ax.invert_xaxis()
ax.set_xlim(5.5, -0.5)

# Peak annotations with color-matched connectors
max_intensity = spectrum.max()
peak_annotations = [
    (0.0, "TMS · 0.00 ppm", 0.26, peak_palette["TMS"]),
    (1.18, "CH₃ · 1.18 ppm", 0.18, peak_palette["CH₃ triplet"]),
    (2.61, "OH · 2.61 ppm", 0.32, peak_palette["OH singlet"]),
    (3.69, "CH₂ · 3.69 ppm", 0.30, peak_palette["CH₂ quartet"]),
]
for peak_ppm, label, offset_frac, color in peak_annotations:
    peak_idx = np.argmin(np.abs(ppm - peak_ppm))
    peak_intensity = spectrum[peak_idx]
    text_y = peak_intensity + max_intensity * offset_frac
    ax.annotate(
        label,
        xy=(peak_ppm, peak_intensity),
        xytext=(peak_ppm, text_y),
        fontsize=8,
        fontweight="medium",
        ha="center",
        va="bottom",
        color=color,
        arrowprops={"arrowstyle": "-", "color": color, "lw": 0.8},
    )

# Style
title = "¹H NMR Spectrum of Ethanol · spectrum-nmr · python · seaborn · anyplot.ai"
title_n = len(title)
title_fs = max(8, round(12 * 67 / title_n)) if title_n > 67 else 12

ax.set_title(title, fontsize=title_fs, fontweight="semibold", pad=10, color=INK)
ax.set_xlabel("Chemical Shift (ppm)", fontsize=10, color=INK)
ax.set_ylabel("Intensity", fontsize=10, color=INK)
ax.set_yticks([])
ax.set_ylim(-0.02, max_intensity * 1.55)
ax.tick_params(axis="x", labelsize=8, colors=INK_SOFT)

# Legend
legend = ax.legend(
    loc="upper right",
    fontsize=8,
    frameon=True,
    framealpha=0.92,
    edgecolor=INK_SOFT,
    facecolor=ELEVATED_BG,
    title="Peak Assignment",
    title_fontsize=8,
)
legend.get_title().set_fontweight("semibold")
legend.get_title().set_color(INK)
for text in legend.get_texts():
    text.set_color(INK_SOFT)

sns.despine(ax=ax, left=True)

# Save — bbox_inches omitted so canvas stays exactly figsize × dpi = 3200 × 1800 px
fig.subplots_adjust(top=0.88, bottom=0.13, left=0.06, right=0.97)
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/spectrum-nmr/seaborn/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "spectrum-nmr",
  "language": "python",
  "library": "seaborn",
  "page": "https://anyplot.ai/spectrum-nmr/python/seaborn",
  "hub": "https://anyplot.ai/spectrum-nmr",
  "code_json": "https://api.anyplot.ai/specs/spectrum-nmr/seaborn/code",
  "spec_json": "https://api.anyplot.ai/specs/spectrum-nmr",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/spectrum-nmr/python/seaborn/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/spectrum-nmr/python/seaborn/plot-dark.png",
  "quality_score": 91.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of NMR Spectrum (Nuclear Magnetic Resonance) on anyplot.ai.

Other implementations