html • Lines: 1439<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Neural Network Visualizer | Interactive Machine Learning Lab - Palash Kanti Kundu</title>
<meta name="title" content="Neural Network Visualizer | Interactive Machine Learning Lab">
<meta name="description"
content="A real-time, from-scratch neural network engine. Build architectures, tune hyperparameters, and visualize weight traces and decision boundaries in your browser.">
<meta name="keywords"
content="Neural Network, Machine Learning, JavaScript, AI Visualization, Deep Learning Tutorial, XOR Problem, Backpropagation Visualizer">
<meta property="og:type" content="website">
<meta property="og:url" content="https://palashkantikundu.in">
<meta property="og:title" content="Neural Network Demystified: Interactive Trace Engine">
<meta property="og:description"
content="Explore the math behind AI. Watch synapses fire and weights adjust in real-time with this browser-based neural network architect.">
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:title" content="Neural Network Demystified: Interactive Trace Engine">
<meta property="twitter:description"
content="Real-time neural network training and weight tracing in pure JavaScript. Build, train, and demystify AI architecture.">
<script src="https://cdn.jsdelivr.net/npm/plotly.js-dist@3.3.1/plotly.min.js"></script>
<style>
:root {
--bg: #12161d;
--panel: #1d1f2e;
--border: #334155;
--neon-cyan: #06b6d4;
--neon-purple: #8b5cf6;
--text: #f1f5f9;
--sub-text: #94a3b8;
--danger: #ef4444;
}
input:disabled {
cursor: not-allowed;
color: var(--neon-cyan);
border-color: var(--border);
font-weight: bold;
text-align: center;
}
#inputX,
#inputY {
width: 100%;
margin-bottom: 12px;
/* Gap after input boxes */
display: block;
}
* {
box-sizing: border-box;
}
@media (max-width: 1024px) {
.dashboard {
display: flex;
flex-direction: column;
width: 100%;
padding: 10px;
}
.panel {
width: 100% !important;
margin-bottom: 15px;
/* Prevent plotly from stretching the container */
overflow: hidden;
}
#plot,
#lossPlot {
width: 100% !important;
height: 350px !important;
/* Slightly shorter for mobile screens */
}
}
body {
font-family: 'Inter', 'Consolas', monospace;
background: var(--bg);
color: var(--text);
padding: 20px;
margin: 0;
letter-spacing: 0.5px;
}
.spinner {
/* Size of the spinner */
width: 30px;
height: 30px;
align-self: center;
align-items: center;
/* Create a circle */
border-radius: 50%;
/* Define the border: solid, color, and thickness */
border: 5px solid rgba(0, 0, 0, 0.1);
/* Light gray border for the track */
/* Apply a contrasting color to only one part of the border */
border-top-color: #3498db;
/* Blue color for the moving part */
/* Apply the animation */
animation: spin 1s linear infinite;
}
/* Define the rotation animation */
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.dashboard {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
width: 95vw;
max-width: 1800px;
margin: auto;
}
.config-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
align-items: center;
width: 100%;
}
/* Fixed Sidebar Vibe */
.controls {
grid-column: 1;
display: flex;
flex-direction: column;
gap: 12px;
background: var(--panel);
padding: 20px;
border: 1px solid var(--border);
border-radius: 12px;
height: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
@media (min-width: 1024px) {
.dashboard {
grid-template-columns: 320px 1fr 1fr;
}
.controls {
grid-row: 1 / span 3;
/* Sidebar spans the height again */
}
.panel[style*="grid-column: span 2;"] {
grid-column: span 2;
}
}
/* Tablet: 2 columns for plots, sidebar on top */
@media (min-width: 768px) and (max-width: 1023px) {
.dashboard {
grid-template-columns: 1fr;
}
.controls {
grid-row: auto;
grid-column: 1;
}
.panel {
grid-column: 1 !important;
/* Force plots to take full width */
}
}
.controls>div {
border-bottom: 1px solid var(--border);
padding-bottom: 5px;
margin-bottom: 5px;
}
.training-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
margin-bottom: 15px;
}
.training-grid input {
width: 100%;
/* Prevents horizontal overflow */
}
/* Dynamic border for layer rows */
.layer-row-container {
border-left: 6px solid #00F0FF;
/* Default Tanh */
padding-left: 10px;
transition: border-color 0.3s ease;
}
/* Visual feedback for paused state */
.btn-paused {
animation: pulse-yellow 1.5s infinite;
border-color: #fbbf24 !important;
color: #fbbf24 !important;
}
@keyframes pulse-yellow {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
.panel {
background: var(--panel);
border: 1px solid var(--border);
padding: 15px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.8);
width: 100%;
/* Force panel to fill grid cell */
min-width: 0;
/* Prevents grid blowout */
}
#plot,
#lossPlot {
width: 100% !important;
/* Force plotly container to fill panel */
height: 400px;
}
/* High-Visibility Training Button */
#trainBtn {
width: 100%;
margin-bottom: 8px;
color: #10b981;
border-color: #10b981;
}
/* Dataset and Control Buttons */
.btn-group {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}
.btn-group button {
font-size: 0.65rem;
padding: 6px 2px;
}
/* Fix Overflows */
.config-row {
display: flex;
gap: 4px;
margin-bottom: 6px;
align-items: center;
}
.l-size {
width: 60px !important;
}
/* Prevents crowding */
.l-act {
flex: 1;
}
.delete-btn {
background: rgba(239, 68, 68, 0.1);
color: var(--danger);
border: 1px solid var(--danger);
padding: 4px 8px;
}
.active-dataset {
background: rgba(6, 182, 212, 0.2) !important;
/* Subtle Cyan tint */
color: var(--neon-cyan) !important;
border: 1px solid var(--neon-cyan) !important;
box-shadow: 0 0 8px rgba(6, 182, 212, 0.3);
font-weight: 600;
}
canvas {
background: #1e1e24;
width: 100%;
height: 400px;
border: 1px solid var(--border);
border-radius: 8px;
margin-top: 10px;
}
button {
cursor: pointer;
font-family: inherit;
border-radius: 6px;
padding: 8px 12px;
font-size: 0.75rem;
font-weight: 600;
transition: all 0.2s;
border: 1px solid var(--border);
background: #334155;
color: var(--text);
text-transform: uppercase;
margin-top: 5px;
margin-bottom: 5px;
}
button:hover {
background: #475569;
}
btn-primary {
background: var(--neon-cyan);
color: #000;
border: none;
}
.btn-primary:hover {
background: #22d3ee;
box-shadow: 0 0 12px rgba(6, 182, 212, 0.4);
}
input,
select,
button {
background: #272c30;
border: 1px solid var(--border);
color: var(--text);
padding: 8px;
border-radius: 4px;
transition: all 0.2s ease;
}
input:focus,
select:focus {
border-color: var(--neon-cyan);
box-shadow: 0 0 8px rgba(0, 240, 255, 0.2);
outline: none;
}
/* Numerical Matrix Styling */
#weightInspector {
grid-column: 2 / span 2;
max-height: 250px;
overflow-x: auto;
white-space: nowrap;
border-top: 2px solid var(--border);
padding-top: 15px;
}
.matrix-table {
border-collapse: collapse;
margin: 5px;
display: inline-block;
vertical-align: top;
border: 1px solid var(--border);
background: #0c0c0b;
font-size: 0.7rem;
}
.matrix-table td {
border: 1px solid #011b27;
padding: 4px 8px;
min-width: 50px;
font-family: 'Consolas', monospace;
}
.layer-title {
color: var(--neon-cyan);
text-transform: uppercase;
font-size: 0.75rem;
padding: 5px;
background: #0a0a0a;
border-bottom: 1px solid var(--border);
}
label {
font-size: 0.7rem;
color: var(--sub-text);
text-transform: uppercase;
font-weight: 600;
display: block;
margin-bottom: 4px;
}
.btn-group button {
flex: 1 1 calc(33% - 4px);
font-size: 0.65rem;
padding: 6px 2px;
cursor: pointer;
}
.delete-btn {
flex: 0 0 30px;
background: rgba(255, 0, 85, 0.1);
border: 1px solid #ff0055;
color: #ff0055;
cursor: pointer;
}
</style>
</head>
<body>
<h2 style="text-align: center;">Neural Network Visualizer</h2>
<p style="text-align: center;">This tool accompanies the learning guide: <a href="/#neural-network">Neural Network</a></p>
<div class="dashboard">
<div class="controls">
<div>
<label>Network Structure</label>
<div class="config-row"
style="opacity: 0.7; margin-bottom: 10px; border-bottom: 1px dashed var(--border); padding-bottom: 10px;">
<span style="font-size: 0.7rem; width: 60px;">INPUT:</span>
<input type="text" id="inputFeaturesCount" disabled value="2" class="l-size"
style="background: transparent; border-style: dotted;">
<span style="font-size: 0.6rem; color: var(--sub-text);">Features (based on input data)</span>
</div>
<div id="layersConfig">
<div class="layer-row-container" style="display:flex; gap:2px; margin-bottom: 20px;">
<input type="number" class="l-size network-ctl" onchange="buildNetwork()" value="4">
<select class="l-act network-ctl" onchange="updateLayerColor(this)">
<option value="tanh">Tanh</option>
<option value="relu">ReLU</option>
<option value="lrelu">Leaky ReLU</option>
</select>
</div>
</div>
<button onclick="addLayerUI()">+ ADD LAYER</button>
<div style="margin-top: 20px; margin-bottom: 10px; border-top: 0px solid #444;">
<label>Output Layer</label>
<div class="layer-row-container"
style="display: flex; gap: 4px; align-items: center; border-left: solid #FF0055;">
<input type="text" id="outputNeuronsCount" disabled value="1" class="l-size"
style="width: 50px !important; background: transparent; border-style: dotted;">
<select id="outputAct" class="network-ctl" onchange="updateLayerColor(this)">
<option value="sigmoid">Sigmoid (Classification 0-1)</option>
<option value="softmax">Softmax (Multi-class)</option>
<option value="linear">Regression (Linear)</option>
</select>
</div>
</div>
</div>
<div>
<label>Data Management</label>
<input id="inputX" value="0,0; 0,1; 1,0; 1,1">
<input id="inputY" value="1; 0; 0; 1">
<div class="btn-group" style="margin-bottom: 15px;">
<button onclick="setDataset('OR', this)">OR</button>
<button onclick="setDataset('XOR', this)">XOR</button>
<button onclick="setDataset('XNOR', this)">XNOR</button>
<button onclick="setDataset('XOR3', this)">3 Input XOR</button>
<button onclick="setDataset('XNOR3', this)">3 Input XNOR</button>
<button onclick="setDataset('SIN', this)">SIN</button>
<button onclick="setDataset('COS', this)">COS</button>
<button onclick="setDataset('MOON', this)">MOONS</button>
<button onclick="setColorDataset(this)">Color Temp</button>
</div>
</div>
<div>
<label>Training Parameters</label>
<div class="training-grid">
<div>
<label>Epochs</label>
<input type="number" id="epochs" value="5000" class="network-ctl">
</div>
<div>
<label>LR</label>
<input type="number" id="lr" value="0.1" class="network-ctl" step="0.01" style="width: 100%;">
</div>
<div>
<label>Delay</label>
<input type="number" id="delay" value="10" class="network-ctl" />
</div>
</div>
</div>
<div>
<div class="btn-group">
<button id="trainBtn" class="control-btn" onclick="handleTrainClick()"
style="font-size: 20px; grid-column: span 2;" title="Start/Pause">
►
</button>
<button onclick="resetEverything()" style="font-size: 20px; border-color: #ef4444; color: #ef4444;"
class="control-btn" title="STOP">
⏹
</button>
</div>
<div id="status" style="margin-top:10px;">Awaiting Initialization</div>
</div>
<div>
<label>Model Inference & Live Test</label>
<div id="dynamicTestUI"
style="background: #101029; padding: 10px; border: 1px solid #333; margin-top: 5px; min-height: 80px;">
</div>
<div id="predictionResult" style="margin-top: 10px; font-size: 0.85rem;">
Result: awaiting input...
</div>
</div>
</div>
<div class="panel" style="grid-column: span 2;">
<label>Topology & Weight Magnitude (Live)</label>
<canvas id="netCanvas"></canvas>
</div>
<div class="panel"><label>Prediction Plot</label>
<div id="plot" style="height: 400px;"></div>
</div>
<div class="panel"><label>Loss History</label>
<div id="lossPlot" style="height: 400px;"></div>
</div>
<div id="weightInspector" class="panel">
<label style="margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--border);">Numerical
Weight & Bias Matrices (Trace)</label>
<div id="matrixDisplay"></div>
</div>
</div>
<script>
let layers = [], isTraining = false, isPaused = false, stopFlag = false, lossHistory = [];
const actColors = {
input: '#ffffff',
relu: '#7000FF', // Neon Purple
tanh: '#00F0FF', // Neon Cyan
sigmoid: '#FF0055', // Neon Pink
lrelu: '#f97316',
softmax: '#00ff41',
linear: '#FDE047'
};
const Act = {
relu: { f: x => Math.max(0, x), df: x => x > 0 ? 1 : 0 },
tanh: { f: x => Math.tanh(x), df: x => 1 - Math.pow(Math.tanh(x), 2) },
sigmoid: { f: x => 1 / (1 + Math.exp(-x)), df: x => { let s = 1 / (1 + Math.exp(-x)); return s * (1 - s); } },
lrelu: { f: x => x > 0 ? x : 0.01 * x, df: x => x > 0 ? 1 : 0.01 },
softmax: {
// Softmax is applied to a whole row (array)
f: (arr) => {
const max = Math.max(...arr); // For numerical stability
const exps = arr.map(v => Math.exp(v - max));
const sum = exps.reduce((a, b) => a + b);
return exps.map(v => v / sum);
},
df: x => 1
},
linear: { f: x => x, df: x => 1 }
};
class Layer {
constructor(inD, outD, act) {
this.act = act;
let s = Math.sqrt(2 / (inD + outD)); // Xavier Init
this.w = Array.from({ length: inD }, () => Array.from({ length: outD }, () => (Math.random() * 2 - 1) * s));
this.b = new Array(outD).fill(0);
this.lastZ = []; this.lastA = [];
}
forward(X) {
// Linear Step: Z = XW + B
this.lastZ = X.map(row => this.b.map((b, j) => row.reduce((sum, v, i) => sum + v * this.w[i][j], 0) + b));
// Activation Step
if (this.act === 'softmax') {
this.lastA = this.lastZ.map(row => Act.softmax.f(row));
} else {
this.lastA = this.lastZ.map(row => row.map(v => Act[this.act].f(v)));
}
return this.lastA;
}
}
function updateLayerColor(selectElement) {
const color = actColors[selectElement.value] || '#ffffff';
selectElement.parentElement.style.borderLeft = `6px solid ${color}`;
buildNetwork();
}
let currentMode = 'LOGIC'; // Default
function updateTestUI(type) {
const container = document.getElementById('dynamicTestUI');
container.innerHTML = ''; // Clear previous
currentMode = type;
if (['OR', 'XOR', 'XNOR', 'XOR3', 'XNOR3'].includes(type)) {
const inputs = type.includes('3') ? 3 : 2;
let html = '<div style="display:flex; gap:15px; align-items:center;">';
for (let i = 0; i < inputs; i++) {
html += `<label style="color:#888">BIT ${i} <input type="checkbox" class="logic-sw" onchange="runLiveTest()" style="width:20px; height:20px;"></label>`;
}
html += '<div id="bulb" style="width:30px; height:30px; border-radius:50%; background:#272c30; border:2px solid #555; margin-left:20px; transition: 0.2s;"></div>';
html += '<span id="logicVal" style="margin-left:10px; font-weight:bold;">0.00</span></div>';
container.innerHTML = html;
}
else if (['SIN', 'COS'].includes(type)) {
container.innerHTML = `
<div style="display:flex; flex-direction:column; gap:8px;">
<label>Range Evaluation:</label>
<div style="display:flex; flex-wrap: wrap; gap:5px;">
<input type="number" id="rangeStart" value="0" step="0.1" style="flex: 1 1 60px; min-width: 0;" oninput="runLiveTest()">
<input type="number" id="rangeEnd" value="6.28" step="0.1" style="flex: 1 1 60px; min-width: 0;" oninput="runLiveTest()">
<input type="number" id="rangeSteps" value="50" style="flex: 0.5 1 40px; min-width: 0;" oninput="runLiveTest()">
</div>
<div id="mathResultDisplay" style="color:var(--neon-cyan); font-size:0.8rem; margin-top:5px;">
Generating curve...
</div>
</div>`;
}
else if (type === 'MOON') {
container.innerHTML = `
<div style="display:flex; flex-direction:column; gap:10px;">
<div style="display:flex; flex-wrap: wrap; gap:8px;">
<div style="flex:1; min-width: 80px;">
<label style="display:block; font-size:9px;">INPUT 0 (X)</label>
<input type="number" id="moonX" value="0.5" step="0.1" oninput="runLiveTest()" style="width:100%; min-width:0;">
</div>
<div style="flex:1; min-width: 80px;">
<label style="display:block; font-size:9px;">INPUT 1 (Y)</label>
<input type="number" id="moonY" value="0.2" step="0.1" oninput="runLiveTest()" style="width:100%; min-width:0;">
</div>
</div>
<div style="display:flex; justify-content:space-around; align-items:center; background:#000; padding:10px; border-radius:5px; border: 1px solid #333;">
<div style="text-align:center">
<div id="orbA" style="width:25px; height:25px; border-radius:50%; background:#333; border:2px solid #ff3131; margin:auto;"></div>
<label style="font-size:10px">CLASS A</label>
</div>
<div style="text-align:center">
<div id="orbB" style="width:25px; height:25px; border-radius:50%; background:#333; border:2px solid #00ff41; margin:auto;"></div>
<label style="font-size:10px">CLASS B</label>
</div>
</div>
</div>`;
}
else if (type === 'COLOR') {
container.innerHTML = `
<label>Pick Color:</label>
<input type="color" id="colPick" oninput="runLiveTest()" style="height:40px;">`;
}
}
function updateMathLabel(val) {
const unit = document.getElementById('mathUnit').value;
const displayVal = unit === 'deg' ? (val * 180 / Math.PI).toFixed(1) + "°" : val + " rad";
document.getElementById('mathValDisplay').innerText = displayVal;
runLiveTest();
}
// Helper to run prediction automatically on UI change
function runLiveTest() {
if (['SIN', 'COS'].includes(currentMode)) {
const start = parseFloat(document.getElementById('rangeStart').value);
const end = parseFloat(document.getElementById('rangeEnd').value);
const steps = parseInt(document.getElementById('rangeSteps').value);
let rangeInputs = [];
let xValues = [];
for (let i = 0; i <= steps; i++) {
let val = start + (i * (end - start) / steps);
xValues.push(val);
rangeInputs.push([val]); // Network expects array of arrays [[x1], [x2]...]
}
if (layers.length > 0) executeRangeInference(xValues, rangeInputs);
return
}
let inputVector = [];
if (['OR', 'XOR', 'XNOR', 'XOR3', 'XNOR3'].includes(currentMode)) {
inputVector = Array.from(document.querySelectorAll('.logic-sw')).map(sw => sw.checked ? 1 : 0);
} else if (['SIN', 'COS'].includes(currentMode)) {
inputVector = [parseFloat(document.querySelector('#dynamicTestUI input[type="range"]').value)];
} else if (currentMode === 'MOON') {
inputVector = [parseFloat(document.getElementById('moonX').value), parseFloat(document.getElementById('moonY').value)];
} else if (currentMode === 'COLOR') {
const hex = document.getElementById('colPick').value;
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
inputVector = [r, g, b];
}
if (layers.length > 0) executeInference(inputVector);
}
function executeRangeInference(xValues, rangeInputs) {
let currentA = rangeInputs;
for (let L of layers) currentA = L.forward(currentA);
// Rescale outputs back to -1 to 1 range for visualization
const yValues = currentA.map(out => (out[0] * 2) - 1);
const trace = {
x: xValues,
y: yValues,
mode: 'lines',
name: 'Model Range Prediction',
line: { color: '#00ff41', width: 3 }
};
const layout = {
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#0f0' },
margin: { t: 10, b: 30, l: 30, r: 10 },
xaxis: {
gridcolor: 'rgba(51, 65, 85, 0.3)', // Very faint grid lines
zerolinecolor: 'rgba(51, 65, 85, 0.5)', title: 'Input (rad)'
},
yaxis: {
gridcolor: 'rgba(51, 65, 85, 0.3)', // Very faint grid lines
zerolinecolor: 'rgba(51, 65, 85, 0.5)', title: 'f(x)'
}
};
Plotly.react('plot', [trace], layout);
document.getElementById('mathResultDisplay').innerText = `Plotted ${xValues.length} points across range.`;
}
function executeInference(inputVector) {
if (['SIN', 'COS'].includes(currentMode)) {
inputVector = [inputVector[0] % (Math.PI * 2)];
}
let currentA = [inputVector];
for (let L of layers) currentA = L.forward(currentA);
const outputs = currentA[0];
const resultDiv = document.getElementById('predictionResult');
if (['OR', 'XOR', 'XNOR', 'XOR3', 'XNOR3'].includes(currentMode)) {
const val = outputs[0];
document.getElementById('bulb').style.background = `rgba(0, 255, 65, ${val})`;
document.getElementById('bulb').style.boxShadow = val > 0.5 ? `0 0 15px #00ff41` : 'none';
document.getElementById('logicVal').innerText = val.toFixed(3);
resultDiv.innerHTML = `Signal: ${(val * 100).toFixed(1)}%`;
resultDiv.innerHTML = `
<div style="margin-top: 15px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
<label style="font-size: 0.7rem; color: var(--sub-text); letter-spacing: 1px;">SIGNAL STRENGTH</label>
<span style="color: #00ff41; font-family: monospace; font-weight: bold;">${(val * 100).toFixed(1)}%</span>
</div>
<div style="width: 100%; height: 8px; background: #1a1a1a; border-radius: 4px; border: 1px solid #333; overflow: hidden;">
<div id="signalLine" style="
width: ${val * 100}%;
height: 100%;
background: linear-gradient(90deg, #00441b, #00ff41);
transition: width 0.2s ease-out;
box-shadow: 0 0 10px rgba(0, 255, 65, 0.4);
"></div>
</div>
</div>`;
}
else if (currentMode === 'MOON') {
const val = outputs[0]; // Assuming 1 output neuron for binary
const probB = val;
const probA = 1 - val;
// Update Orb A (Red - Class 0)
document.getElementById('orbA').style.background = `rgba(255, 49, 49, ${probA})`;
document.getElementById('orbA').style.boxShadow = probA > 0.5 ? `0 0 15px #ff3131` : 'none';
// Update Orb B (Green - Class 1)
document.getElementById('orbB').style.background = `rgba(0, 255, 65, ${probB})`;
document.getElementById('orbB').style.boxShadow = probB > 0.5 ? `0 0 15px #00ff41` : 'none';
resultDiv.innerHTML = `Classification: ${val > 0.5 ? 'Moon B' : 'Moon A'} (${(val * 100).toFixed(1)}%)`;
}
else if (['SIN', 'COS'].includes(currentMode)) {
const rawVal = outputs[0];
const finalVal = (rawVal * 2) - 1;
document.getElementById('mathResultDisplay').innerText = `f(x) ≈ ${finalVal.toFixed(4)}`;
resultDiv.innerHTML = `Regression Output: ${rawVal.toFixed(4)}`;
}
else {
displayOutputResults(outputs); // Handles Color
}
}
function displayOutputResults(outputs) {
let html = `<div style="color: #fff; margin-top:10px;">Network Output:</div>`;
outputs.forEach((val, i) => {
// Map index to labels
const labels = ['WARM', 'COOL', 'NEUTRAL'];
const barColor = i === 0 ? '#ef4444' : i === 1 ? '#06b6d4' : '#94a3b8'; // Red, Cyan, Gray
html += `
<div style="margin-bottom: 4px;">
<span style="font-size:0.7rem">${labels[i]}: ${(val * 100).toFixed(1)}%</span>
<div style="background: #1e293b; width: 100%; height: 8px; border-radius: 4px; overflow: hidden;">
<div style="background: ${barColor}; width: ${val * 100}%; height: 100%; transition: width 0.3s ease;"></div>
</div>
</div>`;
});
document.getElementById('predictionResult').innerHTML = html;
}
// --- DATASET GENERATORS ---
function setDataset(type, btnElement) {
// 1. UI Highlight Logic
document.querySelectorAll('.btn-group button').forEach(btn => btn.classList.remove('active-dataset'));
if (btnElement) btnElement.classList.add('active-dataset');
// 2. Clear previous inference/results
document.getElementById('predictionResult').innerHTML = "Result: awaiting input...";
lossHistory = []; // Reset loss history for the new dataset
// 3. Dataset Definitions
if (type === 'OR') {
document.getElementById('inputX').value = "0,0; 0,1; 1,0; 1,1";
document.getElementById('inputY').value = "0; 1; 1; 1";
} else if (type === 'XOR') {
document.getElementById('inputX').value = "0,0; 0,1; 1,0; 1,1";
document.getElementById('inputY').value = "0; 1; 1; 0";
} else if (type === 'XNOR') {
document.getElementById('inputX').value = "0,0; 0,1; 1,0; 1,1";
document.getElementById('inputY').value = "1; 0; 0; 1";
} else if (type === 'XOR3') {
document.getElementById('inputX').value = "0,0,0; 0,0,1; 0,1,0; 0,1,1; 1,0,0; 1,0,1; 1,1,0; 1,1,1";
document.getElementById('inputY').value = "0; 1; 1; 0; 1; 0; 0; 1";
} else if (type === 'XNOR3') {
document.getElementById('inputX').value = "0,0,0; 0,0,1; 0,1,0; 0,1,1; 1,0,0; 1,0,1; 1,1,0; 1,1,1";
document.getElementById('inputY').value = "1; 0; 0; 1; 0; 1; 1; 0";
} else if (type === 'COS') {
let x = [], y = [];
for (let i = 0; i <= 20; i++) {
x.push((i / 5).toFixed(4));
y.push(((Math.cos(i / 5 * Math.PI) + 1) / 2).toFixed(4));
}
document.getElementById('inputX').value = x.join('; ');
document.getElementById('inputY').value = y.join('; ');
} else if (type === 'SIN') {
let x = [], y = [];
for (let i = 0; i <= 20; i++) {
x.push((i / 5).toFixed(4));
y.push(((Math.sin(i / 5 * Math.PI) + 1) / 2).toFixed(4));
}
document.getElementById('inputX').value = x.join('; ');
document.getElementById('inputY').value = y.join('; ');
} else if (type === 'MOON') {
let x = [], y = [];
for (let i = 0; i < 40; i++) {
let p = (i / 40) * Math.PI;
// Restricting to 2 decimal places for cleaner input strings
let x1 = Math.cos(p).toFixed(2);
let y1 = Math.sin(p).toFixed(2);
let x2 = (1 - Math.cos(p)).toFixed(2);
let y2 = (0.5 - Math.sin(p)).toFixed(2);
x.push(`${x1},${y1}`); y.push(0);
x.push(`${x2},${y2}`); y.push(1);
}
document.getElementById('inputX').value = x.join('; ');
document.getElementById('inputY').value = y.join('; ');
}
updateTestUI(type);
buildNetwork();
}
function addLayerUI() {
const div = document.createElement('div');
div.className = "config-row layer-row-container"; // Uses the new flex row logic
div.style.borderLeft = `6px solid ${actColors.tanh}`;
div.innerHTML = `
<input type="number" onchange="buildNetwork()" class="l-size network-ctl" value="4">
<select class="l-act network-ctl" onchange="updateLayerColor(this)">
<option value="tanh">Tanh</option>
<option value="relu">ReLU</option>
<option value="lrelu">Leaky ReLU</option>
</select>
<button class="delete-btn" onclick="this.parentElement.remove()">x</button>
`;
document.getElementById('layersConfig').appendChild(div);
buildNetwork();
}
async function handleTrainClick() {
const btn = document.getElementById('trainBtn');
if (!isTraining) {
// Start fresh training
toggleTraining();
btn.innerHTML = "⏸"; // Switch to Pause Icon
} else {
// Toggle the paused state
isPaused = !isPaused;
if (isPaused) {
btn.innerHTML = "►"; // Show Play to resume
btn.classList.add('btn-paused');
document.getElementById('status').innerHTML = "<small>Paused: </small>" + document.getElementById('status').innerHTML;
} else {
btn.innerHTML = "⏸"; // Show Pause while running
btn.classList.remove('btn-paused');
}
}
}
function buildNetwork() {
if (isTraining) return; // Prevent rebuilding during training
console.log("Building Network...");
document.getElementById('predictionResult').innerHTML = "<small>Train The Network First</small>";
const xRaw = document.getElementById('inputX').value.split(';').map(r => r.split(',').map(v => parseFloat(v.trim())));
const yRaw = document.getElementById('inputY').value.split(';').map(r => r.split(',').map(v => parseFloat(v.trim())));
document.getElementById('inputFeaturesCount').value = xRaw[0].length;
document.getElementById('outputNeuronsCount').value = yRaw[0].length;
layers = [];
let curIn = xRaw[0].length;
// Build Hidden Layers
document.querySelectorAll('#layersConfig > div:not([style*="border-top"])').forEach(el => {
let sInput = el.querySelector('.l-size');
if (!sInput) return; // Skip the output config div
let s = parseInt(sInput.value);
let a = el.querySelector('.l-act').value;
layers.push(new Layer(curIn, s, a));
curIn = s;
});
// NEW: Build Output Layer based on User Choice
const outputDim = yRaw[0].length;
const finalAct = document.getElementById('outputAct').value;
layers.push(new Layer(curIn, outputDim, finalAct));
renderWeights();
initPlots();
drawNet();
document.getElementById('status').innerHTML = "<small>Network Built. Ready to Train.</small>";
}
function predictUniversal() {
if (layers.length === 0) {
document.getElementById('predictionResult').innerHTML = "<small>Error: Build and train the network first.</small>";
return;
}
// 1. Parse the input string
const inputStr = document.getElementById('testInput').value;
const inputVector = inputStr.split(',').map(v => parseFloat(v.trim()));
// Validation: Ensure input length matches the first layer's weights
if (inputVector.length !== layers[0].w.length) {
document.getElementById('predictionResult').innerHTML =
`<small>Error: Expected ${layers[0].w.length} inputs, but got ${inputVector.length}.</small>`;
return;
}
// 2. Forward Pass: Pass the input through all layers
let currentA = [inputVector];
for (let L of layers) {
currentA = L.forward(currentA);
}
// 3. Display Results
const outputs = currentA[0];
let html = `<div style="color: #fff; margin-bottom: 5px;">Output Activations:</div>`;
outputs.forEach((val, i) => {
const percentage = (val * 100).toFixed(2);
// Create a simple visual bar for each output
html += `
<div style="margin-bottom: 4px;">
<span style="display:inline-block; width: 80px;">Out[${i}]:</span>
<span style="color: #00ff41;">${percentage}%</span>
<div style="background: #333; width: 100%; height: 4px; margin-top: 2px;">
<div style="background: #00ff41; width: ${val * 100}%; height: 100%;"></div>
</div>
</div>`;
});
// For classification: Highlight the winner
const maxIdx = outputs.indexOf(Math.max(...outputs));
html += `<div style="margin-top: 8px; border-top: 1px solid #444; padding-top: 5px;">
Primary Class: <strong style="color:#00ff41;">Index ${maxIdx}</strong>
</div>`;
document.getElementById('predictionResult').innerHTML = html;
}
function toggleDatasetButtons(disabled) {
console.log("Toggling dataset buttons. Disabled:", disabled);
const buttons = document.querySelectorAll('.btn-group button:not(.control-btn), .controls button:not(.control-btn), .network-ctl, #outputAct');
console.log("Buttons found:", buttons);
buttons.forEach(btn => {
btn.disabled = disabled;
btn.style.opacity = disabled ? "0.5" : "1";
btn.style.cursor = disabled ? "not-allowed" : "pointer";
});
}
function resetEverything() {
stopFlag = true;
isTraining = false;
isPaused = false;
lossHistory = [];
document.getElementById('matrixDisplay').innerHTML = "";
const trainBtn = document.getElementById('trainBtn');
trainBtn.innerHTML = "►"; // Restore Play icon
trainBtn.classList.remove('btn-paused'); // Stop the pulsing
// Reset UI Elements
document.getElementById('status').innerHTML = "<small>Reset - Awaiting Initialization</small>";
document.getElementById('predictionResult').innerHTML = `<small>Train the Network First</small>`;
// Clear Plots
// initPlots();
// Re-enable buttons
toggleDatasetButtons(false);
}
function setColorDataset(btnElement) {
document.querySelectorAll('.btn-group button').forEach(btn => btn.classList.remove('active-dataset'));
if (btnElement) btnElement.classList.add('active-dataset');
const data = [
// --- WARM COLORS (Output: 1,0,0) ---
// Characteristics: High Red, Low Blue
{ x: "1,0,0", y: "1,0,0" }, // Pure Red
{ x: "1,0.5,0", y: "1,0,0" }, // Orange
{ x: "1,1,0", y: "1,0,0" }, // Yellow
{ x: "0.8,0.2,0.1", y: "1,0,0" }, // Brick Red
{ x: "0.6,0,0", y: "1,0,0" }, // Deep Maroon
{ x: "1,0.3,0.3", y: "1,0,0" }, // Coral/Pink
{ x: "0.9,0.4,0.1", y: "1,0,0" }, // Burnt Orange
{ x: "0.5,0.2,0", y: "1,0,0" }, // Brown
// --- COOL COLORS (Output: 0,1,0) ---
// Characteristics: High Blue or Green, Low Red
{ x: "0,0,1", y: "0,1,0" }, // Pure Blue
{ x: "0,1,0", y: "0,1,0" }, // Pure Green
{ x: "0,1,1", y: "0,1,0" }, // Cyan
{ x: "0.5,0,1", y: "0,1,0" }, // Purple (Moved to Cool)
{ x: "0.2,0.2,0.8", y: "0,1,0" }, // Soft Blue
{ x: "0,0.5,0.5", y: "0,1,0" }, // Teal
{ x: "0.1,0.8,0.2", y: "0,1,0" }, // Bright Green
{ x: "0.7,0.3,1", y: "0,1,0" }, // Violet
// --- NEUTRAL/MUTED (Output: 0,0,1) ---
// Characteristics: R, G, and B are nearly equal
{ x: "0,0,0", y: "0,0,1" }, // Black
{ x: "1,1,1", y: "0,0,1" }, // White
{ x: "0.5,0.5,0.5", y: "0,0,1" }, // Mid Gray
{ x: "0.2,0.2,0.2", y: "0,0,1" }, // Dark Gray
{ x: "0.8,0.8,0.8", y: "0,0,1" }, // Light Gray
{ x: "0.4,0.4,0.4", y: "0,0,1" }, // Charcoal
{ x: "0.6,0.6,0.6", y: "0,0,1" }, // Silver
{ x: "0.1,0.1,0.1", y: "0,0,1" } // Near Black
];
document.getElementById('inputX').value = data.map(d => d.x).join('; ');
document.getElementById('inputY').value = data.map(d => d.y).join('; ');
updateTestUI('COLOR');
buildNetwork();
}
async function toggleTraining() {
if (isTraining) return;
if (layers.length === 0) buildNetwork();
isTraining = true;
stopFlag = false;
toggleDatasetButtons(true);
document.getElementById('predictionResult').innerHTML = "<small>Training in progress...</small>";
document.getElementById('trainBtn').innerHTML = '<div class="spinner"></div>';
const xRaw = document.getElementById('inputX').value.split(';').map(r => r.split(',').map(v => parseFloat(v.trim())));
const yRaw = document.getElementById('inputY').value.split(';').map(r => r.split(',').filter(v => v.trim() !== "").map(v => parseFloat(v.trim())));
const maxEpochs = parseInt(document.getElementById('epochs').value);
const delay = parseInt(document.getElementById('delay').value);
const lr = parseFloat(document.getElementById('lr').value);
lossHistory = [];
for (let ep = 0; ep <= maxEpochs; ep++) {
while (isPaused) await new Promise(r => setTimeout(r, 100));
if (stopFlag) break;
// Forward
let acts = [xRaw];
for (let L of layers) acts.push(L.forward(acts[acts.length - 1]));
// Backward
let out = acts[acts.length - 1];
let error = out.map((row, i) => row.map((v, j) => v - yRaw[i][j]));
let mse = error.flat().reduce((a, b) => a + b * b, 0) / xRaw.length;
lossHistory.push(mse);
let dZ = error.map((row, i) => row.map((err, j) => err * (2 / xRaw.length) * Act[layers[layers.length - 1].act].df(layers[layers.length - 1].lastZ[i][j])));
for (let i = layers.length - 1; i >= 0; i--) {
let L = layers[i], prevA = acts[i];
let dW = L.w.map((row, rI) => row.map((_, cI) => dZ.reduce((s, dzR, rI2) => s + dzR[cI] * prevA[rI2][rI], 0)));
let dB = dZ[0].map((_, cI) => dZ.reduce((s, dzR) => s + dzR[cI], 0));
if (i > 0) {
let prevL = layers[i - 1];
dZ = dZ.map((dzR, rI) => prevL.w[0].map((_, cI) => {
let da = dzR.reduce((s, dzV, curCI) => s + dzV * L.w[cI][curCI], 0);
return da * Act[prevL.act].df(prevL.lastZ[rI][cI]);
}));
}
L.w = L.w.map((row, r) => row.map((v, c) => v - lr * dW[r][c]));
L.b = L.b.map((v, c) => v - lr * dB[c]);
}
if (ep % 20 === 0) {
updateViz(xRaw, yRaw, out, mse, ep);
renderWeights();
await new Promise(r => setTimeout(r, delay));
}
}
isTraining = false;
document.getElementById('trainBtn').innerHTML = "►";
toggleDatasetButtons(false);
document.getElementById('trainBtn').innerHTML = "►";
}
function updateViz(x, y, preds, loss, ep) {
document.getElementById('status').innerHTML = `<small>Epoch: ${ep} | Loss: ${loss.toFixed(4)}</small>`;
const inDim = x[0].length;
const outDim = y[0].length;
let traces = [];
let layout = {
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#0f0' }, margin: { t: 0, b: 0, l: 0, r: 0 },
xaxis: {
gridcolor: 'rgba(51, 65, 85, 0.3)', // Very faint grid lines
zerolinecolor: 'rgba(51, 65, 85, 0.5)'
},
yaxis: {
gridcolor: 'rgba(51, 65, 85, 0.3)',
zerolinecolor: 'rgba(51, 65, 85, 0.5)'
}
};
if (inDim === 1) {
// --- 1D REGRESSION (SIN/COS) ---
traces.push({ x: x.map(r => r[0]), y: y.map(r => r[0]), mode: 'markers', name: 'Target' });
traces.push({ x: x.map(r => r[0]), y: preds.map(r => r[0]), mode: 'lines', name: 'Network' });
}
else if (inDim === 2) {
// --- 2D CLASSIFICATION (XOR/MOONS) ---
const isXOR = currentMode.includes('XOR');
const rangeLimit = isXOR ? 1.1 : 1.5; // Tighter view for XOR, wider for Moons
const rangeStart = isXOR ? -0.1 : -0.7;
// 1. Update Layout for Ticks and Visibility
layout.margin = { t: 20, b: 40, l: 50, r: 20 }; // Increased margin for tick labels
layout.xaxis = {
title: { text: "Input X", font: { size: 10 } },
range: [rangeStart, rangeLimit],
gridcolor: '#222',
showticklabels: true,
tickfont: { color: '#00ff41', size: 10 },
tickmode: 'auto',
nticks: 10,
zeroline: true,
zerolinecolor: '#444'
};
layout.yaxis = {
title: { text: "Input Y", font: { size: 10 } },
range: [rangeStart, rangeLimit],
gridcolor: '#222',
showticklabels: true,
tickfont: { color: '#00ff41', size: 10 },
tickmode: 'auto',
nticks: 10,
zeroline: true,
zerolinecolor: '#444'
};
// 2. Generate Heatmap Grid
const resolution = 30;
let gridX = [], gridY = [], gridZ = [];
for (let i = 0; i <= resolution; i++) {
gridX.push(rangeStart + (i / resolution) * (rangeLimit - rangeStart));
gridY.push(rangeStart + (i / resolution) * (rangeLimit - rangeStart));
}
for (let j = 0; j <= resolution; j++) {
let py = gridY[j];
let rowZ = [];
for (let i = 0; i <= resolution; i++) {
let px = gridX[i];
// Forward pass
let out = [px, py];
for (let L of layers) out = L.forward([out])[0];
// Push prediction
rowZ.push(outDim === 3 ? out[0] - out[1] : out[0]);
}
gridZ.push(rowZ); // Now gridZ[j][i] matches [Y][X]
}
traces.push({
x: gridX,
y: gridY,
z: gridZ,
type: 'heatmap',
transpose: false,
colorscale: [
[0, '#FF4136'], // Red
[1, '#2ECC40'] // Green
],
reversescale: false,
opacity: 0.4,
showscale: false,
hoverinfo: 'skip'
});
// 3. Add Data Points
traces.push({
x: x.map(r => r[0]), y: x.map(r => r[1]),
mode: 'markers',
marker: {
color: y.map(r => r[0] > 0.5 ? '#00ff41' : '#ff3131'),
size: 9,
line: { color: '#fff', width: 1 }
},
name: 'Data'
});
} else if (inDim === 3) {
// --- 3D CLASSIFICATION (XOR3/COLOR) ---
// 1. Data Points
traces.push({
x: x.map(r => r[0]), y: x.map(r => r[1]), z: x.map(r => r[2]),
type: 'scatter3d', mode: 'markers',
marker: {
color: currentMode === 'COLOR' ? x.map(r => `rgb(${r[0] * 255},${r[1] * 255},${r[2] * 255})`) : y.map(r => r[0] > 0.5 ? '#00ff41' : '#ff3131'),
size: 5, symbol: 'circle', line: { color: '#fff', width: 1 }
}
});
// 2. Decision Boundary (Slice Plane at Z=0.5)
let sliceX = [], sliceY = [], sliceZ = [];
for (let i = 0; i <= 10; i++) {
let rowX = [], rowY = [], rowZ = [];
for (let j = 0; j <= 10; j++) {
let px = i / 10, py = j / 10, pz = 0.5;
let out = [px, py, pz];
for (let L of layers) out = L.forward([out])[0];
rowX.push(px); rowY.push(py); rowZ.push(out[0]);
}
sliceX.push(rowX); sliceY.push(rowY); sliceZ.push(rowZ);
}
// This adds a translucent "prediction sheet" inside the 3D cube
traces.push({
x: sliceX, y: sliceY, z: sliceZ,
type: 'surface', opacity: 0.3, showscale: false,
colorscale: [
[0, '#FF4136'], // Red
[1, '#2ECC40'] // Green
],
});
layout.scene = {
xaxis: { title: 'R / In1', backgroundcolor: "#000", gridcolor: "#333" },
yaxis: { title: 'G / In2', backgroundcolor: "#000", gridcolor: "#333" },
zaxis: { title: 'B / In3', backgroundcolor: "#000", gridcolor: "#333" },
aspectmode: 'cube'
};
}
Plotly.react('plot', traces, layout);
Plotly.react('lossPlot', [{ y: lossHistory, type: 'scatter', line: { color: 'red' } }],
{ ...layout, margin: { t: 10, b: 30, l: 30, r: 10 }, yaxis: { type: 'log' } });
// This ensures the neuron/weight drawing still happens!
drawNet();
}
function renderWeights() {
let html = "";
layers.forEach((L, i) => {
// Create a container for each layer's weights
html += `<div class="matrix-table">
<div class="layer-title">Layer ${i} → ${i + 1} (${L.act})</div>
<table>`;
// Rows = Input Neurons, Cols = Output Neurons
L.w.forEach((row, rIdx) => {
html += "<tr>";
row.forEach(v => {
// Color coding: Green for positive, Red for negative
const color = isNaN(v) ? '#fbbf24' : v > 0 ? '#10b981' : '#f43f5e';
const opacity = Math.min(1, Math.abs(v));
html += `<td style="color:${color}; opacity:${0.3 + opacity}">${v.toFixed(4)}</td>`;
});
html += "</tr>";
});
html += `</table><div class="layer-title" style="background:#1e293b; padding:2px;">Biases</div><table style="width:100%"><tr>`;
L.b.forEach(v => {
html += `<td>${v.toFixed(4)}</td>`;
});
html += "</tr></table></div>";
});
document.getElementById('matrixDisplay').innerHTML = html;
}
function drawNet() {
const canvas = document.getElementById('netCanvas');
const ctx = canvas.getContext('2d');
canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight;
if (layers.length === 0) return;
const counts = [layers[0].w.length, ...layers.map(l => l.w[0].length)];
const xStep = canvas.width / (counts.length + 1);
const pos = counts.map((c, i) => Array.from({ length: c }, (_, j) => ({ x: xStep * (i + 1), y: (canvas.height / (c + 1)) * (j + 1) })));
layers.forEach((L, lIdx) => {
L.w.forEach((row, i) => row.forEach((w, j) => {
ctx.beginPath(); ctx.moveTo(pos[lIdx][i].x, pos[lIdx][i].y); ctx.lineTo(pos[lIdx + 1][j].x, pos[lIdx + 1][j].y);
ctx.strokeStyle = isNaN(w) ? '#fbbf24' : w > 0 ? `rgba(16, 185, 129, ${Math.abs(w) * 0.4})` : `rgba(244, 63, 94, ${Math.abs(w) * 0.4})`;
ctx.lineWidth = Math.min(5, Math.abs(w) * 2); ctx.stroke();
}));
});
pos.forEach((layerNodes, lIdx) => {
// Determine color: first layer is 'input', others come from layers[lIdx-1]
const actType = lIdx === 0 ? 'input' : layers[lIdx - 1].act;
const nodeColor = actColors[actType] || '#ffffff';
layerNodes.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, 10, 0, 7); // Slightly larger nodes
ctx.shadowBlur = 16;
ctx.shadowColor = nodeColor; // Adds a subtle glow per activation type
ctx.strokeStyle = nodeColor;
ctx.lineWidth = 2.5; // Thickness of the ring
ctx.stroke();
ctx.fillStyle = nodeColor + '33'; // Adds 22 hex transparency (approx 13%)
ctx.fill();
ctx.shadowBlur = 0; // Reset shadow for next draw
});
});
}
function initPlots() {
const config = {
responsive: true,
displayModeBar: false, // Hides the annoying floating menu on mobile
scrollZoom: false // Prevents chart from capturing scroll gestures
};
const emptyLayout = {
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: {
gridcolor: 'rgba(51, 65, 85, 0.3)', // Very faint grid lines
zerolinecolor: 'rgba(51, 65, 85, 0.5)'
},
margin: { t: 20, b: 40, l: 40, r: 20 },
yaxis: {
gridcolor: 'rgba(51, 65, 85, 0.3)',
zerolinecolor: 'rgba(51, 65, 85, 0.5)'
}
};
Plotly.newPlot('plot', [], emptyLayout);
Plotly.newPlot('lossPlot', [], emptyLayout);
}
// Automatically build the initial network and UI on load
window.onload = () => {
initPlots();
const xorBtn = Array.from(document.querySelectorAll('.btn-group button'))
.find(btn => btn.innerText === 'XOR');
console.log('Initializing with XOR dataset...', xorBtn);
xorBtn.classList.add('active-dataset');
setDataset('XOR', xorBtn); // This will trigger buildNetwork() and updateTestUI()
};
// This forces all Plotly charts to resize whenever the window size changes
window.addEventListener('resize', function () {
Plotly.Plots.resize('plot');
Plotly.Plots.resize('lossPlot');
});
// Also call it inside your updateViz function to ensure
// they snap to the correct size during training
function forceResize() {
Plotly.Plots.resize('plot');
Plotly.Plots.resize('lossPlot');
}
// Force charts to re-calculate their width for touch devices
const resizeObserver = new ResizeObserver(() => {
Plotly.Plots.resize('plot');
Plotly.Plots.resize('lossPlot');
drawNet(); // Also keep your canvas in sync
});
// Start observing the dashboard container
resizeObserver.observe(document.querySelector('.dashboard'));
</script>
</body>
</html>