fix: make SignalChart more robust

- Use ResizeObserver to handle width changes
- Use tick() to ensure DOM is ready before drawing
- Access reactive values in effect to trigger on changes
- Fixed canvas sizing to use percentage width
This commit is contained in:
shokollm
2026-04-12 04:11:34 +00:00
parent a253aae766
commit 01ec8bc539

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Signal } from '$lib/api'; import type { Signal } from '$lib/api';
import { onMount } from 'svelte'; import { onMount, tick } from 'svelte';
interface Props { interface Props {
signals?: Signal[]; signals?: Signal[];
@@ -13,22 +13,50 @@
let width = $state(800); let width = $state(800);
let containerEl: HTMLDivElement; let containerEl: HTMLDivElement;
let canvasEl: HTMLCanvasElement; let canvasEl: HTMLCanvasElement;
let initialized = $state(false);
onMount(() => { onMount(() => {
// Set initial width
if (containerEl) { if (containerEl) {
width = containerEl.clientWidth; width = containerEl.clientWidth;
}
// Resize observer
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
width = entry.contentRect.width;
drawChart(); drawChart();
} }
}); });
$effect(() => { if (containerEl) {
if (canvasEl && (signals.length > 0 || klines.length > 0)) { resizeObserver.observe(containerEl);
drawChart();
} }
initialized = true;
return () => {
resizeObserver.disconnect();
};
});
// Draw when data changes
$effect(() => {
// Access reactive values to trigger effect
const currentSignals = signals;
const currentKlines = klines;
const currentWidth = width;
// Wait for DOM to be ready
tick().then(() => {
drawChart();
});
}); });
function drawChart() { function drawChart() {
if (!canvasEl) return; if (!canvasEl) {
return;
}
const ctx = canvasEl.getContext('2d'); const ctx = canvasEl.getContext('2d');
if (!ctx) return; if (!ctx) return;
@@ -38,18 +66,29 @@
canvasEl.height = height * dpr; canvasEl.height = height * dpr;
ctx.scale(dpr, dpr); ctx.scale(dpr, dpr);
// Clear // Clear canvas
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
// Get price data (convert strings to numbers) // Check if we have data
const priceData = klines.length > 0 if (klines.length === 0 && signals.length === 0) {
? klines.map(k => ({ time: k.time, price: parseFloat(k.close) || 0 })) return;
: signals.map(s => ({ time: 0, price: s.price })); }
// Get price data
let priceData: { time: number; price: number }[] = [];
if (klines.length > 0) {
priceData = klines.map(k => ({
time: k.time,
price: typeof k.close === 'string' ? parseFloat(k.close) : k.close
})).filter(d => !isNaN(d.price) && d.price > 0);
} else if (signals.length > 0) {
priceData = signals.map(s => ({ time: 0, price: s.price }));
}
if (priceData.length === 0) return; if (priceData.length === 0) return;
const prices = priceData.map(d => d.price); const prices = priceData.map(d => d.price);
const padding = { top: 20, right: 20, bottom: 30, left: 60 }; const padding = { top: 20, right: 20, bottom: 30, left: 60 };
const chartWidth = width - padding.left - padding.right; const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom; const chartHeight = height - padding.top - padding.bottom;
@@ -66,7 +105,7 @@
} }
function indexToX(index: number): number { function indexToX(index: number): number {
return padding.left + (index / (prices.length - 1 || 1)) * chartWidth; return padding.left + (index / Math.max(prices.length - 1, 1)) * chartWidth;
} }
// Draw grid lines // Draw grid lines
@@ -81,7 +120,7 @@
} }
// Draw Y axis labels // Draw Y axis labels
ctx.fillStyle = '#666'; ctx.fillStyle = '#888';
ctx.font = '10px monospace'; ctx.font = '10px monospace';
ctx.textAlign = 'right'; ctx.textAlign = 'right';
for (let i = 0; i <= 4; i++) { for (let i = 0; i <= 4; i++) {
@@ -94,11 +133,9 @@
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = '#667eea'; ctx.strokeStyle = '#667eea';
ctx.lineWidth = 2; ctx.lineWidth = 2;
for (let i = 0; i < prices.length; i++) { ctx.moveTo(indexToX(0), priceToY(prices[0]));
const x = indexToX(i); for (let i = 1; i < prices.length; i++) {
const y = priceToY(prices[i]); ctx.lineTo(indexToX(i), priceToY(prices[i]));
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
} }
ctx.stroke(); ctx.stroke();
@@ -107,26 +144,34 @@
ctx.lineTo(indexToX(0), padding.top + chartHeight); ctx.lineTo(indexToX(0), padding.top + chartHeight);
ctx.closePath(); ctx.closePath();
const gradient = ctx.createLinearGradient(0, padding.top, 0, padding.top + chartHeight); const gradient = ctx.createLinearGradient(0, padding.top, 0, padding.top + chartHeight);
gradient.addColorStop(0, 'rgba(102, 126, 234, 0.2)'); gradient.addColorStop(0, 'rgba(102, 126, 234, 0.3)');
gradient.addColorStop(1, 'rgba(102, 126, 234, 0)'); gradient.addColorStop(1, 'rgba(102, 126, 234, 0)');
ctx.fillStyle = gradient; ctx.fillStyle = gradient;
ctx.fill(); ctx.fill();
// Draw signal markers // Draw signal markers
if (signals.length > 0) { if (signals.length > 0) {
// Draw line to each signal point
signals.forEach((signal) => { signals.forEach((signal) => {
const signalIndex = klines.length > 0 // Find closest price match
? klines.findIndex(k => Math.abs(k.close - signal.price) < 0.0001) const signalPrice = signal.price;
: signals.indexOf(signal); let closestIndex = 0;
let closestDiff = Infinity;
if (signalIndex >= 0) { for (let i = 0; i < priceData.length; i++) {
const x = indexToX(signalIndex); const diff = Math.abs(priceData[i].price - signalPrice);
const y = priceToY(signal.price); if (diff < closestDiff) {
closestDiff = diff;
closestIndex = i;
}
}
// Vertical line from top const x = indexToX(closestIndex);
const y = priceToY(signalPrice);
const color = signal.signal_type === 'buy' ? '#22c55e' : '#ef4444';
// Vertical dashed line
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = signal.signal_type === 'buy' ? '#22c55e' : '#ef4444'; ctx.strokeStyle = color;
ctx.setLineDash([4, 4]); ctx.setLineDash([4, 4]);
ctx.moveTo(x, padding.top); ctx.moveTo(x, padding.top);
ctx.lineTo(x, y); ctx.lineTo(x, y);
@@ -136,42 +181,38 @@
// Signal dot // Signal dot
ctx.beginPath(); ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2); ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fillStyle = signal.signal_type === 'buy' ? '#22c55e' : '#ef4444'; ctx.fillStyle = color;
ctx.fill(); ctx.fill();
ctx.strokeStyle = '#fff'; ctx.strokeStyle = '#fff';
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.stroke(); ctx.stroke();
}
}); });
} }
// Legend // Legend
ctx.fillStyle = '#888';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
if (signals.length > 0) { if (signals.length > 0) {
const buyCount = signals.filter(s => s.signal_type === 'buy').length; const buyCount = signals.filter(s => s.signal_type === 'buy').length;
const sellCount = signals.filter(s => s.signal_type === 'sell').length; const sellCount = signals.filter(s => s.signal_type === 'sell').length;
ctx.fillText(`📈 ${buyCount} Buy | ${sellCount} Sell | ${priceData.length} Candles`, width / 2, height - 8);
ctx.fillStyle = '#888'; } else {
ctx.font = '12px sans-serif'; ctx.fillText(`${priceData.length} Candles (No signals generated)`, width / 2, height - 8);
ctx.textAlign = 'center';
ctx.fillText(`📈 ${buyCount} Buy | ${sellCount} Sell | ${prices.length} Candles`, width / 2, height - 8);
} else if (klines.length > 0) {
ctx.fillStyle = '#888';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`${prices.length} Candles (No signals generated)`, width / 2, height - 8);
} }
} }
</script> </script>
<div class="signal-chart" bind:this={containerEl}> <div class="signal-chart" bind:this={containerEl}>
{#if signals.length === 0 && klines.length === 0} {#if klines.length === 0 && signals.length === 0}
<div class="empty-state"> <div class="empty-state">
<p>No data to display. Start a simulation to see price movements.</p> <p>No data to display. Start a simulation to see price movements.</p>
</div> </div>
{:else} {:else}
<canvas <canvas
bind:this={canvasEl} bind:this={canvasEl}
style="width: {width}px; height: {height}px;" style="width: 100%; height: {height}px;"
></canvas> ></canvas>
{/if} {/if}
</div> </div>
@@ -198,5 +239,6 @@
canvas { canvas {
display: block; display: block;
width: 100%;
} }
</style> </style>