// ============================================
// Blockchain Transaction Visualizer
// Interactive Tool for IoT Education
// ============================================
{
// 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",
blue: "#3498DB",
darkBlue: "#1A5276",
darkGreen: "#1D8348"
};
// Consensus mechanism configurations
const consensusMechanisms = {
pow: {
name: "Proof of Work (PoW)",
shortName: "PoW",
description: "Miners compete to solve cryptographic puzzles. First to solve creates the block.",
color: colors.orange,
energyCost: "Very High",
confirmationTime: "10-60 min",
throughput: "3-7 TPS",
decentralization: "High",
iotSuitability: "Low",
iotReason: "High energy consumption unsuitable for battery-powered IoT devices"
},
pos: {
name: "Proof of Stake (PoS)",
shortName: "PoS",
description: "Validators are selected based on their stake. Energy-efficient alternative to PoW.",
color: colors.teal,
energyCost: "Low",
confirmationTime: "12-32 sec",
throughput: "15-100 TPS",
decentralization: "Medium-High",
iotSuitability: "Medium",
iotReason: "Lower energy, but stake requirements may exclude small devices"
},
pbft: {
name: "Practical Byzantine Fault Tolerance",
shortName: "PBFT",
description: "Nodes vote on proposed blocks. Tolerates up to 1/3 faulty nodes.",
color: colors.purple,
energyCost: "Very Low",
confirmationTime: "1-5 sec",
throughput: "1000+ TPS",
decentralization: "Low-Medium",
iotSuitability: "High",
iotReason: "Fast finality and low energy, ideal for private IoT networks"
},
raft: {
name: "Raft Consensus",
shortName: "Raft",
description: "Leader-based consensus. Leader proposes blocks, followers vote.",
color: colors.blue,
energyCost: "Very Low",
confirmationTime: "< 1 sec",
throughput: "10000+ TPS",
decentralization: "Low",
iotSuitability: "Very High",
iotReason: "Very fast, efficient for trusted private IoT networks"
}
};
// Network nodes configuration - 7 nodes in circular arrangement
const centerX = 300;
const centerY = 200;
const radius = 140;
const networkNodes = [];
for (let i = 0; i < 7; i++) {
const angle = (i * 2 * Math.PI / 7) - Math.PI / 2;
networkNodes.push({
id: i,
label: `N${i + 1}`,
x: centerX + radius * Math.cos(angle),
y: centerY + radius * Math.sin(angle),
isLeader: false,
stake: [1000, 2500, 1800, 3200, 900, 1500, 2100][i],
hashPower: [15, 25, 18, 32, 10, 20, 22][i],
state: "idle" // idle, preparing, voting, committed
});
}
// State management
let state = {
consensus: "pbft",
nodes: JSON.parse(JSON.stringify(networkNodes)),
blockchain: [],
pendingTransaction: null,
isAnimating: false,
selectedBlock: null,
transactionCount: 0,
animationPhase: "",
metrics: {
totalTransactions: 0,
avgConfirmationTime: 0,
energyUsed: 0
}
};
// Initialize genesis block
function initializeBlockchain() {
state.blockchain = [{
index: 0,
timestamp: new Date().toISOString(),
transactions: [],
previousHash: "0".repeat(64),
hash: generateHash("genesis"),
nonce: 0,
validator: "Genesis",
consensusType: "N/A"
}];
}
// Simple hash generator (for demonstration)
function generateHash(data) {
let hash = 0;
const str = JSON.stringify(data) + Date.now() + Math.random();
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16).padStart(64, '0').substring(0, 64);
}
// Create main container
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", "1000px")
.style("margin", "0 auto");
// Title
container.append("div")
.style("text-align", "center")
.style("margin-bottom", "15px")
.append("h3")
.style("color", colors.navy)
.style("margin", "0")
.text("Blockchain Transaction Visualizer");
// Control Panel
const controlPanel = container.append("div")
.style("background", colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px")
.style("margin-bottom", "15px");
// Consensus selector row
const consensusRow = controlPanel.append("div")
.style("display", "flex")
.style("gap", "15px")
.style("flex-wrap", "wrap")
.style("align-items", "center")
.style("margin-bottom", "15px");
consensusRow.append("label")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("font-size", "13px")
.text("Consensus Mechanism:");
const consensusSelect = consensusRow.append("select")
.style("padding", "8px 12px")
.style("border-radius", "6px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "13px")
.style("cursor", "pointer")
.style("min-width", "250px");
Object.entries(consensusMechanisms).forEach(([key, mech]) => {
consensusSelect.append("option")
.attr("value", key)
.attr("selected", key === state.consensus ? true : null)
.text(mech.name);
});
// Consensus info display
const consensusInfo = controlPanel.append("div")
.attr("class", "consensus-info")
.style("padding", "12px")
.style("background", colors.white)
.style("border-radius", "6px")
.style("margin-bottom", "15px")
.style("border-left", `4px solid ${consensusMechanisms.pbft.color}`);
function updateConsensusInfo() {
const mech = consensusMechanisms[state.consensus];
consensusInfo
.style("border-left-color", mech.color)
.html(`
<div style="font-weight: bold; color: ${mech.color}; margin-bottom: 5px;">${mech.name}</div>
<div style="font-size: 12px; color: ${colors.gray};">${mech.description}</div>
`);
}
// Transaction form
const txForm = controlPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr 2fr auto")
.style("gap", "10px")
.style("align-items", "end");
// Sender input
const senderGroup = txForm.append("div");
senderGroup.append("label")
.style("display", "block")
.style("font-size", "11px")
.style("color", colors.gray)
.style("margin-bottom", "4px")
.text("Sender");
const senderInput = senderGroup.append("input")
.attr("type", "text")
.attr("placeholder", "Device_001")
.attr("value", "Sensor_A")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "12px")
.style("box-sizing", "border-box");
// Receiver input
const receiverGroup = txForm.append("div");
receiverGroup.append("label")
.style("display", "block")
.style("font-size", "11px")
.style("color", colors.gray)
.style("margin-bottom", "4px")
.text("Receiver");
const receiverInput = receiverGroup.append("input")
.attr("type", "text")
.attr("placeholder", "Gateway_001")
.attr("value", "Gateway_B")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "12px")
.style("box-sizing", "border-box");
// Data payload input
const dataGroup = txForm.append("div");
dataGroup.append("label")
.style("display", "block")
.style("font-size", "11px")
.style("color", colors.gray)
.style("margin-bottom", "4px")
.text("Data Payload");
const dataInput = dataGroup.append("input")
.attr("type", "text")
.attr("placeholder", "temperature: 23.5C")
.attr("value", "temperature: 24.5C, humidity: 65%")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "4px")
.style("border", `1px solid ${colors.gray}`)
.style("font-size", "12px")
.style("box-sizing", "border-box");
// Submit button
const submitBtn = txForm.append("button")
.text("Submit Transaction")
.style("padding", "10px 20px")
.style("background", colors.teal)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "6px")
.style("cursor", "pointer")
.style("font-weight", "bold")
.style("font-size", "12px")
.style("white-space", "nowrap")
.style("min-height", "44px");
// Main visualization area
const mainArea = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "420px 1fr")
.style("gap", "15px")
.style("margin-bottom", "15px");
// Network visualization
const networkPanel = mainArea.append("div")
.style("background", colors.white)
.style("border", `2px solid ${colors.gray}`)
.style("border-radius", "8px")
.style("padding", "10px");
networkPanel.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("font-size", "13px")
.style("margin-bottom", "10px")
.text("Network Nodes");
const networkSvg = networkPanel.append("svg")
.attr("viewBox", "0 0 600 420")
.attr("width", "100%")
.style("min-height", "380px");
// Definitions for SVG
const defs = networkSvg.append("defs");
// Glow filter for active nodes
const glowFilter = defs.append("filter")
.attr("id", "glow")
.attr("x", "-50%")
.attr("y", "-50%")
.attr("width", "200%")
.attr("height", "200%");
glowFilter.append("feGaussianBlur")
.attr("stdDeviation", "4")
.attr("result", "coloredBlur");
const feMerge = glowFilter.append("feMerge");
feMerge.append("feMergeNode").attr("in", "coloredBlur");
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
// Pulse animation
const pulseFilter = defs.append("filter")
.attr("id", "pulse");
pulseFilter.append("feGaussianBlur")
.attr("stdDeviation", "2")
.attr("result", "blur");
// Arrow marker for directed links
defs.append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "-0 -5 10 10")
.attr("refX", 8)
.attr("refY", 0)
.attr("orient", "auto")
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.append("path")
.attr("d", "M 0,-5 L 10,0 L 0,5")
.attr("fill", colors.gray);
// Groups for layered rendering
const linkGroup = networkSvg.append("g").attr("class", "links");
const packetGroup = networkSvg.append("g").attr("class", "packets");
const nodeGroup = networkSvg.append("g").attr("class", "nodes");
const labelGroup = networkSvg.append("g").attr("class", "labels");
// Status display area
const statusArea = mainArea.append("div")
.style("display", "flex")
.style("flex-direction", "column")
.style("gap", "15px");
// Animation status
const statusPanel = statusArea.append("div")
.style("background", colors.navy)
.style("padding", "15px")
.style("border-radius", "8px")
.style("color", colors.white);
statusPanel.append("div")
.style("font-weight", "bold")
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("Consensus Status");
const statusDisplay = statusPanel.append("div")
.attr("class", "status-display")
.style("font-size", "12px")
.style("line-height", "1.6");
// Phase indicator
const phaseIndicator = statusPanel.append("div")
.style("margin-top", "12px")
.style("padding", "10px")
.style("background", "rgba(255,255,255,0.1)")
.style("border-radius", "6px")
.style("display", "none");
// Metrics display
const metricsPanel = statusArea.append("div")
.style("background", colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px");
metricsPanel.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("Performance Metrics");
const metricsGrid = metricsPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "8px");
function updateMetricsDisplay() {
const mech = consensusMechanisms[state.consensus];
metricsGrid.html(`
<div style="background: ${colors.white}; padding: 10px; border-radius: 6px; text-align: center;">
<div style="font-size: 10px; color: ${colors.gray};">Confirmation Time</div>
<div style="font-size: 16px; font-weight: bold; color: ${colors.teal};">${mech.confirmationTime}</div>
</div>
<div style="background: ${colors.white}; padding: 10px; border-radius: 6px; text-align: center;">
<div style="font-size: 10px; color: ${colors.gray};">Throughput</div>
<div style="font-size: 16px; font-weight: bold; color: ${colors.orange};">${mech.throughput}</div>
</div>
<div style="background: ${colors.white}; padding: 10px; border-radius: 6px; text-align: center;">
<div style="font-size: 10px; color: ${colors.gray};">Energy Cost</div>
<div style="font-size: 16px; font-weight: bold; color: ${mech.energyCost === 'Very High' ? colors.red : colors.green};">${mech.energyCost}</div>
</div>
<div style="background: ${colors.white}; padding: 10px; border-radius: 6px; text-align: center;">
<div style="font-size: 10px; color: ${colors.gray};">Decentralization</div>
<div style="font-size: 16px; font-weight: bold; color: ${colors.purple};">${mech.decentralization}</div>
</div>
`);
}
// Block explorer section
const explorerSection = container.append("div")
.style("background", colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px")
.style("margin-bottom", "15px");
explorerSection.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("font-size", "13px")
.style("margin-bottom", "10px")
.text("Block Explorer");
const blockchainContainer = explorerSection.append("div")
.style("display", "flex")
.style("gap", "10px")
.style("overflow-x", "auto")
.style("padding", "10px 0");
const blockDetailsPanel = explorerSection.append("div")
.attr("class", "block-details")
.style("margin-top", "15px")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "6px")
.style("display", "none");
// IoT considerations callout
const iotCallout = container.append("div")
.style("background", colors.white)
.style("border", `2px solid ${colors.teal}`)
.style("border-radius", "8px")
.style("padding", "15px");
iotCallout.append("div")
.style("font-weight", "bold")
.style("color", colors.teal)
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("IoT-Specific Considerations");
const iotContent = iotCallout.append("div")
.attr("class", "iot-content")
.style("font-size", "12px")
.style("line-height", "1.6");
function updateIoTContent() {
const mech = consensusMechanisms[state.consensus];
const suitabilityColor = mech.iotSuitability === 'Very High' ? colors.darkGreen :
(mech.iotSuitability === 'High' ? colors.green :
(mech.iotSuitability === 'Medium' ? colors.orange : colors.red));
iotContent.html(`
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
<div>
<div style="font-weight: bold; color: ${colors.navy}; margin-bottom: 8px;">IoT Suitability</div>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
<div style="padding: 6px 12px; background: ${suitabilityColor}; color: white; border-radius: 20px; font-weight: bold; font-size: 13px;">
${mech.iotSuitability}
</div>
</div>
<div style="color: ${colors.gray}; font-size: 11px;">
${mech.iotReason}
</div>
</div>
<div>
<div style="font-weight: bold; color: ${colors.navy}; margin-bottom: 8px;">Key Considerations</div>
<ul style="margin: 0; padding-left: 18px; color: ${colors.gray}; font-size: 11px;">
${state.consensus === 'pow' ? `
<li>Resource-constrained devices cannot mine</li>
<li>High latency unsuitable for real-time IoT</li>
<li>Consider lightweight alternatives</li>
` : ''}
${state.consensus === 'pos' ? `
<li>Lower energy consumption than PoW</li>
<li>Stake requirements may exclude small devices</li>
<li>Good for IoT gateways and edge nodes</li>
` : ''}
${state.consensus === 'pbft' ? `
<li>Fast finality ideal for IoT applications</li>
<li>Low computational overhead</li>
<li>Limited to smaller networks (typically <100 nodes)</li>
` : ''}
${state.consensus === 'raft' ? `
<li>Excellent for private IoT networks</li>
<li>Very low latency (<1 second)</li>
<li>Single leader can be bottleneck at scale</li>
` : ''}
</ul>
</div>
</div>
`);
}
// Render network nodes
function renderNetwork() {
linkGroup.selectAll("*").remove();
nodeGroup.selectAll("*").remove();
labelGroup.selectAll("*").remove();
// Draw connections between all nodes (mesh network)
for (let i = 0; i < state.nodes.length; i++) {
for (let j = i + 1; j < state.nodes.length; j++) {
const n1 = state.nodes[i];
const n2 = state.nodes[j];
linkGroup.append("line")
.attr("class", `link-${i}-${j}`)
.attr("x1", n1.x)
.attr("y1", n1.y)
.attr("x2", n2.x)
.attr("y2", n2.y)
.attr("stroke", colors.lightGray)
.attr("stroke-width", 1)
.style("opacity", 0.4);
}
}
// Draw nodes
state.nodes.forEach(node => {
const g = nodeGroup.append("g")
.attr("class", `node node-${node.id}`)
.attr("transform", `translate(${node.x}, ${node.y})`);
// Outer ring for state indication
g.append("circle")
.attr("class", "node-ring")
.attr("r", 38)
.attr("fill", "none")
.attr("stroke", "transparent")
.attr("stroke-width", 4);
// Node circle
const nodeColor = node.isLeader ? colors.orange :
(node.state === 'voting' ? colors.yellow :
(node.state === 'committed' ? colors.green : colors.navy));
g.append("circle")
.attr("class", "node-circle")
.attr("r", 32)
.attr("fill", nodeColor)
.attr("stroke", colors.white)
.attr("stroke-width", 3);
// Node label
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "-0.1em")
.attr("fill", colors.white)
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text(node.label);
// Stake/Power display based on consensus
if (state.consensus === 'pos') {
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "1.3em")
.attr("fill", colors.white)
.attr("font-size", "9px")
.text(`$${node.stake}`);
} else if (state.consensus === 'pow') {
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "1.3em")
.attr("fill", colors.white)
.attr("font-size", "9px")
.text(`${node.hashPower}H/s`);
}
// Leader/validator indicator
if (node.isLeader) {
g.append("text")
.attr("class", "leader-label")
.attr("y", -48)
.attr("text-anchor", "middle")
.attr("fill", colors.orange)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text(state.consensus === 'raft' ? "LEADER" :
(state.consensus === 'pos' ? "VALIDATOR" :
(state.consensus === 'pow' ? "MINER" : "PRIMARY")));
}
});
// Network info
labelGroup.append("text")
.attr("x", 300)
.attr("y", 400)
.attr("text-anchor", "middle")
.attr("fill", colors.gray)
.attr("font-size", "11px")
.text(`${state.nodes.length}-Node ${consensusMechanisms[state.consensus].shortName} Network`);
}
// Render blockchain
function renderBlockchain() {
blockchainContainer.selectAll("*").remove();
state.blockchain.forEach((block, index) => {
// Chain link arrow (before block, except for first)
if (index > 0) {
blockchainContainer.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("color", colors.teal)
.style("font-size", "24px")
.style("font-weight", "bold")
.html("→");
}
const blockDiv = blockchainContainer.append("div")
.style("min-width", "130px")
.style("background", index === 0 ? colors.gray : colors.navy)
.style("padding", "12px")
.style("border-radius", "8px")
.style("color", colors.white)
.style("cursor", "pointer")
.style("transition", "transform 0.2s, box-shadow 0.2s")
.style("flex-shrink", "0")
.style("border", `3px solid ${index === state.blockchain.length - 1 && index > 0 ? colors.teal : 'transparent'}`)
.on("click", () => showBlockDetails(block))
.on("mouseenter", function() {
d3.select(this)
.style("transform", "scale(1.05)")
.style("box-shadow", "0 4px 12px rgba(0,0,0,0.2)");
})
.on("mouseleave", function() {
d3.select(this)
.style("transform", "scale(1)")
.style("box-shadow", "none");
});
blockDiv.append("div")
.style("font-weight", "bold")
.style("font-size", "13px")
.style("margin-bottom", "6px")
.text(`Block #${block.index}`);
blockDiv.append("div")
.style("font-size", "9px")
.style("opacity", "0.8")
.style("word-break", "break-all")
.style("font-family", "monospace")
.text(`${block.hash.substring(0, 10)}...`);
blockDiv.append("div")
.style("font-size", "10px")
.style("margin-top", "6px")
.style("color", colors.lightGray)
.text(`${block.transactions.length} tx`);
if (index > 0) {
blockDiv.append("div")
.style("font-size", "9px")
.style("margin-top", "4px")
.style("color", colors.teal)
.text(block.validator);
}
});
}
// Show block details
function showBlockDetails(block) {
state.selectedBlock = block;
blockDetailsPanel
.style("display", "block")
.html(`
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<div>
<div style="font-weight: bold; color: ${colors.navy}; margin-bottom: 12px; font-size: 14px;">Block #${block.index}</div>
<div style="font-size: 11px; margin-bottom: 10px;">
<span style="color: ${colors.gray}; font-weight: bold;">Hash:</span><br>
<code style="font-size: 10px; word-break: break-all; background: ${colors.lightGray}; padding: 4px; border-radius: 3px; display: inline-block; margin-top: 4px;">${block.hash}</code>
</div>
<div style="font-size: 11px; margin-bottom: 10px;">
<span style="color: ${colors.gray}; font-weight: bold;">Previous Hash:</span><br>
<code style="font-size: 10px; word-break: break-all; background: ${colors.lightGray}; padding: 4px; border-radius: 3px; display: inline-block; margin-top: 4px;">${block.previousHash}</code>
</div>
<div style="font-size: 11px; margin-bottom: 8px;">
<span style="color: ${colors.gray}; font-weight: bold;">Timestamp:</span> ${new Date(block.timestamp).toLocaleString()}
</div>
<div style="font-size: 11px;">
<span style="color: ${colors.gray}; font-weight: bold;">Consensus:</span> ${block.consensusType || 'N/A'}
</div>
</div>
<div>
<div style="font-weight: bold; color: ${colors.navy}; margin-bottom: 12px; font-size: 14px;">Transactions (${block.transactions.length})</div>
${block.transactions.length === 0 ?
'<div style="color: ' + colors.gray + '; font-size: 11px; font-style: italic;">No transactions (Genesis block)</div>' :
block.transactions.map(tx => `
<div style="background: ${colors.lightGray}; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 11px;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<span style="font-weight: bold; color: ${colors.navy};">${tx.sender}</span>
<span style="color: ${colors.teal};">→</span>
<span style="font-weight: bold; color: ${colors.navy};">${tx.receiver}</span>
</div>
<div style="color: ${colors.gray}; font-size: 10px; background: ${colors.white}; padding: 6px; border-radius: 4px; font-family: monospace;">${tx.data}</div>
</div>
`).join('')
}
<div style="margin-top: 12px; font-size: 11px; padding: 8px; background: ${colors.lightGray}; border-radius: 4px;">
<span style="color: ${colors.gray}; font-weight: bold;">Validator:</span>
<span style="color: ${colors.teal}; font-weight: bold;">${block.validator}</span>
</div>
</div>
</div>
`);
}
// Update status display
function updateStatus(message, phase = "") {
const mech = consensusMechanisms[state.consensus];
state.animationPhase = phase;
statusDisplay.html(`
<div style="margin-bottom: 8px;">
<span style="color: ${colors.lightGray};">Phase:</span>
<span style="color: ${mech.color}; font-weight: bold;">${phase || "Idle"}</span>
</div>
<div style="color: ${colors.lightGray};">${message}</div>
`);
// Update phase indicator
if (phase && phase !== "Idle") {
phaseIndicator.style("display", "block");
const phases = getConsensusPhases();
const currentIndex = phases.indexOf(phase);
phaseIndicator.html(`
<div style="display: flex; justify-content: space-between; font-size: 10px; margin-bottom: 6px;">
${phases.map((p, i) => `
<span style="color: ${i <= currentIndex ? mech.color : 'rgba(255,255,255,0.3)'}; font-weight: ${i === currentIndex ? 'bold' : 'normal'};">
${p}
</span>
`).join('')}
</div>
<div style="height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden;">
<div style="width: ${((currentIndex + 1) / phases.length) * 100}%; height: 100%; background: ${mech.color}; transition: width 0.3s;"></div>
</div>
`);
} else {
phaseIndicator.style("display", "none");
}
}
function getConsensusPhases() {
switch (state.consensus) {
case 'pow': return ['Broadcast', 'Mining', 'Block Found', 'Propagation', 'Complete'];
case 'pos': return ['Broadcast', 'Selection', 'Validation', 'Propagation', 'Complete'];
case 'pbft': return ['Request', 'Pre-Prepare', 'Prepare', 'Commit', 'Complete'];
case 'raft': return ['Request', 'Leader Election', 'Replication', 'Commit', 'Complete'];
default: return [];
}
}
// Animate packet movement between nodes
function animatePacket(fromNode, toNode, color, label, duration = 300) {
return new Promise(resolve => {
const packet = packetGroup.append("g");
packet.append("circle")
.attr("r", 12)
.attr("fill", color)
.attr("stroke", colors.white)
.attr("stroke-width", 2);
packet.append("text")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.attr("fill", colors.white)
.attr("font-size", "8px")
.attr("font-weight", "bold")
.text(label);
packet
.attr("transform", `translate(${fromNode.x}, ${fromNode.y})`)
.transition()
.duration(duration)
.attr("transform", `translate(${toNode.x}, ${toNode.y})`)
.on("end", () => {
packet.remove();
resolve();
});
});
}
// Highlight node with animation
function highlightNode(nodeId, color, duration = 500) {
const nodeEl = nodeGroup.select(`.node-${nodeId}`);
nodeEl.select(".node-ring")
.attr("stroke", color)
.attr("stroke-width", 4)
.transition()
.duration(duration)
.attr("stroke", "transparent");
nodeEl.select(".node-circle")
.transition()
.duration(200)
.attr("filter", "url(#glow)")
.attr("fill", color)
.transition()
.duration(duration)
.attr("fill", state.nodes[nodeId].isLeader ? colors.orange : colors.navy)
.attr("filter", "none");
}
// Broadcast animation - send to multiple nodes simultaneously
async function broadcastToAll(fromNode, color, label) {
const promises = state.nodes
.filter(n => n.id !== fromNode.id)
.map(n => animatePacket(fromNode, n, color, label, 400));
await Promise.all(promises);
}
// Consensus animations
async function animatePoW(transaction) {
updateStatus("Broadcasting transaction to network...", "Broadcast");
// Broadcast from first node to all others
await broadcastToAll(state.nodes[0], colors.orange, "TX");
await new Promise(r => setTimeout(r, 200));
updateStatus("Miners competing to solve cryptographic puzzle...", "Mining");
// Simulate mining competition with visual feedback
const miningIntervals = state.nodes.map((node, i) => {
return new Promise(resolve => {
const baseTime = 600 + Math.random() * 1200;
const adjustedTime = baseTime * (100 / (node.hashPower + 50)); // Higher hash power = faster
let pulseCount = 0;
const pulseInterval = setInterval(() => {
highlightNode(i, colors.yellow, 200);
pulseCount++;
}, 200);
setTimeout(() => {
clearInterval(pulseInterval);
resolve({ node, time: adjustedTime });
}, adjustedTime);
});
});
const winner = await Promise.race(miningIntervals);
// Clear all mining states
state.nodes.forEach(n => n.isLeader = false);
state.nodes[winner.node.id].isLeader = true;
updateStatus(`${winner.node.label} found the solution first!`, "Block Found");
highlightNode(winner.node.id, colors.green, 800);
renderNetwork();
await new Promise(r => setTimeout(r, 600));
updateStatus("Broadcasting new block to all nodes...", "Propagation");
await broadcastToAll(winner.node, colors.green, "BLK");
return winner.node;
}
async function animatePoS(transaction) {
updateStatus("Broadcasting transaction to validator pool...", "Broadcast");
await broadcastToAll(state.nodes[0], colors.orange, "TX");
await new Promise(r => setTimeout(r, 300));
updateStatus("Selecting validator based on stake weight...", "Selection");
// Visual stake comparison
for (const node of state.nodes) {
highlightNode(node.id, colors.yellow, 300);
await new Promise(r => setTimeout(r, 100));
}
// Select validator based on stake (weighted random)
const totalStake = state.nodes.reduce((sum, n) => sum + n.stake, 0);
let random = Math.random() * totalStake;
let selectedValidator = state.nodes[0];
for (const node of state.nodes) {
random -= node.stake;
if (random <= 0) {
selectedValidator = node;
break;
}
}
state.nodes.forEach(n => n.isLeader = n.id === selectedValidator.id);
renderNetwork();
updateStatus(`${selectedValidator.label} selected (stake: $${selectedValidator.stake})`, "Validation");
highlightNode(selectedValidator.id, colors.teal, 600);
await new Promise(r => setTimeout(r, 500));
updateStatus("Validator creating and signing block...", "Propagation");
await broadcastToAll(selectedValidator, colors.green, "BLK");
return selectedValidator;
}
async function animatePBFT(transaction) {
// Select primary (node 0 for simplicity)
const primary = state.nodes[0];
state.nodes.forEach(n => n.isLeader = n.id === primary.id);
renderNetwork();
updateStatus("Client sending request to primary node...", "Request");
await animatePacket(state.nodes[3], primary, colors.orange, "REQ", 400);
await new Promise(r => setTimeout(r, 200));
updateStatus("Primary broadcasting PRE-PREPARE to all replicas...", "Pre-Prepare");
await broadcastToAll(primary, colors.purple, "PP");
await new Promise(r => setTimeout(r, 200));
updateStatus("Replicas exchanging PREPARE messages (2f+1 needed)...", "Prepare");
// Prepare phase - show exchanges between non-primary nodes
const preparePromises = [];
for (let i = 1; i < state.nodes.length; i++) {
for (let j = i + 1; j < state.nodes.length; j++) {
preparePromises.push(animatePacket(state.nodes[i], state.nodes[j], colors.blue, "P", 250));
}
}
await Promise.all(preparePromises);
await new Promise(r => setTimeout(r, 200));
updateStatus("Nodes exchanging COMMIT messages (achieving consensus)...", "Commit");
// Commit phase - all nodes broadcast commit
for (const node of state.nodes) {
highlightNode(node.id, colors.green, 300);
state.nodes[node.id].state = 'committed';
}
await new Promise(r => setTimeout(r, 400));
updateStatus("Consensus achieved! Block committed to all nodes.", "Complete");
return primary;
}
async function animateRaft(transaction) {
// Check for existing leader or elect one
let leader = state.nodes.find(n => n.isLeader);
if (!leader) {
updateStatus("No leader found. Starting leader election...", "Leader Election");
// Election simulation
const candidate = state.nodes[0];
// Request votes
for (const node of state.nodes) {
if (node.id !== candidate.id) {
await animatePacket(candidate, node, colors.yellow, "RV", 200);
}
}
// Receive votes
for (const node of state.nodes) {
if (node.id !== candidate.id) {
await animatePacket(node, candidate, colors.green, "V", 200);
}
}
leader = candidate;
state.nodes.forEach(n => n.isLeader = n.id === leader.id);
renderNetwork();
updateStatus(`${leader.label} elected as leader`, "Leader Election");
await new Promise(r => setTimeout(r, 300));
}
updateStatus("Client sending request to leader...", "Request");
await animatePacket(state.nodes[3], leader, colors.orange, "REQ", 400);
updateStatus("Leader replicating log entry to all followers...", "Replication");
// AppendEntries to all followers
const replicationPromises = state.nodes
.filter(n => n.id !== leader.id)
.map(async (node) => {
await animatePacket(leader, node, colors.blue, "AE", 300);
await new Promise(r => setTimeout(r, 50));
await animatePacket(node, leader, colors.green, "OK", 200);
highlightNode(node.id, colors.green, 400);
});
await Promise.all(replicationPromises);
updateStatus("Majority confirmed - entry committed to state machine", "Commit");
for (const node of state.nodes) {
highlightNode(node.id, colors.green, 300);
}
await new Promise(r => setTimeout(r, 300));
return leader;
}
// Submit transaction handler
async function submitTransaction() {
if (state.isAnimating) return;
const sender = senderInput.node().value.trim() || "Device_001";
const receiver = receiverInput.node().value.trim() || "Gateway_001";
const data = dataInput.node().value.trim() || "sensor_reading: 42";
state.isAnimating = true;
submitBtn.text("Processing...").style("background", colors.gray).style("cursor", "not-allowed");
const transaction = {
id: ++state.transactionCount,
sender,
receiver,
data,
timestamp: new Date().toISOString()
};
let validator;
try {
switch (state.consensus) {
case 'pow':
validator = await animatePoW(transaction);
break;
case 'pos':
validator = await animatePoS(transaction);
break;
case 'pbft':
validator = await animatePBFT(transaction);
break;
case 'raft':
validator = await animateRaft(transaction);
break;
}
// Create new block
const previousBlock = state.blockchain[state.blockchain.length - 1];
const newBlock = {
index: state.blockchain.length,
timestamp: new Date().toISOString(),
transactions: [transaction],
previousHash: previousBlock.hash,
hash: generateHash({ transaction, previousHash: previousBlock.hash }),
nonce: Math.floor(Math.random() * 1000000),
validator: validator.label,
consensusType: consensusMechanisms[state.consensus].shortName
};
state.blockchain.push(newBlock);
renderBlockchain();
updateStatus("Transaction confirmed and added to blockchain!", "Complete");
state.metrics.totalTransactions++;
// Reset node states
if (state.consensus === 'pow') {
state.nodes.forEach(n => n.isLeader = false);
renderNetwork();
}
state.nodes.forEach(n => n.state = 'idle');
} catch (error) {
updateStatus("Error processing transaction. Please try again.", "Error");
console.error(error);
}
state.isAnimating = false;
submitBtn.text("Submit Transaction").style("background", colors.teal).style("cursor", "pointer");
}
// Event handlers
consensusSelect.on("change", function() {
if (state.isAnimating) return;
state.consensus = this.value;
state.nodes.forEach(n => {
n.isLeader = false;
n.state = 'idle';
});
// For Raft, always have a leader
if (state.consensus === 'raft') {
state.nodes[0].isLeader = true;
}
updateConsensusInfo();
updateMetricsDisplay();
updateIoTContent();
renderNetwork();
updateStatus("Ready for transaction", "Idle");
});
submitBtn.on("click", submitTransaction);
// Hover effects for submit button
submitBtn
.on("mouseenter", function() {
if (!state.isAnimating) {
d3.select(this).style("background", colors.darkGreen);
}
})
.on("mouseleave", function() {
if (!state.isAnimating) {
d3.select(this).style("background", colors.teal);
}
});
// Initialize
initializeBlockchain();
updateConsensusInfo();
updateMetricsDisplay();
updateIoTContent();
renderNetwork();
renderBlockchain();
updateStatus("Ready for transaction. Enter details above and click Submit.", "Idle");
// Legend
const legend = container.append("div")
.style("display", "flex")
.style("justify-content", "center")
.style("gap", "20px")
.style("margin-top", "15px")
.style("flex-wrap", "wrap")
.style("font-size", "11px")
.style("padding", "10px")
.style("background", colors.lightGray)
.style("border-radius", "6px");
const legendItems = [
{ color: colors.navy, label: "Network Node" },
{ color: colors.orange, label: "Leader/Validator" },
{ color: colors.yellow, label: "Processing" },
{ color: colors.green, label: "Confirmed" },
{ color: colors.purple, label: "PBFT Message" },
{ color: colors.blue, label: "Replication" }
];
legendItems.forEach(item => {
const legendItem = legend.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "5px");
legendItem.append("div")
.style("width", "14px")
.style("height", "14px")
.style("background", item.color)
.style("border-radius", "50%")
.style("border", `2px solid ${colors.white}`);
legendItem.append("span")
.style("color", colors.navy)
.text(item.label);
});
return container.node();
}