This commit is contained in:
@@ -185,8 +185,22 @@
|
||||
<label class="block text-sm font-bold text-slate-400 mb-2">Exchanges (optional, komma-separiert)</label>
|
||||
<input type="text" id="customExchanges" class="input-glass" placeholder="z.B. EIX,LS,XETRA,FRA,GETTEX,STU,QUOTRIX,DUSA,HAMA,HANA" onchange="updateCustomGraph(); updateUrlParams()">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-400 mb-2">Chart-Typ</label>
|
||||
<select id="customChartType" class="input-glass" onchange="updateCustomGraph(); updateUrlParams()">
|
||||
<option value="line">Linien</option>
|
||||
<option value="stacked">Stacked Area (Sektorrotation)</option>
|
||||
<option value="percent">Prozentual (100% Stacked)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 mt-4">
|
||||
<label class="flex items-center gap-2 text-sm text-slate-400 cursor-pointer">
|
||||
<input type="checkbox" id="hideUnknown" class="w-4 h-4" checked onchange="updateCustomGraph(); updateUrlParams()">
|
||||
"Unbekannt" ausblenden
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button onclick="updateCustomGraph()" class="glass px-6 py-3 bg-sky-500 hover:bg-sky-600 text-white font-bold rounded-lg transition-colors cursor-pointer">Graph aktualisieren</button>
|
||||
<button onclick="shareCustomGraph()" class="glass px-6 py-3 ml-2 text-sky-400 hover:text-sky-200 font-bold rounded-lg transition-colors cursor-pointer">🔗 Link teilen</button>
|
||||
</div>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user