feat(chart): Alt+Click removes nearest horizontal line
IPriceLine has no native click event, so use a coordinate-matching pattern: - On Alt+Click, compute candle.priceToCoordinate for each hline - Find the one whose y is closest to the click y - If within ±6px, remove it via removeLine Complements the chips ✕ button — clicking the line itself on the chart is more natural than aiming at a small ✕. Shift+Click for add still wins when both modifiers are held (more frequent, checked first). - StockChart toolbar title and ShortcutsHelp updated to surface the gesture
This commit is contained in:
@@ -24,6 +24,7 @@ const SHORTCUTS: Shortcut[] = [
|
|||||||
{ scope: "종목 페이지", keys: ["F"], label: "차트 전체화면 토글" },
|
{ scope: "종목 페이지", keys: ["F"], label: "차트 전체화면 토글" },
|
||||||
{ scope: "종목 페이지", keys: ["H"], label: "현재 가격에 수평선 추가" },
|
{ scope: "종목 페이지", keys: ["H"], label: "현재 가격에 수평선 추가" },
|
||||||
{ scope: "종목 페이지", keys: ["Shift", "클릭"], label: "차트 위 클릭 위치에 수평선 추가" },
|
{ scope: "종목 페이지", keys: ["Shift", "클릭"], label: "차트 위 클릭 위치에 수평선 추가" },
|
||||||
|
{ scope: "종목 페이지", keys: ["Alt", "클릭"], label: "차트의 가장 가까운 수평선 제거" },
|
||||||
{ scope: "종목 페이지", keys: ["←", "→"], label: "기간 탭 이전 / 다음" },
|
{ scope: "종목 페이지", keys: ["←", "→"], label: "기간 탭 이전 / 다음" },
|
||||||
{ scope: "종목 페이지", keys: ["S"], label: "관심종목 추가 / 제거" },
|
{ scope: "종목 페이지", keys: ["S"], label: "관심종목 추가 / 제거" },
|
||||||
{ scope: "검색 팝오버", keys: ["↑", "↓"], label: "결과 이동" },
|
{ scope: "검색 팝오버", keys: ["↑", "↓"], label: "결과 이동" },
|
||||||
|
|||||||
@@ -712,33 +712,56 @@ export function StockChart({ chart, prediction, targets }: Props) {
|
|||||||
currentPriceRef.current = p;
|
currentPriceRef.current = p;
|
||||||
}, [hover, lastSummary]);
|
}, [hover, lastSummary]);
|
||||||
|
|
||||||
// Shift+Click 으로 차트 위 임의 가격에 수평선 추가.
|
// 차트 위 모디파이어 클릭으로 수평선 추가/제거.
|
||||||
// H 키 / 버튼은 hover 가격(또는 마지막 봉)에 묶여 있어 마우스 사용자가 "지금 hover 한 곳이
|
// Shift+Click — 클릭 y 좌표를 candle.coordinateToPrice 로 변환해 새 라인 추가.
|
||||||
// 아닌, 다른 위치" 에 그릴 방법이 없었음. Shift+Click 은 클릭 y 좌표를 candle.coordinateToPrice
|
// H 키 / 버튼은 hover 가격(또는 마지막 봉)에 묶여 있어 마우스 사용자가 "지금 hover 한
|
||||||
// 로 변환해 직접 지정하게 함 — TradingView 의 보조 도구 모디파이어 클릭 컨벤션과 호환.
|
// 곳이 아닌 다른 위치" 에 그릴 방법이 없었음.
|
||||||
|
// Alt+Click — 클릭 y 좌표 근처(픽셀 임계값 내)의 hline 한 개 제거. chips 의 ✕ 버튼이
|
||||||
|
// 좁고 마우스 정밀 클릭을 요구하는 반면, 차트 위 라인 근처 클릭은 자연스러움.
|
||||||
|
// (lightweight-charts `IPriceLine` 자체에는 클릭 이벤트가 없어 좌표 매칭 패턴 사용.)
|
||||||
// 단순 Click 은 기존 차트 인터랙션(crosshair/drag) 그대로 — 모디파이어가 있을 때만 가로챔.
|
// 단순 Click 은 기존 차트 인터랙션(crosshair/drag) 그대로 — 모디파이어가 있을 때만 가로챔.
|
||||||
|
// Shift+Alt 동시 입력은 add 가 우선 (먼저 검사, 빈도 더 높음).
|
||||||
|
const HLINE_HIT_PX = 6; // 라인까지 ±6px 이내 클릭이면 그 라인 hit 으로 간주.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = containerRef.current;
|
const el = containerRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const onClick = (e: MouseEvent) => {
|
const onClick = (e: MouseEvent) => {
|
||||||
if (!e.shiftKey) return;
|
if (!e.shiftKey && !e.altKey) return;
|
||||||
// 최대치 도달 시 silently 무시 — 사용자가 chips 의 (N/10) 카운터로 확인 가능.
|
|
||||||
if (hlines.length >= 10) return;
|
|
||||||
const candle = candleRef.current;
|
const candle = candleRef.current;
|
||||||
if (!candle) return;
|
if (!candle) return;
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const y = e.clientY - rect.top;
|
const y = e.clientY - rect.top;
|
||||||
// 차트 영역 밖(헤더 위 등) → 음수/큰값 → coordinateToPrice 가 null 또는 비정상값.
|
if (e.shiftKey) {
|
||||||
|
if (hlines.length >= 10) return; // cap — chips 의 (N/10) 카운터로 확인 가능.
|
||||||
const raw = candle.coordinateToPrice(y);
|
const raw = candle.coordinateToPrice(y);
|
||||||
if (raw == null) return;
|
if (raw == null) return;
|
||||||
const price = Number(raw);
|
const price = Number(raw);
|
||||||
if (!Number.isFinite(price) || price <= 0) return;
|
if (!Number.isFinite(price) || price <= 0) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setHlines(addLine(chart.code, price));
|
setHlines(addLine(chart.code, price));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Alt+Click — 가장 가까운 hline 찾아서 임계값 내면 제거.
|
||||||
|
if (hlines.length === 0) return;
|
||||||
|
let nearestId: string | null = null;
|
||||||
|
let nearestDist = Infinity;
|
||||||
|
for (const hl of hlines) {
|
||||||
|
const yLine = candle.priceToCoordinate(hl.price);
|
||||||
|
if (yLine == null) continue;
|
||||||
|
const d = Math.abs(Number(yLine) - y);
|
||||||
|
if (d < nearestDist) {
|
||||||
|
nearestDist = d;
|
||||||
|
nearestId = hl.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nearestId != null && nearestDist <= HLINE_HIT_PX) {
|
||||||
|
e.preventDefault();
|
||||||
|
setHlines(removeLine(chart.code, nearestId));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
el.addEventListener("click", onClick);
|
el.addEventListener("click", onClick);
|
||||||
return () => el.removeEventListener("click", onClick);
|
return () => el.removeEventListener("click", onClick);
|
||||||
}, [chart.code, hlines.length]);
|
}, [chart.code, hlines]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const c = chartRef.current;
|
const c = chartRef.current;
|
||||||
@@ -978,7 +1001,7 @@ export function StockChart({ chart, prediction, targets }: Props) {
|
|||||||
}}
|
}}
|
||||||
disabled={hlines.length >= 10 || (hover?.close ?? lastSummary?.close ?? null) == null}
|
disabled={hlines.length >= 10 || (hover?.close ?? lastSummary?.close ?? null) == null}
|
||||||
className="ml-auto flex items-center gap-1 rounded-full border border-zinc-800 px-2 py-0.5 text-[10px] text-zinc-400 transition hover:border-zinc-600 hover:text-zinc-200 disabled:cursor-not-allowed disabled:opacity-40"
|
className="ml-auto flex items-center gap-1 rounded-full border border-zinc-800 px-2 py-0.5 text-[10px] text-zinc-400 transition hover:border-zinc-600 hover:text-zinc-200 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
title="현재 가격(hover 또는 마지막 봉 종가)에 수평선 추가 (H · Shift+클릭으로 임의 위치)"
|
title="현재 가격(hover 또는 마지막 봉 종가)에 수평선 추가 (H · Shift+클릭으로 임의 위치 · Alt+클릭으로 라인 제거)"
|
||||||
>
|
>
|
||||||
─ 수평선 ({hlines.length}/10)
|
─ 수평선 ({hlines.length}/10)
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user