147 lines
5.9 KiB
JavaScript
147 lines
5.9 KiB
JavaScript
(function () {
|
|
const app = document.querySelector('[data-bc-home]');
|
|
if (!app) return;
|
|
|
|
const chartShell = app.querySelector('[data-bc-chart]');
|
|
const instrumentSelect = app.querySelector('[data-bc-instrument]');
|
|
const instrumentNameNode = app.querySelector('[data-bc-instrument-name]');
|
|
const instrumentMetaNode = app.querySelector('[data-bc-instrument-meta]');
|
|
const rangeButtons = Array.from(app.querySelectorAll('[data-range]'));
|
|
const statusNode = app.querySelector('[data-bc-chart-status]');
|
|
const summaryNode = app.querySelector('[data-bc-chart-summary]');
|
|
const endpoint = app.getAttribute('data-chart-endpoint') || '';
|
|
const instrumentsScript = app.querySelector('[data-bc-instruments-json]');
|
|
const instrumentMap = new Map();
|
|
if (instrumentsScript?.textContent) {
|
|
try {
|
|
const items = JSON.parse(instrumentsScript.textContent);
|
|
if (Array.isArray(items)) {
|
|
items.forEach((item) => instrumentMap.set(String(item.instrument_id), item));
|
|
}
|
|
} catch (_error) {}
|
|
}
|
|
let activeRange = '1m';
|
|
let currentPayload = null;
|
|
|
|
function pointsForRange(payload, range) {
|
|
if (!payload) return [];
|
|
const daily = payload.daily || [];
|
|
const weekly = payload.weekly || [];
|
|
const monthly = payload.monthly || [];
|
|
switch (range) {
|
|
case '1d': return daily.slice(-2);
|
|
case '5d': return daily.slice(-5);
|
|
case '1m': return daily.slice(-22);
|
|
case '3m': return daily.slice(-66);
|
|
case '6m': return weekly.slice(-26);
|
|
case '1y': return weekly.slice(-52);
|
|
case '5y': return monthly.slice(-60);
|
|
default: return daily.slice(-22);
|
|
}
|
|
}
|
|
|
|
function renderChart(points) {
|
|
if (!chartShell) return;
|
|
chartShell.classList.remove('bc-panel-fade');
|
|
void chartShell.offsetWidth;
|
|
chartShell.classList.add('bc-panel-fade');
|
|
|
|
if (!points || points.length === 0) {
|
|
chartShell.innerHTML = '<div class="muted">Keine Chartdaten verfuegbar.</div>';
|
|
return;
|
|
}
|
|
|
|
const values = points.map((point) => Number(point.close || 0));
|
|
const min = Math.min(...values);
|
|
const max = Math.max(...values);
|
|
const width = 920;
|
|
const height = 340;
|
|
const paddingX = 24;
|
|
const paddingY = 28;
|
|
const usableWidth = width - paddingX * 2;
|
|
const usableHeight = height - paddingY * 2;
|
|
const spread = max - min || 1;
|
|
|
|
const coords = points.map((point, index) => {
|
|
const x = paddingX + (usableWidth * index / Math.max(points.length - 1, 1));
|
|
const y = paddingY + usableHeight - ((Number(point.close || 0) - min) / spread) * usableHeight;
|
|
return { x, y };
|
|
});
|
|
|
|
const path = coords.map((coord, index) => `${index === 0 ? 'M' : 'L'}${coord.x.toFixed(2)},${coord.y.toFixed(2)}`).join(' ');
|
|
const area = `${path} L${coords[coords.length - 1].x.toFixed(2)},${height - paddingY} L${coords[0].x.toFixed(2)},${height - paddingY} Z`;
|
|
const grid = [0, 1, 2, 3].map((step) => {
|
|
const y = paddingY + (usableHeight * step / 3);
|
|
return `<line x1="${paddingX}" y1="${y}" x2="${width - paddingX}" y2="${y}"></line>`;
|
|
}).join('');
|
|
|
|
chartShell.innerHTML = `
|
|
<svg class="bc-chart-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none">
|
|
<defs>
|
|
<linearGradient id="bc-chart-fill" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stop-color="rgba(94,234,212,0.32)"></stop>
|
|
<stop offset="100%" stop-color="rgba(94,234,212,0.02)"></stop>
|
|
</linearGradient>
|
|
</defs>
|
|
<g class="bc-chart-grid">${grid}</g>
|
|
<path class="bc-chart-area" d="${area}"></path>
|
|
<path class="bc-chart-path" d="${path}"></path>
|
|
</svg>
|
|
`;
|
|
|
|
const first = values[0];
|
|
const last = values[values.length - 1];
|
|
const delta = last - first;
|
|
const percent = first !== 0 ? (delta / first) * 100 : 0;
|
|
if (summaryNode) {
|
|
summaryNode.textContent = `${last.toFixed(2)} | ${delta >= 0 ? '+' : ''}${delta.toFixed(2)} (${percent.toFixed(2)}%)`;
|
|
}
|
|
}
|
|
|
|
async function loadChart() {
|
|
const instrumentId = instrumentSelect ? instrumentSelect.value : '';
|
|
if (!instrumentId || !endpoint) {
|
|
if (statusNode) statusNode.textContent = 'Keine Aktie fuer den Chart ausgewaehlt.';
|
|
if (summaryNode) summaryNode.textContent = '-';
|
|
if (chartShell) chartShell.innerHTML = '<div class="muted">Keine Chartdaten verfuegbar.</div>';
|
|
return;
|
|
}
|
|
if (statusNode) statusNode.textContent = 'Chartdaten werden geladen...';
|
|
try {
|
|
const response = await fetch(`${endpoint}${endpoint.includes('?') ? '&' : '?'}instrument_id=${encodeURIComponent(instrumentId)}`, { headers: { Accept: 'application/json' } });
|
|
const payload = await response.json();
|
|
if (!payload.ok) {
|
|
throw new Error(payload.message || 'Chartdaten konnten nicht geladen werden.');
|
|
}
|
|
currentPayload = payload;
|
|
renderChart(pointsForRange(payload, activeRange));
|
|
if (statusNode) statusNode.textContent = `Quelle: Alpha Vantage | Symbol ${payload.symbol || ''}`;
|
|
} catch (error) {
|
|
currentPayload = null;
|
|
chartShell.innerHTML = `<div class="muted">${error.message}</div>`;
|
|
if (statusNode) statusNode.textContent = 'Fehler beim Laden der Chartdaten.';
|
|
}
|
|
}
|
|
|
|
rangeButtons.forEach((button) => {
|
|
button.addEventListener('click', () => {
|
|
activeRange = button.getAttribute('data-range') || '1m';
|
|
rangeButtons.forEach((item) => item.setAttribute('aria-pressed', item === button ? 'true' : 'false'));
|
|
renderChart(pointsForRange(currentPayload, activeRange));
|
|
});
|
|
});
|
|
|
|
if (instrumentSelect) {
|
|
instrumentSelect.addEventListener('change', () => {
|
|
const meta = instrumentMap.get(String(instrumentSelect.value));
|
|
if (meta) {
|
|
if (instrumentNameNode) instrumentNameNode.textContent = meta.instrument_name || 'Keine Aktie ausgewaehlt';
|
|
if (instrumentMetaNode) instrumentMetaNode.textContent = `${meta.symbol || ''} · ${meta.isin || '-'}`;
|
|
}
|
|
loadChart();
|
|
});
|
|
}
|
|
|
|
loadChart();
|
|
})();
|