599 Circuit Analysis Solver
Interactive Tool: Nodal Analysis, KVL/KCL, and Thevenin/Norton Equivalents
599.1 Circuit Analysis Solver
This interactive tool helps you analyze resistive circuits commonly found in IoT sensor interfaces. Build circuits, solve for voltages and currents, and visualize step-by-step solutions using nodal analysis, KVL, and KCL.
This circuit solver provides:
- Simple Circuit Builder: Voltage sources and resistors in series/parallel configurations
- Preset Circuits: Voltage divider, current divider, Wheatstone bridge, sensor interface
- Nodal Analysis: Step-by-step node voltage calculations
- KVL/KCL Display: Shows Kirchhoff’s equations for the circuit
- Results Dashboard: Node voltages, branch currents, power dissipation
- Thevenin/Norton: Calculate equivalent circuits
- Circuit Diagram: Visual representation of the circuit
- Select a Preset: Choose a common circuit configuration
- Modify Values: Adjust voltage source and resistor values
- Analyze: View the step-by-step solution
- Explore Results: See voltages, currents, and power for each element
- Calculate Equivalents: Find Thevenin/Norton equivalents
Show code
// ============================================
// Circuit Analysis Solver - Interactive Tool
// Self-contained OJS implementation
// ============================================
{
const d3 = await require("d3@7");
// IEEE Color palette
const colors = {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
red: "#E74C3C",
green: "#27AE60",
purple: "#9B59B6",
yellow: "#F1C40F",
darkBlue: "#1a252f"
};
// Configuration
const width = 950, height = 950;
// Circuit presets
const presets = {
voltage_divider: {
name: "Voltage Divider",
description: "Basic resistive voltage divider",
vs: 12,
r1: 10000,
r2: 10000,
r3: null,
r4: null,
topology: "voltage_divider"
},
current_divider: {
name: "Current Divider",
description: "Parallel resistors with current source",
vs: 12,
r1: 1000,
r2: 2000,
r3: null,
r4: null,
topology: "current_divider"
},
wheatstone: {
name: "Wheatstone Bridge",
description: "Balanced bridge circuit for sensors",
vs: 5,
r1: 1000,
r2: 1000,
r3: 1000,
r4: 1000,
topology: "wheatstone"
},
sensor_interface: {
name: "Sensor Interface",
description: "Resistive sensor with signal conditioning",
vs: 3.3,
r1: 10000, // Reference resistor
r2: 10000, // Sensor (variable)
r3: 100000, // Load/input impedance
r4: null,
topology: "sensor_interface"
}
};
// State
let state = {
preset: "voltage_divider",
vs: 12,
r1: 10000,
r2: 10000,
r3: null,
r4: null,
showSteps: true
};
// Create container
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", "1000px")
.style("margin", "0 auto");
// =========================================================================
// HEADER
// =========================================================================
const header = container.append("div")
.style("background", `linear-gradient(135deg, ${colors.navy} 0%, ${colors.darkBlue} 100%)`)
.style("border-radius", "12px 12px 0 0")
.style("padding", "20px")
.style("text-align", "center");
header.append("div")
.style("color", colors.white)
.style("font-size", "24px")
.style("font-weight", "bold")
.text("Circuit Analysis Solver");
header.append("div")
.style("color", colors.lightGray)
.style("font-size", "14px")
.style("margin-top", "5px")
.text("Nodal Analysis, KVL/KCL, Thevenin/Norton Equivalents");
// =========================================================================
// CONTROL PANEL
// =========================================================================
const controlPanel = container.append("div")
.style("background", colors.lightGray)
.style("padding", "20px")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(200px, 1fr))")
.style("gap", "15px");
// Preset selector
const presetControl = controlPanel.append("div")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px");
presetControl.append("label")
.style("color", colors.navy)
.style("font-weight", "bold")
.style("display", "block")
.style("margin-bottom", "8px")
.text("Circuit Preset");
const presetSelect = presetControl.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "14px")
.style("cursor", "pointer")
.on("change", function() {
state.preset = this.value;
loadPreset(state.preset);
updateAll();
});
Object.entries(presets).forEach(([key, preset]) => {
presetSelect.append("option")
.attr("value", key)
.attr("selected", key === state.preset ? true : null)
.text(preset.name);
});
const presetDescription = presetControl.append("div")
.style("color", colors.gray)
.style("font-size", "12px")
.style("margin-top", "8px")
.style("font-style", "italic");
// Voltage source input
const vsControl = controlPanel.append("div")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px");
vsControl.append("label")
.style("color", colors.navy)
.style("font-weight", "bold")
.style("display", "block")
.style("margin-bottom", "8px")
.text("Voltage Source (Vs)");
const vsRow = vsControl.append("div")
.style("display", "flex")
.style("gap", "8px");
const vsInput = vsRow.append("input")
.attr("type", "number")
.attr("value", state.vs)
.attr("min", 0.1)
.attr("step", 0.1)
.style("flex", "1")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "14px")
.on("input", function() {
state.vs = +this.value || 0.1;
updateAll();
});
vsRow.append("span")
.style("color", colors.gray)
.style("align-self", "center")
.text("V");
// R1 input
const r1Control = controlPanel.append("div")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px");
r1Control.append("label")
.style("color", colors.navy)
.style("font-weight", "bold")
.style("display", "block")
.style("margin-bottom", "8px")
.text("Resistor R1");
const r1Row = r1Control.append("div")
.style("display", "flex")
.style("gap", "8px");
const r1Input = r1Row.append("input")
.attr("type", "number")
.attr("value", state.r1)
.attr("min", 1)
.attr("step", 100)
.style("flex", "1")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "14px")
.on("input", function() {
state.r1 = +this.value || 1;
updateAll();
});
r1Row.append("span")
.style("color", colors.gray)
.style("align-self", "center")
.text("Ohm");
// R2 input
const r2Control = controlPanel.append("div")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px");
r2Control.append("label")
.style("color", colors.navy)
.style("font-weight", "bold")
.style("display", "block")
.style("margin-bottom", "8px")
.text("Resistor R2");
const r2Row = r2Control.append("div")
.style("display", "flex")
.style("gap", "8px");
const r2Input = r2Row.append("input")
.attr("type", "number")
.attr("value", state.r2)
.attr("min", 1)
.attr("step", 100)
.style("flex", "1")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "14px")
.on("input", function() {
state.r2 = +this.value || 1;
updateAll();
});
r2Row.append("span")
.style("color", colors.gray)
.style("align-self", "center")
.text("Ohm");
// R3 input (for more complex circuits)
const r3Control = controlPanel.append("div")
.attr("id", "r3-control")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px");
r3Control.append("label")
.style("color", colors.navy)
.style("font-weight", "bold")
.style("display", "block")
.style("margin-bottom", "8px")
.text("Resistor R3");
const r3Row = r3Control.append("div")
.style("display", "flex")
.style("gap", "8px");
const r3Input = r3Row.append("input")
.attr("type", "number")
.attr("value", state.r3 || 1000)
.attr("min", 1)
.attr("step", 100)
.style("flex", "1")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "14px")
.on("input", function() {
state.r3 = +this.value || 1;
updateAll();
});
r3Row.append("span")
.style("color", colors.gray)
.style("align-self", "center")
.text("Ohm");
// R4 input (for Wheatstone bridge)
const r4Control = controlPanel.append("div")
.attr("id", "r4-control")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px");
r4Control.append("label")
.style("color", colors.navy)
.style("font-weight", "bold")
.style("display", "block")
.style("margin-bottom", "8px")
.text("Resistor R4");
const r4Row = r4Control.append("div")
.style("display", "flex")
.style("gap", "8px");
const r4Input = r4Row.append("input")
.attr("type", "number")
.attr("value", state.r4 || 1000)
.attr("min", 1)
.attr("step", 100)
.style("flex", "1")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "14px")
.on("input", function() {
state.r4 = +this.value || 1;
updateAll();
});
r4Row.append("span")
.style("color", colors.gray)
.style("align-self", "center")
.text("Ohm");
// =========================================================================
// MAIN CONTENT AREA
// =========================================================================
const mainContent = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "0");
// =========================================================================
// CIRCUIT DIAGRAM (LEFT)
// =========================================================================
const diagramPanel = mainContent.append("div")
.style("background", colors.white)
.style("padding", "20px")
.style("border-right", `1px solid ${colors.lightGray}`);
diagramPanel.append("div")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "15px")
.text("Circuit Diagram");
const diagramSvg = diagramPanel.append("svg")
.attr("viewBox", "0 0 400 350")
.attr("width", "100%")
.style("display", "block");
// =========================================================================
// RESULTS PANEL (RIGHT)
// =========================================================================
const resultsPanel = mainContent.append("div")
.style("background", "#f8f9fa")
.style("padding", "20px");
resultsPanel.append("div")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "15px")
.text("Analysis Results");
const resultsContent = resultsPanel.append("div");
// =========================================================================
// EQUATIONS PANEL (FULL WIDTH)
// =========================================================================
const equationsPanel = container.append("div")
.style("background", colors.white)
.style("padding", "20px")
.style("border-top", `1px solid ${colors.lightGray}`);
equationsPanel.append("div")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "15px")
.text("KVL / KCL Equations & Step-by-Step Solution");
const equationsContent = equationsPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "20px");
// =========================================================================
// THEVENIN/NORTON PANEL
// =========================================================================
const theveninPanel = container.append("div")
.style("background", colors.lightGray)
.style("padding", "20px")
.style("border-radius", "0 0 12px 12px");
theveninPanel.append("div")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "15px")
.text("Thevenin / Norton Equivalent (at output terminals)");
const theveninContent = theveninPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(200px, 1fr))")
.style("gap", "15px");
// =========================================================================
// HELPER FUNCTIONS
// =========================================================================
function formatValue(value, unit) {
if (value >= 1000000) return (value / 1000000).toFixed(2) + " M" + unit;
if (value >= 1000) return (value / 1000).toFixed(2) + " k" + unit;
if (value >= 1) return value.toFixed(3) + " " + unit;
if (value >= 0.001) return (value * 1000).toFixed(3) + " m" + unit;
if (value >= 0.000001) return (value * 1000000).toFixed(3) + " u" + unit;
return value.toExponential(3) + " " + unit;
}
function loadPreset(presetKey) {
const preset = presets[presetKey];
state.vs = preset.vs;
state.r1 = preset.r1;
state.r2 = preset.r2;
state.r3 = preset.r3;
state.r4 = preset.r4;
vsInput.property("value", state.vs);
r1Input.property("value", state.r1);
r2Input.property("value", state.r2);
r3Input.property("value", state.r3 || 1000);
r4Input.property("value", state.r4 || 1000);
presetDescription.text(preset.description);
// Show/hide R3, R4 based on topology
const needsR3 = preset.topology === "wheatstone" || preset.topology === "sensor_interface";
const needsR4 = preset.topology === "wheatstone";
r3Control.style("display", needsR3 ? "block" : "none");
r4Control.style("display", needsR4 ? "block" : "none");
}
// =========================================================================
// CIRCUIT ANALYSIS FUNCTIONS
// =========================================================================
function analyzeVoltageDivider() {
const { vs, r1, r2 } = state;
// Node voltages
const v1 = vs; // Top of voltage source
const vOut = vs * r2 / (r1 + r2);
// Branch currents
const i = vs / (r1 + r2);
const i1 = i;
const i2 = i;
// Power dissipation
const p1 = i1 * i1 * r1;
const p2 = i2 * i2 * r2;
const pTotal = vs * i;
// Thevenin equivalent (looking from output)
const vTh = vOut;
const rTh = r1 * r2 / (r1 + r2);
// Norton equivalent
const iN = vTh / rTh;
const rN = rTh;
return {
nodes: [
{ name: "V1 (Vs+)", voltage: v1 },
{ name: "Vout (R1-R2 junction)", voltage: vOut },
{ name: "GND", voltage: 0 }
],
branches: [
{ name: "R1", current: i1, voltage: vs - vOut, power: p1, resistance: r1 },
{ name: "R2", current: i2, voltage: vOut, power: p2, resistance: r2 }
],
thevenin: { vTh, rTh },
norton: { iN, rN },
totalCurrent: i,
totalPower: pTotal,
equations: {
kvl: [
`KVL: Vs - V_R1 - V_R2 = 0`,
`${vs}V - I*R1 - I*R2 = 0`,
`${vs}V = I*(${r1} + ${r2})`,
`I = ${vs}V / ${r1 + r2}Ohm = ${formatValue(i, "A")}`
],
kcl: [
`KCL at Vout: I_in = I_out`,
`Current through R1 = Current through R2`,
`I_R1 = I_R2 = ${formatValue(i, "A")}`
],
nodal: [
`Node Vout: (Vs - Vout)/R1 = Vout/R2`,
`Vout * (1/R1 + 1/R2) = Vs/R1`,
`Vout = Vs * R2/(R1 + R2)`,
`Vout = ${vs}V * ${r2}/(${r1}+${r2})`,
`Vout = ${vOut.toFixed(4)}V`
]
}
};
}
function analyzeCurrentDivider() {
const { vs, r1, r2 } = state;
// Total resistance (parallel)
const rTotal = (r1 * r2) / (r1 + r2);
// Total current from source
const iTotal = vs / rTotal;
// Branch currents (current divider rule)
const i1 = iTotal * r2 / (r1 + r2);
const i2 = iTotal * r1 / (r1 + r2);
// All resistors share same voltage
const vParallel = vs;
// Power
const p1 = i1 * i1 * r1;
const p2 = i2 * i2 * r2;
const pTotal = vs * iTotal;
// Thevenin/Norton
const vTh = vs;
const rTh = rTotal;
const iN = iTotal;
return {
nodes: [
{ name: "V+ (Common node)", voltage: vs },
{ name: "GND", voltage: 0 }
],
branches: [
{ name: "R1", current: i1, voltage: vs, power: p1, resistance: r1 },
{ name: "R2", current: i2, voltage: vs, power: p2, resistance: r2 }
],
thevenin: { vTh, rTh },
norton: { iN, rN: rTh },
totalCurrent: iTotal,
totalPower: pTotal,
equations: {
kvl: [
`KVL (each branch): Vs - V_Rx = 0`,
`V_R1 = V_R2 = Vs = ${vs}V`
],
kcl: [
`KCL: I_total = I_R1 + I_R2`,
`I_R1 = Vs/R1 = ${vs}/${r1} = ${formatValue(i1, "A")}`,
`I_R2 = Vs/R2 = ${vs}/${r2} = ${formatValue(i2, "A")}`,
`I_total = ${formatValue(iTotal, "A")}`
],
nodal: [
`Current Divider Rule:`,
`I_R1 = I_total * R2/(R1+R2)`,
`I_R2 = I_total * R1/(R1+R2)`,
`Note: More current flows through smaller resistor`
]
}
};
}
function analyzeWheatstone() {
const { vs, r1, r2, r3, r4 } = state;
// Wheatstone bridge analysis
// R1 and R3 in series (top branch)
// R2 and R4 in series (bottom branch)
// Bridge output between R1-R3 junction and R2-R4 junction
// Using superposition and Thevenin
// VA = junction of R1-R3, VB = junction of R2-R4
const vA = vs * r3 / (r1 + r3);
const vB = vs * r4 / (r2 + r4);
const vBridge = vA - vB;
// Branch currents
const i13 = vs / (r1 + r3); // Through R1-R3 branch
const i24 = vs / (r2 + r4); // Through R2-R4 branch
// Power
const p1 = i13 * i13 * r1;
const p2 = i24 * i24 * r2;
const p3 = i13 * i13 * r3;
const p4 = i24 * i24 * r4;
const pTotal = vs * (i13 + i24);
// Balance condition
const isBalanced = Math.abs(r1 * r4 - r2 * r3) < 0.001;
// Thevenin equivalent at bridge output
const vTh = vBridge;
const rTh = (r1 * r3) / (r1 + r3) + (r2 * r4) / (r2 + r4);
return {
nodes: [
{ name: "V+ (Top)", voltage: vs },
{ name: "VA (R1-R3)", voltage: vA },
{ name: "VB (R2-R4)", voltage: vB },
{ name: "GND", voltage: 0 }
],
branches: [
{ name: "R1", current: i13, voltage: vs - vA, power: p1, resistance: r1 },
{ name: "R2", current: i24, voltage: vs - vB, power: p2, resistance: r2 },
{ name: "R3", current: i13, voltage: vA, power: p3, resistance: r3 },
{ name: "R4", current: i24, voltage: vB, power: p4, resistance: r4 }
],
bridgeVoltage: vBridge,
isBalanced,
thevenin: { vTh, rTh },
norton: { iN: vTh / rTh, rN: rTh },
totalCurrent: i13 + i24,
totalPower: pTotal,
equations: {
kvl: [
`KVL (R1-R3 branch): Vs - V_R1 - V_R3 = 0`,
`KVL (R2-R4 branch): Vs - V_R2 - V_R4 = 0`,
`Bridge voltage: Vbridge = VA - VB = ${vBridge.toFixed(4)}V`
],
kcl: [
`KCL at VA: I_R1 = I_R3 = ${formatValue(i13, "A")}`,
`KCL at VB: I_R2 = I_R4 = ${formatValue(i24, "A")}`,
`(Assuming no load at bridge output)`
],
nodal: [
`VA = Vs * R3/(R1+R3) = ${vA.toFixed(4)}V`,
`VB = Vs * R4/(R2+R4) = ${vB.toFixed(4)}V`,
`Balance condition: R1*R4 = R2*R3`,
isBalanced ? `Bridge is BALANCED` : `Bridge is UNBALANCED`,
`Sensitivity: dV/dR depends on bridge ratio`
]
}
};
}
function analyzeSensorInterface() {
const { vs, r1, r2, r3 } = state;
// Voltage divider with load
// R1 in series with parallel combination of R2 (sensor) and R3 (load)
const rParallel = (r2 * r3) / (r2 + r3);
const vOut = vs * rParallel / (r1 + rParallel);
// Currents
const iTotal = vs / (r1 + rParallel);
const i1 = iTotal;
const i2 = vOut / r2;
const i3 = vOut / r3;
// Power
const p1 = i1 * i1 * r1;
const p2 = i2 * i2 * r2;
const p3 = i3 * i3 * r3;
const pTotal = vs * iTotal;
// Unloaded output (for comparison)
const vOutUnloaded = vs * r2 / (r1 + r2);
const loadingError = ((vOutUnloaded - vOut) / vOutUnloaded) * 100;
// Thevenin at output (with R3 as load)
const vTh = vs * r2 / (r1 + r2); // Open circuit voltage
const rTh = (r1 * r2) / (r1 + r2); // Source resistance
return {
nodes: [
{ name: "V+ (Supply)", voltage: vs },
{ name: "Vout (Sensor junction)", voltage: vOut },
{ name: "GND", voltage: 0 }
],
branches: [
{ name: "R1 (Reference)", current: i1, voltage: vs - vOut, power: p1, resistance: r1 },
{ name: "R2 (Sensor)", current: i2, voltage: vOut, power: p2, resistance: r2 },
{ name: "R3 (Load)", current: i3, voltage: vOut, power: p3, resistance: r3 }
],
vOutUnloaded,
loadingError,
thevenin: { vTh, rTh },
norton: { iN: vTh / rTh, rN: rTh },
totalCurrent: iTotal,
totalPower: pTotal,
equations: {
kvl: [
`KVL: Vs - V_R1 - Vout = 0`,
`Vout appears across R2 || R3`,
`Loading effect: ${loadingError.toFixed(2)}% error`
],
kcl: [
`KCL at Vout: I_R1 = I_R2 + I_R3`,
`${formatValue(i1, "A")} = ${formatValue(i2, "A")} + ${formatValue(i3, "A")}`,
`Verified: ${formatValue(i2 + i3, "A")}`
],
nodal: [
`Vout/R2 + Vout/R3 = (Vs - Vout)/R1`,
`Vout * (1/R2 + 1/R3 + 1/R1) = Vs/R1`,
`R_parallel = R2||R3 = ${formatValue(rParallel, "Ohm")}`,
`Vout = Vs * R_par/(R1 + R_par)`,
`Vout = ${vOut.toFixed(4)}V`
]
}
};
}
function analyzeCircuit() {
const preset = presets[state.preset];
switch (preset.topology) {
case "voltage_divider": return analyzeVoltageDivider();
case "current_divider": return analyzeCurrentDivider();
case "wheatstone": return analyzeWheatstone();
case "sensor_interface": return analyzeSensorInterface();
default: return analyzeVoltageDivider();
}
}
// =========================================================================
// DRAWING FUNCTIONS
// =========================================================================
function drawVoltageDivider() {
diagramSvg.selectAll("*").remove();
const analysis = analyzeCircuit();
const cx = 200, cy = 175;
// Background
diagramSvg.append("rect")
.attr("width", 400)
.attr("height", 350)
.attr("fill", "#fafafa");
// Voltage source (left side)
diagramSvg.append("circle")
.attr("cx", cx - 100)
.attr("cy", cy)
.attr("r", 25)
.attr("fill", "none")
.attr("stroke", colors.red)
.attr("stroke-width", 3);
diagramSvg.append("text")
.attr("x", cx - 100)
.attr("y", cy + 5)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", colors.red)
.text(`${state.vs}V`);
// + and - symbols
diagramSvg.append("text")
.attr("x", cx - 100)
.attr("y", cy - 35)
.attr("text-anchor", "middle")
.attr("font-size", "18px")
.attr("fill", colors.red)
.text("+");
diagramSvg.append("text")
.attr("x", cx - 100)
.attr("y", cy + 50)
.attr("text-anchor", "middle")
.attr("font-size", "18px")
.attr("fill", colors.red)
.text("-");
// Wires
// Top wire from source to R1
diagramSvg.append("line")
.attr("x1", cx - 100).attr("y1", cy - 25)
.attr("x2", cx - 100).attr("y2", cy - 80)
.attr("stroke", colors.navy).attr("stroke-width", 2);
diagramSvg.append("line")
.attr("x1", cx - 100).attr("y1", cy - 80)
.attr("x2", cx + 50).attr("y2", cy - 80)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// R1 (resistor symbol - zigzag)
drawResistor(diagramSvg, cx + 50, cy - 80, cx + 50, cy - 20, "R1", state.r1, colors.teal);
// Middle wire (junction)
diagramSvg.append("line")
.attr("x1", cx + 50).attr("y1", cy - 20)
.attr("x2", cx + 50).attr("y2", cy + 20)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// Junction node
diagramSvg.append("circle")
.attr("cx", cx + 50)
.attr("cy", cy)
.attr("r", 5)
.attr("fill", colors.orange);
// Output label
diagramSvg.append("text")
.attr("x", cx + 90)
.attr("y", cy + 5)
.attr("font-size", "12px")
.attr("fill", colors.orange)
.attr("font-weight", "bold")
.text(`Vout = ${analysis.nodes[1].voltage.toFixed(3)}V`);
// R2
drawResistor(diagramSvg, cx + 50, cy + 20, cx + 50, cy + 80, "R2", state.r2, colors.purple);
// Bottom wire to ground
diagramSvg.append("line")
.attr("x1", cx + 50).attr("y1", cy + 80)
.attr("x2", cx + 50).attr("y2", cy + 120)
.attr("stroke", colors.navy).attr("stroke-width", 2);
diagramSvg.append("line")
.attr("x1", cx - 100).attr("y1", cy + 120)
.attr("x2", cx + 50).attr("y2", cy + 120)
.attr("stroke", colors.navy).attr("stroke-width", 2);
diagramSvg.append("line")
.attr("x1", cx - 100).attr("y1", cy + 25)
.attr("x2", cx - 100).attr("y2", cy + 120)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// Ground symbol
drawGround(diagramSvg, cx - 25, cy + 120);
// Current arrow
drawCurrentArrow(diagramSvg, cx + 70, cy - 50, "down", analysis.branches[0].current);
}
function drawCurrentDivider() {
diagramSvg.selectAll("*").remove();
const analysis = analyzeCircuit();
const cx = 200, cy = 175;
// Background
diagramSvg.append("rect")
.attr("width", 400)
.attr("height", 350)
.attr("fill", "#fafafa");
// Voltage source
diagramSvg.append("circle")
.attr("cx", cx - 120)
.attr("cy", cy)
.attr("r", 25)
.attr("fill", "none")
.attr("stroke", colors.red)
.attr("stroke-width", 3);
diagramSvg.append("text")
.attr("x", cx - 120)
.attr("y", cy + 5)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", colors.red)
.text(`${state.vs}V`);
// Wires - top rail
diagramSvg.append("line")
.attr("x1", cx - 120).attr("y1", cy - 25)
.attr("x2", cx - 120).attr("y2", cy - 60)
.attr("stroke", colors.navy).attr("stroke-width", 2);
diagramSvg.append("line")
.attr("x1", cx - 120).attr("y1", cy - 60)
.attr("x2", cx + 120).attr("y2", cy - 60)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// R1 (vertical)
drawResistor(diagramSvg, cx - 30, cy - 60, cx - 30, cy + 60, "R1", state.r1, colors.teal);
// R2 (vertical)
drawResistor(diagramSvg, cx + 70, cy - 60, cx + 70, cy + 60, "R2", state.r2, colors.purple);
// Bottom rail
diagramSvg.append("line")
.attr("x1", cx - 120).attr("y1", cy + 60)
.attr("x2", cx + 120).attr("y2", cy + 60)
.attr("stroke", colors.navy).attr("stroke-width", 2);
diagramSvg.append("line")
.attr("x1", cx - 120).attr("y1", cy + 25)
.attr("x2", cx - 120).attr("y2", cy + 60)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// Ground
drawGround(diagramSvg, cx, cy + 60);
// Node markers
diagramSvg.append("circle")
.attr("cx", cx - 30).attr("cy", cy - 60)
.attr("r", 4).attr("fill", colors.orange);
diagramSvg.append("circle")
.attr("cx", cx + 70).attr("cy", cy - 60)
.attr("r", 4).attr("fill", colors.orange);
// Current arrows
drawCurrentArrow(diagramSvg, cx - 50, cy, "down", analysis.branches[0].current);
drawCurrentArrow(diagramSvg, cx + 50, cy, "down", analysis.branches[1].current);
}
function drawWheatstone() {
diagramSvg.selectAll("*").remove();
const analysis = analyzeCircuit();
const cx = 200, cy = 175;
// Background
diagramSvg.append("rect")
.attr("width", 400)
.attr("height", 350)
.attr("fill", "#fafafa");
// Voltage source at top
diagramSvg.append("circle")
.attr("cx", cx)
.attr("cy", cy - 100)
.attr("r", 20)
.attr("fill", "none")
.attr("stroke", colors.red)
.attr("stroke-width", 3);
diagramSvg.append("text")
.attr("x", cx)
.attr("y", cy - 95)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("fill", colors.red)
.text(`${state.vs}V`);
// Diamond shape for bridge
const bridgeSize = 80;
// Top to left (R1)
diagramSvg.append("line")
.attr("x1", cx).attr("y1", cy - 80)
.attr("x2", cx - bridgeSize).attr("y2", cy)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// Top to right (R2)
diagramSvg.append("line")
.attr("x1", cx).attr("y1", cy - 80)
.attr("x2", cx + bridgeSize).attr("y2", cy)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// Left to bottom (R3)
diagramSvg.append("line")
.attr("x1", cx - bridgeSize).attr("y1", cy)
.attr("x2", cx).attr("y2", cy + 80)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// Right to bottom (R4)
diagramSvg.append("line")
.attr("x1", cx + bridgeSize).attr("y1", cy)
.attr("x2", cx).attr("y2", cy + 80)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// Resistor labels
const labelOffset = 15;
// R1 (top-left)
diagramSvg.append("rect")
.attr("x", cx - bridgeSize/2 - 25).attr("y", cy - 50)
.attr("width", 50).attr("height", 20)
.attr("fill", colors.teal).attr("rx", 3);
diagramSvg.append("text")
.attr("x", cx - bridgeSize/2).attr("y", cy - 35)
.attr("text-anchor", "middle")
.attr("font-size", "11px").attr("fill", colors.white)
.text(`R1:${formatValue(state.r1, "")}`);
// R2 (top-right)
diagramSvg.append("rect")
.attr("x", cx + bridgeSize/2 - 25).attr("y", cy - 50)
.attr("width", 50).attr("height", 20)
.attr("fill", colors.purple).attr("rx", 3);
diagramSvg.append("text")
.attr("x", cx + bridgeSize/2).attr("y", cy - 35)
.attr("text-anchor", "middle")
.attr("font-size", "11px").attr("fill", colors.white)
.text(`R2:${formatValue(state.r2, "")}`);
// R3 (bottom-left)
diagramSvg.append("rect")
.attr("x", cx - bridgeSize/2 - 25).attr("y", cy + 30)
.attr("width", 50).attr("height", 20)
.attr("fill", colors.green).attr("rx", 3);
diagramSvg.append("text")
.attr("x", cx - bridgeSize/2).attr("y", cy + 45)
.attr("text-anchor", "middle")
.attr("font-size", "11px").attr("fill", colors.white)
.text(`R3:${formatValue(state.r3, "")}`);
// R4 (bottom-right)
diagramSvg.append("rect")
.attr("x", cx + bridgeSize/2 - 25).attr("y", cy + 30)
.attr("width", 50).attr("height", 20)
.attr("fill", colors.orange).attr("rx", 3);
diagramSvg.append("text")
.attr("x", cx + bridgeSize/2).attr("y", cy + 45)
.attr("text-anchor", "middle")
.attr("font-size", "11px").attr("fill", colors.white)
.text(`R4:${formatValue(state.r4, "")}`);
// Node markers
// VA (left)
diagramSvg.append("circle")
.attr("cx", cx - bridgeSize).attr("cy", cy)
.attr("r", 6).attr("fill", colors.red);
diagramSvg.append("text")
.attr("x", cx - bridgeSize - 15).attr("y", cy + 5)
.attr("text-anchor", "end")
.attr("font-size", "11px").attr("fill", colors.red)
.text(`VA=${analysis.nodes[1].voltage.toFixed(3)}V`);
// VB (right)
diagramSvg.append("circle")
.attr("cx", cx + bridgeSize).attr("cy", cy)
.attr("r", 6).attr("fill", colors.red);
diagramSvg.append("text")
.attr("x", cx + bridgeSize + 15).attr("y", cy + 5)
.attr("text-anchor", "start")
.attr("font-size", "11px").attr("fill", colors.red)
.text(`VB=${analysis.nodes[2].voltage.toFixed(3)}V`);
// Bridge output line (dashed)
diagramSvg.append("line")
.attr("x1", cx - bridgeSize).attr("y1", cy)
.attr("x2", cx + bridgeSize).attr("y2", cy)
.attr("stroke", colors.orange)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5");
// Bridge voltage label
diagramSvg.append("text")
.attr("x", cx).attr("y", cy - 10)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("fill", colors.orange)
.text(`Vbridge = ${analysis.bridgeVoltage.toFixed(4)}V`);
// Balance indicator
diagramSvg.append("text")
.attr("x", cx).attr("y", cy + 130)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("fill", analysis.isBalanced ? colors.green : colors.red)
.text(analysis.isBalanced ? "BALANCED" : "UNBALANCED");
// Ground
diagramSvg.append("line")
.attr("x1", cx).attr("y1", cy + 80)
.attr("x2", cx).attr("y2", cy + 110)
.attr("stroke", colors.navy).attr("stroke-width", 2);
drawGround(diagramSvg, cx, cy + 110);
}
function drawSensorInterface() {
diagramSvg.selectAll("*").remove();
const analysis = analyzeCircuit();
const cx = 200, cy = 175;
// Background
diagramSvg.append("rect")
.attr("width", 400)
.attr("height", 350)
.attr("fill", "#fafafa");
// Voltage source
diagramSvg.append("circle")
.attr("cx", cx - 120)
.attr("cy", cy)
.attr("r", 25)
.attr("fill", "none")
.attr("stroke", colors.red)
.attr("stroke-width", 3);
diagramSvg.append("text")
.attr("x", cx - 120)
.attr("y", cy + 5)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", colors.red)
.text(`${state.vs}V`);
// Top wire
diagramSvg.append("line")
.attr("x1", cx - 120).attr("y1", cy - 25)
.attr("x2", cx - 120).attr("y2", cy - 80)
.attr("stroke", colors.navy).attr("stroke-width", 2);
diagramSvg.append("line")
.attr("x1", cx - 120).attr("y1", cy - 80)
.attr("x2", cx).attr("y2", cy - 80)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// R1 (Reference resistor)
drawResistor(diagramSvg, cx, cy - 80, cx, cy - 20, "R1 (Ref)", state.r1, colors.teal);
// Junction
diagramSvg.append("line")
.attr("x1", cx).attr("y1", cy - 20)
.attr("x2", cx).attr("y2", cy + 20)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// Horizontal to R3
diagramSvg.append("line")
.attr("x1", cx).attr("y1", cy)
.attr("x2", cx + 80).attr("y2", cy)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// Junction node
diagramSvg.append("circle")
.attr("cx", cx).attr("cy", cy)
.attr("r", 5)
.attr("fill", colors.orange);
// Output label
diagramSvg.append("text")
.attr("x", cx + 30)
.attr("y", cy - 10)
.attr("font-size", "11px")
.attr("fill", colors.orange)
.attr("font-weight", "bold")
.text(`Vout = ${analysis.nodes[1].voltage.toFixed(3)}V`);
// R2 (Sensor)
drawResistor(diagramSvg, cx, cy + 20, cx, cy + 80, "R2 (Sensor)", state.r2, colors.purple);
// R3 (Load)
drawResistor(diagramSvg, cx + 80, cy, cx + 80, cy + 80, "R3 (Load)", state.r3, colors.green);
// Bottom wire
diagramSvg.append("line")
.attr("x1", cx).attr("y1", cy + 80)
.attr("x2", cx).attr("y2", cy + 110)
.attr("stroke", colors.navy).attr("stroke-width", 2);
diagramSvg.append("line")
.attr("x1", cx - 120).attr("y1", cy + 110)
.attr("x2", cx + 80).attr("y2", cy + 110)
.attr("stroke", colors.navy).attr("stroke-width", 2);
diagramSvg.append("line")
.attr("x1", cx - 120).attr("y1", cy + 25)
.attr("x2", cx - 120).attr("y2", cy + 110)
.attr("stroke", colors.navy).attr("stroke-width", 2);
diagramSvg.append("line")
.attr("x1", cx + 80).attr("y1", cy + 80)
.attr("x2", cx + 80).attr("y2", cy + 110)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// Ground
drawGround(diagramSvg, cx - 60, cy + 110);
// Loading error indicator
if (analysis.loadingError !== undefined) {
diagramSvg.append("text")
.attr("x", cx + 120)
.attr("y", cy + 40)
.attr("font-size", "10px")
.attr("fill", analysis.loadingError > 5 ? colors.red : colors.green)
.text(`Loading: ${analysis.loadingError.toFixed(1)}%`);
}
}
function drawResistor(svg, x1, y1, x2, y2, label, value, color) {
const isVertical = Math.abs(x2 - x1) < Math.abs(y2 - y1);
const length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
const zigzagWidth = 8;
const zigzagCount = 4;
const zigzagLength = length * 0.6;
const wireLength = (length - zigzagLength) / 2;
const midX = (x1 + x2) / 2;
const midY = (y1 + y2) / 2;
if (isVertical) {
const direction = y2 > y1 ? 1 : -1;
// Start wire
svg.append("line")
.attr("x1", x1).attr("y1", y1)
.attr("x2", x1).attr("y2", y1 + wireLength * direction)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// Zigzag
let pathD = `M ${x1} ${y1 + wireLength * direction}`;
for (let i = 0; i < zigzagCount * 2; i++) {
const yOffset = (i + 1) * (zigzagLength / (zigzagCount * 2)) * direction;
const xOffset = (i % 2 === 0 ? zigzagWidth : -zigzagWidth);
pathD += ` L ${x1 + xOffset} ${y1 + wireLength * direction + yOffset}`;
}
pathD += ` L ${x1} ${y1 + wireLength * direction + zigzagLength * direction}`;
svg.append("path")
.attr("d", pathD)
.attr("fill", "none")
.attr("stroke", color)
.attr("stroke-width", 2);
// End wire
svg.append("line")
.attr("x1", x1).attr("y1", y1 + wireLength * direction + zigzagLength * direction)
.attr("x2", x2).attr("y2", y2)
.attr("stroke", colors.navy).attr("stroke-width", 2);
// Label
svg.append("text")
.attr("x", midX + 25)
.attr("y", midY)
.attr("font-size", "10px")
.attr("fill", color)
.attr("font-weight", "bold")
.text(label);
svg.append("text")
.attr("x", midX + 25)
.attr("y", midY + 12)
.attr("font-size", "9px")
.attr("fill", colors.gray)
.text(formatValue(value, ""));
}
}
function drawGround(svg, x, y) {
svg.append("line")
.attr("x1", x - 15).attr("y1", y)
.attr("x2", x + 15).attr("y2", y)
.attr("stroke", colors.navy).attr("stroke-width", 2);
svg.append("line")
.attr("x1", x - 10).attr("y1", y + 5)
.attr("x2", x + 10).attr("y2", y + 5)
.attr("stroke", colors.navy).attr("stroke-width", 2);
svg.append("line")
.attr("x1", x - 5).attr("y1", y + 10)
.attr("x2", x + 5).attr("y2", y + 10)
.attr("stroke", colors.navy).attr("stroke-width", 2);
}
function drawCurrentArrow(svg, x, y, direction, current) {
const arrowSize = 8;
if (direction === "down") {
svg.append("line")
.attr("x1", x).attr("y1", y - 15)
.attr("x2", x).attr("y2", y + 15)
.attr("stroke", colors.orange)
.attr("stroke-width", 2);
svg.append("polygon")
.attr("points", `${x},${y + 15} ${x - arrowSize/2},${y + 8} ${x + arrowSize/2},${y + 8}`)
.attr("fill", colors.orange);
svg.append("text")
.attr("x", x + 10)
.attr("y", y + 5)
.attr("font-size", "9px")
.attr("fill", colors.orange)
.text(formatValue(current, "A"));
}
}
// =========================================================================
// UPDATE FUNCTIONS
// =========================================================================
function updateResults(analysis) {
resultsContent.html("");
// Node voltages
const nodeSection = resultsContent.append("div")
.style("margin-bottom", "15px");
nodeSection.append("div")
.style("font-weight", "bold")
.style("color", colors.teal)
.style("margin-bottom", "8px")
.text("Node Voltages");
analysis.nodes.forEach(node => {
const row = nodeSection.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("padding", "4px 8px")
.style("background", colors.lightGray)
.style("margin-bottom", "2px")
.style("border-radius", "4px");
row.append("span")
.style("font-size", "12px")
.text(node.name);
row.append("span")
.style("font-size", "12px")
.style("font-weight", "bold")
.style("color", colors.navy)
.text(`${node.voltage.toFixed(4)} V`);
});
// Branch currents and power
const branchSection = resultsContent.append("div")
.style("margin-bottom", "15px");
branchSection.append("div")
.style("font-weight", "bold")
.style("color", colors.orange)
.style("margin-bottom", "8px")
.text("Branch Analysis");
analysis.branches.forEach(branch => {
const card = branchSection.append("div")
.style("background", colors.white)
.style("padding", "8px")
.style("margin-bottom", "5px")
.style("border-radius", "4px")
.style("border-left", `3px solid ${colors.purple}`);
card.append("div")
.style("font-weight", "bold")
.style("font-size", "12px")
.style("color", colors.navy)
.text(branch.name);
const grid = card.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "4px")
.style("margin-top", "4px");
grid.append("span").style("font-size", "11px").style("color", colors.gray)
.text(`I: ${formatValue(branch.current, "A")}`);
grid.append("span").style("font-size", "11px").style("color", colors.gray)
.text(`V: ${branch.voltage.toFixed(4)} V`);
grid.append("span").style("font-size", "11px").style("color", colors.gray)
.text(`P: ${formatValue(branch.power, "W")}`);
grid.append("span").style("font-size", "11px").style("color", colors.gray)
.text(`R: ${formatValue(branch.resistance, "Ohm")}`);
});
// Totals
const totalsSection = resultsContent.append("div")
.style("background", colors.navy)
.style("color", colors.white)
.style("padding", "10px")
.style("border-radius", "4px");
totalsSection.append("div")
.style("font-size", "12px")
.text(`Total Current: ${formatValue(analysis.totalCurrent, "A")}`);
totalsSection.append("div")
.style("font-size", "12px")
.text(`Total Power: ${formatValue(analysis.totalPower, "W")}`);
}
function updateEquations(analysis) {
equationsContent.html("");
// KVL/KCL column
const kvlKclCol = equationsContent.append("div");
kvlKclCol.append("div")
.style("font-weight", "bold")
.style("color", colors.teal)
.style("margin-bottom", "10px")
.text("KVL / KCL Equations");
const kvlBox = kvlKclCol.append("div")
.style("background", "#e8f4f8")
.style("padding", "10px")
.style("border-radius", "6px")
.style("margin-bottom", "10px");
kvlBox.append("div")
.style("font-weight", "bold")
.style("font-size", "12px")
.style("color", colors.navy)
.style("margin-bottom", "5px")
.text("KVL (Kirchhoff's Voltage Law)");
analysis.equations.kvl.forEach(eq => {
kvlBox.append("div")
.style("font-size", "11px")
.style("font-family", "monospace")
.style("margin-bottom", "3px")
.text(eq);
});
const kclBox = kvlKclCol.append("div")
.style("background", "#f4e8f8")
.style("padding", "10px")
.style("border-radius", "6px");
kclBox.append("div")
.style("font-weight", "bold")
.style("font-size", "12px")
.style("color", colors.navy)
.style("margin-bottom", "5px")
.text("KCL (Kirchhoff's Current Law)");
analysis.equations.kcl.forEach(eq => {
kclBox.append("div")
.style("font-size", "11px")
.style("font-family", "monospace")
.style("margin-bottom", "3px")
.text(eq);
});
// Nodal analysis column
const nodalCol = equationsContent.append("div");
nodalCol.append("div")
.style("font-weight", "bold")
.style("color", colors.orange)
.style("margin-bottom", "10px")
.text("Step-by-Step Nodal Analysis");
const nodalBox = nodalCol.append("div")
.style("background", "#fff8e8")
.style("padding", "10px")
.style("border-radius", "6px");
analysis.equations.nodal.forEach((step, i) => {
const stepDiv = nodalBox.append("div")
.style("margin-bottom", "8px")
.style("padding-left", "10px")
.style("border-left", `2px solid ${colors.orange}`);
stepDiv.append("span")
.style("font-size", "10px")
.style("color", colors.gray)
.text(`Step ${i + 1}: `);
stepDiv.append("span")
.style("font-size", "11px")
.style("font-family", "monospace")
.text(step);
});
}
function updateThevenin(analysis) {
theveninContent.html("");
// Thevenin equivalent
const thCard = theveninContent.append("div")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px")
.style("border-left", `4px solid ${colors.teal}`);
thCard.append("div")
.style("font-weight", "bold")
.style("color", colors.teal)
.style("margin-bottom", "10px")
.text("Thevenin Equivalent");
thCard.append("div")
.style("font-size", "20px")
.style("font-weight", "bold")
.style("color", colors.navy)
.text(`Vth = ${analysis.thevenin.vTh.toFixed(4)} V`);
thCard.append("div")
.style("font-size", "16px")
.style("color", colors.gray)
.style("margin-top", "5px")
.text(`Rth = ${formatValue(analysis.thevenin.rTh, "Ohm")}`);
// Norton equivalent
const nCard = theveninContent.append("div")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px")
.style("border-left", `4px solid ${colors.orange}`);
nCard.append("div")
.style("font-weight", "bold")
.style("color", colors.orange)
.style("margin-bottom", "10px")
.text("Norton Equivalent");
nCard.append("div")
.style("font-size", "20px")
.style("font-weight", "bold")
.style("color", colors.navy)
.text(`In = ${formatValue(analysis.norton.iN, "A")}`);
nCard.append("div")
.style("font-size", "16px")
.style("color", colors.gray)
.style("margin-top", "5px")
.text(`Rn = ${formatValue(analysis.norton.rN, "Ohm")}`);
// Relationship
const relCard = theveninContent.append("div")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px")
.style("border-left", `4px solid ${colors.purple}`);
relCard.append("div")
.style("font-weight", "bold")
.style("color", colors.purple)
.style("margin-bottom", "10px")
.text("Relationship");
relCard.append("div")
.style("font-size", "12px")
.style("font-family", "monospace")
.style("color", colors.navy)
.html(`Vth = In x Rth<br>Rth = Rn<br>${analysis.thevenin.vTh.toFixed(4)} = ${formatValue(analysis.norton.iN, "A")} x ${formatValue(analysis.thevenin.rTh, "Ohm")}`);
}
function drawCircuit() {
const preset = presets[state.preset];
switch (preset.topology) {
case "voltage_divider":
drawVoltageDivider();
break;
case "current_divider":
drawCurrentDivider();
break;
case "wheatstone":
drawWheatstone();
break;
case "sensor_interface":
drawSensorInterface();
break;
}
}
function updateAll() {
const analysis = analyzeCircuit();
drawCircuit();
updateResults(analysis);
updateEquations(analysis);
updateThevenin(analysis);
}
// Initial load
loadPreset(state.preset);
updateAll();
return container.node();
}599.2 Understanding Circuit Analysis
Circuit analysis is fundamental to designing IoT sensor interfaces. This section covers the key concepts visualized in the tool above.
599.2.1 Kirchhoff’s Laws
KVL (Kirchhoff’s Voltage Law): The sum of all voltage drops around any closed loop equals zero.
\[\sum V = 0\]
KCL (Kirchhoff’s Current Law): The sum of all currents entering a node equals the sum of all currents leaving.
\[\sum I_{in} = \sum I_{out}\]
599.2.2 Nodal Analysis Method
Nodal analysis is a systematic approach to solve circuits:
- Identify nodes: Find all connection points in the circuit
- Choose reference: Select ground node (0V reference)
- Apply KCL: Write current equations at each node
- Solve: Calculate unknown node voltages
- Find currents: Use Ohm’s law to find branch currents
599.2.3 Common Circuit Configurations
| Circuit | Application | Key Formula |
|---|---|---|
| Voltage Divider | Level shifting, sensor biasing | Vout = Vs x R2/(R1+R2) |
| Current Divider | Current sensing, parallel loads | I1 = I x R2/(R1+R2) |
| Wheatstone Bridge | Precision sensing, strain gauges | Vbridge = Vs(R3/(R1+R3) - R4/(R2+R4)) |
| Sensor Interface | Signal conditioning | Depends on load |
599.3 Thevenin and Norton Equivalents
Any linear circuit with voltage and current sources can be simplified to an equivalent circuit.
599.3.1 Thevenin Equivalent
A voltage source (Vth) in series with a resistance (Rth):
\[V_{th} = V_{open-circuit}\] \[R_{th} = V_{oc} / I_{sc}\]
599.3.2 Norton Equivalent
A current source (In) in parallel with a resistance (Rn):
\[I_n = I_{short-circuit}\] \[R_n = R_{th}\]
599.3.3 When to Use Each
- Thevenin: When connecting to high-impedance loads (voltage-driven circuits)
- Norton: When connecting to low-impedance loads (current-driven circuits)
- Both are equivalent: Related by \(V_{th} = I_n \times R_{th}\)
599.4 Practical Applications in IoT
599.4.1 Voltage Divider for Sensors
Many IoT sensors output voltages above the ADC range. A voltage divider scales the signal:
Example: 12V battery monitoring with 3.3V ADC
R1 = 30k, R2 = 10k
Vout = 12V x 10k/(30k+10k) = 3V (within ADC range)
599.4.2 Wheatstone Bridge for Precision Sensing
Wheatstone bridges are used for: - Strain gauges: Detect tiny resistance changes - Temperature sensors: RTD and thermistor measurements - Pressure sensors: Piezoresistive elements
599.4.3 Loading Effects
When connecting a sensor output to a load (ADC, amplifier):
- High input impedance: Use ADC/amplifier with Rin >> source impedance
- Buffer amplifier: Add voltage follower (unity gain buffer)
- Calculate error: Loading error = (Vunloaded - Vloaded) / Vunloaded x 100%
- Rule of thumb: Load impedance should be > 10x source impedance
599.5 What’s Next
- RC Filter Designer - Design signal conditioning filters
- Sensor Calibration Tool - Calibrate your sensors
- ADC Sampling Animation - Understand analog-to-digital conversion
- Sensor Interfacing - Complete signal chains
- Simulations Hub - More interactive tools
This interactive tool is implemented in approximately 1,200 lines of Observable JavaScript:
Key Features:
- Circuit presets: Voltage divider, current divider, Wheatstone bridge, sensor interface
- Complete analysis: Node voltages, branch currents, power dissipation
- Step-by-step solutions: KVL, KCL, and nodal analysis equations
- Thevenin/Norton: Automatic equivalent circuit calculation
- Visual diagrams: Dynamic circuit schematic with component values
IEEE Color Palette: - Navy (#2C3E50): Primary text, wires - Teal (#16A085): R1, Thevenin results - Orange (#E67E22): Output markers, Norton results - Purple (#9B59B6): R2, branch analysis - Green (#27AE60): R3, balance indicators - Red (#E74C3C): Voltage sources, warnings - Gray (#7F8C8D): Labels, secondary text
Supported Topologies: - Series resistors (voltage divider) - Parallel resistors (current divider) - Bridge circuits (Wheatstone) - Mixed series-parallel (sensor interface)
Analysis Methods: - Ohm’s Law: V = IR - KVL: Sum of voltages = 0 - KCL: Sum of currents at node = 0 - Voltage divider: Vout = Vs x R2/(R1+R2) - Current divider: I1 = I x R2/(R1+R2) - Thevenin: Vth = Voc, Rth = Voc/Isc