Files
k5bss.com/js/k5bss.js
T

310 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ─── DATA ───────────────────────────────────────────────────
const RAW_DATA = [{"call":"WE2M","name":"ROBERT A HOGUE","date":"2021-01-24","time":"0553","band":"40M","mode":"FT8","rst_sent":"-20","rst_rcvd":"-10","country":"United States","state":"WA","qth":"E WENATCHEE","lotw":"Y","dist":"2782","grid":"CN97UJ"},{"call":"WD8AXA","name":"CHARLES D AGOSTI","date":"2021-01-24","time":"0555","band":"40M","mode":"FT8","rst_sent":"-07","rst_rcvd":"-11","country":"United States","state":"MI","qth":"PORTAGE","lotw":"N","dist":"1291","grid":"EN72ef"},{"call":"KG4IXS","name":"RICHARD A VAUGHAN","date":"2021-01-24","time":"0558","band":"40M","mode":"FT8","rst_sent":"+00","rst_rcvd":"-13","country":"United States","state":"VA","qth":"CHATHAM","lotw":"Y","dist":"1390","grid":"FM06HU"},{"call":"KE0DC","name":"","date":"2021-01-24","time":"0600","band":"40M","mode":"FT8","rst_sent":"-08","rst_rcvd":"-09","country":"United States","state":"CO","qth":"AURORA","lotw":"Y","dist":"1274","grid":"DM79OQ"},{"call":"KC0HHN","name":"","date":"2021-01-24","time":"0604","band":"40M","mode":"FT8","rst_sent":"-12","rst_rcvd":"-08","country":"United States","state":"IA","qth":"","lotw":"N","dist":"","grid":"EN31"},{"call":"WB0OEW","name":"JAMES D VOGT","date":"2021-01-24","time":"0606","band":"40M","mode":"FT8","rst_sent":"-10","rst_rcvd":"-15","country":"United States","state":"MO","qth":"CAMDENTON","lotw":"Y","dist":"747","grid":"EM37TH"},{"call":"N4JKO","name":"GARY E HORTON","date":"2021-01-24","time":"0608","band":"40M","mode":"FT8","rst_sent":"-12","rst_rcvd":"-15","country":"United States","state":"TN","qth":"","lotw":"N","dist":"","grid":"EM65RT"},{"call":"WW4LL","name":"LESTER P LEEPER","date":"2021-01-24","time":"0610","band":"40M","mode":"FT8","rst_sent":"-14","rst_rcvd":"-13","country":"United States","state":"GA","qth":"BOWDON","lotw":"Y","dist":"575","grid":"EM73DS"},{"call":"K0MBT","name":"MICHAEL B TEDESCO","date":"2021-01-24","time":"0612","band":"40M","mode":"FT8","rst_sent":"-09","rst_rcvd":"-18","country":"United States","state":"MN","qth":"SHAKOPEE","lotw":"Y","dist":"1247","grid":"EN34HE"},{"call":"WB4LHD","name":"ROY A MCDANIEL","date":"2021-01-24","time":"0618","band":"40M","mode":"FT8","rst_sent":"-14","rst_rcvd":"-12","country":"United States","state":"NC","qth":"SANFORD","lotw":"Y","dist":"1118","grid":"FM03HF"},{"call":"N0KJA","name":"FRANK O KORANDA","date":"2021-01-24","time":"0620","band":"40M","mode":"FT8","rst_sent":"-10","rst_rcvd":"-14","country":"United States","state":"KS","qth":"WICHITA","lotw":"Y","dist":"641","grid":"EM17PT"},{"call":"W9JML","name":"JAMES M LAURENDEAU","date":"2021-01-24","time":"0623","band":"40M","mode":"FT8","rst_sent":"-05","rst_rcvd":"-16","country":"United States","state":"IL","qth":"COLLINSVILLE","lotw":"Y","dist":"668","grid":"EM58GI"},{"call":"K9ADV","name":"DWIGHT A VANDERLINDEN","date":"2021-01-24","time":"0625","band":"40M","mode":"FT8","rst_sent":"-11","rst_rcvd":"-13","country":"United States","state":"IN","qth":"INDIANAPOLIS","lotw":"Y","dist":"929","grid":"EM79MF"},{"call":"WB8YYY","name":"ERNEST E MUENZER","date":"2021-01-24","time":"0628","band":"40M","mode":"FT8","rst_sent":"-10","rst_rcvd":"-05","country":"United States","state":"MI","qth":"","lotw":"Y","dist":"","grid":"EN72"},{"call":"KD2UKS","name":"RAYMOND JOSEPH MCDERMOTT","date":"2021-01-24","time":"0641","band":"40M","mode":"FT8","rst_sent":"-12","rst_rcvd":"-15","country":"United States","state":"NY","qth":"MASTIC","lotw":"N","dist":"1561","grid":"FN30HT"},{"call":"KD8IYI","name":"JEFFREY C WHITLEY","date":"2021-01-24","time":"0644","band":"40M","mode":"FT8","rst_sent":"-13","rst_rcvd":"-13","country":"United States","state":"OH","qth":"","lotw":"Y","dist":"","grid":"EM89LG"},{"call":"K9DW","name":"DAN WELBORN","date":"2021-01-24","time":"0647","band":"40M","mode":"FT8","rst_sent":"-09","rst_rcvd":"-06","country":"United States","state":"AL","qth":"DOTHAN","lotw":"Y","dist":"435","grid":"EM64SA"},{"call":"K8DO","name":"DEAN L OSTERMAN","date":"2021-01-24","time":"0649","band":"40M","mode":"FT8","rst_sent":"-11","rst_rcvd":"-10","country":"United States","state":"MI","qth":"ALLENDALE","lotw":"Y","dist":"1298","grid":"EN62OX"},{"call":"K6VMV","name":"VICTOR M VALADEZ","date":"2021-01-24","time":"0651","band":"40M","mode":"FT8","rst_sent":"-08","rst_rcvd":"-12","country":"United States","state":"CA","qth":"MANTECA","lotw":"N","dist":"1954","grid":"CM97KC"},{"call":"W0ULF","name":"MIKE WOLFE","date":"2021-01-24","time":"0653","band":"40M","mode":"FT8","rst_sent":"-11","rst_rcvd":"-13","country":"United States","state":"NE","qth":"OMAHA","lotw":"Y","dist":"1012","grid":"EN21WC"}];
let ALL_DATA = [];
let filteredData = [];
let sortKey = 'date';
let sortDir = -1; // -1 = desc
let currentPage = 1;
const PAGE_SIZE = 50;
// ─── FULL LOG ────────────────────────────────────────────────
const FULL_LOG_URL = 'data/k5bss-log.json';
async function loadFullLog() {
try {
const res = await fetch(FULL_LOG_URL);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.warn('Could not load full log, using sample data:', err);
return [];
}
}
async function init() {
const fullLog = await loadFullLog();
ALL_DATA = fullLog.length ? fullLog : RAW_DATA;
filteredData = [...ALL_DATA];
// Update stats
const countries = new Set(ALL_DATA.map(r => r.country).filter(Boolean));
const states = new Set(ALL_DATA.filter(r => r.country === 'United States').map(r => r.state).filter(Boolean));
document.getElementById('stat-dxcc').textContent = countries.size;
document.getElementById('stat-states').textContent = states.size;
buildDashboard();
populateFilters();
renderTable();
}
// ─── DASHBOARD ───────────────────────────────────────────────
function buildDashboard() {
// Modes
const modes = countBy(ALL_DATA, 'mode');
const topModes = Object.entries(modes).sort((a,b)=>b[1]-a[1]).slice(0,7);
const maxMode = topModes[0][1];
const modesEl = document.getElementById('modes-chart');
const modeColors = { FT8:'#ffaa00', FT4:'#ffcc44', DIG:'#ff8800', USB:'#4499ff', LSB:'#66aaff', SSB:'#88bbff', RTTY:'#ff4455' };
modesEl.innerHTML = topModes.map(([m, c]) => `
<div class="bar-row">
<div class="bar-label">${m}</div>
<div class="bar-track"><div class="bar-fill" style="width:0%;background:${modeColors[m]||'#888'}" data-target="${(c/maxMode*100).toFixed(1)}"></div></div>
<div class="bar-count">${c}</div>
</div>`).join('');
// Bands
const bands = countBy(ALL_DATA, 'band');
const topBands = Object.entries(bands).sort((a,b)=>b[1]-a[1]).slice(0,8);
const maxBand = topBands[0][1];
const bandEl = document.getElementById('bands-chart');
const bandColors = { '20M':'#4499ff','40M':'#00ff88','80M':'#ff4455','10M':'#00ddff','30M':'#88aaff','17M':'#aaddff','15M':'#ffaaff','12M':'#ffdd88' };
bandEl.innerHTML = topBands.map(([b, c]) => `
<div class="bar-row">
<div class="bar-label">${b}</div>
<div class="bar-track"><div class="bar-fill" style="width:0%;background:${bandColors[b]||'#888'}" data-target="${(c/maxBand*100).toFixed(1)}"></div></div>
<div class="bar-count">${c}</div>
</div>`).join('');
// Countries
const countries = countBy(ALL_DATA, 'country');
const topCountries = Object.entries(countries).sort((a,b)=>b[1]-a[1]).slice(0,20);
const ctEl = document.getElementById('countries-list');
ctEl.innerHTML = topCountries.map(([c,n]) => `
<div class="country-row">
<span class="country-name">${c||'Unknown'}</span>
<span class="country-count">${n}</span>
</div>`).join('');
// Animate bars after a tick
requestAnimationFrame(() => {
setTimeout(() => {
document.querySelectorAll('.bar-fill').forEach(el => {
el.style.width = el.dataset.target + '%';
});
}, 100);
});
// Activity chart
drawActivityChart();
}
function drawActivityChart() {
const canvas = document.getElementById('activityCanvas');
const dpr = window.devicePixelRatio || 1;
const W = canvas.parentElement.offsetWidth - 48;
const H = 120;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
// Group by year-month
const monthly = {};
ALL_DATA.forEach(r => {
if (!r.date) return;
const key = r.date.slice(0,7);
monthly[key] = (monthly[key]||0) + 1;
});
const keys = Object.keys(monthly).sort();
const vals = keys.map(k => monthly[k]);
const max = Math.max(...vals);
const pad = { l: 10, r: 10, t: 15, b: 30 };
const chartW = W - pad.l - pad.r;
const chartH = H - pad.t - pad.b;
const barW = Math.max(2, (chartW / keys.length) - 1);
ctx.clearRect(0,0,W,H);
// Grid line
ctx.strokeStyle = 'rgba(30,37,48,0.8)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pad.l, pad.t + chartH);
ctx.lineTo(W - pad.r, pad.t + chartH);
ctx.stroke();
// Bars
keys.forEach((k, i) => {
const x = pad.l + i * (chartW / keys.length);
const h = (vals[i] / max) * chartH;
const y = pad.t + chartH - h;
const grad = ctx.createLinearGradient(0, y, 0, y + h);
grad.addColorStop(0, 'rgba(0,255,136,0.9)');
grad.addColorStop(1, 'rgba(0,255,136,0.15)');
ctx.fillStyle = grad;
ctx.fillRect(x, y, barW, h);
});
// X labels (every 6 months)
ctx.fillStyle = 'rgba(90,112,128,0.9)';
ctx.font = `${9 * dpr / dpr}px 'Share Tech Mono', monospace`;
ctx.textAlign = 'center';
keys.forEach((k, i) => {
if (i % 6 === 0) {
const x = pad.l + i * (chartW / keys.length) + barW/2;
ctx.fillText(k, x, H - 8);
}
});
}
// ─── TABLE ───────────────────────────────────────────────────
function populateFilters() {
const bands = [...new Set(ALL_DATA.map(r=>r.band).filter(Boolean))].sort();
const modes = [...new Set(ALL_DATA.map(r=>r.mode).filter(Boolean))].sort();
const bandSel = document.getElementById('filter-band');
const modeSel = document.getElementById('filter-mode');
bands.forEach(b => { const o = document.createElement('option'); o.value=b; o.text=b; bandSel.appendChild(o); });
modes.forEach(m => { const o = document.createElement('option'); o.value=m; o.text=m; modeSel.appendChild(o); });
}
function applyFilters() {
const q = document.getElementById('search-input').value.trim().toLowerCase();
const band = document.getElementById('filter-band').value;
const mode = document.getElementById('filter-mode').value;
const lotw = document.getElementById('filter-lotw').value;
filteredData = ALL_DATA.filter(r => {
if (band && r.band !== band) return false;
if (mode && r.mode !== mode) return false;
if (lotw && r.lotw !== lotw) return false;
if (q) {
const hay = [r.call, r.name, r.country, r.qth, r.state, r.grid].join(' ').toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
});
currentPage = 1;
renderTable();
}
function sortTable(key) {
if (sortKey === key) sortDir *= -1;
else { sortKey = key; sortDir = key === 'date' || key === 'time' ? -1 : 1; }
filteredData.sort((a,b) => {
let av = a[key]||'', bv = b[key]||'';
if (key === 'dist') { av = parseInt(av)||0; bv = parseInt(bv)||0; }
if (av < bv) return -sortDir;
if (av > bv) return sortDir;
return 0;
});
// Update header indicators
document.querySelectorAll('thead th').forEach(th => {
th.classList.remove('sort-asc','sort-desc');
});
const headers = ['call','name','date','time','band','mode','rst_sent','country','dist','lotw'];
const idx = headers.indexOf(key);
if (idx >= 0) {
const ths = document.querySelectorAll('thead th');
ths[idx].classList.add(sortDir === 1 ? 'sort-asc' : 'sort-desc');
}
renderTable();
}
function renderTable() {
const tbody = document.getElementById('log-tbody');
const total = filteredData.length;
const pages = Math.ceil(total / PAGE_SIZE);
const start = (currentPage - 1) * PAGE_SIZE;
const slice = filteredData.slice(start, start + PAGE_SIZE);
document.getElementById('log-meta').textContent =
`${total.toLocaleString()} QSOs · Page ${currentPage} of ${Math.max(1,pages)}`;
if (slice.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" style="text-align:center;padding:2rem;color:var(--muted);font-family:var(--font-mono);font-size:0.8rem;">NO CONTACTS FOUND</td></tr>';
} else {
tbody.innerHTML = slice.map(r => {
const timeStr = r.time ? r.time.slice(0,2)+':'+r.time.slice(2,4) : '';
const bandClass = `band-${r.band}`;
const lotwClass = r.lotw === 'Y' ? 'lotw-y' : 'lotw-n';
const lotwSym = r.lotw === 'Y' ? '✓' : '·';
const rst = r.rst_sent && r.rst_rcvd ? `${r.rst_sent} / ${r.rst_rcvd}` : r.rst_sent || '';
return `<tr>
<td class="call-cell">${esc(r.call)}</td>
<td>${esc(r.name ? titleCase(r.name) : '')}</td>
<td>${esc(r.date)}</td>
<td>${timeStr}Z</td>
<td><span class="band-badge ${bandClass}">${esc(r.band)}</span></td>
<td class="mode-cell">${esc(r.mode)}</td>
<td class="rst-cell">${esc(rst)}</td>
<td>${esc(r.country||'')}${r.state ? ' <span style="color:var(--muted)">'+esc(r.state)+'</span>' : ''}</td>
<td class="dist-cell">${r.dist ? Number(r.dist).toLocaleString() : ''}</td>
<td class="${lotwClass}" style="text-align:center">${lotwSym}</td>
</tr>`;
}).join('');
}
renderPagination(pages);
}
function renderPagination(pages) {
const el = document.getElementById('pagination');
if (pages <= 1) { el.innerHTML = ''; return; }
let html = `<button class="page-btn" onclick="goPage(${currentPage-1})" ${currentPage===1?'disabled':''}>‹ Prev</button>`;
const range = [];
if (pages <= 7) {
for (let i=1;i<=pages;i++) range.push(i);
} else {
range.push(1);
if (currentPage > 3) range.push('...');
for (let i=Math.max(2,currentPage-1); i<=Math.min(pages-1,currentPage+1); i++) range.push(i);
if (currentPage < pages-2) range.push('...');
range.push(pages);
}
range.forEach(p => {
if (p === '...') html += `<span class="page-info">…</span>`;
else html += `<button class="page-btn ${p===currentPage?'active':''}" onclick="goPage(${p})">${p}</button>`;
});
html += `<button class="page-btn" onclick="goPage(${currentPage+1})" ${currentPage===pages?'disabled':''}>Next ›</button>`;
el.innerHTML = html;
}
function goPage(p) {
currentPage = p;
renderTable();
document.querySelector('.table-wrap').scrollIntoView({behavior:'smooth',block:'nearest'});
}
// ─── HELPERS ─────────────────────────────────────────────────
function countBy(arr, key) {
return arr.reduce((acc, r) => {
const v = r[key];
if (v) acc[v] = (acc[v]||0)+1;
return acc;
}, {});
}
function esc(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function titleCase(s) {
return s.toLowerCase().replace(/\b\w/g, c => c.toUpperCase());
}
function showSection(id, btn) {
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
document.getElementById('section-' + id).classList.add('active');
btn.classList.add('active');
}
window.addEventListener('load', init);
window.addEventListener('resize', () => {
if (document.getElementById('section-dashboard').classList.contains('active')) {
drawActivityChart();
}
});