Tanabe-Sugano Diagram for Crystal Field Theory 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: Tanabe-Sugano Diagram for Crystal Field Theory in Python, R, Julia and JavaScript.

A Tanabe-Sugano diagram plots the energies of the electronic term states of a transition-metal ion in an octahedral ligand field as continuous functions of the field strength. The x-axis is the reduced ligand-field strength Δ_o/B (octahedral splitting divided by the Racah parameter B) and the y-axis is the reduced term energy E/B measured from the ground term, which therefore runs along the x-axis itself as a horizontal line at E/B = 0. Each term — labeled with its full symbol including spin multiplicity and Mulliken label — is one curve, and for d⁴–d⁷ a vertical line marks the high-spin/low-spin crossover where the ground term changes and every curve kinks. Chemists read the chart in reverse: matching ratios of observed UV-Vis band energies against the curves yields both Δ_o and B for a real complex.

Tanabe-Sugano Diagram for Crystal Field Theory rendered with Seaborn

Renders

Python source (Seaborn)

""" anyplot.ai
line-tanabe-sugano: Tanabe-Sugano Diagram for Crystal Field Theory
Library: seaborn 0.13.2 | Python 3.13.15
Quality: 90/100 | Created: 2026-10-01
"""

import os

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


# Theme tokens (see prompts/default-style-guide.md "Theme-adaptive Chrome")
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"

# Imprint palette — spin-allowed is position 1 (brand green), spin-forbidden position 2
ALLOWED = "spin-allowed (quartet)"
FORBIDDEN = "spin-forbidden (doublet)"
SPIN_COLORS = {ALLOWED: "#009E73", FORBIDDEN: "#C475FD"}

# Data — d7 (Co2+) in an octahedral field: the Tanabe-Sugano matrices in the
# strong-field basis, written in units of B with Dq/B on the diagonal
# (Tanabe & Sugano, J. Phys. Soc. Jpn. 9, 753 (1954); C/B is their d7 value).
C_OVER_B = 4.633
delta_over_b = np.linspace(0, 30, 301)
dq = delta_over_b / 10  # Delta_o = 10 Dq
sqrt2, sqrt3 = np.sqrt(2), np.sqrt(3)

ts_blocks = {
    "4T1": ([2 * dq - 3, -8 * dq - 12], {(0, 1): 6}),
    "4T2": ([2 * dq - 15], {}),
    "4A2": ([12 * dq - 15], {}),
    "2A1": ([2 * dq - 11 + 3 * C_OVER_B], {}),
    "2A2": ([2 * dq + 9 + 3 * C_OVER_B], {}),
    "2E": (
        [12 * dq - 6 + 3 * C_OVER_B, 2 * dq + 8 + 6 * C_OVER_B, 2 * dq - 1 + 3 * C_OVER_B, -18 * dq - 8 + 4 * C_OVER_B],
        {(0, 1): -6 * sqrt2, (0, 2): -3 * sqrt2, (1, 2): 10, (1, 3): sqrt3 * (2 + C_OVER_B), (2, 3): 2 * sqrt3},
    ),
    "2T1": (
        [
            12 * dq - 6 + 3 * C_OVER_B,
            2 * dq + 3 * C_OVER_B,
            2 * dq - 6 + 3 * C_OVER_B,
            -8 * dq - 6 + 3 * C_OVER_B,
            -8 * dq - 2 + 3 * C_OVER_B,
        ],
        {
            (0, 1): -3,
            (0, 2): 3,
            (0, 4): -2 * sqrt3,
            (1, 2): -3,
            (1, 3): 3,
            (1, 4): 3 * sqrt3,
            (2, 3): -3,
            (2, 4): -sqrt3,
            (3, 4): 2 * sqrt3,
        },
    ),
    "2T2": (
        [
            12 * dq + 5 * C_OVER_B,
            2 * dq - 6 + 3 * C_OVER_B,
            2 * dq + 4 + 3 * C_OVER_B,
            -8 * dq + 6 + 5 * C_OVER_B,
            -8 * dq - 2 + 3 * C_OVER_B,
        ],
        {
            (0, 1): -3 * sqrt3,
            (0, 2): -5 * sqrt3,
            (0, 3): 4 + 2 * C_OVER_B,
            (0, 4): 2,
            (1, 2): 3,
            (1, 3): -3 * sqrt3,
            (1, 4): -3 * sqrt3,
            (2, 3): -sqrt3,
            (2, 4): sqrt3,
            (3, 4): 10,
        },
    ),
}

# Diagonalize every symmetry block at each field strength (roots come out ascending,
# so terms of the same symmetry and spin only ever approach, never cross)
block_roots = {}
for block, (diagonal, off_diagonal) in ts_blocks.items():
    size = len(diagonal)
    matrices = np.zeros((delta_over_b.size, size, size))
    for i, element in enumerate(diagonal):
        matrices[:, i, i] = element
    for (i, j), element in off_diagonal.items():
        matrices[:, i, j] = matrices[:, j, i] = element
    block_roots[block] = np.linalg.eigvalsh(matrices)

