보조지표 차트 우측 라벨 마우스 스크롤 확대축소
This commit is contained in:
@@ -1215,11 +1215,11 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
const rect = container.getBoundingClientRect();
|
const rect = container.getBoundingClientRect();
|
||||||
const chartX = e.clientX - rect.left;
|
const chartX = e.clientX - rect.left;
|
||||||
const chartY = e.clientY - rect.top;
|
const chartY = e.clientY - rect.top;
|
||||||
if (!m.isCandlePanePriceAxis(chartX, chartY)) return;
|
if (!m.isOnPriceAxis(chartX, chartY)) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
m.zoomMainPriceByWheel(e.deltaY, chartY);
|
m.zoomPriceAxisByWheel(chartX, chartY, e.deltaY);
|
||||||
};
|
};
|
||||||
container.addEventListener('wheel', onWheelCapture, { capture: true, passive: false });
|
container.addEventListener('wheel', onWheelCapture, { capture: true, passive: false });
|
||||||
|
|
||||||
|
|||||||
@@ -4277,36 +4277,139 @@ export class ChartManager {
|
|||||||
return this.getPaneIndexAtChartY(chartY) === 0 && this.isOnPriceAxis(chartX, chartY);
|
return this.getPaneIndexAtChartY(chartY) === 0 && this.isOnPriceAxis(chartX, chartY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 우측 가격축 라벨 영역 휠 — 해당 pane(캔들·거래량·보조지표) 세로 줌 */
|
||||||
|
zoomPriceAxisByWheel(chartX: number, chartY: number, deltaY: number): boolean {
|
||||||
|
if (deltaY === 0 || !this.isOnPriceAxis(chartX, chartY)) return false;
|
||||||
|
const paneIndex = this.getPaneIndexAtChartY(chartY);
|
||||||
|
return this._zoomPanePriceScaleByWheel(paneIndex, deltaY, chartY);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 캔들 pane(0) 우측 가격축 휠 — pane 높이는 유지, 플롯 내 가격 범위만 확대/축소.
|
* 캔들 pane(0) 우측 가격축 휠 — pane 높이는 유지, 플롯 내 가격 범위만 확대/축소.
|
||||||
* wheel up → 줌 인(캔들·그래프가 세로로 커 보임), wheel down → 줌 아웃.
|
* wheel up → 줌 인(캔들·그래프가 세로로 커 보임), wheel down → 줌 아웃.
|
||||||
*/
|
*/
|
||||||
zoomMainPriceByWheel(deltaY: number, anchorChartY?: number): boolean {
|
zoomMainPriceByWheel(deltaY: number, anchorChartY?: number): boolean {
|
||||||
if (!this.mainSeries || deltaY === 0) return false;
|
if (!this.mainSeries || deltaY === 0) return false;
|
||||||
|
return this._zoomPanePriceScaleByWheel(0, deltaY, anchorChartY);
|
||||||
|
}
|
||||||
|
|
||||||
const main = this.getPaneLayouts().find(l => l.paneIndex === 0);
|
/** 거래량·보조지표 pane 우측 가격축 휠 줌 */
|
||||||
if (!main || main.height <= 0) return false;
|
zoomPanePriceByWheel(paneIndex: number, deltaY: number, anchorChartY?: number): boolean {
|
||||||
|
if (paneIndex === 0) return this.zoomMainPriceByWheel(deltaY, anchorChartY);
|
||||||
|
return this._zoomPanePriceScaleByWheel(paneIndex, deltaY, anchorChartY);
|
||||||
|
}
|
||||||
|
|
||||||
let range = this._mainPriceVisibleRange ?? this._readMainPriceRange();
|
private _getPriceScaleForPane(paneIndex: number) {
|
||||||
|
return paneIndex === 1
|
||||||
|
? this.chart.priceScale('volume', 1)
|
||||||
|
: this.chart.priceScale('right', paneIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getRepresentativeSeriesForPane(paneIndex: number): ISeriesApi<SeriesType> | null {
|
||||||
|
if (paneIndex === 0) return this.mainSeries;
|
||||||
|
if (paneIndex === 1 && this.volumeSeries) {
|
||||||
|
try {
|
||||||
|
const vis = (this.volumeSeries.options() as { visible?: boolean }).visible;
|
||||||
|
if (vis !== false) return this.volumeSeries;
|
||||||
|
} catch {
|
||||||
|
return this.volumeSeries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const entry of this.indicators.values()) {
|
||||||
|
if (entry.paneIndex === paneIndex && entry.seriesList.length > 0) {
|
||||||
|
return entry.seriesList[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (paneIndex === 1) return this.volumeSeries;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _readPanePriceRange(paneIndex: number): { from: number; to: number } | null {
|
||||||
|
if (paneIndex === 0) return this._readMainPriceRange();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing = this._getPriceScaleForPane(paneIndex).getVisibleRange();
|
||||||
|
if (existing) return { from: existing.from, to: existing.to };
|
||||||
|
} catch { /* fall through */ }
|
||||||
|
|
||||||
|
const series = this._getRepresentativeSeriesForPane(paneIndex);
|
||||||
|
const layout = this.getPaneLayouts().find(l => l.paneIndex === paneIndex);
|
||||||
|
if (!series || !layout || layout.height <= 0) return null;
|
||||||
|
|
||||||
|
const pTop = series.coordinateToPrice(0);
|
||||||
|
const pBot = series.coordinateToPrice(layout.height);
|
||||||
|
if (pTop === null || pBot === null) return null;
|
||||||
|
return { from: Math.min(pTop, pBot), to: Math.max(pTop, pBot) };
|
||||||
|
}
|
||||||
|
|
||||||
|
private _preparePaneManualPriceScale(paneIndex: number): void {
|
||||||
|
if (paneIndex === 0) {
|
||||||
|
if (!this.mainSeries) return;
|
||||||
|
this.mainSeries.priceScale().setAutoScale(false);
|
||||||
|
this.mainSeries.applyOptions({ autoscaleInfoProvider: undefined });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._getPriceScaleForPane(paneIndex).setAutoScale(false);
|
||||||
|
} catch { /* ok */ }
|
||||||
|
|
||||||
|
if (paneIndex === 1 && this.volumeSeries) {
|
||||||
|
try {
|
||||||
|
this.volumeSeries.applyOptions({ autoscaleInfoProvider: undefined });
|
||||||
|
} catch { /* ok */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of this.indicators.values()) {
|
||||||
|
if (entry.paneIndex !== paneIndex) continue;
|
||||||
|
for (const series of entry.seriesList) {
|
||||||
|
try {
|
||||||
|
series.applyOptions({ autoscaleInfoProvider: undefined });
|
||||||
|
} catch { /* ok */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _zoomPanePriceScaleByWheel(
|
||||||
|
paneIndex: number,
|
||||||
|
deltaY: number,
|
||||||
|
anchorChartY?: number,
|
||||||
|
): boolean {
|
||||||
|
if (deltaY === 0) return false;
|
||||||
|
if (paneIndex === 0 && !this.mainSeries) return false;
|
||||||
|
|
||||||
|
const layout = this.getPaneLayouts().find(l => l.paneIndex === paneIndex);
|
||||||
|
if (!layout || layout.height <= 0) return false;
|
||||||
|
|
||||||
|
const stored = paneIndex === 0
|
||||||
|
? this._mainPriceVisibleRange
|
||||||
|
: this._panePriceVisibleRanges.get(paneIndex);
|
||||||
|
let range = stored ?? this._readPanePriceRange(paneIndex);
|
||||||
if (!range) return false;
|
if (!range) return false;
|
||||||
|
|
||||||
const span = range.to - range.from;
|
const span = range.to - range.from;
|
||||||
if (!Number.isFinite(span) || span <= 0) return false;
|
if (!Number.isFinite(span) || span <= 0) return false;
|
||||||
|
|
||||||
const ps = this.mainSeries.priceScale();
|
if (!stored) {
|
||||||
if (!this._mainPriceVisibleRange) {
|
this._preparePaneManualPriceScale(paneIndex);
|
||||||
ps.setAutoScale(false);
|
if (paneIndex === 0) {
|
||||||
this.mainSeries.applyOptions({ autoscaleInfoProvider: undefined });
|
this._mainPriceVisibleRange = { ...range };
|
||||||
this._mainPriceVisibleRange = { ...range };
|
} else {
|
||||||
|
this._panePriceVisibleRanges.set(paneIndex, { ...range });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const series = this._getRepresentativeSeriesForPane(paneIndex);
|
||||||
const paneLocalY = anchorChartY != null
|
const paneLocalY = anchorChartY != null
|
||||||
? Math.max(0, Math.min(main.height, anchorChartY - main.topY))
|
? Math.max(0, Math.min(layout.height, anchorChartY - layout.topY))
|
||||||
: main.height / 2;
|
: layout.height / 2;
|
||||||
const rawAnchor = this.mainSeries.coordinateToPrice(paneLocalY);
|
let anchorPrice = (range.from + range.to) / 2;
|
||||||
const anchorPrice = (rawAnchor != null && Number.isFinite(rawAnchor))
|
if (series) {
|
||||||
? rawAnchor
|
const rawAnchor = series.coordinateToPrice(paneLocalY);
|
||||||
: (range.from + range.to) / 2;
|
if (rawAnchor != null && Number.isFinite(rawAnchor)) {
|
||||||
|
anchorPrice = rawAnchor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const factor = Math.exp(deltaY * 0.0012);
|
const factor = Math.exp(deltaY * 0.0012);
|
||||||
const newSpan = Math.max(span * 1e-6, span * factor);
|
const newSpan = Math.max(span * 1e-6, span * factor);
|
||||||
@@ -4319,8 +4422,16 @@ export class ChartManager {
|
|||||||
to: anchorPrice + (1 - ratio) * clampedSpan,
|
to: anchorPrice + (1 - ratio) * clampedSpan,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._mainPriceVisibleRange = next;
|
if (paneIndex === 0) {
|
||||||
|
this._mainPriceVisibleRange = next;
|
||||||
|
} else {
|
||||||
|
this._panePriceVisibleRanges.set(paneIndex, next);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const ps = paneIndex === 0 && this.mainSeries
|
||||||
|
? this.mainSeries.priceScale()
|
||||||
|
: this._getPriceScaleForPane(paneIndex);
|
||||||
ps.setVisibleRange(next);
|
ps.setVisibleRange(next);
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -4357,6 +4468,8 @@ export class ChartManager {
|
|||||||
|
|
||||||
/** 캔들 pane 수동 가격 범위 (세로 패닝용) */
|
/** 캔들 pane 수동 가격 범위 (세로 패닝용) */
|
||||||
private _mainPriceVisibleRange: { from: number; to: number } | null = null;
|
private _mainPriceVisibleRange: { from: number; to: number } | null = null;
|
||||||
|
/** 보조지표·거래량 pane 수동 가격 범위 (휠 줌용) */
|
||||||
|
private _panePriceVisibleRanges = new Map<number, { from: number; to: number }>();
|
||||||
|
|
||||||
private _readMainPriceRange(): { from: number; to: number } | null {
|
private _readMainPriceRange(): { from: number; to: number } | null {
|
||||||
if (!this.mainSeries) return null;
|
if (!this.mainSeries) return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user