NMR Spectrum (Nuclear Magnetic Resonance) — Plotly

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 Plotly

Python source (Plotly)

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

import os

import numpy as np
import plotly.graph_objects as go


# 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"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
GRID = "rgba(26,26,23,0.15)" if THEME == "light" else "rgba(240,239,232,0.15)"

# Imprint palette — first series always #009E73
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]
BRAND = IMPRINT_PALETTE[0]  # #009E73 — spectrum line

# Data — synthetic 1H NMR spectrum of ethanol
np.random.seed(42)
chemical_shift = np.linspace(-0.5, 5.0, 6000)
w = 0.012  # peak width (standard deviation)

# TMS reference peak at 0 ppm
intensity = 0.4 * np.exp(-((chemical_shift - 0.0) ** 2) / (2 * w**2))

# CH3 triplet near 1.2 ppm (1:2:1 ratio)
intensity += 0.55 * np.exp(-((chemical_shift - 1.11) ** 2) / (2 * w**2))
intensity += 1.10 * np.exp(-((chemical_shift - 1.18) ** 2) / (2 * w**2))
intensity += 0.55 * np.exp(-((chemical_shift - 1.25) ** 2) / (2 * w**2))

# OH singlet near 2.6 ppm
intensity += 0.35 * np.exp(-((chemical_shift - 2.61) ** 2) / (2 * 0.015**2))

# CH2 quartet near 3.7 ppm (1:3:3:1 ratio)
intensity += 0.25 * np.exp(-((chemical_shift - 3.585) ** 2) / (2 * w**2))
intensity += 0.75 * np.exp(-((chemical_shift - 3.655) ** 2) / (2 * w**2))
intensity += 0.75 * np.exp(-((chemical_shift - 3.725) ** 2) / (2 * w**2))
intensity += 0.25 * np.exp(-((chemical_shift - 3.795) ** 2) / (2 * w**2))

# Subtle baseline noise
intensity += np.random.normal(0, 0.003, len(chemical_shift))
intensity = np.clip(intensity, 0, None)

# Imprint palette assignment per peak group
peak_colors = {
    "TMS": IMPRINT_PALETTE[0],  # brand green
    "CH₃": IMPRINT_PALETTE[2],  # blue
    "OH": IMPRINT_PALETTE[1],  # lavender
    "CH₂": IMPRINT_PALETTE[3],  # ochre
}


def hex_to_rgba(hex_color, alpha):
    r = int(hex_color[1:3], 16)
    g = int(hex_color[3:5], 16)
    b = int(hex_color[5:7], 16)
    return f"rgba({r},{g},{b},{alpha})"


# Plot
fig = go.Figure()

# Main spectrum trace — Imprint brand green
fill_alpha = 0.07 if THEME == "light" else 0.12
fig.add_trace(
    go.Scatter(
        x=chemical_shift,
        y=intensity,
        mode="lines",
        line={"color": BRAND, "width": 2.5, "shape": "spline"},
        fill="tozeroy",
        fillcolor=hex_to_rgba(BRAND, fill_alpha),
        hovertemplate="δ %{x:.2f} ppm<br>Intensity: %{y:.3f}<extra></extra>",
        showlegend=False,
    )
)

# Region shadings — Imprint palette, slightly higher opacity on dark for perceptibility
region_alpha = 0.07 if THEME == "light" else 0.13
peak_regions = [
    {"x0": -0.05, "x1": 0.05, "color": peak_colors["TMS"]},
    {"x0": 1.05, "x1": 1.32, "color": peak_colors["CH₃"]},
    {"x0": 2.53, "x1": 2.69, "color": peak_colors["OH"]},
    {"x0": 3.55, "x1": 3.84, "color": peak_colors["CH₂"]},
]
shapes = [
    {
        "type": "rect",
        "xref": "x",
        "yref": "paper",
        "x0": region["x0"],
        "x1": region["x1"],
        "y0": 0,
        "y1": 1,
        "fillcolor": hex_to_rgba(region["color"], region_alpha),
        "line": {"width": 0},
        "layer": "below",
    }
    for region in peak_regions
]