# Term symbol, its symmetry block, which root of that block, spin class
terms = [
    (r"$^{4}T_{1g}(F)$", "4T1", 0, ALLOWED),
    (r"$^{4}T_{2g}$", "4T2", 0, ALLOWED),
    (r"$^{4}A_{2g}$", "4A2", 0, ALLOWED),
    (r"$^{4}T_{1g}(P)$", "4T1", 1, ALLOWED),
    (r"$^{2}E_{g}$", "2E", 0, FORBIDDEN),
    (r"$^{2}T_{1g}$", "2T1", 0, FORBIDDEN),
    (r"$^{2}T_{2g}$", "2T2", 0, FORBIDDEN),
    (r"$^{2}A_{1g}$", "2A1", 0, FORBIDDEN),
    (r"$^{2}A_{2g}$", "2A2", 0, FORBIDDEN),
]

# Energies are measured from the ground term, which switches from the high-spin
# 4T1g to the low-spin 2Eg at the crossover and kinks every curve there
ground_energy = np.min([block_roots[block][:, root] for _, block, root, _ in terms], axis=0)
curves = pd.concat(
    [
        pd.DataFrame(
            {
                "delta_over_b": delta_over_b,
                "energy_over_b": block_roots[block][:, root] - ground_energy,
                "term": term,
                "spin_class": spin_class,
            }
        )
        for term, block, root, spin_class in terms
    ],
    ignore_index=True,
)
crossover = np.interp(0.0, block_roots["4T1"][:, 0] - block_roots["2E"][:, 0], delta_over_b)

# Plot
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,
    },
)
fig, ax = plt.subplots(figsize=(6, 6), dpi=400)
sns.lineplot(
    data=curves,
    x="delta_over_b",
    y="energy_over_b",
    units="term",
    estimator=None,
    hue="spin_class",
    style="spin_class",
    size="spin_class",
    hue_order=[ALLOWED, FORBIDDEN],
    palette=SPIN_COLORS,
    dashes={ALLOWED: "", FORBIDDEN: (4, 2)},
    sizes={ALLOWED: 3.0, FORBIDDEN: 1.5},
    ax=ax,
)

# High-spin / low-spin crossover
ax.axvline(crossover, color=INK_MUTED, linestyle=(0, (6, 4)), linewidth=1.6, zorder=0)
ax.text(
    crossover - 0.8,
    71,
    f"high-spin → low-spin\n$\\Delta_o/B$ = {crossover:.1f}",
    ha="right",
    va="center",
    fontsize=8,
    color=INK_MUTED,
    linespacing=1.5,
)

# Term labels at the right edge, nudged apart where curves converge
label_y = 0.0
for end_energy, term, spin_class in sorted(
    (float(block_roots[block][-1, root] - ground_energy[-1]), term, spin_class)
    for term, block, root, spin_class in terms
):
    label_y = max(end_energy, label_y + 2.6)
    ax.text(
        delta_over_b[-1] + 0.6, label_y, term, va="center", fontsize=9, color=SPIN_COLORS[spin_class], clip_on=False
    )

# Style
ax.set_title(
    "Co$^{2+}$ d$^7$, C/B = 4.63 · line-tanabe-sugano · python · seaborn · anyplot.ai",
    fontsize=10,
    fontweight="medium",
    color=INK,
    pad=12,
)
ax.set_xlabel("Ligand-field strength $\\Delta_o/B$", fontsize=10)
ax.set_ylabel("Term energy above ground term $E/B$", fontsize=10)
ax.set_xlim(0, 30)
ax.set_ylim(-1.6, 78)
ax.set_xticks(np.arange(0, 31, 5))
ax.set_yticks(np.arange(0, 78, 10))
ax.tick_params(axis="both", labelsize=8, length=3)
ax.grid(True, alpha=0.15, linewidth=0.8)
ax.legend(*ax.get_legend_handles_labels(), title=None, fontsize=8, loc="upper left", borderpad=0.8)
sns.despine(ax=ax)
fig.subplots_adjust(left=0.1, right=0.87, top=0.93, bottom=0.09)

# Save
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/line-tanabe-sugano/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": "line-tanabe-sugano",
  "language": "python",
  "library": "seaborn",
  "page": "https://anyplot.ai/line-tanabe-sugano/python/seaborn",
  "hub": "https://anyplot.ai/line-tanabe-sugano",
  "code_json": "https://api.anyplot.ai/specs/line-tanabe-sugano/seaborn/code",
  "spec_json": "https://api.anyplot.ai/specs/line-tanabe-sugano",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/line-tanabe-sugano/python/seaborn/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/line-tanabe-sugano/python/seaborn/plot-dark.png",
  "quality_score": 90.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Tanabe-Sugano Diagram for Crystal Field Theory on anyplot.ai.

Other implementations