chart error fix on slow network
This commit is contained in:
@@ -9,6 +9,10 @@ import { DISPLAY_COUNT } from '../hooks/useUpbitData';
|
||||
/** Upbit 실시간 — 초기 히스토리가 이 개수 미만이면 setData 하지 않음 (종목 전환 직후 sparse 봉 → 캔들 폭 비정상) */
|
||||
const MIN_BARS_FOR_FULL_RELOAD = Math.min(50, Math.floor(DISPLAY_COUNT / 4));
|
||||
|
||||
/** pane 비율·가격축 계산에 쓸 수 있는 최소 래퍼 높이 (원격·모바일 레이아웃 지연 대응) */
|
||||
const MIN_VALID_LAYOUT_HEIGHT = 80;
|
||||
const LAYOUT_RETRY_MAX = 30;
|
||||
|
||||
function createIndicatorReloadCover(containerEl: HTMLDivElement | null): HTMLDivElement | null {
|
||||
if (!containerEl?.parentElement) return null;
|
||||
const cover = document.createElement('div');
|
||||
@@ -331,7 +335,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
*
|
||||
* wrapper.clientHeight 를 정확한 가용 높이로 ChartManager 에 전달합니다.
|
||||
* CSS Grid 레이아웃 초기화 지연으로 wrapperH=0 이 될 수 있으므로,
|
||||
* retryCount 를 통해 최대 6회 자동 재시도합니다 (100→200→300ms…)
|
||||
* retryCount 를 통해 최대 30회 자동 재시도합니다 (원격·테더링 등 느린 레이아웃 대응).
|
||||
*/
|
||||
const applyPaneLayout = useCallback((mgr: ChartManager, retryCount = 0) => {
|
||||
const wrapper = wrapperRef.current;
|
||||
@@ -339,10 +343,10 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
if (!wrapper || !container) return;
|
||||
|
||||
const wrapperH = wrapper.clientHeight;
|
||||
if (wrapperH <= 0) {
|
||||
// CSS Grid 높이가 아직 계산되지 않은 경우: 지연 후 재시도 (최대 6회)
|
||||
if (retryCount < 6) {
|
||||
setTimeout(() => applyPaneLayout(mgr, retryCount + 1), 100 * (retryCount + 1));
|
||||
if (wrapperH < MIN_VALID_LAYOUT_HEIGHT) {
|
||||
if (retryCount < LAYOUT_RETRY_MAX) {
|
||||
const delay = Math.min(120 * (retryCount + 1), 500);
|
||||
setTimeout(() => applyPaneLayout(mgr, retryCount + 1), delay);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -372,10 +376,23 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
container.style.height = `${required}px`;
|
||||
} else {
|
||||
container.style.height = '';
|
||||
try {
|
||||
if (w > 0 && wrapperH > 0) mgr.resize(w, wrapperH);
|
||||
} catch { /* ok */ }
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [paneLayoutClamp]);
|
||||
|
||||
/** pane 레이아웃 + 가격·시간축 정상화 (느린 네트워크·레이아웃 지연 후 복구용) */
|
||||
const normalizeChartViewport = useCallback((mgr: ChartManager) => {
|
||||
applyPaneLayout(mgr);
|
||||
requestAnimationFrame(() => {
|
||||
try { mgr.autoScale(); } catch { /* ok */ }
|
||||
const fb = mgr.hasIchimoku() ? 28 : 0;
|
||||
mgr.setInitialVisibleRange(DISPLAY_COUNT, fb);
|
||||
});
|
||||
}, [applyPaneLayout]);
|
||||
|
||||
/** 캔들 확대 보기 → 원복 직후 가격·시간축·pane 비율 정상화 */
|
||||
useEffect(() => {
|
||||
const mgr = managerRef.current;
|
||||
@@ -704,7 +721,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
|
||||
onDataLoaded?.();
|
||||
|
||||
reloadSafetyTimers.current = [300, 700, 1400].map(delay =>
|
||||
reloadSafetyTimers.current = [300, 700, 1400, 2500].map(delay =>
|
||||
setTimeout(() => {
|
||||
const m = managerRef.current;
|
||||
if (!m || !m.hasMainSeries()) return;
|
||||
@@ -715,11 +732,9 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
return;
|
||||
}
|
||||
const w = wrapperRef.current;
|
||||
if (!w || w.clientHeight <= 0) return;
|
||||
m.resetPaneHeights(w.clientHeight);
|
||||
const fb = m.hasIchimoku() ? 28 : 0;
|
||||
m.setInitialVisibleRange(DISPLAY_COUNT, fb);
|
||||
if (delay === 1400) onDataLoaded?.();
|
||||
if (!w || w.clientHeight < MIN_VALID_LAYOUT_HEIGHT) return;
|
||||
normalizeChartViewport(m);
|
||||
if (delay === 2500) onDataLoaded?.();
|
||||
}, delay)
|
||||
);
|
||||
} while (reloadCoalesceRef.current);
|
||||
@@ -730,7 +745,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
queueFullReload();
|
||||
}
|
||||
}
|
||||
}, [applyPaneLayout, commitCandleLayout, onCandlesReady, onDataLoaded, displayTimezone, timeframe, market, queueFullReload, queueIndicatorRecovery]);
|
||||
}, [applyPaneLayout, commitCandleLayout, normalizeChartViewport, onCandlesReady, onDataLoaded, displayTimezone, timeframe, market, queueFullReload, queueIndicatorRecovery]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadAllRef.current = reloadAll;
|
||||
@@ -977,8 +992,13 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
// autoSize:true 를 쓰면 LWC 가 내부 ResizeObserver 로 자동 크기 조절.
|
||||
// 이 외부 ResizeObserver 는 applyPaneLayout 재트리거 + 데이터 재로드 보장용.
|
||||
let prevW = 0, prevH = 0;
|
||||
const ro = new ResizeObserver(([entry]) => {
|
||||
const { width, height } = entry.contentRect;
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
for (const e of entries) {
|
||||
width = Math.max(width, e.contentRect.width);
|
||||
height = Math.max(height, e.contentRect.height);
|
||||
}
|
||||
if (width === prevW && height === prevH) return;
|
||||
// 이전 크기가 0이었다가 유효해진 경우 (= display:none → visible 전환)
|
||||
const wasHidden = prevW <= 0 || prevH <= 0;
|
||||
@@ -1035,7 +1055,9 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
}
|
||||
}, 80);
|
||||
});
|
||||
ro.observe(containerRef.current);
|
||||
const wrapperEl = wrapperRef.current;
|
||||
if (wrapperEl) ro.observe(wrapperEl);
|
||||
ro.observe(container);
|
||||
|
||||
const bootstrapIfReady = () => {
|
||||
const m = managerRef.current;
|
||||
@@ -1303,6 +1325,44 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators, chartVisible, reloadIndicatorsWithCover, afterIndicatorPaneMutation, restoreLogicalRange, syncIndicatorTrackingRefs, repairMissingIndicators, flushPendingIndicatorResync]);
|
||||
|
||||
// 원격·모바일 등 레이아웃·데이터 로드 지연 후 pane·가격축 안정화
|
||||
const layoutRecoveryAttemptedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
layoutRecoveryAttemptedRef.current = false;
|
||||
}, [market, timeframe]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartVisible) return;
|
||||
const mgr = managerRef.current;
|
||||
if (!mgr || !chartMgr || bars.length < 2) return;
|
||||
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
||||
if (!mgr.hasMainSeries()) return;
|
||||
|
||||
const timers = [0, 150, 400, 800, 1500, 2500, 4000].map(delay =>
|
||||
window.setTimeout(() => {
|
||||
const m = managerRef.current;
|
||||
if (!m?.hasMainSeries()) return;
|
||||
const w = wrapperRef.current;
|
||||
if (!w || w.clientHeight < MIN_VALID_LAYOUT_HEIGHT) return;
|
||||
normalizeChartViewport(m);
|
||||
if (delay >= 2500 && m.isMainPriceScaleLikelyCorrupt() && !layoutRecoveryAttemptedRef.current) {
|
||||
layoutRecoveryAttemptedRef.current = true;
|
||||
prevBarsKey.current = '';
|
||||
void reloadAllRef.current(
|
||||
m,
|
||||
barsRef.current,
|
||||
prevChartType.current,
|
||||
prevTheme.current,
|
||||
prevLogScale.current,
|
||||
indicatorsRef.current,
|
||||
);
|
||||
}
|
||||
}, delay),
|
||||
);
|
||||
|
||||
return () => { timers.forEach(clearTimeout); };
|
||||
}, [chartMgr, bars.length, barsMarket, market, chartVisible, normalizeChartViewport]);
|
||||
|
||||
// 데이터는 준비됐으나 메인 시리즈가 없는 빈 차트 자동 복구 (종목 전환·레이아웃 전환 직후)
|
||||
useEffect(() => {
|
||||
if (!chartVisible) return;
|
||||
@@ -1311,7 +1371,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
||||
if (mgr.hasMainSeries()) return;
|
||||
|
||||
const timers = [0, 80, 200, 500].map(delay =>
|
||||
const timers = [0, 80, 200, 500, 1200, 2500].map(delay =>
|
||||
window.setTimeout(() => {
|
||||
const m = managerRef.current;
|
||||
if (!m || m.hasMainSeries()) return;
|
||||
|
||||
Reference in New Issue
Block a user