Tanabe-Sugano Diagram for Crystal Field Theory in plotnine (Python)

The same plot in 14 other libraries — Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, Pygal, Seaborn; 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 plotnine

Renders

Python source (plotnine)

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

import os

import numpy as np
import pandas as pd
from plotnine import (
    aes,
    element_blank,
    element_line,
    element_rect,
    element_text,
    geom_line,
    geom_text,
    ggplot,
    labs,
    scale_color_manual,
    scale_linetype_manual,
    scale_size_manual,
    scale_x_continuous,
    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"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
SPIN_ALLOWED = "#009E73"  # Imprint palette position 1 — always the first series
SPIN_FORBIDDEN = "#C475FD"  # Imprint palette position 2

# Data — d2 (V3+) term energies from the Tanabe-Sugano matrices, in units of the
# Racah parameter B at C/B = 4.42, with Dq = (delta_o / B) / 10
c_over_b = 4.42
delta_over_b = np.linspace(0, 40, 321)
dq = delta_over_b / 10.0
ground_term = r"$^{3}T_{1g}(F)$"

# A symmetry that occurs once in the d2 configuration is a single state, so its
# energy is linear in the ligand-field strength
energies = {r"$^{3}T_{2g}$": -8 + 2 * dq, r"$^{3}A_{2g}$": -8 + 12 * dq, r"$^{1}T_{1g}(G)$": 4 + 2 * c_over_b + 2 * dq}

# A symmetry that occurs twice gives a 2x2 matrix (diagonal, off-diagonal mixing,
# diagonal); diagonalizing it keeps the two roots an avoided crossing instead of
# letting the curves touch and swap identity
blocks = {
    (r"$^{3}T_{1g}(F)$", r"$^{3}T_{1g}(P)$"): (-5 - 8 * dq, -6.0, 4 + 2 * dq),
    (r"$^{1}E_{g}(D)$", r"$^{1}E_{g}(G)$"): (1 + 2 * c_over_b - 8 * dq, 2 * np.sqrt(3), 2 * c_over_b + 12 * dq),
    (r"$^{1}T_{2g}(D)$", r"$^{1}T_{2g}(G)$"): (1 + 2 * c_over_b - 8 * dq, 2 * np.sqrt(3), 2 * c_over_b + 2 * dq),
    (r"$^{1}A_{1g}(G)$", r"$^{1}A_{1g}(S)$"): (
        10 + 5 * c_over_b - 8 * dq,
        np.sqrt(6) * (2 + c_over_b),
        8 + 4 * c_over_b + 12 * dq,
    ),
}
for (lower_term, upper_term), (first, mixing, second) in blocks.items():
    block = np.zeros((len(dq), 2, 2))
    block[:, 0, 0] = first
    block[:, 1, 1] = second
    block[:, 0, 1] = block[:, 1, 0] = mixing
    energies[lower_term], energies[upper_term] = np.linalg.eigvalsh(block).T

# Every energy is measured from the ground term, which is flat at E/B = 0 by
# construction; the upper partners of the avoided crossings keep climbing past
# the top of the window, so they are clipped where they leave the frame
terms = pd.DataFrame(energies, index=delta_over_b)
terms = terms.sub(terms[ground_term], axis=0)

df = terms.stack().reset_index()
df.columns = ["delta_over_b", "term", "energy_over_b"]
df = df[df["energy_over_b"] <= 80]
df["spin"] = pd.Categorical(
    np.where(df["term"].str.startswith("$^{3}"), "Spin-allowed (triplet)", "Spin-forbidden (singlet)"),
    categories=["Spin-allowed (triplet)", "Spin-forbidden (singlet)"],
    ordered=True,
)

# Term labels sit in a gutter right of the curves, each at its own end value. The
# two 1D-derived terms stay near-coincident all the way across and the two upper
# partners leave the window, so those four are labelled along the curve instead,
# each resting on the line it names — above it for the upper term of a pair,
# below it for the lower one
along_curve = {r"$^{1}E_{g}(D)$": 35.0, r"$^{1}T_{2g}(D)$": 22.0, r"$^{1}E_{g}(G)$": 30.0, r"$^{1}A_{1g}(S)$": 17.0}
anchors = pd.DataFrame(
    [
        {
            "delta_over_b": x,
            "term": term,
            "energy_over_b": terms[term].to_numpy()[np.searchsorted(delta_over_b, x)],
            "spin": "Spin-forbidden (singlet)",  # all four along-curve terms are singlets
        }
        for term, x in along_curve.items()
    ]
)
lower_of_pair = anchors["term"] == r"$^{1}T_{2g}(D)$"

curve_ends = df[(df["delta_over_b"] == delta_over_b[-1]) & ~df["term"].isin(along_curve)].copy()
curve_ends["delta_over_b"] = 41.2

# Plot
plot = (
    ggplot(df, aes("delta_over_b", "energy_over_b", group="term", color="spin"))
    + geom_line(aes(linetype="spin", size="spin"))
    + geom_text(aes(label="term"), data=curve_ends, size=10, ha="left", show_legend=False)
    + geom_text(aes(label="term"), data=anchors[~lower_of_pair], size=10, ha="right", va="bottom", show_legend=False)
    + geom_text(aes(label="term"), data=anchors[lower_of_pair], size=10, ha="right", va="top", show_legend=False)
    + scale_color_manual(values=[SPIN_ALLOWED, SPIN_FORBIDDEN])
    + scale_linetype_manual(values=["solid", "dashed"])
    + scale_size_manual(values=[1.7, 0.75])
    + scale_x_continuous(limits=(0, 47.5), breaks=range(0, 41, 5), expand=(0.008, 0))
    + scale_y_continuous(limits=(0, 80), breaks=range(0, 81, 10), expand=(0.022, 0))
    + labs(
        x="Ligand-field strength  $\\Delta_{o}/B$",
        y="Term energy  $E/B$",
        title="line-tanabe-sugano · python · plotnine · anyplot.ai",
        subtitle="V$^{3+}$ d$^{2}$ in an octahedral field, C/B = 4.42",
        color="",
        linetype="",
        size="",
    )
    + theme_minimal()
    + theme(
        figure_size=(6, 6),
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG),
        panel_border=element_blank(),
        panel_grid_major=element_line(color=INK, size=0.3, alpha=0.15),
        panel_grid_minor=element_blank(),
        axis_line=element_line(color=INK_SOFT, size=0.5),
        axis_ticks=element_blank(),
        axis_title=element_text(size=10, color=INK),
        axis_text=element_text(size=8, color=INK_SOFT),
        plot_title=element_text(size=12, color=INK),
        plot_subtitle=element_text(size=9, color=INK_SOFT),
        legend_position="bottom",
        legend_background=element_rect(fill=ELEVATED_BG, color=ELEVATED_BG),
        legend_key=element_rect(fill=ELEVATED_BG, color=ELEVATED_BG),
        legend_key_width=26,
        legend_margin=8,
        legend_title=element_blank(),
        legend_text=element_text(size=8, color=INK_SOFT),
    )
)

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

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/line-tanabe-sugano/plotnine/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": "plotnine",
  "page": "https://anyplot.ai/line-tanabe-sugano/python/plotnine",
  "hub": "https://anyplot.ai/line-tanabe-sugano",
  "code_json": "https://api.anyplot.ai/specs/line-tanabe-sugano/plotnine/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/plotnine/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/line-tanabe-sugano/python/plotnine/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