// A global store of active Chart.js instances
let chartInstances = {};
/**
* Mapping from raw JSON metric keys to user-friendly names.
* (Unchanged from your existing code)
*/
const metricNameMap = {
gdp_growth: "GDP Growth (%)",
unemployment_rate: "Unemployment Rate (%)",
ppiaco: "Producer Price Index (All Commodities)",
ism_pmi: "ISM Purchasing Managers’ Index",
outlook: "NAM Outlook Survey",
factory_orders: "Factory Orders",
durable_goods: "Durable Goods Orders",
quarterly_profits: "Quarterly Profits (Manufacturing)",
manemp: "Manufacturing Employment",
jts3000jol: "Manufacturing Job Openings (JOLTS)",
vapgdpma: "Mfg Value-Added Share of GDP",
bopgtb: "Goods Trade Balance",
bopgexp: "Goods Exports",
bopgimp: "Goods Imports",
iron_ore: "Iron Ore Price",
"HG1:COM": "Copper Futures (HG1)",
"LMAHDS03:COM": "Aluminum (LME 3-Month)",
"XAUUSD:CUR": "Gold Spot Price",
"SPX:IND": "S&P 500 Index",
"DXY:CUR": "U.S. Dollar Index",
"SCO:COM": "SCO A 2× short (leveraged) ETF on crude oil prices"
};
/**
* Mapping from metric keys (exactly matching the JSON) to a group name
* that corresponds to
in dashboard.php.
*/
const metricGroupMap = {
// ----- NATIONAL -----
"gdp_growth": "national",
"unemployment_rate": "national",
"ppiaco": "national",
// ----- SENTIMENT -----
"ism_pmi": "sentiment",
"outlook": "sentiment",
// ----- MANUFACTURERS -----
"factory_orders": "manufacturers",
"durable_goods": "manufacturers",
"quarterly_profits": "manufacturers",
"manemp": "manufacturers",
"jts3000jol": "manufacturers",
"vapgdpma": "manufacturers",
// ----- TRADE -----
"bopgtb": "trade",
"bopgexp": "trade",
"bopgimp": "trade",
// ----- commodities -----
"iron_ore": "commodities",
"HG1:COM": "commodities",
"LMAHDS03:COM": "commodities",
"XAUUSD:CUR": "commodities",
"SCO:COM": "commodities",
// ----- MARKETS -----
"SPX:IND": "markets",
"DXY:CUR": "markets"
};
// Fetch the JSON data from the URL provided by WordPress (econData.json_url)
fetch(econData.json_url)
.then(res => res.json())
.then(data => {
// 1. Build tiles in the dashboard
Object.entries(data).forEach(([metric, series]) => {
if (!Array.isArray(series) || series.length < 1) return;
const latest = series[series.length - 1];
const prev = series.length > 1 ? series[series.length - 2] : null;
const value = latest.value;
const delta = latest.yoy_percent_change ?? latest.change_vs_previous ?? 0;
const isUp = delta > 0;
const groupKey = metricGroupMap[metric] || 'other';
const container = document.querySelector(`.dashboard-group[data-group="${groupKey}"]`);
if (!container) return;
// Use a friendly name if available, else fallback
const displayName = metricNameMap[metric] || formatTitle(metric);
// Create the tile
const tile = document.createElement('div');
tile.className = 'metric-tile';
tile.dataset.metric = metric;
tile.innerHTML = `
`;
container.appendChild(tile);
});
// 2. Attach click events for toggling each chart
document.querySelectorAll('.tile-header').forEach(header => {
header.addEventListener('click', () => {
const parent = header.parentElement;
const chartBox = parent.querySelector('.tile-chart');
const canvas = chartBox.querySelector('canvas');
const metric = parent.dataset.metric;
// Toggle the hidden class
if (chartBox.classList.contains('hidden')) {
chartBox.classList.remove('hidden');
if (!chartInstances[metric]) {
// Build the dataset with global no points
const datasetOptions = {
label: displayNameOrFallback(metric),
data: data[metric].map(d => d.value),
borderWidth: 2,
borderColor: '#333',
fill: false,
pointRadius: 0,
pointHoverRadius: 0
};
chartInstances[metric] = new Chart(canvas, {
type: 'line',
data: {
labels: data[metric].map(d => d.date),
datasets: [ datasetOptions ]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: { mode: 'index', intersect: false }
},
scales: {
y: {
title: {
display: true,
text: displayNameOrFallback(metric)
}
},
x: {
ticks: { autoSkip: true, maxTicksLimit: 12 }
}
}
}
});
}
} else {
chartBox.classList.add('hidden');
}
});
});
// 3. Collapse chart button
document.querySelectorAll('.collapse-chart').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const chartBox = btn.closest('.tile-chart');
chartBox.classList.add('hidden');
});
});
// 4. Press 'e' to export all open charts as PNG
document.addEventListener('keydown', e => {
if (e.key === 'e') {
const openCharts = Object.values(chartInstances).filter(c => {
const tileChart = c.canvas.closest('.tile-chart');
return tileChart && !tileChart.classList.contains('hidden');
});
openCharts.forEach(c => {
const link = document.createElement('a');
link.download = `${c.canvas.closest('.metric-tile').dataset.metric}-chart.png`;
link.href = c.toBase64Image();
link.click();
});
}
});
// 5. Build the scrolling ticker AFTER tiles are created
updateTicker();
/**
* Helper function for charts:
* If metricNameMap doesn't have a display name, use formatTitle
*/
function displayNameOrFallback(metricKey) {
return metricNameMap[metricKey] || formatTitle(metricKey);
}
});
// UTILITY FUNCTION: Format numeric values
function formatValue(val) {
if (typeof val === 'number') {
if (val > 1000) return '$' + (val / 1000).toFixed(1) + 'b';
if (val % 1 === 0) return val;
return val.toFixed(1);
}
return val;
}
// UTILITY FUNCTION: Turn e.g. "XAUUSD:CUR" into "Xauusd: Cur"
function formatTitle(key) {
return key
.replace(/_/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase());
}
/*
* NEW FUNCTION: Build the Ticker
* - Grabs all .metric-tile elements
* - For each tile, extracts (title, value, delta)
* - Creates a .ticker-item that scrolls across the ticker
* - Clicking on the ticker item scrolls to that tile & opens its chart
*/
function updateTicker() {
const tickerInner = document.getElementById('ticker-inner');
if (!tickerInner) return;
// Clear any existing ticker content.
tickerInner.innerHTML = '';
// Collect all dashboard metric tiles.
const tiles = document.querySelectorAll('.metric-tile');
tiles.forEach(tile => {
const metric = tile.dataset.metric;
const titleElem = tile.querySelector('.tile-header h3');
const valueElem = tile.querySelector('.tile-value');
const deltaElem = tile.querySelector('.tile-delta');
// Proceed only if we have all necessary elements.
if (!titleElem || !valueElem || !deltaElem) return;
const titleText = titleElem.textContent.trim();
// Assume the first text node in .tile-value is the numeric value.
const valueText = valueElem.childNodes[0].textContent.trim();
const deltaText = deltaElem.textContent.trim();
const upOrDown = deltaElem.classList.contains('up') ? 'up' : 'down';
// Create the ticker item container.
const item = document.createElement('div');
item.className = 'ticker-item';
item.innerHTML = `
${titleText}
${valueText}
${deltaText}
`;
// Click event: scroll to the corresponding tile and open its chart.
item.addEventListener('click', () => {
const targetTile = document.querySelector(`.metric-tile[data-metric="${metric}"]`);
if (!targetTile) return;
// Open the chart if hidden.
const chartBox = targetTile.querySelector('.tile-chart.hidden');
if (chartBox) {
targetTile.querySelector('.tile-header').click();
}
// Smoothly scroll the tile into view.
targetTile.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
tickerInner.appendChild(item);
});
// After building ticker items, add the 'animate' class to start the animation.
tickerInner.classList.add('animate');
}
updateTicker();