NMR Spectrum (Nuclear Magnetic Resonance) — plotnine

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 plotnine

Python source (plotnine)

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

import os
import sys


# This file is named 'plotnine.py' — remove its directory from sys.path so the
# 'plotnine' library is found in site-packages instead of looping back here.
_d = os.path.dirname(os.path.abspath(__file__))
if _d in sys.path:
    sys.path.remove(_d)
del _d

import numpy as np
import pandas as pd
from plotnine import (
    aes,
    annotate,
    coord_cartesian,
    element_blank,
    element_line,
    element_rect,
    element_text,
    geom_area,
    geom_line,
    geom_point,
    geom_segment,
    geom_text,
    geom_vline,
    ggplot,
    labs,
    scale_x_reverse,
    scale_y_continuous,
    theme,
    theme_minimal,
)


# Theme tokens — Imprint palette, theme-adaptive chrome
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
BRAND = "#009E73"  # Imprint palette position 1 — ALWAYS first series

# Title with length-aware fontsize
title = "\xb9H NMR of Ethanol · spectrum-nmr · python · plotnine · anyplot.ai"
n = len(title)
title_fontsize = max(8, round(12 * 67 / n)) if n > 67 else 12

# Data — synthetic 1H NMR spectrum of ethanol (CH3CH2OH)
np.random.seed(42)
chemical_shift = np.linspace(-0.5, 5.0, 6000)

w = 0.015  # default Lorentzian peak width
intensity = np.zeros_like(chemical_shift)

# TMS reference peak at 0 ppm (singlet)
w_tms = 0.012
intensity += 0.4 * w_tms**2 / ((chemical_shift - 0.0) ** 2 + w_tms**2)

# CH3 triplet near 1.18 ppm (ratio 1:2:1)
triplet_center = 1.18
j_coupling = 0.07
intensity += 0.7 * w**2 / ((chemical_shift - (triplet_center - j_coupling)) ** 2 + w**2)
intensity += 1.4 * w**2 / ((chemical_shift - triplet_center) ** 2 + w**2)
intensity += 0.7 * w**2 / ((chemical_shift - (triplet_center + j_coupling)) ** 2 + w**2)

# CH2 quartet near 3.69 ppm (ratio 1:3:3:1)
quartet_center = 3.69
intensity += 0.35 * w**2 / ((chemical_shift - (quartet_center - 1.5 * j_coupling)) ** 2 + w**2)
intensity += 1.05 * w**2 / ((chemical_shift - (quartet_center - 0.5 * j_coupling)) ** 2 + w**2)
intensity += 1.05 * w**2 / ((chemical_shift - (quartet_center + 0.5 * j_coupling)) ** 2 + w**2)
intensity += 0.35 * w**2 / ((chemical_shift - (quartet_center + 1.5 * j_coupling)) ** 2 + w**2)

# OH singlet near 2.61 ppm
intensity += 0.55 * w**2 / ((chemical_shift - 2.61) ** 2 + w**2)

# Slight baseline noise
intensity += np.random.normal(0, 0.003, len(chemical_shift))
intensity = np.maximum(intensity, 0)

df = pd.DataFrame({"chemical_shift": chemical_shift, "intensity": intensity})

# Peak annotation data
labels_df = pd.DataFrame(
    {
        "x": [triplet_center, quartet_center, 2.61, 0.0],
        "y": [1.52, 1.17, 0.67, 0.45],
        "peak_y": [1.40, 1.05, 0.55, 0.40],
        "label": ["CH₃ (triplet)\n1.18 ppm", "CH₂ (quartet)\n3.69 ppm", "OH (singlet)\n2.61 ppm", "TMS\n0.00"],
        "group": ["main", "main", "main", "ref"],
    }
)

main_labels = labels_df[labels_df["group"] == "main"]
ref_labels = labels_df[labels_df["group"] == "ref"]

vline_df = pd.DataFrame({"xintercept": [triplet_center, quartet_center, 2.61]})

# Plot
plot = (
    ggplot(df, aes(x="chemical_shift", y="intensity"))
    + geom_area(fill=BRAND, alpha=0.08)
    + geom_vline(
        data=vline_df,
        mapping=aes(xintercept="xintercept"),
        color=BRAND,
        alpha=0.12,
        size=0.5,
        linetype="dashed",
        inherit_aes=False,
    )
    + geom_line(color=BRAND, size=1.0)
    + geom_point(data=labels_df, mapping=aes(x="x", y="peak_y"), color=BRAND, size=2.5, shape="o", inherit_aes=False)
    + geom_segment(
        data=labels_df,
        mapping=aes(x="x", xend="x", y="peak_y", yend="y"),
        color=INK_SOFT,
        size=0.4,
        linetype="dotted",
        inherit_aes=False,
    )
    + geom_text(
        data=main_labels,
        mapping=aes(x="x", y="y", label="label"),
        size=3.5,
        color=INK,
        fontweight="bold",
        va="bottom",
        ha="center",
        inherit_aes=False,
    )
    + geom_text(
        data=ref_labels,
        mapping=aes(x="x", y="y", label="label"),
        size=3.0,
        color=INK_SOFT,
        va="bottom",
        ha="center",
        inherit_aes=False,
    )
    + annotate("text", x=4.6, y=0.03, label="baseline", size=2.5, color=INK_MUTED, fontstyle="italic")
    + scale_x_reverse(limits=(5.0, -0.5), breaks=[0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0])
    + scale_y_continuous(breaks=[], limits=(-0.05, 1.72))
    + coord_cartesian(expand=False)
    + labs(x="Chemical Shift (ppm)", y="Intensity", title=title)
    + theme_minimal()
    + theme(
        figure_size=(8, 4.5),
        text=element_text(size=7, color=INK_SOFT),
        axis_title_x=element_text(size=10, color=INK, margin={"t": 8}),
        axis_title_y=element_text(size=10, color=INK_MUTED),
        axis_text_x=element_text(size=8, color=INK_SOFT),
        axis_text_y=element_blank(),
        axis_ticks_major_y=element_blank(),
        axis_line_x=element_line(color=INK_SOFT, size=0.6),
        axis_line_y=element_blank(),
        plot_title=element_text(size=title_fontsize, color=INK, margin={"b": 8}),
        panel_background=element_rect(fill=PAGE_BG, color="none"),
        plot_background=element_rect(fill=PAGE_BG, color="none"),
        panel_border=element_blank(),
        panel_grid_major_x=element_line(color=INK, size=0.3, alpha=0.15),
        panel_grid_major_y=element_blank(),
        panel_grid_minor=element_blank(),
        plot_margin=0.04,
    )
)

plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in", verbose=False)

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

Other implementations