rerun complete

This commit is contained in:
Boki 2025-07-04 18:14:44 -04:00
parent 11c6c19628
commit d15e542f20
17 changed files with 4694 additions and 146 deletions

View file

@ -0,0 +1,53 @@
import React, { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<div className="flex items-center justify-center h-full bg-background">
<div className="text-center p-8">
<h2 className="text-lg font-semibold text-text-primary mb-2">
Something went wrong
</h2>
<p className="text-sm text-text-secondary mb-4">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
>
Reload Page
</button>
</div>
</div>
)
);
}
return this.props.children;
}
}

View file

@ -1,5 +1,6 @@
import * as LightweightCharts from 'lightweight-charts';
import { useEffect, useRef, useCallback } from 'react';
import { useEffect, useRef, useCallback, useMemo } from 'react';
import { useChartManager } from '@/hooks/useChartManager';
export interface ChartData {
time: number;
@ -35,6 +36,7 @@ export interface ChartProps {
}>;
tradeMarkers?: TradeMarker[];
className?: string;
chartId?: string;
}
export function Chart({
@ -46,16 +48,32 @@ export function Chart({
overlayData = [],
tradeMarkers = [],
className = '',
chartId = 'default',
}: ChartProps) {
const chartContainerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<LightweightCharts.IChartApi | null>(null);
const mainSeriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
const volumeSeriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
const overlaySeriesRef = useRef<Map<string, LightweightCharts.ISeriesApi<any>>>(new Map());
const { createChart, getChart, setMainSeries, setVolumeSeries, addOverlaySeries, disposeChart } = useChartManager();
// Use a stable unique ID that changes only when chartId prop changes
const uniqueChartId = useMemo(() => {
return `${chartId}-${Date.now()}`;
}, [chartId])
const mountedRef = useRef(true);
const lastChartIdRef = useRef(chartId);
// Track component mount state
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
// Cleanup on unmount
disposeChart(uniqueChartId);
};
}, [disposeChart]);
// Reset zoom handler
const resetZoom = useCallback(() => {
if (chartRef.current && data.length > 0) {
const chartInstance = getChart(uniqueChartId);
if (chartInstance && !chartInstance.isDisposed && data.length > 0) {
// Get the validated data to ensure we're using the correct time values
const validateData = (rawData: any[]) => {
const seen = new Set<number>();
@ -84,23 +102,35 @@ export function Chart({
const timeRange = lastTime - firstTime;
const padding = timeRange * 0.05;
chartRef.current.timeScale().setVisibleRange({
from: (firstTime - padding) as any,
to: (lastTime + padding) as any,
});
try {
chartInstance.chart.timeScale().setVisibleRange({
from: (firstTime - padding) as any,
to: (lastTime + padding) as any,
});
chartInstance.chart.timeScale().fitContent();
} catch (error) {
console.error('Error resetting zoom:', error);
}
}
chartRef.current.timeScale().fitContent();
}
}, [data]);
}, [data, getChart]);
useEffect(() => {
if (!chartContainerRef.current || !data || !data.length) {
return;
}
let handleResize: (() => void) | undefined;
let isCleanedUp = false;
// Use requestAnimationFrame to ensure DOM is ready and avoid conflicts
const rafId = requestAnimationFrame(() => {
const initTimeout = setTimeout(() => {
if (!chartContainerRef.current || isCleanedUp || !mountedRef.current) return;
// Create chart
const chart = LightweightCharts.createChart(chartContainerRef.current, {
// Create chart using the chart manager
const chart = createChart(uniqueChartId, chartContainerRef.current, {
width: chartContainerRef.current.clientWidth,
height: height,
layout: {
@ -138,7 +168,10 @@ export function Chart({
},
});
chartRef.current = chart;
if (!chart) {
console.error('Failed to create chart');
return;
}
// Filter, validate and sort data
const validateAndFilterData = (rawData: any[]) => {
@ -166,8 +199,10 @@ export function Chart({
};
// Create main series
let mainSeries: LightweightCharts.ISeriesApi<any> | null = null;
if (type === 'candlestick' && data[0].open !== undefined) {
mainSeriesRef.current = chart.addCandlestickSeries({
mainSeries = chart.addCandlestickSeries({
upColor: '#10b981',
downColor: '#ef4444',
borderUpColor: '#10b981',
@ -176,9 +211,9 @@ export function Chart({
wickDownColor: '#ef4444',
});
const validData = validateAndFilterData(data);
mainSeriesRef.current.setData(validData as LightweightCharts.CandlestickData[]);
mainSeries.setData(validData as LightweightCharts.CandlestickData[]);
} else if (type === 'line' || (type === 'candlestick' && data[0].value !== undefined)) {
mainSeriesRef.current = chart.addLineSeries({
mainSeries = chart.addLineSeries({
color: '#3b82f6',
lineWidth: 2,
});
@ -187,9 +222,9 @@ export function Chart({
value: d.value ?? d.close ?? 0
}));
const validData = validateAndFilterData(lineData);
mainSeriesRef.current.setData(validData);
mainSeries.setData(validData);
} else if (type === 'area') {
mainSeriesRef.current = chart.addAreaSeries({
mainSeries = chart.addAreaSeries({
lineColor: '#3b82f6',
topColor: '#3b82f6',
bottomColor: 'rgba(59, 130, 246, 0.1)',
@ -200,12 +235,16 @@ export function Chart({
value: d.value ?? d.close ?? 0
}));
const validData = validateAndFilterData(areaData);
mainSeriesRef.current.setData(validData);
mainSeries.setData(validData);
}
if (mainSeries) {
setMainSeries(uniqueChartId, mainSeries);
}
// Add volume if available
if (showVolume && data.some(d => d.volume)) {
volumeSeriesRef.current = chart.addHistogramSeries({
const volumeSeries = chart.addHistogramSeries({
color: '#3b82f680',
priceFormat: {
type: 'volume',
@ -213,7 +252,7 @@ export function Chart({
priceScaleId: 'volume',
});
volumeSeriesRef.current.priceScale().applyOptions({
volumeSeries.priceScale().applyOptions({
scaleMargins: {
top: 0.8,
bottom: 0,
@ -231,11 +270,11 @@ export function Chart({
'#3b82f640',
}))
);
volumeSeriesRef.current.setData(volumeData);
volumeSeries.setData(volumeData);
setVolumeSeries(uniqueChartId, volumeSeries);
}
// Add overlay series
overlaySeriesRef.current.clear();
overlayData.forEach((overlay, index) => {
const series = chart.addLineSeries({
color: overlay.color || ['#ff9800', '#4caf50', '#9c27b0', '#f44336'][index % 4],
@ -260,11 +299,11 @@ export function Chart({
}));
const validatedData = validateAndFilterData(overlayDataWithTime);
series.setData(validatedData);
overlaySeriesRef.current.set(overlay.name, series);
addOverlaySeries(uniqueChartId, overlay.name, series);
});
// Add trade markers
if (tradeMarkers.length > 0 && mainSeriesRef.current) {
if (tradeMarkers.length > 0 && mainSeries) {
// Sort markers by time to ensure they're in ascending order
const sortedMarkers = [...tradeMarkers].sort((a, b) => a.time - b.time);
const markers: LightweightCharts.SeriesMarker<LightweightCharts.Time>[] = sortedMarkers.map(marker => ({
@ -276,7 +315,7 @@ export function Chart({
id: marker.id,
size: 1
}));
mainSeriesRef.current.setMarkers(markers);
mainSeries.setMarkers(markers);
}
// Fit content with a slight delay to ensure all series are loaded
@ -322,29 +361,39 @@ export function Chart({
});
// Handle resize
const handleResize = () => {
if (chartContainerRef.current && chart) {
chart.applyOptions({
width: chartContainerRef.current.clientWidth,
});
handleResize = () => {
if (chartContainerRef.current && !isCleanedUp) {
const chartInstance = getChart(uniqueChartId);
if (chartInstance && !chartInstance.isDisposed) {
try {
chartInstance.chart.applyOptions({
width: chartContainerRef.current.clientWidth,
});
} catch (error) {
// Ignore errors if the chart is being disposed
if (!error?.message?.includes('disposed')) {
console.error('Error resizing chart:', error);
}
}
}
}
};
window.addEventListener('resize', handleResize);
}, 0); // End of setTimeout
}); // End of requestAnimationFrame
// Cleanup
return () => {
window.removeEventListener('resize', handleResize);
// Clear all refs before removing the chart
mainSeriesRef.current = null;
volumeSeriesRef.current = null;
overlaySeriesRef.current.clear();
if (chartRef.current) {
chartRef.current.remove();
chartRef.current = null;
isCleanedUp = true;
cancelAnimationFrame(rafId);
if (handleResize) {
window.removeEventListener('resize', handleResize);
}
// Dispose immediately - the chart manager handles cleanup safely
disposeChart(uniqueChartId);
};
}, [data, height, type, showVolume, theme, overlayData, tradeMarkers]);
}, [data, height, type, showVolume, theme, overlayData, tradeMarkers, createChart, getChart, setMainSeries, setVolumeSeries, addOverlaySeries, disposeChart]);
return (
<div className={`relative ${className}`}>