ieeeColors = ({
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
darkGray: "#34495E",
red: "#E74C3C",
green: "#27AE60",
blue: "#3498DB",
purple: "#9B59B6",
yellow: "#F1C40F",
white: "#FFFFFF"
})
// ============================================================================
// SCENARIO DEFINITIONS
// ============================================================================
scenarios = ({
manufacturing: {
name: "Manufacturing Robot",
description: "Industrial robotic arm with position, torque, and temperature monitoring",
icon: "R",
sensors: [
{ id: "position", name: "Arm Position", unit: "deg", min: 0, max: 360, initial: 45, precision: 1 },
{ id: "torque", name: "Motor Torque", unit: "Nm", min: 0, max: 100, initial: 35, precision: 1 },
{ id: "temperature", name: "Motor Temp", unit: "C", min: 20, max: 120, initial: 55, precision: 1 },
{ id: "cycles", name: "Cycle Count", unit: "", min: 0, max: 100000, initial: 15420, precision: 0 }
],
states: ["IDLE", "RUNNING", "MAINTENANCE", "ERROR", "CALIBRATING"],
alerts: ["Overheat Warning", "Torque Limit Exceeded", "Position Drift Detected", "Maintenance Due", "Collision Risk"],
criticalThresholds: { temperature: 90, torque: 85 },
commands: ["Start Cycle", "Emergency Stop", "Calibrate", "Reset Position", "Clear Faults"]
},
hvac: {
name: "Smart Building HVAC",
description: "Building climate control with temperature, energy, and occupancy management",
icon: "H",
sensors: [
{ id: "temperature", name: "Room Temperature", unit: "C", min: 15, max: 35, initial: 22, precision: 1 },
{ id: "setpoint", name: "Setpoint", unit: "C", min: 16, max: 28, initial: 21, precision: 1 },
{ id: "energy", name: "Energy Usage", unit: "kWh", min: 0, max: 50, initial: 12.5, precision: 1 },
{ id: "occupancy", name: "Occupancy", unit: "ppl", min: 0, max: 100, initial: 45, precision: 0 }
],
states: ["HEATING", "COOLING", "IDLE", "ECO_MODE", "NIGHT_SETBACK"],
alerts: ["High Energy Consumption", "Sensor Fault", "Filter Replacement Due", "Zone Imbalance", "CO2 Level High"],
criticalThresholds: { energy: 40, temperature: 30 },
commands: ["Set Eco Mode", "Override Setpoint", "Force Ventilation", "Reset Schedule", "Calibrate Sensors"]
},
vehicle: {
name: "Connected Vehicle",
description: "Fleet vehicle with speed, location, fuel, and diagnostic monitoring",
icon: "V",
sensors: [
{ id: "speed", name: "Speed", unit: "km/h", min: 0, max: 200, initial: 65, precision: 0 },
{ id: "location", name: "Odometer", unit: "km", min: 0, max: 500000, initial: 45234, precision: 0 },
{ id: "fuel", name: "Fuel Level", unit: "%", min: 0, max: 100, initial: 68, precision: 0 },
{ id: "engineTemp", name: "Engine Temp", unit: "C", min: 60, max: 120, initial: 88, precision: 0 }
],
states: ["PARKED", "DRIVING", "IDLE", "CHARGING", "SERVICE_MODE"],
alerts: ["Low Fuel Warning", "Engine Warning", "Tire Pressure Low", "Service Due", "Speed Limit Exceeded"],
criticalThresholds: { fuel: 15, engineTemp: 105 },
commands: ["Remote Start", "Lock Doors", "Enable Geofence", "Request Diagnostics", "Schedule Service"]
},
medical: {
name: "Medical Device",
description: "Patient monitoring with vital signs and alert management",
icon: "M",
sensors: [
{ id: "heartRate", name: "Heart Rate", unit: "bpm", min: 40, max: 180, initial: 72, precision: 0 },
{ id: "bloodPressure", name: "Blood Pressure", unit: "mmHg", min: 80, max: 180, initial: 120, precision: 0 },
{ id: "oxygenSat", name: "SpO2", unit: "%", min: 85, max: 100, initial: 98, precision: 0 },
{ id: "temperature", name: "Body Temp", unit: "C", min: 35, max: 42, initial: 36.8, precision: 1 }
],
states: ["MONITORING", "ALERT", "STANDBY", "CALIBRATING", "DISCONNECTED"],
alerts: ["Arrhythmia Detected", "Low SpO2 Alert", "High BP Alert", "Sensor Disconnect", "Battery Low"],
criticalThresholds: { heartRate: 120, oxygenSat: 92 },
commands: ["Silence Alarm", "Start Recording", "Request Nurse", "Run Self-Test", "Adjust Thresholds"]
}
})
// ============================================================================
// SYNC MODE DEFINITIONS
// ============================================================================
syncModes = ({
realtime: {
name: "Real-time Streaming",
description: "Continuous updates with < 1s latency",
interval: 100,
color: ieeeColors.teal,
bandwidth: "High",
reliability: "Medium",
useCase: "Critical systems, safety monitoring"
},
periodic: {
name: "Periodic Batch",
description: "Configurable interval updates",
interval: 2000,
color: ieeeColors.blue,
bandwidth: "Low",
reliability: "High",
useCase: "Non-critical monitoring, cost optimization"
},
eventDriven: {
name: "Event-Driven",
description: "On change only, threshold-based",
interval: 0,
color: ieeeColors.orange,
bandwidth: "Variable",
reliability: "High",
useCase: "Battery-powered devices, sparse data"
},
hybrid: {
name: "Hybrid Mode",
description: "Batch + significant events",
interval: 1000,
color: ieeeColors.purple,
bandwidth: "Medium",
reliability: "Very High",
useCase: "Industrial IoT, fleet management"
}
})
// ============================================================================
// CONTROL INPUTS
// ============================================================================
viewof selectedScenario = Inputs.select(Object.keys(scenarios), {
label: "Select Scenario",
format: x => scenarios[x].name,
value: "manufacturing"
})
viewof syncMode = Inputs.select(Object.keys(syncModes), {
label: "Sync Mode",
format: x => syncModes[x].name,
value: "realtime"
})
viewof networkLatency = Inputs.range([0, 2000], {
label: "Network Latency (ms)",
step: 50,
value: 100
})
viewof packetLoss = Inputs.range([0, 50], {
label: "Packet Loss (%)",
step: 1,
value: 0
})
viewof isDeviceOffline = Inputs.toggle({
label: "Simulate Device Offline",
value: false
})
viewof syncInterval = Inputs.range([100, 5000], {
label: "Batch Interval (ms)",
step: 100,
value: 1000
})
// ============================================================================
// DERIVED STATE
// ============================================================================
currentScenario = scenarios[selectedScenario]
currentSyncMode = syncModes[syncMode]
// ============================================================================
// MUTABLE STATE MANAGEMENT
// ============================================================================
mutable physicalState = ({
values: Object.fromEntries(currentScenario.sensors.map(s => [s.id, s.initial])),
state: currentScenario.states[0],
online: true,
lastUpdate: Date.now(),
alerts: [],
commandQueue: []
})
mutable digitalTwinState = ({
values: Object.fromEntries(currentScenario.sensors.map(s => [s.id, s.initial])),
state: currentScenario.states[0],
synced: true,
lastSync: Date.now(),
history: [],
predictions: [],
derivedMetrics: {},
conflictLog: []
})
mutable syncMetrics = ({
latency: [],
avgLatency: 0,
maxLatency: 0,
minLatency: Infinity,
messagesPerSecond: 0,
totalMessages: 0,
successRate: 100,
failedMessages: 0,
dataVolume: 0,
driftDetected: false,
conflicts: 0,
startTime: Date.now(),
lastMessageTime: Date.now()
})
mutable syncMessages = []
mutable eventLog = []
// ============================================================================
// ANIMATION TICK
// ============================================================================
tick = {
const interval = Promises.tick(100);
return interval;
}
// ============================================================================
// SIMULATION ENGINE
// ============================================================================
simulationState = {
tick;
const now = Date.now();
const scenario = currentScenario;
const mode = currentSyncMode;
// Update physical device with realistic variations
if (!isDeviceOffline && Math.random() > 0.2) {
const newValues = { ...mutable physicalState.values };
scenario.sensors.forEach(sensor => {
const range = sensor.max - sensor.min;
const variation = (Math.random() - 0.5) * range * 0.015;
const drift = (Math.random() - 0.5) * range * 0.005;
newValues[sensor.id] = Math.max(sensor.min, Math.min(sensor.max,
newValues[sensor.id] + variation + drift
));
});
// Check for alerts based on thresholds
const alerts = [];
Object.entries(scenario.criticalThresholds || {}).forEach(([key, threshold]) => {
const sensor = scenario.sensors.find(s => s.id === key);
if (sensor && newValues[key] > threshold) {
alerts.push(`${sensor.name} exceeds safe threshold (${newValues[key].toFixed(1)} > ${threshold})`);
}
});
// Random state changes (less frequent)
let newState = mutable physicalState.state;
if (Math.random() > 0.98) {
const availableStates = scenario.states.filter(s => s !== mutable physicalState.state);
newState = availableStates[Math.floor(Math.random() * availableStates.length)];
// Add to event log
mutable eventLog = [...mutable eventLog.slice(-50), {
timestamp: now,
type: "state_change",
message: `Device state: ${mutable physicalState.state} -> ${newState}`,
source: "physical"
}];
}
mutable physicalState = {
...mutable physicalState,
values: newValues,
state: newState,
online: !isDeviceOffline,
lastUpdate: now,
alerts
};
}
// Determine if sync should happen
const shouldSync = (() => {
if (isDeviceOffline) return false;
if (Math.random() * 100 < packetLoss) {
mutable syncMetrics = {
...mutable syncMetrics,
failedMessages: mutable syncMetrics.failedMessages + 1
};
return false;
}
switch (syncMode) {
case "realtime":
return true;
case "periodic":
return (now - mutable digitalTwinState.lastSync) >= syncInterval;
case "eventDriven":
// Sync on significant change (> 5% of range)
return Object.keys(mutable physicalState.values).some(key => {
const sensor = scenario.sensors.find(s => s.id === key);
if (!sensor) return false;
const range = sensor.max - sensor.min;
const change = Math.abs(mutable physicalState.values[key] - mutable digitalTwinState.values[key]);
return change > range * 0.05;
});
case "hybrid":
const significantChange = Object.keys(mutable physicalState.values).some(key => {
const sensor = scenario.sensors.find(s => s.id === key);
if (!sensor) return false;
const range = sensor.max - sensor.min;
const change = Math.abs(mutable physicalState.values[key] - mutable digitalTwinState.values[key]);
return change > range * 0.1;
});
return significantChange || (now - mutable digitalTwinState.lastSync) >= syncInterval;
default:
return true;
}
})();
if (shouldSync) {
const actualLatency = networkLatency + Math.random() * 100 - 50;
const syncLatency = Math.max(10, actualLatency);
// Create sync message
const newMessage = {
id: Math.random().toString(36).substr(2, 9),
timestamp: now,
data: { ...mutable physicalState.values },
state: mutable physicalState.state,
latency: syncLatency,
direction: "physical-to-twin",
status: "syncing",
size: JSON.stringify(mutable physicalState.values).length
};
mutable syncMessages = [...mutable syncMessages.slice(-25), newMessage];
// Simulate async sync with latency
setTimeout(() => {
// Build history
const history = [...mutable digitalTwinState.history, {
timestamp: now,
values: { ...mutable physicalState.values },
state: mutable physicalState.state
}].slice(-100);
// Generate predictions based on trend analysis
const predictions = scenario.sensors.map(sensor => {
const recentValues = history.slice(-20).map(h => h.values[sensor.id]);
let trend = 0;
if (recentValues.length > 1) {
const firstHalf = recentValues.slice(0, Math.floor(recentValues.length / 2));
const secondHalf = recentValues.slice(Math.floor(recentValues.length / 2));
const avgFirst = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length;
const avgSecond = secondHalf.reduce((a, b) => a + b, 0) / secondHalf.length;
trend = (avgSecond - avgFirst) / recentValues.length;
}
const currentValue = mutable physicalState.values[sensor.id];
const predictedValue = Math.max(sensor.min, Math.min(sensor.max,
currentValue + trend * 50
));
// Calculate confidence based on data variance
const variance = recentValues.length > 0
? recentValues.reduce((sum, v) => sum + Math.pow(v - currentValue, 2), 0) / recentValues.length
: 0;
const confidence = Math.max(30, Math.min(99, 100 - Math.sqrt(variance) * 2));
return {
sensor: sensor.id,
name: sensor.name,
currentValue,
predictedValue,
trend: trend > 0.01 ? "increasing" : trend < -0.01 ? "decreasing" : "stable",
trendMagnitude: Math.abs(trend),
confidence,
unit: sensor.unit
};
});
// Calculate derived metrics
const healthFactors = [];
if (mutable physicalState.alerts.length > 0) healthFactors.push(-15 * mutable physicalState.alerts.length);
if (mutable syncMetrics.driftDetected) healthFactors.push(-10);
if (mutable syncMetrics.avgLatency > 500) healthFactors.push(-5);
const derivedMetrics = {
healthScore: Math.max(0, Math.min(100, 100 + healthFactors.reduce((a, b) => a + b, 0))),
efficiency: 70 + Math.random() * 25,
uptime: 99.5 + Math.random() * 0.5,
dataQuality: Math.max(50, 100 - packetLoss * 0.5),
syncAge: now - mutable digitalTwinState.lastSync
};
mutable digitalTwinState = {
values: { ...mutable physicalState.values },
state: mutable physicalState.state,
synced: true,
lastSync: now,
history,
predictions,
derivedMetrics,
conflictLog: mutable digitalTwinState.conflictLog
};
// Update message status
mutable syncMessages = mutable syncMessages.map(m =>
m.id === newMessage.id ? { ...m, status: "completed" } : m
);
// Update metrics
const latencies = [...mutable syncMetrics.latency, syncLatency].slice(-200);
const elapsedSeconds = (now - mutable syncMetrics.startTime) / 1000;
mutable syncMetrics = {
latency: latencies,
avgLatency: latencies.reduce((a, b) => a + b, 0) / latencies.length,
maxLatency: Math.max(...latencies),
minLatency: Math.min(...latencies),
messagesPerSecond: (mutable syncMetrics.totalMessages + 1) / Math.max(1, elapsedSeconds),
totalMessages: mutable syncMetrics.totalMessages + 1,
successRate: ((mutable syncMetrics.totalMessages + 1) /
(mutable syncMetrics.totalMessages + 1 + mutable syncMetrics.failedMessages)) * 100,
failedMessages: mutable syncMetrics.failedMessages,
dataVolume: mutable syncMetrics.dataVolume + newMessage.size,
driftDetected: false,
conflicts: mutable syncMetrics.conflicts,
startTime: mutable syncMetrics.startTime,
lastMessageTime: now
};
}, syncLatency);
}
// Detect drift
if (!isDeviceOffline) {
const drift = Object.keys(mutable physicalState.values).some(key => {
const sensor = scenario.sensors.find(s => s.id === key);
if (!sensor) return false;
const range = sensor.max - sensor.min;
return Math.abs(mutable physicalState.values[key] - mutable digitalTwinState.values[key]) > range * 0.15;
});
if (drift !== mutable syncMetrics.driftDetected) {
mutable syncMetrics = { ...mutable syncMetrics, driftDetected: drift };
if (drift) {
mutable eventLog = [...mutable eventLog.slice(-50), {
timestamp: now,
type: "drift",
message: "State drift detected between physical and digital twin",
source: "sync"
}];
}
}
}
return { now, shouldSync };
}
// ============================================================================
// MAIN VISUALIZATION
// ============================================================================
mainVisualization = {
const width = Math.min(1200, window.innerWidth - 40);
const height = 720;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("background", `linear-gradient(180deg, ${ieeeColors.lightGray} 0%, ${ieeeColors.white} 100%)`);
// Define gradients
const defs = svg.append("defs");
// Physical device gradient
const physicalGradient = defs.append("linearGradient")
.attr("id", "physicalGradient")
.attr("x1", "0%").attr("y1", "0%")
.attr("x2", "0%").attr("y2", "100%");
physicalGradient.append("stop").attr("offset", "0%").attr("stop-color", "#3498DB");
physicalGradient.append("stop").attr("offset", "100%").attr("stop-color", "#2980B9");
// Digital twin gradient
const twinGradient = defs.append("linearGradient")
.attr("id", "twinGradient")
.attr("x1", "0%").attr("y1", "0%")
.attr("x2", "0%").attr("y2", "100%");
twinGradient.append("stop").attr("offset", "0%").attr("stop-color", ieeeColors.teal);
twinGradient.append("stop").attr("offset", "100%").attr("stop-color", "#138D75");
// Sync panel gradient
const syncGradient = defs.append("linearGradient")
.attr("id", "syncGradient")
.attr("x1", "0%").attr("y1", "0%")
.attr("x2", "0%").attr("y2", "100%");
syncGradient.append("stop").attr("offset", "0%").attr("stop-color", ieeeColors.orange);
syncGradient.append("stop").attr("offset", "100%").attr("stop-color", "#D35400");
// Arrow marker
defs.append("marker")
.attr("id", "arrowhead")
.attr("markerWidth", 10)
.attr("markerHeight", 7)
.attr("refX", 9)
.attr("refY", 3.5)
.attr("orient", "auto")
.append("polygon")
.attr("points", "0 0, 10 3.5, 0 7")
.attr("fill", ieeeColors.orange);
// Glow filter for status indicators
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", "2")
.attr("result", "coloredBlur");
const glowMerge = glowFilter.append("feMerge");
glowMerge.append("feMergeNode").attr("in", "coloredBlur");
glowMerge.append("feMergeNode").attr("in", "SourceGraphic");
// Panel dimensions
const panelWidth = width * 0.30;
const panelHeight = height - 50;
const centerWidth = width * 0.36;
const panelMargin = 15;
// =========================================================================
// PHYSICAL DEVICE PANEL (LEFT)
// =========================================================================
const physicalPanel = svg.append("g")
.attr("transform", `translate(${panelMargin}, 25)`);
// Panel background
physicalPanel.append("rect")
.attr("width", panelWidth)
.attr("height", panelHeight)
.attr("rx", 12)
.attr("fill", ieeeColors.white)
.attr("stroke", ieeeColors.blue)
.attr("stroke-width", 3)
.attr("filter", "drop-shadow(0 2px 4px rgba(0,0,0,0.1))");
// Panel header
physicalPanel.append("rect")
.attr("width", panelWidth)
.attr("height", 55)
.attr("rx", 12)
.attr("fill", "url(#physicalGradient)");
physicalPanel.append("rect")
.attr("y", 43)
.attr("width", panelWidth)
.attr("height", 12)
.attr("fill", "url(#physicalGradient)");
physicalPanel.append("text")
.attr("x", panelWidth / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.white)
.attr("font-size", "15px")
.attr("font-weight", "bold")
.text("PHYSICAL DEVICE");
physicalPanel.append("text")
.attr("x", panelWidth / 2)
.attr("y", 43)
.attr("text-anchor", "middle")
.attr("fill", "rgba(255,255,255,0.8)")
.attr("font-size", "10px")
.text(currentScenario.name);
// Device icon container
const deviceIconGroup = physicalPanel.append("g")
.attr("transform", `translate(${panelWidth/2}, 110)`);
// Outer ring with pulse effect
deviceIconGroup.append("circle")
.attr("r", 45)
.attr("fill", isDeviceOffline ? ieeeColors.red : ieeeColors.green)
.attr("opacity", 0.15);
deviceIconGroup.append("circle")
.attr("r", 38)
.attr("fill", isDeviceOffline ? ieeeColors.red : ieeeColors.green)
.attr("opacity", 0.25);
// Main device circle
deviceIconGroup.append("circle")
.attr("r", 30)
.attr("fill", isDeviceOffline ? ieeeColors.red : ieeeColors.green)
.attr("filter", "url(#glow)");
// Device icon letter
deviceIconGroup.append("text")
.attr("text-anchor", "middle")
.attr("dy", 8)
.attr("fill", ieeeColors.white)
.attr("font-size", "26px")
.attr("font-weight", "bold")
.text(currentScenario.icon);
// Online/Offline badge
const statusBadge = physicalPanel.append("g")
.attr("transform", `translate(${panelWidth/2}, 165)`);
statusBadge.append("rect")
.attr("x", -45)
.attr("y", -12)
.attr("width", 90)
.attr("height", 24)
.attr("rx", 12)
.attr("fill", isDeviceOffline ? ieeeColors.red : ieeeColors.green);
statusBadge.append("text")
.attr("text-anchor", "middle")
.attr("dy", 5)
.attr("fill", ieeeColors.white)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text(isDeviceOffline ? "OFFLINE" : "ONLINE");
// Device state section
physicalPanel.append("text")
.attr("x", 15)
.attr("y", 205)
.attr("fill", ieeeColors.darkGray)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text("Current State:");
physicalPanel.append("rect")
.attr("x", 95)
.attr("y", 190)
.attr("width", panelWidth - 110)
.attr("height", 22)
.attr("rx", 4)
.attr("fill", ieeeColors.navy)
.attr("opacity", 0.1);
physicalPanel.append("text")
.attr("x", 95 + (panelWidth - 110) / 2)
.attr("y", 206)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.navy)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text(mutable physicalState.state);
// Sensor readings header
physicalPanel.append("text")
.attr("x", 15)
.attr("y", 235)
.attr("fill", ieeeColors.darkGray)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("Sensor Readings");
physicalPanel.append("line")
.attr("x1", 15)
.attr("y1", 242)
.attr("x2", panelWidth - 15)
.attr("y2", 242)
.attr("stroke", ieeeColors.lightGray)
.attr("stroke-width", 1);
// Sensor value bars
currentScenario.sensors.forEach((sensor, i) => {
const y = 260 + i * 55;
const value = mutable physicalState.values[sensor.id];
const percentage = (value - sensor.min) / (sensor.max - sensor.min);
const isWarning = currentScenario.criticalThresholds?.[sensor.id] &&
value > currentScenario.criticalThresholds[sensor.id];
// Sensor label
physicalPanel.append("text")
.attr("x", 15)
.attr("y", y)
.attr("fill", ieeeColors.darkGray)
.attr("font-size", "10px")
.text(sensor.name);
// Value display
physicalPanel.append("text")
.attr("x", panelWidth - 15)
.attr("y", y)
.attr("text-anchor", "end")
.attr("fill", isWarning ? ieeeColors.red : ieeeColors.navy)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text(`${value.toFixed(sensor.precision)} ${sensor.unit}`);
// Progress bar background
physicalPanel.append("rect")
.attr("x", 15)
.attr("y", y + 8)
.attr("width", panelWidth - 30)
.attr("height", 16)
.attr("rx", 4)
.attr("fill", ieeeColors.lightGray);
// Progress bar fill
physicalPanel.append("rect")
.attr("x", 15)
.attr("y", y + 8)
.attr("width", Math.max(4, (panelWidth - 30) * percentage))
.attr("height", 16)
.attr("rx", 4)
.attr("fill", isWarning ? ieeeColors.red : ieeeColors.blue);
// Threshold marker if applicable
if (currentScenario.criticalThresholds?.[sensor.id]) {
const thresholdX = 15 + (panelWidth - 30) *
((currentScenario.criticalThresholds[sensor.id] - sensor.min) / (sensor.max - sensor.min));
physicalPanel.append("line")
.attr("x1", thresholdX)
.attr("y1", y + 6)
.attr("x2", thresholdX)
.attr("y2", y + 26)
.attr("stroke", ieeeColors.red)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "2,2");
}
});
// Alerts section
const alertY = 260 + currentScenario.sensors.length * 55 + 15;
physicalPanel.append("text")
.attr("x", 15)
.attr("y", alertY)
.attr("fill", ieeeColors.darkGray)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("Active Alerts");
if (mutable physicalState.alerts.length === 0) {
physicalPanel.append("rect")
.attr("x", 15)
.attr("y", alertY + 8)
.attr("width", panelWidth - 30)
.attr("height", 28)
.attr("rx", 6)
.attr("fill", ieeeColors.green)
.attr("opacity", 0.15);
physicalPanel.append("text")
.attr("x", panelWidth / 2)
.attr("y", alertY + 27)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.green)
.attr("font-size", "11px")
.text("No active alerts");
} else {
mutable physicalState.alerts.slice(0, 2).forEach((alert, i) => {
physicalPanel.append("rect")
.attr("x", 15)
.attr("y", alertY + 8 + i * 28)
.attr("width", panelWidth - 30)
.attr("height", 24)
.attr("rx", 4)
.attr("fill", ieeeColors.red)
.attr("opacity", 0.15);
physicalPanel.append("text")
.attr("x", 20)
.attr("y", alertY + 25 + i * 28)
.attr("fill", ieeeColors.red)
.attr("font-size", "10px")
.text("! " + (alert.length > 35 ? alert.substring(0, 32) + "..." : alert));
});
}
// Last update timestamp
physicalPanel.append("text")
.attr("x", panelWidth / 2)
.attr("y", panelHeight - 12)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.gray)
.attr("font-size", "9px")
.text(`Last Update: ${new Date(mutable physicalState.lastUpdate).toLocaleTimeString()}`);
// =========================================================================
// SYNC VISUALIZATION PANEL (CENTER)
// =========================================================================
const centerPanel = svg.append("g")
.attr("transform", `translate(${panelMargin + panelWidth + 10}, 25)`);
// Panel background
centerPanel.append("rect")
.attr("width", centerWidth)
.attr("height", panelHeight)
.attr("rx", 12)
.attr("fill", ieeeColors.white)
.attr("stroke", ieeeColors.orange)
.attr("stroke-width", 3)
.attr("filter", "drop-shadow(0 2px 4px rgba(0,0,0,0.1))");
// Panel header
centerPanel.append("rect")
.attr("width", centerWidth)
.attr("height", 55)
.attr("rx", 12)
.attr("fill", "url(#syncGradient)");
centerPanel.append("rect")
.attr("y", 43)
.attr("width", centerWidth)
.attr("height", 12)
.attr("fill", "url(#syncGradient)");
centerPanel.append("text")
.attr("x", centerWidth / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.white)
.attr("font-size", "15px")
.attr("font-weight", "bold")
.text("SYNCHRONIZATION");
centerPanel.append("text")
.attr("x", centerWidth / 2)
.attr("y", 43)
.attr("text-anchor", "middle")
.attr("fill", "rgba(255,255,255,0.8)")
.attr("font-size", "10px")
.text("Real-time Data Flow");
// Sync mode display
centerPanel.append("text")
.attr("x", centerWidth / 2)
.attr("y", 80)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.darkGray)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text("Active Sync Mode");
centerPanel.append("rect")
.attr("x", 20)
.attr("y", 88)
.attr("width", centerWidth - 40)
.attr("height", 32)
.attr("rx", 6)
.attr("fill", currentSyncMode.color);
centerPanel.append("text")
.attr("x", centerWidth / 2)
.attr("y", 109)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.white)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text(currentSyncMode.name);
centerPanel.append("text")
.attr("x", centerWidth / 2)
.attr("y", 135)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.gray)
.attr("font-size", "9px")
.text(currentSyncMode.description);
// Data flow visualization area
centerPanel.append("text")
.attr("x", centerWidth / 2)
.attr("y", 165)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.darkGray)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text("Message Flow");
const messageAreaY = 175;
const messageAreaHeight = 180;
centerPanel.append("rect")
.attr("x", 10)
.attr("y", messageAreaY)
.attr("width", centerWidth - 20)
.attr("height", messageAreaHeight)
.attr("rx", 8)
.attr("fill", ieeeColors.lightGray)
.attr("opacity", 0.4);
// Draw visible sync messages
const visibleMessages = mutable syncMessages.slice(-8);
visibleMessages.forEach((msg, i) => {
const y = messageAreaY + 18 + i * 20;
const progress = msg.status === "completed" ? 1 :
Math.min(1, (Date.now() - msg.timestamp) / Math.max(1, msg.latency));
// Message track
centerPanel.append("line")
.attr("x1", 25)
.attr("y1", y)
.attr("x2", centerWidth - 25)
.attr("y2", y)
.attr("stroke", ieeeColors.gray)
.attr("stroke-width", 1)
.attr("stroke-dasharray", "4,4")
.attr("opacity", 0.4);
// Moving packet indicator
centerPanel.append("circle")
.attr("cx", 25 + (centerWidth - 50) * progress)
.attr("cy", y)
.attr("r", 7)
.attr("fill", msg.status === "completed" ? ieeeColors.green : ieeeColors.orange)
.attr("filter", "url(#glow)");
// Latency label
centerPanel.append("text")
.attr("x", centerWidth - 30)
.attr("y", y + 4)
.attr("text-anchor", "end")
.attr("fill", ieeeColors.gray)
.attr("font-size", "8px")
.text(`${msg.latency.toFixed(0)}ms`);
});
// Direction indicators
const directionY = messageAreaY + messageAreaHeight + 15;
centerPanel.append("text")
.attr("x", 25)
.attr("y", directionY)
.attr("fill", ieeeColors.blue)
.attr("font-size", "10px")
.attr("font-weight", "bold")
.text("Physical");
centerPanel.append("line")
.attr("x1", 75)
.attr("y1", directionY - 5)
.attr("x2", centerWidth - 75)
.attr("y2", directionY - 5)
.attr("stroke", ieeeColors.orange)
.attr("stroke-width", 3)
.attr("marker-end", "url(#arrowhead)");
centerPanel.append("text")
.attr("x", centerWidth - 25)
.attr("y", directionY)
.attr("text-anchor", "end")
.attr("fill", ieeeColors.teal)
.attr("font-size", "10px")
.attr("font-weight", "bold")
.text("Digital Twin");
// Metrics dashboard
const metricsY = directionY + 25;
centerPanel.append("text")
.attr("x", centerWidth / 2)
.attr("y", metricsY)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.darkGray)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text("Sync Metrics");
const metricsData = [
{ label: "Avg Latency", value: `${mutable syncMetrics.avgLatency.toFixed(0)}ms`, color: ieeeColors.blue },
{ label: "Max Latency", value: `${mutable syncMetrics.maxLatency.toFixed(0)}ms`, color: ieeeColors.orange },
{ label: "Min Latency", value: `${mutable syncMetrics.minLatency === Infinity ? 0 : mutable syncMetrics.minLatency.toFixed(0)}ms`, color: ieeeColors.teal },
{ label: "Msg/sec", value: mutable syncMetrics.messagesPerSecond.toFixed(1), color: ieeeColors.green },
{ label: "Success Rate", value: `${mutable syncMetrics.successRate.toFixed(1)}%`, color: mutable syncMetrics.successRate > 95 ? ieeeColors.green : ieeeColors.red },
{ label: "Data Volume", value: `${(mutable syncMetrics.dataVolume / 1024).toFixed(1)}KB`, color: ieeeColors.purple },
{ label: "Total Msgs", value: mutable syncMetrics.totalMessages.toString(), color: ieeeColors.navy },
{ label: "Failed", value: mutable syncMetrics.failedMessages.toString(), color: mutable syncMetrics.failedMessages > 0 ? ieeeColors.red : ieeeColors.gray }
];
metricsData.forEach((metric, i) => {
const row = Math.floor(i / 2);
const col = i % 2;
const metricX = 15 + col * (centerWidth / 2 - 10);
const metricY = metricsY + 15 + row * 42;
centerPanel.append("rect")
.attr("x", metricX)
.attr("y", metricY)
.attr("width", centerWidth / 2 - 25)
.attr("height", 36)
.attr("rx", 6)
.attr("fill", metric.color)
.attr("opacity", 0.1);
centerPanel.append("text")
.attr("x", metricX + 8)
.attr("y", metricY + 14)
.attr("fill", ieeeColors.gray)
.attr("font-size", "9px")
.text(metric.label);
centerPanel.append("text")
.attr("x", metricX + 8)
.attr("y", metricY + 30)
.attr("fill", metric.color)
.attr("font-size", "13px")
.attr("font-weight", "bold")
.text(metric.value);
});
// Drift warning
if (mutable syncMetrics.driftDetected) {
centerPanel.append("rect")
.attr("x", 15)
.attr("y", panelHeight - 55)
.attr("width", centerWidth - 30)
.attr("height", 35)
.attr("rx", 6)
.attr("fill", ieeeColors.red);
centerPanel.append("text")
.attr("x", centerWidth / 2)
.attr("y", panelHeight - 32)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.white)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("! STATE DRIFT DETECTED");
}
// Network conditions indicator
centerPanel.append("text")
.attr("x", centerWidth / 2)
.attr("y", panelHeight - 12)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.gray)
.attr("font-size", "9px")
.text(`Network: ${networkLatency}ms latency, ${packetLoss}% loss`);
// =========================================================================
// DIGITAL TWIN PANEL (RIGHT)
// =========================================================================
const twinPanel = svg.append("g")
.attr("transform", `translate(${panelMargin + panelWidth + centerWidth + 20}, 25)`);
// Panel background
twinPanel.append("rect")
.attr("width", panelWidth)
.attr("height", panelHeight)
.attr("rx", 12)
.attr("fill", ieeeColors.white)
.attr("stroke", ieeeColors.teal)
.attr("stroke-width", 3)
.attr("filter", "drop-shadow(0 2px 4px rgba(0,0,0,0.1))");
// Panel header
twinPanel.append("rect")
.attr("width", panelWidth)
.attr("height", 55)
.attr("rx", 12)
.attr("fill", "url(#twinGradient)");
twinPanel.append("rect")
.attr("y", 43)
.attr("width", panelWidth)
.attr("height", 12)
.attr("fill", "url(#twinGradient)");
twinPanel.append("text")
.attr("x", panelWidth / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.white)
.attr("font-size", "15px")
.attr("font-weight", "bold")
.text("DIGITAL TWIN");
twinPanel.append("text")
.attr("x", panelWidth / 2)
.attr("y", 43)
.attr("text-anchor", "middle")
.attr("fill", "rgba(255,255,255,0.8)")
.attr("font-size", "10px")
.text("Virtual Representation");
// Digital twin icon
const twinIconGroup = twinPanel.append("g")
.attr("transform", `translate(${panelWidth/2}, 110)`);
// Dashed circle representing virtual nature
twinIconGroup.append("circle")
.attr("r", 45)
.attr("fill", mutable digitalTwinState.synced ? ieeeColors.teal : ieeeColors.orange)
.attr("opacity", 0.15);
twinIconGroup.append("circle")
.attr("r", 38)
.attr("fill", "none")
.attr("stroke", mutable digitalTwinState.synced ? ieeeColors.teal : ieeeColors.orange)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "6,4");
twinIconGroup.append("circle")
.attr("r", 30)
.attr("fill", mutable digitalTwinState.synced ? ieeeColors.teal : ieeeColors.orange)
.attr("filter", "url(#glow)");
twinIconGroup.append("text")
.attr("text-anchor", "middle")
.attr("dy", 4)
.attr("fill", ieeeColors.white)
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text("DT");
twinIconGroup.append("text")
.attr("text-anchor", "middle")
.attr("dy", 16)
.attr("fill", ieeeColors.white)
.attr("font-size", "10px")
.text(currentScenario.icon);
// Sync status badge
const twinStatusBadge = twinPanel.append("g")
.attr("transform", `translate(${panelWidth/2}, 165)`);
twinStatusBadge.append("rect")
.attr("x", -40)
.attr("y", -12)
.attr("width", 80)
.attr("height", 24)
.attr("rx", 12)
.attr("fill", mutable digitalTwinState.synced ? ieeeColors.teal : ieeeColors.orange);
twinStatusBadge.append("text")
.attr("text-anchor", "middle")
.attr("dy", 5)
.attr("fill", ieeeColors.white)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text(mutable digitalTwinState.synced ? "SYNCED" : "SYNCING");
// Synchronized values header
twinPanel.append("text")
.attr("x", 15)
.attr("y", 205)
.attr("fill", ieeeColors.darkGray)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("Synchronized Values");
twinPanel.append("line")
.attr("x1", 15)
.attr("y1", 212)
.attr("x2", panelWidth - 15)
.attr("y2", 212)
.attr("stroke", ieeeColors.lightGray)
.attr("stroke-width", 1);
// Display synced values with drift indicators
currentScenario.sensors.forEach((sensor, i) => {
const y = 230 + i * 38;
const twinValue = mutable digitalTwinState.values[sensor.id];
const physValue = mutable physicalState.values[sensor.id];
const diff = Math.abs(twinValue - physValue);
const range = sensor.max - sensor.min;
const hasDrift = diff > range * 0.05;
twinPanel.append("text")
.attr("x", 15)
.attr("y", y)
.attr("fill", ieeeColors.darkGray)
.attr("font-size", "10px")
.text(sensor.name);
twinPanel.append("text")
.attr("x", panelWidth - 15)
.attr("y", y)
.attr("text-anchor", "end")
.attr("fill", hasDrift ? ieeeColors.orange : ieeeColors.teal)
.attr("font-size", "11px")
.attr("font-weight", "bold")
.text(`${twinValue.toFixed(sensor.precision)} ${sensor.unit}`);
if (hasDrift) {
twinPanel.append("text")
.attr("x", panelWidth - 15)
.attr("y", y + 14)
.attr("text-anchor", "end")
.attr("fill", ieeeColors.orange)
.attr("font-size", "8px")
.text(`drift: ${diff.toFixed(2)}`);
}
});
// Predictions section
const predY = 230 + currentScenario.sensors.length * 38 + 15;
twinPanel.append("text")
.attr("x", 15)
.attr("y", predY)
.attr("fill", ieeeColors.darkGray)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("Predictions (10s ahead)");
mutable digitalTwinState.predictions.slice(0, 3).forEach((pred, i) => {
const y = predY + 18 + i * 28;
const trendIcon = pred.trend === "increasing" ? "^" :
pred.trend === "decreasing" ? "v" : "-";
const trendColor = pred.trend === "increasing" ? ieeeColors.red :
pred.trend === "decreasing" ? ieeeColors.blue : ieeeColors.gray;
twinPanel.append("text")
.attr("x", 15)
.attr("y", y)
.attr("fill", ieeeColors.gray)
.attr("font-size", "9px")
.text(pred.name || pred.sensor);
twinPanel.append("text")
.attr("x", panelWidth / 2 - 10)
.attr("y", y)
.attr("fill", trendColor)
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text(trendIcon);
twinPanel.append("text")
.attr("x", panelWidth - 15)
.attr("y", y)
.attr("text-anchor", "end")
.attr("fill", ieeeColors.purple)
.attr("font-size", "10px")
.text(`${pred.predictedValue?.toFixed(1) || "..."} (${pred.confidence?.toFixed(0) || 0}%)`);
});
// Derived analytics
const analyticsY = predY + 18 + 3 * 28 + 15;
twinPanel.append("text")
.attr("x", 15)
.attr("y", analyticsY)
.attr("fill", ieeeColors.darkGray)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("Analytics");
const analyticsData = [
{ label: "Health Score", value: `${mutable digitalTwinState.derivedMetrics.healthScore?.toFixed(0) || 100}%`, color: ieeeColors.green },
{ label: "Efficiency", value: `${mutable digitalTwinState.derivedMetrics.efficiency?.toFixed(1) || 0}%`, color: ieeeColors.teal },
{ label: "Data Quality", value: `${mutable digitalTwinState.derivedMetrics.dataQuality?.toFixed(0) || 100}%`, color: ieeeColors.blue }
];
analyticsData.forEach((item, i) => {
const y = analyticsY + 18 + i * 22;
twinPanel.append("text")
.attr("x", 15)
.attr("y", y)
.attr("fill", ieeeColors.gray)
.attr("font-size", "9px")
.text(item.label);
twinPanel.append("text")
.attr("x", panelWidth - 15)
.attr("y", y)
.attr("text-anchor", "end")
.attr("fill", item.color)
.attr("font-size", "10px")
.attr("font-weight", "bold")
.text(item.value);
});
// History buffer indicator
twinPanel.append("text")
.attr("x", 15)
.attr("y", panelHeight - 35)
.attr("fill", ieeeColors.gray)
.attr("font-size", "9px")
.text(`History: ${mutable digitalTwinState.history.length}/100 records`);
// Progress bar for history
twinPanel.append("rect")
.attr("x", 15)
.attr("y", panelHeight - 25)
.attr("width", panelWidth - 30)
.attr("height", 6)
.attr("rx", 3)
.attr("fill", ieeeColors.lightGray);
twinPanel.append("rect")
.attr("x", 15)
.attr("y", panelHeight - 25)
.attr("width", (panelWidth - 30) * (mutable digitalTwinState.history.length / 100))
.attr("height", 6)
.attr("rx", 3)
.attr("fill", ieeeColors.teal);
// Last sync timestamp
twinPanel.append("text")
.attr("x", panelWidth / 2)
.attr("y", panelHeight - 12)
.attr("text-anchor", "middle")
.attr("fill", ieeeColors.gray)
.attr("font-size", "9px")
.text(`Last Sync: ${new Date(mutable digitalTwinState.lastSync).toLocaleTimeString()}`);
return svg.node();
}