/**
* WiFi Channel Utilization Chart Component
*
* Displays channel utilization as a bar chart with recommendations.
* Shows AP count, client count, and utilization score per channel.
*/
const ChannelChart = (function() {
'use strict';
// ==========================================================================
// Configuration
// ==========================================================================
const CONFIG = {
height: 120,
barWidth: 14,
barSpacing: 2,
padding: { top: 15, right: 10, bottom: 25, left: 30 },
colors: {
low: '#22c55e', // Green - low utilization
medium: '#eab308', // Yellow - medium
high: '#ef4444', // Red - high
recommended: '#3b82f6', // Blue - recommended
},
thresholds: {
low: 0.3,
medium: 0.6,
},
};
// 2.4 GHz non-overlapping channels
const CHANNELS_2_4 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const NON_OVERLAPPING_2_4 = [1, 6, 11];
// 5 GHz channels (non-DFS)
const CHANNELS_5 = [36, 40, 44, 48, 149, 153, 157, 161, 165];
// ==========================================================================
// State
// ==========================================================================
let container = null;
let currentBand = '2.4';
let channelStats = [];
let recommendations = [];
// ==========================================================================
// Initialization
// ==========================================================================
function init(containerId, options = {}) {
container = document.getElementById(containerId);
if (!container) {
console.warn('[ChannelChart] Container not found:', containerId);
return;
}
Object.assign(CONFIG, options);
render();
}
// ==========================================================================
// Update
// ==========================================================================
function update(stats, recs) {
channelStats = stats || [];
recommendations = recs || [];
render();
}
function setBand(band) {
currentBand = band;
render();
}
// ==========================================================================
// Rendering
// ==========================================================================
function render() {
if (!container) return;
const channels = currentBand === '2.4' ? CHANNELS_2_4 : CHANNELS_5;
const nonOverlapping = currentBand === '2.4' ? NON_OVERLAPPING_2_4 : CHANNELS_5;
// Build stats map
const statsMap = {};
channelStats.forEach(s => {
statsMap[s.channel] = s;
});
// Build recommendations map
const recsMap = {};
recommendations.forEach((r, i) => {
recsMap[r.channel] = { rank: i + 1, ...r };
});
// Calculate dimensions
const width = channels.length * (CONFIG.barWidth + CONFIG.barSpacing) + CONFIG.padding.left + CONFIG.padding.right;
const height = CONFIG.height + CONFIG.padding.top + CONFIG.padding.bottom;
const chartHeight = CONFIG.height;
// Find max values for scaling
let maxApCount = 1;
channelStats.forEach(s => {
if (s.ap_count > maxApCount) maxApCount = s.ap_count;
});
// Build SVG with viewBox for responsive scaling
let svg = `
`;
// Add legend
svg += renderLegend();
// Add recommendations
if (recommendations.length > 0) {
svg += renderRecommendations();
}
container.innerHTML = svg;
}
function renderYAxis(chartHeight, maxApCount) {
const ticks = [];
const tickCount = Math.min(5, maxApCount);
const step = Math.ceil(maxApCount / tickCount);
for (let i = 0; i <= maxApCount; i += step) {
const y = CONFIG.padding.top + chartHeight - (i / maxApCount * chartHeight);
ticks.push(`