From fb045c2dd4020439216f2222d39a39c363508a82 Mon Sep 17 00:00:00 2001 From: Melchior Reimers Date: Sat, 24 Jan 2026 07:44:35 +0100 Subject: [PATCH] updated dashboard --- dashboard/public/index.html | 275 ++++++++++++++++++++++++++---------- 1 file changed, 198 insertions(+), 77 deletions(-) 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 @@ -
+
- @@ -200,15 +263,19 @@ +
@@ -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); };