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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user