elementData = [
{z: 1, symbol: "H", name: "Hydrogen", config: "1s1"},
{z: 2, symbol: "He", name: "Helium", config: "1s2"},
{z: 3, symbol: "Li", name: "Lithium", config: "[He] 2s1"},
{z: 4, symbol: "Be", name: "Beryllium", config: "[He] 2s2"},
{z: 5, symbol: "B", name: "Boron", config: "[He] 2s2 2p1"},
{z: 6, symbol: "C", name: "Carbon", config: "[He] 2s2 2p2"},
{z: 7, symbol: "N", name: "Nitrogen", config: "[He] 2s2 2p3"},
{z: 8, symbol: "O", name: "Oxygen", config: "[He] 2s2 2p4"},
{z: 9, symbol: "F", name: "Fluorine", config: "[He] 2s2 2p5"},
{z: 10, symbol: "Ne", name: "Neon", config: "[He] 2s2 2p6"},
{z: 11, symbol: "Na", name: "Sodium", config: "[Ne] 3s1"},
{z: 12, symbol: "Mg", name: "Magnesium", config: "[Ne] 3s2"},
{z: 13, symbol: "Al", name: "Aluminum", config: "[Ne] 3s2 3p1"},
{z: 14, symbol: "Si", name: "Silicon", config: "[Ne] 3s2 3p2"},
{z: 15, symbol: "P", name: "Phosphorus", config: "[Ne] 3s2 3p3"},
{z: 16, symbol: "S", name: "Sulfur", config: "[Ne] 3s2 3p4"},
{z: 17, symbol: "Cl", name: "Chlorine", config: "[Ne] 3s2 3p5"},
{z: 18, symbol: "Ar", name: "Argon", config: "[Ne] 3s2 3p6"},
{z: 19, symbol: "K", name: "Potassium", config: "[Ar] 4s1"},
{z: 20, symbol: "Ca", name: "Calcium", config: "[Ar] 4s2"},
{z: 21, symbol: "Sc", name: "Scandium", config: "[Ar] 4s2 3d1"},
{z: 22, symbol: "Ti", name: "Titanium", config: "[Ar] 4s2 3d2"},
{z: 23, symbol: "V", name: "Vanadium", config: "[Ar] 4s2 3d3"},
{z: 24, symbol: "Cr", name: "Chromium", config: "[Ar] 4s1 3d5"},
{z: 25, symbol: "Mn", name: "Manganese", config: "[Ar] 4s2 3d5"},
{z: 26, symbol: "Fe", name: "Iron", config: "[Ar] 4s2 3d6"},
{z: 27, symbol: "Co", name: "Cobalt", config: "[Ar] 4s2 3d7"},
{z: 28, symbol: "Ni", name: "Nickel", config: "[Ar] 4s2 3d8"},
{z: 29, symbol: "Cu", name: "Copper", config: "[Ar] 4s1 3d10"},
{z: 30, symbol: "Zn", name: "Zinc", config: "[Ar] 4s2 3d10"},
{z: 31, symbol: "Ga", name: "Gallium", config: "[Ar] 4s2 3d10 4p1"},
{z: 32, symbol: "Ge", name: "Germanium", config: "[Ar] 4s2 3d10 4p2"},
{z: 33, symbol: "As", name: "Arsenic", config: "[Ar] 4s2 3d10 4p3"},
{z: 34, symbol: "Se", name: "Selenium", config: "[Ar] 4s2 3d10 4p4"},
{z: 35, symbol: "Br", name: "Bromine", config: "[Ar] 4s2 3d10 4p5"},
{z: 36, symbol: "Kr", name: "Krypton", config: "[Ar] 4s2 3d10 4p6"},
{z: 37, symbol: "Rb", name: "Rubidium", config: "[Kr] 5s1"},
{z: 38, symbol: "Sr", name: "Strontium", config: "[Kr] 5s2"},
{z: 39, symbol: "Y", name: "Yttrium", config: "[Kr] 5s2 4d1"},
{z: 40, symbol: "Zr", name: "Zirconium", config: "[Kr] 5s2 4d2"},
{z: 41, symbol: "Nb", name: "Niobium", config: "[Kr] 5s1 4d4"},
{z: 42, symbol: "Mo", name: "Molybdenum", config: "[Kr] 5s1 4d5"},
{z: 43, symbol: "Tc", name: "Technetium", config: "[Kr] 5s2 4d5"},
{z: 44, symbol: "Ru", name: "Ruthenium", config: "[Kr] 5s1 4d7"},
{z: 45, symbol: "Rh", name: "Rhodium", config: "[Kr] 5s1 4d8"},
{z: 46, symbol: "Pd", name: "Palladium", config: "[Kr] 4d10"},
{z: 47, symbol: "Ag", name: "Silver", config: "[Kr] 5s1 4d10"},
{z: 48, symbol: "Cd", name: "Cadmium", config: "[Kr] 5s2 4d10"},
{z: 49, symbol: "In", name: "Indium", config: "[Kr] 5s2 4d10 5p1"},
{z: 50, symbol: "Sn", name: "Tin", config: "[Kr] 5s2 4d10 5p2"},
{z: 51, symbol: "Sb", name: "Antimony", config: "[Kr] 5s2 4d10 5p3"},
{z: 52, symbol: "Te", name: "Tellurium", config: "[Kr] 5s2 4d10 5p4"},
{z: 53, symbol: "I", name: "Iodine", config: "[Kr] 5s2 4d10 5p5"},
{z: 54, symbol: "Xe", name: "Xenon", config: "[Kr] 5s2 4d10 5p6"},
{z: 55, symbol: "Cs", name: "Cesium", config: "[Xe] 6s1"},
{z: 56, symbol: "Ba", name: "Barium", config: "[Xe] 6s2"},
{z: 57, symbol: "La", name: "Lanthanum", config: "[Xe] 6s2 5d1"},
{z: 58, symbol: "Ce", name: "Cerium", config: "[Xe] 6s2 4f1 5d1"},
{z: 59, symbol: "Pr", name: "Praseodymium", config: "[Xe] 6s2 4f3"},
{z: 60, symbol: "Nd", name: "Neodymium", config: "[Xe] 6s2 4f4"},
{z: 61, symbol: "Pm", name: "Promethium", config: "[Xe] 6s2 4f5"},
{z: 62, symbol: "Sm", name: "Samarium", config: "[Xe] 6s2 4f6"},
{z: 63, symbol: "Eu", name: "Europium", config: "[Xe] 6s2 4f7"},
{z: 64, symbol: "Gd", name: "Gadolinium", config: "[Xe] 6s2 4f7 5d1"},
{z: 65, symbol: "Tb", name: "Terbium", config: "[Xe] 6s2 4f9"},
{z: 66, symbol: "Dy", name: "Dysprosium", config: "[Xe] 6s2 4f10"},
{z: 67, symbol: "Ho", name: "Holmium", config: "[Xe] 6s2 4f11"},
{z: 68, symbol: "Er", name: "Erbium", config: "[Xe] 6s2 4f12"},
{z: 69, symbol: "Tm", name: "Thulium", config: "[Xe] 6s2 4f13"},
{z: 70, symbol: "Yb", name: "Ytterbium", config: "[Xe] 6s2 4f14"},
{z: 71, symbol: "Lu", name: "Lutetium", config: "[Xe] 6s2 4f14 5d1"},
{z: 72, symbol: "Hf", name: "Hafnium", config: "[Xe] 6s2 4f14 5d2"},
{z: 73, symbol: "Ta", name: "Tantalum", config: "[Xe] 6s2 4f14 5d3"},
{z: 74, symbol: "W", name: "Tungsten", config: "[Xe] 6s2 4f14 5d4"},
{z: 75, symbol: "Re", name: "Rhenium", config: "[Xe] 6s2 4f14 5d5"},
{z: 76, symbol: "Os", name: "Osmium", config: "[Xe] 6s2 4f14 5d6"},
{z: 77, symbol: "Ir", name: "Iridium", config: "[Xe] 6s2 4f14 5d7"},
{z: 78, symbol: "Pt", name: "Platinum", config: "[Xe] 6s1 4f14 5d9"},
{z: 79, symbol: "Au", name: "Gold", config: "[Xe] 6s1 4f14 5d10"},
{z: 80, symbol: "Hg", name: "Mercury", config: "[Xe] 6s2 4f14 5d10"},
{z: 81, symbol: "Tl", name: "Thallium", config: "[Xe] 6s2 4f14 5d10 6p1"},
{z: 82, symbol: "Pb", name: "Lead", config: "[Xe] 6s2 4f14 5d10 6p2"},
{z: 83, symbol: "Bi", name: "Bismuth", config: "[Xe] 6s2 4f14 5d10 6p3"},
{z: 84, symbol: "Po", name: "Polonium", config: "[Xe] 6s2 4f14 5d10 6p4"},
{z: 85, symbol: "At", name: "Astatine", config: "[Xe] 6s2 4f14 5d10 6p5"},
{z: 86, symbol: "Rn", name: "Radon", config: "[Xe] 6s2 4f14 5d10 6p6"},
{z: 87, symbol: "Fr", name: "Francium", config: "[Rn] 7s1"},
{z: 88, symbol: "Ra", name: "Radium", config: "[Rn] 7s2"},
{z: 89, symbol: "Ac", name: "Actinium", config: "[Rn] 7s2 6d1"},
{z: 90, symbol: "Th", name: "Thorium", config: "[Rn] 7s2 6d2"},
{z: 91, symbol: "Pa", name: "Protactinium", config: "[Rn] 7s2 5f2 6d1"},
{z: 92, symbol: "U", name: "Uranium", config: "[Rn] 7s2 5f3 6d1"},
{z: 93, symbol: "Np", name: "Neptunium", config: "[Rn] 7s2 5f4 6d1"},
{z: 94, symbol: "Pu", name: "Plutonium", config: "[Rn] 7s2 5f6"},
{z: 95, symbol: "Am", name: "Americium", config: "[Rn] 7s2 5f7"},
{z: 96, symbol: "Cm", name: "Curium", config: "[Rn] 7s2 5f7 6d1"},
{z: 97, symbol: "Bk", name: "Berkelium", config: "[Rn] 7s2 5f9"},
{z: 98, symbol: "Cf", name: "Californium", config: "[Rn] 7s2 5f10"},
{z: 99, symbol: "Es", name: "Einsteinium", config: "[Rn] 7s2 5f11"},
{z: 100, symbol: "Fm", name: "Fermium", config: "[Rn] 7s2 5f12"},
{z: 101, symbol: "Md", name: "Mendelevium", config: "[Rn] 7s2 5f13"},
{z: 102, symbol: "No", name: "Nobelium", config: "[Rn] 7s2 5f14"},
{z: 103, symbol: "Lr", name: "Lawrencium", config: "[Rn] 7s2 5f14 7p1"},
{z: 104, symbol: "Rf", name: "Rutherfordium", config: "[Rn] 7s2 5f14 6d2"},
{z: 105, symbol: "Db", name: "Dubnium", config: "[Rn] 7s2 5f14 6d3"},
{z: 106, symbol: "Sg", name: "Seaborgium", config: "[Rn] 7s2 5f14 6d4"},
{z: 107, symbol: "Bh", name: "Bohrium", config: "[Rn] 7s2 5f14 6d5"},
{z: 108, symbol: "Hs", name: "Hassium", config: "[Rn] 7s2 5f14 6d6"},
{z: 109, symbol: "Mt", name: "Meitnerium", config: "[Rn] 7s2 5f14 6d7"},
{z: 110, symbol: "Ds", name: "Darmstadtium", config: "[Rn] 7s2 5f14 6d8"},
{z: 111, symbol: "Rg", name: "Roentgenium", config: "[Rn] 7s2 5f14 6d9"},
{z: 112, symbol: "Cn", name: "Copernicium", config: "[Rn] 7s2 5f14 6d10"},
{z: 113, symbol: "Nh", name: "Nihonium", config: "[Rn] 7s2 5f14 6d10 7p1"},
{z: 114, symbol: "Fl", name: "Flerovium", config: "[Rn] 7s2 5f14 6d10 7p2"},
{z: 115, symbol: "Mc", name: "Moscovium", config: "[Rn] 7s2 5f14 6d10 7p3"},
{z: 116, symbol: "Lv", name: "Livermorium", config: "[Rn] 7s2 5f14 6d10 7p4"},
{z: 117, symbol: "Ts", name: "Tennessine", config: "[Rn] 7s2 5f14 6d10 7p5"},
{z: 118, symbol: "Og", name: "Oganesson", config: "[Rn] 7s2 5f14 6d10 7p6"}
]
// Exception elements - those that don't follow simple aufbau prediction
exceptionElements = ({
"24": { expected: "[Ar] 4s² 3d⁴", reason: "Exchange energy stabilization from half-filled 3d⁵ outweighs 4s² pairing" },
"29": { expected: "[Ar] 4s² 3d⁹", reason: "Filled 3d¹⁰ subshell provides extra stability" },
"41": { expected: "[Kr] 5s² 4d³", reason: "4d and 5s nearly degenerate; 4d⁴ 5s¹ minimizes electron repulsion" },
"42": { expected: "[Kr] 5s² 4d⁴", reason: "Exchange energy favors half-filled 4d⁵ with single 5s electron" },
"44": { expected: "[Kr] 5s² 4d⁶", reason: "4d⁷ 5s¹ configuration minimizes electron-electron repulsion" },
"45": { expected: "[Kr] 5s² 4d⁷", reason: "4d⁸ 5s¹ configuration is lower energy due to orbital size effects" },
"46": { expected: "[Kr] 5s² 4d⁸", reason: "Filled 4d¹⁰ subshell (no 5s!) provides maximum stability" },
"47": { expected: "[Kr] 5s² 4d⁹", reason: "Filled 4d¹⁰ with single 5s electron is more stable" },
"57": { expected: "[Xe] 6s² 4f¹", reason: "5d slightly lower than 4f at this Z; 5d fills first" },
"58": { expected: "[Xe] 6s² 4f²", reason: "Both 4f and 5d are occupied due to near-degeneracy" },
"64": { expected: "[Xe] 6s² 4f⁸", reason: "Exchange energy from half-filled 4f⁷ plus 5d¹ electron" },
"78": { expected: "[Xe] 4f¹⁴ 6s² 5d⁸", reason: "5d⁹ 6s¹ configuration minimizes electron repulsion" },
"79": { expected: "[Xe] 4f¹⁴ 6s² 5d⁹", reason: "Filled 5d¹⁰ with single 6s electron provides stability" },
"89": { expected: "[Rn] 7s² 5f¹", reason: "6d slightly lower than 5f at this Z" },
"90": { expected: "[Rn] 7s² 5f²", reason: "6d² is more stable than 5f² at thorium" },
"91": { expected: "[Rn] 7s² 5f³", reason: "Mixed 5f and 6d occupancy due to near-degeneracy" },
"92": { expected: "[Rn] 7s² 5f⁴", reason: "Mixed 5f and 6d occupancy; 5f³ 6d¹ is more stable" }
})
isException = exceptionElements[String(selectedZ)] !== undefined
exceptionInfo = isException ? exceptionElements[String(selectedZ)] : null
// Orbital definitions with aufbau filling order and energy levels
// Energy is relative (higher number = higher energy for display)
orbitalDefs = [
{name: "1s", n: 1, l: 0, ml: [0], maxElectrons: 2, energy: 1, fillOrder: 1},
{name: "2s", n: 2, l: 0, ml: [0], maxElectrons: 2, energy: 2, fillOrder: 2},
{name: "2p", n: 2, l: 1, ml: [-1, 0, 1], maxElectrons: 6, energy: 3, fillOrder: 3},
{name: "3s", n: 3, l: 0, ml: [0], maxElectrons: 2, energy: 4, fillOrder: 4},
{name: "3p", n: 3, l: 1, ml: [-1, 0, 1], maxElectrons: 6, energy: 5, fillOrder: 5},
{name: "4s", n: 4, l: 0, ml: [0], maxElectrons: 2, energy: 6, fillOrder: 6},
{name: "3d", n: 3, l: 2, ml: [-2, -1, 0, 1, 2], maxElectrons: 10, energy: 7, fillOrder: 7},
{name: "4p", n: 4, l: 1, ml: [-1, 0, 1], maxElectrons: 6, energy: 8, fillOrder: 8},
{name: "5s", n: 5, l: 0, ml: [0], maxElectrons: 2, energy: 9, fillOrder: 9},
{name: "4d", n: 4, l: 2, ml: [-2, -1, 0, 1, 2], maxElectrons: 10, energy: 10, fillOrder: 10},
{name: "5p", n: 5, l: 1, ml: [-1, 0, 1], maxElectrons: 6, energy: 11, fillOrder: 11},
{name: "6s", n: 6, l: 0, ml: [0], maxElectrons: 2, energy: 12, fillOrder: 12},
{name: "4f", n: 4, l: 3, ml: [-3, -2, -1, 0, 1, 2, 3], maxElectrons: 14, energy: 13, fillOrder: 13},
{name: "5d", n: 5, l: 2, ml: [-2, -1, 0, 1, 2], maxElectrons: 10, energy: 14, fillOrder: 14},
{name: "6p", n: 6, l: 1, ml: [-1, 0, 1], maxElectrons: 6, energy: 15, fillOrder: 15},
{name: "7s", n: 7, l: 0, ml: [0], maxElectrons: 2, energy: 16, fillOrder: 16},
{name: "5f", n: 5, l: 3, ml: [-3, -2, -1, 0, 1, 2, 3], maxElectrons: 14, energy: 17, fillOrder: 17},
{name: "6d", n: 6, l: 2, ml: [-2, -1, 0, 1, 2], maxElectrons: 10, energy: 18, fillOrder: 18},
{name: "7p", n: 7, l: 1, ml: [-1, 0, 1], maxElectrons: 6, energy: 19, fillOrder: 19}
]Learning Lab: Electron Configurations
mutable selectedZValue = 6
// Controls - slider + number input (NO dependency on selectedZValue to avoid circular updates)
viewof selectedZInput = {
const initialZ = 6; // Fixed initial value
const container = document.createElement("div");
container.style.marginBottom = "10px";
container.style.display = "flex";
container.style.alignItems = "center";
container.style.gap = "10px";
const label = document.createElement("label");
label.innerHTML = "Atomic number (<i>Z</i>):";
label.style.fontWeight = "500";
label.style.whiteSpace = "nowrap";
const numberInput = document.createElement("input");
numberInput.type = "number";
numberInput.min = 1;
numberInput.max = 118;
numberInput.step = 1;
numberInput.value = initialZ;
numberInput.style.width = "70px";
numberInput.style.padding = "4px 8px";
numberInput.style.fontSize = "14px";
numberInput.style.fontWeight = "bold";
numberInput.style.textAlign = "center";
numberInput.style.border = "1px solid #ccc";
numberInput.style.borderRadius = "4px";
const slider = document.createElement("input");
slider.type = "range";
slider.min = 1;
slider.max = 118;
slider.step = 1;
slider.value = initialZ;
slider.style.flex = "1";
// Helper to update both controls and dispatch event
const updateValue = (val, forceValid = false) => {
const parsed = parseInt(val);
// If forceValid (blur/change), always clamp to valid range
if (forceValid || !isNaN(parsed)) {
val = Math.max(1, Math.min(118, parsed || 1));
slider.value = val;
numberInput.value = val;
container.value = val;
container.dispatchEvent(new CustomEvent("input", {bubbles: true}));
} else {
// During typing, allow empty/partial input without resetting
// Just update slider to show current valid value if any
if (numberInput.value === "") {
// Don't update anything - let user type
}
}
};
slider.oninput = () => updateValue(slider.value, true);
// On input: only update if valid number, allow empty for typing
numberInput.oninput = () => {
const val = numberInput.value;
const parsed = parseInt(val);
if (!isNaN(parsed) && parsed >= 1 && parsed <= 118) {
// Valid number - update everything
slider.value = parsed;
container.value = parsed;
container.dispatchEvent(new CustomEvent("input", {bubbles: true}));
}
// Otherwise let user keep typing (don't reset)
};
// On blur/change: validate and reset to valid value
numberInput.onblur = () => updateValue(numberInput.value, true);
numberInput.onchange = () => updateValue(numberInput.value, true);
// Method for external updates (from Widget 2)
container.setZ = (val) => {
val = Math.max(1, Math.min(118, parseInt(val) || 1));
if (container.value !== val) {
slider.value = val;
numberInput.value = val;
container.value = val;
container.dispatchEvent(new CustomEvent("input", {bubbles: true}));
}
};
container.appendChild(label);
container.appendChild(numberInput);
container.appendChild(slider);
container.value = initialZ;
return container;
}
// Sync slider changes to mutable (slider → mutable)
// This runs whenever selectedZInput changes
syncFromInput = {
mutable selectedZValue = selectedZInput;
return selectedZInput;
}
// selectedZ derives from the mutable (allows programmatic updates)
selectedZ = selectedZValuecurrentElement = elementData.find(e => e.z === selectedZ)
// Parse electron configuration to get orbital occupancies
function parseConfig(configStr, z) {
// For actual configs, we need to compute from Z using aufbau
// But we also need to handle exceptions (Cr, Cu, etc.)
// For now, parse the stored config string
const occupancies = {};
// Expand noble gas notation
let expanded = configStr;
const nobleGases = {
"[He]": "1s2",
"[Ne]": "1s2 2s2 2p6",
"[Ar]": "1s2 2s2 2p6 3s2 3p6",
"[Kr]": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6",
"[Xe]": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6",
"[Rn]": "1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6"
};
for (const [ng, exp] of Object.entries(nobleGases)) {
if (expanded.includes(ng)) {
expanded = expanded.replace(ng, exp + " ");
break;
}
}
// Parse orbital notation (e.g., "1s2", "2p6")
const regex = /(\d[spdf])(\d+)/g;
let match;
while ((match = regex.exec(expanded)) !== null) {
const orbital = match[1];
const count = parseInt(match[2]);
occupancies[orbital] = count;
}
return occupancies;
}
occupancies = parseConfig(currentElement.config, selectedZ)
// Count unpaired electrons for para/diamagnetic
function countUnpaired(occ) {
let unpaired = 0;
for (const [orbital, count] of Object.entries(occ)) {
const orbDef = orbitalDefs.find(o => o.name === orbital);
if (!orbDef) continue;
const numOrbitals = orbDef.ml.length;
const maxPerOrbital = 2;
if (count <= numOrbitals) {
// All unpaired (Hund's rule)
unpaired += count;
} else {
// Some paired
const paired = count - numOrbitals;
unpaired += numOrbitals - paired;
}
}
return unpaired;
}
unpairedCount = countUnpaired(occupancies)
isPara = unpairedCount > 0
// Count valence electrons (simplified: outermost shell)
function countValence(occ, z) {
// Find highest n
let maxN = 0;
for (const orbital of Object.keys(occ)) {
const n = parseInt(orbital[0]);
if (n > maxN) maxN = n;
}
// For main group: count electrons in highest n
// For transition metals: include (n-1)d
let valence = 0;
for (const [orbital, count] of Object.entries(occ)) {
const n = parseInt(orbital[0]);
const subshell = orbital[1];
if (n === maxN) {
valence += count;
} else if (n === maxN - 1 && subshell === 'd') {
valence += count;
} else if (n === maxN - 2 && subshell === 'f') {
valence += count;
}
}
return valence;
}
valenceCount = countValence(occupancies, selectedZ)
coreCount = selectedZ - valenceCount
// Determine which orbitals are valence vs core
function getOrbitalTypes(occ, z) {
const types = {};
let maxN = 0;
for (const orbital of Object.keys(occ)) {
const n = parseInt(orbital[0]);
if (n > maxN) maxN = n;
}
for (const [orbital, count] of Object.entries(occ)) {
const n = parseInt(orbital[0]);
const subshell = orbital[1];
// Valence: highest n, or (n-1)d for transition metals, or (n-2)f for lanthanides/actinides
if (n === maxN) {
types[orbital] = 'valence';
} else if (n === maxN - 1 && subshell === 'd') {
types[orbital] = 'valence';
} else if (n === maxN - 2 && subshell === 'f') {
types[orbital] = 'valence';
} else {
types[orbital] = 'core';
}
}
return types;
}
orbitalTypes = getOrbitalTypes(occupancies, selectedZ)
// Generate full configuration (no noble gas abbreviation)
function getFullConfig(occ) {
// Sort by aufbau order
const aufbauOrder = ['1s', '2s', '2p', '3s', '3p', '4s', '3d', '4p', '5s', '4d', '5p', '6s', '4f', '5d', '6p', '7s', '5f', '6d', '7p'];
const parts = [];
for (const orb of aufbauOrder) {
if (occ[orb]) {
parts.push({ orbital: orb, count: occ[orb] });
}
}
return parts;
}
fullConfigParts = getFullConfig(occupancies)// Main visualization
ecDiagram = {
const width = 800;
const height = 580;
const margin = {top: 70, right: 20, bottom: 80, left: 70};
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("class", "ec-builder-svg")
.style("max-width", `${width}px`)
.style("height", "auto")
.style("display", "block")
.style("margin", "0 auto");
// Background
svg.append("rect")
.attr("width", width)
.attr("height", height)
.attr("class", "ec-bg");
// Arrow marker definition (must be in defs before use)
svg.append("defs").append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 0 14 8")
.attr("refX", 7)
.attr("refY", 4)
.attr("markerWidth", 7)
.attr("markerHeight", 4)
.attr("orient", "auto-start-reverse")
.append("path")
.attr("d", "M 0 0 L 14 4 L 0 8 z")
.attr("class", "ec-arrow-marker");
// Title with element info
const titleText = svg.append("text")
.attr("x", width / 2)
.attr("y", 35)
.attr("text-anchor", "middle")
.attr("class", "ec-title");
// Determine configuration certainty level
const configCertainty = selectedZ <= 100 ? "experimental" : (selectedZ <= 109 ? "calculated" : "predicted");
const certaintyLabels = {
"experimental": "experimental",
"calculated": "calculated",
"predicted": "predicted"
};
// Build title with optional exception warning
let titleHtml = `<tspan font-weight="bold" font-size="28">${currentElement.symbol}</tspan>` +
`<tspan font-size="18" dx="10">${currentElement.name}</tspan>` +
`<tspan font-size="14" dx="10" class="ec-z-label">(<tspan font-style="italic">Z</tspan> = ${currentElement.z})</tspan>`;
if (isException) {
titleHtml += `<tspan dx="8" fill="#FF9800" font-size="16">⚠</tspan>`;
}
titleText.html(titleHtml);
// Determine which orbitals to show based on element
const maxFillOrder = Math.max(...Object.keys(occupancies).map(name => {
const od = orbitalDefs.find(o => o.name === name);
return od ? od.fillOrder : 0;
}));
// Show occupied orbitals plus next 1-2 to fill
const relevantOrbitals = orbitalDefs.filter(o => {
const occ = occupancies[o.name] || 0;
return occ > 0 || (o.fillOrder <= maxFillOrder + 2 && o.fillOrder <= 11);
});
// Box dimensions - SQUARE boxes
const boxSize = 28;
const boxGap = 3;
// Column offsets for each subshell type (s, p, d, f)
// Column widths: s=28px, p=90px, d=152px, f=214px (total 484px)
// Uniform 60px gaps between columns, spans 664px total
// baseX=90 to 754 (26px padding from right edge)
const columnOffsets = {
's': 0,
'p': 88,
'd': 238,
'f': 450
};
// Relative energy positions (approximate, with larger gaps between shells)
// These are Y positions from top (higher energy = smaller Y = higher on screen)
// For hydrogen (Z=1), energy depends only on n, so orbitals with same n have same energy
const multiElectronEnergies = {
'1s': 420,
'2s': 360,
'2p': 340,
'3s': 290,
'3p': 270,
'4s': 230,
'3d': 210,
'4p': 190,
'5s': 155,
'4d': 140,
'5p': 125,
'6s': 95,
'4f': 82,
'5d': 70,
'6p': 58,
'7s': 35,
'5f': 25,
'6d': 15,
'7p': 5
};
// For hydrogen: energy depends only on n (degenerate subshells)
const hydrogenEnergies = {
'1s': 420,
'2s': 340, '2p': 340,
'3s': 260, '3p': 260, '3d': 260,
'4s': 180, '4p': 180, '4d': 180, '4f': 180,
'5s': 110, '5p': 110, '5d': 110, '5f': 110,
'6s': 55, '6p': 55, '6d': 55,
'7s': 15, '7p': 15
};
const energyPositions = selectedZ === 1 ? hydrogenEnergies : multiElectronEnergies;
const diagramTop = margin.top + 20;
const diagramBottom = height - margin.bottom - 40;
const diagramHeight = diagramBottom - diagramTop;
// Scale energy positions to fit diagram
const maxEnergyPos = Math.max(...Object.values(energyPositions));
const minEnergyPos = Math.min(...Object.values(energyPositions));
function getYPosition(orbitalName) {
const rawY = energyPositions[orbitalName] || 200;
// Map: higher rawY (lower energy) -> lower on screen (higher Y in SVG)
// Lower energy orbitals have higher rawY values, should appear at bottom (larger Y)
return diagramTop + ((rawY - minEnergyPos) / (maxEnergyPos - minEnergyPos)) * diagramHeight;
}
// Energy axis arrow (extends down to align with bottom of 1s box)
svg.append("line")
.attr("x1", margin.left - 20)
.attr("y1", diagramBottom + boxSize + 5)
.attr("x2", margin.left - 20)
.attr("y2", diagramTop - 10)
.attr("class", "ec-axis-line")
.attr("marker-end", "url(#arrow)");
svg.append("text")
.attr("x", margin.left - 32)
.attr("y", (diagramTop + diagramBottom) / 2)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr("class", "ec-axis-label")
.text("E");
const baseX = margin.left + 20;
// Store tooltip data for later (we'll create tooltip LAST for z-order)
const tooltipData = [];
// Draw each orbital
relevantOrbitals.forEach((orbital) => {
const subshell = orbital.name.slice(-1); // 's', 'p', 'd', or 'f'
const numBoxes = orbital.ml.length;
const y = getYPosition(orbital.name);
const startX = baseX + columnOffsets[subshell];
const orbitalGroup = svg.append("g")
.attr("class", "ec-orbital-group");
// Orbital label (full name, e.g., "1s", "2p", "3d")
// Calculate center of all boxes for this orbital
const totalBoxWidth = numBoxes * boxSize + (numBoxes - 1) * boxGap;
const boxesCenterX = startX + totalBoxWidth / 2;
orbitalGroup.append("text")
.attr("x", startX - 10)
.attr("y", y + boxSize / 2)
.attr("text-anchor", "end")
.attr("dominant-baseline", "central")
.attr("class", "ec-orbital-label")
.text(orbital.name);
const electronCount = occupancies[orbital.name] || 0;
// Draw boxes for each ml value - SQUARE boxes
orbital.ml.forEach((ml, boxIdx) => {
const boxX = startX + boxIdx * (boxSize + boxGap);
const hasElectrons = electronCount > 0;
// Determine if THIS specific box has electrons
const numOrbitals = orbital.ml.length;
let hasSpinUp = false;
let hasSpinDown = false;
if (electronCount > 0) {
if (electronCount <= numOrbitals) {
// All spin up, one per orbital until exhausted
if (boxIdx < electronCount) {
hasSpinUp = true;
}
} else {
// First fill all spin up, then spin down
hasSpinUp = true;
const spinDownCount = electronCount - numOrbitals;
if (boxIdx < spinDownCount) {
hasSpinDown = true;
}
}
}
const boxHasElectron = hasSpinUp || hasSpinDown;
const orbType = orbitalTypes[orbital.name] || 'empty';
// Box - SQUARE, colored by core/valence/empty
let boxClass = "ec-box ec-box-empty";
if (boxHasElectron) {
boxClass = orbType === 'core' ? "ec-box ec-box-core" : "ec-box ec-box-valence";
}
orbitalGroup.append("rect")
.attr("x", boxX)
.attr("y", y)
.attr("width", boxSize)
.attr("height", boxSize)
.attr("class", boxClass);
// Draw electrons as LaTeX-style harpoon arrows (slightly thicker stem, single barb)
// Up harpoon (↿): stem with LEFT barb
const arrowUp = "M 0.7,-10 L 0.7,10 L -0.7,10 L -0.7,-6 L -5,-3 L -0.7,-10 Z";
// Down harpoon (⇂): stem with RIGHT barb
const arrowDown = "M -0.7,10 L -0.7,-10 L 0.7,-10 L 0.7,6 L 5,3 L 0.7,10 Z";
// Fixed positions for arrows (don't shift when paired)
const upArrowX = boxX + boxSize/2 - 3;
const downArrowX = boxX + boxSize/2 + 3;
const arrowY = y + boxSize/2;
// Unpaired = has spin up but no spin down
const isUnpaired = hasSpinUp && !hasSpinDown;
if (hasSpinUp) {
orbitalGroup.append("path")
.attr("d", arrowUp)
.attr("transform", `translate(${upArrowX}, ${arrowY})`)
.attr("class", isUnpaired ? "ec-arrow ec-arrow-unpaired" : "ec-arrow ec-arrow-paired")
.attr("data-n", orbital.n)
.attr("data-l", orbital.l)
.attr("data-ml", ml)
.attr("data-ms", "+½")
.attr("data-x", upArrowX)
.attr("data-y", y);
}
if (hasSpinDown) {
orbitalGroup.append("path")
.attr("d", arrowDown)
.attr("transform", `translate(${downArrowX}, ${arrowY})`)
.attr("class", "ec-arrow ec-arrow-paired")
.attr("data-n", orbital.n)
.attr("data-l", orbital.l)
.attr("data-ml", ml)
.attr("data-ms", "−½")
.attr("data-x", downArrowX)
.attr("data-y", y);
}
});
});
// Create tooltip LAST so it renders on top of everything
const tooltip = svg.append("g")
.attr("class", "ec-tooltip")
.style("opacity", 0)
.style("pointer-events", "none");
tooltip.append("rect")
.attr("class", "ec-tooltip-bg")
.attr("rx", 4)
.attr("ry", 4);
const tooltipText = tooltip.append("text")
.attr("class", "ec-tooltip-text");
// Track if tooltip is pinned (clicked)
let tooltipPinned = false;
let pinnedElement = null;
function showTooltip(el, pinned = false) {
const n = el.attr("data-n");
const l = el.attr("data-l");
const ml = el.attr("data-ml");
const ms = el.attr("data-ms");
const x = parseFloat(el.attr("data-x"));
const y = parseFloat(el.attr("data-y"));
// Build styled tooltip text with tspans
// Format: n = 1, l = 0, m_l = 0, m_s = +½
tooltipText.html('');
tooltipText.append("tspan").attr("class", "ec-tt-var").text("n");
tooltipText.append("tspan").text(" = " + n + ", ");
tooltipText.append("tspan").attr("class", "ec-tt-var").text("l");
tooltipText.append("tspan").text(" = " + l + ", ");
tooltipText.append("tspan").attr("class", "ec-tt-var").text("m");
tooltipText.append("tspan").attr("class", "ec-tt-sub").attr("dy", "3").attr("font-size", "9px").text("l");
tooltipText.append("tspan").attr("dy", "-3").text(" = " + ml + ", ");
tooltipText.append("tspan").attr("class", "ec-tt-var").text("m");
tooltipText.append("tspan").attr("class", "ec-tt-sub").attr("dy", "3").attr("font-size", "9px").text("s");
tooltipText.append("tspan").attr("dy", "-3").text(" = " + ms);
const bbox = tooltipText.node().getBBox();
tooltip.select("rect")
.attr("x", bbox.x - 10)
.attr("y", bbox.y - 6)
.attr("width", bbox.width + 20)
.attr("height", bbox.height + 12);
tooltip.attr("transform", `translate(${x}, ${y - 18})`)
.transition().duration(100).style("opacity", 1);
if (pinned) {
// Remove highlight from previous pinned element
if (pinnedElement) {
d3.select(pinnedElement).classed("ec-arrow-selected", false);
}
tooltipPinned = true;
pinnedElement = el.node();
// Add highlight to newly pinned element
el.classed("ec-arrow-selected", true);
}
}
function hideTooltip(force = false) {
if (!tooltipPinned || force) {
tooltip.transition().duration(100).style("opacity", 0);
if (force) {
// Remove highlight from pinned element
if (pinnedElement) {
d3.select(pinnedElement).classed("ec-arrow-selected", false);
}
tooltipPinned = false;
pinnedElement = null;
}
}
}
// Add mouse and click events to all electron arrows
svg.selectAll(".ec-arrow")
.on("mouseenter", function(event) {
if (!tooltipPinned) {
showTooltip(d3.select(this));
}
})
.on("mouseleave", function() {
if (!tooltipPinned) {
hideTooltip();
}
})
.on("click", function(event) {
event.stopPropagation();
const el = d3.select(this);
if (pinnedElement === this) {
// Clicking same arrow unpins
hideTooltip(true);
} else {
// Pin to this arrow
showTooltip(el, true);
}
});
// Click anywhere else on SVG to dismiss pinned tooltip
svg.on("click", function(event) {
if (tooltipPinned) {
hideTooltip(true);
}
});
// Info panel at bottom
const infoY1 = height - 68;
const infoY2 = height - 48;
const infoY3 = height - 18;
// Helper to build formatted config with proper superscripts
// Key: put dy reset on the NEXT orbital tspan, not an empty one
function buildConfigString(textEl, parts) {
parts.forEach((part, i) => {
// Orbital name - include dy="5" to reset from previous superscript (except first)
const orbTspan = textEl.append("tspan").text((i > 0 ? " " : "") + part.orbital);
if (i > 0) orbTspan.attr("dy", "5");
// Superscript electron count
textEl.append("tspan")
.attr("dy", "-5")
.attr("font-size", "9px")
.text(part.count);
});
// Final reset so text element ends at baseline
textEl.append("tspan").attr("dy", "5").text("");
}
// Noble gas configuration (line 1) with label
svg.append("text")
.attr("x", margin.left)
.attr("y", infoY1)
.attr("class", "ec-config-label")
.text("Core notation:");
const configText1 = svg.append("text")
.attr("x", margin.left + 90)
.attr("y", infoY1)
.attr("class", "ec-config-text");
// Parse noble gas notation for display
const configStr = currentElement.config;
const nobleMatch = configStr.match(/^\[([A-Za-z]+)\]\s*/);
if (nobleMatch) {
configText1.append("tspan").text("[" + nobleMatch[1] + "] ");
const remainder = configStr.slice(nobleMatch[0].length);
const orbRegex = /(\d[spdf])(\d+)/g;
let m;
let needsReset = false;
while ((m = orbRegex.exec(remainder)) !== null) {
// Orbital name with reset if after a superscript
const orbTspan = configText1.append("tspan").text((needsReset ? " " : "") + m[1]);
if (needsReset) orbTspan.attr("dy", "5");
// Superscript
configText1.append("tspan").attr("dy", "-5").attr("font-size", "9px").text(m[2]);
needsReset = true;
}
configText1.append("tspan").attr("dy", "5").text("");
} else {
buildConfigString(configText1, fullConfigParts);
}
// Full configuration (line 2) with label
svg.append("text")
.attr("x", margin.left)
.attr("y", infoY2)
.attr("class", "ec-config-label")
.text("Full notation:");
const configText2 = svg.append("text")
.attr("x", margin.left + 90)
.attr("y", infoY2)
.attr("class", "ec-config-text ec-config-full");
buildConfigString(configText2, fullConfigParts);
// Core/valence counts (centered) and para/diamagnetic (line 3)
svg.append("text")
.attr("x", width / 2)
.attr("y", infoY3)
.attr("text-anchor", "middle")
.attr("class", "ec-counts")
.text(`Core: ${coreCount} | Valence: ${valenceCount}`);
svg.append("text")
.attr("x", width - margin.right)
.attr("y", infoY3)
.attr("text-anchor", "end")
.attr("class", isPara ? "ec-para" : "ec-dia")
.text(isPara ? `Paramagnetic (${unpairedCount} unpaired)` : "Diamagnetic");
// Certainty badge below config notation
svg.append("text")
.attr("x", margin.left)
.attr("y", infoY3)
.attr("text-anchor", "start")
.attr("class", `ec-certainty-badge ec-certainty-${configCertainty}`)
.text(certaintyLabels[configCertainty]);
// Aufbau diagram dimensions (defined early so PT can position relative to it)
const aufbauCellW = 34;
const aufbauCellH = 24;
const aufbauPadTop = 22;
const aufbauPadSide = 12;
const aufbauPadBot = 14;
const aufbauW = 4 * aufbauCellW + aufbauPadSide * 2;
const aufbauH = 7 * aufbauCellH + aufbauPadTop + aufbauPadBot;
const aufbauX = width - aufbauW - 8;
const aufbauY = height - aufbauH - 76;
// Mini Periodic Table skeleton showing current position and path
// Standard periodic table layout (18 columns, 7 rows + 2 for f-block)
const ptCellW = 11;
const ptCellH = 11;
const ptGap = 1;
const ptPadTop = 20;
const ptPadSide = 8;
const ptPadBot = 8;
// Element positions in periodic table [row (0-indexed), col (0-indexed)]
// Row 0-6 are main table, lanthanides/actinides shown in rows 0-6 at their actual positions
const ptPositions = [
[0, 0], // 1 H
[0, 17], // 2 He
[1, 0], // 3 Li
[1, 1], // 4 Be
[1, 12], [1, 13], [1, 14], [1, 15], [1, 16], [1, 17], // 5-10 B-Ne
[2, 0], // 11 Na
[2, 1], // 12 Mg
[2, 12], [2, 13], [2, 14], [2, 15], [2, 16], [2, 17], // 13-18 Al-Ar
[3, 0], // 19 K
[3, 1], // 20 Ca
[3, 2], [3, 3], [3, 4], [3, 5], [3, 6], [3, 7], [3, 8], [3, 9], [3, 10], [3, 11], // 21-30 Sc-Zn
[3, 12], [3, 13], [3, 14], [3, 15], [3, 16], [3, 17], // 31-36 Ga-Kr
[4, 0], // 37 Rb
[4, 1], // 38 Sr
[4, 2], [4, 3], [4, 4], [4, 5], [4, 6], [4, 7], [4, 8], [4, 9], [4, 10], [4, 11], // 39-48 Y-Cd
[4, 12], [4, 13], [4, 14], [4, 15], [4, 16], [4, 17], // 49-54 In-Xe
[5, 0], // 55 Cs
[5, 1], // 56 Ba
// Lanthanides 57-71 (La-Lu shown in row 7, cols 2-16)
[7, 2], [7, 3], [7, 4], [7, 5], [7, 6], [7, 7], [7, 8], [7, 9], [7, 10], [7, 11], [7, 12], [7, 13], [7, 14], [7, 15], [7, 16],
// Col 2 is blank (lanthanide placeholder), Hf starts at col 3
[5, 3], [5, 4], [5, 5], [5, 6], [5, 7], [5, 8], [5, 9], [5, 10], [5, 11], // 72-80 Hf-Hg
[5, 12], [5, 13], [5, 14], [5, 15], [5, 16], [5, 17], // 81-86 Tl-Rn
[6, 0], // 87 Fr
[6, 1], // 88 Ra
// Actinides 89-103 (Ac-Lr shown in row 8, cols 2-16)
[8, 2], [8, 3], [8, 4], [8, 5], [8, 6], [8, 7], [8, 8], [8, 9], [8, 10], [8, 11], [8, 12], [8, 13], [8, 14], [8, 15], [8, 16],
// Col 2 is blank (actinide placeholder), Rf starts at col 3
[6, 3], [6, 4], [6, 5], [6, 6], [6, 7], [6, 8], [6, 9], [6, 10], [6, 11], // 104-112 Rf-Cn
[6, 12], [6, 13], [6, 14], [6, 15], [6, 16], [6, 17], // 113-118 Nh-Og
];
const ptW = 18 * (ptCellW + ptGap) + ptPadSide * 2;
const ptH = 9 * (ptCellH + ptGap) + ptPadTop + ptPadBot;
const ptX = width - aufbauW - ptW - 20;
const ptY = aufbauY + (aufbauH - ptH) / 2; // Vertically center with aufbau
const ptGroup = svg.append("g")
.attr("class", "pt-mini")
.attr("transform", `translate(${ptX}, ${ptY})`);
// Background
ptGroup.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", ptW)
.attr("height", ptH)
.attr("rx", 5)
.attr("class", "pt-mini-bg");
// Title
ptGroup.append("text")
.attr("x", ptW / 2)
.attr("y", 12)
.attr("text-anchor", "middle")
.attr("class", "pt-mini-title")
.text("Periodic Table");
// Arrow marker for PT
const ptDefs = ptGroup.append("defs");
ptDefs.append("marker")
.attr("id", "pt-mini-arrowhead")
.attr("viewBox", "0 0 8 5")
.attr("refX", 0)
.attr("refY", 2.5)
.attr("markerWidth", 5)
.attr("markerHeight", 3)
.attr("orient", "auto")
.append("path")
.attr("d", "M 0 0 L 8 2.5 L 0 5 z")
.attr("class", "pt-mini-arrowhead");
// Draw all cells as skeleton
const blockColors = {
s: [0, 1], // columns 0-1
d: [2, 11], // columns 2-11
p: [12, 17], // columns 12-17
f: [2, 16] // f-block in rows 7-8, cols 2-16
};
// Determine block for a cell position
function getBlock(r, c) {
// f-block: rows 7-8
if (r >= 7) return "f";
// s-block: cols 0-1, plus Helium (row 0, col 17)
if (c <= 1 || (r === 0 && c === 17)) return "s";
// d-block: cols 2-11 (only rows 3+)
if (c >= 2 && c <= 11) return "d";
// p-block: cols 12-17
return "p";
}
// Build reverse lookup: [row, col] -> Z
const posToZ = {};
ptPositions.forEach(([r, c], idx) => {
posToZ[`${r},${c}`] = idx + 1; // Z is 1-indexed
});
// Draw skeleton cells for standard table shape
const cellPositions = [];
// Row 0: cols 0, 17
[[0,0], [0,17]].forEach(([r,c]) => cellPositions.push([r,c]));
// Rows 1-2: cols 0-1, 12-17
for (let r = 1; r <= 2; r++) {
for (let c = 0; c <= 1; c++) cellPositions.push([r,c]);
for (let c = 12; c <= 17; c++) cellPositions.push([r,c]);
}
// Rows 3-4: cols 0-17
for (let r = 3; r <= 4; r++) {
for (let c = 0; c <= 17; c++) cellPositions.push([r,c]);
}
// Rows 5-6: cols 0-17 but skip col 2 (lanthanide/actinide placeholder)
for (let r = 5; r <= 6; r++) {
for (let c = 0; c <= 17; c++) {
if (c !== 2) cellPositions.push([r,c]);
}
}
// Rows 7-8 (f-block): cols 2-16
for (let r = 7; r <= 8; r++) {
for (let c = 2; c <= 16; c++) cellPositions.push([r,c]);
}
// Draw skeleton cells with block colors and click handlers
cellPositions.forEach(([r, c]) => {
const x = ptPadSide + c * (ptCellW + ptGap);
const y = ptPadTop + r * (ptCellH + ptGap);
const block = getBlock(r, c);
const z = posToZ[`${r},${c}`];
const cell = ptGroup.append("rect")
.attr("x", x)
.attr("y", y)
.attr("width", ptCellW)
.attr("height", ptCellH)
.attr("class", `pt-mini-cell pt-mini-block-${block}`);
// Add click handler if this cell has an element
if (z) {
const zValue = z; // Capture in closure
cell
.style("cursor", "pointer")
.on("click", function() {
// Update the mutable which triggers reactive updates
mutable selectedZValue = zValue;
// Also update the input widget to stay in sync
const inputWidget = document.querySelector('input[type="range"][max="118"]');
const numberWidget = document.querySelector('input[type="number"][max="118"]');
if (inputWidget) inputWidget.value = zValue;
if (numberWidget) numberWidget.value = zValue;
});
}
});
// Add block labels (s, p, d, f) centered on their regions
// These go above squares but below arrows, with pointer-events: none
const blockLabels = [
{ label: "s", col: 0.5, row: 3.5 }, // s-block: cols 0-1
{ label: "p", col: 14.5, row: 3.5 }, // p-block: cols 12-17
{ label: "d", col: 7, row: 4.5 }, // d-block: cols 3-11
{ label: "f", col: 9, row: 7.5 } // f-block: cols 2-16, rows 7-8
];
blockLabels.forEach(({ label, col, row }) => {
const x = ptPadSide + col * (ptCellW + ptGap) + ptCellW / 2;
const y = ptPadTop + row * (ptCellH + ptGap) + ptCellH / 2;
ptGroup.append("text")
.attr("x", x)
.attr("y", y)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr("class", "pt-mini-block-label")
.style("pointer-events", "none")
.text(label);
});
// Draw path arrows and highlight current element
if (selectedZ >= 1 && selectedZ <= 118) {
// Build path through elements 1 to selectedZ
const pathSegments = [];
let prevPos = null;
for (let z = 1; z <= selectedZ; z++) {
const pos = ptPositions[z - 1];
if (pos) {
const x = ptPadSide + pos[1] * (ptCellW + ptGap) + ptCellW / 2;
const y = ptPadTop + pos[0] * (ptCellH + ptGap) + ptCellH / 2;
if (prevPos) {
pathSegments.push({ x1: prevPos.x, y1: prevPos.y, x2: x, y2: y });
}
prevPos = { x, y };
}
}
// Draw path - only rightward arrows (x2 >= x1)
const rightwardSegments = pathSegments.filter(seg => seg.x2 >= seg.x1);
rightwardSegments.forEach((seg, i) => {
const isLast = i === rightwardSegments.length - 1;
ptGroup.append("line")
.attr("x1", seg.x1)
.attr("y1", seg.y1)
.attr("x2", seg.x2)
.attr("y2", seg.y2)
.attr("class", "pt-mini-arrow")
.attr("marker-end", isLast ? "url(#pt-mini-arrowhead)" : null);
});
// Highlight current element position
const curPos = ptPositions[selectedZ - 1];
if (curPos) {
const hx = ptPadSide + curPos[1] * (ptCellW + ptGap);
const hy = ptPadTop + curPos[0] * (ptCellH + ptGap);
ptGroup.append("rect")
.attr("x", hx - 1)
.attr("y", hy - 1)
.attr("width", ptCellW + 2)
.attr("height", ptCellH + 2)
.attr("class", "pt-mini-current");
}
}
// Mini Aufbau diagram in bottom-right corner
// Classic layout: columns for s, p, d, f with diagonal arrows showing fill order
const aufbauOrbitals = [
["1s"],
["2s", "2p"],
["3s", "3p", "3d"],
["4s", "4p", "4d", "4f"],
["5s", "5p", "5d", "5f"],
["6s", "6p", "6d"],
["7s", "7p"]
];
// Map orbital name to [row, col] position
const orbitalPos = {};
aufbauOrbitals.forEach((row, r) => {
row.forEach((orb, c) => {
orbitalPos[orb] = [r, c];
});
});
// Determine which orbitals are filled and which is current
const filledOrbitals = new Set(Object.keys(occupancies));
const lastOrbital = fullConfigParts.length > 0 ? fullConfigParts[fullConfigParts.length - 1].orbital : null;
// Use pre-defined aufbau dimensions
const cellW = aufbauCellW;
const cellH = aufbauCellH;
const padTop = aufbauPadTop;
const padSide = aufbauPadSide;
const padBot = aufbauPadBot;
const aufbauGroup = svg.append("g")
.attr("class", "aufbau-mini")
.attr("transform", `translate(${aufbauX}, ${aufbauY})`);
// Background
aufbauGroup.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", aufbauW)
.attr("height", aufbauH)
.attr("rx", 6)
.attr("class", "aufbau-mini-bg");
// Title
aufbauGroup.append("text")
.attr("x", aufbauW / 2)
.attr("y", 15)
.attr("text-anchor", "middle")
.attr("class", "aufbau-mini-title")
.text("Aufbau Order");
// Down-left diagonal groups (n+l constant) - includes single orbitals
const diagonals = [
["1s"], // n+l=1
["2s"], // n+l=2
["2p", "3s"], // n+l=3
["3p", "4s"], // n+l=4
["3d", "4p", "5s"], // n+l=5
["4d", "5p", "6s"], // n+l=6
["4f", "5d", "6p", "7s"], // n+l=7
["5f", "6d"], // n+l=8
["7p"], // n+l=9
];
// Fill sequence to determine what's been reached
const fillSequence = [
"1s", "2s", "2p", "3s", "3p", "4s", "3d", "4p", "5s", "4d",
"5p", "6s", "4f", "5d", "6p", "7s", "5f", "6d", "7p"
];
const currentIdx = lastOrbital ? fillSequence.indexOf(lastOrbital) : -1;
// Arrow marker definition
const aufbauDefs = aufbauGroup.append("defs");
aufbauDefs.append("marker")
.attr("id", "aufbau-mini-arrowhead")
.attr("viewBox", "0 0 8 5")
.attr("refX", 0)
.attr("refY", 2.5)
.attr("markerWidth", 6)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("path")
.attr("d", "M 0 0 L 8 2.5 L 0 5 z")
.attr("class", "aufbau-mini-arrowhead");
// Calculate the actual down-left diagonal angle based on grid dimensions
// Each diagonal step: col decreases by 1 (-cellW), row increases by 1 (+cellH)
const downLeftAngle = Math.atan2(cellH, -cellW); // Consistent angle for all arrows
// Draw down-left diagonal arrows only
diagonals.forEach(diag => {
// Check if we've reached the first orbital in this diagonal
const firstOrbIdx = fillSequence.indexOf(diag[0]);
if (firstOrbIdx > currentIdx) return; // Haven't reached this diagonal yet
// Find how far through this diagonal we've progressed
let lastReachedIdx = -1;
for (let i = 0; i < diag.length; i++) {
const orbIdx = fillSequence.indexOf(diag[i]);
if (orbIdx <= currentIdx) {
lastReachedIdx = i;
}
}
if (lastReachedIdx < 0) return;
// Get start and end points for the arrow
const startOrb = diag[0];
const endOrb = diag[lastReachedIdx];
const [r1, c1] = orbitalPos[startOrb];
const [r2, c2] = orbitalPos[endOrb];
const x1 = padSide + c1 * cellW + cellW / 2;
const y1 = padTop + r1 * cellH + cellH / 2;
const x2 = padSide + c2 * cellW + cellW / 2;
const y2 = padTop + r2 * cellH + cellH / 2;
// Always use the same down-left angle for consistency
const angle = downLeftAngle;
// Arrow length for single orbital, or extend past endpoints for multi
const extend = 18;
let sx, sy, ex, ey;
if (lastReachedIdx === 0) {
// Single orbital - draw short arrow through it
sx = x1 - Math.cos(angle) * extend;
sy = y1 - Math.sin(angle) * extend;
ex = x1 + Math.cos(angle) * extend;
ey = y1 + Math.sin(angle) * extend;
} else {
// Multiple orbitals - extend before and after
sx = x1 - Math.cos(angle) * extend;
sy = y1 - Math.sin(angle) * extend;
ex = x2 + Math.cos(angle) * extend;
ey = y2 + Math.sin(angle) * extend;
}
aufbauGroup.append("line")
.attr("x1", sx)
.attr("y1", sy)
.attr("x2", ex)
.attr("y2", ey)
.attr("class", "aufbau-mini-arrow")
.attr("marker-end", "url(#aufbau-mini-arrowhead)");
});
// Draw orbitals (on top of arrows)
aufbauOrbitals.forEach((row, rowIdx) => {
row.forEach((orb, colIdx) => {
const cx = padSide + colIdx * cellW + cellW / 2;
const cy = padTop + rowIdx * cellH + cellH / 2;
const isFilled = filledOrbitals.has(orb);
const isCurrent = orb === lastOrbital;
const isPartial = isFilled && occupancies[orb] < orbitalDefs.find(o => o.name === orb)?.maxElectrons;
let orbClass = "aufbau-mini-empty";
if (isCurrent) {
orbClass = isPartial ? "aufbau-mini-current-partial" : "aufbau-mini-current";
} else if (isFilled) {
orbClass = "aufbau-mini-filled";
}
aufbauGroup.append("text")
.attr("x", cx)
.attr("y", cy)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr("class", `aufbau-mini-orb ${orbClass}`)
.text(orb);
});
});
return svg.node();
}// Styles
ec_styles = html`
<style>
/* ========== LIGHT MODE ========== */
.ec-builder-svg .ec-bg {
fill: #fafafa;
}
.ec-builder-svg .ec-title {
fill: #1a1a1a;
}
.ec-builder-svg .ec-z-label {
fill: #888;
}
.ec-builder-svg .ec-certainty-badge {
font-size: 10px;
font-style: italic;
}
.ec-builder-svg .ec-certainty-experimental {
fill: #2e7d32;
}
.ec-builder-svg .ec-certainty-calculated {
fill: #7b1fa2;
}
.ec-builder-svg .ec-certainty-predicted {
fill: #c62828;
}
.ec-builder-svg .ec-axis-label {
fill: #666;
font-size: 14px;
font-weight: bold;
font-style: italic;
}
.ec-builder-svg .ec-axis-line {
stroke: #666;
stroke-width: 2;
}
.ec-builder-svg .ec-arrow-marker {
fill: #666;
}
.ec-builder-svg .ec-orbital-label {
fill: #333;
font-size: 13px;
font-weight: 500;
}
.ec-builder-svg .ec-box {
stroke: #666;
stroke-width: 1.5;
}
.ec-builder-svg .ec-box-empty {
fill: #fff;
}
.ec-builder-svg .ec-box-core {
fill: #e0e0e0;
}
.ec-builder-svg .ec-box-valence {
fill: #e3f2fd;
}
.ec-builder-svg .ec-arrow {
cursor: pointer;
transition: fill 0.15s;
}
.ec-builder-svg .ec-arrow-paired {
fill: #333;
}
.ec-builder-svg .ec-arrow-paired:hover {
fill: #ff9800;
filter: drop-shadow(0 0 3px #ff9800) drop-shadow(0 0 6px #ff980080);
}
.ec-builder-svg .ec-arrow-unpaired {
fill: #c62828;
}
.ec-builder-svg .ec-arrow-unpaired:hover {
fill: #ff9800;
filter: drop-shadow(0 0 3px #ff9800) drop-shadow(0 0 6px #ff980080);
}
.ec-builder-svg .ec-arrow-selected {
fill: #ff9800 !important;
filter: drop-shadow(0 0 3px #ff9800) drop-shadow(0 0 6px #ff980080);
}
.ec-builder-svg .ec-config-label {
fill: #666;
font-size: 11px;
font-family: system-ui, -apple-system, sans-serif;
}
.ec-builder-svg .ec-config-text {
fill: #333;
font-size: 12px;
font-family: system-ui, -apple-system, sans-serif;
}
.ec-builder-svg .ec-config-full {
fill: #555;
}
.ec-builder-svg .ec-config-sup {
font-size: 9px;
}
.ec-builder-svg .ec-para {
fill: #c62828;
font-size: 13px;
font-weight: 500;
}
.ec-builder-svg .ec-dia {
fill: #388e3c;
font-size: 13px;
font-weight: 500;
}
.ec-builder-svg .ec-counts {
fill: #555;
font-size: 12px;
}
.ec-builder-svg .ec-tooltip-bg {
fill: #333;
opacity: 0.92;
}
.ec-builder-svg .ec-tooltip-text {
fill: #fff;
font-size: 12px;
font-family: system-ui, -apple-system, sans-serif;
dominant-baseline: middle;
text-anchor: middle;
}
.ec-builder-svg .ec-tt-var {
font-style: italic;
}
.ec-builder-svg .ec-tt-sub {
font-size: 9px;
font-style: italic;
}
/* Mini Periodic Table */
.ec-builder-svg .pt-mini-bg {
fill: rgba(255, 255, 255, 0.95);
stroke: #ccc;
stroke-width: 1;
}
.ec-builder-svg .pt-mini-title {
fill: #555;
font-size: 9px;
font-weight: 600;
}
.ec-builder-svg .pt-mini-cell {
fill: #e8e8e8;
stroke: none;
}
.ec-builder-svg .pt-mini-block-s {
fill: #ce93d8;
}
.ec-builder-svg .pt-mini-block-p {
fill: #4dd0e1;
}
.ec-builder-svg .pt-mini-block-d {
fill: #ef9a9a;
}
.ec-builder-svg .pt-mini-block-f {
fill: #81c784;
}
.ec-builder-svg .pt-mini-cell:hover {
filter: brightness(0.85);
}
.ec-builder-svg .pt-mini-block-label {
fill: rgba(0, 0, 0, 0.25);
font-size: 18px;
font-weight: 700;
font-family: system-ui, -apple-system, sans-serif;
}
.ec-builder-svg .pt-mini-current {
fill: none;
stroke: #c62828;
stroke-width: 1.5;
}
.ec-builder-svg .pt-mini-arrow {
stroke: #1976d2;
stroke-width: 1.5;
opacity: 0.5;
stroke-linecap: round;
}
.ec-builder-svg .pt-mini-arrowhead {
fill: #1976d2;
}
/* Mini Aufbau diagram */
.ec-builder-svg .aufbau-mini-bg {
fill: rgba(255, 255, 255, 0.95);
stroke: #ccc;
stroke-width: 1;
}
.ec-builder-svg .aufbau-mini-title {
fill: #555;
font-size: 11px;
font-weight: 600;
}
.ec-builder-svg .aufbau-mini-orb {
font-size: 13px;
font-family: system-ui, -apple-system, sans-serif;
}
.ec-builder-svg .aufbau-mini-empty {
fill: #bbb;
}
.ec-builder-svg .aufbau-mini-filled {
fill: #1976d2;
font-weight: 600;
}
.ec-builder-svg .aufbau-mini-current {
fill: #c62828;
font-weight: 700;
}
.ec-builder-svg .aufbau-mini-current-partial {
fill: #e65100;
font-weight: 700;
}
.ec-builder-svg .aufbau-mini-arrow {
stroke: #e91e63;
stroke-width: 2;
opacity: 0.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.ec-builder-svg .aufbau-mini-arrowhead {
fill: #e91e63;
}
/* ========== DARK MODE - CYBERPUNK THEME ========== */
body.quarto-dark .ec-builder-svg .ec-bg,
.quarto-dark .ec-builder-svg .ec-bg,
[data-bs-theme="dark"] .ec-builder-svg .ec-bg {
fill: #0a0a14;
}
body.quarto-dark .ec-builder-svg .ec-title,
.quarto-dark .ec-builder-svg .ec-title,
[data-bs-theme="dark"] .ec-builder-svg .ec-title {
fill: #00f0ff;
filter: drop-shadow(0 0 4px #00f0ff60);
}
body.quarto-dark .ec-builder-svg .ec-z-label,
.quarto-dark .ec-builder-svg .ec-z-label,
[data-bs-theme="dark"] .ec-builder-svg .ec-z-label {
fill: #7dd3fc;
}
body.quarto-dark .ec-builder-svg .ec-certainty-experimental,
.quarto-dark .ec-builder-svg .ec-certainty-experimental,
[data-bs-theme="dark"] .ec-builder-svg .ec-certainty-experimental {
fill: #00ff88;
filter: drop-shadow(0 0 2px #00ff8850);
}
body.quarto-dark .ec-builder-svg .ec-certainty-calculated,
.quarto-dark .ec-builder-svg .ec-certainty-calculated,
[data-bs-theme="dark"] .ec-builder-svg .ec-certainty-calculated {
fill: #bf5fff;
filter: drop-shadow(0 0 2px #bf5fff50);
}
body.quarto-dark .ec-builder-svg .ec-certainty-predicted,
.quarto-dark .ec-builder-svg .ec-certainty-predicted,
[data-bs-theme="dark"] .ec-builder-svg .ec-certainty-predicted {
fill: #ff2d6a;
filter: drop-shadow(0 0 2px #ff2d6a50);
}
body.quarto-dark .ec-builder-svg .ec-axis-label,
.quarto-dark .ec-builder-svg .ec-axis-label,
[data-bs-theme="dark"] .ec-builder-svg .ec-axis-label {
fill: #00f0ff;
filter: drop-shadow(0 0 3px #00f0ff50);
}
body.quarto-dark .ec-builder-svg .ec-axis-line,
.quarto-dark .ec-builder-svg .ec-axis-line,
[data-bs-theme="dark"] .ec-builder-svg .ec-axis-line {
stroke: #00f0ff;
filter: drop-shadow(0 0 2px #00f0ff60);
}
body.quarto-dark .ec-builder-svg .ec-arrow-marker,
.quarto-dark .ec-builder-svg .ec-arrow-marker,
[data-bs-theme="dark"] .ec-builder-svg .ec-arrow-marker {
fill: #00f0ff;
}
body.quarto-dark .ec-builder-svg .ec-orbital-label,
.quarto-dark .ec-builder-svg .ec-orbital-label,
[data-bs-theme="dark"] .ec-builder-svg .ec-orbital-label {
fill: #e0f7ff;
filter: drop-shadow(0 0 2px #00f0ff40);
}
body.quarto-dark .ec-builder-svg .ec-box,
.quarto-dark .ec-builder-svg .ec-box,
[data-bs-theme="dark"] .ec-builder-svg .ec-box {
stroke: #00f0ff60;
}
body.quarto-dark .ec-builder-svg .ec-box-empty,
.quarto-dark .ec-builder-svg .ec-box-empty,
[data-bs-theme="dark"] .ec-builder-svg .ec-box-empty {
fill: #0d0d1a;
}
body.quarto-dark .ec-builder-svg .ec-box-core,
.quarto-dark .ec-builder-svg .ec-box-core,
[data-bs-theme="dark"] .ec-builder-svg .ec-box-core {
fill: #1a1a2e;
filter: drop-shadow(0 0 2px #00f0ff20);
}
body.quarto-dark .ec-builder-svg .ec-box-valence,
.quarto-dark .ec-builder-svg .ec-box-valence,
[data-bs-theme="dark"] .ec-builder-svg .ec-box-valence {
fill: #0f2847;
filter: drop-shadow(0 0 4px #00f0ff30);
}
body.quarto-dark .ec-builder-svg .ec-arrow-paired,
.quarto-dark .ec-builder-svg .ec-arrow-paired,
[data-bs-theme="dark"] .ec-builder-svg .ec-arrow-paired {
fill: #00f0ff;
filter: drop-shadow(0 0 2px #00f0ff80);
}
body.quarto-dark .ec-builder-svg .ec-arrow-paired:hover,
.quarto-dark .ec-builder-svg .ec-arrow-paired:hover,
[data-bs-theme="dark"] .ec-builder-svg .ec-arrow-paired:hover {
fill: #ffdd00;
filter: drop-shadow(0 0 6px #ffdd00) drop-shadow(0 0 12px #ffdd0080);
}
body.quarto-dark .ec-builder-svg .ec-arrow-unpaired,
.quarto-dark .ec-builder-svg .ec-arrow-unpaired,
[data-bs-theme="dark"] .ec-builder-svg .ec-arrow-unpaired {
fill: #ff00ff;
filter: drop-shadow(0 0 3px #ff00ff80);
}
body.quarto-dark .ec-builder-svg .ec-arrow-unpaired:hover,
.quarto-dark .ec-builder-svg .ec-arrow-unpaired:hover,
[data-bs-theme="dark"] .ec-builder-svg .ec-arrow-unpaired:hover {
fill: #ffdd00;
filter: drop-shadow(0 0 6px #ffdd00) drop-shadow(0 0 12px #ffdd0080);
}
body.quarto-dark .ec-builder-svg .ec-arrow-selected,
.quarto-dark .ec-builder-svg .ec-arrow-selected,
[data-bs-theme="dark"] .ec-builder-svg .ec-arrow-selected {
fill: #ffdd00 !important;
filter: drop-shadow(0 0 6px #ffdd00) drop-shadow(0 0 12px #ffdd0080) drop-shadow(0 0 20px #ffdd0040) !important;
}
body.quarto-dark .ec-builder-svg .ec-config-label,
.quarto-dark .ec-builder-svg .ec-config-label,
[data-bs-theme="dark"] .ec-builder-svg .ec-config-label {
fill: #7dd3fc;
}
body.quarto-dark .ec-builder-svg .ec-config-text,
.quarto-dark .ec-builder-svg .ec-config-text,
[data-bs-theme="dark"] .ec-builder-svg .ec-config-text {
fill: #e0f7ff;
filter: drop-shadow(0 0 1px #00f0ff40);
}
body.quarto-dark .ec-builder-svg .ec-config-full,
.quarto-dark .ec-builder-svg .ec-config-full,
[data-bs-theme="dark"] .ec-builder-svg .ec-config-full {
fill: #7dd3fc;
}
body.quarto-dark .ec-builder-svg .ec-para,
.quarto-dark .ec-builder-svg .ec-para,
[data-bs-theme="dark"] .ec-builder-svg .ec-para {
fill: #ff00ff;
filter: drop-shadow(0 0 2px #ff00ff50);
}
body.quarto-dark .ec-builder-svg .ec-dia,
.quarto-dark .ec-builder-svg .ec-dia,
[data-bs-theme="dark"] .ec-builder-svg .ec-dia {
fill: #00ff88;
filter: drop-shadow(0 0 2px #00ff8850);
}
body.quarto-dark .ec-builder-svg .ec-counts,
.quarto-dark .ec-builder-svg .ec-counts,
[data-bs-theme="dark"] .ec-builder-svg .ec-counts {
fill: #7dd3fc;
}
body.quarto-dark .ec-builder-svg .ec-tooltip-bg,
.quarto-dark .ec-builder-svg .ec-tooltip-bg,
[data-bs-theme="dark"] .ec-builder-svg .ec-tooltip-bg {
fill: #0a0a14;
stroke: #00f0ff;
stroke-width: 1;
filter: drop-shadow(0 0 4px #00f0ff40);
}
body.quarto-dark .ec-builder-svg .ec-tooltip-text,
.quarto-dark .ec-builder-svg .ec-tooltip-text,
[data-bs-theme="dark"] .ec-builder-svg .ec-tooltip-text {
fill: #00f0ff;
}
body.quarto-dark .ec-builder-svg .ec-tt-var,
.quarto-dark .ec-builder-svg .ec-tt-var,
[data-bs-theme="dark"] .ec-builder-svg .ec-tt-var {
fill: #ff00ff;
}
body.quarto-dark .ec-builder-svg .ec-tt-sub,
.quarto-dark .ec-builder-svg .ec-tt-sub,
[data-bs-theme="dark"] .ec-builder-svg .ec-tt-sub {
fill: #ff00ff;
}
/* Mini Periodic Table dark mode - Cyberpunk */
body.quarto-dark .ec-builder-svg .pt-mini-bg,
.quarto-dark .ec-builder-svg .pt-mini-bg,
[data-bs-theme="dark"] .ec-builder-svg .pt-mini-bg {
fill: rgba(10, 10, 20, 0.95);
stroke: #00f0ff40;
filter: drop-shadow(0 0 4px #00f0ff20);
}
body.quarto-dark .ec-builder-svg .pt-mini-title,
.quarto-dark .ec-builder-svg .pt-mini-title,
[data-bs-theme="dark"] .ec-builder-svg .pt-mini-title {
fill: #00f0ff;
filter: drop-shadow(0 0 2px #00f0ff40);
}
body.quarto-dark .ec-builder-svg .pt-mini-cell,
.quarto-dark .ec-builder-svg .pt-mini-cell,
[data-bs-theme="dark"] .ec-builder-svg .pt-mini-cell {
fill: #1a1a2e;
}
/* s-block: coral pink (matches ptblock.svg) */
body.quarto-dark .ec-builder-svg .pt-mini-block-s,
.quarto-dark .ec-builder-svg .pt-mini-block-s,
[data-bs-theme="dark"] .ec-builder-svg .pt-mini-block-s {
fill: #ff6b7a;
filter: drop-shadow(0 0 3px #ff6b7a50);
}
/* p-block: neon green (matches ptblock.svg) */
body.quarto-dark .ec-builder-svg .pt-mini-block-p,
.quarto-dark .ec-builder-svg .pt-mini-block-p,
[data-bs-theme="dark"] .ec-builder-svg .pt-mini-block-p {
fill: #7bed9f;
filter: drop-shadow(0 0 3px #7bed9f50);
}
/* d-block: bright cyan (matches ptblock.svg) */
body.quarto-dark .ec-builder-svg .pt-mini-block-d,
.quarto-dark .ec-builder-svg .pt-mini-block-d,
[data-bs-theme="dark"] .ec-builder-svg .pt-mini-block-d {
fill: #00d9ff;
filter: drop-shadow(0 0 3px #00d9ff50);
}
/* f-block: neon purple (matches ptblock.svg) */
body.quarto-dark .ec-builder-svg .pt-mini-block-f,
.quarto-dark .ec-builder-svg .pt-mini-block-f,
[data-bs-theme="dark"] .ec-builder-svg .pt-mini-block-f {
fill: #c084fc;
filter: drop-shadow(0 0 3px #c084fc50);
}
body.quarto-dark .ec-builder-svg .pt-mini-cell:hover,
.quarto-dark .ec-builder-svg .pt-mini-cell:hover,
[data-bs-theme="dark"] .ec-builder-svg .pt-mini-cell:hover {
filter: brightness(1.15) drop-shadow(0 0 6px currentColor);
}
/* Block labels: dark text for visibility on bright fills */
body.quarto-dark .ec-builder-svg .pt-mini-block-label,
.quarto-dark .ec-builder-svg .pt-mini-block-label,
[data-bs-theme="dark"] .ec-builder-svg .pt-mini-block-label {
fill: rgba(0, 0, 0, 0.35);
filter: none;
}
body.quarto-dark .ec-builder-svg .pt-mini-current,
.quarto-dark .ec-builder-svg .pt-mini-current,
[data-bs-theme="dark"] .ec-builder-svg .pt-mini-current {
stroke: #ffdd00;
filter: drop-shadow(0 0 4px #ffdd00);
}
body.quarto-dark .ec-builder-svg .pt-mini-arrow,
.quarto-dark .ec-builder-svg .pt-mini-arrow,
[data-bs-theme="dark"] .ec-builder-svg .pt-mini-arrow {
stroke: #ff00ff;
opacity: 0.7;
filter: drop-shadow(0 0 2px #ff00ff60);
}
body.quarto-dark .ec-builder-svg .pt-mini-arrowhead,
.quarto-dark .ec-builder-svg .pt-mini-arrowhead,
[data-bs-theme="dark"] .ec-builder-svg .pt-mini-arrowhead {
fill: #ff00ff;
}
/* Mini Aufbau dark mode - Cyberpunk */
body.quarto-dark .ec-builder-svg .aufbau-mini-bg,
.quarto-dark .ec-builder-svg .aufbau-mini-bg,
[data-bs-theme="dark"] .ec-builder-svg .aufbau-mini-bg {
fill: rgba(10, 10, 20, 0.95);
stroke: #ff00ff40;
filter: drop-shadow(0 0 4px #ff00ff20);
}
body.quarto-dark .ec-builder-svg .aufbau-mini-title,
.quarto-dark .ec-builder-svg .aufbau-mini-title,
[data-bs-theme="dark"] .ec-builder-svg .aufbau-mini-title {
fill: #ff00ff;
filter: drop-shadow(0 0 2px #ff00ff40);
}
body.quarto-dark .ec-builder-svg .aufbau-mini-empty,
.quarto-dark .ec-builder-svg .aufbau-mini-empty,
[data-bs-theme="dark"] .ec-builder-svg .aufbau-mini-empty {
fill: #2a2a3e;
}
body.quarto-dark .ec-builder-svg .aufbau-mini-filled,
.quarto-dark .ec-builder-svg .aufbau-mini-filled,
[data-bs-theme="dark"] .ec-builder-svg .aufbau-mini-filled {
fill: #00f0ff;
filter: drop-shadow(0 0 2px #00f0ff60);
}
body.quarto-dark .ec-builder-svg .aufbau-mini-current,
.quarto-dark .ec-builder-svg .aufbau-mini-current,
[data-bs-theme="dark"] .ec-builder-svg .aufbau-mini-current {
fill: #ffdd00;
filter: drop-shadow(0 0 4px #ffdd00);
}
body.quarto-dark .ec-builder-svg .aufbau-mini-current-partial,
.quarto-dark .ec-builder-svg .aufbau-mini-current-partial,
[data-bs-theme="dark"] .ec-builder-svg .aufbau-mini-current-partial {
fill: #ff9500;
filter: drop-shadow(0 0 4px #ff9500);
}
body.quarto-dark .ec-builder-svg .aufbau-mini-arrow,
.quarto-dark .ec-builder-svg .aufbau-mini-arrow,
[data-bs-theme="dark"] .ec-builder-svg .aufbau-mini-arrow {
stroke: #ff00ff;
opacity: 0.6;
filter: drop-shadow(0 0 2px #ff00ff60);
}
body.quarto-dark .ec-builder-svg .aufbau-mini-arrowhead,
.quarto-dark .ec-builder-svg .aufbau-mini-arrowhead,
[data-bs-theme="dark"] .ec-builder-svg .aufbau-mini-arrowhead {
fill: #ff00ff;
}
</style>
`function formatConfigSuperscript(config) {
const superscripts = {'0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹'};
return config.replace(/(\d[spdf])(\d+)/g, (match, orbital, num) => {
const superNum = num.split('').map(d => superscripts[d] || d).join('');
return orbital + superNum;
});
}
// Exception info panel - shown when an exception element is selected
exceptionPanel = {
if (!isException) {
return html``;
}
const formattedActual = formatConfigSuperscript(currentElement.config);
return html`
<div class="ec-exception-panel">
<div class="ec-exception-header">
<span class="ec-exception-icon">⚠</span>
<strong>Configuration Exception</strong>
</div>
<div class="ec-exception-content">
<p><strong>Expected by simple aufbau:</strong> ${exceptionInfo.expected}</p>
<p><strong>Actual configuration:</strong> ${formattedActual}</p>
<p><strong>Why?</strong> ${exceptionInfo.reason}</p>
</div>
</div>
<style>
.ec-exception-panel {
margin-top: 16px;
padding: 12px 16px;
border-radius: 8px;
background: #FFF3E0;
border: 1px solid #FFB74D;
max-width: 700px;
font-size: 14px;
line-height: 1.5;
}
.ec-exception-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
color: #E65100;
}
.ec-exception-icon {
font-size: 18px;
}
.ec-exception-content p {
margin: 6px 0;
color: #5D4037;
}
.ec-exception-content strong {
color: #4E342E;
}
/* Dark mode */
.quarto-dark .ec-exception-panel,
[data-bs-theme="dark"] .ec-exception-panel {
background: #2d2510;
border-color: #8b6914;
}
.quarto-dark .ec-exception-header,
[data-bs-theme="dark"] .ec-exception-header {
color: #FFB74D;
}
.quarto-dark .ec-exception-content p,
[data-bs-theme="dark"] .ec-exception-content p {
color: #d4c4a8;
}
.quarto-dark .ec-exception-content strong,
[data-bs-theme="dark"] .ec-exception-content strong {
color: #fff8e1;
}
</style>
`;
}orbitalData2 = await FileAttachment("/files/json/orbital-energies.json").json()
// Define the three crossover regions (non-overlapping ranges for clean auto-switching)
regions = [
{ id: "3d4s", label: "3d/4s", zRange: [18, 36], orbitals: ["3p", "4s", "3d", "4p"], tickStep: 1 },
{ id: "4d5s", label: "4d/5s", zRange: [37, 54], orbitals: ["4p", "5s", "4d", "5p"], tickStep: 1 },
{ id: "5d6s", label: "5d/6s/4f", zRange: [55, 71], orbitals: ["5p", "6s", "4f", "5d"], tickStep: 1 }
]
// Default region for initial load (3d4s is the most commonly taught crossover)
autoRegion = "3d4s"
// Selected region state (UI rendered later, below the chart)
mutable selectedRegionState = "3d4s"
// Get current region config (default to first region if not found)
currentRegion = regions.find(r => r.id === selectedRegionState) || regions[0]
// Colors for each orbital (distinct neon-friendly colors for light/dark modes)
orbitalColors = ({
"1s": "#6366f1", "2s": "#8b5cf6", "3s": "#a855f7", "4s": "#c084fc", "5s": "#d946ef", "6s": "#e879f9", "7s": "#f0abfc",
"2p": "#3b82f6", "3p": "#22c55e", "4p": "#f97316", "5p": "#06b6d4", "6p": "#84cc16",
"3d": "#ec4899", "4d": "#f43f5e", "5d": "#fb7185",
"4f": "#a855f7", "5f": "#c026d3"
})
// Prepare data for the chart
chartData2 = {
const [zMin, zMax] = currentRegion.zRange;
const orbitals = currentRegion.orbitals;
// Build series for each orbital
const series = orbitals.map(orbital => {
const points = [];
for (let z = zMin; z <= zMax; z++) {
const el = orbitalData2.elements[String(z)];
if (el && el.orbitals[orbital] !== undefined) {
points.push({
z: z,
symbol: el.symbol,
energy: el.orbitals[orbital],
orbital: orbital
});
}
}
return { orbital, points, color: orbitalColors[orbital] };
});
return series;
}
// Find energy range for y-axis - dynamic based on actual data
energyRange2 = {
let min = 0, max = 0;
chartData2.forEach(series => {
series.points.forEach(p => {
if (p.energy < min) min = p.energy;
if (p.energy > max) max = p.energy;
});
});
// Add padding (10% on each side)
const range = max - min;
const padding = range * 0.1;
return [min - padding, max + padding];
}
// Get current Z from Widget 1 (falls back to 6 if not available)
currentZ = typeof selectedZValue !== 'undefined' ? selectedZValue : 6
// Determine which region currentZ belongs to (null if outside all regions)
regionForZ = {
const region = regions.find(r => currentZ >= r.zRange[0] && currentZ <= r.zRange[1]);
return region ? region.id : null;
}
// Auto-switch region when currentZ moves to a different region
autoRegionSwitch = {
const targetRegion = regionForZ;
if (targetRegion && targetRegion !== selectedRegionState) {
mutable selectedRegionState = targetRegion;
// Update the button UI to reflect the new active region
const buttons = document.querySelectorAll('.region-selector-btn');
const regionLabel = regions.find(r => r.id === targetRegion)?.label;
buttons.forEach(btn => {
btn.classList.remove('active');
if (btn.textContent === regionLabel) {
btn.classList.add('active');
}
});
}
return targetRegion;
}// Build the D3 chart with zoom/pan support
orbitalEnergyChart = {
// Initialize zoom state in window object (non-reactive, won't trigger re-renders)
if (!window._orbitalChartZoom) {
window._orbitalChartZoom = { yDomain: null, regionId: null };
}
// Capture Widget 1's slider reference at cell level (for click handler to update)
const widget1Slider = viewof selectedZInput;
const width = 720;
const height = 460;
const margin = { top: 50, right: 100, bottom: 70, left: 65 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const [zMin, zMax] = currentRegion.zRange;
// Create scales (these will be modified by zoom)
// Add padding to x-domain so edge data points don't get clipped
const xPadding = 0.5;
const xScale = d3.scaleLinear()
.domain([zMin - xPadding, zMax + xPadding])
.range([0, innerWidth]);
const yScale = d3.scaleLinear()
.domain(energyRange2)
.range([innerHeight, 0]);
// Store original scales for reset
const xScaleOrig = xScale.copy();
const yScaleOrig = yScale.copy();
// Line generator (uses current scales)
const line = d3.line()
.x(d => xScale(d.z))
.y(d => yScale(d.energy))
.curve(d3.curveMonotoneX);
// Create SVG (responsive - uses viewBox, max-width constrains to container)
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("class", "orbital-energy-chart")
.style("max-width", "100%")
.style("height", "auto");
// Define clip path to prevent overflow
svg.append("defs")
.append("clipPath")
.attr("id", "chart-clip")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", innerWidth)
.attr("height", innerHeight);
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Chart area with clipping
const chartArea = g.append("g")
.attr("clip-path", "url(#chart-clip)");
// Check if we need to reset zoom (region changed)
const savedState = window._orbitalChartZoom || { yDomain: null, regionId: null };
const shouldResetZoom = savedState.regionId !== currentRegion.id;
// Restore saved y-domain if same region
if (!shouldResetZoom && savedState.yDomain) {
yScale.domain(savedState.yDomain);
}
// Zoom behavior (y-axis only, zooms to mouse position)
const zoom = d3.zoom()
.scaleExtent([1, 10])
.filter(event => {
// Allow wheel zoom and drag, but not on click (so data point clicks work)
return event.type === 'wheel' || event.type === 'mousedown' || event.type === 'touchstart';
})
.on("zoom", zoomed);
// Invisible rect to capture zoom events
const zoomRect = chartArea.append("rect")
.attr("width", innerWidth)
.attr("height", innerHeight)
.attr("fill", "none")
.attr("pointer-events", "all")
.call(zoom);
// Groups for chart elements (for easy updating)
const gridGroup = chartArea.append("g").attr("class", "grid-group");
const zeroLineGroup = chartArea.append("g").attr("class", "zero-line-group");
const linesGroup = chartArea.append("g").attr("class", "lines-group");
const dotsGroup = chartArea.append("g").attr("class", "dots-group");
const markerGroup = chartArea.append("g").attr("class", "marker-group");
const markerLabelGroup = g.append("g").attr("class", "marker-label-group");
// X-axis group
const xAxisGroup = g.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${innerHeight})`);
// Y-axis group
const yAxisGroup = g.append("g")
.attr("class", "y-axis");
// Function to update the chart on zoom/pan
function updateChart() {
// Update gridlines
const gridLines = gridGroup.selectAll("line")
.data(yScale.ticks(8));
gridLines.enter().append("line")
.attr("stroke", "var(--chart-grid, #e0e0e0)")
.attr("stroke-width", 0.5)
.merge(gridLines)
.attr("x1", 0)
.attr("x2", innerWidth)
.attr("y1", d => yScale(d))
.attr("y2", d => yScale(d));
gridLines.exit().remove();
// Update zero line
zeroLineGroup.selectAll("line").remove();
const [yMin, yMax] = [yScale.invert(innerHeight), yScale.invert(0)];
if (yMin < 0 && yMax > 0) {
zeroLineGroup.append("line")
.attr("x1", 0)
.attr("x2", innerWidth)
.attr("y1", yScale(0))
.attr("y2", yScale(0))
.attr("stroke", "var(--chart-zero, #999)")
.attr("stroke-width", 1)
.attr("stroke-dasharray", "4,4");
}
// Update lines
linesGroup.selectAll("path")
.attr("d", d => line(d));
// Update dots
dotsGroup.selectAll("circle")
.attr("cx", d => xScale(d.z))
.attr("cy", d => yScale(d.energy));
// Update marker
if (currentZ >= zMin && currentZ <= zMax) {
const markerX = xScale(currentZ);
markerGroup.selectAll("line")
.attr("x1", markerX)
.attr("x2", markerX);
markerGroup.selectAll("circle")
.attr("cx", d => xScale(d.z))
.attr("cy", d => yScale(d.energy));
markerLabelGroup.selectAll("rect")
.attr("x", markerX - 14);
markerLabelGroup.selectAll("text")
.attr("x", markerX);
}
// Update axes
const tickStep = currentRegion.tickStep || 2;
xAxisGroup.call(d3.axisBottom(xScale)
.tickValues(d3.range(zMin, zMax + 1, tickStep).filter(z => {
const xPos = xScale(z);
return xPos >= 0 && xPos <= innerWidth;
}))
.tickFormat(z => {
const el = orbitalData2.elements[String(z)];
return el ? el.symbol : z;
}));
xAxisGroup.selectAll("text")
.attr("fill", "var(--chart-text, #333)")
.style("font-size", "10px");
xAxisGroup.select(".domain").attr("stroke", "var(--chart-axis, #666)");
xAxisGroup.selectAll(".tick line").attr("stroke", "var(--chart-axis, #666)");
yAxisGroup.call(d3.axisLeft(yScale)
.ticks(8)
.tickFormat(d => d.toFixed(2).replace('-', '−')));
yAxisGroup.selectAll("text").attr("fill", "var(--chart-text, #333)");
yAxisGroup.select(".domain").attr("stroke", "var(--chart-axis, #666)");
yAxisGroup.selectAll(".tick line").attr("stroke", "var(--chart-axis, #666)");
}
// Track if zoomed (for UI indicator)
let isZoomed = !shouldResetZoom && savedState.yDomain !== null;
// Zoom handler (y-axis only)
function zoomed(event) {
const transform = event.transform;
// Only update y-scale based on transform (keep x-scale fixed)
const newYScale = transform.rescaleY(yScaleOrig);
yScale.domain(newYScale.domain());
// x-scale stays at original domain (no horizontal zoom)
xScale.domain(xScaleOrig.domain());
updateChart();
// Save zoom state (non-reactive, won't trigger re-render)
isZoomed = transform.k > 1;
window._orbitalChartZoom = {
yDomain: isZoomed ? yScale.domain().slice() : null,
regionId: currentRegion.id
};
// Update zoom indicator visibility
zoomIndicator.style("opacity", isZoomed ? 1 : 0);
}
// Function to reset zoom
function resetZoom() {
zoomRect.transition().duration(300).call(zoom.transform, d3.zoomIdentity);
window._orbitalChartZoom = { yDomain: null, regionId: currentRegion.id };
isZoomed = false;
}
// Helper to split points into consecutive segments (no gaps in Z)
function getConsecutiveSegments(points) {
if (points.length === 0) return [];
const segments = [];
let currentSegment = [points[0]];
for (let i = 1; i < points.length; i++) {
if (points[i].z === points[i-1].z + 1) {
// Consecutive - add to current segment
currentSegment.push(points[i]);
} else {
// Gap found - start new segment
if (currentSegment.length > 1) {
segments.push(currentSegment);
}
currentSegment = [points[i]];
}
}
// Don't forget the last segment
if (currentSegment.length > 1) {
segments.push(currentSegment);
}
return segments;
}
// Initial draw of lines - only connect consecutive data points
chartData2.forEach(series => {
const segments = getConsecutiveSegments(series.points);
segments.forEach(segment => {
linesGroup.append("path")
.datum(segment)
.attr("fill", "none")
.attr("stroke", series.color)
.attr("stroke-width", 2.5)
.attr("d", line)
.attr("class", `line-${series.orbital}`);
});
// Add dots for each data point
dotsGroup.selectAll(`.dot-${series.orbital}`)
.data(series.points)
.join("circle")
.attr("class", `dot-${series.orbital} clickable-dot`)
.attr("cx", d => xScale(d.z))
.attr("cy", d => yScale(d.energy))
.attr("r", 4)
.attr("fill", series.color)
.attr("stroke", "var(--chart-bg, white)")
.attr("stroke-width", 1.5)
.style("cursor", "pointer")
.on("click", function(event, d) {
event.stopPropagation();
if (widget1Slider && widget1Slider.setZ) {
widget1Slider.setZ(d.z);
}
})
.on("mouseenter", function(event, d) {
d3.select(this)
.attr("r", 7)
.attr("stroke-width", 2);
const tooltipText = `${d.symbol} (Z=${d.z}): ${d.energy.toFixed(3).replace('-', '−')} Ha`;
const tooltipX = xScale(d.z);
const tooltipY = yScale(d.energy) - 14;
chartArea.append("rect")
.attr("class", "hover-tooltip")
.attr("x", tooltipX - 55)
.attr("y", tooltipY - 10)
.attr("width", 110)
.attr("height", 14)
.attr("rx", 3)
.attr("fill", "var(--chart-bg, white)")
.attr("stroke", "var(--chart-axis, #666)")
.attr("stroke-width", 0.5)
.attr("pointer-events", "none");
chartArea.append("text")
.attr("class", "hover-tooltip")
.attr("x", tooltipX)
.attr("y", tooltipY)
.attr("text-anchor", "middle")
.attr("fill", "var(--chart-text, #333)")
.attr("font-size", "10px")
.attr("font-weight", "bold")
.attr("pointer-events", "none")
.text(tooltipText);
})
.on("mouseleave", function(event, d) {
d3.select(this)
.attr("r", 4)
.attr("stroke-width", 1.5);
chartArea.selectAll(".hover-tooltip").remove();
});
});
// Add vertical marker for current element (if in range)
if (currentZ >= zMin && currentZ <= zMax) {
const markerX = xScale(currentZ);
const el = orbitalData2.elements[String(currentZ)];
const symbol = el ? el.symbol : currentZ;
markerGroup.append("line")
.attr("x1", markerX)
.attr("x2", markerX)
.attr("y1", 0)
.attr("y2", innerHeight)
.attr("stroke", "#FF5722")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "6,3")
.attr("opacity", 0.8);
const labelY = 15;
markerLabelGroup.append("rect")
.attr("x", markerX - 14)
.attr("y", labelY - 10)
.attr("width", 28)
.attr("height", 16)
.attr("rx", 3)
.attr("fill", "#FF5722")
.attr("opacity", 0.9);
markerLabelGroup.append("text")
.attr("x", markerX)
.attr("y", labelY + 2)
.attr("text-anchor", "middle")
.attr("fill", "white")
.attr("font-weight", "bold")
.attr("font-size", "11px")
.text(symbol);
// Highlight circles for current Z
chartData2.forEach(series => {
const point = series.points.find(p => p.z === currentZ);
if (point) {
markerGroup.append("circle")
.datum(point)
.attr("cx", xScale(point.z))
.attr("cy", yScale(point.energy))
.attr("r", 7)
.attr("fill", "none")
.attr("stroke", "#FF5722")
.attr("stroke-width", 2);
}
});
}
// Initial axis draw
updateChart();
// X-axis label
g.append("text")
.attr("x", innerWidth / 2)
.attr("y", innerHeight + 42)
.attr("text-anchor", "middle")
.attr("fill", "var(--chart-text, #333)")
.style("font-size", "12px")
.html("Atomic Number (<tspan font-style='italic'>Z</tspan>)");
// Y-axis label
g.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -innerHeight / 2)
.attr("y", -50)
.attr("text-anchor", "middle")
.attr("fill", "var(--chart-text, #333)")
.style("font-size", "12px")
.text("Orbital Energy (Hartrees)");
// Title
svg.append("text")
.attr("x", width / 2)
.attr("y", 22)
.attr("text-anchor", "middle")
.attr("fill", "var(--chart-text, #333)")
.style("font-size", "14px")
.style("font-weight", "bold")
.text(`Orbital Energies: ${currentRegion.label} Crossover Region`);
// Legend
const legend = svg.append("g")
.attr("transform", `translate(${width - margin.right + 12}, ${margin.top + 10})`);
chartData2.forEach((series, i) => {
const ly = i * 24;
legend.append("line")
.attr("class", "legend-line")
.attr("x1", 0).attr("x2", 20)
.attr("y1", ly).attr("y2", ly)
.attr("stroke", series.color)
.attr("stroke-width", 2.5);
legend.append("circle")
.attr("class", "legend-dot")
.attr("cx", 10).attr("cy", ly).attr("r", 4)
.attr("fill", series.color);
legend.append("text")
.attr("class", "legend-text")
.attr("x", 26).attr("y", ly + 4)
.attr("fill", "var(--chart-text, #333)")
.style("font-size", "12px")
.text(series.orbital);
});
// Zoom indicator and reset button (hidden initially)
const zoomIndicator = svg.append("g")
.attr("transform", `translate(${width - margin.right - 70}, ${margin.top - 5})`)
.style("opacity", 0)
.style("cursor", "pointer")
.on("click", resetZoom);
zoomIndicator.append("rect")
.attr("x", 0).attr("y", -12)
.attr("width", 58).attr("height", 18)
.attr("rx", 4)
.attr("fill", "#2196F3")
.attr("opacity", 0.9);
zoomIndicator.append("text")
.attr("x", 29).attr("y", 2)
.attr("text-anchor", "middle")
.attr("fill", "white")
.style("font-size", "10px")
.style("font-weight", "bold")
.text("⟲ Reset");
// Zoom instructions
svg.append("text")
.attr("x", margin.left + 5)
.attr("y", height - 6)
.attr("fill", "var(--chart-text, #333)")
.attr("opacity", 0.6)
.style("font-size", "10px")
.text("↓ Lower energy = more stable • Scroll to zoom (vertical), drag to pan");
// Show reset button if zoomed, update region tracking
if (isZoomed) {
zoomIndicator.style("opacity", 1);
}
if (shouldResetZoom) {
// Update regionId when region changed
window._orbitalChartZoom = { yDomain: null, regionId: currentRegion.id };
}
return svg.node();
}// CSS for dark mode support - Cyberpunk theme
html`<style>
.orbital-energy-chart {
background: var(--chart-bg, white);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* Light mode defaults */
:root {
--chart-bg: white;
--chart-text: #333;
--chart-axis: #666;
--chart-grid: #e0e0e0;
--chart-zero: #999;
}
/* ========== DARK MODE - CYBERPUNK THEME ========== */
[data-bs-theme="dark"] .orbital-energy-chart,
.quarto-dark .orbital-energy-chart {
--chart-bg: #0a0a14;
--chart-text: #00f0ff;
--chart-axis: #00f0ff80;
--chart-grid: #00f0ff20;
--chart-zero: #ff00ff60;
background: #0a0a14;
box-shadow: 0 0 20px #00f0ff20, 0 0 40px #00f0ff10, inset 0 0 60px #00f0ff05;
border: none;
}
[data-bs-theme="dark"],
.quarto-dark {
--chart-bg: #0a0a14;
--chart-text: #00f0ff;
--chart-axis: #00f0ff80;
--chart-grid: #00f0ff20;
--chart-zero: #ff00ff60;
}
/* Subtle glow effects for chart elements (reduced for clarity) */
[data-bs-theme="dark"] .orbital-energy-chart text,
.quarto-dark .orbital-energy-chart text {
filter: drop-shadow(0 0 1px currentColor);
}
[data-bs-theme="dark"] .orbital-energy-chart .domain,
[data-bs-theme="dark"] .orbital-energy-chart .tick line,
.quarto-dark .orbital-energy-chart .domain,
.quarto-dark .orbital-energy-chart .tick line {
filter: drop-shadow(0 0 1px #00f0ff30);
}
/* Legend styling in dark mode - no filter to preserve inline colors */
[data-bs-theme="dark"] .orbital-energy-chart .legend-line,
.quarto-dark .orbital-energy-chart .legend-line {
filter: none;
}
[data-bs-theme="dark"] .orbital-energy-chart .legend-dot,
.quarto-dark .orbital-energy-chart .legend-dot {
filter: none;
}
[data-bs-theme="dark"] .orbital-energy-chart .legend-text,
.quarto-dark .orbital-energy-chart .legend-text {
filter: none;
}
/* Lines and dots in dark mode (reduced glow for clarity) */
[data-bs-theme="dark"] .orbital-energy-chart .lines-group path,
.quarto-dark .orbital-energy-chart .lines-group path {
filter: drop-shadow(0 0 1.5px currentColor);
}
[data-bs-theme="dark"] .orbital-energy-chart .dots-group circle,
.quarto-dark .orbital-energy-chart .dots-group circle {
filter: drop-shadow(0 0 2px currentColor);
}
[data-bs-theme="dark"] .orbital-energy-chart .clickable-dot:hover,
.quarto-dark .orbital-energy-chart .clickable-dot:hover {
filter: drop-shadow(0 0 4px currentColor) drop-shadow(0 0 6px currentColor);
}
/* Current element marker glow (subtle) */
[data-bs-theme="dark"] .orbital-energy-chart .marker-group line,
.quarto-dark .orbital-energy-chart .marker-group line {
filter: drop-shadow(0 0 2px #ff5722);
}
[data-bs-theme="dark"] .orbital-energy-chart .marker-group circle,
.quarto-dark .orbital-energy-chart .marker-group circle {
filter: drop-shadow(0 0 3px #ff5722);
}
/* Region selector styling */
.region-selector {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-top: 8px;
font-size: 13px;
}
.region-selector-label {
font-weight: 500;
color: var(--chart-text, #333);
white-space: nowrap;
}
.region-selector-options {
display: flex;
gap: 6px;
}
.region-selector-btn {
padding: 4px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: var(--chart-bg, white);
color: var(--chart-text, #333);
cursor: pointer;
font-size: 12px;
transition: all 0.15s;
}
.region-selector-btn:hover {
border-color: #1976d2;
background: #e3f2fd;
}
.region-selector-btn.active {
background: #1976d2;
color: white;
border-color: #1976d2;
}
/* Cyberpunk region selector buttons */
.quarto-dark .region-selector-btn,
[data-bs-theme="dark"] .region-selector-btn {
border-color: #00f0ff40;
background: #0a0a14;
color: #00f0ff;
box-shadow: 0 0 4px #00f0ff20;
}
.quarto-dark .region-selector-btn:hover,
[data-bs-theme="dark"] .region-selector-btn:hover {
border-color: #00f0ff;
background: #0f2847;
box-shadow: 0 0 8px #00f0ff40, 0 0 16px #00f0ff20;
}
.quarto-dark .region-selector-btn.active,
[data-bs-theme="dark"] .region-selector-btn.active {
background: #00f0ff;
color: #0a0a14;
border-color: #00f0ff;
box-shadow: 0 0 12px #00f0ff60, 0 0 24px #00f0ff30;
font-weight: 600;
}
.quarto-dark .region-selector-label,
[data-bs-theme="dark"] .region-selector-label {
color: #00f0ff;
text-shadow: 0 0 8px #00f0ff60;
}
</style>`// Region selector UI (displayed below chart)
viewof regionSelector = {
const container = document.createElement("div");
container.className = "region-selector";
const label = document.createElement("span");
label.className = "region-selector-label";
label.textContent = "Crossover Region:";
container.appendChild(label);
const options = document.createElement("div");
options.className = "region-selector-options";
// Get reference to Widget 1's slider for updating Z when region changes
const widget1Slider = typeof viewof selectedZInput !== 'undefined' ? viewof selectedZInput : null;
regions.forEach(region => {
const btn = document.createElement("button");
btn.className = "region-selector-btn" + (region.id === selectedRegionState ? " active" : "");
btn.textContent = region.label;
btn.onclick = () => {
options.querySelectorAll(".region-selector-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
mutable selectedRegionState = region.id;
// Also update selected element to first Z in this region
const firstZ = region.zRange[0];
if (widget1Slider && widget1Slider.setZ) {
widget1Slider.setZ(firstZ);
}
container.value = region.id;
container.dispatchEvent(new CustomEvent("input", {bubbles: true}));
};
options.appendChild(btn);
});
container.appendChild(options);
container.value = selectedRegionState;
return container;
}