feat: advanced analytics with deep linking, URL params, and progressive UI
All checks were successful
Deployment / deploy-docker (push) Successful in 16s

This commit is contained in:
Melchior Reimers
2026-01-23 19:33:39 +01:00
parent 5b712d8111
commit ecfdea5f6c

View File

@@ -312,7 +312,12 @@
const titles = { 'dashboard': 'Market Overview', 'analytics': 'Custom Report Builder', 'metadata': 'Entity Metadata' }; const titles = { 'dashboard': 'Market Overview', 'analytics': 'Custom Report Builder', 'metadata': 'Entity Metadata' };
document.getElementById('pageTitle').innerText = titles[viewId]; document.getElementById('pageTitle').innerText = titles[viewId];
if (viewId === 'analytics') renderAnalyticsReport(); else fetchData(); if (viewId === 'analytics') {
// If it's a fresh visit without params, we might want to reset or keep state
// renderAnalyticsReport();
} else {
fetchData();
}
} }
function handlePresetChange() { function handlePresetChange() {
@@ -323,12 +328,11 @@
function updateSubGroupOptions() { function updateSubGroupOptions() {
const x = document.getElementById('axisX').value; const x = document.getElementById('axisX').value;
const sub = document.getElementById('axisSub'); const sub = document.getElementById('axisSub');
const container = document.getElementById('subGroupContainer');
// Contextual Logic: If X is already a metadata field, don't allow it as series (too complex) // Contextual Logic: If X is already a metadata field, don't allow it as series
container.classList.remove('hidden');
Array.from(sub.options).forEach(opt => { Array.from(sub.options).forEach(opt => {
opt.disabled = (opt.value === x); opt.disabled = (opt.value === x);
if (opt.value === x && sub.value === x) sub.value = '';
}); });
} }
@@ -357,9 +361,9 @@
if (!store.pinnedIsins.find(p => p.isin === isin)) { if (!store.pinnedIsins.find(p => p.isin === isin)) {
store.pinnedIsins.push({ isin, name }); store.pinnedIsins.push({ isin, name });
updateFilterChips(); updateFilterChips();
// Close search
document.getElementById('isinSearch').value = ''; document.getElementById('isinSearch').value = '';
document.getElementById('suggestions').classList.add('hidden'); document.getElementById('suggestions').classList.add('hidden');
if (window.activeView === 'analytics') proceedToStep(5);
} }
} }
@@ -376,8 +380,8 @@
${p.isin} <span class="ml-2 cursor-pointer text-slate-500 hover:text-white" onclick="removeFilter('${p.isin}')">×</span> ${p.isin} <span class="ml-2 cursor-pointer text-slate-500 hover:text-white" onclick="removeFilter('${p.isin}')">×</span>
</div> </div>
`).join(''); `).join('');
container.innerHTML = html; if (container) container.innerHTML = html;
pins.innerHTML = html; if (pins) pins.innerHTML = html;
} }
function getDates() { function getDates() {
@@ -424,6 +428,9 @@
const y = document.getElementById('axisY').value; const y = document.getElementById('axisY').value;
const isins = store.pinnedIsins.map(p => p.isin).join(','); const isins = store.pinnedIsins.map(p => p.isin).join(',');
// Validate that basic steps are done
if (!dates.from || !x || !y) return;
let url = `${API}/analytics?metric=${y}&group_by=${x}`; let url = `${API}/analytics?metric=${y}&group_by=${x}`;
if (sub) url += `&sub_group_by=${sub}`; if (sub) url += `&sub_group_by=${sub}`;
if (dates.from) url += `&date_from=${dates.from}`; if (dates.from) url += `&date_from=${dates.from}`;
@@ -504,7 +511,7 @@
'#8b5cf6', // Purple '#8b5cf6', // Purple
'#f97316', // Orange '#f97316', // Orange
'#ec4899', // Pink '#ec4899', // Pink
'#475569' // Slate '#6366f1' // Indigo
]; ];
charts.continent = new Chart(contCtx, { charts.continent = new Chart(contCtx, {
@@ -523,12 +530,18 @@
// --- Progressive Report Configuration --- // --- Progressive Report Configuration ---
function proceedToStep(n) { function proceedToStep(n) {
// First, enable/show the step
const step = document.getElementById(`step${n}`); const step = document.getElementById(`step${n}`);
if (step) { if (step) {
step.classList.remove('hidden'); step.classList.remove('hidden');
setTimeout(() => step.classList.remove('opacity-50'), 50); setTimeout(() => step.classList.remove('opacity-50'), 50);
} }
// Contextual Visibility Logic: If we are at step 2, handle custom date toggles
if (n === 2) handlePresetChange(); if (n === 2) handlePresetChange();
// Auto-scroll logic if sidebar is long
if (n > 2) step.scrollIntoView({ behavior: 'smooth', block: 'center' });
} }
function checkCustomDates() { function checkCustomDates() {
@@ -552,7 +565,7 @@
function fillMetadataTable() { function fillMetadataTable() {
const tbody = document.getElementById('metadataRows'); const tbody = document.getElementById('metadataRows');
tbody.innerHTML = store.metadata.map(r => ` tbody.innerHTML = store.metadata.map(r => `
<tr class="hover:bg-white/5 transition border-b border-white/5 cursor-pointer" onclick="addFilter('${r[0]}', '${r[1]}'); showView('analytics');"> <tr class="hover:bg-white/5 transition border-b border-white/5 cursor-pointer" onclick="deepLinkToAnalytics('${r[0]}', '${r[1]}')">
<td class="p-5 px-8 font-mono text-sky-400 font-bold">${r[0]}</td> <td class="p-5 px-8 font-mono text-sky-400 font-bold">${r[0]}</td>
<td class="p-5 font-semibold text-slate-200">${r[1]}</td> <td class="p-5 font-semibold text-slate-200">${r[1]}</td>
<td class="p-5 text-slate-500">${r[2]}</td> <td class="p-5 text-slate-500">${r[2]}</td>
@@ -561,12 +574,51 @@
`).join(''); `).join('');
} }
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');
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) { function filterMetadata(q) {
const rows = document.querySelectorAll('#metadataRows tr'); const rows = document.querySelectorAll('#metadataRows tr');
rows.forEach(r => r.style.display = r.innerText.toLowerCase().includes(q.toLowerCase()) ? '' : 'none'); rows.forEach(r => r.style.display = r.innerText.toLowerCase().includes(q.toLowerCase()) ? '' : 'none');
} }
window.onload = () => { fetchData(); setInterval(fetchData, 30000); }; window.onload = async () => {
await fetchData();
handleUrlParams();
setInterval(fetchData, 30000);
};
</script> </script>
</body> </body>