diff --git a/dashboard/public/index.html b/dashboard/public/index.html
index 8af042a..4aed718 100644
--- a/dashboard/public/index.html
+++ b/dashboard/public/index.html
@@ -113,6 +113,69 @@
border-color: #38bdf8;
background: rgba(56, 189, 248, 0.05);
}
+
+ .report-step {
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+ position: relative;
+ padding: 1rem;
+ border-radius: 12px;
+ overflow: hidden;
+ max-height: 500px;
+ }
+
+ .step-active {
+ background: rgba(56, 189, 248, 0.05);
+ border: 1px solid rgba(56, 189, 248, 0.2);
+ opacity: 1 !important;
+ }
+
+ .step-completed {
+ max-height: 50px;
+ opacity: 0.6;
+ cursor: pointer;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ }
+
+ .step-completed:hover {
+ opacity: 1;
+ background: rgba(255, 255, 255, 0.02);
+ }
+
+ .step-completed select,
+ .step-completed input {
+ display: none;
+ }
+
+ .step-summary {
+ font-size: 0.8rem;
+ color: #38bdf8;
+ font-weight: 700;
+ margin-top: -2px;
+ background: rgba(56, 189, 248, 0.1);
+ padding: 2px 8px;
+ border-radius: 4px;
+ display: inline-block;
+ }
+
+ .spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid rgba(56, 189, 248, 0.1);
+ border-top: 3px solid #38bdf8;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ }
+
+ @keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
@@ -188,9 +251,9 @@
-
+
-
-
+
+
-
+
+
@@ -252,8 +322,8 @@
-
+
@@ -272,7 +342,20 @@
onclick="setChartType('bar')">Bar
-
+
ANALYTICS
@@ -304,6 +387,7 @@
let searchTimeout;
function showView(viewId) {
+ window.activeView = viewId;
document.querySelectorAll('.view').forEach(v => v.classList.add('hidden'));
document.getElementById(`view-${viewId}`).classList.remove('hidden');
document.querySelectorAll('#sidebar a').forEach(a => a.classList.remove('active-nav'));
@@ -313,8 +397,7 @@
document.getElementById('pageTitle').innerText = titles[viewId];
if (viewId === 'analytics') {
- // If it's a fresh visit without params, we might want to reset or keep state
- // renderAnalyticsReport();
+ updateUrlParams();
} else {
fetchData();
}
@@ -328,8 +411,6 @@
function updateSubGroupOptions() {
const x = document.getElementById('axisX').value;
const sub = document.getElementById('axisSub');
-
- // Contextual Logic: If X is already a metadata field, don't allow it as series
Array.from(sub.options).forEach(opt => {
opt.disabled = (opt.value === x);
if (opt.value === x && sub.value === x) sub.value = '';
@@ -363,6 +444,7 @@
updateFilterChips();
document.getElementById('isinSearch').value = '';
document.getElementById('suggestions').classList.add('hidden');
+ updateUrlParams();
if (window.activeView === 'analytics') proceedToStep(5);
}
}
@@ -370,6 +452,7 @@
function removeFilter(isin) {
store.pinnedIsins = store.pinnedIsins.filter(p => p.isin !== isin);
updateFilterChips();
+ updateUrlParams();
}
function updateFilterChips() {
@@ -422,15 +505,22 @@
function setChartType(type) { currentChartType = type; renderAnalyticsReport(); }
async function renderAnalyticsReport() {
+ const btn = document.getElementById('btnGenerate');
+ const loader = document.getElementById('chartLoader');
+ const errBox = document.getElementById('chartError');
const dates = getDates();
const x = document.getElementById('axisX').value;
const sub = document.getElementById('axisSub').value;
const y = document.getElementById('axisY').value;
const isins = store.pinnedIsins.map(p => p.isin).join(',');
- // Validate that basic steps are done
if (!dates.from || !x || !y) return;
+ btn.innerText = 'GENERATING...';
+ btn.disabled = true;
+ loader.classList.remove('hidden');
+ errBox.classList.add('hidden');
+
let url = `${API}/analytics?metric=${y}&group_by=${x}`;
if (sub) url += `&sub_group_by=${sub}`;
if (dates.from) url += `&date_from=${dates.from}`;
@@ -438,7 +528,10 @@
if (isins) url += `&isins=${isins}`;
try {
- const res = await fetch(url).then(r => r.json());
+ const res = await fetch(url).then(r => {
+ if (!r.ok) throw new Error(`Server Error: ${r.status}`);
+ return r.json();
+ });
const data = res.dataset || [];
const ctx = document.getElementById('analyticsChart').getContext('2d');
@@ -456,7 +549,7 @@
const row = data.find(r => r[0] === l && r[1] === name);
return row ? row[2] : 0;
}),
- backgroundColor: `hsla(${hue}, 75%, 50%, 0.8)`,
+ backgroundColor: `hsla(${hue}, 75%, 50%, 0.7)`,
borderColor: `hsla(${hue}, 75%, 50%, 1)`,
borderWidth: 2,
fill: currentChartType === 'line'
@@ -464,12 +557,12 @@
});
} else {
datasets.push({
- label: y,
+ label: y.toUpperCase(),
data: labels.map(l => {
const row = data.find(r => r[0] === l);
return row ? row[1] : 0;
}),
- backgroundColor: '#38bdf888',
+ backgroundColor: '#38bdf866',
borderColor: '#38bdf8',
borderWidth: 2,
fill: currentChartType === 'line'
@@ -482,12 +575,24 @@
data: { labels: labels.map(l => (x === 'day' || x === 'month') ? new Date(l).toLocaleDateString() : l), datasets },
options: {
responsive: true, maintainAspectRatio: false,
+ animation: { duration: 800, easing: 'easeOutQuart' },
scales: { y: { stacked: true, grid: { color: 'rgba(255,255,255,0.05)' } }, x: { stacked: true, grid: { display: false } } },
- plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } }
+ plugins: {
+ legend: { position: 'bottom', labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true } },
+ tooltip: { backgroundColor: '#1e293b', titleColor: '#38bdf8' }
+ }
}
});
- document.getElementById('reportTitle').innerText = `Analysis: ${y} by ${x} ${sub ? ' (Splitted by ' + sub + ')' : ''}`;
- } catch (err) { console.error(err); }
+ document.getElementById('reportTitle').innerText = `Market Analytics: ${y.toUpperCase()} vs ${x.toUpperCase()}`;
+ } catch (err) {
+ console.error(err);
+ document.getElementById('errorMsg').innerText = err.message;
+ errBox.classList.remove('hidden');
+ } finally {
+ btn.innerText = 'GENERATE REPORT';
+ btn.disabled = false;
+ loader.classList.add('hidden');
+ }
}
function renderDashboardCharts() {
@@ -503,17 +608,7 @@
const contCtx = document.getElementById('continentChart').getContext('2d');
if (charts.continent) charts.continent.destroy();
const labels = store.summary.map(r => r[0]);
- const baseColors = [
- '#38bdf8', // Blue
- '#f43f5e', // Red
- '#10b981', // Green
- '#fbbf24', // Yellow
- '#8b5cf6', // Purple
- '#f97316', // Orange
- '#ec4899', // Pink
- '#6366f1' // Indigo
- ];
-
+ const baseColors = ['#38bdf8', '#f43f5e', '#10b981', '#fbbf24', '#8b5cf6', '#f97316', '#ec4899', '#6366f1'];
charts.continent = new Chart(contCtx, {
type: 'doughnut',
data: {
@@ -528,20 +623,29 @@
});
}
- // --- Progressive Report Configuration ---
function proceedToStep(n) {
- // First, enable/show the step
- const step = document.getElementById(`step${n}`);
- if (step) {
- step.classList.remove('hidden');
- setTimeout(() => step.classList.remove('opacity-50'), 50);
- }
+ document.querySelectorAll('.report-step').forEach((s, idx) => {
+ const stepNum = idx + 1;
+ s.classList.toggle('step-active', stepNum === n);
+ s.classList.toggle('step-completed', stepNum < n);
+ if (stepNum > n) s.classList.add('hidden', 'opacity-50');
+ else s.classList.remove('hidden');
- // Contextual Visibility Logic: If we are at step 2, handle custom date toggles
- if (n === 2) handlePresetChange();
-
- // Auto-scroll logic if sidebar is long
- if (n > 2) step.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ const summary = document.getElementById(`summary-step${stepNum}`);
+ if (summary) {
+ if (stepNum < n) {
+ const input = s.querySelector('select, input');
+ let val = input ? (input.options && input.selectedIndex >= 0 ? input.options[input.selectedIndex].text : input.value) : '';
+ summary.innerText = val;
+ summary.classList.remove('hidden');
+ s.onclick = () => proceedToStep(stepNum);
+ } else {
+ summary.classList.add('hidden');
+ s.onclick = null;
+ }
+ }
+ });
+ updateUrlParams();
}
function checkCustomDates() {
@@ -550,16 +654,60 @@
if (from && to) proceedToStep(2);
}
+ function updateUrlParams() {
+ const params = new URLSearchParams(window.location.search);
+ const time = document.getElementById('timeRangePreset').value;
+ const x = document.getElementById('axisX').value;
+ const sub = document.getElementById('axisSub') ? document.getElementById('axisSub').value : '';
+ const y = document.getElementById('axisY').value;
+ const isins = store.pinnedIsins.map(p => p.isin).join(',');
+
+ if (time) params.set('time', time); else params.delete('time');
+ if (x) params.set('x', x); else params.delete('x');
+ if (sub) params.set('sub', sub); else params.delete('sub');
+ if (y) params.set('y', y); else params.delete('y');
+ if (isins) params.set('isins', isins); else params.delete('isins');
+ if (window.activeView) params.set('view', window.activeView);
+
+ // Important: Use hash only if it exists
+ const cleanUrl = window.location.pathname + '?' + params.toString();
+ window.history.replaceState({ selection: params.toString() }, '', cleanUrl);
+ }
+
+ function syncStateFromUrl() {
+ const params = new URLSearchParams(window.location.search);
+ const time = params.get('time');
+ const x = params.get('x');
+ const sub = params.get('sub');
+ const y = params.get('y');
+ const isins = params.get('isins');
+ const view = params.get('view');
+
+ if (view) showView(view);
+ if (time) { document.getElementById('timeRangePreset').value = time; proceedToStep(2); }
+ if (x) { document.getElementById('axisX').value = x; updateSubGroupOptions(); proceedToStep(3); }
+ if (sub) { document.getElementById('axisSub').value = sub; proceedToStep(4); }
+ if (y) { document.getElementById('axisY').value = y; proceedToStep(5); }
+ if (isins) {
+ setTimeout(() => {
+ isins.split(',').forEach(id => {
+ const ent = store.metadata.find(m => m[0] === id);
+ addFilter(id, ent ? ent[1] : id);
+ });
+ if (x && y && time) renderAnalyticsReport();
+ }, 800);
+ }
+ }
+
function resetReportConfig() {
- document.querySelectorAll('.report-step').forEach((s, i) => {
- if (i > 0) s.classList.add('hidden', 'opacity-50');
- });
+ document.querySelectorAll('.report-step').forEach((s, i) => { if (i > 0) s.classList.add('hidden', 'opacity-50'); else s.classList.add('step-active'); });
document.getElementById('timeRangePreset').value = '';
document.getElementById('axisX').value = '';
document.getElementById('axisSub').value = '';
document.getElementById('axisY').value = '';
store.pinnedIsins = [];
updateFilterChips();
+ updateUrlParams();
}
function fillMetadataTable() {
@@ -575,50 +723,23 @@
}
function deepLinkToAnalytics(isin, name) {
- // Set values as requested: Time: 30d, X: Day, Y: Volume, Filter: ISIN
resetReportConfig();
document.getElementById('timeRangePreset').value = '30';
document.getElementById('axisX').value = 'day';
- document.getElementById('axisSub').value = '';
document.getElementById('axisY').value = 'volume';
-
store.pinnedIsins = [{ isin, name }];
updateFilterChips();
-
- // Show all steps progressively for the user
- for (let i = 1; i <= 5; i++) proceedToStep(i);
-
showView('analytics');
+ for (let i = 1; i <= 5; i++) proceedToStep(i);
renderAnalyticsReport();
}
- function handleUrlParams() {
- const params = new URLSearchParams(window.location.search);
- const view = params.get('view');
- const isin = params.get('isin');
- if (view) showView(view);
- if (view === 'analytics' && isin) {
- // If isin is provided in URL, we wait for metadata to be fetched then deep link
- const checkStore = setInterval(() => {
- if (store.metadata.length > 0) {
- const entity = store.metadata.find(r => r[0] === isin);
- if (entity) deepLinkToAnalytics(isin, entity[1]);
- clearInterval(checkStore);
- }
- }, 500);
- }
- }
-
function filterMetadata(q) {
const rows = document.querySelectorAll('#metadataRows tr');
rows.forEach(r => r.style.display = r.innerText.toLowerCase().includes(q.toLowerCase()) ? '' : 'none');
}
- window.onload = async () => {
- await fetchData();
- handleUrlParams();
- setInterval(fetchData, 30000);
- };
+ window.onload = async () => { await fetchData(); syncStateFromUrl(); setInterval(fetchData, 30000); };