(stats)
| 2760 | ctx.fillStyle = '#64748b'; ctx.textAlign = 'center'; ctx.font = `${11 * K}px sans-serif`; |
| 2761 | ctx.fillText('No APs on selected band', W / 2, H / 2); |
| 2762 | canvas._wifiGeom = null; |
| 2763 | return; |
| 2764 | } |
| 2765 | // Per-band channel range + x segment |
| 2766 | const gap = Math.round(14 * K), segW = (plotW - gap * (bands.length - 1)) / bands.length; |
| 2767 | const ranges = {}; |
| 2768 | bands.forEach((b, i) => { |
| 2769 | // The range has to cover the channels an AP actually *occupies*, not |
| 2770 | // just its centre — otherwise a 160 MHz BSS draws off the end of its |
| 2771 | // band segment and over the neighbouring one. |
| 2772 | const list = d.aps.filter(a => a.band === b); |
| 2773 | const edges = []; |
| 2774 | list.forEach(a => { |
| 2775 | const c = a.center_freq ? _wifiFreqToChannel(a.center_freq) : a.channel; |
| 2776 | const half = (a.width || 20) / 10; |
| 2777 | edges.push(a.channel, c - half, c + half); |
| 2778 | }); |
| 2779 | let lo = Math.min(...edges), hi = Math.max(...edges); |
| 2780 | if (b === '2.4') { lo = Math.min(lo, 1); hi = Math.max(hi, 13); } |
| 2781 | const padCh = b === '2.4' ? 0.5 : 2; |
| 2782 | lo -= padCh; hi += padCh; if (hi - lo < 4) { hi += 2; lo -= 2; } |
| 2783 | const x0 = padL + i * (segW + gap); |
| 2784 | ranges[b] = { lo, hi, x0, x1: x0 + segW }; |
| 2785 | }); |
| 2786 | const xFor = (band, ch) => { const r = ranges[band]; return r.x0 + (ch - r.lo) / (r.hi - r.lo) * (r.x1 - r.x0); }; |
| 2787 | const chFor = (band, x) => { const r = ranges[band]; return r.lo + (x - r.x0) / (r.x1 - r.x0) * (r.hi - r.lo); }; |
| 2788 | // Band labels + channel ticks + DFS shading |
| 2789 | ctx.textAlign = 'center'; |
| 2790 | bands.forEach(b => { |
| 2791 | const r = ranges[b]; |
| 2792 | ctx.fillStyle = _WIFI_BAND_COLOR[b]; ctx.font = `bold ${11 * K}px sans-serif`; |
| 2793 | ctx.fillText(b + ' GHz', (r.x0 + r.x1) / 2, padT - 4); |
| 2794 | // DFS channel shading |
| 2795 | (d.radar_channels[b] || []).forEach(ch => { |
| 2796 | if (ch < r.lo || ch > r.hi) return; |
| 2797 | const cx = xFor(b, ch); |
| 2798 | ctx.fillStyle = 'rgba(167,139,250,0.08)'; |
| 2799 | ctx.fillRect(cx - 3 * K, padT, 6 * K, plotH); |
| 2800 | }); |
| 2801 | // Channel ticks (channels present) |
| 2802 | ctx.fillStyle = '#64748b'; ctx.font = `${9 * K}px sans-serif`; |
| 2803 | const present = [...new Set(d.aps.filter(a => a.band === b).map(a => a.channel))].sort((x, y) => x - y); |
| 2804 | present.forEach(ch => ctx.fillText(ch + '', xFor(b, ch), H - padB + 12 * K)); |
no test coverage detected