157 líneas
6,7 KiB
JavaScript
//+------------------------------------------------------------------+
| |||
//| app.js |
| |||
//| Interfaz: carga el .bin, muestra la ficha del evento y la tabla |
| |||
//| de valores con lista virtual (solo se dibujan las filas cerca |
| |||
//| de la pantalla; el resto se lee del buffer cuando se hace scroll)|
| |||
//+------------------------------------------------------------------+
| |||
(function () {
| |||
'use strict';
| |||
| |||
const ROW_H = 28; // alto fijo de fila (px): permite calcular que filas ver sin medirlas
| |||
const RENDER_SCREENS = 4; // se dibujan ~4 pantallas de filas alrededor de la vista, el resto on-demand
| |||
| |||
const $ = (id) => document.getElementById(id);
| |||
const el = {
| |||
file: $('file'), drop: $('drop'), msg: $('msg'), info: $('info'), title: $('title'), sub: $('sub'),
| |||
chips: $('chips'), stats: $('stats'), warn: $('warn'), tableWrap: $('tableWrap'), scroller: $('scroller'),
| |||
body: $('body'), rows: $('rows'), order: $('order'), count: $('count')
| |||
};
| |||
| |||
let H = null; // cabecera parseada
| |||
let desc = true; // true = mas reciente primero
| |||
let start = 0, end = 0; // rango de filas dibujado [start, end)
| |||
let ticking = false;
| |||
| |||
//--- Utilidades
| |||
function esc(s) {
| |||
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
| |||
}
| |||
function setMsg(text, isErr) { el.msg.textContent = text; el.msg.className = isErr ? 'err' : ''; }
| |||
| |||
//--- Carga del archivo
| |||
async function loadFile(file) {
| |||
if (!file) return;
| |||
const t0 = performance.now();
| |||
try {
| |||
const buf = await file.arrayBuffer();
| |||
H = CalFmt.parseHeader(buf);
| |||
} catch (e) {
| |||
H = null;
| |||
el.info.hidden = true; el.tableWrap.hidden = true;
| |||
setMsg('No se pudo leer "' + file.name + '": ' + e.message, true);
| |||
return;
| |||
}
| |||
setMsg(file.name + ' — ' + (file.size / 1024).toFixed(1) + ' KB, cabecera leida en ' + (performance.now() - t0).toFixed(1) + ' ms', false);
| |||
showInfo(file);
| |||
resetTable();
| |||
}
| |||
| |||
function chip(k, v) { return '<span class="chip"><b>' + esc(k) + '</b> ' + esc(v) + '</span>'; }
| |||
| |||
function showInfo(file) {
| |||
const C = CalFmt;
| |||
el.title.textContent = H.name || '(sin nombre)';
| |||
let sub = esc(H.code);
| |||
if (/^https?:\/\//i.test(H.url)) sub += ' · <a href="' + esc(H.url) + '" target="_blank" rel="noopener noreferrer">' + esc(H.url) + '</a>';
| |||
else if (H.url) sub += ' · ' + esc(H.url);
| |||
el.sub.innerHTML = sub;
| |||
| |||
el.chips.innerHTML = [
| |||
chip('Pais', (C.COUNTRY[H.countryId] || 'Desconocido') + ' (' + H.countryId + ')'),
| |||
chip('Importancia', C.label(C.IMPORTANCE, H.importance)),
| |||
chip('Tipo', C.label(C.TYPE, H.type)),
| |||
chip('Sector', C.label(C.SECTOR, H.sector)),
| |||
chip('Frecuencia', C.label(C.FREQUENCY, H.frequency)),
| |||
chip('Hora', C.label(C.TIMEMODE, H.timeMode)),
| |||
chip('Unidad', C.label(C.UNIT, H.unit)),
| |||
chip('Multiplicador', C.label(C.MULT_NAME, H.multiplier)),
| |||
chip('Decimales', H.digits),
| |||
chip('ID evento', H.id)
| |||
].join('');
| |||
| |||
let first = '—', last = '—';
| |||
if (H.count > 0) {
| |||
first = C.fmtTime(C.readRow(H, 0).time);
| |||
last = C.fmtTime(C.readRow(H, H.count - 1).time);
| |||
}
| |||
el.stats.textContent = H.count.toLocaleString() + ' registros · ' + first + ' → ' + last + ' (hora del servidor)';
| |||
el.warn.hidden = !H.warning;
| |||
el.warn.textContent = H.warning || '';
| |||
el.info.hidden = false;
| |||
}
| |||
| |||
//--- Tabla virtual
| |||
function resetTable() {
| |||
el.tableWrap.hidden = H.count === 0;
| |||
el.count.textContent = H.count.toLocaleString() + ' filas';
| |||
el.body.style.height = H.count * ROW_H + 'px';
| |||
el.scroller.scrollTop = 0;
| |||
start = end = 0;
| |||
el.rows.innerHTML = '';
| |||
render(true);
| |||
}
| |||
| |||
function visibleRows() { return Math.max(1, Math.ceil(el.scroller.clientHeight / ROW_H)); }
| |||
| |||
function rowHtml(k) {
| |||
const C = CalFmt;
| |||
const i = desc ? H.count - 1 - k : k; // k = posicion en pantalla, i = indice en el archivo
| |||
const r = C.readRow(H, i);
| |||
const imp = r.impact === 1 ? 'pos' : (r.impact === 2 ? 'neg' : '');
| |||
return '<div class="row ' + (k % 2 ? 'alt' : '') + '" style="height:' + ROW_H + 'px">' +
| |||
'<span class="c-n">' + (i + 1) + '</span>' +
| |||
'<span class="c-t">' + C.fmtTime(r.time) + '</span>' +
| |||
'<span class="c-v">' + C.fmtValue(r.actual, H) + '</span>' +
| |||
'<span class="c-v">' + C.fmtValue(r.forecast, H) + '</span>' +
| |||
'<span class="c-v">' + C.fmtValue(r.previous, H) + '</span>' +
| |||
'<span class="c-v">' + C.fmtValue(r.revised, H) + '</span>' +
| |||
'<span class="c-i ' + imp + '">' + C.label(C.IMPACT, r.impact) + '</span>' +
| |||
'<span class="c-id">' + r.id + '</span>' +
| |||
'<span class="c-m">' + r.countryIdx + ' / ' + r.eventIdx + (r.processed ? ' · proc.' : '') + '</span>' +
| |||
'</div>';
| |||
}
| |||
| |||
// Dibuja ~RENDER_SCREENS pantallas alrededor de la vista. Solo vuelve a dibujar cuando la vista
| |||
// se acerca al borde de lo ya dibujado (o con force).
| |||
function render(force) {
| |||
if (!H || H.count === 0) return;
| |||
const vis = visibleRows();
| |||
const first = Math.min(H.count - 1, Math.floor(el.scroller.scrollTop / ROW_H));
| |||
const last = Math.min(H.count, first + vis);
| |||
| |||
// Se redibuja solo si la vista se sale de lo dibujado o se acerca a un borde (margen = media pantalla)
| |||
const margin = Math.floor(vis * 0.5);
| |||
const nearTop = start > 0 && first < start + margin;
| |||
const nearBottom = end < H.count && last > end - margin;
| |||
if (!force && !nearTop && !nearBottom && first >= start && last <= end) return;
| |||
| |||
const extra = Math.ceil(vis * (RENDER_SCREENS - 1) / 2);
| |||
start = Math.max(0, first - extra);
| |||
end = Math.min(H.count, last + extra);
| |||
| |||
let html = '';
| |||
for (let k = start; k < end; k++) html += rowHtml(k);
| |||
el.rows.style.top = start * ROW_H + 'px';
| |||
el.rows.innerHTML = html;
| |||
}
| |||
| |||
function onScroll() {
| |||
if (ticking) return;
| |||
ticking = true;
| |||
requestAnimationFrame(() => { ticking = false; render(false); });
| |||
}
| |||
| |||
//--- Eventos
| |||
el.file.addEventListener('change', () => loadFile(el.file.files[0]));
| |||
el.scroller.addEventListener('scroll', onScroll, { passive: true });
| |||
window.addEventListener('resize', () => render(true));
| |||
el.order.addEventListener('click', () => {
| |||
desc = !desc;
| |||
el.order.textContent = desc ? 'Mas reciente primero' : 'Mas antiguo primero';
| |||
if (H) { el.scroller.scrollTop = 0; render(true); }
| |||
});
| |||
| |||
['dragenter', 'dragover'].forEach((ev) => el.drop.addEventListener(ev, (e) => { e.preventDefault(); el.drop.classList.add('over'); }));
| |||
['dragleave', 'drop'].forEach((ev) => el.drop.addEventListener(ev, (e) => { e.preventDefault(); el.drop.classList.remove('over'); }));
| |||
el.drop.addEventListener('drop', (e) => loadFile(e.dataTransfer.files[0]));
| |||
})();
|