실시간 차트 보조지표 탭 문제 수정
This commit is contained in:
@@ -33,42 +33,6 @@ interface PaneItem {
|
||||
|
||||
const MIN_SUB_PANE_HEIGHT = 12;
|
||||
|
||||
interface DomPaneLayout {
|
||||
topY: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* LWC 테이블 DOM 에서 보조지표 pane 행만 읽음 (pane index 무관).
|
||||
* orphan 빈 pane 행이 있으면 제외하고 하단 expectCount 개만 사용.
|
||||
*/
|
||||
function getDomSubIndicatorLayouts(
|
||||
containerEl: HTMLElement,
|
||||
expectCount: number,
|
||||
): DomPaneLayout[] {
|
||||
const containerRect = containerEl.getBoundingClientRect();
|
||||
const rows = Array.from(containerEl.querySelectorAll('tr'))
|
||||
.map(tr => tr.getBoundingClientRect())
|
||||
.filter(rect => rect.height > MIN_SUB_PANE_HEIGHT)
|
||||
.sort((a, b) => a.top - b.top);
|
||||
|
||||
// pane 0=캔들(첫 행). 둘째 행이 낮으면 거래량 pane → 보조지표는 그 다음부터
|
||||
let startIdx = 1;
|
||||
if (rows.length > 1 && rows[1].height < Math.min(100, rows[0].height * 0.35)) {
|
||||
startIdx = 2;
|
||||
}
|
||||
|
||||
let subs = rows.slice(startIdx).map(rect => ({
|
||||
topY: Math.round(rect.top - containerRect.top),
|
||||
height: Math.round(rect.height),
|
||||
}));
|
||||
|
||||
if (expectCount > 0 && subs.length > expectCount) {
|
||||
subs = subs.slice(subs.length - expectCount);
|
||||
}
|
||||
return subs;
|
||||
}
|
||||
|
||||
interface PaneCapture {
|
||||
dataUrl: string;
|
||||
cssWidth: number;
|
||||
@@ -247,88 +211,80 @@ function fillMissingLayoutsByPaneIndex(
|
||||
}
|
||||
}
|
||||
|
||||
/** DOM 행 중 paneIndex 레이아웃 topY 와 가장 가까운 행 선택 */
|
||||
function fillMissingLayoutsFromDom(
|
||||
/**
|
||||
* 실제 시리즈가 붙은 sub-pane 만으로 좌표 보강 (orphan 빈 pane 제외).
|
||||
* paneIndex 미등록(-1)·탭 일괄 교체 직후에도 레이블 위치를 맞춘다.
|
||||
*/
|
||||
function fillMissingLayoutsFromActiveSubPanes(
|
||||
items: PaneItem[],
|
||||
containerEl: HTMLElement,
|
||||
paneLayouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
||||
subLayouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
||||
): void {
|
||||
const missing = items.filter(i => i.layoutTopY == null && i.paneIndex >= 2);
|
||||
if (subLayouts.length === 0) return;
|
||||
|
||||
const missing = items.filter(i => i.layoutTopY == null);
|
||||
if (missing.length === 0) return;
|
||||
|
||||
const domSubs = getDomSubIndicatorLayouts(containerEl, missing.length);
|
||||
if (domSubs.length === 0) return;
|
||||
|
||||
const usedDom = new Set<number>();
|
||||
const usedPane = new Set<number>();
|
||||
const sorted = [...missing].sort((a, b) => a.paneIndex - b.paneIndex);
|
||||
|
||||
for (const item of sorted) {
|
||||
const ref = layoutForPaneIndex(item.paneIndex, paneLayouts);
|
||||
let bestIdx = -1;
|
||||
let bestDist = Infinity;
|
||||
for (let i = 0; i < domSubs.length; i++) {
|
||||
if (usedDom.has(i)) continue;
|
||||
const dist = ref
|
||||
? Math.abs(domSubs[i].topY - ref.topY)
|
||||
: domSubs[i].topY;
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
if (bestIdx < 0) continue;
|
||||
if (ref && bestDist > 96) continue;
|
||||
usedDom.add(bestIdx);
|
||||
applyPaneLayoutToItem(item, domSubs[bestIdx]);
|
||||
if (item.paneIndex < 2) continue;
|
||||
const lay = subLayouts.find(l => l.paneIndex === item.paneIndex && !usedPane.has(l.paneIndex));
|
||||
if (!lay) continue;
|
||||
applyPaneLayoutToItem(item, lay);
|
||||
usedPane.add(lay.paneIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function buildPaneItems(
|
||||
manager: ChartManager,
|
||||
containerEl: HTMLElement | null,
|
||||
indicators: IndicatorConfig[],
|
||||
optPaneMap?: Map<string, number>,
|
||||
): PaneItem[] {
|
||||
const paneMap = new Map(optPaneMap ?? []);
|
||||
for (const info of manager.getIndicatorPaneInfo()) {
|
||||
paneMap.set(info.id, info.paneIndex);
|
||||
}
|
||||
const wantedIds = new Set(
|
||||
indicators
|
||||
.filter(ind => {
|
||||
const def = getIndicatorDef(ind.type);
|
||||
return def && !def.overlay && !def.returnsMarkers;
|
||||
})
|
||||
.map(ind => ind.id),
|
||||
);
|
||||
|
||||
// ChartManager 에 실제 로드된 지표만 — React state 만 있고 pane 미부착인 항목 제외
|
||||
const items: PaneItem[] = [];
|
||||
for (const ind of indicators) {
|
||||
for (const info of manager.getIndicatorPaneInfo()) {
|
||||
if (!wantedIds.has(info.id)) continue;
|
||||
const ind = info.config;
|
||||
const def = getIndicatorDef(ind.type);
|
||||
if (!def || def.overlay || def.returnsMarkers) continue;
|
||||
const plotColors = (ind.plots ?? def.plots ?? []).map(p => (p.color ?? '#aaa').slice(0, 7));
|
||||
|
||||
let chartPane = paneMap.get(ind.id) ?? -1;
|
||||
let chartPane = info.paneIndex;
|
||||
if (ind.mergedWith) {
|
||||
const hostPane = paneMap.get(ind.mergedWith);
|
||||
if (hostPane != null && hostPane >= 2) chartPane = hostPane;
|
||||
const host = manager.getIndicatorPaneIndex(ind.mergedWith);
|
||||
if (host >= 2) chartPane = host;
|
||||
}
|
||||
|
||||
items.push({
|
||||
id: ind.id,
|
||||
id: info.id,
|
||||
paneIndex: chartPane,
|
||||
label: makeLabel(ind),
|
||||
plotColors,
|
||||
plotColors: info.plotColors.map(c => c.slice(0, 7)),
|
||||
hidden: ind.hidden === true,
|
||||
});
|
||||
}
|
||||
|
||||
if (items.length === 0) return [];
|
||||
|
||||
items.sort((a, b) => a.paneIndex - b.paneIndex);
|
||||
|
||||
const subLayouts = manager.getIndicatorSubPaneLayouts();
|
||||
|
||||
for (const item of items) {
|
||||
const lay = manager.getIndicatorPaneScreenLayout(item.id);
|
||||
if (lay) applyPaneLayoutToItem(item, lay, true);
|
||||
}
|
||||
|
||||
const paneLayouts = manager.getPaneLayouts();
|
||||
fillMissingLayoutsByPaneIndex(items, paneLayouts);
|
||||
fillMissingLayoutsByPaneIndex(items, manager.getIndicatorSubPaneLayouts());
|
||||
|
||||
if (containerEl?.isConnected) {
|
||||
fillMissingLayoutsFromDom(items, containerEl, paneLayouts);
|
||||
}
|
||||
fillMissingLayoutsByPaneIndex(items, subLayouts);
|
||||
fillMissingLayoutsFromActiveSubPanes(items, subLayouts);
|
||||
|
||||
items.sort((a, b) => {
|
||||
const ay = a.layoutTopY ?? 1e9;
|
||||
@@ -353,6 +309,17 @@ function paneItemLayout(
|
||||
return l ? { topY: l.topY, height: l.height } : null;
|
||||
}
|
||||
|
||||
/** 렌더 시점 pane 좌표 — resetPaneHeights 후 캐시 topY 가 어긋나지 않도록 시리즈 DOM 우선 */
|
||||
function resolvePaneItemLayout(
|
||||
manager: ChartManager,
|
||||
item: PaneItem,
|
||||
layouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
||||
): { topY: number; height: number } | null {
|
||||
const live = manager.getIndicatorPaneScreenLayout(item.id);
|
||||
if (live) return live;
|
||||
return paneItemLayout(item, layouts);
|
||||
}
|
||||
|
||||
// ── 아이콘 SVG ────────────────────────────────────────────────────────────────
|
||||
/** ⤡ 전체화면 확장 — 네 꼭지점 바깥 방향 화살표 */
|
||||
const IconExpand = () => (
|
||||
@@ -422,6 +389,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
}) => {
|
||||
const [paneItems, setPaneItems] = useState<PaneItem[]>([]);
|
||||
const [paneLayouts, setPaneLayouts] = useState<Array<{ paneIndex: number; topY: number; height: number }>>([]);
|
||||
const [layoutEpoch, setLayoutEpoch] = useState(0);
|
||||
const [values, setValues] = useState<Record<string, number[]>>({});
|
||||
const [dragState, setDragState] = useState<DragState | null>(null);
|
||||
const retryRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||
@@ -451,11 +419,10 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
useEffect(() => { indicatorsRef.current = indicators; }, [indicators]);
|
||||
|
||||
const syncPaneItems = useCallback(() => {
|
||||
const el = containerElRef.current;
|
||||
if (!el?.isConnected) return;
|
||||
const map = manager.getIndicatorPaneMapping();
|
||||
if (!containerElRef.current?.isConnected) return;
|
||||
setPaneLayouts(manager.getPaneLayouts());
|
||||
setPaneItems(buildPaneItems(manager, el, indicators, map));
|
||||
setPaneItems(buildPaneItems(manager, indicators));
|
||||
setLayoutEpoch(e => e + 1);
|
||||
}, [indicators, manager]);
|
||||
|
||||
const refreshLayouts = useCallback(() => {
|
||||
@@ -486,7 +453,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
setPaneItems([]);
|
||||
scheduleRetry();
|
||||
return () => retryRef.current.forEach(clearTimeout);
|
||||
}, [scheduleRetry]);
|
||||
}, [scheduleRetry, indicators]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = manager.subscribePaneLayout(() => scheduleRetry());
|
||||
@@ -505,13 +472,6 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = manager.subscribeCrosshair((p: MouseEventParams<Time>) => {
|
||||
const map = manager.getIndicatorPaneMapping();
|
||||
const layouts = manager.getPaneLayouts();
|
||||
setPaneLayouts(layouts);
|
||||
const el = containerElRef.current;
|
||||
if (el?.isConnected) {
|
||||
setPaneItems(buildPaneItems(manager, el, indicatorsRef.current, map));
|
||||
}
|
||||
if (!p.time) { setValues({}); return; }
|
||||
setValues(manager.getIndicatorValuesByIdFromParams(p));
|
||||
});
|
||||
@@ -525,16 +485,16 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
if (!items.length) return null;
|
||||
const rect = containerElRef.current.getBoundingClientRect();
|
||||
if (idx <= 0) {
|
||||
const pl = paneItemLayout(items[0], layouts);
|
||||
const pl = resolvePaneItemLayout(manager, items[0], layouts);
|
||||
return pl ? rect.top + pl.topY : null;
|
||||
}
|
||||
if (idx >= items.length) {
|
||||
const pl = paneItemLayout(items[items.length - 1], layouts);
|
||||
const pl = resolvePaneItemLayout(manager, items[items.length - 1], layouts);
|
||||
return pl ? rect.top + pl.topY + pl.height : null;
|
||||
}
|
||||
const pl = paneItemLayout(items[idx - 1], layouts);
|
||||
const pl = resolvePaneItemLayout(manager, items[idx - 1], layouts);
|
||||
return pl ? rect.top + pl.topY + pl.height : null;
|
||||
}, []);
|
||||
}, [manager]);
|
||||
|
||||
const unbindPaneDragRef = useRef<(() => void) | null>(null);
|
||||
|
||||
@@ -553,7 +513,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
const fromIndex = paneItemsRef.current.findIndex(i => i.id === id);
|
||||
if (!item || fromIndex === -1) return;
|
||||
|
||||
const layout = paneItemLayout(item, paneLayoutsRef.current);
|
||||
const layout = resolvePaneItemLayout(manager, item, paneLayoutsRef.current);
|
||||
const paneCapture = layout
|
||||
? capturePaneRegion(containerElRef.current, layout.topY, layout.height)
|
||||
: null;
|
||||
@@ -628,7 +588,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
|
||||
for (const item of items) {
|
||||
if (getPaneHostId(item.id, inds) === dragHost) continue;
|
||||
const l = paneItemLayout(item, layouts);
|
||||
const l = resolvePaneItemLayout(manager, item, layouts);
|
||||
if (!l || mouseY < l.topY || mouseY >= l.topY + l.height) continue;
|
||||
const rel = (mouseY - l.topY) / Math.max(l.height, 1);
|
||||
if (rel > 0.28 && rel < 0.72) {
|
||||
@@ -640,7 +600,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
|
||||
if (dropMode === 'reorder') {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const l = paneItemLayout(items[i], layouts);
|
||||
const l = resolvePaneItemLayout(manager, items[i], layouts);
|
||||
if (l && mouseY < l.topY + l.height / 2) {
|
||||
drop = i;
|
||||
break;
|
||||
@@ -674,6 +634,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
? getDropLineY(dragState!.dropBeforeIndex)
|
||||
: null;
|
||||
const containerRect = containerEl.getBoundingClientRect();
|
||||
void layoutEpoch;
|
||||
|
||||
const draggingHostId = isDragging
|
||||
? getPaneHostId(dragState!.draggingId, indicators)
|
||||
@@ -684,20 +645,20 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
: -1;
|
||||
|
||||
const draggingLayout = draggingItemIdx >= 0
|
||||
? paneItemLayout(paneItems[draggingItemIdx], paneLayouts)
|
||||
? resolvePaneItemLayout(manager, paneItems[draggingItemIdx], paneLayouts)
|
||||
: null;
|
||||
|
||||
const mergeTargetLayout = isDragging && dragState?.mergeTargetId
|
||||
? (() => {
|
||||
const t = paneItems.find(i => i.id === dragState.mergeTargetId);
|
||||
return t ? paneItemLayout(t, paneLayouts) : null;
|
||||
return t ? resolvePaneItemLayout(manager, t, paneLayouts) : null;
|
||||
})()
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{paneItems.map((item, itemIdx) => {
|
||||
const pl = paneItemLayout(item, paneLayouts);
|
||||
const pl = resolvePaneItemLayout(manager, item, paneLayouts);
|
||||
if (!pl) return null;
|
||||
|
||||
const screenX = containerRect.left;
|
||||
@@ -918,8 +879,8 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
<>
|
||||
{/* ① 전체 지표 영역 dim — 고스트·shift 가 더 선명하게 보이도록 */}
|
||||
{paneItems.length > 0 && (() => {
|
||||
const firstL = paneItemLayout(paneItems[0], paneLayouts);
|
||||
const lastL = paneItemLayout(paneItems[paneItems.length - 1], paneLayouts);
|
||||
const firstL = resolvePaneItemLayout(manager, paneItems[0], paneLayouts);
|
||||
const lastL = resolvePaneItemLayout(manager, paneItems[paneItems.length - 1], paneLayouts);
|
||||
if (!firstL || !lastL) return null;
|
||||
return (
|
||||
<div style={{
|
||||
|
||||
Reference in New Issue
Block a user