크로스헤어 세로선 날짜표시
This commit is contained in:
@@ -1000,6 +1000,27 @@ html.theme-blue {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 크로스헤어 세로선 — 캔들 pane 하단 시간축 교차 시각 */
|
||||||
|
.candle-pane-time-axis__crosshair {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 13;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
background: #50535e;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
/* Loading overlay */
|
/* Loading overlay */
|
||||||
/* 과거 데이터 추가 로드 중 표시 (차트 좌상단 소형 배지) */
|
/* 과거 데이터 추가 로드 중 표시 (차트 좌상단 소형 배지) */
|
||||||
.chart-history-loading {
|
.chart-history-loading {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* 캔들 pane 하단 시간축 — 거래량·보조지표가 아래에 있을 때 캔들 영역 바로 아래에도 시간 표시.
|
* 캔들 pane 하단 시간축 — 거래량·보조지표가 아래에 있을 때 캔들 영역 바로 아래에도 시간 표시.
|
||||||
*
|
* 크로스헤어 세로선 위치에 해당 날짜·시간 배지를 함께 표시한다.
|
||||||
* 드래그 패닝 중에는 라벨 좌표를 재계산하지 않고 translate3d 로 차트와 동일 픽셀만큼 이동한다.
|
|
||||||
* (setVisibleLogicalRange 직후 LWC 좌표 API가 한 프레임 늦게 갱신되는 문제 회피)
|
|
||||||
*/
|
*/
|
||||||
import React, { useEffect, useCallback, useRef } from 'react';
|
import React, { useEffect, useCallback, useRef } from 'react';
|
||||||
|
import type { MouseEventParams, Time } from 'lightweight-charts';
|
||||||
import type { ChartManager } from '../utils/ChartManager';
|
import type { ChartManager } from '../utils/ChartManager';
|
||||||
import { subscribeChartTimeFormat } from '../utils/chartTimeFormat';
|
import { subscribeChartTimeFormat } from '../utils/chartTimeFormat';
|
||||||
|
|
||||||
@@ -44,8 +43,36 @@ function applyOverlayLayout(
|
|||||||
|
|
||||||
const CandlePaneTimeAxis: React.FC<CandlePaneTimeAxisProps> = ({ manager, containerEl }) => {
|
const CandlePaneTimeAxis: React.FC<CandlePaneTimeAxisProps> = ({ manager, containerEl }) => {
|
||||||
const axisRef = useRef<HTMLDivElement>(null);
|
const axisRef = useRef<HTMLDivElement>(null);
|
||||||
|
const crosshairRef = useRef<HTMLDivElement>(null);
|
||||||
const labelPoolRef = useRef<HTMLSpanElement[]>([]);
|
const labelPoolRef = useRef<HTMLSpanElement[]>([]);
|
||||||
|
|
||||||
|
const updateCrosshairLabel = useCallback((p: MouseEventParams<Time>) => {
|
||||||
|
const el = crosshairRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const band = manager.getCandlePaneTimeAxisBand();
|
||||||
|
if (!band || !p.time || p.point?.x == null) {
|
||||||
|
el.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = p.point.x;
|
||||||
|
if (!Number.isFinite(x) || x < 0 || x > band.plotWidth) {
|
||||||
|
el.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.style.display = 'flex';
|
||||||
|
el.style.top = `${band.topY}px`;
|
||||||
|
el.style.height = `${band.height}px`;
|
||||||
|
el.style.left = `${x}px`;
|
||||||
|
el.textContent = manager.formatCrosshairAxisTime(p.time);
|
||||||
|
}, [manager]);
|
||||||
|
|
||||||
|
const hideCrosshairLabel = useCallback(() => {
|
||||||
|
if (crosshairRef.current) crosshairRef.current.style.display = 'none';
|
||||||
|
}, []);
|
||||||
|
|
||||||
const renderOverlay = useCallback(() => {
|
const renderOverlay = useCallback(() => {
|
||||||
const root = axisRef.current;
|
const root = axisRef.current;
|
||||||
if (!root || !containerEl.isConnected) return;
|
if (!root || !containerEl.isConnected) return;
|
||||||
@@ -88,6 +115,10 @@ const CandlePaneTimeAxis: React.FC<CandlePaneTimeAxisProps> = ({ manager, contai
|
|||||||
const unsubLayout = manager.subscribePaneLayout(renderOverlay);
|
const unsubLayout = manager.subscribePaneLayout(renderOverlay);
|
||||||
const unsubRange = manager.subscribeViewport(renderOverlay);
|
const unsubRange = manager.subscribeViewport(renderOverlay);
|
||||||
const unsubFormat = subscribeChartTimeFormat(renderOverlay);
|
const unsubFormat = subscribeChartTimeFormat(renderOverlay);
|
||||||
|
const unsubCrosshair = manager.subscribeCrosshair(updateCrosshairLabel);
|
||||||
|
|
||||||
|
const onLeave = () => hideCrosshairLabel();
|
||||||
|
containerEl.addEventListener('mouseleave', onLeave);
|
||||||
|
|
||||||
const timers = [0, 50, 150, 400].map(ms => window.setTimeout(renderOverlay, ms));
|
const timers = [0, 50, 150, 400].map(ms => window.setTimeout(renderOverlay, ms));
|
||||||
return () => {
|
return () => {
|
||||||
@@ -95,17 +126,27 @@ const CandlePaneTimeAxis: React.FC<CandlePaneTimeAxisProps> = ({ manager, contai
|
|||||||
unsubLayout();
|
unsubLayout();
|
||||||
unsubRange();
|
unsubRange();
|
||||||
unsubFormat();
|
unsubFormat();
|
||||||
|
unsubCrosshair();
|
||||||
|
containerEl.removeEventListener('mouseleave', onLeave);
|
||||||
timers.forEach(clearTimeout);
|
timers.forEach(clearTimeout);
|
||||||
};
|
};
|
||||||
}, [manager, containerEl, renderOverlay]);
|
}, [manager, containerEl, renderOverlay, updateCrosshairLabel, hideCrosshairLabel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
ref={axisRef}
|
<div
|
||||||
className="candle-pane-time-axis"
|
ref={axisRef}
|
||||||
style={{ display: 'none' }}
|
className="candle-pane-time-axis"
|
||||||
aria-hidden
|
style={{ display: 'none' }}
|
||||||
/>
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
ref={crosshairRef}
|
||||||
|
className="candle-pane-time-axis__crosshair"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -267,8 +267,12 @@ export class ChartManager {
|
|||||||
},
|
},
|
||||||
crosshair: {
|
crosshair: {
|
||||||
mode: CrosshairMode.Normal, /* 기본: 마우스 위치 자유 이동 (자석모드 OFF) */
|
mode: CrosshairMode.Normal, /* 기본: 마우스 위치 자유 이동 (자석모드 OFF) */
|
||||||
vertLine: { color: t.crosshairColor, labelBackgroundColor: '#9598A1' },
|
vertLine: {
|
||||||
horzLine: { color: t.crosshairColor, labelBackgroundColor: '#9598A1' },
|
color: t.crosshairColor,
|
||||||
|
labelVisible: true,
|
||||||
|
labelBackgroundColor: '#50535E',
|
||||||
|
},
|
||||||
|
horzLine: { color: t.crosshairColor, labelBackgroundColor: '#50535E' },
|
||||||
},
|
},
|
||||||
timeScale: {
|
timeScale: {
|
||||||
borderColor: t.borderColor,
|
borderColor: t.borderColor,
|
||||||
@@ -522,7 +526,14 @@ export class ChartManager {
|
|||||||
panes: applyPaneSeparatorToChartOptions(this._paneSeparatorOptions).layout.panes,
|
panes: applyPaneSeparatorToChartOptions(this._paneSeparatorOptions).layout.panes,
|
||||||
},
|
},
|
||||||
grid: { vertLines: { color: t.gridColor }, horzLines: { color: t.gridColor } },
|
grid: { vertLines: { color: t.gridColor }, horzLines: { color: t.gridColor } },
|
||||||
crosshair: { vertLine: { color: t.crosshairColor, labelBackgroundColor: '#9598A1' }, horzLine: { color: t.crosshairColor, labelBackgroundColor: '#9598A1' } },
|
crosshair: {
|
||||||
|
vertLine: {
|
||||||
|
color: t.crosshairColor,
|
||||||
|
labelVisible: true,
|
||||||
|
labelBackgroundColor: '#50535E',
|
||||||
|
},
|
||||||
|
horzLine: { color: t.crosshairColor, labelBackgroundColor: '#50535E' },
|
||||||
|
},
|
||||||
timeScale: { borderColor: t.borderColor },
|
timeScale: { borderColor: t.borderColor },
|
||||||
rightPriceScale: { borderColor: t.borderColor },
|
rightPriceScale: { borderColor: t.borderColor },
|
||||||
});
|
});
|
||||||
@@ -2730,14 +2741,23 @@ export class ChartManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 캔들 pane 하단 시간축 오버레이 — 거래량·보조지표가 있을 때 캔들 영역 바로 아래에도 시각 표시.
|
* 캔들 pane 하단 시간축 밴드 위치 (보조지표·거래량이 아래에 있을 때).
|
||||||
*/
|
*/
|
||||||
getCandlePaneTimeAxisOverlay(): {
|
getCandlePaneTimeAxisBand(): { topY: number; height: number; plotWidth: number } | null {
|
||||||
topY: number;
|
return this._getCandlePaneTimeAxisBand();
|
||||||
height: number;
|
}
|
||||||
plotWidth: number;
|
|
||||||
labels: Array<{ x: number; text: string }>;
|
/** 크로스헤어 세로선 시간 — 설정된 차트 시간 포맷·시간대 적용 */
|
||||||
} | null {
|
formatCrosshairAxisTime(time: Time): string {
|
||||||
|
return formatLwcTimeWithPattern(
|
||||||
|
time as number | { year: number; month: number; day: number },
|
||||||
|
this.displayTimeframe,
|
||||||
|
this.displayTimezone,
|
||||||
|
this.chartTimeFormat,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCandlePaneTimeAxisBand(): { topY: number; height: number; plotWidth: number } | null {
|
||||||
if (!this.mainSeries || this.rawBars.length < 2) return null;
|
if (!this.mainSeries || this.rawBars.length < 2) return null;
|
||||||
|
|
||||||
const layouts = this.getPaneLayouts();
|
const layouts = this.getPaneLayouts();
|
||||||
@@ -2748,11 +2768,31 @@ export class ChartManager {
|
|||||||
if (!main || main.height < 48) return null;
|
if (!main || main.height < 48) return null;
|
||||||
|
|
||||||
const AXIS_H = 22;
|
const AXIS_H = 22;
|
||||||
|
const plotWidth = Math.max(40, this.chart.timeScale().width());
|
||||||
|
return {
|
||||||
|
topY: main.topY + main.height - AXIS_H,
|
||||||
|
height: AXIS_H,
|
||||||
|
plotWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 캔들 pane 하단 시간축 오버레이 — 거래량·보조지표가 있을 때 캔들 영역 바로 아래에도 시각 표시.
|
||||||
|
*/
|
||||||
|
getCandlePaneTimeAxisOverlay(): {
|
||||||
|
topY: number;
|
||||||
|
height: number;
|
||||||
|
plotWidth: number;
|
||||||
|
labels: Array<{ x: number; text: string }>;
|
||||||
|
} | null {
|
||||||
|
const band = this._getCandlePaneTimeAxisBand();
|
||||||
|
if (!band) return null;
|
||||||
|
|
||||||
const ts = this.chart.timeScale();
|
const ts = this.chart.timeScale();
|
||||||
const lr = ts.getVisibleLogicalRange();
|
const lr = ts.getVisibleLogicalRange();
|
||||||
if (!lr) return null;
|
if (!lr) return null;
|
||||||
|
|
||||||
const plotWidth = Math.max(40, ts.width());
|
const { topY, height, plotWidth } = band;
|
||||||
const lrSpan = lr.to - lr.from;
|
const lrSpan = lr.to - lr.from;
|
||||||
if (!Number.isFinite(lrSpan) || lrSpan <= 0) return null;
|
if (!Number.isFinite(lrSpan) || lrSpan <= 0) return null;
|
||||||
|
|
||||||
@@ -2779,8 +2819,8 @@ export class ChartManager {
|
|||||||
if (labels.length === 0) return null;
|
if (labels.length === 0) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
topY: main.topY + main.height - AXIS_H,
|
topY,
|
||||||
height: AXIS_H,
|
height,
|
||||||
plotWidth,
|
plotWidth,
|
||||||
labels,
|
labels,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user