// ============================================
// Network Conditions Emulator
// Interactive Tool for IoT Protocol 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",
darkGray: "#34495E",
lightBlue: "#85C1E9",
lightGreen: "#82E0AA",
lightRed: "#F5B7B1",
lightOrange: "#FAD7A0",
darkTeal: "#0E6655"
};
// Protocol Definitions
const protocols = {
mqtt: {
name: "MQTT",
fullName: "Message Queuing Telemetry Transport",
transport: "TCP",
color: colors.teal,
description: "Lightweight pub/sub protocol over TCP. Reliable delivery with QoS levels.",
retransmits: true,
overhead: 2, // bytes minimal header
ackRequired: true,
connectionOriented: true,
characteristics: ["Persistent connection", "QoS 0/1/2", "Low overhead", "Pub/Sub model"]
},
coap: {
name: "CoAP",
fullName: "Constrained Application Protocol",
transport: "UDP",
color: colors.orange,
description: "RESTful protocol over UDP. Optional confirmable messages.",
retransmits: true, // For CON messages
overhead: 4, // bytes header
ackRequired: true, // For CON
connectionOriented: false,
characteristics: ["Connectionless", "Confirmable/Non-confirmable", "REST-like", "Observe pattern"]
},
http: {
name: "HTTP",
fullName: "Hypertext Transfer Protocol",
transport: "TCP",
color: colors.purple,
description: "Standard web protocol. High overhead but universal support.",
retransmits: true,
overhead: 200, // bytes typical header
ackRequired: true,
connectionOriented: true,
characteristics: ["Request/Response", "Stateless", "High overhead", "TLS support"]
},
udp: {
name: "Raw UDP",
fullName: "User Datagram Protocol",
transport: "UDP",
color: colors.blue,
description: "Minimal protocol with no delivery guarantee. Lowest latency.",
retransmits: false,
overhead: 8, // bytes header
ackRequired: false,
connectionOriented: false,
characteristics: ["No guarantee", "Lowest overhead", "Fastest", "Fire-and-forget"]
}
};
// Pre-built Network Scenarios
const scenarios = {
ideal: {
name: "Ideal Network",
icon: "star",
description: "Perfect conditions - minimal latency, no packet loss",
latency: 10,
latencyDistribution: "fixed",
packetLoss: 0,
bandwidth: 10000, // kbps
jitter: 0,
connectionDropRate: 0,
color: colors.green
},
wifi: {
name: "Wi-Fi",
icon: "wifi",
description: "Typical home/office Wi-Fi - moderate latency, occasional drops",
latency: 25,
latencyDistribution: "normal",
packetLoss: 2,
bandwidth: 50000, // 50 Mbps
jitter: 15,
connectionDropRate: 1,
color: colors.teal
},
cellular4g: {
name: "Cellular 4G/LTE",
icon: "signal",
description: "Mobile 4G network - higher latency, some packet loss",
latency: 50,
latencyDistribution: "normal",
packetLoss: 5,
bandwidth: 20000, // 20 Mbps
jitter: 30,
connectionDropRate: 3,
color: colors.blue
},
cellular2g: {
name: "Cellular 2G/Edge",
icon: "signal-low",
description: "Legacy mobile network - high latency, significant loss",
latency: 300,
latencyDistribution: "uniform",
packetLoss: 15,
bandwidth: 50, // 50 kbps
jitter: 100,
connectionDropRate: 10,
color: colors.orange
},
satellite: {
name: "Satellite",
icon: "globe",
description: "Geostationary satellite - very high latency, moderate loss",
latency: 600,
latencyDistribution: "fixed",
packetLoss: 3,
bandwidth: 5000, // 5 Mbps
jitter: 50,
connectionDropRate: 5,
color: colors.purple
},
lorawan: {
name: "LoRaWAN",
icon: "broadcast",
description: "LPWAN - extreme constraints, very limited bandwidth",
latency: 1000,
latencyDistribution: "uniform",
packetLoss: 10,
bandwidth: 0.3, // 300 bps
jitter: 200,
connectionDropRate: 8,
color: colors.darkTeal
}
};
// State Management
let state = {
// Network conditions
latency: 50,
latencyDistribution: "normal", // fixed, uniform, normal
packetLoss: 5,
bandwidth: 1000, // kbps
jitter: 20,
connectionDropRate: 2,
// Protocol selection
selectedProtocol: "mqtt",
comparisonProtocol: "coap",
comparisonMode: false,
// Simulation state
messageId: 1,
isSimulating: false,
messageCount: 10,
messageSize: 100, // bytes
// Animation
animationSpeed: 1
};
// Statistics tracking
let stats = {
mqtt: { sent: 0, received: 0, lost: 0, retransmissions: 0, totalLatency: 0, bytes: 0 },
coap: { sent: 0, received: 0, lost: 0, retransmissions: 0, totalLatency: 0, bytes: 0 },
http: { sent: 0, received: 0, lost: 0, retransmissions: 0, totalLatency: 0, bytes: 0 },
udp: { sent: 0, received: 0, lost: 0, retransmissions: 0, totalLatency: 0, bytes: 0 }
};
// Message timeline for visualization
let messageTimeline = [];
// Utility functions
function random() {
return Math.random();
}
function gaussianRandom() {
// Box-Muller transform for normal distribution
let u = 0, v = 0;
while (u === 0) u = random();
while (v === 0) v = random();
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
function calculateLatency(baseLatency, distribution, jitter) {
let lat = baseLatency;
switch (distribution) {
case "fixed":
lat = baseLatency;
break;
case "uniform":
lat = baseLatency + (random() - 0.5) * jitter * 2;
break;
case "normal":
lat = baseLatency + gaussianRandom() * (jitter / 2);
break;
}
return Math.max(1, lat);
}
function simulatePacketDelivery(protocol) {
const isLost = random() * 100 < state.packetLoss;
const isDropped = random() * 100 < state.connectionDropRate;
if (isDropped) {
return { success: false, reason: "connection_drop" };
}
if (isLost) {
return { success: false, reason: "packet_loss" };
}
return { success: true };
}
function calculateTransmissionTime(bytes, bandwidthKbps) {
// Time in ms to transmit bytes at given bandwidth
const bitsPerSecond = bandwidthKbps * 1000;
const bits = bytes * 8;
return (bits / bitsPerSecond) * 1000;
}
// Create main container
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", "1200px")
.style("margin", "0 auto");
// Header
const header = container.append("div")
.style("background", `linear-gradient(135deg, ${colors.navy} 0%, ${colors.darkTeal} 100%)`)
.style("border-radius", "12px 12px 0 0")
.style("padding", "20px")
.style("color", colors.white);
header.append("h3")
.style("margin", "0 0 8px 0")
.style("font-size", "22px")
.text("Network Conditions Emulator");
header.append("p")
.style("margin", "0")
.style("opacity", "0.9")
.style("font-size", "14px")
.text("Simulate how network conditions affect IoT protocol performance");
// Scenario Selection Panel
const scenarioPanel = container.append("div")
.style("background", colors.lightGray)
.style("padding", "15px 20px")
.style("display", "flex")
.style("flex-wrap", "wrap")
.style("gap", "10px")
.style("align-items", "center");
scenarioPanel.append("span")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("font-size", "13px")
.style("margin-right", "10px")
.text("Network Scenarios:");
Object.entries(scenarios).forEach(([key, scenario]) => {
scenarioPanel.append("button")
.attr("class", `scenario-btn scenario-${key}`)
.text(scenario.name)
.style("padding", "8px 14px")
.style("background", colors.white)
.style("border", `2px solid ${scenario.color}`)
.style("border-radius", "6px")
.style("cursor", "pointer")
.style("font-size", "12px")
.style("color", colors.navy)
.style("font-weight", "bold")
.style("transition", "all 0.2s")
.attr("title", scenario.description)
.on("mouseover", function() {
d3.select(this).style("background", scenario.color).style("color", colors.white);
})
.on("mouseout", function() {
d3.select(this).style("background", colors.white).style("color", colors.navy);
})
.on("click", () => loadScenario(key));
});
// Main content grid
const mainGrid = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "300px 1fr")
.style("gap", "0")
.style("background", colors.white);
// Left sidebar - Controls
const sidebar = mainGrid.append("div")
.style("background", colors.navy)
.style("padding", "20px")
.style("color", colors.white);
// Network Conditions Section
sidebar.append("h4")
.style("margin", "0 0 15px 0")
.style("font-size", "16px")
.style("border-bottom", `2px solid ${colors.teal}`)
.style("padding-bottom", "8px")
.text("Network Conditions");
// Latency slider
const latencyGroup = sidebar.append("div").style("margin-bottom", "20px");
const latencyLabel = latencyGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("margin-bottom", "8px");
latencyLabel.append("span").style("font-size", "13px").text("Latency");
latencyLabel.append("span")
.attr("class", "latency-value")
.style("font-weight", "bold")
.style("color", colors.teal)
.text(`${state.latency}ms`);
const latencySlider = latencyGroup.append("input")
.attr("type", "range")
.attr("min", "0")
.attr("max", "2000")
.attr("value", state.latency)
.style("width", "100%")
.style("cursor", "pointer")
.on("input", function() {
state.latency = parseInt(this.value);
container.select(".latency-value").text(`${state.latency}ms`);
});
// Latency distribution
const latencyDistGroup = latencyGroup.append("div")
.style("display", "flex")
.style("gap", "5px")
.style("margin-top", "8px");
["fixed", "uniform", "normal"].forEach(dist => {
latencyDistGroup.append("button")
.attr("class", `dist-btn dist-${dist}`)
.text(dist.charAt(0).toUpperCase() + dist.slice(1))
.style("flex", "1")
.style("padding", "5px")
.style("background", state.latencyDistribution === dist ? colors.teal : "transparent")
.style("border", `1px solid ${colors.teal}`)
.style("border-radius", "4px")
.style("color", colors.white)
.style("font-size", "10px")
.style("cursor", "pointer")
.on("click", function() {
state.latencyDistribution = dist;
latencyDistGroup.selectAll("button").style("background", "transparent");
d3.select(this).style("background", colors.teal);
});
});
// Packet Loss slider
const lossGroup = sidebar.append("div").style("margin-bottom", "20px");
const lossLabel = lossGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("margin-bottom", "8px");
lossLabel.append("span").style("font-size", "13px").text("Packet Loss");
lossLabel.append("span")
.attr("class", "loss-value")
.style("font-weight", "bold")
.style("color", colors.orange)
.text(`${state.packetLoss}%`);
const lossSlider = lossGroup.append("input")
.attr("type", "range")
.attr("min", "0")
.attr("max", "100")
.attr("value", state.packetLoss)
.style("width", "100%")
.style("cursor", "pointer")
.on("input", function() {
state.packetLoss = parseInt(this.value);
container.select(".loss-value").text(`${state.packetLoss}%`);
});
// Bandwidth slider
const bwGroup = sidebar.append("div").style("margin-bottom", "20px");
const bwLabel = bwGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("margin-bottom", "8px");
bwLabel.append("span").style("font-size", "13px").text("Bandwidth");
bwLabel.append("span")
.attr("class", "bandwidth-value")
.style("font-weight", "bold")
.style("color", colors.blue)
.text(formatBandwidth(state.bandwidth));
function formatBandwidth(kbps) {
if (kbps >= 1000) return `${(kbps/1000).toFixed(1)} Mbps`;
if (kbps >= 1) return `${kbps.toFixed(0)} kbps`;
return `${(kbps * 1000).toFixed(0)} bps`;
}
const bwSlider = bwGroup.append("input")
.attr("type", "range")
.attr("min", "0")
.attr("max", "100")
.attr("value", 50)
.style("width", "100%")
.style("cursor", "pointer")
.on("input", function() {
// Logarithmic scale: 1 kbps to 10 Mbps
const val = parseInt(this.value);
state.bandwidth = Math.pow(10, val / 25); // 0->1, 25->10, 50->100, 75->1000, 100->10000
container.select(".bandwidth-value").text(formatBandwidth(state.bandwidth));
});
// Jitter slider
const jitterGroup = sidebar.append("div").style("margin-bottom", "20px");
const jitterLabel = jitterGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("margin-bottom", "8px");
jitterLabel.append("span").style("font-size", "13px").text("Jitter");
jitterLabel.append("span")
.attr("class", "jitter-value")
.style("font-weight", "bold")
.style("color", colors.purple)
.text(`${state.jitter}ms`);
const jitterSlider = jitterGroup.append("input")
.attr("type", "range")
.attr("min", "0")
.attr("max", "500")
.attr("value", state.jitter)
.style("width", "100%")
.style("cursor", "pointer")
.on("input", function() {
state.jitter = parseInt(this.value);
container.select(".jitter-value").text(`${state.jitter}ms`);
});
// Connection Drops slider
const dropGroup = sidebar.append("div").style("margin-bottom", "20px");
const dropLabel = dropGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("margin-bottom", "8px");
dropLabel.append("span").style("font-size", "13px").text("Connection Drops");
dropLabel.append("span")
.attr("class", "drop-value")
.style("font-weight", "bold")
.style("color", colors.red)
.text(`${state.connectionDropRate}%/min`);
const dropSlider = dropGroup.append("input")
.attr("type", "range")
.attr("min", "0")
.attr("max", "50")
.attr("value", state.connectionDropRate)
.style("width", "100%")
.style("cursor", "pointer")
.on("input", function() {
state.connectionDropRate = parseInt(this.value);
container.select(".drop-value").text(`${state.connectionDropRate}%/min`);
});
// Divider
sidebar.append("hr")
.style("border", "none")
.style("border-top", `1px solid ${colors.gray}`)
.style("margin", "20px 0");
// Protocol Selection Section
sidebar.append("h4")
.style("margin", "0 0 15px 0")
.style("font-size", "16px")
.style("border-bottom", `2px solid ${colors.teal}`)
.style("padding-bottom", "8px")
.text("Protocol Selection");
const protocolGrid = sidebar.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "8px")
.style("margin-bottom", "15px");
Object.entries(protocols).forEach(([key, protocol]) => {
const btn = protocolGrid.append("button")
.attr("class", `protocol-btn protocol-${key}`)
.style("padding", "10px 8px")
.style("background", state.selectedProtocol === key ? protocol.color : "transparent")
.style("border", `2px solid ${protocol.color}`)
.style("border-radius", "6px")
.style("cursor", "pointer")
.style("color", colors.white)
.style("font-size", "12px")
.style("font-weight", "bold")
.style("transition", "all 0.2s")
.on("click", () => {
state.selectedProtocol = key;
protocolGrid.selectAll("button").style("background", "transparent");
d3.select(`.protocol-${key}`).style("background", protocol.color);
updateProtocolInfo();
});
btn.append("div").text(protocol.name);
btn.append("div")
.style("font-size", "9px")
.style("font-weight", "normal")
.style("opacity", "0.8")
.text(protocol.transport);
});
// Protocol Info
const protocolInfo = sidebar.append("div")
.attr("class", "protocol-info")
.style("background", "rgba(255,255,255,0.1)")
.style("border-radius", "8px")
.style("padding", "12px")
.style("margin-bottom", "15px");
function updateProtocolInfo() {
const protocol = protocols[state.selectedProtocol];
protocolInfo.html("");
protocolInfo.append("div")
.style("font-weight", "bold")
.style("color", protocol.color)
.style("margin-bottom", "8px")
.text(protocol.fullName);
protocolInfo.append("div")
.style("font-size", "11px")
.style("margin-bottom", "8px")
.style("opacity", "0.9")
.text(protocol.description);
const charList = protocolInfo.append("div")
.style("font-size", "10px");
protocol.characteristics.forEach(char => {
charList.append("div")
.style("padding", "2px 0")
.html(`- ${char}`);
});
}
updateProtocolInfo();
// Comparison Mode Toggle
const comparisonGroup = sidebar.append("div")
.style("margin-bottom", "15px");
comparisonGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("align-items", "center")
.style("margin-bottom", "10px");
comparisonGroup.select("div")
.append("span")
.style("font-size", "13px")
.text("Comparison Mode");
const compToggle = comparisonGroup.select("div").append("label")
.style("position", "relative")
.style("display", "inline-block")
.style("width", "44px")
.style("height", "22px")
.style("cursor", "pointer");
compToggle.append("input")
.attr("type", "checkbox")
.style("opacity", "0")
.style("width", "0")
.style("height", "0")
.on("change", function() {
state.comparisonMode = this.checked;
renderMainContent();
});
compToggle.append("span")
.style("position", "absolute")
.style("top", "0")
.style("left", "0")
.style("right", "0")
.style("bottom", "0")
.style("background", colors.gray)
.style("border-radius", "22px")
.style("transition", "0.3s");
// Comparison protocol selector
const compSelect = comparisonGroup.append("select")
.attr("class", "comparison-select")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "6px")
.style("border", "none")
.style("background", colors.darkGray)
.style("color", colors.white)
.style("font-size", "12px")
.style("display", state.comparisonMode ? "block" : "none")
.on("change", function() {
state.comparisonProtocol = this.value;
});
Object.entries(protocols).forEach(([key, protocol]) => {
if (key !== state.selectedProtocol) {
compSelect.append("option")
.attr("value", key)
.attr("selected", key === state.comparisonProtocol ? true : null)
.text(`Compare with: ${protocol.name}`);
}
});
// Simulation Controls
sidebar.append("hr")
.style("border", "none")
.style("border-top", `1px solid ${colors.gray}`)
.style("margin", "20px 0");
sidebar.append("h4")
.style("margin", "0 0 15px 0")
.style("font-size", "16px")
.style("border-bottom", `2px solid ${colors.teal}`)
.style("padding-bottom", "8px")
.text("Simulation Controls");
// Message count
const msgCountGroup = sidebar.append("div").style("margin-bottom", "15px");
const msgCountLabel = msgCountGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("margin-bottom", "8px");
msgCountLabel.append("span").style("font-size", "13px").text("Messages to Send");
msgCountLabel.append("span")
.attr("class", "msg-count-value")
.style("font-weight", "bold")
.style("color", colors.teal)
.text(state.messageCount);
msgCountGroup.append("input")
.attr("type", "range")
.attr("min", "1")
.attr("max", "50")
.attr("value", state.messageCount)
.style("width", "100%")
.style("cursor", "pointer")
.on("input", function() {
state.messageCount = parseInt(this.value);
container.select(".msg-count-value").text(state.messageCount);
});
// Message size
const msgSizeGroup = sidebar.append("div").style("margin-bottom", "20px");
const msgSizeLabel = msgSizeGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("margin-bottom", "8px");
msgSizeLabel.append("span").style("font-size", "13px").text("Message Size");
msgSizeLabel.append("span")
.attr("class", "msg-size-value")
.style("font-weight", "bold")
.style("color", colors.orange)
.text(`${state.messageSize} bytes`);
msgSizeGroup.append("input")
.attr("type", "range")
.attr("min", "10")
.attr("max", "1000")
.attr("value", state.messageSize)
.style("width", "100%")
.style("cursor", "pointer")
.on("input", function() {
state.messageSize = parseInt(this.value);
container.select(".msg-size-value").text(`${state.messageSize} bytes`);
});
// Send and Reset buttons
const buttonGroup = sidebar.append("div")
.style("display", "flex")
.style("gap", "10px");
buttonGroup.append("button")
.attr("class", "send-btn")
.text("Send Messages")
.style("flex", "1")
.style("padding", "12px")
.style("background", colors.green)
.style("border", "none")
.style("border-radius", "8px")
.style("color", colors.white)
.style("font-size", "14px")
.style("font-weight", "bold")
.style("cursor", "pointer")
.style("transition", "all 0.2s")
.on("mouseover", function() {
d3.select(this).style("background", colors.darkTeal);
})
.on("mouseout", function() {
d3.select(this).style("background", colors.green);
})
.on("click", runSimulation);
buttonGroup.append("button")
.text("Reset")
.style("padding", "12px 20px")
.style("background", colors.red)
.style("border", "none")
.style("border-radius", "8px")
.style("color", colors.white)
.style("font-size", "14px")
.style("font-weight", "bold")
.style("cursor", "pointer")
.style("transition", "all 0.2s")
.on("click", resetSimulation);
// Right content area
const contentArea = mainGrid.append("div")
.attr("class", "content-area")
.style("padding", "20px")
.style("background", colors.white)
.style("display", "flex")
.style("flex-direction", "column")
.style("gap", "20px");
// Timeline visualization area
const timelineSection = contentArea.append("div")
.attr("class", "timeline-section");
timelineSection.append("h4")
.style("margin", "0 0 15px 0")
.style("color", colors.navy)
.style("display", "flex")
.style("align-items", "center")
.style("gap", "10px")
.html(`Message Timeline <span style="font-size:12px;font-weight:normal;color:${colors.gray}">(Time flows left to right)</span>`);
const timelineSvgContainer = timelineSection.append("div")
.attr("class", "timeline-svg-container")
.style("background", colors.lightGray)
.style("border-radius", "8px")
.style("padding", "15px")
.style("min-height", "300px")
.style("overflow-x", "auto");
const timelineSvg = timelineSvgContainer.append("svg")
.attr("class", "timeline-svg")
.attr("width", "100%")
.attr("height", "300")
.style("display", "block");
// Draw initial empty timeline
drawEmptyTimeline();
// Statistics Panel
const statsSection = contentArea.append("div")
.attr("class", "stats-section");
statsSection.append("h4")
.style("margin", "0 0 15px 0")
.style("color", colors.navy)
.text("Statistics");
const statsGrid = statsSection.append("div")
.attr("class", "stats-grid")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(150px, 1fr))")
.style("gap", "15px");
// Create stat cards
function createStatCard(label, value, color, key) {
const card = statsGrid.append("div")
.attr("class", `stat-card stat-${key}`)
.style("background", colors.lightGray)
.style("border-radius", "8px")
.style("padding", "15px")
.style("text-align", "center")
.style("border-left", `4px solid ${color}`);
card.append("div")
.attr("class", `stat-value-${key}`)
.style("font-size", "28px")
.style("font-weight", "bold")
.style("color", color)
.text(value);
card.append("div")
.style("font-size", "12px")
.style("color", colors.gray)
.style("margin-top", "5px")
.text(label);
}
createStatCard("Messages Sent", 0, colors.blue, "sent");
createStatCard("Received", 0, colors.green, "received");
createStatCard("Lost", 0, colors.red, "lost");
createStatCard("Avg Latency", "0ms", colors.teal, "latency");
createStatCard("Throughput", "0 kbps", colors.purple, "throughput");
createStatCard("Retransmissions", 0, colors.orange, "retransmissions");
// Latency Distribution Chart
const chartSection = contentArea.append("div")
.attr("class", "chart-section");
chartSection.append("h4")
.style("margin", "0 0 15px 0")
.style("color", colors.navy)
.text("Latency Distribution");
const chartContainer = chartSection.append("div")
.style("background", colors.lightGray)
.style("border-radius", "8px")
.style("padding", "15px")
.style("height", "200px");
const chartSvg = chartContainer.append("svg")
.attr("class", "chart-svg")
.attr("width", "100%")
.attr("height", "180")
.style("display", "block");
// Functions
function loadScenario(scenarioKey) {
const scenario = scenarios[scenarioKey];
if (!scenario) return;
state.latency = scenario.latency;
state.latencyDistribution = scenario.latencyDistribution;
state.packetLoss = scenario.packetLoss;
state.bandwidth = scenario.bandwidth;
state.jitter = scenario.jitter;
state.connectionDropRate = scenario.connectionDropRate;
// Update UI
container.select(".latency-value").text(`${state.latency}ms`);
container.select(".loss-value").text(`${state.packetLoss}%`);
container.select(".bandwidth-value").text(formatBandwidth(state.bandwidth));
container.select(".jitter-value").text(`${state.jitter}ms`);
container.select(".drop-value").text(`${state.connectionDropRate}%/min`);
latencySlider.property("value", state.latency);
lossSlider.property("value", state.packetLoss);
jitterSlider.property("value", state.jitter);
dropSlider.property("value", state.connectionDropRate);
// Update distribution buttons
sidebar.selectAll(".dist-btn").style("background", "transparent");
sidebar.select(`.dist-${state.latencyDistribution}`).style("background", colors.teal);
// Calculate bandwidth slider position
const bwVal = Math.log10(state.bandwidth) * 25;
bwSlider.property("value", Math.max(0, Math.min(100, bwVal)));
}
function drawEmptyTimeline() {
const svg = container.select(".timeline-svg");
svg.selectAll("*").remove();
const width = 800;
const height = 300;
svg.attr("viewBox", `0 0 ${width} ${height}`);
// Background
svg.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", colors.lightGray)
.attr("rx", 8);
// Center message
svg.append("text")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("text-anchor", "middle")
.attr("fill", colors.gray)
.attr("font-size", "16px")
.text("Click 'Send Messages' to start simulation");
// Time axis
svg.append("line")
.attr("x1", 50)
.attr("y1", height - 30)
.attr("x2", width - 50)
.attr("y2", height - 30)
.attr("stroke", colors.gray)
.attr("stroke-width", 2);
svg.append("text")
.attr("x", width / 2)
.attr("y", height - 10)
.attr("text-anchor", "middle")
.attr("fill", colors.gray)
.attr("font-size", "12px")
.text("Time (ms)");
}
function runSimulation() {
if (state.isSimulating) return;
state.isSimulating = true;
container.select(".send-btn").text("Simulating...");
messageTimeline = [];
const protocolsToRun = state.comparisonMode
? [state.selectedProtocol, state.comparisonProtocol]
: [state.selectedProtocol];
protocolsToRun.forEach(protocolKey => {
simulateProtocol(protocolKey);
});
// Update visualization
setTimeout(() => {
drawTimeline();
updateStats();
drawLatencyChart();
state.isSimulating = false;
container.select(".send-btn").text("Send Messages");
}, 100);
}
function simulateProtocol(protocolKey) {
const protocol = protocols[protocolKey];
const protocolStats = stats[protocolKey];
for (let i = 0; i < state.messageCount; i++) {
const msgId = state.messageId++;
const startTime = i * 50; // Stagger message sending
protocolStats.sent++;
// Calculate transmission time based on bandwidth
const totalBytes = state.messageSize + protocol.overhead;
const txTime = calculateTransmissionTime(totalBytes, state.bandwidth);
// Simulate delivery
let attempt = 0;
let delivered = false;
let finalLatency = 0;
let wasRetransmitted = false;
const maxAttempts = protocol.retransmits ? 4 : 1;
while (attempt < maxAttempts && !delivered) {
const result = simulatePacketDelivery(protocolKey);
const latency = calculateLatency(state.latency, state.latencyDistribution, state.jitter);
if (result.success) {
delivered = true;
finalLatency = latency + txTime + (attempt * (latency + 100)); // Include retransmit delay
protocolStats.received++;
protocolStats.totalLatency += finalLatency;
protocolStats.bytes += totalBytes;
} else if (protocol.retransmits && attempt < maxAttempts - 1) {
protocolStats.retransmissions++;
wasRetransmitted = true;
attempt++;
} else {
protocolStats.lost++;
finalLatency = latency + txTime;
attempt++;
}
}
messageTimeline.push({
id: msgId,
protocol: protocolKey,
startTime: startTime,
endTime: startTime + finalLatency,
latency: finalLatency,
delivered: delivered,
retransmitted: wasRetransmitted,
attempts: attempt + 1
});
}
}
function drawTimeline() {
const svg = container.select(".timeline-svg");
svg.selectAll("*").remove();
if (messageTimeline.length === 0) {
drawEmptyTimeline();
return;
}
const width = 800;
const height = 300;
const margin = { top: 40, right: 50, bottom: 50, left: 80 };
svg.attr("viewBox", `0 0 ${width} ${height}`);
// Calculate time range
const maxTime = Math.max(...messageTimeline.map(m => m.endTime), 1000);
const xScale = d3.scaleLinear()
.domain([0, maxTime])
.range([margin.left, width - margin.right]);
// Group by protocol
const protocolGroups = {};
messageTimeline.forEach(msg => {
if (!protocolGroups[msg.protocol]) {
protocolGroups[msg.protocol] = [];
}
protocolGroups[msg.protocol].push(msg);
});
const protocolKeys = Object.keys(protocolGroups);
const rowHeight = (height - margin.top - margin.bottom) / (protocolKeys.length + 1);
// Draw background
svg.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", colors.white)
.attr("rx", 8);
// Draw protocol labels and message lines
protocolKeys.forEach((protocolKey, rowIndex) => {
const protocol = protocols[protocolKey];
const yPos = margin.top + rowHeight * (rowIndex + 0.5);
const messages = protocolGroups[protocolKey];
// Protocol label
svg.append("text")
.attr("x", 10)
.attr("y", yPos + 5)
.attr("fill", protocol.color)
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text(protocol.name);
// Base line
svg.append("line")
.attr("x1", margin.left)
.attr("y1", yPos)
.attr("x2", width - margin.right)
.attr("y2", yPos)
.attr("stroke", colors.lightGray)
.attr("stroke-width", 1)
.attr("stroke-dasharray", "4,4");
// Draw messages
messages.forEach((msg, idx) => {
const x1 = xScale(msg.startTime);
const x2 = xScale(msg.endTime);
const yOffset = (idx % 3 - 1) * 8; // Stagger vertically
// Message line
svg.append("line")
.attr("x1", x1)
.attr("y1", yPos + yOffset)
.attr("x2", x2)
.attr("y2", yPos + yOffset)
.attr("stroke", msg.delivered ? protocol.color : colors.red)
.attr("stroke-width", 2)
.attr("stroke-dasharray", msg.retransmitted ? "4,2" : "none");
// Start dot
svg.append("circle")
.attr("cx", x1)
.attr("cy", yPos + yOffset)
.attr("r", 4)
.attr("fill", protocol.color);
// End indicator
if (msg.delivered) {
svg.append("circle")
.attr("cx", x2)
.attr("cy", yPos + yOffset)
.attr("r", 5)
.attr("fill", colors.green)
.attr("stroke", colors.white)
.attr("stroke-width", 1);
} else {
// X for lost
svg.append("text")
.attr("x", x2)
.attr("y", yPos + yOffset + 4)
.attr("text-anchor", "middle")
.attr("fill", colors.red)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("X");
}
});
});
// Time axis
const xAxis = d3.axisBottom(xScale)
.ticks(10)
.tickFormat(d => `${d}ms`);
svg.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(xAxis)
.selectAll("text")
.attr("fill", colors.gray)
.attr("font-size", "10px");
// Legend
const legend = svg.append("g")
.attr("transform", `translate(${width - margin.right - 150}, 15)`);
[
{ color: colors.green, label: "Delivered" },
{ color: colors.red, label: "Lost" },
{ text: "- - -", label: "Retransmitted" }
].forEach((item, i) => {
const g = legend.append("g")
.attr("transform", `translate(${i * 80}, 0)`);
if (item.color) {
g.append("circle")
.attr("r", 5)
.attr("fill", item.color);
} else {
g.append("text")
.attr("x", -10)
.attr("y", 4)
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text(item.text);
}
g.append("text")
.attr("x", 10)
.attr("y", 4)
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text(item.label);
});
}
function updateStats() {
const protocolKey = state.selectedProtocol;
const s = stats[protocolKey];
container.select(".stat-value-sent").text(s.sent);
container.select(".stat-value-received").text(s.received);
container.select(".stat-value-lost").text(s.lost);
container.select(".stat-value-latency").text(
s.received > 0 ? `${Math.round(s.totalLatency / s.received)}ms` : "0ms"
);
container.select(".stat-value-retransmissions").text(s.retransmissions);
// Calculate throughput (bits per second)
const totalTime = messageTimeline.length > 0
? Math.max(...messageTimeline.filter(m => m.protocol === protocolKey).map(m => m.endTime))
: 1;
const throughputBps = s.bytes * 8 / (totalTime / 1000);
const throughputKbps = throughputBps / 1000;
container.select(".stat-value-throughput").text(
throughputKbps >= 1 ? `${throughputKbps.toFixed(1)} kbps` : `${throughputBps.toFixed(0)} bps`
);
}
function drawLatencyChart() {
const svg = container.select(".chart-svg");
svg.selectAll("*").remove();
const relevantMessages = messageTimeline.filter(m => m.delivered);
if (relevantMessages.length === 0) {
svg.append("text")
.attr("x", 300)
.attr("y", 90)
.attr("text-anchor", "middle")
.attr("fill", colors.gray)
.text("No data yet");
return;
}
const width = 600;
const height = 180;
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
svg.attr("viewBox", `0 0 ${width} ${height}`);
// Group latencies into bins
const latencies = relevantMessages.map(m => m.latency);
const maxLatency = Math.max(...latencies);
const binCount = 20;
const binWidth = maxLatency / binCount;
const bins = Array(binCount).fill(0);
latencies.forEach(lat => {
const binIndex = Math.min(Math.floor(lat / binWidth), binCount - 1);
bins[binIndex]++;
});
const maxCount = Math.max(...bins);
const xScale = d3.scaleLinear()
.domain([0, maxLatency])
.range([margin.left, width - margin.right]);
const yScale = d3.scaleLinear()
.domain([0, maxCount])
.range([height - margin.bottom, margin.top]);
// Draw bars
bins.forEach((count, i) => {
if (count > 0) {
svg.append("rect")
.attr("x", xScale(i * binWidth))
.attr("y", yScale(count))
.attr("width", (width - margin.left - margin.right) / binCount - 2)
.attr("height", height - margin.bottom - yScale(count))
.attr("fill", colors.teal)
.attr("rx", 2);
}
});
// Axes
svg.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(xScale).ticks(5).tickFormat(d => `${Math.round(d)}ms`))
.selectAll("text")
.attr("fill", colors.gray);
svg.append("g")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(yScale).ticks(5))
.selectAll("text")
.attr("fill", colors.gray);
// Labels
svg.append("text")
.attr("x", width / 2)
.attr("y", height - 5)
.attr("text-anchor", "middle")
.attr("font-size", "11px")
.attr("fill", colors.gray)
.text("Latency (ms)");
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -(height / 2))
.attr("y", 15)
.attr("text-anchor", "middle")
.attr("font-size", "11px")
.attr("fill", colors.gray)
.text("Count");
// Mean line
const meanLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;
svg.append("line")
.attr("x1", xScale(meanLatency))
.attr("y1", margin.top)
.attr("x2", xScale(meanLatency))
.attr("y2", height - margin.bottom)
.attr("stroke", colors.orange)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "4,4");
svg.append("text")
.attr("x", xScale(meanLatency))
.attr("y", margin.top - 5)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("fill", colors.orange)
.text(`Mean: ${Math.round(meanLatency)}ms`);
}
function resetSimulation() {
// Reset stats
Object.keys(stats).forEach(key => {
stats[key] = { sent: 0, received: 0, lost: 0, retransmissions: 0, totalLatency: 0, bytes: 0 };
});
messageTimeline = [];
state.messageId = 1;
// Update UI
updateStats();
drawEmptyTimeline();
container.select(".chart-svg").selectAll("*").remove();
container.select(".chart-svg")
.append("text")
.attr("x", 300)
.attr("y", 90)
.attr("text-anchor", "middle")
.attr("fill", colors.gray)
.text("No data yet");
}
function renderMainContent() {
container.select(".comparison-select")
.style("display", state.comparisonMode ? "block" : "none");
}
// Export Panel
const exportPanel = container.append("div")
.style("background", colors.lightGray)
.style("padding", "15px 20px")
.style("border-radius", "0 0 12px 12px")
.style("display", "flex")
.style("gap", "15px")
.style("align-items", "center")
.style("justify-content", "flex-end")
.style("border-top", `2px solid ${colors.gray}`);
exportPanel.append("span")
.style("font-size", "13px")
.style("color", colors.navy)
.style("font-weight", "bold")
.style("margin-right", "auto")
.text("Export Config:");
// Export JSON button
exportPanel.append("button")
.text("Download JSON")
.style("padding", "10px 20px")
.style("background", colors.teal)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "6px")
.style("cursor", "pointer")
.style("font-size", "13px")
.style("font-weight", "bold")
.style("transition", "all 0.2s")
.on("mouseover", function() {
d3.select(this).style("background", colors.darkTeal);
})
.on("mouseout", function() {
d3.select(this).style("background", colors.teal);
})
.on("click", () => {
const protocolKey = state.selectedProtocol;
const s = stats[protocolKey];
const avgLatency = s.received > 0 ? Math.round(s.totalLatency / s.received) : 0;
const exportConfig = {
exportedAt: new Date().toISOString(),
tool: "Network Conditions Emulator",
networkConditions: {
latency: state.latency,
latencyDistribution: state.latencyDistribution,
packetLoss: state.packetLoss,
bandwidth: state.bandwidth,
bandwidthFormatted: formatBandwidth(state.bandwidth),
jitter: state.jitter,
connectionDropRate: state.connectionDropRate
},
protocolSettings: {
selectedProtocol: state.selectedProtocol,
protocolInfo: protocols[state.selectedProtocol],
comparisonMode: state.comparisonMode,
comparisonProtocol: state.comparisonMode ? state.comparisonProtocol : null
},
simulationSettings: {
messageCount: state.messageCount,
messageSize: state.messageSize
},
results: {
sent: s.sent,
received: s.received,
lost: s.lost,
retransmissions: s.retransmissions,
avgLatency: avgLatency,
bytesTransferred: s.bytes
},
availableScenarios: Object.keys(scenarios)
};
const blob = new Blob([JSON.stringify(exportConfig, null, 2)], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "network-emulator-config.json";
a.click();
URL.revokeObjectURL(url);
});
// Copy to clipboard button
exportPanel.append("button")
.text("Copy to Clipboard")
.style("padding", "10px 20px")
.style("background", colors.navy)
.style("color", colors.white)
.style("border", "none")
.style("border-radius", "6px")
.style("cursor", "pointer")
.style("font-size", "13px")
.style("font-weight", "bold")
.style("transition", "all 0.2s")
.on("mouseover", function() {
d3.select(this).style("background", colors.darkGray);
})
.on("mouseout", function() {
d3.select(this).style("background", colors.navy);
})
.on("click", function() {
const protocolKey = state.selectedProtocol;
const s = stats[protocolKey];
const avgLatency = s.received > 0 ? Math.round(s.totalLatency / s.received) : 0;
const exportConfig = {
exportedAt: new Date().toISOString(),
tool: "Network Conditions Emulator",
networkConditions: {
latency: state.latency,
latencyDistribution: state.latencyDistribution,
packetLoss: state.packetLoss,
bandwidth: state.bandwidth,
bandwidthFormatted: formatBandwidth(state.bandwidth),
jitter: state.jitter,
connectionDropRate: state.connectionDropRate
},
protocolSettings: {
selectedProtocol: state.selectedProtocol,
protocolInfo: protocols[state.selectedProtocol],
comparisonMode: state.comparisonMode,
comparisonProtocol: state.comparisonMode ? state.comparisonProtocol : null
},
simulationSettings: {
messageCount: state.messageCount,
messageSize: state.messageSize
},
results: {
sent: s.sent,
received: s.received,
lost: s.lost,
retransmissions: s.retransmissions,
avgLatency: avgLatency,
bytesTransferred: s.bytes
},
availableScenarios: Object.keys(scenarios)
};
navigator.clipboard.writeText(JSON.stringify(exportConfig, null, 2)).then(() => {
const btn = d3.select(this);
btn.text("Copied!").style("background", colors.green);
setTimeout(() => {
btn.text("Copy to Clipboard").style("background", colors.navy);
}, 2000);
});
});
// Initial render
drawEmptyTimeline();
return container.node();
}