차트로딩중 적용
This commit is contained in:
@@ -947,6 +947,10 @@ html.theme-blue {
|
|||||||
.tv-chart-wrap--candle-only {
|
.tv-chart-wrap--candle-only {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
/* 초기 로드 중 LWC pane·구분선이 순차 노출되는 것 방지 */
|
||||||
|
.tv-chart-wrap--bootstrapping .chart-container {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
.tv-chart-wrap::-webkit-scrollbar { width: 6px; }
|
.tv-chart-wrap::-webkit-scrollbar { width: 6px; }
|
||||||
.tv-chart-wrap::-webkit-scrollbar-track { background: transparent; }
|
.tv-chart-wrap::-webkit-scrollbar-track { background: transparent; }
|
||||||
.tv-chart-wrap::-webkit-scrollbar-thumb { background: var(--bg4); border-radius: 3px; }
|
.tv-chart-wrap::-webkit-scrollbar-thumb { background: var(--bg4); border-radius: 3px; }
|
||||||
@@ -1142,6 +1146,12 @@ html.theme-blue {
|
|||||||
color: var(--text2);
|
color: var(--text2);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
/* 실시간 차트 영역 중앙 단일 로딩 (tv-chart-wrap 내부) */
|
||||||
|
.tv-chart-wrap .chart-loading--chart-area {
|
||||||
|
z-index: 920;
|
||||||
|
background: var(--bg, #131722);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
width: 32px; height: 32px;
|
width: 32px; height: 32px;
|
||||||
border: 3px solid var(--bg4);
|
border: 3px solid var(--bg4);
|
||||||
|
|||||||
+7
-14
@@ -2274,23 +2274,10 @@ function App() {
|
|||||||
legend={legend}
|
legend={legend}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 로딩 오버레이 */}
|
|
||||||
{useUpbit && isLoading && (
|
|
||||||
<div className="chart-loading">
|
|
||||||
<div className="loading-spinner" />
|
|
||||||
<span>업비트 데이터 로딩 중...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isSingleLoadingMore && (
|
|
||||||
<div className="chart-history-loading">
|
|
||||||
<div className="loading-spinner" style={{ width: 14, height: 14 }} />
|
|
||||||
<span>과거 데이터 로딩...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TradingChart
|
<TradingChart
|
||||||
key={chartMountKey}
|
key={chartMountKey}
|
||||||
chartVisible={chartVisible}
|
chartVisible={chartVisible}
|
||||||
|
dataLoading={useUpbit && isLoading}
|
||||||
bars={bars} barsMarket={barsMarket} market={symbol} timeframe={timeframe} displayTimezone={displayTimezone}
|
bars={bars} barsMarket={barsMarket} market={symbol} timeframe={timeframe} displayTimezone={displayTimezone}
|
||||||
chartType={chartType} theme={theme} mode={mode}
|
chartType={chartType} theme={theme} mode={mode}
|
||||||
indicators={effectiveIndicators} drawingTool={drawingTool}
|
indicators={effectiveIndicators} drawingTool={drawingTool}
|
||||||
@@ -2362,6 +2349,12 @@ function App() {
|
|||||||
volumeVisible={chartVolumeVisible}
|
volumeVisible={chartVolumeVisible}
|
||||||
paneSeparatorOptions={chartPaneSeparator}
|
paneSeparatorOptions={chartPaneSeparator}
|
||||||
/>
|
/>
|
||||||
|
{isSingleLoadingMore && (
|
||||||
|
<div className="chart-history-loading">
|
||||||
|
<div className="loading-spinner" style={{ width: 14, height: 14 }} />
|
||||||
|
<span>과거 데이터 로딩...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 백테스팅 결과 통계 배지 */}
|
{/* 백테스팅 결과 통계 배지 */}
|
||||||
{btMatchesChart && layoutDef.count === 1 && (
|
{btMatchesChart && layoutDef.count === 1 && (
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import CandlePaneControls from './CandlePaneControls';
|
|||||||
import { getKoreanName } from '../utils/marketNameCache';
|
import { getKoreanName } from '../utils/marketNameCache';
|
||||||
import { DEFAULT_DISPLAY_TIMEZONE } from '../utils/timezone';
|
import { DEFAULT_DISPLAY_TIMEZONE } from '../utils/timezone';
|
||||||
import { chartHasStaleIndicators, classifyIndicatorChartChange, singleIndParamKey } from '../utils/indicatorChartSync';
|
import { chartHasStaleIndicators, classifyIndicatorChartChange, singleIndParamKey } from '../utils/indicatorChartSync';
|
||||||
|
import { sortIndicatorsForPaneLoad } from '../utils/indicatorPaneMerge';
|
||||||
import type { ChartPaneSeparatorOptions } from '../types/chartPaneSeparator';
|
import type { ChartPaneSeparatorOptions } from '../types/chartPaneSeparator';
|
||||||
import PaneSeparatorOverlay from './PaneSeparatorOverlay';
|
import PaneSeparatorOverlay from './PaneSeparatorOverlay';
|
||||||
|
|
||||||
@@ -148,6 +149,8 @@ interface TradingChartProps {
|
|||||||
showPaneLegend?: boolean;
|
showPaneLegend?: boolean;
|
||||||
/** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 (기본 true) */
|
/** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 (기본 true) */
|
||||||
crosshairInfoVisible?: boolean;
|
crosshairInfoVisible?: boolean;
|
||||||
|
/** 외부 캔들 데이터 fetch 중 (실시간 차트 초기 로딩) */
|
||||||
|
dataLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TradingChart: React.FC<TradingChartProps> = ({
|
const TradingChart: React.FC<TradingChartProps> = ({
|
||||||
@@ -184,6 +187,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
paneLayoutClamp = false,
|
paneLayoutClamp = false,
|
||||||
showPaneLegend = true,
|
showPaneLegend = true,
|
||||||
crosshairInfoVisible = true,
|
crosshairInfoVisible = true,
|
||||||
|
dataLoading = false,
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null); // 스크롤 래퍼
|
const wrapperRef = useRef<HTMLDivElement>(null); // 스크롤 래퍼
|
||||||
@@ -252,6 +256,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
|
|
||||||
const chartTimeFormat = useChartTimeFormat();
|
const chartTimeFormat = useChartTimeFormat();
|
||||||
const [chartMgr, setChartMgr] = useState<ChartManager | null>(null);
|
const [chartMgr, setChartMgr] = useState<ChartManager | null>(null);
|
||||||
|
/** 초기 reloadAll 완료 전 — pane 구분선·부분 레이아웃 노출 방지 */
|
||||||
|
const [chartPaintReady, setChartPaintReady] = useState(false);
|
||||||
/** 캔들 pane 전체보기 (오버레이 지표 유지, 하단 보조지표 pane 숨김) */
|
/** 캔들 pane 전체보기 (오버레이 지표 유지, 하단 보조지표 pane 숨김) */
|
||||||
const [candleOnlyMode, setCandleOnlyMode] = useState(false);
|
const [candleOnlyMode, setCandleOnlyMode] = useState(false);
|
||||||
const paneDragRef = useRef<((id: string, e: React.PointerEvent) => void) | null>(null);
|
const paneDragRef = useRef<((id: string, e: React.PointerEvent) => void) | null>(null);
|
||||||
@@ -637,6 +643,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
|
|
||||||
barsRef.current = newBars;
|
barsRef.current = newBars;
|
||||||
reloadInFlightRef.current = true;
|
reloadInFlightRef.current = true;
|
||||||
|
setChartPaintReady(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
@@ -657,21 +664,12 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
mgr.setTheme(th);
|
mgr.setTheme(th);
|
||||||
mgr.setLogScale(ls);
|
mgr.setLogScale(ls);
|
||||||
mgr.setDisplayTimezone(displayTimezone, timeframe, chartTimeFormat);
|
mgr.setDisplayTimezone(displayTimezone, timeframe, chartTimeFormat);
|
||||||
commitCandleLayout(mgr);
|
|
||||||
onCandlesReady?.();
|
|
||||||
lastAppliedMarketRef.current = market;
|
lastAppliedMarketRef.current = market;
|
||||||
|
|
||||||
let stale = false;
|
const sortedInds = sortIndicatorsForPaneLoad(inds);
|
||||||
for (const ind of inds) {
|
if (sortedInds.length > 0) {
|
||||||
if (gen !== reloadGenRef.current) {
|
await mgr.addIndicatorsBatch(sortedInds);
|
||||||
prevBarsKey.current = '';
|
|
||||||
stale = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await mgr.addIndicator(ind);
|
|
||||||
}
|
}
|
||||||
if (stale) break;
|
|
||||||
|
|
||||||
if (gen !== reloadGenRef.current) {
|
if (gen !== reloadGenRef.current) {
|
||||||
prevBarsKey.current = '';
|
prevBarsKey.current = '';
|
||||||
break;
|
break;
|
||||||
@@ -688,7 +686,6 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
if (gen !== reloadGenRef.current) break;
|
if (gen !== reloadGenRef.current) break;
|
||||||
}
|
}
|
||||||
if (mgr.getLoadedIndicatorCount() < expected) {
|
if (mgr.getLoadedIndicatorCount() < expected) {
|
||||||
commitCandleLayout(mgr);
|
|
||||||
if (reloadRetryRef.current < 6) {
|
if (reloadRetryRef.current < 6) {
|
||||||
reloadRetryRef.current += 1;
|
reloadRetryRef.current += 1;
|
||||||
queueIndicatorRecovery();
|
queueIndicatorRecovery();
|
||||||
@@ -706,10 +703,10 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
lastAppliedMarketRef.current = market;
|
lastAppliedMarketRef.current = market;
|
||||||
|
|
||||||
await new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => {
|
await new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||||
applyPaneLayout(mgr);
|
normalizeChartViewport(mgr);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const futureBars = mgr.hasIchimoku() ? 28 : 0;
|
onCandlesReady?.();
|
||||||
mgr.setInitialVisibleRange(DISPLAY_COUNT, futureBars);
|
setChartPaintReady(true);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
})));
|
})));
|
||||||
@@ -740,12 +737,15 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
} while (reloadCoalesceRef.current);
|
} while (reloadCoalesceRef.current);
|
||||||
} finally {
|
} finally {
|
||||||
reloadInFlightRef.current = false;
|
reloadInFlightRef.current = false;
|
||||||
|
if (managerRef.current?.hasMainSeries()) {
|
||||||
|
setChartPaintReady(true);
|
||||||
|
}
|
||||||
if (reloadCoalesceRef.current) {
|
if (reloadCoalesceRef.current) {
|
||||||
reloadCoalesceRef.current = false;
|
reloadCoalesceRef.current = false;
|
||||||
queueFullReload();
|
queueFullReload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [applyPaneLayout, commitCandleLayout, normalizeChartViewport, onCandlesReady, onDataLoaded, displayTimezone, timeframe, market, queueFullReload, queueIndicatorRecovery]);
|
}, [normalizeChartViewport, onCandlesReady, onDataLoaded, displayTimezone, timeframe, market, queueFullReload, queueIndicatorRecovery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reloadAllRef.current = reloadAll;
|
reloadAllRef.current = reloadAll;
|
||||||
@@ -1022,7 +1022,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
resizeDebounceRef.current = window.setTimeout(() => {
|
resizeDebounceRef.current = window.setTimeout(() => {
|
||||||
resizeDebounceRef.current = null;
|
resizeDebounceRef.current = null;
|
||||||
const m = managerRef.current;
|
const m = managerRef.current;
|
||||||
if (!m) return;
|
if (!m || reloadInFlightRef.current) return;
|
||||||
|
|
||||||
// 컨테이너가 처음으로 유효한 크기를 가질 때:
|
// 컨테이너가 처음으로 유효한 크기를 가질 때:
|
||||||
// 데이터가 이미 barsRef에 있지만 차트에 아직 세팅되지 않은 경우 (예: 멀티레이아웃 초기 렌더) 재로드
|
// 데이터가 이미 barsRef에 있지만 차트에 아직 세팅되지 않은 경우 (예: 멀티레이아웃 초기 렌더) 재로드
|
||||||
@@ -1123,6 +1123,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
reloadCoalesceRef.current = false;
|
reloadCoalesceRef.current = false;
|
||||||
reloadSafetyTimers.current.forEach(clearTimeout);
|
reloadSafetyTimers.current.forEach(clearTimeout);
|
||||||
reloadSafetyTimers.current = [];
|
reloadSafetyTimers.current = [];
|
||||||
|
setChartPaintReady(false);
|
||||||
managerRef.current?.clearForSymbolChange();
|
managerRef.current?.clearForSymbolChange();
|
||||||
managerRef.current?.cancelPendingIndicatorUpdates();
|
managerRef.current?.cancelPendingIndicatorUpdates();
|
||||||
}, [market, timeframe]);
|
}, [market, timeframe]);
|
||||||
@@ -1338,8 +1339,9 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
||||||
if (!mgr.hasMainSeries()) return;
|
if (!mgr.hasMainSeries()) return;
|
||||||
|
|
||||||
const timers = [0, 150, 400, 800, 1500, 2500, 4000].map(delay =>
|
const timers = [600, 1200, 2500, 4000].map(delay =>
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
|
if (reloadInFlightRef.current) return;
|
||||||
const m = managerRef.current;
|
const m = managerRef.current;
|
||||||
if (!m?.hasMainSeries()) return;
|
if (!m?.hasMainSeries()) return;
|
||||||
const w = wrapperRef.current;
|
const w = wrapperRef.current;
|
||||||
@@ -1498,20 +1500,28 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
setCtxMenu(null);
|
setCtxMenu(null);
|
||||||
}, [ctxMenu, market]);
|
}, [ctxMenu, market]);
|
||||||
|
|
||||||
|
const showChartLoading = !chartPaintReady || dataLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// wrapperRef: 스크롤 영역 (overflow-y: auto → App.css .tv-chart-wrap)
|
// wrapperRef: 스크롤 영역 (overflow-y: auto → App.css .tv-chart-wrap)
|
||||||
<div
|
<div
|
||||||
ref={wrapperRef}
|
ref={wrapperRef}
|
||||||
className={`tv-chart-wrap${candleOnlyMode ? ' tv-chart-wrap--candle-only' : ''}`}
|
className={`tv-chart-wrap${candleOnlyMode ? ' tv-chart-wrap--candle-only' : ''}${chartPaintReady ? '' : ' tv-chart-wrap--bootstrapping'}`}
|
||||||
style={{ flex: 1, position: 'relative', minHeight: 0 }}
|
style={{ flex: 1, position: 'relative', minHeight: 0 }}
|
||||||
onPointerMove={handleWrapperMouseMove}
|
onPointerMove={handleWrapperMouseMove}
|
||||||
>
|
>
|
||||||
|
{showChartLoading && (
|
||||||
|
<div className="chart-loading chart-loading--chart-area" role="status" aria-live="polite">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
<span>차트 로딩중...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="chart-main-row">
|
<div className="chart-main-row">
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="chart-container"
|
className="chart-container"
|
||||||
/>
|
/>
|
||||||
{chartMgr && !candleOnlyMode && showPaneLegend && (
|
{chartMgr && !candleOnlyMode && showPaneLegend && chartPaintReady && (
|
||||||
<ChartRightToolbar
|
<ChartRightToolbar
|
||||||
manager={chartMgr}
|
manager={chartMgr}
|
||||||
indicators={indicators}
|
indicators={indicators}
|
||||||
@@ -1546,7 +1556,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
getContainer={() => containerRef.current}
|
getContainer={() => containerRef.current}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{chartMgr && paneSeparatorOptions && (
|
{chartMgr && chartPaintReady && paneSeparatorOptions && (
|
||||||
<PaneSeparatorOverlay
|
<PaneSeparatorOverlay
|
||||||
manager={chartMgr}
|
manager={chartMgr}
|
||||||
options={paneSeparatorOptions}
|
options={paneSeparatorOptions}
|
||||||
@@ -1576,7 +1586,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
onRestore={() => setCandleOnlyMode(false)}
|
onRestore={() => setCandleOnlyMode(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{chartMgr && !candleOnlyMode && showPaneLegend && (
|
{chartMgr && !candleOnlyMode && showPaneLegend && chartPaintReady && (
|
||||||
<PaneLegendPortal
|
<PaneLegendPortal
|
||||||
key={indKey(indicators)}
|
key={indKey(indicators)}
|
||||||
manager={chartMgr}
|
manager={chartMgr}
|
||||||
|
|||||||
@@ -56,9 +56,9 @@
|
|||||||
|
|
||||||
/* 목록 한 줄 = 알림상세 · 신호일치율 · 캔들 · 보조지표(1칸) — 가로 100% 꽉 참 */
|
/* 목록 한 줄 = 알림상세 · 신호일치율 · 캔들 · 보조지표(1칸) — 가로 100% 꽉 참 */
|
||||||
.tnl-row-gallery {
|
.tnl-row-gallery {
|
||||||
--tnl-summary-card-width: 320px;
|
--tnl-summary-card-width: 288px;
|
||||||
--tnl-signal-slot-width: 300px;
|
--tnl-signal-slot-width: 308px;
|
||||||
--tnl-chart-card-width: 280px;
|
--tnl-chart-card-width: 294px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@@ -71,17 +71,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tnl-row-gallery-detail {
|
.tnl-row-gallery-detail {
|
||||||
flex: 1.15 1 0;
|
flex: 1 1 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tnl-row-gallery-signal {
|
.tnl-row-gallery-signal {
|
||||||
flex: 1 1 0;
|
flex: 1.05 1 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tnl-row-gallery-candle {
|
.tnl-row-gallery-candle {
|
||||||
flex: 1 1 0;
|
flex: 1.05 1 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tnl-row-gallery-indicators {
|
.tnl-row-gallery-indicators {
|
||||||
flex: 1 1 0;
|
flex: 1.05 1 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,9 +123,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tnl-row-gallery--chart-expanded .tnl-row-gallery-detail {
|
.tnl-row-gallery--chart-expanded .tnl-row-gallery-detail {
|
||||||
flex: 0 0 min(28%, var(--tnl-summary-card-width));
|
flex: 0 0 min(26%, var(--tnl-summary-card-width));
|
||||||
width: min(28%, var(--tnl-summary-card-width));
|
width: min(26%, var(--tnl-summary-card-width));
|
||||||
min-width: min(240px, var(--tnl-summary-card-width));
|
min-width: min(220px, var(--tnl-summary-card-width));
|
||||||
max-width: var(--tnl-summary-card-width);
|
max-width: var(--tnl-summary-card-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1213,7 +1213,7 @@ ul.tnl-list.tnl-list--grid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tnl-row-gallery {
|
.tnl-row-gallery {
|
||||||
--tnl-summary-card-width: min(100%, 320px);
|
--tnl-summary-card-width: min(100%, 288px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tnl-row-gallery-detail,
|
.tnl-row-gallery-detail,
|
||||||
|
|||||||
@@ -869,11 +869,12 @@ export class ChartManager {
|
|||||||
this._reapplyAllPatternMarkers();
|
this._reapplyAllPatternMarkers();
|
||||||
this._removeExtraSubPanes();
|
this._removeExtraSubPanes();
|
||||||
|
|
||||||
// ② 새 순서로 재추가 (병합 호스트 → 멤버 순)
|
// ② 새 순서로 재추가 (병합 호스트 → 멤버 순) — 레이아웃은 마지막에 한 번만
|
||||||
this._indRunning = false;
|
this._indRunning = false;
|
||||||
for (const ind of sortIndicatorsForPaneLoad(inds)) {
|
const sorted = sortIndicatorsForPaneLoad(inds);
|
||||||
|
for (const ind of sorted) {
|
||||||
if (this._indicatorLoadStale(loadGen)) break;
|
if (this._indicatorLoadStale(loadGen)) break;
|
||||||
await this.addIndicator(ind);
|
await this.addIndicator(ind, { skipLayout: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._indicatorLoadStale(loadGen)) return;
|
if (this._indicatorLoadStale(loadGen)) return;
|
||||||
|
|||||||
@@ -97,17 +97,13 @@ export function formatChartAxisPrice(price: number): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 보조지표 pane 우측 가격축·시리즈 라벨 (소수점 표시) */
|
/** 보조지표 pane 우측 가격축·시리즈 라벨 (소수점 2자리 고정) */
|
||||||
export function formatIndicatorAxisPrice(price: number): string {
|
export function formatIndicatorAxisPrice(price: number): string {
|
||||||
if (!Number.isFinite(price)) return String(price);
|
if (!Number.isFinite(price)) return String(price);
|
||||||
const abs = Math.abs(price);
|
return price.toLocaleString('en-US', {
|
||||||
if (abs >= 1_000) {
|
minimumFractionDigits: 2,
|
||||||
return price.toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
maximumFractionDigits: 2,
|
||||||
}
|
});
|
||||||
if (abs >= 1) {
|
|
||||||
return price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
||||||
}
|
|
||||||
return price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 4 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatTime(ts: number, timeframe: Timeframe): string {
|
export function formatTime(ts: number, timeframe: Timeframe): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user