<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Expandable 2x2 Grid with Plotly</title>
<script src="./js/tailwindcss-3.4.17.js"></script>
<script src="./js/plotly-3.3.0.min.js"></script>
<style>
.grid-item {
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
display: flex;
flex-direction: column;
height: 350px;
}
.grid-item.expanded {
height: 600px;
border-color: #3b82f6;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.toggle-icon {
transition: transform 0.3s ease;
}
.expanded .toggle-icon {
transform: rotate(180deg);
}
.chart-container {
width: 100%;
height: 100%;
flex-grow: 1;
}
.action-btn {
opacity: 0.8;
transition: opacity 0.2s, background-color 0.2s, transform 0.1s;
position: relative;
}
.action-btn:hover {
opacity: 1;
background-color: #f3f4f6;
transform: translateY(-1px);
}
#custom-tooltip {
transition: opacity 0.15s ease;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen p-4 md:p-8">
<div class="max-w-6xl mx-auto mb-6">
<h1 class="text-2xl font-bold text-gray-800">System Dashboard</h1>
<p class="text-gray-600">Click the hover icon to cycle through nearest point, X-axis lock, and Y-axis lock modes.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-6xl mx-auto">
<div id="card-1" class="grid-item relative bg-white rounded-2xl shadow-md overflow-hidden border border-gray-200 p-4 col-span-1">
<div class="flex justify-between items-start mb-2">
<h2 class="text-lg font-bold text-gray-800 z-10 pointer-events-none truncate mr-2">1. Low-Pass Response</h2>
<div class="flex items-center space-x-1 z-20 flex-shrink-0">
<button onclick="resetZoom('plot-1')" class="action-btn p-2 rounded-full" data-tooltip="Reset Zoom">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
</button>
<button onclick="zoomIn('plot-1')" class="action-btn p-2 rounded-full" data-tooltip="Zoom In">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7"></path></svg>
</button>
<button onclick="zoomOut('plot-1')" class="action-btn p-2 rounded-full" data-tooltip="Zoom Out">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7"></path></svg>
</button>
<button id="hover-toggle-plot-1" onclick="toggleHoverMode('plot-1')" class="action-btn p-2 rounded-full" data-tooltip="Toggle Hover Mode: Nearest Point">
<svg id="hover-icon-plot-1" class="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 12h16M12 4v16" />
</svg>
</button>
<button onclick="downloadCSV('plot-1', 'low_pass_data')" class="action-btn p-2 rounded-full" data-tooltip="Download CSV">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
</button>
<button onclick="downloadImage('plot-1', 'low_pass_chart')" class="action-btn p-2 rounded-full" data-tooltip="Download Image (PNG)">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
</button>
<button onclick="toggleExpand('card-1')" class="action-btn p-2 rounded-full" data-tooltip="Expand View">
<svg class="toggle-icon w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path></svg>
</button>
</div>
</div>
<div id="plot-1" class="chart-container"></div>
</div>
<div id="card-2" class="grid-item relative bg-white rounded-2xl shadow-md overflow-hidden border border-gray-200 p-4 col-span-1">
<div class="flex justify-between items-start mb-2">
<h2 class="text-lg font-bold text-gray-800 z-10 pointer-events-none truncate mr-2">2. High-Pass Response</h2>
<div class="flex items-center space-x-1 z-20 flex-shrink-0">
<button onclick="resetZoom('plot-2')" class="action-btn p-2 rounded-full" data-tooltip="Reset Zoom">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
</button>
<button onclick="zoomIn('plot-2')" class="action-btn p-2 rounded-full" data-tooltip="Zoom In">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7"></path></svg>
</button>
<button onclick="zoomOut('plot-2')" class="action-btn p-2 rounded-full" data-tooltip="Zoom Out">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7"></path></svg>
</button>
<button id="hover-toggle-plot-2" onclick="toggleHoverMode('plot-2')" class="action-btn p-2 rounded-full" data-tooltip="Toggle Hover Mode: Nearest Point">
<svg id="hover-icon-plot-2" class="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 12h16M12 4v16" />
</svg>
</button>
<button onclick="downloadCSV('plot-2', 'high_pass_data')" class="action-btn p-2 rounded-full" data-tooltip="Download CSV">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
</button>
<button onclick="downloadImage('plot-2', 'high_pass_chart')" class="action-btn p-2 rounded-full" data-tooltip="Download Image (PNG)">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
</button>
<button onclick="toggleExpand('card-2')" class="action-btn p-2 rounded-full" data-tooltip="Expand View">
<svg class="toggle-icon w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path></svg>
</button>
</div>
</div>
<div id="plot-2" class="chart-container"></div>
</div>
<div id="card-3" class="grid-item relative bg-white rounded-2xl shadow-md overflow-hidden border border-gray-200 p-4 col-span-1">
<div class="flex justify-between items-start mb-2">
<h2 class="text-lg font-bold text-gray-800 z-10 pointer-events-none truncate mr-2">3. Band-Pass Filter</h2>
<div class="flex items-center space-x-1 z-20 flex-shrink-0">
<button onclick="resetZoom('plot-3')" class="action-btn p-2 rounded-full" data-tooltip="Reset Zoom">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
</button>
<button onclick="zoomIn('plot-3')" class="action-btn p-2 rounded-full" data-tooltip="Zoom In">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7"></path></svg>
</button>
<button onclick="zoomOut('plot-3')" class="action-btn p-2 rounded-full" data-tooltip="Zoom Out">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7"></path></svg>
</button>
<button id="hover-toggle-plot-3" onclick="toggleHoverMode('plot-3')" class="action-btn p-2 rounded-full" data-tooltip="Toggle Hover Mode: Nearest Point">
<svg id="hover-icon-plot-3" class="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 12h16M12 4v16" />
</svg>
</button>
<button onclick="downloadCSV('plot-3', 'band_pass_data')" class="action-btn p-2 rounded-full" data-tooltip="Download CSV">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
</button>
<button onclick="downloadImage('plot-3', 'band_pass_chart')" class="action-btn p-2 rounded-full" data-tooltip="Download Image (PNG)">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
</button>
<button onclick="toggleExpand('card-3')" class="action-btn p-2 rounded-full" data-tooltip="Expand View">
<svg class="toggle-icon w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path></svg>
</button>
</div>
</div>
<div id="plot-3" class="chart-container"></div>
</div>
<div id="card-4" class="grid-item relative bg-white rounded-2xl shadow-md overflow-hidden border border-gray-200 p-4 col-span-1">
<div class="flex justify-between items-start mb-2">
<h2 class="text-lg font-bold text-gray-800 z-10 pointer-events-none truncate mr-2">4. Notch Filter</h2>
<div class="flex items-center space-x-1 z-20 flex-shrink-0">
<button onclick="resetZoom('plot-4')" class="action-btn p-2 rounded-full" data-tooltip="Reset Zoom">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
</button>
<button onclick="zoomIn('plot-4')" class="action-btn p-2 rounded-full" data-tooltip="Zoom In">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7"></path></svg>
</button>
<button onclick="zoomOut('plot-4')" class="action-btn p-2 rounded-full" data-tooltip="Zoom Out">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7"></path></svg>
</button>
<button id="hover-toggle-plot-4" onclick="toggleHoverMode('plot-4')" class="action-btn p-2 rounded-full" data-tooltip="Toggle Hover Mode: Nearest Point">
<svg id="hover-icon-plot-4" class="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 12h16M12 4v16" />
</svg>
</button>
<button onclick="downloadCSV('plot-4', 'notch_filter_data')" class="action-btn p-2 rounded-full" data-tooltip="Download CSV">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
</button>
<button onclick="downloadImage('plot-4', 'notch_filter_chart')" class="action-btn p-2 rounded-full" data-tooltip="Download Image (PNG)">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
</button>
<button onclick="toggleExpand('card-4')" class="action-btn p-2 rounded-full" data-tooltip="Expand View">
<svg class="toggle-icon w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path></svg>
</button>
</div>
</div>
<div id="plot-4" class="chart-container"></div>
</div>
</div>
<script>
const tooltip = document.createElement('div');
tooltip.id = 'custom-tooltip';
tooltip.className = 'fixed bg-gray-800 text-white text-xs rounded py-1 px-2 z-50 pointer-events-none hidden shadow-lg whitespace-nowrap';
document.body.appendChild(tooltip);
document.addEventListener('mouseover', e => {
const target = e.target.closest('[data-tooltip]');
if (target) {
tooltip.textContent = target.dataset.tooltip;
tooltip.classList.remove('hidden');
const rect = target.getBoundingClientRect();
requestAnimationFrame(() => {
let left = rect.left + rect.width / 2 - tooltip.offsetWidth / 2;
let top = rect.bottom + 8;
if (left + tooltip.offsetWidth > window.innerWidth - 10) {
left = window.innerWidth - tooltip.offsetWidth - 10;
}
if (left < 10) left = 10;
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
});
}
});
document.addEventListener('mouseout', e => {
if (e.target.closest('[data-tooltip]')) {
tooltip.classList.add('hidden');
}
});
const hoverModeMap = {
'plot-1': 'closest',
'plot-2': 'closest',
'plot-3': 'closest',
'plot-4': 'closest'
};
const HOVER_CYCLES = ['closest', 'x', 'y', false];
function updateHoverIcon(plotId, mode) {
const iconContainer = document.getElementById(`hover-icon-${plotId}`);
const button = document.getElementById(`hover-toggle-${plotId}`);
let svgContent = '';
let tooltipText = '';
switch (mode) {
case 'closest':
tooltipText = 'Toggle Hover Mode: Nearest Point';
svgContent = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 12h16M12 4v16" />';
break;
case 'x':
tooltipText = 'Toggle Hover Mode: X-Axis Lock';
svgContent = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12H3M17 8l4 4-4 4M7 8l-4 4 4 4" />';
break;
case 'y':
tooltipText = 'Toggle Hover Mode: Y-Axis Lock';
svgContent = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 21V3M8 17l4 4 4-4M8 7l4-4 4 4" />';
break;
case false:
default:
tooltipText = 'Toggle Hover Mode: Disabled';
svgContent = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />';
break;
}
iconContainer.innerHTML = svgContent;
button.dataset.tooltip = tooltipText;
const plotlyMode = mode === false ? false : mode; Plotly.relayout(plotId, { hovermode: plotlyMode });
}
function toggleHoverMode(plotId) {
const currentMode = hoverModeMap[plotId] === false ? 'off' : hoverModeMap[plotId];
const currentIndex = HOVER_CYCLES.indexOf(currentMode);
const nextIndex = (currentIndex + 1) % HOVER_CYCLES.length;
const newMode = HOVER_CYCLES[nextIndex];
hoverModeMap[plotId] = newMode;
updateHoverIcon(plotId, newMode);
}
function generateLogFreqs(start, end, count) {
const freqs = [];
const step = Math.log10(end / start) / (count - 1);
for (let i = 0; i < count; i++) {
freqs.push(start * Math.pow(10, step * i));
}
return freqs;
}
const freqs = generateLogFreqs(20, 20000, 200);
function lowPass(f, fc) { return -10 * Math.log10(1 + Math.pow(f / fc, 2)); }
function highPass(f, fc) { return -10 * Math.log10(1 + Math.pow(fc / f, 2)); }
function bandPass(f, f0, q) {
const bw = f0 / q;
const val = Math.pow((f * bw), 2) / (Math.pow(f0*f0 - f*f, 2) + Math.pow(f * bw, 2));
return 10 * Math.log10(val);
}
const commonLayout = {
margin: { t: 30, r: 10, b: 40, l: 60 },
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
font: { family: 'Inter, sans-serif' },
showlegend: true,
legend: {
x: 1, xanchor: 'right', y: 1, bgcolor: 'rgba(255,255,255,0.5)',
},
xaxis: {
title: {
text: 'Frequency (Hz)'
},
type: 'log',
gridcolor: '#e5e7eb',
range: [Math.log10(20), Math.log10(20000)]
},
yaxis: {
title: {
text: 'Magnitude (dB)'
},
gridcolor: '#e5e7eb'
},
title: { text: "S21 (dB)" },
hovermode: 'closest' };
const plotlyConfig = {responsive: true, displayModeBar: false};
Plotly.newPlot('plot-1', [
{ x: freqs, y: freqs.map(f => lowPass(f, 1000)), mode: 'lines', name: 'Butterworth', line: { color: '#3b82f6', width: 3 } },
{ x: freqs, y: freqs.map(f => lowPass(f, 2500) - 3), mode: 'lines', name: 'Bessel', line: { color: '#93c5fd', width: 2, dash: 'dash' } }
], commonLayout, plotlyConfig);
Plotly.newPlot('plot-2', [
{ x: freqs, y: freqs.map(f => highPass(f, 500)), mode: 'lines', name: 'Cutoff 500Hz', line: { color: '#a855f7', width: 3 } },
{ x: freqs, y: freqs.map(f => highPass(f, 100) - 6), mode: 'lines', name: 'Cutoff 100Hz', line: { color: '#d8b4fe', width: 2 } }
], commonLayout, plotlyConfig);
Plotly.newPlot('plot-3', [
{ x: freqs, y: freqs.map(f => bandPass(f, 1000, 2)), mode: 'lines', name: 'Wide (Q=2)', line: { color: '#f97316', width: 3 } },
{ x: freqs, y: freqs.map(f => bandPass(f, 1000, 10)), mode: 'lines', name: 'Narrow (Q=10)', line: { color: '#fdba74', width: 2 } }
], commonLayout, plotlyConfig);
const notchY = freqs.map(f => {
let val = 0;
if (f > 50 && f < 70) val -= 20 * Math.exp(-Math.pow(f - 60, 2) / 10);
if (f > 3000 && f < 5000) val -= 15 * Math.exp(-Math.pow(f - 4000, 2) / 50000);
val += lowPass(f, 15000);
return val;
});
Plotly.newPlot('plot-4', [
{ x: freqs, y: notchY, mode: 'lines', name: 'System', line: { color: '#10b981', width: 2 } },
{ x: freqs, y: freqs.map(() => 0), mode: 'lines', name: 'Reference', line: { color: '#d1fae5', width: 2, dash: 'dot' } }
], commonLayout, plotlyConfig);
function resetZoom(divId) {
Plotly.relayout(divId, {
'xaxis.autorange': true,
'yaxis.autorange': true
});
}
function zoomPlot(divId, factor) {
const gd = document.getElementById(divId);
if (!gd || !gd._fullLayout) return;
const xaxis = gd._fullLayout.xaxis;
const yaxis = gd._fullLayout.yaxis;
const getNewRange = (range, isLog) => {
const min = range[0];
const max = range[1];
const center = (max + min) / 2;
const span = max - min;
const newSpan = span * factor;
return [center - newSpan / 2, center + newSpan / 2];
};
const newX = getNewRange(xaxis.range, xaxis.type === 'log');
const newY = getNewRange(yaxis.range, yaxis.type === 'log');
Plotly.relayout(divId, {
'xaxis.range': newX,
'yaxis.range': newY,
'xaxis.autorange': false,
'yaxis.autorange': false
});
}
function zoomIn(divId) { zoomPlot(divId, 0.7); }
function zoomOut(divId) { zoomPlot(divId, 1.3); }
function downloadImage(divId, filename) {
const plot = document.getElementById(divId);
Plotly.downloadImage(plot, {
format: 'png',
filename: filename,
height: 800,
width: 1200,
scale: 2 });
}
function downloadCSV(divId, filename) {
const plot = document.getElementById(divId);
const data = plot.data;
let csvContent = "data:text/csv;charset=utf-8,";
let headers = ["Frequency (Hz)"];
data.forEach(trace => {
headers.push(`Magnitude (dB) - ${trace.name}`);
});
csvContent += headers.join(",") + "\r\n";
const xValues = data[0].x;
for (let i = 0; i < xValues.length; i++) {
let row = [xValues[i]];
data.forEach(trace => {
row.push(trace.y[i] !== undefined ? trace.y[i] : "");
});
csvContent += row.join(",") + "\r\n";
}
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", filename + ".csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function toggleExpand(cardId) {
const card = document.getElementById(cardId);
const allCards = document.querySelectorAll('.grid-item');
const isExpanded = card.classList.contains('expanded');
allCards.forEach(item => {
item.classList.remove('expanded');
item.classList.remove('md:col-span-2');
item.classList.remove('row-span-2');
});
if (!isExpanded) {
card.classList.add('expanded');
card.classList.add('md:col-span-2');
card.classList.add('row-span-2');
}
setTimeout(() => {
allCards.forEach(item => {
const plotDiv = item.querySelector('.chart-container');
Plotly.Plots.resize(plotDiv);
});
}, 450);
}
window.addEventListener('resize', () => {
const plots = document.querySelectorAll('.chart-container');
plots.forEach(p => Plotly.Plots.resize(p));
});
</script>
</body>
</html>