385 lines
12 KiB
JavaScript
385 lines
12 KiB
JavaScript
const API_BASE = '/api/v1';
|
|
let chart = null;
|
|
let currentSeriesData = [];
|
|
let currentPage = 1;
|
|
const itemsPerPage = 20;
|
|
|
|
const popularSeries = ['GNPCA', 'GDP', 'UNRATE', 'CPIAUCSL', 'FEDFUNDS', 'M2SL', 'SP500', 'EXUSEU'];
|
|
|
|
function init() {
|
|
updateTime();
|
|
setInterval(updateTime, 1000);
|
|
loadPopularSeries();
|
|
loadCategories();
|
|
loadReleases();
|
|
initChart();
|
|
}
|
|
|
|
function updateTime() {
|
|
const now = new Date();
|
|
document.getElementById('current-time').textContent = now.toLocaleString();
|
|
}
|
|
|
|
function initChart() {
|
|
const ctx = document.getElementById('dataChart').getContext('2d');
|
|
chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
label: 'Value',
|
|
data: [],
|
|
borderColor: '#1a73e8',
|
|
backgroundColor: 'rgba(26, 115, 232, 0.1)',
|
|
fill: true,
|
|
tension: 0.3,
|
|
pointRadius: 3,
|
|
pointHoverRadius: 6
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top'
|
|
},
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
ticks: {
|
|
maxRotation: 45,
|
|
minRotation: 45
|
|
}
|
|
},
|
|
y: {
|
|
beginAtZero: false
|
|
}
|
|
},
|
|
interaction: {
|
|
mode: 'nearest',
|
|
axis: 'x',
|
|
intersect: false
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async function searchSeries() {
|
|
const searchText = document.getElementById('search-input').value.trim();
|
|
if (!searchText) {
|
|
alert('Please enter a search term');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await axios.get(`${API_BASE}/series/search`, {
|
|
params: { search_text: searchText, limit: 20 }
|
|
});
|
|
|
|
if (response.data && response.data.seriess && response.data.seriess.length > 0) {
|
|
displaySearchResults(response.data.seriess);
|
|
} else {
|
|
alert('No results found');
|
|
}
|
|
} catch (error) {
|
|
console.error('Search error:', error);
|
|
alert('Search failed: ' + (error.response?.data?.error_message || error.message));
|
|
}
|
|
}
|
|
|
|
function displaySearchResults(results) {
|
|
const select = document.getElementById('series-select');
|
|
select.innerHTML = '<option value="">-- Search Results --</option>';
|
|
|
|
results.forEach(series => {
|
|
const option = document.createElement('option');
|
|
option.value = series.id;
|
|
option.textContent = `${series.id} - ${series.title}`;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
if (results.length > 0) {
|
|
select.value = results[0].id;
|
|
loadSeriesData();
|
|
}
|
|
}
|
|
|
|
async function loadPopularSeries() {
|
|
const select = document.getElementById('series-select');
|
|
select.innerHTML = '<option value="">-- Popular Series --</option>';
|
|
|
|
popularSeries.forEach(seriesId => {
|
|
const option = document.createElement('option');
|
|
option.value = seriesId;
|
|
option.textContent = seriesId;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
if (popularSeries.length > 0) {
|
|
select.value = popularSeries[0];
|
|
loadSeriesData();
|
|
}
|
|
}
|
|
|
|
async function loadSeriesData() {
|
|
const seriesId = document.getElementById('series-select').value;
|
|
if (!seriesId) {
|
|
document.getElementById('chart-placeholder').style.display = 'block';
|
|
document.getElementById('dataChart').style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const [seriesRes, obsRes] = await Promise.all([
|
|
axios.get(`${API_BASE}/series`, { params: { series_id: seriesId } }),
|
|
axios.get(`${API_BASE}/series/observations`, { params: { series_id: seriesId, limit: 1000 } })
|
|
]);
|
|
|
|
const series = seriesRes.data.series;
|
|
const observations = obsRes.data.observations || [];
|
|
|
|
displaySeriesInfo(series);
|
|
displayChart(observations, seriesId);
|
|
currentSeriesData = observations;
|
|
renderTable(1);
|
|
updatePagination();
|
|
|
|
document.getElementById('chart-placeholder').style.display = 'none';
|
|
document.getElementById('dataChart').style.display = 'block';
|
|
} catch (error) {
|
|
console.error('Load series error:', error);
|
|
alert('Failed to load series: ' + (error.response?.data?.error_message || error.message));
|
|
}
|
|
}
|
|
|
|
function displaySeriesInfo(series) {
|
|
const info = document.getElementById('series-info');
|
|
info.innerHTML = `
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<p><strong>Series ID:</strong> ${series.id}</p>
|
|
<p><strong>Title:</strong> ${series.title}</p>
|
|
<p><strong>Frequency:</strong> ${series.frequency}</p>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<p><strong>Units:</strong> ${series.units}</p>
|
|
<p><strong>Seasonal Adjustment:</strong> ${series.seasonal_adjustment}</p>
|
|
<p><strong>Last Updated:</strong> ${series.last_updated}</p>
|
|
</div>
|
|
</div>
|
|
${series.notes ? `<p class="mt-2"><strong>Notes:</strong> ${series.notes}</p>` : ''}
|
|
`;
|
|
document.getElementById('chart-title').textContent = series.title || series.id;
|
|
}
|
|
|
|
function displayChart(observations, seriesId) {
|
|
const labels = observations.map(o => o.date).reverse();
|
|
const data = observations.map(o => parseFloat(o.value) || null).reverse();
|
|
|
|
chart.data.labels = labels;
|
|
chart.data.datasets[0].data = data;
|
|
chart.data.datasets[0].label = seriesId;
|
|
chart.update();
|
|
}
|
|
|
|
function renderTable(page) {
|
|
currentPage = page;
|
|
const start = (page - 1) * itemsPerPage;
|
|
const end = start + itemsPerPage;
|
|
const pageData = currentSeriesData.slice(start, end);
|
|
|
|
const tbody = document.getElementById('data-table-body');
|
|
if (pageData.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="2" class="text-center text-muted">No data available</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = pageData.map(obs => `
|
|
<tr>
|
|
<td>${obs.date}</td>
|
|
<td>${obs.value || 'N/A'}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
function updatePagination() {
|
|
const totalPages = Math.ceil(currentSeriesData.length / itemsPerPage);
|
|
const pagination = document.getElementById('pagination');
|
|
|
|
if (totalPages <= 1) {
|
|
pagination.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
|
|
if (currentPage > 1) {
|
|
html += `<li class="page-item"><a class="page-link" href="#" onclick="renderTable(${currentPage - 1})">Prev</a></li>`;
|
|
}
|
|
|
|
for (let i = 1; i <= totalPages; i++) {
|
|
if (i === currentPage) {
|
|
html += `<li class="page-item active"><a class="page-link" href="#">${i}</a></li>`;
|
|
} else if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
|
|
html += `<li class="page-item"><a class="page-link" href="#" onclick="renderTable(${i})">${i}</a></li>`;
|
|
} else if (i === currentPage - 3 || i === currentPage + 3) {
|
|
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
|
}
|
|
}
|
|
|
|
if (currentPage < totalPages) {
|
|
html += `<li class="page-item"><a class="page-link" href="#" onclick="renderTable(${currentPage + 1})">Next</a></li>`;
|
|
}
|
|
|
|
pagination.innerHTML = html;
|
|
}
|
|
|
|
async function loadCategories() {
|
|
try {
|
|
const response = await axios.get(`${API_BASE}/category/children`, {
|
|
params: { category_id: 0 }
|
|
});
|
|
|
|
const list = document.getElementById('categories-list');
|
|
if (response.data && response.data.categories) {
|
|
list.innerHTML = response.data.categories.map(cat => `
|
|
<div class="category-item" onclick="loadCategorySeries(${cat.id}, '${cat.name}')">
|
|
${cat.name}
|
|
</div>
|
|
`).join('');
|
|
}
|
|
} catch (error) {
|
|
console.error('Load categories error:', error);
|
|
}
|
|
}
|
|
|
|
async function loadCategorySeries(categoryId, categoryName) {
|
|
try {
|
|
const response = await axios.get(`${API_BASE}/category/series`, {
|
|
params: { category_id: categoryId, limit: 10 }
|
|
});
|
|
|
|
const select = document.getElementById('series-select');
|
|
select.innerHTML = `<option value="">-- ${categoryName} --</option>`;
|
|
|
|
if (response.data && response.data.seriess) {
|
|
response.data.seriess.forEach(series => {
|
|
const option = document.createElement('option');
|
|
option.value = series.id;
|
|
option.textContent = `${series.id} - ${series.title}`;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
if (response.data.seriess.length > 0) {
|
|
select.value = response.data.seriess[0].id;
|
|
loadSeriesData();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Load category series error:', error);
|
|
}
|
|
}
|
|
|
|
async function loadReleases() {
|
|
try {
|
|
const response = await axios.get(`${API_BASE}/releases/dates`, {
|
|
params: { limit: 10, start_date: getDateDaysAgo(30) }
|
|
});
|
|
|
|
const list = document.getElementById('releases-list');
|
|
if (response.data && response.data.release_dates) {
|
|
list.innerHTML = response.data.release_dates.map(rel => `
|
|
<div class="release-item" onclick="loadReleaseSeries(${rel.release_id}, '${rel.release_name}')">
|
|
<div class="fw-bold">${rel.release_name}</div>
|
|
<div class="release-date">${rel.date}</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
} catch (error) {
|
|
console.error('Load releases error:', error);
|
|
}
|
|
}
|
|
|
|
async function loadReleaseSeries(releaseId, releaseName) {
|
|
try {
|
|
const response = await axios.get(`${API_BASE}/release/series`, {
|
|
params: { release_id: releaseId, limit: 10 }
|
|
});
|
|
|
|
const select = document.getElementById('series-select');
|
|
select.innerHTML = `<option value="">-- ${releaseName} --</option>`;
|
|
|
|
if (response.data && response.data.seriess) {
|
|
response.data.seriess.forEach(series => {
|
|
const option = document.createElement('option');
|
|
option.value = series.id;
|
|
option.textContent = `${series.id} - ${series.title}`;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
if (response.data.seriess.length > 0) {
|
|
select.value = response.data.seriess[0].id;
|
|
loadSeriesData();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Load release series error:', error);
|
|
}
|
|
}
|
|
|
|
function showSeriesModal() {
|
|
const modal = new bootstrap.Modal(document.getElementById('addSeriesModal'));
|
|
modal.show();
|
|
}
|
|
|
|
function addSeriesFromModal() {
|
|
const seriesId = document.getElementById('modal-series-id').value.trim();
|
|
if (!seriesId) {
|
|
alert('Please enter a series ID');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('series-select').value = seriesId;
|
|
bootstrap.Modal.getInstance(document.getElementById('addSeriesModal')).hide();
|
|
loadSeriesData();
|
|
}
|
|
|
|
function exportData() {
|
|
if (!currentSeriesData.length) {
|
|
alert('No data to export');
|
|
return;
|
|
}
|
|
|
|
let csv = 'Date,Value\n';
|
|
currentSeriesData.forEach(obs => {
|
|
csv += `${obs.date},${obs.value}\n`;
|
|
});
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `fred_data_${new Date().toISOString().split('T')[0]}.csv`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function getDateDaysAgo(days) {
|
|
const date = new Date();
|
|
date.setDate(date.getDate() - days);
|
|
return date.toISOString().split('T')[0];
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
document.getElementById('search-input').addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
searchSeries();
|
|
}
|
|
}); |