# Annotations — color-coded, theme-adaptive text and backgrounds
annotations_data = [
    {"x": 0.0, "y": 0.42, "text": "<b>TMS</b><br><i>δ</i> 0.00", "key": "TMS", "ay": -55},
    {"x": 1.18, "y": 1.13, "text": "<b>CH₃</b> triplet<br><i>δ</i> 1.18", "key": "CH₃", "ay": -50},
    {"x": 2.61, "y": 0.38, "text": "<b>OH</b> singlet<br><i>δ</i> 2.61", "key": "OH", "ay": -55},
    {"x": 3.69, "y": 0.78, "text": "<b>CH₂</b> quartet<br><i>δ</i> 3.69", "key": "CH₂", "ay": -50},
]
styled_annotations = [
    {
        "x": ann["x"],
        "y": ann["y"],
        "text": ann["text"],
        "showarrow": True,
        "arrowhead": 3,
        "arrowsize": 1.2,
        "arrowwidth": 2,
        "arrowcolor": peak_colors[ann["key"]],
        "font": {"size": 11, "color": INK, "family": "Arial, sans-serif"},
        "ax": 0,
        "ay": ann["ay"],
        "bgcolor": ELEVATED_BG,
        "bordercolor": peak_colors[ann["key"]],
        "borderwidth": 1.5,
        "borderpad": 4,
    }
    for ann in annotations_data
]

# Title — n=60 < 67 baseline, use default 16px
title_text = "Ethanol ¹H NMR · spectrum-nmr · python · plotly · anyplot.ai"

# Layout
fig.update_layout(
    autosize=False,
    paper_bgcolor=PAGE_BG,
    plot_bgcolor=PAGE_BG,
    font={"color": INK, "family": "Arial, sans-serif"},
    title={
        "text": title_text,
        "font": {"size": 16, "color": INK, "family": "Arial, sans-serif"},
        "x": 0.5,
        "xanchor": "center",
    },
    xaxis={
        "title": {"text": "Chemical Shift <i>δ</i> (ppm)", "font": {"size": 12, "color": INK}, "standoff": 12},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "autorange": "reversed",
        "range": [5.0, -0.5],
        "dtick": 0.5,
        "showgrid": False,
        "zeroline": False,
        "showline": True,
        "linecolor": INK_SOFT,
        "linewidth": 1.5,
        "ticks": "outside",
        "ticklen": 8,
        "tickwidth": 1.5,
        "tickcolor": INK_SOFT,
        "minor": {"dtick": 0.1, "ticks": "outside", "ticklen": 4, "tickcolor": INK_MUTED},
        "spikemode": "across",
        "spikethickness": 1,
        "spikecolor": INK_MUTED,
        "spikedash": "dot",
    },
    yaxis={
        "title": {"text": "Intensity (a.u.)", "font": {"size": 12, "color": INK}, "standoff": 8},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "showgrid": True,
        "gridcolor": GRID,
        "gridwidth": 1,
        "griddash": "dot",
        "zeroline": True,
        "zerolinecolor": INK_SOFT,
        "zerolinewidth": 1.5,
        "showline": True,
        "linecolor": INK_SOFT,
        "linewidth": 1.5,
        "ticks": "outside",
        "ticklen": 8,
        "tickwidth": 1.5,
        "tickcolor": INK_SOFT,
        "rangemode": "tozero",
    },
    template=None,
    annotations=styled_annotations,
    shapes=shapes,
    showlegend=False,
    margin={"l": 80, "r": 40, "t": 80, "b": 60},
    hoverlabel={"bgcolor": ELEVATED_BG, "bordercolor": BRAND, "font": {"size": 11, "color": INK}},
    hovermode="x unified",
)

# Save — landscape 3200×1800
fig.write_image(f"plot-{THEME}.png", width=800, height=450, scale=4)
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

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

Other implementations