실시간 차트 보조지표명 겹침오류 수정
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
* PaneLegend
|
||||
* 각 서브 pane 좌측 상단에 인디케이터 명칭 + 현재 크로스헤어 값 표시.
|
||||
* 드래그 핸들(⠿) / 전체화면 / 닫기 버튼 포함.
|
||||
* 레이아웃 갱신 시 top transition 을 쓰지 않음 — 설정 변경 직후 라벨 이중 표시 방지.
|
||||
*
|
||||
* ─── 패인 인덱스 구조 ───────────────────────────────────────────────────────
|
||||
* Pane 0 : 메인 캔들차트
|
||||
@@ -10,6 +11,7 @@
|
||||
* ────────────────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { MouseEventParams, Time } from 'lightweight-charts';
|
||||
import type { IndicatorConfig } from '../types';
|
||||
import { ChartManager } from '../utils/ChartManager';
|
||||
@@ -56,6 +58,8 @@ export interface PaneLegendProps {
|
||||
manager: ChartManager;
|
||||
indicators: IndicatorConfig[];
|
||||
containerEl: HTMLElement;
|
||||
/** 지표 파라미터 동기화 중 — 잘못된 좌표로 라벨이 겹쳐 보이는 것 방지 */
|
||||
paused?: boolean;
|
||||
onHoverName: (id: string, sx: number, sy: number) => void;
|
||||
onLeaveName: () => void;
|
||||
onDoubleClick?: (id: string) => void;
|
||||
@@ -179,16 +183,6 @@ function makeLabel(config: IndicatorConfig): string {
|
||||
return nums.length ? `${name} ${nums.join(', ')}` : name;
|
||||
}
|
||||
|
||||
/** paneIndex ↔ LWC pane DOM 위치 (배열 순서·orphan pane 과 무관) */
|
||||
function layoutForPaneIndex(
|
||||
paneIndex: number,
|
||||
layouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
||||
): { topY: number; height: number } | null {
|
||||
if (paneIndex < 2) return null;
|
||||
const lay = layouts.find(l => l.paneIndex === paneIndex && l.height > MIN_SUB_PANE_HEIGHT);
|
||||
return lay ? { topY: lay.topY, height: lay.height } : null;
|
||||
}
|
||||
|
||||
function applyPaneLayoutToItem(
|
||||
item: PaneItem,
|
||||
layout: { topY: number; height: number },
|
||||
@@ -199,43 +193,6 @@ function applyPaneLayoutToItem(
|
||||
item.layoutHeight = layout.height;
|
||||
}
|
||||
|
||||
/** paneIndex 기준으로만 좌표 보강 (지표 배열 순서와 화면 pane 순서가 달라도 안전) */
|
||||
function fillMissingLayoutsByPaneIndex(
|
||||
items: PaneItem[],
|
||||
layouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
||||
): void {
|
||||
for (const item of items) {
|
||||
if (item.layoutTopY != null || item.paneIndex < 2) continue;
|
||||
const lay = layoutForPaneIndex(item.paneIndex, layouts);
|
||||
if (lay) applyPaneLayoutToItem(item, lay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제 시리즈가 붙은 sub-pane 만으로 좌표 보강 (orphan 빈 pane 제외).
|
||||
* paneIndex 미등록(-1)·탭 일괄 교체 직후에도 레이블 위치를 맞춘다.
|
||||
*/
|
||||
function fillMissingLayoutsFromActiveSubPanes(
|
||||
items: PaneItem[],
|
||||
subLayouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
||||
): void {
|
||||
if (subLayouts.length === 0) return;
|
||||
|
||||
const missing = items.filter(i => i.layoutTopY == null);
|
||||
if (missing.length === 0) return;
|
||||
|
||||
const usedPane = new Set<number>();
|
||||
const sorted = [...missing].sort((a, b) => a.paneIndex - b.paneIndex);
|
||||
|
||||
for (const item of sorted) {
|
||||
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,
|
||||
indicators: IndicatorConfig[],
|
||||
@@ -249,11 +206,15 @@ function buildPaneItems(
|
||||
.map(ind => ind.id),
|
||||
);
|
||||
|
||||
const configById = new Map(indicators.map(ind => [ind.id, ind]));
|
||||
|
||||
// ChartManager 에 실제 로드된 지표만 — React state 만 있고 pane 미부착인 항목 제외
|
||||
const items: PaneItem[] = [];
|
||||
const seenIds = new Set<string>();
|
||||
for (const info of manager.getIndicatorPaneInfo()) {
|
||||
if (!wantedIds.has(info.id)) continue;
|
||||
const ind = info.config;
|
||||
if (!wantedIds.has(info.id) || seenIds.has(info.id)) continue;
|
||||
seenIds.add(info.id);
|
||||
const ind = configById.get(info.id) ?? info.config;
|
||||
const def = getIndicatorDef(ind.type);
|
||||
if (!def || def.overlay || def.returnsMarkers) continue;
|
||||
|
||||
@@ -276,16 +237,11 @@ function buildPaneItems(
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
fillMissingLayoutsByPaneIndex(items, subLayouts);
|
||||
fillMissingLayoutsFromActiveSubPanes(items, subLayouts);
|
||||
|
||||
items.sort((a, b) => {
|
||||
const ay = a.layoutTopY ?? 1e9;
|
||||
const by = b.layoutTopY ?? 1e9;
|
||||
@@ -293,31 +249,17 @@ function buildPaneItems(
|
||||
return a.paneIndex - b.paneIndex;
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function paneItemLayout(
|
||||
item: PaneItem,
|
||||
layouts: Array<{ paneIndex: number; topY: number; height: number }>,
|
||||
): { topY: number; height: number } | null {
|
||||
if (item.layoutTopY != null && item.layoutHeight != null) {
|
||||
return { topY: item.layoutTopY, height: item.layoutHeight };
|
||||
}
|
||||
const l = layouts.find(
|
||||
x => x.paneIndex === item.paneIndex && x.height > MIN_SUB_PANE_HEIGHT,
|
||||
return items.filter(
|
||||
item => item.layoutTopY != null && (item.layoutHeight ?? 0) > MIN_SUB_PANE_HEIGHT,
|
||||
);
|
||||
return l ? { topY: l.topY, height: l.height } : null;
|
||||
}
|
||||
|
||||
/** 렌더 시점 pane 좌표 — resetPaneHeights 후 캐시 topY 가 어긋나지 않도록 시리즈 DOM 우선 */
|
||||
/** 렌더 시점 pane 좌표 — 시리즈 DOM 기준만 사용 (stale paneIndex 폴백 제거) */
|
||||
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);
|
||||
return manager.getIndicatorPaneScreenLayout(item.id);
|
||||
}
|
||||
|
||||
// ── 아이콘 SVG ────────────────────────────────────────────────────────────────
|
||||
@@ -383,7 +325,7 @@ const IconClose = () => (
|
||||
|
||||
// ── 컴포넌트 ──────────────────────────────────────────────────────────────────
|
||||
const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
manager, indicators, containerEl,
|
||||
manager, indicators, containerEl, paused = false,
|
||||
onHoverName, onLeaveName, onDoubleClick,
|
||||
onReorder, onMerge, onSplit, onExpand, onRemove, onDuplicate, focusedId, onRestore,
|
||||
}) => {
|
||||
@@ -408,6 +350,20 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
const onSplitRef = useRef(onSplit);
|
||||
const onDuplicateRef = useRef(onDuplicate);
|
||||
const indicatorsRef = useRef(indicators);
|
||||
const [portalHost, setPortalHost] = useState<HTMLElement | null>(null);
|
||||
|
||||
/** React portal 전용 호스트 — chart-container 직접 조작·removeChild 경합 방지 */
|
||||
useEffect(() => {
|
||||
const sel = ':scope > .pane-legend-portal-host';
|
||||
let host = containerEl.querySelector<HTMLElement>(sel);
|
||||
if (!host) {
|
||||
host = document.createElement('div');
|
||||
host.className = 'pane-legend-portal-host';
|
||||
containerEl.appendChild(host);
|
||||
}
|
||||
setPortalHost(host);
|
||||
return () => { setPortalHost(null); };
|
||||
}, [containerEl]);
|
||||
|
||||
useEffect(() => { paneItemsRef.current = paneItems; }, [paneItems]);
|
||||
useEffect(() => { paneLayoutsRef.current = paneLayouts; }, [paneLayouts]);
|
||||
@@ -420,8 +376,9 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
|
||||
const syncPaneItems = useCallback(() => {
|
||||
if (!containerElRef.current?.isConnected) return;
|
||||
const nextItems = buildPaneItems(manager, indicators);
|
||||
setPaneLayouts(manager.getPaneLayouts());
|
||||
setPaneItems(buildPaneItems(manager, indicators));
|
||||
setPaneItems(nextItems);
|
||||
setLayoutEpoch(e => e + 1);
|
||||
}, [indicators, manager]);
|
||||
|
||||
@@ -434,15 +391,9 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
retryRef.current.forEach(clearTimeout);
|
||||
retryRef.current = [
|
||||
setTimeout(syncPaneItems, 0),
|
||||
setTimeout(syncPaneItems, 16),
|
||||
setTimeout(syncPaneItems, 50),
|
||||
setTimeout(syncPaneItems, 120),
|
||||
setTimeout(syncPaneItems, 250),
|
||||
setTimeout(syncPaneItems, 200),
|
||||
setTimeout(syncPaneItems, 500),
|
||||
setTimeout(syncPaneItems, 900),
|
||||
setTimeout(syncPaneItems, 1200),
|
||||
setTimeout(syncPaneItems, 1800),
|
||||
setTimeout(syncPaneItems, 2500),
|
||||
];
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(syncPaneItems);
|
||||
@@ -450,10 +401,14 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
}, [syncPaneItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (paused) {
|
||||
setPaneItems([]);
|
||||
return;
|
||||
}
|
||||
setPaneItems([]);
|
||||
scheduleRetry();
|
||||
return () => retryRef.current.forEach(clearTimeout);
|
||||
}, [scheduleRetry, indicators]);
|
||||
}, [scheduleRetry, indicators, paused]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = manager.subscribePaneLayout(() => scheduleRetry());
|
||||
@@ -485,14 +440,14 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
if (!items.length) return null;
|
||||
const rect = containerElRef.current.getBoundingClientRect();
|
||||
if (idx <= 0) {
|
||||
const pl = resolvePaneItemLayout(manager, items[0], layouts);
|
||||
const pl = resolvePaneItemLayout(manager, items[0]);
|
||||
return pl ? rect.top + pl.topY : null;
|
||||
}
|
||||
if (idx >= items.length) {
|
||||
const pl = resolvePaneItemLayout(manager, items[items.length - 1], layouts);
|
||||
const pl = resolvePaneItemLayout(manager, items[items.length - 1]);
|
||||
return pl ? rect.top + pl.topY + pl.height : null;
|
||||
}
|
||||
const pl = resolvePaneItemLayout(manager, items[idx - 1], layouts);
|
||||
const pl = resolvePaneItemLayout(manager, items[idx - 1]);
|
||||
return pl ? rect.top + pl.topY + pl.height : null;
|
||||
}, [manager]);
|
||||
|
||||
@@ -513,7 +468,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
const fromIndex = paneItemsRef.current.findIndex(i => i.id === id);
|
||||
if (!item || fromIndex === -1) return;
|
||||
|
||||
const layout = resolvePaneItemLayout(manager, item, paneLayoutsRef.current);
|
||||
const layout = resolvePaneItemLayout(manager, item);
|
||||
const paneCapture = layout
|
||||
? capturePaneRegion(containerElRef.current, layout.topY, layout.height)
|
||||
: null;
|
||||
@@ -588,7 +543,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
|
||||
for (const item of items) {
|
||||
if (getPaneHostId(item.id, inds) === dragHost) continue;
|
||||
const l = resolvePaneItemLayout(manager, item, layouts);
|
||||
const l = resolvePaneItemLayout(manager, item);
|
||||
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) {
|
||||
@@ -600,7 +555,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
|
||||
if (dropMode === 'reorder') {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const l = resolvePaneItemLayout(manager, items[i], layouts);
|
||||
const l = resolvePaneItemLayout(manager, items[i]);
|
||||
if (l && mouseY < l.topY + l.height / 2) {
|
||||
drop = i;
|
||||
break;
|
||||
@@ -645,25 +600,25 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
: -1;
|
||||
|
||||
const draggingLayout = draggingItemIdx >= 0
|
||||
? resolvePaneItemLayout(manager, paneItems[draggingItemIdx], paneLayouts)
|
||||
? resolvePaneItemLayout(manager, paneItems[draggingItemIdx])
|
||||
: null;
|
||||
|
||||
const mergeTargetLayout = isDragging && dragState?.mergeTargetId
|
||||
? (() => {
|
||||
const t = paneItems.find(i => i.id === dragState.mergeTargetId);
|
||||
return t ? resolvePaneItemLayout(manager, t, paneLayouts) : null;
|
||||
return t ? resolvePaneItemLayout(manager, t) : null;
|
||||
})()
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
if (paused || !portalHost) return null;
|
||||
|
||||
const layer = (
|
||||
<div className="pane-legend-layer" aria-hidden={paneItems.length === 0}>
|
||||
{paneItems.map((item, itemIdx) => {
|
||||
const pl = resolvePaneItemLayout(manager, item, paneLayouts);
|
||||
const pl = resolvePaneItemLayout(manager, item);
|
||||
if (!pl) return null;
|
||||
|
||||
const screenX = containerRect.left;
|
||||
const screenY = containerRect.top + pl.topY;
|
||||
const paneBottomY = containerRect.top + pl.topY + pl.height;
|
||||
const paneBottomY = pl.topY + pl.height;
|
||||
const itemVals = item.hidden ? [] : (values[item.id] ?? []);
|
||||
const itemHostId = getPaneHostId(item.id, indicators);
|
||||
const samePaneItems = paneItems.filter(p => p.paneIndex === item.paneIndex);
|
||||
@@ -673,7 +628,9 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
const isMergedPane = samePaneItems.length > 1;
|
||||
const showSplitBtn = isMergedPane && item.id === itemHostId && !!onSplit;
|
||||
const PANE_BTN_STEP = 24;
|
||||
const paneBtnRight = (slot: number) => containerRect.right - 28 - slot * PANE_BTN_STEP;
|
||||
const paneBtnRight = (slot: number) => 28 + slot * PANE_BTN_STEP;
|
||||
const hoverScreenX = containerRect.left;
|
||||
const hoverScreenY = containerRect.top + pl.topY;
|
||||
let btnSlot = 0;
|
||||
const closeSlot = btnSlot++;
|
||||
const copySlot = onDuplicate ? btnSlot++ : -1;
|
||||
@@ -690,11 +647,7 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
draggingLayout.height,
|
||||
)
|
||||
: 0;
|
||||
const legendTop = screenY + 5 + stackIdx * 22;
|
||||
// 드래그 중 shift 전환은 transform, 드래그 후 위치 재정렬은 top
|
||||
const shiftTrans = isDragging
|
||||
? 'transform 0.18s ease'
|
||||
: 'top 0.22s ease, height 0.22s ease, transform 0.18s ease';
|
||||
const shiftTrans = isDragging ? 'transform 0.18s ease' : undefined;
|
||||
|
||||
return (
|
||||
<React.Fragment key={item.id}>
|
||||
@@ -702,10 +655,10 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
<div
|
||||
className="pane-bg-overlay"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: screenX,
|
||||
top: screenY,
|
||||
width: containerRect.width,
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: pl.topY,
|
||||
width: '100%',
|
||||
height: pl.height,
|
||||
background: isBeingDragged
|
||||
? 'transparent'
|
||||
@@ -714,8 +667,8 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
: bgColor,
|
||||
zIndex: 480,
|
||||
pointerEvents: 'none',
|
||||
transform: `translateY(${shiftY}px)`,
|
||||
transition: shiftTrans + ', background 0.12s ease',
|
||||
transform: shiftY ? `translateY(${shiftY}px)` : undefined,
|
||||
transition: shiftTrans,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -723,22 +676,22 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
<div
|
||||
className="pane-legend-item"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: screenX + 6,
|
||||
top: legendTop,
|
||||
position: 'absolute',
|
||||
left: 6,
|
||||
top: pl.topY + 5 + stackIdx * 22,
|
||||
zIndex: 502,
|
||||
pointerEvents: isDragging ? 'none' : 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
opacity: isBeingDragged ? 0 : 1,
|
||||
transform: `translateY(${shiftY}px)`,
|
||||
transition: shiftTrans + ', opacity 0.12s ease',
|
||||
transform: shiftY ? `translateY(${shiftY}px)` : undefined,
|
||||
transition: shiftTrans,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`pane-legend-name${isPaneHidden ? ' pane-legend-name--hidden' : ''}`}
|
||||
onMouseEnter={() => !isDragging && onHoverName(item.id, screenX, screenY)}
|
||||
onMouseEnter={() => !isDragging && onHoverName(item.id, hoverScreenX, hoverScreenY)}
|
||||
onMouseLeave={() => !isDragging && onLeaveName()}
|
||||
onDoubleClick={() => !isDragging && onDoubleClick?.(item.id)}
|
||||
>
|
||||
@@ -757,13 +710,14 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
<button
|
||||
className="pane-btn pane-btn-split"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: paneBtnRight(splitSlot),
|
||||
top: screenY + 5,
|
||||
position: 'absolute',
|
||||
right: paneBtnRight(splitSlot),
|
||||
top: pl.topY + 5,
|
||||
zIndex: 505,
|
||||
opacity: isBeingDragged ? 0 : undefined,
|
||||
transform: `translateY(${shiftY}px)`,
|
||||
transform: shiftY ? `translateY(${shiftY}px)` : undefined,
|
||||
transition: shiftTrans,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
@@ -778,13 +732,14 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
<button
|
||||
className="pane-btn pane-btn-copy"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: paneBtnRight(copySlot),
|
||||
top: screenY + 5,
|
||||
position: 'absolute',
|
||||
right: paneBtnRight(copySlot),
|
||||
top: pl.topY + 5,
|
||||
zIndex: 505,
|
||||
opacity: isBeingDragged ? 0 : undefined,
|
||||
transform: `translateY(${shiftY}px)`,
|
||||
transform: shiftY ? `translateY(${shiftY}px)` : undefined,
|
||||
transition: shiftTrans,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
@@ -799,13 +754,14 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
<button
|
||||
className="pane-btn pane-btn-close"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: paneBtnRight(closeSlot),
|
||||
top: screenY + 5,
|
||||
position: 'absolute',
|
||||
right: paneBtnRight(closeSlot),
|
||||
top: pl.topY + 5,
|
||||
zIndex: 504,
|
||||
opacity: isBeingDragged ? 0 : undefined,
|
||||
transform: `translateY(${shiftY}px)`,
|
||||
transform: shiftY ? `translateY(${shiftY}px)` : undefined,
|
||||
transition: shiftTrans,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
onClick={e => { e.stopPropagation(); onRemove(item.id); }}
|
||||
title="지표 제거"
|
||||
@@ -820,13 +776,14 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
<div
|
||||
className="pane-drag-handle"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: screenX + 4,
|
||||
position: 'absolute',
|
||||
left: 4,
|
||||
top: paneBottomY - 30,
|
||||
zIndex: 510,
|
||||
opacity: isBeingDragged ? 0 : undefined,
|
||||
transform: `translateY(${shiftY}px)`,
|
||||
transition: isDragging ? 'transform 0.18s ease' : 'top 0.22s ease, transform 0.18s ease',
|
||||
transform: shiftY ? `translateY(${shiftY}px)` : undefined,
|
||||
transition: shiftTrans,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
onPointerDown={e => handleDragHandlePointerDown(e, item.id)}
|
||||
onMouseEnter={onLeaveName}
|
||||
@@ -848,13 +805,14 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
<button
|
||||
className={`pane-btn pane-btn-expand${isFocused ? ' active' : ''}`}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: screenX + 4,
|
||||
position: 'absolute',
|
||||
left: 4,
|
||||
top: paneBottomY - 56,
|
||||
zIndex: 504,
|
||||
opacity: isBeingDragged ? 0 : undefined,
|
||||
transform: `translateY(${shiftY}px)`,
|
||||
transform: shiftY ? `translateY(${shiftY}px)` : undefined,
|
||||
transition: shiftTrans,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
@@ -879,8 +837,8 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
<>
|
||||
{/* ① 전체 지표 영역 dim — 고스트·shift 가 더 선명하게 보이도록 */}
|
||||
{paneItems.length > 0 && (() => {
|
||||
const firstL = resolvePaneItemLayout(manager, paneItems[0], paneLayouts);
|
||||
const lastL = resolvePaneItemLayout(manager, paneItems[paneItems.length - 1], paneLayouts);
|
||||
const firstL = resolvePaneItemLayout(manager, paneItems[0]);
|
||||
const lastL = resolvePaneItemLayout(manager, paneItems[paneItems.length - 1]);
|
||||
if (!firstL || !lastL) return null;
|
||||
return (
|
||||
<div style={{
|
||||
@@ -996,8 +954,10 @@ const PaneLegend: React.FC<PaneLegendProps> = ({
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
||||
return createPortal(layer, portalHost);
|
||||
};
|
||||
|
||||
function formatVal(v: number): string {
|
||||
|
||||
Reference in New Issue
Block a user