← Home + Registro Mi perfil
WTI
Brent
Gold
Nat.Gas
Copper
VIX
DXY
S&P 500
US 10Y
Silver
Wheat
Coffee
EUR/USD
USD/MXN
XLK
XLE
XLF
WTI
Brent
Gold
Nat.Gas
Copper
VIX
DXY
S&P 500
US 10Y
Loading live data...
Microstates
PIB
Inflación
Tasa
Desempleo
Deuda
IDH
CO₂
Gobern.
Renovable
FX
+
Click a country on the map
or select from the dropdown above
COMPARECountry Analysis
Select 2–5 countries · 35 years of data
No countries selected yet
MACRO MONETARIO / FISCAL LABORAL / SOCIAL EXTERNO / CLIMA
Add 2 or more countries above
to start comparing
'); var w = window.open('','_blank','width=900,height=1100,scrollbars=yes'); if (w) { w.document.write(P.join('')); w.document.close(); } else { alert('Permite ventanas emergentes para exportar el PDF.\nCtrl+P \u2192 Guardar como PDF'); } } // ── HELPERS ────────────────────────────────────────────────────────────────── function _getRumboVars(sector) { const concernVarMap = {inflacion:'cpi', credito:'rate', tipocambio:'fxChg12m', demanda:'unemp', insumos:'ppi', expansion:'gdpG'}; const primary = concernVarMap[RUMBO_STATE.concern]; const vars = [...(sector?.vars || ['cpi','rate','gdpG','unemp','ppi','fxChg12m'])]; if(primary && vars.includes(primary)) { vars.splice(vars.indexOf(primary),1); vars.unshift(primary); } return vars.slice(0, 6); } function _getPrimaryVar(sector, d) { const vars = _getRumboVars(sector); for(const v of vars) { const h = _getRumboHist(v, d); if(h.length > 5) return v; } return vars[0]; } function _getRumboHist(field, d) { if(d.hist && d.hist[field]) return d.hist[field].filter(v => v != null); if(field === 'gdpG' && d.gdpGH) return d.gdpGH.filter(v => v != null); return []; } function _rumboSignal(field, val) { if(val == null) return 'gray'; const neg = {cpi:[4,7], cpiC:[3.5,6], ppi:[5,10], rate:[6,12], unemp:[5,8]}; const pos = {gdpG:2.5, lfp:65, investment:25}; if(neg[field]) { const [a,r]=neg[field]; return val<=a?'green':val<=r?'amber':'red'; } if(pos[field] !== undefined) return val>=pos[field]?'green':val>=(pos[field]/2)?'amber':'red'; if(field==='fxChg12m') { const a=Math.abs(val); return a<=5?'green':a<=12?'amber':'red'; } return 'gray'; } function _rumboInterpKPI(field, val) { if(val == null) return ''; const t = { cpi: val>7?'Inflación alta — ajusta precios y revisa contratos':val>4?'Inflación moderada — vigila tus márgenes':'Inflación controlada — contexto favorable', ppi: val>8?'Insumos muy caros — negocia contratos largos':val>4?'Insumos elevados — monitorea de cerca':'Costos de producción moderados', rate: val>10?'Crédito muy caro — evita deuda variable':val>7?'Tasas altas — evalúa bien el ROI':'Tasas razonables para financiar', unemp:val>7?'Mercado débil — menor demanda del consumidor':val>4?'Desempleo moderado':'Empleo sólido — buena demanda', gdpG: val<0?'Economía en contracción — cautela':val<1.5?'Crecimiento débil':'Crecimiento sólido', fxChg12m: Math.abs(val)>10?'Alta volatilidad cambiaria':Math.abs(val)>5?'Volatilidad moderada':'Tipo de cambio estable', lfp: val>65?'Fuerza laboral activa — buena oferta de talento':'Participación laboral baja', investment: val>25?'Alta inversión — señal positiva':'Inversión moderada', }; return t[field] || ''; } // ═══════════════════════════════════════════════════════════════════════════ // D — REAL-TIME KPIs FROM BANXICO / NATIONAL SOURCES // ═══════════════════════════════════════════════════════════════════════════ let _rumboLiveData = null; // cache for national data async function fetchRumboLiveData(iso3) { // Only MEX has the national endpoint for now if(iso3 !== 'MEX') return null; try { const resp = await fetch(`${RUMBO_BACKEND}/api/v3/mexico/current`, {signal: AbortSignal.timeout(8000)}); if(!resp.ok) return null; const d = await resp.json(); // Must have at least one data field to be useful if(!d.rate && !d.cpi && !d.unemp) return null; return d; } catch(e) { console.warn('[Rumbo D] Live data fetch failed:', e.message); return null; } } // Overrides _buildKPI to inject live data + source badge async function _buildKPILive(field, d, iso3) { const card = _buildKPI(field, d); // build the base card first // Try to overlay live national data for MEX if(iso3 === 'MEX' && _rumboLiveData) { const liveVal = _rumboLiveData[field]; const hierarchy = _rumboLiveData.source_hierarchy || {}; const source = hierarchy[field] || null; if(liveVal != null && source) { // Update the value display with live data const valEl = card.querySelector('.rumbo-kpi-val'); if(valEl) { const sign = ['cpi','rate','unemp','gdpG','fxChg12m','ppi'].includes(field) ? (liveVal > 0 ? '+' : '') : ''; valEl.textContent = sign + liveVal.toFixed(2) + '%'; valEl.style.color = '#042C53'; } card.classList.add('updated'); // Add source badge const badge = document.createElement('div'); badge.className = 'rumbo-kpi-source'; badge.innerHTML = `${source} · En vivo`; card.appendChild(badge); } } else if(iso3 === 'MEX') { // Show WB source const badge = document.createElement('div'); badge.className = 'rumbo-kpi-source'; badge.innerHTML = `World Bank · Anual`; card.appendChild(badge); } return card; } // ═══════════════════════════════════════════════════════════════════════════ // E — SCENARIO CALCULATOR // ═══════════════════════════════════════════════════════════════════════════ // Econometric elasticities by sector (beta coefficients) // Source: typical pass-through rates in economic literature for Mexico const RUMBO_ELASTICITIES = { alimentos: { fx: 0.35, rate: -0.15, cpi: 0.80, oil: 0.12 }, construc: { fx: 0.28, rate: -0.40, cpi: 0.45, oil: 0.20 }, comercio: { fx: 0.22, rate: -0.18, cpi: 0.75, oil: 0.08 }, logistica: { fx: 0.30, rate: -0.12, cpi: 0.50, oil: 0.55 }, salud: { fx: 0.40, rate: -0.10, cpi: 0.65, oil: 0.05 }, manufactura: { fx: 0.45, rate: -0.25, cpi: 0.55, oil: 0.25 }, agro: { fx: 0.38, rate: -0.20, cpi: 0.60, oil: 0.18 }, tecnologia: { fx: 0.60, rate: -0.30, cpi: 0.40, oil: 0.05 }, }; function updateScenario() { const card = document.getElementById('rumbo-scenario'); if(!card) return; const elast = JSON.parse(card.dataset.elast || '{}'); const fxDelta = parseFloat(document.getElementById('sc-fx')?.value || 0); const rateDelta = parseFloat(document.getElementById('sc-rate')?.value || 0); const oilDelta = parseFloat(document.getElementById('sc-oil')?.value || 0); // Update display values const fxEl = document.getElementById('sc-fx-val'); const rateEl = document.getElementById('sc-rate-val'); const oilEl = document.getElementById('sc-oil-val'); if(fxEl) fxEl.textContent = (fxDelta > 0 ? '+' : '') + fxDelta + '%'; if(rateEl) rateEl.textContent = (rateDelta > 0 ? '+' : '') + rateDelta + 'pp'; if(oilEl) oilEl.textContent = (oilDelta > 0 ? '+' : '') + oilDelta + '%'; // Color the values [['sc-fx-val', fxDelta], ['sc-rate-val', rateDelta], ['sc-oil-val', oilDelta]].forEach(([id, val]) => { const el = document.getElementById(id); if(el) el.style.color = val > 0 ? '#DC2626' : val < 0 ? '#059669' : 'var(--r-navy)'; }); // Calculate impacts using elasticities // Cost impact: FX pass-through + oil pass-through (rate affects financing cost) const costImpact = fxDelta * elast.fx + oilDelta * elast.oil + rateDelta * Math.abs(elast.rate) * 2; // Demand impact: rate change + FX effect on purchasing power const demandImpact = rateDelta * elast.rate * 3 - fxDelta * elast.cpi * 0.5; // Margin: inverse of cost impact adjusted by demand const marginImpact = -costImpact * 0.6 + demandImpact * 0.4; // Update impact cards const fmt = v => (v > 0 ? '+' : '') + v.toFixed(1) + '%'; const col = v => v > 0 ? '#DC2626' : v < 0 ? '#059669' : 'var(--r-muted)'; const costEl = document.getElementById('sc-cost'); const demandEl = document.getElementById('sc-demand'); const marginEl = document.getElementById('sc-margin'); const sigEl = document.getElementById('sc-signal'); const sigTxtEl = document.getElementById('sc-signal-text'); const costDirEl = document.getElementById('sc-cost-dir'); const demandDirEl = document.getElementById('sc-demand-dir'); const marginDirEl = document.getElementById('sc-margin-dir'); if(costEl) { costEl.textContent = fmt(costImpact); costEl.style.color = col(costImpact); } if(demandEl) { demandEl.textContent = fmt(demandImpact); demandEl.style.color = col(demandImpact); } if(marginEl) { marginEl.textContent = fmt(marginImpact); marginEl.style.color = col(-marginImpact); } if(costDirEl) costDirEl.textContent = costImpact > 1 ? '⬆ Costos al alza' : costImpact < -1 ? '⬇ Costos bajan' : 'Sin cambio relevante'; if(demandDirEl) demandDirEl.textContent = demandImpact > 1 ? '⬆ Demanda mejora' : demandImpact < -1 ? '⬇ Demanda cae' : 'Demanda estable'; if(marginDirEl) marginDirEl.textContent = marginImpact < -1 ? '⬇ Margen comprimido': marginImpact > 1 ? '⬆ Margen mejora' : 'Margen estable'; // Overall signal const score = marginImpact - Math.abs(costImpact) * 0.3; let emoji = '', label = 'Neutro', labelColor = 'var(--r-muted)'; if(score > 2) { emoji = ''; label = 'Favorable'; labelColor = '#059669'; } else if(score > 0.5){ emoji = ''; label = 'Leve mejora'; labelColor = '#D97706'; } else if(score < -2) { emoji = ''; label = 'Desfavorable'; labelColor = '#DC2626'; } else if(score < -0.5){ emoji = ''; label = 'Atención'; labelColor = '#D97706'; } if(sigEl) sigEl.textContent = emoji; if(sigTxtEl) { sigTxtEl.textContent = label; sigTxtEl.style.color = labelColor; } } // ═══════════════════════════════════════════════════════════════════════════ // F — PERSONALIZED ALERTS (localStorage) // ═══════════════════════════════════════════════════════════════════════════ const RUMBO_ALERT_VARS = [ {id:'cpi', label:'Inflación CPI', unit:'%', op:'gt', default:5}, {id:'rate', label:'Tasa de interés',unit:'%', op:'gt', default:8}, {id:'gdpG', label:'Crecimiento PIB',unit:'%', op:'lt', default:0}, {id:'unemp', label:'Desempleo', unit:'%', op:'gt', default:6}, {id:'fxChg12m',label:'Var. Cambiaria 12M',unit:'%',op:'gt',default:10}, ]; function loadRumboAlerts() { try { return JSON.parse(localStorage.getItem('rumbo_alerts') || '[]'); } catch(e) { return []; } } function saveRumboAlerts(alerts) { localStorage.setItem('rumbo_alerts', JSON.stringify(alerts)); } function addRumboAlert(varId, operator, threshold, iso3) { const alerts = loadRumboAlerts(); alerts.push({ varId, operator, threshold: parseFloat(threshold), iso3, created: Date.now() }); saveRumboAlerts(alerts); renderAlertCard(); } function deleteRumboAlert(idx) { const alerts = loadRumboAlerts(); alerts.splice(idx, 1); saveRumboAlerts(alerts); renderAlertCard(); } function checkRumboAlerts() { const alerts = loadRumboAlerts(); if(!alerts.length) return; alerts.forEach(alert => { const d = Object.assign({}, VD[alert.iso3]||VD['MEX']||{}, STATIC_EXTRAS[alert.iso3]||{}); const val = d[alert.varId]; if(val == null) return; const meta = RUMBO_ALERT_VARS.find(v => v.id === alert.varId) || {label: alert.varId, unit:'%'}; const triggered = alert.operator === 'gt' ? val > alert.threshold : val < alert.threshold; if(triggered) { const opLabel = alert.operator === 'gt' ? 'superó' : 'cayó por debajo de'; showAlertToast( '️ Alerta Activada', `${meta.label} ${opLabel} ${alert.threshold}${meta.unit}. Valor actual: ${val.toFixed(1)}${meta.unit}`, ); } }); } function showAlertToast(title, body) { // Remove existing toasts document.querySelectorAll('.rumbo-alert-toast').forEach(el => el.remove()); const toast = document.createElement('div'); toast.className = 'rumbo-alert-toast'; toast.innerHTML = `
${title}
${body}
`; document.body.appendChild(toast); // Auto-remove after 8s setTimeout(() => toast?.remove(), 8000); } function buildAlertCard() { const card = document.createElement('div'); card.className = 'rumbo-card'; card.id = 'rumbo-alert-card'; renderAlertCardInto(card); return card; } function renderAlertCard() { const card = document.getElementById('rumbo-alert-card'); if(card) renderAlertCardInto(card); } function renderAlertCardInto(card) { const alerts = loadRumboAlerts(); const iso3 = RUMBO_STATE.country || 'MEX'; const d = Object.assign({}, VD[iso3]||VD['MEX']||{}, STATIC_EXTRAS[iso3]||{}); let alertsHTML = alerts.length ? alerts.map((alert, idx) => { const meta = RUMBO_ALERT_VARS.find(v => v.id === alert.varId) || {label: alert.varId, unit:'%'}; const opLabel = alert.operator === 'gt' ? '>' : '<'; const val = d[alert.varId]; const triggered = val != null && (alert.operator === 'gt' ? val > alert.threshold : val < alert.threshold); return `
${triggered ? '' : ''} ${meta.label} ${opLabel} ${alert.threshold}${meta.unit} ${val != null ? val.toFixed(1)+meta.unit : 'N/A'}
`; }).join('') : '
Aún no tienes alertas configuradas.
'; card.innerHTML = `
Mis Alertas
${alertsHTML}
`; } // ═══════════════════════════════════════════════════════════════════════════ // ── CB Meeting Dates — load from Railway API and update VD ────────────────── async function loadCBMeetings() { try { const res = await fetch(MERIDIAN_API + '/api/v3/cb/meetings', { signal: AbortSignal.timeout(15000) }); if (!res.ok) return; const json = await res.json(); const data = json.data || {}; let updated = 0; Object.keys(data).forEach(function(iso3) { const dt = data[iso3]; if (!dt) return; // Parse YYYY-MM-DD to friendly display format try { const d = new Date(dt + 'T12:00:00Z'); const opts = { month: 'short', day: 'numeric' }; const friendly = d.toLocaleDateString('en-US', opts) + ', ' + d.getFullYear(); if (VD[iso3]) { VD[iso3].nextMtg = friendly; updated++; } } catch(e) { if (VD[iso3]) { VD[iso3].nextMtg = dt; updated++; } } }); console.log('[CB Meetings] Updated', updated, 'countries'); // Re-render money section if currently visible if (typeof CID !== 'undefined' && CID && typeof renderPanel === 'function') { const activePill = document.querySelector('.cp-sec-pill.active'); if (activePill && activePill.textContent.includes('Monetario')) { renderPanel(CID); } } } catch(e) { console.debug('[CB Meetings] load failed:', e.message); } } // Energy: load on-demand per country when selected (proven reliable path) window.ENERGY_DATA = window.ENERGY_DATA || {}; window._energyLoading = window._energyLoading || {}; function _mergeEnergyIntoExtras(iso3, e) { if (!e || !iso3) return; if (!STATIC_EXTRAS[iso3]) STATIC_EXTRAS[iso3] = {}; const ex = STATIC_EXTRAS[iso3]; const fields = [ 'elec_ren_share','elec_solar_share','elec_wind_share','elec_coal_share', 'elec_gas_share','elec_hydro_share','elec_nuclear_share','elec_fossil_share', 'elec_oil_share','elec_lowcarbon_share','elec_total_twh','elec_ren_twh', 'fossilFuelShare_energy','renewShare_energy','co2pc','ghg_total_mt', 'oilProd_twh','gasProd_twh','coalProd_twh','energyPC_kwh','elecPC_kwh', 'carbonIntensity_elec','energy_year', ]; fields.forEach(function(f) { if (e[f] != null) ex[f] = e[f]; }); } async function loadCountryEnergy(iso3) { if (!iso3) return; // Already loaded or loading if (window.ENERGY_DATA[iso3] || window._energyLoading[iso3]) return; window._energyLoading[iso3] = true; try { const res = await fetch(MERIDIAN_API + '/api/v3/country/' + iso3 + '/energy', { signal: AbortSignal.timeout(20000) }); if (!res.ok) return; const data = await res.json(); if (data && !data.error) { window.ENERGY_DATA[iso3] = data; _mergeEnergyIntoExtras(iso3, data); console.log('[Energy]', iso3, ':', data.elec_ren_share + '% ren, year=' + data.energy_year); // Re-render energy section if country is still active if (typeof CID !== 'undefined' && CID === iso3 && CP_ACTIVE_SECTION === 'energy') { const el = document.getElementById('cp-content'); if (el) el.innerHTML = renderSectionContent('energy', iso3); } } } catch(e) { console.debug('[Energy]', iso3, e.message); } finally { delete window._energyLoading[iso3]; } } // Keep for backward compat (called in init) async function loadGlobalEnergyData() { // Pre-load energy for current country only; others load on demand if (typeof CID !== 'undefined' && CID) { await loadCountryEnergy(CID); } } async function init(){ buildDD(); // Pre-populate ticker immediately with static values — no blank dashes while loading updateCommodityTicker(); try{ const r=await fetch('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json'); GEO=await r.json(); document.getElementById('fload').style.display='none'; renderMap(); }catch(e){document.getElementById('fload').textContent='Map failed to load';} buildMicrostatePanel(); // Populate ticker with static values immediately (before backend responds) Object.keys(CMDT_STATIC).forEach(function(k){ if(!CMDT_CACHE[k]) CMDT_CACHE[k]=CMDT_STATIC[k]; }); updateCommodityTicker(); loadAllAPIs(); // ── Auto-start EMPRESA si hay perfil guardado (flujo: Landing → Onboarding → Empresa) ── (function _autoStartEmpresa() { try { var _p = (typeof loadProfile === 'function') ? loadProfile() : null; if (_p && _p.sector) { window.RUMBO_STATE = Object.assign(window.RUMBO_STATE || {}, _p); RUMBO_STATE = window.RUMBO_STATE; // Normalizar sector granular → clave de sectorMeta (misma tabla que en activateRumbo) var _snm = { agricultura:'agro',ganaderia:'agro',avicultura:'agro',acuicultura:'agro', floricultura:'agro',forestal:'agro',apicultura:'agro', alimentos_procesados:'alimentos',panaderia_tortilleria:'alimentos', lacteos:'alimentos',carnicos:'alimentos',bebidas:'alimentos', aceites_grasas:'alimentos',azucar_confiteria:'alimentos',restaurantes:'alimentos', automotriz:'manufactura',electronica:'manufactura',metalmecanica:'manufactura', plasticos_caucho:'manufactura',quimica_petroquimica:'manufactura', textil_confeccion:'manufactura',papel_carton:'manufactura', maquinaria_equipo:'manufactura',farmaceutica:'manufactura', construccion_residencial:'construccion',construccion_comercial:'construccion', infraestructura:'construccion',vidrio_cristal:'construccion', cemento_materiales:'construccion',acero_aluminio:'construccion', madera_muebles:'construccion', comercio_mayoreo:'comercio',comercio_menudeo:'comercio',ecommerce:'comercio', distribucion_importacion:'comercio',abarrotes:'comercio', transporte_carga:'transporte',transporte_pasajeros:'transporte', logistica_almacenamiento:'transporte',paqueteria:'transporte', transporte_maritimo:'transporte', petroleo_gas:'energia',energia_electrica:'energia',energia_renovable:'energia', mineria_metalica:'energia',mineria_no_metalica:'energia',agua:'energia', hoteleria:'turismo',agencias_viaje:'turismo',entretenimiento:'turismo', financiero_banca:'financiero',seguros:'financiero', bienes_raices:'realestate', consultoria:'tecnologia',tecnologia_software:'tecnologia', medios_comunicacion:'tecnologia', salud_hospitales:'salud',educacion:'salud',otro:'comercio', }; var _raw = RUMBO_STATE.sector; var _norm = RUMBO_STATE.sectorBeta || _snm[_raw] || _raw; RUMBO_STATE.sector = _norm; window.RUMBO_STATE.sector = _norm; // Delay para que todo el DOM esté listo antes de activar setTimeout(function() { if (typeof activateRumbo === 'function') activateRumbo(); }, 300); return; // No seleccionar USA en modo Empresa } } catch(e) { console.warn('[AutoStart]', e); } })(); selC('USA'); // Start live data refresh (60s FX/markets, 10min V2 country data) startLiveRefresh(); // Load live data from Railway backend (non-blocking) loadBulkCountryData(); loadCBMeetings(); loadGlobalEnergyData(); // Flash de Datos — check Railway for recent macro events (delayed so main data loads first) setTimeout(_initFlash, 5000); setInterval(_initFlash, 30 * 60 * 1000); // refresh every 30 min // ── MERIDIAN SCENARIO STUDIO — Slider Engine ───────────────────────────────── // _scUpdate(): reads 4 sliders → calls window._calcSc() → renders live P&L // _scPreset(name): loads preset shock values → calls _scUpdate() function _scUpdate() { if (!window._SC || typeof window._calcSc !== 'function') return; var S = window._SC; var dg = parseFloat(document.getElementById('sc-sl-gdp')?.value || 0); var dc = parseFloat(document.getElementById('sc-sl-cpi')?.value || 0); var dr = parseFloat(document.getElementById('sc-sl-rate')?.value || 0); var df = parseFloat(document.getElementById('sc-sl-fx')?.value || 0); // Update delta labels and result values var bases = {gdp: S.base.gdpG, cpi: S.base.cpi, rate: S.base.rate, fx: S.base.fx}; var sliders = [ {id:'sc-sl-gdp', val:dg, base:bases.gdp, unit:'%', fmt:1, inv:false, color:'#2563EB'}, {id:'sc-sl-cpi', val:dc, base:bases.cpi, unit:'pp', fmt:1, inv:true, color:'#DC2626'}, {id:'sc-sl-rate', val:dr, base:bases.rate, unit:'%', fmt:2, inv:true, color:'#7C3AED'}, {id:'sc-sl-fx', val:df, base:bases.fx, unit:'%', fmt:1, inv:true, color:'#D97706'}, ]; sliders.forEach(function(sl) { var dEl = document.getElementById(sl.id + '-d'); var vEl = document.getElementById(sl.id + '-v'); if (dEl) { var sign = sl.val >= 0 ? '+' : ''; dEl.textContent = sign + sl.val.toFixed(sl.fmt) + sl.unit; var isNeg = sl.inv ? sl.val > 0 : sl.val < 0; dEl.style.color = sl.val === 0 ? '#9CA3AF' : (isNeg ? '#DC2626' : '#059669'); } if (vEl) { var result = sl.base + sl.val; vEl.textContent = (sl.fmt === 2 ? result.toFixed(2) : result.toFixed(1)) + '%'; vEl.style.color = sl.color; } }); // Compute P&L for 3 horizons var res3 = window._calcSc(dg, dc, dr, df); // uses the closed-over renderRumboMain vars var res6 = res3; // calcScenario returns {h3,h6,h12} — use them var h3 = res3.h3, h6 = res3.h6, h12 = res3.h12; // Helper: color by value direction function valColor(v, inv) { if (Math.abs(v) < 0.05) return '#6B7280'; if (inv) return v > 0 ? '#991B1B' : '#065F46'; return v > 0 ? '#065F46' : '#991B1B'; } function valFmt(v) { return (v >= 0 ? '+' : '') + v.toFixed(1) + 'pp'; } function sigText(v, inv) { var pos = inv ? v < -0.3 : v > 0.5; var neg = inv ? v > 1 : v < -0.3; if (pos) return '▲ POSITIVO'; if (neg) return '▼ ADVERSO'; return '◆ NEUTRO'; } function sigColor(v, inv) { var pos = inv ? v < -0.3 : v > 0.5; var neg = inv ? v > 1 : v < -0.3; return pos ? '#065F46' : (neg ? '#991B1B' : '#6B7280'); } // Row definitions → map to h3/h6/h12 values var rows = [ {id:'rev', v3:h3.rev, v6:h6.rev, v12:h12.rev, inv:false}, {id:'fxrev', v3:h3.rev-h3.rev*0.6, v6:h6.rev-h6.rev*0.6, v12:h12.fxRev||0, inv:false}, {id:'cpicost', v3:h3.cpiCost||h3.cost*0.4, v6:h6.cpiCost||h6.cost*0.4, v12:h12.cpiCost||h12.cost*0.4, inv:true}, {id:'fxcost', v3:h3.fxCost||0, v6:h6.fxCost||0, v12:h12.fxCost||0, inv:true}, {id:'rcost', v3:h3.rCost||0, v6:h6.rCost||0, v12:h12.rCost||0, inv:true}, {id:'wcost', v3:h3.wCost||0, v6:h6.wCost||0, v12:h12.wCost||0, inv:true}, {id:'margin', v3:h3.margin, v6:h6.margin, v12:h12.margin, inv:false}, ]; rows.forEach(function(row) { ['3','6','12'].forEach(function(hz) { var el = document.getElementById('sc-c' + hz + '-' + row.id); if (!el) return; var v = hz === '3' ? row.v3 : (hz === '6' ? row.v6 : row.v12); if (v === 0 && row.id !== 'margin') { el.textContent = '—'; el.style.color = '#9CA3AF'; return; } el.textContent = valFmt(v); el.style.color = valColor(v, row.inv); }); var sigEl = document.getElementById('sc-sig-' + row.id); if (sigEl) { sigEl.textContent = sigText(row.v12, row.inv); sigEl.style.color = sigColor(row.v12, row.inv); } }); // Update signal bar var margin12 = h12.margin; var signalEl = document.getElementById('sc-signal-text'); var marginEl = document.getElementById('sc-margin-12'); var barEl = document.getElementById('sc-signal-bar'); var sigTxt, sigC, sigBg, sigBorder; if (margin12 > 0.8) { sigTxt='▲ FAVORABLE'; sigC='#065F46'; sigBg='#ECFDF5'; sigBorder='#A7F3D0'; } else if (margin12 < -0.8) { sigTxt='▼ ADVERSO'; sigC='#7F1D1D'; sigBg='#FEF2F2'; sigBorder='#FECACA'; } else { sigTxt='◆ NEUTRAL'; sigC='#78350F'; sigBg='#FFFBEB'; sigBorder='#FDE68A'; } if (signalEl) { signalEl.textContent = sigTxt; signalEl.style.color = sigC; } if (marginEl) { marginEl.textContent = valFmt(margin12); marginEl.style.color = sigC; } if (barEl) { barEl.style.background = sigBg; barEl.style.borderBottomColor = sigBorder; } } function _scPreset(name) { if (!window._SC) return; var p = name === 'reset' ? {dg:0,dc:0,dr:0,df:0} : window._SC.presets[name]; if (!p) return; var map = { 'sc-sl-gdp': p.dg, 'sc-sl-cpi': p.dc, 'sc-sl-rate': p.dr, 'sc-sl-fx': p.df, }; Object.keys(map).forEach(function(id) { var el = document.getElementById(id); if (el) el.value = map[id]; }); _scUpdate(); } // Run once when tab opens (rumboTab already calls _scUpdate via drawAllCharts hook) // Also expose for rumboTab to call when switching to tab 'sc' window._scUpdate = _scUpdate; } const ro=new ResizeObserver(()=>{if(GEO)renderMap();}); ro.observe(document.getElementById('fmap')); init(); // ── MERIDIAN TOOLTIP SYSTEM ────────────────────────────────────────────────── // Lightweight tooltip: reads content from window._TIPS[data-tipkey]. // Smart positioning: appears above element, flips below if near top of screen. function _tipInit() { if (document.getElementById('m-tipbox')) return; var box = document.createElement('div'); box.id = 'm-tipbox'; document.body.appendChild(box); } function _tipShow(evt, el) { var box = document.getElementById('m-tipbox'); if (!box) { _tipInit(); box = document.getElementById('m-tipbox'); } if (!box) return; var key = el.getAttribute('data-tipkey'); var txt = (window._TIPS && window._TIPS[key]) ? window._TIPS[key] : key; box.textContent = txt; box.style.display = 'block'; // Measure after showing so offsetHeight is correct var bW = Math.min(300, window.innerWidth - 20); box.style.maxWidth = bW + 'px'; var rect = el.getBoundingClientRect(); var bH = box.offsetHeight; // Horizontal: center on element, clamp to screen var left = rect.left + rect.width / 2 - bW / 2; left = Math.max(8, Math.min(left, window.innerWidth - bW - 8)); // Vertical: prefer above, flip below if not enough room var topAbove = rect.top - bH - 10; var topBelow = rect.bottom + 10; var top = topAbove > 8 ? topAbove : topBelow; box.style.left = left + 'px'; box.style.top = top + 'px'; // Flip arrow if showing below box.style.setProperty('--arrow-top', topAbove > 8 ? 'auto' : '-7px'); } function _tipHide() { var box = document.getElementById('m-tipbox'); if (box) box.style.display = 'none'; } // ── MERIDIAN PDF EXPORT ────────────────────────────────────────────────────── // Converts canvas charts to PNG images, injects section labels, then prints. // beforeprint/afterprint restore the original DOM state. function _meridianPrint() { window.print(); } window.addEventListener('beforeprint', function() { // Only run when EMPRESA report is active var main = document.getElementById('rumbo-main'); if (!main || main.style.display === 'none') return; window._mpr = []; // ── 1. Canvas → PNG images ────────────────────────────────────────────── main.querySelectorAll('canvas').forEach(function(canvas) { try { var h = canvas.offsetHeight || parseInt(canvas.style.height) || 90; var w = canvas.offsetWidth || canvas.width || 320; // Draw to a fresh canvas to avoid taint issues var tmp = document.createElement('canvas'); tmp.width = Math.max(w * 2, 640); // 2× for retina-quality print tmp.height = Math.max(h * 2, 180); var tCtx = tmp.getContext('2d'); tCtx.drawImage(canvas, 0, 0, tmp.width, tmp.height); var img = document.createElement('img'); img.src = tmp.toDataURL('image/png', 1.0); img.className = 'm-print-img'; img.style.cssText = 'display:block;width:100%;height:'+h+'px;border-radius:4px;object-fit:fill'; canvas.parentNode.insertBefore(img, canvas); window._mpr.push({type:'img', canvas:canvas, img:img}); } catch(e) { // Tainted or empty canvas — skip silently } }); // ── 2. Section divider labels ──────────────────────────────────────────── var SECS = [ {id:'e-tab-ov', txt:'Vista General'}, {id:'e-tab-fc', txt:'Forecast Macroeconómico — 4 Trimestres'}, {id:'e-tab-sc', txt:'Escenarios & Stress Test'}, {id:'e-tab-rx', txt:'Matriz de Riesgos'}, {id:'e-tab-ac', txt:'Plan de Acciones Estratégicas'}, ]; SECS.forEach(function(s, idx) { var tab = document.getElementById(s.id); if (!tab) return; var inner = tab.firstElementChild; if (!inner) return; var lbl = document.createElement('div'); lbl.className = 'm-print-section'; lbl.textContent = 'MERIDIAN ENTERPRISE ANALYTICS · ' + s.txt.toUpperCase(); inner.insertBefore(lbl, inner.firstChild); window._mpr.push({type:'label', el:lbl}); }); }); window.addEventListener('afterprint', function() { if (!window._mpr) return; window._mpr.forEach(function(item) { if (item.type === 'img') { if (item.img && item.img.parentNode) item.img.parentNode.removeChild(item.img); } else if (item.type === 'label') { if (item.el && item.el.parentNode) item.el.parentNode.removeChild(item.el); } }); window._mpr = null; }); // Polls Railway /api/v3/flash for recent Banxico/INEGI events. // Shows banner via meridianFlash(). Caches per-session to avoid repeat shows. async function _initFlash() { try { // Determine active country (prefer EMPRESA country if in that mode, else MEX default) var iso3 = 'MEX'; if (typeof RUMBO_STATE !== 'undefined' && RUMBO_STATE.country) iso3 = RUMBO_STATE.country; else if (typeof window._activeISO === 'string') iso3 = window._activeISO; // Skip if this flash was already dismissed or shown this session var sessionKey = 'mflash_' + iso3 + '_' + new Date().toISOString().slice(0,10); if (sessionStorage.getItem(sessionKey + '_dismissed')) return; var url = MERIDIAN_API + '/api/v3/flash?iso3=' + iso3; var r = await fetch(url, { signal: AbortSignal.timeout(10000) }); if (!r.ok) return; var data = await r.json(); if (!data.has_flash) return; // Don't re-show same event text within this session var lastShown = sessionStorage.getItem(sessionKey + '_text'); if (lastShown === data.text) return; sessionStorage.setItem(sessionKey + '_text', data.text); // Show — clicking link navigates to EMPRESA for the active country var linkFn = null; if (data.event_type === 'cb_decision' || data.event_type === 'data_release') { linkFn = function() { if (typeof activateRumbo === 'function') { // Pre-set country and go to Forecast tab if (typeof RUMBO_STATE !== 'undefined') RUMBO_STATE.country = iso3; activateRumbo(); setTimeout(function() { if (typeof rumboTab === 'function') rumboTab('fc'); }, 400); } document.getElementById('m-flash').style.display = 'none'; sessionStorage.setItem(sessionKey + '_dismissed', '1'); }; } // Style banner by severity var flashEl = document.getElementById('m-flash'); if (flashEl && data.severity === 'high') { flashEl.style.background = '#FEF2F2'; flashEl.style.borderColor = '#FECACA'; flashEl.style.color = '#7F1D1D'; var dot = flashEl.querySelector('.m-flash-dot'); if (dot) dot.style.background = '#EF4444'; } meridianFlash(data.text, data.link_text || 'Ver impacto →', linkFn); } catch(e) { // Flash is non-critical — fail silently console.log('[Flash] No disponible:', e.message); } } // Track the active country for flash context var _origSelC = typeof selC === 'function' ? selC : null; if (_origSelC) { selC = function(iso) { window._activeISO = iso; return _origSelC(iso); }; }
Meridian
Global Economics
⌨ Atajos MERIDIAN
M MacroE Empresa15 Tabs
PaísP PDFD Dark
Presiona ? para mostrar / ocultar