@forgecharts/sdk 1.1.28 → 1.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +8100 -48
- package/dist/index.js.map +1 -1
- package/dist/internal.js +8851 -31
- package/dist/internal.js.map +1 -1
- package/dist/react/index.js +11558 -27
- package/dist/react/index.js.map +1 -1
- package/dist/react/internal.js +12147 -43
- package/dist/react/internal.js.map +1 -1
- package/package.json +1 -1
- package/dist/__tests__/backwardCompatibility.test.js +0 -159
- package/dist/__tests__/backwardCompatibility.test.js.map +0 -1
- package/dist/__tests__/candleInvariant.test.js +0 -415
- package/dist/__tests__/candleInvariant.test.js.map +0 -1
- package/dist/__tests__/public-api-surface.js +0 -38
- package/dist/__tests__/public-api-surface.js.map +0 -1
- package/dist/__tests__/timeframeBoundary.test.js +0 -452
- package/dist/__tests__/timeframeBoundary.test.js.map +0 -1
- package/dist/api/DrawingManager.js +0 -190
- package/dist/api/DrawingManager.js.map +0 -1
- package/dist/api/EventBus.js +0 -44
- package/dist/api/EventBus.js.map +0 -1
- package/dist/api/IndicatorDAG.js +0 -316
- package/dist/api/IndicatorDAG.js.map +0 -1
- package/dist/api/IndicatorRegistry.js +0 -39
- package/dist/api/IndicatorRegistry.js.map +0 -1
- package/dist/api/LayoutManager.js +0 -51
- package/dist/api/LayoutManager.js.map +0 -1
- package/dist/api/PaneManager.js +0 -119
- package/dist/api/PaneManager.js.map +0 -1
- package/dist/api/ReferenceAPI.js +0 -153
- package/dist/api/ReferenceAPI.js.map +0 -1
- package/dist/api/TChart.js +0 -765
- package/dist/api/TChart.js.map +0 -1
- package/dist/api/createChart.js +0 -42
- package/dist/api/createChart.js.map +0 -1
- package/dist/api/drawing tools/fib gann menu/fibRetracement.js +0 -22
- package/dist/api/drawing tools/fib gann menu/fibRetracement.js.map +0 -1
- package/dist/api/drawing tools/lines menu/crossLine.js +0 -16
- package/dist/api/drawing tools/lines menu/crossLine.js.map +0 -1
- package/dist/api/drawing tools/lines menu/disjointChannel.js +0 -59
- package/dist/api/drawing tools/lines menu/disjointChannel.js.map +0 -1
- package/dist/api/drawing tools/lines menu/extendedLine.js +0 -17
- package/dist/api/drawing tools/lines menu/extendedLine.js.map +0 -1
- package/dist/api/drawing tools/lines menu/flatTopBottom.js +0 -41
- package/dist/api/drawing tools/lines menu/flatTopBottom.js.map +0 -1
- package/dist/api/drawing tools/lines menu/horizontal.js +0 -19
- package/dist/api/drawing tools/lines menu/horizontal.js.map +0 -1
- package/dist/api/drawing tools/lines menu/horizontalRay.js +0 -20
- package/dist/api/drawing tools/lines menu/horizontalRay.js.map +0 -1
- package/dist/api/drawing tools/lines menu/infoLine.js +0 -107
- package/dist/api/drawing tools/lines menu/infoLine.js.map +0 -1
- package/dist/api/drawing tools/lines menu/insidePitchfork.js +0 -31
- package/dist/api/drawing tools/lines menu/insidePitchfork.js.map +0 -1
- package/dist/api/drawing tools/lines menu/modifiedSchiffPitchfork.js +0 -15
- package/dist/api/drawing tools/lines menu/modifiedSchiffPitchfork.js.map +0 -1
- package/dist/api/drawing tools/lines menu/parallelChannel.js +0 -43
- package/dist/api/drawing tools/lines menu/parallelChannel.js.map +0 -1
- package/dist/api/drawing tools/lines menu/pitchfork.js +0 -12
- package/dist/api/drawing tools/lines menu/pitchfork.js.map +0 -1
- package/dist/api/drawing tools/lines menu/ray.js +0 -23
- package/dist/api/drawing tools/lines menu/ray.js.map +0 -1
- package/dist/api/drawing tools/lines menu/regressionTrend.js +0 -127
- package/dist/api/drawing tools/lines menu/regressionTrend.js.map +0 -1
- package/dist/api/drawing tools/lines menu/schiffPitchfork.js +0 -15
- package/dist/api/drawing tools/lines menu/schiffPitchfork.js.map +0 -1
- package/dist/api/drawing tools/lines menu/trendAngle.js +0 -51
- package/dist/api/drawing tools/lines menu/trendAngle.js.map +0 -1
- package/dist/api/drawing tools/lines menu/trendline.js +0 -11
- package/dist/api/drawing tools/lines menu/trendline.js.map +0 -1
- package/dist/api/drawing tools/lines menu/vertical.js +0 -11
- package/dist/api/drawing tools/lines menu/vertical.js.map +0 -1
- package/dist/api/drawing tools/pointers menu/crosshair.js +0 -16
- package/dist/api/drawing tools/pointers menu/crosshair.js.map +0 -1
- package/dist/api/drawing tools/pointers menu/cursor.js +0 -15
- package/dist/api/drawing tools/pointers menu/cursor.js.map +0 -1
- package/dist/api/drawing tools/pointers menu/demonstration.js +0 -30
- package/dist/api/drawing tools/pointers menu/demonstration.js.map +0 -1
- package/dist/api/drawing tools/pointers menu/dot.js +0 -23
- package/dist/api/drawing tools/pointers menu/dot.js.map +0 -1
- package/dist/api/drawing tools/shapes menu/rectangle.js +0 -19
- package/dist/api/drawing tools/shapes menu/rectangle.js.map +0 -1
- package/dist/api/drawing tools/shapes menu/text.js +0 -25
- package/dist/api/drawing tools/shapes menu/text.js.map +0 -1
- package/dist/api/drawingUtils.js +0 -83
- package/dist/api/drawingUtils.js.map +0 -1
- package/dist/core/CanvasLayer.js +0 -56
- package/dist/core/CanvasLayer.js.map +0 -1
- package/dist/core/Chart.js +0 -839
- package/dist/core/Chart.js.map +0 -1
- package/dist/core/CoordTransform.js +0 -224
- package/dist/core/CoordTransform.js.map +0 -1
- package/dist/core/Crosshair.js +0 -186
- package/dist/core/Crosshair.js.map +0 -1
- package/dist/core/IndicatorEngine.js +0 -181
- package/dist/core/IndicatorEngine.js.map +0 -1
- package/dist/core/InteractionManager.js +0 -698
- package/dist/core/InteractionManager.js.map +0 -1
- package/dist/core/PriceScale.js +0 -113
- package/dist/core/PriceScale.js.map +0 -1
- package/dist/core/Series.js +0 -114
- package/dist/core/Series.js.map +0 -1
- package/dist/core/TimeScale.js +0 -150
- package/dist/core/TimeScale.js.map +0 -1
- package/dist/datafeed/DatafeedConnector.js +0 -268
- package/dist/datafeed/DatafeedConnector.js.map +0 -1
- package/dist/engine/CandleEngine.js +0 -318
- package/dist/engine/CandleEngine.js.map +0 -1
- package/dist/engine/__tests__/CandleEngine.test.js +0 -300
- package/dist/engine/__tests__/CandleEngine.test.js.map +0 -1
- package/dist/engine/candleInvariants.js +0 -134
- package/dist/engine/candleInvariants.js.map +0 -1
- package/dist/engine/mergeUtils.js +0 -64
- package/dist/engine/mergeUtils.js.map +0 -1
- package/dist/engine/timeframeUtils.js +0 -100
- package/dist/engine/timeframeUtils.js.map +0 -1
- package/dist/licensing/ChartRuntimeResolver.js +0 -310
- package/dist/licensing/ChartRuntimeResolver.js.map +0 -1
- package/dist/licensing/LicenseManager.js +0 -114
- package/dist/licensing/LicenseManager.js.map +0 -1
- package/dist/licensing/__tests__/ChartRuntimeResolver.test.js +0 -177
- package/dist/licensing/__tests__/ChartRuntimeResolver.test.js.map +0 -1
- package/dist/licensing/__tests__/LicenseManager.test.js +0 -153
- package/dist/licensing/__tests__/LicenseManager.test.js.map +0 -1
- package/dist/licensing/licenseTypes.js +0 -2
- package/dist/licensing/licenseTypes.js.map +0 -1
- package/dist/pine/PineCompiler.js +0 -44
- package/dist/pine/PineCompiler.js.map +0 -1
- package/dist/pine/diagnostics.js +0 -11
- package/dist/pine/diagnostics.js.map +0 -1
- package/dist/pine/index.js +0 -5
- package/dist/pine/index.js.map +0 -1
- package/dist/pine/pine-ast.js +0 -19
- package/dist/pine/pine-ast.js.map +0 -1
- package/dist/pine/pine-lexer.js +0 -249
- package/dist/pine/pine-lexer.js.map +0 -1
- package/dist/pine/pine-parser.js +0 -416
- package/dist/pine/pine-parser.js.map +0 -1
- package/dist/pine/pine-transpiler.js +0 -260
- package/dist/pine/pine-transpiler.js.map +0 -1
- package/dist/pixi/LayerName.js +0 -35
- package/dist/pixi/LayerName.js.map +0 -1
- package/dist/pixi/PixiCandlestickRenderer.js +0 -107
- package/dist/pixi/PixiCandlestickRenderer.js.map +0 -1
- package/dist/pixi/PixiChart.js +0 -367
- package/dist/pixi/PixiChart.js.map +0 -1
- package/dist/pixi/PixiCrosshairRenderer.js +0 -110
- package/dist/pixi/PixiCrosshairRenderer.js.map +0 -1
- package/dist/pixi/PixiDrawingRenderer.js +0 -111
- package/dist/pixi/PixiDrawingRenderer.js.map +0 -1
- package/dist/pixi/PixiGridRenderer.js +0 -114
- package/dist/pixi/PixiGridRenderer.js.map +0 -1
- package/dist/pixi/PixiLayerManager.js +0 -92
- package/dist/pixi/PixiLayerManager.js.map +0 -1
- package/dist/react/canvas/ChartCanvas.js +0 -604
- package/dist/react/canvas/ChartCanvas.js.map +0 -1
- package/dist/react/canvas/ChartContextMenu.js +0 -5
- package/dist/react/canvas/ChartContextMenu.js.map +0 -1
- package/dist/react/canvas/ChartSettingsDialog.js +0 -28
- package/dist/react/canvas/ChartSettingsDialog.js.map +0 -1
- package/dist/react/canvas/IndicatorLabel.js +0 -196
- package/dist/react/canvas/IndicatorLabel.js.map +0 -1
- package/dist/react/canvas/IndicatorPane.js +0 -395
- package/dist/react/canvas/IndicatorPane.js.map +0 -1
- package/dist/react/canvas/PointerOverlay.js +0 -61
- package/dist/react/canvas/PointerOverlay.js.map +0 -1
- package/dist/react/canvas/toolbars/LeftToolbar.js +0 -407
- package/dist/react/canvas/toolbars/LeftToolbar.js.map +0 -1
- package/dist/react/hooks/useChartCapabilities.js +0 -66
- package/dist/react/hooks/useChartCapabilities.js.map +0 -1
- package/dist/react/shell/ManagedAppShell.js +0 -440
- package/dist/react/shell/ManagedAppShell.js.map +0 -1
- package/dist/react/trading/TradingBridge.js +0 -73
- package/dist/react/trading/TradingBridge.js.map +0 -1
- package/dist/react/workspace/ChartWorkspace.js +0 -42
- package/dist/react/workspace/ChartWorkspace.js.map +0 -1
- package/dist/react/workspace/FloatingPanel.js +0 -82
- package/dist/react/workspace/FloatingPanel.js.map +0 -1
- package/dist/react/workspace/IndicatorsDialog.js +0 -121
- package/dist/react/workspace/IndicatorsDialog.js.map +0 -1
- package/dist/react/workspace/LayoutMenu.js +0 -113
- package/dist/react/workspace/LayoutMenu.js.map +0 -1
- package/dist/react/workspace/SymbolSearchDialog.js +0 -245
- package/dist/react/workspace/SymbolSearchDialog.js.map +0 -1
- package/dist/react/workspace/TabBar.js +0 -29
- package/dist/react/workspace/TabBar.js.map +0 -1
- package/dist/react/workspace/toolbars/BottomToolbar.js +0 -236
- package/dist/react/workspace/toolbars/BottomToolbar.js.map +0 -1
- package/dist/react/workspace/toolbars/RightToolbar.js +0 -18
- package/dist/react/workspace/toolbars/RightToolbar.js.map +0 -1
- package/dist/react/workspace/toolbars/TopToolbar.js +0 -82
- package/dist/react/workspace/toolbars/TopToolbar.js.map +0 -1
- package/dist/renderers/CandlestickRenderer.js +0 -98
- package/dist/renderers/CandlestickRenderer.js.map +0 -1
- package/dist/renderers/HistogramRenderer.js +0 -50
- package/dist/renderers/HistogramRenderer.js.map +0 -1
- package/dist/renderers/LineRenderer.js +0 -64
- package/dist/renderers/LineRenderer.js.map +0 -1
- package/dist/theme/colors.js +0 -19
- package/dist/theme/colors.js.map +0 -1
- package/dist/tools/barDivergenceCheck.js +0 -200
- package/dist/tools/barDivergenceCheck.js.map +0 -1
- package/dist/trading/TradingOverlayStore.js +0 -139
- package/dist/trading/TradingOverlayStore.js.map +0 -1
- package/dist/trading/UnmanagedIngestion.js +0 -114
- package/dist/trading/UnmanagedIngestion.js.map +0 -1
- package/dist/trading/__tests__/ManagedTradingController.test.js +0 -271
- package/dist/trading/__tests__/ManagedTradingController.test.js.map +0 -1
- package/dist/trading/__tests__/TradingOverlayStore.test.js +0 -267
- package/dist/trading/__tests__/TradingOverlayStore.test.js.map +0 -1
- package/dist/trading/__tests__/UnmanagedIngestion.test.js +0 -170
- package/dist/trading/__tests__/UnmanagedIngestion.test.js.map +0 -1
- package/dist/trading/managed/ManagedTradingController.js +0 -247
- package/dist/trading/managed/ManagedTradingController.js.map +0 -1
- package/dist/trading/managed/managedCapabilities.js +0 -79
- package/dist/trading/managed/managedCapabilities.js.map +0 -1
- package/dist/trading/managed/managedTypes.js +0 -13
- package/dist/trading/managed/managedTypes.js.map +0 -1
- package/dist/trading/tradingTypes.js +0 -8
- package/dist/trading/tradingTypes.js.map +0 -1
- package/dist/tscript/TScriptIndicator.js +0 -47
- package/dist/tscript/TScriptIndicator.js.map +0 -1
- package/dist/tscript/ast.js +0 -22
- package/dist/tscript/ast.js.map +0 -1
- package/dist/tscript/lexer.js +0 -187
- package/dist/tscript/lexer.js.map +0 -1
- package/dist/tscript/parser.js +0 -318
- package/dist/tscript/parser.js.map +0 -1
- package/dist/tscript/runtime.js +0 -475
- package/dist/tscript/runtime.js.map +0 -1
- package/dist/tscript/series.js +0 -79
- package/dist/tscript/series.js.map +0 -1
- package/dist/types/IChart.js +0 -2
- package/dist/types/IChart.js.map +0 -1
- package/dist/types/IRenderer.js +0 -2
- package/dist/types/IRenderer.js.map +0 -1
- package/dist/types/ISeries.js +0 -2
- package/dist/types/ISeries.js.map +0 -1
|
@@ -1,604 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* ChartCanvas — stacks the main price chart above one or more indicator panes.
|
|
4
|
-
*
|
|
5
|
-
* Features:
|
|
6
|
-
* - Creates a TChart instance and attaches any `initialIndicators`.
|
|
7
|
-
* - Each non-overlay indicator gets its own sub-pane (managed by TChart's PaneManager).
|
|
8
|
-
* - Draggable dividers let the user resize panes; the price-split fraction is
|
|
9
|
-
* reported via onPriceFractionChange (no localStorage in this package).
|
|
10
|
-
* - A requestAnimationFrame loop forwards viewport changes (pan/zoom) to the
|
|
11
|
-
* indicator canvases so they stay in sync with the main chart’s time axis.
|
|
12
|
-
*
|
|
13
|
-
* Previously exported as MultiPaneChart — that name is kept as an alias.
|
|
14
|
-
*/
|
|
15
|
-
import { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react';
|
|
16
|
-
import { TChart, IndicatorDAG } from '../../';
|
|
17
|
-
import { IndicatorPane } from './IndicatorPane';
|
|
18
|
-
import { LeftToolbar } from './toolbars/LeftToolbar';
|
|
19
|
-
import { IndicatorLabel, IndicatorConfigDialog } from './IndicatorLabel';
|
|
20
|
-
import { ChartContextMenu } from './ChartContextMenu';
|
|
21
|
-
import { ChartSettingsDialog } from './ChartSettingsDialog';
|
|
22
|
-
import { PointerOverlay } from './PointerOverlay';
|
|
23
|
-
// ─── Defaults ──────────────────────────────────────────────────────────────────
|
|
24
|
-
const OVERLAY_COLORS = ['#2196f3', '#4caf50', '#ff9800', '#e040fb', '#00bcd4', '#f44336'];
|
|
25
|
-
/** Candle duration in seconds per timeframe — mirrors Chart.ts _TF_SECONDS. */
|
|
26
|
-
const _TF_SECONDS = {
|
|
27
|
-
'1s': 1, '5s': 5, '10s': 10, '30s': 30,
|
|
28
|
-
'1m': 60, '3m': 180, '5m': 300, '15m': 900, '30m': 1800,
|
|
29
|
-
'1h': 3600, '2h': 7200, '4h': 14400, '6h': 21600, '12h': 43200,
|
|
30
|
-
'1d': 86400, '3d': 259200, '1w': 604800, '1M': 2592000,
|
|
31
|
-
};
|
|
32
|
-
/**
|
|
33
|
-
* Returns true if a hex background color is perceptually light (luminance > 0.5).
|
|
34
|
-
* Used to derive a contrasting axis/text color automatically.
|
|
35
|
-
*/
|
|
36
|
-
function _isLightBg(hex) {
|
|
37
|
-
const c = hex.replace('#', '');
|
|
38
|
-
if (c.length < 6)
|
|
39
|
-
return false;
|
|
40
|
-
const r = parseInt(c.slice(0, 2), 16) / 255;
|
|
41
|
-
const g = parseInt(c.slice(2, 4), 16) / 255;
|
|
42
|
-
const b = parseInt(c.slice(4, 6), 16) / 255;
|
|
43
|
-
// sRGB luminance (WCAG formula)
|
|
44
|
-
const toLinear = (v) => v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
45
|
-
const L = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
46
|
-
return L > 0.179;
|
|
47
|
-
}
|
|
48
|
-
function Divider({ onDrag }) {
|
|
49
|
-
const lastY = useRef(0);
|
|
50
|
-
const handleMouseDown = useCallback((e) => {
|
|
51
|
-
e.preventDefault();
|
|
52
|
-
lastY.current = e.clientY;
|
|
53
|
-
const onMove = (me) => {
|
|
54
|
-
onDrag(me.clientY - lastY.current);
|
|
55
|
-
lastY.current = me.clientY;
|
|
56
|
-
};
|
|
57
|
-
const onUp = () => {
|
|
58
|
-
window.removeEventListener('mousemove', onMove);
|
|
59
|
-
window.removeEventListener('mouseup', onUp);
|
|
60
|
-
};
|
|
61
|
-
window.addEventListener('mousemove', onMove);
|
|
62
|
-
window.addEventListener('mouseup', onUp);
|
|
63
|
-
}, [onDrag]);
|
|
64
|
-
return (_jsx("div", { onMouseDown: handleMouseDown, style: {
|
|
65
|
-
height: 4,
|
|
66
|
-
cursor: 'ns-resize',
|
|
67
|
-
background: 'var(--border, #2a2e39)',
|
|
68
|
-
flexShrink: 0,
|
|
69
|
-
userSelect: 'none',
|
|
70
|
-
} }));
|
|
71
|
-
}
|
|
72
|
-
// ─── Price helpers ───────────────────────────────────────────────────────────
|
|
73
|
-
function _fmtPrice(v) {
|
|
74
|
-
const abs = Math.abs(v);
|
|
75
|
-
const dec = abs >= 100 ? 2 : abs >= 1 ? 2 : abs >= 0.0001 ? 4 : 6;
|
|
76
|
-
return v.toLocaleString('en-US', { minimumFractionDigits: dec, maximumFractionDigits: dec });
|
|
77
|
-
}
|
|
78
|
-
function _fmtVol(v) {
|
|
79
|
-
if (v >= 1_000_000)
|
|
80
|
-
return `${(v / 1_000_000).toFixed(2)}M`;
|
|
81
|
-
if (v >= 1_000)
|
|
82
|
-
return `${(v / 1_000).toFixed(1)}K`;
|
|
83
|
-
return v.toFixed(0);
|
|
84
|
-
}
|
|
85
|
-
// ─── PricePaneHeader ──────────────────────────────────────────────────────────
|
|
86
|
-
function PricePaneHeader({ symbol, timeframe, bars, theme, indicators, overlayColors, onRemove, onConfigure, getExchangeLogoUrl, }) {
|
|
87
|
-
const [collapsed, setCollapsed] = useState(false);
|
|
88
|
-
const lastBar = bars[bars.length - 1];
|
|
89
|
-
const prevBar = bars[bars.length - 2];
|
|
90
|
-
const change = lastBar && prevBar ? lastBar.close - prevBar.close : 0;
|
|
91
|
-
const pctChange = prevBar && prevBar.close !== 0 ? (change / prevBar.close) * 100 : 0;
|
|
92
|
-
const isUp = change >= 0;
|
|
93
|
-
const changeColor = isUp ? 'var(--up, #26a641)' : 'var(--down, #f85149)';
|
|
94
|
-
const muted = theme === 'dark' ? 'rgba(255,255,255,0.42)' : 'rgba(0,0,0,0.38)';
|
|
95
|
-
const textColor = theme === 'dark' ? 'rgba(255,255,255,0.82)' : 'rgba(0,0,0,0.82)';
|
|
96
|
-
const overlayInds = indicators.filter((ind) => ind.config.overlay === true);
|
|
97
|
-
return (_jsxs("div", { style: {
|
|
98
|
-
position: 'absolute',
|
|
99
|
-
top: 6,
|
|
100
|
-
left: 6,
|
|
101
|
-
zIndex: 3,
|
|
102
|
-
pointerEvents: 'auto',
|
|
103
|
-
maxWidth: 'calc(100% - 80px)',
|
|
104
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
|
|
105
|
-
userSelect: 'none',
|
|
106
|
-
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'baseline', flexWrap: 'wrap', gap: '0 5px', fontSize: 11.5, lineHeight: 1.7 }, children: [(() => {
|
|
107
|
-
const exchange = symbol.includes(':') ? symbol.split(':')[0].toUpperCase() : '';
|
|
108
|
-
if (!exchange)
|
|
109
|
-
return null;
|
|
110
|
-
const logoUrl = getExchangeLogoUrl?.(exchange);
|
|
111
|
-
if (!logoUrl)
|
|
112
|
-
return null;
|
|
113
|
-
return (_jsx("img", { src: logoUrl, alt: exchange, width: 14, height: 14, style: { display: 'inline-block', verticalAlign: 'middle', marginBottom: 1, borderRadius: 2, flexShrink: 0 }, onError: (e) => { e.currentTarget.style.display = 'none'; } }));
|
|
114
|
-
})(), _jsx("span", { style: { fontWeight: 700, color: textColor }, children: symbol }), _jsxs("span", { style: { color: muted }, children: ["\u00B7 ", timeframe] }), lastBar && (_jsxs(_Fragment, { children: [_jsxs("span", { children: [_jsx("span", { style: { color: muted }, children: "O" }), _jsx("span", { style: { color: muted }, children: " " }), _jsx("span", { style: { color: textColor }, children: _fmtPrice(lastBar.open) })] }), _jsxs("span", { children: [_jsx("span", { style: { color: '#26a641', fontWeight: 500 }, children: "H" }), _jsx("span", { style: { color: muted }, children: " " }), _jsx("span", { style: { color: textColor }, children: _fmtPrice(lastBar.high) })] }), _jsxs("span", { children: [_jsx("span", { style: { color: '#f85149', fontWeight: 500 }, children: "L" }), _jsx("span", { style: { color: muted }, children: " " }), _jsx("span", { style: { color: textColor }, children: _fmtPrice(lastBar.low) })] }), _jsxs("span", { children: [_jsx("span", { style: { color: muted }, children: "C" }), _jsx("span", { style: { color: muted }, children: " " }), _jsx("span", { style: { color: textColor }, children: _fmtPrice(lastBar.close) })] }), _jsxs("span", { style: { color: changeColor }, children: [isUp ? '+' : '', _fmtPrice(change), " (", isUp ? '+' : '', pctChange.toFixed(2), "%)"] })] }))] }), overlayInds.length > 0 && (_jsxs(_Fragment, { children: [!collapsed && (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 1, marginTop: 2 }, children: overlayInds.map((ind, i) => (_jsx(IndicatorLabel, { indicator: ind, color: ind.config.color ?? overlayColors[i % overlayColors.length] ?? '#2196f3', onRemove: () => onRemove(ind.id), onConfigure: () => onConfigure(ind) }, ind.id))) })), _jsx("button", { onClick: () => setCollapsed((c) => !c), title: collapsed ? 'Show indicators' : 'Hide indicators', style: {
|
|
115
|
-
display: 'flex',
|
|
116
|
-
alignItems: 'center',
|
|
117
|
-
justifyContent: 'center',
|
|
118
|
-
marginTop: 3,
|
|
119
|
-
width: 20,
|
|
120
|
-
height: 14,
|
|
121
|
-
background: 'rgba(100,100,100,0.25)',
|
|
122
|
-
border: 'none',
|
|
123
|
-
borderRadius: 3,
|
|
124
|
-
color: muted,
|
|
125
|
-
fontSize: 9,
|
|
126
|
-
cursor: 'pointer',
|
|
127
|
-
padding: 0,
|
|
128
|
-
lineHeight: 1,
|
|
129
|
-
}, children: collapsed ? '▾' : '▴' })] }))] }));
|
|
130
|
-
}
|
|
131
|
-
export const ChartCanvas = forwardRef(function ChartCanvas({ symbol, timeframe, datafeed, theme = 'dark', timezone = 'UTC', initialIndicators = [], layoutToRestore, onIndicatorsChanged, priceFraction: priceFractionProp, onPriceFractionChange, getExchangeLogoUrl, tradingBridge, }, ref) {
|
|
132
|
-
const outerRef = useRef(null);
|
|
133
|
-
const priceRef = useRef(null);
|
|
134
|
-
const overlayCanvasRef = useRef(null);
|
|
135
|
-
/** Crosshair X pixel (relative to chart column) — updated via mousemove, no re-render needed. */
|
|
136
|
-
const crosshairXRef = useRef(null);
|
|
137
|
-
const chartRef = useRef(null);
|
|
138
|
-
const transformRef = useRef(null);
|
|
139
|
-
const [loading, setLoading] = useState(true);
|
|
140
|
-
const [error, setError] = useState(null);
|
|
141
|
-
const [bars, setBars] = useState([]);
|
|
142
|
-
const [panes, setPanes] = useState([]);
|
|
143
|
-
const [indicators, setIndicators] = useState([]);
|
|
144
|
-
const [totalHeight, setTotalHeight] = useState(600);
|
|
145
|
-
const [activeTool, setActiveTool] = useState('cursor');
|
|
146
|
-
const [ctxMenu, setCtxMenu] = useState(null);
|
|
147
|
-
const [chartCtxMenu, setChartCtxMenu] = useState(null);
|
|
148
|
-
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
149
|
-
const [canvasSettings, setCanvasSettings] = useState({
|
|
150
|
-
background: '#000000',
|
|
151
|
-
gridColor: '#2a2a2a',
|
|
152
|
-
gridVisible: true,
|
|
153
|
-
});
|
|
154
|
-
const [configTarget, setConfigTarget] = useState(null);
|
|
155
|
-
// Flag to suppress chart context menu when a drawing context menu was triggered
|
|
156
|
-
const drawingCtxJustFired = useRef(false);
|
|
157
|
-
const dagRef = useRef(null);
|
|
158
|
-
const [computedResults, setComputedResults] = useState(() => new Map());
|
|
159
|
-
// Stable ref to the overlay draw function — rebuilt whenever data changes,
|
|
160
|
-
// called directly from the RAF loop to stay in the same frame as candles.
|
|
161
|
-
const drawOverlayRef = useRef(null);
|
|
162
|
-
const [priceFraction, setPriceFraction] = useState(priceFractionProp ?? 0.6);
|
|
163
|
-
// ── Imperative handle (tab snapshot save/load) ─────────────────────────────
|
|
164
|
-
useImperativeHandle(ref, () => ({
|
|
165
|
-
getLayoutSnapshot: () => chartRef.current?.saveLayout() ?? null,
|
|
166
|
-
loadLayoutSnapshot: (layout) => {
|
|
167
|
-
chartRef.current?.loadLayout(layout);
|
|
168
|
-
},
|
|
169
|
-
addIndicator: (config) => chartRef.current?.addIndicator(config) ?? null,
|
|
170
|
-
getOverlayStore: () => chartRef.current?.getTradingOverlayStore() ?? null,
|
|
171
|
-
captureScreenshot: async () => {
|
|
172
|
-
const container = outerRef.current;
|
|
173
|
-
if (!container)
|
|
174
|
-
return null;
|
|
175
|
-
const containerRect = container.getBoundingClientRect();
|
|
176
|
-
const { width, height } = containerRect;
|
|
177
|
-
if (width === 0 || height === 0)
|
|
178
|
-
return null;
|
|
179
|
-
const dpr = window.devicePixelRatio || 1;
|
|
180
|
-
const composite = document.createElement('canvas');
|
|
181
|
-
composite.width = Math.round(width * dpr);
|
|
182
|
-
composite.height = Math.round(height * dpr);
|
|
183
|
-
const ctx = composite.getContext('2d');
|
|
184
|
-
if (!ctx)
|
|
185
|
-
return null;
|
|
186
|
-
// Fill background colour from CSS variable
|
|
187
|
-
const bgColor = getComputedStyle(document.documentElement)
|
|
188
|
-
.getPropertyValue('--bg').trim() || '#000000';
|
|
189
|
-
ctx.fillStyle = bgColor;
|
|
190
|
-
ctx.fillRect(0, 0, composite.width, composite.height);
|
|
191
|
-
// Composite every canvas element inside the chart container
|
|
192
|
-
const canvases = container.querySelectorAll('canvas');
|
|
193
|
-
for (const canvas of canvases) {
|
|
194
|
-
const rect = canvas.getBoundingClientRect();
|
|
195
|
-
const x = Math.round((rect.left - containerRect.left) * dpr);
|
|
196
|
-
const y = Math.round((rect.top - containerRect.top) * dpr);
|
|
197
|
-
const w = Math.round(rect.width * dpr);
|
|
198
|
-
const h = Math.round(rect.height * dpr);
|
|
199
|
-
try {
|
|
200
|
-
ctx.drawImage(canvas, x, y, w, h);
|
|
201
|
-
}
|
|
202
|
-
catch { /* tainted canvas — skip */ }
|
|
203
|
-
}
|
|
204
|
-
return composite.toDataURL('image/png');
|
|
205
|
-
},
|
|
206
|
-
}));
|
|
207
|
-
// ── Mount: create TChart ────────────────────────────────────────────────────
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
const container = priceRef.current;
|
|
210
|
-
if (!container)
|
|
211
|
-
return;
|
|
212
|
-
const chart = new TChart({
|
|
213
|
-
container,
|
|
214
|
-
symbol,
|
|
215
|
-
interval: timeframe,
|
|
216
|
-
theme,
|
|
217
|
-
...(datafeed !== undefined ? { datafeed } : {}),
|
|
218
|
-
});
|
|
219
|
-
// Add initial indicators — non-overlay ones get sub-panes created automatically
|
|
220
|
-
for (const config of initialIndicators) {
|
|
221
|
-
chart.addIndicator(config);
|
|
222
|
-
}
|
|
223
|
-
transformRef.current = chart.getTransform();
|
|
224
|
-
const syncChartState = () => {
|
|
225
|
-
setBars(chart.getBars());
|
|
226
|
-
setPanes(chart.getPanes());
|
|
227
|
-
setIndicators(chart.getIndicators());
|
|
228
|
-
};
|
|
229
|
-
chart.on('dataLoading', () => {
|
|
230
|
-
setLoading(true);
|
|
231
|
-
setError(null);
|
|
232
|
-
});
|
|
233
|
-
chart.on('dataLoaded', ({ interval: loadedInterval }) => {
|
|
234
|
-
setLoading(false);
|
|
235
|
-
syncChartState();
|
|
236
|
-
// Only restore a saved viewport when it is genuinely valid for the current
|
|
237
|
-
// timeframe. Two guards prevent stale data from locking the view:
|
|
238
|
-
// 1. interval must match — compares the *loaded* interval (from event payload,
|
|
239
|
-
// not a stale closure) against the saved layout's interval. This correctly
|
|
240
|
-
// handles the case where the user switches timeframes after mount.
|
|
241
|
-
// 2. span must be ≥ 50 candles — avoids restoring zoomed-in states.
|
|
242
|
-
// In all other cases fall back to fitDefaultView() so the user always sees
|
|
243
|
-
// a sensible ~150-candle window on first load or after a timeframe switch.
|
|
244
|
-
const tfSec = _TF_SECONDS[loadedInterval] ?? 3600;
|
|
245
|
-
const savedSpan = layoutToRestore?.viewport
|
|
246
|
-
? (layoutToRestore.viewport.timeRange.to - layoutToRestore.viewport.timeRange.from)
|
|
247
|
-
: 0;
|
|
248
|
-
const hasValidViewport = !!layoutToRestore?.viewport &&
|
|
249
|
-
layoutToRestore.interval === loadedInterval &&
|
|
250
|
-
savedSpan >= tfSec * 50; // require at least 50 candles visible — avoids restoring zoomed-in states
|
|
251
|
-
if (hasValidViewport) {
|
|
252
|
-
chart.restoreViewport(layoutToRestore.viewport);
|
|
253
|
-
chart.resetPriceScale();
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
chart.fitDefaultView();
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
chart.on('dataError', ({ error: msg }) => {
|
|
260
|
-
setLoading(false);
|
|
261
|
-
setError(msg);
|
|
262
|
-
});
|
|
263
|
-
chart.on('barsUpdated', syncChartState);
|
|
264
|
-
chart.on('indicatorAdded', () => { syncChartState(); onIndicatorsChanged?.(); });
|
|
265
|
-
chart.on('indicatorRemoved', () => { syncChartState(); onIndicatorsChanged?.(); });
|
|
266
|
-
// Auto-revert to cursor after each drawing placement.
|
|
267
|
-
chart.on('drawingCreated', () => {
|
|
268
|
-
setActiveTool('cursor');
|
|
269
|
-
chart.setCrosshairEnabled(false); // back to arrow — no crosshair
|
|
270
|
-
});
|
|
271
|
-
// Show context menu when user right-clicks a drawing.
|
|
272
|
-
chart.on('drawingContextMenu', ({ id, x, y }) => {
|
|
273
|
-
const canvas = priceRef.current;
|
|
274
|
-
if (!canvas)
|
|
275
|
-
return;
|
|
276
|
-
const rect = canvas.getBoundingClientRect();
|
|
277
|
-
drawingCtxJustFired.current = true;
|
|
278
|
-
setTimeout(() => { drawingCtxJustFired.current = false; }, 0);
|
|
279
|
-
setCtxMenu({ drawingId: id, x: rect.left + x, y: rect.top + y });
|
|
280
|
-
});
|
|
281
|
-
// Restore layout from the explicit snapshot when provided.
|
|
282
|
-
// Without a snapshot the chart starts empty (no localStorage fallback —
|
|
283
|
-
// persistence is handled by DB auto-save in App.tsx).
|
|
284
|
-
if (layoutToRestore) {
|
|
285
|
-
try {
|
|
286
|
-
chart.loadLayout(layoutToRestore);
|
|
287
|
-
}
|
|
288
|
-
catch { /* invalid snapshot — ignore */ }
|
|
289
|
-
}
|
|
290
|
-
chartRef.current = chart;
|
|
291
|
-
syncChartState(); // read panes added synchronously for initialIndicators
|
|
292
|
-
return () => {
|
|
293
|
-
chart.destroy();
|
|
294
|
-
chartRef.current = null;
|
|
295
|
-
transformRef.current = null;
|
|
296
|
-
};
|
|
297
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
298
|
-
}, []);
|
|
299
|
-
// ── Symbol / timeframe propagation ──────────────────────────────────────────
|
|
300
|
-
useEffect(() => { chartRef.current?.setSymbol(symbol); }, [symbol]);
|
|
301
|
-
useEffect(() => { chartRef.current?.setInterval(timeframe); }, [timeframe]);
|
|
302
|
-
useEffect(() => { chartRef.current?.setTimezone(timezone); }, [timezone]);
|
|
303
|
-
useEffect(() => { chartRef.current?.setTheme(theme); }, [theme]);
|
|
304
|
-
// ── DAG: recompute indicators when bars or indicator list changes ────────────
|
|
305
|
-
useEffect(() => {
|
|
306
|
-
if (!dagRef.current)
|
|
307
|
-
dagRef.current = new IndicatorDAG();
|
|
308
|
-
const dag = dagRef.current;
|
|
309
|
-
dag.sync(indicators);
|
|
310
|
-
let changed = false;
|
|
311
|
-
try {
|
|
312
|
-
changed = dag.execute(bars);
|
|
313
|
-
}
|
|
314
|
-
catch (e) {
|
|
315
|
-
console.error('[ForgeCharts] Indicator computation error:', e);
|
|
316
|
-
}
|
|
317
|
-
if (changed)
|
|
318
|
-
setComputedResults(new Map(dag.getAllResults()));
|
|
319
|
-
}, [bars, indicators]);
|
|
320
|
-
// ── Overlay indicators: draw script/ema/etc lines on the price pane canvas ──
|
|
321
|
-
// The draw function is stored in a ref so the RAF loop can call it directly
|
|
322
|
-
// every frame — bypassing the React state update cycle and keeping the
|
|
323
|
-
// indicator overlay pixel-perfect in sync with the candle layer.
|
|
324
|
-
useEffect(() => {
|
|
325
|
-
drawOverlayRef.current = () => {
|
|
326
|
-
const canvas = overlayCanvasRef.current;
|
|
327
|
-
const transform = transformRef.current;
|
|
328
|
-
if (!canvas || !transform)
|
|
329
|
-
return;
|
|
330
|
-
const curPriceH = panes.length > 0 ? totalHeight * priceFraction : totalHeight;
|
|
331
|
-
const dpr = window.devicePixelRatio ?? 1;
|
|
332
|
-
const cssW = canvas.offsetWidth || (canvas.parentElement?.clientWidth ?? 800);
|
|
333
|
-
const physW = Math.round(cssW * dpr);
|
|
334
|
-
const physH = Math.round(curPriceH * dpr);
|
|
335
|
-
if (canvas.width !== physW || canvas.height !== physH) {
|
|
336
|
-
canvas.width = physW;
|
|
337
|
-
canvas.height = physH;
|
|
338
|
-
}
|
|
339
|
-
const ctx = canvas.getContext('2d');
|
|
340
|
-
if (!ctx)
|
|
341
|
-
return;
|
|
342
|
-
ctx.clearRect(0, 0, physW, physH);
|
|
343
|
-
const overlayInds = indicators.filter((ind) => ind.config.overlay === true);
|
|
344
|
-
if (overlayInds.length === 0)
|
|
345
|
-
return;
|
|
346
|
-
ctx.save();
|
|
347
|
-
ctx.scale(dpr, dpr);
|
|
348
|
-
const plotW = transform.plotWidth;
|
|
349
|
-
let colorIdx = 0;
|
|
350
|
-
for (const ind of overlayInds) {
|
|
351
|
-
const result = computedResults.get(ind.id);
|
|
352
|
-
if (!result || result.kind !== 'series' || result.points.length === 0)
|
|
353
|
-
continue;
|
|
354
|
-
const color = ind.config.color ?? OVERLAY_COLORS[colorIdx++ % OVERLAY_COLORS.length];
|
|
355
|
-
ctx.strokeStyle = color;
|
|
356
|
-
ctx.lineWidth = 1.5;
|
|
357
|
-
ctx.beginPath();
|
|
358
|
-
let started = false;
|
|
359
|
-
for (const p of result.points) {
|
|
360
|
-
const x = transform.timeToX(p.time);
|
|
361
|
-
if (x < -20 || x > plotW + 20) {
|
|
362
|
-
started = false;
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
const y = transform.priceToY(p.value);
|
|
366
|
-
if (!started) {
|
|
367
|
-
ctx.moveTo(x, y);
|
|
368
|
-
started = true;
|
|
369
|
-
}
|
|
370
|
-
else {
|
|
371
|
-
ctx.lineTo(x, y);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
ctx.stroke();
|
|
375
|
-
}
|
|
376
|
-
ctx.restore();
|
|
377
|
-
};
|
|
378
|
-
// Trigger an immediate repaint whenever the data (bars, indicator config,
|
|
379
|
-
// canvas size) changes — viewport-only changes are handled by the RAF loop.
|
|
380
|
-
drawOverlayRef.current();
|
|
381
|
-
}, [computedResults, totalHeight, priceFraction, panes, indicators]);
|
|
382
|
-
// ── ResizeObserver for total container height ──────────────────────────────
|
|
383
|
-
useEffect(() => {
|
|
384
|
-
const el = outerRef.current;
|
|
385
|
-
if (!el)
|
|
386
|
-
return;
|
|
387
|
-
const ro = new ResizeObserver(([entry]) => {
|
|
388
|
-
if (entry)
|
|
389
|
-
setTotalHeight(entry.contentRect.height);
|
|
390
|
-
});
|
|
391
|
-
ro.observe(el);
|
|
392
|
-
return () => ro.disconnect();
|
|
393
|
-
}, []);
|
|
394
|
-
// ── RAF loop: keep indicator overlay in sync with the candle layer ──────────
|
|
395
|
-
// Calls the draw function directly every animation frame — no React state
|
|
396
|
-
// update roundtrip — so indicators move in the exact same frame as candles.
|
|
397
|
-
useEffect(() => {
|
|
398
|
-
let rafId;
|
|
399
|
-
const tick = () => {
|
|
400
|
-
drawOverlayRef.current?.();
|
|
401
|
-
rafId = requestAnimationFrame(tick);
|
|
402
|
-
};
|
|
403
|
-
rafId = requestAnimationFrame(tick);
|
|
404
|
-
return () => cancelAnimationFrame(rafId);
|
|
405
|
-
}, []);
|
|
406
|
-
// ── Chart-level right-click (canvas background, not a drawing) ────────────
|
|
407
|
-
const handleCanvasContextMenu = useCallback((e) => {
|
|
408
|
-
e.preventDefault();
|
|
409
|
-
if (drawingCtxJustFired.current)
|
|
410
|
-
return;
|
|
411
|
-
setChartCtxMenu({ x: e.clientX, y: e.clientY });
|
|
412
|
-
}, []);
|
|
413
|
-
const handleClearDrawings = useCallback(() => {
|
|
414
|
-
chartRef.current?.clearDrawings();
|
|
415
|
-
}, []);
|
|
416
|
-
const [drawingsHidden, setDrawingsHidden] = useState(false);
|
|
417
|
-
const [indicatorsHidden, setIndicatorsHidden] = useState(false);
|
|
418
|
-
const handleVisibilityAction = useCallback((action) => {
|
|
419
|
-
const chart = chartRef.current;
|
|
420
|
-
if (!chart)
|
|
421
|
-
return;
|
|
422
|
-
if (action === 'hideDrawings') {
|
|
423
|
-
const next = !drawingsHidden;
|
|
424
|
-
setDrawingsHidden(next);
|
|
425
|
-
chart.setAllDrawingsVisible(!next);
|
|
426
|
-
}
|
|
427
|
-
else if (action === 'hideIndicators') {
|
|
428
|
-
const next = !indicatorsHidden;
|
|
429
|
-
setIndicatorsHidden(next);
|
|
430
|
-
chart.setIndicatorsVisible(!next);
|
|
431
|
-
}
|
|
432
|
-
else if (action === 'hideAll') {
|
|
433
|
-
const nextD = !drawingsHidden;
|
|
434
|
-
const nextI = !indicatorsHidden;
|
|
435
|
-
setDrawingsHidden(nextD);
|
|
436
|
-
setIndicatorsHidden(nextI);
|
|
437
|
-
chart.setAllDrawingsVisible(!nextD);
|
|
438
|
-
chart.setIndicatorsVisible(!nextI);
|
|
439
|
-
}
|
|
440
|
-
// hidePositions: positions system not yet implemented
|
|
441
|
-
}, [drawingsHidden, indicatorsHidden]);
|
|
442
|
-
const visibilityActiveAction = drawingsHidden && indicatorsHidden ? 'hideAll' :
|
|
443
|
-
drawingsHidden ? 'hideDrawings' :
|
|
444
|
-
indicatorsHidden ? 'hideIndicators' :
|
|
445
|
-
null;
|
|
446
|
-
const handleVisibilityDeactivate = useCallback(() => {
|
|
447
|
-
const chart = chartRef.current;
|
|
448
|
-
if (!chart)
|
|
449
|
-
return;
|
|
450
|
-
setDrawingsHidden(false);
|
|
451
|
-
setIndicatorsHidden(false);
|
|
452
|
-
chart.setAllDrawingsVisible(true);
|
|
453
|
-
chart.setIndicatorsVisible(true);
|
|
454
|
-
}, []);
|
|
455
|
-
const handleClearIndicators = useCallback(() => {
|
|
456
|
-
const chart = chartRef.current;
|
|
457
|
-
if (!chart)
|
|
458
|
-
return;
|
|
459
|
-
for (const ind of chart.getIndicators()) {
|
|
460
|
-
chart.removeIndicator(ind.id);
|
|
461
|
-
}
|
|
462
|
-
}, []);
|
|
463
|
-
const handleResetView = useCallback(() => {
|
|
464
|
-
chartRef.current?.scrollToEnd();
|
|
465
|
-
}, []);
|
|
466
|
-
const handleApplySettings = useCallback((settings) => {
|
|
467
|
-
setCanvasSettings(settings);
|
|
468
|
-
const light = _isLightBg(settings.background);
|
|
469
|
-
chartRef.current?.applyOptions({
|
|
470
|
-
colors: {
|
|
471
|
-
background: settings.background,
|
|
472
|
-
backgroundSecondary: light ? '#f5f5f5' : '#0a0a0a',
|
|
473
|
-
text: light ? '#000000' : '#ffffff',
|
|
474
|
-
textMuted: light ? '#666666' : '#888888',
|
|
475
|
-
crosshair: light ? '#000000' : '#ffffff',
|
|
476
|
-
border: light ? '#cccccc' : '#333333',
|
|
477
|
-
...(settings.gridVisible
|
|
478
|
-
? { grid: settings.gridColor }
|
|
479
|
-
: { grid: 'transparent' }),
|
|
480
|
-
},
|
|
481
|
-
});
|
|
482
|
-
}, []);
|
|
483
|
-
// ── Indicator remove / configure ──────────────────────────────────────────
|
|
484
|
-
const handleRemoveIndicator = useCallback((id) => {
|
|
485
|
-
chartRef.current?.removeIndicator(id);
|
|
486
|
-
}, []);
|
|
487
|
-
const handleConfigureIndicator = useCallback((indicator) => {
|
|
488
|
-
setConfigTarget(indicator);
|
|
489
|
-
}, []);
|
|
490
|
-
const handleSaveConfig = useCallback((newConfig) => {
|
|
491
|
-
const chart = chartRef.current;
|
|
492
|
-
if (!chart || !configTarget)
|
|
493
|
-
return;
|
|
494
|
-
chart.removeIndicator(configTarget.id);
|
|
495
|
-
chart.addIndicator(newConfig);
|
|
496
|
-
setConfigTarget(null);
|
|
497
|
-
}, [configTarget]);
|
|
498
|
-
// ── Drawing tool selection ─────────────────────────────────────────────────
|
|
499
|
-
const POINTER_TOOLS = ['cursor', 'crosshair', 'dot', 'demonstration'];
|
|
500
|
-
const handleToolSelect = useCallback((tool) => {
|
|
501
|
-
setActiveTool(tool);
|
|
502
|
-
const chart = chartRef.current;
|
|
503
|
-
if (!chart)
|
|
504
|
-
return;
|
|
505
|
-
const isPointerTool = POINTER_TOOLS.includes(tool);
|
|
506
|
-
if (isPointerTool) {
|
|
507
|
-
chart.cancelDrawingTool();
|
|
508
|
-
// Only the crosshair tool uses the SDK crosshair lines.
|
|
509
|
-
// dot/demonstration use PointerOverlay for their own visuals.
|
|
510
|
-
chart.setCrosshairEnabled(tool === 'crosshair');
|
|
511
|
-
// Hide the native OS cursor for any tool that provides its own visual.
|
|
512
|
-
chart.setNativeCursorHidden(tool !== 'cursor');
|
|
513
|
-
}
|
|
514
|
-
else {
|
|
515
|
-
chart.setCrosshairEnabled(false); // drawing tool uses its own cursor
|
|
516
|
-
chart.startDrawingTool(tool);
|
|
517
|
-
}
|
|
518
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
519
|
-
}, []);
|
|
520
|
-
// ── Divider drag: price | sub-pane area split ──────────────────────────────
|
|
521
|
-
const handlePriceDividerDrag = useCallback((dy) => {
|
|
522
|
-
if (totalHeight === 0)
|
|
523
|
-
return;
|
|
524
|
-
setPriceFraction((prev) => {
|
|
525
|
-
const next = Math.min(0.85, Math.max(0.3, prev + dy / totalHeight));
|
|
526
|
-
onPriceFractionChange?.(next);
|
|
527
|
-
return next;
|
|
528
|
-
});
|
|
529
|
-
}, [totalHeight]);
|
|
530
|
-
// ── Divider drag: between two sub-panes ──────────────────────────────────
|
|
531
|
-
const handleSubPaneDividerDrag = useCallback((paneId, dy) => {
|
|
532
|
-
const chart = chartRef.current;
|
|
533
|
-
if (!chart || totalHeight === 0)
|
|
534
|
-
return;
|
|
535
|
-
const subH = totalHeight * (1 - priceFraction);
|
|
536
|
-
if (subH === 0)
|
|
537
|
-
return;
|
|
538
|
-
const currentPanes = chart.getPanes();
|
|
539
|
-
const pane = currentPanes.find((p) => p.id === paneId);
|
|
540
|
-
if (!pane)
|
|
541
|
-
return;
|
|
542
|
-
const newFraction = Math.min(0.9, Math.max(0.05, pane.heightFraction + dy / subH));
|
|
543
|
-
chart.resizePane(paneId, newFraction);
|
|
544
|
-
setPanes([...chart.getPanes()]);
|
|
545
|
-
}, [totalHeight, priceFraction]);
|
|
546
|
-
// ── Derived layout dims ────────────────────────────────────────────────────
|
|
547
|
-
const hasSubPanes = panes.length > 0;
|
|
548
|
-
const priceH = hasSubPanes ? totalHeight * priceFraction : totalHeight;
|
|
549
|
-
const subH = totalHeight * (1 - priceFraction);
|
|
550
|
-
return (_jsxs("div", { ref: outerRef, style: {
|
|
551
|
-
position: 'relative',
|
|
552
|
-
width: '100%',
|
|
553
|
-
height: '100%',
|
|
554
|
-
display: 'flex',
|
|
555
|
-
gap: 8,
|
|
556
|
-
overflow: 'hidden',
|
|
557
|
-
}, children: [_jsx(LeftToolbar, { activeTool: activeTool, onSelectTool: handleToolSelect, onVisibilityAction: handleVisibilityAction, visibilityActiveAction: visibilityActiveAction, onVisibilityDeactivate: handleVisibilityDeactivate, onDeleteClick: () => chartRef.current?.deleteSelectedDrawing() }), _jsxs("div", { className: activeTool === 'crosshair' ? 'chart-crosshair-mode' : undefined, style: {
|
|
558
|
-
flex: 1,
|
|
559
|
-
display: 'flex',
|
|
560
|
-
flexDirection: 'column',
|
|
561
|
-
overflow: 'hidden',
|
|
562
|
-
position: 'relative',
|
|
563
|
-
minWidth: 0,
|
|
564
|
-
cursor: activeTool === 'crosshair' ? 'none' :
|
|
565
|
-
activeTool === 'dot' || activeTool === 'demonstration' ? 'none' :
|
|
566
|
-
activeTool === 'cursor' ? 'default' :
|
|
567
|
-
'crosshair', // drawing tools
|
|
568
|
-
}, onMouseMove: (e) => {
|
|
569
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
570
|
-
crosshairXRef.current = e.clientX - rect.left;
|
|
571
|
-
}, onMouseLeave: () => { crosshairXRef.current = null; }, children: [_jsxs("div", { style: { position: 'relative', height: `${priceH}px`, flexShrink: 0 }, onContextMenu: handleCanvasContextMenu, children: [_jsx("div", { ref: priceRef, style: { width: '100%', height: '100%' } }), _jsx("canvas", { ref: overlayCanvasRef, style: {
|
|
572
|
-
position: 'absolute', top: 0, left: 0,
|
|
573
|
-
width: '100%', height: '100%',
|
|
574
|
-
pointerEvents: 'none', zIndex: 2,
|
|
575
|
-
} }), " ", (activeTool === 'dot' || activeTool === 'demonstration') && (_jsx(PointerOverlay, { mode: activeTool, width: priceRef.current?.clientWidth ?? 0, height: priceH })), " ", _jsx(PricePaneHeader, { symbol: symbol, timeframe: timeframe, bars: bars, theme: theme, indicators: indicators, overlayColors: OVERLAY_COLORS, onRemove: handleRemoveIndicator, onConfigure: handleConfigureIndicator, ...(getExchangeLogoUrl ? { getExchangeLogoUrl } : {}) })] }), loading && (_jsx("div", { style: {
|
|
576
|
-
position: 'absolute',
|
|
577
|
-
top: priceH / 2 - 12,
|
|
578
|
-
left: '50%',
|
|
579
|
-
transform: 'translateX(-50%)',
|
|
580
|
-
color: 'var(--text-muted, #787b86)',
|
|
581
|
-
fontSize: 13,
|
|
582
|
-
pointerEvents: 'none',
|
|
583
|
-
zIndex: 10,
|
|
584
|
-
}, children: "Loading\u2026" })), error && (_jsx("div", { style: {
|
|
585
|
-
position: 'absolute',
|
|
586
|
-
top: priceH / 2 - 12,
|
|
587
|
-
left: '50%',
|
|
588
|
-
transform: 'translateX(-50%)',
|
|
589
|
-
color: '#ef5350',
|
|
590
|
-
fontSize: 13,
|
|
591
|
-
pointerEvents: 'none',
|
|
592
|
-
zIndex: 10,
|
|
593
|
-
}, children: error })), hasSubPanes && (_jsxs(_Fragment, { children: [_jsx(Divider, { onDrag: handlePriceDividerDrag }), panes.map((pane, idx) => {
|
|
594
|
-
const paneH = Math.max(60, Math.round(subH * pane.heightFraction));
|
|
595
|
-
const paneIndicators = indicators.filter((ind) => pane.indicatorIds.includes(ind.id));
|
|
596
|
-
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsx(IndicatorPane, { indicators: paneIndicators, bars: bars, transform: transformRef.current, height: paneH, theme: theme, computedResults: computedResults, crosshairXRef: crosshairXRef, onRemove: handleRemoveIndicator, onConfigure: handleConfigureIndicator }), idx < panes.length - 1 && (_jsx(Divider, { onDrag: (dy) => handleSubPaneDividerDrag(pane.id, dy) }))] }, pane.id));
|
|
597
|
-
})] }))] }), configTarget && (_jsx(IndicatorConfigDialog, { indicator: configTarget, onSave: handleSaveConfig, onClose: () => setConfigTarget(null) })), ctxMenu && (_jsxs(_Fragment, { children: [_jsx("div", { className: "drawing-ctx-backdrop", onClick: () => setCtxMenu(null), onContextMenu: (e) => { e.preventDefault(); setCtxMenu(null); } }), _jsx("div", { className: "drawing-ctx-menu", style: { left: ctxMenu.x, top: ctxMenu.y }, children: _jsx("button", { className: "drawing-ctx-item", onClick: () => {
|
|
598
|
-
chartRef.current?.removeDrawing(ctxMenu.drawingId);
|
|
599
|
-
setCtxMenu(null);
|
|
600
|
-
}, children: "Remove" }) })] })), chartCtxMenu && (_jsx(ChartContextMenu, { x: chartCtxMenu.x, y: chartCtxMenu.y, onClose: () => setChartCtxMenu(null), onSettings: () => setSettingsOpen(true), onClearDrawings: handleClearDrawings, onClearIndicators: handleClearIndicators, onResetView: handleResetView })), settingsOpen && (_jsx(ChartSettingsDialog, { initialSettings: canvasSettings, onApply: handleApplySettings, onClose: () => setSettingsOpen(false) }))] }));
|
|
601
|
-
});
|
|
602
|
-
/** @deprecated Use ChartCanvas */
|
|
603
|
-
export const MultiPaneChart = ChartCanvas;
|
|
604
|
-
//# sourceMappingURL=ChartCanvas.js.map
|