import { useEffect, useMemo, useRef } from 'react'; import { CandlestickSeries, ColorType, CrosshairMode, HistogramSeries, type IChartApi, type ISeriesApi, LineStyle, LineSeries, createChart, type UTCTimestamp, type CandlestickData, type HistogramData, type LineData, type WhitespaceData, } from 'lightweight-charts'; import type { Candle, SeriesPoint } from '../../lib/api'; import { FibRetracementPrimitive, type FibAnchor, type FibRetracement } from './FibRetracementPrimitive'; type Props = { candles: Candle[]; oracle?: SeriesPoint[]; sma20?: SeriesPoint[]; ema20?: SeriesPoint[]; bb20?: { upper: SeriesPoint[]; lower: SeriesPoint[]; mid: SeriesPoint[] }; showIndicators: boolean; bucketSeconds: number; seriesKey: string; fib?: FibRetracement | null; fibOpacity?: number; fibSelected?: boolean; priceAutoScale?: boolean; onReady?: (api: { chart: IChartApi; candles: ISeriesApi<'Candlestick', UTCTimestamp> }) => void; onChartClick?: (p: FibAnchor & { target: 'chart' | 'fib' }) => void; onChartCrosshairMove?: (p: FibAnchor) => void; onPointerEvent?: (p: { type: 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel'; logical: number; price: number; x: number; y: number; target: 'chart' | 'fib'; event: PointerEvent; }) => { consume?: boolean; capturePointer?: boolean } | void; }; type LinePoint = LineData | WhitespaceData; type BuildSample = { t: number; v: number }; function toTime(t: number): UTCTimestamp { return t as UTCTimestamp; } function resolveBucketSeconds(bucketSeconds: number, candles: Candle[]): number { if (Number.isFinite(bucketSeconds) && bucketSeconds > 0) return bucketSeconds; if (candles.length >= 2) { const last = candles[candles.length - 1]?.time; const prev = candles[candles.length - 2]?.time; const delta = typeof last === 'number' && typeof prev === 'number' ? last - prev : 0; if (Number.isFinite(delta) && delta > 0) return delta; } return 60; } function samplePriceFromCandles(candles: Candle[]): number | null { for (let i = candles.length - 1; i >= 0; i -= 1) { const close = candles[i]?.close; if (typeof close !== 'number') continue; if (!Number.isFinite(close)) continue; if (close === 0) continue; return close; } return null; } function priceFormatForSample(price: number) { const abs = Math.abs(price); if (!Number.isFinite(abs) || abs === 0) return { type: 'price' as const, precision: 2, minMove: 0.01 }; if (abs >= 1000) return { type: 'price' as const, precision: 0, minMove: 1 }; if (abs >= 1) return { type: 'price' as const, precision: 2, minMove: 0.01 }; const exponent = Math.floor(Math.log10(abs)); // negative for abs < 1 const precision = Math.min(8, Math.max(4, -exponent + 3)); return { type: 'price' as const, precision, minMove: Math.pow(10, -precision) }; } function toCandleData(candles: Candle[]): CandlestickData[] { return candles.map((c) => ({ time: toTime(c.time), open: c.open, high: c.high, low: c.low, close: c.close, })); } function toVolumeData(candles: Candle[]): HistogramData[] { return candles.map((c) => { return { time: toTime(c.time), value: c.volume ?? 0, color: 'rgba(148,163,184,0.22)', }; }); } function toLineSeries(points: SeriesPoint[] | undefined): LinePoint[] { if (!points?.length) return []; return points.map((p) => (p.value == null ? ({ time: toTime(p.time) } as WhitespaceData) : { time: toTime(p.time), value: p.value })); } export default function TradingChart({ candles, oracle, sma20, ema20, bb20, showIndicators, bucketSeconds, seriesKey, fib, fibOpacity = 1, fibSelected = false, priceAutoScale = true, onReady, onChartClick, onChartCrosshairMove, onPointerEvent, }: Props) { const containerRef = useRef(null); const chartRef = useRef(null); const fibPrimitiveRef = useRef(null); const fibRef = useRef(fib ?? null); const fibOpacityRef = useRef(fibOpacity); const priceAutoScaleRef = useRef(priceAutoScale); const prevPriceAutoScaleRef = useRef(priceAutoScale); const onReadyRef = useRef(onReady); const onChartClickRef = useRef(onChartClick); const onChartCrosshairMoveRef = useRef(onChartCrosshairMove); const onPointerEventRef = useRef(onPointerEvent); const capturedOverlayPointerRef = useRef(null); const buildSamplesRef = useRef>(new Map()); const buildKeyRef = useRef(null); const lastBuildCandleStartRef = useRef(null); const seriesRef = useRef<{ candles?: ISeriesApi<'Candlestick'>; volume?: ISeriesApi<'Histogram'>; build?: ISeriesApi<'Line'>; oracle?: ISeriesApi<'Line'>; sma20?: ISeriesApi<'Line'>; ema20?: ISeriesApi<'Line'>; bbUpper?: ISeriesApi<'Line'>; bbLower?: ISeriesApi<'Line'>; bbMid?: ISeriesApi<'Line'>; }>({}); const candleData = useMemo(() => toCandleData(candles), [candles]); const volumeData = useMemo(() => toVolumeData(candles), [candles]); const oracleData = useMemo(() => toLineSeries(oracle), [oracle]); const smaData = useMemo(() => toLineSeries(sma20), [sma20]); const emaData = useMemo(() => toLineSeries(ema20), [ema20]); const bbUpper = useMemo(() => toLineSeries(bb20?.upper), [bb20?.upper]); const bbLower = useMemo(() => toLineSeries(bb20?.lower), [bb20?.lower]); const bbMid = useMemo(() => toLineSeries(bb20?.mid), [bb20?.mid]); const priceFormat = useMemo(() => { const sample = samplePriceFromCandles(candles); return sample == null ? { type: 'price' as const, precision: 2, minMove: 0.01 } : priceFormatForSample(sample); }, [candles]); useEffect(() => { onReadyRef.current = onReady; }, [onReady]); useEffect(() => { onChartClickRef.current = onChartClick; }, [onChartClick]); useEffect(() => { onChartCrosshairMoveRef.current = onChartCrosshairMove; }, [onChartCrosshairMove]); useEffect(() => { onPointerEventRef.current = onPointerEvent; }, [onPointerEvent]); useEffect(() => { fibRef.current = fib ?? null; }, [fib]); useEffect(() => { fibOpacityRef.current = fibOpacity; fibPrimitiveRef.current?.setOpacity(fibOpacity); }, [fibOpacity]); useEffect(() => { priceAutoScaleRef.current = priceAutoScale; }, [priceAutoScale]); useEffect(() => { if (!containerRef.current) return; if (chartRef.current) return; const chart = createChart(containerRef.current, { layout: { background: { type: ColorType.Solid, color: 'rgba(0,0,0,0)' }, textColor: '#e6e9ef', fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, sans-serif', }, grid: { vertLines: { color: 'rgba(255,255,255,0.06)' }, horzLines: { color: 'rgba(255,255,255,0.06)' }, }, crosshair: { mode: CrosshairMode.Normal, vertLine: { color: 'rgba(255,255,255,0.18)', style: LineStyle.Dashed }, horzLine: { color: 'rgba(255,255,255,0.18)', style: LineStyle.Dashed }, }, rightPriceScale: { borderColor: 'rgba(255,255,255,0.08)' }, timeScale: { borderColor: 'rgba(255,255,255,0.08)', timeVisible: true, secondsVisible: false }, handleScale: { mouseWheel: true, pinch: true }, handleScroll: { mouseWheel: true, pressedMouseMove: true, horzTouchDrag: true, vertTouchDrag: true }, }); chartRef.current = chart; const candleSeries = chart.addSeries(CandlestickSeries, { upColor: '#22c55e', downColor: '#ef4444', borderVisible: false, wickUpColor: '#22c55e', wickDownColor: '#ef4444', priceFormat, }); const fibPrimitive = new FibRetracementPrimitive(); candleSeries.attachPrimitive(fibPrimitive); fibPrimitiveRef.current = fibPrimitive; fibPrimitive.setFib(fib ?? null); fibPrimitive.setOpacity(fibOpacityRef.current); const volumeSeries = chart.addSeries(HistogramSeries, { priceFormat: { type: 'volume' }, priceScaleId: '', color: 'rgba(255,255,255,0.15)', }); volumeSeries.priceScale().applyOptions({ scaleMargins: { top: 0.88, bottom: 0 }, }); const buildSeries = chart.addSeries(LineSeries, { color: '#60a5fa', lineWidth: 2, priceFormat, priceScaleId: 'build', lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); buildSeries.priceScale().applyOptions({ scaleMargins: { top: 0.72, bottom: 0.12 }, visible: false, borderVisible: false, }); const oracleSeries = chart.addSeries(LineSeries, { color: 'rgba(251,146,60,0.9)', lineWidth: 1, lineStyle: LineStyle.Dotted, priceFormat, }); const smaSeries = chart.addSeries(LineSeries, { color: 'rgba(248,113,113,0.9)', lineWidth: 1, priceFormat }); const emaSeries = chart.addSeries(LineSeries, { color: 'rgba(52,211,153,0.9)', lineWidth: 1, priceFormat }); const bbUpperSeries = chart.addSeries(LineSeries, { color: 'rgba(250,204,21,0.6)', lineWidth: 1, priceFormat }); const bbLowerSeries = chart.addSeries(LineSeries, { color: 'rgba(163,163,163,0.6)', lineWidth: 1, priceFormat }); const bbMidSeries = chart.addSeries(LineSeries, { color: 'rgba(250,204,21,0.35)', lineWidth: 1, lineStyle: LineStyle.Dashed, priceFormat, }); seriesRef.current = { candles: candleSeries, volume: volumeSeries, build: buildSeries, oracle: oracleSeries, sma20: smaSeries, ema20: emaSeries, bbUpper: bbUpperSeries, bbLower: bbLowerSeries, bbMid: bbMidSeries, }; onReadyRef.current?.({ chart, candles: candleSeries as ISeriesApi<'Candlestick', UTCTimestamp> }); const onClick = (param: any) => { if (!param?.point) return; const logical = param.logical ?? chart.timeScale().coordinateToLogical(param.point.x); if (logical == null) return; const price = candleSeries.coordinateToPrice(param.point.y); if (price == null) return; const currentFib = fibRef.current; let target: 'chart' | 'fib' = 'chart'; if (currentFib) { const x1 = chart.timeScale().logicalToCoordinate(currentFib.a.logical as any); const x2 = chart.timeScale().logicalToCoordinate(currentFib.b.logical as any); if (x1 != null && x2 != null) { const tol = 6; const left = Math.min(x1, x2) - tol; const right = Math.max(x1, x2) + tol; const p0 = currentFib.a.price; const delta = currentFib.b.price - p0; const yRatioMin = 0; const yRatioMax = 4.236; const pMin = Math.min(p0 + delta * yRatioMin, p0 + delta * yRatioMax); const pMax = Math.max(p0 + delta * yRatioMin, p0 + delta * yRatioMax); const y1 = candleSeries.priceToCoordinate(pMin); const y2 = candleSeries.priceToCoordinate(pMax); if (y1 != null && y2 != null) { const top = Math.min(y1, y2) - tol; const bottom = Math.max(y1, y2) + tol; if (param.point.x >= left && param.point.x <= right && param.point.y >= top && param.point.y <= bottom) { target = 'fib'; } } } } onChartClickRef.current?.({ logical: Number(logical), price: Number(price), target }); }; chart.subscribeClick(onClick); const onCrosshairMove = (param: any) => { if (!param?.point) return; const logical = param.logical ?? chart.timeScale().coordinateToLogical(param.point.x); if (logical == null) return; const price = candleSeries.coordinateToPrice(param.point.y); if (price == null) return; onChartCrosshairMoveRef.current?.({ logical: Number(logical), price: Number(price) }); }; chart.subscribeCrosshairMove(onCrosshairMove); const container = containerRef.current; const toAnchorFromPoint = (x: number, y: number): FibAnchor | null => { const logical = chart.timeScale().coordinateToLogical(x); if (logical == null) return null; const price = candleSeries.coordinateToPrice(y); if (price == null) return null; return { logical: Number(logical), price: Number(price) }; }; const isOverFib = (x: number, y: number, currentFib: FibRetracement): boolean => { const x1 = chart.timeScale().logicalToCoordinate(currentFib.a.logical as any); const x2 = chart.timeScale().logicalToCoordinate(currentFib.b.logical as any); if (x1 == null || x2 == null) return false; const tol = 6; const left = Math.min(x1, x2) - tol; const right = Math.max(x1, x2) + tol; const p0 = currentFib.a.price; const delta = currentFib.b.price - p0; const yRatioMin = 0; const yRatioMax = 4.236; const pMin = Math.min(p0 + delta * yRatioMin, p0 + delta * yRatioMax); const pMax = Math.max(p0 + delta * yRatioMin, p0 + delta * yRatioMax); const y1 = candleSeries.priceToCoordinate(pMin); const y2 = candleSeries.priceToCoordinate(pMax); if (y1 == null || y2 == null) return false; const top = Math.min(y1, y2) - tol; const bottom = Math.max(y1, y2) + tol; return x >= left && x <= right && y >= top && y <= bottom; }; const onOverlayPointer = (e: PointerEvent) => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const anchor = toAnchorFromPoint(x, y); if (!anchor) return; const currentFib = fibRef.current; const target: 'chart' | 'fib' = currentFib && isOverFib(x, y, currentFib) ? 'fib' : 'chart'; const type = e.type === 'pointerdown' ? ('pointerdown' as const) : e.type === 'pointermove' ? ('pointermove' as const) : e.type === 'pointerup' ? ('pointerup' as const) : ('pointercancel' as const); const decision = onPointerEventRef.current?.({ type, logical: anchor.logical, price: anchor.price, x, y, target, event: e, }); if (decision?.capturePointer) { capturedOverlayPointerRef.current = e.pointerId; try { containerRef.current.setPointerCapture(e.pointerId); } catch { // ignore } } if (decision?.consume) { e.preventDefault(); e.stopImmediatePropagation(); } if (type === 'pointerup' || type === 'pointercancel') { if (capturedOverlayPointerRef.current === e.pointerId) { capturedOverlayPointerRef.current = null; try { containerRef.current.releasePointerCapture(e.pointerId); } catch { // ignore } } } }; container?.addEventListener('pointerdown', onOverlayPointer, { capture: true }); container?.addEventListener('pointermove', onOverlayPointer, { capture: true }); container?.addEventListener('pointerup', onOverlayPointer, { capture: true }); container?.addEventListener('pointercancel', onOverlayPointer, { capture: true }); const onWheel = (e: WheelEvent) => { if (!e.ctrlKey) return; if (!containerRef.current) return; e.preventDefault(); e.stopPropagation(); if (priceAutoScaleRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const y = e.clientY - rect.top; const pivotPrice = candleSeries.coordinateToPrice(y); if (pivotPrice == null) return; const ps = candleSeries.priceScale(); const range = ps.getVisibleRange(); let low: number; let high: number; if (range) { low = Math.min(range.from, range.to); high = Math.max(range.from, range.to); } else { const top = candleSeries.coordinateToPrice(0); const bottom = candleSeries.coordinateToPrice(rect.height); if (top == null || bottom == null) return; low = Math.min(top, bottom); high = Math.max(top, bottom); } const span = high - low; if (!(span > 0)) return; const zoomFactor = Math.exp(e.deltaY * 0.002); if (!Number.isFinite(zoomFactor) || zoomFactor <= 0) return; const t = Math.min(1, Math.max(0, (pivotPrice - low) / span)); const minSpan = span * 1e-6; const maxSpan = span * 1e6; const nextSpan = Math.min(maxSpan, Math.max(minSpan, span * zoomFactor)); const nextLow = pivotPrice - t * nextSpan; const nextHigh = nextLow + nextSpan; ps.setAutoScale(false); ps.setVisibleRange({ from: nextLow, to: nextHigh }); }; container?.addEventListener('wheel', onWheel, { passive: false, capture: true }); let pan: { pointerId: number; startPrice: number; startRange: { from: number; to: number } } | null = null; const onPointerDown = (e: PointerEvent) => { if (e.button !== 0) return; if (!e.ctrlKey) return; if (priceAutoScaleRef.current) return; if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const y = e.clientY - rect.top; const startPrice = candleSeries.coordinateToPrice(y); if (startPrice == null) return; const ps = candleSeries.priceScale(); const range = ps.getVisibleRange(); let from: number; let to: number; if (range) { from = range.from; to = range.to; } else { const top = candleSeries.coordinateToPrice(0); const bottom = candleSeries.coordinateToPrice(rect.height); if (top == null || bottom == null) return; from = Math.min(top, bottom); to = Math.max(top, bottom); } if (!(to > from)) return; pan = { pointerId: e.pointerId, startPrice, startRange: { from, to } }; try { containerRef.current.setPointerCapture(e.pointerId); } catch { // ignore } e.preventDefault(); e.stopPropagation(); }; const onPointerMove = (e: PointerEvent) => { if (!pan) return; if (pan.pointerId !== e.pointerId) return; if (!containerRef.current) return; if (!e.ctrlKey || priceAutoScaleRef.current) { pan = null; return; } const rect = containerRef.current.getBoundingClientRect(); const y = e.clientY - rect.top; const currentPrice = candleSeries.coordinateToPrice(y); if (currentPrice == null) return; const delta = pan.startPrice - currentPrice; const ps = candleSeries.priceScale(); ps.setAutoScale(false); ps.setVisibleRange({ from: pan.startRange.from + delta, to: pan.startRange.to + delta }); e.preventDefault(); e.stopPropagation(); }; const stopPan = (e: PointerEvent) => { if (!pan) return; if (pan.pointerId !== e.pointerId) return; pan = null; try { containerRef.current?.releasePointerCapture(e.pointerId); } catch { // ignore } }; container?.addEventListener('pointerdown', onPointerDown, { capture: true }); container?.addEventListener('pointermove', onPointerMove, { capture: true }); container?.addEventListener('pointerup', stopPan, { capture: true }); container?.addEventListener('pointercancel', stopPan, { capture: true }); const ro = new ResizeObserver(() => { if (!containerRef.current) return; const { width, height } = containerRef.current.getBoundingClientRect(); chart.applyOptions({ width: Math.floor(width), height: Math.floor(height) }); }); ro.observe(containerRef.current); return () => { chart.unsubscribeClick(onClick); chart.unsubscribeCrosshairMove(onCrosshairMove); container?.removeEventListener('pointerdown', onOverlayPointer, { capture: true }); container?.removeEventListener('pointermove', onOverlayPointer, { capture: true }); container?.removeEventListener('pointerup', onOverlayPointer, { capture: true }); container?.removeEventListener('pointercancel', onOverlayPointer, { capture: true }); container?.removeEventListener('wheel', onWheel, { capture: true }); container?.removeEventListener('pointerdown', onPointerDown, { capture: true }); container?.removeEventListener('pointermove', onPointerMove, { capture: true }); container?.removeEventListener('pointerup', stopPan, { capture: true }); container?.removeEventListener('pointercancel', stopPan, { capture: true }); ro.disconnect(); if (fibPrimitiveRef.current) { candleSeries.detachPrimitive(fibPrimitiveRef.current); fibPrimitiveRef.current = null; } chart.remove(); chartRef.current = null; seriesRef.current = {}; }; }, []); useEffect(() => { const s = seriesRef.current; if (!s.candles || !s.volume || !s.build) return; s.candles.setData(candleData); s.volume.setData(volumeData); s.oracle?.setData(oracleData); s.sma20?.setData(smaData); s.ema20?.setData(emaData); s.bbUpper?.setData(bbUpper); s.bbLower?.setData(bbLower); s.bbMid?.setData(bbMid); if (buildKeyRef.current !== seriesKey) { buildSamplesRef.current.clear(); buildKeyRef.current = seriesKey; lastBuildCandleStartRef.current = null; } const bs = resolveBucketSeconds(bucketSeconds, candles); const eps = 1e-3; const maxPointsPerCandle = 600; const minStep = Math.max(0.5, bs / maxPointsPerCandle); const map = buildSamplesRef.current; const visibleStarts = new Set(candles.map((c) => c.time)); for (const start of Array.from(map.keys())) { if (!visibleStarts.has(start)) map.delete(start); } const last = candles[candles.length - 1]; if (last) { const prevStart = lastBuildCandleStartRef.current; if (prevStart != null && prevStart !== last.time) { const prevCandle = candles.find((c) => c.time === prevStart); if (prevCandle) { const endT = prevStart + bs - eps; const finalDelta = prevCandle.close - prevCandle.open; const list = map.get(prevStart) ?? []; const lastT = list.length ? list[list.length - 1]!.t : -Infinity; if (endT > lastT + eps) { list.push({ t: endT, v: finalDelta }); } else if (list.length) { list[list.length - 1] = { t: lastT, v: finalDelta }; } map.set(prevStart, list); } } const start = last.time; const endT = start + bs - eps; const delta = last.close - last.open; const nowT = Date.now() / 1000; const tClamped = Math.min(endT, Math.max(start + eps, nowT)); const list = map.get(start) ?? []; if (list.length) { const lastPt = list[list.length - 1]!; if (tClamped - lastPt.t < minStep) { list[list.length - 1] = { t: tClamped, v: delta }; } else { const t = Math.min(endT, Math.max(lastPt.t + eps, tClamped)); if (t > lastPt.t) list.push({ t, v: delta }); } } else { list.push({ t: tClamped, v: delta }); } map.set(start, list); lastBuildCandleStartRef.current = start; } const buildRaw: LinePoint[] = []; for (const c of candles) { const list = map.get(c.time); if (!list?.length) continue; const startT = c.time + eps; const endT = c.time + bs - eps; if (!(endT > startT)) continue; buildRaw.push({ time: toTime(c.time) } as WhitespaceData); buildRaw.push({ time: toTime(startT), value: 0 } as LineData); let lastT = startT; for (const p of list) { let t = p.t; if (t <= lastT + eps) t = lastT + eps; if (t >= endT) break; buildRaw.push({ time: toTime(t), value: p.v } as LineData); lastT = t; } const finalDelta = c.close - c.open; if (endT > lastT + eps) { buildRaw.push({ time: toTime(endT), value: finalDelta } as LineData); } else if (buildRaw.length) { const prev = buildRaw[buildRaw.length - 1]; if ('value' in prev) { buildRaw[buildRaw.length - 1] = { ...prev, value: finalDelta } as LineData; } } } const buildColored: LinePoint[] = []; let lastLineIdx: number | null = null; let lastLineVal: number | null = null; for (const p of buildRaw) { if (!('value' in p)) { buildColored.push(p); lastLineIdx = null; lastLineVal = null; continue; } if (lastLineIdx != null && lastLineVal != null) { const delta = p.value - lastLineVal; const color = delta > 0 ? '#22c55e' : delta < 0 ? '#ef4444' : '#60a5fa'; const prev = buildColored[lastLineIdx] as LineData; buildColored[lastLineIdx] = { ...prev, color }; } buildColored.push({ time: p.time, value: p.value } as LineData); lastLineIdx = buildColored.length - 1; lastLineVal = p.value; } s.build.setData(buildColored); s.sma20?.applyOptions({ visible: showIndicators }); s.ema20?.applyOptions({ visible: showIndicators }); s.bbUpper?.applyOptions({ visible: showIndicators }); s.bbLower?.applyOptions({ visible: showIndicators }); s.bbMid?.applyOptions({ visible: showIndicators }); }, [candleData, volumeData, oracleData, smaData, emaData, bbUpper, bbLower, bbMid, showIndicators, candles, bucketSeconds, seriesKey]); useEffect(() => { const s = seriesRef.current; if (!s.candles) return; s.candles.applyOptions({ priceFormat }); s.build?.applyOptions({ priceFormat }); s.oracle?.applyOptions({ priceFormat }); s.sma20?.applyOptions({ priceFormat }); s.ema20?.applyOptions({ priceFormat }); s.bbUpper?.applyOptions({ priceFormat }); s.bbLower?.applyOptions({ priceFormat }); s.bbMid?.applyOptions({ priceFormat }); }, [priceFormat]); useEffect(() => { fibPrimitiveRef.current?.setFib(fib ?? null); }, [fib]); useEffect(() => { fibPrimitiveRef.current?.setSelected(Boolean(fibSelected)); }, [fibSelected]); useEffect(() => { const candlesSeries = seriesRef.current.candles; if (!candlesSeries) return; const ps = candlesSeries.priceScale(); const prev = prevPriceAutoScaleRef.current; prevPriceAutoScaleRef.current = priceAutoScale; if (priceAutoScale) { ps.setAutoScale(true); return; } ps.setAutoScale(false); if (!prev) return; if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const top = candlesSeries.coordinateToPrice(0); const bottom = candlesSeries.coordinateToPrice(rect.height); if (top == null || bottom == null) return; const from = Math.min(top, bottom); const to = Math.max(top, bottom); if (!(to > from)) return; ps.setVisibleRange({ from, to }); }, [priceAutoScale]); return
; }