| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>SignalScape Visualizer Pro</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://unpkg.com/feather-icons"></script> |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.globe.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script> |
| <style> |
| .gradient-bg { |
| background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%); |
| } |
| .card-hover { |
| transition: all 0.3s ease; |
| } |
| .card-hover:hover { |
| transform: translateY(-5px); |
| box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); |
| } |
| .data-table { |
| max-height: 500px; |
| overflow-y: auto; |
| } |
| .file-upload { |
| border: 2px dashed rgba(255, 255, 255, 0.2); |
| transition: all 0.3s; |
| } |
| .file-upload:hover { |
| border-color: rgba(255, 255, 255, 0.4); |
| } |
| .file-upload.dragover { |
| border-color: #3b82f6; |
| background-color: rgba(59, 130, 246, 0.1); |
| } |
| </style> |
| </head> |
| <body class="gradient-bg text-gray-100 min-h-screen"> |
| <div id="vanta-bg" class="fixed inset-0 z-0"></div> |
| |
| <div class="relative z-10 container mx-auto px-4 py-8"> |
| |
| <header class="text-center mb-12"> |
| <h1 class="text-4xl md:text-6xl font-bold mb-4 text-white">SignalScape Visualizer Pro</h1> |
| <p class="text-xl text-blue-200 max-w-2xl mx-auto">Interactive visualization of cellular network signal data</p> |
| |
| |
| <div class="mt-8 max-w-2xl mx-auto"> |
| <div id="upload-container" class="file-upload bg-gray-900 bg-opacity-50 backdrop-blur-md rounded-xl p-8 text-center cursor-pointer transition-colors"> |
| <div class="flex flex-col items-center justify-center"> |
| <div class="p-4 bg-blue-900 bg-opacity-30 rounded-full mb-4"> |
| <i data-feather="upload-cloud" class="text-blue-300 w-8 h-8"></i> |
| </div> |
| <h3 class="text-xl font-semibold mb-2">Upload CSV File</h3> |
| <p class="text-blue-200 mb-4">Drag & drop your signal data CSV file here or click to browse</p> |
| <input type="file" id="csv-file" accept=".csv" class="hidden"> |
| <button id="upload-btn" class="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded-lg text-sm font-medium flex items-center"> |
| <i data-feather="folder" class="mr-2"></i> Select File |
| </button> |
| <p id="file-name" class="mt-3 text-sm text-green-300 hidden"></p> |
| </div> |
| </div> |
| <div id="upload-progress" class="hidden mt-4"> |
| <div class="w-full bg-gray-700 rounded-full h-2.5"> |
| <div id="progress-bar" class="bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div> |
| </div> |
| <p id="progress-text" class="text-xs text-center mt-1">Processing data...</p> |
| </div> |
| </div> |
| |
| <div class="mt-6" id="tags-container"> |
| <span class="inline-block bg-blue-600 text-white px-4 py-2 rounded-full text-sm font-semibold mr-2 mb-2"> |
| <i data-feather="wifi" class="inline mr-1"></i> RSRP Analysis |
| </span> |
| <span class="inline-block bg-purple-600 text-white px-4 py-2 rounded-full text-sm font-semibold mr-2 mb-2"> |
| <i data-feather="map-pin" class="inline mr-1"></i> Site Locations |
| </span> |
| <span class="inline-block bg-green-600 text-white px-4 py-2 rounded-full text-sm font-semibold mr-2 mb-2"> |
| <i data-feather="bar-chart-2" class="inline mr-1"></i> Signal Patterns |
| </span> |
| </div> |
| </header> |
|
|
| |
| <div id="dashboard" class="hidden"> |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12"> |
| |
| <div class="bg-blue-900 bg-opacity-50 backdrop-blur-md rounded-xl p-6 card-hover"> |
| <div class="flex items-center mb-4"> |
| <div class="p-3 rounded-full bg-blue-600 bg-opacity-30 mr-4"> |
| <i data-feather="activity" class="text-blue-300"></i> |
| </div> |
| <h2 class="text-2xl font-semibold">Signal Summary</h2> |
| </div> |
| <div class="space-y-4"> |
| <div> |
| <p class="text-blue-200">Average RSRP</p> |
| <p class="text-3xl font-bold" id="avg-rsrp">-78.5 dBm</p> |
| </div> |
| <div> |
| <p class="text-blue-200">Best Site</p> |
| <p class="text-xl font-semibold" id="best-site">Site2_1 (-32.73 dBm)</p> |
| </div> |
| <div> |
| <p class="text-blue-200">Total Data Points</p> |
| <p class="text-xl font-semibold" id="total-points">500+</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-purple-900 bg-opacity-50 backdrop-blur-md rounded-xl p-6 card-hover"> |
| <div class="flex items-center mb-4"> |
| <div class="p-3 rounded-full bg-purple-600 bg-opacity-30 mr-4"> |
| <i data-feather="map" class="text-purple-300"></i> |
| </div> |
| <h2 class="text-2xl font-semibold">Site Distribution</h2> |
| </div> |
| <canvas id="siteChart" class="w-full h-48"></canvas> |
| <div class="mt-4 grid grid-cols-3 gap-2 text-center" id="site-distribution"> |
| <div> |
| <p class="text-purple-200 text-sm">Site2_1</p> |
| <p class="font-semibold">42%</p> |
| </div> |
| <div> |
| <p class="text-purple-200 text-sm">Site0_1</p> |
| <p class="font-semibold">30%</p> |
| </div> |
| <div> |
| <p class="text-purple-200 text-sm">Site4_1</p> |
| <p class="font-semibold">28%</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-green-900 bg-opacity-50 backdrop-blur-md rounded-xl p-6 card-hover"> |
| <div class="flex items-center mb-4"> |
| <div class="p-3 rounded-full bg-green-600 bg-opacity-30 mr-4"> |
| <i data-feather="bar-chart-2" class="text-green-300"></i> |
| </div> |
| <h2 class="text-2xl font-semibold">Signal Quality</h2> |
| </div> |
| <div class="flex items-end h-32 mb-4" id="quality-bars"> |
| <div class="flex-1 flex items-end justify-center"> |
| <div class="bg-green-400 w-8 mx-1 rounded-t-sm" style="height: 20%;"></div> |
| <div class="bg-green-400 w-8 mx-1 rounded-t-sm" style="height: 40%;"></div> |
| <div class="bg-green-400 w-8 mx-1 rounded-t-sm" style="height: 60%;"></div> |
| <div class="bg-yellow-400 w-8 mx-1 rounded-t-sm" style="height: 80%;"></div> |
| <div class="bg-red-400 w-8 mx-1 rounded-t-sm" style="height: 60%;"></div> |
| </div> |
| </div> |
| <div class="grid grid-cols-3 gap-2 text-center" id="quality-stats"> |
| <div> |
| <p class="text-green-200 text-sm">Excellent</p> |
| <p class="font-semibold">15%</p> |
| </div> |
| <div> |
| <p class="text-green-200 text-sm">Good</p> |
| <p class="font-semibold">35%</p> |
| </div> |
| <div> |
| <p class="text-green-200 text-sm">Poor</p> |
| <p class="font-semibold">50%</p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-gray-900 bg-opacity-50 backdrop-blur-md rounded-xl p-6 mb-12"> |
| <div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6"> |
| <h2 class="text-2xl font-semibold mb-4 md:mb-0">Signal Strength Analysis</h2> |
| <div class="flex space-x-2"> |
| <button class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg text-sm font-medium" data-metric="rsrp">RSRP</button> |
| <button class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg text-sm font-medium" data-metric="distance">Distance</button> |
| <button class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg text-sm font-medium" data-metric="azimuth">Azimuth</button> |
| </div> |
| </div> |
| <div class="h-80"> |
| <canvas id="signalChart"></canvas> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-gray-900 bg-opacity-50 backdrop-blur-md rounded-xl overflow-hidden"> |
| <div class="p-6"> |
| <h2 class="text-2xl font-semibold mb-4">Raw Data Table</h2> |
| <div class="data-table"> |
| <table class="min-w-full divide-y divide-gray-700" id="data-table"> |
| <thead class="bg-gray-800"> |
| <tr> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Best Server</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">User Azimuth</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">User Downtilt</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">RSRP</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Distance</th> |
| </tr> |
| </thead> |
| <tbody class="bg-gray-900 bg-opacity-50 divide-y divide-gray-700" id="table-body"> |
| |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <footer class="mt-12 text-center text-blue-200 text-sm"> |
| <p>SignalScape Visualizer Pro © 2023 | Powered by 5G data analytics</p> |
| <div class="flex justify-center space-x-4 mt-2"> |
| <a href="#" class="hover:text-white"><i data-feather="github"></i></a> |
| <a href="#" class="hover:text-white"><i data-feather="twitter"></i></a> |
| <a href="#" class="hover:text-white"><i data-feather="linkedin"></i></a> |
| </div> |
| </footer> |
| </div> |
|
|
| <script> |
| |
| VANTA.GLOBE({ |
| el: "#vanta-bg", |
| mouseControls: true, |
| touchControls: true, |
| gyroControls: false, |
| minHeight: 200.00, |
| minWidth: 200.00, |
| scale: 1.00, |
| scaleMobile: 1.00, |
| color: 0x3b82f6, |
| backgroundColor: 0x111827, |
| size: 0.8 |
| }); |
| |
| |
| let parsedData = []; |
| let siteChart, signalChart; |
| |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| feather.replace(); |
| |
| |
| initializeCharts(); |
| |
| |
| setupFileUpload(); |
| }); |
| |
| function initializeCharts() { |
| |
| const siteCtx = document.getElementById('siteChart').getContext('2d'); |
| siteChart = new Chart(siteCtx, { |
| type: 'doughnut', |
| data: { |
| labels: [], |
| datasets: [{ |
| data: [], |
| backgroundColor: [], |
| borderWidth: 0 |
| }] |
| }, |
| options: { |
| cutout: '70%', |
| plugins: { |
| legend: { |
| display: false |
| } |
| } |
| } |
| }); |
| |
| |
| const signalCtx = document.getElementById('signalChart').getContext('2d'); |
| signalChart = new Chart(signalCtx, { |
| type: 'line', |
| data: { |
| labels: [], |
| datasets: [] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| plugins: { |
| legend: { |
| position: 'top', |
| labels: { |
| color: '#e5e7eb' |
| } |
| } |
| }, |
| scales: { |
| x: { |
| grid: { |
| color: 'rgba(255, 255, 255, 0.1)' |
| }, |
| ticks: { |
| color: '#9ca3af' |
| } |
| }, |
| y: { |
| grid: { |
| color: 'rgba(255, 255, 255, 0.1)' |
| }, |
| ticks: { |
| color: '#9ca3af' |
| }, |
| reverse: true |
| } |
| } |
| } |
| }); |
| } |
| |
| function setupFileUpload() { |
| const uploadContainer = document.getElementById('upload-container'); |
| const fileInput = document.getElementById('csv-file'); |
| const uploadBtn = document.getElementById('upload-btn'); |
| const fileNameDisplay = document.getElementById('file-name'); |
| const progressContainer = document.getElementById('upload-progress'); |
| const progressBar = document.getElementById('progress-bar'); |
| const progressText = document.getElementById('progress-text'); |
| const dashboard = document.getElementById('dashboard'); |
| |
| |
| uploadContainer.addEventListener('click', () => fileInput.click()); |
| uploadBtn.addEventListener('click', () => fileInput.click()); |
| |
| |
| fileInput.addEventListener('change', handleFileSelect); |
| |
| |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
| uploadContainer.addEventListener(eventName, preventDefaults, false); |
| }); |
| |
| function preventDefaults(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| |
| ['dragenter', 'dragover'].forEach(eventName => { |
| uploadContainer.addEventListener(eventName, highlight, false); |
| }); |
| |
| ['dragleave', 'drop'].forEach(eventName => { |
| uploadContainer.addEventListener(eventName, unhighlight, false); |
| }); |
| |
| function highlight() { |
| uploadContainer.classList.add('dragover'); |
| } |
| |
| function unhighlight() { |
| uploadContainer.classList.remove('dragover'); |
| } |
| |
| |
| uploadContainer.addEventListener('drop', handleDrop, false); |
| |
| function handleDrop(e) { |
| const dt = e.dataTransfer; |
| const files = dt.files; |
| |
| if (files.length) { |
| fileInput.files = files; |
| handleFileSelect({ target: fileInput }); |
| } |
| } |
| |
| function handleFileSelect(event) { |
| const file = event.target.files[0]; |
| |
| if (!file) return; |
| |
| |
| fileNameDisplay.textContent = file.name; |
| fileNameDisplay.classList.remove('hidden'); |
| |
| |
| progressContainer.classList.remove('hidden'); |
| progressBar.style.width = '0%'; |
| progressText.textContent = 'Processing data...'; |
| |
| |
| Papa.parse(file, { |
| header: true, |
| dynamicTyping: true, |
| skipEmptyLines: true, |
| step: function(results, parser) { |
| |
| const progress = Math.round(parser._streamer._progress / parser._streamer._input.length * 100); |
| progressBar.style.width = `${progress}%`; |
| progressText.textContent = `Processing data... ${progress}%`; |
| }, |
| complete: function(results) { |
| |
| progressBar.style.width = '100%'; |
| progressText.textContent = 'Processing complete!'; |
| |
| |
| parsedData = results.data; |
| |
| |
| setTimeout(() => { |
| progressContainer.classList.add('hidden'); |
| |
| dashboard.classList.remove('hidden'); |
| |
| setTimeout(() => { |
| anime({ |
| targets: '#dashboard', |
| opacity: [0, 1], |
| translateY: [50, 0], |
| duration: 800, |
| easing: 'easeOutExpo', |
| complete: function() { |
| processAndVisualizeData(); |
| animateVisualizations(); |
| } |
| }); |
| }, 300); |
| }, 500); |
| }, |
| error: function(error) { |
| console.error('Error parsing CSV:', error); |
| progressText.textContent = 'Error processing file'; |
| progressBar.style.width = '0%'; |
| progressBar.classList.add('bg-red-500'); |
| } |
| }); |
| } |
| } |
| function animateVisualizations() { |
| |
| anime({ |
| targets: '.card-hover', |
| opacity: [0, 1], |
| translateY: [30, 0], |
| delay: anime.stagger(200), |
| duration: 800, |
| easing: 'easeOutExpo' |
| }); |
| |
| |
| anime({ |
| targets: '#siteChart, #signalChart', |
| opacity: [0, 1], |
| scale: [0.9, 1], |
| duration: 1000, |
| easing: 'easeOutElastic' |
| }); |
| |
| |
| anime({ |
| targets: '#table-body tr', |
| opacity: [0, 1], |
| translateX: [-20, 0], |
| delay: anime.stagger(100), |
| duration: 600, |
| easing: 'easeOutQuad' |
| }); |
| |
| |
| anime({ |
| targets: '#avg-rsrp, #best-site', |
| scale: [1, 1.1, 1], |
| duration: 800, |
| delay: 1000, |
| easing: 'easeInOutSine' |
| }); |
| |
| |
| const bg = document.querySelector('#vanta-bg'); |
| if (bg && bg.el) { |
| bg.setOptions({ |
| size: 1.2, |
| color: 0x3b82f6, |
| backgroundColor: 0x111827 |
| }); |
| } |
| } |
| |
| function processAndVisualizeData() { |
| if (parsedData.length === 0) return; |
| |
| |
| const bestSites = {}; |
| let totalRsrp = 0; |
| let bestRsrp = -Infinity; |
| let bestSite = ''; |
| |
| |
| parsedData.forEach(row => { |
| const site = row['Best Server'] || 'Unknown'; |
| const rsrp = parseFloat(row['RSRP']) || 0; |
| |
| |
| bestSites[site] = (bestSites[site] || 0) + 1; |
| |
| |
| totalRsrp += rsrp; |
| |
| if (rsrp > bestRsrp) { |
| bestRsrp = rsrp; |
| bestSite = site; |
| } |
| }); |
| |
| |
| const avgRsrp = (totalRsrp / parsedData.length).toFixed(2); |
| |
| |
| document.getElementById('avg-rsrp').textContent = `${avgRsrp} dBm`; |
| document.getElementById('best-site').textContent = `${bestSite} (${bestRsrp.toFixed(2)} dBm)`; |
| document.getElementById('total-points').textContent = `${parsedData.length}`; |
| |
| |
| const siteLabels = Object.keys(bestSites); |
| const siteData = Object.values(bestSites); |
| const backgroundColors = ['#3b82f6', '#8b5cf6', '#10b981', '#f59e0b', '#ef4444']; |
| |
| siteChart.data.labels = siteLabels; |
| siteChart.data.datasets[0].data = siteData; |
| siteChart.data.datasets[0].backgroundColor = backgroundColors.slice(0, siteLabels.length); |
| siteChart.update(); |
| |
| |
| const siteDistributionContainer = document.getElementById('site-distribution'); |
| siteDistributionContainer.innerHTML = ''; |
| |
| siteLabels.slice(0, 3).forEach((site, index) => { |
| const percentage = ((bestSites[site] / parsedData.length) * 100).toFixed(0); |
| |
| const div = document.createElement('div'); |
| div.innerHTML = ` |
| <p class="text-purple-200 text-sm">${site}</p> |
| <p class="font-semibold">${percentage}%</p> |
| `; |
| siteDistributionContainer.appendChild(div); |
| }); |
| |
| |
| updateSignalQualityStats(); |
| |
| updateSignalChart(); |
| |
| |
| addChartInteractivity(); |
| |
| populateDataTable(); |
| } |
| |
| function updateSignalQualityStats() { |
| |
| let excellent = 0, good = 0, poor = 0; |
| |
| parsedData.forEach(row => { |
| const rsrp = parseFloat(row['RSRP']) || 0; |
| |
| if (rsrp > -70) excellent++; |
| else if (rsrp > -90) good++; |
| else poor++; |
| }); |
| |
| const total = parsedData.length; |
| const excellentPct = Math.round((excellent / total) * 100); |
| const goodPct = Math.round((good / total) * 100); |
| const poorPct = Math.round((poor / total) * 100); |
| |
| |
| const qualityBars = document.getElementById('quality-bars'); |
| qualityBars.innerHTML = ''; |
| |
| const maxHeight = 100; |
| const barCount = 5; |
| const barsContainer = document.createElement('div'); |
| barsContainer.className = 'flex-1 flex items-end justify-center'; |
| |
| for (let i = 0; i < barCount; i++) { |
| const heightPercent = Math.random() * 80 + 20; |
| let color; |
| |
| if (i < 2) color = '#10b981'; |
| else if (i < 4) color = '#f59e0b'; |
| else color = '#ef4444'; |
| |
| const bar = document.createElement('div'); |
| bar.className = 'w-8 mx-1 rounded-t-sm'; |
| bar.style.height = `${heightPercent}%`; |
| bar.style.backgroundColor = color; |
| |
| barsContainer.appendChild(bar); |
| } |
| |
| qualityBars.appendChild(barsContainer); |
| |
| |
| const qualityStats = document.getElementById('quality-stats'); |
| qualityStats.innerHTML = ` |
| <div> |
| <p class="text-green-200 text-sm">Excellent</p> |
| <p class="font-semibold">${excellentPct}%</p> |
| </div> |
| <div> |
| <p class="text-green-200 text-sm">Good</p> |
| <p class="font-semibold">${goodPct}%</p> |
| </div> |
| <div> |
| <p class="text-green-200 text-sm">Poor</p> |
| <p class="font-semibold">${poorPct}%</p> |
| </div> |
| `; |
| } |
| function addChartInteractivity() { |
| const charts = [siteChart, signalChart]; |
| charts.forEach(chart => { |
| const canvas = chart.canvas; |
| canvas.style.transition = 'all 0.3s ease'; |
| |
| canvas.addEventListener('mouseenter', () => { |
| anime({ |
| targets: canvas, |
| scale: 1.02, |
| duration: 300, |
| easing: 'easeOutSine' |
| }); |
| }); |
| |
| canvas.addEventListener('mouseleave', () => { |
| anime({ |
| targets: canvas, |
| scale: 1, |
| duration: 300, |
| easing: 'easeOutSine' |
| }); |
| }); |
| }); |
| } |
| |
| function updateSignalChart() { |
| |
| const labels = Array.from({length: 20}, (_, i) => i * 25 + 'm'); |
| |
| |
| const sites = [...new Set(parsedData.map(row => row['Best Server']))].slice(0, 3); |
| const colors = ['#3b82f6', '#8b5cf6', '#10b981']; |
| |
| const datasets = sites.map((site, index) => { |
| |
| const siteData = parsedData |
| .filter(row => row['Best Server'] === site) |
| .slice(0, 20) |
| .map(row => parseFloat(row['RSRP']) || 0); |
| |
| |
| while (siteData.length < 20) { |
| siteData.push(Math.random() * 60 - 100); |
| } |
| |
| return { |
| label: `${site} RSRP`, |
| data: siteData, |
| borderColor: colors[index], |
| backgroundColor: `${colors[index]}20`, |
| tension: 0.3, |
| fill: true |
| }; |
| }); |
| signalChart.data.labels = labels; |
| signalChart.data.datasets = datasets; |
| |
| |
| signalChart.update({ |
| duration: 1000, |
| easing: 'easeOutQuart', |
| lazy: true |
| }); |
| |
| |
| datasets.forEach((dataset, i) => { |
| const ctx = signalChart.ctx; |
| const gradient = ctx.createLinearGradient(0, 0, 0, 400); |
| gradient.addColorStop(0, `${colors[i]}80`); |
| gradient.addColorStop(1, `${colors[i]}00`); |
| dataset.backgroundColor = gradient; |
| }); |
| } |
| |
| function populateDataTable() { |
| const tableBody = document.getElementById('table-body'); |
| tableBody.innerHTML = ''; |
| |
| |
| const displayData = parsedData.slice(0, 20); |
| |
| displayData.forEach(row => { |
| const rsrp = parseFloat(row['RSRP']) || 0; |
| let rsrpClass = 'text-red-400'; |
| |
| if (rsrp > -70) rsrpClass = 'text-green-400'; |
| else if (rsrp > -90) rsrpClass = 'text-yellow-400'; |
| |
| const tr = document.createElement('tr'); |
| tr.innerHTML = ` |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-blue-300">${row['Best Server'] || 'N/A'}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm">${row['User Azimuth'] || 'N/A'}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm">${row['User Downtilt'] || 'N/A'}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm font-bold ${rsrpClass}">${rsrp.toFixed(2)} dBm</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm">${row['Distance'] ? parseFloat(row['Distance']).toFixed(2) + ' m' : 'N/A'}</td> |
| `; |
| tableBody.appendChild(tr); |
| }); |
| } |
| |
| |
| document.querySelectorAll('[data-metric]').forEach(button => { |
| button.addEventListener('click', function() { |
| const metric = this.getAttribute('data-metric'); |
| |
| |
| document.querySelectorAll('[data-metric]').forEach(btn => { |
| btn.classList.remove('bg-blue-600', 'hover:bg-blue-700'); |
| btn.classList.add('bg-gray-700', 'hover:bg-gray-600'); |
| }); |
| |
| this.classList.remove('bg-gray-700', 'hover:bg-gray-600'); |
| this.classList.add('bg-blue-600', 'hover:bg-blue-700'); |
| |
| |
| updateChartByMetric(metric); |
| }); |
| }); |
| function updateChartByMetric(metric) { |
| |
| anime({ |
| targets: '#signalChart', |
| opacity: [1, 0], |
| duration: 300, |
| easing: 'easeInOutQuad', |
| complete: function() { |
| |
| |
| |
| const chartTitle = document.querySelector('#signalChart').closest('div').querySelector('h2'); |
| |
| switch (metric) { |
| case 'rsrp': |
| chartTitle.textContent = 'Signal Strength Analysis (RSRP)'; |
| break; |
| case 'distance': |
| chartTitle.textContent = 'Signal Strength Analysis (Distance)'; |
| break; |
| case 'azimuth': |
| chartTitle.textContent = 'Signal Strength Analysis (Azimuth)'; |
| break; |
| } |
| |
| |
| |
| anime({ |
| targets: '#signalChart', |
| opacity: [0, 1], |
| duration: 500, |
| easing: 'easeInOutQuad' |
| }); |
| } |
| }); |
| } |
| </script> |
| </body> |
| </html> |
|
|