This commit is contained in:
@@ -113,6 +113,69 @@
|
|||||||
border-color: #38bdf8;
|
border-color: #38bdf8;
|
||||||
background: rgba(56, 189, 248, 0.05);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -188,9 +251,9 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- Step 1: Time Range -->
|
<!-- Step 1: Time Range -->
|
||||||
<div class="report-step" id="step1">
|
<div class="report-step step-active" id="step1">
|
||||||
<label class="field-label">1. Choose Analysis Period</label>
|
<label class="field-label">1. Choose Analysis Period</label>
|
||||||
<select id="timeRangePreset" class="input-glass" onchange="proceedToStep(2)">
|
<select id="timeRangePreset" class="input-glass" onchange="updateUrlParams(); proceedToStep(2)">
|
||||||
<option value="" disabled selected>Select... </option>
|
<option value="" disabled selected>Select... </option>
|
||||||
<option value="1">Today</option>
|
<option value="1">Today</option>
|
||||||
<option value="7">Last 7 Days</option>
|
<option value="7">Last 7 Days</option>
|
||||||
@@ -200,15 +263,19 @@
|
|||||||
<option value="custom">Custom Range...</option>
|
<option value="custom">Custom Range...</option>
|
||||||
</select>
|
</select>
|
||||||
<div id="customDates" class="hidden grid grid-cols-2 gap-2 mt-2">
|
<div id="customDates" class="hidden grid grid-cols-2 gap-2 mt-2">
|
||||||
<input type="date" id="dateFrom" class="input-glass text-xs p-2" onchange="checkCustomDates()">
|
<input type="date" id="dateFrom" class="input-glass text-xs p-2"
|
||||||
<input type="date" id="dateTo" class="input-glass text-xs p-2" onchange="checkCustomDates()">
|
onchange="updateUrlParams(); checkCustomDates()">
|
||||||
|
<input type="date" id="dateTo" class="input-glass text-xs p-2"
|
||||||
|
onchange="updateUrlParams(); checkCustomDates()">
|
||||||
</div>
|
</div>
|
||||||
|
<div id="summary-step1" class="step-summary"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 2: X-Axis -->
|
<!-- Step 2: X-Axis -->
|
||||||
<div class="report-step hidden opacity-50" id="step2">
|
<div class="report-step hidden opacity-50" id="step2">
|
||||||
<label class="field-label">2. Primary Grouping (X-Axis)</label>
|
<label class="field-label">2. Primary Grouping (X-Axis)</label>
|
||||||
<select id="axisX" class="input-glass" onchange="proceedToStep(3); updateSubGroupOptions();">
|
<select id="axisX" class="input-glass"
|
||||||
|
onchange="updateUrlParams(); proceedToStep(3); updateSubGroupOptions();">
|
||||||
<option value="" disabled selected>Select... </option>
|
<option value="" disabled selected>Select... </option>
|
||||||
<option value="day">Time (Daily)</option>
|
<option value="day">Time (Daily)</option>
|
||||||
<option value="month">Time (Monthly)</option>
|
<option value="month">Time (Monthly)</option>
|
||||||
@@ -217,28 +284,31 @@
|
|||||||
<option value="sector">Industry Sector</option>
|
<option value="sector">Industry Sector</option>
|
||||||
<option value="isin">Specific ISIN</option>
|
<option value="isin">Specific ISIN</option>
|
||||||
</select>
|
</select>
|
||||||
|
<div id="summary-step2" class="step-summary"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 3: Breakdown -->
|
<!-- Step 3: Breakdown -->
|
||||||
<div class="report-step hidden opacity-50" id="step3">
|
<div class="report-step hidden opacity-50" id="step3">
|
||||||
<label class="field-label">3. Secondary Breakdown (Series)</label>
|
<label class="field-label">3. Secondary Breakdown (Series)</label>
|
||||||
<select id="axisSub" class="input-glass" onchange="proceedToStep(4)">
|
<select id="axisSub" class="input-glass" onchange="updateUrlParams(); proceedToStep(4)">
|
||||||
<option value="">None (Unified)</option>
|
<option value="">None (Unified)</option>
|
||||||
<option value="exchange">By Exchange</option>
|
<option value="exchange">By Exchange</option>
|
||||||
<option value="continent">By Continent</option>
|
<option value="continent">By Continent</option>
|
||||||
<option value="sector">By Sector</option>
|
<option value="sector">By Sector</option>
|
||||||
</select>
|
</select>
|
||||||
|
<div id="summary-step3" class="step-summary"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 4: Y-Axis -->
|
<!-- Step 4: Y-Axis -->
|
||||||
<div class="report-step hidden opacity-50" id="step4">
|
<div class="report-step hidden opacity-50" id="step4">
|
||||||
<label class="field-label">4. Select Metric (Y-Axis)</label>
|
<label class="field-label">4. Select Metric (Y-Axis)</label>
|
||||||
<select id="axisY" class="input-glass" onchange="proceedToStep(5)">
|
<select id="axisY" class="input-glass" onchange="updateUrlParams(); proceedToStep(5)">
|
||||||
<option value="" disabled selected>Select... </option>
|
<option value="" disabled selected>Select... </option>
|
||||||
<option value="volume">Trade Volume (€)</option>
|
<option value="volume">Trade Volume (€)</option>
|
||||||
<option value="count">Number of Trades</option>
|
<option value="count">Number of Trades</option>
|
||||||
<option value="avg_price">Average Performance</option>
|
<option value="avg_price">Average Performance</option>
|
||||||
</select>
|
</select>
|
||||||
|
<div id="summary-step4" class="step-summary"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 5: Filters -->
|
<!-- Step 5: Filters -->
|
||||||
@@ -252,8 +322,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-8 space-y-3">
|
<div class="pt-8 space-y-3">
|
||||||
<button onclick="renderAnalyticsReport()"
|
<button id="btnGenerate" onclick="renderAnalyticsReport()"
|
||||||
class="btn-primary w-full shadow-sky-500/20">REGENERATE G-DRIVE REPORT</button>
|
class="btn-primary w-full shadow-sky-500/20">GENERATE REPORT</button>
|
||||||
<button onclick="resetReportConfig()"
|
<button onclick="resetReportConfig()"
|
||||||
class="w-full text-xs text-slate-500 hover:text-white transition">Reset
|
class="w-full text-xs text-slate-500 hover:text-white transition">Reset
|
||||||
Configuration</button>
|
Configuration</button>
|
||||||
@@ -272,7 +342,20 @@
|
|||||||
onclick="setChartType('bar')">Bar</button>
|
onclick="setChartType('bar')">Bar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-h-0 z-10"><canvas id="analyticsChart"></canvas></div>
|
<div class="flex-1 min-h-0 z-10 relative">
|
||||||
|
<div id="chartLoader"
|
||||||
|
class="hidden absolute inset-0 flex items-center justify-center bg-[#0b1120]/50 z-20">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
<div id="chartError"
|
||||||
|
class="hidden absolute inset-0 flex items-center justify-center bg-red-900/20 z-20 p-10 text-center">
|
||||||
|
<div class="glass p-6 border-red-500/30">
|
||||||
|
<p class="text-red-400 font-bold mb-2">Analysis Failed</p>
|
||||||
|
<p id="errorMsg" class="text-xs text-slate-400"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas id="analyticsChart"></canvas>
|
||||||
|
</div>
|
||||||
<!-- Subtle watermark -->
|
<!-- Subtle watermark -->
|
||||||
<div class="absolute bottom-8 right-8 text-white/5 text-6xl font-black pointer-events-none">
|
<div class="absolute bottom-8 right-8 text-white/5 text-6xl font-black pointer-events-none">
|
||||||
ANALYTICS</div>
|
ANALYTICS</div>
|
||||||
@@ -304,6 +387,7 @@
|
|||||||
let searchTimeout;
|
let searchTimeout;
|
||||||
|
|
||||||
function showView(viewId) {
|
function showView(viewId) {
|
||||||
|
window.activeView = viewId;
|
||||||
document.querySelectorAll('.view').forEach(v => v.classList.add('hidden'));
|
document.querySelectorAll('.view').forEach(v => v.classList.add('hidden'));
|
||||||
document.getElementById(`view-${viewId}`).classList.remove('hidden');
|
document.getElementById(`view-${viewId}`).classList.remove('hidden');
|
||||||
document.querySelectorAll('#sidebar a').forEach(a => a.classList.remove('active-nav'));
|
document.querySelectorAll('#sidebar a').forEach(a => a.classList.remove('active-nav'));
|
||||||
@@ -313,8 +397,7 @@
|
|||||||
document.getElementById('pageTitle').innerText = titles[viewId];
|
document.getElementById('pageTitle').innerText = titles[viewId];
|
||||||
|
|
||||||
if (viewId === 'analytics') {
|
if (viewId === 'analytics') {
|
||||||
// If it's a fresh visit without params, we might want to reset or keep state
|
updateUrlParams();
|
||||||
// renderAnalyticsReport();
|
|
||||||
} else {
|
} else {
|
||||||
fetchData();
|
fetchData();
|
||||||
}
|
}
|
||||||
@@ -328,8 +411,6 @@
|
|||||||
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');
|
||||||
|
|
||||||
// Contextual Logic: If X is already a metadata field, don't allow it as series
|
|
||||||
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 = '';
|
if (opt.value === x && sub.value === x) sub.value = '';
|
||||||
@@ -363,6 +444,7 @@
|
|||||||
updateFilterChips();
|
updateFilterChips();
|
||||||
document.getElementById('isinSearch').value = '';
|
document.getElementById('isinSearch').value = '';
|
||||||
document.getElementById('suggestions').classList.add('hidden');
|
document.getElementById('suggestions').classList.add('hidden');
|
||||||
|
updateUrlParams();
|
||||||
if (window.activeView === 'analytics') proceedToStep(5);
|
if (window.activeView === 'analytics') proceedToStep(5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,6 +452,7 @@
|
|||||||
function removeFilter(isin) {
|
function removeFilter(isin) {
|
||||||
store.pinnedIsins = store.pinnedIsins.filter(p => p.isin !== isin);
|
store.pinnedIsins = store.pinnedIsins.filter(p => p.isin !== isin);
|
||||||
updateFilterChips();
|
updateFilterChips();
|
||||||
|
updateUrlParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFilterChips() {
|
function updateFilterChips() {
|
||||||
@@ -422,15 +505,22 @@
|
|||||||
function setChartType(type) { currentChartType = type; renderAnalyticsReport(); }
|
function setChartType(type) { currentChartType = type; renderAnalyticsReport(); }
|
||||||
|
|
||||||
async function renderAnalyticsReport() {
|
async function renderAnalyticsReport() {
|
||||||
|
const btn = document.getElementById('btnGenerate');
|
||||||
|
const loader = document.getElementById('chartLoader');
|
||||||
|
const errBox = document.getElementById('chartError');
|
||||||
const dates = getDates();
|
const dates = getDates();
|
||||||
const x = document.getElementById('axisX').value;
|
const x = document.getElementById('axisX').value;
|
||||||
const sub = document.getElementById('axisSub').value;
|
const sub = document.getElementById('axisSub').value;
|
||||||
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;
|
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}`;
|
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}`;
|
||||||
@@ -438,7 +528,10 @@
|
|||||||
if (isins) url += `&isins=${isins}`;
|
if (isins) url += `&isins=${isins}`;
|
||||||
|
|
||||||
try {
|
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 data = res.dataset || [];
|
||||||
const ctx = document.getElementById('analyticsChart').getContext('2d');
|
const ctx = document.getElementById('analyticsChart').getContext('2d');
|
||||||
|
|
||||||
@@ -456,7 +549,7 @@
|
|||||||
const row = data.find(r => r[0] === l && r[1] === name);
|
const row = data.find(r => r[0] === l && r[1] === name);
|
||||||
return row ? row[2] : 0;
|
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)`,
|
borderColor: `hsla(${hue}, 75%, 50%, 1)`,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
fill: currentChartType === 'line'
|
fill: currentChartType === 'line'
|
||||||
@@ -464,12 +557,12 @@
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
datasets.push({
|
datasets.push({
|
||||||
label: y,
|
label: y.toUpperCase(),
|
||||||
data: labels.map(l => {
|
data: labels.map(l => {
|
||||||
const row = data.find(r => r[0] === l);
|
const row = data.find(r => r[0] === l);
|
||||||
return row ? row[1] : 0;
|
return row ? row[1] : 0;
|
||||||
}),
|
}),
|
||||||
backgroundColor: '#38bdf888',
|
backgroundColor: '#38bdf866',
|
||||||
borderColor: '#38bdf8',
|
borderColor: '#38bdf8',
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
fill: currentChartType === 'line'
|
fill: currentChartType === 'line'
|
||||||
@@ -482,12 +575,24 @@
|
|||||||
data: { labels: labels.map(l => (x === 'day' || x === 'month') ? new Date(l).toLocaleDateString() : l), datasets },
|
data: { labels: labels.map(l => (x === 'day' || x === 'month') ? new Date(l).toLocaleDateString() : l), datasets },
|
||||||
options: {
|
options: {
|
||||||
responsive: true, maintainAspectRatio: false,
|
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 } } },
|
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 + ')' : ''}`;
|
document.getElementById('reportTitle').innerText = `Market Analytics: ${y.toUpperCase()} vs ${x.toUpperCase()}`;
|
||||||
} catch (err) { console.error(err); }
|
} 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() {
|
function renderDashboardCharts() {
|
||||||
@@ -503,17 +608,7 @@
|
|||||||
const contCtx = document.getElementById('continentChart').getContext('2d');
|
const contCtx = document.getElementById('continentChart').getContext('2d');
|
||||||
if (charts.continent) charts.continent.destroy();
|
if (charts.continent) charts.continent.destroy();
|
||||||
const labels = store.summary.map(r => r[0]);
|
const labels = store.summary.map(r => r[0]);
|
||||||
const baseColors = [
|
const baseColors = ['#38bdf8', '#f43f5e', '#10b981', '#fbbf24', '#8b5cf6', '#f97316', '#ec4899', '#6366f1'];
|
||||||
'#38bdf8', // Blue
|
|
||||||
'#f43f5e', // Red
|
|
||||||
'#10b981', // Green
|
|
||||||
'#fbbf24', // Yellow
|
|
||||||
'#8b5cf6', // Purple
|
|
||||||
'#f97316', // Orange
|
|
||||||
'#ec4899', // Pink
|
|
||||||
'#6366f1' // Indigo
|
|
||||||
];
|
|
||||||
|
|
||||||
charts.continent = new Chart(contCtx, {
|
charts.continent = new Chart(contCtx, {
|
||||||
type: 'doughnut',
|
type: 'doughnut',
|
||||||
data: {
|
data: {
|
||||||
@@ -528,20 +623,29 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Progressive Report Configuration ---
|
|
||||||
function proceedToStep(n) {
|
function proceedToStep(n) {
|
||||||
// First, enable/show the step
|
document.querySelectorAll('.report-step').forEach((s, idx) => {
|
||||||
const step = document.getElementById(`step${n}`);
|
const stepNum = idx + 1;
|
||||||
if (step) {
|
s.classList.toggle('step-active', stepNum === n);
|
||||||
step.classList.remove('hidden');
|
s.classList.toggle('step-completed', stepNum < n);
|
||||||
setTimeout(() => step.classList.remove('opacity-50'), 50);
|
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
|
const summary = document.getElementById(`summary-step${stepNum}`);
|
||||||
if (n === 2) handlePresetChange();
|
if (summary) {
|
||||||
|
if (stepNum < n) {
|
||||||
// Auto-scroll logic if sidebar is long
|
const input = s.querySelector('select, input');
|
||||||
if (n > 2) step.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
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() {
|
function checkCustomDates() {
|
||||||
@@ -550,16 +654,60 @@
|
|||||||
if (from && to) proceedToStep(2);
|
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() {
|
function resetReportConfig() {
|
||||||
document.querySelectorAll('.report-step').forEach((s, i) => {
|
document.querySelectorAll('.report-step').forEach((s, i) => { if (i > 0) s.classList.add('hidden', 'opacity-50'); else s.classList.add('step-active'); });
|
||||||
if (i > 0) s.classList.add('hidden', 'opacity-50');
|
|
||||||
});
|
|
||||||
document.getElementById('timeRangePreset').value = '';
|
document.getElementById('timeRangePreset').value = '';
|
||||||
document.getElementById('axisX').value = '';
|
document.getElementById('axisX').value = '';
|
||||||
document.getElementById('axisSub').value = '';
|
document.getElementById('axisSub').value = '';
|
||||||
document.getElementById('axisY').value = '';
|
document.getElementById('axisY').value = '';
|
||||||
store.pinnedIsins = [];
|
store.pinnedIsins = [];
|
||||||
updateFilterChips();
|
updateFilterChips();
|
||||||
|
updateUrlParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillMetadataTable() {
|
function fillMetadataTable() {
|
||||||
@@ -575,50 +723,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deepLinkToAnalytics(isin, name) {
|
function deepLinkToAnalytics(isin, name) {
|
||||||
// Set values as requested: Time: 30d, X: Day, Y: Volume, Filter: ISIN
|
|
||||||
resetReportConfig();
|
resetReportConfig();
|
||||||
document.getElementById('timeRangePreset').value = '30';
|
document.getElementById('timeRangePreset').value = '30';
|
||||||
document.getElementById('axisX').value = 'day';
|
document.getElementById('axisX').value = 'day';
|
||||||
document.getElementById('axisSub').value = '';
|
|
||||||
document.getElementById('axisY').value = 'volume';
|
document.getElementById('axisY').value = 'volume';
|
||||||
|
|
||||||
store.pinnedIsins = [{ isin, name }];
|
store.pinnedIsins = [{ isin, name }];
|
||||||
updateFilterChips();
|
updateFilterChips();
|
||||||
|
|
||||||
// Show all steps progressively for the user
|
|
||||||
for (let i = 1; i <= 5; i++) proceedToStep(i);
|
|
||||||
|
|
||||||
showView('analytics');
|
showView('analytics');
|
||||||
|
for (let i = 1; i <= 5; i++) proceedToStep(i);
|
||||||
renderAnalyticsReport();
|
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 = async () => {
|
window.onload = async () => { await fetchData(); syncStateFromUrl(); setInterval(fetchData, 30000); };
|
||||||
await fetchData();
|
|
||||||
handleUrlParams();
|
|
||||||
setInterval(fetchData, 30000);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user