(async () => {
const mount = document.getElementById('nam-econ-dashboard');
if (!mount) return;
// 1. Define your sets at top-level
const money = new Set([
'gdp_from_mfg',
'goods_exported',
'goods_imported',
'goods_trade_balance',
'mfg_quarterly_profits'
]);
const pct = new Set([
'gdp_growth_rate',
'mfg_pct_gdp',
'capacity_utilization',
'unemployment_rate'
]);
// 2. Fetch data and metadata
const [rows, meta] = await Promise.all([
fetch(NAM_DASH.data_url, { cache:'no-cache' }).then(r => r.json()),
fetch(NAM_DASH.meta_url, { cache:'no-cache' }).then(r => r.json())
]);
const metaMap = Object.fromEntries(meta.map(m => [m.indicator, m]));
// 3. Formatter uses those sets
function formatValue(ind, raw) {
const v = Number(raw);
const abs = Math.abs(v);
const isInt = Number.isInteger(abs);
// Thousands with comma separators
if (ind === 'mfg_job_openings') {
return v.toLocaleString(undefined, { maximumFractionDigits: 0 });
}
// Money ? $ prefix, B suffix
if (money.has(ind)) {
const num = isInt ? abs.toFixed(0) : abs.toFixed(1);
return (v < 0 ? '-$' : '$') + num + 'B';
}
// Percent ? % suffix
if (pct.has(ind)) {
const num = isInt ? abs.toFixed(0) : abs.toFixed(1);
return (v < 0 ? '-' : '') + num + '%';
}
// Default numeric
return isInt ? abs.toFixed(0) : abs.toFixed(1);
}
// 4. Group by indicator and render
const byInd = rows.reduce((m, r) => {
(m[r.indicator] ||= []).push(r);
return m;
}, {});
for (const [ind, series] of Object.entries(byInd)) {
const info = metaMap[ind];
if (!info) continue;
series.sort((a,b) => new Date(a.date) - new Date(b.date));
const diff = info.current_value - info.previous_value;
const diffSign = diff > 0 ? '+' : diff < 0 ? '-' : '';
const diffFormatted = formatValue(ind, Math.abs(diff));
const card = document.createElement('article');
card.className = 'card';
card.innerHTML = `
${info.current_date}
△ vs ${info.previous_date}: ${diffSign}${diffFormatted}