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);
}