Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 79 additions & 24 deletions frontend/src/components/Chart/TradingChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const CHART_CONSTANTS = {
DEFAULT_SYMBOL_NAME: 'btcusdt',
PAGE_SIZE: 120,
SCROLL_THRESHOLD: 10,
INITIAL_FILL_DELAY_MS: 500,
};

export const TradingChart = () => {
Expand All @@ -32,26 +31,37 @@ export const TradingChart = () => {
const lastCandleTimeRef = useRef<number>(0);

// Pagination & Infinite Scroll Control
const isFetchingRef = useRef<boolean>(false);
const activeRequestRef = useRef<{ id: number, generation: number } | null>(null);
const requestIdRef = useRef(0);
const requestGenerationRef = useRef(0);
const isInitializingRef = useRef(true);
const hasUserNavigatedRef = useRef(false);
const hasMoreRef = useRef<boolean>(true);
const rawDataRef = useRef<{ candles: ChartCandle[], volumes: VolumeBar[] }>({ candles: [], volumes: [] });
const ensureFullViewRef = useRef<() => void>(() => {});

// State
const [activeTimeframe, setActiveTimeframe] = useState<OhlcInterval>('M1');
const [isLoading, setIsLoading] = useState(true);

// --- Data Loading & Merging ---
const loadChartData = useCallback(async (to?: string) => {
if (isFetchingRef.current || (!hasMoreRef.current && to)) return;
const loadChartData = useCallback(async (generation: number, to?: string): Promise<boolean> => {
const activeRequest = activeRequestRef.current;
if (activeRequest?.generation === generation || (!hasMoreRef.current && to)) {
return false;
}

isFetchingRef.current = true;
const requestId = ++requestIdRef.current;
activeRequestRef.current = { id: requestId, generation };
try {
const response = await getOhlcData(CHART_CONSTANTS.DEFAULT_SYMBOL_ID, activeTimeframe, CHART_CONSTANTS.PAGE_SIZE, to);

if (generation !== requestGenerationRef.current) {
return false;
}

if (response.candles.length === 0) {
hasMoreRef.current = false;
return;
return false;
}

const newCandles: ChartCandle[] = [];
Expand Down Expand Up @@ -92,21 +102,29 @@ export const TradingChart = () => {
lastCandleTimeRef.current = Math.max(lastCandleTimeRef.current, maxTime);
}

// If initial load doesn't fill viewport, load more
if (!to) {
setTimeout(() => ensureFullViewRef.current(), CHART_CONSTANTS.INITIAL_FILL_DELAY_MS);
}

return true;
} catch (err) {
console.error("[TradingChart] Failed to load data", err);
if (generation === requestGenerationRef.current) {
console.error("[TradingChart] Failed to load data", err);
}
return false;
} finally {
isFetchingRef.current = false;
setIsLoading(false);
if (activeRequestRef.current?.id === requestId) {
activeRequestRef.current = null;
}
}
}, [activeTimeframe]);

const ensureFullView = useCallback(() => {
if (!mainChartRef.current || isFetchingRef.current || !hasMoreRef.current) return;
const generation = requestGenerationRef.current;
const isCurrentGenerationFetching = activeRequestRef.current?.generation === generation;
if (!mainChartRef.current
|| isInitializingRef.current
|| !hasUserNavigatedRef.current
|| isCurrentGenerationFetching
|| !hasMoreRef.current) {
return;
}

const timeScale = mainChartRef.current.timeScale();
const range = timeScale.getVisibleLogicalRange();
Expand All @@ -116,15 +134,11 @@ export const TradingChart = () => {
if (oldestCandle) {
const toStr = new Date((oldestCandle.time as number) * 1000).toISOString().split('.')[0];
console.log(`[TradingChart] Infinite Scroll Triggered. Loading before: ${toStr}`);
loadChartData(toStr);
void loadChartData(generation, toStr);
}
}
}, [loadChartData]);

useEffect(() => {
ensureFullViewRef.current = ensureFullView;
}, [ensureFullView]);

// --- Real-time Data Handling ---
const handleWebSocketMessage = useCallback((msg: WsMessage) => {
if (!mainSeriesRef.current || !volumeSeriesRef.current) return;
Expand Down Expand Up @@ -179,10 +193,16 @@ export const TradingChart = () => {
useEffect(() => {
if (!mainContainerRef.current || !volumeContainerRef.current) return;

const generation = ++requestGenerationRef.current;
let initializationFrame: number | null = null;

// Reset
rawDataRef.current = { candles: [], volumes: [] };
hasMoreRef.current = true;
lastCandleTimeRef.current = 0;
isInitializingRef.current = true;
hasUserNavigatedRef.current = false;
setIsLoading(true);

// Chart Creation
const mainChart = createChart(mainContainerRef.current, {
Expand Down Expand Up @@ -248,9 +268,23 @@ export const TradingChart = () => {
else mainChart.clearCrosshairPosition();
});

// Load & Fit
loadChartData();
mainChart.timeScale().fitContent();
// Load first, then fit. Programmatic range changes must not trigger pagination.
const initializeChart = async () => {
const loaded = await loadChartData(generation);
if (generation !== requestGenerationRef.current) return;

if (loaded) {
mainTimeScale.fitContent();
}

initializationFrame = requestAnimationFrame(() => {
if (generation === requestGenerationRef.current) {
isInitializingRef.current = false;
setIsLoading(false);
}
});
};
void initializeChart();

// Resize
const handleResize = () => {
Expand All @@ -264,8 +298,29 @@ export const TradingChart = () => {
const resizeObserver = new ResizeObserver(() => handleResize());
resizeObserver.observe(mainContainerRef.current);

const markUserNavigation = () => {
hasUserNavigatedRef.current = true;
};
const navigationTargets = [mainContainerRef.current, volumeContainerRef.current];
navigationTargets.forEach((target) => {
target.addEventListener('wheel', markUserNavigation, { passive: true });
target.addEventListener('pointerdown', markUserNavigation, { passive: true });
target.addEventListener('touchstart', markUserNavigation, { passive: true });
});

return () => {
if (requestGenerationRef.current === generation) {
requestGenerationRef.current += 1;
}
if (initializationFrame !== null) {
cancelAnimationFrame(initializationFrame);
}
resizeObserver.disconnect();
navigationTargets.forEach((target) => {
target.removeEventListener('wheel', markUserNavigation);
target.removeEventListener('pointerdown', markUserNavigation);
target.removeEventListener('touchstart', markUserNavigation);
});
mainTimeScale.unsubscribeVisibleLogicalRangeChange(syncVolRange);
volTimeScale.unsubscribeVisibleLogicalRangeChange(syncMainRange);
mainChart.remove();
Expand Down
Loading