diff --git a/dashboard/public/index.html b/dashboard/public/index.html index 71d713e..4e10c8a 100644 --- a/dashboard/public/index.html +++ b/dashboard/public/index.html @@ -185,8 +185,22 @@ +
+ + +
+
+ +
- + @@ -750,64 +764,106 @@ const yAxis = document.getElementById('customYAxis').value; const groupBy = document.getElementById('customGroupBy').value; const exchanges = document.getElementById('customExchanges').value; - + const chartType = document.getElementById('customChartType')?.value || 'line'; + const hideUnknown = document.getElementById('hideUnknown')?.checked ?? true; + if (!dateFrom || !dateTo) { console.log('Bitte wählen Sie Start- und Enddatum'); return; } - + try { let url = `${API}/custom-analytics?date_from=${dateFrom}&date_to=${dateTo}&x_axis=date&y_axis=${yAxis}&group_by=${groupBy}`; if (exchanges) { url += `&exchanges=${encodeURIComponent(exchanges)}`; } - + const res = await fetch(url).then(r => r.json()); const data = res.dataset || []; const columns = res.columns || []; - + if (!data.length) { console.log('Keine Daten für den gewählten Zeitraum'); return; } - + const ctx = document.getElementById('customAnalyticsChart').getContext('2d'); if (charts.customAnalytics) charts.customAnalytics.destroy(); - + const xIdx = columns.findIndex(c => c.name === 'x_value'); const groupIdx = columns.findIndex(c => c.name === 'group_value'); const yIdx = columns.findIndex(c => c.name === 'y_value'); - - const groups = [...new Set(data.map(r => r[groupIdx]).filter(g => g && g !== '' && g !== 'NONE'))]; + + // Filtere Gruppen - entferne "Unbekannt" wenn Checkbox aktiviert + let groups = [...new Set(data.map(r => r[groupIdx]).filter(g => g && g !== '' && g !== 'NONE'))]; + if (hideUnknown) { + groups = groups.filter(g => g !== 'Unbekannt' && g !== 'Unknown' && g !== ''); + } const dates = [...new Set(data.map(r => r[xIdx]))].sort(); - + if (groups.length === 0) { console.log('No valid groups found in data'); return; } - - // Erweiterte Farben für mehr Exchanges (EIX, LS, XETRA, FRA, GETTEX, STU, QUOTRIX) - const colors = ['#38bdf8', '#f43f5e', '#10b981', '#fbbf24', '#8b5cf6', '#f97316', '#ec4899', '#14b8a6', '#84cc16', '#a855f7']; - const datasets = groups.map((group, idx) => ({ - label: group || 'Unknown', - data: dates.map(d => { + + // Berechne Rohdaten für jede Gruppe + const rawData = {}; + groups.forEach(group => { + rawData[group] = dates.map(d => { const row = data.find(r => r[xIdx] === d && r[groupIdx] === group); return row ? (row[yIdx] || 0) : 0; - }), - borderColor: colors[idx % colors.length], - backgroundColor: colors[idx % colors.length] + '33', - borderWidth: 2, + }); + }); + + // Für prozentuale Darstellung: Berechne Summen pro Tag + let processedData = rawData; + if (chartType === 'percent') { + const dailyTotals = dates.map((_, i) => + groups.reduce((sum, g) => sum + rawData[g][i], 0) + ); + processedData = {}; + groups.forEach(group => { + processedData[group] = rawData[group].map((val, i) => + dailyTotals[i] > 0 ? (val / dailyTotals[i]) * 100 : 0 + ); + }); + } + + // Sektoren-Farben (konsistent für bekannte Sektoren) + const sectorColors = { + 'Technology': '#38bdf8', + 'Healthcare': '#10b981', + 'Financial Services': '#fbbf24', + 'Consumer Cyclical': '#f43f5e', + 'Industrials': '#8b5cf6', + 'Communication Services': '#f97316', + 'Consumer Defensive': '#ec4899', + 'Energy': '#14b8a6', + 'Basic Materials': '#84cc16', + 'Real Estate': '#a855f7', + 'Utilities': '#06b6d4' + }; + const defaultColors = ['#38bdf8', '#f43f5e', '#10b981', '#fbbf24', '#8b5cf6', '#f97316', '#ec4899', '#14b8a6', '#84cc16', '#a855f7', '#06b6d4', '#eab308', '#ef4444', '#22c55e']; + + const isStacked = chartType === 'stacked' || chartType === 'percent'; + + const datasets = groups.map((group, idx) => ({ + label: group || 'Unknown', + data: processedData[group], + borderColor: sectorColors[group] || defaultColors[idx % defaultColors.length], + backgroundColor: (sectorColors[group] || defaultColors[idx % defaultColors.length]) + (isStacked ? 'aa' : '33'), + borderWidth: isStacked ? 1 : 2, tension: 0.3, - fill: false + fill: isStacked ? 'origin' : false, + pointRadius: isStacked ? 0 : 3 })); - + // Nutze Config für Y-Achsen-Label (falls verfügbar) let yAxisLabel = 'Wert'; if (typeof ANALYTICS_CONFIG !== 'undefined' && ANALYTICS_CONFIG.yAxis && ANALYTICS_CONFIG.yAxis[yAxis]) { const config = ANALYTICS_CONFIG.yAxis[yAxis]; yAxisLabel = `${config.label}${config.unit ? ' (' + config.unit + ')' : ''}`; } else { - // Fallback const labels = { 'volume': 'Volumen (€)', 'trade_count': 'Anzahl Trades', @@ -815,11 +871,21 @@ }; yAxisLabel = labels[yAxis] || 'Wert'; } - + + if (chartType === 'percent') { + yAxisLabel = 'Anteil (%)'; + } + + const chartTypeLabels = { + 'line': '', + 'stacked': ' - Stacked Area', + 'percent': ' - Prozentuale Verteilung' + }; + charts.customAnalytics = new Chart(ctx, { type: 'line', data: { - labels: dates.map(d => new Date(d).toLocaleDateString()), + labels: dates.map(d => new Date(d).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })), datasets: datasets }, options: { @@ -830,35 +896,49 @@ y: { type: 'linear', display: true, + stacked: isStacked, title: { display: true, text: yAxisLabel, color: '#94a3b8' }, grid: { color: 'rgba(255,255,255,0.05)' }, - ticks: { color: '#64748b' } + ticks: { + color: '#64748b', + callback: chartType === 'percent' ? (v) => v + '%' : undefined + }, + max: chartType === 'percent' ? 100 : undefined }, x: { title: { display: true, text: 'Datum', color: '#94a3b8' }, grid: { display: false }, - ticks: { color: '#64748b' } + ticks: { color: '#64748b', maxRotation: 45 } } }, plugins: { legend: { display: true, position: 'top', - labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 15 } + labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 10 } }, tooltip: { backgroundColor: '#1e293b', titleColor: '#38bdf8', bodyColor: '#e2e8f0', borderColor: 'rgba(255,255,255,0.1)', - borderWidth: 1 + borderWidth: 1, + callbacks: { + label: (ctx) => { + const val = ctx.parsed.y; + if (chartType === 'percent') { + return `${ctx.dataset.label}: ${val.toFixed(1)}%`; + } + return `${ctx.dataset.label}: ${val.toLocaleString()}`; + } + } } } } }); - + document.getElementById('customGraphTitle').innerText = - `${yAxisLabel} nach ${groupBy === 'exchange' ? 'Exchange' : groupBy === 'sector' ? 'Sektor' : 'Datum'} (${dateFrom} bis ${dateTo})`; + `${yAxisLabel} nach ${groupBy === 'exchange' ? 'Exchange' : groupBy === 'sector' ? 'Sektor' : 'Datum'}${chartTypeLabels[chartType]} (${dateFrom} bis ${dateTo})`; } catch (err) { console.error('Error loading custom analytics:', err); } @@ -871,9 +951,11 @@ params.set('date_to', document.getElementById('customDateTo').value); params.set('y_axis', document.getElementById('customYAxis').value); params.set('group_by', document.getElementById('customGroupBy').value); + params.set('chart_type', document.getElementById('customChartType')?.value || 'line'); + params.set('hide_unknown', document.getElementById('hideUnknown')?.checked ? '1' : '0'); const exchanges = document.getElementById('customExchanges').value; if (exchanges) params.set('exchanges', exchanges); - + const url = window.location.origin + window.location.pathname + '?' + params.toString(); navigator.clipboard.writeText(url).then(() => { alert('Link in Zwischenablage kopiert!'); @@ -881,17 +963,19 @@ prompt('Link zum Teilen:', url); }); } - + function updateUrlParams() { const params = new URLSearchParams(window.location.search); const view = window.activeView; if (view) params.set('view', view); - + if (view === 'custom-analytics') { params.set('date_from', document.getElementById('customDateFrom').value || ''); params.set('date_to', document.getElementById('customDateTo').value || ''); params.set('y_axis', document.getElementById('customYAxis').value || ''); params.set('group_by', document.getElementById('customGroupBy').value || ''); + params.set('chart_type', document.getElementById('customChartType')?.value || 'line'); + params.set('hide_unknown', document.getElementById('hideUnknown')?.checked ? '1' : '0'); const exchanges = document.getElementById('customExchanges').value; if (exchanges) params.set('exchanges', exchanges); else params.delete('exchanges'); @@ -899,7 +983,7 @@ const period = document.getElementById('statisticsPeriod')?.value; if (period) params.set('period', period); } - + window.history.replaceState({}, '', window.location.pathname + '?' + params.toString()); } @@ -915,13 +999,19 @@ const yAxis = params.get('y_axis'); const groupBy = params.get('group_by'); const exchanges = params.get('exchanges'); - + const chartType = params.get('chart_type'); + const hideUnknown = params.get('hide_unknown'); + if (dateFrom) document.getElementById('customDateFrom').value = dateFrom; if (dateTo) document.getElementById('customDateTo').value = dateTo; if (yAxis) document.getElementById('customYAxis').value = yAxis; if (groupBy) document.getElementById('customGroupBy').value = groupBy; if (exchanges) document.getElementById('customExchanges').value = exchanges; - + if (chartType) document.getElementById('customChartType').value = chartType; + if (hideUnknown !== null) { + document.getElementById('hideUnknown').checked = hideUnknown === '1'; + } + if (dateFrom && dateTo) { setTimeout(() => updateCustomGraph(), 500); }