{"spec_id":"line-tanabe-sugano","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-tanabe-sugano: Tanabe-Sugano Diagram for Crystal Field Theory\n// Library: d3 7.9.0 | JavaScript 22.23.3\n// Quality: 90/100 | Created: 2026-10-01\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 124, right: 152, bottom: 96, left: 112 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\nconst FIELD_MAX = 40; // Δ_o/B\nconst ENERGY_MAX = 80; // E/B\nconst LABEL_PX = 18;\n// Imprint muted anchor — the \"rest\" class, here the weak spin-forbidden terms.\nconst MUTED = window.ANYPLOT_THEME === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\n\n// Term symbols are written in a tiny markup — \"^{3}T_{1g}(F)\" — and typeset into\n// tspans so the multiplicity is a true superscript and the Mulliken index a true\n// subscript, instead of printing the plain-text key.\nconst typeset = (node, markup, px) => {\n  let shift = 0;\n  for (const [, mark, marked, plain] of markup.matchAll(/([\\^_])\\{([^}]*)\\}|([^\\^_]+)/g)) {\n    const baseline = mark === \"^\" ? -0.44 * px : mark === \"_\" ? 0.26 * px : 0;\n    node\n      .append(\"tspan\")\n      .attr(\"dy\", baseline - shift)\n      .attr(\"font-size\", mark ? `${Math.round(px * 0.72)}px` : null)\n      .text(mark ? marked : plain);\n    shift = baseline;\n  }\n};\n\n// --- Data: Tanabe-Sugano matrices for d² (V³⁺) at C/B = 4.42 ------------------\n// Energies in units of B relative to the Racah A parameter, with q = Dq/B. Every\n// term is an eigenvalue of its strong-field block, so terms sharing symmetry and\n// multiplicity repel each other instead of crossing — the avoided crossings fall\n// out of the algebra. At q = 0 the eigenvalues reduce to the free-ion terms\n// (³F −8B, ³P 7B, ¹D −3B+2C, ¹G 4B+2C, ¹S 14B+7C).\nconst CB = 4.42;\nconst eigen = (a, b, offDiag) => {\n  const centre = (a + b) / 2;\n  const spread = Math.hypot((a - b) / 2, offDiag);\n  return [centre - spread, centre + spread];\n};\nconst tripletT1 = (q) => eigen(-5 - 12 * q, 4 - 2 * q, 6);\nconst singletE = (q) => eigen(1 + 2 * CB - 12 * q, 2 * CB + 8 * q, 2 * Math.sqrt(3));\nconst singletT2 = (q) => eigen(1 + 2 * CB - 12 * q, 2 * CB - 2 * q, 2 * Math.sqrt(3));\nconst singletA1 = (q) => eigen(10 + 5 * CB - 12 * q, 8 + 4 * CB + 8 * q, Math.sqrt(6) * (2 + CB));\n\nconst terms = [\n  { symbol: \"^{3}T_{1g}(F)\", allowed: true, color: t.palette[0], energy: (q) => tripletT1(q)[0] },\n  { symbol: \"^{3}T_{2g}\", allowed: true, color: t.palette[1], energy: (q) => -8 - 2 * q },\n  { symbol: \"^{3}T_{1g}(P)\", allowed: true, color: t.palette[2], energy: (q) => tripletT1(q)[1] },\n  { symbol: \"^{3}A_{2g}\", allowed: true, color: t.palette[3], energy: (q) => -8 + 8 * q },\n  { symbol: \"^{1}E_{g}(D)\", allowed: false, color: MUTED, energy: (q) => singletE(q)[0] },\n  { symbol: \"^{1}T_{2g}(D)\", allowed: false, color: MUTED, energy: (q) => singletT2(q)[0] },\n  { symbol: \"^{1}T_{1g}\", allowed: false, color: MUTED, energy: (q) => 4 + 2 * CB - 2 * q },\n  { symbol: \"^{1}A_{1g}(G)\", allowed: false, color: MUTED, energy: (q) => singletA1(q)[0] },\n  { symbol: \"^{1}T_{2g}(G)\", allowed: false, color: MUTED, energy: (q) => singletT2(q)[1] },\n  { symbol: \"^{1}E_{g}(G)\", allowed: false, color: MUTED, energy: (q) => singletE(q)[1] },\n  { symbol: \"^{1}A_{1g}(S)\", allowed: false, color: MUTED, energy: (q) => singletA1(q)[1] },\n];\n\n// Δ_o/B = 10·Dq/B, 321 samples; the ground term ³T₁g(F) is the energy reference,\n// so it is a flat curve along E/B = 0.\nconst field = d3.range(0, FIELD_MAX + 1e-9, 0.125);\nconst curves = terms.map((term) => ({\n  ...term,\n  points: field.map((fieldStrength) => ({\n    fieldStrength,\n    energy: term.energy(fieldStrength / 10) - tripletT1(fieldStrength / 10)[0],\n  })),\n}));\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales ------------------------------------------------------------------\nconst x = d3.scaleLinear().domain([0, FIELD_MAX]).range([0, iw]);\nconst y = d3.scaleLinear().domain([0, ENERGY_MAX]).range([ih, 0]);\n\n// --- Grid --------------------------------------------------------------------\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(x.ticks(8))\n  .join(\"line\")\n  .attr(\"x1\", x)\n  .attr(\"x2\", x)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid);\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(8))\n  .join(\"line\")\n  .attr(\"y1\", y)\n  .attr(\"y2\", y)\n  .attr(\"x2\", iw)\n  .attr(\"stroke\", t.grid);\n\n// --- Axes --------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8).tickSize(0).tickPadding(14));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(8).tickSize(0).tickPadding(14));\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"17px\");\n  axis.select(\".domain\").attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1.4);\n}\n\nconst xLabel = svg\n  .append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 28)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"19px\");\ntypeset(xLabel, \"Reduced ligand-field strength Δ_{o}/B\", 19);\nsvg\n  .append(\"text\")\n  .attr(\"transform\", `translate(36,${margin.top + ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"19px\")\n  .text(\"Reduced term energy E/B\");\n\n// --- Term curves: spin-allowed thick and solid, spin-forbidden thin and dashed\nsvg.append(\"clipPath\").attr(\"id\", \"plot-area\").append(\"rect\").attr(\"width\", iw).attr(\"height\", ih);\nconst line = d3\n  .line()\n  .x((p) => x(p.fieldStrength))\n  .y((p) => y(p.energy));\ng.append(\"g\")\n  .attr(\"clip-path\", \"url(#plot-area)\")\n  .selectAll(\"path\")\n  .data(curves)\n  .join(\"path\")\n  .attr(\"d\", (curve) => line(curve.points))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", (curve) => curve.color)\n  .attr(\"stroke-width\", (curve) => (curve.allowed ? 4 : 2))\n  .attr(\"stroke-dasharray\", (curve) => (curve.allowed ? null : \"11 8\"));\n\n// --- Term labels at the right edge, or where a curve leaves the top ----------\nconst labels = curves.map((curve) => {\n  const end = curve.points[curve.points.length - 1];\n  if (end.energy <= ENERGY_MAX) {\n    return { ...curve, atEdge: true, px: iw + 16, py: y(end.energy) };\n  }\n  // The curve leaves the top of the frame: label it along the curve instead,\n  // sitting just above the line where it is still well inside.\n  const anchor = curve.points.filter((p) => p.energy <= 0.86 * ENERGY_MAX).pop();\n  return {\n    ...curve,\n    atEdge: false,\n    px: x(anchor.fieldStrength) - 12,\n    py: y(anchor.energy) - 20,\n  };\n});\n// Where two curves end near-coincident, the labels move apart — the curves stay\n// on their data values.\nconst atEdge = labels.filter((label) => label.atEdge).sort((a, b) => a.py - b.py);\natEdge.forEach((label, i) => {\n  if (i > 0) label.py = Math.max(label.py, atEdge[i - 1].py + LABEL_PX + 6);\n});\nfor (const label of labels) {\n  const node = svg\n    .append(\"text\")\n    .attr(\"x\", margin.left + label.px)\n    .attr(\"y\", margin.top + label.py)\n    .attr(\"text-anchor\", label.atEdge ? \"start\" : \"end\")\n    .attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", label.color)\n    .style(\"font-size\", `${LABEL_PX}px`)\n    .style(\"font-weight\", label.allowed ? \"600\" : \"400\");\n  typeset(node, label.symbol, LABEL_PX);\n}\n\n// --- Title, subtitle and the spin-selection key ------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 54)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"line-tanabe-sugano · javascript · d3 · anyplot.ai\");\nconst subtitle = svg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 92)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"18px\");\ntypeset(subtitle, \"d^{2} ion (V^{3+}) in an octahedral field · C/B = 4.42\", 18);\n\nconst legend = g.append(\"g\").attr(\"transform\", \"translate(28,30)\");\nconst key = [\n  { text: \"Spin-allowed (ΔS = 0)\", color: t.palette[0], dash: null, stroke: 4 },\n  { text: \"Spin-forbidden\", color: MUTED, dash: \"11 8\", stroke: 2 },\n];\nkey.forEach((entry, i) => {\n  legend\n    .append(\"line\")\n    .attr(\"x2\", 54)\n    .attr(\"y1\", i * 34)\n    .attr(\"y2\", i * 34)\n    .attr(\"stroke\", entry.color)\n    .attr(\"stroke-width\", entry.stroke)\n    .attr(\"stroke-dasharray\", entry.dash);\n  legend\n    .append(\"text\")\n    .attr(\"x\", 70)\n    .attr(\"y\", i * 34)\n    .attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"17px\")\n    .text(entry.text);\n});\n"}